Vue vite 框架

锋选菁英招聘管理平台项目

一、项目架构搭建

1. vite搭建项目

Vite下一代的前端工具链,为开发提供极速响应。

  1. 创建项目

yarn create vite
填项目名
选vue
选Typescript
  1. 安装依赖

cd fxjy-admin
yarn
  1. 启动项目

yarn dev

2.目录架构认识及改造

src
    api    管理异步请求方法
    components  公共组件
    layout    基本布局骨架
    store   状态机
    router   路由
    utils   公共方法包
    views   业务组件(页面)
    assets   静态资源(img、css)
    App.vue   根组件
    main.ts   入口文件

3. vite配置路径别名@

  1. vite.config.ts配置

     ...
     import { join } from "path";
     export default defineConfig({
       ...
       resolve: {
         alias: {
           "@": join(__dirname, "src"),
         },
       },
     });
    ​
  2. 根目录新建 tsconfig.json 配置

    {
       "compilerOptions": {
         "experimentalDecorators": true,
         "baseUrl": "./",
         "paths": {
           "@/*": ["src/*"],
           "components/*": ["src/components/*"],
           "assets/*": ["src/assets/*"],
           "views/*": ["src/views/*"],
           "common/*": ["src/common/*"]
         }
       },
       "exclude": ["node_modules", "dist"]
     }
    ​
  3. 安装path的类型提示

    yarn add @types/node

  4. 重启 vscode

4. AntDesign 组件库

Ant Design Vue 文档

  1. 安装组件库

yarn add ant-design-vue
  1. 按需导入辅助包

yarn add unplugin-vue-components --dev
  1. 修改vite.config.ts配置

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [AntDesignVueResolver()],
    }),
  ],
  ...
});
​
  1. 在任意.vue组件中,直接使用组件,如App.vue

​
<script setup lang="ts">
</script>
​
<template>
  <a-button type="primary">Primary Button</a-button>
</template>
​
<style scoped>
</style>
​

5. 搭建主面板骨架

  1. 从antd-vue官方获取layout布局

不要照着代码敲,直接复制即可,然后只需要为a-layout容器添加高度

// src/layout/index.vue 主要的布局文件
<template>
  <a-layout style="min-height: 100vh">
    <SiderMenu />
    <a-layout>
      <a-layout-header style="background: #fff; padding: 0" />
      <a-layout-content style="margin: 0 16px">
        <a-breadcrumb style="margin: 16px 0">
          <a-breadcrumb-item>User</a-breadcrumb-item>
          <a-breadcrumb-item>Bill</a-breadcrumb-item>
        </a-breadcrumb>
        <div
          :style="{ padding: '24px', background: '#fff', minHeight: '360px' }"
        >
          Bill is a cat.
        </div>
      </a-layout-content>
      <a-layout-footer style="text-align: center">
        Ant Design ©2018 Created by Ant UED
      </a-layout-footer>
    </a-layout>
  </a-layout>
</template>
<script setup lang="ts">
import {
  PieChartOutlined,
  DesktopOutlined,
  UserOutlined,
  TeamOutlined,
  FileOutlined,
} from "@ant-design/icons-vue";
import { ref } from "vue";
import SiderMenu from "./components/sider-menu.vue";
const collapsed = ref(false);
const selectedKeys = ref(["1"]);
</script>
<style scoped>
.logo {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.3);
}
​
.site-layout .site-layout-background {
  background: #fff;
}
[data-theme="dark"] .site-layout .site-layout-background {
  background: #141414;
}
</style>
  1. App.vue引入 主界面的布局文件

<script setup lang="ts">
import MainLayout from "@/layout/index.vue";
</script>
​
<template>
  <MainLayout />
</template>
​
<style scoped></style>
  1. 查看浏览器,预览运行结果

二、VueRouter路由运用

2.1.路由基本配置

  1. 安装

    默认安装的是vue-router@4,使用时需要注意版本号

    yarn add vue-router
  2. 新建页面组件

    └─views
        │  dashboard.vue   可视化图表页
        ├─category     分类管理
        │      list.vue
        │      pub.vue
        └─job          岗位管理
                list.vue
                pub.vue
  3. 配置路由

    // src/router/index.ts
    import { createRouter, createWebHashHistory } from "vue-router";
    ​
    const router = createRouter({
      history: createWebHashHistory(),
      routes: [
        {
          path: "/",
          component: () => import("@/views/dashboard.vue"),
        },
        {
          path: "/category/list",
          component: () => import("@/views/category/list.vue"),
        },
        {
          path: "/category/pub",
          component: () => import("@/views/category/pub.vue"),
        },
      ],
    });
    export default router;
    ​
    ​
  4. 呈现路由组件

    需要在MainLayout的内容区域添加一个RouterView组件,用来动态显示路由匹配到的组件。

  5. 通过手动修改地址栏地址,可以测试路由组件切换效果

2.2 动态渲染菜单

  1. 改造路由数据包

    export const routes = [
      {
        path: "/",
        component: () => import("@/views/dashboard.vue"),
        meta: {
          label: "数据可视化",
          icon: "area-chart-outlined",
        },
      },
      {
        path: "/category",
        component: () => import("@/views/category/index.vue"),
        meta: {
          label: "分类管理",
          icon: "area-chart-outlined",
        },
        children: [
          {
            path: "/category/list",
            component: () => import("@/views/category/list.vue"),
            meta: {
              label: "分类列表",
            },
          },
          {
            path: "/category/pub",
            component: () => import("@/views/category/pub.vue"),
            meta: {
              label: "发布分类",
            },
          },
        ],
      },
    ];
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    });

  2. 单独拆分侧边菜单组件

    考虑到代码的可维护性,我们将MainLayout中的侧边菜单逻辑交互单独拆分为一个组件

    组件存放位置:/src/layout/components/side-menu.vue

  3. 在side-menu.vue中使用路由数据包动态渲染

    <template>
      <a-layout-sider v-model:collapsed="collapsed" collapsible>
        <div class="logo" />
        <a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
          <template v-for="(item, index) in routes" :key="index">
            <a-menu-item v-if="!item.children" :key="item.path">
              <component :is="item.meta!.icon"></component>
              <span>{{ item.meta!.label }}</span>
            </a-menu-item>
            <a-sub-menu v-else :key="index">
              <template #title>
                <span>
                  <component :is="item.meta!.icon"></component>
                  <span>{{ item.meta!.label }}</span>
                </span>
              </template>
              <a-menu-item v-for="child in item.children" :key="child.path">{{
                item.meta!.label
              }}</a-menu-item>
            </a-sub-menu>
          </template>
        </a-menu>
      </a-layout-sider>
    </template>
    ​
    <script setup lang="ts">
    import { ref } from "vue";
    import { routes } from "@/router";
    const collapsed = ref(false);
    const selectedKeys = ref(["1"]);
    </script>
    ​
    <style scoped></style>
    ​
  4. 通过useRouter触发路由切换

    const router = useRouter();
    const handleMenu = ({ key }: { key: string }) => { //此处的key是由menu组件点击事件提供
      console.log(key);
      router.push(key);
    };

