1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2025-12-28 20:10:22 +08:00

186 Commits

Author SHA1 Message Date
张传龙
bdbe9b8483 refactor: dynamic routes 2022-09-18 15:10:22 +08:00
张传龙
30211e14ea chore: update deps 2022-09-18 15:08:06 +08:00
张传龙
e7b1896d9e chore: update deps 2022-09-15 21:44:58 +08:00
张传龙
a5c1046e67 mod: remove 2022-09-14 09:14:37 +08:00
张传龙
31670cd671 mod: base demo 2022-09-11 17:08:36 +08:00
张传龙
b0e3a94e12 style: remove extra code 2022-09-10 16:00:39 +08:00
张传龙
2b2a324a62 refactor: simplify noNeedToken judge 2022-09-10 15:56:53 +08:00
张传龙
40483e09e6 refactor: keep alive 2022-09-09 09:53:49 +08:00
张传龙
a5a3472486 refactor: setupExtend replace with defineOptions 2022-09-08 15:08:28 +08:00
张传龙
fd1752693a chore: update deps 2022-09-08 15:06:23 +08:00
张传龙
2f3a83758a mod: reload page 2022-09-08 09:00:28 +08:00
张传龙
738212c84b Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2022-09-07 16:13:50 +08:00
张传龙
a4f3e16007 style: update icon and title 2022-09-07 16:12:38 +08:00
Ronnie Zhang
5b2d1c68dd Merge pull request #26 from haichao0817/main
style: change 'loging' to 'loading'
2022-09-07 14:56:17 +08:00
wukang
7b8b50322c change 'loging' to 'loading' 2022-09-07 11:01:02 +08:00
张传龙
bb171866b6 refactor: routes and file 2022-09-04 12:18:47 +08:00
张传龙
f1bc9edbac refactor: request error tip 2022-09-04 11:35:30 +08:00
张传龙
3a38adc71e fix: mock api error 2022-09-03 22:43:57 +08:00
Ronnie Zhang
b760cc34dd Merge pull request #25 from zclzone/feature/crud-table
Feature/crud table
2022-09-03 22:34:49 +08:00
张传龙
b59e47b5dd feat: finish curd table 2022-09-03 22:28:37 +08:00
张传龙
d1dd58215d wip: crud table 2022-09-03 17:33:20 +08:00
张传龙
661aed1a94 style: add annotation 2022-09-03 17:32:30 +08:00
张传龙
f2e2fc6819 wip: crud table 2022-09-01 14:53:18 +08:00
张传龙
9ea8ffd7fd wip: crud table 2022-08-31 10:16:38 +08:00
张传龙
af983d16b9 wip: commonPage 2022-08-29 10:08:18 +08:00
张传龙
079761b6fd feat: add page components 2022-08-28 19:37:23 +08:00
张传龙
841bab0d63 release: release v1.0.0 2022-08-27 14:51:08 +08:00
张传龙
453148fc8d build: update commitlint 2022-08-27 14:50:34 +08:00
张传龙
7ec078bd7a Revert "mod: gh-pages gzip"
This reverts commit dd0bc3e6e8.
2022-08-27 14:47:18 +08:00
张传龙
dd0bc3e6e8 mod: gh-pages gzip 2022-08-27 14:36:21 +08:00
张传龙
8c665c727b style: format 2022-08-27 14:26:14 +08:00
张传龙
da98aa1c7d docs: update readme 2022-08-27 14:23:12 +08:00
张传龙
51b47ea722 chore: upgrade to vite3 2022-08-27 14:22:09 +08:00
张传龙
220a7800f7 refactor: refactor 2022-08-27 14:09:32 +08:00
张传龙
230e3a72d9 chore: update deps 2022-08-27 12:03:58 +08:00
张传龙
0cefadc2a5 refactor: custom icon 2022-08-27 11:46:34 +08:00
张传龙
2f1b747243 feat: add compress plugin 2022-08-27 11:04:07 +08:00
张传龙
296d5ea6f0 perf: png replace with webp 2022-08-27 10:58:22 +08:00
张传龙
3a415703d4 perf: table demo 2022-08-27 10:36:07 +08:00
张传龙
006f730457 perf: table demo 2022-08-26 22:48:03 +08:00
张传龙
606c5a2df0 docs: update readme 2022-08-25 18:26:29 +08:00
张传龙
30c375cc1d fix(components): fix tags contenxtmenu
ISSUES CLOSED: #23
2022-08-18 10:56:28 +08:00
张传龙
ddf14053da chore: update deps 2022-08-18 09:42:08 +08:00
张传龙
38edbcb68a mod: update mock data 2022-08-18 09:39:09 +08:00
Ronnie Zhang
3e54a82abb Merge pull request #22 from amplest/main
fix: get请求无法接收参数
2022-08-12 22:59:43 +08:00
Xiongxing
df6225a752 fix: get请求无法接收参数 2022-08-12 21:10:39 +08:00
张传龙
63c1f2f132 refactor: routes sort 2022-08-08 15:44:16 +08:00
张传龙
0bb2a904e7 refactor: adjust routes 2022-08-08 15:36:43 +08:00
张传龙
ef3aaa5be5 refactor: refactor async routes 2022-08-07 22:25:28 +08:00
张传龙
869a68812c chore: adjust commitlink config 2022-08-04 18:04:08 +08:00
张传龙
fd0032e0e9 docs: update readme 2022-08-02 09:34:25 +08:00
张传龙
b53d7daaa1 docs: update readme 2022-08-02 09:31:17 +08:00
张传龙
856bdfd0ee docs: update readme 2022-07-31 18:08:13 +08:00
张传龙
9f9884759c refactor: simplify permission-guard 2022-07-30 22:11:53 +08:00
张传龙
7dad43d003 docs: update readme 2022-07-29 16:48:58 +08:00
张传龙
7762e02b31 refactor: refactor api usage 2022-07-25 18:36:22 +08:00
张传龙
e5768fa1e3 style: modify login page 2022-07-23 22:17:23 +08:00
张传龙
7ee613d8cf style: modify footer 2022-07-23 22:10:01 +08:00
张传龙
80a5b7f053 style: modify custom scrollbar 2022-07-21 17:47:11 +08:00
张传龙
eb160731da feat: login page compatible mobile 2022-07-20 18:26:38 +08:00
张传龙
789231a7f4 style: format 2022-07-20 09:13:07 +08:00
张传龙
6ea6e1c267 chore: update settings.json 2022-07-20 09:10:40 +08:00
张传龙
d971e7e4ba fix: fix incorrent judgment of isHash 2022-07-19 16:29:07 +08:00
张传龙
215998dc66 perf: optimize login page 2022-07-17 20:37:44 +08:00
张传龙
40f9ac1a6b fix: fix incorrent usage of vue router 2022-07-17 14:54:45 +08:00
张传龙
6ec5588ed4 chore: setup lint-staged 2022-07-15 14:48:40 +08:00
张传龙
380e5768c4 chore: update settings.json 2022-07-15 14:06:57 +08:00
张传龙
5856f601fa style: use unocss rewrite css 2022-07-14 18:05:47 +08:00
张传龙
d10b8f0e96 chore(prettier): update prettier config 2022-07-14 18:04:00 +08:00
张传龙
3860cf9ebb style: simplify unocss test page 2022-07-14 16:40:25 +08:00
张传龙
94b46d9bf6 chore(unocss): update unocss config 2022-07-14 16:39:24 +08:00
张传龙
4df7d44bf1 docs: modify annotation 2022-07-13 22:58:09 +08:00
张传龙
42b8aca37b docs(readme): update readme 2022-07-11 16:07:07 +08:00
张传龙
0c96d0e937 docs(readme): update readme 2022-07-11 12:29:48 +08:00
张传龙
b540f5599f fix: fix incorrect text 2022-07-11 12:28:33 +08:00
张传龙
18b8a81640 fix(other): disabled unplugin generate .d.ts 2022-07-10 22:24:23 +08:00
张传龙
06b3afc2de build(deps): update unplugin deps 2022-07-10 22:20:21 +08:00
92376
3088773ebe fix: modify exit full screen icon
* !1 退出全屏 icon
* 退出全屏 icon
2022-07-10 12:55:45 +00:00
张传龙
83b42bf6b8 chore(projects): add husky and commitlint 2022-07-10 14:02:02 +08:00
张传龙
fd08d25ccf perf: optimize ScrooX component. 2022-07-09 15:03:39 +08:00
张传龙
76c3f0b64c perf: optimize ScrooX component. 2022-07-09 14:38:38 +08:00
张传龙
a1db8273f5 chore: update unocss dependencies. 2022-07-08 22:35:55 +08:00
张传龙
f5ab04112f chore: update settings.json 2022-07-08 17:43:57 +08:00
张传龙
805b2e066f docs: update readme 2022-07-06 10:10:23 +08:00
Ronnie Zhang
6979b245a9 Merge pull request #19 from sean3112/main
微调一下demo
2022-07-05 20:15:27 +08:00
Sean Huang
dff8862c75 feat: Add response code 400.
fix: Change the parameter naming of the get method to params.
2022-07-05 18:35:05 +08:00
Sean Huang
1da5e8d573 Merge remote-tracking branch 'origin/main' 2022-07-05 18:12:01 +08:00
张传龙
7f97dd2f5a style: update prettier format rules 2022-07-03 14:52:49 +08:00
Sean Huang
1f69f07100 Merge remote-tracking branch 'origin/main' 2022-07-03 00:15:38 +08:00
张传龙
f97beeb54b perf: add remember me feature 2022-07-02 00:03:34 +08:00
张传龙
57bc68e7b0 refactor: simplify wrapper storage 2022-07-01 23:27:05 +08:00
Ronnie Zhang
90aa54d4a4 Merge pull request #18 from sean3112/patch-1
Breakpoints issues, depends on 'vite-plugin-vue-setup-extend-plus' instead of 'vite-plugin-vue-setup-extend'
2022-07-01 15:24:32 +08:00
Sean Huang
7564f115d6 Breakpoints issues
Solved the problem that the breakpoint is not in the source code location when debugging.
2022-07-01 15:07:01 +08:00
Sean Huang
8d3753a80e Update package.json
Breakpoints are not in the source code location during debugging
2022-07-01 12:58:43 +08:00
Sean Huang
a816028560 调试时,断点不在源码位置处,更新插件依赖vite-plugin-vue-setup-extend为vite-plugin-vue-setup-extend-plus即可。 2022-07-01 12:38:38 +08:00
张传龙
acde2c1004 feat: Breadcrumb add Icon 2022-06-30 18:29:26 +08:00
张传龙
cb5dd34e17 refactor: simplify mock setup 2022-06-26 18:42:07 +08:00
张传龙
73c82520ca mod: use unocss rewrite the demo page 2022-06-26 18:25:14 +08:00
张传龙
e465ee50bf mod: use unocss rewrite the 404 page 2022-06-26 15:39:44 +08:00
张传龙
2be3f095aa mod: delete extra code 2022-06-26 15:37:57 +08:00
张传龙
26ecafffdc docs: update readme 2022-06-26 15:26:52 +08:00
张传龙
7150d93394 docs: update readme 2022-06-26 15:09:00 +08:00
张传龙
6e26769679 docs: update readme 2022-06-25 14:45:50 +08:00
张传龙
b7ce7912a7 revert: 简化构建步骤,撤销app.config.js功能 2022-06-25 14:45:23 +08:00
张传龙
1fa9d4d472 fix: 修复common文件夹大小写问题 2022-06-25 13:48:54 +08:00
张传龙
fa11b1bc64 fix: 修复common文件夹大小写问题 2022-06-25 13:47:30 +08:00
张传龙
4bf8916fdc style: update settings.json 2022-06-24 15:12:16 +08:00
张传龙
8496c08646 docs: update readme 2022-06-23 19:30:22 +08:00
张传龙
065868a40b fix: 修复高度溢出问题 2022-06-22 20:36:53 +08:00
张传龙
dd4cd871ba style: 样式调整 2022-06-19 19:41:49 +08:00
张传龙
a2b84d35f7 mod: 修改文件夹名大小写 2022-06-19 19:18:12 +08:00
张传龙
e128dfabc7 refactor: Refactor Naive UI AppProvider 2022-06-19 17:11:38 +08:00
张传龙
21c1d6d3aa feat: 添加白屏loading效果 2022-06-19 16:27:44 +08:00
张传龙
3990d4da80 feat: 集成unplugin-auto-import自动引入 2022-06-19 15:22:01 +08:00
张传龙
ac9ccbadf0 mod: 使用unocss重写首页样式 2022-06-19 15:02:58 +08:00
张传龙
8ae4046285 mod: 修改友好提示 2022-06-19 14:50:50 +08:00
张传龙
f88b4f52a1 refactor: 重构图标使用方式,集成自定应图标 2022-06-19 13:35:36 +08:00
张传龙
00ba77c15e chore: 依赖更新 2022-06-18 22:07:50 +08:00
张传龙
39a80926bf perf: 全局样式调整 2022-06-18 22:00:58 +08:00
张传龙
16957a96b7 mod: 使用unocss重写登录页 2022-06-18 19:14:50 +08:00
张传龙
ef33b28492 mod: 细节调整 2022-06-17 23:05:57 +08:00
张传龙
ae43ffb94f refactor: 重构异常处理 2022-06-17 22:44:32 +08:00
张传龙
f0b6ce7d20 chore: update unocss.config.js 2022-06-17 17:48:25 +08:00
张传龙
c3354afa6c docs: update readme 2022-06-15 12:50:49 +08:00
张传龙
08ef914528 chore: update settings.json 2022-06-12 11:37:33 +08:00
张传龙
d86ee26ad6 chore: update naive ui dependencies. 2022-06-11 22:15:15 +08:00
张传龙
c8495f7a5f style: 主题相关调整 2022-06-11 22:14:23 +08:00
张传龙
0636ac4716 perf: 优化多标签滚动 2022-06-11 20:17:30 +08:00
张传龙
67d966e096 refactor: 简化unocss集成 2022-06-11 16:55:36 +08:00
张传龙
b5ac614943 docs: update readme 2022-06-05 18:15:19 +08:00
张传龙
9151b2d297 feat: 菜单自定义排序 2022-06-03 22:42:33 +08:00
张传龙
84f8431134 mod: 删除非必要代码 2022-06-03 19:50:10 +08:00
张传龙
fdc49f6dcc refactor: 重构axios封装 2022-06-03 19:49:38 +08:00
张传龙
d2b88a8300 mod: 调整import顺序 2022-05-25 19:53:57 +08:00
张传龙
ffc042167a fear: 集成Naive UI Notification组件 2022-05-24 15:13:11 +08:00
张传龙
85f9c91d6e mod: 移除非必要代码 2022-05-23 18:05:18 +08:00
张传龙
21391b202f feat: 头部增加github源码网站 2022-05-22 20:50:32 +08:00
张传龙
36ddb23db6 docs: update README 2022-05-21 17:22:35 +08:00
张传龙
f3c391c031 fix: 修改配置路由写法以修复热更新问题 2022-05-20 18:33:13 +08:00
张传龙
df378f784b chore: 依赖更新 2022-05-20 18:31:21 +08:00
张传龙
2154267615 mod: 修改mock数据 2022-05-19 15:52:22 +08:00
张传龙
3203a9a459 chore: update vite.config.js 2022-05-19 15:40:02 +08:00
张传龙
5ce2150706 fix: 修复vite热重启后proxy失效问题 2022-05-19 12:02:01 +08:00
张传龙
5bd380037c mod: 示例代码修改 2022-05-18 20:40:31 +08:00
张传龙
e63e9f5cf2 chore: update settings.json 2022-05-17 11:01:23 +08:00
张传龙
74c244cf37 chore: update extensions 2022-05-17 11:01:12 +08:00
张传龙
5cd85cf72d mod: 修改工具方法 2022-05-16 18:29:32 +08:00
张传龙
96d88a97f1 chore: 依赖更新 2022-05-16 18:27:56 +08:00
张传龙
00c32a950a chore: 依赖更新 2022-05-15 18:51:57 +08:00
张传龙
bfd048d40a mod: 调整示例页面 2022-05-13 15:09:41 +08:00
张传龙
1254a199d7 refactor: 重构sidebar 2022-05-13 14:52:11 +08:00
张传龙
1190d08a87 refactor: 规范文件夹结构 2022-05-12 19:50:21 +08:00
张传龙
958589edd0 refactor: 重构header 2022-05-11 14:54:12 +08:00
张传龙
2338ded165 feat: 集成全屏功能 2022-05-10 17:18:04 +08:00
张传龙
4d6a58bfc8 feat: 集成vueUse 2022-05-10 17:17:49 +08:00
张传龙
f15e21b0a0 feat: 集成菜单栏伸缩功能 2022-05-08 11:59:01 +08:00
张传龙
f88820b727 style: 调整404页面 2022-05-07 23:01:30 +08:00
张传龙
598d256be4 style: lint fix 2022-05-06 22:36:22 +08:00
张传龙
5b51cfb4f1 chore: update extensions 2022-05-06 22:35:33 +08:00
张传龙
45c2e3aebe chore: 简化eslint配置 2022-05-06 22:34:55 +08:00
张传龙
c2249d531f chore: 依赖更新 2022-05-05 21:13:45 +08:00
张传龙
44c6b420d0 chore: 依赖更新 2022-05-04 19:49:55 +08:00
张传龙
117a46a251 chore: update extensions 2022-05-04 18:03:41 +08:00
张传龙
d8569a4eb1 refactor: 简化文件夹结构 2022-05-03 22:37:51 +08:00
张传龙
c268b3c75d mod: 修改table示例的写法 2022-05-03 22:28:51 +08:00
张传龙
21e0d86fcd refactor: app-main 弃用naive ui虚拟滚动条,改成自定义滚动条 2022-05-02 17:39:31 +08:00
张传龙
76bd414941 mod: 调整多标签滑动写法 2022-05-01 18:16:41 +08:00
张传龙
894b87426a fix: 修复标签栏溢出换行问题 2022-04-30 18:39:42 +08:00
张传龙
f9c2362cd8 mod: 规范化调整.vue文件命名 2022-04-29 20:40:29 +08:00
张传龙
d922dcc224 docs: update readme 2022-04-26 19:59:59 +08:00
张传龙
7c8a17bbb2 fix: 修复递归判断单个子路由错误问题 2022-04-25 09:56:23 +08:00
张传龙
321e19a3a5 fix: 修复isHidden路由未正确判断问题 2022-04-25 09:51:02 +08:00
张传龙
bf2d45416f feat: 单个菜单时替换父菜单处理 2022-04-23 22:34:12 +08:00
张传龙
cf1b83d3f1 feat: 集成多标签右键菜单 2022-04-23 19:23:12 +08:00
张传龙
bf63fb5ab7 mod: 修改侧边菜单渲染菜单使用封装的renderIcon方法 2022-04-23 19:10:55 +08:00
张传龙
a6f86ee315 feat: 封装renderIcon方法 2022-04-23 19:09:06 +08:00
张传龙
b2cf78b36d mod: 删除测试代码 2022-04-22 08:58:43 +08:00
张传龙
c9c0c35343 revert: 撤销router-view页面组件多根节点支持 2022-04-21 22:45:10 +08:00
张传龙
3c46d2c159 feat: 集成重新加载页面功能 2022-04-21 22:35:26 +08:00
张传龙
585bf4a4c4 fix: 修复router-view页面组件因transition导致使用多个根节点告警问题 2022-04-20 18:03:23 +08:00
张传龙
2bd85e6e60 feat: 集成rollup打包分析插件 2022-04-19 21:50:19 +08:00
张传龙
967ae1c483 style: 调整layout布局 2022-04-18 22:01:30 +08:00
张传龙
238bceb500 fix: 修复http拦截强制覆盖Authorization问题 2022-04-17 21:16:30 +08:00
张传龙
c2145c0ddb mod: 修改路由name 2022-04-17 16:33:12 +08:00
张传龙
d759c9b9ae docs: update readme 2022-04-16 22:54:03 +08:00
张传龙
3fdba613d3 docs: update readme 2022-04-15 22:19:09 +08:00
153 changed files with 7087 additions and 2790 deletions

