当前位置: 首页 > news >正文

Vue2后台管理:项目开发全流程(二)

​🌈个人主页:前端青山
🔥系列专栏:vue篇
🔖人终将被年少不可得之物困其一生

依旧青山,本期给大家带来vue篇专栏内容:Vue2后台管理:项目开发全流程(二)

目录

功能实现

8、会员用户管理

①使用数据模拟文件插入数据

②使用表格显示数据

③数据的排序、筛选和查询

④添加会员用户

⑤删除会员

⑥编辑会员信息

9、路由切换动画

统计实现

1、数据展示统计

2、统计图展示

1、折线图、柱状图

2、饼状图

3、数据标注地图

四、第三方库使用

1、数据导出

2、数据导入

3、富文本编辑器

4、markdown编辑器

五、扩展补充

1、商品管理

2、上传图片实现

3、权限判断

4、权限管理

5、共享数据存储到vuex中

6、打包上线

功能实现

8、会员用户管理

查询 get方式获取到会员用户的JSON数据 并通过UI组件库以表格方式展示,筛选、排序、表头固定。。。

添加 弹出框表单数据进行正则校验,校验通过发送请求添加会员用户

修改

删除

①使用数据模拟文件插入数据

使用fakejs编写模拟数据,插入到数据库中。

image-20230215142711128

# 执行脚本插入数据
node mock.js
②使用表格显示数据

表格组件 https://element.eleme.io/#/zh-CN/component/table

用户数据表的管理,基本数据表操作

src\views\Admin\User.vue

