19. Melhorias no cliente Vue.js
19.1. Introduction
Vamos testar o projeto [vuejs-21] no servidor de desenvolvimento. Portanto, precisaremos novamente que o servidor envie os cabeçalhos CORS. Portanto, é necessário que o arquivo [config.json] da versão 14 do servidor de cálculo de impostos permita esses cabeçalhos:

O projeto [vuejs-21] é criado inicialmente por meio da duplicação do projeto [vuejs-20]. Em seguida, ele é alterado para [3].
Surgem novos arquivos:
- [session.js]: exporta um objeto [session] que irá encapsular informações sobre a sessão atual;
- [pluginSession]: disponibiliza o objeto [session] anterior na propriedade [$session] das visualizações;
- [NotFound.vue]: uma nova visualização exibida quando o usuário solicita manualmente um URL que não existe;
Serão modificados os seguintes arquivos:
- [main.js]: inicializará a sessão atual e, quando o usuário digitar manualmente um URL, irá restaurá-la;
- [router.js]: são adicionadas verificações para lidar com o caso de URL digitados pelo usuário;
- [store.js]: uma nova alteração é adicionada;
- [config.js]: uma nova configuração é adicionada;
- diferentes visualizações, essencialmente para salvar a sessão atual em momentos-chave do ciclo de vida do aplicativo. Essa sessão é então restaurada sempre que o usuário digita manualmente os códigos URL;
19.2. O store [Vuex]
O script [./store] sofre as seguintes alterações:
// plug-in Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// loja Vuex
const store = new Vuex.Store({
state: {
// a tabela de simulações
simulations: [],
// o número da última simulação
idSimulation: 0
},
mutations: {
// exclusão da linha nº de índice
deleteSimulation(state, index) {
...
},
// adição de uma simulação
addSimulation(state, simulation) {
...
},
// limpeza do estado
clear(state) {
// sem mais simulações
state.simulations = [];
// a numeração das simulações recomeça do 0
state.idSimulation = 0;
}
}
});
// exportação do objeto [store]
export default store;
- linhas 24-29: a alteração [clear] exclui a lista de simulações registradas e zera o número da última simulação.
19.3. A sessão
A necessidade de uma sessão decorre do fato de que, quando o usuário digita um URL no campo de endereço do navegador, o script [main.js] é executado novamente. Ora, este contém a instrução:
// store Vuex
import store from './store'
Essa instrução importa o seguinte arquivo [./store]:
// plug-in Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// armazenamento Vuex
const store = new Vuex.Store({
state: {
// a tabela de simulações
simulations: [],
// o número da última simulação
idSimulation: 0
},
mutations: {
...
}
});
// exportação do objeto [store]
export default store;
Vemos, nas linhas 7 a 13, que é importada uma matriz de simulações vazia. Portanto, se houvesse simulações antes de o usuário digitar um URL no campo de endereço do navegador, depois não haveria mais. A ideia é:
- utilizar uma sessão que armazene as informações que queremos conservar caso o usuário digite manualmente URL;
- salvá-la em momentos-chave do aplicativo;
- restaurá-la em [main.js], que sempre é executado quando um URL é digitado manualmente;
O script [./session] é o seguinte:
// importamos o store Vuex
import store from './store'
// importa-se a configuração
import config from './config';
// o objeto [session]
const session = {
// sessão iniciada
started: false,
// autenticação
authenticated: false,
// hora do salvamento
saveTime: "",
// camada [métier]
métier: null,
// estado Vuex
state: null,
// salvamento da sessão em uma string jSON
save() {
// adiciona-se algumas propriedades à sessão
this.saveTime = Date.now();
this.state = store.state;
// transforma-se em jSON
const json = JSON.stringify(this);
// armazenando-a no navegador
localStorage.setItem("session", json);
// eslint-disable-next-line no-console
console.log("session save", json);
},
// restauração da sessão
restore() {
// recuperamos a sessão jSON do navegador
const json = localStorage.getItem("session")
// se tiver sido recuperado algo
if (json) {
// restauram-se todas as chaves da sessão
const restore = JSON.parse(json);
for (var key in restore) {
if (restore.hasOwnProperty(key)) {
this[key] = restore[key];
}
}
// se tiver passado um determinado tempo de inatividade desde o início da sessão, recomeça do zero
let durée = Date.now() - this.saveTime;
if (durée > config.duréeSession) {
// esvaziamos a sessão — ela também será salva
session.clear();
} else {
// regenera-se o store do Vuex
store.replaceState(JSON.parse(JSON.stringify(this.state)));
}
}
// eslint-disable-next-line no-console
console.log("session restore", this);
},
// limpa-se a sessão
clear() {
// eslint-disable-next-line no-console
console.log("session clear");
// zerando alguns campos da sessão
this.authenticated = false;
this.saveTime = "";
this.started = false;
if (this.métier) {
// reinicializa-se o campo [taxAdminData]
this.métier.taxAdminData = null;
}
// o store Vuex também é limpo
store.commit("clear");
// a nova sessão é salva
this.save();
},
}
// exportação do objeto [session]
export default session;
Comentários
- linha 2: a sessão também encapsulará o store [Vuex] (lista de simulações, número da última simulação realizada);
- linhas 7-17: as informações mantidas pela sessão:
- [started]: a sessão jSON com o servidor foi iniciada ou não;
- [authenticated]: se o usuário se autenticou ou não;
- [saveTime]: a data, em milissegundos, do último salvamento;
- [métier]: uma referência à camada [métier]. Esta contém o dado [taxAdminData], que permite o cálculo do imposto;
- [state]: o estado do armazenamento [Vuex] (lista de simulações, número da última simulação realizada);
- linhas 20-30: o método [save] salva a sessão localmente no navegador que está executando o aplicativo;
- linha 22: registra-se a hora do salvamento;
- linha 23: recupera-se o [state] da loja [Vuex];
- linha 25: cria-se a string jSON da sessão;
- linha 27: é armazenada localmente no navegador, associada à chave [session];
- linhas 33-57: o método [restore] permite restaurar uma sessão a partir de seu backup local no navegador;
- linha 35: recupera-se o backup local jSON;
- linha 37: se algo tiver sido recuperado;
- linhas 39-44: o objeto [session] é reconstituído;
- linha 46: calcula-se o tempo decorrido desde o último backup;
- linhas 47-50: se esse tempo for superior a um valor [config.duréeSession] definido na configuração, a sessão é reinicializada (linha 49) e, nessa ocasião, é gravada;
- linha 52: caso contrário, regenera-se o atributo [state] do armazenamento [Vuex];
- linhas 60-75: o método [clear] reinicializa a sessão;
- linhas 64-70: as propriedades da sessão são reinicializadas para seus valores iniciais;
- linha 72: assim como o armazenamento [Vuex];
- linha 74: a nova sessão é salva;
19.4. O arquivo de configuração [config]
O arquivo [./config] sofre as seguintes alterações:
// uso da biblioteca [axios]
const axios = require('axios');
// tempo limite das solicitações HTTP
axios.defaults.timeout = 2000;
...
// exportação da configuração
export default {
// objeto [axios]
axios: axios,
// tempo máximo de inatividade da sessão: 5 min = 300 s = 300.000 ms
duréeSession: 300000
}
- linha 12: vamos gerenciar a sessão do aplicativo de maneira semelhante à de uma sessão da web. Aqui, definimos um tempo máximo de inatividade de 5 minutos;
19.5. O plugin [pluginSession]
Como já foi feito várias vezes, o plug-in [pluginSession] permitirá que as visualizações tenham acesso à sessão por meio da propriedade [this.$session]:
export default {
install(Vue, session) {
// adiciona uma propriedade [$session] à classe da visualização
Object.defineProperty(Vue.prototype, '$session', {
// quando Vue.$session é referenciado, o segundo parâmetro é definido como [session]
get: () => session,
})
}
}
19.6. O script principal [main]
O script principal [./main.js] sofre as seguintes alterações:
// log de inicialização
// eslint-disable-next-line no-console
console.log("main started");
// importações
import Vue from 'vue'
...
// instanciação da camada [métier]
import Métier from './couches/Métier';
const métier = new Métier();
// plug-in [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
// armazenamento Vuex
import store from './store'
// sessão
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
// a sessão é restaurada antes da reinicialização
session.restore();
// restaurando a camada [métier]
if (session.métier && session.métier.taxAdminData) {
métier.setTaxAdminData(session.métier.taxAdminData);
}
// inicialização do UI
new Vue({
el: '#aplicativo,
// o roteador
router: router,
// a persiana Vuex
store: store,
// a tela principal
render: h => h(Main),
})
// log de encerramento
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
- linha 19: importa-se a sessão;
- linha 20: importa-se seu plugin;
- linha 21: o plug-in [pluginSession] é integrado ao [Vue]. Após essa instrução, todas as visualizações passam a dispor da sessão em seu atributo [$session];
- linha 27: a sessão é restaurada. A sessão importada na linha 11 é então inicializada com o conteúdo de seu último salvamento;
- após a linha 16, as visualizações passam a dispor de uma propriedade [$métier] inicializada na linha 12. Essa propriedade não contém a informação [taxAdminData], necessária para calcular o imposto;
- linhas 30-32: se a restauração que acabou de ser feita restaurou a propriedade [session.métier.taxAdminData], então a propriedade [$métier] das visualizações é inicializada com esse valor;
19.7. O arquivo de roteamento [router]
O arquivo de roteamento [./router] sofre as seguintes alterações:
// importações
import Vue from 'vue'
import VueRouter from 'vue-router'
// as visualizações
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// a sessão
import session from './session'
// plug-in de roteamento
Vue.use(VueRouter)
// as rotas do aplicativo
const routes = [
// autenticação
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification', component: Authentification },
// cálculo do imposto
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// lista de simulações
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// fim da sessão
{
path: '/fin-session', name: 'finSession'
},
// página desconhecida
{
path: '*', name: 'notFound', component: NotFound,
},
]
// o roteador
const router = new VueRouter({
// as rotas
routes,
// o modo de exibição do URL
mode: 'history',
// o URL básico do aplicativo
base: '/client-vuejs-impot/'
})
// verificação das rotas
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// rota reservada para usuários autenticados?
if (to.meta.authenticated && !session.authenticated) {
next({
// passando para a autenticação
name: 'authentification',
})
// retorno ao ciclo de eventos
return;
}
// caso específico do fim da sessão
if (to.name === "finSession") {
// limpa-se a sessão
session.clear();
// navegando para a visualização [authentification]
next({
name: 'authentification',
})
// retorno ao ciclo de eventos
return;
}
// outros casos — próxima visualização normal do roteamento
next();
})
// exportação do roteador
export default router
Comentários
- linhas 16-38: algumas rotas foram complementadas com informações adicionais;
- linha 19: foi criada uma nova rota para acessar a vista [Authentification];
- linhas 21-24: a rota que leva à visualização [CalculImpot] agora possui uma propriedade [meta] (esse nome é obrigatório). O conteúdo desse objeto pode ser qualquer um e é definido pelo desenvolvedor;
- linha 23: insere-se em [meta] a propriedade [authenticated] (esse nome pode ser qualquer um). Isso significará, para nós, que, para acessar a visualização [CalculImpot], o usuário deve estar autenticado;
- linhas 26-29: fazemos o mesmo para a rota que leva à visualização [ListeSimulations]. Nesse caso também, o usuário deve estar autenticado;
- a propriedade [meta.authenticated] nos permitirá verificar que um usuário que digitar manualmente os URL das visualizações [CalculImpot, ListeSimulations] não poderá obtê-los se não estiver autenticado;
- linhas 51-76: o método [beforeEach] é executado antes que uma visualização seja encaminhada. Esse é o momento certo para realizar verificações;
- [to]: a próxima rota caso não seja realizada nenhuma ação;
- [from]: a última rota exibida;
- [next]: função que permite alterar a próxima rota exibida;
- linha 55: verifica-se se a próxima rota exige que o usuário esteja autenticado;
- linhas 56-59: se sim e o usuário não estiver autenticado, altera-se a próxima rota para a visualização [Authentification];
- linhas 64-73: trata-se do caso específico da rota [finSession] das linhas 30-32. Essa rota não possui uma visualização associada;
- linha 66: reinicializa-se a sessão para seu valor inicial;
- linhas 68-70: programa-se a visualização [Authentification] como próxima visualização;
- linha 75: se não estivermos nos dois casos anteriores, simplesmente passamos para a rota prevista pelo arquivo de roteamento;
- linhas 35-37: prevê-se uma vista [NotFound] caso a rota digitada pelo usuário não corresponda a nenhuma rota conhecida. Essa vista é importada na linha 8. As rotas são verificadas na ordem do arquivo de roteamento. Portanto, se chegarmos à linha 36, é porque a rota solicitada não é nenhuma das rotas das linhas 18 a 33;
19.8. A visualização [NotFound]
A visualização [NotFound] é exibida se a rota digitada pelo usuário não corresponder a nenhuma rota conhecida:

O código da visualização é o seguinte:
<!-- definição HTML da visualização -->
<template>
<!-- formatação -->
<Layout :left="true" :right="true">
<!-- alerta na coluna da direita -->
<template slot="right">
<!-- mensagem em fundo amarelo -->
<b-alert show variant="danger" align="center">
<h4>Cette page n'existe pas</h4>
</b-alert>
</template>
<!-- menu de navegação na coluna da esquerda -->
<Menu slot="left" :options="options" />
</Layout>
</template>
<script>
// importações
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// componentes
components: {
Layout,
Menu
},
// estado interno do componente
data() {
return {
// opções do menu de navegação
options: [
{
text: "Authentification",
path: "/"
}
]
};
},
// ciclo de vida
created() {
// eslint-desativar-linha-seguinte
console.log("NotFound created");
// analisamos quais opções de menu oferecer
if (this.$session.authenticated && this.$métier.taxAdminData) {
// o usuário pode realizar simulações
Array.prototype.push.apply(this.options, [
{
text: "Calcul de l'impôt",
path: "/calcul-impot"
},
{
text: "Liste des simulations",
path: "/liste-des-simulations"
}
]);
}
}
};
</script>
Comentários
- linha 4: utiliza as duas colunas das visualizações roteadas;
- linhas 6-11: uma mensagem de erro;
- linha 13: o menu de navegação ocupa a coluna da esquerda;
- linhas 31-36: as opções padrão do menu;
- linhas 40-57: código executado quando a visualização é criada;
- linha 44: verifica-se se o usuário pode realizar simulações;
- linhas 45-55: se sim, adicionam-se duas opções ao menu de navegação, aquelas que exigem autenticação e uma camada [métier] operacional (linhas 46-55);
19.9. A visualização [Authentification]
A visualização [Authentification] evolui da seguinte forma:
<!-- definição HTML da visualização -->
<template>
<Layout :left="false" :right="true">
...
</Layout>
</template>
<!-- dinâmica da visualização -->
<script>
import Layout from "./Layout";
export default {
// estado do componente
data() {
return {
// usuário
user: "",
// sua senha
password: "",
// controla a exibição de uma mensagem de erro
showError: false,
// a mensagem de erro
message: ""
};
},
// componentes utilizados
components: {
Layout
},
// propriedades calculadas
computed: {
// entradas válidas
valid() {
return this.user && this.password && this.$session.started;
}
},
// gerenciadores de eventos
methods: {
// ----------- autenticação
async login() {
try {
// início da espera
this.$emit("loading", true);
// ainda não estamos autenticados
this.$session.authenticated = false;
// autenticação bloqueada pelo servidor
const response = await this.$dao.authentifierUtilisateur(
this.user,
this.password
);
// fim do carregamento
this.$emit("loading", false);
// análise da resposta do servidor
if (response.état != 200) {
// exibindo o erro
this.message = response.réponse;
this.showError = true;
// retorno ao ciclo de eventos
return;
}
// sem erro
this.showError = false;
// autenticação realizada
this.$session.authenticated = true;
// --------- agora solicitamos os dados da administração fiscal
// inicialmente, sem dados
this.$métier.setTaxAdminData(null);
// início da espera
this.$emit("loading", true);
// solicitação em espera no servidor
const response2 = await this.$dao.getAdminData();
// fim do carregamento
this.$emit("loading", false);
// análise da resposta
if (response2.état != 1000) {
// exibição do erro
this.message = response2.réponse;
this.showError = true;
// retorno ao ciclo de eventos
return;
}
// sem erro
this.showError = false;
// os dados recebidos são armazenados na camada [métier]
this.$métier.setTaxAdminData(response2.réponse);
// é possível prosseguir para o cálculo do imposto
this.$router.push({ name: "calculImpot" });
} catch (error) {
// o erro é repassado ao componente principal
this.$emit("error", error);
} finally {
// atualização da sessão
this.$session.métier = this.$métier;
// a sessão é salva
this.$session.save();
}
}
},
// ciclo de vida: o componente acaba de ser criado
created() {
// eslint-disable-next-line
console.log("Authentification created");
// o usuário pode realizar simulações?
if (
this.$session.started &&
this.$session.authenticated &&
this.$métier.taxAdminData
) {
// então o usuário pode realizar simulações
this.$router.push({ name: "calculImpot" });
// retorno ao ciclo de eventos
return;
}
// se a sessão jSON já tiver sido iniciada, ela não será reiniciada
if (!this.$session.started) {
// início da espera
this.$emit("loading", true);
// inicializa-se a sessão com o servidor — solicitação assíncrona
// utiliza-se a promessa retornada pelos métodos da camada [dao]
this.$dao
// inicializando uma sessão jSON
.initSession()
// a resposta foi obtida
.then(response => {
// fim da espera
this.$emit("loading", false);
// análise da resposta
if (response.état != 700) {
// exibindo o erro
this.message = response.réponse;
this.showError = true;
// retorno ao ciclo de eventos
return;
}
// a sessão foi iniciada
this.$session.started = true;
})
// em caso de erro
.catch(error => {
// o erro é encaminhado para a visualização [Main]
this.$emit("error", error);
})
// em todos os casos
.finally(() => {
// a sessão é salva
this.$session.save();
});
}
}
};
</script>
Comentários
- destacamos em amarelo as instruções que utilizam a sessão inserida nesta versão do cliente [Vue.js];
- linhas 97, 148: ao final dos métodos [login, created], a sessão é salva independentemente do resultado das consultas HTTP que ocorrem nesses métodos (cláusula [finally] em ambos os casos);
- o método [created] das linhas 102-150 é executado sempre que a visualização [Authentification] é criada. Se foi o usuário quem digitou o URL da visualização, a sessão nos permitirá saber o que fazer;
- linhas 106-115: se a sessão jSON estiver iniciada, o usuário autenticado e os dados [this.$métier.taxAdminData] inicializados, então o usuário pode acessar diretamente o formulário de cálculo do imposto (linha 112);
- linha 117: o método [created] era utilizado na versão anterior para inicializar uma sessão jSON com o servidor. Essa etapa é desnecessária se já tiver ocorrido;
- linhas 42-66: o método de autenticação;
- linha 66: se a autenticação for bem-sucedida, isso é registrado na sessão;
- linhas 67-92: a solicitação ao servidor dos dados da administração fiscal [taxAdminData];
- linha 95: ao final desta fase, atualiza-se a propriedade [métier] da sessão, independentemente de a operação ter sido bem-sucedida ou não;
19.10. A visualização [CalculImpot]
O código da visualização [CalculImpot] é alterado da seguinte forma:
<!-- definição HTML da visualização -->
<template>
...
</template>
<script>
// importações
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
export default {
// estado interno
data() {
return {
// opções do menu
options: [
{
text: "Liste des simulations",
path: "/liste-des-simulations"
},
{
text: "Fin de session",
path: "/fin-session"
}
],
// resultado do cálculo do imposto
résultat: "",
résultatObtenu: false
};
},
// componentes utilizados
components: {
Layout,
FormCalculImpot,
Menu
},
// métodos de gerenciamento de eventos
methods: {
// resultado do cálculo do imposto
handleResultatObtenu(résultat) {
// o resultado é construído em cadeia HTML
...
// uma simulação de +
this.$store.commit("addSimulation", résultat);
// a sessão é salva
this.$session.save();
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
}
};
</script>
Comentários
- linha 45: a simulação calculada é adicionada ao store [Vuex]. Isso afeta a sessão, que inclui a propriedade [state] do store. Por isso, salvamos a sessão (linha 47);
- linha 51: cria-se um método [created] para acompanhar, nos logs, a criação das visualizações;
19.11. A visualização [ListeSimulations]
A visão [ListeSimulations] sofre as seguintes alterações:
<!-- definição HTML da visualização -->
<template>
...
</div>
</template>
<script>
// importações
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// componentes
components: {
Layout,
Menu
},
// estado interno
data() {
...
},
// estado interno calculado
computed: {
// lista de simulações selecionadas no armazenamento Vuex
simulations() {
return this.$store.state.simulations;
}
},
// métodos
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// exclusão da simulação nº [index]
this.$store.commit("deleteSimulation", index);
// salvamos a sessão
this.$session.save();
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
}
};
</script>
Comentários
- linha 36: após a exclusão de uma simulação na linha 34, salva-se a sessão para refletir essa mudança de estado;
- linhas 40-43: continua-se acompanhando a criação das visualizações;
19.12. Execução do projeto

