Skip to content

18. Cliente Vue.js del servidor de cálculo de impuestos

18.1. Architecture

Vamos a implementar una aplicación cliente/servidor con la siguiente arquitectura:

Image

El servidor de cálculo de impuestos será la versión 14 desarrollada en el documento |https://tahe.developpez.com/tutoriels-cours/php7|

18.2. Las vistas de la aplicación

Las vistas de la aplicación [vuejs-10] son las de la versión 13 del documento |https://tahe.developpez.com/tutoriels-cours/php7| del servidor de cálculo de impuestos cuando se utiliza en modo HTML. Sin embargo, en la aplicación actual, estas vistas serán generadas por el cliente JavaScript y no por el servidor PHP.

La primera vista es la de autenticación:

Image

La segunda vista es la del cálculo del impuesto:

Image

La tercera vista es la que muestra la lista de simulaciones realizadas por el usuario:

Image

La pantalla anterior muestra que se puede eliminar la simulación n.º 1. Al hacerlo, se obtiene la siguiente vista:

Image

Si ahora eliminamos la última simulación, obtenemos la siguiente vista:

Image

18.3. Elementos del proyecto [vuejs-20]

La estructura de árbol del proyecto [vuejs-20] es la siguiente:

Image

Los elementos del proyecto son los siguientes:

  • [assets/logo.jpg]: el logotipo del proyecto;
  • [couches]: las capas [métier] y [dao] de la aplicación;
  • [plugins]: los complementos de la aplicación;
  • [views]: las vistas de la aplicación;
  • [config.js]: configura la aplicación;
  • [router.js]: define el enrutamiento de la aplicación;
  • [store.js]: el almacén de [Vuex];
  • [main.js]: el script principal de la aplicación;

18.3.1. Las capas [métier] y [dao]

18.3.1.1. La capa [dao]

La capa [dao] está implementada por la clase [Dao] del párrafo |vuejs-10|

18.3.1.2. La capa [métier]

La capa [métier] está implementada por la clase [Métier] del documento |https://tahe.developpez.com/tutoriels-cours/php7|. Se ha agregado el siguiente método [setTaxAdminData]:


// constructor
  constructor(taxAdmindata) {
    // this.taxAdminData: datos de la administración tributaria
    this.taxAdminData = taxAdmindata;
  }

  // ajustador
  setTaxAdminData(taxAdmindata) {
    // this.taxAdminData: datos de la administración tributaria
    this.taxAdminData = taxAdmindata;
}

El método [setTaxAdminData] hace lo mismo que el constructor. Su presencia permite la siguiente secuencia:

  1. instanciar la clase [Métier] con una instrucción [métier=new Métier()] cuando se desea instanciar la clase pero aún no se cuenta con el dato [taxAdminData];
  2. luego, completar posteriormente su propiedad [taxAdminData] mediante una operación [métier.setTaxAdminData(taxAdmindata)];

18.3.2. El archivo de configuración [config]

El archivo [config.js] es el siguiente:


// uso de la biblioteca [axios]
const axios = require('axios');
// tiempo de espera de las solicitudes HTTP
axios.defaults.timeout = 2000;
//: la base de datos del servidor de cálculo de impuestos
// el esquema [https] presenta problemas en Firefox porque el servidor de cálculo
// de impuestos envía un certificado autofirmado. Funciona bien con Chrome y Edge. No se ha probado con Safari.
axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impuestos/versión-14';
// vamos a usar cookies
axios.defaults.withCredentials = true;

// exportación de la configuración
export default {
  axios: axios
}

Esta configuración corresponde a la biblioteca [axios] que la capa [dao] utiliza para realizar sus consultas HTTP. Cabe destacar, en la línea 8, que el servidor opera en el puerto seguro [https].

18.3.3. Los complementos

Los complementos [pluginDao, pluginMétier, pluginConfig] tienen como objetivo crear tres nuevas propiedades en la función o clase [Vue]:

  • [$dao]: tendrá como valor una instancia de la clase [Dao];
  • [$métier]: tendrá como valor una instancia de la clase [Métier];
  • [$config]: tendrá como valor el objeto exportado por el archivo de configuración [config];

[pluginDao]


export default {
  install(Vue, dao) {
    // Agrega una propiedad [$dao] a la clase Vue
    Object.defineProperty(Vue.prototype, '$dao', {
      // cuando se hace referencia a Vue.$dao, se devuelve el segundo parámetro [dao]
      get: () => dao,
    })
  }
}

[pluginMétier]

export default {
  install(Vue, métier) {
    // agrega una propiedad [$métier] a la clase Vue
    Object.defineProperty(Vue.prototype, '$métier', {
      //: cuando se hace referencia a Vue.$métier, se devuelve el segundo parámetro [métier]
      get: () => métier,
    })
  }
}

[pluginConfig]


export default {
  install(Vue, config) {
    // agrega una propiedad [$config] a la clase Vue
    Object.defineProperty(Vue.prototype, '$config', {
      //: cuando se hace referencia a Vue.$config, se devuelve el segundo parámetro [config]
      get: () => config,
    })
  }
}

18.3.4. La persiana [Vuex]