45
.cz-config.js Normal file
View File

@@ -0,0 +1,45 @@
module.exports = {
types: [
{ value: 'feat', name:'feat: 新增功能' },
{ value: 'fix', name:'fix: 修复bug' },
{ value: 'docs', name:'docs: 文档变更' },
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name:'perf: 性能优化' },
{ value: 'test', name:'test: 添加、修改测试用例' },
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name:'revert: 回滚 commit' },
{ value: 'wip', name:'wip: 开发中' },
{ value: 'mod', name:'mod: 不确定分类的修改' },
{ value: 'release', name:'release: 发布' },
],
scopes: [
['custom', '自定义'],
['projects', '项目搭建'],
['components', '组件相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
['other', '其他修改'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
}
}),
messages: {
type: '确保本次提交遵循 Angular 规范!选择你要提交的类型:\n',
scope: '选择一个 scope可选',
customScope: '请输入自定义的 scope',
subject: '填写简短精炼的变更描述:',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
breaking: '列举非兼容性重大的变更(可选):',
footer: '列举出所有变更的 Issues Closed可选。 例如: #31, #34',
confirmCommit: '确认提交?'
},
allowBreakingChanges: ['feat', 'fix'],
subjectLimit: 100,
breaklineChar: '|'
}

5
.env
View File

