19. Удосконалення клієнта Vue.js
19.1. Introduction
Ми будемо тестувати проект [vuejs-21] на сервері розробки. Тому нам знову знадобиться, щоб сервер надсилав заголовки CORS. Отже, файл [config.json] версії 14 сервера розрахунку податків повинен підтримувати такі заголовки:

Проєкт [vuejs-21] спочатку створюється шляхом дублювання проєкту [vuejs-20]. Потім його змінюють на [3].
З’являються нові файли:
- [session.js]: експортує об’єкт [session], який буде містити інформацію про поточну сесію;
- [pluginSession]: робить попередній об’єкт [session] доступним у властивості [$session] переглядів;
- [NotFound.vue]: новий вигляд, що відображається, коли користувач вручну запитує URL, якого не існує;
Будуть змінені такі файли:
- [main.js]: ініціалізує поточну сесію, а потім, коли користувач вручну введе URL, відновить її;
- [router.js]: додано перевірки для обробки випадків, коли користувач вводить URL;
- [store.js]: додано нову зміну;
- [config.js]: додано нову конфігурацію;
- різні види, в основному для збереження поточної сесії в ключові моменти роботи додатка. Потім вона відновлюється щоразу, коли користувач вручну вводить URL;
19.2. Магазин [Vuex]
Скрипт [./store] змінюється наступним чином:
// плагін Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// Vuex-сторінка
const store = new Vuex.Store({
state: {
// таблиця моделювань
simulations: [],
// номер останнього моделювання
idSimulation: 0
},
mutations: {
// видалення рядка № індексу
deleteSimulation(state, index) {
...
},
// додавання симуляції
addSimulation(state, simulation) {
...
},
// очищення стану
clear(state) {
// більше симуляцій
state.simulations = [];
// нумерація симуляцій починається з 0
state.idSimulation = 0;
}
}
});
// експорт об’єкта [store]
export default store;
- рядки 24–29: зміна [clear] видаляє список збережених симуляцій і обнуляє номер останньої симуляції.
19.3. Сесія
Необхідність у сесії пояснюється тим, що коли користувач вводить URL у поле адреси браузера, скрипт [main.js] виконується знову. Однак він містить інструкцію:
// Vuex-сторінка
import store from './store'
Ця інструкція імпортує наступний файл [./store]:
// плагін Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// архів Vuex
const store = new Vuex.Store({
state: {
// таблиця симуляцій
simulations: [],
// номер останнього моделювання
idSimulation: 0
},
mutations: {
...
}
});
// експорт об’єкта [store]
export default store;
Як бачимо в рядках 7–13, імпортується порожній масив симуляцій. Отже, якщо симуляції існували до того, як користувач ввів URL у поле адреси браузера, то після цього їх більше немає. Ідея полягає в тому, щоб:
- використовувати сесію, яка зберігатиме інформацію, яку ми хочемо зберегти, якщо користувач вручну вводить URL;
- зберігати її в ключові моменти роботи додатка;
- відновити її в [main.js], який завжди виконується, коли URL вводиться вручну;
Скрипт [./session] має такий вигляд:
// імпортуємо Vuex-сторі
import store from './store'
// імпортуємо конфігурацію
import config from './config';
// об’єкт [session]
const session = {
// сесія запущена
started: false,
// аутентифікація
authenticated: false,
// час збереження
saveTime: "",
// шар [métier]
métier: null,
// стан Vuex
state: null,
// збереження сеансу в рядку jSON
save() {
// до сеансу додаються деякі властивості
this.saveTime = Date.now();
this.state = store.state;
// перетворюємо її на jSON
const json = JSON.stringify(this);
// зберігаємо її в браузері
localStorage.setItem("session", json);
// eslint-disable-next-line no-console
console.log("session save", json);
},
// відновлення сесії
restore() {
// отримуємо сесію jSON з браузера
const json = localStorage.getItem("session")
// якщо щось було отримано
if (json) {
// відновлюються всі ключі сеансу
const restore = JSON.parse(json);
for (var key in restore) {
if (restore.hasOwnProperty(key)) {
this[key] = restore[key];
}
}
// якщо з моменту початку сеансу минув певний час бездіяльності, починаємо з нуля
let durée = Date.now() - this.saveTime;
if (durée > config.duréeSession) {
// очищаємо сесію — вона також буде збережена
session.clear();
} else {
// регенерується сховище Vuex
store.replaceState(JSON.parse(JSON.stringify(this.state)));
}
}
// eslint-disable-next-line no-console
console.log("session restore", this);
},
// очищаємо сесію
clear() {
// eslint-disable-next-line no-console
console.log("session clear");
// очищення деяких полів сесії
this.authenticated = false;
this.saveTime = "";
this.started = false;
if (this.métier) {
// поле [taxAdminData] скидається
this.métier.taxAdminData = null;
}
// також очищається Vuex-сторінг
store.commit("clear");
// зберігається нова сесія
this.save();
},
}
// експорт об’єкта [session]
export default session;
Коментарі
- рядок 2: сесія також інкапсулює сховище [Vuex] (список симуляцій, номер останньої виконаної симуляції);
- рядки 7–17: інформація, що зберігається сесією:
- [started]: чи розпочалася сесія jSON із сервером;
- [authenticated]: чи пройшов користувач аутентифікацію;
- [saveTime]: дата останнього збереження у мілісекундах;
- [métier]: посилання на рівень [métier]. Воно містить дані [taxAdminData], які дозволяють розрахувати податок;
- [state]: стан сховища [Vuex] (список симуляцій, номер останньої виконаної симуляції);
- рядки 20–30: метод [save] зберігає сесію локально в браузері, що виконує додаток;
- рядок 22: фіксується час збереження;
- рядок 23: отримується [state] із сховища [Vuex];
- рядок 25: створюється рядок сеансу jSON;
- рядок 27: зберігається локально в браузері, пов’язаному з ключем [session];
- рядки 33–57: метод [restore] дозволяє відновити сесію з її локальної резервної копії в браузері;
- рядок 35: завантажується локальна резервна копія jSON;
- рядок 37: якщо щось було відновлено;
- рядки 39–44: об’єкт [session] відновлюється;
- рядок 46: обчислюється час, що минув з моменту останньої резервної копії;
- рядки 47–50: якщо цей проміжок часу перевищує значення [config.duréeSession], встановлене в налаштуваннях, сесія перезапускається (рядок 49) і при цьому зберігається;
- рядок 52: в іншому випадку регенерується атрибут [state] сховища [Vuex];
- рядки 60–75: метод [clear] скидає сесію;
- рядки 64–70: властивості сеансу скидаються до початкових значень;
- рядок 72: так само як і сховище [Vuex];
- рядок 74: нова сесія зберігається;
19.4. Конфігураційний файл [config]
Файл [./config] змінюється наступним чином:
// використання бібліотеки [axios]
const axios = require('axios');
// тайм-аут запитів HTTP
axios.defaults.timeout = 2000;
...
// експорт конфігурації
export default {
// об’єкт [axios]
axios: axios,
// максимальний час бездіяльності сеансу: 5 хв = 300 с = 300000 мс
duréeSession: 300000
}
- рядок 12: сесію додатка будемо керувати приблизно так само, як і веб-сесію. Тут встановлюємо максимальний час бездіяльності — 5 хвилин;
19.5. Плагін [pluginSession]
Як це вже робилося багато разів, плагін [pluginSession] дозволить подам отримувати доступ до сесії через властивість [this.$session]:
export default {
install(Vue, session) {
// додає властивість [$session] до класу представлення
Object.defineProperty(Vue.prototype, '$session', {
// коли відбувається звернення до Vue.$session, повертається другий параметр [session]
get: () => session,
})
}
}
19.6. Головний скрипт [main]
Головний скрипт [./main.js] змінюється наступним чином:
// журнал запуску
// eslint-disable-next-line no-console
console.log("main started");
// імпорти
import Vue from 'vue'
...
// інстанціювання шару [métier]
import Métier from './couches/Métier';
const métier = new Métier();
// плагін [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
// Vuex-сховище
import store from './store'
// сесія
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
// перед перезапуском сесія відновлюється
session.restore();
// відновлюється рівень [métier]
if (session.métier && session.métier.taxAdminData) {
métier.setTaxAdminData(session.métier.taxAdminData);
}
// запуск програми UI
new Vue({
el: '#додаток,
// маршрутизатор
router: router,
// жалюзі Vuex
store: store,
// головний екран
render: h => h(Main),
})
// завершальний журнал
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
- рядок 19: імпортується сесія;
- рядок 20: імпортується його плагін;
- рядок 21: плагін [pluginSession] інтегрується в [Vue]. Після цієї інструкції всі подання мають сесію у своєму атрибуті [$session];
- рядок 27: сесія відновлюється. Сесія, імпортована в рядку 11, ініціалізується вмістом її останнього збереження;
- після рядка 16 перегляди мають властивість [$métier], ініціалізовану в рядку 12. Ця властивість не містить інформації [taxAdminData], яка дозволяє розрахувати податок;
- рядки 30–32: якщо під час щойно виконаного відновлення було відновлено властивість [session.métier.taxAdminData], то властивість [$métier] переглядів ініціалізується цим значенням;
19.7. Файл маршрутизації [router]
Файл маршрутизації [./router] змінюється наступним чином:
// імпорти
import Vue from 'vue'
import VueRouter from 'vue-router'
// види
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// сесія
import session from './session'
// плагін маршрутизації
Vue.use(VueRouter)
// маршрути додатка
const routes = [
// аутентифікація
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification', component: Authentification },
// розрахунок податку
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// список моделювань
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// завершення сеансу
{
path: '/fin-session', name: 'finSession'
},
// невідома сторінка
{
path: '*', name: 'notFound', component: NotFound,
},
]
// маршрутизатор
const router = new VueRouter({
// маршрути
routes,
// режим відображення URL
mode: 'history',
// основний URL додатка
base: '/client-vuejs-impot/'
})
// перевірка маршрутів
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// шлях, доступний лише для автентифікованих користувачів?
if (to.meta.authenticated && !session.authenticated) {
next({
// переходимо до автентифікації
name: 'authentification',
})
// повернення до циклу обробки подій
return;
}
// особливий випадок завершення сеансу
if (to.name === "finSession") {
// очищення сесії
session.clear();
// перехід до подання [authentification]
next({
name: 'authentification',
})
// повернення до циклу обробки подій
return;
}
// інші випадки — наступний звичайний вигляд маршрутизації
next();
})
// експорт маршрутизатора
export default router
Коментарі
- рядки 16–38: деякі маршрути було доповнено додатковою інформацією;
- рядок 19: створено новий маршрут до виду [Authentification];
- рядки 21–24: маршрут, що веде до подання [CalculImpot], тепер має властивість [meta] (ця назва є обов’язковою). Вміст цього об’єкта може бути будь-яким і визначається розробником;
- рядок 23: у [meta] задаємо властивість [authenticated] (ця назва може бути будь-якою). Для нас це означатиме, що для переходу до подання [CalculImpot] користувач повинен пройти автентифікацію;
- рядки 26–29: те саме робимо для маршруту, що веде до подання [ListeSimulations]. Тут також користувач повинен пройти автентифікацію;
- властивість [meta.authenticated] дозволить нам перевірити, що користувач, який вручну вводить URL з переглядів [CalculImpot, ListeSimulations], не зможе отримати їх, якщо він не пройшов аутентифікацію;
- рядки 51–76: метод [beforeEach] виконується до того, як погляд буде маршрутизовано. Це вдалий момент для проведення перевірок;
- [to]: наступний маршрут, якщо нічого не робити;
- [from]: останній відображений маршрут;
- [next]: функція, що дозволяє змінити наступний маршрут, який відображається;
- рядок 55: перевіряємо, чи наступний маршрут вимагає автентифікації користувача;
- рядки 56–59: якщо так і користувач не пройшов автентифікацію, наступний маршрут змінюється на перегляд [Authentification];
- рядки 64–73: обробляється особливий випадок маршруту [finSession] із рядків 30–32. Цей маршрут не має пов’язаного з ним виду;
- рядок 66: сесія скидається до початкового стану;
- рядки 68–70: призначаємо вигляд [Authentification] як наступний вигляд;
- рядок 75: якщо ситуація не відповідає жодному з двох попередніх випадків, просто переходимо до маршруту, передбаченого файлом маршрутизації;
- рядки 35–37: передбачається вигляд [NotFound], якщо маршрут, введений користувачем, не відповідає жодному відомому маршруту. Цей вигляд імпортується у рядку 8. Маршрути перевіряються у порядку, заданому файлом маршрутизації. Отже, якщо програма дійшла до рядка 36, це означає, що запитуваний маршрут не є жодним із маршрутів, зазначених у рядках 18–33;
19.8. Вигляд [NotFound]
Вигляд [NotFound] відображається, якщо маршрут, введений користувачем, не відповідає жодному відомому маршруту:

Код цього виду такий:
<!-- визначення HTML подання -->
<template>
<!-- верстка -->
<Layout :left="true" :right="true">
<!-- сповіщення у правій колонці -->
<template slot="right">
<!-- повідомлення на жовтому тлі -->
<b-alert show variant="danger" align="center">
<h4>Cette page n'existe pas</h4>
</b-alert>
</template>
<!-- меню навігації у лівій колонці -->
<Menu slot="left" :options="options" />
</Layout>
</template>
<script>
// імпорт
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// компоненти
components: {
Layout,
Menu
},
// внутрішній стан компонента
data() {
return {
// параметри меню навігації
options: [
{
text: "Authentification",
path: "/"
}
]
};
},
// життєвий цикл
created() {
// eslint-disable-next-line
console.log("NotFound created");
// визначаємо, які опції меню пропонувати
if (this.$session.authenticated && this.$métier.taxAdminData) {
// користувач може проводити симуляції
Array.prototype.push.apply(this.options, [
{
text: "Calcul de l'impôt",
path: "/calcul-impot"
},
{
text: "Liste des simulations",
path: "/liste-des-simulations"
}
]);
}
}
};
</script>
Коментарі
- рядок 4: використовуються обидва стовпці маршрутизованих поглядів;
- рядки 6–11: повідомлення про помилку;
- рядок 13: меню навігації займає ліву колонку;
- рядки 31–36: опції меню за замовчуванням;
- рядки 40–57: код, що виконується під час створення подання;
- рядок 44: перевіряється, чи може користувач виконувати симуляції;
- рядки 45–55: якщо так, до меню навігації додаються два пункти, для яких необхідна авторизація та наявність робочого шару [métier] (рядки 46–55);
19.9. Вигляд [Authentification]
Вигляд [Authentification] змінюється наступним чином:
<!-- визначення HTML подання -->
<template>
<Layout :left="false" :right="true">
...
</Layout>
</template>
<!-- динаміка подання -->
<script>
import Layout from "./Layout";
export default {
// стан компонента
data() {
return {
// користувач
user: "",
// його пароль
password: "",
// контролює відображення повідомлення про помилку
showError: false,
// повідомлення про помилку
message: ""
};
},
// використані компоненти
components: {
Layout
},
// розраховані властивості
computed: {
// дійсні введені дані
valid() {
return this.user && this.password && this.$session.started;
}
},
// обробники подій
methods: {
// ----------- автентифікація
async login() {
try {
// початок очікування
this.$emit("loading", true);
// аутентифікація ще не виконана
this.$session.authenticated = false;
// автентифікація, що блокує роботу сервера
const response = await this.$dao.authentifierUtilisateur(
this.user,
this.password
);
// завершення завантаження
this.$emit("loading", false);
// аналіз відповіді сервера
if (response.état != 200) {
// відображення помилки
this.message = response.réponse;
this.showError = true;
// повернення до циклу обробки подій
return;
}
// помилки немає
this.showError = false;
// автентифікація пройшла успішно
this.$session.authenticated = true;
// --------- тепер запитуються дані податкової адміністрації
// спочатку даних немає
this.$métier.setTaxAdminData(null);
// початок очікування
this.$emit("loading", true);
// запит, що блокує роботу сервера
const response2 = await this.$dao.getAdminData();
// завершення завантаження
this.$emit("loading", false);
// аналіз відповіді
if (response2.état != 1000) {
// виводиться повідомлення про помилку
this.message = response2.réponse;
this.showError = true;
// повернення до циклу обробки подій
return;
}
// помилки немає
this.showError = false;
// збереження отриманих даних у шарі [métier]
this.$métier.setTaxAdminData(response2.réponse);
// можна перейти до розрахунку податку
this.$router.push({ name: "calculImpot" });
} catch (error) {
// помилку передаємо до головного компонента
this.$emit("error", error);
} finally {
// Оновлення сесії
this.$session.métier = this.$métier;
// зберігаємо сесію
this.$session.save();
}
}
},
// життєвий цикл: компонент щойно створено
created() {
// eslint-disable-next-line
console.log("Authentification created");
// чи може користувач проводити симуляції?
if (
this.$session.started &&
this.$session.authenticated &&
this.$métier.taxAdminData
) {
// тоді користувач може проводити симуляції
this.$router.push({ name: "calculImpot" });
// повернення до циклу подій
return;
}
// якщо сесія jSON вже запущена, її не запускають знову
if (!this.$session.started) {
// початок очікування
this.$emit("loading", true);
// ініціалізуємо сесію з сервером — асинхронний запит
// використовується обіцянка, повернута методами шару [dao]
this.$dao
// ініціалізується сесія jSON
.initSession()
// отримано відповідь
.then(response => {
// завершено очікування
this.$emit("loading", false);
// аналіз відповіді
if (response.état != 700) {
// виводиться повідомлення про помилку
this.message = response.réponse;
this.showError = true;
// повернення до циклу обробки подій
return;
}
// сесія розпочалася
this.$session.started = true;
})
// у разі помилки
.catch(error => {
// помилка передається до подання [Main]
this.$emit("error", error);
})
// у будь-якому випадку
.finally(() => {
// сесія зберігається
this.$session.save();
});
}
}
};
</script>
Коментарі
- жовтим кольором виділено інструкції, які використовують сесію, введену в цій версії клієнта [Vue.js];
- рядки 97, 148: у кінці методів [login, created] сесія зберігається незалежно від результату запитів HTTP, що виконуються в цих методах (у обох випадках використовується оператор [finally]);
- метод [created] у рядках 102–150 виконується щоразу, коли створюється представлення [Authentification]. Якщо користувач ввів URL для цього подання, сесія дозволить нам визначити, що робити;
- рядки 106–115: якщо сесія jSON запущена, користувач автентифікований, а дані [this.$métier.taxAdminData] ініціалізовані, то користувач може безпосередньо перейти до форми розрахунку податку (рядок 112);
- рядок 117: метод [created] використовувався в попередній версії для ініціалізації сесії jSON із сервером. Цей етап є зайвим, якщо він уже відбувся;
- рядки 42–66: метод автентифікації;
- рядок 66: якщо автентифікація пройшла успішно, це фіксується в сесії;
- рядки 67–92: запит до сервера щодо даних податкової адміністрації [taxAdminData];
- рядок 95: наприкінці цього етапу оновлюється властивість [métier] сесії незалежно від того, чи операція була успішною;
19.10. Вигляд [CalculImpot]
Код подання [CalculImpot] змінюється наступним чином:
<!-- визначення HTML для подання -->
<template>
...
</template>
<script>
// імпорт
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
export default {
// внутрішній стан
data() {
return {
// параметри меню
options: [
{
text: "Liste des simulations",
path: "/liste-des-simulations"
},
{
text: "Fin de session",
path: "/fin-session"
}
],
// результат розрахунку податку
résultat: "",
résultatObtenu: false
};
},
// використані компоненти
components: {
Layout,
FormCalculImpot,
Menu
},
// методи управління подіями
methods: {
// результат розрахунку податку
handleResultatObtenu(résultat) {
// результат формується у вигляді ланцюжка HTML
...
// моделювання +
this.$store.commit("addSimulation", résultat);
// збереження сеансу
this.$session.save();
}
},
// життєвий цикл
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
}
};
</script>
Коментарі
- рядок 45: розрахована симуляція додається до сховища [Vuex]. Це впливає на сесію, яка охоплює властивість [state] цього сховища. Тому сесію зберігають (рядок 47);
- рядок 51: створюється метод [created] для відстеження у журналах створення переглядів;
19.11. Вигляд [ListeSimulations]
Вигляд [ListeSimulations] змінюється наступним чином:
<!-- визначення HTML подання -->
<template>
...
</div>
</template>
<script>
// імпорти
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// компоненти
components: {
Layout,
Menu
},
// внутрішній стан
data() {
...
},
// розрахований внутрішній стан
computed: {
// список симуляцій, взятих із сховища Vuex
simulations() {
return this.$store.state.simulations;
}
},
// методи
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// видалення симуляції № [index]
this.$store.commit("deleteSimulation", index);
// збереження сесії
this.$session.save();
}
},
// життєвий цикл
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
}
};
</script>
Коментарі
- рядок 36: після видалення симуляції з рядка 34 сесія зберігається, щоб врахувати цю зміну стану;
- рядки 40–43: продовжується відстеження створення поглядів;
19.12. Виконання проєкту