2.3 动态图标及路由跳转

  1. 安装

    yarn add @ant-design/icons-vue

  2. main.ts全局注册

    import * as antIcons from "@ant-design/icons-vue";
    const app = createApp(App);
    // 注册组件
    Object.keys(antIcons).forEach((key: any) => {
      app.component(key, antIcons[key as keyof typeof antIcons]);
    });
    // 添加到全局
    app.config.globalProperties.$antIcons = antIcons;
  3. 动态组件

    <component is="xxx">

2.4 面包屑交互

  1. 封装面包屑组件

    封装面包屑组件

    使用useRoute、结合watch监听,在面包屑组件中监听路由路径的变化

    // src/layout/component/app-breadcrumb.vue
    <template>
      <a-breadcrumb>
        <a-breadcrumb-item>首页</a-breadcrumb-item>
      </a-breadcrumb>
    </template>
    
    <script setup lang="ts">
    import { watch } from "vue";
    import { useRoute } from "vue-router";
    
    const route = useRoute();
    watch(
      route,
      (newValue) => {
        console.log("路由发生变化了", newValue.path);
      }
    );
    </script>
    
    <style scoped></style>
    
  2. 整合面包屑数据包

    封装一个方法函数,将路由数据包,处理为【path--菜单名】的映射格式:

    {

    '/dashboard':'数据统计',

    '/category':'分类管理',

    '/category/list':'分类列表'

    ......

    }

代码参考

// utils/tools.ts

import { routes } from "@/router";
import { RouteRecordRaw } from "vue-router";
interface RouteMap {
  [key: string]: any;
}
export function routeMapTool() {
  let routeMap: RouteMap = {};
  //递归函数
  function loop(arr: RouteRecordRaw[]) {
    arr.forEach((item: RouteRecordRaw) => {
      routeMap[item.path] = item.meta!.label;
      if (item.children) {
        recursion(item.children);
      }
    });
  }
  recursion(routes[0].children!);
  return routeMap;
}
  1. 封装方法函数,对路由路径进行处理

路由路径:/category/list

处理目标:[ '/category' , '/category/list' ]

import { routeMapTool } from "@/utils/tools";
const route = useRoute();
const routeMap = routeMapTool();  //调用方法,获取面包屑映射数据包

const pathList = ref<string[]>([]);
const pathToArr = (path: string) => {
  let arr = path.split("/").filter((i) => i);
  let pathArr = arr.map((item, index) => {
    return "/" + arr.slice(0, index + 1).join("/");
  });
  console.log(arr);
  return pathArr; //['/category','/category/list']
};
pathList.value = pathToArr(route.path); //初始化调用,保证刷新后面包屑显示正常
  1. 在watch监听内部,响应式保存处理后的路径数组

watch(
  route,
  (newValue) => {
    pathList.value = pathToArr(newValue.path);
  }
);
  1. 动态渲染面包屑

<a-breadcrumb>
    <a-breadcrumb-item>首页</a-breadcrumb-item>
    <a-breadcrumb-item v-for="item in pathList">
        {{routeMap[item]}}
    </a-breadcrumb-item>
</a-breadcrumb>

2.5 登录页路由规划

  1. 搭建登录页

先新建空页面,等待路由调整完毕后再完成页面结构搭建

  1. 配置登录路由

如果按照目前的路由层级直接配置,会导致登录页也出现侧边菜单栏,所以需要在App.vue中再新增一层路由,这层路由只负责控制两个组件:

  1. MainLayout组件

  2. login组件

  3. 原本所有的路由都设置为MainLayout的子路由

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    component: () => import("@/layout/index.vue"),
    children: [
      {
        path: "/dashbord",
        component: dashboardVue,
        meta: {
          label: "数据统计",
          icon: "area-chart-outlined",
        },
      },
      ...其他业务页面路由
    ],
  },
  {
    path: "/login",
    component: () => import("@/views/login.vue"),
  },
];
  1. 将原本用到了routes数据包的业务逻辑代码进行调整

原本使用了routes数据包的地方需要统一换为routes[0].children

  1. side-menu.vue组件

  2. tools.js 文件

  1. 在App.vue中新增RouterView组件,并测试路由是否正常

  2. 完成登录面板搭建

利用如下三个组件实现:

  1. 栅格式组件 a-row

  2. 卡片组件 a-card

  3. 表单组件 a-form 【重难点】

  1. 安装sass,提高样式编写效率

yarn add sass

三、后端服务及前端请求

3.1 LeanCloud云服务

扮演后端的角色

  1. 介绍LeanCloud

第三方公司开发的一套通用的后端接口云服务 ServerLess 无服务、弱服务 官网

  1. 使用流程

    • 注册LeanCloud账号,并实名认证

    • 进入控制台--新建应用--选择开发版

    • 进入应用获取三条信息 -- 设置 -- 应用凭证

      • AppID

      • AppKey

      • RestApi 服务器地址

3.2 RestFull-API介绍

  1. 普通风格接口

通过后端给的不同url地址,来区分每个接口的作用

app.post('/banner/add')  新增轮播
app.post('/banner/list')  查询轮播
app.post('/banner/edit')  修改轮播
app.post('/banner/delete')  删除轮播
  1. Rest风格接口

通过前后端对接时所用的不同方法来区分接口的作用

app.post('/banner')  新增轮播
app.get('/banner')  查询轮播
app.put('/banner')  修改轮播
app.delete('/banner')  删除轮播
  1. 看懂LeanCloud接口文档 示例

  • 接口地址

  • 请求方法

  • 传递的参数

  • 返回的结果

  1. 使用Rest接口访问LeanCloud

RestFull 风格的API LeanCloud接口文档

curl -X POST \  //请求所用的方法时POST
  -H "X-LC-Id: PhooC2pGuFn5MkTPdTRn7O99-gzGzoHsz" \ //请求头headers相关信息
  -H "X-LC-Key: 4x587AuiHPH0eZspQnvR5qaH" \
  -H "Content-Type: application/json" \
  -d '{"content": ""}' \  //请求接口时需要携带的数据包
  https://API_BASE_URL/1.1/classes/自定义表名  //接口地址

3.3 ApiPost工具运用