@@ -1,6 +1,3 @@
VITE_APP_TITLE = 'Vue Naive Admin' VITE_TITLE = 'Vue Naive Admin'
VITE_PORT = 3100 VITE_PORT = 3100
# 打包时自动生成CNAME文件用于配置github pages自定义域名如不需要可注释或者直接删除
# VITE_APP_GLOB_CNAME = 'template.qszone.com'

View File

@@ -2,13 +2,13 @@
VITE_PUBLIC_PATH = '/' VITE_PUBLIC_PATH = '/'
# 是否启用MOCK # 是否启用MOCK
VITE_APP_USE_MOCK = true VITE_USE_MOCK = true
# proxy # 是否启用MOCK
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]] VITE_USE_PROXY = false
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
VITE_PROXY_TYPE = 'dev'
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_BASE_API = '/api'
# test base api
VITE_APP_GLOB_BASE_API_TEST = '/api-test'

View File

@@ -1,16 +1,13 @@
# 自定义域名CNAME # 自定义域名CNAME
# VITE_APP_GLOB_CNAME = 'template.qszone.com' # VITE_CNAME = 'template.qszone.com'
# 资源公共路径,需要以 /开头和结尾 # 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/vue-naive-admin/' VITE_PUBLIC_PATH = '/vue-naive-admin/'
VITE_APP_USE_HASH = true VITE_USE_HASH = true
# 是否启用MOCK # 是否启用MOCK
VITE_APP_USE_MOCK = true VITE_USE_MOCK = true
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_BASE_API = '/api'
# test base api
VITE_APP_GLOB_BASE_API_TEST = '/api-test'

View File

@@ -2,10 +2,13 @@
VITE_PUBLIC_PATH = '/' VITE_PUBLIC_PATH = '/'
# 是否启用MOCK # 是否启用MOCK
VITE_APP_USE_MOCK = true VITE_USE_MOCK = true
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_BASE_API = '/api'
# test base api # 是否启用压缩
VITE_APP_GLOB_BASE_API_TEST = '/api-test' VITE_USE_COMPRESS = true
# 压缩类型
VITE_COMPRESS_TYPE = gzip

View File

@@ -1,10 +1,7 @@
VITE_PUBLIC_PATH = '/' VITE_PUBLIC_PATH = '/'
# 是否启用MOCK # 是否启用MOCK
VITE_APP_USE_MOCK = true VITE_USE_MOCK = true
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_BASE_API = '/api'
# test base api
VITE_APP_GLOB_BASE_API_TEST = '/api-test'

View File

@@ -1,3 +1,4 @@
node_modules node_modules
dist dist
public public
package.json

View File

@@ -1,17 +1,6 @@
// * https://zhuanlan.zhihu.com/p/388703150
module.exports = { module.exports = {
root: true, root: true,
env: {
browser: true, // browser global variables
node: true,
es2021: true, // adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.
},
parserOptions: {
ecmaVersion: 2020,
},
parser: 'vue-eslint-parser',
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'], extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
plugins: ['prettier'],
rules: { rules: {
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'vue/valid-template-root': 'off', 'vue/valid-template-root': 'off',

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
dist dist
*.local *.local
stats.html

36
.husky/_/husky.sh Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env sh
if [ -z "$husky_skip_init" ]; then
debug () {
if [ "$HUSKY_DEBUG" = "1" ]; then
echo "husky (debug) - $1"
fi
}
readonly hook_name="$(basename -- "$0")"
debug "starting $hook_name..."
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
readonly husky_skip_init=1
export husky_skip_init
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
if [ $exitCode = 127 ]; then
echo "husky - command not found in PATH=$PATH"
fi
exit $exitCode
fi

4
.husky/commit-msg Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

4
.husky/pre-commit Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:staged

View File

@@ -1,13 +1,9 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "vue.volar",
"esbenp.prettier-vscode", "antfu.iconify",
"johnsoncodehk.volar",
"hollowtree.vue-snippets",
"esbenp.prettier-vscode",
"mikestead.dotenv", "mikestead.dotenv",
"wayou.vscode-todo-highlight", "sdras.vue-vscode-snippets",
"aaron-bond.better-comments", "cipchk.cssrem",
"antfu.iconify"
] ]
} }

38
.vscode/settings.json vendored
View File

@@ -1,30 +1,34 @@
{ {
"files.eol": "\n",
"path-intellisense.mappings": { "path-intellisense.mappings": {
"@/": "${workspaceRoot}/src" "@/": "${workspaceRoot}/src",
"~/": "${workspaceRoot}"
}, },
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": { "prettier.printWidth": 120,
"editor.defaultFormatter": "esbenp.prettier-vscode" "prettier.singleQuote": true,
}, "prettier.semi": false,
"[html]": { "prettier.endOfLine": "lf",
"editor.defaultFormatter": "esbenp.prettier-vscode" "files.eol": "\n",
},
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.formatOnSave": false
}, },
"[css]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.formatOnSave": false
},
"[typescriptreact]": {
"editor.formatOnSave": false
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.formatOnSave": false
}, },
"[markdown]": { "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
},
"eslint.validate": ["javascript", "javascriptreact", "typescript"],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"editor.formatOnSave": true "files.associations": {
"*.env.*": "dotenv",
"*.css": "postcss"
}
} }

130
README.md
View File

