Vue3 的中后台管理系统基础建设
vue-cli 创建项目
如果你需要升级版本,那么可以通过以下指令进行升级:
1 | npm update -g @vue/cli |
终端输入 vue create 项目名称 ,即可进入 模板选择
1 | // 利用 vue-cli 创建项目 |
代码规范
eslint
.eslintrc.js 文件
1 | module.exports = { |
prettier 代码格式化
在
VSCode中安装prettier插件。在项目中新建
.prettierrc文件,该文件为perttier默认配置文件在该文件中写入如下配置:
1
2
3
4
5
6
7
8{
// 不尾随分号
"semi": false,
// 使用单引号
"singleQuote": true,
// 多行逗号分割的语法中,最后一行不加逗号
"trailingComma": "none"
}打开
VSCode《设置面板》, 搜索 save,勾选Format On Save。
git 提交规范
使用较多的 Angular 团队规范 延伸出的 Conventional Commits specification(约定式提交)
git提交规范
- 约定式提交规范要求如下:
1 | <type>[optional scope]: <description> |
全局安装
Commitizen1
npm install -g commitizen@4.2.4
安装并配置
cz-customizable插件使用
npm下载cz-customizable1
npm i cz-customizable@6.3.0 --save-dev
添加以下配置到
package.json中1
2
3
4
5
6...
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
项目根目录下创建
.cz-config.js自定义提示文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31module.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,
};使用
git cz代替git commit
使用git cz代替git commit,即可看到提示内容安装依赖:
1
npm install --save-dev @commitlint/config-conventional@12.1.4 @commitlint/cli@12.1.4
创建
commitlint.config.js文件1
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
打开
commitlint.config.js, 增加配置项( config-conventional 默认配置点击可查看 ):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26module.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],
},
};安装依赖:
1
npm install husky@7.0.1 --save-dev
启动
hooks, 生成.husky文件夹1
npx husky install
在
package.json中生成prepare指令( 需要 npm > 7.0 版本 )1
npm set-script prepare "husky install"
执行
prepare指令1
npm run prepare
执行成功
添加
commitlint的hook到husky中,并指令在commit-msg的hooks下执行npx --no-install commitlint --edit "$1"指令1
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
修改
package.json配置1
2
3
4
5
6"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
}
1 |
|
svg 组件开发
1 | <template> |
创建 utils/validate.js:
1 | /** |
在 views/login/index.vue 中使用 外部 svg (https://res.lgdsunday.club/user.svg):
1 | <span class="svg-container"> |
- icons 文件下 index.js 代码:
1 | import SvgIcon from "@/components/SvgIcon"; |
- 在
main.js中引入该文件
1 | ... |
axios 封装
1 | import axios from "axios"; |
- 在用户登陆时,记录当前 登录时间
- 制定一个 失效时长
- 在接口调用时,根据 当前时间 对比 登录时间 ,看是否超过了 时效时长
- 如果未超过,则正常进行后续操作
- 如果超过,则进行 退出登录 操作
1 | import { TIME_STAMP, TOKEN_TIMEOUT_VALUE } from "@/constant"; |
面包屑结构数据
1 | <template> |
国际化 vue-i18n
vue3 下需要使用 V 9.x 的 i18n
安装
vue-i18n1
npm install vue-i18n@next
创建
i18n/index.js文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { createI18n } from "vue-i18n";
import mZhLocale from "./lang/zh";
import mEnLocale from "./lang/en";
import store from "@/store";
const messages = {
en: {
msg: {
...mEnLocale,
},
},
zh: {
msg: {
...mZhLocale,
},
},
};
// vuex管理语言的state
function getLanguage() {
return store && store.getters && store.getters.language;
}
const i18n = createI18n({
// 使用 Composition API 模式,则需要将其设置为false
legacy: false,
// 全局注入 $t 函数
globalInjection: true,
locale: getLanguage(),
messages,
});
export default i18n;在
main.js中导入1
2
3import i18n from "@/i18n";
app.use(i18n);element-puls 语言处理
1
2
3
4
5
6
7
8
9
10
11import zh from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";
import "element-plus/dist/index.css";
app
.use(ElementPlus, {
locale: store.getters.language === "en" ? en : zh,
})
.use(store)
.use(router)
.use(i18n)
.mount("#app");
动态换肤原理
动态换肤在实际的后台管理系统项目开发中基本用不到的,只简单讲下实现原理方法。
动态换肤的关键是修改 css 变量 的值, vuex 设置全局 css 变量动态棒的。
element-plus 主体变更通过修改 scss 变量 的形式修改主题色完成主题变更。获取 element-plus 所有的 style,更换样式的部分使用正则完成替换。把替换后的样式写入到 style 标签中。
rgb-hex:转换 RGB(A)颜色为十六进制
- css-color-function:在 CSS 中提出的颜色函数的解析器和转换器
页面全屏
浏览器本身已经提供 API,Document.exitFullscreen()和 Element.requestFullscreen(),但有一定的小问题,比如 appmain 区域背景颜色为黑色。
screenfull
1
npm i screenfull@5.1.0
创建
components/Screenfull/index1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<template>
<div>
<svg-icon
:icon="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
@click="onToggle"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import screenfull from "screenfull";
// 是否全屏
const isFullscreen = ref(false);
// 监听变化
const change = () => {
isFullscreen.value = screenfull.isFullscreen;
};
// 切换事件
const onToggle = () => {
screenfull.toggle();
};
// 设置侦听器
onMounted(() => {
screenfull.on("change", change);
});
// 删除侦听器
onUnmounted(() => {
screenfull.off("change", change);
});
</script>
<style lang="scss" scoped></style>
headerSearch 原理及方案分析
指定搜索框中对当前应用中所有页面进行检索,以 select 的形式展示出被检索的页面,以达到快速进入的目的
- 获取所有的页面数据,用作被检索的数据源
根据用户输入内容在数据源中进行模糊搜索
1
npm install --save fuse.js
把搜索到的内容以
select进行展示- 监听
select的change事件,完成对应跳转
tabsView
监听路由的变化,获取路由的数据
右键根据不同类型处理展示数据
/TagsView/ContextMenu.vue
1 | <template> |
/TagsView/index.vue
1 | <template> |
1 | /** |
1 | import { TAGS_VIEW } from "@/constant"; |
局部打印
浏览器右键时,其实可以直接看到对应的 打印 选项,但是这个打印选项是直接打印整个页面,不能指定打印页面中的某一部分的。
1 | npm i vue3-print-nb@0.1.4 |
然后利用该工具完成下载功能:
指定
printLoading1
2
3
4
5
6<el-button
type="primary"
:loading="printLoading"
>{{ $t('msg.userInfo.print')}}</el-button>
// 打印相关
const printLoading = ref(false)创建打印对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14const printObj = {
// 打印区域
id: "userInfoBox",
// 打印标题
popTitle: "imooc-vue-element-admin",
// 打印前
beforeOpenCallback(vue) {
printLoading.value = true;
},
// 执行打印
openCallback(vue) {
printLoading.value = false;
},
};指定打印区域
id匹配1
<div id="userInfoBox" class="user-info-box"></div>
写入如下代码
1
2
3
4
5import print from "vue3-print-nb";
export default (app) => {
app.use(print);
};在
main.js中导入该指令1
2import installDirective from "@/directives";
installDirective(app);将打印指令挂载到
el-button中1
2
3<el-button type="primary" v-print="printObj" :loading="printLoading"
>{{ $t('msg.userInfo.print') }}</el-button
>
RBAC 权限系统
基于 角色的权限 控制 用户的访问。员工管理为用户指定角色、通过角色列表为角色指定权限、通过权限列表查看当前项目所有权限。
permission.js,在 main.js 引入,进行路由权限控制判断,获取当前用户权限菜单进行数据处理整合,最好路由菜单的渲染
1 | import router from "./router"; |
1 | // 专门处理权限路由的模块,privateRoutes:依据权限进行动态配置的 |
按钮级别权限自定义指令
我们期望最终可以通过这样格式的指令进行功能受控
v-permission="['importUser']"以此创建对应的自定义指令
directives/permission1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import store from "@/store";
function checkPermission(el, binding) {
// 获取绑定的值,此处为权限
const { value } = binding;
// 获取所有的功能指令
const points = store.getters.userInfo.permission.points;
// 当传入的指令集为数组时
if (value && value instanceof Array) {
// 匹配对应的指令
const hasPermission = points.some((point) => {
return value.includes(point);
});
// 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
// eslint-disabled-next-line
throw new Error('v-permission value is ["admin","editor"]');
}
}
export default {
// 在绑定元素的父组件被挂载后调用
mounted(el, binding) {
checkPermission(el, binding);
},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
update(el, binding) {
checkPermission(el, binding);
},
};在
directives/index中绑定该指令1
2
3
4
5
6
7...
import permission from './permission'
export default (app) => {
...
app.directive('permission', permission)
}在所有功能中,添加该指令
views/role-list/index1
<el-button v-permission="['distributePermission']"> 删除 </el-button>
动态表格渲染方案
展示可勾选的列,动态展示表格的列。通过 v-for 渲染 el-table-column ,当数据改变时 el-table-column 的渲染自然也就发生了变化
v-for 渲染所有可勾选的列的列,默认全部勾选,当数据发生变化,通过 watch 监听,filter 得到勾选后变化的列。
table
1 | <el-table ref="tableRef" :data="tableData" border> |
markdown
功能需要满足基本需求,star 在 10K 以上的,作者对该库的维护程度,文档越详尽越好。
markdown编辑器:- tui.editor:
Markdown所见即所得编辑器-高效且可扩展,使用 MIT 开源协议。 - editor:纯文本
markdown编辑器 - editor.md:开源可嵌入的在线
markdown编辑器(组件),基于CodeMirror&jQuery&Marked。国产 - markdown-here:谷歌开源,但是已经 多年不更新 了
- stackedit:基于
PageDown,Stack Overflow和其他 Stack Exchange 站点使用的Markdown库的功能齐全的开源 Markdown 编辑器。两年未更新了 - markdown-it:可配置语法,可添加、替换规则。挺长时间未更新了
- tui.editor:
- 富文本编辑器:
- wangEditor:国产、文档详尽、更新快速
- tinymce:对
IE6+和Firefox1.5+都有着非常良好的支持 - quill:代码高亮功能、视频加载功能、公式处理比较强。
- ckeditor5:编辑能力强
- wysiwyg-editor:收费的 , 就是牛
使用
下载 tui.editor
1
npm i @toast-ui/editor@3.0.2
渲染基本结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<template>
<div class="markdown-container">
<!-- 渲染区 -->
<div id="markdown-box"></div>
</div>
</div>
</template>
<script setup>
import {} from 'vue'
</script>
<style lang="scss" scoped>
.markdown-container {
.bottom {
margin-top: 20px;
text-align: right;
}
}
</style>初始化
editor,处理国际化内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<script setup>
import MkEditor from "@toast-ui/editor";
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/i18n/zh-cn";
import { onMounted } from "vue";
import { useStore } from "vuex";
// Editor实例
let mkEditor;
// 处理离开页面切换语言导致 dom 无法被获取
let el;
onMounted(() => {
el = document.querySelector("#markdown-box");
initEditor();
});
const store = useStore();
const initEditor = () => {
mkEditor = new MkEditor({
el,
height: "500px",
previewStyle: "vertical",
language: store.getters.language === "zh" ? "zh-CN" : "en",
});
mkEditor.getMarkdown();
};
</script>在语言改变时,重置
editor1
2
3
4
5
6
7
8
9import { watchSwitchLang } from "@/utils/i18n";
watchSwitchLang(() => {
if (!el) return;
const htmlStr = mkEditor.getHTML();
mkEditor.destroy();
initEditor();
mkEditor.setHTML(htmlStr);
});
wangeditor 富文本
1 | npm i wangeditor@4.7.6 |
安装完成之后,我们就去实现对应的代码逻辑:
创建基本组件结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<template>
<div class="editor-container">
<div id="editor-box"></div>
<div class="bottom">
<el-button type="primary" @click="onSubmitClick">{{
$t("msg.article.commit")
}}</el-button>
</div>
</div>
</template>
<script setup>
import {} from "vue";
</script>
<style lang="scss" scoped>
.editor-container {
.bottom {
margin-top: 20px;
text-align: right;
}
}
</style>初始化
wangEditor1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<script setup>
import E from "wangeditor";
import { onMounted } from "vue";
// Editor实例
let editor;
// 处理离开页面切换语言导致 dom 无法被获取
let el;
onMounted(() => {
el = document.querySelector("#editor-box");
initEditor();
});
const initEditor = () => {
editor = new E(el);
editor.config.zIndex = 1;
// 菜单栏提示
editor.config.showMenuTooltips = true;
editor.config.menuTooltipPosition = "down";
editor.create();
};
</script>wangEditor的 国际化处理,官网支持 i18next,所以想要处理wangEditor的国际化,那么我们需要安装 i18next1
npm i --save i18next@20.4.0
处理提交事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import { onMounted, defineProps, defineEmits } from 'vue'
import { commitArticle } from './commit'
const props = defineProps({
title: {
required: true,
type: String
}
})
const emits = defineEmits(['onSuccess'])
...
const onSubmitClick = async () => {
// 创建文章
await commitArticle({
title: props.title,
content: editor.txt.html()
})
editor.txt.html('')
emits('onSuccess')
}不要忘记在
article-create中处理对应事件1
<editor :title="title" :detail="detail" @onSuccess="onSuccess"></editor>
最后处理编辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39const props = defineProps({
...
detail: {
type: Object
}
})
// 编辑相关
watch(
() => props.detail,
(val) => {
if (val && val.content) {
editor.txt.html(val.content)
}
},
{
immediate: true
}
)
const onSubmitClick = async () => {
if (props.detail && props.detail._id) {
// 编辑文章
await editArticle({
id: props.detail._id,
title: props.title,
content: editor.txt.html()
})
} else {
// 创建文章
await commitArticle({
title: props.title,
content: editor.txt.html()
})
}
editor.txt.html('')
emits('onSuccess')
}