El control [Vuex] está implementado por 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: {
    // eliminación de la línea n.º índice
    deleteSimulation(state, index) {
      // eslint-disable-next-line no-console
      console.log("mutation deleteSimulation");
      // se elimina la línea n.º [index]
      state.simulations.splice(index, 1);
      // eslint-disable-next-line no-console
      console.log("store simulations", state.simulations);
    },
    // se agrega una simulación
    addSimulation(state, simulation) {
      // eslint-disable-next-line no-console
      console.log("mutation addSimulation");
      // N.º de la simulación
      state.idSimulation++;
      simulation.id = state.idSimulation;
      // se agrega la simulación a la tabla de simulaciones
      state.simulations.push(simulation);
    },
    // limpieza del estado
    clear(state) {
      state.simulations = [];
      state.idSimulation = 1;
    }
  }
});
// exportación del objeto [store]
export default store;

Comentarios

  • líneas 2-4: el complemento [Vuex] se integra al marco [Vue];
  • líneas 8-13: agregamos los siguientes elementos al almacén de [Vuex]:
    • [simulations]: la lista de simulaciones realizadas por el usuario;
    • [idSimulation]: el número de la última simulación realizada por el usuario;

Recordemos que el almacén se compartirá entre las vistas y que su contenido es reactivo: cuando se modifica, las vistas que lo utilizan se actualizan automáticamente. En nuestra aplicación, solo el elemento [simulations] necesita ser reactivo, no el elemento [idSimulation]. Se ha dejado este elemento en el almacén por conveniencia;

  • líneas 14-40: las modificaciones permitidas en el objeto [state] de las líneas 8-13. Recordamos que estas siempre reciben el objeto [state] de las líneas 8-13 como primer parámetro;
    • línea 16: la modificación [deleteSimulation] permite eliminar una simulación cuyo número es [index];
    • línea 25: la mutación [addSimulation] permite agregar una nueva simulación a la tabla de simulaciones;
    • línea 35: la modificación [clear] permite reiniciar el objeto [state] de las líneas 8 a 13;

18.3.5. El archivo de enrutamiento [router]

El archivo de enrutamiento es el siguiente:


// 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'

// plugin de enrutamiento
Vue.use(VueRouter)

// las rutas de la aplicación
const routes = [
  // autenticación
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // cálculo de impuestos
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // lista de simulaciones
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de sesión
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// el enrutador
const router = new VueRouter({
  // las rutas
  routes,
  // modo de visualización de las rutas en el navegador
  mode: 'history',
})

// exportación del enrutador
export default router

Comentarios

  • línea 16: al iniciar la aplicación, se muestra la vista [Authentification] porque su URL es la raíz [/];
  • línea 20: se muestra la vista [CalculImpot] cuando se solicita la vista URL [/calcul-impot];
  • línea 24: la vista [ListeSimulations] se muestra cuando se solicita la vista URL o [/liste-des-simualtions];
  • línea 28: se muestra la vista [Authentification] cuando se solicita la vista URL [/fin-session];
  • líneas 33-38: se crea un objeto [router] con estas rutas (línea 35) y el modo [history] (línea 37) de gestión de URL;
  • línea 41: se exporta este enrutador;

18.3.6. El script principal [main.js]

El script [main.js] es el siguiente:


// importaciones
import Vue from 'vue'

// vista principal
import Main from './views/Main.vue'

// complemento [bootstrap-vue]
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

// CSS bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// enrutador
import router from './router'

// complemento [config]
import config from './config';
import pluginConfig from './plugins/pluginConfig'
Vue.use(pluginConfig, config)

// instanciación de capa [dao]
import Dao from './couches/Dao';
const dao = new Dao(config.axios);

// complemento [dao]
import pluginDao from './plugins/pluginDao'
Vue.use(pluginDao, dao)

// instanciación de la 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'

// 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),
})

Cabe destacar lo siguiente:

  • líneas 18-21: el objeto exportado por el script [./config] estará disponible en el atributo [Vue.$config], por lo que estará disponible en todas las vistas de la aplicación. En este caso no era necesario, ya que el objeto [config] solo lo utiliza el script [main] (línea 25). Sin embargo, es frecuente que la configuración sea necesaria para varias vistas. Por lo tanto, en este caso se ha querido mantener el principio de hacerlo disponible en un atributo de la vista;
  • líneas 24-25: instanciación de la capa [dao]. La clase [Dao] se importa en la línea 24 y luego se instancia en la línea 25. Su constructor admite como único parámetro el objeto [axios], una propiedad de configuración;
  • líneas 27-29: la capa [dao] se pone a disposición en el atributo [$dao] de todas las vistas;
  • líneas 31-37: se repite la misma secuencia para la capa [métier]. El constructor de la clase [Métier] tiene como parámetro a [taxAdminData], que representa los datos de la administración tributaria. Aún no contamos con este dato. Por lo tanto, el objeto [métier] de la línea 33 deberá completarse posteriormente;
  • línea 40: se importa el almacén [Vuex];
  • líneas 43-51: se instancia la vista principal [Main] (líneas 5 y 50), pasándole dos parámetros:
    • línea 46: el enrutador [router] definido en la línea 16;
    • línea 48: se define el store [Vuex] [store] en la línea 40;
    • en ambos casos, el nombre de la propiedad está a la izquierda y su valor a la derecha. Los nombres de las propiedades [router, store] están definidos por los marcos [vue-router] y [vuex]. Los valores asociados, por su parte, pueden ser cualesquiera;

