19. Mejoras del cliente Vue.js
19.1. Introduction
Vamos a probar el proyecto [vuejs-21] con el servidor de desarrollo. Por lo tanto, necesitaremos de nuevo que el servidor envíe los encabezados CORS. Por lo tanto, es necesario que el archivo [config.json] de la versión 14 del servidor de cálculo de impuestos permita estos encabezados:

El proyecto [vuejs-21] se crea inicialmente duplicando el proyecto [vuejs-20]. A continuación, se modifica a [3].
Aparecen nuevos archivos:
- [session.js]: exporta un objeto [session] que encapsulará información sobre la sesión actual;
- [pluginSession]: pone a disposición el objeto [session] anterior en la propiedad [$session] de las vistas;
- [NotFound.vue]: una nueva vista que se muestra cuando el usuario solicita manualmente un URL que no existe;
Se modificarán los siguientes archivos:
- [main.js]: inicializará la sesión actual y, cuando el usuario introduzca manualmente URL, la restaurará;
- [router.js]: se añaden controles para gestionar el caso de los URL introducidos por el usuario;
- [store.js]: se añade una nueva mutación;
- [config.js]: se añade una nueva configuración;
- diferentes vistas destinadas principalmente a guardar la sesión actual en momentos clave del ciclo de vida de la aplicación. Esta se restaura cada vez que el usuario introduce manualmente URL;
19.2. El almacén [Vuex]
El script [./store] evoluciona de la siguiente manera:
// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// almacén de Vuex
const store = new Vuex.Store({
state: {
// la tabla de simulaciones
simulations: [],
// el n.º de la última simulación
idSimulation: 0
},
mutations: {
// eliminación de la línea n.º índice
deleteSimulation(state, index) {
...
},
// añadir una simulación
addSimulation(state, simulation) {
...
},
// limpieza del estado
clear(state) {
// No hay más simulaciones
state.simulations = [];
// la numeración de las simulaciones vuelve a empezar desde 0
state.idSimulation = 0;
}
}
});
// exportación del objeto [store]
export default store;
- líneas 24-29: la mutación [clear] elimina la lista de simulaciones guardadas y pone a 0 el número de la última simulación.
19.3. La sesión
La necesidad de una sesión se debe a que, cuando el usuario escribe URL en el campo de dirección del navegador, el script [main.js] se ejecuta de nuevo. Sin embargo, este contiene la instrucción:
// store Vuex
import store from './store'
Esta instrucción importa el siguiente archivo [./store]:
// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// almacén Vuex
const store = new Vuex.Store({
state: {
// la tabla de simulaciones
simulations: [],
// el n.º de la última simulación
idSimulation: 0
},
mutations: {
...
}
});
// exportación del objeto [store]
export default store;
Se observa, en las líneas 7-13, que se importa una matriz de simulaciones vacía. Por lo tanto, si teníamos simulaciones antes de que el usuario escribiera URL en la barra de direcciones del navegador, después ya no las tenemos. La idea es:
- utilizar una sesión que almacene la información que queremos conservar si el usuario escribe manualmente URL;
- guardarla en momentos clave de la aplicación;
- restaurarla en [main.js], que siempre se ejecuta cuando se escribe manualmente un URL;
El script [./session] es el siguiente:
// se importa el store Vuex
import store from './store'
// se importa la configuración
import config from './config';
// el objeto [session]
const session = {
// sesión iniciada
started: false,
// autenticación
authenticated: false,
// hora de guardado
saveTime: "",
// capa [métier]
métier: null,
// estado Vuex
state: null,
// guardado de la sesión en una cadena jSON
save() {
// se añaden algunas propiedades a la sesión
this.saveTime = Date.now();
this.state = store.state;
// se transforma en jSON
const json = JSON.stringify(this);
// se almacena en el navegador
localStorage.setItem("session", json);
// eslint-disable-next-line no-console
console.log("session save", json);
},
// restauración de la sesión
restore() {
// se recupera la sesión jSON del navegador
const json = localStorage.getItem("session")
// si se ha recuperado algo
if (json) {
// se restauran todas las claves de la sesión
const restore = JSON.parse(json);
for (var key in restore) {
if (restore.hasOwnProperty(key)) {
this[key] = restore[key];
}
}
// si se ha superado un cierto tiempo de inactividad desde el inicio de la sesión, se vuelve a empezar desde cero
let durée = Date.now() - this.saveTime;
if (durée > config.duréeSession) {
// se vacía la sesión; esta también se guardará
session.clear();
} else {
// se regenera el almacén Vuex
store.replaceState(JSON.parse(JSON.stringify(this.state)));
}
}
// eslint-disable-next-line no-console
console.log("session restore", this);
},
// limpiamos la sesión
clear() {
// eslint-disable-next-line no-console
console.log("session clear");
// se borran algunos campos de la sesión
this.authenticated = false;
this.saveTime = "";
this.started = false;
if (this.métier) {
// se reinicia el campo [taxAdminData]
this.métier.taxAdminData = null;
}
// el almacén Vuex también se limpia
store.commit("clear");
// se guarda la nueva sesión
this.save();
},
}
// exportación del objeto [session]
export default session;
Comentarios
- línea 2: la sesión también encapsulará el almacén [Vuex] (lista de simulaciones, n.º de la última simulación realizada);
- líneas 7-17: la información conservada por la sesión:
- [started]: si se ha iniciado o no la sesión jSON con el servidor;
- [authenticated]: si el usuario se ha autenticado o no;
- [saveTime]: la fecha en milisegundos de la última copia de seguridad;
- [métier]: una referencia a la capa [métier]. Esta contiene el dato [taxAdminData] que permite el cálculo del impuesto;
- [state]: el estado del almacén [Vuex] (lista de simulaciones, n.º de la última simulación realizada);
- líneas 20-30: el método [save] guarda la sesión localmente en el navegador que ejecuta la aplicación;
- línea 22: se anota la hora de la guardada;
- línea 23: se recupera el [state] del almacén [Vuex];
- línea 25: se crea la cadena jSON de la sesión;
- línea 27: se almacena localmente en el navegador asociada a la clave [session];
- líneas 33-57: el método [restore] permite restaurar una sesión a partir de su copia de seguridad local en el navegador;
- línea 35: se recupera la copia de seguridad local jSON;
- línea 37: si se ha recuperado algo;
- líneas 39-44: se reconstruye el objeto [session];
- línea 46: se calcula el tiempo transcurrido desde la última copia de seguridad;
- líneas 47-50: si este tiempo es superior a un valor [config.duréeSession] fijado por configuración, la sesión se reinicia (línea 49) y, al hacerlo, se guarda;
- línea 52: en caso contrario, se regenera el atributo [state] del almacén [Vuex];
- líneas 60-75: el método [clear] reinicia la sesión;
- líneas 64-70: las propiedades de la sesión se reinician a sus valores iniciales;
- línea 72: así como el almacén [Vuex];
- línea 74: se guarda la nueva sesión;
19.4. El archivo de configuración [config]
El archivo [./config] evoluciona de la siguiente manera:
// uso de la biblioteca [axios]
const axios = require('axios');
// tiempo de espera de las solicitudes HTTP
axios.defaults.timeout = 2000;
...
// exportación de la configuración
export default {
// objeto [axios]
axios: axios,
// tiempo máximo de inactividad de la sesión: 5 min = 300 s = 300 000 ms
duréeSession: 300000
}
- línea 12: gestionaremos la sesión de la aplicación de forma similar a como se gestiona una sesión web. Aquí establecemos un tiempo máximo de inactividad de 5 minutos;
19.5. El plugin [pluginSession]
Como ya se ha hecho en numerosas ocasiones, el plugin [pluginSession] permitirá a las vistas acceder a la sesión a través de la propiedad [this.$session]:
export default {
install(Vue, session) {
// añade una propiedad [$session] a la clase vista
Object.defineProperty(Vue.prototype, '$session', {
// cuando se hace referencia a Vue.$session, se devuelve el segundo parámetro [session]
get: () => session,
})
}
}
19.6. El script principal [main]
El script principal [./main.js] evoluciona de la siguiente manera:
// registro de inicio
// eslint-disable-next-line no-console
console.log("main started");
// importaciones
import Vue from 'vue'
...
// instanciación de capa [métier]
import Métier from './couches/Métier';
const métier = new Métier();
// complemento [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
// almacén Vuex
import store from './store'
// sesión
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
// se restaura la sesión antes de reiniciar
session.restore();
// se restaura la capa [métier]
if (session.métier && session.métier.taxAdminData) {
métier.setTaxAdminData(session.métier.taxAdminData);
}
// inicio de la aplicación UI
new Vue({
el: '#app',
// el router
router: router,
// la persiana Vuex
store: store,
// la vista principal
render: h => h(Main),
})
// registro de fin
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
- línea 19: se importa la sesión;
- línea 20: se importa su complemento;
- línea 21: el complemento [pluginSession] se integra en [Vue]. Tras esta instrucción, todas las vistas disponen de la sesión en su atributo [$session];
- línea 27: se restaura la sesión. La sesión importada en la línea 11 se inicializa entonces con el contenido de su última copia de seguridad;
- después de la línea 16, las vistas disponen de una propiedad [$métier] inicializada en la línea 12. Esta propiedad no contiene la información [taxAdminData] que permite calcular el impuesto;
- líneas 30-32: si la restauración que se acaba de realizar ha restaurado la propiedad [session.métier.taxAdminData], entonces la propiedad [$métier] de las vistas se inicializa con este valor;
19.7. El archivo de enrutamiento [router]
El archivo de enrutamiento [./router] evoluciona de la siguiente manera:
// importaciones
import Vue from 'vue'
import VueRouter from 'vue-router'
// las vistas
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la sesión
import session from './session'
// plugin de enrutamiento
Vue.use(VueRouter)
// las rutas de la aplicación
const routes = [
// autenticación
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification', component: Authentification },
// cálculo de impuestos
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// lista de simulaciones
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// fin de sesión
{
path: '/fin-session', name: 'finSession'
},
// página desconocida
{
path: '*', name: 'notFound', component: NotFound,
},
]
// el router
const router = new VueRouter({
// las rutas
routes,
// el modo de visualización de URL
mode: 'history',
// la base de la aplicación URL
base: '/client-vuejs-impot/'
})
// verificación de rutas
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// ¿Ruta reservada para usuarios autenticados?
if (to.meta.authenticated && !session.authenticated) {
next({
// pasamos a la autenticación
name: 'authentification',
})
// vuelta al bucle de eventos
return;
}
// caso particular del fin de sesión
if (to.name === "finSession") {
// se limpia la sesión
session.clear();
// se pasa a la vista [authentification]
next({
name: 'authentification',
})
// vuelta al bucle de eventos
return;
}
// otros casos: siguiente vista normal del enrutamiento
next();
})
// exportación del enrutador
export default router
Comentarios
- líneas 16-38: se ha añadido información adicional a algunas rutas;
- línea 19: se ha creado una nueva ruta para ir a la vista [Authentification];
- líneas 21-24: la ruta que conduce a la vista [CalculImpot] tiene ahora una propiedad [meta] (este nombre es obligatorio). El contenido de este objeto puede ser cualquiera y lo establece el desarrollador;
- línea 23: se asigna a [meta] la propiedad [authenticated] (este nombre puede ser cualquiera). Para nosotros, esto significará que, para acceder a la vista [CalculImpot], el usuario debe estar autenticado;
- líneas 26-29: hacemos lo mismo para la ruta que lleva a la vista [ListeSimulations]. Aquí también, el usuario debe estar autenticado;
- La propiedad [meta.authenticated] nos permitirá comprobar que un usuario que introduzca manualmente los URL de las vistas [CalculImpot, ListeSimulations] no pueda obtenerlos si no está autenticado;
- líneas 51-76: el método [beforeEach] se ejecuta antes de que se redirija una vista. Es el momento adecuado para realizar comprobaciones;
- [to]: la siguiente ruta si no se hace nada;
- [from]: la última ruta mostrada;
- [next]: función que permite cambiar la siguiente ruta mostrada;
- línea 55: se comprueba si la siguiente ruta requiere que el usuario esté autenticado;
- líneas 56-59: si es así y el usuario no está autenticado, se cambia la siguiente ruta a la vista [Authentification];
- líneas 64-73: se trata el caso particular de la ruta [finSession] de las líneas 30-32. Esta no tiene ninguna vista asociada;
- línea 66: se reinicia la sesión a su valor inicial;
- líneas 68-70: se programa la vista [Authentification] como próxima vista;
- línea 75: si no se da ninguno de los dos casos anteriores, simplemente se pasa a la ruta prevista por el archivo de enrutamiento;
- líneas 35-37: se prevé una vista [NotFound] si la ruta introducida por el usuario no se corresponde con ninguna ruta conocida. Esta vista se importa en la línea 8. Las rutas se comprueban en el orden del archivo de enrutamiento. Por lo tanto, si se llega a la línea 36, es porque la ruta solicitada no es ninguna de las rutas de las líneas 18-33;
19.8. La vista [NotFound]
La vista [NotFound] se muestra si la ruta introducida por el usuario no se corresponde con ninguna ruta conocida:

