1
第
8
章 “微商城”后台开发文档
8.1
准备工作
8.1.1
导入项目
(
1
)创建
D:\vue\chapter08
目录。
(
2
)从配套源代码中,将项目模板“
shop-system-template
”文件夹复制到
chapter08
目
录,并将其重命名为“
shop-system
”。
(
3
)使用命令提示符打开
D:\vue\chapter08\shop-system
目录,安装依赖。
yarn
yarn add axios@1.2.2 --save
yarn add element-plus@2.2.27 --save
yarn add @element-plus/icons-vue@2.0.10 --save
yarn add pinia@2.0.27 --save
yarn add pinia-plugin-persist@1.0 --save
yarn add vue-router@4.0.13 --save
yarn add sass@1.57.1 --save
(
4
)打开
index.html
修改标题。
<title>
后台管理系统
</title>
说明:本文档中标注红的代码为当前步骤新增或修改的代码。
8.1.2
定义路由
(
1
)创建
src\router\index.js
,具体代码如下。
import { createWebHistory, createRouter } from 'vue-router'
import Index from '../pages/Index.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
2
redirect: '/index',
component: Index,
meta: { title: '
首页
' },
children: [
{
path: '/index',
name: 'index',
component: () => import('../pages/subpages/Index.vue'),
meta: { title: '
首页
' },
},
{
path: '/category',
name: 'category',
component: () => import('../pages/subpages/Category.vue'),
meta: { title: '
分类管理
' },
},
{
path: '/goods',
name: 'goods',
component: () => import('../pages/subpages/Goods.vue'),
meta: { title: '
商品管理
' },
},
{
path: '/setting',
name: 'setting',
component: () => import('../pages/subpages/Setting.vue'),
meta: { title: '
个人中心
' },
},
],
},
{
path: '/login',
name: 'login',
component: () => import('../pages/Login.vue'),
meta: { title: '
登录
' },
}
]
})
3
export default router
(
2
)创建
src\pages\Index.vue
,具体代码如下。
<template>
<router-view></router-view>
</template>
(
3
)创建
src\pages\subpages\Index.vue
,具体代码如下。
<template>
Index
</template>
(
4
)创建
src\pages\subpages\Category.vue
,具体代码如下。
<template>
Category
</template>
(
5
)创建
src\pages\subpages\Goods.vue
,具体代码如下。
<template>
Goods
</template>
(
6
)创建
src\pages\subpages\Setting.vue
,具体代码如下。
<template>
Setting
</template>
(
7
)创建
src\pages\Login.vue
,具体代码如下。
<template>
Login
</template>
(
8
)修改
src\App.vue
中的内容,修改为中文语言,具体代码如下。
<template>
<el-config-provider :locale="zhCn">
<router-view></router-view>
</el-config-provider>
</template>
<script setup>
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
</script>
(
9
)修改
src\main.js
中的所有内容,具体代码如下。
import { createApp } from 'vue'
import './style.css'
4
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const app = createApp(App)
app.use(ElementPlus)
const pinia = createPinia()
pinia.use(piniaPluginPersist)
app.use(pinia)
app.use(router)
app.mount('#app')
(
10
)启动项目,命令如下。
yarn dev
(
11
)访问
http://127.0.0.1:5173
,页面效果如下图所示。
8.1.3
封装网络请求
(
1
)创建
src\config.js
,具体代码如下。
export default {
baseURL: 'http://127.0.0.1:8360'
}
(
2
)创建
src\stores\token.js
,具体代码如下。
5
import { defineStore } from 'pinia'
import { ref } from 'vue'
const useToken = defineStore('token', () => {
const token = ref(null)
const updateToken = val => token.value = val
const removeToken = () => token.value = null
return { token, updateToken, removeToken }
}, {
persist: {
enabled: true,
strategies: [
{
key: 'token',
storage: localStorage
}
]
}
})
export default useToken
(
3
)创建
src\utils\notification.js
,具体代码如下。
import { ElNotification } from 'element-plus'
var notificationInstance = null
export default (options) => {
notificationInstance && notificationInstance.close()
notificationInstance = ElNotification(options)
}
(
4
)创建
src\utils\request.js
,具体代码如下。
import axios from 'axios'
import useToken from '../stores/token'
import { ElLoading } from 'element-plus'
import config from '../config'
import notification from './notification'
import router from '../router'
const baseURL = configRL
6
var loadingInstance = null
const service = axios.create({ baseURL })
service.interceptors.request.use(config => {
loadingInstance = ElLoading.service()
const { token } = useToken()
if (token) {
config.headers.jwt = token
}
return config
})
service.interceptors.response.use(
response => {
loadingInstance.close()
const { errno, data, errmsg } = response.data
if (errno === 0) {
if (errmsg !== '') {
notification({
message: errmsg,
type: 'success'
})
}
return data || true
}
notification({
message: errmsg,
type: 'error'
})
if (errno === 2) {
router.push({ name: 'login' })
}
return false
},
error => {
loadingInstance.close()
7
notification({
message: '
请求失败
',
type: 'error'
})
console.log(error)
}
)
export default service
8.2
登录页面
8.2.1
显示登录页面
(
1
)打开
src\style.css
添加全局样式,具体代码如下。
html {
height: 100%;
}
body {
margin: 0;
height: 100%;
overflow: hidden;
}
#app {
height: 100%;
}
a {
text-decoration: none;
}
(
2
)修改
src\pages\Login.vue
中的所有内容,具体代码如下。
<template>
<el-card class="box-card">
<el-card class="box-form">
<template #header>
<div class="card-header">
<h3>
“微商城”后台管理系统
</h3>
8
</div>
</template>
<el-form ref="ruleFormRef" status-icon :model="form" :rules="rules"
label-width="120px">
<el-form-item prop="username" label="
用户名:
">
<el-input v-model="form.username" placeholder="
请输入用户名
" />
</el-form-item>
<el-form-item prop="password" label="
密 码:
">
<el-input v-model="form.password" type="password" show-password
placeholder="
请输入密码
" />
</el-form-item>
<el-form-item>
<el-button class="button" @click="submitForm(ruleFormRef)" type
="primary" size="large">
登录
</el-button>
<el-button class="button" @click="resetForm" type="info" size="l
arge">
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
const form = reactive({
username: '',
password: ''
})
const ruleFormRef = ref()
//
表单提交
const submitForm = formEl => {
}
//
表单重置
const resetForm = () => {
ruleFormRef.value.resetFields()
}
9
const rules = reactive({
username: [
{ required: true, message: '
请输入用户名
', trigger: 'blur' },
{ min: 3, max: 12, message: '
用户名长度为
3~12
个字符
', trigger: 'blur' },
],
password: [
{ required: true, message: '
请输入密码
', trigger: 'blur' },
{ min: 6, max: 24, message: '
密码长度为
6~24
个字符
', trigger: 'blur' },
]
})
</script>
<style lang="scss" scoped>
.box-card {
height: 100%;
background: rgba(38, 72, 176) url('/images/loginBg.jpg') center / cover
no-repeat;
.box-form {
position: absolute;
top: 50%;
left: 50%;
width: 70%;
max-width: 750px;
transform: translate(-50%, -50%);
.card-header {
display: flex;
justify-content: center;
align-items: center;
}
.el-form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-form-item {
width: 70%;
display: flex;
10
align-items: center;
justify-content: center;
--el-form-label-font-size: 16px;
margin-top: 15px;;
margin-bottom: 15px;
.button {
width: 90px;
}
&.center {
display: flex;
justify-content: center;
}
}
}
}
}
</style>
8.2.2
实现登录功能
(
1
)创建
src\api\index.js
,具体代码如下。
import request from '../utils/request'
//
登录接口
export function login(data) {
return request.post('/admin/login', data)
}
(
2
)修改
src\pages\Login.vue
,具体代码如下。
import { ref, reactive } from 'vue'
import { login } from '../api'
(
3
)修改
src\pages\Login.vue
,添加表单验证,调用登录接口,具体代码如下。
//
表单提交
const submitForm = formEl => {
formEl.validate(async valid => {
if (valid) {
const data = await login(form)
if (data) {
11
//
登录成功
}
} else {
//
表单填写有误
'
}
})
}
(
4
)修改
src\pages\Login.vue
,具体代码如下。
import { login } from '../api'
import { useRouter } from 'vue-router'
import useToken from '../stores/token'
import notification from '../utils/notification'
const router = useRouter()
const { updateToken } = useToken()
(
5
)修改
src\pages\Login.vue
,当登录成功后,保存
token
并进行页面跳转,当验证
失败后,弹出提示信息,具体代码如下。
formEl.validate(async valid => {
if (valid) {
const data = await login(form)
if (data) {
updateToken(data.token)
router.push({ name: 'index' })
}
} else {
notification({
message: '
表单填写有误
',
type: 'error'
})
}
})
8.2.3
检查用户是否登录
(
1
)创建
src\router\permission.js
,具体代码如下。
import router from './'
import useToken from '../stores/token'
12
import notification from '../utils/notification'
const whiteList = ['/login']
router.beforeEach((to, from, next) => {
document.title = to.meta.title + ' - ' + '
后台管理系统
'
const { token } = useToken()
if (token) {
next()
} else {
if (whiteList.includes(to.path)) {
next()
} else {
notification({
message: '
请先登录
',
type: 'error'
})
next('/login')
}
}
})
(
2
)修改
src\main.js
中的部分代码,具体代码如下。
import piniaPluginPersist from 'pinia-plugin-persist'
import './router/permission'
(
3
)访问
http://127.0.0.1:5173/index
,测试能否自动跳转到登录页。
13
8.3
后台首页
8.3.1
实现后台页面布局
(
1
)创建
src\components\Header.vue
,具体代码如下。
<template>
Header
</template>
(
2
)创建
src\components\Aside.vue
,具体代码如下。
<template>
Aside
</template>
(
3
)修改
src\pages\Index.vue
中的所有内容,具体代码如下。
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-container>
<el-aside>
<Aside></Aside>
</el-aside>
<el-main>
<el-card class="box-card">
<router-view></router-view>
</el-card>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
import Header from '../components/Header.vue'
import Aside from '../components/Aside.vue'
14
</script>
<style lang="scss" scoped>
.el-container {
height: 100%;
.el-header {
background: -webkit-gradient(linear, left top, right top, from(#1493f
a), to(#01c6fa));
text-align: center;
line-height: 60px;
color: #333;
}
.el-aside {
width: 200px;
height: 100%;
color: #333;
background: white
}
.el-main {
height: 100vh;
background-color: #e9eef3;
color: #333;
}
}
</style>
8.3.2
实现头部区域
(
1
)创建
src\stores\admin.js
,存储管理员数据,具体代码如下。
import { defineStore } from 'pinia'
import { reactive } from 'vue'
const useAdmin = defineStore('admin', () => {
const defaultAdmin = {
username: '',
avatar: ''
}
15
const admin = reactive(Object.assign({}, defaultAdmin))
const updateAdmin = options => {
Object.assign(admin, options)
return admin
}
const removeAdmin = () => {
Object.assign(admin, defaultAdmin)
return admin
}
return { admin, updateAdmin, removeAdmin }
}, {
persist: {
enabled: true,
strategies: [
{
key: 'admin',
storage: localStorage,
}
]
}
}
)
export default useAdmin
(
2
)修改
src\components\Header.vue
中的所有内容,具体代码如下。
<template>
<div></div>
<el-menu class="el-menu-demo" mode="horizontal" :ellipsis="false">
<div class="navbar">
“微商城”后台管理系统
</div>
<el-sub-menu class="menu" index="1">
<template #title>
<el-avatar class="avatar" :src="admin.avatar"> {{ admin.username
}} </el-avatar>
</template>
<router-link :to="{ name: 'setting' }">
<el-menu-item index="5">
个人中心
</el-menu-item>
</router-link>
<el-menu-item index="6" @click="onLogout">
退出登录
</el-menu-item>
</el-sub-menu>
16
</el-menu>
</template>
<script setup>
import useAdmin from '../stores/admin'
const { admin } = useAdmin()
const onLogout = async () => {
}
</script>
<style scoped lang="scss">
.el-menu-demo {
background: linear-gradient(90deg, #1493fa, #01c6fa);
}
.navbar {
color: white;
font-size: 20px;
}
.menu {
margin-left: auto;
}
</style>
(
3
)访问
http://127.0.0.1:5173/index
,初始页面如下图所示。
(
4
)在
src\api\index.js
中添加如下代码。
//
用户信息接口
export function getAdmin() {
return request.get('/admin/admin')
}
17
(
5
)修改
src\components\Header.vue
,具体代码如下。
import { onMounted } from 'vue'
import { getAdmin } from '../api'
const { admin
, updateAdmin
} = useAdmin()
onMounted(() => {
loadAdmin()
})
const loadAdmin = async () => {
let data = await getAdmin()
updateAdmin({
username: data.username,
avatar: data.avatar
})
}
(
6
)修改
src\components\Header.vue
,具体代码如下。
import useAdmin from '../stores/admin'
import router from '../router'
import useToken from '../stores/token'
import notification from '../utils/notification'
const { removeToken } = useToken()
const { admin, updateAdmin
, removeAdmin
} = useAdmin()
(
7
)修改
src\components\Header.vue
,实现用户退出功能,具体代码如下。
const onLogout = async () => {
removeToken()
removeAdmin()
router.push({ name: 'login' })
notification({
message: '
退出成功
',
type: 'success'
})
}
(
8
)访 问
http://127.0.0.1:5173/index
, 单 击 退 出 登 录 按 钮 , 跳 转 到
http://127.0.0.1:5173/login
页面。
18
8.3.3
实现侧边区域
修改
src\components\Aside.vue
中的所有内容,具体代码如下。
<template>
<el-row>
<el-col :span="24">
<el-menu active-text-color="#white" class="el-menu-vertical-demo" :
default-active="active" text-color="#333">
<!--
首页
-->
<router-link :to="{ name: 'index' }">
<el-menu-item index="1">
<el-icon>
<HomeFilled />
</el-icon>
<span>
首页
</span>
</el-menu-item>
</router-link>
<!--
分类管理
-->
<router-link :to="{ name: 'category' }">
<el-menu-item index="2">
<el-icon>
<List />
</el-icon>
<span>
分类管理
</span>
</el-menu-item>
</router-link>
<!--
商品管理
-->
<router-link :to="{ name: 'goods' }">
<el-menu-item index="3">
19
<el-icon>
<List />
</el-icon>
<span>
商品管理
</span>
</el-menu-item>
</router-link>
<!--
个人中心
-->
<router-link :to="{ name: 'setting' }">
<el-menu-item index="4">
<el-icon>
<Setting />
</el-icon>
<span>
个人中心
</span>
</el-menu-item>
</router-link>
</el-menu>
</el-col>
</el-row>
</template>
<script setup>
import { HomeFilled, Setting, List } from '@element-plus/icons-vue'
import { ref } from 'vue'
import router from '../router'
const menuIndex = {
'index': '1',
'category': '2',
'goods': '3',
'setting': '4'
}
const active = ref(menuIndex[router.currentRoute.value.name] || '0')
</script>
<style lang="scss" scoped>
.el-menu {
border: 0 !important;
.is-active {
background: linear-gradient(90deg, #1493fa, #01c6fa) !important
20
}
a {
text-decoration: none;
color: white;
}
}
</style>
8.3.4
实现后台首页内容
(
1
)修改
src\pages\subpages\Index.vue
中的所有内容,加载首页数据,具体代码如下。
<script setup>
import { reactive, onMounted } from 'vue'
import { getAdmin } from '../../api'
import useAdmin from '../../stores/admin'
const { admin, updateAdmin } = useAdmin()
onMounted(() => {
loadAdmin()
})
const loadAdmin = async () => {
let data = await getAdmin()
updateAdmin({
username: data.username,
avatar: data.avatar
})
}
//
用户登录信息(模拟数据)
const loginInfo = reactive({
loginTime: '2023-07-22 09:00:00',
loginPlace: '
北京
'
})
</script>
(
2
)修改
src\pages\subpages\Index.vue
,添加如下代码,显示用户登录信息。
21
<template>
<el-row :gutter="20">
<el-col :span="6">
<el-card class="box-card">
<template #header>
<div class="card-header">
<el-avatar class="avatar" :src="admin.avatar" shape="square" :
size="40"> </el-avatar>
<span style="font-size: 24px;">{{ admin.username }} </span>
</div>
</template>
<div class="info">
<p>
登录时间:
{{ loginInfo.loginTime }}</p>
<p>
登录地点:
{{ loginInfo.loginPlace }}</p>
</div>
</el-card>
</el-col>
<!--
单月统计信息展示
-->
</el-row>
<!--
图表区域
-->
</template>
<style lang="scss" scoped>
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.el-col {
.box-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.info {
font-size: 14px;
}
22
}
}
}
</style>
(
3
)修改
src\pages\subpages\Index.vue
,具体代码如下。
import useAdmin from '../../stores/admin'
import { Memo } from '@element-plus/icons-vue'
(
4
)修改
src\pages\subpages\Index.vue
,在“单月统计信息展示区域”编写如下代码。
<!--
单月统计信息展示
-->
<el-col :span="18">
<el-card class="box-card">
<template #header>
<div class="card-header">
6
月统计信息
</div>
</template>
<div class="info">
<el-row :gutter="24">
<!--
商品数量
-->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style="background-color: #EEA
D0E;">
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">500</p>
<p>
商品数量
(
个
)</p>
</div>
</div>
</el-col>
<!--
商品分类数量
-->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style="background-color: #AB8
2FF;">
23
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">20</p>
<p>
商品分类数量
(
个
)</p>
</div>
</div>
</el-col>
<!--
用户访问次数
-->
<el-col :span="8">
<div class="card-container">
<div class="card-left-container" style=" background-color: #63
B8FF;">
<el-icon :size="90">
<Memo />
</el-icon>
</div>
<div class="card-right-container">
<p class="number">121</p>
<p>
用户访问次数
(
次
)</p>
</div>
</div>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
(
5
)修改
src\pages\subpages\Index.vue
中的样式代码,具体如下。
<style lang="scss" scoped>
……(原有代码)
.card-container {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #e4e7ed;
text-align: center;
24
padding-right: 20px;
.card-left-container {
color: white;
}
.card-right-container {
.number {
font-size: 18px;
font-weight: bold;
}
}
}
</style>
(
6
)在
src\pages\subpages\Index.vue
中的编写“图表区域”的代码,具体如下。
<!--
图表区域
-->
<el-row :gutter="20">
<el-col :span="12">
<!--
通过折线图展示
2022
年月度销售额
-->
<el-card class="box-card">
<div id="salesVolume" style="width: auto; height:400px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<!--
通过柱状图展示
2022
年订单数量
-->
<el-card class="box-card">
<div id="orderQuantity" style="width: auto; height:400px;"></div>
</el-card>
</el-col>
</el-row>
(
7
)使用命令提示符打开
D:\vue\chapter08\shop-system
目录,安装依赖。
yarn add echarts@5.4.2 --save
(
8
)修改
src\pages\subpages\Index.vue
,具体代码如下。
import * as echarts from 'echarts'
(
9
)在
src\pages\subpages\Index.vue
中编写如下代码,在页面挂载完成后加载图表。
onMounted(() => {
loadAdmin()
initCharts1()
initCharts2()
})
25
//
图表
1
:月度销售额
const initCharts1 = () => {
}
//
图表
2
:
2022
年订单数量
const initCharts2 = () => {
}
(
10
)在
src\pages\subpages\Index.vue
中编写“月度销售额”图表区域的代码,具体代
码如下。
const initCharts1 = () => {
const myChart = echarts.init(document.getElementById('salesVolume'))
myChart.setOption({
color: ['#1493fa'],
title: { text: '2022
年月度销售额
' },
xAxis: {
data: ['1
月
', '2
月
', '3
月
', '4
月
', '5
月
', '6
月
', '7
月
', '8
月
', '9
月
', '10
月
', '11
月
', '12
月
'],
name: '
月份
',
axisLabel: {
interval: 0
},
},
yAxis: {
name: '
单位(千万元)
',
},
grid: {
left: '3%',
right: '8%',
bottom: '5%',
containLabel: true,
},
legend: {},
series: [
{
data: [6, 7, 8.5, 8, 9, 10, 13, 12, 10, 16, 15, 14],
type: 'line',
name: '
销售额
',
26
smooth: true,
label: {
show: true,
position: 'top',
}
}
]
})
//
图表自适应大小
window.onresize = () => {
myChart.resize()
}
}
(
11
)在
src\pages\subpages\Index.vue
中编写“
2022
年订单数量”图表区域的代码,
具体代码如下。
const initCharts2 = () => {
const myChart = echarts.init(document.getElementById('orderQuantity'))
myChart.setOption({
title: { text: '2022
年订单数量
' },
color: ['#1493fa'],
grid: {
left: '3%',
right: '8%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: ['1
月
', '2
月
', '3
月
', '4
月
', '5
月
', '6
月
', '7
月
', '8
月
', '9
月
', '10
月
', '11
月
', '12
月
'],
name: '
月份
',
//
类目轴中在
boundaryGap
为
true
的时候有效,可以保证刻度线和标签对齐
axisTick: {
alignWithLabel: true,
},
axisLabel: {
interval: 0,rotate: 45 //
设置刻度标签旋转角度为
45
度
},
27
},
legend: {},
yAxis: {
name: '
单位(个)
',
},
series: [
{
data: [400, 450, 300, 230, 250, 300, 400, 350, 160, 350, 380, 400],
type: 'bar',
barWidth: '60%',
name: '
订单总数
',
label: {
show: true,
position: 'top',
}
}
]
})
//
图表自适应大小
window.onresize = () => {
myChart.resize()
}
}
(
12
)访问
http://127.0.0.1:5173/index
,首页如下图所示。
28
8.4
个人中心页
8.4.1
添加相关接口
(
1
)修改
src\api\index.js
,导入配置文件,具体代码如下。
import request from '../utils/request'
import config from '../config'
(
2
)修改
src\api\index.js
,添加接口,具体代码如下。
//
修改密码接口
export function changeAdminPassword(data) {
return request.post('/admin/admin/changePassword', data)
}
//
修改头像接口
export function changeAdminAvatar(data) {
return request.post('/admin/admin/changeAvatar', data)
}
//
更新图片地址
29
export function uploadPictureURL() {
return config.baseURL + '/admin/upload/picture'
}
8.4.2
显示个人中心页
(
1
)修改
src\pages\subpages\Setting.vue
中的所有内容,具体代码如下。
<template>
<el-row :gutter="20">
<el-col :span="8">
<el-card class="box-card">
<template #header>
<div class="card-header">
头像信息
</div>
</template>
<div class="text item">
<div class="avatar">
<el-avatar class="avatar" shape="square" :size="50" :src="avat
arURL" />
</div>
<el-upload
ref="uploadRef"
class="upload-demo"
:limit="1"
:action="uploadURL"
:headers="headers"
:data="uploadData"
:auto-upload="false"
:on-success="uploadSuccess">
<template #trigger>
<p><el-button type="primary">
选择头像
</el-button></p>
</template>
<div>
<el-button type="success" @click="submitUpload">
上传头像
</el
button>
</div>
<template #tip>
<div class="el-upload__tip"><p>
限制上传
1
个文件,且旧文件会被新文
30
件覆盖
</p></div>
</template>
</el-upload>
</div>
</el-card>
</el-col>
<el-col :span="16">
<el-card class="box-card">
<template #header>
个人信息
</template>
<div class="change-password-box">
<el-form ref="ruleFormRef" status-icon :model="form" :rules="rul
es" label-width="140px">
<el-form-item prop="password" label="
修改密码
">
<el-input type="password" v-model="form.password" />
</el-form-item>
<el-form-item prop="password2" label="
请再次输入密码
">
<el-input type="password" v-model="form.password2" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">
提
交
</el-button>
<el-button @click="resetForm">
重置
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
import { ref, reactive } from 'vue'
import defaultAvatarURL from '/images/avatar-default.png'
import { uploadPictureURL } from '../../api'
import useToken from '../../stores/token'
import useAdmin from '../../stores/admin'
const { admin } = useAdmin()
31
const { token } = useToken()
const headers = { jwt: token }
const uploadURL = uploadPictureURL()
const uploadData = { type: 'admin_avatar' }
const form = reactive({
password: '',
password2: ''
})
const avatarURL = ref(admin.avatar || defaultAvatarURL)
const ruleFormRef = ref()
const uploadRef = ref()
//
修改密码
const submitForm = formEl => {
}
const resetForm = () => {
ruleFormRef.value.resetFields();
}
const submitUpload = () => {
uploadRef.value.submit()
}
//
上传成功
const uploadSuccess = async response => {
}
const validatePass = (rule, value, callback) => {
if (value !== form.password) {
callback(new Error('
两次输入密码不一致!
'))
} else {
callback()
}
}
32
const rules = reactive({
password: [
{ required: true, message: '
请输入密码
', trigger: 'blur' },
{ min: 6, max: 24, message: '
密码长度为
6~24
个字符
', trigger: 'blur' },
],
password2: [
{ required: true, message: '
请输入密码
', trigger: 'blur' },
{ validator: validatePass, trigger: 'blur' }
]
})
</script>
<style lang="scss" scoped>
.avatar {
text-align: center;
}
.upload-demo {
text-align: center;
}
.box-card {
height: 316px;
}
.change-password-box {
padding-top: 38px;
}
</style>
(
2
)访问
http://127.0.0.1:5173/setting
,“个人中心”页如下图所示。
33
8.4.3
实现修改密码功能
(
1
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
import router from '../../router'
(
2
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
const { admin
, removeAdmin
} = useAdmin()
(
3
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
const { token
, removeToken
} = useToken()
(
4
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
import {
changeAdminPassword,
uploadPictureURL } from '../../api'
import notification from '../../utils/notification'
(
5
)修改
src\pages\subpages\Setting.vue
,实现修改密码功能,具体代码如下。
const submitForm = formEl => {
formEl.validate(async valid => {
if (valid) {
await changeAdminPassword({ password: form.password })
resetForm()
//
退出登录
removeToken()
removeAdmin()
router.push({ name: 'login' })
notification({
message: '
修改密码后,请重新登录
',
type: 'warning'
})
} else {
notification({
message: '
表单填写有误
',
type: 'error'
})
}
})
}
(
6
)访问
http://127.0.0.1:5173/setting
,在个人信息区域输入修改后的密码,单击提交
之后,页面效果如下。
34
8.4.4
实现修改头像功能
(
1
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
const { admin
, updateAdmin
, removeAdmin } = useAdmin()
(
2
)修改
src\pages\subpages\Setting.vue
,具体代码如下。
import { changeAdminPassword,
changeAdminAvatar,
uploadPictureURL } from
'../../api'
(
3
)修改
src\pages\subpages\Setting.vue
,实现修改头像的功能,具体代码如下。
const uploadSuccess = async response => {
const { errno, errmsg, data } = response
if (errno !== 0) {
notification({
message: errmsg,
type: 'error'
})
} else {
if (errmsg !== '') {
notification({
message: errmsg,
type: 'success'
})
}
await changeAdminAvatar({
avatar: data.savepath
})
updateAdmin({
35
avatar: data.url
})
avatarURL.value = data.url
}
uploadRef.value.clearFiles()
}
(
4
)访问
http://127.0.0.1:5173/setting
,“个人中心”页如下图所示。
(
5
)单击“选择头像”按钮,在弹出的文件夹中选择喜爱的图片作为头像,单击确定,
页面效果如下图所示。
上传头像文件之后,在红框处左侧会展示已上传头像的名称,单击右侧的“
”可以
删除所选头像。
(
6
)单击“上传头像”后,“个人中心”页面效果如下图所示。
36
(
7
)“首页”页面效果如下图所示。
8.5
分类管理页
8.5.1
加载分类列表数据
(
1
)修改
src\api\index.js
,具体代码如下。
//
分类列表接口
export function getCategoryList() {
return request.get('/admin/category/list')
37
}
(
2
)修改
src\pages\subpages\Category.vue
中的所有内容,具体代码如下。
<script setup>
import { ref, onMounted } from 'vue'
import { getCategoryList } from '../../api'
const tableData = ref([])
onMounted(() => {
loadCategoryList()
})
//
查询分类列表
const loadCategoryList = async () => {
let data = await getCategoryList()
tableData.value = convertToTree(data)
}
//
将一维数组转换成树形结构的方法
const convertToTree = data => {
const treeData = []
const map = {}
//
遍历一维数组数据,建立节点映射表
for (const item of data) {
map[item.id] = { ...item, children: [] }
}
//
遍历映射表,将节点添加到父节点的
children
中
for (const item of data) {
const node = map[item.id]
if (item.pid === 0) {
treeData.push(node)
} else {
const parent = map[item.pid]
parent.children.push(node)
}
}
return treeData
38
}
</script>
8.5.2
显示分类列表页面
(
1
)修改
src\pages\subpages\Category.vue
,具体代码如下。
<template>
<div>
<el-button type="primary" style="margin-bottom: 10px" @click="addRow
">
新增分类
</el-button>
<!--
分类管理
-->
<el-table ref="tableRef" :data="tableData" style="margin-bottom: 20px
" row-key="id" border default-expand-all>
<el-table-column prop="name" label="
分类名称
" sortable />
<el-table-column label="
分类级别
">
<template #default="{ row }">
<span v-if="row.pid == 0">
一级分类
</span>
<span v-else>
二级分类
</span>
</template>
</el-table-column>
<el-table-column prop="id" label="
分类编号
" />
<el-table-column label="
分类图片
">
<template #default="{ row }">
<el-image v-if="row.picture != ''" :src="row.picture" fit="conta
in" style="display: flex; align-items: center; height: 60px" />
</template>
</el-table-column>
<el-table-column fixed="right" label="
操作
" width="180">
<template #default="{ row }">
<el-button type="warning" @click="editRow(row)">
编辑
</el-button>
<el-button type="danger" @click="delRow(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
(
2
)修改
src\pages\subpages\Category.vue
,具体代码如下。
39
<script setup>
//
新增分类
const addRow = () => {
}
//
修改分类
const editRow = row => {
}
//
删除分类
const delRow = row => {
}
</script>
(
3
)访问
http://127.0.0.1:5173/category
,“分类列表”页如下图所示。
8.5.3
实现弹出框
(
1
)修改
src\pages\subpages\Category.vue
,显示弹出框,具体代码如下。
<el-button type="primary" style="margin-bottom: 10px" @click="addRow">
新
增分类
</el-button>
<!--
新增分类的弹出框
-->
40
<el-dialog v-model="dialogVisible" :title="id ? '
修改分类
' : '
新增分类
'">
</el-dialog>
(
2
)修改
src\pages\subpages\Category.vue
,具体代码如下。
const tableData = ref([])
const dialogVisible = ref(false)
const id = ref(0)
(
3
)修改
src\pages\subpages\Category.vue
,具体代码如下。
//
新增分类
const addRow = () => {
id.value = 0
dialogVisible.value = true
}
//
修改分类
const editRow = row => {
id.value = row.id
dialogVisible.value = true
}
//
关闭弹出框前
const handleBeforeClose = () => {
}
(
4
)修改
src\pages\subpages\Category.vue
,编写关闭弹窗前的函数,具体代码如下。
<el-dialog v-model="dialogVisible" :title="id ? '
修改分类
' : '
新增分类
'"
:b
efore-close="handleBeforeClose"
>
(
5
)修改
src\pages\subpages\Category.vue
,引入消息弹框组件,具体代码如下。
import { ElMessageBox } from 'element-plus'
(
6
)修 改
src\pages\subpages\Category.vue
, 编 写 关 闭 弹 窗 前 调 用 的 函 数
handleBeforeClose
,具体代码如下。
const handleBeforeClose = () => {
ElMessageBox.confirm('
确定关闭对话框吗?
', {
showClose: false,
closeOnClickModal: false,
confirmButtonText: '
确定
',
cancelButtonText: '
取消
',
}).then(() => {
dialogVisible.value = false
}).catch(() => {})
41
}
(
7
)创建
src\components\CategoryEdit.vue
,具体代码如下。
<template>
<el-form ref="formRef" :model="form" label-width="120px">
<!--
分类名称
-->
<el-form-item prop="name" label="
分类名称
" style="width: 92%">
<el-input v-model="form.name" placeholder="
请填写分类名称
" />
</el-form-item>
<!--
操作按钮
-->
<el-form-item>
<el-button type="primary" @click="editSubmit" v-if="id">
修改
</el-bu
tton>
<el-button type="primary" @click="addSubmit" v-else>
新增
</el-button>
<el-button @click="btnCancel">
重置
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
const props = defineProps({
id: {
type: Number
},
})
const form = reactive({
id: props.id,
name: '',
pid: '',
picture: ''
})
const formRef = ref()
//
新增操作
42
const addSubmit = () => {
}
//
修改操作
const editSubmit = () => {
}
//
重置表单
const btnCancel = () => {
formRef.value.resetFields()
}
</script>
(
8
)修改
src\pages\subpages\Category.vue
,引入
CategoryEdit
组件,具体代码如下。
import CategoryEdit from '../../components/CategoryEdit.vue'
(
9
)修改
src\pages\subpages\Category.vue
,具体代码如下。
<el-dialog v-model="dialogVisible" :title="id ? '
修改分类
' : '
新增分类
'" :b
efore-close="handleBeforeClose">
<CategoryEdit :id="id" @success="editSuccess" />
</el-dialog>
const editSuccess = () => {
loadCategoryList()
dialogVisible.value = false
}
(
10
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页的“新增分类”按钮,
页面效果如下图所示。
43
(
11
)单击“ ”按钮,弹出消息提示框,如下图所示。
单击“确认”按钮后,弹出框关闭,单击“取消”按钮之后,页面效果参考步骤(
10
)
中的页面效果。
(
12
)单击“分类列表”页中操作列的“编辑”按钮,页面效果如下图所示。
8.5.4
加载分类数据
(
1
)修改
src\api\index.js
,具体代码如下。
//
查询单个分类接口
export function getCategory(params) {
return request.get('/admin/category', { params })
44
}
(
2
)修改
src\components\CategoryEdit.vue
,具体代码如下。
import { ref, reactive
, onMounted
} from 'vue'
import { getCategory, getCategoryList } from '../api'
(
3
)修改
src\components\CategoryEdit.vue
,,具体代码如下。
const categoryList = ref([])
onMounted(() => {
loadCategory()
})
const loadCategory = async () => {
if (form.id) {
const data = await getCategory({ id: form.id })
Object.assign(form, data)
}
const list = await getCategoryList()
categoryList.value = list.filter(item => item.pid == 0)
}
(
4
)修改
src\components\CategoryEdit.vue
,在显示弹出框时更新表单,具体代码如下。
const resetForm = id => {
form.id = id
btnCancel()
}
defineExpose({ resetForm })
const btnCancel = () => {
formRef.value.resetFields()
loadCategory()
}
(
5
)
src\pages\subpages\Category.vue
,具体代码如下。
<CategoryEdit
ref="categoryForm"
:id="id" @success="editSuccess"></Categ
oryEdit>
(
6
)
src\pages\subpages\Category.vue
,具体代码如下。
const id = ref(0)
const categoryForm = ref()
//
新增分类
45
const addRow = () => {
if (categoryForm.value) {
categoryForm.value.resetForm(0)
}
……(原有代码)
}
//
修改分类
const editRow = row => {
if (categoryForm.value) {
categoryForm.value.resetForm(row.id)
}
……(原有代码)
}
(
7
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页某条分类数据的操作列
的“编辑”按钮,页面效果如下图所示。
8.5.5
实现选择上级分类
(
1
)修改
src\components\CategoryEdit.vue
,具体代码如下。
const categoryList = ref([])
const showMore = ref(false)
(
2
)修改
src\components\CategoryEdit.vue
,具体代码如下。
<!--
分类名称
-->
<el-form-item prop="name" label="
分类名称
" style="width: 92%">
<el-input v-model="form.name" placeholder="
请填写分类名称
" />
46
</el-form-item>
<!--
是否为二级分类
-->
<el-form-item label="
二级分类
">
<el-radio-group v-model="showMore">
<el-radio :label="true" :disabled="form.id !== 0 && !form.pid">
是
</el
-radio>
<el-radio :label="false" :disabled="form.id !== 0 && !form.pid">
否
</e
l-radio>
</el-radio-group>
</el-form-item>
<!--
上级分类
-->
<el-form-item v-show="showMore" label="
上级分类
" prop="pid">
<el-select v-model="form.pid" placeholder="
请选择上级分类名称
" >
<el-option v-for="item in categoryList" :key="item.id" :label="item.n
ame" :value="item.id" />
</el-select>
</el-form-item>
<!--
操作按钮
-->
(
3
)修改
src\components\CategoryEdit.vue
,具体代码如下。
const loadCategory = async() => {
……(原有代码)
showMore.value = form.pid != 0
}
(
4
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页某条一级分类信息中操
作列的“编辑”按钮,页面效果如下图所示。
(
5
)单击某条二级分类信息中操作列的“编辑”按钮,页面效果如下图所示。
47
8.5.6
实现分类图片上传
(
1
)修改
src\components\CategoryEdit.vue
,具体代码如下。
<!--
上级分类
-->
<el-form-item v-show="showMore" label="
上级分类
" prop="pid">
<el-select v-model="form.pid" placeholder="
请选择上级分类名称
">
<el-option v-for="item in categoryList" :key="item.id" :label="item.n
ame" :value="item.id" />
</el-select>
</el-form-item>
<!--
分类图片
-->
<el-form-item label="
分类图片
" v-show="showMore">
<el-upload
ref="uploadRef"
class="upload-demo"
v-model:file-list="fileList"
:action="uploadPictureURL()"
:headers="{ jwt: token }"
:data="{ type: 'category_picture' }"
:limit="1"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
>
<template #trigger>
<el-button type="primary">
请选择图片
</el-button>
48
</template>
<template #tip>
<div class="el-upload__tip">
图片文件大小不超过
500KB</div>
</template>
</el-upload>
</el-form-item>
<!--
操作按钮
-->
(
2
)修改
src\components\CategoryEdit.vue
的样式,具体代码如下。
<style scoped>
.upload-demo {
text-align: left;
width: 91%;
}
</style>
(
3
)修改
src\components\CategoryEdit.vue
,具体代码如下。
import { getCategory, getCategoryList
, uploadPictureURL
} from '../api'
import useToken from '../stores/token'
const categoryList = ref([])
const fileList = ref([])
const uploadRef = ref()
const { token } = useToken()
(
4
)修改
src\components\CategoryEdit.vue
,编写重置表单、上传文件超出限制个数的
函数、文件上传成功之后的函数,具体代码如下。
//
重置表单
const btnCancel = () => {
……(原有代码)
form.picture = ''
uploadRef.value.clearFiles()
loadCategory()
}
//
文件超出个数限制时替换已有图片
const handleExceed = files => {
uploadRef.value.clearFiles()
uploadRef.value.handleStart(files[0])
uploadRef.value.submit()
}
49
//
上传成功
const uploadSuccess = response => {
const { errno, errmsg, data } = response
if (errno !== 0) {
notification({
message: errmsg,
type: 'error'
})
} else {
if (errmsg !== '') {
notification({
message: errmsg,
type: 'success'
})
}
form.picture = data.savepath
}
}
(
5
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页“新增分类”按钮,页
面效果如下图所示。
(
6
)单击“请选择图片”按钮,在弹出的文件夹中选择对应分类的图片,例如选择名
称为“
orange.png
”的图片,单击“打开”按钮后,页面效果如下图所示。
50
选择图片文件之后,会展示上传后的文件列表,左侧表示已选择的文件名称,单击列
表右侧的“ ”按钮,可以将已选择的文件删除。
(
7
)当再次单击“请选择图片”按钮后,在弹出的文件夹中选择名称为“
grapefruit.png
”
的图像文件,页面效果如下图所示。
(
8
)修改
src\components\CategoryEdit.vue
,实现在修改分类时显示已有图片,具体代
码如下。
const loadCategory = async() => {
if (form.id) {
const data = await getCategory({ id: form.id })
if (data.picture !== '') {
const fileName = data.picture.substring(data.picture.lastIndexOf('/
') + 1)
51
if (fileName) {
fileList.value = [{ name: fileName, url: data.picture }]
}
}
Object.assign(form, data)
}
……(原有代码)
}
(
9
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页二级分类中的某条数据
中操作列的“编辑”按钮,页面效果如下图所示。
8.5.7
实现新增和修改分类
(
1
)修改
src\api\index.js
,具体代码如下。
//
新增分类接口
export function addCategory(data) {
return request.post('/admin/category/add', data)
}
//
修改分类接口
export function editCategory(data) {
return request.post('/admin/category/save', data)
}
52
(
2
)修改
src\components\CategoryEdit.vue
,具体代码如下。
import { getCategory, getCategoryList, uploadPictureURL
, addCategory, ed
itCategory
} from '../api'
const props = defineProps({
id: {
type: Number
}
})
const emit = defineEmits(['success'])
(
3
)修改
src\components\CategoryEdit.vue
,实现新增和编辑分类操作,具体代码如下。
//
新增操作
const addSubmit =
async
() => {
const data = {
name: form.name,
picture: form.picture,
pid: form.pid
}
if (await addCategory(data)) {
emit('success')
}
}
//
修改操作
const editSubmit =
async
() => {
if (await editCategory(form)) {
emit('success')
}
}
(
4
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页的“新增分类”按钮,
在弹窗中编写所需的分类信息,如下图所示。
53
(
5
)单击“新增”按钮后,弹窗关闭,在“分类列表”页查看新增的分类信息,如下
图所示。
(
6
)单击“分类列表”页的“新增分类”按钮,在弹窗中编写所需的二级分类信息,
如下图所示。
54
(
7
)单击“新增”按钮后,在“分类列表”页查看新增的分类,如下图所示。
(
8
)单击笔记本分类列后的“编辑”按钮,进行分类信息修改,如下图所示。
55
(
9
)单击“修改”按钮后,关闭弹窗,“分类列表”页如下图所示。
8.5.8
实现删除分类
(
1
)修改
src\api\index.js
,具体代码如下。
//
删除分类接口
export function delCategory(data) {
return request.post('/admin/category/del', data)
}
56
(
2
)修改
src\pages\subpages\Category.vue
,具体代码如下。
import { getCategoryList
, delCategory
} from '../../api'
import { ElMessageBox
, ElMessage
} from 'element-plus'
//
删除分类
const delRow = row => {
if (row.pid == 0 && row.children.length != 0) {
ElMessage({
type: 'warning',
message: '
该分类下存在二级分类,请先删除二级分类再删除此分类
'
})
} else {
ElMessageBox.confirm('
确定要删除此分类吗?
', {
closeOnClickModal: false,
confirmButtonText: '
确定
',
cancelButtonText: '
取消
',
}).then(async () => {
if (await delCategory({ id: row.id })) {
loadCategoryList()
}
}).catch(() => {})
}
}
(
3
)访问
http://127.0.0.1:5173/category
,单击“分类列表”页的中“办公用品”分类
的操作列中的“删除”按钮,如下图所示。
57
(
4
)单击“笔记本
-
纪念版”分类中操作列中的“删除”按钮,会弹出消息提示框,
如下图所示。
(
5
)单击“确定”按钮后,分类删除,“分类列表”页如下图所示。
58
(
6
)单击“办公用品”操作列中的“删除按钮”,即可对该分类进行删除。
(
7
)
单击“确定”按钮后,“分类列表”页如下图所示。
59
8.6
商品管理页
8.6.1
加载商品列表数据
(
1
)修改
src\api\index.js
,具体代码如下。
//
商品列表接口
export function getGoodsList(params) {
return request.get('/admin/goods/list', { params })
}
(
2
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<script setup>
import { ref, onMounted } from 'vue'
import { getGoodsList } from '../../api'
const goodsList = ref([])
const page = ref(1)
const pagesize = ref(10)
const total = ref(0)
onMounted(() => {
loadGoodsList()
})
const loadGoodsList = async () => {
const params = {
page: page.value,
pagesize: pagesize.value
}
const data = await getGoodsList(params)
goodsList.value = data.list.map(item => {
item.description = removeTages(item.description)
return item
})
total.value = data.total
}
60
//
去掉标签,仅显示文字
const removeTages = str => {
return str.replace(/<[^>]+>/g, '')
}
</script>
8.6.2
显示商品列表页面
(
1
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<template>
<div>
<el-button type="primary" style="margin-bottom: 10px;" @click="addRow
">
新增商品
</el-button>
<!--
商品列表
-->
<el-table :data="goodsList" style="width: 100%; margin-bottom: 20px"
row-key="id" border default-expand-all>
<el-table-column prop="id" label="
商品编号
" width="100" />
<el-table-column prop="name" label="
商品名称
" width="260" />
<el-table-column prop="price" label="
商品价格
" width="100" />
<el-table-column prop="stock" label="
商品库存
" width="100" />
<el-table-column prop="description" label="
商品简介
" />
<el-table-column prop="picture" label="
商品图片
" width="120">
<template #default="{ row }">
<el-image v-if="row.picture != ''" :src="row.picture" fit="conta
in"
style="display: flex; align-items: center; height: 60px;" />
</template>
</el-table-column>
<el-table-column fixed="right" label="
操作
" width="200">
<template #default="{ row }">
<el-button type="warning" @click="editRow(row)">
编辑
</el-button>
<el-button type="danger" @click="delRow(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
61
background layout="prev, pager, next"
:total="total"
:page-size="pagesize"
@current-change="handleCurrentChange"
style="margin-bottom: 50px;"
/>
</div>
</template>
(
2
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<script setup>
……(原有代码)
//
新增商品
const addRow = () => {
}
//
修改商品
const editRow = row => {
}
//
删除商品
const delRow = row => {
}
//
换页
const handleCurrentChange = value => {
page.value = value
loadGoodsList()
}
</script>
(
3
)访问
http://127.0.0.1:5173/goods
,“商品列表”页如下图所示。
62
8.6.3
实现弹出框
(
1
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<el-button type="primary" style="margin-bottom: 10px;" @click="addRow">
新增商品
</el-button>
<!--
新增商品的弹出框
-->
<el-dialog v-model="dialogVisible" :title="id ? '
修改商品
' : '
新增商品
'" :b
efore-close="handleBeforeClose">
<GoodsEdit :id="id" @success="editSuccess" />
</el-dialog>
(
2
)创建
src\components\GoodsEdit.vue
,具体代码如下。
<template>
GoodsEdit
</template>
<script setup>
const props = defineProps({
id: {
type: Number
}
})
const emit = defineEmits(['success'])
</script>
63
(
3
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
import { getGoodsList } from '../../api'
import GoodsEdit from '../../components/GoodsEdit.vue'
import { ElMessageBox } from 'element-plus'
const total = ref(0)
const id = ref()
const dialogVisible = ref(false)
//
新增商品
const addRow = () => {
id.value = 0
dialogVisible.value = true
}
//
修改商品
const editRow = row => {
id.value = row.id
dialogVisible.value = true
}
(
4
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<script setup>
……(原有代码)
//
编辑完成
const editSuccess = () => {
loadGoodsList()
dialogVisible.value = false
}
//
关闭弹出框前
const handleBeforeClose = () => {
ElMessageBox.confirm('
确定关闭对话框吗?
', {
showClose: false,
closeOnClickModal: false,
confirmButtonText: '
确定
',
cancelButtonText: '
取消
',
}).then(() => {
dialogVisible.value = false
}).catch(() => {})
64
}
</script>
(
5
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,页
面效果如下图所示。
(
6
)单击“ ”按钮,弹出消息提示框,如下图所示。
单击“确认”按钮后,弹出框关闭,单击“取消”按钮之后,页面效果参考步骤(
5
)
中的页面效果。
(
7
)单击“商品列表”页中操作列的“编辑”按钮,页面效果如下图所示。
8.6.4
显示商品编辑页面
(
1
)修改
src\components\GoodsEdit.vue
中模板部分的代码,具体代码如下。
<template>
<el-form ref="formRef" :model="form" label-width="120px">
<!--
商品名称
-->
<el-form-item label="
商品名称
" prop="name" style="width: 92%">
65
<el-input v-model="form.name" placeholder="
请填写商品名称
" />
</el-form-item>
<!--
商品分类
-->
<el-form-item label="
分类名称
" prop="category_id">
</el-form-item>
<!--
商品价格
-->
<el-form-item label="
商品价格
" prop="price" style="width: 92%">
<el-input v-model="form.price" placeholder="
请填写商品价格
" />
</el-form-item>
<!--
商品图片
-->
<el-form-item label="
商品图片
" prop="picture">
</el-form-item>
<!--
商品相册
-->
<el-form-item label="
图片相册
" prop="album">
</el-form-item>
<!--
商品库存
-->
<el-form-item label="
商品库存
" prop="stock" style="width: 92%">
<el-input v-model="form.stock" placeholder="
请填写库存数量
" />
</el-form-item>
<!--
商品规格
-->
<el-form-item label="
商品规格
" prop="spec" style="width: 92%">
<el-input v-model="form.spec" placeholder="
请填写商品规格
" />
</el-form-item>
<!--
商品简介
-->
<el-form-item label="
商品简介
" prop="description" style="width: 92%" c
lass="desc">
</el-form-item>
<!--
操作按钮
-->
<el-form-item>
<el-button type="primary" @click="editSubmit()" v-if="form.id">
修改
</el-button>
<el-button type="primary" @click="addSubmit()" v-else>
新增
</el-butt
on>
<el-button @click="btnCancel">
重置
</el-button>
</el-form-item>
</el-form>
</template>
(
2
)修改
src\components\GoodsEdit.vue
,具体代码如下。
66
import { reactive, ref} from 'vue'
const props = defineProps({
id: {
type: Number
}
})
const emit = defineEmits(['success'])
const form = reactive({
id: props.id,
name: '',
category_id: '',
price: '',
picture: '',
album: [],
stock: '',
spec: '',
description: ''
})
const formRef = ref()
//
新增操作
const addSubmit = () => {
}
//
修改操作
const editSubmit = () => {
}
//
重置表单
const btnCancel = () => {
formRef.value.resetFields()
}
(
3
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,页
面效果如下图所示。
67
8.6.5
显示分类名称下拉菜单
(
1
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import { reactive, ref
, onMounted
} from 'vue'
import { getCategoryList } from '../api'
const formRef = ref()
const categoryList = ref([])
onMounted(() => {
loadGoods()
})
const resetForm = id => {
form.id = id
btnCancel()
}
defineExpose({ resetForm })
const loadGoods = async () => {
const data = await getCategoryList()
68
categoryList.value = convertToTree(data)
}
const convertToTree = data => {
const treeData = []
const map = {}
for (const item of data) {
map[item.id] = { ...item, children: [] }
}
for (const item of data) {
const node = map[item.id]
if (item.pid === 0) {
treeData.push(node)
} else {
const parent = map[item.pid]
parent.children.push(node)
}
}
return treeData
}
(
2
)修改
src\components\GoodsEdit.vue
,具体代码如下。
const btnCancel = () => {
……(原有代码)
loadGoods()
}
(
3
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
<GoodsEdit
ref="goodsForm"
:id="id" @success="editSuccess"></GoodsEdit>
(
4
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
const dialogVisible = ref(false)
const goodsForm = ref()
//
新增商品
const addRow = () => {
if (goodsForm.value) {
goodsForm.value.resetForm(0)
}
……(原有代码)
}
69
//
修改商品
const editRow = row => {
if (goodsForm.value) {
goodsForm.value.resetForm(row.id)
}
……(原有代码)
}
(
5
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<!--
商品分类
-->
<el-form-item label="
分类名称
" prop="category_id">
<el-select v-model="form.category_id" placeholder="
请选择二级分类名称
" >
<el-option-group v-for="category in categoryList" :key="category.id"
:label="category.name">
<el-option v-for="item in category.children" :key="item.id" :label=
"item.name" :value="item.id" />
</el-option-group>
</el-select>
</el-form-item>
(
6
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,页
面效果如下图所示。
70
8.6.6
实现商品图片上传
(
1
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<!--
商品图片
-->
<el-form-item label="
商品图片
">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
class="upload-demo"
:action="uploadURL"
:headers="headers"
:data="{ type: 'goods_picture' }"
:limit="1"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
>
<template #trigger>
<el-button type="primary" style="text-align: left;">
选择图片
</el-but
ton>
</template>
<template #tip>
<div class="el-upload__tip">
图片文件大小不超过
500KB
</div>
</template>
</el-upload>
</el-form-item>
(
2
)修改
src\components\GoodsEdit.vue
的样式,具体代码如下。
<style scoped>
.upload-demo {
text-align: left;
width: 91%;
}
</style>
(
3
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import { getCategoryList
, uploadPictureURL
} from '../api'
import useToken from '../stores/token'
71
const categoryList = ref([])
const fileList = ref([])
const uploadRef = ref()
const uploadURL = uploadPictureURL()
const { token } = useToken()
const headers = { jwt: token }
(
4
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<script setup>
……(原有代码)
//
文件超出个数限制时替换已有图片
const handleExceed = files => {
uploadRef.value.clearFiles()
uploadRef.value.handleStart(files[0])
uploadRef.value.submit()
}
//
上传成功
const uploadSuccess = response => {
const { errno, errmsg, data } = response
if (errno !== 0) {
notification({
message: errmsg,
type: 'error'
})
} else {
if (errmsg !== '') {
notification({
message: errmsg,
type: 'success'
})
}
form.picture = data.savepath
}
}
</script>
(
5
)修改
src\components\GoodsEdit.vue
,具体代码如下。
72
const btnCancel = () => {
……(原有代码)
form.picture = ''
uploadRef.value.clearFiles()
loadGoods()
}
(
6
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,页
面效果如下图所示。
(
7
)单击“选择图片”按钮后,页面效果如下图所示。
73
8.6.7
实现商品相册
(
1
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<!--
商品相册
-->
<el-form-item label="
图片相册
" prop="album">
<el-upload
ref="albumUploadRef"
class="upload-demo"
list-type="picture-card"
v-model:file-list="albumFileList"
:action="uploadURL"
:data="{ type: 'goods_album' }"
:headers="headers"
:on-preview="handlePictureCardPreview"
:on-remove="albumHandleRemove"
:on-success="albumUploadSuccess"
:multiple="true"
>
<el-icon>
<Plus />
</el-icon>
74
</el-upload>
<el-dialog v-model="albumDialogVisible" align-center width="30%">
<el-Image :src="albumDialogImageUrl" />
</el-dialog>
</el-form-item>
(
2
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import useToken from '../stores/token'
import { Plus } from '@element-plus/icons-vue'
(
3
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<script setup>
……(原有代码)
const albumUploadRef = ref()
const albumDialogImageUrl = ref('')
const albumDialogVisible = ref(false)
const albumFileList = ref([])
//
相册图上传成功
const albumUploadSuccess = response => {
const { errno, errmsg, data } = response
if (errno !== 0) {
notification({
message: errmsg,
type: 'error'
})
} else {
if (errmsg !== '') {
notification({
message: errmsg,
type: 'success'
})
}
form.album.push(data.savepath)
}
}
//
删除相册图
const albumHandleRemove = (removeFile, uploadFiles) => {
form.album = []
75
uploadFiles.forEach(item => {
form.album.push(item.url.replace(/^https?:\/\/.*?\//, ''))
})
}
//
预览已上传的相册图
const handlePictureCardPreview = uploadFile => {
albumDialogImageUrl.value = uploadFile.url
albumDialogVisible.value = true
}
</script>
(
4
)修改
src\components\GoodsEdit.vue
,具体代码如下。
const btnCancel = () => {
……(原有代码)
form.album = []
albumUploadRef.value.clearFiles()
loadGoods()
}
(
5
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,页
面效果如下图所示。
(
6
)单击上图中红框处的“ ”按钮后,选择所需的商品图片后的页面效果如下图所
示。
76
(
7
)当鼠标指针移入商品相册中的图片是,出现遮罩,页面效果如下图所示。
(
8
)单击“ ”按钮,可以进行图片预览,如下图所示。
77
(
9
)单击“ ”按钮,删除选中的图片,删除后的页面效果参考步骤(
5
)的图。
8.6.8
实现新增和修改商品
(
1
)修改
src\api\index.js
,具体代码如下。
//
查询单个商品接口
export function getGoods(params) {
return request.get('/admin/goods', { params })
}
(
2
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import { getCategoryList, uploadPictureURL
, getGoods
} from '../api'
(
3
)修改
src\components\GoodsEdit.vue
,具体代码如下。
const loadGoods = async () => {
if (form.id) {
const goods = await getGoods({ id: form.id })
if (goods.picture !== '') {
const fileName = goods.picture.substring(goods.picture.lastIndexOf
('/') + 1)
if (fileName) {
fileList.value = [{ name: fileName, url: goods.picture }]
}
}
78
albumFileList.value = goods.album.map(item => {
return {
name: item.picture.substring(item.picture.lastIndexOf('/') + 1),
url: item.picture
}
})
goods.album = goods.album.map(item => {
return item.picture.replace(/^https?:\/\/.*?\//, '')
})
Object.assign(form, goods)
}
……(原有代码)
}
(
4
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的某个商品数据中操作列
的“编辑”按钮,页面效果如下图所示。
(
5
)修改
src\api\index.js
,具体代码如下。
//
新增商品接口
export function addGoods(data) {
return request.post('/admin/goods/add', data)
}
79
//
修改商品接口
export function editGoods(data) {
return request.post('/admin/goods/save', data)
}
(
6
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import { getCategoryList, uploadPictureURL, getGoods
, addGoods, editGood
s
} from '../api'
//
新增商品
const addSubmit =
async
() => {
const data = {
name: form.name,
category_id: form.category_id,
price: form.price,
picture: form.picture,
album: form.album,
stock: form.stock,
spec: form.spec,
description: form.description
}
if (await addGoods(data)) {
emit('success')
}
}
//
修改商品
const editSubmit =
async
() => {
if (await editGoods(form)) {
emit('success')
}
}
(
7
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮,在
弹窗中编写所需的商品信息,如下图所示。
80
(
8
)单击“新增”按钮后,弹窗关闭,在“商品列表”页查看新增的商品信息,如下
图所示。
(
9
)单击“茄子”后的“编辑”按钮,进行信息修改,如下图所示。
81
(
10
)单击“修改”按钮后,关闭弹窗,“商品列表”页如下图所示。
(
11
)防止下次打开时出现图片动画,修改
src\pages\subpages\Goods.vue
中的
handleBeforeClose()
函数中的代码,具体如下。
}).then(() => {
dialogVisible.value = false
setTimeout(() => {
82
goodsForm.value.resetForm()
}, 500)
}).catch(() => { })
读者可以进行测试,观察图片动画是否出现。
8.6.9
实现商品详情编辑器
(
1
)使用命令提示符打开
D:\vue\chapter08\shop-system
目录,安装依赖。
yarn add tinymce@6.6.2 --save
yarn add @tinymce/tinymce-vue@5.1 --save
(
2
)修改
src\components\GoodsEdit.vue
,具体代码如下。
<!--
商品简介
-->
<el-form-item label="
商品简介
" prop="description" style="width: 92%" clas
s="desc">
<Editor :init="initEditor" v-model="form.description"></Editor>
</el-form-item>
(
3
)修改
src\components\GoodsEdit.vue
,具体代码如下。
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/tinymce'
import 'tinymce/models/dom'
import 'tinymce/themes/silver'
import 'tinymce/icons/default'
import 'tinymce/plugins/image'
//
编辑器配置
let initEditor = {
width: '100%',
skin_url: '/tinymce/skins/ui/oxide',
content_css: '/tinymce/skins/content/default/content.css',
language_url: '/tinymce/langs/zh-Hans.js',
language: 'zh-Hans',
menubar: false,
statusbar: false,
toolbar: 'bold underline italic strikethrough image undo redo',
plugins: 'image',
83
}
(
4
)修改
src\style.css
,为了防止编辑器的弹出层被
Element Plus
弹出层遮挡,具体代
码如下。
.tox-tinymce-aux {
z-index: 5000!important;
}
(
5
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的“新增分类”按钮查看
商品详情编辑器,如下图所示。
8.6.10
实现商品删除
(
1
)修改
src\api\index.js
,具体代码如下。
//
删除商品接口
export function delGoods(data) {
return request.post('/admin/goods/del', data)
}
(
2
)修改
src\pages\subpages\Goods.vue
,具体代码如下。
import { getGoodsList
, delGoods
} from '../../api'
84
//
删除商品
const delRow = row => {
ElMessageBox.confirm('
确定要删除此商品吗?
', {
closeOnClickModal: false,
confirmButtonText: '
确定
',
cancelButtonText: '
取消
',
}).then(async () => {
if (await delGoods({ id: row.id })) {
loadGoodsList()
}
}).catch(() => {})
}
(
3
)访问
http://127.0.0.1:5173/goods
,单击“商品列表”页的中“茄子”操作列中的
“删除”按钮,如下图所示。
(
4
)
单击“确定”按钮后,该商品成功删除。