项目创建流程

393 阅读7分钟

01_项目创建

npm create vue@latest

$ npm create vue@latest

Vue.js - The Progressive JavaScript Framework

√ 请输入项目名称: ... 01_vue_admin                         项目名随意, 不要出现汉字或特殊符号, 最好全字母
√ 是否使用 TypeScript 语法? ... 否 / 是                    当前项目不需要
√ 是否启用 JSX 支持? ... 否 / 是                           当前项目不需要
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是       需要
√ 是否引入 Pinia 用于状态管理? ... 否 / 是                 当前项目不需要
√ 是否引入 Vitest 用于单元测试? ... 否 / 是                当前项目不需要
√ 是否要引入一款端到端(End to End)测试工具? » 不需要         当前项目不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是                当前项目不需要

正在构建项目 C:\Users\41099\Desktop\飞秋共享\BK_2310_2\三阶段\03_周\01_vue_admin...

项目构建完成,可执行以下命令:

cd 01_vue_admin         作用: 终端地址进入到项目根目录
npm install             作用: 下载当前项目所需要的依赖
npm run dev             作用: 启动项目

02_项目目录说明

  1. .vscode
    • 当前编辑器的一些配置文件
  2. node_modules
    • 当前项目需要使用的依赖
  3. public
    • 当前项目需要使用的一些固定的静态资源(不会发生变化, 主要存放别人处理好的第三方文件)
  4. src (重要)
    • assets: 当前项目使用的一些静态资源(这里的静态资源可能会发生变化, 存放自己的静态资源文件)
    • components: 当前项目的公共组件
    • router: 路由相关内容
    • views: 页面文件
    • App.vue: 项目根组件
    • main.js: 项目入口文件
  5. .gitignore
    • git 的配置文件
  6. index.html
    • 项目的基础文件
  7. package-lock.json
    • 依赖包的详细版本与安装地址, 不重要
  8. package.json
    • 依赖包的安装版本与项目的一起启动打包等命令
  9. README.md
    • 项目的说明书
  10. vite.config.js
    • 项目的配置文件

03_项目开发流程

公司人员

  • 开发人员 (前端, 后端, ios, 安卓)
    • 开发人员配比
      • 3 个前端
      • 5~6 个后端 (有些特殊情况, 后端人员会比较少)
      • UI/交互/测试 都是一个人 (如果项目工程比较大, 测试人员可能会多两个)
      • 产品 一般都是一个人
  • 测试人员
  • 设计人员 (UI, 交互)
  • 产品经理 (主要负责想一个项目)

开发流程

  1. 产品需求碰头会
    • 此时产品已经和老板确定了要写什么项目
    • 这个会的主要目的是和开发/测试/UI 确定项目的功能
    • 参会的前端人员一般是 项目的组长+项目的开发人员
    • 特殊情况, 项目比较大的情况, 前端负责人可能也会去
  2. 测试碰头会
    • 现在项目功能已经全部确定
    • 此时测试会和开发(前端_后端)对接测试用例
    • 参会的前端人员是 项目的组长+开发人员
    • 当前会议一定要确保清楚 测试人员测什么
    • 有的时候可能 UI 也会参与测试
      • 如果 UI 比较重要, 会在 测试期 检测项目的 UI 还原度
      • 否则, 可能会等待一个随机时间, 检测随机项目
  3. 前后端接口碰头会
    • 现在功能和测试用例已经确认完毕
    • 此时 产品需求文档 和 UI 设计图基本已经设计完毕了
    • 我们需要和后端对接我们功能所需要的参数/数据
      • 将开发的时候可能会遇到对接的参数数据不够, 差一个两个
      • 此时先思考, 能不能利用已经有的数据完成
      • 如果确实完成不了, 那么再找后端 添加一个新的数据
  4. 前端组内会议
    • 一般就是开发人员比较多的时候开会
    • 主要是分配一下每个人, 负责的功能
  5. 开发期 (1 天/3 天/5 天/10 天)
    • 此时是前端后端同时开发
    • 所以前端没有真实后端接口
    • 后端没有真实前端页面
    • 所以前端此时使用的基本都是 mock 假数据
  6. 前后端接口联调 (1 天/3 天)
    • 前端后端功能已经写完了(其实完成了 90%)
    • 开始对接 前端后端接口是否有问题
    • 此时大部分问题, 可能会处在后端上
  7. 测试期
    • 此时前端功能必须 100% 完毕
    • 此时前端后端就等着 测试指定 bug, 一般会通过 禅道
    • 有些公司会在 项目完成后 进行 代码复盘/禅道问题统计
  8. 项目上线
    • 上线日期一般都是提前定好的
    • 上线的时间一般都是 22:00 以后
    • 所以前端需要留下一名开发者, 解决上线过程中可能会出现的问题

04_引入 UI 组件库

UI 组件库是面向 UI 设计师和前端开发人员使用的 提供了一些样式与一些基本的功能, 但是详细的 JS 还需要我们自己完成

  • 当前项目使用 ElementPlus
    • 官网地址: https://element-plus.gitee.io/zh-CN/
    • 如果将来项目中 vue 版本是 2 版本
    • 那么需要使用 ElementUi
      • 官网地址: https://element.eleme.io/#/zh-CN
  • ElementPlus 安装命令
    • npm install element-plus --save

用法

1. 完整引入 (不推荐, 了解即可)
  • 相当于 将 elementPlus 提供的所有组件, 全部注册成全局组件
  • 使用的时候比较方便, 但是项目书写完毕后, 打包的时候 项目的文件大小会比较大
// main.js
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";

const app = createApp(App);

app.use(ElementPlus);
app.mount("#app");
2. 按需导入 (当前项目使用的就是这个)

首先你需要安装 unplugin-vue-components 和 unplugin-auto-import 这两款插件 然后把下列代码加入到我们的项目中

Vite (我们当前项目需要使用)
// vite.config.js
import { defineConfig } from "vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";

export default defineConfig({
    // ...
    plugins: [
        // ...
        AutoImport({
            resolvers: [ElementPlusResolver()],
        }),
        Components({
            resolvers: [ElementPlusResolver()],
        }),
    ],
});
Webpack (当前项目不需要使用, 了解即可)
// webpack.config.js
const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");

module.exports = {
    // ...
    plugins: [
        AutoImport({
            resolvers: [ElementPlusResolver()],
        }),
        Components({
            resolvers: [ElementPlusResolver()],
        }),
    ],
};
3. 手动导入 (不推荐使用)
<!-- App.vue -->
<template>
    <el-button>我是 ElButton</el-button>
</template>
<script>
import { ElButton } from "element-plus";
export default {
    components: { ElButton },
};
</script>
// vite.config.js
import { defineConfig } from "vite";
import ElementPlus from "unplugin-element-plus/vite";

export default defineConfig({
    // ...
    plugins: [ElementPlus()],
});
4. 导入完毕后加入测试代码
<template>
    <h1>App.vue</h1>

    <el-row class="mb-4">
        <el-button @click="clickBtn1">Default</el-button>
        <el-button type="primary"> {{ msg }} </el-button>
        <el-button type="success">Success</el-button>
        <el-button type="info">Info</el-button>
        <el-button type="warning">Warning</el-button>
        <el-button type="danger">Danger</el-button>
    </el-row>
</template>

05_引入 CSS 格式化文件

因为我们在书写基本结构的时候, 很多的标签是具有一些默认样式的, 如果自己去除那么会很麻烦, 所以我们借助一个 第三方包

  • 安装: normalize.css
    • 安装命令 npm i normalize.css
  • 安装完毕后在 main.js 引入
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import "normalize.css/normalize.css";

const app = createApp(App);
app.use(router);
app.mount("#app");

06_引入 sass

需要安装 sass 安装命令 npm i sass

  • sass 是 css 的一个预编译语言
    • 通过 sass 能够让我们像书写 JS 一样 书写 CSS
    • 如果不使用 sass 我们也可以完成页面的样式书写, 只不过语法没有那么便捷
    • 借助 sass 我们可以更加简洁的书写 css 样式
  • 安装完毕后, 只需要在书写 css 的 style 标签中添加一个属性 'lang=scss'
    • 注意: 属性就是 'lang=scss', 不是 sass

07_完成首页静态结构搭建

1. 调整路由模式

// ...
const router = createRouter({
    // 将路由模式更改为 hash 模式
    history: createWebHashHistory(import.meta.env.BASE_URL),
    routes,
});
// ...

