19. Mejoras en el cliente Vue.js
19.1. Introduction
Vamos a probar el proyecto [vuejs-21] con el servidor de desarrollo. Por lo tanto, necesitaremos nuevamente 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]. Posteriormente, 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 un URL, la restaurará;
- [router.js]: se agregan controles para manejar el caso de los URL ingresados por el usuario;
- [store.js]: se agrega una nueva modificación;
- [config.js]: se agrega una nueva configuración;
- diferentes vistas, principalmente para guardar la sesión actual en momentos clave del ciclo de vida de la aplicación. Esta sesión se restaura cada vez que el usuario ingresa manualmente los valores de 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) {
...
},
// agregar 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 modificació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 nuevamente. Sin embargo, este contiene la instrucción:
// store Vuex
import store from './store'
Esta instrucción importa el siguiente archivo [./store]:
// plugin de 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úmero de la última simulación
idSimulation: 0
},
mutations: {
...
}
});
// exportación del objeto [store]
export default store;
Se observa, en las líneas 7 a 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 el campo de dirección 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 ingresa manualmente un URL;
- guardarla en momentos clave de la aplicación;
- restablecerla en [main.js], que siempre se ejecuta cuando se escribe manualmente un URL;
El script [./session] es el siguiente:
// se importa la tienda 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 agregan 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 reinicia 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 de 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 ciertos 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;
}
// también se limpia el almacén de Vuex
store.commit("clear");
// se guarda la nueva sesión
this.save();
},
}
// se exporta el objeto [session]
export default session;
Comentarios
- línea 2: la sesión también encapsulará el almacén [Vuex] (lista de simulaciones, número de la última simulación realizada);
- líneas 7-17: la información que conserva la sesión:
- [started]: si la sesión jSON con el servidor se ha iniciado o no;
- [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 calcular el impuesto;
- [state]: el estado del almacén [Vuex] (lista de simulaciones, número 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 registra la hora de guardado;
- 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 mayor que un valor [config.duréeSession] establecido en la configuración, se reinicia la sesión (línea 49) y, al hacerlo, se guarda;
- línea 52: de lo 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] cambia de la siguiente manera:
// se utiliza 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 minutos = 300 s = 300 000 ms
duréeSession: 300000
}
- línea 12: gestionaremos la sesión de la aplicación de manera similar a como se gestiona una sesión web. Aquí establecemos un tiempo máximo de inactividad de 5 minutos;
19.5. El complemento [pluginSession]
Como ya se ha hecho en numerosas ocasiones, el complemento [pluginSession] permitirá que las vistas tengan acceso a la sesión a través de la propiedad [this.$session]:
export default {
install(Vue, session) {
// agrega 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 la capa [métier]
import Métier from './couches/Métier';
const métier = new Métier();
// plugin [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
// almacén de 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 enrutador
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 cuentan con 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 cuentan con 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 restablecido la propiedad [session.métier.taxAdminData], entonces la propiedad [$métier] de las vistas se inicializa con ese 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 del impuesto
{
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 enrutador
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 las 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',
})
// regreso al bucle de eventos
return;
}
// caso particular del fin de la sesión
if (to.name === "finSession") {
// se limpia la sesión
session.clear();
// se pasa a la vista [authentification]
next({
name: 'authentification',
})
// regreso 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 agregado información adicional a algunas rutas;
- línea 19: se ha creado una nueva ruta para llegar a la vista [Authentification];
- líneas 21-24: la ruta que conduce a la vista [CalculImpot] ahora tiene una propiedad [meta] (este nombre es obligatorio). El contenido de este objeto puede ser cualquiera y lo establece el desarrollador;
- línea 23: en [meta], se establece 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 conduce a la vista [ListeSimulations]. Aquí también, el usuario debe estar autenticado;
- La propiedad [meta.authenticated] nos permitirá verificar que un usuario que ingrese 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 enrute una vista. Este es el momento adecuado para realizar verificaciones;
- [to]: la siguiente ruta si no se realiza ninguna acción;
- [from]: la última ruta mostrada;
- [next]: función que permite cambiar la siguiente ruta que se mostrará;
- línea 55: se verifica si la próxima 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 una vista asociada;
- línea 66: se reinicia la sesión a su valor inicial;
- líneas 68-70: se programa la vista [Authentification] como la siguiente 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 ingresada por el usuario no corresponde a ninguna ruta conocida. Esta vista se importa en la línea 8. Las rutas se verifican en el orden del archivo de enrutamiento. Por lo tanto, si se llega a la línea 36, significa que la ruta solicitada no es ninguna de las rutas de las líneas 18-33;
19.8. La vista [NotFound]
Se muestra la vista [NotFound] si la ruta ingresada por el usuario no corresponde a ninguna ruta conocida:

El código de la vista es el siguiente:
<!-- definición HTML de la vista -->
<template>
<!-- diseño de página -->
<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-desactivar-línea-siguiente
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 verifica si el usuario puede realizar simulaciones;
- líneas 45-55: si es así, se agregan dos opciones al menú de navegación, aquellas para las que es necesario estar autenticado y contar con 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;
}
},
// controladores de eventos
methods: {
// ----------- autenticación
async login() {
try {
// inicio de la espera
this.$emit("loading", true);
// aún no se ha autenticado
this.$session.authenticated = false;
// autenticación bloqueante 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;
// regreso 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 inicio, no hay datos
this.$métier.setTaxAdminData(null);
// comienza 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;
// regreso 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 reporta el error al componente principal
this.$emit("error", error);
} finally {
// Actualizar 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" });
// regreso al bucle de eventos
return;
}
// si la sesión jSON ya se ha iniciado, no se vuelve a iniciar
if (!this.$session.started) {
// comienza 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;
// regreso 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 se realizan en dichos 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 fue el usuario quien ingresó el URL de la vista, la sesión nos indicará qué hacer;
- líneas 106-115: si se inicia la sesión jSON, el usuario está autenticado y los datos de [this.$métier.taxAdminData] están inicializados, 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 etapa es innecesaria si ya se ha realizado;
- líneas 42-66: el método de autenticación;
- línea 66: si la autenticación es exitosa, se registra 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 se realizó con é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 agrega 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 dar seguimiento en los registros a la creación de las vistas;
19.11. La vista [ListeSimulations]
La vista [ListeSimulations] cambia 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 tomadas 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: después de eliminar una simulación de la línea 34, se guarda la sesión para reflejar este cambio de estado;
- líneas 40-43: se continúa con el seguimiento de la creación de las vistas;
19.12. Ejecución del proyecto

Durante las pruebas, verifique los siguientes puntos:
- si el usuario «utiliza» la aplicación a través de los enlaces del menú de navegación y los botones o enlaces de acción, esta funciona;
- si el usuario ingresa manualmente URL, la aplicación sigue funcionando. Realice, en particular, la siguiente prueba:
- realice simulaciones;
- una vez en la vista [ListeSimulations], actualice (F5) la vista. En la aplicación anterior, [vuejs-20], se perdían las simulaciones. Aquí no es así: se recuperan las simulaciones ya realizadas;
- revisa los registros para entender:
- en qué momento se ejecuta el script [main]. Deberías ver que se ejecuta cada vez que el usuario ingresa un URL manualmente;
- en qué momentos se crean las vistas. Deberían ver que se crean cada vez que van a mostrarse;
- cómo funciona el enrutamiento. Antes de cada enrutamiento se genera un registro que le indica:
- la ruta de la que vienes;
- la ruta a la que te diriges;
19.13. Implementación de la aplicación en un servidor local
Como ejercicio, sigue el apartado |Implementación en un servidor local| para implementar el proyecto [vuejs-21] en el servidor Laragon local. Luego, pruébalo.
19.14. Desarrollo de la versión móvil
En teoría, el uso de Bootstrap debería permitirnos tener una aplicación que funcione en diferentes dispositivos: teléfonos inteligentes, tabletas, computadoras portátiles y de escritorio. Lo que diferencia a estos dispositivos es el tamaño de su pantalla.
Si probamos la versión [vuejs-21] en un celular, observamos que la visualización de las vistas es un caos. La versión [vuejs-22] corrige este problema. Todos los cambios se realizaron en las plantillas de las vistas. Consistieron principalmente en ajustar la visualización para la pantalla de un teléfono inteligente. Una vez que esto está resuelto, la visualización en pantallas de mayor tamaño se realiza de manera 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 otras 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>
<!-- área 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] cambia de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- formulario HTML: los valores se envían mediante 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 entrada 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 establecía un número de columnas para la etiqueta del campo de entrada. Al no existir este atributo, la etiqueta se encuentra encima del campo de entrada. Esto se adapta mejor a las pantallas de los teléfonos inteligentes;
19.14.4. La vista [CalculImpot]
La vista [CalculImpot] cambia de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formulario de cálculo del impuesto a la derecha -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menú de navegación a la izquierda -->
<Menu slot="left" :options="options" />
</Layout>
<!-- área de visualización de los resultados del cálculo del impuesto debajo del formulario -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- área vacía de tres columnas -->
<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 actualizan 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 los 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 ingresado nada, el ingreso se considera válido. Esto permite tener un ingreso válido cuando se muestra la vista por primera vez. En la versión anterior, el ingreso aparecía inicialmente como erróneo;
- 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] cambia de la siguiente manera:
<!-- definición HTML de la vista -->
<template>
<b-card class="mb-3">
<!-- menú vertical de Bootstrap -->
<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 agrega la etiqueta <b-card> para rodear el menú con un borde delgado. Esto permite ubicar 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 existe la posibilidad de lograr vistas aún mejor adaptadas a dispositivos móviles. Me refiero, en particular, al menú de navegación, que podría mejorarse, pero hay otros aspectos también. El objetivo principal de este documento no era la creación de una aplicación móvil. En ese caso, tal vez hubiéramos recurrido a un marco de trabajo como Ionic |https://ionicframework.com/|.