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

news2024/9/20 0:51:58

​🌈个人主页:前端青山
🔥系列专栏: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,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() {
        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-input
                    placeholder="请输入手机号" 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);
            // 字段的key
            const 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.pageSize
                let end = this.currentPage * this.pageSize
                return 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、搜索操作区 -->
        <div
            style="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);
            // 字段的key
            const 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.pageSize
                let end = this.currentPage * this.pageSize
                return 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、搜索操作区 -->
        <div
            style="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-button
                    type="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);
            // 字段的key
            const 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.pageSize
                let end = this.currentPage * this.pageSize
                return 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、搜索操作区 -->
        <div
            style="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-button
                    type="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);
            // 字段的key
            const 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 = false
                            this.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 = true
            this.form = row
        },
        // 添加会员用户
        handleAdd() {
            this.form = {
                username: '',
                sex: '',
                age: '',
                phone: ''
            }
            this.direction = 'rtl'
            this.dialog = true
        }
    },
    computed: {
        parseList: {
            get() {
                let start = (this.currentPage - 1) * this.pageSize
                let end = this.currentPage * this.pageSize
                return 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');
            // 初始化echarts
            var 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.data
                this.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、搜索操作区 -->
        <div
            style="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-23
                fileName: '会员用户管理' + 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) //new
            toExcel.saveExcel() //保存
        }
    },
   computed: {
        parseList: {
            get() {
                let start = (this.currentPage - 1) * this.pageSize
                let end = this.currentPage * this.pageSize
                return 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.editor
        if (editor == null) return
        editor.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.pageSize
            let end = this.currentPage * this.pageSize
            return 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 = false
            this.$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 = false
                            this.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 = false
            this.$message({
                message: '已取消',
                duration: 700
            })
        },
        // 保存数据
        save() {
            // 判断是修改还是添加 formStatus
            if (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 = false
                                this.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 = false
                                this.loadData()
                            }
                        })
                    }
                })
            }
            // console.log(this.form);

        },
        // 编辑处理
        handleEdit(index, row) {
            // 弹出表单
            this.dialogFormVisible = true
            // 修改表单状态为编辑
            this.formStatus = false
            // 将当前编辑的数据内容赋值表单项
            this.form = row
            // 设置当前修改的id
            this.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,
            // token
            token: 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: User
        component: () => import('@/views/Admin/User.vue')
      },
      {
        path: 'goods',
        name: 'goods',
        // component:Goods
        component: () => import('@/views/Admin/Goods.vue')
      },
      {
        path: 'notice',
        name: 'notice',
        // component:Notice
        component: () => 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路由

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1814989.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国标GB/T 28181详解:事件通知流程详细解释

目录 一、定义 二、特点和作用 1、系统事件的实时性 2、整个系统数据变化的协同性 3、智能化应用 4、可管理性 5、安全性 6、灵活和实用性 三、基本要求 1、事件订阅通知基本要求 2、关键要素 四、命令流程 1、流程图 2、流程描述 五、协议接口 六、实际应用效…

大众点评全国亲子POI采集46万家-2024年5月底

大众点评全国亲子POI采集46万家-2024年5月底 店铺POI点位示例&#xff1a; 店铺id H1IZ2B7vPGVGfbFJ 店铺名称 儿童天堂旗舰店(全国总店) 十分制服务评分 9.2 十分制环境评分 9.1 十分制划算评分 9.1 人均价格 4624 评价数量 17003 店铺地址 八一路金渝大厦1楼&#x…

流媒体传输协议HTTP-FLV、WebSocket-FLV、HTTP-TS 和 WebSocket-WS的详细介绍、应用场景及对比

一、前言 HTTP-FLV、WS-FLV、HTTP-TS 和 WS-TS 是针对 FLV 和 TS 格式视频流的不同传输方式。它们通过不同的协议实现视频流的传输&#xff0c;以满足不同的应用场景和需求。接下来我们对这些流媒体传输协议进行剖析。 二、传输协议 1、HTTP-FLV 介绍&#xff1a;基于 HTTP…

ESP RainMaker®为企业提供AIoT云解决方案,启明云端乐鑫代理商

在AIoT的浪潮中&#xff0c;企业面临着前所未有的机遇与挑战。如何快速响应市场变化&#xff0c;开发出具有竞争力的智能产品&#xff1f;如何确保数据安全&#xff0c;同时实现高效的设备管理&#xff1f;这些问题&#xff0c;ESP RainMaker给出了答案。 ESP RainMaker是一个…

鸿蒙开发:通过startAbilityByType拉起垂类应用

通过startAbilityByType拉起垂类应用 使用场景 开发者可通过特定的业务类型如导航、金融等&#xff0c;调用startAbilityByType接口拉起对应的垂域面板&#xff0c;该面板将展示目标方接入的垂域应用&#xff0c;由用户选择打开指定应用以实现相应的垂类意图。垂域面板为调用…

404 页面代码

<template> <div class"container"><h1>404</h1> <div ><p class"text-center">当前页面无法访问,可能没有权限或已删除</p><p class"text-center"> 去别处看看吧</p> </div> <…

语义分割——mmsegmentation框架使用

目录 1.mmsegmentation简介 2.mmsegmentation安装 3.mmsegmentation使用&#xff08;代码结构介绍&#xff09; 4.mmsegmentation使用实战&#xff08;deeplab v3为例&#xff09; 4.1配置 4.2训练&#xff1a; 4.3预测&#xff1a; 1.mmsegmentation简介 mmsegmentatio…

结合兴趣行业和手头资源来看计算机专业是否还是好的选择,再前行路上FlowUs息流一直陪你成长

在这个数字化时代&#xff0c;计算机专业以其广阔的就业前景和持续的行业需求&#xff0c;依旧是许多有志青年的优选。无论你是被编程的逻辑之美所吸引&#xff0c;还是对人工智能的无限可能充满好奇&#xff0c;选择计算机专业意味着你将踏上一条充满挑战与机遇的道路。而Flow…

橘子叶子病害分类数据集38432张5类别

数据集类型&#xff1a;图像分类用&#xff0c;不可用于目标检测无标注文件 数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;38432 分类类别数&#xff1a;5 类别名称:["Citrus_Canker_Diseases_L…

有人说C语言99%的代码是可以包含在c++中,那剩下的1%是什么?

关于C语言99%的代码可以包含在C中&#xff0c;这个说法基本上是正确的&#xff0c;但并非完全绝对。至于剩下的1%不兼容的部分&#xff0c;这主要涉及一些C99标准中引入的新特性&#xff0c;这些特性在C中默认是不支持的。以下是一些主要的不兼容点&#xff1a; 刚好我有一些资…

大众点评全国丽人POI采集225万家-2024年5月底

大众点评全国丽人POI采集225万家-2024年5月底 店铺POI点位示例&#xff1a; 店铺id Hav6zIYtzhyyopIZ 店铺名称 防屏蔽 十分制服务评分 8.9 十分制环境评分 8.9 十分制划算评分 8.9 人均价格 210 评价数量 19935 店铺地址 建北一支路观音桥步行街红鼎国际A座9-9 店铺…

【perfetto分析性能学习笔记】

1.perfetto网站 https://ui.perfetto.dev/ 2.快捷键 3.线程状态分析 Runnable 表示线程正在运行或者等待CPU执行 Runnable (Preempted) 表示线程正在运行&#xff0c;但在运行过程中被其他高优先级线程抢占 Running 表示线程正在运行 Uninterruptible Sleep Uninterru…

光电液位传感器可以实现哪些功能

光电液位传感器安装于机器水箱的底部时&#xff0c;它的主要功能是监控水箱内的最低水位。当水位下降至低于传感器的预设水平时&#xff0c;传感器会立即发出信号&#xff0c;提示缺水状态。这一信号可以触发相关设备停止工作&#xff0c;以避免因缺水而导致的设备损坏或故障。…

企业IT运维管理体系-总体规划

企业IT运维管理体系-总体规划 企业IT运维管理体系的总体规划通过科学的调研、分析、设计和建设&#xff0c;提升管理成熟度、增强服务能力、实现技术创新和优化资源配置。重点在于建立组织保障体系、制定运维制度、构建运维平台和完善度量指标。通过明确运维治理模式和外包管理…

开放式耳机哪个品牌比较好?五大公认品牌推荐!

想要购买开放式耳机&#xff0c;但面对众多品牌和型号&#xff0c;你是否感到无从下手&#xff1f;别担心&#xff0c;作为耳机发烧友和测评专家&#xff0c;我为大家带来了几款热门开放式耳机的横向对比。从音质、设计、功能等方面进行详细对比&#xff0c;让你一目了然地了解…

使用Vue3+ElementPlus+高德地图实现在浏览器中搜索地点并被标记在地图中

效果描述 在页面的输入框中输入想要查询的地点&#xff0c;在输入框的下方会提示跟输入的关键字有关地点&#xff0c;然后按下回车键或者选择下方罗列的地点即可让地图跳转到搜索的位置。 效果展示 页面渲染完成的时候 输入想要查询的地点 按下回车键之后 代码实现 <temp…

图像处理:Python使用OpenCV 减少图片噪音

文章目录 1. 均值滤波 (Mean Filtering)2. 高斯滤波 (Gaussian Filtering)3. 中值滤波 (Median Filtering)4.代码实现示例5.效果展示 在图像处理中&#xff0c;均值滤波、高斯滤波和中值滤波是三种常用的降噪方法。它们的实现原理各有不同&#xff1a; 1. 均值滤波 (Mean Filte…

SD文生图超详参数使用技巧和方法-看这一篇就懂了!!!

【Stable Diffusion】文生图超详参数使用技巧和方法推荐 仁者见仁&#xff0c;智者见智&#xff0c;一千个读者&#xff0c;一千个哈姆雷特。 此章为记录学习和分享&#xff0c;为后继初学者提供便利。 作者&#xff1a;AI时代社 链接&#xff1a;https://zhuanlan.zhihu.com…

hot100 -- 栈

目录 &#x1f6a9;有效的括号 &#x1f33c;最小栈 AC 栈 AC 链表 &#x1f33c;字符串解码 &#x1f43b;每日温度 &#x1f352;柱状图中的最大矩形 解释 AC 单调栈 &#x1f6a9;有效的括号 20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 1&#xf…

Seal^_^【送书活动第6期】——《Docker快速入门》

Seal^_^【送书活动第6期】——《Docker快速入门》 一、参与方式二、本期推荐图书2.1 作者荐语2.2 编辑推荐2.3 图书简介2.4 前 言2.5 目 录 三、正版购买 从Docker Desktop入手&#xff0c;快速掌握容器技术&#xff01;&#xff01;&#xff01;&#xff01; 一、参与方式 1、…