# vue3-vuex-ts-cms **Repository Path**: liu-zhiyong-wx/vue3-vuex-ts-cms ## Basic Information - **Project Name**: vue3-vuex-ts-cms - **Description**: 基于vue3+ts+vuex+vue-router利用高级组件封装实现的一个后台管理系统 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-03-31 - **Last Updated**: 2023-03-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue3-vuex-ts-cms ## 大致效果 本项目是基于Vue3 + TS + VueX 实现的一个后台管理系统,大致的功能点有:权限控制,登录不同账号系统会显示不同的菜单栏、常见的增删改查、Element-plus的使用、Echarts5.x的基本使用、富文本编辑器的使用等 在该项目中将主要采用组件化、模块化的思想 **大致效果如下:** 1 2 3 4 5 ## 项目创建 1 1 ## 接口文档说明 **接口文档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 ## 项目中常见配置文件说明 ### 集成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 7 ### tsconfig.json TypeScript 使用 tsconfig.json 文件作为其配置文件,当一个目录中存在 tsconfig.json 文件,则认为该目录为 TypeScript 项目的根目录 ```json { "compilerOptions": { // 目标代码(ts-js(es5/6/7)) "target": "esnext", // 目标代码需要使用的模块化方案 "module": "esnext", // 严格模式 "strict": true, // 需不需要对jsx代码进行某些处理 "jsx": "preserve", // 辅助的导入功能 "importHelpers": true, // 按照node的方式去解析模块 "moduleResolution": "node", // 跳过对一些库的类型检测,比如:axios等 "skipLibCheck": true, // export default 和 module.exports = {} 是否可以混合使用 "esModuleInterop": true, "allowSyntheticDefaultImports": true, // 是否强制代码中使用的模块文件名必须和文件系统中的文件名保持大小写一致 "forceConsistentCasingInFileNames": true, // 将 class 声明中的字段语义从 [[Set]] 变更到 [[Define]] "useDefineForClassFields": true, // 是否需要生成映射文件 "sourceMap": true, // 文件路径在解析时,基本的url--基于当前路径 "baseUrl": ".", // 指定具体要解析的类型 "types": ["webpack-env"], // 编译阶段的路径解析 "paths": { "@/*": ["src/*"] }, // 可以指定在项目中可以使用哪些库的类型 "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, // 当前有哪些文件是需要进行解析的 "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx", "auto-imports.d.ts", "components.d.ts" ], // 排除哪些文件是无需解析的 "exclude": ["node_modules"] } ``` 在项目中创建好项目后,一般不建议去修改 tsconfig.json 文件,表示比较固定的了已经 但是如果在项目中确实有一些文件配置要单独去配置,建议在 tsconfig.config.json 文件里进行修改 ### 使用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的插件 9 5.VSCod中的配置 - settings =>format on save => 勾选上 - 10 - settings => editor default format => 选择 prettier - 10 6.测试prettier是否生效 * 测试一:在代码中保存代码; * 测试二:配置一次性修改的命令; 在package.json中配置一个scripts: ```json "prettier": "prettier --write ." ``` ### .eslintrc.cjs 使用ESLint检测 1.在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。 2.VSCode需要安装ESLint插件: 7 3.解决eslint和prettier冲突的问题: 安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装),未选择时需要手动安装: ```shell npm install eslint-plugin-prettier eslint-config-prettier -D npm uninstall eslint-plugin-prettier eslint-config-prettier -D ``` 对 .eslintrc.js 进行修改: ```json extends: [ "plugin:vue/vue3-essential", "eslint:recommended", "@vue/typescript/recommended", "@vue/prettier", ++ "@vue/prettier/@typescript-eslint", ++ 'plugin:prettier/recommended' ], ``` **注意:** 此时可能会报:`ERROR in [eslint] Failed to load config "@vue/prettier" to extend from.` 错误 解决的办法就是:`npm install @vue/eslint-config-prettier @vue/eslint-config-typescript -D` 即可 解决完后可能还是会报另一个错:`ERROR in [eslint] Failed to load config "@vue/prettier/@typescript-eslint" to extend from.` 原因就是 @vue/eslint-config-prettier 版本过新 ,解决的办法就是:降低版本:`npm install @vue/eslint-config-prettier@6/7.* -D` ### git Husky和eslint 虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了: 16 * 也就是我们希望保证代码仓库中的代码都是符合eslint规范的; * 那么我们需要在组员执行 `git commit ` 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复; 那么如何做到这一点呢?可以通过Husky工具: * husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push 如何使用husky呢? 这里我们可以使用自动配置命令: ```shell npx husky-init ; npm install ``` **这里会做三件事:**就像下面这样一步一步去做,但是没得必要,我们可直接通过上面的命令一步完成 1.安装husky相关的依赖: 16 2.在项目目录下创建 `.husky` 文件夹: 16 3.在package.json中添加一个脚本: 16 接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本: 16 这个时候我们执行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: 16 并且在package.json中进行配置: 16 这个时候我们提交代码需要使用 `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 | 代码回退 | 16 * 第二步选择本次修改的范围(作用域) 16 * 第三步选择此次提交的描述信息 16 * 第四步提交详细的描述信息,可写可不写 16 * 第五步是否是一次重大的更改--N 16 * 第六步是否影响某个open issue--N 16 我们也可以在scripts中构建一个命令来执行 cz: **这样依赖我们在提交代码的时候就需要按照如下步骤进行:** 16 #### 代码提交验证 如果我们按照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" ``` 会在 .husky 目录下自动生成如下文件: 16 ### vue.config.js配置 vue.config.js有三种配置方式: * 方式一:直接通过CLI提供给我们的选项来配置: * 比如publicPath:配置应用程序部署的子目录(默认是 `/`,相当于部署在 `https://www.my-app.com/`); * 比如outputDir:修改输出的文件夹; * 方式二:通过configureWebpack修改webpack的配置: * 可以是一个对象,直接会被合并; * 可以是一个函数,会接收一个config,可以通过config来修改配置; * 方式三:通过chainWebpack修改webpack的配置: * 是一个函数,会接收一个基于 [webpack-chain](https://github.com/mozilla-neutrino/webpack-chain) 的config对象,可以对配置进行修改; ```js const path = require('path') module.exports = { outputDir: './build', // configureWebpack: { // resolve: { // alias: { // views: '@/views' // } // } // } // configureWebpack: (config) => { // config.resolve.alias = { // '@': path.resolve(__dirname, 'src'), // views: '@/views' // } // }, chainWebpack: (config) => { config.resolve.alias.set('@', path.resolve(__dirname, 'src')).set('views', '@/views') } } ``` ### VSCode配置 ```json { "workbench.iconTheme": "vscode-great-icons", "editor.fontSize": 17, "eslint.migration.2_x": "off", "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "files.autoSave": "afterDelay", "editor.tabSize": 2, "terminal.integrated.fontSize": 16, "editor.renderWhitespace": "all", "editor.quickSuggestions": { "strings": true }, "debug.console.fontSize": 15, "window.zoomLevel": 1, "emmet.includeLanguages": { "javascript": "javascriptreact" }, "explorer.confirmDragAndDrop": false, "workbench.tree.indent": 16, "javascript.updateImportsOnFileMove.enabled": "always", "editor.wordWrap": "on", "path-intellisense.mappings": { "@": "${workspaceRoot}/src" }, "hediet.vscode-drawio.local-storage": "eyIuZHJhd2lvLWNvbmZpZyI6IntcImxhbmd1YWdlXCI6XCJcIixcImN1c3RvbUZvbnRzXCI6W10sXCJsaWJyYXJpZXNcIjpcImdlbmVyYWw7YmFzaWM7YXJyb3dzMjtmbG93Y2hhcnQ7ZXI7c2l0ZW1hcDt1bWw7YnBtbjt3ZWJpY29uc1wiLFwiY3VzdG9tTGlicmFyaWVzXCI6W1wiTC5zY3JhdGNocGFkXCJdLFwicGx1Z2luc1wiOltdLFwicmVjZW50Q29sb3JzXCI6W1wiRkYwMDAwXCIsXCIwMENDNjZcIixcIm5vbmVcIixcIkNDRTVGRlwiLFwiNTI1MjUyXCIsXCJGRjMzMzNcIixcIjMzMzMzM1wiLFwiMzMwMDAwXCIsXCIwMENDQ0NcIixcIkZGNjZCM1wiLFwiRkZGRkZGMDBcIl0sXCJmb3JtYXRXaWR0aFwiOjI0MCxcImNyZWF0ZVRhcmdldFwiOmZhbHNlLFwicGFnZUZvcm1hdFwiOntcInhcIjowLFwieVwiOjAsXCJ3aWR0aFwiOjExNjksXCJoZWlnaHRcIjoxNjU0fSxcInNlYXJjaFwiOnRydWUsXCJzaG93U3RhcnRTY3JlZW5cIjp0cnVlLFwiZ3JpZENvbG9yXCI6XCIjZDBkMGQwXCIsXCJkYXJrR3JpZENvbG9yXCI6XCIjNmU2ZTZlXCIsXCJhdXRvc2F2ZVwiOnRydWUsXCJyZXNpemVJbWFnZXNcIjpudWxsLFwib3BlbkNvdW50ZXJcIjowLFwidmVyc2lvblwiOjE4LFwidW5pdFwiOjEsXCJpc1J1bGVyT25cIjpmYWxzZSxcInVpXCI6XCJcIn0ifQ==", "hediet.vscode-drawio.theme": "Kennedy", "editor.fontFamily": "Source Code Pro, 'Courier New', monospace", "editor.smoothScrolling": true, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "workbench.colorTheme": "Atom One Dark", "vetur.completion.autoImport": false, "security.workspace.trust.untrustedFiles": "open", "eslint.lintTask.enable": true, "eslint.alwaysShowStatus": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } } ``` ### shims-vue.d.ts 给 typescript 做的适配定义文件,因为.vue 文件不是一个常规的文件类型,ts 是不能理解 vue 文件是干嘛的,加这一段是是告诉 ts,vue 文件是这种类型的。这一段删除,会发现 import 的所有 vue 类型的文件都会报错 ```ts declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` ### env.d.ts 放置一些类型定义/类型声明文件 ```ts // 放置一些类型定义/类型声明文件 /// declare module "*.vue" { import type { defineComponent } from "vue"; const component: defineComponent; export default component; } declare module "*.json"; ``` ## 初始化项目 ### 项目目录结构 31 ### **CSS样式重置** 下载插件:`npm i normalize.css` 在 main.ts 里引入: ```ts import "normalize.css"; ``` ### 路由及404配置 router/index.ts ```js import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; const routes: Array = [ { 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"), }, ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); export default router; ``` App.vue ```vuue ``` **404配置:** 当用户访问的页面不存在时,提示用户 views/not-found/not-found.vue ```vue ``` 1 ### Element-plus集成 Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库: * 相信在Vue2中都使用过element-ui,而element-plus正是element-ui针对于vue3开发的一个UI组件库; * 它的使用方式和很多其他的组件库是一样的,所以学会element-plus,其他类似于ant-design-vue、NaiveUI、VantUI都是差不多的; 通过命令:`npm install element-plus --save`,进行下载 [官网地址](https://element-plus.gitee.io/zh-CN/guide/installation.html#%E4%BD%BF%E7%94%A8%E5%8C%85%E7%AE%A1%E7%90%86%E5%99%A8) #### 全局引入 main.ts ```ts // main.ts 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') ``` 如果使用 Volar,请在 `tsconfig.json` 中通过 `compilerOptions.type` 指定全局组件类型 ```json // tsconfig.json { "compilerOptions": { // ... "types": ["element-plus/global"] } } ``` #### 局部引入 也就是在开发中用到某个组件对某个组件进行引入: ```vue ``` 但是我们会发现是没有对应的样式的,引入样式有两种方式: * 全局引用样式(像之前做的那样); * 局部引用样式(通过babel的插件); 1.安装babel的插件: ```shell npm install babel-plugin-import -D ``` 2.配置babel.config.js ```js module.exports = { plugins: [ [ 'import', { libraryName: 'element-plus', customStyleName: (name) => { return `element-plus/lib/theme-chalk/${name}.css` } } ] ], presets: ['@vue/cli-plugin-babel/preset'] } ``` 但是这里依然有个弊端: * 这些组件我们在多个页面或者组件中使用的时候,都需要导入并且在components中进行注册; * 所以我们可以将它们在全局注册一次; global/register-element.ts ```ts import { App } from 'vue' import { ElButton, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElCheckbox, ElLink, ElMenu, ElMenuItem, ElSubmenu, ElContainer, ElAside, ElHeader, ElMain, ElDropdown, ElDropdownMenu, ElDropdownItem, ElAvatar, ElButtonGroup, ElCol, ElRow, ElSelect, ElOption, ElDatePicker, ElBreadcrumb, ElBreadcrumbItem, ElTable, ElTableColumn, ElPagination, ElConfigProvider, ElDialog, ElImage, ElTree, ElDescriptions, ElDescriptionsItem, ElTag, ElCard, ElTooltip } from 'element-plus' import 'element-plus/lib/theme-chalk/base.css' const components = [ ElButton, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElCheckbox, ElLink, ElMenu, ElSubmenu, ElMenuItem, ElContainer, ElAside, ElHeader, ElMain, ElDropdown, ElDropdownMenu, ElDropdownItem, ElAvatar, ElButtonGroup, ElRow, ElCol, ElSelect, ElOption, ElDatePicker, ElBreadcrumb, ElBreadcrumbItem, ElTable, ElTableColumn, ElPagination, ElConfigProvider, ElDialog, ElImage, ElTree, ElDescriptions, ElDescriptionsItem, ElTag, ElCard, ElTooltip ] function registerElement(app: App): void { for (const cpn of components) { app.component(cpn.name, cpn) } } export default registerElement ``` global/index.ts ```ts import { App } from 'vue' import registerElement from './register-element' import registerProperties from './register-properties' export default function (app: App): void { registerElement(app) registerProperties(app) } ``` main.ts ```ts import { createApp } from 'vue' import App from './App.vue' import './assets/css/index.css' import 'normalize.css' import router from './router' import store, { setupStore } from './store' ++ import registerApp from './global' const app = createApp(App) ++ registerApp(app) setupStore() app.use(router).use(store).mount('#app') ``` #### 按需引入 安装 `npm install -D unplugin-vue-components unplugin-auto-import` 在vue.config.ts中进行配置: ```ts // webpack.config.js const { defineConfig } = require("@vue/cli-service"); const AutoImport = require('unplugin-auto-import/webpack') const Components = require('unplugin-vue-components/webpack') const { ElementPlusResolver } = require('unplugin-vue-components/resolvers') module.exports = defineConfig({ transpileDependencies: true, ... configureWebpack: { plugins: [ AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], }, }); ``` tsconfig.json:类型提示 ```json "include": [ "env.d.ts", "src/**/*", "src/**/*.vue", ++ "auto-imports.d.ts", ++ "components.d.ts" ], ``` 17 App.vue ```vue ``` 17 ### 区分开发/生成环境配置 **在开发中,有时候我们需要根据不同的环境设置不同的环境变量,常见的有三种环境:** 开发环境:development; 生产环境:production; 测试环境:test; **如何区分环境变量呢?常见有三种方式:** 1. 方式一:手动修改不同的变量; ```bash export const API_BASE_URL = 'https://coderwhy/org/dev' export const API_BASE_URL = 'https://coderwhy/org/prod' ``` 2. 方式二:根据process.env.NODE_ENV的值进行区分; ```bash let baseURL = '' if (process.env.NODE_ENV === 'production') { baseURL = 'https://coderwhy/org/prod' } else if (process.env.NODE_ENV === 'development') { baseURL = 'https://coderwhy/org/dev' } else { baseURL = 'https://coderwhy/org/test' } ``` 3. 方式三:编写不同的环境变量配置文件; .env.development ```bash VUE_APP_BASE_URL=/api ``` .env.production ```bash VUE_APP_BASE_URL=http://152.136.185.210:4000/ ``` .env.test ```bash VUE_APP_BASE_URL=https://lwj/org/test ``` ```bash export const API_BASE_URL = process.env.VUE_APP_BASE_URL; export const TIME_OUT = 10000; ``` 可通过如下方式进行读取: ```bash console.log(process.env.VUE_APP_XXX) ``` ### axios集成 **功能特点:** - 在浏览器中发送 XMLHttpRequests 请求 - 在 node.js 中发送 http请求 - 支持 Promise API - 拦截请求和响应 - 转换请求和响应数据 等等 33 32 **为什么要创建axios的实例呢?** - 当我们从axios模块中导入对象时, 使用的实例是默认的实例. - 当给该实例设置一些默认配置时, 这些配置就被固定下来了. - 但是后续开发中, 某些配置可能会不太一样. - 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等. - 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息. **axios的也可以设置拦截器:拦截每次请求和响应** axios.interceptors.request.use(请求成功拦截, 请求失败拦截) axios.interceptors.response.use(响应成功拦截, 响应失败拦截) **安装axios:** ```shell npm install axios@1.1.3 ``` **封装axios:** 34 utils/cache.ts ```ts class LocalCache { setCache(key: string, value: any) { window.localStorage.setItem(key, JSON.stringify(value)); } getCache(key: string) { const value = window.localStorage.getItem(key); if (value) { return JSON.parse(value); } } deleteCache(key: string) { window.localStorage.removeItem(key); } clearLocal() { window.localStorage.clear(); } } export default new LocalCache(); ``` service/request/config.ts ```ts // 1.区分环境变量方式一: // export const API_BASE_URL = 'https://coderwhy/org/dev' // export const API_BASE_URL = 'https://coderwhy/org/prod' // 2.区分环境变量方式二: // let baseURL = '' // if (process.env.NODE_ENV === 'production') { // baseURL = 'https://coderwhy/org/prod' // } else if (process.env.NODE_ENV === 'development') { // baseURL = 'https://coderwhy/org/dev' // } else { // baseURL = 'https://coderwhy/org/test' // } // 3.区分环境变量方式三: 加载.env文件 export const API_BASE_URL = process.env.VUE_APP_BASE_URL; export const TIME_OUT = 10000; ``` service/request/type.ts ```ts export interface Result { code: number; data: T; } ``` service/request/request.ts ```ts import axios from "axios"; import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from "axios"; import { ElLoading } from "element-plus"; // 以前的引入方式 // import { ILoadingInstance } from "element-plus/lib/el-loading/src/loading.type"; import { LoadingInstance } from "element-plus/lib/components/loading/src/loading"; interface LWJRequestConfig extends AxiosRequestConfig { showLoading?: boolean; interceptorHooks?: InterceptorHooks; } interface InterceptorHooks { requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig; requestInterceptorCatch?: (error: any) => any; responseInterceptor?: (response: AxiosResponse) => AxiosResponse; responseInterceptorCatch?: (error: any) => any; } interface HYData { data: T; returnCode: string; success: boolean; } class HYRequest { config: AxiosRequestConfig; interceptorHooks?: InterceptorHooks; showLoading: boolean; loading?: LoadingInstance; instance: AxiosInstance; constructor(options: LWJRequestConfig) { this.config = options; this.interceptorHooks = options.interceptorHooks; this.showLoading = options.showLoading ?? true; this.instance = axios.create(options); this.setupInterceptor(); } setupInterceptor(): void { this.instance.interceptors.request.use( this.interceptorHooks?.requestInterceptor, this.interceptorHooks?.requestInterceptorCatch ); this.instance.interceptors.response.use( this.interceptorHooks?.responseInterceptor, this.interceptorHooks?.responseInterceptorCatch ); this.instance.interceptors.request.use((config) => { if (this.showLoading) { this.loading = ElLoading.service({ lock: true, text: "Loading", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }); } return config; }); this.instance.interceptors.response.use( (res) => { this.loading?.close(); return res; }, (err) => { this.loading?.close(); return err; } ); } request(config: LWJRequestConfig): Promise { if (!config.showLoading) { this.showLoading = false; } return new Promise((resolve, reject) => { this.instance .request>(config) .then((res) => { resolve(res.data); this.showLoading = true; }) .catch((err) => { reject(err); this.showLoading = true; }); }); } get(config: LWJRequestConfig): Promise { return this.request({ ...config, method: "GET" }); } post(config: LWJRequestConfig): Promise { return this.request({ ...config, method: "POST" }); } delete(config: LWJRequestConfig): Promise { return this.request({ ...config, method: "DELETE" }); } patch(config: LWJRequestConfig): Promise { return this.request({ ...config, method: "PATCH" }); } } export default HYRequest; ``` service/index.ts ```ts import LWJRequest from "./request/request"; import { API_BASE_URL, TIME_OUT } from "./request/config"; import localCache from "@/utils/cache"; const lwjRequest = new LWJRequest({ baseURL: API_BASE_URL, timeout: TIME_OUT, interceptorHooks: { requestInterceptor: (config) => { const token = localCache.getCache("token"); if (token) { config.headers!.Authorization = `Bearer ${token}`; } return config; }, requestInterceptorCatch: (err) => { return err; }, responseInterceptor: (res) => { // return res.data; return res; }, responseInterceptorCatch: (err) => { return err; }, }, }); export default lwjRequest; ``` ### 全屏问题 后台管理项目一般都是占满整屏的,这样才能更方便的布局,因此我们只需在App.vue里进行如下设置即可: ```vue ``` ## 登录页开发 35 当用户进入到后台系统时,若处于未登录状态,则跳转到登录界面 反之进入主页 ### **布局思路** 大致分为顶部标题区域、中间tab栏区域以及底部的控制区域、按钮区域组成 35 ### 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) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } } export default registerIcons; ``` main.ts ```ts import registerIcons from "./global/register-icons"; const app = createApp(App); app.use(registerIcons); ``` ### 封装本地存储工具 utils/cache.ts ```ts enum CacheType { Local, Session } class Cache { storage: Storage constructor(type: CacheType) { this.storage = type === CacheType.Local ? localStorage : sessionStorage } setCache(key: string, value: any) { if (value) { this.storage.setItem(key, JSON.stringify(value)) } } getCache(key: string) { const value = this.storage.getItem(key) if (value) { return JSON.parse(value) } } removeCache(key: string) { this.storage.removeItem(key) } clear() { this.storage.clear() } } const localCache = new Cache(CacheType.Local) const sessionCache = new Cache(CacheType.Session) export { localCache, sessionCache } ``` ### 基本结构 2 自定义表单校验规则:https://element-plus.gitee.io/zh-CN/component/form.html#%E8%A1%A8%E5%8D%95%E6%A0%A1%E9%AA%8C el-form必须要有的属性:rules--匹配规则 model--指定当前绑定的表单对象 el-form-item必须要有的属性:prop-指明具体匹配的名称 el-inpit必须要有的属性:v-model--双向绑定 views/login/login.vue ```vue ``` views/login/cpns/loginPanel.vue ```vue ``` views/login/cpns/panelAccount.vue ```vue ``` views/login/cpns/panelPhone.vue ```vue ``` ### 解决element-plus内置组件ElMessage等不生效问题 ```bash ElMessage({ message: "Oops, 请您输入正确的格式后再操作~~.", type: "error" }); ``` 这里其实是生效了的,只是由于样式未生效 这里有要以下几种方案去解决: **第一种:在main.ts里直接将所有element-plus组件的样式全部引入** main.ts ```ts import 'element-plus/dist/index.css' 第一种方式是在main.ts里全部引入 ``` **第二种:在main.ts里在element-plus里找到相应的组件样式然后引入** main.ts ```ts // ElMessage、ElLoading等内置组件样式的引入 import "element-plus/theme-chalk/el-message.css"; ``` **第三种:通过第三方插件实现自动引入** ### 点击登录按钮 当点击登录时获取到表单里填写的数据,然后发送给后端认证,认证通过后跳转到首页 通过分析不难发现登录按钮在父组件--loginPanel.vue里,而数据则在子组件--panelAccount.vue里,那么如何才能获取到数据? 通过 ref 在父组件loginPanel.vue里给子组件--panelAccount.vue绑定ref,通过ref去操作子组件--panelAccount.vue里的方法或者获取数据等操作 登录逻辑是写在父组件--loginPanel.vue里?还是写在子组件--panelAccount.vue里?这里选择写在子组件--panelAccount.vue里,因为如果将登录逻辑写在父组件--loginPanel.vue里,那么有一天我还做手机登录的逻辑的话,也应该照此写在父组件--loginPanel.vue里,即意味着当前父组件--loginPanel.vue里既管理账号登录的逻辑又管理着手机号登录的逻辑,可能会导致父组件--loginPanel.vue里变得臃肿,因此这里的做法就是在父组件--loginPanel.vue里只告诉已经点击完登录了,然后让子组件去执行操作,去验证是否正确,去发送请求,返回结果。当然也可以不这样做 ### 网络请求 - 登录的逻辑(网络请求,拿到数据后的处理)--放到vuex里,由actions完成请求 - 数据保存到某一个位置(vuex) - 发送其他请求(请求当前用户信息) - 拿到用户菜单 - 跳转到首页 service/type.ts ```ts export interface IDataType { code: number; data: T; } ``` service/login/type.ts ```ts export interface IAccount { name: string; password: string; } export interface ILoginResult { id: number; name: string; token: string; } ``` service/login/login.ts ```ts import lwjRequest from ".."; import { IAccount, ILoginResult } from "./type"; import { IDataType } from "../type"; enum LoginAPI { AccountLogin = "/login", LoginUserInfo = "/users/", // 用法: /users/1 UserMenus = "/role/", // 用法: role/1/menu } // 登录 export function accountLoginRequest(account: IAccount) { return lwjRequest.post>({ url: LoginAPI.AccountLogin, data: account, }); } // 根据id获取用户详细信息 export function requestUserInfoById(id: number) { return lwjRequest.get({ url: LoginAPI.LoginUserInfo + id, showLoading: false, }); } // 根据id查找当前用户所拥有的菜单权限 export function requestUserMenusByRoleId(id: number) { return lwjRequest.get({ url: LoginAPI.UserMenus + id + "/menu", showLoading: false, }); } ``` ### 状态管理 - 登录的逻辑(网络请求,拿到数据后的处理)--放到vuex里,由actions完成请求 - 数据保存到某一个位置(vuex) - 发送其他请求(请求当前用户信息) - 拿到用户菜单 - 跳转到首页 由于手动刷新后会导致vuex存储的数据失效,因此这里有两种做法可以使其不失效: 第一种就是利用本地存储: ```bash state() { return { token: localCache.getCache("token") ?? "", userInfo: localCache.getCache("userInfo") ?? {}, userMenu: localCache.getCache("userMenu") ?? [], }; }, ``` 第二种就是在actions定义一个方法去防止用户登录进页面后手动刷新页面,导致vuex里面存储的token等数据被清空,然后在store/index.ts里定义一个方法,通过dispatch去派发,再将其导出去在main.ts里注册store时引用,本项目采用的就是该方法 store/types/type.ts--全局 ```ts export interface IRootState { name: string; age: number; } ``` store/login/type.ts--局部 ```ts export interface ILoginState { token: string; userInfo: any; userMenu: any; } ``` store/login/login.ts ```ts import { Module } from "vuex"; import router from "@/router"; import { ILoginState } from "./type"; import localCache from "@/utils/cache"; import { IRootState } from "../types/type"; import { accountLoginRequest, requestUserInfoById, requestUserMenusByRoleId, } from "@/service/login/login"; const loginModule: Module = { namespaced: true, state() { return { token: "", userInfo: {}, userMenu: [], }; }, mutations: { changeToken(state, token: string) { state.token = token; }, changeUserInfo(state, userInfo: any) { state.userInfo = userInfo; }, changeUserMenu(state, userMenu: any) { state.userMenu = userMenu; }, }, actions: { // 登录 async accountLoginAction({ commit }, payload: any) { // 1. 登录 const loginResult = await accountLoginRequest(payload); // console.log(loginResult.data.id); const { id, token } = loginResult.data; commit("changeToken", token); localCache.setCache("token", token); // 2. 根据id获取用户详细信息 const userInfoResult = await requestUserInfoById(id); const userInfo = userInfoResult.data; commit("changeUserInfo", userInfo); localCache.setCache("userInfo", userInfo); // 3. 根据id请求当前用户所拥有的菜单权限 const userMenuResult = await requestUserMenusByRoleId(userInfo.role.id); const userMenus = userMenuResult.data; commit("changeUserMenu", userMenus); localCache.setCache("userMenu", userMenus); // 4.跳到首页 router.push("/main"); }, // 防止用户登录进页面后手动刷新页面,导致vuex里面存储的token等数据被清空 loadLocalLogin({ commit, dispatch }) { const token = localCache.getCache("token"); if (token) { commit("changeToken", token); } const userInfo = localCache.getCache("userInfo"); if (userInfo) { commit("changeUserInfo", userInfo); } const userMenus = localCache.getCache("userMenu"); if (userMenus) { commit("changeUserMenu", userMenus); } }, }, getters: {}, }; export default loginModule; ``` store/index.ts ```ts import { createStore } from "vuex"; import { IRootState } from "./types/type"; import login from "./login/login"; const store = createStore({ state() { return { name: "xxx", age: 18, }; }, getters: {}, mutations: {}, actions: {}, modules: { login, }, }); export function setupStore() { store.dispatch("login/loadLocalLogin"); } export default store; ``` main.ts ```ts ... ++ import { setupStore } from "./store"; const app = createApp(App); app.use(registerIcons); app.use(store); ++ setupStore(); app.use(router); app.mount("#app"); ``` ### 跨域处理 vue.config.js ```ts module.exports = defineConfig({ transpileDependencies: true, devServer: { // 自定义服务配置 // open: true, // 自动打开浏览器 port: 9999, host: "127.0.0.1", proxy: { "^/api": { target: "http://152.136.185.210:5000", changeOrigin: true, pathRewrite: { "^/api": "", }, }, }, }, } ``` ### 实现 3 - 登录的逻辑(网络请求,拿到数据后的处理)--放到vuex里,由actions完成请求 - 数据保存到某一个位置(vuex) - 发送其他请求(请求当前用户信息) - 拿到用户菜单 - 跳转到首页 请求方法在vuex里完成,然后在对应的.vue文件里通过调用vuex里的actioons里的相应的action,从而实现网络请求--本项目采用 另一种就是哪个组件需要发送网络请求就在哪个组件里进行相关处理 views/login/cpns/loginPanel.vue ```vue ``` views/login/cpns/panelAccount.vue ```vue ``` router/index.ts ```ts import localCache from "@/utils/cache"; router.beforeEach((to) => { console.log(to.path); const token = localCache.getCache("token"); if (to.path !== "/login") { if (!token) { return "/login"; } } else if (to.path === "/login") { if (token) { return "/main"; } } }); ``` ## 菜单权限管理问题 ### 概述 **不同账号登录进后台后其展示的内容也就是左侧菜单栏不一样:** 4 比如张三(主管)登录进后台后可以查看所有的权限即左侧菜单栏完全展示;李四(运营)登录进后台后无法查看管理员信息即左侧菜单不显示对应的菜单项;王五(普通员工)登录进后台后只可查看与自己权限相关的一些页面,其余界面左侧菜单项不显示... ### 如何进行判断 如果根据每个人去进行分配的话就很复杂了,因为职位不同、而且人员是流动的有增有减 这就跟后端有关了,通常叫做RBAC(role based access control)即基于角色的访问控制 后台会根据当前的角色比如总裁、总监、主管、开发人员、运营人员、客服人员/测试人员、普通员工等,分别分配对应的角色:超级管理员(拥有所有权限)、管理员(拥有部分权限) ### 后端实现方案 后台大致设计:至少有三/四张表--用户表-角色表-关系表(记录角色拥有的权限)-权限表 23 ### 前端实现方案 前端在拿到菜单后,我们此时是不知道哪个用户(角色)会登录,因此菜单是不能写死的,是根据用户返回的菜单进行渲染的,根据其对应的菜单映射不同的路由 这里的做法有如下几种: 方式一: 在主页路由下的children里将所有的路由全部注册上,但是会有一个很大的弊端,那就是可以在浏览器里通过手动输入地址进行访问 方式二: 先将不同角色的路由全部注册好,然后当用户登录后,根据其返回的权限信息去进行赛选,将赛选到的路由动态的加载出来,但是这样做也会有一个弊端,那就是如果新增角色了,在前端里原来并没有配置该角色所对应的路由,那么此时就只能修改前端代码,再重新注册 方式三: 获取到菜单后根据菜单去进行动态的生成路由映射。在菜单里是有url的,url在路由里面对应的就是path,有了这个path之后我们可以让这个path对应某个component(组件),这样一来就会产生一个路由数组,再将这个路由数组动态的添加到主页的children里 这里的动态生成就会有两种:一种是后台返回的信息菜单中就存在一个component字段,在该字段里就会存在要加载的组件名称比如role.vue,告诉我们需要去加载哪一个组件,这样的话就是我们创建的组件名称和路径要和后端返回的保持一致;第二种就是菜单里会有url,在前端代码里我们原来就已经配置好了path和component的映射关系,此时我们只需根据url去动态的加载已经配置好了的某个/多个对象 ## 后台主页 38 ### 总体基本布局结构 主要分为三部分:左侧菜单栏区域、右侧头部区域、右侧主体内容区域 当点击左侧菜单栏时,右侧内容会进行相应的改变 38 - home.vue - homeMenu.vue - homeHeader.vue - headerBreadcrumb.vue - homeInfo.vue views/layout/cpns/homeMenu.vue ```vue ``` views/layout/cpns/homeHeader.vue ```vue ``` views/layout/cpns/homeInfo.vue ```vue ``` views/layout/cpns/headerBreadcrumb.vue ```vue ``` views/layout/home.vue ```vue ``` ### 左侧菜单栏区域实现 40 对于这一部分主要分为上下两个区域 #### 顶部图标及标题区域 40 views/layout/cpns/homeMenu.vue ```vue ``` #### 底部菜单区域 42 ##### 手动搭建 views/layout/homeMenu.vue ```vue ``` 这样做当展开全部菜单时便会出现滚动条,隐藏滚动条处理方法如下: 5 views/layout/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; } } ``` 5 ##### 动态搭建 我们在做登录页的时候已经将当前所登陆用户所具有的菜单权限保存在了 vuex 里,因此这里只需用即可!!! ###### useStore类型问题 ![43](images/43.jpg) 在 login/cpns/leftMenu.vue 里可以通过如上图的方式去获取,但是这样做就少了类型检测,也就是在login.后面哪些东西是可以通过.出来的,哪些是不可以通过.出来的,而不是一个any类型 这是因为ts对vuex这块的支持其实是很差的,其中一个就体现在了useStore这里 这里为了让useStore更好用一点,我们会对其进行封装处理: store/login/type.ts ```ts export interface ILoginState { token: string; userInfo: any; userMenu: any; } ``` store/types/type.ts ```ts import { ILoginState } from "../login/type"; export interface IRootState { name: string; age: number; } export interface IRootWidthModule { login: ILoginState; } export type IStoreType = IRootState & IRootWidthModule; ``` store/index.ts ```ts import { createStore, Store, useStore as useVuexStore } from "vuex"; import { IRootState, IStoreType } from "./types/type"; import login from "./login/login"; const store = createStore({ state() { return { name: "xxx", age: 18, }; }, getters: {}, mutations: {}, actions: {}, modules: { login, }, }); export function setupStore() { store.dispatch("login/loadLocalLogin"); } // 自定义封装useStore,使其对ts更友好 export function useStore(): Store { return useVuexStore(); } export default store; ``` 效果: 44 44 这样就解决了!!! ###### 利用动态组件加载图标 views/home/cpns/homeMenu.vue ```bash ``` 44 ###### 基本展示 注意需要在 el-sub-menu 上添加 index 属性,这样才能保证每个菜单之间互不影响即点击展开第一个菜单时后面的菜单不会同时展开 注意需要在 el-menu-item 上添加 index 属性,这样才可以保证当点击 item 项即每个子菜单时,当前子菜单高亮,不会影响其他子菜单 src/layout/cpns/homeMenu.vue ```vue ``` 7 ###### 点击菜单项实现页面切换 点击菜单实现页面切换,这其实就是菜单和路由之间的一个映射关系!!! 路由映射的时候,为了满足所有进入系统的用户,需要注册所有的路由,但是这样做会导致没有权限的用户可以直接通过在浏览器地址栏里输入相应的url路由也可以进行访问未获权的界面 **为了杜绝这种现象的发生,这里采取动态路由的形式进行开发:** 动态路由:根据不同用户(菜单),动态的注册该用户下所拥有的路由,而不是一次性全部将所有的路由进行注册 **动态路由的实现也有两种方案:** 方式一:基于角色的动态路由管理 先将不同角色的路由全部注册好,然后当用户登录后,根据其返回的权限信息去进行赛选,将赛选到的路由动态的加载出来,但是这样做也会有一个弊端,那就是如果新增角色了,在前端里原来并没有配置该角色所对应的路由,那么此时就只能修改前端代码,再重新注册或者由后端解决,由后端返回这个对象 方式二:基于菜单的动态路由管理 获取到菜单后根据菜单去进行动态的生成路由映射。在菜单里是有url的,url在路由里面对应的就是path,有了这个path之后我们可以让这个path对应某个component(组件),这样一来就会产生一个路由数组,再将这个路由数组动态的添加到主页的children里 这里的动态生成就会有两种: 1. 一种是后台返回的信息菜单中就存在一个component字段(component: role.vue),在该字段里就会存在要加载的组件名称比如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) **封装读取本地路由以及动态加载路由方法:** utils/map-menus.ts ```ts import type { RouteRecordRaw } from "vue-router"; // 1. 动态添加路由 export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] { const routes: RouteRecordRaw[] = []; // 1. 先去加载默认所有的 routes,并将其放入到一个路由数组里 const allRoutes: RouteRecordRaw[] = []; /** * 参数一:查找的文件位置 * 参数二:是否需要开启递归 * 参数三:查找的正则规则 */ const routeFiles = require.context("../router/main", true, /\.ts/); routeFiles.keys().forEach((key) => { // console.log(key); const route = require("../router/main" + key.split(".")[1]); // console.log(route); allRoutes.push(route.default); }); // console.log(allRoutes); // 2. 根据菜单获取需要添加的 routes /** * userMenu: * type===1 -> children * type===2 -> url -> route * ... */ const _recurseGetRoute = (menus: any[]) => { for (const menu of menus) { if (menu.type === 2) { const route = allRoutes.find((route) => { return route.path === menu.url; // 找到 }); if (route) routes.push(route); } else { _recurseGetRoute(menu.children); } } }; _recurseGetRoute(userMenus); return routes; } ``` **实现点击左侧菜单项实现路由跳转:** 方式一: src/stores/login/login.ts ```ts ... import { mapMenusToRoutes } from "@/utils/map-menus"; const loginModule: Module = { namespaced: true, state() { return { ... }; }, mutations: { ... changeUserMenu(state, userMenu: any) { state.userMenu = userMenu; // 动态添加路由 const routes = mapMenusToRoutes(userMenu); // console.log(routes); // 将routes添加到路由里的main里 routes.forEach((route) => { router.addRoute("main", route); }); }, }, ... ``` layout/cpns/homeMenu.vue ```vue import { useRouter } from "vue-router"; const router = useRouter(); // 2. 监听二级菜单点击 const handleMenuItemClick = (item: any) => { // console.log(item); router.push({ path: item.url ?? "/not-found", }); }; ``` 方式二:有BUG router/index.ts ```ts // 导航守卫 router.beforeEach((to) => { // console.log(to.path); const token = localCache.getCache("token"); if (to.path !== "/login") { if (!token) { return "/login"; } // // 动态添加路由 // const userMenu = (store.state as any).login.userMenu; // const routes = mapMenusToRoutes(userMenu); // // 将routes添加到路由里的main里 // routes.forEach((route) => { // router.addRoute("main", route); // }); // console.log(router.getRoutes()); // console.log(to); } else if (to.path === "/login") { if (token) { return "/main"; } } }); ``` layout/cpns/homeMenu.vue ```vue import { useRouter } from "vue-router"; const router = useRouter(); // 2. 监听二级菜单点击 const handleMenuItemClick = (item: any) => { // console.log(item); router.push({ path: item.url ?? "/not-found", }); }; ``` 9 ###### 解决进入首页后点击某一菜单栏后刷新出现路径和菜单栏不匹配问题 当我们在后台首页里点击某一菜单后,此时手动刷新页面,会出现当前高亮的菜单与路径不匹配 12 通过分析不难发现就是 el-menu 元素里的default-active属性所对应的值,不应该写死,而是动态的!!! **思路:** - 获取当前点击菜单的路径 - 根据路径去匹配菜单(menu) - 获取该路径所对应的菜单的id - 将id赋值给default-active属性所对应的值即可解决 utils/map-menus.ts ```ts // 2. 根据路径去匹配需要显示的菜单 export function pathMapToMenu(userMenus: any[], currentPath: string): any { for (const menu of userMenus) { if (menu.type === 1) { const findMenu = pathMapToMenu(menu.children ?? [], currentPath); if (findMenu) { return findMenu; } } else if (menu.type === 2 && menu.url === currentPath) { return menu; } } } ``` layout/cpns/homeMenu.vue ```js import { computed, ref } from "vue"; import { useRouter, useRoute } from "vue-router"; import { pathMapToMenu } from "@/utils/map-menus"; // 使用自己的useStore import { useStore } from "@/store"; // 0. 定义props ... const router = useRouter(); const route = useRoute(); // 1. 获取用户权限 ... // 2. 监听二级菜单点击 ... // 3. 默认子菜单选中项 const currentPath = route.path; const menu = pathMapToMenu(userMenus.value, currentPath); const defaultValue = ref(menu.id + ""); ``` 13 ###### 实现用户登录进系统后跳转到默认页面 正常进来的时候应该匹配到某一个页面,也就是动态注册所有路由中的第一个页面----核心技术 54 出现以上情况是因为当我们来到首页,即 `http://localhost:9999/` 的时候,之前在路由里我们配置了当用户来到首页即 `http://localhost:9999/` 的时候会重定向到 `/main`,但是重定向到 `/main` 之后,就会拿到 `/main` 去和后台返回的菜单进行匹配,这是匹配不到的,因此就会变为 undefined,然后再从 undefined 里面去取 id 的时候就会报 undefiend没有id属性 其实当我们知道路径是 `/main`的时候我们应该继续给它做一个重定向,使其重定向到当前用户所拥有菜单数组里的第一个,即--- `http://localhost:9999/main/analysis/overview` 核心技术 utils/map-menu.ts ```ts // 动态注册所有路由中的第一个路由 ++ export let firstMenu: any = null; // 1. 动态添加路由 export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] { const routes: RouteRecordRaw[] = []; // 1. 先去加载默认所有的 routes,并将其放入到一个路由数组里 const allRoutes: RouteRecordRaw[] = []; /** * 参数一:查找的文件位置 * 参数二:是否需要开启递归 * 参数三:查找的正则规则 */ const routeFiles = require.context("../router/main", true, /\.ts/); routeFiles.keys().forEach((key) => { // console.log(key); const route = require("../router/main" + key.split(".")[1]); // console.log(route); allRoutes.push(route.default); }); // console.log(allRoutes); // 2. 根据菜单获取需要添加的 routes /** * userMenu: * type===1 -> children * type===2 -> url -> route * ... */ const _recurseGetRoute = (menus: any[]) => { for (const menu of menus) { if (menu.type === 2) { const route = allRoutes.find((route) => { return route.path === menu.url; // 找到 }); if (route) routes.push(route); ++ if (!firstMenu && route) firstMenu = menu; } else { _recurseGetRoute(menu.children); } } }; _recurseGetRoute(userMenus); 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; } }); ``` 14 ### 右侧头部区域实现 头部区域分为左中右三个区域: 47 #### 基本结构 layout/cpns/homeHeader.vue ```vue ``` 48 #### 点击图标展开/收起菜单栏 当点击图标时,当前为展开则折叠;当前为折叠则展开 控制左侧菜单的宽度变化即 `` 需要配合 el-menu 的 `collapse` 属性 在子组件-homeHeader.vue中点击折叠/展开的图标时,父组件home.vue里el-aside的 `width` 属性值变化,同时子组件-homeMenu.vue里el-menu中的 `collapse` 属性值也要变化 **实现方式:** 1. 事件总线 2. 子传父-父传子 3. ref 4. .... ##### 动态组件实现图标切换 49 当然除此之外也可以利用v-if/v-else-if来实现 ##### 实现 - 子组件-homeHeader通过自定义事件将是否折叠isFold的值传递给父组件 * 父组件通过自定义事件foldChange进行接收,并将其赋值给父组件中定义的变量isCollapse * 再由父组件-home通过父传子将isCollapse值传递给子组件homeMenu * 在子组件-homeMenu里接收并与menu属性中的collapse值对应 layout/cpns/homeHeader.vue ```vue ``` layout/home.vue ```vue ``` layout/cpns/homeMenu.vue ```vue