<template><div><!-- data 表格显示列表数据 --><el-table :data="parseList" style="width: 100%" stripe><!-- 通过index 显示行号 --><el-table-column type="index" label="序号" width="50" align="center"></el-table-column><!-- 表格的列  字段 --><!-- prop 就是数据对应的key --><!-- label 表头的文字 --><el-table-column prop="username" label="姓名" align="center"></el-table-column><el-table-column prop="sex" label="性别" align="center"></el-table-column><el-table-column prop="age" label="年龄" align="center"></el-table-column><el-table-column prop="phone" label="手机号" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button></template></el-table-column></el-table><div style="display: flex;justify-content: center;padding: 10px;"><!-- 分页按钮 --><!-- total 数据总数 --><el-pagination background layout="prev, pager, next,sizes" :total="list.length" @current-change="changePage"@size-change="handleSizeChange" :page-sizes="[2, 4, 6, 8, 10]" :page-size="pageSize"></el-pagination></div></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {return {list: [],// 本地存储当前页面  初始化从1开始currentPage: 1,// 每页显示几个pageSize: 9}},methods: {changePage(value) {// 通过点击页码按钮  将当前的页码数进行赋值修改this.currentPage = value},// 修改每页显示的条数handleSizeChange(value) {// console.log(value);this.pageSize = value}},computed: {parseList() {// start end// 第1页  0,10// 第2页  10,20// 第3页  20,30let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSize// console.log(start, end);// console.log(this.list);return this.list.slice(start, end)}},created() {req.get(url.Members).then(res => {console.log(res);if (res.data.code === 0) {this.list = res.data.data}})}
}
</script><style lang="scss" scoped></style>

翻页实现:

前端翻页:数据全部加载回来 前端切割进行分页

​ js截取数组

服务端翻页:传递页码参数 获取到不同页码的数据 sql 语句里的limit语法

​ limt(start,length)

​ select * from members limit 1,5

③数据的排序、筛选和查询

排序和筛选可以使用表格组件配置项实现 在前端进行操作

查询

前端搜索 根据关键字 遍历数据 匹配字符串

后端搜索 传递关键字给服务端接口 并将返回的数据进行渲染显示

src\views\Admin\User.vue

<template><div><div style="display: flex;justify-content: flex-end;"><div style="width: 300px;padding: 20px;display: flex;justify-content: space-around;"><el-button type="primary" @click="loadList" style="margin-right: 10px;">重置</el-button> <el-inputplaceholder="请输入手机号" v-model="phone" clearable></el-input><el-button type="primary" @click="searchPhone" style="margin-left: 10px;">搜索</el-button></div></div><!-- data 表格显示列表数据 --><el-table :data="parseList" style="width: 100%" stripe><!-- 通过index 显示行号 --><el-table-column type="index" label="序号" width="50" align="center"></el-table-column><!-- 表格的列  字段 --><!-- prop 就是数据对应的key --><!-- label 表头的文字 --><el-table-column prop="username" label="姓名" align="center"></el-table-column><!-- filters 筛选 --><el-table-column prop="sex" label="性别" align="center":filters="[{ text: '男', value: '男' }, { text: '女', value: '女' }]" :filter-method="filterSex"></el-table-column><!-- sortable排序 --><el-table-column prop="age" label="年龄" align="center" sortable></el-table-column><el-table-column prop="phone" label="手机号" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button></template></el-table-column></el-table><div style="display: flex;justify-content: center;padding: 10px;"><!-- 分页按钮 --><!-- total 数据总数 --><el-pagination background layout="prev, pager, next,sizes" :total="list.length" @current-change="changePage"@size-change="handleSizeChange" :page-sizes="[2, 4, 6, 8, 9, 10, list.length]" :page-size="pageSize"></el-pagination></div></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {return {list: [],// 本地存储当前页面  初始化从1开始currentPage: 1,// 每页显示几个pageSize: 9,// 查询手机号phone: ''}},methods: {// 加载会员列表数据loadList() {req.get(url.Members).then(res => {console.log(res);if (res.data.code === 0) {this.list = res.data.data}})},changePage(value) {// 通过点击页码按钮  将当前的页码数进行赋值修改this.currentPage = value},// 修改每页显示的条数handleSizeChange(value) {// console.log(value);this.pageSize = value},// 筛选性别filterSex(value, row, column) {// console.log(value);// console.log(row);// console.log(column);// 字段的keyconst property = column['property'];return row[property] === value// return row['sex'] === value},// 通过手机号模糊查询用户信息searchPhone() {req.get(url.Member, {params: { phone: this.phone }}).then(res => {console.log(res);if (res.data.code === 0) {this.list = res.data.data}})}},computed: {parseList: {get() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.list.slice(start, end)},// set(value) {//     console.log(value)//     return value// }}// parseList() {//     // start end//     // 第1页  0,10//     // 第2页  10,20//     // 第3页  20,30//     let start = (this.currentPage - 1) * this.pageSize//     let end = this.currentPage * this.pageSize//     // console.log(start, end);//     // console.log(this.list);//     return this.list.slice(start, end)// }},created() {this.loadList()}
}
</script><style lang="scss" scoped></style>
④添加会员用户

添加按钮点击触发抽屉弹出层

弹出层中布局会员信息表单,实现校验规则

收集表单并调用添加会员接口,添加会员数据,并返回提示

<template><div><!-- 1、搜索操作区 --><divstyle="display: flex;justify-content:space-between;background-color: #fff;margin-bottom: 20px;border-radius: 10px;"><div style="display: flex;align-items: center;"><el-button type="primary" @click="dialog = true" style="margin-left: 10px;">添加会员</el-button></div><div style="width: 300px;padding: 20px;display: flex;justify-content: space-around;"><el-button type="primary" @click="loadList" style="margin-right: 10px;">重置</el-button><el-input placeholder="请输入手机号" v-model="phone" clearable></el-input><el-button type="primary" @click="searchPhone" style="margin-left: 10px;">搜索</el-button></div></div><!-- 2、表格显示 --><div style="padding: 15px;background-color: #fff;border-radius: 10px;"><!-- data 表格显示列表数据 --><el-table :data="parseList" style="width: 100%" max-height="500" stripe><!-- 通过index 显示行号 --><el-table-column type="index" label="序号" width="50" align="center"></el-table-column><!-- 表格的列  字段 --><!-- prop 就是数据对应的key --><!-- label 表头的文字 --><el-table-column prop="username" label="姓名" align="center"></el-table-column><!-- filters 筛选 --><el-table-column prop="sex" label="性别" align="center":filters="[{ text: '男', value: '男' }, { text: '女', value: '女' }]" :filter-method="filterSex"></el-table-column><!-- sortable排序 --><el-table-column prop="age" label="年龄" align="center" sortable></el-table-column><el-table-column prop="phone" label="手机号" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button></template></el-table-column></el-table></div><!-- 3、翻页组件 --><div style="display: flex;justify-content: center;padding: 10px;background-color: #fff;"><!-- 分页按钮 --><!-- total 数据总数 --><el-pagination background layout="prev, pager, next,sizes" :total="list.length" @current-change="changePage"@size-change="handleSizeChange" :page-sizes="[2, 4, 6, 7, 8, 9, 10, list.length]" :page-size="pageSize"></el-pagination></div><!-- 抽屉表单 --><!-- before-close 关闭抽屉时触发 --><!-- visible 是否显示抽屉 --><!-- direction 弹出的位置 --><el-drawer title="添加会员" :before-close="handleClose" :visible.sync="dialog" direction="rtl" ref="drawer" size="40%"><div class="drawer__content"><!-- 会员表单 --><!-- :rules="rules" 校验规则 --><el-form :model="form" :rules="rules"><el-form-item label="姓名" prop="username" :label-width="formLabelWidth"><el-input v-model="form.username"></el-input></el-form-item><el-form-item label="性别" prop="sex" :label-width="formLabelWidth"><el-select v-model="form.sex" placeholder="请选择性别"><el-option label="男" value="男"></el-option><el-option label="女" value="女"></el-option></el-select></el-form-item><el-form-item label="年龄" prop="age" :label-width="formLabelWidth"><el-input v-model="form.age" autocomplete="off"></el-input></el-form-item><el-form-item label="手机号" prop="phone" :label-width="formLabelWidth"><el-input v-model="form.phone" autocomplete="off"></el-input></el-form-item></el-form><div class="drawer__footer"><el-button @click="cancelForm">取 消</el-button><el-button type="primary" :loading="loading" @click="save">{{ loading ? '提交中 ...' : '确 定' }}</el-button></div></div></el-drawer></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {// 检测年龄合法性var checkAge = (rule, value, callback) => {if (!value) {return callback(new Error('年龄不能为空'));}setTimeout(() => {try {value = Number(value)} catch {callback(new Error('请输入数字值'));}if (!Number.isInteger(value)) {callback(new Error('请输入数字值'));} else {if (value < 18) {callback(new Error('必须年满18岁'));} else {callback();}}}, 1000);};return {list: [],// 本地存储当前页面  初始化从1开始currentPage: 1,// 每页显示几个pageSize: 7,// 查询手机号phone: '',// 会员表单数据form: {username: '',sex: '',age: '',phone: ''},// 表单的宽度formLabelWidth: '80px',// 抽屉是否弹出dialog: false,// 表单提交加载状态loading: false,// 表单校验规则rules: {username: [{ required: true, message: '请输入会员名', trigger: 'blur' },{ min: 2, max: 6, message: '长度在 2 到 6 个字符', trigger: 'blur' }],sex: [{ required: true, message: '请选择性别', trigger: 'blur' },],age: [{ required: true, message: '请输入年龄', trigger: 'blur' },{ validator: checkAge, trigger: 'blur' }],phone: [{ required: true, message: '请输入手机号', trigger: 'blur' },{ min: 11, max: 11, message: '手机号格式错误', trigger: 'blur' }],}}},methods: {// 加载会员列表数据loadList() {req.get(url.Members).then(res => {console.log(res);if (res.data.code === 0) {this.list = res.data.data}})},changePage(value) {// 通过点击页码按钮  将当前的页码数进行赋值修改this.currentPage = value},// 修改每页显示的条数handleSizeChange(value) {// console.log(value);this.pageSize = value},// 筛选性别filterSex(value, row, column) {// console.log(value);// console.log(row);// console.log(column);// 字段的keyconst property = column['property'];return row[property] === value// return row['sex'] === value},// 通过手机号模糊查询用户信息searchPhone() {req.get(url.Member, {params: { phone: this.phone }}).then(res => {console.log(res);if (res.data.code === 0) {// 将搜索结果进行赋值this.list = res.data.data}})},// 抽屉弹出层操作方法handleClose(done) {if (this.loading) {return;}this.$confirm('确定要关闭表单吗?').then(_ => {this.dialog = false;}).catch(_ => { });},cancelForm() {this.loading = false;this.dialog = false;},// 提交表单保存数据save() {this.$confirm('确定要提交表单吗?').then(_ => {this.loading = true;// 发送请求添加会员req.post(url.Members, this.form).then(res => {// console.log(res);if (res.data.code === 0) {this.$message({message: '添加会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '添加会员失败',duration: 1000,type: 'error'})}// 关闭抽屉弹出层this.dialog = false// 发送请求调用新数据this.loadList()})// 动画关闭需要一定的时间setTimeout(() => {this.loading = false;}, 400);}).catch(_ => { });}},computed: {parseList: {get() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.list.slice(start, end)},// set(value) {//     console.log(value)//     return value// }}// parseList() {//     // start end//     // 第1页  0,10//     // 第2页  10,20//     // 第3页  20,30//     let start = (this.currentPage - 1) * this.pageSize//     let end = this.currentPage * this.pageSize//     // console.log(start, end);//     // console.log(this.list);//     return this.list.slice(start, end)// }},created() {this.loadList()}
}
</script><style lang="scss" scoped>
.drawer__content {padding: 20px;
}.drawer__footer {padding-left: 20px;
}
</style>
⑤删除会员
<template><div><!-- 1、搜索操作区 --><divstyle="display: flex;justify-content:space-between;background-color: #fff;margin-bottom: 20px;border-radius: 10px;"><div style="display: flex;align-items: center;"><el-button type="primary" @click="dialog = true" style="margin-left: 10px;">添加会员</el-button><!-- <el-button type="primary" @click="size='medium'" style="margin-left: 10px;">大表格</el-button><el-buttontype="primary" @click="size = 'small'" style="margin-left: 10px;">小表格</el-button> --></div><div style="width: 300px;padding: 20px;display: flex;justify-content: space-around;"><el-button type="primary" @click="loadList" style="margin-right: 10px;">重置</el-button><el-input placeholder="请输入手机号" v-model="phone" clearable></el-input><el-button type="primary" @click="searchPhone" style="margin-left: 10px;">搜索</el-button></div></div><!-- 2、表格显示 --><div style="padding: 15px;background-color: #fff;border-radius: 10px;"><!-- data 表格显示列表数据 --><el-table :size="size" :data="parseList" style="width: 100%" max-height="500" stripe><!-- 通过index 显示行号 --><el-table-column type="index" label="序号" width="50" align="center"></el-table-column><!-- 表格的列  字段 --><!-- prop 就是数据对应的key --><!-- label 表头的文字 --><el-table-column prop="username" label="姓名" align="center"></el-table-column><!-- filters 筛选 --><el-table-column prop="sex" label="性别" align="center":filters="[{ text: '男', value: '男' }, { text: '女', value: '女' }]" :filter-method="filterSex"></el-table-column><!-- sortable排序 --><el-table-column prop="age" label="年龄" align="center" sortable></el-table-column><el-table-column prop="phone" label="手机号" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button type="success" size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-popconfirm confirm-button-text='删除' cancel-button-text='取消' icon="el-icon-info" icon-color="red"title="确定删除吗?" @confirm="handleDelete(scope.$index, scope.row)"><!-- 注意以下按钮需要使用slot插槽 不使用插槽就不显示了 --><el-button size="mini" type="danger" slot="reference">删除</el-button></el-popconfirm></template></el-table-column></el-table></div><!-- 3、翻页组件 --><div style="display: flex;justify-content: center;padding: 10px;background-color: #fff;"><!-- 分页按钮 --><!-- total 数据总数 --><el-pagination background layout="prev, pager, next,sizes" :total="list.length" @current-change="changePage"@size-change="handleSizeChange" :page-sizes="[2, 4, 6, 7, 8, 9, 10, list.length]" :page-size="pageSize"></el-pagination></div><!-- 抽屉表单 --><!-- before-close 关闭抽屉时触发 --><!-- visible 是否显示抽屉 --><!-- direction 弹出的位置 --><el-drawer title="添加会员" :before-close="handleClose" :visible.sync="dialog" direction="rtl" ref="drawer" size="40%"><div class="drawer__content"><!-- 会员表单 --><!-- :rules="rules" 校验规则 --><el-form :model="form" :rules="rules"><el-form-item label="姓名" prop="username" :label-width="formLabelWidth"><el-input v-model="form.username"></el-input></el-form-item><el-form-item label="性别" prop="sex" :label-width="formLabelWidth"><el-select v-model="form.sex" placeholder="请选择性别"><el-option label="男" value="男"></el-option><el-option label="女" value="女"></el-option></el-select></el-form-item><el-form-item label="年龄" prop="age" :label-width="formLabelWidth"><el-input v-model="form.age" autocomplete="off"></el-input></el-form-item><el-form-item label="手机号" prop="phone" :label-width="formLabelWidth"><el-input v-model="form.phone" autocomplete="off"></el-input></el-form-item></el-form><div class="drawer__footer"><el-button @click="cancelForm">取 消</el-button><el-button type="primary" :loading="loading" @click="save">{{ loading ? '提交中 ...' : '确 定' }}</el-button></div></div></el-drawer></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {// 检测年龄合法性var checkAge = (rule, value, callback) => {if (!value) {return callback(new Error('年龄不能为空'));}setTimeout(() => {try {value = Number(value)} catch {callback(new Error('请输入数字值'));}if (!Number.isInteger(value)) {callback(new Error('请输入数字值'));} else {if (value < 18) {callback(new Error('必须年满18岁'));} else {callback();}}}, 1000);};return {list: [],// 本地存储当前页面  初始化从1开始currentPage: 1,// 每页显示几个pageSize: 7,// 查询手机号phone: '',// 会员表单数据form: {username: '',sex: '',age: '',phone: ''},// 表单的宽度formLabelWidth: '80px',// 抽屉是否弹出dialog: false,// 表单提交加载状态loading: false,// 表单校验规则rules: {username: [{ required: true, message: '请输入会员名', trigger: 'blur' },{ min: 2, max: 6, message: '长度在 2 到 6 个字符', trigger: 'blur' }],sex: [{ required: true, message: '请选择性别', trigger: 'blur' },],age: [{ required: true, message: '请输入年龄', trigger: 'blur' },{ validator: checkAge, trigger: 'blur' }],phone: [{ required: true, message: '请输入手机号', trigger: 'blur' },{ min: 11, max: 11, message: '手机号格式错误', trigger: 'blur' }],},// 表格显示大小size: 'medium'}},methods: {// 加载会员列表数据loadList() {req.get(url.Members).then(res => {console.log(res);if (res.data.code === 0) {this.list = res.data.data}})},changePage(value) {// 通过点击页码按钮  将当前的页码数进行赋值修改this.currentPage = value},// 修改每页显示的条数handleSizeChange(value) {// console.log(value);this.pageSize = value},// 筛选性别filterSex(value, row, column) {// console.log(value);// console.log(row);// console.log(column);// 字段的keyconst property = column['property'];return row[property] === value// return row['sex'] === value},// 通过手机号模糊查询用户信息searchPhone() {req.get(url.Member, {params: { phone: this.phone }}).then(res => {console.log(res);if (res.data.code === 0) {// 将搜索结果进行赋值this.list = res.data.data}})},// 抽屉弹出层操作方法handleClose(done) {if (this.loading) {return;}this.$confirm('确定要关闭表单吗?').then(_ => {this.dialog = false;}).catch(_ => { });},cancelForm() {this.loading = false;this.dialog = false;},// 提交表单保存数据save() {this.$confirm('确定要提交表单吗?').then(_ => {this.loading = true;// 发送请求添加会员req.post(url.Members, this.form).then(res => {// console.log(res);if (res.data.code === 0) {this.$message({message: '添加会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '添加会员失败',duration: 1000,type: 'error'})}// 关闭抽屉弹出层this.dialog = false// 发送请求调用新数据this.loadList()})// 动画关闭需要一定的时间setTimeout(() => {this.loading = false;}, 400);}).catch(_ => { });},// 删除会员用户handleDelete(index, row) {// console.log(index);// console.log(row);req.delete(`${url.Members}/${row.phone}`).then(res => {console.log(res);if (res.data.code === 0) {this.$message({message: '删除会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '删除会员失败',duration: 1000,type: 'error'})}this.loadList()// window.location.reload()})}},computed: {parseList: {get() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.list.slice(start, end)},// set(value) {//     console.log(value)//     return value// }}// parseList() {//     // start end//     // 第1页  0,10//     // 第2页  10,20//     // 第3页  20,30//     let start = (this.currentPage - 1) * this.pageSize//     let end = this.currentPage * this.pageSize//     // console.log(start, end);//     // console.log(this.list);//     return this.list.slice(start, end)// }},created() {this.loadList()}
}
</script><style lang="scss" scoped>
.drawer__content {padding: 20px;
}.drawer__footer {padding-left: 20px;
}
</style>
⑥编辑会员信息
<template><div><!-- 1、搜索操作区 --><divstyle="display: flex;justify-content:space-between;background-color: #fff;margin-bottom: 20px;border-radius: 10px;"><div style="display: flex;align-items: center;"><el-button type="primary" @click="handleAdd" style="margin-left: 10px;">添加会员</el-button><!-- <el-button type="primary" @click="size='medium'" style="margin-left: 10px;">大表格</el-button><el-buttontype="primary" @click="size = 'small'" style="margin-left: 10px;">小表格</el-button> --></div><div style="width: 300px;padding: 20px;display: flex;justify-content: space-around;"><el-button type="primary" @click="loadList" style="margin-right: 10px;">重置</el-button><el-input placeholder="请输入手机号" v-model="phone" clearable></el-input><el-button type="primary" @click="searchPhone" style="margin-left: 10px;">搜索</el-button></div></div><!-- 2、表格显示 --><div style="padding: 15px;background-color: #fff;border-radius: 10px;"><!-- data 表格显示列表数据 --><el-table :size="size" :data="parseList" style="width: 100%" max-height="500" stripe><!-- 通过index 显示行号 --><el-table-column type="index" label="序号" width="50" align="center"></el-table-column><!-- 表格的列  字段 --><!-- prop 就是数据对应的key --><!-- label 表头的文字 --><el-table-column prop="username" label="姓名" align="center"></el-table-column><!-- filters 筛选 --><el-table-column prop="sex" label="性别" align="center":filters="[{ text: '男', value: '男' }, { text: '女', value: '女' }]" :filter-method="filterSex"></el-table-column><!-- sortable排序 --><el-table-column prop="age" label="年龄" align="center" sortable></el-table-column><el-table-column prop="phone" label="手机号" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-popconfirm confirm-button-text='删除' cancel-button-text='取消' icon="el-icon-info" icon-color="red"title="确定删除吗?" @confirm="handleDelete(scope.$index, scope.row)"><!-- 注意以下按钮需要使用slot插槽 不使用插槽就不显示了 --><el-button type="danger" size="small" slot="reference" style="margin-left: 10px;">删除</el-button></el-popconfirm></template></el-table-column></el-table></div><!-- 3、翻页组件 --><div style="display: flex;justify-content: center;padding: 10px;background-color: #fff;"><!-- 分页按钮 --><!-- total 数据总数 --><el-pagination background layout="prev, pager, next,sizes" :total="list.length" @current-change="changePage"@size-change="handleSizeChange" :page-sizes="[2, 4, 6, 7, 8, 9, 10, list.length]" :page-size="pageSize"></el-pagination></div><!-- 抽屉表单 --><!-- before-close 关闭抽屉时触发 --><!-- visible 是否显示抽屉 --><!-- direction 弹出的位置 --><!-- <el-drawer title="添加会员" :before-close="handleClose" :visible.sync="dialog" direction="rtl" ref="drawer" size="40%"> --><el-drawer :title="direction === 'rtl' ? '添加会员' : '修改会员'" :visible.sync="dialog" :direction="direction" ref="drawer"size="40%"><div class="drawer__content"><!-- 会员表单 --><!-- :rules="rules" 校验规则 --><el-form :model="form" :rules="rules" ref="ruleForm"><el-form-item label="姓名" prop="username" :label-width="formLabelWidth"><el-input v-model="form.username"></el-input></el-form-item><el-form-item label="性别" prop="sex" :label-width="formLabelWidth"><el-select v-model="form.sex" placeholder="请选择性别"><el-option label="男" value="男"></el-option><el-option label="女" value="女"></el-option></el-select></el-form-item><el-form-item label="年龄" prop="age" :label-width="formLabelWidth"><el-input v-model="form.age" autocomplete="off"></el-input></el-form-item><el-form-item label="手机号" prop="phone" :label-width="formLabelWidth"><el-input v-model="form.phone" autocomplete="off" :disabled="direction === 'ltr'"></el-input></el-form-item></el-form><div class="drawer__footer"><el-button @click="cancelForm">取 消</el-button><el-button type="primary" :loading="loading" @click="save">{{ loading ? '提交中 ...' : '确 定' }}</el-button></div></div></el-drawer></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {// 检测年龄合法性var checkAge = (rule, value, callback) => {if (!value) {return callback(new Error('年龄不能为空'));}setTimeout(() => {try {value = Number(value)} catch {callback(new Error('请输入数字值'));}if (!Number.isInteger(value)) {callback(new Error('请输入数字值'));} else {if (value < 18) {callback(new Error('必须年满18岁'));} else {callback();}}}, 1000);};return {list: [],// 本地存储当前页面  初始化从1开始currentPage: 1,// 每页显示几个pageSize: 7,// 查询手机号phone: '',// 会员表单数据form: {username: '',sex: '',age: '',phone: ''},// 表单的宽度formLabelWidth: '80px',// 抽屉是否弹出dialog: false,// 抽屉弹出的位置direction: 'rtl',// 表单提交加载状态loading: false,// 表单校验规则rules: {username: [{ required: true, message: '请输入会员名', trigger: 'blur' },{ min: 2, max: 6, message: '长度在 2 到 6 个字符', trigger: 'blur' }],sex: [{ required: true, message: '请选择性别', trigger: 'blur' },],age: [{ required: true, message: '请输入年龄', trigger: 'blur' },{ validator: checkAge, trigger: 'blur' }],phone: [{ required: true, message: '请输入手机号', trigger: 'blur' },{ min: 11, max: 11, message: '手机号格式错误', trigger: 'blur' }],},// 表格显示大小size: 'medium'}},methods: {// 加载会员列表数据loadList() {req.get(url.Members).then(res => {// console.log(res);if (res.data.code === 0) {this.list = res.data.data}})},changePage(value) {// 通过点击页码按钮  将当前的页码数进行赋值修改this.currentPage = value},// 修改每页显示的条数handleSizeChange(value) {// console.log(value);this.pageSize = value},// 筛选性别filterSex(value, row, column) {// console.log(value);// console.log(row);// console.log(column);// 字段的keyconst property = column['property'];return row[property] === value// return row['sex'] === value},// 通过手机号模糊查询用户信息searchPhone() {req.get(url.Member, {params: { phone: this.phone }}).then(res => {console.log(res);if (res.data.code === 0) {// 将搜索结果进行赋值this.list = res.data.data}})},// 抽屉弹出层操作方法handleClose(done) {if (this.loading) {return;}this.$confirm('确定要关闭表单吗?').then(_ => {this.dialog = false;}).catch(_ => { });},cancelForm() {this.loading = false;this.dialog = false;this.loadList()},// 提交表单保存数据save() {this.$confirm('确定要提交表单吗?').then(_ => {// 表单进行校验this.$refs.ruleForm.validate((valid) => {if (valid) {this.loading = true;// 根据抽屉弹出位置 确定是修改还是添加if (this.direction === 'rtl') {// 发送请求添加会员req.post(url.Members, this.form).then(res => {// console.log(res);if (res.data.code === 0) {this.$message({message: '添加会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '添加会员失败',duration: 1000,type: 'error'})}})} else {// 修改会员信息// 发送请求添加会员req.put(url.Members, this.form).then(res => {console.log(res);if (res.data.code === 0) {this.$message({message: '修改会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '修改会员失败',duration: 1000,type: 'error'})}})}// 关闭抽屉弹出层this.dialog = falsethis.loading = false// 发送请求调用新数据this.loadList()} else {// 校验不通过console.log('error submit!!');return false;}})})},// 删除会员用户handleDelete(index, row) {// console.log(index);// console.log(row);req.delete(`${url.Members}/${row.phone}`).then(res => {console.log(res);if (res.data.code === 0) {this.$message({message: '删除会员成功',duration: 1000,type: 'success'})} else {this.$message({message: '删除会员失败',duration: 1000,type: 'error'})}this.loadList()// window.location.reload()})},// 编辑会员用户handleEdit(index, row) {this.direction = 'ltr'this.dialog = truethis.form = row},// 添加会员用户handleAdd() {this.form = {username: '',sex: '',age: '',phone: ''}this.direction = 'rtl'this.dialog = true}},computed: {parseList: {get() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.list.slice(start, end)},}},created() {this.loadList()}
}
</script><style lang="scss" scoped>
.drawer__content {padding: 20px;
}.drawer__footer {padding-left: 20px;
}
</style>

9、路由切换动画

<transition> 元素作为单个元素/组件的过渡效果。<transition> 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。

src\views\Admin\Admin.vue

<!-- 路由切换过渡动画 -->
<transition name="el-zoom-in-center"><router-view></router-view>
</transition>

统计实现

1、数据展示统计

image-20230217103817527

动画效果库

npm i animate.css

src\views\Admin\Dashboard\Dashboard.vue

<template><div><!-- <el-switch v-model="style" active-text="矩形" inactive-text="圆形"></el-switch> --><div class="container"><div :class="style ? 'card' : 'circle'" :style="{ background: item.color }" v-for="item in counts"><div>{{ item.name }}</div><div>{{ item.num }}</div></div></div></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
// 导入animate.css动画库
import 'animate.css';
export default {data() {return {counts: [],// 数据统计展示的样式style: false}},created() {this.loadCount()},methods: {// 请求获取统计数据loadCount() {req.get(url.Counts).then(res => {console.log(res);this.counts = res.data})}},
}
</script><style lang="scss" scoped>
.container {display: flex;justify-content: space-around;background-color: #fff;margin-bottom: 20px;border-radius: 10px;padding: 20px;
}.card {width: 180px;height: 60px;border-radius: 10px;border: 1px solid #ccc;display: flex;flex-direction: column;justify-content: center;align-items: center;color: white;
}.circle {width: 150px;height: 150px;border-radius: 50%;border: 1px solid #ccc;display: flex;flex-direction: column;justify-content: center;align-items: center;color: white;/* transition:width 1s;transition:height 1s; *//* & 当前选择器 */&:hover{cursor: pointer;/* width: 160px;height: 160px; *//* animation:rotateIn; */animation: heartBeat;animation-duration: 1s;}
}
</style>

2、统计图展示

将数据统计的结果使用图表展示,使其更加直观

实现折线图,柱状图,饼状图,地图标注等...

echarts实现图表统计 https://echarts.apache.org/zh/index.html

1、折线图、柱状图

①安装echarts

npm i echarts

②根据echarts示例实现

③调用数据,并替换图例的数据

④调整图例的样式,根据需求决定是否进行组件封装

src\views\Admin\Dashboard\components\LineMap.vue

<template><!-- echarts图表渲染容器 必须具有宽高 --><!-- v-if 判断是否渲染  请求数据返回后再渲染 --><div id="main" style="width: 600px;height:400px;background-color: #fff;border-radius: 10px;"></div>
</template><script>
import * as echarts from 'echarts';
import url from '@/config/url';
import req from '@/utils/request'
export default {data() {return {saleCount: [],counts: 1}},mounted() {// 发送请求获取销售量统计数据this.loadData()// 调用折线图// this.loadLineMap()// 动态调用数据渲染图表// setInterval(() => {//     this.counts++//     this.loadData()// }, 1000)},methods: {loadData() {req.get(url.SaleCount).then(res => {this.saleCount = res.data// 调用折线图// this.loadLineMap()})},loadLineMap() {// 查找渲染容器var chartDom = document.getElementById('main');// 初始化echartsvar myChart = echarts.init(chartDom);var option;// 配置项option = {// 标题title: {text: '近七日销售量趋势图'},// 图例legend: {data: ['折线图', '柱状图']},// 配置显示颜色color: ['red', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],// x轴配置xAxis: {type: 'category',data: this.xdata},yAxis: {type: 'value'},series: [{name: '折线图',// 统计显示的数据data: this.ydata,// 图表类型type: 'line',// 平滑曲线smooth: true},{name: '柱状图',data: this.ydata,type: 'bar',smooth: true},]};// 设置echarts的配置项option && myChart.setOption(option);}},computed: {xdata() {// console.log(this.saleCount.map(item => item.name));return this.saleCount.map(item => item.name)},ydata() {return this.saleCount.map(item => item.num * this.counts)}},watch: {saleCount(newValue, oldValue) {this.loadLineMap()}},
}
</script><style lang="scss" scoped></style>
2、饼状图

src\views\Admin\Dashboard\components\Pie.vue

<template><div id="pie" style="width: 400px;height:400px;background-color: #fff;border-radius: 10px;"></div>
</template><script>
import * as echarts from 'echarts';
import url from '@/config/url';
import req from '@/utils/request'
export default {data() {return {sexCount: []}},mounted() {this.loadData()},methods: {// 加载数据loadData() {req.get(url.SexCount).then(res => {this.sexCount = res.data.reverse()// 调用饼图this.loadPieMap()})},// 加载饼图loadPieMap() {var chartDom = document.getElementById('pie');var myChart = echarts.init(chartDom);var option;option = {title: {text: '用户性别分布情况',padding: [15,  // 上10, // 右5,  // 下15, // 左]},color: ['#f56c6c', '#409eff'],// legend: {//     top: 'bottom'// },// 工具箱toolbox: {show: true,feature: {mark: { show: true },// dataView: { show: true, readOnly: false },// 还原数据restore: { show: true },// 下载保存为图片saveAsImage: { show: true }}},// 显示图例的具体数据值tooltip: {trigger: 'item'},series: [{name: '用户性别',type: 'pie',// 图例最小和最大显示// radius: [40, 150],radius: ['40%', '70%'],// 图例显示的位置 横向 竖向center: ['50%', '50%'],// roseType: 'area',itemStyle: {// 图例显示的圆角// borderRadius: 8,borderRadius: 10,borderColor: '#fff',borderWidth: 2},label: {show: false,position: 'center'},emphasis: {label: {show: true,fontSize: 40,fontWeight: 'bold'}},// data: [//     { value: 40, name: 'rose 1' },//     { value: 38, name: 'rose 2' },//     { value: 32, name: 'rose 3' },//     { value: 30, name: 'rose 4' },//     { value: 28, name: 'rose 5' },//     { value: 26, name: 'rose 6' },//     { value: 22, name: 'rose 7' },//     { value: 18, name: 'rose 8' }// ]// data: [//     { value: 65, name: '女' },//     { value: 56, name: '男' }// ]data: this.sexCount}]};option && myChart.setOption(option);}},
}
</script><style lang="scss" scoped></style>
3、数据标注地图

①获取地图的经纬度范围 geo数据

http://datav.aliyun.com/portal/school/atlas/area_selector

②找到地图的示例

③根据配置项将地图的范围参数进行设置

④地图数据标注

src\views\Admin\Dashboard\components\Map.vue

<template><div id="map" style="width: 100%;height:900px;background-color: #fff;border-radius: 10px;"></div>
</template><script>
import * as echarts from 'echarts'
import axios from 'axios'
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {return {population: []}},mounted() {this.loadData()},methods: {loadData() {req.get(url.Population).then(res => {this.population = res.datathis.loadMap()})},loadMap() {var chartDom = document.getElementById('map');var myChart = echarts.init(chartDom);var option;myChart.showLoading();axios.get('http://localhost:3000/china').then(res => {myChart.hideLoading();// 注册地图geo数据 经纬度echarts.registerMap('china', res.data[0]);// 图表设置myChart.setOption((option = {// 标题title: {padding: [15,  // 上10, // 右5,  // 下15, // 左],text: '全国各地区人口普查数据(2020)',subtext: '数据来源:统计局',},tooltip: {trigger: 'item',formatter: '{b}<br/>{c}人'},toolbox: {show: true,orient: 'vertical',left: 'right',top: 'center',feature: {dataView: { readOnly: false },restore: {},saveAsImage: {}}},// 视觉映射组件visualMap: {min: 1,max: 50000000,text: ['High', 'Low'],realtime: false,calculable: true,// 颜色范围inRange: {color: ['#409eff', 'yellow', 'red']}},series: [{name: '全国地区人口',type: 'map',map: 'china',// 地图缩放比例zoom: 1.4,roam: true, //是否开启平游或缩放scaleLimit: {//滚轮缩放的极限控制min: 1,max: 10},label: {show: true},data: this.population,}]}));})option && myChart.setOption(option);}},
}
</script><style lang="scss" scoped></style>

四、第三方库使用

1、数据导出

数据较少时,可以选择在客户端浏览器导出,如果数据量较多时,建议由服务端导出,生成一个文件下载地址返回给客户端浏览器直接下载文件即可。

实际数据在哪儿就在哪儿导出

安装js-export-excel导出库

npm i js-export-excel
npm install dayjs

src\views\Admin\User.vue

<template><div><!-- 1、搜索操作区 --><divstyle="display: flex;justify-content:space-between;background-color: #fff;margin-bottom: 20px;border-radius: 10px;"><div style="display: flex;align-items: center;"><el-button type="primary" @click="handleAdd" style="margin-left: 10px;">添加会员</el-button><!--数据导出按钮 绑定导出方法 --><el-button type="primary" @click="handleExportCurrentExcel" style="margin-left: 10px;">导出</el-button></div><div style="width: 300px;padding: 20px;display: flex;justify-content: space-around;"><el-button type="primary" @click="loadList" style="margin-right: 10px;">重置</el-button><el-input placeholder="请输入手机号" v-model="phone" clearable></el-input><el-button type="primary" @click="searchPhone" style="margin-left: 10px;">搜索</el-button></div></div></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
// 导入js-export-excel
import ExportJsonExcel from 'js-export-excel'
// 引入day.js时间处理库
import dayjs from 'dayjs'
export default {data() {//................return {list: [],//........},methods: {//........// 导出会员数据为excel表格handleExportCurrentExcel() {// 表格对应的字段key 对应数据let sheetFilter = ['username', 'sex', 'age', 'phone']// 数据表配置项let option = {// 导出的excel文件名称  会员用户管理-2023-06-01-11-10-23fileName: '会员用户管理' + dayjs().format('YYYY-MM-DD-HH-mm-ss'),// 导出的数据匹配项datas: [{sheetData: this.parseList,sheetName: 'Sheet1',sheetFilter: sheetFilter,// 表头sheetHeader: ['姓名', '性别', '年龄', '手机号'],// 列宽度columnWidths: [8, 8, 8, 8, 8, 8]}]}var toExcel = new ExportJsonExcel(option) //newtoExcel.saveExcel() //保存}},computed: {parseList: {get() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.list.slice(start, end)},}},}
</script>

2、数据导入

安装xlsx excel解析库

npm i xlsx

数据导入,需要先制定一个导入的excel模板,将数据填充好,再进行上传导入

src\views\Admin\User.vue

<template><div><!-- 数据导入 --><el-button type="primary" style="margin-left: 10px;position: absolute;left:187px">上传导入</el-button><input type="file" id="file" style="margin-left: 10px;width: 100px;z-index: 99;opacity: 0;cursor: pointer;"@change="importExcel" /></div>
</template><script>//导入xlsx
import * as XLSX from 'xlsx'export default {methods: {// excel导入importExcel() {// 获取到上传的excel表  文件对应的DOM对象const file = document.getElementById('file')// console.log([file]);// console.log(file.files[0]);const reader = new FileReader()reader.readAsBinaryString(file.files[0]) // 转成 二进制格式reader.onload = () => {const workbook = XLSX.read(reader.result, { type: 'binary' })// console.log(workbook);const t = workbook.Sheets['Sheet1'] // 拿到表格数据// console.log(t)const r = XLSX.utils.sheet_to_json(t) // 转换成json格式// console.log(r)const result = r.map(item => ({ name: item['姓名'], age: item['年龄'], sex: item['性别'], phone: item['手机号'] }))console.log(result);}}},}
</script><style lang="scss" scoped></style>

3、富文本编辑器

CKEditor 5是一个超现代的JavaScript富文本编辑器

https://ckeditor.com/docs/ckeditor5/latest/installation/integrations/vuejs-v2.html#quick-start

安装

npm install --save @ckeditor/ckeditor5-vue2 @ckeditor/ckeditor5-build-classic

main.js引入注册

import Vue from 'vue';
import CKEditor from '@ckeditor/ckeditor5-vue2';Vue.use( CKEditor );

src\views\Admin\Notice.vue

<template><div><!-- 调用显示富文本编辑器 --><ckeditor :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor><div v-html="editorData"></div></div>
</template><script>
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';export default {data() {return {editor: ClassicEditor,// 用户输入的内容信息editorData: '',// 编辑器配置editorConfig: {// The configuration of the editor.}};}
}
</script>

TinyMce

TinyMCE 是一个轻量级的,基于浏览器的,所见即所得编辑器,支持目前流行的各种浏览器,由 JavaScript 写成。功能配置灵活简单(两行代码就可以将编辑器嵌入网页中),支持 AJAX。另一特点是加载速度非常快,如果你的服务器采用的脚本语言是 PHP,那还可以进一步优化。最重要的是,TinyMCE 是一个根据 LGPL license 发布的自由软件,你可以把它用于商业应用。

https://www.tiny.cloud/docs/tinymce/6/vue-cloud/

安装

npm install --save "@tinymce/tinymce-vue@^3"

notice.vue

<template><main id="sample"><Editor api-key="no-api-key" :init="{plugins: 'lists link image table code help wordcount'}" /></main>
</template><script>
import Editor from '@tinymce/tinymce-vue'
export default {components: {Editor}
}
</script><style lang="scss" scoped>
.tox.tox-tinymce {width: 80% !important;
}@media (min-width: 1024px) {#sample {display: flex;flex-direction: column;place-items: center;width: 100vw;}
}
</style>

wangEditor 5

开源 Web 富文本编辑器,开箱即用,配置简单

快速接入,配置简单,几行代码即可生成。集成了所有常见功能,无需二次开发。在 Vue React 也可以快速接入。

不依赖任何第三方框架,可用于 jQuery Vue React 等。wangEditor 提供了官方的 Vue React 组件。

安装

# 安装编辑器
npm install @wangeditor/editor --save# 安装编辑器vue的组件
npm install @wangeditor/editor-for-vue --save

使用

①创建组件

src\components\MyEditor.vue

<template><div>预览内容:<div v-html="html"></div><div style="border: 1px solid #ccc;"><Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" /><Editor style="height: 500px; overflow-y: hidden;" v-model="html" :defaultConfig="editorConfig" :mode="mode"@onCreated="onCreated" /></div></div>
</template><script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'export default Vue.extend({components: { Editor, Toolbar },data() {return {editor: null,html: '<p>hello</p>',toolbarConfig: {},editorConfig: { placeholder: '请输入内容...' },mode: 'default', // or 'simple'}},methods: {onCreated(editor) {this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错},},mounted() {// 模拟 ajax 请求,异步渲染编辑器// setTimeout(() => {//     this.html = '<p>模拟 Ajax 异步设置内容 HTML</p>'// }, 1500)},beforeDestroy() {const editor = this.editorif (editor == null) returneditor.destroy() // 组件销毁时,及时销毁编辑器}
})
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style lang="scss" scoped></style>

②在需要的地方进行引入使用

src\views\Admin\Notice.vue

<template><div><MyEditor></MyEditor></div>
</template><script>
import MyEditor from '@/components/MyEditor.vue';export default {components:{MyEditor}}
</script><style lang="scss" scoped></style>

4、markdown编辑器

v-md-editor 可以在线编辑markdown语法,并实现预览效果

http://ckang1229.gitee.io/vue-markdown-editor/zh/

安装

npm i @kangc/v-md-editor -S

main.js

import Vue from 'vue';
import VueMarkdownEditor from '@kangc/v-md-editor';
import '@kangc/v-md-editor/lib/style/base-editor.css';
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
import '@kangc/v-md-editor/lib/theme/style/vuepress.css'
VueMarkdownEditor.use(vuepressTheme);
Vue.use(VueMarkdownEditor);

src\views\Admin\Notice.vue

<template><v-md-editor v-model="text" height="400px"></v-md-editor>
</template>
<script>
export default {data() {return {text: '',};},
};
</script>
<style lang="scss" scoped></style>

五、扩展补充

1、商品管理

运营系统和管理系统中,对于一些业务管理,实际就是对应所存储的数据进行判断查询,数据添加,修改维护,删除等操作。数据筛选,搜索搜索,排大小。

商品管理中需要对应商品的图片进行管理,所以需要进行图片上传操作。其他管理和会员用户管理基本类似。

管理功能实现步骤:

①创建路由及其组件 配置菜单

②获取商品数据展示数据列表 实现对应的查询等工作

③添加商品信息

④编辑修改商品信息

⑤删除商品信息

①商品管理列表分页展示

src\views\Admin\Goods.vue

<template><div><div></div><div style="background-color: white;padding: 20px;border-radius: 10px;"><!-- 数据表格 --><el-table :data="parseGoodsList" style="width: 100%"><el-table-column prop="id" label="序号" align="center"></el-table-column><el-table-column prop="title" label="商品名称" align="center"></el-table-column><el-table-column prop="img" label="商品图片" align="center"><template slot-scope="scope"><el-image style="width: 40px; height: 40px" :src="scope.row.img" :preview-src-list="[scope.row.img]"lazy></el-image></template></el-table-column><el-table-column prop="subtitle" label="二级标题" align="center"></el-table-column><el-table-column prop="price" label="商品价格" align="center"></el-table-column><el-table-column prop="desc" label="商品描述" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-popconfirm confirm-button-text='删除' cancel-button-text='取消' icon="el-icon-info" icon-color="red"title="确定删除吗?" @confirm="handleDelete(scope.$index, scope.row)"><!-- 注意以下按钮需要使用slot插槽 不使用插槽就不显示了 --><el-button type="danger" size="small" slot="reference" style="margin-left: 10px;">删除</el-button></el-popconfirm></template></el-table-column></el-table><div style="margin-top: 20px;"><el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange":current-page="currentPage" :page-sizes="[1, 2, 4, 6, 8]" :page-size="pageSize"layout="total, sizes, prev, pager, next, jumper" :total="goodsList.length"></el-pagination></div></div></div>
</template><script>
import req from '@/utils/request'
import url from '@/config/url'
export default {data() {return {goodsList: [],// 当前页码currentPage: 1,// 每页的条数pageSize: 1}},created() {this.loadData()},methods: {// 加载商品列表数据loadData() {req.get(url.Goods).then(res => {console.log(res);this.goodsList = res.data})},// 切换当前页handleCurrentChange(value) {this.currentPage = value},// 每页显示几条切换handleSizeChange(value) {this.pageSize = value}},computed: {// 分页显示数据parseGoodsList() {let start = (this.currentPage - 1) * this.pageSizelet end = this.currentPage * this.pageSizereturn this.goodsList.slice(start, end)}},
}
</script><style lang="scss" scoped></style>

②添加商品信息

<template><div><el-button type="primary" @click="add">添加商品</el-button><!-- 添加弹出框   --><el-dialog title="添加商品" :visible.sync="dialogFormVisible"><el-form :model="form"><el-form-item label="商品名称" :label-width="formLabelWidth"><el-input v-model="form.title" autocomplete="off"></el-input></el-form-item><el-form-item label="商品预览图" :label-width="formLabelWidth"><el-input v-model="form.img" autocomplete="off"></el-input></el-form-item><el-form-item label="二级标题" :label-width="formLabelWidth"><el-input v-model="form.subtitle" autocomplete="off"></el-input></el-form-item><el-form-item label="商品价格" :label-width="formLabelWidth"><el-input v-model="form.price" autocomplete="off"></el-input></el-form-item><el-form-item label="商品描述" :label-width="formLabelWidth"><el-input v-model="form.desc" autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="handleCancel">取 消</el-button><el-button type="primary" @click="save">确 定</el-button></div></el-dialog></div>
</template><script>export default {data() {return {// 表单是否弹出dialogFormVisible: false,form: {title: '',img: '',subtitle: '',price: '',desc: '',},formLabelWidth: '120px'}},methods: {// 添加打开表单add() {// 弹出表单this.dialogFormVisible = true},// 取消按钮handleCancel() {this.dialogFormVisible = falsethis.$message({message: '已取消',duration: 700})},// 保存数据save() {// console.log(this.form);req.post(url.Goods, this.form).then(res => {if (res) {this.$message({type: 'success',message: '添加商品成功',duration: 1000,onClose: () => {this.form = {title: '',img: '',subtitle: '',price: '',desc: '',}this.dialogFormVisible = falsethis.loadData()}})}})}},}
</script><style lang="scss" scoped></style>

③编辑修改商品信息

<template><div><el-button type="primary" @click="add">添加商品</el-button><!-- 数据表格 --><el-table :data="parseGoodsList" style="width: 100%"><el-table-column prop="id" label="序号" align="center"></el-table-column><el-table-column prop="title" label="商品名称" align="center"></el-table-column><el-table-column prop="img" label="商品图片" align="center"><template slot-scope="scope"><el-image style="width: 40px; height: 40px" :src="scope.row.img" :preview-src-list="[scope.row.img]"lazy></el-image></template></el-table-column><el-table-column prop="subtitle" label="二级标题" align="center"></el-table-column><el-table-column prop="price" label="商品价格" align="center"></el-table-column><el-table-column prop="desc" label="商品描述" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-popconfirm confirm-button-text='删除' cancel-button-text='取消' icon="el-icon-info" icon-color="red"title="确定删除吗?" @confirm="handleDelete(scope.$index, scope.row)"><!-- 注意以下按钮需要使用slot插槽 不使用插槽就不显示了 --><el-button type="danger" size="small" slot="reference" style="margin-left: 10px;">删除</el-button></el-popconfirm></template></el-table-column></el-table><!-- 添加弹出框   --><el-dialog :title="formStatus ? '添加商品' : '编辑商品'" :visible.sync="dialogFormVisible"><el-form :model="form"><el-form-item label="商品名称" :label-width="formLabelWidth"><el-input v-model="form.title" autocomplete="off"></el-input></el-form-item><el-form-item label="商品预览图" :label-width="formLabelWidth"><el-input v-model="form.img" autocomplete="off"></el-input></el-form-item><el-form-item label="二级标题" :label-width="formLabelWidth"><el-input v-model="form.subtitle" autocomplete="off"></el-input></el-form-item><el-form-item label="商品价格" :label-width="formLabelWidth"><el-input v-model="form.price" autocomplete="off"></el-input></el-form-item><el-form-item label="商品描述" :label-width="formLabelWidth"><el-input v-model="form.desc" autocomplete="off"></el-input></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="handleCancel">取 消</el-button><el-button type="primary" @click="save">确 定</el-button></div></el-dialog></div>
</template><script>export default {data() {return {// 表单是否弹出dialogFormVisible: false,form: {title: '',img: '',subtitle: '',price: '',desc: '',},formLabelWidth: '120px',// 表单状态  true 添加表单  false 编辑表单formStatus: true,// 当前编辑的商品id,id: 0}},methods: {// 添加打开表单add() {// 弹出表单this.dialogFormVisible = true},// 取消按钮handleCancel() {this.dialogFormVisible = falsethis.$message({message: '已取消',duration: 700})},// 保存数据save() {// 判断是修改还是添加 formStatusif (this.formStatus) {// 添加req.post(url.Goods, this.form).then(res => {if (res) {this.$message({type: 'success',message: '添加商品成功',duration: 1000,onClose: () => {this.form = {title: '',img: '',subtitle: '',price: '',desc: '',}this.dialogFormVisible = falsethis.loadData()}})}})} else {// 修改// this.id 点击编辑时 将当前操作数据的id 作为全局数据req.put(url.Goods + '/' + this.id, this.form).then(res => {if (res) {this.$message({type: 'success',message: '修改商品成功',duration: 1000,onClose: () => {this.form = {title: '',img: '',subtitle: '',price: '',desc: '',}this.dialogFormVisible = falsethis.loadData()}})}})}// console.log(this.form);},// 编辑处理handleEdit(index, row) {// 弹出表单this.dialogFormVisible = true// 修改表单状态为编辑this.formStatus = false// 将当前编辑的数据内容赋值表单项this.form = row// 设置当前修改的idthis.id = row.id}},}
</script><style lang="scss" scoped></style>

④删除商品信息

<template><div>
<!-- 数据表格 --><el-table :data="parseGoodsList" style="width: 100%"><el-table-column prop="id" label="序号" align="center"></el-table-column><el-table-column prop="title" label="商品名称" align="center"></el-table-column><el-table-column prop="img" label="商品图片" align="center"><template slot-scope="scope"><el-image style="width: 40px; height: 40px" :src="scope.row.img" :preview-src-list="[scope.row.img]"lazy></el-image></template></el-table-column><el-table-column prop="subtitle" label="二级标题" align="center"></el-table-column><el-table-column prop="price" label="商品价格" align="center"></el-table-column><el-table-column prop="desc" label="商品描述" align="center"></el-table-column><el-table-column label="操作" align="center"><!-- 作用域插槽  子传父数据 --><template slot-scope="scope"><el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button><el-popconfirm confirm-button-text='删除' cancel-button-text='取消' icon="el-icon-info" icon-color="red"title="确定删除吗?" @confirm="handleDelete(scope.$index, scope.row)"><!-- 注意以下按钮需要使用slot插槽 不使用插槽就不显示了 --><el-button type="danger" size="small" slot="reference" style="margin-left: 10px;">删除</el-button></el-popconfirm></template></el-table-column></el-table></div>
</template><script>export default {methods: {// 删除商品handleDelete(index, row) {req.delete(url.Goods + '/' + row.id).then(res => {this.$message({message: '删除商品成功',duration: 1000,type: 'success',onClose: () => {this.loadData()}})})}},}
</script><style lang="scss" scoped></style>

2、上传图片实现

①确认服务端接口可以通过调试工具正常上传文件

②使用代码编辑上传逻辑

<template><div><!-- 图片上传开始 --><!-- action 上传地址 --><!-- header 请求头 添加token --><!-- name 服务端接口上传文件的名称 --><el-upload action="http://localhost:5000/api/v1/upload" :headers="{Authorization: token}" name="filename" list-type="picture-card" :on-preview="handlePictureCardPreview":on-remove="handleRemove" :on-success="handleSuccess"style="margin-left: 50px;margin-bottom: 10px;"><i class="el-icon-plus"></i></el-upload><el-dialog :visible.sync="dialogVisible"><img width="100%" :src="dialogImageUrl" alt=""></el-dialog><!-- 图片上传结束 --></div>
</template><script>export default {data() {return {dialogImageUrl: '',dialogVisible: false,// tokentoken: localStorage.getItem('token') ?? ''}},}
</script><style lang="scss" scoped>
methods: {handleRemove(file, fileList) {console.log(file, fileList);},handlePictureCardPreview(file) {this.dialogImageUrl = file.url;this.dialogVisible = true;},// 上传成功时触发handleSuccess(response, file, fileList) {// console.log(response,file,fileList);// 将上传成功返回的文件地址赋值给表单项this.form.img = response.data.filename}},
</style>

3、权限判断

不同用户登录系统,应该具有不同的页面或者按钮权限。有的功能可以使用,有的功能没有权限使用。

当用户登录后,将用户具有的权限进行返回。

用户根据权限,觉得是否可以操作到某个功能。

不给用户显示不具有权限功能对应菜单项,就需要根据用户的权限来显示菜单项

需要在src\views\Admin\Admin.vue中引入使用的菜单组件

<Menu></Menu>export default {components:{Menu},
}

src\views\Admin\components\Menu.vue

<template><!-- 菜单 --><!-- default-active 根据路由路径匹配 选中的对应的菜单高亮 --><el-menu router :default-active="$route.path" background-color="#001529" text-color="#ccc"><!-- index 开启router路由模式 会作为路由跳转的路径 --><el-menu-item index="/admin/dashboard"><!-- icon图标 菜单左侧 --><i class="el-icon-data-line"></i><span slot="title">控制台</span></el-menu-item><!-- 根据不同的菜单列表  显示不同的组件标签 --><el-menu-item v-for="item in parseMenuList" :index="item.path"><i :class="item.icon"></i><span slot="title">{{ item.title }}</span></el-menu-item></el-menu>
</template><script>
export default {data() {return {menuList: [{path: '/admin/user',icon: 'el-icon-user',title: '用户管理'},{path: '/admin/goods',icon: 'el-icon-goods',title: '商品管理'},{path: '/admin/notice',icon: 'el-icon-bell',title: '公告管理'}]}},computed: {parseMenuList() {// 如果管理员用户名为admin时,具有所有权限let username = localStorage.getItem('username') ?? ''console.log(username);if (username !== 'admin') {// console.log(1111);// 当管理员用户身份不是admin时,根据实际的acl权限来进行显示菜单// 当前登录用户所具有的权限let acl = JSON.parse(localStorage.getItem('acl'))let tmp = []this.menuList.forEach((item) => {console.log(item);// 判断菜单项中的每一个路径是否是用户允许访问的路径if (acl.includes(item.path)) {tmp.push(item)}})// console.log(tmp);return tmp} else {return this.menuList}}},
}
</script><style lang="scss" scoped></style>

以上操作虽然可以让用户根据菜单来访问功能,但是如果直接访问路由地址,没有进行限制情况下,还是会出现越权访问。可以通过以下两种方式,来进行路由的拦截。

方法一:路由守卫拦截 根据权限判断

src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import { Message } from 'element-ui';
import url from '@/config/url';
import req from '@/utils/request'
import Register from '../views/Register.vue'
import Login from '../views/Login.vue'
import Admin from '../views/Admin/Admin.vue'
import Dashboard from '../views/Admin/Dashboard/Dashboard.vue'
// import User from '../views/Admin/User.vue'
import Notice from '../views/Admin/Notice.vue'
import NotFound from '../views/NotFound.vue'
import Goods from '../views/Admin/Goods.vue'Vue.use(VueRouter)const routes = [{path: '/',redirect: '/login'},{path: '/register',name: 'register',component: Register,meta: {isAuth: false}},{path: '/login',name: 'login',component: Login,// 路由元信息 路由传参meta: {isAuth: false}},{path: '/admin',name: 'admin',redirect: '/admin/dashboard',// component: Admin,component: () => import('@/views/Admin/Admin.vue'),children: [{// 嵌套路由中 path 不需要写/  path: 'dashboard',name: 'dashboard',// component: Dashboard,component: () => import('@/views/Admin/Dashboard/Dashboard.vue')},{path: 'user',name: 'user',// component: Usercomponent: () => import('@/views/Admin/User.vue')},{path: 'goods',name: 'goods',// component:Goodscomponent: () => import('@/views/Admin/Goods.vue')},{path: 'notice',name: 'notice',// component:Noticecomponent: () => import('@/views/Admin/Notice.vue')}],// 路由 独享守卫beforeEnter: (to, from, next) => {//================ 根据用户权限判断是否可以跳转开始===============// console.log(to);// 如果访问的是控制台页面 直接放行if (to.path === '/admin/dashboard') {next()} else {// 在路由守卫中 判断用户跳转的路由是否具有 访问权限 如果没有,就拦截下来let acl = JSON.parse(localStorage.getItem('acl')) ?? []let username = localStorage.getItem('username') ?? ''if (username !== 'admin' && acl.length > 0 && acl.includes(to.path)) {// 具有权限列表并且将要跳转的路由path是在权限列表中的 具有权限next()} else {// 没有访问权限 跳转到没有权限页面   创建没有权限的页面 配置路由next('/notpermission')}}//================ 根据用户权限判断是否可以跳转结束===============// 如果本地存储未报错token 肯定没有登录if (!localStorage.getItem('token')) {Message({message: '未登录,请先登录',type: 'error',duration: 1000,onClose: () => {// 跳转到登录界面next('/login')}})} else {// 校验token的有效性req.get(url.Profile).then(res => {// console.log(res);if (res.data.code === 0) {// 存储管理员登录信息localStorage.setItem('username', res.data.data.username)// 存储用户具有的访问权限localStorage.setItem('acl', JSON.stringify(res.data.data.acl))next()} else {Message({message: '登录失效,重新登录',type: 'error',duration: 1000,onClose: () => {// 跳转到登录界面next('/login')}})}})}},},// 访问没有权限 跳转的页面{path: '/notpermission',component: ()=>import('@/views/NotPermission.vue')},// 404 页面匹配到跳转的页面{path: '*',component: NotFound}// {//   path: '/about',//   name: 'about',//   // route level code-splitting//   // this generates a separate chunk (about.[hash].js) for this route//   // which is lazy-loaded when the route is visited.//   component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')// }
]const router = new VueRouter({mode: 'history',base: process.env.BASE_URL,routes
})
// // 全局前置守卫
// router.beforeEach((to, from, next) => {
//   console.log(to, from);
//   // 根据路由元信息 判断哪些路由是需要校验登录
//   if (to.meta.isAuth === false) {
//     next()
//   } else {
//     // 如果本地存储未报错token 肯定没有登录
//     if (!localStorage.getItem('token')) {
//       Message({
//         message: '未登录,请先登录',
//         type: 'error',
//         duration: 1000,
//         onClose: () => {
//           // 跳转到登录界面
//           next('/login')
//         }
//       })
//     } else {
//       // 校验token的有效性
//       req.get(url.Profile).then(res => {
//         console.log(res);
//         if (res.data.code === 0) {
//           // 存储管理员登录信息
//           localStorage.setItem('username', res.data.data.username)
//         } else {
//           Message({
//             message: '登录失效,重新登录',
//             type: 'error',
//             duration: 1000,
//             onClose: () => {
//               // 跳转到登录界面
//               next('/login')
//             }
//           })
//         }
//       })
//       next()
//     }
//   }// })export default router

方法二:addRouter动态添加用户的路由

4、权限管理

①配置路由和生成页面组件

②在页面组件中显示一个管理员列表,并设置权限按钮

③点击权限按钮,弹出设置权限的选择项

④选择并提交到服务端接口

src\views\Admin\Permission.vue

<template><div><!-- 管理员列表表格 --><el-table :data="adminList" style="width: 100%"><el-table-column prop="_id" label="id" align="center"></el-table-column><el-table-column prop="username" label="管理员名称" align="center"></el-table-column><el-table-column fixed="right" label="操作" align="center"><template slot-scope="scope"><el-button type="success" size="small" @click="setPermission(scope.$index, scope.row)">权限</el-button></template></el-table-column></el-table><!-- 设置权限的弹出框 --><el-dialog title="权限设置" :visible.sync="dialogVisible" width="50%"><!-- 设置管理员权限的穿梭框 --><!-- el-transfer v-model绑定数组对应的下标代表选择已哪些权限 --><!-- data 所有权限 --><el-transfer v-model="value" :data="permissionList" style="margin-left: 40px;":titles="['全部权限', '已有权限']"></el-transfer><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="save">确 定</el-button></span></el-dialog></div>
</template>

 

5、共享数据存储到vuex中

将管理员用户登录的用户数据进行存储,username,acl权限列表

src\store\index.js

/****   组件状态共享工具*   将一些在多个组件中都使用的到数据  进行统一存储 统一的修改方式* * */
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {userInfo: JSON.parse(localStorage.getItem('userInfo')) ?? {username: '',acl: []}},getters: {},mutations: {// 设置用户权限  存储用户名和acl权限列表saveAuth(state, payload) {state.userInfo = payload// vuex数据默认存储在变量中 一刷新数据就不存在了,需要存储到浏览器本地实现永久化存储localStorage.setItem('userInfo', JSON.stringify(state.userInfo))}},actions: {},modules: {}
})

src\views\Login.vue

将数据存储到store,写vuex的数据

获取store中的数据,读vuex数据

管理员名称数据

src\views\Admin\Admin.vue

src\views\Admin\components\Menu.vue

6、打包上线

构建打包

npm run build

打包后可以使用http-server预览打包后的文件是否可以正常访问

npm i -g http-server
cd dist
http-server

上线

将打包好的dist文件夹,上传到服务器对应访问目录即可。一般由管理服务器的人员去操作。

注意:路由刷新404的问题

如果路由使用的history历史路由模式,历史路由的path路径会被认为是真实存在的服务器资源地址,实际是不存在的,就会导致服务端无法对应资源,找不到(404).

解决方案思路:

第一种方案:如果服务端无法找到对应路径资源时,直接访问index.html即可。把路由切换的权限又回到了前端页面,从而可以找到历史路由路径

第二种方案:使用hash路由

相关文章:

  • 如何给让公众号合集通过调整顺序增加文章阅读量?
  • Java---BigInteger和BigDecimal和枚举
  • JS常用HOOK脚本
  • C++中的封装,继承和多态
  • Python实现base64加密/解密
  • Vue 路由传递参数 query、params
  • Uber 提升 Presto 集群稳定性的 GC 调优方法
  • 4 最简单的 C 程序设计—顺序程序设计-4.6 顺序结构程序设计举例
  • ROS rospy和roscpp
  • Flink 命令行提交、展示和取消作业
  • Diffusers代码学习-多个ControlNet组合
  • 110.网络游戏逆向分析与漏洞攻防-装备系统数据分析-装备与技能描述信息的处理
  • 统一电荷控制模型与异质结场效应晶体管中的亚阈值电流
  • 面试题:谈谈你对乐观锁和悲观锁的理解?
  • 用链表实现的C语言队列
  • 分享一款快速APP功能测试工具
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 【MySQL经典案例分析】 Waiting for table metadata lock
  • CentOS7 安装JDK
  • CSS 提示工具(Tooltip)
  • Electron入门介绍
  • ES学习笔记(12)--Symbol
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • Java反射-动态类加载和重新加载
  • VUE es6技巧写法(持续更新中~~~)
  • 大快搜索数据爬虫技术实例安装教学篇
  • 动态规划入门(以爬楼梯为例)
  • 浮现式设计
  • 类orAPI - 收藏集 - 掘金
  • 巧用 TypeScript (一)
  • 如何设计一个比特币钱包服务
  • 入门到放弃node系列之Hello Word篇
  • 容器镜像
  • 正则表达式-基础知识Review
  • ​如何在iOS手机上查看应用日志
  • ​学习笔记——动态路由——IS-IS中间系统到中间系统(报文/TLV)​
  • #pragma data_seg 共享数据区(转)
  • (1)Hilt的基本概念和使用
  • (2024最新)CentOS 7上在线安装MySQL 5.7|喂饭级教程
  • (介绍与使用)物联网NodeMCUESP8266(ESP-12F)连接新版onenet mqtt协议实现上传数据(温湿度)和下发指令(控制LED灯)
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (转)程序员技术练级攻略
  • .NET delegate 委托 、 Event 事件,接口回调
  • .NET 的程序集加载上下文
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • .NET/C# 中设置当发生某个特定异常时进入断点(不借助 Visual Studio 的纯代码实现)
  • .Net程序帮助文档制作
  • .NET开发者必备的11款免费工具
  • .NET连接数据库方式
  • .NET正则基础之——正则委托
  • 。。。。。
  • [AIGC 大数据基础]hive浅谈
  • [AIGC] HashMap的扩容与缩容:动态调整容量以提高性能
  • [Algorithm][动态规划][两个数组的DP][正则表达式匹配][交错字符串][两个字符串的最小ASCII删除和][最长重复子数组]详细讲解