El código de la vista es el siguiente:
<!-- definición HTML de la vista -->
<template>
<!-- diseño -->
<Layout :left="true" :right="true">
<!-- alerta en la columna de la derecha -->
<template slot="right">
<!-- mensaje sobre fondo amarillo -->
<b-alert show variant="danger" align="center">
<h4>Cette page n'existe pas</h4>
</b-alert>
</template>
<!-- menú de navegación en la columna de la izquierda -->
<Menu slot="left" :options="options" />
</Layout>
</template>
<script>
// importaciones
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// componentes
components: {
Layout,
Menu
},
// estado interno del componente
data() {
return {
// opciones del menú de navegación
options: [
{
text: "Authentification",
path: "/"
}
]
};
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("NotFound created");
// se analiza qué opciones de menú ofrecer
if (this.$session.authenticated && this.$métier.taxAdminData) {
// el usuario puede realizar simulaciones
Array.prototype.push.apply(this.options, [
{
text: "Calcul de l'impôt",
path: "/calcul-impot"
},
{
text: "Liste des simulations",
path: "/liste-des-simulations"
}
]);
}
}
};
</script>
Comentarios
- línea 4: utiliza las dos columnas de las vistas enrutadas;
- líneas 6-11: un mensaje de error;
- línea 13: el menú de navegación ocupa la columna de la izquierda;
- líneas 31-36: las opciones predeterminadas del menú;
- líneas 40-57: código que se ejecuta al crear la vista;
- línea 44: se comprueba si el usuario puede realizar simulaciones;
- líneas 45-55: si es así, se añaden dos opciones al menú de navegación, aquellas en las que es necesario estar autenticado y disponer de una capa [métier] operativa (líneas 46-55);
19.9. La vista [Authentification]
La vista [Authentification] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<Layout :left="false" :right="true">
...
</Layout>
</template>
<!-- dinámica de la vista -->
<script>
import Layout from "./Layout";
export default {
// estado del componente
data() {
return {
// usuario
user: "",
// su contraseña
password: "",
// controla la visualización de un mensaje de error
showError: false,
// el mensaje de error
message: ""
};
},
// componentes utilizados
components: {
Layout
},
// propiedades calculadas
computed: {
// entradas válidas
valid() {
return this.user && this.password && this.$session.started;
}
},
// gestores de eventos
methods: {
// ----------- autenticación
async login() {
try {
// Inicio de espera
this.$emit("loading", true);
// aún no se ha autenticado
this.$session.authenticated = false;
// autenticación bloqueada en el servidor
const response = await this.$dao.authentifierUtilisateur(
this.user,
this.password
);
// fin de la carga
this.$emit("loading", false);
// análisis de la respuesta del servidor
if (response.état != 200) {
// se muestra el error
this.message = response.réponse;
this.showError = true;
// vuelta al bucle de eventos
return;
}
// sin error
this.showError = false;
// se ha autenticado
this.$session.authenticated = true;
// --------- ahora se solicitan los datos de la administración tributaria
// al principio, no hay datos
this.$métier.setTaxAdminData(null);
// Inicio de la espera
this.$emit("loading", true);
// solicitud bloqueada en el servidor
const response2 = await this.$dao.getAdminData();
// fin de la carga
this.$emit("loading", false);
// análisis de la respuesta
if (response2.état != 1000) {
// se muestra el error
this.message = response2.réponse;
this.showError = true;
// vuelta al bucle de eventos
return;
}
// sin error
this.showError = false;
// se almacena en la capa [métier] el dato recibido
this.$métier.setTaxAdminData(response2.réponse);
// se puede pasar al cálculo del impuesto
this.$router.push({ name: "calculImpot" });
} catch (error) {
// se remite el error al componente principal
this.$emit("error", error);
} finally {
// actualización de sesión
this.$session.métier = this.$métier;
// se guarda la sesión
this.$session.save();
}
}
},
// ciclo de vida: el componente acaba de crearse
created() {
// eslint-disable-next-line
console.log("Authentification created");
// ¿Puede el usuario realizar simulaciones?
if (
this.$session.started &&
this.$session.authenticated &&
this.$métier.taxAdminData
) {
// entonces el usuario puede realizar simulaciones
this.$router.push({ name: "calculImpot" });
// vuelta al bucle de eventos
return;
}
// si la sesión jSON ya se ha iniciado, no se vuelve a iniciar
if (!this.$session.started) {
// Inicio de la espera
this.$emit("loading", true);
// se inicializa la sesión con el servidor: solicitud asíncrona
// se utiliza la promesa devuelta por los métodos de la capa [dao]
this.$dao
// se inicializa una sesión jSON
.initSession()
// se ha obtenido la respuesta
.then(response => {
// fin de la espera
this.$emit("loading", false);
// análisis de la respuesta
if (response.état != 700) {
// se muestra el error
this.message = response.réponse;
this.showError = true;
// vuelta al bucle de eventos
return;
}
// la sesión ha comenzado
this.$session.started = true;
})
// en caso de error
.catch(error => {
// se remite el error a la vista [Main]
this.$emit("error", error);
})
// en todos los casos
.finally(() => {
// se guarda la sesión
this.$session.save();
});
}
}
};
</script>
Comentarios
- se han resaltado en amarillo las instrucciones que utilizan la sesión introducida en esta versión del cliente [Vue.js];
- líneas 97, 148: al final de los métodos [login, created], la sesión se guarda independientemente del resultado de las consultas HTTP que tienen lugar en estos métodos (cláusula [finally] en ambos casos);
- el método [created] de las líneas 102-150 se ejecuta cada vez que se crea la vista [Authentification]. Si es el usuario quien ha introducido el URL de la vista, la sesión nos permitirá saber qué hacer;
- líneas 106-115: si se inicia la sesión jSON, se autentica al usuario y se inicializan los datos de [this.$métier.taxAdminData], entonces el usuario puede ir directamente al formulario de cálculo del impuesto (línea 112);
- línea 117: el método [created] se utilizaba en la versión anterior para inicializar una sesión jSON con el servidor. Esta fase es innecesaria si ya se ha realizado;
- líneas 42-66: el método de autenticación;
- línea 66: si la autenticación se realiza con éxito, se anota en la sesión;
- líneas 67-92: la solicitud al servidor de los datos de la administración tributaria [taxAdminData];
- línea 95: al final de esta fase, se actualiza la propiedad [métier] de la sesión, independientemente de si la operación ha tenido éxito o no;
19.10. La vista [CalculImpot]
El código de la vista [CalculImpot] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
...
</template>
<script>
// importaciones
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
export default {
// estado interno
data() {
return {
// opciones del menú
options: [
{
text: "Liste des simulations",
path: "/liste-des-simulations"
},
{
text: "Fin de session",
path: "/fin-session"
}
],
// resultado del cálculo del impuesto
résultat: "",
résultatObtenu: false
};
},
// componentes utilizados
components: {
Layout,
FormCalculImpot,
Menu
},
// métodos de gestión de eventos
methods: {
// resultado del cálculo del impuesto
handleResultatObtenu(résultat) {
// se construye el resultado en cadena HTML
...
// una simulación de +
this.$store.commit("addSimulation", résultat);
// se guarda la sesión
this.$session.save();
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
}
};
</script>
Comentarios
- línea 45: la simulación calculada se añade al almacén [Vuex]. Esto afecta a la sesión, que incluye la propiedad [state] del almacén. Por lo tanto, se guarda la sesión (línea 47);
- línea 51: se crea un método [created] para realizar un seguimiento en los registros de la creación de vistas;
19.11. La vista [ListeSimulations]
La vista [ListeSimulations] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
...
</div>
</template>
<script>
// importaciones
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// componentes
components: {
Layout,
Menu
},
// estado interno
data() {
...
},
// estado interno calculado
computed: {
// lista de simulaciones extraída del almacén Vuex
simulations() {
return this.$store.state.simulations;
}
},
// métodos
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// eliminación de la simulación n.º [index]
this.$store.commit("deleteSimulation", index);
// se guarda la sesión
this.$session.save();
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
}
};
</script>
Comentarios
- línea 36: tras la eliminación de una simulación de la línea 34, se guarda la sesión para tener en cuenta este cambio de estado;
- líneas 40-43: se sigue el proceso de creación de las vistas;
19.12. Ejecución del proyecto