2. App.vue 中添加默认样式与开启路由出口

<template>
    <router-view></router-view>
</template>

<style lang="scss">
html,
body,
#app {
    width: 100%;
    height: 100%;
}
</style>

3. 从 ElementPlus 中找到一个合适的布局, 并书写一些基本的样式

<template>
    <div class="common-layout">
        <el-container>
            <el-aside width="200px">Aside</el-aside>
            <el-container>
                <el-header>Header</el-header>
                <el-main>Main</el-main>
            </el-container>
        </el-container>
    </div>
</template>

<style lang="scss" scoped>
.common-layout {
    height: 100%;
    .el-container {
        height: 100%;

        .el-aside {
            background-color: rgb(217, 236, 255);
        }

        .el-header {
            background-color: rgb(198, 226, 255);
        }
        .el-main {
            background-color: rgb(236, 245, 255);
        }
    }
}
</style>

08_展开收起侧边栏

1. 静态结构搭建

<template>
    <div class="common-layout">
        <el-container>
            <el-aside width="200px">
                <div class="logo_box">
                    <el-icon size="40">
                        <Shop />
                    </el-icon>
                    <span>商城后台管理系统</span>
                </div>
            </el-aside>
            <el-container>
                <el-header>
                    <el-icon size="45" @click="isShow = !isShow">
                        <Fold v-if="isShow" />
                        <Expand v-else />
                    </el-icon>
                </el-header>
                <el-main>Main</el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script>
import { Shop, Fold, Expand } from "@element-plus/icons-vue";
export default {
    data() {
        return {
            isShow: true,
        };
    },
    components: {
        Shop,
        Fold,
        Expand,
    },
};
</script>

<style lang="scss" scoped>
// ...
.el-aside {
    background-color: rgb(217, 236, 255);

    .logo_box {
        display: flex;
        align-items: center;
        justify-content: center;
    }
}

.el-header {
    background-color: rgb(198, 226, 255);
    display: flex;
    align-items: center;
}
// ...
</style>

2. 功能逻辑处理

<template>
    <div class="common-layout">
        <el-container>
            <!-- <el-aside width="200px"> -->
            <!-- <el-aside :width="asideWidth"> -->
            <el-aside>
                <div class="logo_box">
                    <el-icon size="40">
                        <Shop />
                    </el-icon>
                    <span v-show="isShow">商城后台管理系统</span>
                </div>
            </el-aside>
            <el-container>
                <el-header>
                    <el-icon size="45" @click="isShow = !isShow">
                        <component :is="iconName"></component>
                    </el-icon>
                </el-header>
                <el-main>Main</el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script>
import { Shop, Fold, Expand } from "@element-plus/icons-vue";
export default {
    data() {
        return {
            isShow: true,
        };
    },
    computed: {
        asideWidth() {
            return this.isShow ? "200px" : "50px";
        },
        iconName() {
            return this.isShow ? "Fold" : "Expand";
        },
    },
    components: {
        Shop,
        Fold,
        Expand,
    },
};
</script>

<style lang="scss" scoped>
.common-layout {
    height: 100%;

    .el-container {
        height: 100%;

        .el-aside {
            background-color: rgb(217, 236, 255);
            // width: 200px;
            // width: asideWidth;	// 错误写法, 因为在 css 中 属性的属性值 绝对是一个字符串
            width: v-bind(
                asideWidth
            ); // 通过 v-bind 函数, 帮我们给 width 属性绑定一个 动态的值

            .logo_box {
                display: flex;
                align-items: center;
                justify-content: center;
            }
        }

        .el-header {
            background-color: rgb(198, 226, 255);
            display: flex;
            align-items: center;
        }
        .el-main {
            background-color: rgb(236, 245, 255);
        }
    }
}
</style>

10_进入公司看项目的时候看什么

  1. 看当前项目如何发送一个请求
    • 最好的方式是查看某一个组件内, 如何发送的请求
    • 后续按照别人写好的风格, 继续完成我们的请求
  2. 分清项目的路由
    • 去查看 src 内部的 router 文件夹
    • 根据路由确定每个页面对应的 .vue 文件在哪里

11_登录功能_原生 ajax_无输入校验_无参数加密

接口文档: http://121.89.205.189:3000/admindoc/ 请求基准地址: http://121.89.205.189:3000/admin

<template>
    <div class="login_box">
        <h1>商城后台管理系统</h1>
        <p>
            账号:
            <el-input
                v-model="form.adminname"
                type="text"
                placeholder="请输入您的用户名"
            />
        </p>
        <p>
            密码:
            <el-input
                v-model="form.password"
                type="password"
                placeholder="请输入您的密码"
                show-password
            />
        </p>
        <el-button type="primary" @click="login">登录</el-button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            form: {
                adminname: "admin",
                password: "123456",
            },
        };
    },
    methods: {
        login() {
            const xhr = new XMLHttpRequest();
            xhr.open("post", "http://121.89.205.189:3000/admin/admin/login");
            xhr.setRequestHeader("content-type", "application/json");
            xhr.send(JSON.stringify(this.form));

            xhr.onload = function () {
                console.log(xhr.responseText);
            };
        },
    },
};
</script>

<style lang="scss" scoped>
.login_box {
    width: 300px;
    height: 200px;
    background-color: #dddcdc;
    border-radius: 10px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%);
    padding: 20px 30px;

    h1 {
        text-align: center;
    }

    .el-input {
        width: 85%;
    }
    .el-button {
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
    }
}
</style>

12_登录功能_axios_参数正则校验_md5 加密

<template>
    <!-- ... -->
</template>

<script>
import { ElMessage } from "element-plus";
// import md5 from "md5";
import axios from "axios";
export default {
    // ...
    methods: {
        async login() {
            // 1. 获取到用户输入的账号密码
            // console.log(this.form)

            // 2. 对账号密码添加非空校验
            if (this.form.adminname === "" || this.form.password === "") {
                return ElMessage({
                    message: "账号或密码不能为空",
                    type: "warning",
                });
            }

            // 3. 对账号密码添加正则校验
            // /^[\w-]{4,16}$/;
            // /^\d{6}$/

            // 4. 发送请求 (可以根据项目决定是否添加参数加密)
            // console.log("正常的用户名: ", this.form.adminname);
            // console.log("正常的密码: ", this.form.password);
            // console.log("加密后的用户名: ", md5(this.form.adminname));
            // console.log("加密后的密码: ", md5(this.form.password));
            const { data } = await axios.post(
                "http://121.89.205.189:3000/admin/admin/login",
                this.form
            );

            // 4.1 错误前置
            if (data.code !== "200") {
                return ElMessage({
                    type: "warning",
                    message: data.message,
                });
            }

            // 4.2 当代码运行到这个位置的时候, 说明登陆成功

            // 4.2.1 提醒用户登陆成功
            ElMessage({
                type: "success",
                message: data.message,
            });

            // 4.2.2 存储用户的一些信息 (ID, TOKEN)

            // 4.2.3 跳转到首页
            this.$router.push("/");
        },
    },
};
</script>

<style lang="scss" scoped>
// ...
</style>

13_基于 axios 的二次封装

1. src 内新建一个 utils 目录, 内部新建一个 request.js

当前文件内最后导出的 ajax 函数 是为了给 api 目录下的文件中封装的请求函数使用的 因为当前文件只会导出一个函数, 所以使用的是 export default function ajax () {...} 这样的导出将来导入的时候可以用 import XXX from 'request.js 文件的地址' src/utils/request.js

// 基于 axios 封装一个我们自己的 'ajax'

// 1. 引入 axios
import axios from "axios";

// 2. 基于 axios 创建一个我们自己的 'ajax'
const instance = axios.create({
    // 请求的基准地址
    baseURL: "http://121.89.205.189:3000/admin",
    // 请求超时时间
    timeout: 60000,
});

// 3. 基于我们自己的 'ajax', 封装一个请求函数       当前的函数是给 api 目录下某些文件中的函数使用的
export default function ajax(options) {
    /**
     *  method: 请求方式, 默认为 get
     *  url: 请求地址, 没有默认值, 必须传递
     *  data: 请求的参数, 默认为 空对象 {}
     */

    /**
     *  解构赋值的时候 也可以做默认值处理
     *  只有当我们结构出来的数据是 undefined 默认值才会生效
     */
    const { url, method = "get", data = {} } = options;

    // 判断一个字符串是不是 'get', 并且不限制大小写
    const reg = /^get$/i;

    if (reg.test(method)) {
        return instance.get(url, {
            params: data,
        });
    } else {
        return instance.post(url, data);
    }
}