18.4. Las vistas de la aplicación

18.4.1. La vista principal [Main]

El código de la vista principal [Main] es el siguiente:


<!-- definición de la vista HTML -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col cols="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col cols="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      <!-- error en la consulta HTTP -->
      <b-alert
        show
        variant="danger"
        v-if="showError"
      >L'erreur suivante s'est produite : {{error.message}}</b-alert>
      <!-- vista actual -->
      <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
      <!-- cargando -->
      <b-alert show v-if="showLoading" variant="light">
        <strong>Requête au serveur de calcul d'impôt en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
    </b-card>
  </div>
</template>

<script>
export default {
  // nombre
  name: "app",
  // estado interno
  data() {
    return {
      // controla la alerta de espera
      showLoading: false,
      // controla la alerta de error
      showError: false,
      // controla la visualización de la vista de enrutamiento actual
      showView: true,
      // un mensaje de error
      error: ""
    };
  },
  // controladores de eventos
  methods: {
    // error en la solicitud asíncrona
    mShowError(error) {
      // eslint-disable-next-line
      console.log("Main evt error");
      // se muestra el mensaje de error
      this.error = error;
      this.showError = true;
      // se oculta la vista enrutada
      this.showView = false;
      // se oculta el mensaje de espera
      this.showLoading = false;
    },
    // Mostrar o no un ícono de espera
    mShowLoading(value) {
      // eslint-disable-next-line
      console.log("Main evt showLoading");
      // muestra o no la alerta de espera
      this.showLoading = value;
    }
  }
};
</script>

Comentarios

  • La vista [Main] se encarga del diseño de la vista enrutada y mostrada en la línea 23:

Image

  • las líneas 5 a 15 muestran el área 1;
  • la línea 23 muestra la vista enrutada [2];
  • líneas 16 a 19: se muestra una alerta solo en caso de error de comunicación con el servidor de cálculo de impuestos;
  • líneas 25 a 28: un mensaje de espera que se muestra con cada solicitud HTTP enviada al servidor;
  • todas las vistas se mostrarán con este diseño, ya que cada vista enrutada se muestra mediante las líneas 20-24. La vista [Main] sirve para factorizar lo que puede ser compartido por las diferentes vistas;
  • línea 23: cada vista enrutada puede generar tres eventos:
    • [loading]: se lanzó una consulta HTTP. Se debe mostrar el mensaje de espera de la respuesta;
    • [error]: la solicitud HTTP terminó con un error. Se debe mostrar el mensaje de error y ocultar la vista enrutada;
  • líneas 38-49: el estado de la vista:
    • línea 41: [showLoading] controla la visualización del mensaje de espera hasta que finalice una solicitud HTTP (línea 25);
    • línea 43: [showError] controla la visualización del mensaje de error de una consulta HTTP (líneas 17-21);
    • línea 45: [showView] controla la visualización de la vista enrutada (línea 23);
  • líneas 53-63: el método [mShowError] gestiona el evento [error] emitido por la vista enrutada (línea 23);
  • líneas 65-70: el método [mShowLoading] maneja el evento [loading] emitido por la vista enrutada (línea 23);
  • línea 23: prestaremos atención a los eventos [error] y [loading]. Estos solo se interceptan si se muestra la vista enrutada [showView=true]. Por eso, la vista enrutada se muestra inicialmente (línea 45). Solo se oculta en caso de error (línea 60). Para evitar este problema, se podría haber utilizado la directiva [v-show] en lugar de [v-if]. La diferencia entre estas dos directivas es la siguiente:
    • [v-if=’false’] oculta el bloque controlado al eliminarlo del código global HTML. De este modo, los eventos de la vista enrutada ya no pueden ser interceptados;
    • [v-show=’false’] oculta el bloque controlado modificando su CSS, pero el código del bloque permanece en el HTML global y, por lo tanto, puede interceptar los eventos de la vista enrutada;

18.4.2. La vista de diseño [Layout]

El código de la vista [Layout] es el siguiente:


<!-- 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 cols="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- área de nueve columnas a la derecha -->
      <b-col cols="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

<script>
  export default {
    // parámetros de la vista
    props: {
      // controla la columna de la izquierda
      left: {
        type: Boolean
      },
      // controla la columna de la derecha
      right: {
        type: Boolean
      }
    }
  };
</script>

Comentarios

  • La vista [Layout] permite dividir la vista enrutada en dos zonas:
    • una zona de 3 columnas de Bootstrap a la izquierda (líneas 7-9). Esta zona albergará el menú de navegación cuando haya uno;
    • una zona de 9 columnas a la derecha (líneas 11-13). Esta zona albergará la información proporcionada por la vista enrutada;

18.4.3. La vista [Authentification]

La vista de autenticación es la siguiente:

Image

Esta vista se obtiene a partir de la [Layout] eliminando la columna de la izquierda para mostrar únicamente la columna de la derecha.