开发过程中,可以使用ApiPost工具测试后端接口可用性

ApiPost官网

  1. 自行下载安装

  2. 使用ApiPost向LeanCloud新增数据

  3. 查询数据演示

  4. 更新数据演示

  5. 删除数据演示

3.4 axios发起异步请求

Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js axios文档

  1. 安装

    npm i axios
  2. 引入配置

    • 常用配置项

    {
      
      url: '/user',   // `url` 是用于请求的服务器 URL
      method: 'get', // `method` 是创建请求时使用的方法,默认GET
      baseURL: 'https://some-domain.com/api/', //接口地址中,每个接口都一样的部分
      headers: {AppId,Appkey}, //配置请求头,携带验证信息给后端
      params: {  //携带query数据给后端
        ID: 12345
      },
      data: {   //携带body数据给后端
        firstName: 'Fred'
      },
    }
  • axios封装 request.js

  // 在此处对axios做集中配置
  import axios from 'axios'

  const instance = axios.create({
    baseURL:'https://phooc2pg.lc-cn-n1-shared.com/1.1', //配置通用基础地址
    headers:{ //配置通用的请求头
      'X-LC-Id': '务必使用自己的ID',
      'X-LC-Key': '务必使用自己的Key',
      'Content-Type': 'application/json'
    }
  })

  export default instance  //配置后的axios
  1. 发起请求

request.post('/classes/Banner',{  //此处request时封装过的axios
  name:'价值100万的广告',
  url:'http://www.1000phone.com',
  desc:'我为千锋带盐',
  isshow:true
})
  1. 常用的axios请求方法

axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.get(url[, config])
axios.delete(url[, config])

四、分类管理

4.1 分类发布

AntDesignVue版本升级

安装命令: yarn add ant-design-vue@4.x

AntDesignVueV4文档

  1. 分类数据字段分析

    • objectId 数据库自动分配的唯一id

    • name 分类名称 【a-input组件】

    • parentId 父级类目id 【a-select组件】

    • icon 分类图标 【a-upload图片上传组件】

  2. 搭建分类发布页

建议基于ant-design-vue官方案例进行改造,如:useForm 基本表单

<template>
  <a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 14 }">
    <a-form-item label="分类名称" v-bind="validateInfos.name">
      <a-input v-model:value="modelRef.name" />
    </a-form-item>
    <a-form-item label="父级类目" v-bind="validateInfos.parentId">
      <a-select v-model:value="modelRef.parentId" placeholder="请选择父级类目">
        <a-select-option value="0-0">顶级类目</a-select-option>
        <a-select-option value="beijing">Zone two</a-select-option>
      </a-select>
    </a-form-item>
    <a-form-item label="分类图标" v-bind="validateInfos.icon">
      <a-input v-model:value="modelRef.icon" />
    </a-form-item>
    <a-form-item :wrapper-col="{ span: 14, offset: 4 }">
      <a-button type="primary" @click.prevent="onSubmit">Create</a-button>
      <a-button style="margin-left: 10px" @click="resetFields">Reset</a-button>
    </a-form-item>
  </a-form>
</template>
<script lang="ts" setup>
import { defineComponent, reactive, toRaw } from "vue";
import { Form } from "ant-design-vue";

const useForm = Form.useForm;
const modelRef = reactive({
  name: "开发",
  parentId: "0-0",
  icon: "img.png",
});
const rulesRef = reactive({
  name: [
    {
      required: true,
      message: "请输入分类名称",
    },
  ],
  parentId: [
    {
      required: true,
      message: "请选择父级类目",
    },
  ],
  icon: [
    {
      required: true,
      message: "请上传分类图标",
    },
  ],
});
const { resetFields, validate, validateInfos } = useForm(modelRef, rulesRef, {
  onValidate: (...args) => console.log(...args),
});
const onSubmit = () => {
  validate()
    .then(() => {
      console.log(toRaw(modelRef));
    })
    .catch((err) => {
      console.log("error", err);
    });
};
</script>
  1. 定义类型约束模块

// src/types/pro.d.ts

export interface CategoryType {
  objectId: string;
  name: string;
  icon: string;
  parentId: string;
}

  1. 封装api方法

// src/api/pro.ts

import request from "@/utils/request";
import { CategoryType } from "@/types/pro";

export const categoryPost = (cateObj: CategoryType) => {
  return request.post("classes/category");
};
  1. 在分类录入表单中向数据库录入数据

import { categoryPost } from "@/api/pro";
const onSubmit = () => {
  validate()
    .then(() => {
      categoryPost(modelRef); //使用api方法向后端发请求
    })
    .catch((err) => {
      console.log("error", err);
    });
};
  1. 录入7个顶级类目

开发、测试、设计、运维、运营、行政、数据

4.2 二级类类目录入

  1. 认识LeanCloud约束查询接口

查询约束接口文档

curl -X GET \
  -H "X-LC-Id: 3YdGuHbgN2Z1M7RyG2tGynBs-gzGzoHsz" \
  -H "X-LC-Key: zlEVi2que169Bl8zljJSKrGi" \
  -H "Content-Type: application/json" \
  -G \
  --data-urlencode 'where={"pubUser":"官方客服"}' \
  https://API_BASE_URL/1.1/classes/Post
  1. 封装类目查询api

//查询类目,支持查询所有类目,也支持指定查询主类目
export const categoryGet = (all?: boolean) => {
  let where = all ? {} : { parentId: "0-0" };
  return request.get("classes/category", {
    params: {
      where,
    },
  });
};
  1. 在分类发布页请求查询接口

//主类目列表渲染
const parentList = ref<CategoryType[]>([]);
categoryGet().then((res) => {
  parentList.value = res.data.results;
});
  1. 渲染主类目列表

<a-select v-model:value="modelRef.parentId" placeholder="请选择父级类目">
    <a-select-option value="0-0">顶级类目</a-select-option>
    <a-select-option v-for="item in parentList" :value="item.objectId">
        {{ item.name }}
    </a-select-option>
</a-select>
  1. 为开发,设计,录入部分子类目

开发:前端开发、Java开发...

设计:UI设计、室内设计...

4.3 分类列表渲染

  1. 搭建分类列表页并渲染

主要知识点:a-table组件的使用

  1. columns 表格配置项

  2. data-source 待渲染的数据包

  3. 自定义插槽

    • headerCell 自定义表格头

    • bodyCell 自定义表格内容

<template>
  <a-table :columns="columns" :data-source="data">
    <template #bodyCell="{ column, record }">
      <template v-if="column.key === 'action'">
        <a-space>
          <a-button type="primary" size="small">编辑</a-button>
          <a-button type="danger" size="small">删除</a-button>
        </a-space>
      </template>
    </template>
  </a-table>
