电商CMS系统

一、效果展示


在线预览


二、安装依赖


  1. element-ui

  2. 按需引入
    先按安装插件 babel-plugin-component

1
npm i babel-plugin-component -D

创建 babel.conifg.js,进行如下设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.export = {

presets: [

'@vue/cli-plugin-babel/preset'

],

plugin: [

"component",

{

libraryName: 'element-ui',

styleLibraryName: 'theme-chalk'

}

]

}

创建 plugin/element.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
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import Vue from 'vue'

import {

Button,

Form,

FormItem,

Input,

Message,

Container,

Header,

Aside,

Main,

Menu,

Submenu,

MenuItem,

Breadcrumb,

BreadcrumbItem,

Card,

Row,

Col,

Table,

TableColumn,

Switch,

Tooltip,

Pagination,

Dialog,

MessageBox,

Tag,

Tree,

Select,

Option,

Cascader,

Alert,

Tabs,

TabPane,

Steps,

Step,

CheckboxGroup,

Checkbox,

Upload,

Timeline,

TimelineItem,

Image,

Loading

} from 'element-ui'

Vue.use(Button)

Vue.use(Form)

Vue.use(FormItem)

Vue.use(Input)

Vue.use(Container)

Vue.use(Header)

Vue.use(Aside)

Vue.use(Main)

Vue.use(Menu)

Vue.use(Submenu)

Vue.use(MenuItem)

Vue.use(Breadcrumb)

Vue.use(BreadcrumbItem)

Vue.use(Card)

Vue.use(Row)

Vue.use(Col)

Vue.use(Table)

Vue.use(TableColumn)

Vue.use(Switch)

Vue.use(Tooltip)

Vue.use(Pagination)

Vue.use(Dialog)

Vue.use(Tag)

Vue.use(Tree)

Vue.use(Select)

Vue.use(Option)

Vue.use(Cascader)

Vue.use(Alert)

Vue.use(Tabs)

Vue.use(TabPane)

Vue.use(Steps)

Vue.use(Step)

Vue.use(CheckboxGroup)

Vue.use(Checkbox)

Vue.use(Upload)

Vue.use(Timeline)

Vue.use(TimelineItem)

// 使用 ElImageViewer 时必须引入,否则显示不正常

Vue.use(Image)

Vue.prototype.$message = Message

Vue.prototype.$confirm = MessageBox.confirm

Vue.prototype.$loading = Loading.service
  1. axios

  2. echarts

  3. lodash

  4. vue-quill-editor

  5. vue-table-with-tree-grid

  6. @babel/plugin-syntax-dynamic-import
    支持路由进行懒加载

  7. babel-plugin-transform-remove-console
    编译时移除所有 console.log 日志输出
    在 babel.config.js 中加入如下配置即可让其只在生产环境中生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const prodPlugins = []

if (process.env.NODE_ENV == 'production')
prodPlugins.push('tranform-remove-console')

module.export = {

...

plugins: [

...

...prodPlugins

]

}
  1. vue-cli-plugin-element
    用 element-ui 展示 vue ui 界面

三、全局样式


  1. base.css
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
* {

margin: 0;

padding: 0;

box - sizing: border - box;

}

/* 字体不倾斜 */

em,
i {

font - style: normal;

}

/* 去掉小圆点 */

li {

list - style: none;

}

img {

border: 0;
/* 低版本浏览器的img有边框 */

vertical - align: middle;
/* 取消图片底部的空白间隙 */

}

button {

pointer: cursor;

border: 0;

}

a {

color: #666;

text-decoration: none;

}

a:hover {

color: black;

}

button,
input {

font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB,
'5B8B4F53', sans-serif;
/* 5B8B4F53代表宋体 */

border: 0;

outline: none;
/* 取消蓝色边框 */

}

body {

-webkit-font-smoothing: antialiased;
/*
CSS3的抗锯齿,让字体显示得更清晰 */

background-color: # fff;

font: 12 px / 1.5 Microsoft YaHei,
Heiti SC,
tahoma,
arial,
Hiragino Sans GB,
'5B8B4F53',
sans - serif;

color: #666;

}

.hide,
.none {

display: none;

}
  1. global.css
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
html,
body,
#app {

margin: 0;

padding: 0;

width: 100% min-width: 1366px;

}

.el-breadcrumb {

margin-bottom: 15px;

font-size: 12px;

}

.el-card {

box-shadow: 0 1px 1px rgba(0, 0, 0, .12) !important;

}

.el-pagination {

margin-top: 15px;

}

.el-cascader-menu__wrap,

.el-scrollbar__wrap {

height: 290px !important;

}

.ql-editor {

min-height: 500px;

}

四、封装请求模块


  1. 创建utils/request.js用于封装axios请求模块
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
import axios from 'axios'

import Vue from 'vue'

axios.defaults.baseURL = Vue.prototype.$httpBaseUrl

axios.defaults.timeout = 5000

let loadingIns = null

axios.interceptors.request.use(config => {

loadingIns = Vue.prototype.$loading({
fullscreen: true
})

conifg.headers.Authorization = localStorage.getItem('token')

return config

},

err => {

loadingIns.close()

return Promise.reject(err)

})

axios.interceptors.response.use(config => {

loadingIns.close()

return config

},

err => {

loadingIns.close()

return Promise.reject(err)

})

Vue.prototype.$http = axios
  1. 创建api/index.js用于封装接口API
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
export const LoginAPI = data => this.$http.post('login', data)

export const MenuAPI = () => this.$http.get('menus')

export const UserListAPI = data => this.$http.get('users', {
params: data
})

export const ChangeUserStateAPI = (userid, userstate) =>
this.$http.put(`users/${userid}/state/${userstate}`)

export const GetUserInfoAPI = userId => this.$http.get('users/' +
userId)

export const RemoveUserAPI = userId => this.$http.delete('users/' +
userId)

export const EditUserInfoAPI = (userId, email, mobile) =>
this.$http.put('users/' + userId, {
email,
mobile
})

export const AddUserAPI = data => this.$http.post('users', data)

export const GetRoleListAPI = () => this.$http.get('roles')

export const SetRoleAPI = (userId, roleId) =>
this.$http.put(`users/${userId}/role`, {
rid: roleId
})

export const GetRoleInfoAPI = roleId => this.$http.get('roles/' +
roleId)

export const EditRoleInfoAPI = data => this.$http.put('roles/' +
data.roleId, {

roleName: data.roleName,

roleDesc: data.roleDesc

})

export const DeleteRoleAPI = roleId => this.$http.delete('roles/' +
roleId)

export const AddRoleApI = data => this.$http.post('roles', data)

export const GetRightListAPI = type => this.$http.get('rights/' +
type)

export const DeleteRightAPI = (roleId, rightId) =>
this.$http.delete(`roles/${roleId}/rights/${rightId}`)

export const SetRightAPI = (roleId, keys) =>
this.$http.post(`roles/${roleId}/rights`, {
rids: keys
})

export const GetGoodListAPI = data => this.$http.get('goods', data)

export const GetGoodInfoAPI = goodId => this.$http.get('goods/' +
goodId)

export const EditGoodInfoAPI = (goodId, data) =>
this.$http.put('goods/' + goodId, data)

export const RemoveGoodAPI = goodId => this.$http.delete('goods/' +
goodId)

export const GetCateListAPI = (params = null) =>
this.$http.get('categories', {
params
})

export const GetCateInfoAPI = cat_id => this.$http.get('categories/' +
cat_id)

export const AddCateAPI = data => this.$http.post('categories', data)

export const EditCateAPI = (cat_id, cat_name) =>
this.$http.put('categories/' + cat_id, {
cat_name
})

export const DeleteCateAPI = cat_id =>
this.$http.delete('categories/' + cat_id)

export const GetAttributesAPI = (cateId, type) =>
this.$http.get(`categories/${cateId}/attributes`, {
params: {
sel: type
}
})

export const GetAttrInfoAPI = (cateId, attrId, attr_sel) =>
this.$http.get(`categories/${cateId}/attributes/${attrId}`. {
params: {
attr_sel
}
})

export const AddAttributeAPI = (cateId, attr_name, attr_sel) =>
this.$http.post(`categories/${cateId}/attributes`, {
attr_name,
attr_sel
})

export const EditAttributeAPI = (cateId, attr_id, attr_name, attr_sel) => this.$http.put(`categories/${cateId}/attributes/${attrId}`, {
attr_name,
attr_sel
})

export const SetAttrValsAPI = (cateId, attrId, attr_sel, attr_name,
attr_vals) =>
this.$http.put(`categories/${cateId}/attributes/${attrId}`, {
attr_sel,
attr_name,
attr_vals
})

export const DeleteAttributeAPI = (cateId, attrId) =>
this.$http.delete(`categories` / $ {
cateId
}
/attributes/$ {
attrId
})

export const UploadImgAPI = data => this.$http.post('upload', data)

export const GetOrderListAPI = queryInfo => this.$http.get('orders',
queryInfo)

export const GetEMSInfoAPI = () =>
this.$http.get('kuaidi/1106975712662')

export const GetReport = () => this.$http.get('reports/type/1')

五、登录页面


  1. 在路由中增加导航守卫,禁止未登录的用户访问内部页面:
1
2
3
4
5
6
7
8
router.beforeEach((to, from, next) => {

if (to === '/login' || localStorage.getItem('token')) return
next()

next('/login')

})
  1. 创建Login/Login.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
<template>
<div class="login-container">
<div class="login-box">
<div class="avatar-box">
<img src="./assets/manager.png" />
</div>

<el-form
:model="loginForm"
:rules="loginFormRules"
ref="loginFormRef"
label-width="0"
class="login-form"
@keyup.enter.native="login"
>
<el-form-item prop="username">
<el-input
placeholder="请输入用户名"
prefix-icon="iconfont-icon-user"
v-model="loginForm.username"
@input="change($event)"
/>
</el-form-item>

<el-form-item prop="password">
<el-input
type="password"
placeholder="请输入密码"
prefix-icon="iconfont icon-3702mima"
v-model="loginForm.password"
@input="change($event)"
/>
</el-form-item>

<el-form-item class="btns">
<el-button type="primary" @click="login" :loading="loginLoading"
>登录</el-button
>

