使用 Vue 2.5 實作「簽到簿」應用程式

環境

  • Windows 7
  • node 8.11.1
  • npm 5.6.0

安裝

1
npm install -g @vue/cli

建立專案

1
vue create guestbook-vue

啟動圖形化介面

1
vue ui

安裝套件

安裝 Axios 套件。

1
npm install --save axios vue-axios

安裝 Lodash 套件。

1
npm install --save vue-lodash

安裝 Bootstrap Vue 套件。

1
npm install --save bootstrap-vue

main.js 檔引入相關套件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios'
import VueAxios from 'vue-axios'
import VueLodash from 'vue-lodash'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.config.productionTip = false

Vue.use(VueAxios, axios);
axios.defaults.baseURL = 'http://guestbook.test'; //設定 API 基礎位址

Vue.use(VueLodash, {
name: 'lodash'
});

Vue.use(BootstrapVue);

new Vue({
router,
render: h => h(App)
}).$mount('#app')

建立路由

router.js 檔建立路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'home',
component: () => import('./views/Home.vue')
},
{
path: '/signatures',
name: 'signature.index',
component: () => import('./views/signature/Index.vue')
},
{
path: '/signatures/create',
name: 'signature.create',
component: () => import('./views/signature/Create.vue')
}
]
})

建立視圖

新增 App.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div id="app">
<b-navbar toggleable type="dark" variant="dark" class="mb-3">
<b-navbar-toggle target="nav_text_collapse"></b-navbar-toggle>
<b-navbar-brand>Guestbook</b-navbar-brand>
<b-collapse is-nav id="nav_text_collapse">
<b-navbar-nav class="ml-auto">
<b-nav-item to="/" exact>Home</b-nav-item>
<b-nav-item to="/signatures" exact>Signatures</b-nav-item>
<b-nav-item to="/signatures/create" exact>Sign</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>

<b-container>
<router-view/>
</b-container>
</div>
</template>

新增 views/signature/Index.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<template>
<div>
<b-row class="mb-3">
<b-col md="3" offset-md="9">
<b-form-select v-model="meta.per_page" :options="options"/>
</b-col>
</b-row>

<b-table striped hover :fields="fields" :items="signatures">
<template slot="action" slot-scope="signature">
<b-button size="sm" variant="danger" @click.prevent="destroy(signature.item.id)">刪除</b-button>
</template>
</b-table>

<b-pagination
align="center"
v-model="meta.current_page"
:first-text="meta.first_text"
:prev-text="meta.prev_text"
:next-text="meta.next_text"
:last-text="meta.last_text"
:limit="meta.limit"
:per-page="meta.per_page"
:total-rows="meta.total_rows">
</b-pagination>
</div>
</template>

<script>
export default {
data() {
return {
url: '/api/signatures',
signatures: [],
meta: {
current_page: 1,
first_text: '第一頁',
prev_text: '前一頁',
next_text: '下一頁',
last_text: '最後頁',
limit: 7,
per_page: 10,
total_rows: 0
},
options: [
{ value: 5, text: 5 },
{ value: 10, text: 10 },
{ value: 15, text: 15 }
],
fields: [
{ key: 'id', label: '編號' },
{ key: 'name', label: '名字' },
{ key: 'content', label: '內容' },
{ key: 'action', label: '' }
]
};
},
created() {
this.fetch();
},
watch: {
'meta.current_page': function() {
this.fetch();
},
'meta.per_page': function() {
this.fetch();
}
},
methods: {
fetch() {
this.axios.get('/api/signatures' + '?page=' + this.meta.current_page + '&per_page=' + this.meta.per_page)
.then(({data}) => {
this.signatures = data.data;
this.meta.total_rows = data.meta.total;
});
},
destroy(id) {
if (confirm('確定刪除?')) {
this.axios.delete(this.url + '/' + id)
.then(() => {
this.data = this.lodash.remove(this.data, function (data) {
return data.id !== id;
});
this.fetch(this.url + '?page=' + this.meta.current_page);
});
}
}
}
}
</script>

新增 views/signature/Create.vue 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<template>
<div>
<b-form class="mb-3" @submit.prevent="onSubmit" @reset="onReset">
<b-form-group
label="名字"
label-for="name"
description="">
<b-form-input
id="name"
type="text"
v-model="signature.name"
:class="{
'is-valid': nameIsValid,
'is-invalid': nameIsInvalid
}"
required>
</b-form-input>
<span class="invalid-feedback" v-if="nameIsInvalid">
{{ errors.name[0] }}
</span>
</b-form-group>

<b-form-group
label="信箱"
label-for="email"
description="">
<b-form-input
id="email"
type="email"
v-model="signature.email"
:class="{
'is-valid': emailIsValid,
'is-invalid': emailIsInvalid
}"
required>
</b-form-input>
<span class="invalid-feedback" v-if="emailIsInvalid">
{{ errors.email[0] }}
</span>
</b-form-group>

<b-form-group
label="內容"
label-for="content"
description="">
<b-form-textarea
id="content"
v-model="signature.content"
:class="{
'is-valid': contentIsValid,
'is-invalid': contentIsInvalid
}"
:rows="3"
:max-rows="6"
required>
</b-form-textarea>
<span class="invalid-feedback" v-if="contentIsInvalid">
{{ errors.content[0] }}
</span>
</b-form-group>

<b-button type="submit" variant="primary">送出</b-button>&nbsp;
<b-button type="reset" variant="danger">重設</b-button>
</b-form>

<b-alert show dismissible fade v-if="saved">
表單已送出!
</b-alert>
</div>
</template>

<script>
export default {
data() {
return {
url: '/api/signatures',
signature: {
name: '',
email: '',
content: ''
},
validation: {
name: /^[a-zA-Z0-9\u4e00-\u9fa5]{3,30}$/,
email: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/,
content: /^.{3,30}$/
},
saved: false,
errors: []
};
},
computed: {
nameIsValid: function() {
return this.validation.name.test(this.signature.name.trim());
},
nameIsInvalid: function() {
return (!this.validation.name.test(this.signature.name.trim()) && this.errors.name);
},
emailIsValid: function() {
return this.validation.email.test(this.signature.email.trim());
},
emailIsInvalid: function() {
return (!this.validation.email.test(this.signature.email.trim()) && this.errors.email);
},
contentIsValid: function() {
return this.validation.content.test(this.signature.content.trim());
},
contentIsInvalid: function() {
return (!this.validation.content.test(this.signature.content.trim()) && this.errors.content);
}
},
methods: {
onSubmit() {
this.saved = false;
this.axios.post(this.url, this.signature)
.then(({data}) => {
this.success()
})
.catch(({response}) => {
this.error(response.data)
});
},
success() {
this.saved = true;
this.onReset();
},
error(data) {
this.errors = data;
},
onReset() {
this.errors = [];
this.signature = {
name: '',
email: '',
content: ''
};
}
}
}
</script>

部署

1
npm run build

程式碼