</template>
<script lang="ts" setup>
import { categoryGet } from "@/api/pro";
import { CategoryType } from "@/types/pro";
import { ref } from "vue";
const columns = [
  {
    title: "分类名称",
    dataIndex: "name",
    key: "name",
  },

  {
    title: "分类图标",
    dataIndex: "icon",
    key: "icon",
  },
  {
    title: "操作",
    key: "action",
  },
];

const data = ref<Array<CategoryType>>([]);
categoryGet(true).then((res) => {
  data.value = res.data.results;
});
</script>
  1. 树形表格渲染

树形表格示例文档

表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或配置为其他字段可以用 childrenColumnName 进行配置。 可以通过设置 indentSize 以控制每一层的缩进宽度。

  1. 树形数据处理函数封装

// utils/tools.ts
//分类树形数据处理
export function categoryToTree(results: CategoryType[]) {
  let parentArr = results.filter((item) => item.parentId == "0-0");
  parentArr.forEach((item) => {
    let child = results.filter((child) => child.parentId == item.objectId);
    if (child.length) {
      item.children = child;
    }
  });
  return parentArr;
}
  1. 将处理后的数据渲染至表格

const data = ref<Array<CategoryType>>([]);
categoryGet(true).then((res) => {
  data.value = categoryToTree(res.data.results);
});

4.4 图片上传

前端

  • a-upload 图片上传组件

  • 方法 1:使用 action 后端提供的图片上传接口 (action 地址、on-success 成功回调)

  • 方法 2:使用 customRequest 覆盖默认的 action 上传

  • 资源对象转 base64 编码

function getBase64(img: Blob, callback: (base64Url: string) => void) {
  const reader = new FileReader();
  reader.addEventListener("load", () => callback(reader.result as string));
  reader.readAsDataURL(img);
}

后端

后端的图片上传接口如何调用,是后端接口来决定的

  • LeanCloud

  • 使用 SDK 上传 (SDK 就是一个 npm 模块,内部存放了一些方法函数)

  • SDK 使用流程

  1. 安装 文档

npm install leancloud-storage --save
  1. 封装init-leancloud.ts初始化 SDK

让 SDK 知道应该向哪个空间上传图片

Cloud.init({
  appId: "自己的 ID",
  appKey: "自己的 Key",
  serverURL: "自己的域名"
});
  1. main.js 中引入init-leancloud.ts

import "./utils/init-leancloud";

  1. 使用 (将本地资源转化为 LeanCloud 资源)文档

const handleCustomRequest = (info: any) => {
  getBase64(info.file, (base64) => {
    const data = { base64 };
    const file = new Cloud.File("fxjy.png", data); //将base64的编码,构建为LeanCloud资源对象
    //上传图片并获取后端下发的图片链接
    file.save().then((res: any) => {
      imageUrl.value = res.attributes.url; //展示预览图
    });
  });
};

代码

<template>
  <a-upload
    v-model:file-list="fileList"
    name="avatar"
    list-type="picture-card"
    class="avatar-uploader"
    :show-upload-list="false"
    :before-upload="beforeUpload"
    :customRequest="handleCustomRequest"
  >
    <img class="preview" v-if="imageUrl" :src="imageUrl" alt="avatar" />
    <div v-else>
      <loading-outlined v-if="loading"></loading-outlined>
      <plus-outlined v-else></plus-outlined>
      <div class="ant-upload-text">Upload</div>
    </div>
  </a-upload>