<el-button type="info" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>

import { LoginAPI } from '@/api'

export default {

data() {

loginForm: {

username: '',

password: ''

},

loginLoading: false,

loginFormRules: {

username: [

{
required: true,
message: '请输入用户名',
trigger: 'blur'
},

{
min: 3,
max: 10,
message: '长度在 3 到 10 个字符',
trigger: 'blur'
}

],

password: [

{
required: true,
message: '请输入密码',
trigger: 'blur'
},

{
min: 6,
max: 15,
message: '长度在 6 到15 个字符',
trigger: 'blur'
}

]

}

},

methods: {

login() {

this.loginLoading = true

this.$refs.loginFormRef.validate(async valid => {

if (!valid) return this.loginLoading = false

const {
data: res
} = await LoginAPI(this.loginForm)

if (res.meta.status !== 200) {

this.loginLoading = false

this.$message.error('登录失败,账号或用户名错误!')

}

this.$message.success('登录成功!')

localStorage.setItem('token', res.data.token)

this.$router.push('/home')

this.loginLoading = false

})

},

reset() {

this.$refs.loginFormRef.resetFields()

},
change(e) {
this.$forceUpdate()
}

}
}

</script>

<style lang="scss" scoped>
.login-container {
color: #35495e;

height: 100%;

.login-box {
width: 450px;

height: 300px;

position: relative;

top: 50%;

left: 50%;

transform: translate(-50%, -50%);

background-color: white;

border-radius: 3px;

.avatar-box {
position: absolute;

left: 50%;

transform: translate(-50%, -50%);

border-radius: 50%;

padding: 10px;

background-color: white;

border: 1px solid white;

width: 130px;

height: 130px;

box-shadow: 0 0 10px #ddd;

img {
width: 100%;

height: 100%;

border-radius: 50%;

background-color: #eee;
}
}

.login-form {
position: absolute;

bottom: 0;

width: 100%;

box-sizing: border-box;

padding: 0 20px;

.btns {
display: flex;

justify-content: flex-end;
}
}
}
}
</style>

注意:Element-ui的input组件偶尔会出现无法输入的问题,解决办法是给input组件的input事件添加一个forceUpdate方法[1]


六、主页


创建home/Home.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
<template>

<div class="home-container">

<el-header>

<div class="left-box">

<img src="~assets/manager.png" />

<span>电商管理系统</span>

</div>

<el-button type="info" @click="logout">退出</el-button>

</el-header>

<el-container>

<el-aside :width="isCollapse ? '61px' : '200px'">

<div class="toggle-button" @click="isCollapse = !isCollapse">

<i :class="isCollapse ? ' el-icon-caret-right' :
'el-icon-caret-left'" />

</div>

<el-menu background-color="#333744" text-color="#fff" active-text-color="#409EFF" :unique-opened="true"
:collapse="isCollapse" :collapse-transition="false" :router="true" :default-active="activeObj.path">

<el-submenu v-for="item in menuList" :index="item.id + ''" :key="item.id">
<!-- 注意:这里如果使用#title,打包发布到外网后会出现Cannot create property 'title' on boolean 'true'报错 -->
<template slot="title">

<i :class="iconObj[item.id]" />

<span>{{ item.authName }}</span>

</template>

<el-menu-item v-for="subItem in item.children" :index="'/' + subItem.path" :key="subItem.id"
@click="saveToActivePath('/' + subItem.path, item.authName, subItem,authName)">

<template slot="title">

<i class="el-icon-menu" />

<span>{{ subItem.authName }}</span>

</template>

<el-menu-item>

</el-submenu>

</el-menu>

</el-aside>

<el-main>

<router-view>

<template #breadcrumb>

<bread-crumb :name1="activeObj.name1" :name2="activeObj.name2">
</breadcrumb>

</template>

</router-view>

</el-main>

</el-container>

</div>

</template>
<script>

import { MenuAPI } form '@/api'

import BreadCrumb form '@/component/breadcrumb/BreadCrumb'

export default {

components: {

BreadCrumb

},

data() {

return {

isCollapse: false,

activeObj: {

path: '/users',

menuName1: '用户管理',

menuName2: '用户列表'

},

menuList: [],

iconsObj: {

125: 'iconfont icon-users',

103: 'iconfont icon-tijikongjian',

101: 'iconfont icon-shangpin',

102: 'iconfont icon-danju',

145: 'iconfont icon-baobiao'

}

}

},

created() {

this.getMenuList()

this.activeObj = JSON.parse(localStorage.getItem('activeObj')) ||
this.activeObj

},

methods: {

async getMenuList() {

const {
data: res
} = await this.MenuAPI()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取菜单列表成功!')

this.menuList = res.data

},

logout() {

localStorage.clear()

this.$router.push('/login')

},

saveToActivePath(path, name1, name2) {

this.activeObj.path = path

this.activeObj.name1 = name1

this.activeObj.name2 = name2

localStorage.setItem('activeObj', JSON.stringify(this.activeObj))

}

}

}

</script>

<style lang="scss" scoped>
.home-container {
height: 100% .el-header {
display: flex;

justify-content: space-between;

align-items: center;

font-size: 20px;

color: #fff;

background-color: #373d3f;

.left-box {
display: flex;

align-items: center;

img {
width: 45px;

height: 45px;

padding: 3px;

background-color: #fff;

box-shadow: 0 0 10px #ddd;

border-radius: 50%;
}

span {
margin-left: 10px;
}
}
}

.el-aside {
background-color: #333744;

.toggle-button {
background-color: #4a5064;

font-size: 10px;

text-align: center;

line-height: 24px;

color: #fff;

cursor: pointer;
}

.el-menu {
border-right: none;
}
}

.el-main {
background-color: #eaedf1;
}
}
</style>

七、面包屑


创建components/breadcrumb/BreadCrumb.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>

<el-breadcrumb-item>{{ name1 }}</el-breadcrumb-item>

<el-breadcrumb-item>{{ name2 }}</el-breadcrumb-item>
</el-breadcrumb>
</template>

<script>
export default {
props: {
name1: String,

name2: String,
},
};
</script>

八、用户页面


创建components/user/User.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
<template>

<div class="user-container">

<slot name="breadcrumb"></slot>

<el-card>

<el-row :gutter="20">

<el-col :span="7">

<el-input placeholder="请输入内容" clearable @clear="getUserList" v-model="queryInfo.query"
@keyup.enter.native="getUserList" @input="change($event)">

<el-button slot="append" icon="el-icon-search" @click="getUserList"></el-button>

</el-input>

</el-col>

<el-col :span="4">

<el-button type="primary" @click="addUserDialogVisible =
true">添加用户</el-button>

</el-col>

</el-row>

<el-row>

<el-table :data="userList" style="width: 100%" stripe border>

<el-table-column type="index" label="#" :index="i =>
getCurIndex(i)"></el-table-column>

<el-table-column label="姓名" prop="username"></el-table-column>

<el-table-column label="邮箱" prop="email"></el-table-column>

<el-table-column label="电话" prop="mobile"></el-table-column>

<el-table-column label="角色" prop="role_name"></el-table-column>

<el-table-column label="状态">

<template v-slot="{ row }">

<el-switch v-model="row.mg_state" active-color="#13ce66" inactive-color="#ff4949"
@change="onUserStateChange(row)"></el-switch>

</template>

</el-table-column>

<el-table-column>

<template v-slot="{ row }">

<el-button icon="el-icon-edit" size="mini" type="primary" @click="showEditDialog(row.id)">
</el-button>

<el-button icon="el-icon-delete" size="mini" type="danger" @click="removeUser(row.id)">
</el-button>

<el-tooltip effect="dark" content="分配角色" placement="top" :enterable="false">

<el-button icon="el-icon-setting" size="mini" type="warning"
@click="showSetRoleDialog(row)">

</el-tooltip>

</template>

</el-table-column>

</el-table>

<el-pagination @size-change="onSizeChange" @current-change="onCurrentChange"
:current-page="queryInfo.pagenum" :page-size="queryInfo.pagesize" :page-sizes="[5, 10, 15, 20]"
:total="total" layout="total, sizes, prev, pager, next, jumper"></el-pagination>

</el-row>

</el-card>

<el-dialog title="添加用户" :visible.sync="addUserDialogVisible" width="50%" @close="onAddUserDialogClosed"
@keyup.enter.native="addUser">

<el-form :model="addForm" ref="addFormRef" label-width="70px" rules="addFormRules">

<el-form-item label="用户名" prop="username">

<el-input v-model="addForm.username" @input="change($event)" />

</el-form-item>

<el-form-item label="密码" prop="password">

<el-input type="password" v-model="addForm.password" @input="change($event)" />

</el-form-item>

<el-form-item label="邮箱" prop="email">

<el-input v-model="addForm.email" @input="change($event)" />

</el-form-item>

<el-form-item label="电话" prop="mobile">

<el-input v-model="addForm.mobile" @input="change($event)" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="addUserDialogVisible = false">取消</el-button>

<el-button type="primary" @click="addUser">确定</el-button>

</span>

</el-dialog>

<el-dialog title="编辑用户" :visible.sync="editUserDialogVisible" width="50%" @close="onEditUserDialogClosed"
@keyup.enter.native="editUser">

<el-form :model="editForm" ref="editFormRef" label-width="70px" rules="editFormRules">

<el-form-item label="用户名">

<el-input :value="editForm.username" disabled />

</el-form-item>

<el-form-item label="邮箱" prop="email">

<el-input v-model="editForm.email" @input="change($event)" />

</el-form-item>

<el-form-item label="电话" prop="mobile">

<el-input v-model="editForm.mobile" @input="change($event)" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="editUserDialogVisible = false">取消</el-button>

<el-button @click="editUser" type="primary">确定</el-button>

</span>

</el-dialog>

<el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%" @close="setRoleDialogClosed"
@keyup.enter.native="setRole">

<div>

<p>当前用户:{{ userinfo.username }}</p>

<p>当前角色:{{ userinfo.role_name }}</p>

<p>

分配新角色:

<el-select v-model="selectedRoleId" placeholder="请选择" ref="selectRoleRef">

<el-option v-for="item in roleinfo" :key="item.id" :value="item.id" :label="item.roleName">
</el-option>