Durante las pruebas, compruebe los siguientes puntos:
- si el usuario «utiliza» la aplicación a través de los enlaces del menú de navegación y los botones/enlaces de acción, esta funciona;
- si el usuario introduce manualmente URL, la aplicación sigue funcionando. Realice, en particular, la siguiente prueba:
- realice simulaciones;
- una vez en la vista [ListeSimulations], recargue (F5) la vista. En la aplicación anterior [vuejs-20], se perdían entonces las simulaciones. Aquí no es el caso: se recuperan correctamente las simulaciones ya realizadas;
- mira los registros para entender:
- en qué momento se ejecuta el script [main]. Debería ver que se ejecuta cada vez que el usuario escribe un URL manualmente;
- en qué momentos se crean las vistas. Debería ver que se crean cada vez que van a mostrarse;
- el funcionamiento del enrutamiento. Antes de cada enrutamiento se genera un registro que le indica:
- la ruta de la que vienes;
- la ruta a la que se dirige;
19.13. Implementación de la aplicación en un servidor local
Como ejercicio, siga el apartado |Implementación en un servidor local| para implementar el proyecto [vuejs-21] en el servidor Laragon local. A continuación, pruébelo.
19.14. Puesta a punto de la versión móvil
En teoría, el uso de Bootstrap debería permitirnos tener una aplicación utilizable en diferentes dispositivos: smartphones, tabletas, ordenadores portátiles y de sobremesa. Lo que diferencia a estos dispositivos es el tamaño de su pantalla.
Si probamos la versión [vuejs-21] en un móvil, observamos que la visualización de las vistas es un caos. La versión [vuejs-22] corrige este punto. Todas las modificaciones se han realizado en las plantillas de las vistas. Han consistido esencialmente en ajustar la visualización para una pantalla de smartphone. Una vez ajustada, la visualización en pantallas de mayor tamaño se realiza de forma fluida gracias a Bootstrap.