</template>
<script setup lang="ts">
import { PlusOutlined, LoadingOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { ref } from "vue";
import type { UploadChangeParam, UploadProps } from "ant-design-vue";
import Cloud from "leancloud-storage";
function getBase64(img: Blob, callback: (base64Url: string) => void) {
  const reader = new FileReader();
  reader.addEventListener("load", () => callback(reader.result as string));
  reader.readAsDataURL(img);
}

const fileList = ref([]);
const loading = ref<boolean>(false);
const imageUrl = ref<string>("");

const handleCustomRequest = (info: any) => {
  getBase64(info.file, (base64) => {
    const data = { base64 };
    const file = new Cloud.File("fxjy.png", data); //将base64的编码,构建为LeanCloud资源对象
    //上传图片并获取后端下发的图片链接
    file.save().then((res: any) => {
      imageUrl.value = res.attributes.url; //展示预览图
    });
  });
};

const beforeUpload = (file: any) => {
  const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
  if (!isJpgOrPng) {
    message.error("You can only upload JPG file!");
  }
  const isLt2M = file.size / 1024 / 1024 < 2;
  if (!isLt2M) {
    message.error("Image must smaller than 2MB!");
  }
  return isJpgOrPng && isLt2M;
};
</script>
<style>
.avatar-uploader > .ant-upload {
  width: 128px;
  height: 128px;
}
.ant-upload-select-picture-card i {
  font-size: 32px;
  color: #999;
}

.ant-upload-select-picture-card .ant-upload-text {
  margin-top: 8px;
  color: #666;
}
.preview {
  height: 100%;
}
</style>

4.5 表单获取图片链接

  1. 使用 组件 v-model 的知识点 文档

  2. 调用ImgUpload 组件时绑定 v-model

// src/category/pub.vue
<a-form-item label="分类图标" v-bind="validateInfos.icon">
    <!-- <a-input v-model:value="modelRef.icon" /> -->
    <img-upload v-model="modelRef.icon" />
</a-form-item>
  1. ImgUpload 组件内触发$emits

interface EmitsType {  //1. 约束emit的类型
  (e: "update:modelValue", url: string): void;
}
const emits = defineEmits<EmitsType>(); //2.使用泛型约束定义emit
const handleCustomRequest = (info: any) => {
  getBase64(info.file, (base64) => {
    ...
    file.save().then((res: any) => {
      let { url } = res.attributes;
      imageUrl.value = url; //展示预览图
      emits("update:modelValue", url); // 3. 调用emits方法,将后端下发的图片链接透传给父组件
    });
  });
};
  1. 在表单中测试

测试a-form是否能够通过v-model成功获取其内部自定义img-upload组件提供的数据

// src/category/pub.vue
const onSubmit = () => {
  validate()
    .then(() => {
      console.log(toRaw(modelRef));
      // categoryPost(modelRef);
    })
    .catch((err) => {
      console.log("error", err);
    });
};

4.6 分类编辑

  1. 搭建分类编辑页

复用分类发布页pub.vue

  1. 配置路由

// router/index.ts
{
    path: "/category",
    redirect: "/category/list",
    meta: {
        label: "分类管理",
        icon: "appstore-outlined",
    },
    children: [
        ...
        {
            path: "/category/edit",
            component: () => import("@/views/category/edit.vue"),
            meta: {
                label: "分类编辑",
                hidden: true,  //在meta中新增一个自定义属性,用以控制侧边菜单隐藏
            },
        },
    ],
},
  1. 侧边菜单渲染控制

注意a-menu-item组件上使用v-show无效

<div
     v-for="child in item.children"
     v-show="!child.meta!.hidden"
     :key="child.path"
 >
    <a-menu-item>{{ child.meta!.label }}</a-menu-item>
</div>
  1. 分类列表跳转编辑页

Omit 从interface中排除某个指定的字段

Omit<CategoryType, "children"> 从CategoryType接口中排除children字段

【注意】在LeanCloud数据库将createAt等非必要字段设为客户端不可见,不然影响后续更新数据提交。

//编辑
const router = useRouter();
const handleEdit = (record: Omit<CategoryType, "children">) => { 
  router.push({
    path: "/category/edit",
    query: record,
  });
};
  1. 详情页初始化渲染待编辑内容

// category/edit.vue
const modelRef = ref<CategoryType>({  //将原本的reactive修改为ref,方便整体进行响应式修改
  name: "",
  parentId: "",
  icon: "",
});
...

//编辑
const route = useRoute();
let obj = route.query as unknown as CategoryType;
modelRef.value = obj;
  1. 封装更新api

//更新类目
export const categoryPut = (objectId: string, cateObj: CategoryType) => {
  return request.put(`classes/category/${objectId}`, cateObj);
};
  1. 触发更新请求

const onSubmit = () => {
  validate()
    .then(() => {
      console.log(toRaw(modelRef.value));
      categoryPut(modelRef.value.objectId as string, modelRef.value);
    })
    .catch((err) => {
      console.log("error", err);
    });
};
  1. 表格图片自定义渲染

<a-table :columns="columns" :data-source="data" rowKey="objectId">
    <template #bodyCell="{ column, record }">
        <template v-if="column.key == 'icon'">
            <img :src="record.icon" alt="" class="icon" />
        </template>
    	...
    </template>
</a-table>

五、角色权限管理

5.1 RBAC模型

  1. RBAC模型介绍

    Role-Based Access Control 基于角色的用户访问权限控制

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示

  • User(用户):每个用户都有唯一的UID识别,并被授予不同的角色

  • Role(角色):不同角色具有不同的权限

  • Permission(权限):访问权限

  • 用户-角色映射:用户和角色之间的映射关系

  • 角色-权限映射:角色和权限之间的映射

例如下图,管理员和普通用户被授予不同的权限,普通用户只能去修改和查看个人信息,而不能创建用户和冻结用户,而管理员由于被授予所有权限,所以可以做所有操作。

  1. RBAC实践流程

本项目的权限管理基于RBAC基本模型设计而成,具体实践流程如下图所示

  1. RBAC开发流程

先设定角色

超级管理员 --- 有权访问所有页面 管理员 企业用户 普通员工

分配账号并关联角色

张三丰 ---- 超级管理员 无忌 --- 管理员

登录不同的账号,得到其所关联的角色,再根据角色获取对应的访问权限

5.2 角色管理

  1. Drawer 组件

  2. Tree 组件

  3. 渲染 tree 数据

#title插槽 自定义TreeNode显示的文本

fieldNames 自定义key

<a-tree
        v-model:checkedKeys="formState.checkedKeys"
        checkable
        :tree-data="routes[0].children"
        :fieldNames="{ key: 'path' }"
        >
    <template #title="scope">
{{ scope.meta.label }}
    </template>
</a-tree>
  1. 获取 checkedKeys

    通过为 a-tree 组件绑定 v-model:selectedKeys 获取

    <a-tree
            v-model:checkedKeys="formState.checkedKeys"
            checkable
            :tree-data="routes[0].children"
            :fieldNames="{ key: 'path' }"
            >
        <template #title="scope">
    	{{ scope.meta.label }}
        </template>
    </a-tree>
  2. 将角色数据录入数据库

    //新增角色
    export const rolePost = (roleObj) => {
      return request.post("classes/VueRole", roleObj);
    };

5.3 角色列表

  1. axios拦截器全局提示

    import axios from "axios";
    import { message } from "ant-design-vue";
    
    const instance = axios.create({
      baseURL: "https://wojhrvmp.lc-cn-n1-shared.com/1.1/",
      headers: {
        "X-LC-Id": "WojHRvmpUDdDfo2kr9mfUVc2-gzGzoHsz",
        "X-LC-Key": "RIiXkMSxvm1XzeptOeTOgvik",
        "Content-Type": "application/json",
      },
    });
    
    // 添加请求拦截器
    instance.interceptors.request.use(
      function (config) {
        // 在发送请求之前做些什么
        return config;
      },
      function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
      }
    );
    
    // 添加响应拦截器
    instance.interceptors.response.use(
      function (response) {
        // 2xx 范围内的状态码都会触发该函数。
        // 对响应数据做点什么
        message.success("操作成功");
        return response;
      },
      function (error) {
        // 超出 2xx 范围的状态码都会触发该函数。
        // 对响应错误做点什么
        message.error("操作失败");
        return Promise.reject(error);
      }
    );
    
    export default instance;
    

  2. 角色列表自定义渲染

    <template>
      <a-button type="primary" @click="open = true">新增角色</a-button>
      <a-table :columns="columns" :data-source="data">
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'checkedKeys'">
            <a-tag color="blue" v-for="item in record.checkedKeys">{{
              routeMap[item]
            }}</a-tag>
          </template>
          <template v-if="column.key === 'action'">
            <a-space>
              <a-button type="primary" size="small">编辑</a-button>
              <a-button type="primary" size="small" danger>删除</a-button>
            </a-space>
          </template>
        </template>
      </a-table>
      <a-drawer
        v-model:open="open"
        class="custom-class"
        root-class-name="root-class-name"
        title="新增角色"
        placement="right"
      >
        <RoleForm />
      </a-drawer>
    </template>
    <script lang="ts" setup>
    import RoleForm from "./components/role-form.vue";
    import { ref } from "vue";
    import { RoleType } from "@/types/user";
    import { roleGet } from "@/api/user";
    import { routeMapTool } from "@/utils/tools";
    const routeMap = routeMapTool();
    const columns = [
      {
        title: "角色名称",
        dataIndex: "name",
        key: "name",
      },
      {
        title: "角色权限",
        dataIndex: "checkedKeys",
        key: "checkedKeys",
      },
      {
        title: "操作",
        key: "action",
      },
    ];
    
    const open = ref<boolean>(false);
    const data = ref<RoleType[]>([]);
    roleGet().then((res) => {
      data.value = res.data.results;
    });
    </script>
    