后台管理系统

// 0. 定义props defineProps({ isFold: { type: Boolean, default: false } }); ``` 8 #### 个人信息区域 50 ##### 基本布局 layout/cpns/homeInfo.vue ```vue ``` 10 ##### 实现退出登录功能 layout/cpns/homeInfo.vue ```vue 退出系统 ``` #### 面包屑实现 utils/map-menus.ts ```ts interface IBreadcrumb { name: string; path?: string; } // 2. 根据路径去匹配需要显示的菜单 export function pathMapToMenu( userMenus: any[], currentPath: string, ++ breadcrumbs?: IBreadcrumb[] ): any { for (const menu of userMenus) { if (menu.type === 1) { const findMenu = pathMapToMenu(menu.children ?? [], currentPath); if (findMenu) { // breadcrumbs?.push({ name: menu.name, path: menu.url }); // breadcrumbs?.push({ name: findMenu.name, path: findMenu.url }); ++ breadcrumbs?.push({ name: menu.name }); ++ breadcrumbs?.push({ name: findMenu.name }); ++ return findMenu; } } else if (menu.type === 2 && menu.url === currentPath) { return menu; } } } // 3. 面包屑 // /main/system/role --> type===2 对应的menu ++ export function mapPathToBreadcrumbs(userMenus: any[], currentPath: string) { const breacrumbs: IBreadcrumb[] = []; pathMapToMenu(userMenus, currentPath, breacrumbs); return breacrumbs; } ``` layout/cpns/homeHeader.vue ```vue import { ref, computed } from "vue"; import { mapPathToBreadcrumbs } from "@/utils/map-menus"; import { useStore } from "@/store"; import { useRoute } from "vue-router"; // 2. 面包屑处理 const store = useStore(); const route = useRoute(); const breadcrumbs = computed(() => { const userMenus = store.state.login.userMenu; const currentPath = route.path; return mapPathToBreadcrumbs(userMenus, currentPath); }); ``` layout/cpns/headerBreadcrumb.vue ```vue ``` ## 高级封装搜索区域组件 52 类似于上面的搜索区域,在一个项目里可能在很多界面都会用到类似的结构,我们当然可以在类似的界面里去一个个的搭建结构,重复写代码,但是这并不是最好的做法,我们可以将类似于这样的区域抽离成一个单独的组件,然后以传入配置文件的方式,就可以自动快速的帮我们生成不同的搜索区域界面,从而实现复用!!! ### 日历组件默认显示切换为中文 由于 Element Plus 的默认语言为英语,因此我们要将其改为中文的话就需要进行国际化配置:https://element-plus.gitee.io/zh-CN/guide/i18n.html 这里主要有两种方式:全局配置和非全局配置 对于全局配置需要element-plus是全局引入的,但是在本项目中我们的element-plus是按需引入的,因此就只能使用非全局配置的方式: App.vue ```vue ``` 对于 .mjs 这种文件无法识别在ts里,那么就可以对其进行声明: shims-vue.d.ts ```bash declare module "*.mjs"; ``` 53 ### 封装基本结构 base-ui/from/src/type/type.ts ```ts type IFormType = "input" | "password" | "select" | "datepicker"; export interface IFromItem { type: IFormType; label: string; // 验证规则 rules?: any[]; placeholder?: string; // select下拉框的options options?: any[]; // 针对特殊的属性,比如DatePicker otherOptions?: any; } export interface IForm { formItems: IFromItem[]; labelWidth?: string; itemStyle: any; colLayout: any; } ``` base-ui/from/src/form.vue ```vue ``` base-ui/from/index.ts ```ts import LwjForm from "./src/form.vue"; // export * from "./type/type"; export default LwjForm; ``` ### 用户界面尝试效果 views/system/user/config/searchConfig.ts ```ts import { IForm } from "@/base-ui/from/type/type"; export const searchFormConfig: IForm = { labelWidth: "100px", itemStyle: { padding: "5px 40px" }, colLayout: { xl: 6, // >=1920 lg: 8, // >=1200 md: 12, // >=992 sm: 24, // >=768 xs: 24, // <=768 }, formItems: [ { type: "input", label: "用户名", placeholder: "请输入用户名", }, { type: "select", label: "状态", placeholder: "请选择状态", options: [ { label: "启用", value: 1, }, { label: "禁用", value: 0, }, ], otherOptions: {}, }, { type: "datepicker", label: "创建时间", otherOptions: { startPlaceholder: "开始时间", endPlaceholder: "结束时间", type: "daterange", }, placeholder: "请选择创建时间", }, ], }; ``` views/system/user/user.vue ```vue ``` 11 ### 升级改造完善 按照上面这样做,我们如何获取表单里面的数据? 我们在 user 界面里通过 reactive 定义一个 formData 对象,并在该对象里定义类似于如下的初始值: ```bash const formData = reactive({ name: "", realname: "", enable: "", createAt: " ", }); ``` 再将其传递给子组件--封装的form组件,接收后,通过类似于如下的形式进行v-model双向绑定:`v-model="formData[`${item.prop}`]"` 这里的 prop 属性需要在user的配置文件里添加上,这样才能实现双向绑定 但是这样做我们其实违背了单向数据流的原则,我们在子组件form里,通过在输入框里输入值,然后在父组件user里便可获取到了 正确的做法是在子组件里通过emit传递出一个方法,在父组件里通过自定义方法去获取,进而操作 当然还有另一种方式,就是在子组件里通过v-model,实现组件的双向绑定---本项目采用 注意:组件的双向绑定默认传递过去的名字叫--modelValue #### 利用v-model实现组件间的双向绑定 views/system/user/user.vue ```vue ``` base-ui/form/src/form.vue ```vue ``` #### 最终代码 base-ui/from/src/type/type.ts ```ts type IFormType = "input" | "password" | "select" | "datepicker"; interface ISelectOption { label: string; value: any; } export interface IFromItem { type: IFormType; label: string; prop: string; // 验证规则 rules?: any[]; placeholder?: string; // select下拉框的options options?: ISelectOption[]; // 针对特殊的属性,比如DatePicker otherOptions?: any; // 默认值 // defaultValue?: any; // // isHidden?: boolean; } export interface IForm { // title?: string; formItems: IFromItem[]; labelWidth?: string; itemStyle: any; colLayout: any; } ``` base-ui/from/src/form.vue ```vue ``` base-ui/from/index.ts ```ts import LwjForm from "./src/form.vue"; // export * from "./type/type"; export default LwjForm; ``` components/pageSearch/src/page-search.vue ```vue ``` components/pageSearch/src/index.ts ```ts import PageSearch from "./src/page-search.vue"; export default PageSearch; ``` views/system/user/user.vue ```vue ``` components/page-content/pageContent.vue ```vue ``` #### 采用非双向绑定实现重置功能 base-ui/from/src/form.vue ```vue ``` views/system/user/user.vue ```vue ``` 其余界面与上面一样,未有变化 #### 用户界面尝试效果 views/system/user/config/searchConfig.ts ```ts import { IForm } from "@/base-ui/from/type/type"; export const searchFormConfig: IForm = { labelWidth: "100px", itemStyle: { padding: "5px 40px" }, colLayout: { xl: 6, // >=1920 lg: 8, // >=1200 md: 12, // >=992 sm: 24, // >=768 xs: 24, // <=768 }, formItems: [ { type: "input", label: "用户名", prop: "name", placeholder: "请输入用户名", }, { type: "input", label: "真实姓名", prop: "realname", placeholder: "请输入真实姓名", rules: [], }, { type: "select", label: "状态", prop: "enable", placeholder: "请选择状态", options: [ { label: "启用", value: 1, }, { label: "禁用", value: 0, }, ], otherOptions: {}, }, { type: "datepicker", label: "创建时间", prop: "createAt", otherOptions: { startPlaceholder: "开始时间", endPlaceholder: "结束时间", type: "daterange", }, placeholder: "请选择创建时间", }, ], }; ``` hooks/usePageSearch.ts ```ts import { ref } from "vue"; import type pageContent from "@/components/pageContent"; export function usePageSearch() { const pageContentRef = ref>(); // 1. 重置 const handleResetClick = () => { pageContentRef.value?.getPageData(); }; // 2. 搜索 const handleQueryClick = (queryInfo: any) => { pageContentRef.value?.getPageData(queryInfo); }; return [pageContentRef, handleResetClick, handleQueryClick]; } ``` views/system/user/user.vue ```vue ``` service/system/system.ts ```ts import lwjRequest from ".."; import { IDataType } from "../type"; // 1. 获取列表数据 export function getPageListData(url: string, queryInfo: any) { return lwjRequest.post({ url, data: queryInfo, }); } ``` service/index.ts ```ts export interface IDataType { code: number; data: T; } ``` store/system/type.ts ```ts export interface ISystemState { userList: any[]; userCount: number; roleList: any[]; roleCount: number; } ``` store/system/system.ts ```ts import { Module } from "vuex"; import { IRootState } from "../types/type"; import { ISystemState } from "./type"; import { getPageListData } from "@/service/system/system"; const systemModule: Module = { namespaced: true, state() { return { userList: [], userCount: 0, roleList: [], roleCount: 0, }; }, mutations: { changeUserListMutation(state, userList: any[]) { state.userList = userList; }, changeUserTotalCountMutation(state, userTotalCount: number) { state.userCount = userTotalCount; }, changeRoleListMutation(state, roleList: any[]) { state.roleList = roleList; }, changeRoleTotalCountMutation(state, roleTotalCount: number) { state.roleCount = roleTotalCount; }, }, getters: { pageListData(state) { return (pageName: string) => { // 方式一 // switch (pageName) { // case "user": // return state.userList; // break; // case "role": // return state.roleList; // break; // } // 方式二 const listData: any[] = (state as any)[`${pageName}List`] ?? []; return listData; }; }, }, actions: { async getPageListAction({ commit }, payload: any) { // 1. 获取pageUrl const pageName = payload.pageName; // 方式一 // const pageUrl = `/${pageName}/list` // 方式二 let pageUrl = ""; switch (pageName) { case "user": pageUrl = "/users/list"; break; case "role": pageUrl = "/role/list"; break; case "department": pageUrl = "/department/list"; break; } // 2. 发送请求 const pageResult = await getPageListData(pageUrl, payload.queryInfo); // 3. 将数据存储到state中 const { list, totalCount } = pageResult.data; // 方式一 // const changePageName = pageName.slice(0, 1).toUpperCase() + pageName.slice(1); // commit(`change${changePageName}ListMutation`, list); // commit(`change${changePageName}TotalCountMutation`, totalCount); // 方式二 switch (pageName) { case "user": commit("changeUserListMutation", list); commit("changeUserTotalCountMutation", totalCount); case "role": commit("changeRoleListMutation", list); commit("changeRoleTotalCountMutation", totalCount); } }, }, }; export default systemModule; ``` 15 ## 高级封装tab表格区域组件 ### 数据处理 对于网络请求数据这块,这里有两种思路:第一种方式是在对应的.vue组件里通过调用相应的请求函数进行数据的请求和处理;第二种方式是将所有网路请求的代码放到vuex里,然后通过在actions里进行请求,在需要用到的组件里请求其对应的actions即可 #### 网络请求 service/type.ts ```ts export interface IDataType { code: number; data: T; } ``` service/system/type.ts ```ts ``` service/system/system.ts ```ts import lwjRequest from ".."; import { IDataType } from "../type"; // 1. 获取列表数据 export function getPageListData(url: string, queryInfo: any) { return lwjRequest.post({ url, data: queryInfo, }); } ``` #### 状态管理 store/system/type.ts ```ts export interface ISystemState { userList: any[]; userCount: number; } ``` store/system/system.ts ```ts import { Module } from "vuex"; import { IRootState } from "../types/type"; import { ISystemState } from "./type"; import { getPageListData } from "@/service/system/system"; const systemModule: Module = { namespaced: true, state() { return { userList: [], userCount: 0, }; }, mutations: { changeUserListMutation(state, userList: any[]) { state.userList = userList; }, changeUserTotalCountMutation(state, userTotalCount: number) { state.userCount = userTotalCount; }, }, actions: { async getPageListAction({ commit }, payload: any) { const pageResult = await getPageListData( payload.pageUrl, payload.queryInfo ); const { list, totalCount } = pageResult.data; commit("changeUserListMutation", list); commit("changeUserTotalCountMutation", totalCount); }, }, }; export default systemModule; ``` store/types/type.ts ```ts import { ILoginState } from "../login/type"; ++import { ISystemState } from "../system/type"; export interface IRootState { name: string; age: number; } export interface IRootWidthModule { login: ILoginState; ++ system: ISystemState; } export type IStoreType = IRootState & IRootWidthModule; ``` store/index.ts ```ts import { createStore, Store, useStore as useVuexStore } from "vuex"; import { IRootState, IStoreType } from "./types/type"; import login from "./login/login"; ++import system from "./system/system"; const store = createStore({ state() { return { name: "xxx", age: 18, }; }, getters: {}, mutations: {}, actions: {}, modules: { login, ++ system, }, }); export function setupStore() { store.dispatch("login/loadLocalLogin"); } // 自定义封装useStore,使其对ts更友好 export function useStore(): Store { return useVuexStore(); } export default store; ``` #### 在用户管理界面获取数据 views/system/user/user.vue ```bash import { useStore } from "vuex"; //1. 获取数据列表 const store = useStore(); store.dispatch("system/getPageListAction", { pageUrl: "/users/list", queryInfo: { offset: 0, size: 10, }, }); ``` 55 ### 封装基本结构 base-ui/table/src/table.vue ```vue ``` base-ui/table/type/type.ts ```ts ``` base-ui/table/index.ts ```ts import lwjTable from "./src/table.vue"; export default lwjTable; ``` ### 用户界面尝试效果 views/system/user/use.vue ```vue ``` 56 ### 格式化时间 安装:`npm i dayjs` utils/date-fotmat.ts ```ts const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm:ss"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; dayjs.extend(utc); export function formatUtcString( utcString: string, format: string = DATE_TIME_FORMAT ) { return dayjs.utc(utcString).format(format); } ``` global/registerProperties.ts ```ts import { App } from "vue"; import { formatUtcString } from "@/utils/date-formate"; // 对vue进行类型补充说明,防止报:$filters属性不存在的错误 declare module "@vue/runtime-core" { interface ComponentCustomProperties { $filters: any; } } export default function registerProperties(app: App) { app.config.globalProperties.$filters = { formatTime(value: string) { return formatUtcString(value); }, }; } ``` global/index.ts ```ts import { App } from "vue"; import registerProperties from "./registerProperties"; export function globalRegister(app: App): void { app.use(registerProperties); } ``` main.ts ```ts import { globalRegister } from "@/global"; const app = createApp(App); app.use(globalRegister); ``` 使用方式: ```bash {{ $filters.formatTime(scope.row.createAt) }} ``` ### 最终代码 base-ui/src/table.vue ```vue ``` base-ui/type/type.ts ```ts ``` base-ui/index.ts ```ts import lwjTable from "./src/table.vue"; export default lwjTable; ``` components/pageContent/src/page-content.vue ```vue ``` components/pageContent/index.ts ```ts import pageContent from "./src/page-content.vue"; export default pageContent; ``` hooks/usePageContent.ts ```ts import { ref } from "vue"; import type pageContent from "@/components/pageContent"; export function usePageSearch() { const pageContentRef = ref>(); // 1. 重置 const handleResetClick = () => { pageContentRef.value?.getPageData(); }; // 2. 搜索 const handleQueryClick = (queryInfo: any) => { pageContentRef.value?.getPageData(queryInfo); }; return [pageContentRef, handleResetClick, handleQueryClick]; } ``` store/system/type.ts ```ts export interface ISystemState { userList: any[]; userCount: number; roleList: any[]; roleCount: number; goodsList: any[]; goodsCount: number; menuList: any[]; menuCount: number; departmentList: any[]; departmentCount: number; categoryList: any[]; categoryCount: number; } ``` store/system/system.ts ```ts import { Module } from "vuex"; import { IRootState } from "../types/type"; import { ISystemState } from "./type"; import { getPageListData } from "@/service/system/system"; const systemModule: Module = { namespaced: true, state() { return { userList: [], userCount: 0, roleList: [], roleCount: 0, goodsList: [], goodsCount: 0, menuList: [], menuCount: 0, departmentList: [], departmentCount: 0, categoryList: [], categoryCount: 0, }; }, mutations: { changeUserListMutation(state, userList: any[]) { state.userList = userList; }, changeUserTotalCountMutation(state, userTotalCount: number) { state.userCount = userTotalCount; }, changeRoleListMutation(state, roleList: any[]) { state.roleList = roleList; }, changeRoleTotalCountMutation(state, roleTotalCount: number) { state.roleCount = roleTotalCount; }, changeGoodsListMutation(state, goodsList: any[]) { state.goodsList = goodsList; }, changeGoodsTotalCountMutation(state, goodsTotalCount: number) { state.goodsCount = goodsTotalCount; }, changeMenuListMutation(state, menuList: any[]) { state.menuList = menuList; }, changeMenuTotalCountMutation(state, menuTotalCount: number) { state.menuCount = menuTotalCount; }, changeDepartmentListMutation(state, departmentList: any[]) { state.departmentList = departmentList; }, changeDepartmentTotalCountMutation(state, departmentCount: number) { state.departmentCount = departmentCount; }, changeCategoryListMutation(state, categoryList: any[]) { state.categoryList = categoryList; }, changeCategoryTotalCountMutation(state, categoryCount: number) { state.categoryCount = categoryCount; }, }, getters: { pageListData(state) { return (pageName: string) => { // 方式一 // switch (pageName) { // case "user": // return state.userList; // break; // case "role": // return state.roleList; // break; // } // 方式二 const listData: any[] = (state as any)[`${pageName}List`] ?? []; return listData; }; }, pageListCount(state) { return (pageName: string) => { // const listCount: any[] = (state as any)[`${pageName}Count`] ?? []; // return listCount; return (state as any)[`${pageName}Count`]; }; }, }, actions: { async getPageListAction({ commit }, payload: any) { // 1. 获取pageUrl const pageName = payload.pageName; // 方式一 // const pageUrl = `/${pageName}/list` // 方式二 let pageUrl = ""; switch (pageName) { case "user": pageUrl = "/users/list"; break; case "role": pageUrl = "/role/list"; break; case "goods": pageUrl = "/goods/list"; break; case "menu": pageUrl = "/menu/list"; break; case "department": pageUrl = "/department/list"; break; case "category": pageUrl = "/category/list"; break; } // 2. 发送请求 const pageResult = await getPageListData(pageUrl, payload.queryInfo); // 3. 将数据存储到state中 const { list, totalCount } = pageResult.data; // 方式一 // const changePageName = pageName.slice(0, 1).toUpperCase() + pageName.slice(1); // commit(`change${changePageName}ListMutation`, list); // commit(`change${changePageName}TotalCountMutation`, totalCount); // 方式二 switch (pageName) { case "user": commit("changeUserListMutation", list); commit("changeUserTotalCountMutation", totalCount); case "role": commit("changeRoleListMutation", list); commit("changeRoleTotalCountMutation", totalCount); case "goods": commit("changeGoodsListMutation", list); commit("changeGoodsTotalCountMutation", totalCount); case "menu": commit("changeMenuListMutation", list); commit("changeMenuTotalCountMutation", totalCount); case "department": commit("changeDepartmentListMutation", list); commit("changeDepartmentTotalCountMutation", totalCount); case "category": commit("changeCategoryListMutation", list); commit("changeCategoryTotalCountMutation", totalCount); } }, }, }; export default systemModule; ``` store/types/type.ts ```ts import { ILoginState } from "../login/type"; import { ISystemState } from "../system/type"; export interface IRootState { name: string; age: number; } export interface IRootWidthModule { login: ILoginState; system: ISystemState; } export type IStoreType = IRootState & IRootWidthModule; ``` store/index.ts ```ts import { createStore, Store, useStore as useVuexStore } from "vuex"; import { IRootState, IStoreType } from "./types/type"; import login from "./login/login"; import system from "./system/system"; const store = createStore({ state() { return { name: "xxx", age: 18, }; }, getters: {}, mutations: {}, actions: {}, modules: { login, system, }, }); export function setupStore() { store.dispatch("login/loadLocalLogin"); } // 自定义封装useStore,使其对ts更友好 export function useStore(): Store { return useVuexStore(); } export default store; ``` ## 按钮权限 用户登录进系统后,返回的信息里就包含了该用户所具有的一些权限:菜单权限以及按钮权限 我们通过什么去进行判断该用户是否拥有按钮权限呢? 72 方式一: 通过 id 进行判断,但是 id 是动态生成的,当我们对数据进行操作的时候,id可能是变化的 方式二: 通过 name 进行判断,但是name是一些文本,是中文,这种中文的文本存在很大的一种随机性 方式三: 通过后台设计时专门设置的字段,这里比如-permission 72 ### 映射按钮权限 utils/map-menu.ts ```ts /** * 按钮权限 * @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/type.ts ```ts export interface ILoginState { ... permissions: string[]; } ``` store/login/login.ts ```ts import { Module } from "vuex"; import router from "@/router"; import { ILoginState } from "./type"; import localCache from "@/utils/cache"; import { IRootState } from "../types/type"; import { accountLoginRequest, requestUserInfoById, requestUserMenusByRoleId, } from "@/service/login/login"; import { mapMenusToRoutes, mapUserMenuListToPermissions, } from "@/utils/map-menus"; const loginModule: Module = { namespaced: true, state() { return { ... permissions: [], }; }, mutations: { changeToken(state, token: string) { state.token = token; }, changeUserInfo(state, userInfo: any) { state.userInfo = userInfo; }, changeUserMenu(state, userMenu: any) { ... // 获取用户按钮权限 ++ const permissions = mapUserMenuListToPermissions(userMenu); ++ state.permissions = permissions; }, }, actions: { ... }, // 防止用户登录进页面后手动刷新页面,导致vuex里面存储的token等数据被清空 loadLocalLogin({ commit, dispatch }) { ... }, getters: {}, }; export default loginModule; ``` ### 实现按钮权限 hooks/usePermissions.ts ```ts import { useStore } from "@/store"; export function usePermission(pageName: string, handle: string) { const store = useStore(); const permissions = store.state.login.permissions; const handlePermission = `${pageName}:${handle}`; return !!permissions.find((item) => item.indexOf(handlePermission) !== -1); } ``` base-ui/pageContent/pageContent.vue ```vue {{ contentConfig.header?.btnTitle ?? "新建数据" }} 编辑 删除 import usePermissions from "@/hooks/usePermissions"; // 0. 获取是否有对应的增删改查的组件 const isCreate = usePermission(props.pageName, "create"); const isDelete = usePermission(props.pageName, "delete"); const isUpdate = usePermission(props.pageName, "update"); const isQuery = usePermission(props.pageName, "query"); ``` base-ui/pageSearch/pageSearch.vue ```vue