19.14.1. La vista [Main]
La vista [Main] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<div class="container">
<b-card>
<!-- jumbotron -->
<b-jumbotron>
<b-row>
<b-col sm="4">
<img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
</b-col>
<b-col sm="8">
<h1>Calculez votre impôt</h1>
</b-col>
</b-row>
</b-jumbotron>
....
</b-card>
</div>
</template>
Comentarios
- línea 8: donde antes estaba [cols=’4’], ahora se escribe [sm=’4’]. [sm] significa [small]. Las pantallas de los smartphones entran en esta categoría. Las demás categorías son [xs=extra small, md=medium, lg=large, xl=extra large];
- línea 11: lo mismo;
19.14.2. La vista [Layout]
La vista [Layout] evoluciona de la siguiente manera:
<!-- definición HTML del diseño de la vista enrutada -->
<template>
<!-- línea -->
<div>
<b-row>
<!-- área de tres columnas a la izquierda -->
<b-col sm="3" v-if="left">
<slot name="left" />
</b-col>
<!-- zona de nueve columnas a la derecha -->
<b-col sm="9" v-if="right">
<slot name="right" />
</b-col>
</b-row>
</div>
</template>
19.14.3. La vista [Authentification]
La vista [Authentification] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- formulario HTML: se envían los valores con la acción [authentifier-utilisateur] -->
<b-form @submit.prevent="login">
<!-- título -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- Primera línea -->
<b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
<!-- campo de entrada de usuario -->
<b-col sm="6">
<b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
</b-col>
</b-form-group>
<!-- segunda línea -->
<b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
<!-- campo de introducción de contraseña -->
<b-col sm="6">
<b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
</b-col>
</b-form-group>
<!-- tercera línea -->
<b-alert
show
variant="danger"
v-if="showError"
class="mt-3"
>L'erreur suivante s'est produite : {{message}}</b-alert>
<!-- botón de tipo [submit] en una tercera línea -->
<b-row>
<b-col sm="2">
<b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
</b-col>
</b-row>
</b-form>
</template>
</Layout>
</template>
Comentarios
- líneas 11 y 19: se ha eliminado el atributo [label-cols] que fijaba un número de columnas para la etiqueta del campo de entrada. Al no existir este atributo, la etiqueta se sitúa encima del campo de entrada. Esto se adapta mejor a las pantallas de los smartphones;
19.14.4. La vista [CalculImpot]
La vista [CalculImpot] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formulario de cálculo de impuestos a la derecha -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menú de navegación a la izquierda -->
<Menu slot="left" :options="options" />
</Layout>
<!-- zona de visualización de los resultados del cálculo del impuesto debajo del formulario -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- área de tres columnas vacía -->
<b-col sm="3" />
<!-- área de nueve columnas -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
19.14.5. La vista [FormCalculImpot]
La vista [FormCalculImpot] cambia de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<!-- formulario HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- mensaje de 12 columnas sobre fondo azul -->
<b-row>
<b-col sm="12">
<b-alert show variant="primary">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</b-alert>
</b-col>
</b-row>
<!-- elementos del formulario -->
<!-- primera línea -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- botones de opción en 5 columnas-->
<b-col sm="5">
<b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
<b-form-radio v-model="marié" value="non">Non</b-form-radio>
</b-col>
</b-form-group>
<!-- segunda línea -->
<b-form-group label="Nombre d'enfants à charge" label-for="enfants">
<b-form-input
type="text"
id="enfants"
placeholder="Indiquez votre nombre d'enfants"
v-model="enfants"
:state="enfantsValide"
></b-form-input>
<!-- posible mensaje de error -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- tercera línea -->
<b-form-group
label="Salaire annuel net imposable"
label-for="salaire"
description="Arrondissez à l'euro inférieur"
>
<b-form-input
type="text"
id="salaire"
placeholder="Salaire annuel"
v-model="salaire"
:state="salaireValide"
></b-form-input>
<!-- posible mensaje de error -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- cuarta línea, botón [submit] -->
<b-col sm="3">
<b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>
</b-form>
</template>
Comentarios
- líneas 15, 23, 35: se ha eliminado el atributo [label-cols];
Además, se han actualizado las pruebas de validez:
...
// estado interno calculado
computed: {
// validación del formulario
formInvalide() {
return (
// salario no válido
!this.salaire.match(/^\s*\d+\s*$/) ||
// o hijos no válidos
!this.enfants.match(/^\s*\d+\s*$/) ||
// o datos fiscales no obtenidos
!this.$métier.taxAdminData
);
},
// validación del salario
salaireValide() {
// debe ser un número >=0
return Boolean(
this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
);
},
// validación de hijos
enfantsValide() {
// debe ser un número >=0
return Boolean(
this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
);
}
},
...
Comentarios
- línea 19: cuando no se ha introducido nada, la entrada se considera válida. Esto permite tener una entrada válida cuando se muestra inicialmente la vista. En la versión anterior, la entrada aparecía inicialmente como errónea;
- línea 26: lo mismo;
- líneas 5-14: el botón de validación solo está activo si ambas entradas contienen algo y son válidas;
19.14.6. La vista [Menu]
La vista [Menu] evoluciona de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<b-card class="mb-3">
<!-- menú Bootstrap vertical -->
<b-nav vertical>
<!-- opciones del menú -->
<b-nav-item
v-for="(option,index) of options"
:key="index"
:to="option.path"
exact
exact-active-class="active"
>{{option.text}}</b-nav-item>
</b-nav>
</b-card>
</template>
Comentarios
- línea 3: se añade la etiqueta <b-card> para rodear el menú con un borde fino. Esto permite localizar mejor el menú en el smartphone;
19.14.7. La vista [ListeSimulations]
La vista [ListeSimulations] permanece sin cambios:
<!-- definición HTML de la vista -->
<template>
<div>
<!-- diseño -->
<Layout :left="true" :right="true">
<!-- simulaciones en la columna de la derecha -->
<template slot="right">
<template v-if="simulations.length==0">
<!-- sin simulaciones -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length!=0">
<!-- hay simulaciones -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- tabla de simulaciones -->
<b-table striped hover responsive :items="simulations" :fields="fields">
<template v-slot:cell(action)="data">
<b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
</template>
</b-table>
</template>
</template>
<!-- menú de navegación en la columna de la izquierda -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
Comentarios
- línea 20: cabe destacar el atributo [responsive], que hace que la visualización de la tabla se adapte al tamaño de la pantalla:

- en [2], en pantallas pequeñas, una barra de desplazamiento horizontal permite visualizar la tabla;
19.14.8. La vista [NotFound]
permanece sin cambios.
19.14.9. Las vistas en dispositivos móviles


Nota: seguramente es posible conseguir vistas aún mejor adaptadas al móvil. Me refiero, en particular, al menú de navegación, que podría mejorarse, pero hay otros aspectos. El objetivo principal de este documento no era la creación de una aplicación móvil. En ese caso, quizá habríamos recurrido a un marco de trabajo como Ionic |https://ionicframework.com/|.