5.4 角色编辑

  1. 复用 roleForm 组件(新增、修改)

  2. 如何让 roleForm 区分新增、修改? 【向 roleForm 传递数据、下标】

  3. roleForm 根据传递的数据不同,渲染不同按钮【新增按钮】【修改按钮】

    • 在 roleForm 中通过 mounted ,检测 props 变化

mounted() {
    console.log("roleForm组件渲染了");
    if (this.rowData) {
      //修改
      let { name, checkedKeys } = this.rowData;
      this.name = name;
      this.$refs.treeRef.setCheckedKeys(checkedKeys);
    } else {
      //新增
      this.name = "";
      this.$refs.treeRef.setCheckedKeys([]);
    }
  },
  1. 【修改按钮】触发相关逻辑

  2. 【!!注意!!】el-drawer 组件会导致内部 roleForm 的 mounted 无法实时触发

    解决方案,每次关闭 drawer 时,销毁 roleForm 组件

<RoleForm :row-data="editData" v-if="drawer" />

5.5 删除判断

关联了账号的角色,需要先清除账号,才能删角色

  1. _User 表默认不允许发起 get 查询,需要修改 LeanCloud 的数据表权限

  2. 通过 roleid 查询_User 表格 (get 请求如何带数据给后端)

    export const userGet = (roleId) => {
       return request.get("users", {
         params: {
           where: {
             roleid: roleId,
           },
         },
       });
     };

5.6 账号分配

六、Pinia状态机的运用

6.1 Pinia 基本使用

Pinia | The intuitive store for Vue.js (vuejs.org)

vue2 --- vuex3 vue3 --- vuex4 vue3 --- pinia (vuex5) 状态机模块:vuex4、pinia 项目中有某些数据需要:跨组件、同步、共享,的时候,应该考虑使用状态机

函数式编程思想,更适合跟组合式API配合使用

  1. 安装

Pinia 文档

npm i pinia -S
  1. 项目中引入和使用

// main.ts
import { createPinia } from "pinia";

// 返回一个vue插件
const store = createPinia();

// 使用pinia插件
app.use(store);
  1. 创建 store

  • store 是一个用reactive 包裹的对象

  • 可以根据需要定义任意数量的 store ,我们以一个 userStore 为例

  • pinia 怎么知道有多少个 store 呢?(答案是,只要定义的 store 执行了 defineStore 返回的函数,pinia 就认为这个 store 激活了)

import { defineStore } from "pinia";

// defineStore('user', ...) 和下面的形式相同
export const useUserStore = defineStore({
  id: "user",
  state: () => ({
    userid: localStorage.getItem("userid") || "",
  }),
  actions: {
    setUserId(payload: string) {
      this.userid = payload;
    },
  },
});

6.2 pinia核心API

  1. state的操作

获取 state的方式

<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { computed } from "vue";
import { storeToRefs } from "pinia";
// 方式1, 通过user.userid的方式使用   【推荐】
const user = useUserStore();
// 方式2, 借助pinia提供的api: storeToRefs 实现,直接解构会丢失响应性
const { userid } = storeToRefs(user);
</script>

修改 state的方式

<script setup lang="ts">
import { useUserStore } from "@/stores/user";

const user = useUserStore();
// 方式1: 直接修改,vuex不允许这种方式(需要提交mutation),但pinia是允许的
user.userid = "xxx";    【推荐】
// 方式2:
user.$patch({ userid: "xxx" });    【推荐】

</script>
  1. actions的使用

  • actions 包含同步 actions 和异步 actions

const login = (): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("userid");
    }, 2000);
  });
};
// defineStore('user', ...) 和下面的形式相同
export const useUserStore = defineStore({
  id: "user",
  state: () => ({
    userid: localStorage.getItem("userid") || "",
  }),
  actions: {
    // 同步actions
    setUserId(payload: string) {
      console.log(payload);
      this.userid = payload;
    },
    // 异步actions
    async getUser() {
      // actions可以互相调用
      this.setUserId(await login());
    },
  },
});
  • actions 可以互相调用,我们把 actions 调用关系互换一下

actions: {
  // 在同步actions中调用异步actions
  setUserId(payload: string) {
    console.log(payload)
    this.getUser()
  },
    async getUser() {
      // this.setUserId(await login())
      this.userid = await login()
    }
}
  1. getters的使用

  • 相当于组件中的 computed 也具有缓存

  • 接收"状态"作为第一个参数

  • state: () => ({
      userid: localStorage.getItem('userid') || '',
      counter: 0
    }),
    getters: {
      doubleCount: (state) => state.counter * 2,
    },

6.3 plugins插件

默认情况下,状态机数据包刷新后会丢失

持久化插件配置后,能够自动将状态机数据同步存储到本地 sesseionStorage 或 localStorage

pinia 持久化插件

  1. 安装

npm i pinia-plugin-persistedstate
  1. 配置 main.ts

import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); //给pinia配置plugin
  1. 启用

import { defineStore } from "pinia";

export const useCounter = defineStore("count", {
  state: () => {
    return {
      num: 100,
    };
  },
  getters: {
    double(): number {
      return this.num * 2;
    },
  },
  persist: {
    key: "pinia-count-num", //指定持久化存储的名称
  },
});

6.4 登录功能

结合pinia状态机,实现用户的登录及用户信息的跨组件共享。

思考:实现登录流程时,是否有必要使用 pinia?

  1. 前端通过表单获取账号、密码

  2. 携带账号、密码向后端登录接口发请求

curl -X POST \
-H "Content-Type: application/json" \
-H "X-LC-Id: 务必使用自己的ID" \
-H "X-LC-Key: 务必使用自己的Key" \
-d '{"username":"tom","password":"f32@ds*@&dsa"}' \ 必须是username、password
https://API_BASE_URL/1.1/login
  1. 登录成功后,后端下发用户信息(id、账号、token、头像)

  2. 前端存储用户信息

    • 存入状态机【跨组件实时同步】

      • 新建 store/user.js

      • Login.vue 中触发改变用户信息 userStore

      • vuex 里面的数据都是临时存储,刷新后会丢失

    • 存入本地存储 localStorage

      由persist持久化插件自动完成

    • 跳转主面板 router.push('/')

  1. 路由拦截中的登录判断,也使用状态机中的用户信息来判断

6.5 token 的作用

加密字符串 token 令牌 (authToken、authoration) 锦衣卫 令牌

  1. 登录成功后向后端换取 token 令牌 (sessionToken)

  2. 想要修改跟用户相关的信息时,后端要求必须在 headers 携带 token