Su código es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulario HTML: se envían sus 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" label-cols="3">
          <!-- campo de entrada de usuario -->
          <b-col cols="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" label-cols="3">
          <!-- campo de entrada de contraseña -->
          <b-col cols="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 cols="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </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: "",
      // sesión iniciada
      sessionStarted: false
    };
  },

  // componentes utilizados
  components: {
    Layout
  },

  // propiedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.sessionStarted;
    }
  },

  // controladores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      try {
        // inicio de espera
        this.$emit("loading", true);
        // 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
        if (response.état != 200) {
          // se muestra el error
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // sin error
        this.showError = false;
        // --------- ahora se solicitan los datos de la administración tributaria
        // Inicio de la espera
        this.$emit("loading", true);
        // solicitud bloqueante al 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;
          return;
        }
        // sin error
        this.showError = false;
        // se almacenan los datos recibidos en la capa [métier]
        this.$métier.setTaxAdminData(response2.réponse);
        // se pasa a la vista de cálculo del impuesto
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // se reporta el error al componente principal
        this.$emit("error", error);
      }
    }
  },
  // ciclo de vida: el componente acaba de crearse
  created() {
    // eslint-disable-next-line
    console.log("authentification", "created");
    // se inicia una sesión jSON con el servidor
    // 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 inicia 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;
          return;
        }
        // la sesión ha comenzado
        this.sessionStarted = true;
      })
      // en caso de error
      .catch(error => {
        // se reporta el error a la vista [Main]
        this.$emit("error", error);
      });
  }
};
</script>

Comentarios

  • línea 3: la vista [Authentification] utiliza únicamente la columna de la derecha de [Layout] (líneas 3 y 4);
  • líneas 6-38: el formulario Bootstrap que genera el área 1 de la captura de pantalla anterior;
  • línea 6: el evento [@submit] ocurre cuando el usuario hace clic en el botón de tipo [submit] de la línea 35. El modificador [prevent] indica que la página no se recargue durante el evento [submit]. También se podría haber escrito:
    • una etiqueta <b-form> sin manejo del evento [submit];
    • una etiqueta <b-button> con el evento [@click=’login’] y sin el atributo [type=’submit’];

Esto también funciona. La ventaja de la solución elegida es que el envío se realiza no solo al hacer clic en el botón [Valider], sino también al presionar la tecla de validación ([Entrée]) en los campos de entrada. Por lo tanto, se eligió aquí la solución [<b-form @submit.prevent="login">] para mayor comodidad del usuario;

  • líneas 33-37: una alerta que aparece cuando el servidor ha rechazado las credenciales ingresadas por el usuario:

Image

  • línea 35: el botón [Valider] no siempre está activo. Su estado depende del atributo calculado [valid] de las líneas 71-73. El atributo [valid] es verdadero si:
    • hay algo en los campos [user, password] del formulario;
    • la sesión jSON se ha iniciado. Al principio, esta sesión no se ha iniciado (línea 59) y, por lo tanto, el botón [Valider] está inactivo.
  • líneas 49-60: el estado de la vista;
    • [user] representa la entrada del usuario en el campo [user] (líneas 12-17) del formulario. La directiva [v-model] de la línea 15 establece un enlace bidireccional entre la entrada del usuario y el atributo [user] de la vista;
    • [password] representa la entrada del usuario en el campo [password] (líneas 19-24) del formulario. La directiva [v-model] de la línea 22 establece un vínculo bidireccional entre la entrada del usuario y el atributo [password] de la vista;
    • [showError] controla (línea 29) la visualización de la alerta de las líneas 26 a 31;
    • [message] es el mensaje de error (línea 31) que se mostrará en la alerta de las líneas 26 a 31;
    • [sessionStarted] indica si la sesión jSON con el servidor se ha iniciado o no. Inicialmente, este atributo tiene el valor [false] (línea 59). La sesión jSON con el servidor se inicializa en el evento [created] del ciclo de vida de la vista, líneas 126-156. Si el servidor responde afirmativamente, entonces el atributo [sessionStarted] cambia a [true] (línea 149);
  • líneas 126-156: la función [created] se ejecuta cuando se ha creado la vista [Authentification] (aunque aún no se haya mostrado necesariamente). En segundo plano, se inicia entonces una sesión jSON con el servidor. Sabemos que esta es la primera acción que se debe realizar con el servidor de cálculo de impuestos. Para ello, se utiliza la capa [dao] de la aplicación (línea 134). Todos los métodos de esta capa son asíncronos. Aquí se utiliza la promesa (Promise) devuelta por el método [$dao.initSession], que inicia la sesión jSON con el servidor.
  • líneas 138-150: el código que se ejecuta cuando el servidor ha devuelto su respuesta sin errores;
  • línea 142: se verifica la propiedad [état] de la respuesta. Debe tener el valor [700] para que la operación se haya realizado con éxito. De lo contrario, se ha producido un error cuya causa se indica en la propiedad [response.réponse] (línea 144). A continuación, se muestra el mensaje de error de la vista (línea 145);
  • línea 149: se observa que la sesión jSON se ha iniciado;
  • líneas 152-155: el código que se ejecuta en caso de error. Este se remite a la vista principal [Main], la cual
    • mostrará el error;
    • ocultará el mensaje de espera;
    • ocultará la vista enrutada, la vista [Autentification];
  • líneas 79-124: el método [login] procesa el clic en el botón [Valider];
  • línea 79: al método se le ha añadido el prefijo de la palabra clave [async] para permitir el uso de la palabra clave [await], líneas 84 y 103;
  • líneas 84-87: llamada bloqueante al método [$dao.authentifierUtilisateur(user, password)]. Se podría haber utilizado una promesa [Promise], tal como se hizo en la función [created]. Queríamos variar los estilos. No hay riesgo de bloquear al usuario, ya que hemos establecido un [timeout] de 2 segundos para todas las solicitudes HTTP. No tendrá que esperar mucho tiempo. Además, no puede hacer nada hasta que el servidor haya devuelto su respuesta, ya que, en ese momento, el botón [Valider] permanece inactivo;
  • línea 91: el servidor de cálculo de impuestos envía respuestas jSON, todas con la estructura [{‘action’:action, ‘état’:val, ‘réponse’:réponse}]. La autenticación se ha realizado con éxito si [état==200]. De lo contrario, se muestra un mensaje de error, líneas 93-94;
  • línea 98: se oculta cualquier mensaje de error de una operación anterior;
  • líneas 99-116: ahora se solicitan al servidor los datos de la administración tributaria que permiten calcular el impuesto. En [this.$métier] tenemos una instancia de la clase [Métier] que, por el momento, no puede hacer nada porque no cuenta con esos datos;
  • línea 103: los datos de la administración tributaria se solicitan al servidor mediante una operación bloqueante;
  • líneas 107-112: se analiza la respuesta del servidor. Debe tener un valor de estado igual a 1000; de lo contrario, significa que se ha producido un error. En este último caso, se muestra el mensaje de error (líneas 109-110);
  • líneas 113-118: si la operación se realiza con éxito, se:
    • se oculta el mensaje de error, línea 114;
    • se transmiten los datos de la administración tributaria a la capa [métier] (línea 116);
    • se muestra la vista [CalculImpot], línea 118. Recordemos que [this.$router] designa el enrutador de la aplicación. El método [push] permite establecer la siguiente vista enrutada. Aquí la designamos por su atributo [name]. También podríamos haberla designado por su atributo [path]. Esta información se encuentra en el archivo de enrutamiento:

// cálculo del impuesto
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  • líneas 119-122: el [catch] se activa cuando una de las dos solicitudes HTTP ha fallado (servidor no disponible, tiempo de espera agotado, etc.). A continuación, se notifica el error a la vista principal [Main], la cual lo mostrará, ocultará el mensaje de espera y la vista [Authentification];

18.4.4. La vista [CalculImpot]

La vista [CalculImpot] es la siguiente:

Image

  • [1]: un menú de navegación ocupa la columna izquierda de la vista enrutada;
  • [2]: el formulario de cálculo del impuesto ocupa la columna derecha de la vista enrutada;

El código de la vista [CalculImpot] es el siguiente:


<!-- 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 cols="3" />
      <!-- área de nueve columnas -->
      <b-col cols="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<script>
// importaciones
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // informe 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
      const impôt = "Montant de l'impôt : " + résultat.impôt + " euro(s)";
      const décôte = "Décôte : " + résultat.décôte + " euro(s)";
      const réduction = "Réduction : " + résultat.réduction + " euro(s)";
      const surcôte = "Surcôte : " + résultat.surcôte + " euro(s)";
      const taux = "Taux d'imposition : " + résultat.taux;
      this.résultat =
        impôt +
        "<br/>" +
        décôte +
        "<br/>" +
        réduction +
        "<br/>" +
        surcôte +
        "<br/>" +
        taux;
      // visualización del resultado
      this.résultatObtenu = true;
      // ---- actualización del almacén [Vuex]
      // una simulación de +
      this.$store.commit("addSimulation", résultat);
    }
  }
};
</script>

Comentarios

  • línea 4: aquí están presentes las dos columnas de [Layout];
  • línea 6: el formulario de cálculo del impuesto ocupa la columna de la derecha. Emite el evento [resultatObtenu] cuando se obtiene el resultado del cálculo del impuesto. Cabe señalar que los nombres de los eventos y los nombres de los métodos que los gestionan no pueden contener caracteres acentuados;
  • línea 8: el menú de navegación ocupa la columna de la izquierda;
  • líneas 11-20: el resultado del cálculo del impuesto se muestra debajo del formulario:

Image

  • línea 11: el resultado solo se muestra si el atributo [résultatObtenu] (línea 47) tiene el valor [true];
  • líneas 34-48: el estado de la vista:
    • [options]: la lista de opciones del menú de navegación. Esta tabla se pasa como parámetro al componente [Menu], línea 8;
    • [résultat]: el resultado del cálculo del impuesto. Este resultado es una cadena HTML. Por eso se utilizó la directiva [v-html] en la línea 17 para mostrarlo;
    • [résultatObtenu]: el valor booleano que controla la visualización del resultado, línea 11;
  • líneas 59-81: el método [handleResultatObtenu] muestra el resultado del cálculo del impuesto que le envió la vista hija [FormCalculImpot], línea 6. Este resultado es un objeto con las propiedades [impot, décôte, réduction, surcôte, taux, marié, enfants, salaire];
  • líneas 61-75: se inserta el objeto [impot, décôte, réduction, surcôte, taux] en un texto HTML que se visualiza en la línea 17 de la plantilla;
  • línea 77: se muestra este resultado;
  • línea 80: se invoca la mutación [addSimulation] del almacén Vuex, que agregará [résultat] a las simulaciones ya presentes en el almacén;

18.4.5. El menú de navegación [Menu]

El menú de navegación se muestra en la columna de la izquierda de las vistas enrutadas:

Image

El código de la vista [Menu] es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <!-- 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>
</template>

<script>
export default {
  // parámetros de la vista
  props: {
    options: {
      type: Array
    }
  }
};
</script>

Comentarios

  • las opciones del menú son proporcionadas por el parámetro [options] (líneas 7, 20-22);
  • cada elemento de la tabla [options] tiene una propiedad [text] (línea 12), que es el texto del enlace, y una propiedad [path] (línea 9), que será la ruta de la vista de destino del enlace;

18.4.6. La vista [FormCalculImpot]

Esta vista proporciona el formulario de cálculo del impuesto:

Image

Su código es el siguiente:


  <!-- definición HTML de la vista -->
  <template>
  <!-- formulario HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- mensaje en 12 columnas sobre fondo azul -->
    <b-alert show variant="primary">
      <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
    </b-alert>
    <!-- elementos del formulario -->
    <!-- primera línea -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
      <!-- botones de opción en 5 columnas-->
      <b-col cols="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-cols="4" label-for="enfants">
      <b-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      />
      <!-- 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"
      label-cols="4"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      />
      <!-- 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] en 5 columnas -->
    <b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

<!-- script -->
<script>
export default {
  // estado interno
  data() {
    return {
      // casado o soltero
      marié: "non",
      // número de hijos
      enfants: "",
      // salario anual
      salaire: ""
    };
  },
  // estado interno calculado
  computed: {
    // validación del formulario
    formInvalide() {
      return (
        // salario no válido
        !this.salaireValide ||
        // o hijos no válidos
        !this.enfantsValide ||
        // 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*$/));
    },
    // validación de los hijos
    enfantsValide() {
      // debe ser un número >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/));
    }
  },
  // gestor de eventos
  methods: {
    calculerImpot() {
      // se calcula el impuesto utilizando la capa [métier]
      const résultat = this.$métier.calculerImpot(
        this.marié,
        this.enfants,
        this.salaire
      );
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // se completa el resultado
      résultat.marié = this.marié;
      résultat.enfants = this.enfants;
      résultat.salaire = this.salaire;
      // se emite el evento [resultatObtenu]
      this.$emit("resultatObtenu", résultat);
    }
  }
};
</script>

Comentarios

  • líneas 4-51: el formulario Bootstrap;
  • líneas 11-17: un grupo de botones de opción con sus etiquetas;
  • líneas 14-15: la etiqueta <b-form-radio> garantiza la visualización de un botón de opción:
    • línea 14: la directiva [v-model] garantiza que, al hacer clic en el botón, el atributo [marié] de la línea 61 reciba el valor [oui] (atributo [value="oui"]);
    • línea 15: la directiva [v-model] garantiza que, al hacer clic en el botón, el atributo [marié] de la línea 61 reciba el valor [non] (atributo [value="non"]);
  • líneas 19-29: la parte donde se ingresa el número de hijos:
    • línea 24: la entrada del número de hijos está vinculada al atributo [enfants] de la línea 63;
    • línea 25: la validez de la entrada se verifica mediante el atributo calculado [enfantsValide] de las líneas 87-89;
    • línea 28: garantiza que se muestre un mensaje de error si la entrada no es válida;
  • líneas 31 a 45: la parte de entrada del salario anual:
    • línea 35: muestra un mensaje de ayuda justo debajo del campo de entrada;
    • línea 41: la entrada del salario está vinculada al atributo [salaire] de la línea 65;
    • línea 42: la validez de la entrada se verifica mediante el atributo calculado [salaireValide] de las líneas 82-85;
    • línea 45: garantiza que se muestre un mensaje de error si la entrada no es válida;
  • líneas 48-50: un botón de tipo [submit]. Al hacer clic en este botón o al validar una entrada con la tecla [Entrée], se ejecuta el método [calculerImpot] (línea 94);
    • línea 49: el estado del botón (activo/inactivo) se controla mediante el atributo calculado [formInvalide] de las líneas 71-80;
  • líneas 71-80: el formulario es válido si:
    • el número de hijos es válido;
    • el salario es válido;
    • la aplicación haya obtenido del servidor los datos de la administración tributaria que permiten calcular el impuesto. Cabe recordar que este dato se guarda en la propiedad [$métier.taxAdminData]. La vista [FormCalculImpot] puede mostrarse antes de que se haya obtenido este dato, ya que se solicita de manera asíncrona al mismo tiempo que se muestra la vista. Aquí nos aseguramos de que el usuario no pueda hacer clic en el botón [Valider] hasta que se hayan obtenido los datos;
  • líneas 94-109: el método de cálculo del impuesto:
    • líneas 96-100: es la capa [métier] la que realiza este cálculo. Se trata de un cálculo sincrónico. Una vez que se ha obtenido el dato [taxAdminData], el cliente [Vue] ya no necesita comunicarse con el servidor. Todo se realiza localmente. Se obtiene un objeto [résultat] con las propiedades de [impôt, décôte, surcôte, réduction, taux];
    • líneas 104-106: se agregan las propiedades [marié, enfants, salaire] al resultado;
    • línea 108: el resultado se pasa a la vista principal [CalculImpot] a través del evento [resultatObtenu]. Esta vista se encarga de mostrar el resultado;

18.4.7. La vista [ListeSimulations]

La vista [ListeSimulations] muestra la lista de simulaciones realizadas por el usuario:

Image

El código de la vista es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <div>
    <!-- diseño de página -->
    <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>

<script>
  // importaciones
  import Layout from "./Layout";
  import Menu from "./Menu";
  export default {
    // componentes
    components: {
      Layout,
      Menu
    },
    // estado interno
    data() {
      return {
        // opciones del menú de navegación
        options: [
          {
            text: "Calcul de l'impôt",
            path: "/calcul-impot"
          },
          {
            text: "Fin de session",
            path: "/fin-session"
          }
        ],
        // parámetros de la tabla HTML
        fields: [
          { label: "#", key: "id" },
          { label: "Marié", key: "marié" },
          { label: "Nombre d'enfants", key: "enfants" },
          { label: "Salaire", key: "salaire" },
          { label: "Impôt", key: "impôt" },
          { label: "Décôte", key: "décôte" },
          { label: "Réduction", key: "réduction" },
          { label: "Surcôte", key: "surcôte" },
          { label: "", key: "action" }
        ]
      };
    },
    // 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);
      }
    }
  };