Durante os testes, verifique os seguintes pontos:
- se o usuário “utilizar” o aplicativo por meio dos links do menu de navegação e dos botões/links de ação, o aplicativo funciona;
- se o usuário digitar manualmente URL, o aplicativo continua funcionando. Faça, em especial, o seguinte teste:
- faça simulações;
- uma vez na tela [ListeSimulations], atualize (F5) a tela. No aplicativo anterior, [vuejs-20], as simulações eram perdidas nesse momento. Aqui, isso não acontece: as simulações já realizadas são recuperadas;
- verifique os logs para entender:
- em que momento o script [main] é executado. Você deve perceber que ele é executado sempre que o usuário digita um URL manualmente;
- em que momentos as visualizações são criadas. Você deve perceber que elas são criadas sempre que vão ser exibidas;
- o funcionamento do roteamento. Antes de cada roteamento, é gerado um log que indica:
- a rota de onde você vem;
- a rota para onde você está indo;
19.13. Implantação do aplicativo em um servidor local
Como exercício, siga o parágrafo |Implantação em um servidor local| para implantar o projeto [vuejs-21] no servidor Laragon local. Em seguida, teste-o.
19.14. Ajustes na versão móvel
Teoricamente, o uso do Bootstrap deveria nos permitir ter um aplicativo utilizável em diferentes dispositivos: smartphones, tablets, laptops e computadores de mesa. O que diferencia esses dispositivos é o tamanho de suas telas.
Se testarmos a versão [vuejs-21] em um celular, perceberemos que a exibição das visualizações fica um caos. A versão [vuejs-22] corrige esse problema. Todas as alterações ocorreram nos modelos das visualizações. Elas consistiram essencialmente em ajustar a exibição para a tela de um smartphone. Quando isso está ajustado, a exibição em telas maiores ocorre de maneira fluida graças ao Bootstrap.