6.6 退出登录

 handleLogout() {
  this.$store.commit('user/initInfoMut',null)  //重置状态机
  localStorage.removeItem('userInfo')  //清除本地存储
  this.$router.push('/login')
}

七、企业账号设置

7.1 账号设置页搭建

企业账号字段分析

objectId 企业ID

username 企业名称

logo 企业LOGO 【图片上传组件】

intro 企业简介 【富文本编辑器】

address 企业位置信息 【地图选址】

lnglat 企业位置经纬度 【地图选址】

province 省份 【地图选址】

city 城市 【地图选址】

district 地区 【地图选址】

7.2 账号信息初始化

  1. 从状态机提取数据

  2. 将数据设置给表单所绑定的响应式数据包

7.3 高德地图选址

  1. 搭建 a-drawer 弹窗

  2. 渲染地图 在 vue 中使用高德地图

  3. 点击地图时获取信息

    • 省、市、区

    • 具体位置名称信息

    • 经纬度

  4. 高德地图使用流程

    • 注册、登录、新建应用、创建 key

    • 新建 god-map 组件

      • div 容器、样式

    • 调用 initMap 进行地图的初始化

    • 在 company/pub.vue 中引入、注册、调用地图组件

  5. 使用逆地理编码案例,实现点击地图获取位置信息 参考案例

  6. 配置安全秘钥【切记】

    在 public/index.html 的 head 标签内中配置安全秘钥

<script type="text/javascript">
  window._AMapSecurityConfig = {
    securityJsCode: "1e9ae2832c334c3cfb0d30cff1decc14", //此处换成自己申请key时分配的安全秘钥
  };
</script>

7.4 富文本编辑器使用

VueQuill 富文本编辑器文档

  1. 安装

    npm install @vueup/vue-quill@latest --save
  2. 引入挂载

    import { QuillEditor } from '@vueup/vue-quill'
    import '@vueup/vue-quill/dist/vue-quill.snow.css';
  3. 调用

    contentType="html" 指定富文本获取的内容类型,默认是一个对象

    <el-form-item label="公司简介" prop="intro">
       <div class="editor">
         <QuillEditor v-model:content="ruleForm.intro" contentType="html" />
       </div>
     </el-form-item>

7.5 账号信息更新

  1. 账号更新接口

在通常的情况下,没有人会允许别人来改动他们自己的数据。为了做好权限认证,确保只有用户自己可以修改个人数据,在更新用户信息的时候,必须在 HTTP 头部加入一个 X-LC-Session 项来请求更新,这个 session token 在注册和登录时会返回。

为了改动一个用户已经有的数据,需要对这个用户的 URL 发送一个 PUT 请求。任何你没有指定的 key 都会保持不动,所以你可以只改动用户数据中的一部分。

curl -X PUT \
  -H "X-LC-Id: {{appid}}" \
  -H "X-LC-Key: {{appkey}}" \
  -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \
  -H "Content-Type: application/json" \
  -d '{"phone":"18600001234"}' \
  https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f
  1. 封装更新api

  2. 触发更新请求

八、岗位管理

5.1 岗位录入

  1. 分析岗位数据库表结构

根据设计图的要求,按需规划岗位表:

企业LOGO -- 从用户信息中提取 -- brandLogo

企业名称 -- 从用户信息中提取 -- brandName

地理位置 -- 从用户信息中提取-- 城市cityName、地区areaDistrict、经纬度lnglat

岗位名称 -- 普通输入框 -- jobName

薪资待遇 -- 普通输入框 -- salaryDesc

归属类目 -- Cascader选择器 -- lv1、lv2

福利待遇 -- TreeSelect组件 -- welfareList

技能要求 -- TextArea 组件-- skills

  1. 使用form表单搭建基本结构

  2. 整合岗位数据与企业信息

  3. 录入岗位数据

export const jobPost = (job: JobType) => {
  return request.post("classes/job", job);
};

5.2 岗位备份数据导入及渲染

  1. 在leancloud导入备份json数据

  2. 封装岗位列表请求api

//岗位列表
export const jobGet = () => {
  return request.get("classes/job");
};
  1. 渲染岗位列表

<template>
  <a-row class="search" gutter="20" justify="between">
    <a-col :span="8">
      <a-input placeholder="输入岗位名称查询"></a-input>
    </a-col>
    <a-col :span="8">
      <a-select
        :options="cateList"
        :fieldNames="{ label: 'name', value: 'name' }"
        placeholder="选择岗位类型进行筛选"
      />
    </a-col>
    <a-col :span="6">
      <a-button type="primary">查询</a-button>
    </a-col>
  </a-row>
  <a-table sticky :columns="columns" :data-source="data" :scroll="{ x: 1500 }">
    <template #bodyCell="{ column }">
      <template v-if="column.key === 'operation'">
        <a-space>
          <a-button type="primary" size="small">编辑</a-button>
          <a-button type="primary" danger size="small">删除</a-button>
        </a-space>
      </template>
    </template>
  </a-table>
</template>
<script lang="ts" setup>
import { categoryGet, jobGet } from "@/api/pro";
import { CategoryType } from "@/types/pro";
import type { TableColumnsType } from "ant-design-vue";
import { ref } from "vue";
const columns = ref<TableColumnsType>([
  {
    title: "岗位名称",
    dataIndex: "jobName",
    key: "jobName",
    fixed: "left",
  },
  {
    title: "岗位薪资",
    dataIndex: "salaryDesc",
    key: "salaryDesc",
    fixed: "left",
  },
  {
    title: "岗位分类",
    dataIndex: "lv1",
    key: "lv1",
  },
  {
    title: "城市",
    dataIndex: "cityName",
    key: "cityName",
  },
  {
    title: "地区",
    dataIndex: "areaDistrict",
    key: "areaDistrict",
  },
  {
    title: "公司规模",
    dataIndex: "brandScaleName",
    key: "brandScaleName",
  },
  {
    title: "所属行业",
    dataIndex: "brandIndustry",
    key: "brandIndustry",
  },
  {
    title: "Action",
    key: "operation",
    fixed: "right",
    width: 150,
  },
]);

const data: any = ref([]);
jobGet().then((res) => {
  data.value = res.data.results;
});

//岗位类型
const cateList = ref<CategoryType[]>([]);
categoryGet().then((res) => {
  cateList.value = res.data.results;
});
</script>
<style scoped>
#components-table-demo-summary tfoot th,
#components-table-demo-summary tfoot td {
  background: #fafafa;
}
[data-theme="dark"] #components-table-demo-summary tfoot th,
[data-theme="dark"] #components-table-demo-summary tfoot td {
  background: #1d1d1d;
}
.search {
  margin-bottom: 20px;
}
</style>

