​个人主页:前端青山
系列专栏: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路由

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部