</el-select>

</p>

</div>

<span slot="footer" class="dialog-footer">

<el-button @click="setRoleDialogVisible = false">取消</el-button>

<el-button type="primary" @click="setRole">确定</el-button>

</span>

</el-dialog>

</div>

</template>

<script>

import {
UserListAPI,
ChangeUserStateAPI,
GetUserInfoAPI,
RemoveUserAPI,
GetRoleListAPI,
EditUserInfoAPI,
SetRoleAPI,
AddUserAPI
} from '@/api'

export default {

data() {

let checkEmail = (rule, value, callback) => {

const regEmail = /^w+@w+(.w+)+$/

if (regEmail.test(value)) return callback()

return callback(new Error('请输入合法的邮箱'))

}

let checkMobile = (rule, value, callback) => {

const regMobile = /^1[34578]d{9}$/

if (regMobile.test(value)) return callback()

return callback(new Error('请输入合法的电话'))

}

return {

queryInfo: {

query: '',

pagenum: 1,

pagesize: 5

},

userList: [],

total: 0,

addDialogVisible: false,

addForm: {},

addFormRules: {

username: [

{
required: true,
message: '请输入用户名'
trigger: 'blur'
},

{
min: 3,
max: 10,
message: '长度在 3 到 10 个字符',
trigger: 'blur }

],

password: [

{
required: true,
message: '请输入密码',
trigger: 'blur'
},

{
min: 6,
max 15,
messgae: '长度在 615 个字符',
trigger: 'blur'
}

],

email: [

{
required: true,
message: '请输入邮箱',
trigger: 'blur'
},

{
validator: checkEmail,
trigger: 'blur'
}

],

mobile: [

{
required: true,
message: '请输入电话',
trigger: 'blur'
},

{
validator: checkMobile,
trigger: 'blur'
}

]

},

editDialogVisible: false,

editForm: {},

editFormRules: {

email: [

{
required: true,
message: '请输入邮箱',
trigger: 'blur'
},

{
validator: checkEmail,
trigger: 'blur'
}

],

mobile: [

{
required: true,
message: '请输入电话',
trigger: 'blur'
},

{
validator: checkMobile,
trigger: 'blur'
}

]

},

userinfo;
[],

roleinfo: [],

setRoleDialogVisible: false,

selectedRoleId: ''

}

},

created() {

this,
getUserList()

},

methods: {

async getUserList() {

const {
data: res
} = await this.UsetListAPI(this.queryInfo)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success(res.meta.msg)

this.userList = res.data.users

this.total = res.data.total

},

getCurIndex(index) {

return (this.queryInfo.pagenum - 1) * this.queryInfo.pagesize + index +
1

},

async onUserStateChange(userinfo) {

const {
data: res
} = await ChangeUserState(userinfo.id,
userinfo.mg_state)

if (res.meta.status !== 200) {

userinfo.mg_state = !userinfo.mg_state

return this.$message.error('更新用户状态失败')

}

this.$message.success('更新用户状态成功!')

},

addUser() {

this.$refs.addFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await AddUserAPI(this.addForm)

if (res.meta.status !== 201) return this.$message.error(res.meta.msg)

this.$message.success('添加用户成功!')

this.getUserList()

this.addUserDialogVisible = false

})

},

addUserDialogClosed() {

this.$refs.addFormRef.resetFields()

},

showEditDialog(id) {

const {
data: res
} = await GetUserInfoAPI(id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取用户信息成功!')

this.editDialogVisible = true

// 保证在resetFields时不会变成上一次请求的值

this.$nextTick(() => this.editForm = res.data)

},

editUserDialogClosed() {

this.$refs.editFormRef.resetFields()

},

editUser() {

this.$refs.editFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = editUserAPI(this.editForm)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功修改用户信息')

this.getUserList()

this.editUserDialogVisible = false

})

},

async removeUser(id) {

const confirmRes = awaith
this.$confirm('此操作将永久删除该用户,是否继续?', '提示', {
type: 'warning'
}).catch(err => err)

if (confirmRes !== 'confirm') return
this.$message.info('您取消了删除操作')

const {
data: res
} = await RemoveUserAPI(id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('删除成功')

this.getUserList()

},

async showSetRoleDialog(userinfo) {

this.userinfo = userinfo

const {
data: res
} = await GetRoleList()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.roleInfo = res.data.filter(x => x.roleName !== useinfo.role_name)

},

setRoleDialogClosed() {

this.userinfo = ''

this.selectedRoleId = ''

},

async setRole() {

this.$refs.selectRoleRef.blur()

if (!this.selectedRoleId) return
this.$message.error('请选择要分配的角色')

const {
data: res
} = await SetRoleAPI(this.userinfo.id,
this.selectedRoleId)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功分配角色')

this.getUserList()

this.setRoleDialogVisible = false

},

handleCurrentChange(newVal) {

this.queryInfo.pagenum = newVal

this.getUserList()

},

handleSizeChange(newVal) {

this.queryInfo.pagesize = newVal

this.getUserList()

}

},
change(e) {
this.$forceUpdate()
}

}

</script>

<style>

.el-table {

margin-top: 15px;

}

p {

margin: 20px 0;

}

</style>

九、角色列表页面


创建components/power/Roles.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
<template>

<div class="roles-container">

<slot name="breadcrumb" />

<el-card>

<el-row>

<el-col>

<el-button type="primary" @click="addRoleDialogVisible =
true">添加角色</el-button>

</el-col>

<el-col>

<el-table :data="roleList" stripe border>

<el-table-column type="expand">

<template v-slot="{ row }">

<el-row class="[i1 === 0 ? 'bdtop' : '', 'bdbottom',
'vcenter']" v-for="(item1, i1) in row.children" :key="item1.id">

<el-col :span="5">

<el-tag closable @close="removeRight(row, item1.id)">{{
item1.authName }}</el-tag>

<i class="el-icon-caret-right"></i>

</el-col>

<el-col :span="19">

<el-row v-for="(item2, i2) in item1.children" :key="item2.id"
:class="[i2 === 0 : '' : 'bdtop', 'vcenter']">

<el-col :span="6">

<el-tag @close="removeRight(row, item2.id)">{{ item2.authName
}}</el-tag>

<i class="el-icon-caret-right" />

</el-col>

<el-col :span="18">

<el-tag v-for="item3 in item2.children" :key="item3.id" closable
@close="removeRight(row, item3.id)">{{ item3.authName }}</el-tag>

</el-col>

</el-row>

</el-col>

</el-row>

</template>

</el-table-column>

<el-table-column type="index" label="#"></el-table-column>

<el-table-column label="角色名称" prop="roleName"></el-table-column>

<el-table-column label="角色描述" prop="roleDesc"></el-table-column>

<el-table-column label="操作">

<template v-slot="{ row }">

<el-button icon="el-icon-edit" size="mini" type="primary" @click="showEditDialog(row.id)">编辑
</el-button>

<el-button icon="el-icon-delete" size="mini" type="danger" @click="removeRole(row.id)">删除
</el-button>

<el-button icon="el-icon-setting" size="mini" type="warning"
@click="showSetRightDialog(row)">分配权限</el-button>

</template>

</el-table-column>

</el-table>

</el-col>

</el-row>

</el-card>

<el-dialog title="添加角色" :visible.sync="addRoleDialogVisible" @close="addRoleDialogClosed"
@keyup.enter.native="addRole" width="50%">

<el-form :model="addForm" rules="addFormRules" ref="addFormRef" label-width="100px">

<el-form-item label="角色名称" prop="roleName">

<el-input v-model="addForm.roleName" @input="change($event)"></el-input>

</el-form-item>

<el-form-item label="角色描述" prop="roleDesc">

<el-input v-model="addForm.roleDesc" @input="change($event)" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="addRoleDialogVisible = false">取消</el-button>

<el-button type="primary" @click="addRole">确定</el-button>

</span>

</el-dialog>

<el-dialog title="编辑角色" :visible.sync="editRoleDialogVisible" @close="editRoleDialogClosed"
@keyup.enter.native="editRole" width="50%">

<el-form :model="editForm" rules="editFormRules" ref="editFormRef" label-width="100px">

<el-form-item label="角色名称" prop="roleName">

<el-input v-model="editForm.roleName" @input="change($event)" />

</el-form-item>

<el-form-item label="角色描述" prop="roleDesc">

<el-input v-model="editForm.roleDesc" @input="change($event)" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="editRoleDialogVisible = false">取消</el-button>

<el-button type="primary" @click="editRole">确定</el-button>

</span>

</el-dialog>

<el-dialog title="分配权限" :visible.sync="setRightDialogVisible" @close="setRightDialogClosed"
@keyup.enter.native="setRight" width="50%">

<el-tree :data="rightList" :props="treeProps" node-key="id" show-checkbox :default-expand-all="true"
:default-checked-keys="defKeys" ref="treeRef" />

<span slot="footer" class="dialog-footer">

<el-button @click="setRightDialogVisible = false">取消</el-button>

<el-button type="primary" @click="setRight">确定</el-button>

</span>

</el-dialog>

</div>

</template>

<script>

import {
GetRoleListAPI,
DeleteRightAPI,
GetRoleInfoAPI,
EditRoleInfoAPI,
DeleteRoleAPI,
AddRoleAPI,
GetRightTreeAPI,
SetRightAPI
} from '@/api'

export default {

data() {

return {

roleList: [],

addRoleDialogVisible: false,

addForm: {},

addFormRules: {

roleName: [

{
required: true,
message: '请输入角色名称',
trigger: 'blur'
},

{
min: 2,
max: 10,
message: '长度在 2 到 10 个字符',
trigger: 'blur'
}

],

roleDesc: [

{
required: true,
message: '请输入角色描述',
trigger: 'blur'
},

{
min: 5,
max: 30,
message: '长度在 5 到 30 个字符',
trigger: 'blur'
}

]

},

editRoleDialogVisible: false,

editForm: {},

editFormRules: {

roleName: [

{
required: true,
message: '请输入角色名称',
trigger: 'blur'
},

{
min: 2,
max: 10,
message: '长度在 2 到 10 个字符',
trigger: 'blur'
}

],

roleDesc: [

{
required: true,
message: '请输入角色描述',
trigger: 'blur'
},

{
min: 5,
max: 30,
message: '长度在 5 到 30 个字符',
trigger: 'blur'
}

]

},

setRightDialogVisible: false,

roleId: '',

rightList: [],

defKeys: [],

treeProps: {

children: 'children',

label: 'authName'

}

}

},

created() {

this.getRoleList()

},

methods: {

async getRoleList() {

const {
data: res
} = await GetRoleListAPI()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取角色列表成功!')

this.roleList = res.data

},

addRole() {

this.$refs.addFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await AddRoleAPI(this.addForm)

if (res.meta.status !== 201) return this.$message.error(res.meta.msg)

this.$message.success('添加角色成功!')

this.getRoleList()

this.addRoleDialogVisible = false

})

},