5.3 岗位条件查询

  1. 约束查询接口

curl -X GET \
  -H "X-LC-Id: {{appid}}" \
  -H "X-LC-Key: {{appkey}}" \
  -H "Content-Type: application/json" \
  -G \
  --data-urlencode 'where={"pubUser":"官方客服"}' \
  https://{{host}}/1.1/classes/Post

  1. 模糊查询接口

curl -X GET \
  -H "X-LC-Id: {{appid}}" \
  -H "X-LC-Key: {{appkey}}" \
  -H "Content-Type: application/json" \
  -G \
  --data-urlencode 'where={"title":{"$regex":"^WTO.*","$options":"i"}}' \
  https://{{host}}/1.1/classes/Post
  1. 封装模糊查询api

//岗位列表
interface ConditionType {
  jobName: string | { $regex: any; $options: "i" };
  lv1: string;
}
export const jobGet = (condition: ConditionType = {} as ConditionType) => {
  let { jobName, lv1 } = condition;
  let query: ConditionType = {} as ConditionType;
  if (jobName) {
    // query.jobName = jobName; //普通约束查询
    query.jobName = { $regex: jobName, $options: "i" }; //模糊查询
  }
  if (lv1) {
    query.lv1 = lv1;
  }
  let params = JSON.stringify(query);
  return request.get(`classes/job?where=${params}`);
};

九、访问权限控制

9.1 登录成功后获取角色权限信息

  1. 修改 store/modules/user.js,登录成功后用角色 id,请求角色表,获取角色数据

    actions: {
     userLoginAction(context, account) {
       //拿到组件提交的账号密码、向后端发登录请求
       // console.log(account);
       userLogin(account).then(async (res) => {
         console.log("登录成功", res);
         let { roleId } = res.data;
         let role = await roleGet(roleId); //以当前用户的角色id,获取角色权限
         console.log("当前用户的角色数据", role);
         res.data.checkedKeys = role.data.checkedKeys; //给用户信息中,追加权限信息
         context.commit("initInfoMute", res.data); //存入状态机
         localStorage.setItem("vue-admin-2301", JSON.stringify(res.data)); //存入本地存储
         router.push("/"); //登录成功后跳转至主面板
       });
     },
    },
  2. 调整 api/user.js 里面的 roleGet 方法,让它既支持加载角色列表,也支持获取单个角色数据

export const roleGet = (roleid) => {
  let search = roleid ? `/${roleid}` : "";
  return request.get(`classes/VueRole${search}`);
};

9.2 侧边菜单渲染控制

  1. 通过用户权限,处理menu数据

const account = useAccount();
const menuFn = (permission: string[]) => {
  let menu = cloneDeep(routes[0].children) as RouteRecordRaw[];
  function loop(arr: RouteRecordRaw[]) {
    for (let i = arr.length - 1; i >= 0; i--) {
      let bool = !permission.includes(arr[i].path); // 可能为/category
      if (bool) {
        // 在删除前查看permission中有没有相近的路径 ['/category/list']
        let findChild = permission.filter( 
          (item) => item.indexOf(arr[i].path) != -1
        );
        //如果不做这一步判断,/category包会被整体删除,导致无法看到/category/list菜单
        if (!findChild.length) {
          arr.splice(i, 1);
          continue;
        }
      }
      if (arr[i].children) {
        loop(arr[i].children!);
      }
    }
  }
  loop(menu);
  return menu;
};
const menuList = ref<RouteRecordRaw[]>([]);
onMounted(() => {
  menuList.value = menuFn(account.userInfo!.checkedKeys);
});
  1. 使用处理后的menu数据,渲染菜单

<a-menu theme="dark" mode="inline" @click="handleMenu">
    <template v-for="item in menuList">
      <a-menu-item v-if="!item.children" :key="item.path">
        <component :is="item.meta!.icon" />
        <span>{{ item.meta!.label }}</span>
      </a-menu-item>
      <a-sub-menu v-else :key="item.path + '0'">
        <template #title>
          <span>
            <component :is="item.meta!.icon" />
            <span>{{ item.meta!.label }}</span>
          </span>
        </template>
        <!-- <div v-for="child in item.children" v-show="!child.meta.hidden"> -->
        <div v-for="child in item.children">
          <a-menu-item :key="child.path">
            {{ child.meta!.label }}
          </a-menu-item>
        </div>
      </a-sub-menu>
    </template>
  </a-menu>

9.3 路由白名单思路

  1. 路由守卫拦截

  2. 检查已登录用户的 checkedKeys(路由白名单)

  3. 用户当前所访问的路由

    • 如果在白名单内,直接 next

    • 如果不在白名单内,跳转至【没有权限】提示页

9.4 权限控制

  1. 调整路由守卫

router.beforeEach((to, from, next) => {
  // console.log(to, from, next);
  let userInfo = store.state.user.userInfo;
  if (!["/login", "/permission"].includes(to.path)) {
    if (userInfo) {
      let { checkedKeys } = userInfo; //获取当前用户有权访问的路由
      let whiteList = ["/"].concat(checkedKeys);
      let bool = whiteList.includes(to.path); //用户想要访问的路由,是否存在于权限数组中
      if (bool) {
        next(); //有权限用户直接放行
      } else {
        next("/permission");
      }
    } else {
      next("/login"); //没登录的用户,强行跳转到登录
    }
  } else {
    next(); //login、permission直接放行
  }
});
  1. 登录不同账号,测试权限行为

  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue Shop Vite框架是一个基于Vue3和Vite前端快速开发平台。它使用最新的前端技术和架构,。该框架的优点包括快速开发、高效的热重载、灵活的模块化、优秀的性能等。它提供了一套完整的工具和组件,方便开发者构建现代化的Web应用程序。在Vue Shop Vite框架中,可以使用Vuex进行状态管理,可以通过store/index.js文件来创建和配置Vuex实例,。框架还支持Qiankun微前端架构,可以将应用程序拆分成多个独立的子应用,实现灵活的微服务架构,。总的来说,Vue Shop Vite框架是一个快速、灵活且具有良好性能的前端开发框架。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Vue Shop ViteVue Admin Plus、Vue Admin Pro官网、文档](https://blog.csdn.net/qq_16622915/article/details/131136752)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [vue3+vite+qiankun+monorepo框架](https://download.csdn.net/download/qq_38862234/86293271)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Vite+Vue3构建前端框架及模板代码及重要知识点](https://blog.csdn.net/weixin_62650212/article/details/128268486)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值