Перед вами четвёртая часть серии материалов, которые посвящены разработке веб-приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой, второй и третьей частях речь шла о создании основных серверных и клиентских компонентов приложения. Сегодня мы продолжим развитие проекта, а именно — займёмся списками документов и клиентов. Кроме того, нельзя не заметить, что к настоящему моменту сделано уже немало, поэтому вполне можно критически взглянуть на то, что получилось, и поработать над повторным использованием кода.
Budget
на List
. Кроме того, переименуем три компонента, которые находятся в этой папке. А именно, BudgetList
теперь будет называться List
, BudgetListHeader
получит название ListHeader
, а BudgetListBody — ListBody
. В итоге папка и файлы компонентов должны выглядеть так, как показано на рисунке ниже.List
и приведём его к такому виду:<template>
<section class="l-list-container">
<slot name="list-header"></slot>
<slot name="list-body"></slot>
</section>
</template>
<script>
export default {}
</script>
ListHeader
и внесём в него следующие изменения:<template>
<header class="l-list-header">
<div class="md-list-header white--text"
v-if="headers != null"
v-for="header in headers">
{{ header }}
</div>
</header>
</template>
<script>
export default {
props: ['headers']
}
</script>
<style lang="scss">
@import "./../../assets/styles";
.l-list-header {
display: none;
width: 100%;
@media (min-width: 601px) {
margin: 25px 0 0;
display: flex;
}
.md-list-header {
width: 100%;
background-color: $background-color;
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 45px;
align-items: center;
justify-content: center;
font-size: 22px;
@media (min-width: 601px) {
justify-content: flex-start;
}
}
}
</style>
props
). Так мы сможем повторно использовать этот компонент на других страницах.ListBody
:<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != null"
v-for="item in data">
<div class="md-info white--text" v-for="info in item" v-if="info != item._id">
{{ info }}
</div>
<div class="l-actions">
<v-btn small flat color="light-blue lighten-1">
<v-icon small>visibility</v-icon>
</v-btn>
<v-btn small flat color="yellow accent-1">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
<script>
export default {
props: ['data']
}
</script>
<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-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;
}
}
.l-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;
}
}
}
}
</style>
Home
и отредактируем его:<template>
<main class="l-home-page">
<app-header></app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list>
<list-header slot="list-header" :headers="budgetHeaders"></list-header>
<list-body slot="list-body" :data="budgets"></list-body>
</list>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
color="red lighten-1"
v-model="snackbar">
{{ message }}
</v-snackbar>
</main>
</template>
<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 {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
snackbar: false,
timeout: 6000,
message: ''
}
},
mounted () {
this.getAllBudgets()
},
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')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
}
}
</script>
<style lang="scss" scoped>
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
</style>
snackbar
, что даёт возможность показывать сообщения об ошибках в том случае, если нам не удастся получить данные. Кроме того, мы добавили сюда новый массив для данных компонента, budgetHeaders
, а также данные, необходимые для snackbar
.budgetHearders
для показа заголовков списка. Кроме того, мы внесли некоторые изменения в метод getAllBudgets
: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')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
options
с использованием оператора расширения.parsedItem
.options
, после завершения его подготовки он будет помещён в массив parsedData
, который мы возвращаем из этого метода.snackbar
.router
и откроем index.js
:import Vue from 'vue'
import Router from 'vue-router'
import * as Auth from '@/components/pages/Authentication'
// Pages
import Home from '@/components/pages/Home'
import Authentication from '@/components/pages/Authentication/Authentication'
// Global components
import Header from '@/components/Header'
import List from '@/components/List/List'
// Register components
Vue.component('app-header', Header)
Vue.component('list', List)
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
list: List
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
router.beforeEach((to, from, next) => {
if (to.path !== '/login') {
if (Auth.default.user.authenticated) {
next()
} else {
router.push('/login')
}
} else {
next()
}
})
export default router
router.beforeEach
, так как мы собираемся защищать любой маршрут, отличающийся от login
, мы убираем meta
из маршрута страницы Home
.Home
. Поэтому вернёмся к компоненту Home
и создадим новый массив в данных компонента, дав ему имя clients
, а также создадим массив clientHeaders
и логическую переменную budgetsVisible
:return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: ''
}
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, '_id', 'client', 'email', 'phone')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
mounted () {
this.getAllBudgets()
this.getAllClients()
},
Home
:<template>
<main class="l-home-page">
<app-header :budgetsVisible="budgetsVisible" @toggleVisibleData="budgetsVisible = !budgetsVisible"></app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list>
<list-header slot="list-header" :headers="budgetsVisible ? budgetHeaders : clientHeaders"></list-header>
<list-body slot="list-body"
:budgetsVisible="budgetsVisible"
:data="budgetsVisible ? budgets : clients">
</list-body>
</list>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
color="red lighten-1"
v-model="snackbar">
{{ message }}
</v-snackbar>
</main>
</template>
<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 {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: false,
snackbar: false,
timeout: 6000,
message: ''
}
},
mounted () {
this.getAllBudgets()
this.getAllClients()
},
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')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
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', 'client', 'email', 'phone')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
}
}
</script>
<style lang="scss" scoped>
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
</style>
budgetVisible
в Header
и, кроме того, используем эту переменную в тернарном операторе сравнения для вывода нужных данных. В Header
так же попадает переменная toggleVisibleData
, где мы инвертируем значение budgetsVisible
. Причина, по которой мы передаём в Header
свойства, заключается в том, что благодаря такому подходу мы можем сделать ещё некоторые улучшения, о которых поговорим ниже. Кроме того, в слотах list-header
и list-body
мы используем тернарные операторы сравнения.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="search"
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>
</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>
<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible'],
data () {
return {
search: '',
status: '',
statusItems: [
'All', 'Approved', 'Denied', 'Waiting', 'Writing', 'Editing'
]
}
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
<style lang="scss">
@import "./../assets/styles";
.l-header-container {
background-color: $background-color;
margin: 0 auto;
padding: 0 15px;
min-width: 272px;
.l-budgets-header {
label, input, .icon, .input-group__selections__comma {
color: #29b6f6!important;
}
}
.l-clients-header {
label, input, .icon, .input-group__selections__comma {
color: #66bb6a!important;
}
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.btn {
margin-top: 15px;
}
}
</style>
budgetsVisible
. Если документы видимы, заголовок будет иметь светло-синий цвет, если нет — зелёный.budgetVisible
, по её щелчку вызывается обработчик соответствующего события, меняющий состояние логической переменной.ListBody
:<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != 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 }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="light-blue lighten-1">
<v-icon small>visibility</v-icon>
</v-btn>
<v-btn small flat color="yellow accent-1">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
<script>
export default {
props: ['data', 'budgetsVisible']
}
</script>
<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>
Header
.Home
, добавим следующий код ниже 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">
<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">
<v-icon>account_circle</v-icon>
</v-btn>
<span>Add new Client</span>
</v-tooltip>
</v-speed-dial>
</v-fab-transition>
Home
:data () {
return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: '',
fab: false
}
},
fab
, которое используется для указания того, активна плавающая кнопка или нет.К сожалению, не доступен сервер mySQL