addRoleDialogClosed() {

this.$refs.addFormRef.resetFields()

},

async showEditRoleDialog(roleId) {

const {
data: res
} = await GetRoleInfo(roleId)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取角色信息成功!')

this.$nextTick(() => this.editForm = res.data)

this.editRoleDialogVisible = true

},

editRole() {

this.$refs.editFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await EditRoleAPI(this.editForm.roleId,
this.editForm.roleName, this.editForm.roleDesc)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('编辑角色成功!')

this.getRoleList()

this.editRoleDialogVisible = false

})

},

editRoleDialogClosed() {

this.$refs.editFormRef.resetFields()

},

async removeRight(role, id) {

const confirmRes = await
this.$confirm('此操作将永久删除该权限,是否继续?', '提示', {
type: 'warning'
}).catch(err => err)

if (confirmRes !== 'confirm') return
this.$message.info('您取消了删除操作')

const {
data: res
} = await DeleteRightAPI(role.id, id)

if (res.meta.status !== 200) return
this.$message.error('res.meta.msg')

this.$message.success('删除权限成功!')

// 直接getRoleList会导致列表缩起来

role.children = res.data

},

aysnc removeRole(roleId) {

const confirmRes = await
this.$confirm('此操作将永久删除该角色,是否继续?', '提示', {
type: 'warning'
}).catch(err => err)

if (confirmRes != 'confirm') return
this.$message.info('您取消了删除操作')

const {
data: res
} = await DeleteRoleAPI(roleId)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('删除角色成功!')

this.getRoleList()

},

showSetRightDialog(role) {

this.roleId = role.id

const {
data: res
} = await GetRightTree('tree')

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.rightList = res.data

this.getLeafKeys(role, this.defKeys)

this.setRightDialogVisible = false

},

getLeafKeys(node, arr) {

if (!node.children) return arr.push(node.id)

node.children.forEach(x => this.getLeafKeys(x, arr))

},

setRightDialogClosed() {

this.defKeys = []

},

async setRight() {

const keys = [...this.$refs.treeRef.getCheckedKeys(),
...this.$refs.treeRef.getHalfCheckedKeys()
]

const idStrs = keys.join(',')

const {
data: res
} = await SetRightAPI(this.roleId, idStrs)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('设置权限成功!')

this.getRoleList()

this.setRightDialogVisible = false

},
change(e) {
this.$forceUpdate()
}

}

}
</script>

<style>

.el-table {

margin-top: 15px;

}

.el-tag {

margin: 7px
}

bdtop {

border-top: 1px solid #eee;

}

bdbottom {

border-bottom: 1px solid #eee;

}

vcenter {

dislplay: flex;

align-items: center;

}
</style>

十、权限列表页面


创建components/power/right/right.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
<template>

<div class="right-container">

<slot name="breadcrumb" />

<el-card>

<el-table :data="rightLists" border stripe>

<el-table-column type="index" label="#"></el-table-column>

<el-table-column label="权限名称" prop="authName"></el-table-column>

<el-table-column label="路径" prop="path"></el-table-column>

<el-table-column label="权限等级">

<template v-slot="{ row }">

<el-tag v-if="row.level === '0'">一级</el-tag>

<el-tag type="success" v-else-if="row.level ===
'1'">二级</el-tag>

<el-tag type="warning" v-else>三级</el-tag>

</template>

</el-table-column>

</el-table>

<el-card>

</div>

</template>

<script>

import {
GetRightListAPI
}
form '@/api'

export default {

data() {

return {

rightList: []

}

},

created() {

this.getRightList()

},

methods: {

async getRightList() {

const {
data: res
} = await GetRightListAPI('list')

if (res.meta.status !== 200) this.$message.error(res.meta.msg)

this.$message.success('获取权限列表成功!')

this.rightList = res.data

}

},

computed: {

rightLists() {

return this.rightList.sort((a, b) => a.level - b.level)

}

}

}

</script>

十一、商品列表页面


  1. 创建components/goods/list/List.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<template>

<div class="list-container">

<el-card>

<el-row :gutter="20">

<el-col :span="7">

<el-input placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getGoodList"
@keyup.enter.native="getGoodList" @input="change($event)">

<el-button slot="append" icon="el-icon-search" @click="getGoodList" />

</el-input>

</el-col>

<el-col :span="4">

<el-button type="primary" @click="goAddPage">添加商品</el-button>

</el-col>

</el-row>

<el-row>

<el-table :data="goodList" style="width: 100%" stripe border>

<el-table-column type="index" label="#" :index="i =>
getCurIndex(i)"></el-table-column>

<el-table-column label="商品名称" prop="goods_name">

<el-table-column label="价格(元)" width="110px">

<template v-slot="{ row }">

{{ row.goods_price | tofixed }}

</template>

</el-table-column>

<el-table-column label="商品重量" prop="goods_weight" width="130px"></el-table-column>

<el-table-column label="创建时间" width="220px">

<template v-slot="{ row }">

{{ row.add_time | dateFormat }}

</template>

</el-table-column>

<el-table-column label="操作">

<template v-slot="{ row }">

<el-button type="primary" size="mini" icon="el-icon-edit" @click="goEditPage" />

<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeGood" />

</template>

</el-table-column>

</el-table>

</el-row>

<el-pagination @cuttent-change="onCurrentChange" @size-change="onSizeChange" :current-page="queryInfo.pagenum"
:page-size="queryInfo.pagesize" :page-sizes="[5, 10, 15, 20]" :total="total"
layout="total, sizes, prev, pager, next, jumper" />

</el-card>

</div>

</template>

<script>

import {
GetGoodListAPI,
DeleteGoodAPI
} from '@/api'