</script>

Comentarios

  • línea 5: la vista ocupa las dos columnas del diseño [Layout] de las vistas enrutadas;
  • líneas 7-26: las simulaciones se colocan en la columna de la derecha;
  • línea 28: el menú de navegación se coloca en la columna de la izquierda;
  • líneas 8, 14, 20, 75: las simulaciones provienen del almacén [Vuex] [$this.store];
  • líneas 8-13: se muestra una alerta cuando la lista de simulaciones está vacía;
  • líneas 14-25: la tabla HTML se muestra cuando la lista de simulaciones no está vacía;
  • líneas 20-24: la tabla HTML se genera mediante una etiqueta <b-table>;
    • línea 20: la tabla de simulaciones se obtiene mediante el atributo calculado [simulations] de las líneas 74-76;
    • línea 20: la configuración de la tabla HTML se realiza mediante el atributo calculado [fields] de las líneas 58 a 69. Línea 67: la columna clave [action] es la última columna de la tabla HTML;
    • líneas 21 a 23: plantilla de la última columna de la tabla HTML;
    • línea 22: aquí se coloca un botón de tipo enlace. Al hacer clic en él, se invoca el método [supprimerSimulation(data.index)], donde [data] representa la fila actual (fila 21). [data.index] representa el número de esta fila en la lista de filas mostradas;
  • línea 28: generación del menú de navegación. Las opciones de este menú son proporcionadas por el atributo [options] de las líneas 47 a 56;
  • líneas 80-85: el método que responde al clic en un enlace [Supprimer] de la página HTML;
    • línea 84: se invoca la mutación [deleteSimulation] del store [Vuex] (véase el párrafo |vuejs-15|);

18.5. Ejecución del proyecto

Image

