vue3微商城前台开发文档

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 [email protected] --save
yarn add [email protected] --save
yarn add @element-plus/[email protected] --save
yarn add [email protected] --save
yarn add [email protected] --save
yarn add [email protected] --save
yarn add [email protected] --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 [email protected] --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 [email protected] --save
yarn add @tinymce/[email protected] --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 )
单击"确定"按钮后,该商品成功删除。

相关推荐
清风细雨_林木木22 分钟前
Vue 2 项目中配置 Tailwind CSS 和 Font Awesome 的最佳实践
前端·css·vue.js
小宁爱Python26 分钟前
深入掌握CSS Flex布局:从原理到实战
前端·javascript·css
weifont1 小时前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情1 小时前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
几何心凉2 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程2 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
懒羊羊我小弟3 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts
运维@小兵3 小时前
vue访问后端接口,实现用户注册
前端·javascript·vue.js
雨汨3 小时前
web:InfiniteScroll 无限滚动
前端·javascript·vue.js
小盐巴小严4 小时前
正则表达式
javascript·正则表达式