export default {

data() {

return {

goodList: [],

queryInfo: {

query: '',

pagenum: 1,

pagesize: 5

},

total: 0

}

},

created() {

this.getGoodList()

},

methods: {

async getGoodList() {

const {
data: res
} = await GetGoodListAPI(this.queryInfo)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取商品列表成功!')

this.goodList = res.data.goods

this.total = res.data.total

},

getCurIndex(index) {

return (this.queryInfo.pagenum - 1) * this.queryInfo.pagesize + index +
1

},

goAddPage() {

this.$router.push('goods/add')

},

goEditPage(id) {

this.$router.push('goods/edit/' + id)

},

async removeGood(id) {

const confirmRes = await
this.$confirm('此操作将永久删除该商品,是否继续?', '提示', {
type: 'warning'
}).catch(err => err)

if (confirmRes !== 'confirm') return
this.$message.info('您取消了删除操作!')

const {
data: res
} = await DeleteGood(id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('删除成功!')

this.getGoodList()

},

onCurrentChange(newPage) {

this.queryInfo.pagenum = newPage

this.getGoodList()

},

onSizeChange(newSize) {

this.queryInfo.pagesize = newSize

this.getGoodList()

},
change(e) {
this.$forceUpdate()
}

}

}

</script>

<style>

.el-table {

margin-top: 15px;

}

</style>
  1. 创建compoents/goods/list/children/Edit.vue和components/goods/list/children/Add.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
<template>

<div class="edit-container">

<bread-crumb name1="商品管理" name2="编辑商品"></bread-crumb>

<el-card>

<el-alert title="编辑商品信息" type="info" center :closable="false" show-icon />

<el-steps align-center :active="activeIndex - 0" finish-status="success">

<el-step title="基本信息" />

<el-step title="商品参数" />

<el-step title="商品属性" />

<el-step title="商品图片" />

<el-step title="商品内容" />

<el-step title="完成" />

</el-steps>

<el-form :model="editForm" :rules="editFormRules" :ref="editFormRef" label-position="top" label-width="100px">

<el-tabs :tab-position="left" v-model="activeIndex" :before-leave="beforeTabLeave">

<el-tab-pane label="基本信息" name="0">

<el-form-item label="商品名称" prop="goods_name">

<el-input v-model="editForm.goods_name" @input="change($event)" />

</el-form-item>

<el-form-item label="商品价格(元)" prop="goods_price">

<el-input v-model="editForm.goods_price" @input="change($event)" />

</el-form-item>

<el-form-item label="商品重量" prop="goods_weight>

<el-input v-model=" editForm.goods_weight" @input="change($event)" />

</el-form-item>

<el-form-item label="商品数量" prop="goods_number">

<el-input v-model="editForm.goods_number" @input="change($event)" />

</el-form-item>

<el-form-item label="商品分类" prop="goods_cat">

<el-cascader v-model="editForm.goods_cat" :options="cateList"
props="{ expandTrigger: 'hover', ...cateProps }" @change="onCateChange" />

</el-form-item>

<el-button type="primary" @click="nextTab">下一步</el-button>

</el-tab-pane>

<el-tab-pane label="商品参数" name="1">

<el-form-item v-for="item in manyTableData" :key="item.attr_id" :label="item.attr_name">

<el-checkbox-group v-model="item.attr_vals">

<el-checkbox v-for="(item2, i2) in item.attr_vals" :label="item2" :key="i2" border>
{{ item2 }}</el-checkbox>

</el-check-group>

</el-form-item>

</el-tab-pane>

<el-tab-pane label="商品属性" name="2">

<el-form-item v-for="item in onlyTableData" :key="item.attr_id" :label="item.attr_name">

<el-input v-model="item.attr_vals" @input="change($event)" />

</el-form-item>

<el-button type="primary" @click="nextTab">下一步</el-button>

</el-tab-pane>

<el-tab-pane label="商品图片" name="3">

<upload-img :requestUrl="$httpBaseUrl + 'upload'" :headers="headerObj" :pics.sync="editForm.pics" />

<el-button type="primary" @click="nextTab">下一步</el-button>

</el-tab-pane>

<el-tab-pane label="商品内容" name="4">

<editor :content.sync="editForm.goods_introduce" />

<el-button type="primary" @click="editGood">完成修改</el-button>

</el-tab-pane>

</el-tabs>

</el-form>

</el-card>

</div>

</template>

<script>

import {
BreadCrumb
} from '@/components/breadcrumb/BreadCrumb'

import {
UploadImg
}
form '@/components/list/children/UploadImg'

import {
Editor
} from '@/components/list/children/Editor'

import _ from 'lodash'

import {
GetGoodInfoAPI,
GetCateListAPI,
GetAttributesAPI,
EditGoodAPI
}
from '@/api'

export default {

data() {

let checkNumber(rule, value, callback) {

if (!/^[0-9]+.?[0-9]*$/.test(value)) return callback(new Error('必须是数字'))

if (value < 0) return callback(new Error('不能小于0'))

callback()

}

return {

activeIndex: '0',

editForm: {

goods_name: '',

goods_price: 0,

goods_weight: 0,

goods_number: 1,

goods_introduce: '',

goods_cat: [],

pics: null,

attrs: null

},

editFormRules: {

goods_name: [

{
required: true,
message: '请输入商品名称',
trigger: 'blur'
},

{
min: 2,
max: 30,
message: '长度在 2 到 30 个字符',
trigger: 'blur'
}

],

goods_price: [

{
required: true,
message: '请输入商品价格',
trigger: 'blur'
},

{
validator: checkNumber,
trigger: 'blur'
}

],

goods_weight: [

{
required: true,
message: '请输入商品重量',
trigger: 'blur'
},

{
validator: checkNumber,
trigger: 'blur'
}

],

goods_number: [

{
required: true,
message: '请输入商品数量',
trigger: 'blur'
},

{
validator: checkNumber,
trigger: 'blur'
}

]

},

cateList: [],

cateProps: {

label: 'cat_name',

value: 'cat_id',

children: 'children'

},

manyTableData: [],

onlyTableData: [],

headerObj: {

Authorization: localStorage.getItem('token')

}

}

},

props: {

id: {

type: String,

default: '0'

}

},

created() {

this.getCateList()

},

methods: {

async getCateList() {

const {
data: res
} = await GetCateList()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取分类列表成功!')

this.cateList = res.data

this.getGoodList()

},

aysnc getGoodList() {

const {
data: res
} = await GetGoodInfo(this.id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取商品信息成功!')

this.editForm = res.data

this.editForm.goods_cat = this.editForm.goods_cat.split(',').map(x =>
x - 0)

},

onCateChange() {

if (this.editForm.goods_cat.length !== 3) this.editForm.goods_cat = []

},

async getManyTableData() {

const {
data: res
} = await GetAttributes(this.cateId, 'many')

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功获取动态参数!')

res.data.forEach(x => x.attr_vals = x.attr_vals.length === 0 ? [] :
x.attr_vals.split(' '))

this.manyTableData = res.data

},

aysnc getOnlyTableData() {

const {
data: res
} = await GetAttributes(this.cateId, 'only')

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功获取静态参数!')

this.onlyTableData = res.data

},

beforeTabLeave(acitveName, oldActiveName) {

if ((oldActiceName === '0' && this.editForm.goods_cat.length !== 3) ||
!this.editForm.goods_name) {

this.$message.error('请先选择商品分类 或 填写商品名称!')

return false

}

},

nextTab() {

this.activeIndex = this.activeIndex - 0 + 1 + ''

},

editGood() {

this.$refs.editFormRef.validate(async valid => {

if (!valid) return this.$message.error('请填写必要表单项')

let form = _.deepClone(this.editForm)

form.goods_cat = form.goods_cat.join(',')

if (form.pics !== null) form.pics = form.pics.map(x => {
return {
x.pics
}
})

form.goods_price = parseFloat(form.goods_price)

form.goods_weight = parseFloat(form.goods_weight)

if (this.manyTableData.length || this.onlyTableData.lenght) form.attrs = []

this.manyTableData.forEach(x => {

const newInfo = {
attr_id: x.attr_id,
attr_vals: x.attr_vals
}

form.attrs.push(newInfo)

})

this.onlyTableData.forEach(x => {

const newInfo = {
attr_id: x.attr_id,
attr_vals: x.attr_vals
}

form.attrs.push(newInfo)

})

try {

const {
data: res
} = await EditGoodAPI(this.id, form)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('编辑商品信息成功!')

this.$router.push('/goods')

} catch (e) {

this.$message.error(e)

}

})

},
change(e) {
this.$forceUpdate()
}

},

watch: {

activeIndex(newVal) {

if (newVal === '1') {

this.getManyTableData()

} else if (newVal === '2') {

this.getOnlyTableData()

}

}

},

computed: {

cateId() {

return this.editForm.goods_cat.length === 3 ?
this.editForm.goods_cat[2] : null

}

}

}
</script>

<style>

.el-form {

margin-top: 40px;

}

.el-tab-pane {

margin: 0 80px 0 20px;

}

.el-steps,
.el-button {

margin-top: 15px;

}

.el-steps__title {

font-size: 13px;

}

.el-checkbox {

margin: 0 5px 0 0 !important;

}

</style>
  1. 创建图片上传组件UploadImg.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
140
141
142
143
<template>

<div class="uploda-img-container">

<el-upload :action="requestUrl" :headers="headers" :on-preview="onPreview" :on-remove="onRemove"
:on-success="onSuccess" accept="image/*" :before-upload="onBeforeUpload" list-type="picture" multiple drag>

<i class="el-icon-upload" />

<div class="el-upload__text">将图片拖到此处,或<em>点击上传</em></div>

</el-upload>

<el-image-viewer v-if="showViewer" :on-close="() => showViewer =
false" :url-list="previewImgList" :z-index="999" :initial-index="imgIndex" />

</div>

</template>

<script>

import ElImageViewer from 'element-ui/packages/image/src/image-viewer'

export default {

components: {

ElImageViewer

},

data() {

return {

showViewer: false,

curFileUid: 0,

picList: []

}

},

props: {

requestUrl: {

required: true,

type: String

},

headers: {

type: Object,

default: {}

}

},

methods: {

onPreview(file) {

this.curFileUid = file.uid

this.showViewer = true

},

onRemove(file) {

this.picList = this.picList.filter(x => x.uid !== file.uid)

},

onSuccess(res, file) {

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('图片上传成功!')

this.picList.push({

uid: file.uid,

pic: res.data.tmp_path,

url: res.data.url

})

},

onBeforeUpload(file) {

if (file.size > 2048000) {

this.$message.error('单张图片不超过2MB')

return false

}

}

},

watch: {

picList(newVal) {

this.$emit('update:pics', newVal)

}

},

computed: {

previewImgList() {

return this.picList.sort((a, b) => a.uid - b.uid).map(x => x.url)

},

imgIndex() {

return this.picList.findIndex(x => x.uid === this.curFileUid)

}

}

}

</script>
  1. 创建编辑器组件Editor.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
140
141
142
143
144
145
<template>

<div class="editor-container">

<quill-editor :content="content" @change="onContentChange($event)" ref="editorRef" :options="editorOptions" />

<form action="" method="post" enctype="multipart/form-data" ref="formRef">

<input ref="inputRef" class="hide" type="file" accept="image/*,
video/*" name="file" @change="uploadImg" multiple />

</form>

</div>

</template>

<script>

import {
UploadImgAPI
}
form '@/api'

export default {

data() {

return {

ediotrOptions: {

placeholder: '请输入内容...'

},

uploadType: ''

}

},

props: {

content: {

type: String,

default: ''

}

},

mounted() {

this.$refs.editorRef.quill.getModule('toolbar').addHandler('image',
this.imgHandler)

this.$refs.editorRef.quill.getModule('toolbar').addHandler('video',
this.videoHandler)

},

methods: {

clickUploadIpt() {

this.$refs.inputRef.click()

},

imgHandler() {

this.uploadType = 'image'

this.clickUploadIpt()

},

videoHandler() {

this.uploadType = 'video'

this.clickUploadIpt()

},

async uploadImg() {

let files = this.$refs.inputRef.files

let postList = []

for (let f in files) {

let formData = new FormData()

formData.append('file', f)

postList.push(UploadImgAPI(formData))

}

Promise.all(postList)

.then(res => {

res.forEach(({
data: res
}) => {

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

let cursor = this.$refs.editorRef.quill.getSelection()

// 后台回复的是127.0.0.1,懒得改后台,就在这里做下处理
let url = Vue.prototype.$httpBaseUrl + new URL(res.data.url).pathname

this.$refs.editorRef.quill.insertEmbed(cursor ? cursor : 0,
this.uploadType, )

})

})

.catch(e => this.$message.error(e))

},

onContentChange({
quill,
html,
text
}) {

this.$emit('update:content', html)

}

}

}

</script>

十二、分类参数页面


创建components/goods/cate/Cate.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
<template>

<slot name="breadcrumb" />

<el-card>

<el-alert title="注意:只允许为第三级分类设置相关参数!" type="warning" :closable="false" />

<el-row class="cat-opt">

<span>选择商品分类:</span>

<el-cascader v-model="selectedKeys" :options="cateList" :props="{
expandTrigger: 'hover', ...cateProps }" @change="onCateChange" @visible-change="loadMore" />

</el-row>

<el-tabs v-model="activeName" @tab-click="onTabClick">

<el-tab-pane label="动态参数" name="many">

<el-button type="primary" size="mini" :disabled="isBtnDisabled" @click="addParamDialogVisible = true">添加参数
</el-button>

<el-table :data="manyTableData" border stripe>

<el-table-column type="expand">

<template v-slot="{ row }">

<el-tag v-for="(item, index) in row.attr_vals" :key="index" @close="onTagClosed(index, row)"
closable>{{ item }}</el-tag>

<el-input @input="change($event)" class="input-new-tag" v-if="row.inputVisible" v-model="row.inputValue" ref="inputRef"
size="small" @keyup.enter.native="onInputConfirm(row)" @blur="onInputConfirm(row)" />

<el-button v-else class="button-new-tag" size="small" @click="showInput(row)">+ New Tag
</el-button>

</template>

</el-table-column>

<el-table-column type="index" label="#"></el-table-column>

<el-table-column label="参数名称" prop="attr_name" />

<el-table-column label="操作">

<template v-slot="{ row }">

<el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(row.attr_id)">
编辑</el-button>

<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeParams(row.attr_id)">删除
</el-button>

</template>

</el-table-column>

</el-table>

</el-tab-pane>

<el-tab-pane labe="静态属性" name="only">

<el-button type="primary" size="mini" :disabled="isBtnDisabled" @click="addParamDialogVisible = true">添加参数
</el-button>

<el-table :data="onlyTableData" border stripe>

<el-table-column type="expand">

<template v-slot="{ row }">

<el-tag v-for="(item, index) in row.attr_vals" :key="index" @close="onTagClosed(index, row)"
closable>{{ item }}</el-tag>

<el-input @input="change($event)" class="input-new-tag" v-if="row.inputVisible" v-model="row.inputValue" ref="inputRef"
size="small" @keyup.enter.native="onInputConfirm(row)" @blur="onInputConfirm(row)">

<el-button v-else class="button-new-tag" size="small" @click="showInput(row)">+ New Tag
</el-button>

</template>

</el-table-column>

<el-table-column label="#" type="index" />

<el-table-column label="参数名称" prop="attr_name" />

<el-table-column label="操作">

<template v-slot="{ row }">

<el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(row.attr_id)">
编辑</el-button>

<el-button type="danger" size="mini" icon="el-icon-delete" @click="removeParams(row.attr_id)">删除
</el-button>

</template>

</el-table-column>

</el-table>

</el-tab-pane>

</el-tabs>

</el-card>

<el-dialog :title="'添加' + titleText" :visible.sync="addParamDialogVisible" width="50%" @close="addParamDialogClosed"
@keyup.enter.native="addParam">

<el-form :model="addParamForm" :rules="addParamRules" ref="addParamRef" label-width="100px" @submit.prevent.native>

<el-form-item :label="titileText" prop="attr_name">

<el-input @input="change($event)" v-model="addParamForm.attr_name" />

<el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="addParamDialogVisible = false">取消</el-button>

<el-button type="primary" @click="addParam">确定</el-button>

</span>

</el-dialog>

<el-dialog :title="'编辑' + titleText" :visible.sync="editParamDialogVisible" width="50%" @close="editParamDialogClosed" @keyup.enter.native="editParam">

<el-form :model="editParamForm" :rules="editoParamRules" ref="editParamRef" label-width="100px"
@submit.prevent.native>

<el-form-item :label="titleText" prop="attr_name">

<el-input @input="change($event)" v-model="editParamForm.attr_name" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="editParamDialogVisible =
false">取消</el-button>

<el-button type="primary" @click="editParam">确定</el-button>

</span>

</el-dialog>

</template>

<script>

import {
GetCateListAPI,
GetAttributesAPI,
AddAttributeAPI,
EditAttributeAPI,
DeleteAttributeAPI,
SetAttrValsAPI
} from '@/api'

export default {

data() {

return {

selectedCateKeys: [],

cateList: [],

cateProps: {

value: 'cat_id',

label: 'cat_name',

children: 'children'

},

activeName: 'many',

manyTableData: [],

onlyTableData: [],

addParamDialogVisible: false,

addParamForm: {},

addFormRules: {

attr_name: [

{
required: true,
message: '请输入分类名称',
trigger: 'blur'
},

{
min: 2,
max: 10,
message: '长度在 2 到10 个字符',
trigger: 'blur'
}

]

}

editParamDialogVisible: false,

editParamForm: {},

editFormRules: {

attr_name: [

{
required: true,
message: '请输入分类名称',
trigger: 'blur'
},

{
min: 2,
max: 10,
message: '长度在 2 到10 个字符',
trigger: 'blur'
}

]

},
// 级联选择器初始最多展示节点
rangeNumber: 9,
// 级联选择器滚动面板DOM
scrollDOM: null

}

},

created() {

this.getCateList()

},

methods: {

async getCateList() {

const {
data: res
} = await GetCateListAPI()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功获取分类列表')

this.cateList = res.data

},

onCateChange() {

this.getParamData()

},

onTabClick() {

this.getParamData()

},

async getParamData() {

if (this.selectedCateKeys.length !== 3) {

this.selectedCateKeys = []

this.manyTableData = []

this.onlyTableData = []

}

const {
data: res
} = await GetAttributes(this.cateId, this.activeName)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

res.data.forEach(item => {

item.attr_vals = item.attr_vals ? item.attr_vals.split(' ') : []

item.inputVisible = false

item.inputValue = ''

})

if (this.activeName === 'many') {

this.manyTableData = res.data

} else if (this.activeName === 'only') {

this.onlyTableData = res.data

}

},

onTagClosed(index, attrInfo) {

attrInfo.attr_vals.splice(index, 1)

this.setAttrVals(attrInfo)

},

async setAttrVals(attrInfo) {

const {
data: res
} = await SetAttrValsAPI(this.cateId,
attrInfo.attr_id, this.activeName, attrInfo.attr_name,
attrInfo.attr_vals.join(' '))

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('修改参数成功!')

},

showInput(attrInfo) {

attrInfo.inputVisible = true

this.$nextTick(() => this.$refs.inputRef.$refs.input.focus())

},

onInputConfirm(attrInfo) {

attrInfo.inputValue = attrInfo.inputValue.trim()

if (attrInfo.inputValue.length > 0) {

attrInfo.attr_vals.push(attrInfo.inputValue)

this.setAttrVals(attrInfo)

}

attrInfo.inputValue = ''

attrInfo.inputVisible = false

},

addParamDialogClosed() {

this.$refs.addParamRef.resetFields()

},

addParam() {

this.$refs.addParamRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await AddAttributeAPI(this.cateId,
this.addParamForm.attr_name, this.activeName)

if (res.meta.status !== 201) return this.$message.error(res.meta.msg)

this.getParamData()

this.addParamDialogVisible = false

})

},

async showEditDialog(id) {

const {
data: res
} = await GetAttrInfoAPI(this.cateId, id,
this.activeName)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.editParamForm = res.data

this.editParamDialogVisible = true

},

editParamDialogClosed() {

this.$refs.editParamRef.resetFields()

},

editParam() {

this.$refs.editParamRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await EditAttributesAPI(this.cateId,
this.editParamForm.attr_id, this.editParamForm.attr_name,
this.activeName)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.getParamData()

this.editParamDialogVisible = false

})

},

async removeParam(id) {

const confirmRes = await
this.$confirm('此操作将永久删除该商品,是否继续?', '提示', {
type: 'warning'
}).catch(err => err)

if (confirmRes !== 'confirm') return
this.$message.info('您取消了删除操作')

const {
data: res
} = await DeleteAttributeAPI(this.cateId, id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('成功删除属性!')

this.getParamData()

},
change(e) {
this.$forceUpdate()
},
// 当滚动条下拉的时候加载新的节点
loadMore(status) {
console.log(status)
if (status) {
// 获取滚动条所在的那一级
this.scrollDOM = document.querySelector('.el-cascader-menu__wrap')
if (this.scrollDOM) {
this.scrollDOM.addEventListener('scroll', this.addRange)
}
return
}
this.scrollDOM.removeEventListener('scroll', this.addRange)
this.rangeNumber = 9
},
addRange() {
// 判断是否滚动到底
// scrollTop是滚动条距离内容页顶端的距离
// clientHeight是显示在浏览器窗口部分的高度
// scrollHeight是内容的实际高度
const condition = this.scrollDOM.scrollTop + this.scrollDOM.clientHeight >= this.scrollDOM.scrollHeight
if (condition) this.rangeNumber += 5
},

},

computed: {

isBtnDisabled() {

return this.selectedCateKeys.length !== 3

},

cateId() {

return this.selectedCateKeys.length === 3 ? this.selectedCateKeys[2] :
null

},

titleText() {

return this.activeName === 'many' ? '动态参数' : '静态参数'

}

}

}

</script>

<style>

.cat-opt,
.el-table {

margin-top: 15px;

}

.el-tag {

margin: 5px 10px;

}

.button-new-tag {

margin: 5px 10px;

height: 32px;

line-height: 32px;

padding-top: 0;

padding-bottom: 0;

}

.input-new-tag {

width: 90px;

margin: 5px 10px;

vertical-align: bottom;

}

</style>

十三、商品分类页面


创建components/goods/cate/Cate.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
<template>

<div class="cate-container">

<slot name="breadcrumb" />

<el-card>

<el-row>

<el-button type="primary" @click="showAddCateDialog">添加分类</el-button>

</el-row>

<el-row>

<tree-table class="tree-table" :data="cateList" :expand-type="false" :selection-type="false"
:columns="columns" border>

<template #isok="{ row }">

<i class="el-icon-success" style="color: lightgreen; transform:
scale(2)" v-if="row.cat_deleted === false" />

<i class="el-icon-error" style="color: red; transform: scale(2)" v-else />

</template>

<template #order="{ row }">

<el-tag size="mini" v-if="row.cat_level === 0">一级</el-tag>

<el-tag size="mini" type="success" v-else-if="row.cat_level ===
1">二级</el-tag>

<el-tag size="mini" type="warning" v-else>三级</el-tag>

</template>

<template #opt="{ row }">

<el-button type="primary" size="mini" @click="showEditCateDialog(row)" icon="el-icon-edit" />

<el-button type="danger" size="mini" @click="removeCate(row.cat_id)" icon="el-icon-delete" />

</template>

</tree-table>

<el-pagination @current-change="onCurrentChange" @size-change="onSizeChange"
:current-page="queryInfo.pagenum" :page-size="queryInfo.pagesize" :page-sizes="[5, 10, 15, 20]"
:total="total" layout="total, sizes, prev, pager, next, jumper" />

</el-row>

</el-card>

<el-dialog title="添加分类" :visible.sync="addCateDialogVisible" width="50%" @close="addCateDialogClosed"
@keyup.enter.native="addCate">

<el-form :model="addCateForm" :rules="addCateFormRules" :ref="addCateFormRef" label-width="100px">

<el-form-item label="分类名称:" prop="cat_name">

<el-input v-model="addCateForm.cat_name" @input="change($event)" />

</el-form-item>

<el-form-item label="父级分类:">

<el-cascader v-model="selectedCateKeys" :options="parentCateList" :props="{ expandTrigger: 'hover', ...cateProps, checkStrictly:
'true' }" @change="onParentCateChange" clearable />

</el-form-item>

<span slot="footer" class="dialog-footer">

<el-button @click="addCateDialogVisible = false">取消</el-button>

<el-button type="primary" @click="addCate">确定</el-button>

</span>

</el-form>

</el-dialog>

<el-dialog title="编辑分类" :visible.sync="editCateDialogVisible" width="50%" @close="editCateDialogClosed"
@keyup.enter.native="editCate">

<el-form :model="editCateForm" :rules="editCateFormRules" : ref="editCateFormRef">

<el-form-item label="分类名称:" prop="cat_name">

<el-input v-model="edtiCateForm.cat_name" @input="change($event)" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="editCateDialogVisible = false">取消</el-button>

<el-button type="primary" @click="editCate">确定</el-button>

</span>

</el-dialog>

</div>

</template>

<script>

import {
GetCateListAPI,
GetCateInfoAPI,
AddCateAPI,
EditCateAPI,
DeleteCateAPI
} from '@/api'

export default {

data() {

return {

cateList: [],

queryInfo: {

type: 3,

pagenum: 1,

pagesize: 5

},

total: 0,

columns: [

{

label: '分类名称',

prop: 'cat_name'

},

{

label: '是否有效',

type: 'template',

template: 'isok'

},

{

label: '级别',

type: 'template',

template: 'order'

},

{

label: '操作',

type: 'template',

template: 'opt'

}

],

addCateDialogVisible: false,

addCateForm: {

cat_pid: 0,

cat_name: '',

cat_level: 0

},

addCateFormRules: {

cat_name: [{
required: true,
message: '请输入分类名称',
trigger: 'blur'
}]

},

parentCateList: [],

cateProps: {

value: 'cat_id',

label: 'cat_name',

children: 'children'

},

selectedCateKeys: [],

editCateDialogVisible: false,

editCateForm: {},

eidtCateFormRules: {

cat_name: [{
required: true,
message: '请输入分类名称',
trigger: 'blur'
}]

}

}

},

created() {

this.getCateList()

},

methods: {

async getCateList() {

const {
data: res
} = await GetCateListAPI(this.queryInfo)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('获取分类列表成功!')

this.cateList = res.data.result

this.total = res.data.total

},

onCurrentChange(newPage) {

this.queryInfo.pagenum = newPage

this.getCateList()

},

onSizeChange(newSize) {

this.queryInfo.pagesize = newSize

this.getCateList()

},

async showAddCateDialog() {

const {
data: res
} = await GetCateListAPI({
type: 2
})

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.parentCateList = res.data

this.addCateDialogVisible = true

},

onParentCateChange() {

if (this.selectedKeys.length <= 0) {

this.addCateForm.cat_id = 0

this.addCateForm.cat_level = 0

return

}

this.addCateForm.cat_id = this.selectedKeys[this.selectedKeys.length -
1]

this.addCateForm.cat_level = this.selectedKeys.length

},

addCate() {

this.$refs.addCateFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await AddCateAPI(this.addCateForm)

if (res.meta.status !== 201) return this.$message.error(res.meta.msg)

this.$message.success('添加分类成功!')

this.getCateList()

this.addCateDialogVisible = false

})

},

addCateDialogClosed() {

this.$refs.addCateFormRef.resetFields()

},

async showEditCateDialog(cateInfo) {

const {
data: res
} = await GetCateInfoAPI(cateInfo.cat_id)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.editCateForm = res.data

this.editCateDialogVisible = true

},

editCate() {

this.$refs.editCateFormRef.validate(async valid => {

if (!valid) return

const {
data: res
} = await EditCate(this.editCateForm.cat_id,
this.editCateForm.cat_name)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('修改分类成功!')

this.getCateList()

this.editCateDialogVisible = false

})

},

editCateDialogClosed() {

this.$refs.editCateFormRef.resetFields()

},

async removeCate(cateId) {

const confirmRes = await
this.$confirm('此操作将永久删除该分类,是否继续?', '提示', {
type: 'warning'
})

if (confirmRes !== 'confirm') return
this.$message.info('您取消了删除操作')

const {
data: res
} = await DeleteCateAPI(cateId)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.$message.success('分类已成功删除!')

this.getCateList()

},
change(e) {
this.$forceUpdate()
}

}

}

</script>

<style>

.tree-table {

margin-top: 15px;

}

.el-cascader {

width: 100%;

}

</style>

十四、订单列表页面


将城市数据保存至utils/cityData.js,

创建页面components/order/Order.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
<template>

<div class="order-container">

<slot name="breadcrumb" />

<el-card>

<el-row>

<el-col :span="8">

<el-input @input="change($event)" placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getOrderList"
@keyup.enter.native="getOrderList">

<el-button slot="append" icon="el-icon-search" @click="getOrderList" />

</el-input>

</el-col>

</el-row>

<el-row>

<el-table :data="orderList" style="width: 100%" border stripe>

<el-table-column label="#" type="index" :index="i =>
getCurIndex(i)" />

<el-table-column label="订单编号" prop="order_number" />

<el-table-column label="订单价格(元)" width="250px">

<template v-slot="{ row }">

{{ row.order_price | tofixed }}

</template>

</el-table-column>

<el-table-column label="是否支付" width="250px">

<template v-slot="{ row }">

<el-tag type="danger" v-if="row.pay_status === 0">未付款</el-tag>

<el-tag type="success" v-else>已付款</el-tag>

</template>

</el-table-column>

<el-table-column label="是否发货" prop="is_send" width="250px" />

<el-table-column label="下单时间" width="250px">

<template v-slot="{ row }">

{{ row.create_time | dateFormat }}

</template>

</el-table-column>

<el-table-column label="操作" width="150px">

<template v-slot="{ row }">

<el-tooltip class="item" effect="dark" content="修改地址" placement="top">

<el-button type="primary" icon="el-icon-edit" @click="showEditAddrDialog" circle />

</el-tooltip>

<el-tooltip class="item" effect="dark" content="物流状态" placement="top">

<el-button type="success" icon="el-icon-location" @click="showEMSDialog" circle />

</el-tooltip>

</template>

</el-table-column>

<el-table>

</el-row>

<el-pagination :current-page="query.pagenum" :page-size="query.pagesize" @curent-change="onCurrentChange"
@size-change="onSizeChange" :page-sizes="[5, 10, 15, 20]" :total="total"
layout="total, sizes, prev, pager, next, jumper" />

</el-card>

<el-dialog title="修改地址" width="50%" @close="onEditAddrDialogClosed" :visible.sync="addrDialogVisible"
@keyup.enter.native="onAddrChange">

<el-form :model="addrForm" :rules="addrFormRules" ref="addrFormRef" label-width="100px">

<el-form-item label="省市区/县" prop="address1">

<el-cascader v-model="addrForm.address1" :options="cityData" :prop="{ expandTrigger: 'hover' }" />

</el-form-item>

<el-form-item label="详细地址" prop="address2">

<el-input @input="change($event)" v-model="addrForm.address2" />

</el-form-item>

</el-form>

<span slot="footer" class="dialog-footer">

<el-button @click="addrDialogVisible = false">取消</el-button>

<el-button type="primary" @click="onAddrChange">确定</el-button>

</span>

</el-dialog>

<el-dialog title="物流进度" :visible.sync="EMSDialogVisible" width="50%" @close="onEMSDialogClosed">

<el-timeline :reverse="false">

<el-timeline-item v-for="(item, index) in EMSInfo" :key="index" :timestamp="item.ftime">

{{ item.context }}

</el-timeline-item>

</el-timeline>

</el-dialog>

</div>

</template>

<script>

import {
GetOrderListAPI,
GetEMSInfoAPI
} from '@/api'

import cityData from '@/utils/cityData.js'

export default {

data() {

return {

orderList: [],

queryInfo: {

pagenum: 1,

pagesize: 5,

query: ''

},

total: 0,

addrDialogVisible: false,

addrForm: {

address1: [],

address2: ''

},

addrFormRules: {

address1: [{
required: true,
message: '请选择省市区/县',
trigger: 'blur'
}],

address2: [{
required: true,
message: '请选择详细地址',
trigger: 'blur'
}]

},

cityData,

EMSDialogVisible: false,

EMSInfo: []

}

},

created() {

this.getOrderList()

},

methods: {

async getOrderList() {

const {
data: res
} = await GetOrderListAPI(this.queryInfo)

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.orderList = res.data.goods

this.total = res.data.total

},

onCurrentChange(newPage) {

this.queryInfo.pagenum = newPage

this.getOrderList()

},

onSizeChange(newSize) {

this.queryInfo.pagesize = newSize

this.getOrderList()

},

showAddrDialog() {

this.addrDialogVisible = true

},

onAddrChange() {

this.$message.success('修改地址成功!')

this.addrDialogVisible = false

},

onAddrDialogClosed() {

this.$refs.addrFormRef.resetFields()

},

async showEMSDialog() {

const {
data: res
} = await GetEMSInfoAPI()

if (res.meta.status !== 200) return this.$message.error(res.meta.msg)

this.EMSInfo = res.data

this.EMSDialogVisible = true

},

getCurIndex(i) {

return (this.queryInfo.pagenum - 1) * this.queryInfo.pagesize + i + 1

},
change(e) {
this.$forceUpdate()
}

}

}
</script>

<style>

.el-table {

margin-top: 15px;

}

</style>

十五、数据统计页面


创建components/report/Report.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
<template>

<div>

<slot name="breadcrumb" />

<div ref="main" style="width: 650px; height: 300px"></div>

</div>

</template>

<script>

import {
GetReportAPI
} from '@/api'

import * as echarts from 'echarts'

import _ from 'lodash'

export default {

data() {

return {

options: {

title: {

text: '用户来源'

},

tooltip: {

trigger: 'axis',

axisPointer: {

type: 'cross',

label: {

backgroundColor: '#333'

}

}

},

grid: {

left: '3%',

right: '4%',

bottom: '3%',

containLabel: true // X坐标轴标名显示

},

xAxis: {

boundaryGap: false // X坐标轴两边留白

}

}

}

},

methods: {

async mounted() {

const {
data: res
} = await GetReportAPI()

if (res.meta.status !== 200) return $this.message.error(res.meta.msg)

let myCharts = echarts.init(this.$refs.main)

const result = _.merge(res.data, this.options)

myCharts.setOption(result)

}

}

}

</script>

十六、项目优化


  1. 生产项目报告
    通过vue-cli-service build –report生成
    通过vue ui生成
    导入项目后,点击运行即可看到分析报告

  1. 区别生产环境跟开发环境
    在vue.config.js中挂载main-dev.js跟main-prod.js入口
  1. main-dev.js 与 main-prod.js 的区别
  • 不引入plugin/element.js

  • 不引入vue-quill-editor相关的css文件

  • 请求的根地址不同

  1. vue.config.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
26
27
28
29
30
31
32
33
34
35
36
module.exports = {

chainWebpack: config => {

config,
when(process.env.NODE_ENV === 'production', config => {

config.entry('app').clear().add('./src/main-prod.js')

config.plugin('html').tap(args => {

args[0].isProd = true

return args

})

})

config.when(process.env.NODE_ENV === 'development', config => {

config.entry('app').clear().add('./src/main-dev.js')

config.plugin('html').tap(args => {

args[0].isProd = false

return args

})

})

}

}
  1. 生产环境插件通过CDN加载
  1. 使用externals排除打进首包的插件

在vue.config.js的生产环境中加入以下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
config.set('externals', {

vue: 'Vue',

'vue-router': 'VueRouter',

lodash: '_',

echarts: 'echarts',

'vue-quill-editor': 'VueQuillEditor'

})

其中的element-ui并没有在main-prod.js中引入,所以不需要排除

  1. 在index.html中引入插件的CDN地址(略)
  1. 路由懒加载
    创建router/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
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import Vue from 'vue'

import VueRouter from 'vue-router'

const Login = () => import(
/* webpackChunkName: "login_home"
*/
'components/login/Login') //
相同chunkName的文件编译后会打包到同一个文件中

const Users = () => import(
/* webpackChunkName: "Users_Rights_Roles"
*/
'components/home/users/Users')

const Rights = () => import(
/* webpackChunkName: "Users_Rights_Roles"
*/
'components/home/power/right/Right')

const Roles = () => import(
/* webpackChunkName: "Users_Rights_Roles"
*/
'components/home/power/roles/Roles')

const Cate = () => import(
/* webpackChunkName: "Cates_Params"
*/
'components/home/goods/cate/Cate.vue')

const Params = () => import(
/* webpackChunkName: "Cates_Params"
*/
'components/home/goods/params/Params.vue')

const GoodsList = () => import(
/* webpackChunkName: "GoodsList_Add"
*/
'components/home/goods/list/List')

const Add = () => import(
/* webpackChunkName: "GoodsList_Add"
*/
'components/home/goods/list/children/Add')

const Edit = () => import(
/* webpackChunkName: "GoodsList_Add"
*/
'components/home/goods/list/children/Edit')

const Order = () => import(
/* webpackChunkName: "Order_Report"
*/
'components/home/order/Order')

const Report = () => import(
/* webpackChunkName: "Order_Report"
*/
'components/home/report/Report')

Vue.use(VueRouter)

const routes = [

{

path: '/',

redirect: '/home'

},

{

path: '/login',

component: Login

},

{

path: '/home',

component: Home,

redirect: to => {

const activeObj = JSON.parse(localStorage.getItem('activeObj'))

return activeObj ? activeObj : '/users'

}

},

children: [

{

path: '/users',

component: Users

},

{

path: '/rights',

component: Rights

},

{

path: '/roles',

component: Roles

},

{

path: '/categories',

component: Cate

},

{

path: '/params',

component: Params

},

{

path: '/goods',

component: GoodsList

},

{

path: '/goods/add',

component: Add

},

{

path: '/goods/edit/:id',

component: Edit,

props: true

},

{

path: '/orders',

component: Order

},

{

path: '/reports',

component: Report

}

]

]

const router = new VueRouter({

mode: 'history', // 历史模式用的路径地址来做路由

routes

})

router.beforeEach((to, from, next) => {

if (to.path === '/login' || localStorage.getItem('token')) return
next()

next('/login')

})

export default router

十七、项目上线


  1. 部署前端项目
  1. 创建express挂载

创建项目

1
2
3
npm init

npm i express

创建app.js:

1
2
3
4
5
6
7
8
9
10
11
const express = require('express')

const app = express()

app.use(express.static('./dist'))

app.listen(80, () => {

console.log('web server running at http://127.0.0.1')

})

在dist所在的目录运行app.js

1
sudo node app.js
  1. 开启gzip压缩

安装gzip

1
npm i compression -S

在app.js中:

1
2
3
const compression = require('compression')

app.use(compression())
  1. 开启https(一般是后台或运维来管的)
  • 申请SSL证书

其中freessl是免费的

首先输入地址后选择品牌

接着输入邮箱

完成申请之后就可以下载到SSL证书了

在后台项目中导入证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
const https = require('https')

const fs = require('fs')

const options = {

cert: fs.readFileSync('./full_chain.pem')

key: fs.readFileSync('./private.key')

}

https.createServer(options, app).listen(443)
  1. 部署后台API

安装pm2插件

1
npm i pm2 -g

启动项目:

1
pm2 start app.js --name vue-shop

查看项目:

1
pm2 ls

重启项目:

1
pm2 restart vue-shop

停止项目:

1
pm2 stop vue-shop

十八、(补)在 CentOS 7 上配置后台API


  1. mysql数据库
  • 安装mysql

直接安装最新的8.0的mysql似乎有问题,所以这里安装了5.6版本

1
2
3
4
5
6
7
8
9
10
11
12
13
# 下载mysql安装包
wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
rpm -ivh mysql-community-release-el7-5.noarch.rpm

# 安装
yum -y install mysql-server mysql-devel

# 启动服务
systemctl start mysql.service

#设置mysql密码
mysql -u root
mysql> set password for 'root'@'localhost' = password('mypasswd');
  • 导入数据
1
2
3
4
5
6
7
# root用户身份登录,输入密码
mysql -u root -p

create database vue_shop_server;
use vue_shop_server;
set names utf8;
source mydb.sql;
  1. 安装Node
1
2
3
4
5
6
# 先安装nvm这个包管理器,借助包管理器安装node
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
# 由于我服务器安装的centos7版本的系统,比较旧,无法支持v12以上的node,所以这里安装了node11
nvm install v11
# 永久使用11版本
nvm alias default 11.15.0
  1. sharp库
    后台原来用的是gm这个库来处理图片,但这个库非常旧,没人维护,而且需要安装第三方软件,整个过程非常繁琐,所以我给它换成了sharp库。
    由于sharp最新版本仅支持node12以上的版本,而我的CentOS 7 系统最高只能安装node11,所以需要使用旧版本的sharp。
    查阅历史更新记录可知0.29.0之前版本可以在node11下安装,故使用0.28.3。
1
npm i sharp@0.28.3

十九、非域名根目录下前端项目部署


  1. vue.config.js中添加以下内容,让静态资源在以下目录下面访问:
1
publicPath: process.env.NODE_ENV ? '/project/eshopcms' : './'
  1. 在路由new的时候添加以下属性,让路由也从以下目录开始访问
1
base: process.env.NODE_ENV ? '/project/eshopcms/' : '/'
  1. nginx配置
    由于项目用的是history路由模式,直接刷新页面或通过Url访问某个指定页面,会出现404问题,所以需要在nginx中进行部署,让所有请求都通过index.html
1
2
3
4
5
6
7
8
9
10
11
12
# root 需要从根目录匹配中提到外面来,这样/project/eshopems才会以root作为根目录
root /usr/share/nginx/html
location / {
index index.html index.htm index.php;
}

location /project/eshopems/ {
# try_files会测试当前$uri在服务器中是否有对应资源,如果返回404,就要访问最后的/project/eshopcms/index.html
# $uri代表/project/eshopems/
try_files $uri /project/eshopems/index.html;
}


二十、后台API的部署


main-prod.js中的$apiBaseUrl需要改成https://ghobam.com/api/private/v1/

接着在nginx中配置转发逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 转发api接口的请求
location /api/private/v1/ {
proxy_redirect off;
proxy_pass http://127.0.0.1:8888/api/private/v1/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 转发访问静态资源的请求
location /tmp_uploads/ {
proxy_redirect off;
proxy_pass http://127.0.0.1:8888/tmp_uploads/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

二十一、一些后续问题

  1. 开启CDN加速之后会导致Token验证失败

前端项目会在头部加上Authorization字段,CDN似乎没有正确转发这个这段,导致验证失败了。



参考:

视频教程

前端项目

后台API项目

syntax-dynamic-import插件实现懒加载

Vue的历史模式

路由懒加载

html-webpack-plugin使用文档

webpack-chain使用文档

webpack文档

element-ui按需引入方法

babel-plugin-transform-remove-console文档

Vue-Quill-Editor文档

vue-table-with-tree-grid文档

NProgress官网和效果展示

NProgress文档

清除浮动的四种方式及其原理理解

el-input绑定键盘按键--按键修饰符

localStorage与sessionStorage的区别

动态返回重定向目标

解决Element resetFields()重置表单不生效的问题

生成项目报告

项目上线

Element UI upload组件点击查看直接预览大图

javascript中怎么判断是否是数字?

nodejs 图片编辑工具 sharp 使用及踩坑指南

vue-quill-editor图片上传

axios配合Promise.all按顺序返回请求

配合 element-ui 实现上传图片/视频到七牛 demo

[转]nodejs Error: request entity too large解决方案

vue路由传参的三种基本方式

TreeTable的文档

Error while installing Nodejs on Godaddy Shared Linux Hosting

centos7 安装mysql

elementUi——Cascader 级联选择器渲染数据很多时卡顿问题解决


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!