# vue3-admin **Repository Path**: nice_995/vue3-admin ## Basic Information - **Project Name**: vue3-admin - **Description**: vue3后台管理系统模板 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-02-12 - **Last Updated**: 2023-05-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue3_admin_project # 一.编程规范配置 : ## 1.ESLint + prettier 统一代码风格 ; 1. 新建.prettierrc 文件 : { "singleQuote": true, // 设置为单引号 "semi": false, // 额外的分号 "trailingComma": "none" // 尾随逗号 } 2. 打开 VSCode 设置 Tab size 为 2 ; 3. 配置 .eslintrc.js 文件的 rules 规则 ,新增: rules:{'space-before-function-paren': 'off'} ## 2.git 代码提交规范配置' ### commitizen --提交规范化工具 #### 1) `全局安装`Commitizen` npm install -g commitizen@4.2.4 #### 2) 安装并配置 `cz-customizable` 插件 npm i cz-customizable@6.3.0 --save-dev #### 3)添加以下配置到 `package.json ` 中 "config": { "commitizen": { "path": "node_modules/cz-customizable" } } #### 4)项目根目录下创建 `.cz-config.js` 自定义提示文件 module.exports = { // 可选类型 types: [ { value: "feat", name: "feat: 新功能" }, { value: "fix", name: "fix: 修复" }, { value: "docs", name: "docs: 文档变更" }, { value: "style", name: "style: 代码格式(不影响代码运行的变动)" }, { value: "refactor", name: "refactor: 重构(既不是增加feature,也不是修复bug)", }, { value: "perf", name: "perf: 性能优化" }, { value: "test", name: "test: 增加测试" }, { value: "chore", name: "chore: 构建过程或辅助工具的变动" }, { value: "revert", name: "revert: 回退" }, { value: "build", name: "build: 打包" }, ], // 消息步骤 messages: { type: "请选择提交类型:", customScope: "请输入修改范围(可选):", subject: "请简要描述提交(必填):", body: "请输入详细描述(可选):", footer: "请输入要关闭的issue(可选):", confirmCommit: "确认使用以上信息提交?(y/n/e/h)", }, // 跳过问题 skipQuestions: ["body", "footer"], // subject文字长度默认是72 subjectLimit: 72, }; #### 5) 使用 git cz 代替 git commit -m 提交代码 ## 3.git Hooks --阻止不合规的提交消息 : ### 1) `commit-msg`:可以用来规范化标准格式,并且可以按需指定是否要拒绝本次提交 ### 2) `pre-commit`:会在提交前被调用,并且可以按需指定是否要拒绝本次提交 ### 3)使用 husky + commitlint 检查提交描述是否符合规范要求 ### commitlint : #### 1)安装依赖 npm install --save-dev @commitlint/config-conventional@12.1.4 @commitlint/cli@12.1.4 #### 2)创建 `commitlint.config.js` 文件 echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js #### 3)打开`commitlint.config.js`,增加配置项 --确保保存为 `UTF-8` 的编码格式 ``` module.exports = { // 继承的规则 extends: ["@commitlint/config-conventional"], // 定义规则类型 rules: { // type 类型定义,表示 git 提交的 type 必须在以下类型范围内 "type-enum": [ 2, "always", [ "feat", // 新功能 feature "fix", // 修复 bug "docs", // 文档注释 "style", // 代码格式(不影响代码运行的变动) "refactor", // 重构(既不增加新功能,也不是修复 bug) "perf", // 性能优化 "test", // 增加测试 "chore", // 构建过程或辅助工具的变动 "revert", // 回退 "build", // 打包 ], ], //subject 大小写不做校验 "subject-case": [0], }, }; ``` ### husky : #### 1)安装依赖 npm install husky@7.0.1 --save-dev #### 2)启动 `hooks` , 生成 `.husky` 文件夹 npx husky install #### 3)在 `package.json` 中生成 `prepare` 指令( **需要 npm > 7.0 版本** ) npm set-script prepare "husky install" #### 4)执行 `prepare` 指令 npm run prepare #### 5)添加 `commitlint` 的 `hook` 到 `husky`中,并指令在 `commit-msg` 的 `hooks` 下执行 `npx --no-install commitlint --edit "$1"` 指令 npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"' ## 4.pre-commit 检测提交时代码规范 #### 1)执行代码 生成对应文件 `pre-commit` ``` npx husky add .husky/pre-commit "npx eslint --ext .js,.vue src" ``` ## 5.lint-staged 自动修复格式错误 #### 1)修改 `package.json` 配置 "lint-staged": { "src/**/*.{js,vue}": [ "eslint --fix", "git add" ] } #### 2)修改 `.husky/pre-commit` 文件 #!/bin/sh . "$(dirname "$0")/\_/husky.sh" npx lint-staged # 二 . 创建 SvgIcon 公共组件 : 1. 新建 SvgIconSvgIcon 组件 ; ``` ``` 1. 接收 props 父组件传入的 icon 名和定义的 className; 2. 计算属性 拼接 `#icon-${icon名}` 2. 新建 icons 文件夹,新建 svg 文件夹(存放下载的 svg 图标),index.js 文件(导入所有 SVG 图标,注册 SvgIcon 组件) index.js : 1. import SvgIcon from "xx" 2. 使用 require.context('./svg',/\.svg$/) //(创建出)一个 context(上下文),其中文件来自 svg 目录,request 以 `.svg` 结尾。 > 即 : > const svgRequire = require.context('./svg', false, /\.svg$/) > 可以接收一个 request 参数 ; 有三个属性 : resolve, keys , id 通过 svgRequire.keys() 获取到所有的 svg 图标,再通过遍历传入 svgRequire 中 最后 暴露出一个函数 > 即 : > export default (app)=>{ app.component('组件名',被注册的组件) } 3. 在 main.js 中 导入 icons/index.js 文件 import 方法名 from 'icons/index.js' 调用方法 : 方法名(app) --> 此处的 app 是 vue 的实例 4. 下载 svg-sprite-loader 插件 [svg-sprite-loader](https://www.npmjs.com/package/svg-sprite-loader) > 创建 vue.config.js , 新增以下配置 : > const path = require('path') const resolve = (dir) => path.join(\_\_dirname, dir) module.exports = { chainWebpack(config) { //设置 svg-sprite-loader config.module.rule('svg').exclude.add(resolve('src/icons')).end() config.module .rule('icons') .test(/\.svg$/) .include.add(resolve('src/icons')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) .end() } } # 三 . axios 企业级封装 1. npm 安装 axios 2. 新建 request.js 文件, --见 request.js 文件; 注意 : 1. 设置 baseURL 需要 在 vue.config.js 中 配置 proxy 代理 转发请求; 2. 最后需要暴露 request . 3. request.js 配置 响应拦截器 :`http://www.axios-js.com/zh-cn/docs/#%E6%8B%A6%E6%88%AA%E5%99%A8` request.interceptors.response.use( response=>{ //根据实际需求设置 }, error=>{ return new Promise.reject(error) } ) 4. request.js 配置 请求拦截器 :`http://www.axios-js.com/zh-cn/docs/#%E6%8B%A6%E6%88%AA%E5%99%A8` request.interceptors.request.use( config=>{ }, error=>{ return new Promise.reject(error) } ) # 四 . 获取 token 后保存在请求头中 : 1. 登录接口请求后,获取到返回的 token ; 2. 将 token 保存在 vuex 的 state 中: 1. 通过 mutations 的 方法 去修改 state 的值;同时将 token 保存在 localStorage 中; 2. state 中的值 如果 localStorage 中没有值,默认设置为''. 3. 在 request.js 中 设置 请求拦截器,将 token 放在请求头中 config.headers.Authorization = `Bearer ${store.getters.token}` # 五 . 登录-首页的完整功能实现 : 1. 登录 : 1. 设置表单验证. 2. 登录功能实现步骤 : 1> 封装登录的接口, 2> 在 vuex 的 action 中封装 login 方法, 返回一个 promise 对象,调用登录 api,将获取到的数据 token 存入 state 中(action 中调用 mutation 的方法 setToken,再通过 setToken 方法修改 state 的数据,同时将 Token 保存在本地存储中) 3> 在 getters 文件中 声明 方法 token,获取 state 的 token 数据;便于后期取用; 4> 登录成功后 跳转至 主页; 3. 登录功能其他配置 : 1> 路由守卫 : 判断当前是否存在 token : a. 如果存在 token ,则不需要再进入登录页面(如果是去登录页,直接跳转至首页,去其他页面则通过) b. 不存在 则跳转至登录页面(如果是白名单页面则通过,如果是其他页面则跳转至登录页) 2> 请求拦截和响应拦截 : a. 响应拦截 : 设置响应拦截器,响应成功后:获取到当前的响应数据(res.data.data),从中解构 data 进行 返回; b. 请求拦截器 : 将获取的token 数据添加至请求头中 > config.headers.Authorization = `Bearer ${store.getters.token}` 3. 主页功能 : 1. 封装 获取用户信息 接口 ; 1. vuex 中的操作 : 1. state 中声明变量 : userInfo :{} ; 1. mutation 中声明方法 : setUserInfo ; 改变 state 中的数据; 1. action 中声明方法 : getUserInfo 调用 获取用户信息的 API,并调用 setUserInfo 方法,传入 res; 1. 在 getters 中声明方法 :hasUserInfo-> 判断是否存在用户信息; userInfo: 将 state 的 userInfo 数据进行保存 1. 在路由前置守卫中, 跳转首页前,判断是否存在用户信息: 不存在 调用 getUserInfo # 六 退出登录 : 1. 主动退出 : 用户点击登录按钮之后退出 2. 被动退出 : token 过期或被其他人'顶下来' 退出 退出登录执行的操作 : 1) 清理掉当前用户缓存数据 2) 清理掉权限相关配置 3) 返回到登录页 3. 被动退出 : 1. 情况划分 : a. token 失效 ; b. 单点登录 : 其他人登录该账号被'顶下来' 2. 解决方案 : a. 主动处理 : 应对 token 失效 1> 在用户登录时,记录当前的登录时间; 2> 制定一个 失效时长 3> 在接口调用时,根据 当前时间 对比 登录时间, 看是否超过了 时效时长 : 1) 未超过,正常操作 2) 超过, 执行 退出登录 操作 b. 被动处理 (服务端进行判断): 应对 token 失效(服务端设定的 token 时效) 与 单点登录(同一账号只能在一个设备中保持在线状态) : ps : 后端增加 状态码进行判定 1. 在响应拦截器 error 中进行判断 # 七 . 动态生成 menu 菜单 : 1. 定义 路由表 对应 menu 菜单规则 : 1. 菜单规则 : 如果有 meta && meta.title && meta.icon 则显示在菜单中;否则不展示 2. 如果存在 children : 则以 el-sub-menu 展示 3. 否则 以 el-menu-item 展示 2. 根据规则制定 路由表 3. 根据规则,依据 路由表 生成 menu 菜单; # 八 . 动态生成面包屑导航 1. 封装 caleBreadcrumb 方法 , 使用 $route.matched 获取到当前的路由记录; 再筛选出自己需要的数据,动态渲染组件 2. 初始化和路由变化的时候调用该方法 const breadcrumbList=ref([]) const caleBreadcrumb =()=>{ breadcrumbList=route.matched.filter(item=> item.meta&&item.meta.title&&item.meta.icon) } watch(()=>route.path,()=>caleBreadcrumb(),{immediate:true}) # 九 . 国际化实现 1. 下载 vue-i18n 插件 : v3 使用 9.x 版本的 2. 新建 i18n 文件夹-index.js : 1. 创建数据源 messages 2. 创建初始语言 locale="en" 3. 创建 i18n 实例 (需引入 createI18n) : import {createI18n} from "vue-i18n" const i18n = createI18n({ messages, // 数据源 locale, // 语言 legacy:true, //使用 composition API 需开启 globalInjection:true // 注册函数 }) 4. 暴露 i18n 实例 3. 在 main.js 中 引入 i18n , 并注册使用 import i18n from "xx" app.use(i18n) 4. 新建 LangSelect 组件, 动态设置获取 i18 的 lang 值; 1. 在 store 中 : state 新建 language 变量 存储 (从本地获取当前的语言值,如果没有默认为 'zh') mutations 创建 修改 state.language 的方法,并将获取的 lang 保存至本地 getters 中快捷获取 language 的值; ps : language 这种特殊变量,一般会使用常量命名,所以在 constant 中声明一个常量; 2. 页面中 创建方法 , 点击时修改 i18n 的 locale 值; 并调用 setLanguage 方法 将值传入; 修改 i18n 的 locale 需 引入方法 : import {useI18n} from "vue-i18n const i18n = useI18n() i18n.locale.value = lang 5. 根据 getters.language 获取的值 动态设置页面的显示状态; 6. 页面使用 : {{ $t('数据源')}} # 九 - 1 element-plus 国际化处理 1. 在 plugin/element.js 中导入 zh,en 的语言包 2. 设置 locale : 判断当前的语言(从 getters 获取),动态设置 element-plus 的语言 export default (app) => { app.use(ElementPlus, { locale: store.getters.language === 'en' ? en : zhCn }) } # 九 - 2 自定义语言包国际化处理 1. 在 i18n/index 文件中 引入自定义的语言包(zh,en), 2. 在 messages 中 注册语言包 , const messages = { en: { msg: { ...mEnLocale } }, zh: { msg: { ...mZhLocale } } } # 九 - end 注意事项 : 1. 在.vue 文件中 使用 import {useI18n} from "vue-i18n" const i18n = useI18n i18n.t('msg.xx.xx') 2. 在.vue 的 template 中使用 {{$t('msg.xx.xx')}} 3. 在 .js 中文件使用 import i18n from "@/i18n" i18n.global.t('msg.xx.xx") 4. 处理 国际化语言 缓存 ; import { createI18n } from "vue-i18n" const i18n = createI18n({ locale:xxx }) # 十 . 全屏功能 : 1. 安装依赖 : `https://www.npmjs.com/package/screenfull` npm i screenfull 2. 创建 screenfull 组件 ; 3. 设置点击事件改变 变量 isScreenfull , 控制svg图标的切换; 4. 引入 screenfull ; import screenfull from "screenfull" 5. 使用 screenfull.toggle() 切换全屏状态 ; # 十一 . TagsView 功能 : 1. 创建 TagsView 组件 2. 将 tagsViewList 保存至 vuex 和 localStorage , 在appMain 中进行保存; 3. tagViewList->数据生成 : 监听页面路由变化,当路由变化时,将route传入 addTagsViewList 方法中 4. addTagsViewList 判断 传过来的tag是否已存在,不存在再添加至[]中; 5. 页面动态渲染tags 6. 鼠标右键菜单事件 : 1. 给tags绑定 @contextmenu.stop.prevent 事件, 触发显示右键菜单; 2. 根据 $event 获取鼠标点击的位置,动态设置contextmenu的显示位置 3. 点击时调用 vuex的方法,删减对应的元素,通过Array.splice()方法 7. 在语言切换时, 已生成的tags没有切换语言 : 1. 调用 i18n.js 封装的方法 watchSwitchLang(监听language的值,变化则触发传入的函数); 2. 传入的函数 : 1) 遍历当前的 tagsViewList , 调用vuex的changeTagsView ;将生成的国际化的tagsViewList覆盖之前的值; # 十二 . excel 导入功能 : 1. 新建uploadExcel组件; uploadExcel.vue : 1. 设置button的点击事件,设置 2. 隐藏input; 点击button时,触发input的点击事件 : const inputUploadRef = ref(null) const handleUpload = () =>{ inputUploadRef.value.click() } 3. 当上传文件时,会触发input的change事件,可以获取到事件源: 1. 从事件源中解构出当前上传文件的信息 const file = e.target.files[0] 2. 判断是否获取到当前上传的文件信息,没有获取到直接 return ; 获取到则将file传入upload方法,(上传事件) if(!file) return upload(file) 4. 设置 upload 上传事件 : 1. 先将当前的input框的值设置为 null ; 2. 判断当前父组件有没有传入 上传前的回调函数 beforeUpload(),如果没有就直接调用 readerData方法,传入file; 传入-> 返回ture的时候再调用readerData方法; const props = defineProps({ beforeUpload:{ type:function }, onSuccess:{ type:function } }) const upload = (file) => { if(!props.beforeUpload){ readerData(file) return } const before = props.beforeUpload(file) if(before){ readerData(file) } } 5. 设置readerData方法: 解析传入的文件 --> 异步操作 const readerData = (file) =>{ // 1. 返回一个promise对象 ; return new Promise ((resolve,reject) => { // 1. 实例化一个 FileReader 对象 : https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader const reader = new FileReader() // 2. 调用 FileReader.onload () 事件,获取到$event reader.onload = (e) =>{ 1)从 e 中解构出readAsArrayBuffer保存的result值 const data = e.target.result 2) 使用 XLSX 解析数据 https://www.npmjs.com/package/xlsx 2.1) 使用 XLSX.read (data,Options) 解析数据 const workbook = XLXS.read(data,{type: 'array'}) 2.2) 获取第一张工作表的名称 const firstSheetName = workbook.SheetNames[0] 2.3) 获取第一张工作表的数据 const firstSheetData = workbook.Sheet[firstSheetName] 2.4) 解析表头 --> 使用通用的方法 getHeaderRow const header = getHeaderRow(firstSheetName) 2.5) 解析数据体: XLXS.utils.sheet_to_json(数据) const result = XLXS.utils.sheet_to_json(firstSheetData) 2.6) 将解析后的数据传入 generateData 方法中 generateData({header,result}) 2.7) 完成异步 resolve() resolve() } // 3. 调用 FileReader.readAsArrayBuffer(file) --> 有这步才有2的onload 事件 reader.readAsArrayBuffer(file) }) } 6. generateData 方法 : 1. 判断当前是否有成功回调, 有就传入解析后的数据 const generateData = (excelData) => { props.onSuccess && props.onSuccess(excelData) } 7. 获取到解析后的数据 : onSuccess(excelData) : 1. apis/user.js --> 封装上传接口 1) 接口所需的数据格式是 : [{"username":"刘备","mobile":15500000000,"role":"管理员" \|\| ["管理员","员工"],"openTime":"2021-08-12"}] 2. 接口需要的数据只有 results 数据; 将results数据处理成对应的格式: 3. import/utils.js : 1) 创建 USER_RELATIONS 保存需要替换的key名; 2) 新建 generateExcelData(results) , 遍历获取每个item的key,再遍历keys,根据USER_RELATIONS进行替换; 3) 生成对应的每一个item项,再push进 arr数组,最后return arr; 4. 调用接口,传入对应的数据; 5. 上传成功,跳转至管理页面 8. 问题: 1) excel时间解析会有问题 ; 调用通用的方法formatDate-->判断当前的key名===openTime , 处理时间数据 2) 上传数据后 刷新才会显示新增的数据 : 是因为 appMain 使用了 进行缓存, 在import/index 调用 onActived 生命周期, 在获取到新数据的时候更新; onActived(getTabelData); ​