19.14.1. A visualização [Main]
A visualização [Main] evolui da seguinte forma:
<!-- definição HTML da visualização -->
<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>
Comentários
- linha 8: onde antes havia [cols=’4’], escreve-se [sm=’4’]. [sm] significa [small]. As telas dos smartphones se enquadram nessa categoria. As outras categorias são [xs=extra small, md=medium, lg=large, xl=extra large];
- linha 11: o mesmo;
19.14.2. A visualização [Layout]
A visualização [Layout] evolui da seguinte forma:
<!-- definição HTML do layout da visualização roteada -->
<template>
<!-- linha -->
<div>
<b-row>
<!-- área de três colunas à esquerda -->
<b-col sm="3" v-if="left">
<slot name="left" />
</b-col>
<!-- área de nove colunas à direita -->
<b-col sm="9" v-if="right">
<slot name="right" />
</b-col>
</b-row>
</div>
</template>
19.14.3. A visualização [Authentification]
A visualização [Authentification] passa a ter a seguinte forma:
<!-- definição HTML da visualização -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- formulário HTML — os valores são enviados com a ação [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>
<!-- 1ª linha -->
<b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
<!-- campo de entrada do usuário -->
<b-col sm="6">
<b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
</b-col>
</b-form-group>
<!-- 2ª linha -->
<b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
<!-- campo de digitação de senha -->
<b-col sm="6">
<b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
</b-col>
</b-form-group>
<!-- 3ª linha -->
<b-alert
show
variant="danger"
v-if="showError"
class="mt-3"
>L'erreur suivante s'est produite : {{message}}</b-alert>
<!-- botão do tipo [submit] na terceira linha -->
<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>
Comentários
- linhas 11 e 19: foi removido o atributo [label-cols], que definia o número de colunas do rótulo do campo de entrada. Na ausência desse atributo, o rótulo fica acima do campo de entrada. Isso se adapta melhor às telas dos smartphones;
19.14.4. A visualização [CalculImpot]
A visualização [CalculImpot] sofreu as seguintes alterações:
<!-- definição HTML da visualização -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formulário de cálculo do imposto à direita -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menu de navegação à esquerda -->
<Menu slot="left" :options="options" />
</Layout>
<!-- área de exibição dos resultados do cálculo do imposto abaixo do formulário -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- área vazia com três colunas -->
<b-col sm="3" />
<!-- área de nove colunas -->
<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. A visualização [FormCalculImpot]
A visualização [FormCalculImpot] passa a ter a seguinte forma:
<!-- definição HTML da visualização -->
<template>
<!-- formulário HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- mensagem em 12 colunas com fundo 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 do formulário -->
<!-- primeira linha -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- botões de opção em 5 colunas-->
<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 linha -->
<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>
<!-- possível mensagem de erro -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- terceira linha -->
<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>
<!-- possível mensagem de erro -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- quarta linha, botão [submit] -->
<b-col sm="3">
<b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>
</b-form>
</template>
Comentários
- linhas 15, 23, 35: o atributo [label-cols] foi removido;
Além disso, estamos atualizando os testes de validade:
...
// estado interno calculado
computed: {
// validação do formulário
formInvalide() {
return (
// salário inválido
!this.salaire.match(/^\s*\d+\s*$/) ||
// ou filhos inválidos
!this.enfants.match(/^\s*\d+\s*$/) ||
// ou dados fiscais não obtidos
!this.$métier.taxAdminData
);
},
// validação do salário
salaireValide() {
// deve ser um número >=0
return Boolean(
this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
);
},
// validação de filhos
enfantsValide() {
// deve ser um valor numérico >=0
return Boolean(
this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
);
}
},
...
Comentários
- linha 19: quando nada foi inserido, a entrada é considerada válida. Isso permite que a entrada seja válida quando a visualização é exibida inicialmente. Na versão anterior, a entrada aparecia inicialmente como incorreta;
- linha 26: o mesmo;
- linhas 5-14: o botão de validação só fica ativo se ambas as entradas contiverem algum valor e forem válidas;
19.14.6. A tela [Menu]
A visualização [Menu] sofre as seguintes alterações:
<!-- definição HTML da visualização -->
<template>
<b-card class="mb-3">
<!-- menu Bootstrap vertical -->
<b-nav vertical>
<!-- opções do menu -->
<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>
Comentários
- linha 3: adiciona-se a tag <b-card> para envolver o menu com uma borda fina. Isso permite localizar melhor o menu no smartphone;
19.14.7. A visualização [ListeSimulations]
A visualização [ListeSimulations] permanece inalterada:
<!-- definição HTML da visualização -->
<template>
<div>
<!-- formatação -->
<Layout :left="true" :right="true">
<!-- simulações na coluna da direita -->
<template slot="right">
<template v-if="simulations.length==0">
<!-- sem simulações -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length!=0">
<!-- há simulações -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- tabela de simulações -->
<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>
<!-- menu de navegação na coluna da esquerda -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
Comentários
- linha 20: observe o atributo [responsive], que faz com que a exibição da tabela se adapte ao tamanho da tela:

- em [2], em telas pequenas, uma barra de rolagem horizontal permite visualizar a tabela;
19.14.8. A visualização [NotFound]
Permanece inalterada.
19.14.9. As visualizações em dispositivos móveis


Observação: certamente é possível obter visualizações ainda mais adaptadas para dispositivos móveis. Estou pensando, em particular, no menu de navegação, que poderia ser aprimorado, mas há outros pontos a serem considerados. O objetivo principal deste documento não era a criação de um aplicativo móvel. Nesse caso, talvez tivéssemos recorrido a um framework como o Ionic |https://ionicframework.com/|.