Під час тестування перевірте наступні моменти:
- чи працює додаток, коли користувач «користується» ним через посилання в меню навігації та кнопки/посилання дій;
- якщо користувач вручну вводить URL, додаток продовжує працювати. Зокрема, проведіть таке тестування:
- проведіть симуляції;
- перейшовши на сторінку [ListeSimulations], перезавантажте (F5) цю сторінку. У попередньому додатку [vuejs-20] у цьому випадку втрачалися симуляції. Тут цього не відбувається: вже виконані симуляції зберігаються;
- перегляньте журнали, щоб зрозуміти:
- у який момент виконується скрипт [main]. Ви повинні побачити, що він виконується щоразу, коли користувач вручну вводить URL;
- у які моменти створюються перегляди. Ви повинні побачити, що вони створюються щоразу, коли їх мають відобразити;
- як працює маршрутизація. Перед кожною маршрутизацією створюється запис у журналі, який вказує:
- маршрут, з якого ви прийшли;
- маршрут, куди ви прямуєте;
19.13. Розгортання додатка на локальному сервері
Як вправу, виконайте вказівки з розділу |Розгортання на локальному сервері|, щоб розгорнути проєкт [vuejs-21] на локальному сервері Laragon. Потім протестуйте його.
19.14. Налагодження мобільної версії
Теоретично використання Bootstrap має дозволити нам отримати додаток, придатний для використання на різних пристроях: смартфонах, планшетах, ноутбуках та настільних комп’ютерах. Ці пристрої відрізняються розміром екрану.
Якщо протестувати версію [vuejs-21] на мобільному пристрої, можна помітити, що відображення сторінок є хаотичним. Версія [vuejs-22] виправляє цю проблему. Усі зміни внесені в шаблони сторінок. Вони полягали, головним чином, у налагодженні відображення для екрану смартфона. Коли це налагоджено, відображення на екранах більшого розміру відбувається плавно завдяки Bootstrap.

