## 项目创建
创建完成之后删除不必要的文件及资源
## 接口文档说明
**接口文档v1版本:**
https://documenter.getpostman.com/view/12387168/TzsfmQvw
baseURL的值:
```
http://152.136.185.210:5000
http://152.136.185.210:4000
```
设置全局token的方法:
```js
const res = pm.response.json();
pm.globals.set("token", res.data.token);
```
**接口文档v2版本:(有部分更新):**
https://documenter.getpostman.com/view/12387168/TzzDKb12
## 项目中常见配置文件说明
TypeScript 使用 tsconfig.json 文件作为其配置文件,当一个目录中存在 tsconfig.json 文件,则认为该目录为 TypeScript 项目的根目录
### tsconfig.json
```json
{
// 继承
"extends": "@vue/tsconfig/tsconfig.web.json",
// 告知TS哪些文件需要编译
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
// 编译选项
"compilerOptions": {
// 配置路径别名,利于VSCode代码提示和读取
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
// 引用
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
```
在项目中创建好项目后,一般不建议去修改 tsconfig.json 文件,表示比较固定的了已经
但是如果在项目中确实有一些文件配置要单独去配置,建议在 tsconfig.config.json 文件里进行修改
### tsconfig.config.json
```json
{
// 继承
"extends": "@vue/tsconfig/tsconfig.node.json",
// 告知TS哪些文件需要编译
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
// 编译选项
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}
```
### 集成editorconfig配置
EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。
.editorconfig
```yaml
# http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行尾的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
```
VSCode需要安装一个插件:EditorConfig for VS Code
### 使用prettier工具
.prettierrc.json
Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
1.安装prettier
```shell
npm install prettier -D
```
2.配置.prettierrc文件:
* useTabs:使用tab缩进还是空格缩进,选择false;
* tabWidth:tab是空格的情况下,是几个空格,选择2个;
* printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
* singleQuote:使用单引号还是双引号,选择true,使用单引号;
* trailingComma:在多行输入的尾逗号是否添加,设置为 `none`,比如对象类型的最后一个属性后面是否加一个,;
* semi:语句末尾是否要加分号,默认值true,选择false表示不加;
```json
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 80,
"singleQuote": false,
"trailingComma": "none",
"semi": true
}
```
3.创建.prettierignore忽略文件
```
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
```
4.VSCode需要安装prettier的插件
5.VSCod中的配置
- settings =>format on save => 勾选上
-
- settings => editor default format => 选择 prettier
-
6.测试prettier是否生效
* 测试一:在代码中保存代码;
* 测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
```json
"prettier": "prettier --write ."
```
### .eslintrc.cjs
使用ESLint检测
1.在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。
2.VSCode需要安装ESLint插件:
3.解决eslint和prettier冲突的问题:
安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
```shell
npm install eslint-plugin-prettier eslint-config-prettier -D
```
添加prettier插件:.eslintrc.cjs
```json
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
parserOptions: {
ecmaVersion: "latest"
},
rules: {
"@typescript-eslint/no-unused-vars": "off"
}
};
```
4.VSCode中eslint的配置(在react里)
```json
"eslint.lintTask.enable": true,
"eslint.alwaysShowStatus": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
```
### git Husky和eslint
虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:
* 也就是我们希望保证代码仓库中的代码都是符合eslint规范的;
* 那么我们需要在组员执行 `git commit ` 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;
那么如何做到这一点呢?可以通过Husky工具:
* husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push
如何使用husky呢?
这里我们可以使用自动配置命令:
```shell
npx husky-init && npm install
```
**这里会做三件事:**
1.安装husky相关的依赖:
2.在项目目录下创建 `.husky` 文件夹:
3.在package.json中添加一个脚本:
接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本:
这个时候我们执行git commit的时候会自动对代码进行lint校验。
### git commit规范
#### 代码提交风格
通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。
但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:Commitizen
* Commitizen 是一个帮助我们编写规范 commit message 的工具;
1.安装Commitizen
```shell
npm install commitizen -D
```
2.安装cz-conventional-changelog,并且初始化cz-conventional-changelog:
```shell
npx commitizen init cz-conventional-changelog --save-dev --save-exact
```
这个命令会帮助我们安装cz-conventional-changelog:
并且在package.json中进行配置:
这个时候我们提交代码需要使用 `npx cz`:
* 第一步是选择type,本次更新的类型
| Type | 作用 |
| -------- | ------------------------------------------------------------ |
| feat | 新增特性 (feature) |
| fix | 修复 Bug(bug fix) |
| docs | 修改文档 (documentation) |
| style | 代码格式修改(white-space, formatting, missing semi colons, etc) |
| refactor | 代码重构(refactor) |
| perf | 改善性能(A code change that improves performance) |
| test | 测试(when adding missing tests) |
| build | 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等) |
| ci | 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 |
| chore | 变更构建流程或辅助工具(比如更改测试环境) |
| revert | 代码回退 |
* 第二步选择本次修改的范围(作用域)
* 第三步选择提交的信息
* 第四步提交详细的描述信息
* 第五步是否是一次重大的更改
* 第六步是否影响某个open issue
我们也可以在scripts中构建一个命令来执行 cz:
#### 代码提交验证
如果我们按照cz来规范了提交风格,但是依然有同事通过 `git commit` 按照不规范的格式提交应该怎么办呢?
* 我们可以通过commitlint来限制提交;
1.安装 @commitlint/config-conventional 和 @commitlint/cli
```shell
npm i @commitlint/config-conventional @commitlint/cli -D
```
2.在根目录创建commitlint.config.js文件,配置commitlint
```js
module.exports = {
extends: ['@commitlint/config-conventional']
}
```
3.使用husky生成commit-msg文件,验证提交信息:
```shell
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
```
### env.d.ts
放置一些类型定义/类型声明文件
```ts
// 放置一些类型定义/类型声明文件
///
### **CSS样式重置**
下载插件:`npm i normalize.css`
在 main.ts 里引入:
```ts
import "./assets/css/main.scss";
import "normalize.css";
```
### 安装及配置 scss
`npm install node-sass sass-loader --save-dev`
这里需要注意的是执行完以上操作安装好scss后可能会报错,这可能是因为版本太高安装的
还有一种报错:
我们可以执行如下命令:`npm install sass --save-dev` 进行解决
注意:在vue3中安装scss只需 通过`npm i sass sass-loader --save-dev`命令下载
在vite.config.ts里新增sccs全局变量配置,这样就可以在任意.vue文件里直接使用,无需每次引入
vite.config.ts
```js
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
// 配置路径别名,便于Vite在打包时进行转换
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
},
// 配置scss
++ css: {
// css预处理器
preprocessorOptions: {
scss: {
// 引入 varable.scss 这样就可以在全局中使用 mixin.scss中预定义的变量了
// 给导入的路径最后加上 ;
additionalData: '@import "@/assets/css/variable.scss";'
}
}
}
});
```
### 路由及404配置
router/index.ts
```js
import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
redirect: "/main"
},
{
path: "/login",
name: "login",
component: () => import("../views/login/login.vue")
},
{
path: "/main",
name: "main",
component: () => import("../views/home/home.vue")
},
{
path: "/:pathMatch(.*)",
component: () => import("../views/not-found/NotFound.vue")
}
]
});
export default router;
```
App.vue
```vuue
### 状态管理配置
store/index.ts
```ts
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;
```
store/counter.ts
```ts
import { defineStore } from "pinia";
const useCounterStore = defineStore("counter", {
state: () => ({
num: 2
}),
actions: {}
});
export default useCounterStore;
```
main.ts
```ts
...
++import pinia from "./stores";
const app = createApp(App);
++app.use(pinia);
...
```
App.vue
```vue
如何区分开发环境和生产环境?
方式一:区分开发环境和生产环境
```js
// 1.区分开发环境和生产环境
// export const BASE_URL = 'http://aaa.dev:8000'
// export const BASE_URL = 'http://aaa.prod:8000'
```
方式二:代码逻辑判断, 判断当前环境
```js
// 2.代码逻辑判断, 判断当前环境
// vite默认提供的环境变量
// console.log(import.meta.env.MODE)
// console.log(import.meta.env.DEV); // 是否开发环境
// console.log(import.meta.env.PROD); // 是否生产环境
// console.log(import.meta.env.SSR); // 是否是服务器端渲染(server side render)
let BASE_URL = "";
if (import.meta.env.PROD) {
// 生产环境
BASE_URL = "http://152.136.185.210:4000";
} else {
// 开发环境
BASE_URL = "http://152.136.185.210:5000";
}
// console.log(BASE_URL);
export const TIME_OUT = 10000;
export { BASE_URL };
```
方式三:通过创建.env文件直接创建变量
.env
```bash
VITE_BASE_URL=http://zzz
VITE_TIME_OUT=1000
```
.env.development
```bash
VITE_BASE_URL=http://xxx
VITE_TIME_OUT=1000
```
.env.production
```bash
VITE_BASE_URL=http://yyy
VITE_TIME_OUT=1000
```
```bash
console.log(import.meta.env.VITE_BASE_URL);
```
**封装axios请求:**
src/service/config/index.ts
```ts
// 1.区分开发环境和生产环境
// export const BASE_URL = 'http://aaa.dev:8000'
// export const BASE_URL = 'http://aaa.prod:8000'
// 2.代码逻辑判断, 判断当前环境
// vite默认提供的环境变量
// console.log(import.meta.env.MODE)
// console.log(import.meta.env.DEV); // 是否开发环境
// console.log(import.meta.env.PROD); // 是否生产环境
// console.log(import.meta.env.SSR); // 是否是服务器端渲染(server side render)
let BASE_URL = "";
if (import.meta.env.PROD) {
// 生产环境
BASE_URL = "http://152.136.185.210:4000";
} else {
// 开发环境
BASE_URL = "http://152.136.185.210:5000";
}
// console.log(BASE_URL);
// 3.通过创建.env文件直接创建变量
// console.log(import.meta.env.VITE_URL);
export const TIME_OUT = 10000;
export { BASE_URL };
```
src/service/request/type.ts
实例拦截器是为了保证封装的灵活性,因为每一个实例中的拦截后处理的操作可能是不一样的,所以在定义实例时,允许我们传入拦截器。
首先我们定义一下interface,方便类型提示,代码如下:
```ts
import type { AxiosRequestConfig, AxiosResponse } from "axios";
// 针对AxiosRequestConfig配置进行扩展
export interface LWJInterceptors
App.vue
```vue
### 全屏问题
后台管理项目一般都是沾满整屏的,这样才能更方便的布局,因此我们只需在App.vue里进行如下设置即可:
```vue
### Element-Plus 图标使用方式
[官网地址](https://element-plus.gitee.io/zh-CN/component/icon.html)
安装: `npm install @element-plus/icons-vue`
方式一:直接在main.ts里引入使用:
main.ts
```ts
// element-plus 图标全局注册
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
```
方式二:抽离成单独的文件--推荐
src下新建global目录
global/registerIcons.ts
```ts
import type { App } from "vue";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
function registerIcons(app: App
那么如何表示该组件创建出来的实例类型呢?
`const accountRef = ref
### 登录后跳转到首页
当用户输入正确的账号和密码后,点击登录则跳转到后台首页;未登录时则跳转到登录页
这就需要用到路由导航守卫--beforeEach
stores/login/login.ts
```ts
import { defineStore } from "pinia";
...
import router from "@/router";
const useLoginStore = defineStore("login", {
state: () => ({
...
}),
actions: {
async loginAccountAction(account: IAccount) {
// 1.账号登录, 获取token等信息
...
// 2.获取登录用户的详细信息(role信息)
// 3.根据角色请求用户的权限(菜单menus)
// 4.进行本地缓存
// 5. 页面跳转
router.push("/main");
}
}
});
export default useLoginStore;
```
router/index.ts
```ts
// 前置守卫
// 参数: to(跳转到的位置)/from(从哪里跳转过来)
// 返回值: 返回值决定导航的路径(不返回或者返回undefined, 默认跳转)
// 举个栗子: / => /main
// to: /main from: / 返回值: /abc
router.beforeEach((to) => {
// 只有登陆成功有token后,方可真正进入到home页
const token = localCache.getCache(LOGIN_TOKEN);
if (to.path.startsWith("/main") && !token) {
return "/login";
}
});
```
## 菜单权限管理问题
### 概述
**不同账号登录进后台后其展示的内容也就是左侧菜单栏不一样:**
比如张三(主管)登录进后台后可以查看所有的权限即左侧菜单栏完全展示;李四(运营)登录进后台后无法查看管理员信息即左侧菜单不显示对应的菜单项;王五(普通员工)登录进后台后只可查看与自己权限相关的一些页面,其余界面左侧菜单项不显示...
### 如何进行判断
如果根据每个人去进行分配的话就很复杂了,因为职位不同、而且人员是流动的有增有减
这就跟后端有关了,通常叫做RBAC(role based access control)即基于角色的访问控制
后台会根据当前的角色比如总裁、总监、主管、开发人员、运营人员、客服人员/测试人员、普通员工等,分别分配对应的角色:超级管理员(拥有所有权限)、管理员(拥有部分权限)
### 后端实现方案
后台大致设计:至少有三/四张表--用户表-角色表-关系表(记录角色拥有的权限)-权限表
### 前端实现方案
前端在拿到菜单后,我们此时是不知道哪个用户(角色)会登录,因此菜单是不能写死的,是根据用户返回的菜单进行渲染的,根据其对应的菜单映射不同的路由
这里的做法有如下几种:
方式一:
在主页路由下的children里将所有的路由全部注册上,但是会有一个很大的弊端,那就是可以在浏览器里通过手动输入地址进行访问
方式二:
先将不同角色的路由全部注册好,然后当用户登录后,根据其返回的权限信息去进行赛选,将赛选到的路由动态的加载出来,但是这样做也会有一个弊端,那就是如果新增角色了,在前端里原来并没有配置该角色所对应的路由,那么此时就只能修改前端代码,再重新注册
方式三:
获取到菜单后根据菜单去进行动态的生成路由映射。在菜单里是有url的,url在路由里面对应的就是path,有了这个path之后我们可以让这个path对应某个component(组件),这样一来就会产生一个路由数组,再将这个路由数组动态的添加到主页的children里
这里的动态生成就会有两种:一种是后台返回的信息菜单中就存在一个component字段,在该字段里就会存在要加载的组件名称比如role.vue,告诉我们需要去加载哪一个组件,这样的话就是我们创建的组件名称和路径要和后端返回的保持一致;第二种就是菜单里会有url,在前端代码里我们原来就已经配置好了path和component的映射关系,此时我们只需根据url去动态的加载已经配置好了的某个/多个对象
### 获取当前登录用户的详细信息
根据id
用户登录--根据id获取用户详细信息(里面就包含了用户当前的角色)--通过角色去判断当前与用户所拥有的哪些权限进而展示不同的界面
**网络请求:**
service/login/login.ts
```ts
import lwjRequest from "..";
import type { IAccount } from "@/type/index";
export function getUserInfoByIdRequest(id: number) {
return lwjRequest.get({
url: `/users/${id}`
// headers: {
// Authorization: 'token' // 携带token,没得必要在每个请求里都这样去写
// }
});
}
```
**header处理(token):**
service/index.ts
```ts
import { LOGIN_TOKEN } from "@/global/constants";
import { BASE_URL, TIME_OUT } from "./config";
import LWJRequest from "./request";
import { localCache } from "@/utils/cache";
const lwjRequest = new LWJRequest({
...
interceptors: {
requestSuccessFn: (config) => {
// 每一个请求都自动携带token
const token = localCache.getCache(LOGIN_TOKEN);
if (config.headers && token) {
// 类型缩小
config.headers.Authorization = "Bearer " + token;
}
return config;
}
}
});
export default lwjRequest;
```
**状态管理-获取信息:**
stores/login/login.ts
```ts
import { defineStore } from "pinia";
import {
accountLoginRequest,
getUserInfoByIdRequest
} from "@/service/login/login";
import type { IAccount } from "@/type";
import { localCache } from "@/utils/cache";
import { LOGIN_TOKEN } from "@/global/constants";
import router from "@/router";
interface ILoginState {
token: string;
userInfo: any;
}
const useLoginStore = defineStore("login", {
state: (): ILoginState => ({
...
userInfo: {}
}),
actions: {
async loginAccountAction(account: IAccount) {
// 1.账号登录, 获取token等信息
...
// 2.获取登录用户的详细信息(role信息)
const userInfoResult = await getUserInfoByIdRequest(id);
// console.log(userInfoResult);
const userInfo = userInfoResult.data;
this.userInfo = userInfo;
// 3.根据角色请求用户的权限(菜单menus)
// 4.进行本地缓存
localCache.setCache("userInfo", userInfo);
// 5. 页面跳转
router.push("/main");
}
}
});
export default useLoginStore;
```
### 根据角色请求用户的权限
即当前用户所拥有的菜单权限
**网络请求:**
service/login/login.ts
```ts
import lwjRequest from "..";
export function getUserMenusByRoleIdRequest(id: number) {
return lwjRequest.get({
url: `/role/${id}/menu`
});
}
```
**状态管理-获取当前用户所拥有的菜单信息:**
stores/login/login.ts
```ts
import { defineStore } from "pinia";
import {
accountLoginRequest,
getUserInfoByIdRequest,
getUserMenusByRoleIdRequest
} from "@/service/login/login";
import type { IAccount } from "@/type";
import { localCache } from "@/utils/cache";
import { LOGIN_TOKEN } from "@/global/constants";
import router from "@/router";
interface ILoginState {
token: string;
userInfo: any;
userMenus: any;
}
const useLoginStore = defineStore("login", {
// 如何制定state的类型
state: (): ILoginState => ({
...
userMenus: localCache.getCache("userMenus") ?? []
}),
actions: {
async loginAccountAction(account: IAccount) {
// 1.账号登录, 获取token等信息
...
// 2.获取登录用户的详细信息(role信息)
...
// 3.根据角色请求用户的权限(菜单menus)
const userMenusResult = await getUserMenusByRoleIdRequest(
this.userInfo.role.id
);
const userMenus = userMenusResult.data;
this.userMenus = userMenus;
// 4.进行本地缓存
...
localCache.setCache("userMenus", userMenus);
// 5. 页面跳转
router.push("/main");
}
}
});
export default useLoginStore;
```
## 后台主页实现
### 基本布局结构
主要分为三部分:左侧菜单栏、右侧头部、主体内容
组件划分:
views/home/cpns/homeMenu.vue
```vue
menus
```
views/home/cpns/homeHeader.vue
```vue
header
```
views/home/home.vue
```vue
### 左侧菜单栏区域实现
对于这一部分主要分为上下两个区域
#### 顶部图标及标题区域
views/home/cpns/homeMenu.vue
```vue
```
#### 底部菜单区域
##### 手动搭建
views/home/cpns/homeMenu.vue
```vue
```
这样做当展开全部菜单时便会出现滚动条,隐藏滚动条处理方法如下:
views/home/home.vue
```scss
.el-aside {
overflow-x: hidden;
overflow-y: auto;
line-height: 200px;
text-align: left;
cursor: pointer;
background-color: #001529;
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
transition: width 0.3s ease;
&::-webkit-scrollbar {
display: none;
}
}
```
##### 动态创建
注意需要在 el-sub-menu 上添加 index 属性,这样才能保证每个菜单之间互不影响即点击展开第一个菜单时后面的菜单不会同时展开
注意需要在 el-menu-item 上添加 index 属性,这样才可以保证当点击 item 项即每个子菜单时,当前子菜单高亮,不会影响其他子菜单
###### 基本展示
views/home/cpns/homeMenu.vue
```vue
```
###### 利用动态组件加载图标
views/home/cpns/homeMenu.vue
```bash
###### 点击菜单项实现页面切换
路由映射的时候,为了满足所有进入系统的用户,需要注册所有的路由,但是这样做会导致没有权限的用户可以直接通过在浏览器地址栏里输入相应的url路由也可以进行访问未获权的界面
**为了杜绝这种现象的发生,这里采取动态路由的形式进行开发:**
动态路由:根据不同用户(菜单),动态的注册该用户下所拥有的路由,而不是一次性全部将所有的路由进行注册
**动态路由的实现也有两种方案:**
方式一:基于角色的动态路由管理
先将不同角色的路由全部注册好,然后当用户登录后,根据其返回的权限信息去进行赛选,将赛选到的路由动态的加载出来,但是这样做也会有一个弊端,那就是如果新增角色了,在前端里原来并没有配置该角色所对应的路由,那么此时就只能修改前端代码,再重新注册或者由后端解决,由后端返回这个对象
方式二:基于菜单的动态路由管理
获取到菜单后根据菜单去进行动态的生成路由映射。在菜单里是有url的,url在路由里面对应的就是path,有了这个path之后我们可以让这个path对应某个component(组件),这样一来就会产生一个路由数组,再将这个路由数组动态的添加到主页的children里
这里的动态生成就会有两种:
1. 一种是后台返回的信息菜单中就存在一个component字段,在该字段里就会存在要加载的组件名称比如role.vue,告诉我们需要去加载哪一个组件,这样的话就是我们创建的组件名称和路径要和后端返回的保持一致;
2. 第二种就是菜单里会有url,在前端代码里我们原来就已经配置好了path和component的映射关系,此时我们只需根据url去动态的加载已经配置好了的某个/多个对象
**创建页面:**
views/analysis/overview/overview.vue
```vue
核心技术
```
views/analysis/dashboard/dashboard.vue
```vue
商品统计
```
views/system/user/user.vue
```vue
用户
```
views/system/department/department.vue
```vue
部门管理
```
views/system/menu/menu.vue
```vue
菜单管理
```
views/system/role/role.vue
```vue
角色管理
```
views/product/category/category.vue
```vue
商品类别
```
views/product/goods/goods.vue
```vue
商品信息
```
views/story/chat/chat.vue
```vue
你的故事
```
views/story/list/list.vue
```vue
故事列表
```
**添加路由对象:**
router/main/analysis/overview/overview.ts
```ts
export default {
path: "/main/analysis/overview",
component: () => import("../views/analysis/overview/overview.vue")
};
```
router/main/analysis/dashboard/dashboard.ts
```ts
export default {
path: "/main/analysis/dashboard",
component: () => import("../views/analysis/dashboard/dashboard.vue")
};
```
router/main/system/department/department.ts
```ts
const department = () => import("../views/system/department/department.vue");
export default {
path: "/main/system/department",
name: "department",
component: department,
children: []
};
```
router/main/system/menu/menu.vue
```ts
const menu = () => import("../views/system/menu/menu.vue");
export default {
path: "/main/system/menu",
name: "menu",
component: menu,
children: []
};
```
router/main/system/role/role.ts
```ts
export default {
path: "/main/system/role",
component: () => import("../views/system/role/role.vue")
};
```
router/main/system/user/user.ts
```ts
export default {
path: "/main/system/user",
component: () => import("../views/system/user/user.vue")
};
```
router/main/prodct/category/category.ts
```ts
const category = () => import("../views/product/category/category.vue");
export default {
path: "/main/product/category",
name: "category",
component: category,
children: []
};
```
router/main/prodct/goods/goods.ts
```ts
const goods = () => import("../views/product/goods/goods.vue");
export default {
path: "/main/product/goods",
name: "goods",
component: goods,
children: []
};
```
router/main/story/chat/chat.ts
```ts
const chat = () => import("../views/story/chat/chat.vue");
export default {
path: "/main/story/chat",
name: "chat",
component: chat,
children: []
};
```
router/main/story/list/list.ts
```ts
const list = () => import("../views/story/list/list.vue");
export default {
path: "/main/story/list",
name: "list",
component: list,
children: []
};
```
**根据菜单动态的添加路由对象:**
在上面已经将所有路由都放在独立的文件夹里了
思路:
1. 获取菜单(userMenus)
2. 动态获取所有的路由对象,放到数组中:路由对象都在独立的文件夹里,从文件中将所有路由对象先读取到数组中
3. 根据菜单去匹配正确的路由:addRoute('main', xxx)
在 src/stores/login/login.ts里,通过 import.meta.glob 方法去将文件中的所有路由对象读取出来,import.meta.glob的具体用法可参考:https://cn.vitejs.dev/guide/features.html#glob-import
login.ts
```ts
...
// 动态添加路由
// const routes = mapMenusToRoutes(userMenus)
// routes.forEach((route) => router.addRoute('main', route))
const localRoutes: RouteRecordRaw[] = [];
// 读取router/main所有的ts文件
const files: Record
**对上面的代码进行封装处理:**
utils/map-menu.ts
```ts
import type { RouteRecordRaw } from "vue-router";
// 1. 本地路由加载
function loadLocalRoutes() {
// 1.1动态获取所有的路由对象, 放到数组中
// * 路由对象都在独立的文件中
// * 从文件中将所有路由对象先读取数组中
const localRoutes: RouteRecordRaw[] = [];
// 1.2
// 读取router/main所有的ts文件
const files: Record
但是当用户登录进后台系统后,刷新页面再次进行点击时则会出现找不到路由
这是因为当页面刷新时上面动态加载路由的方法并没有在路由里,而是点击登录后才在的
如何解决呢?
stores/login/login.ts
```ts
state: (): ILoginState => ({
token: "",
userInfo: {},
userMenus: []
}),
actions: {
...
// 加载本地数据
loadLocalCacheAction() {
// 1. 用户进行刷新默认加载数据
const token = localCache.getCache(LOGIN_TOKEN);
const userInfo = localCache.getCache("userInfo");
const userMenus = localCache.getCache("userMenus");
if (token && userInfo && userMenus) {
this.token = token;
this.userInfo = userInfo;
this.userMenus = userMenus;
// 2. 动态添加路由
const routes = mapMenusToRoutes(userMenus);
routes.forEach((route) => router.addRoute("main", route));
}
}
}
```
main.ts
```ts
const app = createApp(App);
app.use(registerIcons);
++app.use(pinia);
++const loginStore = useLoginStore();
++loginStore.loadLocalCacheAction();
++app.use(router);
app.mount("#app");
```
这样就可以解决该问题了,但是在maiin.ts里app.use后直接写 `const loginStore = useLoginStore();` 的代码看起来不是很好看,因此我们可以对其进行优化(当然也可以就这样不优化)
对stores/index.ts进行如下优化:
```ts
import { createPinia } from "pinia";
import type { App } from "vue";
import useLoginStore from "./login/login";
const pinia = createPinia();
++function registerStore(app: App
当用户退出系统重新登录进去时,匹配的路由是空白的,也就是main无匹配路由
**正常进来的时候应该匹配到某一个页面,也就是动态注册所有路由中的第一个页面----核心技术**
utils/map-menu.ts
```ts
// 动态注册所有路由中的第一个路由
++ export let firstMenu: any = null;
// 2. 动态添加路由
export function mapMenusToRoutes(userMenus: any[]) {
// 2.1 加载本地路由
...
// 2.2 根据生成的菜单去匹配正确的路径
...
for (const menu of userMenus) {
for (const submenu of menu.children) {
...
// 第一次没有值并且匹配到路由的情况下
++ if (!firstMenu && route) {
firstMenu = submenu;
}
}
}
return routes;
}
```
router/index.ts
```ts
import { firstMenu } from "@/utils/map-menu";
router.beforeEach((to) => {
// 只有登陆成功有token后,方可真正进入到home页
const token = localCache.getCache(LOGIN_TOKEN);
...
// 如果是进入到main里
++ if (to.path === "/main" && token) {
return firstMenu?.url;
}
});
```
当用户第一次登录进系统时路由匹配到所有动态路由中的第一个,这个上面已经实现了,那么接下来要实现的就是左侧菜单也应该默认选中第一个,可能会说上面不是已经匹配了吗?那是因为在 homeMenu.vue 里 `el-menu` 标签里的 `default-active="39"` 属性我已经写死了,这里的39代表的就是第一个,但是这是不严谨的,因为我的第一个菜单后期可能会变,那么对应的39肯定也是会变的,这样就会出现访问不到的情况
**那么如何解决呢,具体如下:**
如果直接按照如下做法会出现当点击其他菜单刷新后会回到第一个菜单
views/home/cpns/homeMenu.vue
```vue
utils/map-menus.ts
```ts
/**
* 3. 根据路径去匹配需要显示的菜单
* @param path 需要匹配的路径
* @param userMenus 所有菜单
*/
export function mapPathToMenu(path: string, userMenus: any[]) {
for (const menu of userMenus) {
for (const submenu of menu.children) {
if (submenu.url === path) {
return submenu;
}
}
}
}
```
views/home/cpns/homeMenu.vue
```vue
### 右侧头部区域实现
头部区域分为左中右三个区域:
#### 基本结构
views/home/cpns/homeHeader.vue
```vue
#### 点击图标展开/收起菜单栏
当点击图标时,当前为展开则折叠;当前为折叠则展开
控制左侧菜单的宽度变化即 `
#### 个人信息区域
views/home/cpns/headerInfo.vue
```vue
utils/map-menu.ts
```ts
// 2. 动态添加路由
export function mapMenusToRoutes(userMenus: any[]) {
// 2.1 加载本地路由
...
// 2.2 根据生成的菜单去匹配正确的路径
const routes: RouteRecordRaw[] = [];
for (const menu of userMenus) {
for (const submenu of menu.children) {
...
if (route) {
// 给route的顶层菜单增加重定向功能(但是只需要添加一次即可)
if (!routes.find((item) => item.path === menu.url)) {
routes.push({ path: menu.url, redirect: route.path });
}
// 将二级菜单对应的路径
routes.push(route);
}
// 第一次没有值并且匹配到路由的情况下--记录第一个被匹配到的菜单
...
}
}
return routes;
}
/**
* 4. 面包屑
* @param path 需要匹配的路径
* @param userMenus 所有菜单
*/
interface IBreadcrumbs {
name: string;
path: string;
}
export function mapPathToBreadcrumbs(path: string, userMenus: any[]) {
// 4.1 定义面包屑
const breadCrumbs: IBreadcrumbs[] = [];
// 4.2 获取面包屑的层级
for (const menu of userMenus) {
for (const submenu of menu.children) {
if (submenu.url === path) {
// breadCrumbs.push(submenu);
// 顶层菜单
breadCrumbs.push({ name: menu.name, path: menu.url });
// 匹配菜单
breadCrumbs.push({ name: submenu.name, path: submenu.url });
}
}
}
return breadCrumbs;
}
```
views/home/cpns/headerBreadcrumb.vue
```vue
```
views/home/cpns/homeMenu.vue
```vue
// 3. el-menu 的默认选中菜单
const route = useRoute();
// const pathMenu = mapPathToMenu(route.path, userMenus);
// console.log(route.path);
// const activeDefault = ref(pathMenu.id + "");
const activeDefault = computed(() => {
const pathMenu = mapPathToMenu(route.path, userMenus);
return pathMenu.id + "";
});
```
## 商品统计页面
### 头部数字展示区域
#### 基本结构
views/analysis/dashboard/dashboard.vue
```vue
views/analysis/dashboard/dashboard.vue
```vue
**基本思路:**封装echarts,统一使用
#### 基本使用
components/echarts/src/baseEcharts.vue
```vue
效果图:
components/echarts/src/baseEcharts.vue
```vue
components/echarts/src/pieEcharts.vue
```vue
components/echarts/src/roseEcharts.vue
```vue
components/echarts/src/roseEcharts.vue
```vue
components/echarts/src/barEcharts.vue
```vue
```
components/echarts/index.ts
```ts
import barEcharts from "./src/roseEcharlineEchartsts.vue";
export { barEcharts };
```
views/analysis/dashboard/dashboard.vue
```vue
components/echarts/src/mapEcharts.vue
```vue
由上图可将该页面分为上下两个部分,上面为搜索区域、下面部分又可分为上中下三个部分,分别为:头部区域、表格区域、分页器区域
### 页面基本结构
views/system/user/user.vue
```vue
### 头部搜索区域开发
####基本布局
views/system/user/cpns/userSearch.vue
```vue
#### 日历组件默认显示切换为中文
由于 Element Plus 的默认语言为英语,因此我们要将其改为中文的话就需要进行国际化配置:https://element-plus.gitee.io/zh-CN/guide/i18n.html
这里主要有两种方式:全局配置和非全局配置
对于全局配置需要element-plus是全局引入的,但是在本项目中我们的element-plus是按需引入的,因此就只能使用非全局配置的方式:
App.vue
```vue
#### 实现重置功能
想要通过resetFields()进行重置,必须要绑定prop在每个el-form-item上以及model在el-form上
views/system/user/cpns/userSearch.vue
```vue
views/system/user/cpns/userSearch.vue
```bash
// 自定义事件
const emit = defineEmits(["queryClick", "resetClick"]);
// 1. 重置
const handleResetClick = () => {
// form中的数据全部重置
formRef.value?.resetFields();
// 将事件出去, content内部重新发送网络请求
};
// 2. 查询
const handleQueryClick = () => {
...
emit("queryClick", searchForm);
};
```
views/system/user/user.vue
```vue
### 内容区域开发
#### 基本布局
views/system/user/user.vue
```vue
#### 封装网络请求
service/system/system.ts
```ts
import lwjRequest from "..";
export function postUsersListData(queryInfo: any) {
return lwjRequest.post({
url: "/users/list",
data: queryInfo
});
}
```
stores/system/type.ts
这里对于将后台返回的数据转换成typesctipt,可在 https://transform.tools/json-to-typescript 这个在线工具上进行转换:
```ts
export interface IUser {
id: number;
name: string;
realname: string;
cellphone: number;
enable: number;
departmentId: number;
roleId: number;
createAt: string;
updateAt: string;
}
export interface ISystemState {
usersList: IUser[];
totalCount: number;
}
```
stores/system/system.ts
```ts
import { defineStore } from "pinia";
import { postUsersListData } from "@/service/system/system";
import type { ISystemState } from "./type";
const useSystemStore = defineStore("system", {
state: (): ISystemState => ({
usersList: [],
totalCount: 0
}),
actions: {
// 1. 获取所有用户
async postUsersListAction() {
const usersListResult = await postUsersListData({});
const { list, totalCount } = usersListResult.data;
this.usersList = list;
this.totalCount = totalCount;
}
}
});
export default useSystemStore;
```
views/system/user/cpns/userContent.vue
**注意:**
`const usersList = systemStore.usersList;`
按照上面的这种做法在渲染的时候是拿不到数据的,因为systemStore.postUsersListAction();是异步请求,因此在const usersList = systemStore.usersList;的时候systemStore.postUsersListAction()早就请求完了,解决的方法一种是利用computed计算属性,另一种是storeToRefs,也就是如下做法:
`const { usersList } = storeToRefs(systemStore);`
```bash
import userSystemStore from "@/stores/system/system";
// 1. 发起action 请求userList 数据
const systemStore = userSystemStore();
systemStore.postUsersListAction();
// 2. 获取userList数据进行展示
const { usersList } = storeToRefs(systemStore);
```
#### 数据展示
在这里其实最重要的就是如何使用作用域插槽,使得表格里的数据展现形式可以由我们自己进行决定
最终效果图:
views/system/user/cpns/userContent.vue
```vue
**解决时间问题:**
利用day.js第三库进行格式化操作,安装:`npm install dayjs`
官网地址:https://day.js.org/docs/zh-CN/installation/node-js
封装:
utils/format_date.ts
```ts
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
// 传入日期转为月日
export function formatUTC(utcString: string, format: string = "YYYY-MM-DD HH:mm:ss") {
return dayjs.utc(utcString).utcOffset(8).format(format);
}
```
views/system/user/cpns/userContent.vue
```bash
#### 删除功能开发
当用户点击删除按钮时就执行删除该行数据,但是如何去判断当前点击的是哪一行呢?
这就需要用到作用域插槽来做,将当前点击的该行的数据id传递给后台,从而实现删除功能
##### 封装请求
service/system/system/ts
```ts
// 根据id删除用户
export function deleteUserById(id: number) {
return lwjRequest.delete({
url: `/users/${id}`
});
}
```
##### 状态管理器
stores/system/system.ts
```ts
actions: {
// 1. 获取所有用户
async postUsersListAction(queryInfo: any) {
...
},
// 2. 根据id删除用户
async deleteUserByIdAction(id: number) {
// 删除数据
const deleteResult = await deleteUserById(id);
// 重新请求新的数据
this.postUsersListAction({ offset: 0, size: 10 });
}
}
```
##### 实现删除功能
views/system/user/cpns/userContent.vue
```vue
##### 封装模态框
views/system/user/cpns/userContent.vue
```vue
我们也可能在其他模块用到获取所有角色和所有部门的请求,因此这里我们就将其放在main.ts里
service/main/main.ts
```ts
import lwjRequest from "..";
// 获取所有角色列表
export function getEntireRoles() {
return lwjRequest.post({
url: "/role/list"
});
}
// 获取所有部门
export function getEntireDepartment() {
return lwjRequest.post({
url: "/department/list"
});
}
```
stores/main/type.ts
```ts
export interface IMainState {
entireRoles: any[];
entireDepartments: any[];
}
```
stores/main/main.ts
```ts
import { defineStore } from "pinia";
import { getEntireRoles, getEntireDepartment } from "@/service/main/main";
import type { IMainState } from "./type";
const useMainStore = defineStore("main", {
state: (): IMainState => ({
entireRoles: [],
entireDepartments: []
}),
actions: {
// 获取所有用户列表/所有部门
async fetchEntireRolesDepartmentAction() {
const roleResult = await getEntireRoles();
const departmentResult = await getEntireDepartment();
this.entireRoles = roleResult.data.list;
this.entireDepartments = departmentResult.data.list;
}
}
});
export default useMainStore;
```
我们可以在用户登录成功之后就去获取相关数据
stores/login/login.ts
```ts
import useMainStore from "../main/main";
// 动态添加路由
...
async loginAccountAction(account: IAccount) {
// 请求所有的角色列表和部门数据
const mainStore = useMainStore();
mainStore.fetchEntireRolesDepartmentAction();
}
// 刷新之后可能就会导致数据没有了,但是由于角色数据和部门数据是经常更新的因此没必要进行本地存储,因此可以在
// 加载本地数据方法里重新请求一次去获取最新的数据
// 当然也可以进行本地存储
loadLocalCacheAction() {
// 请求所有的角色列表和部门数据
const mainStore = useMainStore();
mainStore.fetchEntireRolesDepartmentAction();
// 2. 动态添加路由
}
```
views/system/user/cpns/userModal.vue
```vue
当用户在模态框里输入和选择完对应的信息后,点击“确定”按钮后,需要将里面的数据收集起来,然后发送网络请求,从而显示创建新的用户
**当用户点击了”确认“按钮之后,首先会关闭modal框,然后将数据传递给后台:**
封装请求:
service/system/system.ts
```ts
// 新建用户
export function createUserData(userInfo: any) {
return lwjRequest.post({
url: "/users",
data: userInfo
});
}
```
状态管理:
stores/system/system.ts
```ts
import {
...
createUserData
} from "@/service/system/system";
actions: {
// 3. 新建用户
async createUserDataAction(userInfo: any) {
// 创建用户
const createResult = await createUserData(userInfo);
// 重新请求新的数据
this.postUsersListAction({ offset: 0, size: 10 });
}
}
```
views/sys/user/cpns/userModal.vue
```vue
当用户点击编辑按钮时,弹出modal框并进行数据的回显
可以发现新增用户弹出的modal框和编辑用户弹出的modal框是一样的,只是标题和内容不同而已,他们是可以共用一个modal的
views/system/user/cpns/userContent.vue
```vue
stores/system/system.ts
```ts
...
const useSystemStore = defineStore("system", {
state: (): ISystemState => ({
...
}),
actions: {
// 1. 获取所有用户
++ async postUsersListAction(queryInfo: any) {
++ const usersListResult = await postUsersListData(queryInfo);
...
}
}
});
...
```
views/system/user/cpns/userContent.vue
```vue
## 部门管理
实现了上面用户管理界面的开发,也就基本上完成了所谓的增删改查操作这些做后台管理系统常用的
下面将从这里进行更高级的封装
后面页面的搭建我们只需传入配置文件,即可帮我们完成界面的开发工作
### 页面基本结构
这里的页面基本结构和用户管理是差不多的,因此我们可以直接将用户管理文件夹里的代码拷贝过来改个名字以及更换网络请求,便可以直接使用了
#### 网络请求
service/system/system.ts
```ts
/**针对页面的网络请求:增删改查 */
// 前提是网络接口的请求是规范的
// 不规范时我们需要对url先进行处理后再继续操作
function getPageUrl(UrlName: string) {
/**处理逻辑 */
// return xxx;
}
// 1. 获取数据列表
export function postPageListData(pageName: string, queryInfo: any) {
return lwjRequest.post({
url: `/${pageName}/list`,
data: queryInfo
});
}
```
#### 状态管理
stores/system/type.ts
```ts
export interface ISystemState {
pageList: any[];
pageTotalCount: number;
}
```
stores/system/system.ts
```ts
import { defineStore } from "pinia";
import {
postPageListData
} from "@/service/system/system";
import type { ISystemState } from "./type";
const useSystemStore = defineStore("system", {
state: (): ISystemState => ({
pageList: [],
pageTotalCount: 0
}),
actions: {
/**针对页面的数据进行增删改查 */
// 1. 获取数据列表
async postListAction(pageName: string, queryInfo: any) {
const pageListResult = await postPageListData(pageName, queryInfo);
const { list, totalCount } = pageListResult.data;
this.pageList = list;
this.pageTotalCount = totalCount;
}
}
});
export default useSystemStore;
```
#### 界面实现
views/system/department/department.vue
```vue
views/system/department/department.vue
```vue
service/system/system.ts
```ts
// 2.根据id删除
export function deletePageById(pageName: string, id: number) {
return lwjRequest.delete({
url: `/${pageName}/${id}`
});
}
```
stores/system/system.ts
```ts
import {deletePageById} from "@/service/system/system";
actions: {
// 2. 删除
async deletePageByIdAction(pageName: string, id: number) {
const pageDeleteResult = await deletePageById(pageName, id);
this.postListAction(pageName, { offset: 0, size: 10 });
}
}
```
views/system/department/cpns/pageContent.vue
```bash
// 5. 删除操作
const handleDeleteBtnClick = (id: number) => {
ElMessageBox.confirm("确定要继续执行此操作吗?", "Warning", {
confirmButtonText: "OK",
cancelButtonText: "Cancel",
type: "warning",
icon: markRaw(Delete)
})
.then(() => {
++ systemStore.deletePageByIdAction("department", id);
ElMessage({
type: "success",
message: "删除成功"
});
})
.catch(() => {
ElMessage({
type: "info",
message: "已取消删除"
});
});
};
```
### 实现编辑/新建功能
共用:pageModal.vue 组件
逻辑:父-department.vue;子组件1-pageContent.vue 子组件2-pageModal.vue
编辑和新增共用一个模态框-pageSearch.vue,只是里面的内容不同
如何实现?
在子组件-pageContent.vue将相关方法通过子传父的形式,传递给父组件-department.vue,在父组件-department.vue里自定义方法去“接收”子组件-pageContent.vue传递过来的方法
在父组件-department.vue里通过ref去操作子组件-pageModal.vue,同时在父组件-department.vue里的自定义方法里去调用子组件-pageContent.vue里的方法
从而实现兄弟组件之间的通讯
**网络请求:**
service/system/system.ts
```ts
// 3.新建
export function createPageData(pageName: string, pageInfo: any) {
return lwjRequest.post({
url: `/${pageName}`,
data: pageInfo
});
}
// 4. 编辑
export function editPageData(pageName: string, id: number, pageInfo: any) {
return lwjRequest.patch({
url: `/${pageName}/${id}`,
data: pageInfo
});
}
```
**状态管理:**
stores/system/system.ts
```ts
import { createPageData, editPageData } from "@/service/system/system";
actions: {
// 3. 新建
async createPageDataAction(pageName: string, pageInfo: any) {
const createResult = await createPageData(pageName, pageInfo);
this.postListAction(pageName, { offset: 0, size: 10 });
},
// 4. 编辑
async editPageDataAction(pageName: string, id: number, pageInfo: any) {
const editResult = await editPageData(pageName, id, pageInfo);
this.postListAction(pageName, { offset: 0, size: 10 });
}
}
```
**实现:**
views/system/department/department.vue
```vue
同时我们也在状态管理里,通过pageX等一系列action方法实现对不同url请求地址所对应的数据进行增删改查的操作
即使这样做,但是还是不够完美,达不到我们的最终期待--通过传入配置文件自动生成我们想要的界面!!!
因此下面我们在此基础上进一步进行优化处理
## 高度封装抽取
由于pageSearch.vue pageContent.vue、pageModal.vue,会在项目里的多个文件里用到,甚至是在其他项目里也用到,因此我们在src下新建base-ui文件夹,专门用来存放在本项目会经常用到的组件
我们在这里将 views/system/department 目录下的 department.vue文件复制一份,将复制的department.vue文件命名为department_v1.vue,下面的操作就基于现在的department.vue组件进行操作优化处理
### 封装抽取pageSearch
将 views/system/department/cpns/pageModal.vue 组件赋值到 base-ui/pageSearch 目录下
并在views/system/department/目录下新建子目录config(配置文件存放位置),在config目录下新建searchConfig.ts
#### 思路
通过配置文件--searchConfig.ts将类似于如下图所需的配置传递给父组件-department.vue,在父组件-department.vue里,通过父传子传参的形式- `:searchConfig="searchConfig"` 将searchConfig.ts组件里的配置传递给子组件-pageSearch.vue
在子组件-pageSearch.vue里通过如下形式进行接收处理:
最后进行渲染!!!
#### 具体代码
views/system/department/config/searchConfig.ts--配置文件
```ts
const searchConfig = {
formItems: [
{
type: "input",
prop: "name",
label: "部门名称",
placeholder: "请输入查询的部门名称",
elColSpan: 8,
isClearable: true,
initialValue: ""
},
{
type: "input",
prop: "leader",
label: "部门领导",
placeholder: "请输入查询的部门领导",
elColSpan: 8,
isClearable: true,
initialValue: ""
},
{
type: "date-picker",
prop: "createAt",
label: "创建时间",
elColSpan: 8,
rangeSeparator: "-",
startPlaceholder: "开始时间",
endPlaceholder: "结束时间",
datePickerDaterange: "daterange"
}
]
};
export default searchConfig;
```
views/system/department/department.vue
```vue
**补充:对于如何配置下拉选择器-select**
searchConfig.ts
```ts
const searchConfig = {
formItems: [
{
type: "select",
prop: "enable",
label: "状态",
placeholder: "请选择状态",
options: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
]
}
]
};
export default searchConfig;
```
使用方式:
base-ui/pageSearch/pageSearch.vue
```vue
#### 思路
通过配置文件--contentConfig.ts将类似于如下图所需的配置传递给父组件-department.vue,在父组件-department.vue里,通过父传子传参的形式- `:contentConfig="contentConfig"` 将contentConfig.ts组件里的配置传递给子组件-pageContent.vue
在子组件-pageContent.vue里通过如下形式进行接收处理:
最后进行渲染!!!
#### 具体代码
views/system/department/config/contentConfig.ts
```ts
const contentConfig = {
header: {
title: "部门列表",
btnTitle: "新建部门"
},
propsList: [
// 1.selection 2.index
{ type: "selection", label: "选择", width: "80px" },
{ type: "index", label: "序号", width: "80px" },
{ type: "normal", label: "部门名称", prop: "name", width: "150px" },
{ type: "normal", label: "部门领导", prop: "leader", width: "150px" },
{ type: "normal", label: "上级部门", prop: "parentId", width: "150px" },
// { type: "custom", label: "部门领导", prop: "leader", width: "150px", slotName:'leader' },
// { type: "custom", label: "上级部门", prop: "parentId", width: "150px", slotName:'parent' },
{ type: "timer", label: "创建时间", prop: "createAt" },
{ type: "timer", label: "更新时间", prop: "updateAt" },
{ type: "handler", label: "操作", width: "150px" }
]
};
export default contentConfig;
```
views/system/department/department.vue
```vue
就比如上图里面的 'department' 就是写死了的,那么如何让这个url动态决定?
其实很简单,就是在传入配置文件的时候多添加一个属性即可:
views/system/department/config/contentConfig.ts
```ts
const contentConfig = {
++ pageName: "department",
header: {
...
},
propsList: [...]
};
export default contentConfig;
```
base-ui/pageContent/pageContent.vue
```vue
systemStore.deletePageByIdAction(props.contentConfig.pageName, id);
systemStore.postListAction(props.contentConfig.pageName, queryInfo);
```
### 封装抽取pageModal
#### 思路
通过配置文件--modalConfig.ts将类似于如下图所需的配置传递给父组件-department.vue,在父组件-department.vue里,通过父传子传参的形式- `:modalConfig="modalConfig"` 将modalConfig.ts组件里的配置传递给子组件-ModalContent.vue
在子组件-pageModal.vue里通过如下形式进行接收处理:
最后进行渲染!!!
#### 具体代码
views/system/department/config/type.ts
```ts
export interface IModalConfig {
pageName: string;
header: {
newTitle: string;
editTitle: string;
width: string;
};
formItems: any[];
}
export interface IModalProps {
modalConfig: {
header: {
newTitle: string;
editTitle: string;
width: string;
};
formItems: any[];
};
}
```
views/system/department/config/modalConfig.ts
```ts
import type { IModalConfig } from "@/base-ui/pageModal/type";
const modalConfig: IModalConfig = {
pageName: "department",
header: {
newTitle: "新建部门",
editTitle: "编辑部门",
width: "30%"
},
formItems: [
{
type: "input",
label: "部门名称",
prop: "name",
placeholder: "请输入部门名称"
},
{
type: "input",
label: "部门领导",
prop: "leader",
placeholder: "请输入部门领导"
},
{
type: "select",
label: "上级部门",
prop: "parentId",
placeholder: "请选择上级部门",
options: [
// {
// label: "财务部",
// value: "1"
// }
]
}
]
};
export default modalConfig;
```
在这里就会出现一个问题,模态框里的下拉框里的数据是来源于后台的,而不是写死的,因此在modalConfig.ts文件里类型为type: "select"的里面的options数组就不好处理了:
解决措施如下-department.vue:
views/system/department/department.vue
```vue
1和2的逻辑是一样的,在vue2里对于公共逻辑的抽取用的是mixin,但是在vue3里我们可以通过自定义hoos来进行抽取
在 scr 下新建 hooks 文件夹:
hooks/usePageContent.ts
```ts
import { ref } from "vue";
import type pageContent from "@/base-ui/pageContent/pageContent.vue";
function usePageContent() {
const contentRef = ref
**大致逻辑:**
### 搜索区域
使用高度封装的pageSearch组件快速搭建
views/system/role/config/searchConfig.ts
```ts
const searchConfig = {
formItems: [
{
type: "input",
prop: "name",
label: "角色名称",
placeholder: "请输入查询的角色名称",
elColSpan: 8,
isClearable: true,
initialValue: ""
},
{
type: "input",
prop: "leader",
label: "权限介绍",
placeholder: "请输入权限介绍",
elColSpan: 8,
isClearable: true,
initialValue: ""
},
{
type: "date-picker",
prop: "createAt",
label: "创建时间",
elColSpan: 8,
rangeSeparator: "-",
startPlaceholder: "开始时间",
endPlaceholder: "结束时间",
datePickerDaterange: "daterange"
}
]
};
export default searchConfig;
```
views/system/role/role.vue
```vue
### 主体内容区域
views/system/role/config/contentConfig.ts
```ts
// 注意:prop:所对应的值需要与后台返回的一致
const contentConfig = {
pageName: "role",
header: {
title: "角色列表",
btnTitle: "新建角色"
},
propsList: [
{
type: "selection",
label: "选择",
width: "80px"
},
{
type: "index",
label: "序号",
width: "80px"
},
{
type: "normal",
label: "角色名称",
prop: "name",
width: "100px"
},
{
type: "normal",
label: "角色权限",
prop: "intro",
width: "100px"
},
{
type: "timer",
label: "创建时间",
prop: "createAt"
},
{
type: "timer",
label: "更新时间",
prop: "updateAt"
},
{
type: "handler",
label: "操作",
width: "150px"
}
]
};
export default contentConfig;
```
views/system/role/config/modalConfig.ts
```ts
const modalConfig = {
pageName: "role",
header: {
newTitle: "新建角色",
editTitle: "编辑角色",
width: "30%"
},
formItems: [
{
type: "input",
label: "角色名称",
prop: "name",
placeholder: "请输入角色名称"
},
{
type: "input",
label: "角色介绍",
prop: "intro",
placeholder: "请输入简要角色介绍"
}
]
};
export default modalConfig;
```
views/system/role/role.vue
```vue
### 补充:角色权限
在新建角色对话框里利用element-plus的Tree 树形控件,新增分配角色权限功能,这样在用户模块创建新用户时就可以选择对应的角色,从而实现该用户菜单权限的功能
这里就需要用到插槽的技术,传入自定义的内容--树形控件
#### 获取所有菜单数据
service/main/main.ts
```ts
export function getEntireMenus() {
return lwjRequest.post({
url: "/menu/list"
});
}
```
store/main/type.ts
```ts
export interface IMainState {
...
entireMenus: any[];
}
```
store/main/main.ts
```ts
import {getEntireMenus} from "@/service/main/main";
const useMainStore = defineStore("main", {
state: (): IMainState => ({
...
entireMenus: []
}),
actions: {
// 获取所有用户列表/所有部门/所有菜单
async fetchEntireRolesDepartmentAction() {
...
const menuResult = await getEntireMenus();
...
this.entireMenus = menuResult.data.list;
}
}
});
export default useMainStore;
```
#### 界面展示
views/system/role/config/modalConfig.ts
```ts
const modalConfig = {
...
{
type: "custom",
slotName: "menulist"
}
]
};
export default modalConfig;
```
views/system/role/role.vue
```vue
当创建好角色并勾选好当前角色所具备的权限时,当点击 “确认” 按钮,就会自动创建该角色并分配相应的权限
views/system/role/role.vue
```vue
但是这样就存在一个问题:我们最终是想要在页面(role.vue)里获取,而不是在hooks里获取
**如何解决?**
在role.vue里写一个回调函数,然后将该回调函数传给usePageModal.ts hooks 函数,在该hook函数里判断editCallback是否存在,若存在则将itemDa通过editCallback回调函数传递出去,这样在role.vue里就可以获取到**menuList**了
获取到**menuList**后将其里面的itemData对象里的menuList里的所有id全部取出来,放入到数组里,然后就可以将其设置给编辑弹窗里作为默认值
如何设置?通过tree属性空间里的setCheckedKey方法进行设置
如何将所有id取出来?这里在utils里封装一个工具来进行提取
utils/map-menu.ts
```ts
/**
* 5. 菜单映射到id列表
* @param menuList
*/
export function mapMenuListToIds(menuList: any[]) {
const ids: number[] = [];
function recurseGetId(menus: any[]) {
for (const item of menus) {
if (item.children) {
recurseGetId(item.children);
} else {
ids.push(item.id);
}
}
}
recurseGetId(menuList);
return ids;
}
```
hoosk/usePageModal.ts
```ts
import { ref } from "vue";
import type pageModal from "@/base-ui/pageModal/pageModal.vue";
++ type EditFnType = (data: any) => void;
function usePageModal(editCallback?: EditFnType) {
const modalRef = ref
#### 解决新建角色时显示回显
当我们点击编辑的时候正常进行回显,当前角色所具有的权限
但是当我们点击新建时,会发现也会默认选中角色权限,这是因为我们在进行编辑以及新建时都是用的同一个 El-tree 组件
**解决的办法:**
1. 新建和编辑时,各自用一个el-tree,不要用同一个
2. 用同一个el-tree,但是在点击新建的时候,让默认回显回归到“0”,即未选择状态--本项目采用
hooks/usePageModal.ts
```ts
import { ref } from "vue";
import type pageModal from "@/base-ui/pageModal/pageModal.vue";
++type CallbackFnType = (data?: any) => void;
function usePageModal(
++ createCallback?: CallbackFnType,
editCallback?: CallbackFnType
) {
const modalRef = ref
## 菜单管理
### 主体内容区域
对于菜单管理这一部分这里只做中间内容展示区域,至于头部的搜索区域其实就是和上面做角色管理一样传入配置文件即可
这里之所以做菜单管理内容区域是因为table表格里的每一项是可以展开的
对 base-ui/pageContent.vue进行修改:
```vue
## 按钮权限管理
用户登录进系统后,返回的信息里就包含了该用户所具有的一些权限:菜单权限以及按钮权限
我们通过什么去进行判断该用户是否拥有按钮权限呢?
方式一:
通过 id 进行判断,但是 id 是动态生成的,当我们对数据进行操作的时候,id可能是变化的
方式二:
通过 name 进行判断,但是name是一些文本,是中文,这种中文的文本存在很大的一种随机性
方式三:
通过后台设计时专门设置的字段,这里比如-permission
### 映射按钮权限
utils/map-menu.ts
```ts
/**
* 6. 按钮权限
* @param menuList 菜单列表
* @returns 返回的权限数组
*/
export function mapUserMenuListToPermissions(menuList: any[]) {
const permissions: string[] = [];
function recurseGetPermission(menus: any[]) {
for (const item of menus) {
if (item.type === 3) {
permissions.push(item.permission);
} else {
recurseGetPermission(item.children ?? []);
}
}
}
recurseGetPermission(menuList);
return permissions;
}
```
store/login/login.ts
```ts
import {mapUserMenuListToPermissions} from "@/utils/map-menu";
interface ILoginState {
...
permissions: string[];
}
const useLoginStore = defineStore("login", {
// 如何制定state的类型
state: (): ILoginState => ({
...
permissions: []
}),
actions: {
async loginAccountAction(account: IAccount) {
// 1.账号登录, 获取token等信息
...
// 2.获取登录用户的详细信息(role信息)
...
// 3.根据角色请求用户的权限(菜单menus)
...
// 4.进行本地缓存
...
// 获取登录用户的所有按钮权限
const permissions = mapUserMenuListToPermissions(userMenus);
this.permissions = permissions;
// 动态添加路由
...
// 请求所有的角色列表和部门数据
...
// 5. 页面跳转
...
},
// 加载本地数据
loadLocalCacheAction() {
// 1. 用户进行刷新默认加载数据
...
if (token && userInfo && userMenus) {
...
// 请求所有的角色列表和部门数据
...
// 获取按钮权限
// 获取登录用户的所有按钮权限
const permissions = mapUserMenuListToPermissions(userMenus);
this.permissions = permissions;
// 2. 动态添加路由
...
}
}
}
});
export default useLoginStore;
```
### 实现按钮权限
hooks/usePermissions.ts
```ts
import useLoginStore from "@/stores/login/login";
function usePermissions(permissionID: string) {
const loginStore = useLoginStore();
const { permissions } = loginStore;
// 将结果转为boolean类型
// return Boolean(permissions.find((item) => item.includes(permissionID))) // 或者下面做法
return !!permissions.find((item) => item.includes(permissionID));
}
export default usePermissions;
```
base-ui/pageContent/pageContent.vue
```vue