# 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);