@@ -7,109 +7,109 @@
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a> <a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a> <a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a> <a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin/releases"><img allt="releases" src="https://badgen.net/github/releases/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a> <a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p> </p>
<p align='center'>
<b>English</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a>
</p>
### 简介 ### Introduction
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin),一个基于 Vue3.0、Vite、Naive UI 的后台管理模板相较于其他比较流行的后台管理模板此项目相对简洁、轻量学习成本非常低对新手极其友好。不过麻雀虽小五脏俱全权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置以及一些经常用的基础组件封装等等这些该有的都有非常适用于中小型项目或者个人项目也可此模板进行二次封装改造用于大型项目。 [Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin templateBased on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
### 为什么要开发这个模板 ### Features
- Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多 - 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You.
- 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西) - 🍑 Integrated login, logout and permission verification.
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
- 🍎 Integrated `eslint + prettier`.
- 🍌 Integrated `husky + commitlint`.
- 🍉 Integrated `Mock`.
- 🍍 Integrated `pinia`lightweight, simple and easy to use alternative to vuex.
- 📦 Integrated `unplugin` auto import.
- 🤹 Integrated `iconify` iconsupport custom svg icons.
- 🍇 Integrated `unocss`.
### 功能 ### Preview
- 🍒 集成 Naive UI尤大推荐的 UI 组件库,[https://www.naiveui.com](https://www.naiveui.com) [https://template.qszone.com](https://template.qszone.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产和github pages环境
- 🍎 集成 Eslint + Prettier代码约束和格式化统一
- 🍉 集成 Mock 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍇 集成 unocssantfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的
- 🍍 集成 PiniaVuex 的替代方案轻量、简单、易用尤大已表示不会有Vuex5或者说pinia就是Vuex5
- 📦 集成 Vite 自动导入插件unplugin-vue-components解放双手开发效率直接起飞
- 🤹 集成 unplugin-icons插件优雅使用iconify图标
- 🍏 二次封装 Axios支持多 axios 实例,支持线上环境免重新打包修改 baseURL
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
- 🍋 二次封装 localStorage 和 sessionStorage支持设置过期时间
### 预览 [https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
[template.qszone.com](https://template.qszone.com) ### Docs
[github pages](https://zclzone.github.io/vue-naive-admin)
### 文档
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs) [Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
[羽雀文档Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
### 构建 ### Getting Started
```shell ```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf # Recommended setup git autocrlf 为 false
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false git config --global core.autocrlf false
# 克隆项目 # Clone Project
git clone https://github.com/zclzone/vue-naive-admin.git git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin cd vue-naive-admin
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation) # Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
pnpm i # 或者 npm i npm i -g pnpm # Installed and can be ignored
pnpm i # or npm i
# 启动 # Start
npm run dev pnpm dev
``` ```
### 发布 ### Build and Release
```shell ```shell
# 构建测试环境 # Test Environment
npm run build:test pnpm build:test
# 构建github pages环境 # Github Environment
npm run build:github pnpm build:github
# 构建生产环境 # Prod Environment
npm run build pnpm build
``` ```
### 其他指令 ### Other
```shell ```shell
# eslint代码格式检查 # eslint check
npm run lint pnpm lint
# 代码检查并修复 # eslint check and fix
npm run lint:fix pnpm lint:fix
# 预览发布包效果(需先执行构建指令 # PreviewNeed to build first
npm run preview pnpm preview
# Commithusky+commitlint
pnpm cz
``` ```
### 规范 ### TS version: Qs Admin
#### source code
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### preview
- [https://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Communication group & About the author
<a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a>
#### git commit 规范
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
- `mod` 不确定分类的修改

115
README.zh-CN.md Normal file
View File

@@ -0,0 +1,115 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="https://assets.qszone.com/images/logo_qs.svg">
</a>
</p>
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>简体中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">English</a>
</p>
### 简介
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,学习成本非常低,非常适合中小型项目或者个人项目。
### 功能
- 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产和github pages环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
### 预览
[https://template.qszone.com](https://template.qszone.com)
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
### 文档
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
[羽雀文档Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
### 快速开始
```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false
# 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # 装了可忽略
pnpm i # 或者 npm i
# 启动
pnpm dev
```
### 构建发布
```shell
# 构建测试环境
pnpm build:test
# 构建github pages环境
pnpm build:github
# 构建生产环境
pnpm build
```
### 其他指令
```shell
# eslint代码格式检查
pnpm lint
# 代码检查并修复
pnpm lint:fix
# 预览发布包效果(需先执行构建指令)
pnpm preview
# 提交代码husky+commitlint
pnpm cz
```
### TS 版本: Qs Admin
#### 源码
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### 预览
- [https://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### 入群交流 & 关于作者
<a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a>

13
build/config/define.js Normal file
View File

@@ -0,0 +1,13 @@
import dayjs from 'dayjs'
/**
* * 此处定义的是全局常量启动或打包后将添加到window中
* https://vitejs.cn/config/#define
*/
// 项目构建时间
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
export const viteDefine = {
_BUILD_TIME_,
}

2
build/config/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './define'
export * from './proxy'

15
build/config/proxy.js Normal file
View File

@@ -0,0 +1,15 @@
import { getProxyConfig } from '../../settings'
export function createViteProxy(isUseProxy = true, proxyType) {
if (!isUseProxy) return undefined
const proxyConfig = getProxyConfig(proxyType)
const proxy = {
[proxyConfig.prefix]: {
target: proxyConfig.target,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${proxyConfig.prefix}`), ''),
},
}
return proxy
}

View File

@@ -1,3 +1 @@
export const GLOB_CONFIG_FILE_NAME = 'app.config.js'
export const GLOB_CONFIG_NAME = '__APP__GLOB__CONF__'
export const OUTPUT_DIR = 'dist' export const OUTPUT_DIR = 'dist'

15
build/plugin/html.js Normal file
View File

@@ -0,0 +1,15 @@
import { createHtmlPlugin } from 'vite-plugin-html'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_TITLE,
},
},
})
return htmlPlugin
}

40
build/plugin/index.js Normal file
View File

@@ -0,0 +1,40 @@
import vue from '@vitejs/plugin-vue'
/**
* * unocss插件原子css
* https://github.com/antfu/unocss
*/
import Unocss from 'unocss/vite'
// rollup打包分析插件
import visualizer from 'rollup-plugin-visualizer'
// 压缩
import viteCompression from 'vite-plugin-compression'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv?.VITE_USE_MOCK) {
plugins.push(configMockPlugin(isBuild))
}
if (viteEnv.VITE_USE_COMPRESS) {
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
}
if (isBuild) {
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
)
}
return plugins
}

View File

@@ -2,12 +2,11 @@ import { viteMockServe } from 'vite-plugin-mock'
export function configMockPlugin(isBuild) { export function configMockPlugin(isBuild) {
return viteMockServe({ return viteMockServe({
ignore: /^\_/, mockPath: 'mock/api',
mockPath: 'mock',
localEnabled: !isBuild, localEnabled: !isBuild,
prodEnabled: isBuild, prodEnabled: isBuild,
injectCode: ` injectCode: `
import { setupProdMockServer } from '../mock/_create-prod-server'; import { setupProdMockServer } from '../mock';
setupProdMockServer(); setupProdMockServer();
`, `,
}) })

45
build/plugin/unplugin.js Normal file
View File

@@ -0,0 +1,45 @@
import { resolve } from 'path'
import DefineOptions from 'unplugin-vue-define-options/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
/**
* * unplugin-icons插件自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [
DefineOptions(),
AutoImport({
imports: ['vue', 'vue-router'],
dts: false,
}),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
dts: false,
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}),
]

View File

@@ -1,13 +1,14 @@
import { resolve } from 'path'
import chalk from 'chalk' import chalk from 'chalk'
import { writeFileSync } from 'fs-extra' import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant' import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils' import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() { export function runBuildCNAME() {
const { VITE_APP_GLOB_CNAME } = getEnvConfig() const { VITE_CNAME } = getEnvConfig()
if (!VITE_APP_GLOB_CNAME) return if (!VITE_CNAME) return
try { try {
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_GLOB_CNAME) writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
} catch (error) { } catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error)) console.log(chalk.red('CNAME file failed to package:\n' + error))
} }

View File

@@ -1,29 +0,0 @@
import { GLOB_CONFIG_FILE_NAME, GLOB_CONFIG_NAME, OUTPUT_DIR } from '../constant'
import fs, { writeFileSync } from 'fs-extra'
import chalk from 'chalk'
import { getEnvConfig, getRootPath } from '../utils'
function createConfig(option) {
const { config, configName, configFileName } = option
try {
const windowConf = `window.${configName}`
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '')
fs.mkdirp(getRootPath(OUTPUT_DIR))
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error))
}
}
export function runBuildConfig() {
const config = getEnvConfig()
const configName = GLOB_CONFIG_NAME
const configFileName = GLOB_CONFIG_FILE_NAME
createConfig({ config, configName, configFileName })
}

View File

@@ -1,10 +1,8 @@
import chalk from 'chalk' import chalk from 'chalk'
import { runBuildConfig } from './build-config'
import { runBuildCNAME } from './build-cname' import { runBuildCNAME } from './build-cname'
export const runBuild = async () => { export const runBuild = async () => {
try { try {
runBuildConfig()
runBuildCNAME() runBuildCNAME()
console.log(`${chalk.cyan('build successfully!')}`) console.log(`${chalk.cyan('build successfully!')}`)
} catch (error) { } catch (error) {

View File

@@ -2,33 +2,38 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import dotenv from 'dotenv' import dotenv from 'dotenv'
export function wrapperEnv(envOptions) { /**
if (!envOptions) return {} * * 项目根路径
const ret = {} * @descrition 结尾不带/
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
for (const key in envOptions) { /**
let val = envOptions[key] * * 项目src路径
if (['true', 'false'].includes(val)) { * @param srcName src目录名称(默认: "src")
val = val === 'true' * @descrition 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
} }
if (['VITE_PORT'].includes(key)) {
val = +val const httpsReg = /^https:\/\//
export function convertEnv(envOptions) {
const result = {}
if (!envOptions) return result
for (const envKey in envOptions) {
let envVal = envOptions[envKey]
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
result[envKey] = envVal
} }
if (key === 'VITE_PROXY' && val) { return result
try {
val = JSON.parse(val.replace(/'/g, '"'))
} catch (error) {
val = ''
}
}
ret[key] = val
if (typeof key === 'string') {
process.env[key] = val
} else if (typeof key === 'object') {
process.env[key] = JSON.stringify(val)
}
}
return ret
} }
/** /**
@@ -45,7 +50,7 @@ function getConfFiles() {
return ['.env', '.env.local', '.env.production'] return ['.env', '.env.local', '.env.production']
} }
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) { export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
let envConfig = {} let envConfig = {}
confFiles.forEach((item) => { confFiles.forEach((item) => {
try { try {
@@ -65,7 +70,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
}) })
return envConfig return envConfig
} }
export function getRootPath(...dir) {
return path.resolve(process.cwd(), ...dir)
}

View File

@@ -1,32 +0,0 @@
import html from 'vite-plugin-html'
import { version } from '../../../package.json'
import { GLOB_CONFIG_FILE_NAME } from '../../constant'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`
const getAppConfigSrc = () => {
return `${path}${GLOB_CONFIG_FILE_NAME}?v=${version}-${new Date().getTime()}`
}
const htmlPlugin = html({
minify: isBuild,
inject: {
data: {
title: VITE_APP_TITLE,
},
tags: isBuild
? [
{
tag: 'script',
attrs: {
src: getAppConfigSrc(),
},
},
]
: [],
},
})
return htmlPlugin
}

View File

@@ -1,42 +0,0 @@
import vue from '@vitejs/plugin-vue'
/**
* * 扩展setup插件支持在script标签中使用name属性
* usage: <script setup name="MyComp"></script>
*/
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
/**
* * 组件库按需引入插件
* usage: 直接使用组件,无需在任何地方导入组件
*/
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
/**
* * unplugin-icons插件自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { unocss } from './unocss'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [
vue(),
VueSetupExtend(),
Components({
resolvers: [NaiveUiResolver()],
}),
Icons({ compiler: 'vue3', autoInstall: true }),
unocss(),
configHtmlPlugin(viteEnv, isBuild),
]
viteEnv?.VITE_APP_USE_MOCK && plugins.push(configMockPlugin(isBuild))
return plugins
}

View File

@@ -1,9 +0,0 @@
import Unocss from 'unocss/vite'
import { presetUno, presetAttributify, presetIcons } from 'unocss'
// https://github.com/antfu/unocss
export function unocss() {
return Unocss({
presets: [presetUno(), presetAttributify(), presetIcons()],
})
}

View File

@@ -1,18 +0,0 @@
const httpsRE = /^https:\/\//
export function createProxy(list = []) {
const ret = {}
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target)
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {}),
}
}
return ret
}

26
commitlint.config.js Normal file
View File

@@ -0,0 +1,26 @@
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert',
'wip',
'mod',
'release',
],
],
},
}

View File

@@ -8,11 +8,28 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.svg" />
<link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title> <title><%= title %></title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app">
<!-- 白屏时的loading效果 -->
<div class="loading-container">
<div id="loadingLogo" class="loading-svg"></div>
<div class="loading-spin__container">
<div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div>
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
</div>
</div>
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"~/*": ["./*"],
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"jsx": "preserve" "jsx": "preserve"

View File

@@ -1,14 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.globEager('./**/*.js')
const mockModules = []
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return
}
mockModules.push(...modules[key].default)
})
export function setupProdMockServer() {
createProdMockServer(mockModules)
}

View File

@@ -1,4 +1,4 @@
import { resolveToken } from '../_utils' import { resolveToken } from '../utils'
const token = { const token = {
admin: 'admin', admin: 'admin',

5
mock/api/index.js Normal file
View File

@@ -0,0 +1,5 @@
import auth from './auth'
import user from './user'
import post from './post'
export default [...auth, ...user, ...post]

134
mock/api/post.js Normal file
View File

@@ -0,0 +1,134 @@
const posts = [
{
title: '使用纯css优雅配置移动端rem布局',
author: '大脸怪',
category: 'Css',
description: '通常配置rem布局会使用js进行处理比如750的设计稿会这样...',
content: '通常配置rem布局会使用js进行处理比如750的设计稿会这样',
isRecommend: true,
isPublish: true,
createDate: '2021-11-04T04:03:36.000Z',
updateDate: '2021-11-04T04:03:36.000Z',
},
{
title: 'Vue2&Vue3项目风格指南',
author: 'Ronnie',
category: 'Vue',
description: '总结的Vue2和Vue3的项目风格',
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
isRecommend: true,
isPublish: true,
createDate: '2021-10-25T08:57:47.000Z',
updateDate: '2022-02-28T04:02:39.000Z',
},
{
title: '如何优雅的给图片添加水印',
author: '大脸怪',
category: 'JavaScript',
description: '优雅的给图片添加水印',
content: '我之前写过一篇文章记录了一次上传图片的优化史',
isRecommend: true,
isPublish: true,
createDate: '2021-06-24T18:46:19.000Z',
updateDate: '2021-09-23T07:51:22.000Z',
},
{
title: '前端缓存的理解',
author: '大脸怪',
category: 'Http',
description: '谈谈前端缓存的理解',
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true,
isPublish: true,
createDate: '2021-06-10T18:51:19.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
title: 'Promise的五个静态方法',
author: '大脸怪',
category: 'JavaScript',
description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景',
content: '## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true,
isPublish: true,
createDate: '2021-02-22T22:37:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
]
export default [
{
url: '/api/posts',
method: 'get',
response: (data = {}) => {
const { title, pageNo, pageSize } = data.query
let pageData = []
let total = 60
const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0))
if (filterData.length) {
if (pageSize) {
while (pageData.length < pageSize) {
pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))])
}
} else {
pageData = filterData
}
pageData = pageData.map((item, index) => ({
id: pageSize * (pageNo - 1) + index + 1,
...item,
}))
} else {
total = 0
}
return {
code: 0,
message: 'ok',
data: {
pageData,
total,
pageNo,
pageSize,
},
}
},
},
{
url: '/api/post',
method: 'post',
response: ({ body }) => {
return {
code: 0,
message: 'ok',
data: body,
}
},
},
{
url: '/api/post/:id',
method: 'put',
response: ({ query, body }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
body,
},
}
},
},
{
url: '/api/post/:id',
method: 'delete',
response: ({ query }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
},
}
},
},
]