También es necesario iniciar el servidor [Laragon] (véase el documento |https://tahe.developpez.com/tutoriels-cours/php7|) para que el servidor de cálculo de impuestos esté en línea.

18.6. Implementación de la aplicación en un servidor local

Actualmente, nuestro cliente [Vue] está implementado en un servidor de prueba en URL [http://localhost:8080]. Lo vamos a implementar en el servidor [Laragon] en URL [http://localhost:80]. Hay varios pasos que debemos seguir para lograrlo.

Paso 1

En primer lugar, nos aseguraremos de que el cliente [Vue] se implemente en el servidor de prueba en URL y [http://localhost:8080/client-vuejs-impot/].

Creamos un archivo [vue.config.js] en la raíz de nuestro proyecto actual [VSCode]:

Image

El archivo [vue.config.js] [1] tendrá el siguiente contenido:


// vue.config.js
module.exports = {
  // el servicio URL del cliente [vuejs] del servidor de cálculo de impuestos
  publicPath: '/client-vuejs-impot/'
}

También debemos modificar el archivo de enrutamiento [router.js] [2]:


// 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'

// complemento de enrutamiento
Vue.use(VueRouter)

// rutas de la aplicación
const routes = [
  // autenticación
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // cálculo de impuestos
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // lista de simulaciones
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de sesión
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// el enrutador
const router = new VueRouter({
  // las rutas
  routes,
  // modo de visualización de las rutas en el navegador
  mode: 'history',
  // la base de la aplicación URL
  base: '/client-vuejs-impot/'
})

// exportación del enrutador
export default router
  • línea 39: se le indica al enrutador que las rutas definidas en las líneas 13 a 30 son relativas a la ruta definida en la línea 39. Por ejemplo, la ruta de la línea 20, [/calcul-impot], pasará a ser [/client-vuejs-impot/calcul-impot];

A continuación, se puede volver a probar el proyecto [vuejs-20] para verificar el cambio en las rutas de la aplicación:

Image

paso 2

Ahora compilamos la versión de producción del proyecto [vuejs-20]:

Image

  • en [1-2], configuramos la tarea [build] [2] en el archivo [package.json] [1];
  • en [3-5], ejecutamos esta tarea. Es esta la que generará la versión de producción del proyecto [vuejs-20];

La ejecución de la tarea [build] se lleva a cabo en un terminal de [VSCode]:

Image

Image

  • en [3-6], unas advertencias nos indican que el código generado es demasiado grande y que habría que dividirlo en [8]. Esto tiene que ver con la optimización de la arquitectura del código, tema que no abordaremos aquí;
  • en [7], se nos indica que la carpeta [dist] contiene la versión de producción generada:

Image

  • en [3], el archivo [index.html] es el que se utilizará cuando se solicite el URL [https://localhost:80/client-vue-js-impot/];

Aquí tenemos un sitio estático que se puede implementar en cualquier servidor. Lo implementaremos en el servidor Laragon local (véase el documento |https://tahe.developpez.com/tutoriels-cours/php7|). La carpeta [dist] [2] se copia en la carpeta [<laragon>/www] [4], donde <laragon> es la carpeta de instalación del servidor Laragon. Renombramos esta carpeta a [client-vuejs-impot] [5], ya que hemos configurado la versión de producción para que funcione en URL [/client-vuejs-impot/].

paso 3

Agregamos en la carpeta [client-vuejs-impot], que acabamos de crear, el siguiente archivo [.htaccess]:


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /client-vuejs-impot/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /client-vuejs-impot/index.html [L]
</IfModule>

Image

Este archivo es un archivo de configuración del servidor web Apache. Si no lo colocamos ahí y solicitamos directamente el URL [https://localhost/client-vuejs-impot/calcul-impot], sin pasar primero por el URL [https://localhost/client-vuejs-impot/], recibimos un error 404. Con este archivo, sí que obtenemos la vista [CalculImpot].

Una vez hecho esto, iniciamos el servidor Laragon, si aún no lo hemos hecho, y solicitamos el URL [https://localhost/client-vuejs-impot/]:

Image

Se invita al lector a probar la versión de producción de nuestra aplicación.

Podemos modificar el servidor de cálculo de impuestos en un aspecto: los encabezados CORS que envía sistemáticamente a sus clientes. Esto había sido necesario para la versión del cliente que se ejecutaba desde el dominio [localhost:8080]. Ahora que tanto el cliente como el servidor se ejecutan en el dominio [localhost:80], los encabezados CORS ya no son necesarios.

Modificamos el archivo [config.json] de la versión 14 del servidor:

Image

  • por [4], e indicamos que, a partir de ahora, las solicitudes CORS serán rechazadas;

Guardemos este cambio y volvamos a solicitar el URL [https://localhost/client-vuejs-impot/]. Debería seguir funcionando.

18.7. Manejo de las solicitudes manuales

En lugar de utilizar correctamente los enlaces del menú de navegación, es posible que el usuario quiera escribir manualmente las URL de la aplicación en la barra de direcciones del navegador. Intentemos, por ejemplo, acceder a la URL URL [https://client-vuejs-impot/calcul-impot] sin pasar por el paso de autenticación. Un hacker seguramente intentaría esto. Se obtiene la siguiente vista:

Image

Efectivamente, aparece la pantalla de cálculo del impuesto. Ahora intentemos llenar los campos de entrada y validarlos:

Image

Entonces descubrimos que el botón [1] [Valider] sigue desactivado aunque los datos ingresados sean correctos. Veamos el código de la vista [FormCalculImpot]:


<b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>

En la línea 2, vemos que su estado activo/inactivo depende de la propiedad [formInvalide]. Esta es la siguiente propiedad calculada:


formInvalide() {
      return (
        // salario inválido
        !this.salaireValide ||
        // o hijos no válidos
        !this.enfantsValide ||
        // o datos fiscales no obtenidos
        !this.$métier.taxAdminData
      );
},

En la línea 8, se observa que, para que el formulario sea válido, es necesario haber obtenido los datos fiscales. Sin embargo, estos se obtienen durante la validación de la vista [Authentification], que el usuario se ha «saltado». Por lo tanto, no podrá validar el formulario. Si hubiera podido hacerlo, habría recibido un mensaje de error del servidor indicándole que no estaba autenticado. Las verificaciones siempre deben realizarse del lado del servidor. Las verificaciones del lado del navegador siempre pueden eludirse. Basta con utilizar un cliente del tipo [Postman] que envíe solicitudes sin procesar al servidor.

Ahora solicitemos el URL y el [https://localhost/client-vuejs-impot/liste-des-simulations]. Obtenemos la siguiente vista:

Image

Ahora probemos con URL y [https://localhost/client-vuejs-impot/fin-session]. Obtenemos la siguiente vista:

Image

Ahora una vista que no existe, [https://localhost/client-vuejs-impot/abcd]:

Image

Nuestra aplicación resiste bastante bien los URL ingresados manualmente. Cuando se invocan, el enrutador de la aplicación lo detecta. Por lo tanto, es posible intervenir antes de que la vista se muestre finalmente. Analizaremos este punto en el proyecto [vuejs-21].

Otro punto a considerar es el siguiente. Imaginemos que el usuario ha realizado algunas simulaciones según las reglas:

Image

Ahora actualicemos la página con un F5:

Image

Hemos hecho algo que no se recomienda: escribir el código URL a mano (hacer F5 equivale a eso). Por lo tanto, hemos perdido nuestras simulaciones.

El siguiente proyecto, [vuejs-21], propone dos mejoras:

  • verificar los URL que ingresa el usuario;
  • mantener un historial de la aplicación incluso si el usuario ingresa un URL. Arriba, se ve que se ha perdido la lista de simulaciones;