19.14.1. Вигляд [Main]
Вигляд [Main] змінюється наступним чином:
<!-- визначення HTML подання -->
<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>
Коментарі
- рядок 8: там, де було [cols=’4’], записуємо [sm=’4’]. [sm] означає [small]. Екрани смартфонів належать до цієї категорії. Інші категорії — [xs=extra small, md=medium, lg=large, xl=extra large];
- рядок 11: те саме;
19.14.2. Вигляд [Layout]
Вигляд [Layout] змінюється наступним чином:
<!-- визначення HTML макета маршрутизованого виду -->
<template>
<!-- рядок -->
<div>
<b-row>
<!-- зона з трьома стовпцями ліворуч -->
<b-col sm="3" v-if="left">
<slot name="left" />
</b-col>
<!-- зона з дев'ятьма стовпцями праворуч -->
<b-col sm="9" v-if="right">
<slot name="right" />
</b-col>
</b-row>
</div>
</template>
19.14.3. Вигляд [Authentification]
Вигляд [Authentification] змінюється наступним чином:
<!-- визначення HTML для подання -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- форма HTML — значення відправляються за допомогою дії [authentifier-utilisateur] -->
<b-form @submit.prevent="login">
<!-- заголовок -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- 1-й рядок -->
<b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
<!-- поле введення користувача -->
<b-col sm="6">
<b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
</b-col>
</b-form-group>
<!-- 2-й рядок -->
<b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
<!-- поле введення пароля -->
<b-col sm="6">
<b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
</b-col>
</b-form-group>
<!-- третій рядок -->
<b-alert
show
variant="danger"
v-if="showError"
class="mt-3"
>L'erreur suivante s'est produite : {{message}}</b-alert>
<!-- кнопка типу [submit] у третьому рядку -->
<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>
Коментарі
- рядки 11 і 19: було видалено атрибут [label-cols], який встановлював кількість стовпців для мітки поля введення. За відсутності цього атрибута мітка розташовується над полем введення. Це краще підходить для екранів смартфонів;
19.14.4. Вигляд [CalculImpot]
Вигляд [CalculImpot] змінюється наступним чином:
<!-- визначення HTML для подання -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- форма для розрахунку податку праворуч -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- меню навігації ліворуч -->
<Menu slot="left" :options="options" />
</Layout>
<!-- область відображення результатів розрахунку податку під формою -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- порожня область із трьома стовпцями -->
<b-col sm="3" />
<!-- область з дев'ятьма стовпцями -->
<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. Вигляд [FormCalculImpot]
Вигляд [FormCalculImpot] змінюється наступним чином:
<!-- визначення HTML для подання -->
<template>
<!-- форма HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- повідомлення на 12 стовпців на синьому тлі -->
<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>
<!-- елементи форми -->
<!-- перший рядок -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- перемикачі у 5 стовпців-->
<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>
<!-- другий рядок -->
<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>
<!-- можливе повідомлення про помилку -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- третій рядок -->
<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>
<!-- можливе повідомлення про помилку -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- четвертий рядок, кнопка [submit] -->
<b-col sm="3">
<b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>
</b-form>
</template>
Коментарі
- рядки 15, 23, 35: видалено атрибут [label-cols];
Крім того, вдосконалюються тести на валідність:
...
// розрахований внутрішній стан
computed: {
// підтвердження форми
formInvalide() {
return (
// неправильна заробітна плата
!this.salaire.match(/^\s*\d+\s*$/) ||
// або недійсні дані про дітей
!this.enfants.match(/^\s*\d+\s*$/) ||
// або податкові дані не отримано
!this.$métier.taxAdminData
);
},
// перевірка заробітної плати
salaireValide() {
// повинно бути числом >=0
return Boolean(
this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
);
},
// перевірка даних про дітей
enfantsValide() {
// повинно бути числом >=0
return Boolean(
this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
);
}
},
...
Коментарі
- рядок 19: якщо нічого не введено, введення вважається правильним. Це дозволяє мати правильне введення при першому відображенні вікна. У попередній версії введення спочатку відображалося як помилкове;
- рядок 26: те саме;
- рядки 5–14: кнопка підтвердження активна лише тоді, коли обидва поля містять дані та є правильними;
19.14.6. Вікно [Menu]
Вигляд [Menu] змінюється наступним чином:
<!-- визначення HTML для подання -->
<template>
<b-card class="mb-3">
<!-- вертикальне меню Bootstrap -->
<b-nav vertical>
<!-- параметри меню -->
<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>
Коментарі
- рядок 3: додається тег <b-card>, щоб оточити меню тонкою рамкою. Це дозволяє краще розмістити меню на смартфоні;
19.14.7. Вигляд [ListeSimulations]
Вигляд [ListeSimulations] залишається без змін:
<!-- визначення HTML для подання -->
<template>
<div>
<!-- верстка -->
<Layout :left="true" :right="true">
<!-- симуляції у правій колонці -->
<template slot="right">
<template v-if="simulations.length==0">
<!-- без моделювань -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length!=0">
<!-- є симуляції -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- таблиця симуляцій -->
<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 slot="left" :options="options" />
</Layout>
</div>
</template>
Коментарі
- рядок 20: зверніть увагу на атрибут [responsive], завдяки якому відображення таблиці адаптується до розміру екрану:

- у [2] на невеликих екранах горизонтальна смуга прокрутки дозволяє переглядати таблицю;
19.14.8. Вигляд [NotFound]
Залишається без змін.
19.14.9. Види на мобільних пристроях


Примітка: напевно, є можливість отримати види, ще краще адаптовані до мобільних пристроїв. Я маю на увазі, зокрема, меню навігації, яке можна було б вдосконалити, але є й інші моменти. Головною метою цього документа не було створення мобільного додатка. У такому випадку, можливо, ми б звернулися до фреймворку, такого як Ionic |https://ionicframework.com/|.