2. src 内新建一个 api 目录, 内部根据页面新建一些 js 文件

文件命名如: home.js; login.js; banner.js 内部封装的请求函数是给对应的页面使用的, 如 login.js 内部只会封装登录页相关的请求函数 因为一个页面将来需要导出的函数比较多, 所以使用的是 export function 函数名 () {} 那么将来在组件中使用的时候需要 import { 函数名 } from 'api/xxx.js' src/api/login.js

// 存储登录页相关 ajax 请求     当前文件内封装的函数, 是给对应的组件使用的

import ajax from "../utils/request";

// 1. 封装并导出一个登录请求
export function loginRequest(data) {
    return ajax({
        url: "/admin/login",
        method: "post",
        // data: data
        data,
    });
}

3. 在组件中引入在 api 中封装的请求函数, 然后再合适的时机调用

src/views/loginView.vue

// ...
import { loginRequest } from "../api/login";
// ...
async login() {
    // 参数校验
    if (this.form.adminname === "" || this.form.password === "") {
        return ElMessage({
            message: "账号或密码不能为空",
            type: "warning",
        });
    }
    // 发送请求
    const { data } = await loginRequest(this.form);
    // 请求完毕后 做的事情
    if (data.code !== "200") {
        return ElMessage({
            type: "warning",
            message: data.message,
        });
    }
    ElMessage({
        type: "success",
        message: data.message,
    });
    this.$router.push("/");
},
// ...

14_登陆成功后的处理

  • 功能描述
    • 登陆成功后跳转首页时需要在页面右上角渲染用户信息
    • 但是需要根据用户是否登录决定渲染不同内容
    • 登陆后的信息凭证在登录组件, 但是需要在首页组件中使用这个信息
    • 针对这种跨组件传参最好的方式还是选择 vuex

1. 给项目添加 vuex, 在 src 内新建 store 目录, 然后创建一个 index.js 文件

import { createStore } from "vuex";

const store = createStore({
    state() {
        return {
            userInfo: {},
        };
    },
    mutations: {
        setUserInfo(state, info) {
            state.userInfo = info;
        },
    },
});

export default store;

2. 在 main.js 中引入并挂载

// ...
import store from "./store";
// ...
const app = createApp(App);
// ...
app.use(store);
// ...

3. 在登录成功时, 触发 store 中的方法, 修改 数据

async login() {
    // 参数校验...

    // 发送请求...

    // 存储用户信息
    this.$store.commit('setUserInfo', data.data)

    // 存储完毕后, 跳转到首页...
}

4. 首页中, 根据 vuex 中是否有数据, 决定渲染不同的标签

<template>
    <!-- ... -->
    <el-header>
        <el-icon size="45" @click="isShow = !isShow">
            <component :is="iconName"></component>
        </el-icon>

        <p v-if="$store.state.userInfo.adminname">
            欢迎: {{ $store.state.userInfo.adminname }}
            <el-button @click="outLogin">退出登录</el-button>
        </p>
        <el-button v-else @click="goLogin">登录</el-button>
    </el-header>
    <!-- ... -->
</template>

<script>
    export default {
        // ...
        methods: {
            // 1. 退出登录
            outLogin() {
                this.$store.commit("setUserInfo", {});
            },
            // 2. 登录
            goLogin() {
                this.$router.push("/login");
            },
        },
        // ...
    };
</script>

15_处理 vuex 无法数据持久化

  • 在 vuex 中我们的数据默认是没有持久化的, 也就是刷新页面后之前的数据就会丢失
  • 解决方案只要两个
    • 自己处理持久化
    • 借助插件实现持久化
  • 我们目前选择的方案为借助插件: vuex-persistedstate
  • 使用流程如下
// 1. 安装: npm i vuex-persistedstate
// 2. 在 vuex 中引入并配置到 plugins 中
import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";

const store = createStore({
    // ...
    plugins: [createPersistedState()],
});

16_作业渲染商品列表

1. 封装获取商品的请求

// api/home.js
export function getProList() {
    // 因为当前请求需要携带 token, 还没有学习, 所以当前写的是假数据
    return [
        // ...
    ];
}

2. 组件内使用 elementPlus 提供的组件进行渲染, 并添加分页功能

<template>
    <div class="common-layout">
        <el-container>
            {/* ... */}
            <el-container>
                {/* ... */}
                <el-main>
                    <h1>商品列表渲染区域</h1>
                    {/* 商品列表区域 */}
                    <el-table :data="proList" style="width: 100%">
                        <el-table-column
                            prop="category"
                            label="分类"
                            width="180"
                        />
                        <el-table-column
                            prop="brand"
                            label="品牌"
                            width="180"
                        />
                        <el-table-column label="商品图" width="180">
                            <template #default="scope">
                                <el-image :src="scope.row.img1" />
                            </template>
                        </el-table-column>
                        <el-table-column prop="desc" label="描述" />
                    </el-table>
                    {/* 商品列表的分页 */}
                    <el-pagination
                        layout="prev, pager, next"
                        :total="listLength"
                        :default-page-size="5"
                        v-model:current-page="current"
                    />
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

// ...
import { getProList } from "../api/home";
data() {
    return {
        // ...
        current: 1,
        listLength: 0,
    };
}
watch: {
    current: {
        handler() {
            const list = getProList();
            this.listLength = list.length;

            this.proList = list.slice(
                (this.current - 1) * 5,
                this.current * 5
            );
        },
        immediate: true
    },
}
// ...

17_添加二级路由

1. 修改路由表

// ...