View File

@@ -1,4 +1,4 @@
import { resolveToken } from '../_utils' import { resolveToken } from '../utils'
const users = { const users = {
admin: { admin: {

6
mock/index.js Normal file
View File

@@ -0,0 +1,6 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
import api from './api'
export function setupProdMockServer() {
createProdMockServer(api)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +1,40 @@
{ {
"name": "vue-naive-admin", "name": "vue-naive-admin",
"version": "0.3.2", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .js,.vue .", "lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .", "lint:fix": "eslint --fix --ext .js,.vue .",
"build": "vite build && esno ./build/script", "lint:staged": "lint-staged",
"build:test": "vite build --mode test && esno ./build/script", "build": "vite build",
"build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script", "build:github": "vite build --mode github && esno ./build/script",
"preview": "vite preview" "preview": "vite preview",
"prepare": "husky install",
"cz": "cz"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^8.4.2",
"axios": "^0.21.4", "axios": "^0.21.4",
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"md-editor-v3": "^1.11.4", "md-editor-v3": "^1.11.4",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"pinia": "^2.0.13", "pinia": "^2.0.13",
"vue": "^3.2.31", "vue": "^3.2.39",
"vue-router": "^4.0.14" "vue-router": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/mdi": "^1.1.9", "@commitlint/cli": "^17.0.3",
"@iconify-json/simple-icons": "^1.1.7", "@commitlint/config-conventional": "^17.0.3",
"@unocss/preset-attributify": "^0.16.4", "@iconify/json": "^2.1.99",
"@unocss/preset-icons": "^0.16.4", "@iconify/vue": "^3.2.1",
"@unocss/preset-uno": "^0.16.4",
"@vitejs/plugin-vue": "^1.10.2", "@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31", "@vue/compiler-sfc": "^3.2.31",
"chalk": "^5.0.1", "chalk": "^5.0.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.9.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.12.0", "eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
@@ -36,15 +42,31 @@
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0", "esno": "^0.13.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"naive-ui": "^2.27.0", "husky": "^8.0.1",
"lint-staged": "^13.0.3",
"naive-ui": "^2.33.3",
"prettier": "^2.6.1", "prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10", "sass": "^1.49.10",
"unocss": "^0.16.4", "unocss": "^0.43.2",
"unplugin-icons": "^0.14.1", "unplugin-auto-import": "^0.9.2",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.17.21", "unplugin-vue-components": "^0.17.21",
"vite": "^2.9.1", "unplugin-vue-define-options": "^0.11.2",
"vite-plugin-html": "^2.1.2", "vite": "^3.1.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-vue-setup-extend": "^0.3.0" "vite-plugin-svg-icons": "^2.0.1"
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
},
"lint-staged": {
"*.{js,vue}": [
"eslint --ext .js,.vue ."
]
} }
} }

4203
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
.loading-container {
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loading-svg {
width: 128px;
height: 128px;
color: var(--primary-color);
}
.loading-spin__container {
width: 56px;
height: 56px;
margin: 36px 0;
}
.loading-spin {
position: relative;
height: 100%;
animation: loadingSpin 1s linear infinite;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.top-0 {
top: 0;
}
.bottom-0 {
bottom: 0;
}
.loading-spin-item {
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primary-color);
border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes loadingSpin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
.loading-delay-500 {
-webkit-animation-delay: 500ms;
animation-delay: 500ms;
}
.loading-delay-1000 {
-webkit-animation-delay: 1000ms;
animation-delay: 1000ms;
}
.loading-delay-1500 {
-webkit-animation-delay: 1500ms;
animation-delay: 1500ms;
}
.loading-title {
font-size: 28px;
font-weight: 500;
color: #6a6a6a;
}

View File

@@ -0,0 +1,25 @@
/**
* 初始化加载效果的svg格式logo
* @param {string} id - 元素id
*/
function initSvgLogo(id) {
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" style="fill:currentColor"></path></svg>`
const appEl = document.querySelector(id)
const div = document.createElement('div')
div.innerHTML = svgStr
if (appEl) {
appEl.appendChild(div)
}
}
function addThemeColorCssVars() {
const key = '__THEME_COLOR__'
const defaultColor = '#316c72'
const themeColor = window.localStorage.getItem(key) || defaultColor
const cssVars = `--primary-color: ${themeColor}`
document.documentElement.style.cssText = cssVars
}
addThemeColorCssVars()
initSvgLogo('#loadingLogo')

2
settings/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './theme.json'
export * from './proxy-config'

18
settings/proxy-config.js Normal file
View File

@@ -0,0 +1,18 @@
const proxyConfigMappings = {
dev: {
prefix: '/api',
target: 'http://localhost:8080',
},
test: {
prefix: '/api',
target: 'http://localhost:8080',
},
prod: {
prefix: '/api',
target: 'http://localhost:8080',
},
}
export function getProxyConfig(envType = 'dev') {
return proxyConfigMappings[envType]
}

View File

@@ -1,21 +1,17 @@
{ {
"header": {
"height": 60
},
"tags": { "tags": {
"visible": true, "visible": true,
"height": 50 "height": 50
}, },
"header": {
"height": 60
},
"naiveThemeOverrides": { "naiveThemeOverrides": {
"common": { "common": {
"primaryColor": "#316C72FF", "primaryColor": "#316C72FF",
"primaryColorHover": "#316C72E3", "primaryColorHover": "#316C72E3",
"primaryColorPressed": "#2B4C59FF", "primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#316C7263", "primaryColorSuppl": "#316C7263"
"successColor": "#316C72FF",
"successColorHover": "#316C72E3",
"successColorPressed": "#2B4C59FF",
"successColorSuppl": "#316C7263"
} }
} }
} }

View File

@@ -7,14 +7,5 @@
</template> </template>
<script setup> <script setup>
import AppProvider from '@/components/AppProvider/index.vue' import AppProvider from '@/components/common/AppProvider.vue'
</script> </script>
<style lang="scss">
#app {
height: 100%;
.n-config-provider {
height: inherit;
}
}
</style>

View File

@@ -1,16 +0,0 @@
import { defAxios as request } from '@/utils/http'
export const login = (data) => {
return request({
url: '/auth/login',
method: 'post',
data,
})
}
export const refreshToken = () => {
return request({
url: '/auth/refreshToken',
method: 'post',
})
}

6
src/api/index.js Normal file
View File

@@ -0,0 +1,6 @@
import request from '@/utils/http'
export default {
getUser: () => request.get('/user'),
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
}

View File

@@ -1,39 +0,0 @@
import { defAxios as request } from '@/utils/http'
export function getPosts(data = {}) {
return request({
url: '/posts',
method: 'get',
data,
})
}
export function getPostById({ id }) {
return request({
url: `/post/${id}`,
method: 'get',
})
}
export function savePost(id, data = {}) {
if (id) {
return request({
url: `/post/${id}`,
method: 'put',
data,
})
}
return request({
url: '/post',
method: 'post',
data,
})
}
export function deletePost(id) {
return request({
url: `/post/${id}`,
method: 'delete',
})
}

View File

@@ -1,38 +0,0 @@
import { defAxios as request } from '@/utils/http'
export function getUsers(data = {}) {
return request({
url: '/users',
method: 'get',
data,
})
}
export function getUser(id) {
if (id) {
return request({
url: `/user/${id}`,
method: 'get',
})
}
return request({
url: '/user',
method: 'get',
})
}
export function saveUser(data = {}, id) {
if (id) {
return request({
url: '/user',
method: 'put',
data,
})
}
return request({
url: `/user/${id}`,
method: 'put',
data,
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

BIN
src/assets/images/404.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" fill="#316c72"></path></svg>

Before

Width:  |  Height:  |  Size: 825 B

1
src/assets/svg/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z"></path></svg>

After

Width:  |  Height:  |  Size: 811 B

View File

@@ -1,9 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512">
<path
d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z"
></path>
</svg>
</template>
<script setup name="IconLogo"></script>

View File

@@ -1,12 +0,0 @@
export { default as IconGitee } from '~icons/simple-icons/gitee'
export { default as IconChart } from '~icons/mdi/chart-bar'
export { default as IconGithub } from '~icons/mdi/github'
export { default as IconVue } from '~icons/mdi/vuejs'
export { default as IconHome } from '~icons/mdi/home'
export { default as IconLink } from '~icons/mdi/link-variant'
export { default as IconAlert } from '~icons/mdi/alert-circle-outline'
export { default as IconCircle } from '~icons/mdi/circle-outline'
export { default as IconMenu } from '~icons/mdi/menu'
export { default as IconLogo } from './IconLogo.vue'

View File

@@ -1,52 +0,0 @@
<template></template>
<script setup>
import { isNullOrUndef } from '@/utils/is'
import { useDialog } from 'naive-ui'
const NDialog = useDialog()
class Dialog {
success(title, option) {
return this.showDialog('success', { title, ...option })
}
warning(title, option) {
return this.showDialog('warning', { title, ...option })
}
error(title, option) {
return this.showDialog('error', { title, ...option })
}
showDialog(type = 'success', option) {
if (isNullOrUndef(option.title)) {
// ! 没有title的情况
option.showIcon = false
}
return NDialog[type]({
positiveText: 'OK',
closable: false,
...option,
})
}
confirm(option = {}) {
return this.showDialog(option.type || 'error', {
positiveText: '确定',
negativeText: '取消',
onPositiveClick: option.confirm,
onNegativeClick: option.cancel,
onMaskClick: option.cancel,
...option,
})
}
}
window['$dialog'] = new Dialog()
Object.freeze(window.$dialog)
Object.defineProperty(window, '$dialog', {
configurable: false,
writable: false,
})
</script>

View File

@@ -1,10 +0,0 @@
<template></template>
<script setup>
import { useLoadingBar } from 'naive-ui'
window['$loadingBar'] = useLoadingBar()
Object.defineProperty(window, '$loadingBar', {
configurable: false,
writable: false,
})
</script>

View File

@@ -1,72 +0,0 @@
<template></template>
<script setup>
import { useMessage } from 'naive-ui'
const NMessage = useMessage()
let loadingMessage = null
class Message {
/**
* 规则:
* * loading message只显示一个新的message会替换正在显示的loading message
* * loading message不会自动清除除非被替换成非loading message非loading message默认2秒后自动清除
*/
removeMessage(message, duration = 2000) {
setTimeout(() => {
if (message) {
message.destroy()
message = null
}
}, duration)
}
showMessage(type, content, option = {}) {
if (loadingMessage && loadingMessage.type === 'loading') {
// 如果存在则替换正在显示的loading message
loadingMessage.type = type
loadingMessage.content = content
if (type !== 'loading') {
// 非loading message需设置自动清除
this.removeMessage(loadingMessage, option.duration)
}
} else {
// 不存在正在显示的loading则新建一个message,如果新建的message是loading message则将message赋值存储下来
let message = NMessage[type](content, option)
if (type === 'loading') {
loadingMessage = message
}
}
}
loading(content) {
this.showMessage('loading', content, { duration: 0 })
}
success(content, option = {}) {
this.showMessage('success', content, option)
}
error(content, option = {}) {
this.showMessage('error', content, option)
}
info(content, option = {}) {
this.showMessage('info', content, option)
}
warning(content, option = {}) {
this.showMessage('warning', content, option)
}
}
window['$message'] = new Message()
Object.defineProperty(window, '$message', {
configurable: false,
writable: false,
})
</script>

View File

@@ -1,23 +0,0 @@
<template>
<n-config-provider :theme-overrides="useTheme.naiveThemeOverrides">
<n-loading-bar-provider>
<LoadingBar />
<n-dialog-provider>
<DialogContent />
<n-message-provider>
<MessageContent />
<slot></slot>
</n-message-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>
<script setup>
import MessageContent from './MessageContent.vue'
import DialogContent from './DialogContent.vue'
import LoadingBar from './LoadingBar.vue'
import { useThemeStore } from '@/store/modules/theme'
const useTheme = useThemeStore()
</script>

View File

@@ -0,0 +1,13 @@
<template>
<footer text-14 f-c-c flex-col color="#6a6a6a">
<p>
Copyright©2022
<a href="https://github.com/zclzone" target="__blank" hover="decoration-underline color-primary"> 大脸怪</a>
</p>
<p>
<a href="http://beian.miit.gov.cn/" target="__blank" hover="decoration-underline color-primary">
赣ICP备2020015008号-1
</a>
</p>
</footer>
</template>

View File

@@ -0,0 +1,50 @@
<template>
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides">
<n-loading-bar-provider>
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<slot></slot>
<NaiveProviderContent />
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>
<script setup>
import { defineComponent, h } from 'vue'
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
import { useCssVar } from '@vueuse/core'
import { kebabCase } from 'lodash-es'
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
import { naiveThemeOverrides } from '~/settings'
function setupCssVar() {
const common = naiveThemeOverrides.common
for (const key in common) {
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
}
}
// 挂载naive组件的方法至window, 以便在全局使用
function setupNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$notification = useNotification()
window.$message = setupMessage(useMessage())
window.$dialog = setupDialog(useDialog())
}
const NaiveProviderContent = defineComponent({
setup() {
setupCssVar()
setupNaiveTools()
},
render() {
return h('div')
},
})
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
<icon-ic:baseline-keyboard-arrow-left />
</div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right />
</div>
</template>
<div
ref="content"
class="content"
:class="{ overflow: isOverflow && showArrow }"
:style="{
transform: `translateX(${translateX}px)`,
}"
>
<slot />
</div>
</div>
</template>
<script setup>
import { debounce } from '@/utils'
defineProps({
showArrow: {
type: Boolean,
default: true,
},
})
const translateX = ref(0)
const content = ref(null)
const wrapper = ref(null)
const isOverflow = ref(false)
const refreshIsOverflow = debounce(() => {
const wrapperWidth = wrapper.value.offsetWidth
const contentWidth = content.value.offsetWidth
isOverflow.value = contentWidth > wrapperWidth
resetTranslateX(wrapperWidth, contentWidth)
}, 200)
function handleMouseWheel(e) {
const { wheelDelta } = e
const wrapperWidth = wrapper.value.offsetWidth
const contentWidth = content.value.offsetWidth
/**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值
* @wrapperWidth 容器的宽度
* @contentWidth 内容的宽度
*/
if (wheelDelta < 0) {
if (wrapperWidth > contentWidth && translateX.value < -10) return
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
}
if (wheelDelta > 0 && translateX.value > 10) {
return
}
translateX.value += wheelDelta
resetTranslateX(wrapperWidth, contentWidth)
}
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
if (!isOverflow.value) {
translateX.value = 0
} else if (-translateX.value > contentWidth - wrapperWidth) {
translateX.value = wrapperWidth - contentWidth
} else if (translateX.value > 0) {
translateX.value = 0
}
}, 200)
const observer = new MutationObserver(refreshIsOverflow)
onMounted(() => {
refreshIsOverflow()
window.addEventListener('resize', refreshIsOverflow)
// 监听内容宽度刷新是否超出
observer.observe(content.value, { childList: true })
})
onBeforeUnmount(() => {
window.removeEventListener('resize', refreshIsOverflow)
observer.disconnect()
})
</script>
<style lang="scss" scoped>
.wrapper {
display: flex;
background-color: #fff;
z-index: 9;
overflow: hidden;
position: relative;
.content {
padding: 0 10px;
display: flex;
align-items: center;
flex-wrap: nowrap;
transition: transform 0.5s;
&.overflow {
padding-left: 30px;
padding-right: 30px;
}
}
.left,
.right {
background-color: #fff;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
width: 20px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid #e0e0e6;
border-radius: 2px;
z-index: 2;
cursor: pointer;
}
.left {
left: 0;
}
.right {
right: 0;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<script setup>
/** 自定义图标 */
const props = defineProps({
/** 图标名称(assets/svg下的文件名) */
icon: {
type: String,
required: true,
},
size: {
type: Number,
default: 14,
},
color: {
type: String,
default: undefined,
},
})
</script>
<template>
<TheIcon type="custom" v-bind="props" />
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
const props = defineProps({
icon: {
type: String,
required: true,
},
prefix: {
type: String,
default: 'icon-custom',
},
color: {
type: String,
default: 'currentColor',
},
})
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
</script>
<template>
<svg aria-hidden="true" width="1em" height="1em">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
import { renderIcon, renderCustomIcon } from '@/utils/icon'
const props = defineProps({
icon: {
type: String,
required: true,
},
size: {
type: Number,
default: 14,
},
color: {
type: String,
default: undefined,
},
/** iconify | custom */
type: {
type: String,
default: 'iconify',
},
})
const iconCom = computed(() =>
props.type === 'iconify'
? renderIcon(props.icon, { size: props.size, color: props.color })
: renderCustomIcon(props.icon, { size: props.size, color: props.color })
)
</script>
<template>
<component :is="iconCom" />
</template>

View File

@@ -0,0 +1,17 @@
<template>
<transition name="fade-slide" mode="out-in" appear>
<section class="cus-scroll-y wh-full p-15 flex-col bg-[#f5f6fb]">
<slot />
<AppFooter v-if="showFooter" mt-15 />
</section>
</transition>
</template>
<script setup>
defineProps({
showFooter: {
type: Boolean,
default: false,
},
})
</script>

View File

@@ -0,0 +1,33 @@
<template>
<AppPage :show-footer="showFooter">
<header v-if="showHeader" px-15 mb-15 min-h-45 flex justify-between items-center>
<slot v-if="$slots.header" name="header" />
<template v-else>
<h2 color="#333" text-22 font-normal>{{ title || route.meta?.title }}</h2>
<slot name="action" />
</template>
</header>
<n-card rounded-10 flex-1>
<slot />
</n-card>
</AppPage>
</template>
<script setup>
defineProps({
showFooter: {
type: Boolean,
default: false,
},
showHeader: {
type: Boolean,
default: true,
},
title: {
type: String,
default: undefined,
},
})
const route = useRoute()
</script>

View File

@@ -0,0 +1,16 @@
<template>
<div min-h-60 p-15 flex items-start justify-between b-1 bc-ccc rounded-8 bg="#fafafc">
<n-space wrap :size="[35, 15]">
<slot />
</n-space>
<div flex-shrink-0>
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['search', 'reset'])
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div flex items-center>
<label v-if="!isNullOrWhitespace(label)" w-80 flex-shrink-0 :style="{ width: labelWidth + 'px' }">
{{ label }}
</label>
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
<slot />
</div>
</div>
</template>
<script setup>
import { isNullOrWhitespace } from '@/utils/is'
defineProps({
label: {
type: String,
default: '',
},
labelWidth: {
type: Number,
default: 80,
},
contentWidth: {
type: Number,
default: 220,
},
})
</script>

View File

@@ -0,0 +1,48 @@
<template>
<n-modal v-model:show="show" :style="{ width }" preset="card" :title="title" size="huge" :bordered="false">
<slot />
<template v-if="showFooter" #footer>
<footer flex justify-end>
<slot name="footer">
<n-button @click="show = false">取消</n-button>
<n-button :loading="loading" ml-20 type="primary" @click="emit('onSave')">保存</n-button>
</slot>
</footer>
</template>
</n-modal>
</template>
<script setup>
const props = defineProps({
width: {
type: String,
default: '600px',
},
title: {
type: String,
default: '',
},
showFooter: {
type: Boolean,
default: true,
},
visible: {
type: Boolean,
required: true,
},
loading: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:visible', 'onSave'])
const show = computed({
get() {
return props.visible
},
set(v) {
emit('update:visible', v)
},
})
</script>

View File

@@ -0,0 +1,130 @@
<template>
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
<slot name="queryBar" />
</QueryBar>
<n-data-table
:remote="remote"
:loading="loading"
:scroll-x="scrollX"
:columns="columns"
:data="tableData"
:row-key="(row) => row[rowKey]"
:pagination="isPagination ? pagination : false"
@update:checked-row-keys="onChecked"
@update:page="onPageChange"
/>
</template>
<script setup>
const props = defineProps({
/**
* @remote true: 后端分页 false 前端分页
*/
remote: {
type: Boolean,
default: true,
},
/**
* @remote 是否分页
*/
isPagination: {
type: Boolean,
default: true,
},
scrollX: {
type: Number,
default: 1200,
},
rowKey: {
type: String,
default: 'id',
},
columns: {
type: Array,
required: true,
},
/** queryBar中的参数 */
queryItems: {
type: Object,
default() {
return {}
},
},
/** 补充参数(可选) */
extraParams: {
type: Object,
default() {
return {}
},
},
/**
* ! 约定接口入参出参
* * 分页模式需约定分页接口入参
* @pageSize 分页参数一页展示多少条默认10
* @pageNo 分页参数页码默认1
* * 需约定接口出参
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
* @total 分页模式必须非分页模式如果没有total则取上一层data.length
*/
getData: {
type: Function,
required: true,
},
})
const emit = defineEmits(['update:queryItems', 'onChecked'])
const loading = ref(false)
const initQuery = { ...props.queryItems }
const tableData = ref([])
const pagination = reactive({ page: 1, pageSize: 10 })
async function handleQuery() {
try {
loading.value = true
let paginationParams = {}
// 如果非分页模式或者使用前端分页,则无需传分页参数
if (props.isPagination && props.remote) {
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
}
const { data } = await props.getData({ ...props.queryItems, ...props.extraParams, ...paginationParams })
tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length
} catch (error) {
tableData.value = []
pagination.itemCount = 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
handleQuery()
}
async function handleReset() {
const queryItems = { ...props.queryItems }
for (const key in queryItems) {
queryItems[key] = ''
}
emit('update:queryItems', { ...queryItems, ...initQuery })
await nextTick()
pagination.page = 1
handleQuery()
}
function onPageChange(currentPage) {
pagination.page = currentPage
if (props.remote) {
handleQuery()
}
}
function onChecked(rowKeys) {
if (props.columns.some((item) => item.type === 'selection')) {
emit('onChecked', rowKeys)
}
}
defineExpose({
handleSearch,
handleReset,
})
</script>

1
src/composables/index.js Normal file
View File

@@ -0,0 +1 @@
export { default as useCRUD } from './useCRUD'

103
src/composables/useCRUD.js Normal file
View File

@@ -0,0 +1,103 @@
import { isNullOrWhitespace } from '@/utils/is'
const ACTIONS = {
view: '查看',
edit: '编辑',
add: '新增',
}
export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
const modalVisible = ref(false)
const modalAction = ref('')
const modalTitle = computed(() => ACTIONS[modalAction.value] + name)
const modalLoading = ref(false)
const modalFormRef = ref(null)
const modalForm = ref({ ...initForm })
/** 新增 */
function handleAdd() {
modalAction.value = 'add'
modalVisible.value = true
modalForm.value = { ...initForm }
}
/** 修改 */
function handleEdit(row) {
modalAction.value = 'edit'
modalVisible.value = true
modalForm.value = { ...row }
}
/** 查看 */
function handleView(row) {
modalAction.value = 'view'
modalVisible.value = true
modalForm.value = { ...row }
}
/** 保存 */
function handleSave() {
if (!['edit', 'add'].includes(modalAction.value)) {
modalVisible.value = false
return
}
modalFormRef.value?.validate(async (err) => {
if (err) return
const actions = {
add: {
api: () => doCreate(modalForm.value),
cb: () => $message.success('新增成功'),
},
edit: {
api: () => doUpdate(modalForm.value),
cb: () => $message.success('编辑成功'),
},
}
const action = actions[modalAction.value]
try {
modalLoading.value = true
const data = await action.api()
action.cb()
modalLoading.value = modalVisible.value = false
data && refresh(data)
} catch (error) {
modalLoading.value = false
}
})
}
/** 删除 */
function handleDelete(id, confirmOptions) {
if (isNullOrWhitespace(id)) return
$dialog.confirm({
content: '确定删除?',
async confirm() {
try {
modalLoading.value = true
const data = await doDelete(id)
$message.success('删除成功')
modalLoading.value = false
refresh(data)
} catch (error) {
modalLoading.value = false
}
},
...confirmOptions,
})
}
return {
modalVisible,
modalAction,
modalTitle,
modalLoading,
handleAdd,
handleDelete,
handleEdit,
handleView,
handleSave,
modalForm,
modalFormRef,
}
}

View File

@@ -1,17 +1,17 @@
<template> <template>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition name="fade-slide" mode="out-in" appear> <KeepAlive :include="keepAliveRouteNames">
<keep-alive :include="keepAliveRouteNames"> <component :is="Component" v-if="appStore.reloadFlag" :key="route.meta?.key || route.fullPath" />
<component :is="Component" :key="route.path" /> </KeepAlive>
</keep-alive>
</transition>
</router-view> </router-view>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { useAppStore } from '@/store/modules/app'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const appStore = useAppStore()
const router = useRouter() const router = useRouter()
const allRoutes = router.getRoutes() const allRoutes = router.getRoutes()
const keepAliveRouteNames = computed(() => { const keepAliveRouteNames = computed(() => {
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name) return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)

View File

@@ -1,18 +0,0 @@
<template>
<n-breadcrumb>
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
{{ item.meta.title }}
</n-breadcrumb-item>
</n-breadcrumb>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const { currentRoute } = router
function handleBreadClick(path) {
if (path === currentRoute.value.path) return
router.push(path)
}
</script>

View File

@@ -1,96 +0,0 @@
<template>
<n-dropdown :options="options" @select="handleSelect">
<div class="avatar">
<img :src="userStore.avatar" />
<span>{{ userStore.name }}</span>
</div>
</n-dropdown>
</template>
<script setup>
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
import { resetRouter } from '@/router'
import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE } from '@/router/routes'
const userStore = useUserStore()
const router = useRouter()
const options = [
{
label: '切换角色',
key: 'switchRole',
},
{
label: '退出登录',
key: 'logout',
},
]
function handleSelect(key) {
if (key === 'logout') {
logout()
} else if (key === 'switchRole') {
switchRole()
}
}
function logout() {
userStore.logout()
$message.success('已退出登录')
router.push({ path: '/login' })
}
function switchRole() {
const permissionStore = usePermissionStore()
const users = [
{
id: 1,
name: '大脸怪(admin)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['admin'],
},
{
id: 2,
name: '大脸怪(editor)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['editor'],
},
{
id: 3,
name: '访客(guest)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
role: [],
},
]
const switchUser = users[+userStore.userId % users.length]
resetRouter()
userStore.setUserInfo(switchUser)
const accessRoutes = permissionStore.generateRoutes(switchUser.role)
accessRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
router.addRoute(NOT_FOUND_ROUTE)
$message.success(`${switchUser.name}`)
}
</script>
<style lang="scss" scoped>
.avatar {
display: flex;
align-items: center;
cursor: pointer;
img {
width: 100%;
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 10px;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<n-breadcrumb>
<n-breadcrumb-item
v-for="item in route.matched.filter((item) => !!item.meta?.title)"
:key="item.path"
@click="handleBreadClick(item.path)"
>
<component :is="getIcon(item.meta)" />
{{ item.meta.title }}
</n-breadcrumb-item>
</n-breadcrumb>
</template>
<script setup>
import { renderCustomIcon, renderIcon } from '@/utils/icon'
const router = useRouter()
const route = useRoute()
function handleBreadClick(path) {
if (path === route.path) return
router.push(path)
}
function getIcon(meta) {
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
return null
}
</script>

View File

@@ -0,0 +1,12 @@
<template>
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
<icon-ant-design:fullscreen-outlined v-else />
</n-icon>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, toggle } = useFullscreen()
</script>

View File

@@ -0,0 +1,11 @@
<template>
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
<icon-mdi:github />
</n-icon>
</template>
<script setup>
function handleLinkClick() {
window.open('https://github.com/zclzone/vue-naive-admin')
}
</script>

View File

@@ -0,0 +1,12 @@
<template>
<n-icon size="20" cursor-pointer @click="appStore.switchCollapsed">
<icon-mdi:format-indent-increase v-if="appStore.collapsed" />
<icon-mdi:format-indent-decrease v-else />
</n-icon>
</template>
<script setup>
import { useAppStore } from '@/store/modules/app'
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,37 @@
<template>
<n-dropdown :options="options" @select="handleSelect">
<div flex items-center cursor-pointer>
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full />
<span>{{ userStore.name }}</span>
</div>
</n-dropdown>
</template>
<script setup>
import { useUserStore } from '@/store/modules/user'
import { renderIcon } from '@/utils/icon'
const userStore = useUserStore()
const options = [
{
label: '退出登录',
key: 'logout',
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
},
]
function handleSelect(key) {
if (key === 'logout') {
$dialog.confirm({
title: '提示',
type: 'info',
content: '确认退出?',
confirm() {
userStore.logout()
$message.success('已退出登录')
},
})
}
}
</script>

View File

@@ -1,21 +1,19 @@
<template> <template>
<header class="header"> <div flex items-center>
<BreadCrumb /> <MenuCollapse />
<HeaderAction /> <BreadCrumb ml-15 />
</header> </div>
<div ml-auto flex items-center>
<GithubSite />
<FullScreen />
<UserAvatar />
</div>
</template> </template>
<script setup> <script setup>
import BreadCrumb from './BreadCrumb.vue' import BreadCrumb from './components/BreadCrumb.vue'
import HeaderAction from './HeaderAction.vue' import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.vue'
</script> </script>
<style lang="scss" scoped>
.header {
padding: 0 24px;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -1,31 +0,0 @@
<template>
<div class="logo">
<n-icon size="36" color="#316c72">
<IconLogo />
</n-icon>
<router-link to="/">
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
</router-link>
</div>
</template>
<script setup>
import { IconLogo } from '@/components/AppIcons'
const title = import.meta.env.VITE_APP_TITLE
</script>
<style lang="scss" scoped>
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
a {
margin-left: 5px;
.n-gradient-text {
font-size: 14px;
font-weight: bold;
}
}
}
</style>

View File

@@ -1,134 +0,0 @@
<template>
<n-menu
class="side-menu"
accordion
:indent="12"
:root-indent="12"
:options="menuOptions"
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
@update:value="handleMenuSelect"
/>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { computed, h } from 'vue'
import { usePermissionStore } from '@/store/modules/permission'
import { NIcon } from 'naive-ui'
import { IconCircle, IconMenu } from '@/components/AppIcons'
import { isExternal } from '@/utils/is'
const router = useRouter()
const permissionStore = usePermissionStore()
const { currentRoute } = router
const menuOptions = computed(() => {
return generateOptions(permissionStore.routes, '')
})
function resolvePath(basePath, path) {
if (isExternal(path)) return path
return (
'/' +
[basePath, path]
.filter((path) => !!path && path !== '/')
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
.join('/')
)
}
function renderIcon(icon, props = { size: 12 }) {
return () => h(NIcon, { ...props }, { default: () => h(icon) })
}
function isSingleRoute(route) {
let isSingle = true
let curRoute = route
while (curRoute.children && curRoute.children.length) {
if (curRoute.children.length > 1) {
isSingle = false
break
}
if (curRoute.children.length === 1) {
curRoute = curRoute.children[0]
}
}
return isSingle
}
function generateOptions(routes, basePath) {
let options = []
routes.forEach((route) => {
if (route.name && !route.isHidden) {
let curOption = {
label: (route.meta && route.meta.title) || route.name,
key: route.name,
path: resolvePath(basePath, route.path),
}
if (route.children && route.children.length) {
curOption.icon = renderIcon(route.meta?.icon || IconMenu, { size: 16 })
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
} else {
curOption.icon = (route.meta?.icon && renderIcon(route.meta?.icon)) || renderIcon(IconCircle, { size: 8 })
}
options.push(curOption)
}
})
return options
}
function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
router.push(item.path)
}
// 通过path重定向
// router.push({
// path: '/redirect',
// query: { redirect: item.path },
// })
}
</script>
<style lang="scss">
.n-menu {
margin-top: 10px;
padding-left: 10px;
.n-menu-item-content {
&::before {
left: 0;
right: 0;
border-radius: 0;
background-color: unset !important;
}
&:hover,
&.n-menu-item-content--selected {
border-radius: 0 !important;
&::before {
border-right: 3px solid $primaryColor;
background-color: #16243a;
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
}
}
}
.n-menu-item-content-header {
font-size: 14px;
font-weight: bold;
}
.n-submenu-children {
.n-menu-item-content-header {
font-size: 14px;
font-weight: normal;
position: relative;
overflow: visible !important;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<router-link h-60 f-c-c to="/">
<icon-custom-logo text-36 color-primary></icon-custom-logo>
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0>
{{ title }}
</h2>
</router-link>
</template>
<script setup>
import { useAppStore } from '@/store/modules/app'
const title = import.meta.env.VITE_TITLE
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,114 @@
<template>
<n-menu
class="side-menu"
accordion
:indent="18"
:collapsed-icon-size="22"
:collapsed-width="64"
:options="menuOptions"
:value="curRoute.meta?.activeMenu || curRoute.name"
@update:value="handleMenuSelect"
/>
</template>
<script setup>
import { usePermissionStore } from '@/store/modules/permission'
import { isExternal } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { renderCustomIcon, renderIcon } from '@/utils/icon'
const router = useRouter()
const curRoute = useRoute()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const menuOptions = computed(() => {
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
})
function resolvePath(basePath, path) {
if (isExternal(path)) return path
return (
'/' +
[basePath, path]
.filter((path) => !!path && path !== '/')
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
.join('/')
)
}
function getMenuItem(route, basePath = '') {
let menuItem = {
label: (route.meta && route.meta.title) || route.name,
key: route.name,
path: resolvePath(basePath, route.path),
icon: getIcon(route.meta),
order: route.meta?.order || 0,
}
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : []
if (!visibleChildren.length) return menuItem
if (visibleChildren.length === 1) {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem = {
label: singleRoute.meta?.title || singleRoute.name,
key: singleRoute.name,
path: resolvePath(menuItem.path, singleRoute.path),
icon: getIcon(singleRoute.meta),
order: menuItem.order,
}
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : []
if (visibleItems.length === 1) {
menuItem = getMenuItem(visibleItems[0], menuItem.path)
} else if (visibleItems.length > 1) {
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.order - b.order)
}
} else {
menuItem.children = visibleChildren
.map((item) => getMenuItem(item, menuItem.path))
.sort((a, b) => a.order - b.order)
}
return menuItem
}
function getIcon(meta) {
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
return null
}
function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
if (item.path === curRoute.path) {
appStore.reloadPage()
} else {
router.push(item.path)
}
}
}
</script>
<style lang="scss">
.side-menu:not(.n-menu--collapsed) {
.n-menu-item-content {
&::before {
left: 5px;
right: 5px;
}
&.n-menu-item-content--selected,
&:hover {
&::before {
border-left: 4px solid var(--primary-color);
}
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import SideLogo from './SideLogo.vue' import SideLogo from './components/SideLogo.vue'
import SideMenu from './SideMenu.vue' import SideMenu from './components/SideMenu.vue'
</script> </script>
<template> <template>

View File

@@ -0,0 +1,122 @@
<template>
<n-dropdown
:show="show"
:options="options"
:x="x"
:y="y"
placement="bottom-start"
@clickoutside="handleHideDropdown"
@select="handleSelect"
/>
</template>
<script setup>
import { useTagsStore } from '@/store/modules/tags'
import { renderIcon } from '@/utils/icon'
import { useAppStore } from '@/store/modules/app'
const props = defineProps({
show: {
type: Boolean,
default: false,
},
currentPath: {
type: String,
default: '',
},
x: {
type: Number,
default: 0,
},
y: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:show'])
const tagsStore = useTagsStore()
const appStore = useAppStore()
const options = computed(() => [
{
label: '重新加载',
key: 'reload',
disabled: props.currentPath !== tagsStore.activeTag,
icon: renderIcon('mdi:refresh', { size: '14px' }),
},
{
label: '关闭',
key: 'close',
disabled: tagsStore.tags.length <= 1,
icon: renderIcon('mdi:close', { size: '14px' }),
},
{
label: '关闭其他',
key: 'close-other',
disabled: tagsStore.tags.length <= 1,
icon: renderIcon('mdi:arrow-expand-horizontal', { size: '14px' }),
},
{
label: '关闭左侧',
key: 'close-left',
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[0].path,
icon: renderIcon('mdi:arrow-expand-left', { size: '14px' }),
},
{
label: '关闭右侧',
key: 'close-right',
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
},
])
const route = useRoute()
const actionMap = new Map([
[
'reload',
() => {
if (route.meta?.keepAlive) {
// 重置keepAlive
route.meta.key = +new Date()
}
appStore.reloadPage()
},
],
[
'close',
() => {
tagsStore.removeTag(props.currentPath)
},
],
[
'close-other',
() => {
tagsStore.removeOther(props.currentPath)
},
],
[
'close-left',
() => {
tagsStore.removeLeft(props.currentPath)
},
],
[
'close-right',
() => {
tagsStore.removeRight(props.currentPath)
},
],
])
function handleHideDropdown() {
emit('update:show', false)
}
function handleSelect(key) {
const actionFn = actionMap.get(key)
actionFn && actionFn()
handleHideDropdown()
}
</script>

View File

@@ -1,87 +1,86 @@
<template> <template>
<div class="tags-wrapper" :style="{ height: useTheme.tags.height + 'px' }"> <ScrollX>
<n-space>
<n-tag <n-tag
v-for="tag in useTags.tags" v-for="tag in tagsStore.tags"
:key="tag.path" :key="tag.path"
:type="useTags.activeTag === tag.path ? 'primary' : 'default'" class="px-15 mx-5 rounded-4 cursor-pointer hover:color-primary"
:closable="useTags.tags.length > 1" :type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
:closable="tagsStore.tags.length > 1"
@click="handleTagClick(tag.path)" @click="handleTagClick(tag.path)"
@close.stop="handleClose(tag.path)" @close.stop="tagsStore.removeTag(tag.path)"
@contextmenu.prevent="handleContextMenu($event, tag)"
> >
{{ tag.title }} {{ tag.title }}
</n-tag> </n-tag>
</n-space> <ContextMenu
</div> v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</ScrollX>
</template> </template>
<script setup name="Tags"> <script setup>
import { watch } from 'vue' import ContextMenu from './ContextMenu.vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsStore } from '@/store/modules/tags' import { useTagsStore } from '@/store/modules/tags'
import { useThemeStore } from '@/store/modules/theme' import ScrollX from '@/components/common/ScrollX.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const useTags = useTagsStore() const tagsStore = useTagsStore()
const useTheme = useThemeStore()
const contextMenuOption = reactive({
show: false,
x: 0,
y: 0,
currentPath: '',
})
watch( watch(
() => route.path, () => route.path,
() => { () => {
const { name, path } = route const { name, path } = route
const title = route.meta?.title const title = route.meta?.title
useTags.addTag({ name, path, title }) tagsStore.addTag({ name, path, title })
useTags.setActiveTag(path)
}, },
{ immediate: true } { immediate: true }
) )
const handleTagClick = (path) => { const handleTagClick = (path) => {
useTags.setActiveTag(path) tagsStore.setActiveTag(path)
router.push(path) router.push(path)
} }
const handleClose = (path) => { function showContextMenu() {
if (path === useTags.activeTag) { contextMenuOption.show = true
const activeIndex = useTags.tags.findIndex((item) => item.path === path)
if (activeIndex > 0) {
router.push(useTags.tags[activeIndex - 1].path)
} else {
router.push(useTags.tags[activeIndex + 1].path)
} }
function hideContextMenu() {
contextMenuOption.show = false
} }
useTags.removeTag(path) function setContextMenu(x, y, currentPath) {
Object.assign(contextMenuOption, { x, y, currentPath })
}
// 右击菜单
async function handleContextMenu(e, tagItem) {
const { clientX, clientY } = e
hideContextMenu()
setContextMenu(clientX, clientY, tagItem.path)
await nextTick()
showContextMenu()
} }
</script> </script>
<style lang="scss"> <style>
.tags-wrapper {
display: flex;
align-items: center;
background-color: #fff;
padding: 0 10px;
position: sticky;
top: 0;
z-index: 9;
.n-tag {
padding: 0 15px;
cursor: pointer;
.n-tag__close { .n-tag__close {
margin-left: 5px;
box-sizing: content-box; box-sizing: content-box;
border-radius: 50%;
font-size: 12px; font-size: 12px;
padding: 2px; padding: 2px;
border-radius: 50%; transform: scale(0.9);
transition: all 0.7s; transform: translateX(5px);
&:hover { transition: all 0.3s;
color: #fff;
background-color: $primaryColor;
}
}
&:hover {
color: $primaryColor;
}
}
} }
</style> </style>

View File

@@ -1,26 +1,28 @@
<template> <template>
<div class="layout"> <n-layout has-sider wh-full>
<n-layout has-sider position="absolute"> <n-layout-sider
<n-layout-sider bordered :width="200" :collapsed-width="0" :native-scrollbar="false"> bordered
collapse-mode="width"
:collapsed-width="64"
:width="220"
:native-scrollbar="false"
:collapsed="appStore.collapsed"
>
<SideBar /> <SideBar />
</n-layout-sider> </n-layout-sider>
<n-layout>
<n-layout-header :style="{ height: useTheme.header.height + 'px' }" style="border-left: none">
<AppHeader />
</n-layout-header>
<n-layout <article flex-1 flex-col overflow-hidden>
position="absolute" <header bg-white px-15 border-b bc-eee flex items-center :style="`height: ${header.height}px`">
style="background-color: #f5f6fb" <AppHeader />
:style="{ top: useTheme.header.height + 'px' }" </header>
:native-scrollbar="false" <section v-if="tags.visible" border-b bc-eee>
> <AppTags :style="{ height: `${tags.height}px` }" />
<AppTags v-if="useTheme.tags.visible" /> </section>
<section flex-1 overflow-hidden>
<AppMain /> <AppMain />
</section>
</article>
</n-layout> </n-layout>
</n-layout>
</n-layout>
</div>
</template> </template>
<script setup> <script setup>
@@ -28,16 +30,8 @@ import AppHeader from './components/header/index.vue'
import SideBar from './components/sidebar/index.vue' import SideBar from './components/sidebar/index.vue'
import AppMain from './components/AppMain.vue' import AppMain from './components/AppMain.vue'
import AppTags from './components/tags/index.vue' import AppTags from './components/tags/index.vue'
import { useThemeStore } from '@/store/modules/theme' import { useAppStore } from '@/store/modules/app'
import { header, tags } from '~/settings'
const useTheme = useThemeStore() const appStore = useAppStore()
</script> </script>
<style lang="scss" scoped>
.n-layout-header {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #eee;
border-left: 1px solid #eee;
}
</style>

Some files were not shown because too many files have changed in this diff Show More