https://habrahabr.ru/company/ruvds/blog/346784/- Разработка веб-сайтов
- VueJS
- Node.JS
- JavaScript
- Блог компании RUVDS.com
Перед вами перевод пятой части руководства по разработке веб-решений на базе Node.js, Vue.js и MongoDB. В
первой,
второй,
третьей и
четвёртой частях мы рассказывали о поэтапном создании клиентской и серверной частей приложения Budget Manager. Те, кому не терпится увидеть в действии то, что в итоге получилось у автора этого материала, могут заглянуть
сюда. Кроме того,
вот GitHub-репозиторий проекта. Если вы — из тех, кто ценит строгую типизацию, то
здесь и
здесь находятся результаты переноса Budget Manager на TypeScript.
Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения API и доведём Budget Manager до рабочего состояния.
Доработка API
Для начала перейдём в папку
models
и откроем файл
budget.js
. Добавим в него поле
description
для модели:
description: {
type: String,
required: true
},
Теперь перейдём в папку
app/api
и откроем файл
budget.js
, который находится в ней. Тут мы собираемся отредактировать функцию сохранения данных,
store
, для того, чтобы новые документы обрабатывались правильно, добавить функцию
edit
, которая позволит редактировать документы, добавить функцию
remove
, которая нужна для удаления документов, и добавить функцию
getByState
, которая позволит фильтровать документы. Здесь приведён полный код файла. Для того, чтобы его просмотреть, разверните соответствующий блок. В дальнейшем большие фрагменты кода будут оформлены так же.
Исходный кодconst mongoose = require('mongoose');
const api = {};
api.store = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Client.findOne({ _id: req.body.client }, (error, client) => {
if (error) res.status(400).json(error);
if (client) {
const budget = new Budget({
client_id: req.body.client,
user_id: req.query.user_id,
client: client.name,
state: req.body.state,
description: req.body.description,
title: req.body.title,
total_price: req.body.total_price,
items: req.body.items
});
budget.save(error => {
if (error) return res.status(400).json(error)
res.status(200).json({ success: true, message: "Budget registered successfully" })
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ user_id: req.query.user_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAllFromClient = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ client_id: req.query.client_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOne({ _id: req.query._id }, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOneAndUpdate({ _id: req.body._id }, req.body, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getByState = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.find({ state: req.query.state }, (error, budget) => {
console.log(budget)
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Budget.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
Похожие изменения внесём в файл
client.js
из папки
api
:
Исходный кодconst mongoose = require('mongoose');
const api = {};
api.store = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
const client = new Client({
user_id: req.query.user_id,
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
});
client.save(error => {
if (error) return res.status(400).json(error);
res.status(200).json({ success: true, message: "Client registration successful" });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Client, Token) => (req, res) => {
if (Token) {
Client.find({ user_id: req.query.user_id }, (error, client) => {
if (error) return res.status(400).json(error);
res.status(200).json(client);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOne({ _id: req.query._id }, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOneAndUpdate({ _id: req.body._id }, req.body, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
И, наконец, добавим в систему новые маршруты. Для этого перейдём в папку
routes
и откроем файл
budget.js
:
Исходный кодconst passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.budget;
app.route('/api/v1/budget')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Budget, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAllFromClient(models.User, models.Budget, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/state')
.get(passport.authenticate('jwt', config.session), api.getByState(models.User, models.Budget, models.Client, app.get('budgetsecret')))
}
Внесём похожие изменения в файл
client.js
, который находится в той же папке:
Исходный кодconst passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.client;
app.route('/api/v1/client')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Client, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Client, app.get('budgetsecret')))
app.route('/api/v1/client/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Client, app.get('budgetsecret')))
}
Вот и все изменения, которые нужно внести в API.
Доработка маршрутизатора
Теперь добавим новые компоненты в маршруты. Для этого откроем файл
index.js
, находящийся внутри папки
router
.
Исходный код...
// Global components
import Header from '@/components/Header'
import List from '@/components/List/List'
import Create from '@/components/pages/Create'
// Register components
Vue.component('app-header', Header)
Vue.component('list', List)
Vue.component('create', Create)
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
list: List,
create: Create
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
…
Здесь мы импортировали и определили компонент
Create
и назначили его компонентом маршрута
Home
(сам компонент создадим ниже).
Создание новых компонентов
▍Компонент Create
Начнём с компонента
Create
. Перейдём в папку
components/pages
и создадим там новый файл
Create.vue
.
Исходный код<template>
<div class="l-create-page">
<budget-creation v-if="budgetCreation && !editPage" slot="budget-creation" :clients="clients" :saveBudget="saveBudget"></budget-creation>
<client-creation v-if="!budgetCreation && !editPage" slot="client-creation" :saveClient="saveClient"></client-creation>
<budget-edit v-else-if="budgetEdit && editPage"
slot="budget-creation"
:clients="clients"
:selectedBudget="budget"
:fixClientNameAndUpdate="fixClientNameAndUpdate">
</budget-edit>
<client-edit v-else-if="!budgetEdit && editPage"
slot="client-creation"
:selectedClient="client"
:updateClient="updateClient">
</client-edit>
</div>
</template>
<script>
import BudgetCreation from './../Creation/BudgetCreation'
import ClientCreation from './../Creation/ClientCreation'
import BudgetEdit from './../Creation/BudgetEdit'
import ClientEdit from './../Creation/ClientEdit'
export default {
props: [
'budgetCreation', 'clients', 'saveBudget',
'saveClient', 'budget', 'client', 'updateClient',
'fixClientNameAndUpdate', 'editPage', 'budgetEdit'
],
components: {
'budget-creation': BudgetCreation,
'client-creation': ClientCreation,
'budget-edit': BudgetEdit,
'client-edit': ClientEdit
}
}
</script>
Первый именованный слот —
budget-creation
. Он представляет компонент, который мы будем использовать для создания новых финансовых документов. Он будет виден только в том случае, когда свойство
budgetCreation
установлено в значение
true
, а
editPage
— в значение
false
, мы передаём ему всех наших клиентов и метод
saveBudget
.
Второй именованный слот —
client-creation
. Это — компонент, используемый для создания новых клиентов. Он будет видимым лишь в том случае, когда свойство
budgetCreation
установлено в
false
, и
editPage
так же имеет значение
false
. Сюда мы передаём метод
saveClient
.
Третий именованный слот —
budget-edit
. Это — компонент, который применяется для редактирования выбранного документа. Видим он только тогда, когда свойства
budgetEdit
и
editPage
установлены в
true
. Сюда мы передаём всех клиентов, выбранный финансовый документ и метод
fixClientNameAndUpdate
.
И, наконец здесь имеется, последний именованный слот, который используется для редактирования информации о клиентах. Он будет видим тогда, когда свойство
budgetEdit
установлено в
false
, а
editPage
— в
true
. Ему мы передаём выбранного клиента и метод
updateClient
.
▍Компонент BudgetCreation
Разработаем компонент, который используется для создания новых финансовых документов. Перейдём в папку
components
и создадим в ней новую папку, дав ей имя
Creation
. В этой папке создадим файл компонента
BudgetCreation.vue
.
Компонент это довольно большой, разберём его поэтапно, начиная с шаблона.
Шаблон компонента BudgetCreationВот код шаблона компонента<template>
<div class="l-budget-creation">
<v-layout row wrap>
<span class="md-budget-state-hint uppercased white--text">status</span>
<v-flex xs12 md2>
<v-select
label="Status"
:items="states"
v-model="budget.state"
>
</v-select>
</v-flex>
<v-flex xs12 md9 offset-md1>
<v-select
label="Client"
:items="clients"
v-model="budget.client"
item-text="name"
item-value="_id"
>
</v-select>
</v-flex>
<v-flex xs12 md12>
<v-text-field label="Title"
v-model="budget.title"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Description"
v-model="budget.description"
textarea
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">
<v-flex xs12 md1>
<v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Title"
box dark
v-model="item.title"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md1 offset-md1>
<v-text-field label="Price"
box dark
prefix="$"
v-model="item.price"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md1>
<v-text-field label="Quantity"
box dark
min="0"
v-model="item.quantity"
type="number"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2>
<span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>
</v-flex>
</v-layout>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>
</v-flex>
<v-flex xs12 md2 offset-md10>
<span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="saveBudget(budget)">Save</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Тут мы сначала добавляем в шаблон элемент
v-select
для установки состояния документа, затем —
v-select
для выбора клиента, который нам нужен. Далее, у нас имеется поле
v-text-field
для ввода заголовка документа и
v-text-field
для вывода описания.
Затем мы перебираем элементы
budget.items
, что даёт нам возможность добавлять элементы в документ и удалять их из него. Здесь же имеется красная кнопка, которая позволяет вызывать функцию
removeItem
, передавая ей элемент, который нужно удалить.
Далее, здесь есть три поля
v-text-fields
, предназначенные, соответственно, для названия товара, цены за единицу и количества.
В конце ряда имеется простой элемент
span
, в котором выводится промежуточный итог по строке,
subtotal
, представляющий собой произведение количества и цены товара.
Ниже списка товаров имеется ещё три элемента. Это — синяя кнопка, которая используется для добавления новых элементов путём вызова функции
addItem
, элемент
span
, который показывает общую стоимость всех товаров, которые имеются в документе (сумма показателей
subtotal
всех элементов), и зелёная кнопка, которая используется для сохранения документа в базу данных путём вызова функции
saveBudget
с передачей ей, в качестве параметра, документа, который мы хотим сохранить.
Скрипт компонента BudgetCreationВот код, который приводит компонент BudgetCreation в действие<script>
export default {
props: ['clients', 'saveBudget'],
data () {
return {
budget: {
title: null,
description: null,
state: 'writing',
client: null,
get total_price () {
let value = 0
this.items.forEach(({ subtotal }) => {
value += parseInt(subtotal)
})
return value
},
items: [
{
title: null,
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
]
},
states: [
'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'
]
}
},
methods: {
addItem () {
const items = this.budget.items
const item = {
title: '',
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
items.push(item)
},
removeItem (selected) {
const items = this.budget.items
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
}
}
}
</script>
В этом коде мы сначала получаем два свойства —
clients
и
saveBudget
. Источник этих свойств — компонент
Home
.
Затем мы определяем объект и массив, играющие роль данных. Объект имеет имя
budget
. Он используется для создания документа, мы можем добавлять в него значения и сохранять его в базе данных. У этого объекта есть свойства
title
(заголовок),
description
(описание),
state
(состояние, по умолчанию установленное в значение
writing
),
client
(клиент),
total_price
(общая стоимость по документу), и массив товаров
items
. У товаров имеются свойства
title
(название),
quantity
(количество),
price
(цена) и
subtotal
(промежуточный итог).
Здесь же определён массив состояний документа,
states
. Его значения используют для установки состояния документа. Вот эти состояния:
writing
,
editing
,
pending
,
approved
,
denied
и
waiting
.
Ниже, после описания структур данных, имеется пара методов:
addItem
(для добавления товаров) и
removeItem
(для их удаления).
Каждый раз, когда мы щёлкаем по синей кнопке, вызывается метод
addItem
, который добавляет элементы в массив
items
, находящийся внутри объекта
budget
.
Метод
removeItem
выполняет обратное действие. А именно — при щелчке по красной кнопке заданный элемент удаляется из массива
items
.
Стили компонента BudgetCreationВот стили для рассматриваемого компонента<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-budget-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #29b6f6!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #29b6f6!important;
}
}
}
.md-budget-state-hint {
margin: 10px 0;
display: block;
width: 100%;
}
.md-budget-state {
background-color: rgba(41, 182, 246, .6);
display: flex;
height: 35px;
width: 100%;
font-size: 14px;
align-items: center;
justify-content: center;
border-radius: 2px;
margin: 10px 0 15px;
}
.l-budget-item {
align-items: center;
}
.md-budget-item-subtotal {
font-size: 16px;
text-align: center;
display: block;
}
.md-budget-item-total {
font-size: 22px;
text-align: center;
display: block;
width: 100%;
margin: 30px 0 10px;
}
.md-add-item-btn {
margin-top: 30px !important;
display: block;
}
.list__tile__title, .input-group__selections {
text-transform: uppercase !important;
}
</style>
Теперь рассмотрим следующий компонент.
▍Компонент ClientCreation
Этот компонент, по сути, является упрощённой версией только что рассмотренного компонента
BudgetCreation
. Мы так же, как сделано выше, рассмотрим его по частям. Если вы разобрались с устройством компонента
BudgetCreation
, вы без труда поймёте и принципы работы компонента
ClientCreation
.
Шаблон компонента ClientCreation<template>
<div class="l-client-creation">
<v-layout row wrap>
<v-flex xs12 md4>
<v-text-field label="Name"
v-model="client.name"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Email"
v-model="client.email"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Phone"
v-model="client.phone"
required
mask="phone"
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="saveClient(client)">Save</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Скрипт компонента ClientCreation<script>
export default {
props: ['saveClient'],
data () {
return {
client: {
name: null,
email: null,
phone: null
}
}
}
}
</script>
Стили компонента ClientCreation<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-client-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #66bb6a!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #66bb6a!important;
}
}
}
</style>
Теперь пришла очередь компонента
BudgetEdit
.
▍Компонент BudgetEdit
Этот компонент, по сути, является модифицированной версией уже рассмотренного компонента
BudgetCreation
. Рассмотрим его составные части.
Шаблон компонента BudgetEdit<template>
<div class="l-budget-creation">
<v-layout row wrap>
<span class="md-budget-state-hint uppercased white--text">status</span>
<v-flex xs12 md2>
<v-select
label="Status"
:items="states"
v-model="budget.state"
>
</v-select>
</v-flex>
<v-flex xs12 md9 offset-md1>
<v-select
label="Client"
:items="clients"
v-model="budget.client_id"
item-text="name"
item-value="_id"
>
</v-select>
</v-flex>
<v-flex xs12 md12>
<v-text-field label="Title"
v-model="budget.title"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Description"
v-model="budget.description"
textarea
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">
<v-flex xs12 md1>
<v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Title"
box dark
v-model="item.title"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md1 offset-md1>
<v-text-field label="Price"
box dark
prefix="$"
v-model="item.price"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md1>
<v-text-field label="Quantity"
box dark
min="0"
v-model="item.quantity"
type="number"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2>
<span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>
</v-flex>
</v-layout>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>
</v-flex>
<v-flex xs12 md2 offset-md10>
<span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="fixClientNameAndUpdate(budget)">Update</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Единственное различие шаблонов компонентов
BudgetEdit
и
BudgetCreation
заключается в кнопке сохранения изменений и в связанной с ней логике. А именно, в
BudgetCreation
на ней написано
Save
, она вызывает метод
saveBudget
. В
BudgetEdit
эта кнопка несёт на себе надпись
Update
и вызывает метод
fixClientNameAndUpdate
.
Скрипт компонента BudgetCreation<script>
export default {
props: ['clients', 'fixClientNameAndUpdate', 'selectedBudget'],
data () {
return {
budget: {
title: null,
description: null,
state: 'pending',
client: null,
get total_price () {
let value = 0
this.items.forEach(({ subtotal }) => {
value += parseInt(subtotal)
})
return value
},
items: [
{
title: null,
quantity: 0,
price: null,
get subtotal () {
return this.quantity * this.price
}
}
]
},
states: [
'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'
]
}
},
mounted () {
this.parseBudget()
},
methods: {
addItem () {
const items = this.budget.items
const item = {
title: '',
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
items.push(item)
},
removeItem (selected) {
const items = this.budget.items
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
},
parseBudget () {
for (let key in this.selectedBudget) {
if (key !== 'total' && key !== 'items') {
this.budget[key] = this.selectedBudget[key]
}
if (key === 'items') {
const items = this.selectedBudget.items
const buildItems = item => ({
title: item.title,
quantity: item.quantity,
price: item.price,
get subtotal () {
return this.quantity * this.price
}
})
const parseItems = items => items.map(buildItems)
this.budget.items = parseItems(items)
}
}
}
}
}
</script>
Здесь всё начинается с получения трёх свойств. Это —
clients
,
fixClientNameAndUpdate
и
selectedBudget
. Данные тут те же самые, что и в компоненте
BudgetCreation
. А именно, тут имеется объект
Budget
и массив
states
.
Далее, здесь можно видеть обработчик события жизненного цикла компонента
mounted
, в котором мы вызываем метод
parseBudget
, о котором поговорим ниже. И, наконец, здесь есть объект
methods
, в котором присутствуют уже знакомые вам по компоненту
BudgetCreation
методы
addItem
и
removeItem
, а также новый метод
parseBudget
. Этот метод используется для того, чтобы установить значение объекта
budget
в то, которое передано в свойстве
selectedBudget
, но мы, кроме того, используем его для подсчёта промежуточных итогов по товарам документа и общей суммы по документу.
Стиль компонента BudgetCreation<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-budget-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #29b6f6!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #29b6f6!important;
}
}
}
.md-budget-state-hint {
margin: 10px 0;
display: block;
width: 100%;
}
.md-budget-state {
background-color: rgba(41, 182, 246, .6);
display: flex;
height: 35px;
width: 100%;
font-size: 14px;
align-items: center;
justify-content: center;
border-radius: 2px;
margin: 10px 0 15px;
}
.l-budget-item {
align-items: center;
}
.md-budget-item-subtotal {
font-size: 16px;
text-align: center;
display: block;
}
.md-budget-item-total {
font-size: 22px;
text-align: center;
display: block;
width: 100%;
margin: 30px 0 10px;
}
.md-add-item-btn {
margin-top: 30px !important;
display: block;
}
.list__tile__title, .input-group__selections {
text-transform: uppercase !important;
}
</style>
▍Компонент ClientEdit
Этот компонент, по аналогии с только что рассмотренным, похож на соответствующий компонент, используемый для создания клиентов —
ClientCreation
. Главное отличие заключается в том, что тут вместо метода
saveClient
используется метод
updateClient
. Рассмотрим устройство компонента
ClientEdit
.
Шаблон компонента ClientEdit<template>
<div class="l-client-creation">
<v-layout row wrap>
<v-flex xs12 md4>
<v-text-field label="Name"
v-model="client.name"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Email"
v-model="client.email"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Phone"
v-model="client.phone"
required
mask="phone"
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="updateClient(client)">Update</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Скрипт компонента ClientEdit<script>
export default {
props: ['updateClient', 'selectedClient'],
data () {
return {
client: {
name: null,
email: null,
phone: null
}
}
},
mounted () {
this.client = this.selectedClient
}
}
</script>
Стиль компонента ClientEdit<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-client-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #66bb6a!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #66bb6a!important;
}
}
}
</style>
На этом мы завершаем создание новых компонентов и переходим к работе с компонентами, которые уже были в системе.
Доработка существующих компонентов
Теперь осталось лишь внести некоторые изменения в существующие компоненты и приложение будет готово к работе.
Начнём с компонента
ListBody
.
▍Компонент ListBody
Шаблон компонента ListBody
Напомним, что код этого компонента хранится в файле
ListBody.vue
Исходный код<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != null && parsedBudgets === null"
v-for="item in data">
<div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"
v-for="info in item"
v-if="info != item._id && info != item.client_id">
{{ info }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
<div class="md-list-item"
v-if="parsedBudgets !== null"
v-for="item in parsedBudgets">
<div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"
v-for="info in item"
v-if="info != item._id && info != item.client_id">
{{ info }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
В этом компоненте надо выполнить буквально пару изменений и дополнений. Так, сначала добавим новое условие в конструкцию
v-if
блока
md-list-item
:
parsedBudgets === null
Кроме того, мы уберём первую кнопку, которую использовали для вывода документа, так как она нам больше не нужна из-за того, что увидеть документ можно, нажав на кнопку редактирования.
Тут мы добавили метод
getItemAndEdit
к новой первой кнопке и метод
deleteItem
к последней кнопке, передавая этому методу элемент, данные и переменную
budgetsVisible
в качестве параметров.
Ниже всего этого имеется блок
md-item-list
, который мы используем для вывода отфильтрованного после поиска списка документов.
Скрипт компонента ListBody<script>
export default {
props: ['data', 'budgetsVisible', 'deleteItem', 'getBudget', 'getClient', 'parsedBudgets'],
methods: {
getItemAndEdit (item) {
!item.phone ? this.getBudget(item) : this.getClient(item)
}
}
}
</script>
В этом компоненте мы получаем множество свойств. Опишем их:
data
: это либо список документов, либо список клиентов, но никогда и то и другое.
budgetsVisible
: используется для проверки того, просматриваем ли мы список документов или клиентов, может принимать значения true
или false
.
deleteItem
: функция для удаления элемента, которая принимает, в качестве параметра, некий элемент.
getBudget
: функция, которую мы используем для загрузки отдельного документа, который планируется редактировать.
getClient
: функция, используемая для загрузки карточки отдельного клиента для последующего редактирования.
parsedBudgets
: документы, отфильтрованные после выполнения поиска.
В компоненте есть всего один метод,
getItemAndEdit
. Он принимает, в качестве параметра, элемент, при этом, на основе анализа наличия у элемента свойства, содержащего телефонный номер, принимается решение о том, является ли элемент карточкой клиента или финансовым документом.
Стиль компонента ListBody<style lang="scss">
@import "./../../assets/styles";
.l-list-body {
display: flex;
flex-direction: column;
.md-list-item {
width: 100%;
display: flex;
flex-direction: column;
margin: 15px 0;
@media (min-width: 960px) {
flex-direction: row;
margin: 0;
}
.md-budget-info {
flex-basis: 25%;
width: 100%;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 35px;
align-items: center;
justify-content: center;
&:first-of-type, &:nth-of-type(2) {
text-transform: capitalize;
}
&:nth-of-type(3) {
text-transform: uppercase;
}
@media (min-width: 601px) {
justify-content: flex-start;
}
}
.md-client-info {
@extend .md-budget-info;
background-color: rgba(102, 187, 106, 0.45)!important;
&:nth-of-type(2) {
text-transform: none;
}
}
.l-budget-actions {
flex-basis: 25%;
display: flex;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
align-items: center;
justify-content: center;
.btn {
min-width: 45px !important;
margin: 0 5px !important;
}
}
.l-client-actions {
@extend .l-budget-actions;
background-color: rgba(102, 187, 106, 0.45)!important;
}
}
}
</style>
Правку кода компонента
ListBody
мы завершили, займёмся теперь компонентом
Header
.
▍Компонент Header
Шаблон компонента Header<template>
<header class="l-header-container">
<v-layout row wrap :class="budgetsVisible ? 'l-budgets-header' : 'l-clients-header'">
<v-flex xs12 md5>
<v-text-field v-model="searchValue"
label="Search"
append-icon="search"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'">
</v-text-field>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
@click.native="$emit('toggleVisibleData')">
{{ budgetsVisible ? "Clients" : "Budgets" }}
</v-btn>
</v-flex>
<v-flex xs12 offset-md1 md2>
<v-select label="Status"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
v-model="status"
:items="statusItems"
single-line
@change="selectState">
</v-select>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block color="red lighten-1 white--text" @click.native="submitSignout()">Sign out</v-btn>
</v-flex>
</v-layout>
</header>
</template>
Здесь, в первую очередь, мы меняем свойство
v-model
поля поиска на
searchValue
.
Кроме того, мы модифицируем элемент
v-select
, привязывая к его событию
change
метод
selectState
.
Скрипт компонента Header<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible', 'selectState', 'search'],
data () {
return {
searchValue: '',
status: '',
statusItems: [
'all', 'approved', 'denied', 'waiting', 'writing', 'editing'
]
}
},
watch: {
'searchValue': function () {
this.$emit('input', this.searchValue)
}
},
created () {
this.searchValue = this.search
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
Тут добавлены два новых свойства —
selectState
, представляющее собой функцию, и
search
, которое является строкой. В данных
search
теперь используется
searchValue
и приведённый к нижнему регистру массив элементов
statusItems
.
Стиль компонента Header<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible', 'selectState', 'search'],
data () {
return {
searchValue: '',
status: '',
statusItems: [
'all', 'approved', 'denied', 'waiting', 'writing', 'editing'
]
}
},
watch: {
'searchValue': function () {
this.$emit('input', this.searchValue)
}
},
created () {
this.searchValue = this.search
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
С компонентом
Header
мы разобрались, теперь поработаем с компонентом
Home
.
▍Компонент Home
Шаблон компонента Home<template>
<main class="l-home-page">
<app-header :budgetsVisible="budgetsVisible"
@toggleVisibleData="budgetsVisible = !budgetsVisible; budgetCreation = !budgetCreation"
:selectState="selectState"
:search="search"
v-model="search">
</app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list v-if="listPage">
<list-header slot="list-header" :headers="budgetsVisible ? budgetHeaders : clientHeaders"></list-header>
<list-body slot="list-body"
:budgetsVisible="budgetsVisible"
:data="budgetsVisible ? budgets : clients"
:search="search"
:deleteItem="deleteItem"
:getBudget="getBudget"
:getClient="getClient"
:parsedBudgets="parsedBudgets">
</list-body>
</list>
<create v-else-if="createPage"
:budgetCreation="budgetCreation"
:budgetEdit="budgetEdit"
:editPage="editPage"
:clients="clients"
:budget="budget"
:client="client"
:saveBudget="saveBudget"
:saveClient="saveClient"
:fixClientNameAndUpdate="fixClientNameAndUpdate"
:updateClient="updateClient">
</create>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
:color="snackColor"
v-model="snackbar">
{{ message }}
</v-snackbar>
<v-fab-transition>
<v-speed-dial v-model="fab"
bottom
right
fixed
direction="top"
transition="scale-transition">
<v-btn slot="activator"
color="red lighten-1"
dark
fab
v-model="fab">
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
<v-tooltip left>
<v-btn color="light-blue lighten-1"
dark
small
fab
slot="activator"
@click.native="budgetCreation = true; listPage = false; editPage = false; createPage = true">
<v-icon>assignment</v-icon>
</v-btn>
<span>Add new Budget</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="green lighten-1"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = false; editPage = false; createPage = true">
<v-icon>account_circle</v-icon>
</v-btn>
<span>Add new Client</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="purple lighten-2"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = true; budgetsVisible = true">
<v-icon>assessment</v-icon>
</v-btn>
<span>List Budgets</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="deep-orange lighten-2"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = true; budgetsVisible = false;">
<v-icon>supervisor_account</v-icon>
</v-btn>
<span>List Clients</span>
</v-tooltip>
</v-speed-dial>
</v-fab-transition>
</main>
</template>
Пожалуй, этот компонент претерпел наибольшие изменения. Теперь мы передаём ему
budgetsVisible
,
selectState
,
search
и
toggleVisibleData
в качестве свойств, кроме того, мы работаем с другой переменной в
toggleVisibleData
, и мы добавили
v-model
к
search
.
В тег
list
добавлена конструкция
v-if
, в результате он отображается только тогда, когда мы находимся на странице просмотра списков. Также, добавлено много новых свойств к
list-body
.
Сюда добавлен тег
create
, функционал которого похож на функционал
list
, но мы выводим его лишь в том случае, если находимся на странице создания элементов. Ему мы передаём все данные клиента и документов, а также все методы загрузки и обновления элементов.
В
v-fab-transition
добавлены две новые кнопки, что позволяет нам выводить документы и карточки клиентов, а так же создавать эти объекты.
Скрипт компонента Home<script>
import Axios from 'axios'
import Authentication from '@/components/pages/Authentication'
import ListHeader from './../List/ListHeader'
import ListBody from './../List/ListBody'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
components: {
'list-header': ListHeader,
'list-body': ListBody
},
data () {
return {
parsedBudgets: null,
budget: null,
client: null,
state: null,
search: null,
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: '',
fab: false,
listPage: true,
createPage: true,
editPage: false,
budgetCreation: true,
budgetEdit: true,
snackColor: 'red lighten-1'
}
},
mounted () {
this.getAllBudgets()
this.getAllClients()
this.hidden = false
},
watch: {
'search': function () {
if (this.search !== null || this.search !== '') {
const searchTerm = this.search
const regex = new RegExp(`^(${searchTerm})`, 'g')
const results = this.budgets.filter(budget => budget.client.match(regex))
this.parsedBudgets = results
} else {
this.parsedBudgets = null
}
}
},
methods: {
getAllBudgets () {
Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')
}).catch(error => {
this.errorHandler(error)
})
},
getAllClients () {
Axios.get(`${BudgetManagerAPI}/api/v1/client`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.clients = this.dataParser(data, 'name', 'email', '_id', 'phone')
}).catch(error => {
this.errorHandler(error)
})
},
getBudget (budget) {
Axios.get(`${BudgetManagerAPI}/api/v1/budget/single`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: budget._id
}
}).then(({data}) => {
this.budget = data
this.enableEdit('budget')
}).catch(error => {
this.errorHandler(error)
})
},
getClient (client) {
Axios.get(`${BudgetManagerAPI}/api/v1/client/single`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: client._id
}
}).then(({data}) => {
this.client = data
this.enableEdit('client')
}).catch(error => {
this.errorHandler(error)
})
},
enableEdit (type) {
if (type === 'budget') {
this.listPage = false
this.budgetEdit = true
this.budgetCreation = false
this.editPage = true
} else if (type === 'client') {
this.listPage = false
this.budgetEdit = false
this.budgetCreation = false
this.editPage = true
}
},
saveBudget (budget) {
Axios.post(`${BudgetManagerAPI}/api/v1/budget`, budget, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(res => {
this.resetFields(budget)
this.snackbar = true
this.message = res.data.message
this.snackColor = 'green lighten-1'
this.getAllBudgets()
})
.catch(error => {
this.errorHandler(error)
})
},
fixClientNameAndUpdate (budget) {
this.clients.find(client => {
if (client._id === budget.client_id) {
budget.client = client.name
}
})
this.updateBudget(budget)
},
updateBudget (budget) {
Axios.put(`${BudgetManagerAPI}/api/v1/budget/single`, budget, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(() => {
this.snackbar = true
this.message = 'Budget updated'
this.snackColor = 'green lighten-1'
this.listPage = true
this.budgetCreation = false
this.budgetsVisible = true
this.getAllBudgets()
})
.catch(error => {
this.errorHandler(error)
})
},
updateClient (client) {
Axios.put(`${BudgetManagerAPI}/api/v1/client/single`, client, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(() => {
this.snackbar = true
this.message = 'Client updated'
this.snackColor = 'green lighten-1'
this.listPage = true
this.budgetCreation = false
this.budgetsVisible = false
this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
saveClient (client) {
Axios.post(`${BudgetManagerAPI}/api/v1/client`, client, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(res => {
this.resetFields(client)
this.snackbar = true
this.message = res.data.message
this.snackColor = 'green lighten-1'
this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
deleteItem (selected, items, api) {
let targetApi = ''
api ? targetApi = 'budget' : targetApi = 'client'
Axios.delete(`${BudgetManagerAPI}/api/v1/${targetApi}`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: selected._id
}
})
.then(() => {
this.removeItem(selected, items)
})
.then(() => {
api ? this.getAllBudgets() : this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
errorHandler (error) {
const status = error.response.status
this.snackbar = true
this.snackColor = 'red lighten-1'
if (status === 404) {
this.message = 'Invalid request'
} else if (status === 401 || status === 403) {
this.message = 'Unauthorized'
} else if (status === 400) {
this.message = 'Invalid or missing information'
} else {
this.message = error.message
}
},
removeItem (selected, items) {
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
},
resetFields (item) {
for (let key in item) {
item[key] = null
if (key === 'quantity' || key === 'price') {
item[key] = 0
}
item['items'] = []
}
},
selectState (state) {
this.state = state
state === 'all' ? this.getAllBudgets() : this.getBudgetsByState(state)
},
getBudgetsByState (state) {
Axios.get(`${BudgetManagerAPI}/api/v1/budget/state`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id'), state }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')
}).catch(error => {
this.errorHandler(error)
})
}
}
}
</script>
В этот компонент добавлено множество новых данных. Опишем их.
parsedBudgets
: это свойство используется как массив для хранения всех документов, отфильтрованных в ходе поиска.
budget
: выбранный финансовый документ, который можно редактировать.
client
: выбранный клиент, данные которого можно редактировать.
state
: выбранное состояние документа, что позволяет выводить только документы, которым назначено это состояние.
search
: поисковый фильтр, использованный при поиске.
budgets
: все документы, полученные из API.
clients
: все карточки клиентов, полученные из API.
budgetHeaders
: массив, используемый для вывода таблицы документов.
clientHeaders
: массив, хранящий текст, используемый для вывода таблицы клиентов.
budgetsVisible
: используется для указания того, выводится ли список документов или клиентов.
snackbar
: используется для показа панели уведомлений.
timeout
: тайм-аут панели уведомлений.
message
: сообщение, выводимое в панель уведомлений.
fab
: состояние плавающей кнопки, по умолчанию установлено в false
.
listPage
: используется для проверки того, находимся ли мы на странице списка, по умолчанию установлено в true
.
createPage
: используется для проверки того, находимся ли мы на странице создания элемента, по умолчанию установлено в false
.
editPage
: используется для проверки того, находимся ли мы на странице редактирования элемента, по умолчанию установлено в false
.
budgetCreation
: используется для проверки того, создаём ли мы запись о клиенте или новый финансовый документ, по умолчанию установлено в true
.
budgetEdit
: используется для проверки того, редактируем ли мы карточку клиента или финансовый документ, по умолчанию установлено в true
.
snackColor
: цвет панели уведомлений.
Тут, так же, как в одном из примеров выше, назначен обработчик события жизненного цикла
mounted
, в нём мы загружаем все документы и все данные по клиентам.
В этот компонент, к полю списка, добавлена функция
watch
. Спасибо
@mrmonkeytech за то, что предложил воспользоваться здесь регулярными выражениями (я эту часть проекта чрезмерно усложнил).
Здесь мы улучшили все методы и добавили множество новых.
- В методе
getAllBudgets
добавлены новые параметры к dataParser
, теперь мы вызываем errorHandler
в блоке catch
. То же самое касается и метода getAllClients
.
- В компонент добавлены методы
getBudget
и getClient
, которые ответственны за загрузку лишь выбранных элементов из API.
- Метод
enableEdit
принимает, в качестве параметра, строку, и перенаправляет нас на страницу редактирования соответствующего элемента.
- Методы
saveBudget
и saveClient
используются, соответственно, для сохранения документов и карточек клиентов в базе данных.
- Метод
fixClientNameAndUpdate
используется для задания правильного имени клиента, основанного на его ID
, и для обновления документа в базе данных путём вызова метода updateBudget
.
- Метод
updateBudget
используется для обновления документов в базе данных.
- Метод
updateClient
используется для обновления карточек клиентов в базе данных.
- Метод
deleteItem
представляет собой универсальную функцию для удаления элементов из базы данных. Он принимает выбранный элемент, представленный параметром selected
, параметр items
(список документов или клиентов), и строковой параметр api
.
- Метод
errorHandler
применяется для обработки ошибок.
- Метод
removeItem
используется в методе deleteItem
для удаления элемента из интерфейса приложения после того, как он удалён из базы данных.
- Метод
dataParser
остаётся таким же, как был, его мы не изменили.
- Метод
resetFields
используется для сброса всех элементов в состояние по умолчанию после создания нового элемента. В результате пользователь может добавить столько документов или записей о клиентах, сколько нужно, без необходимости самостоятельно очищать заполненные поля после каждого сохранения нового объекта.
- Метод
selectState
используется для выбора нужного состояния документа из элемента v-select
компонента Header
и для фильтрации списка на основе выбранного состояния.
- Метод
getBudgetsByState
используется в методе selectState
для загрузки только тех финансовых документов, состояние которых соответствует выбранному.
Стиль компонента Home<style lang="scss">
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
.snack__content {
justify-content: center !important;
}
</style>
Итоги
На этом работа над веб-приложением Budget Manager завершена. Вот как оно выглядит.
Напомним, что опробовать его в действии можно
здесь, а посмотреть полный код —
здесь.
Надеемся, то, чему вы научились, осваивая это руководство, пригодится вам при разработке ваших собственных проектов.
Уважаемые читатели! Пригодилось ли вам на практике то, что вы узнали из этой серии материалов?