// 当前数组中直接书写的对象属于一级路由
const routes = [
    {
        path: "/",
        name: "home",
        component: HomeView,
        children: [
            // 当前数组中书写的对象属于二级路由
            {
                path: "manager",
                name: "manager",
                component: () =>
                    import("../views/manager/managerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [],
            },
            {
                path: "banner",
                name: "banner",
                component: () => import("../views/banner/bannerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [],
            },
        ],
    },
    // ...
];
// ...

2. 添加二级路由对应的页面

2.1 用户管理页
<!-- src/views/manager/managerIndexView.vue -->
<template>
    <h1>用户管理页面</h1>
</template>
2.2 轮播图管理页
<!-- src/views/banner/bannerIndexView.vue -->
<template>
    <h1>轮播图管理页面</h1>
</template>

3. 添加二级路由出口

<!-- src/views/homeView.vue -->
<template>
    <div class="common-layout">
        <el-container>
            <!-- ... -->
            <el-container>
                <!-- ... -->
                <el-main>
                    <!-- 二级路由出口 -->
                    <router-view></router-view>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

18_添加三级路由

1. 修改路由表

// ...
const routes = [
    {
        path: "/",
        name: "home",
        component: HomeView,
        children: [
            // 当前数组中书写的对象属于二级路由
            {
                // 人员管理
                path: "manager",
                name: "manager",
                component: () =>
                    import("../views/manager/managerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [
                    {
                        // 用户列表
                        path: "managerlist",
                        name: "managerlist",
                        component: () =>
                            import("../views/manager/managerListView.vue"),
                    },
                    {
                        // 管理员列表
                        path: "adminlist",
                        name: "adminlist",
                        component: () =>
                            import("../views/manager/adminListView.vue"),
                    },
                ],
            },
            {
                // 轮播图管理
                path: "banner",
                name: "banner",
                component: () => import("../views/banner/bannerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [
                    {
                        // 轮播图列表
                        path: "bannerlist",
                        name: "bannerlist",
                        component: () =>
                            import("../views/banner/bannerListView.vue"),
                    },
                    {
                        // 添加轮播图
                        path: "addbanner",
                        name: "addbanner",
                        component: () =>
                            import("../views/banner/addBannerView.vue"),
                    },
                ],
            },
        ],
    },
    // ...
];
// ...

2. 添加三级路由对应的页面

2.1 人员列表页面
<!-- src/views/manager/managerListView.vue -->
<template>
    <h1>用户列表页面</h1>
</template>
2.2 管理员列表页面
<!-- src/views/manager/adminListView.vue -->
<template>
    <h1>管理员列表页面</h1>
</template>
2.3 轮播图列表页面
<!-- src/views/banner/bannerListView.vue -->
<template>
    <h1>轮播图列表页面</h1>
</template>
2.4 添加轮播图页面
<!-- src/views/banner/addbannerView.vue -->
<template>
    <h1>添加轮播图页面</h1>
</template>

3. 添加三级路由出口

2.1 人员管理页
<!-- src/views/manager/managerIndexView.vue -->
<template>
    <h1>人员管理页面</h1>

    <!-- 三级路由出口 -->
    <router-view></router-view>
</template>
2.2 轮播图管理页
<!-- src/views/banner/bannerIndexView.vue -->
<template>
    <h1>轮播图管理页面</h1>

    <!-- 三级路由出口 -->
    <router-view></router-view>
</template>

19_侧边导航跳转路由

<template>
    <div class="common-layout">
        <el-container>
            <el-aside>
                <!-- logo 标题 -->
                <div class="logo_box">
                    <el-icon size="40">
                        <Shop />
                    </el-icon>
                    <span v-show="isShow">商城后台管理系统</span>
                </div>
                <!-- 侧边导航 -->
                <el-menu
                    class="el-menu-vertical-demo"
                    router
                    :collapse="!isShow"
                    :collapse-transition="false"
                >
                    <el-menu-item index="/">
                        <el-icon><House /></el-icon>
                        <span>首页</span>
                    </el-menu-item>
                    <el-sub-menu index="/manager">
                        <template #title>
                            <el-icon><location /></el-icon>
                            <span>人员管理</span>
                        </template>
                        <el-menu-item index="/manager/managerlist"
                            >用户列表</el-menu-item
                        >
                        <el-menu-item index="/manager/adminlist"
                            >管理员列表</el-menu-item
                        >
                    </el-sub-menu>
                    <el-sub-menu index="/banner">
                        <template #title>
                            <el-icon><location /></el-icon>
                            <span>轮播图管理</span>
                        </template>
                        <el-menu-item index="/banner/bannerlist"
                            >轮播图列表</el-menu-item
                        >
                        <el-menu-item index="/banner/addbanner"
                            >添加轮播图</el-menu-item
                        >
                    </el-sub-menu>
                </el-menu>
            </el-aside>
            <!-- ... -->
        </el-container>
    </div>
</template>

20_封装侧边导航组件

<!-- src/components/menu.vue -->
<template>
    <el-menu
        class="el-menu-vertical-demo"
        router
        :collapse="!isshow"
        :collapse-transition="false"
    >
        <el-menu-item index="/">
            <el-icon><House /></el-icon>
            <span>首页</span>
        </el-menu-item>
        <el-sub-menu index="/manager">
            <template #title>
                <el-icon><location /></el-icon>
                <span>人员管理</span>
            </template>
            <el-menu-item index="/manager/managerlist">用户列表</el-menu-item>
            <el-menu-item index="/manager/adminlist">管理员列表</el-menu-item>
        </el-sub-menu>
        <el-sub-menu index="/banner">
            <template #title>
                <el-icon><location /></el-icon>
                <span>轮播图管理</span>
            </template>
            <el-menu-item index="/banner/bannerlist">轮播图列表</el-menu-item>
            <el-menu-item index="/banner/addbanner">添加轮播图</el-menu-item>
        </el-sub-menu>
    </el-menu>
</template>

<script>
    import { Location, House } from "@element-plus/icons-vue";
    export default {
        props: ["isshow"],
        components: {
            Location,
            House,
        },
    };
</script>
<!-- src/views/HomeView.vue -->
<template>
    <div class="common-layout">
        <el-container>
            <el-aside>
                <!-- ... -->

                <!-- 侧边导航 -->
                <my_menu :isshow="isShow" />
            </el-aside>
            <!-- ... -->
        </el-container>
    </div>
</template>

21_管理员列表请求_请求响应拦截器

给管理员列表发送请求获取数据的时候发现所有的接口中, 除了 登录接口全都需要添加 token 值 所以最好的方式就是利用请求拦截器添加 同时也扩展了利用响应拦截器完成了错误处理, 如果 code === '10119' 说明是 token 无效, 那么跳转登录页

1. 添加请求与响应拦截器

// src/utils/request.js

// ...

// 添加请求拦截器
instance.interceptors.request.use(
    function (config) {
        // 当前函数的第一个形参 config 是我们请求的 请求报文, 如果在 config 内添加了一些数据, 会被传递给后端

        // console.log('发送请求前, 会执行: ', config)
        if (config.url !== "/admin/login") {
            config.headers.token = store.state.userInfo.token;

            // console.log(JSON.parse(window.localStorage.getItem("vuex")).userInfo.token);
            // console.log(store.state.userInfo.token)
        }

        return config;
    },
    function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

// 添加响应拦截器
instance.interceptors.response.use(
    function (response) {
        // 2xx 范围内的状态码都会触发该函数。
        // 对响应数据做点什么

        // console.log('响应拦截器: ', response)
        if (response.data.code === "10119") {
            // console.log('此时 token 过期, 应该跳转到 登录页')
            router.push("/login");
        }

        // return response;
        // return [1, 2, 3, 4, 5];
        // return { msg: '响应拦截器' };
        return response.data;
    },
    function (error) {
        // 超出 2xx 范围的状态码都会触发该函数。
        // 对响应错误做点什么
        return Promise.reject(error);
    }
);
// ...

2. 封装管理员列表的请求函数

// api/user.js
import ajax from "../utils/request";

// 1. 管理员列表
export function getAdminList() {
    return ajax({
        url: "/admin/list",
    });
}

3. 组件内调用请求函数, 获取最新的数据

<template>
    <h1>管理员页面</h1>
</template>

<script>
    // import { getAdminList } from '../../api/user';
    import { getAdminList } from "@/api/user";

    export default {
        async mounted() {
            const res = await getAdminList();

            console.log("组件内接收到的: ", res);
        },
    };
</script>

22_管理员页面数据展示

<template>
    <h1>
        管理员页面
        <el-button>添加管理员</el-button>
    </h1>
    <el-table :data="adminList" style="width: 100%">
        <el-table-column prop="adminname" label="名字" width="180" />
        <!-- <el-table-column prop="role" label="角色" width="180" /> -->
        <el-table-column prop="role" label="角色" width="180">
            <template #default="scope">
                <el-tag v-if="scope.row.role === 1">超管</el-tag>
                <el-tag v-else type="success">管理员</el-tag>
            </template>
        </el-table-column>
        <el-table-column label="Operations">
            <template #default="scope">
                <el-button size="small">编辑</el-button>
                <el-button size="small" type="danger">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <el-pagination layout="prev, pager, next" :total="50" />
</template>

<script>
    import { getAdminList } from "@/api/user";

    export default {
        data() {
            return {
                adminList: [],
            };
        },
        async mounted() {
            const res = await getAdminList();

            this.adminList = res.data;
            console.log("组件内接收到的: ", this.adminList);
        },
    };
</script>

23_管理员页面分页功能_添加管理员静态结构处理

<template>
    <h1>
        管理员页面
        <el-button @click="dialog = true">添加管理员</el-button>
    </h1>
    <!-- 管理员列表 -->
    <el-table :data="adminListCom" style="width: 100%">
        <el-table-column type="index" width="50" />
        <el-table-column prop="adminname" label="名字" width="180" />
        <!-- <el-table-column prop="role" label="角色" width="180" /> -->
        <el-table-column prop="role" label="角色" width="180">
            <template #default="scope">
                <el-tag v-if="scope.row.role === 1">超管</el-tag>
                <el-tag v-else type="success">管理员</el-tag>
            </template>
        </el-table-column>
        <el-table-column label="Operations">
            <template #default="scope">
                <el-button size="small">编辑</el-button>
                <el-button size="small" type="danger">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- 管理员列表的分页 -->
    <el-pagination
        v-model:current-page="current"
        :page-size="15"
        layout="prev, pager, next"
        :total="adminList.length"
        hide-on-single-page
    />

    <!-- 添加管理员的抽屉组件 -->
    <el-drawer
        v-model="dialog"
        title="添加管理员"
        direction="rtl"
        class="demo-drawer"
    >
        <div class="demo-drawer__content">
            <el-form :model="form">
                <el-form-item label="用户名" label-width="80px">
                    <el-input v-model="form.adminname" />
                </el-form-item>
                <el-form-item label="密码" label-width="80px">
                    <el-input v-model="form.password" />
                </el-form-item>
                <el-form-item label="角色身份" label-width="80px">
                    <el-select v-model="form.role" placeholder="请选择角色身份">
                        <el-option label="超管" value="1" />
                        <el-option label="管理员" value="2" />
                    </el-select>
                </el-form-item>
                <el-form-item label="角色权限" label-width="80px">
                </el-form-item>
            </el-form>
            <div class="demo-drawer__footer">
                <el-button>取消</el-button>
                <el-button type="primary" :loading="loading">确定</el-button>
            </div>
        </div>
    </el-drawer>
</template>

<script>
    // import { getAdminList } from '../../api/user';
    import { getAdminList } from "@/api/user";

    export default {
        data() {
            return {
                adminList: [],
                current: 1,
                dialog: false,
                form: {
                    adminname: "",
                    password: "",
                    role: "",
                    checkedKeys: [],
                },
            };
        },
        async mounted() {
            const res = await getAdminList();

            this.adminList = res.data;
            console.log("组件内接收到的: ", this.adminList);
        },
        computed: {
            adminListCom() {
                return this.adminList.slice(
                    (this.current - 1) * 15,
                    this.current * 15
                );
            },
        },
    };
</script>

24_添加管理员(无用户权限)

<template>
    <h1>
        管理员页面
        <el-button @click="dialog = true">添加管理员</el-button>
    </h1>
    <!-- 管理员列表 -->
    <el-table :data="adminListCom" style="width: 100%">
        <el-table-column type="index" width="50" />
        <el-table-column prop="adminname" label="名字" width="180" />
        <!-- <el-table-column prop="role" label="角色" width="180" /> -->
        <el-table-column prop="role" label="角色" width="180">
            <template #default="scope">
                <el-tag v-if="scope.row.role === 1">超管</el-tag>
                <el-tag v-else type="success">管理员</el-tag>
            </template>
        </el-table-column>
        <el-table-column label="Operations">
            <template #default="scope">
                <el-button size="small">编辑</el-button>
                <el-button size="small" type="danger">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- 管理员列表的分页 -->
    <el-pagination
        v-model:current-page="current"
        :page-size="15"
        layout="prev, pager, next"
        :total="adminList.length"
        hide-on-single-page
    />

    <!-- 添加管理员的抽屉组件 -->
    <el-drawer
        v-model="dialog"
        title="添加管理员"
        direction="rtl"
        class="demo-drawer"
        @close="close"
    >
        <div class="demo-drawer__content">
            <el-form :model="form">
                <el-form-item label="用户名" label-width="80px">
                    <el-input v-model="form.adminname" />
                </el-form-item>
                <el-form-item label="密码" label-width="80px">
                    <el-input v-model="form.password" />
                </el-form-item>
                <el-form-item label="角色身份" label-width="80px">
                    <el-select v-model="form.role" placeholder="请选择角色身份">
                        <el-option label="超管" value="1" />
                        <el-option label="管理员" value="2" />
                    </el-select>
                </el-form-item>
                <el-form-item label="角色权限" label-width="80px">
                </el-form-item>
            </el-form>
            <div class="demo-drawer__footer">
                <el-button @click="close">取消</el-button>
                <el-button type="primary" @click="addUser">确定</el-button>
            </div>
        </div>
    </el-drawer>
</template>

<script>
    // import { getAdminList } from '../../api/user';
    import { getAdminList, addAdmin } from "@/api/user";
    import { ElMessage } from "element-plus";

    export default {
        data() {
            return {
                adminList: [],
                current: 1,
                dialog: false,
                form: {
                    adminname: "",
                    password: "",
                    role: "",
                    checkedKeys: [],
                },
            };
        },
        methods: {
            // 2. 抽屉组件的取消按钮
            close() {
                this.dialog = false;

                this.form = {
                    adminname: "",
                    password: "",
                    role: "",
                    checkedKeys: [],
                };
            },

            // 1. 抽屉组件 添加管理员的确定按钮
            async addUser() {
                // console.log(this.form)

                const res = await addAdmin(this.form);

                if (res.code !== "200") {
                    return ElMessage({
                        type: "warning",
                        message: res.message,
                    });
                }

                ElMessage({
                    type: "success",
                    message: res.message,
                });

                // 请求新数据
                this.adminList = (await getAdminList()).data;

                // 弹框关闭
                this.close();
            },
        },
        async mounted() {
            const res = await getAdminList();

            this.adminList = res.data;
            // console.log("组件内接收到的: ", this.adminList);
        },
        computed: {
            adminListCom() {
                return this.adminList.slice(
                    (this.current - 1) * 15,
                    this.current * 15
                );
            },
        },
    };
</script>

25_添加管理员(包含用户权限)_静态结构

1. 利用 el-tree 树形组件, 在抽屉组件中展示我们的账号权限

<template>
    <!-- ... -->

    <!-- 添加管理员的抽屉组件 -->
    <el-drawer
        v-model="dialog"
        title="添加管理员"
        direction="rtl"
        class="demo-drawer"
        @close="close"
    >
        <div class="demo-drawer__content">
            <el-form :model="form">
                <!-- ... -->
                <el-form-item label="角色权限">
                    <el-tree :data="routesList" show-checkbox />
                </el-form-item>
            </el-form>
            <!-- ... -->
        </div>
    </el-drawer>
</template>

<script>
    // ...
    import { routes } from "@/router";

    export default {
        data() {
            return {
                // ...
                routesList: routes[0].children,
            };
        },
        // ...
    };
</script>

2. 修改路由表

因为 el-tree 树形组件中必须要求传递的数据中有 label 属性和 children 属性 所以我们需要完善路由表中的数据, 给每一项添加一个 label 属性, 用于 el-tree 组件的展示文本

// ...

export const routes = [
    {
        path: "/",
        name: "home",
        component: HomeView,
        children: [
            {
                path: "manager",
                name: "manager",
                label: "人员管理",
                component: () =>
                    import("../views/manager/managerIndexView.vue"),
                children: [
                    {
                        path: "managerlist",
                        name: "managerlist",
                        label: "用户列表",
                        component: () =>
                            import("../views/manager/managerListView.vue"),
                    },
                    {
                        path: "adminlist",
                        name: "adminlist",
                        label: "管理员列表",
                        component: () =>
                            import("../views/manager/adminListView.vue"),
                    },
                ],
            },
            {
                path: "banner",
                name: "banner",
                label: "轮播图管理",
                component: () => import("../views/banner/bannerIndexView.vue"),
                children: [
                    {
                        path: "bannerlist",
                        name: "bannerlist",
                        label: "轮播图列表",
                        component: () =>
                            import("../views/banner/bannerListView.vue"),
                    },
                    {
                        path: "addbanner",
                        name: "addbanner",
                        label: "添加轮播图",
                        component: () =>
                            import("../views/banner/addBannerView.vue"),
                    },
                ],
            },
        ],
    },
    // ...
];
// ...

26_添加管理员(包含用户权限)_完整功能

// 1.5 获取角色权限
getCheckedKeys () {
    // 1.5.1 存储我们获取到的节点, 但是不符合我们的需求, 所以我们需要 进行一个改造
    const list = this.$refs.treeRef.getCheckedNodes(true)

    // 1.5.2 新建一个数组, 用于存储最终的结果
    const result = []

    // 1.5.3 新建一个数组, 用于存储选中节点的父级 label 属性, 后续可以利用 这个属性判断我们之前是否选中过这个节点或者之前有没有遇到过这个父级
    const parentLabel = []

    // 1.5.4 遍历 list 数组, 获取到每一个子节点, 然后寻找这个子节点对应的父级
    list.forEach(item => { // item 就是我们选中的每一个子节点

        // 1.5.4.1 帮助我们的子节点寻找到对应的父节点
        const parent = this.routesList.find(parent => { // 形参中的 parent 是我们的 二级路由 (人员管理和轮播图管理)

            // 只要 人员管理或者轮播图管理 中 children 属性的某一个对象的 label 属性和我们选中节点的 label 属性 相同
            // 就一定能够证明, 当前的 形参 parent 是我们选中的节点的父级
            return parent.children.some(child => {
                return child.label === item.label
            })
        })

        // 利用 includes 方法帮我们去查找 parentLabel 数组中有没有 parent.label 这个数据, 如果没有证明此时是第一次遇到这个父节点
        // 第一次遇到的时候将我们的数据筛选一下添加到 result 中, 并且将 parent.label 添加到 parentLabel 中, 这样能够在下一次遇到时证明之前已经遇到过这个对象, 处理过这个父节点
        // 如果之前遇到过这个父节点, 那么会执行 else 分支, 我们只需要找到这个父节点, 然后给他的 children 中 push 一个新的子节点即可
        if (!parentLabel.includes(parent.label)) {
            // 第一次遇到这个数据
            parentLabel.push(parent.label)

            result.push({
                label: parent.label,
                name: parent.name,
                children: [item]
            })

        } else {
            // 之前遇到过这个数据
            const parentInfo = result.find(t => t.label === parent.label)
            parentInfo.children.push(item)
        }
    })


    // 1.5.5 将我们处理好的数据, 赋值给 我们 form 对象
    this.form.checkedKeys = result

},

// 1. 抽屉组件 添加管理员的确定按钮
async addUser() {
    // 当前函数调用完毕, 已经会将选中项设置给我们的 this.form.checkedKeys 属性
    this.getCheckedKeys()

    const res = await addAdmin(this.form);
    if (res.code !== "200") {
        return ElMessage({
            type: "warning",
            message: res.message,
        });
    }
    ElMessage({
        type: "success",
        message: res.message,
    });
    // 请求新数据
    this.adminList = (await getAdminList()).data;
    // 弹框关闭
    this.close();
},

27_清空角色权限的选中状态_编辑管理员静态结构

1. 清空角色权限的选中状态

<!-- el-tree 必须绑定 node-key 属性 才能完成清空角色权限的选中 -->
<el-tree ref="treeRef" :data="routesList" show-checkbox node-key="label" />

<script>
    close() {
        // ...

        // 2.3 清空角色权限的选中
        this.$refs.treeRef.setCheckedNodes([]);

        // ...
    }
</script>

2. 编辑管理员静态结构

<template>
    <!-- ... -->
    <el-table :data="adminListCom" style="width: 100%">
        <!-- ... -->
        <el-table-column label="Operations">
            <template #default="scope">
                <el-button size="small" @click="editAdmin">编辑</el-button>
                <el-button size="small" type="danger">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- ... -->

    <!-- 添加管理员的抽屉组件 -->
    <el-drawer
        v-model="dialog"
        :title="addOrEdit ? '添加管理员' : '编辑管理员'"
        direction="rtl"
        class="demo-drawer"
        @close="close"
    >
        <div class="demo-drawer__content">
            <!-- ... -->
            <div class="demo-drawer__footer">
                <el-button @click="close">取消</el-button>
                <el-button v-if="addOrEdit" type="primary" @click="addUser"
                    >确定添加</el-button
                >
                <el-button v-else type="primary">确定编辑</el-button>
            </div>
        </div>
    </el-drawer>
</template>

<script>
    // ...

    export default {
        data() {
            return {
                // ...
                /**
                 *  当前变量决定抽屉组件内展示的时 添加还是编辑相关的内容
                 *      默认为 true, 展示添加相关的内容
                 *      如果为 false, 展示编辑相关的内容
                 */
                addOrEdit: true,
            };
        },
        methods: {
            // 3. 点击编辑按钮
            editAdmin() {
                // 控制抽屉组件内展示 编辑相关的内容
                this.addOrEdit = false;
                // 打开抽屉组件
                this.dialog = true;
            },

            // 2. 抽屉组件的取消按钮
            close() {
                // ...

                // 2.4 还原 添加管理员的变量
                this.addOrEdit = true;
            },
            // ...
        },
        // ...
    };
</script>

28_编辑管理员

<template>
    <h1>
        管理员页面
        <el-button @click="dialog = true">添加管理员</el-button>
    </h1>
    <!-- 管理员列表 -->
    <el-table :data="adminListCom" style="width: 100%">
        <el-table-column type="index" width="50" />
        <el-table-column prop="adminname" label="名字" width="180" />
        <!-- <el-table-column prop="role" label="角色" width="180" /> -->
        <el-table-column prop="role" label="角色" width="180">
            <template #default="scope">
                <el-tag v-if="scope.row.role === 1">超管</el-tag>
                <el-tag v-else type="success">管理员</el-tag>
            </template>
        </el-table-column>
        <el-table-column label="Operations">
            <template #default="scope">
                <el-button size="small" @click="editAdmin(scope.row)"
                    >编辑</el-button
                >
                <el-button size="small" type="danger">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- 管理员列表的分页 -->
    <el-pagination
        v-model:current-page="current"
        :page-size="15"
        layout="prev, pager, next"
        :total="adminList.length"
        hide-on-single-page
    />

    <!-- 添加管理员的抽屉组件 -->
    <el-drawer
        v-model="dialog"
        :title="addOrEdit ? '添加管理员' : '编辑管理员'"
        direction="rtl"
        class="demo-drawer"
        @close="close"
    >
        <div class="demo-drawer__content">
            <el-form :model="form">
                <el-form-item
                    v-if="addOrEdit"
                    label="用户名"
                    label-width="80px"
                >
                    <el-input v-model="form.adminname" />
                </el-form-item>
                <el-form-item v-if="addOrEdit" label="密码" label-width="80px">
                    <el-input v-model="form.password" />
                </el-form-item>
                <el-form-item label="角色身份" label-width="80px">
                    <el-select v-model="form.role" placeholder="请选择角色身份">
                        <el-option label="超管" :value="1" />
                        <el-option label="管理员" :value="2" />
                    </el-select>
                </el-form-item>
                <el-form-item label="角色权限">
                    <el-tree
                        ref="treeRef"
                        :data="routesList"
                        show-checkbox
                        node-key="label"
                        :default-checked-keys="defaultCheckedKeys"
                    />
                </el-form-item>
            </el-form>
            <div class="demo-drawer__footer">
                <el-button @click="close">取消</el-button>
                <el-button v-if="addOrEdit" type="primary" @click="addUser"
                    >确定添加</el-button
                >
                <el-button v-else type="primary" @click="editUser"
                    >确定编辑</el-button
                >
            </div>
        </div>
    </el-drawer>
</template>

<script>
    // ...

    export default {
        data() {
            return {
                // ...
                defaultCheckedKeys: [],
            };
        },
        methods: {
            // 4. 确定编辑按钮, 内部收集数据, 发送请求
            async editUser() {
                this.getCheckedKeys();
                const res = await adminUpdate(this.form);
                if (res.code !== "200") {
                    return ElMessage({
                        type: "warning",
                        message: res.message,
                    });
                }
                ElMessage({
                    type: "success",
                    message: res.message,
                });
                // 请求最新的数据
                this.adminList = (await getAdminList()).data;
                // 关闭抽屉组件
                this.close();
            },

            // 3. 点击编辑按钮
            editAdmin(data) {
                // 控制抽屉组件内展示 编辑相关的内容
                this.addOrEdit = false;
                // 打开抽屉组件
                this.dialog = true;

                this.form.adminname = data.adminname;
                this.form.role = data.role;

                const res = [];
                data.checkedKeys.forEach((item) => {
                    item.children.forEach((child) => {
                        res.push(child.label);
                    });
                });
                this.defaultCheckedKeys = res;
            },
            // ...
        },
        // ...
    };
</script>

29_删除管理员

<template>
    <!-- ... -->
    <el-table :data="adminListCom" style="width: 100%">
        <!-- ... -->
        <el-table-column label="Operations">
            <template #default="scope">
                <!-- ... -->
                <el-button
                    size="small"
                    type="danger"
                    @click="delAdmin(scope.row.adminid)"
                    >删除</el-button
                >
            </template>
        </el-table-column>
    </el-table>

    <!-- ... -->

    <!-- 删除的确认弹框 -->
    <el-dialog v-model="dialogVisible" title="删除确认弹框" width="30%">
        <h1>您确定删除吗?</h1>
        <template #footer>
            <span class="dialog-footer">
                <el-button @click="dialogVisible = false">取消</el-button>
                <el-button type="primary" @click="del"> 确定 </el-button>
            </span>
        </template>
    </el-dialog>
</template>

<script>
    // ...
    export default {
        data() {
            return {
                // ...
                defaultCheckedKeys: [],
                dialogVisible: false,
                delId: -1,
            };
        },
        methods: {
            // 6. 点击删除确认弹框的确定按钮
            async del() {
                // console.log("删除一个用户");
                const res = await adminDelete({
                    adminid: this.delId,
                });

                this.dialogVisible = false;

                if (res.code !== "200") {
                    return ElMessage({
                        type: "warning",
                        message: res.message,
                    });
                }

                ElMessage({
                    type: "success",
                    message: res.message,
                });

                // 请求最新的数据
                this.adminList = (await getAdminList()).data;
            },

            // 5. 点击删除按钮
            delAdmin(id) {
                // 存储要背删除的 ID
                this.delId = id;

                // 打开删除确认弹框
                this.dialogVisible = true;
            },
            // ...
        },
        // ...
    };
</script>

30_添加轮播图

<template>
    <h1>添加轮播图页面</h1>
    <el-form :model="form" label-width="120px">
        <el-form-item label="图片链接信息" label-width="120px">
            <el-input v-model="form.link" />
        </el-form-item>

        <el-form-item label="图片提示信息">
            <el-input v-model="form.alt" />
        </el-form-item>

        <el-form-item label="选择图片">
            <el-upload
                :http-request="httpRequest"
                class="avatar-uploader"
                :show-file-list="false"
            >
                <img v-if="imageUrl" :src="imageUrl" class="avatar" />
                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
            </el-upload>
        </el-form-item>
    </el-form>

    <el-button @click="fileUpdate">上传图片</el-button>
</template>

<script>
    import { Plus } from "@element-plus/icons-vue";
    import { addBanner } from "@/api/banner";
    import { ElMessage } from "element-plus";
    export default {
        data() {
            return {
                form: {
                    link: "",
                    alt: "",
                    img: "",
                },
                imageUrl: "",
            };
        },
        methods: {
            async fileUpdate() {
                console.log("提交数据", this.form);
                const res = await addBanner(this.form);

                if (res.code !== "200") {
                    return ElMessage({
                        type: "warning",
                        message: res.message,
                    });
                }

                ElMessage({
                    type: "success",
                    message: res.message,
                });

                this.form = {
                    link: "",
                    alt: "",
                    img: "",
                };

                this.imageUrl = "";
            },
            httpRequest(options) {
                // console.log('您选择了一张图片', options)

                // 当前函数的第一个形参 options 是一个对象, 对象中的 file 是我们的 图片信息
                // 但是当前的图片信息传递给后端没有用, 需要我们前端将图片转换为 base64 格式的字符串, 然后传递给后端

                // console.log(options.file)

                // 1. 创建一个文件加载器, 一会用于将我们的 图片信息转换格式
                const reader = new FileReader();

                // 2. 指定需要加载的文件
                reader.readAsDataURL(options.file);

                const that = this;

                // 3. 加载文件需要一定的时间才能执行完毕, 所以我们需要书写一个 监听加载完毕 的事件
                reader.onload = function () {
                    // 当前函数执行的时候, 文件已经被处理完毕了

                    // console.log(reader.result)

                    that.form.img = reader.result;

                    that.imageUrl = reader.result;
                };
            },
        },
        components: {
            Plus,
        },
    };
</script>

<style scoped>
    .avatar-uploader .avatar {
        width: 178px;
        height: 178px;
        display: block;
    }

    .el-input {
        width: 500px;
    }
</style>

<style>
    .avatar-uploader .el-upload {
        border: 1px dashed var(--el-border-color);
        border-radius: 6px;
        cursor: pointer;
        position: relative;
        overflow: hidden;
        transition: var(--el-transition-duration-fast);
    }

    .avatar-uploader .el-upload:hover {
        border-color: var(--el-color-primary);
    }

    .el-icon.avatar-uploader-icon {
        font-size: 28px;
        color: #8c939d;
        width: 178px;
        height: 178px;
        text-align: center;
    }
</style>

31_封装公共代码_完善路由表

1. 封装公共代码

import { ElMessage } from "element-plus";

export function myMessage(code, message, cb) {
    if (code !== "200") {
        return ElMessage({
            type: "warning",
            message,
        });
    }

    ElMessage({
        type: "success",
        message,
    });

    cb();
}

2. 完善路由表(记得按照路径添加对应的目录与文件)

import { createRouter, createWebHashHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

// 当前数组中直接书写的对象属于一级路由
export const routes = [
    {
        path: "/",
        name: "home",
        component: HomeView,
        children: [
            // 当前数组中书写的对象属于二级路由
            {
                // 人员管理
                path: "manager",
                name: "manager",
                label: "人员管理",
                component: () =>
                    import("../views/manager/managerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [
                    {
                        // 用户列表
                        path: "managerlist",
                        name: "managerlist",
                        label: "用户列表",
                        component: () =>
                            import("../views/manager/managerListView.vue"),
                    },
                    {
                        // 管理员列表
                        path: "adminlist",
                        name: "adminlist",
                        label: "管理员列表",
                        component: () =>
                            import("../views/manager/adminListView.vue"),
                    },
                ],
            },
            {
                // 轮播图管理
                path: "banner",
                name: "banner",
                label: "轮播图管理",
                component: () => import("../views/banner/bannerIndexView.vue"),
                // 当前数组中书写的对象属于三级路由
                children: [
                    {
                        // 轮播图列表
                        path: "bannerlist",
                        name: "bannerlist",
                        label: "轮播图列表",
                        component: () =>
                            import("../views/banner/bannerListView.vue"),
                    },
                    {
                        // 添加轮播图
                        path: "addbanner",
                        name: "addbanner",
                        label: "添加轮播图",
                        component: () =>
                            import("../views/banner/addBannerView.vue"),
                    },
                ],
            },
            {
                path: "echarts",
                name: "echarts",
                label: "图表展示",
                component: () => import("@/views/echarts/echartsIndexView.vue"),
                children: [
                    {
                        path: "echartsshow",
                        name: "echartsshow",
                        label: "echarts展示",
                        component: () =>
                            import("@/views/echarts/echartsShowView.vue"),
                    },
                ],
            },
            {
                path: "excel",
                name: "excel",
                label: "文件处理",
                component: () => import("@/views/excel/excelIndexView.vue"),
                children: [
                    {
                        path: "excelimport",
                        name: "excelimport",
                        label: "excel导入",
                        component: () =>
                            import("@/views/excel/excelImportView.vue"),
                    },
                    {
                        path: "excelexport",
                        name: "excelexport",
                        label: "excel导出",
                        component: () =>
                            import("@/views/excel/excelExportView.vue"),
                    },
                ],
            },
            {
                path: "map",
                name: "map",
                label: "地图展示",
                component: () => import("@/views/map/mapIndexView.vue"),
                children: [
                    {
                        path: "mapshow",
                        name: "mapshow",
                        label: "百度地图展示",
                        component: () => import("@/views/map/mapShowView.vue"),
                    },
                ],
            },
            {
                path: "editor",
                name: "editor",
                label: "富文本编辑器",
                component: () => import("@/views/editor/editorIndexView.vue"),
                children: [
                    {
                        path: "editorshow",
                        name: "editorshow",
                        label: "编辑器",
                        component: () =>
                            import("@/views/editor/editorShowView.vue"),
                    },
                ],
            },
        ],
    },
    {
        path: "/login",
        name: "login",
        component: () => import("../views/loginView.vue"),
    },
];

const router = createRouter({
    history: createWebHashHistory(import.meta.env.BASE_URL),
    routes,
});

export default router;

32_完善侧边栏导航

<template>
    <el-menu
        class="el-menu-vertical-demo"
        router
        :collapse="!isshow"
        :collapse-transition="false"
    >
        <el-menu-item index="/">
            <el-icon><House /></el-icon>
            <span>首页</span>
        </el-menu-item>

        <el-sub-menu
            v-for="item in $store.state.userInfo.checkedkeys"
            :key="item.name"
            :index="'/' + item.name"
        >
            <template #title>
                <el-icon><location /></el-icon>
                <span> {{ item.label }} </span>
            </template>
            <el-menu-item
                v-for="child in item.children"
                :key="child.label"
                :index="'/' + item.name + '/' + child.name"
                >{{ child.label }}</el-menu-item
            >
        </el-sub-menu>
    </el-menu>
</template>

<script>
    import { Location, House } from "@element-plus/icons-vue";
    export default {
        props: ["isshow"],
        components: {
            Location,
            House,
        },
    };
</script>

33_处理 admin 账号无权限

<template>
    <el-menu
        class="el-menu-vertical-demo"
        router
        :collapse="!isshow"
        :collapse-transition="false"
    >
        <el-menu-item index="/">
            <el-icon><House /></el-icon>
            <span>首页</span>
        </el-menu-item>

        <el-sub-menu
            v-for="item in list"
            :key="item.name"
            :index="'/' + item.name"
        >
            <template #title>
                <el-icon><location /></el-icon>
                <span> {{ item.label }} </span>
            </template>
            <el-menu-item
                v-for="child in item.children"
                :key="child.label"
                :index="'/' + item.name + '/' + child.name"
                >{{ child.label }}</el-menu-item
            >
        </el-sub-menu>
    </el-menu>
</template>

<script>
    import { Location, House } from "@element-plus/icons-vue";
    import { routes } from "../router";
    export default {
        props: ["isshow"],
        components: {
            Location,
            House,
        },
        data() {
            return {
                routesList: routes[0].children,
            };
        },

        computed: {
            list() {
                return this.$store.state.userInfo.adminname !== "admin"
                    ? this.$store.state.userInfo.checkedkeys
                    : this.routesList;
            },
        },
    };
</script>

34_展示 echarts

官网地址: echarts.apache.org/zh/index.ht…

/**
 *  图表展示流程
 *
 *      1. 请求图表数据
 *      2. 根据数据渲染图表
 */

// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById("main"));
// 绘制图表
myChart.setOption({
    title: {
        text: "ECharts 入门示例",
    },
    tooltip: {},
    xAxis: {
        data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
    },
    yAxis: {},
    series: [
        {
            name: "商品销量",
            type: "bar",
            data: [5, 20, 36, 10, 10, 20],
        },
    ],
});

35_富文本编辑器

1. 下载文件存档到 public

2. 在 index.html 中引入

<head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>

    <!-- 导入 富文本编辑器需要的 依赖 -->
    <script src="/tinymce/tinymce.min.js"></script>
    <script src="/tinymce/langs/zh-Hans.js"></script>
</head>

3. 在组件中创建一个容器

<textarea id="default-editor" v-model="value"></textarea>

4. 页面打开时将编辑器挂载到容器中

mounted() {
    tinymce.init({
    	selector: "textarea#default-editor",
	});
},

5. 添加配置项

mounted() {
    tinymce.init({
        selector: "textarea#default-editor",
        branding: false, // 取消右下角默认提示
        height: 500,
        plugins: "advlist link image lists paste", // 使用插件
        paste_data_images: true, // 支持图片粘贴
    });
},

6. 获取内容

<template>
    <div>
        <h1>富文本编辑器</h1>
        <textarea id="default-editor" v-model="value"></textarea>
        <el-button @click="handleSubmit">click</el-button>
    </div>
</template>

<script>
    import { defineComponent } from "vue";
    export default defineComponent({
        data() {
            return {
                value: '<p><em>Hello</em>, <span style="text-decoration: underline;"><strong>World!</strong></span></p>',
            };
        },
        methods: {
            handleSubmit() {
                // 获取内容的方式
                console.log(tinymce.activeEditor.getContent());
            },
        },
        mounted() {
            // ...
        },
    });
</script>

<style lang="scss" scoped>
    #default-editor {
        height: 500px;
    }
</style>

36_导出 excel

需要借助一个第三方包, 安装: npm i js-export-excel

<template>
    <h1>excel导出 <el-button @click="fileExport">导出文件</el-button></h1>
    <el-table :data="tableData" style="width: 100%">
        <el-table-column type="index" width="50" />
        <el-table-column prop="brand" label="品牌" width="180" />
        <el-table-column prop="category" label="分类" width="180" />
        <el-table-column prop="desc" label="描述" />
    </el-table>
</template>

<script>
    import { proList } from "@/api/excel";
    import ExportJsonExcel from "js-export-excel";

    export default {
        data() {
            return {
                tableData: [],
            };
        },
        methods: {
            fileExport() {
                const option = {};

                // 配置 文件名
                // option.fileName = "excel";
                // option.datas = [
                //     // 每一个对象就是表格内的一个独立分页, 需要几个分页就书写几个对象
                //     {
                //         // 导出的源数据
                //         sheetData: [
                //             { one: "一行一列", two: "一行二列" },
                //             { one: "二行一列", two: "二行二列" },
                //         ],
                //         // 表格内分页的名字
                //         sheetName: "sheet",
                //         // 配置我们表格内每一列的数据字段名
                //         sheetFilter: ["two", "one"],
                //         // 配置表格中表头的内容
                //         sheetHeader: ["第一列", "第二列"],
                //         // 配置表格中每一列的宽度
                //         columnWidths: [20, 20],
                //     },
                //     {
                //         sheetData: [
                //             { one: "一行一列", two: "一行二列" },
                //             { one: "二行一列", two: "二行二列" },
                //         ],
                //     },
                // ];

                option.fileName = "商品数据";
                option.datas = [
                    {
                        // 导出的源数据
                        sheetData: this.tableData,
                        // 表格内分页的名字
                        sheetName: "sheet",
                        // 配置我们表格内每一列的数据字段名
                        sheetFilter: [
                            "brand",
                            "category",
                            "proname",
                            "desc",
                            "img1",
                        ],
                        // 配置表格中表头的内容
                        sheetHeader: [
                            "品牌",
                            "分类",
                            "商品名",
                            "商品描述",
                            "商品预览图链接",
                        ],
                        // 配置表格中每一列的宽度
                        columnWidths: [10, 100, 200, 50, 100],
                    },
                ];

                const toExcel = new ExportJsonExcel(option); //new
                toExcel.saveExcel(); //保存
            },
        },
        async mounted() {
            const res = await proList();
            // console.log(res)
            this.tableData = res.data;
        },
    };
</script>

37_导入 excel

<template>
    <h1>excel导入 <el-button @click="upFile">文件导入</el-button></h1>
    <input hidden ref="inpRef" type="file" @change="inpChange" />

    <el-table :data="tabelData" style="width: 100%">
        <el-table-column type="index" width="50" />
        <el-table-column prop="brand" label="品牌" width="180" />
        <el-table-column prop="category" label="分类" width="180" />
        <el-table-column prop="desc" label="描述" />
    </el-table>
</template>

<script>
    import * as XLSX from "xlsx/xlsx.mjs";
    export default {
        data() {
            return {
                tabelData: [],
            };
        },
        methods: {
            // 按钮的点击事件, 点击的时候触发 input 框的点击事件
            upFile() {
                this.$refs.inpRef.click();
            },

            // 更改了 input 框文件内容的时候会触发
            inpChange(e) {
                // console.log(e.target.files)

                // 1. 获取到上传的文件
                const file = e.target.files[0];

                // 2. 创建一个文件加载器
                const reader = new FileReader();

                // 3. 指定 解析/加载 的文件
                reader.readAsBinaryString(file);

                const that = this;
                // 4. 监听 解析完毕的事件
                reader.onload = function () {
                    // 二进制数据流已经转换完毕, 我们需要将这个数据转换为我们认识的格式 json
                    // console.log(reader.result)

                    // 4.1 借助 第三方包 XLSX 帮助我们解析 二进制数据流
                    const box = XLSX.read(reader.result, { type: "binary" });

                    // console.log('转换完毕的数据: ', box)
                    // 4.2 存储我们转换完毕的数据
                    let excelInfo = box.Sheets.sheet;

                    // 4.3 将我们看不懂的数据 转换为 json 格式
                    excelInfo = XLSX.utils.sheet_to_json(excelInfo);

                    // console.log(excelInfo);
                    that.tabelData = excelInfo;
                };
            },
        },
    };
</script>

38_百度地图

百度开放平台: lbsyun.baidu.com/index.php?t…

寻找教程

  1. 鼠标移入开发文档
  2. 选择最左侧的 JavaScript API
  3. 选择 Hello World

获取自己的 AK 码

  1. 注册 百度账号
  2. 登录 百度开放平台
  3. 点击右上角 控制台
  4. 点击页面左侧的应用管理/我的应用
  5. 创建应用
    1. 输入应用名称
    2. 应用类型调整为 浏览器端
    3. 将白名单的输入框内写上一个 *
    4. 提交
  6. 复制刚才创建的应用内部的 AK 码 (表格的第三列)