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

265 Commits
v0.4.0 ... 1.x

Author SHA1 Message Date
zclzone
00f3d51c1d fix: 修复VueDevTools不生效问题 2024-04-24 21:24:14 +08:00
zclzone
c9425d8803 build: update settings.json 2024-04-24 21:22:29 +08:00
zclzone
204a7b3a9b docs: update readme 2023-12-20 21:21:34 +08:00
大脸怪
417f85b8ed Merge pull request #58 from seemrcola/main
chore: 使用vue-devtool
2023-12-10 11:34:48 +08:00
seemrcola
b47f3d35ca chore: 使用vue-devtool 2023-12-09 18:54:26 +08:00
大脸怪
91dd2059f8 Merge pull request #57 from mizhexiaoxiao/main
Update README: 新增使用该项目的开源项目
2023-12-08 15:46:37 +08:00
mizhexiaoxiao
c71a717b8d Update README.EN.md 2023-12-08 14:33:15 +08:00
mizhexiaoxiao
89c3923874 Update README.md 2023-12-08 14:30:45 +08:00
zclzone
f4be4bfb36 mod: 修改通知 2023-12-07 22:37:30 +08:00
大脸怪
33da6652a6 Update README.md 2023-12-07 22:31:07 +08:00
大脸怪
4be6e4a3ca Update README.md 2023-12-07 22:29:13 +08:00
大脸怪
08b30bc864 Update README.md 2023-12-07 22:27:03 +08:00
大脸怪
84d8377816 Update README.md 2023-12-07 22:25:38 +08:00
大脸怪
d25941befe Update README.md 2023-12-07 22:25:11 +08:00
大脸怪
0490e4ded6 Update README.md 2023-11-18 17:38:08 +08:00
zclzone
3ca70e11e1 fix: <a> tag's target 2023-11-10 15:46:00 +08:00
zclzone
e149dc90b4 feat: 添加通知 2023-11-10 15:39:50 +08:00
zclzone
2a61038fe3 docs: update README 2023-11-10 10:29:29 +08:00
zclzone
7a9b1b2d34 docs: update README 2023-11-07 21:38:38 +08:00
大脸怪
9e3c434630 docs: Update README.EN.md 2023-11-07 15:40:56 +08:00
大脸怪
63cc4a8b7b docs: Update README.md 2023-11-07 15:40:14 +08:00
zclzone
d1ed8bb933 fix(other): 处理注册路由前获取用户信息和权限信息异常导致白屏的情况
ISSUES CLOSED: #54
2023-11-05 16:02:32 +08:00
zclzone
be1884bd05 chore: 调整别名优先级 2023-11-05 15:42:21 +08:00
大脸怪
60942e82f3 docs: Update README.md 2023-10-29 22:58:23 +08:00
zclzone
db56ee536b mod: 切换域名 2023-10-28 20:27:00 +08:00
zclzone
53de707509 chore: update eslint config 2023-10-16 21:15:21 +08:00
zclzone
60d0162ced mod: 修改备案号 2023-10-16 21:15:10 +08:00
大脸怪
4fff446dca Update FUNDING.yml 2023-10-13 14:44:04 +08:00
zclzone
a4dd39c5db feat: 补充gitee地址 2023-10-13 11:06:32 +08:00
zclzone
f76a3c02c2 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-10-12 15:49:22 +08:00
zclzone
07d5ab4837 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-10-12 15:48:44 +08:00
zclzone
08a11b3058 mod: create FUNDING.yml 2023-10-12 15:48:30 +08:00
zclzone
921e0d18e9 mod: update settings.json 2023-10-07 12:57:37 +08:00
zclzone
3e259877c6 chore: update deps 2023-10-06 22:41:16 +08:00
zclzone
a431a2cf85 fix(other): loginInfo.username绑定错误
ISSUES CLOSED: #55
2023-10-06 22:39:39 +08:00
zclzone
c7c8691164 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-09-26 22:53:50 +08:00
张传龙
1d246d1cd6 fix: 描述 2023-09-26 16:54:12 +08:00
张传龙
1854d2cec4 style: 移除不再需要的样式 2023-09-21 09:25:03 +08:00
zclzone
478d8fccd1 feat: 首页icon 2023-09-18 22:46:49 +08:00
zclzone
0accfe3a4a feat: 调整首页echarts图 2023-09-17 12:25:12 +08:00
zclzone
4bd50e2f9e refactor: 重构首页 2023-09-14 22:54:50 +08:00
zclzone
72857b3862 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-09-09 17:51:19 +08:00
zclzone
a18ffce56c style: 修改图标 2023-09-09 17:50:55 +08:00
Ronnie Zhang
a2222c4dc0 docs: Update README.EN.md 2023-09-09 17:43:05 +08:00
Ronnie Zhang
a357ac36ee Update README.md 2023-09-09 17:42:11 +08:00
Ronnie Zhang
5e1acd7b8e Update README.md 2023-09-06 11:01:40 +08:00
Ronnie Zhang
8bbd04e668 docs: Update README.md 2023-09-06 11:00:59 +08:00
Ronnie Zhang
0c5c3df645 docs: Update README.md 2023-09-06 10:59:34 +08:00
Ronnie Zhang
ed577cd41a docs: Update README.md 2023-09-01 21:14:52 +08:00
Ronnie Zhang
456ad78dd2 Merge pull request #48 from xtazhangzp/main 2023-09-01 20:59:38 +08:00
v_zhangzp
ba20e7a96b fix: 修改时间方法Date.now() 2023-08-31 17:03:07 +08:00
张传龙
3ed1aafa80 chore: update deps 2023-08-28 11:08:06 +08:00
张传龙
5cf1212847 build: 锁定不能升级的依赖包 2023-08-28 10:51:32 +08:00
张传龙
81e0bb7b78 feat: md-editor暗黑主题调整 2023-08-25 10:52:04 +08:00
张传龙
f3125ddec8 chore: update deps 2023-08-25 10:42:18 +08:00
张传龙
3b3cb7ba34 docs: update readme 2023-08-17 18:50:46 +08:00
张传龙
d7c1063102 feat: 更新logo 2023-08-17 18:30:24 +08:00
张传龙
a5aa8a353f build: 新增一键启动脚本,F5直接启动项目 2023-08-15 10:49:44 +08:00
张传龙
cbb6ca4f6b chore: update deps 2023-08-10 16:12:40 +08:00
Ronnie Zhang
2ece015dae docs: update readme 2023-08-05 16:40:26 +08:00
zclzone
a80f83a011 mod: merge commit from github 2023-08-04 22:14:12 +08:00
zclzone
65d4d3848d fix(deps): 修复自定义指令引起的热更新失效问题
ISSUES CLOSED: #45
2023-08-04 22:07:20 +08:00
zclzone
53830256f4 fix: size type 2023-08-04 21:53:55 +08:00
张传龙
22c59c208f feat: add unocss preset rem to px 2023-08-03 17:20:21 +08:00
张传龙
b43f87035b chore: rollback vite-plugin-mock version 2023-07-28 01:10:01 +08:00
张传龙
20eee94630 chore: update unocss config 2023-07-28 00:56:14 +08:00
张传龙
c504ad065c chore: update deps 2023-07-27 14:25:07 +08:00
张传龙
0e764ac748 feat: add message notification 2023-07-21 16:01:35 +08:00
张传龙
8f15e1c655 fix: 多余的reloadPage 2023-07-13 14:46:41 +08:00
张传龙
d702a6703b fix: keepAlive 2023-07-11 15:07:37 +08:00
张传龙
d49af8b574 docs: update readme 2023-07-07 15:33:20 +08:00
张传龙
a036602d3e Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-07-07 15:09:04 +08:00
张传龙
eb173dc38e docs: update readme 2023-07-07 15:05:25 +08:00
zclzone
e5f1ee25c3 mod: update copyright 2023-07-01 10:41:25 +08:00
张传龙
fe11e18197 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-06-27 17:18:24 +08:00
张传龙
0f9fb9f1c9 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-06-27 17:16:56 +08:00
张传龙
50f96b99c7 style: sort package.json 2023-06-27 17:16:45 +08:00
Ronnie Zhang
2eb936bcac Merge pull request #43 from mizhexiaoxiao/main 2023-06-26 20:46:00 +08:00
mizhexiaoxiao
b50731881c fix: 在中文输入法输入字母按enter键会触发查询 2023-06-26 10:46:43 +08:00
zclzone
d4a5cffd81 mod: update copyright 2023-06-23 18:45:58 +08:00
zclzone
70eab22f65 style: 修改注释 2023-06-21 22:06:40 +08:00
zclzone
0247f3ebfa refactor: 简化proxy配置 2023-06-21 21:53:33 +08:00
张传龙
a610c1c6d0 docs: update readme 2023-06-19 09:06:15 +08:00
Ronnie Zhang
5a9c0fb584 Merge pull request #41 from szluyu99/main 2023-06-19 09:01:24 +08:00
szluyu99
cad72b3b73 docs: README 新增使用该项目的开源项目 2023-06-18 21:56:09 +08:00
zclzone
9edd0e5ad6 style: 暗黑模式细节 2023-06-17 10:43:03 +08:00
zclzone
ae6db3ed3c feat: tag标签增加图标展示 2023-06-17 10:42:33 +08:00
张传龙
dcab55055c fix(utils): 修复\\$message后于接口请求定义问题
ISSUES CLOSED: #39
2023-06-15 10:26:03 +08:00
张传龙
5aa4d3d5ae Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-06-03 16:45:11 +08:00
张传龙
eea9fc79f7 style: lint fix 2023-06-03 16:38:45 +08:00
张传龙
526792e22f chore: update .prettierrc 2023-06-03 16:34:50 +08:00
张传龙
855202962c chore: update path-intellisense settings 2023-06-03 16:34:01 +08:00
zclzone
74a58fafb9 docs: update README 2023-05-24 21:18:18 +08:00
zclzone
ad451dae1e mod: merge 2023-05-13 23:12:49 +08:00
zclzone
35ed004b2e chore: 移除plugin unplugin-vue-define-options(vue官方已支持) 2023-05-13 17:25:44 +08:00
zclzone
fcdd31c935 chore: update deps 2023-05-13 17:24:43 +08:00
张传龙
386d9ec27a style: lint fix 2023-05-08 14:15:03 +08:00
张传龙
e84dd01365 feat: add @unocss/eslint-config 2023-05-08 14:13:59 +08:00
zclzone
6da56ec881 Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-05-07 22:25:09 +08:00
zclzone
4e9e3469b0 chore: update deps 2023-05-07 22:24:46 +08:00
zclzone
3b86597eff style: lint fix 2023-05-07 22:20:57 +08:00
zclzone
ebffe52c7c style: lint fix 2023-05-07 22:05:13 +08:00
zclzone
6d863e1a63 chore: eslint -> @zclzone/eslint-config 2023-05-07 22:00:25 +08:00
Ronnie Zhang
b2e1c2d22c Merge pull request #35 from liaocp666/patch-1
docs: 修正文档中的错别字
2023-05-03 20:25:42 +08:00
Kent Liao
2e283c3b19 修正错别字 2023-04-28 10:58:00 +08:00
zclzone
6300758fd0 fix: 更新失效图片链接 2023-04-23 22:35:33 +08:00
zclzone
b5717f6b8d build: gh-pages -> vercel 2023-04-23 22:18:00 +08:00
张传龙
ff11cdf73f build: remove vercel env 2023-04-23 14:46:12 +08:00
张传龙
9a01e42915 build: add vercel.json 2023-04-23 14:38:50 +08:00
zclzone
7c74578afc build: 触发构建 2023-04-20 21:56:53 +08:00
zclzone
a6a8002c59 build: 触发构建 2023-04-20 21:21:12 +08:00
zclzone
daf747348e chore: 添加vercel环境 2023-04-20 20:56:21 +08:00
张传龙
60b5ce1817 fix: redirect 2023-04-17 14:14:10 +08:00
张传龙
4c75be67f2 feat: 图片上传 2023-04-17 13:36:14 +08:00
张传龙
be1c875a72 Merge branch 'main' of https://gitee.com/zclzone/vue-naive-admin 2023-04-17 13:24:48 +08:00
张传龙
329a6e29cb chore: update jsconfig.json 2023-04-17 13:18:44 +08:00
zclzone
af907932fb perf: 优化ScrollX 2023-04-16 21:41:27 +08:00
张传龙
681b3144d1 feat: 增加表格导出功能 2023-04-13 12:13:32 +08:00
张传龙
8304970a59 chore: update deps 2023-04-13 08:20:45 +08:00
zclzone
5cef8e4a01 fix: 暗黑模式细节 2023-03-12 21:25:14 +08:00
张传龙
0b50e1dbee Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2023-03-08 11:01:00 +08:00
张传龙
88a93c4e57 chore: update deps 2023-03-08 10:58:08 +08:00
zclzone
38ae35ee95 fix: 文件名称 2023-02-12 12:26:47 +08:00
zclzone
c7471a66db docs: 默认中文README 2023-02-12 12:25:49 +08:00
zclzone
a4531be904 fix: 修复暗色模式下部分显示问题 2023-02-12 12:10:26 +08:00
张传龙
c58605de54 refactor: 重构暗黑模式 2023-01-30 15:37:10 +08:00
zclzone
c3dc0b4b2c chore: update deps 2022-12-31 18:17:36 +08:00
zclzone
2d3e9988ec chore: update deps 2022-12-31 18:07:05 +08:00
zclzone
9548a0bfc8 feat: 集成简易暗黑模式 2022-12-18 19:19:42 +08:00
张传龙
dda778fdde mod: update 2022-12-12 09:32:50 +08:00
张传龙
98e3f13185 fix: 示例组件name 2022-12-09 14:17:21 +08:00
张传龙
7e79c51630 feat: 全局中文设定 2022-12-08 15:08:27 +08:00
zclzone
181aed4897 chore: update deps 2022-11-28 12:13:02 +08:00
zclzone
c626d2b785 feat: 当前标签页始终显示在视野内 2022-11-27 20:04:11 +08:00
zclzone
ed79e81b13 perf: 切换tab时自动展开对应的菜单 2022-11-25 09:16:01 +08:00
zclzone
264119a142 fix: $message 2022-11-24 21:05:42 +08:00
zclzone
4c1c77821f feat: 增加多级菜单示例 2022-11-24 18:16:37 +08:00
zclzone
67b11f04fc fix: fullPath 2022-11-14 18:29:41 +08:00
zclzone
649fe1d4e8 fix: route path 2022-11-12 19:09:24 +08:00
张传龙
c3192423c6 mod: remove 2022-10-31 15:23:40 +08:00
张传龙
911fc74305 chore: update deps 2022-10-31 14:09:07 +08:00
张传龙
2fcfd6b4d1 docs: add docs 2022-10-28 15:03:04 +08:00
张传龙
bac868d071 docs: add docs 2022-10-27 17:15:34 +08:00
张传龙
ea5460488a docs: add docs 2022-10-26 17:23:08 +08:00
张传龙
61d42ead21 chore: update deps 2022-10-21 17:39:01 +08:00
张传龙
a98555beb1 chore: update deps 2022-10-19 17:59:09 +08:00
张传龙
820eb516ce docs: update readme 2022-10-16 16:46:56 +08:00
张传龙
6cd0dc1eff chore: update deps 2022-10-09 17:56:23 +08:00
张传龙
5dcb2958a1 chore: update deps 2022-10-04 17:07:34 +08:00
张传龙
c25476278b fix: rich text editor keep alive 2022-09-29 16:53:54 +08:00
张传龙
99ddb4fe70 feat: rich text editor 2022-09-29 08:55:26 +08:00
张传龙
82c47ffc72 chore: update deps 2022-09-28 16:44:01 +08:00
张传龙
1f1678800f style: md editor 2022-09-28 16:43:44 +08:00
张传龙
efdd89cd50 feat: back top 2022-09-26 17:40:41 +08:00
张传龙
100b91a118 style: reset.css 2022-09-25 17:14:01 +08:00
张传龙
26b71f0ec6 perf: unocss demo 2022-09-25 17:13:35 +08:00
张传龙
92e7ada37b fix: toLogin 2022-09-24 15:40:34 +08:00
张传龙
2d879d0592 refactor: http interceptors 2022-09-24 15:27:26 +08:00
张传龙
8806a6cb43 fix: keep-alive key 2022-09-24 14:44:59 +08:00
张传龙
85a04fd06d fix: logout 2022-09-21 17:14:41 +08:00
张传龙
4a5b8dd005 fix: fix 2022-09-21 17:13:39 +08:00
张传龙
2f7da255e5 style: naive theme 2022-09-18 20:18:55 +08:00
张传龙
6664ae8f7b refactor: folders 2022-09-18 20:05:40 +08:00
张传龙
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
162 changed files with 9835 additions and 3340 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: '|'
}

2
.env
View File

@@ -1,3 +1,3 @@
VITE_APP_TITLE = 'Vue Naive Admin'
VITE_TITLE = 'Vue Naive Admin'
VITE_PORT = 3100

View File

@@ -2,13 +2,10 @@
VITE_PUBLIC_PATH = '/'
# 是否启用MOCK
VITE_APP_USE_MOCK = true
VITE_USE_MOCK = true
# proxy
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]]
# 是否启用MOCK
VITE_USE_PROXY = true
# base api
VITE_APP_BASE_API = '/api'
# test base api
VITE_APP_BASE_API_TEST = '/api-test'
VITE_BASE_API = '/api'

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"defineOptions": true,
"$dialog": true,
"$notification": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -1,15 +0,0 @@
module.exports = {
root: true,
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
rules: {
'prettier/prettier': 'warn',
'vue/valid-template-root': 'off',
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': [
'error',
{
ignores: ['index', '401', '404'],
},
],
},
}

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://afdian.net/a/isme-admin']

View File

@@ -1,38 +0,0 @@
name: deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: use pnpm 6.32.2
uses: pnpm/action-setup@v2.2.1
with:
version: 6.32.2
- name: Build
run: |
pnpm install
pnpm run build:github
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
publish_dir: ./dist
github_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
user_name: ${{ secrets.USER_NAME }}
user_email: ${{ secrets.USER_EMAIL }}
force_orphan: true
commit_message: deploy gh-pages

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

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "ignore"
}

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "一键启动",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
]
}

28
.vscode/settings.json vendored
View File

@@ -1,12 +1,30 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.printWidth": 120,
"prettier.singleQuote": true,
"prettier.semi": false,
"prettier.endOfLine": "lf",
"files.eol": "\n",
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src"
"[javascript]": {
"editor.formatOnSave": false
},
"[typescript]": {
"editor.formatOnSave": false
},
"[typescriptreact]": {
"editor.formatOnSave": false
},
"[vue]": {
"editor.formatOnSave": false
},
"editor.formatOnSave": false,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"cssrem.rootFontSize": 4, // 适配unocss1rem = 4px ==> 0.25rem = 1px
"files.associations": {
"*.env.*": "dotenv",
"*.css": "postcss"
}
}

208
README.EN.md Normal file
View File

@@ -0,0 +1,208 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<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">中文</a>
</p>
### Introduction
[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
- 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You.
- 🍑 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
[https://template.isme.top](https://template.isme.top)
[https://base.isme.top](https://base.isme.top)
### Docs
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
### Getting Started
```shell
# Recommended setup git autocrlf 为 false
git config --global core.autocrlf false
# Clone Project
git clone https://github.com/zclzone/vue-naive-admin.git
cd vue-naive-admin
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored
pnpm i # or npm i
# Start
pnpm dev
```
### Build and Release
```shell
# Test Environment
pnpm build:test
# Github Environment
pnpm build:github
# Prod Environment
pnpm build
```
### Other
```shell
# eslint check
pnpm lint
# eslint check and fix
pnpm lint:fix
# PreviewNeed to build first
pnpm preview
# Commithusky+commitlint
pnpm cz
```
### Directory description
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS version: Qs Admin
#### source code
- github: [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.isme.top](https://admin.isme.top)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Open source projects that use this project:
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): A full-stack blog project in Golang, the frontend of the blog backend is based on vue-naive-admin and integrates with a real backend service, implementing features such as backend-controlled routing.
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): A Python backend management project that integrates RBAC permission management, dynamic routing, and JWT authentication, helping small and medium-sized applications to quickly establish a foundation.
### Communication group & About the author
<a href="https://blog.isme.top/about/">
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
</a>

219
README.md
View File

@@ -1,53 +1,61 @@
<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">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</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>
<a href="./LICENSE"><img alt="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.EN.md">English</a>
</p>
> 🎉🎉🎉 2.0 已开源,全新重构,全面简化,后端使用 nestjs + mysql + typeOrm[👉点击前往2.0版本 | 分支 2.x](https://github.com/zclzone/vue-naive-admin/tree/2.x),
- 体验地址: [admin.isme.top](https://admin.isme.top)
- 后端服务: [isme-nest-serve](https://github.com/zclzone/isme-nest-serve)
- 文档: [vue-naive-admin-docs](https://docs.isme.top/web/#/624306705)
### 简介
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin)一个基于 Vue3.0、Vite、Naive UI 的后台管理模板,相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低对新手极其友好。不过麻雀虽小五脏俱全权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置以及一些经常用的基础组件封装等等这些该有的都有,非常适用于中小型项目或者个人项目,也可此模板进行二次封装改造用于大型项目
### 为什么要开发这个模板
- Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
- 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite4、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,上手成本非常低,非常适中小型项目或者个人项目。
### 功能
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,[https://www.naiveui.com](https://www.naiveui.com)
- 🍒 集成 [Naive UI](https://www.naiveui.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 实例
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
- 🍋 二次封装 localStorage 和 sessionStorage支持设置过期时间
- 🍐 集成多环境配置dev、测试、生产环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
> ✨✨ 双十一香港特惠服务器推荐,~~**2C4G 100M** `71/年` `142/两年`~~[👉点击前往](https://blog.isme.top/vps-recommend/)
### 预览
[template.qszone.com](https://template.qszone.com)
[https://template.isme.top](https://template.isme.top)
[github pages](https://zclzone.github.io/vue-naive-admin)
[https://base.isme.top](https://base.isme.top)
### 文档
[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)
从0到1搭建后台: [从0到1带你搭建Vite+Vue3+Pinia+Naive UI后台](https://juejin.cn/column/7093180796424421384)
### 构建
如何安装pnpm: [安装pnpm](docs/安装pnpm.md)
如何使用图标: [使用图标](docs/使用图标.md)
如何使用unocss: [保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
### 快速开始
```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
@@ -61,62 +69,161 @@ 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
# 启动
npm run dev
pnpm dev
```
### 发布
### 构建发布
```shell
# 构建测试环境
npm run build:test
pnpm build:test
# 构建github pages环境
npm run build:github
pnpm build:github
# 构建生产环境
npm run build
pnpm build
```
### 其他指令
```shell
# eslint代码格式检查
npm run lint
pnpm lint
# 代码检查并修复
npm run lint:fix
pnpm lint:fix
# 预览发布包效果(需先执行构建指令)
npm run preview
pnpm preview
# 提交代码husky+commitlint
pnpm cz
```
### 规范
#### git commit 规范
### 目录说明
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
- `mod` 不确定分类的修改
- `release` 发布
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS 版本: Qs Admin
#### 源码
<p align="center">
<img src="https://assets.qszone.com/image/Snipaste_2022-06-23_19-26-26.png" />
- github: [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.isme.top](https://admin.isme.top)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### 使用该项目的开源项目
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin对接真实后端服务实现了后端控制路由等特性。
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): Python 后台管理项目, 融合了 RBAC 权限管理、动态路由JWT 鉴权,助力中小型应用快速搭建。
### 入群交流 & 关于作者
<a href="https://blog.isme.top/about/">
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
</a>
### ☕ 赞助我
> 开源不易,请作者喝杯咖啡吧
<p>
<img src="https://static.isme.top/images/zhifu_weixin.jpg" style="width: 220px" />
<img src="https://static.isme.top/images/zhifu_zhifubao.jpg" style="width: 220px" />
</p>

View File

@@ -1 +1,33 @@
export const OUTPUT_DIR = 'dist'
export const PROXY_CONFIG = {
/**
* @desc 替换匹配值
* @请求路径 http://localhost:3100/api/user
* @转发路径 http://localhost:8080/user
*/
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
/**
* @desc 不替换匹配值
* @请求路径 http://localhost:3100/api/v2/user
* @转发路径 http://localhost:8080/api/v2/user
*/
'/api/v2': {
target: 'http://localhost:8080',
changeOrigin: true,
},
/**
* @desc 替换部分匹配值
* @请求路径 http://localhost:3100/api/v3/user
* @转发路径 http://localhost:8080/user
*/
'/api/v3': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
}

View File

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

View File

@@ -1,11 +1,5 @@
import vue from '@vitejs/plugin-vue'
/**
* * 扩展setup插件支持在script标签中使用name属性
* usage: <script setup name="MyComp"></script>
*/
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
/**
* * unocss插件原子css
* https://github.com/antfu/unocss
@@ -14,18 +8,26 @@ import Unocss from 'unocss/vite'
// rollup打包分析插件
import visualizer from 'rollup-plugin-visualizer'
// 压缩
import viteCompression from 'vite-plugin-compression'
// vite-vuedevtool
import VueDevTools from 'vite-plugin-vue-devtools'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), VueSetupExtend(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
const plugins = [VueDevTools(), vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv?.VITE_APP_USE_MOCK) {
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({

View File

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

View File

@@ -1,3 +1,4 @@
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
@@ -10,13 +11,16 @@ import IconsResolver from 'unplugin-icons/resolver'
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getRootPath } from '../utils'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
const customIconPath = getRootPath('src', 'assets/icons')
export default [
AutoImport({
imports: ['vue', 'vue-router'],
dts: false,
}),
Icons({
compiler: 'vue3',
@@ -27,6 +31,16 @@ export default [
defaultClass: 'inline-block',
}),
Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
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 { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() {
const { VITE_APP_CNAME } = getEnvConfig()
if (!VITE_APP_CNAME) return
const { VITE_CNAME } = getEnvConfig()
if (!VITE_CNAME) return
try {
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_CNAME)
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}

View File

@@ -2,53 +2,36 @@ import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
const httpsReg = /^https:\/\//
export function wrapperEnv(envOptions) {
if (!envOptions) return {}
const ret = {}
for (const key in envOptions) {
let val = envOptions[key]
if (['true', 'false'].includes(val)) {
val = val === 'true'
}
if (['VITE_PORT'].includes(key)) {
val = +val
}
if (key === 'VITE_PROXY' && val && typeof val === 'string') {
try {
val = JSON.parse(val.replace(/'/g, '"'))
} catch (error) {
val = ''
}
}
ret[key] = val
if (typeof val === 'string') {
process.env[key] = val
} else if (typeof val === 'object') {
process.env[key] = JSON.stringify(val)
}
}
return ret
/**
* * 项目根路径
* @description 结尾不带/
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
export function createProxy(list = []) {
const ret = {}
for (const [prefix, target] of list) {
const isHttps = httpsReg.test(target)
/**
* * 项目src路径
* @param srcName src目录名称(默认: "src")
* @description 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
}
// 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 } : {}),
}
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
}
return ret
return result
}
/**
@@ -65,7 +48,7 @@ function getConfFiles() {
return ['.env', '.env.local', '.env.production']
}
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
@@ -85,7 +68,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
})
return envConfig
}
export function getRootPath(...dir) {
return path.resolve(process.cwd(), ...dir)
}

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',
],
],
},
}

3
docs/使用unocss.md Normal file
View File

@@ -0,0 +1,3 @@
推荐阅读作者在掘金的文章:
[保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)

40
docs/使用图标.md Normal file
View File

@@ -0,0 +1,40 @@
## 使用 iconify 图标
首先去图标库地址:[icones](https://icones.js.org/) 找合适的图标
### 1. 结合 unocss 使用
```html
<i i-carbon-sun />
<i class="i-carbon-sun" />
```
### 2. 结合插件 unplugin-icons 自定义标签使用
`<icon-[iconify图标名称]`
```html
<icon-ant-design:fullscreen-exit-outlined />
<icon-ant-design:fullscreen-outlined />
```
这种方式还支持自定义 svg 图标,本项目自定义 svg 图标固定放在 src/assets/svg 下
`<icon-custom-[svg图标文件名]`
```
<icon-custom-logo />
```
具体配置参看 build/plugin/unplugin.js
### 3. 结合 Naive UI 的 NIcon 组件封装使用
```html
<!-- iconify图标 -->
<TheIcon icon="material-symbols:delete-outline" />
<!-- 自定义svg图标 -->
<TheIcon icon="logo" type="custom" />
```
封装组件参看 src/components/icon

32
docs/安装pnpm.md Normal file
View File

@@ -0,0 +1,32 @@
## 安装pnpm
### 使用Corepack安装推荐
从 v16.13 开始Node.js 发布了 Corepack 来管理包管理器。 这是一项实验性功能,需要通过运行如下脚本来启用它:
```
npx corepack enable // 可能需要管理员权限
```
这将自动在您的系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。 若要升级,请检查[最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/latest) 并运行,如 7.14.0
```
corepack prepare pnpm@7.14.0 --activate
```
如果是 Node.js v16.17 或者更新的版本,可以直接安装最新版本的 pnpm
```
corepack prepare pnpm@latest --activate
```
### 使用npm安装
```
npm i -g pnpm
```
更新,卸了重新装
```
npm uninstall -g pnpm
npm i -g pnpm
```

View File

@@ -7,7 +7,7 @@
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title>
@@ -15,10 +15,9 @@
<body>
<div id="app">
<!-- 白屏时的loading效果 -->
<div class="loading-container">
<div id="loadingLogo" class="loading-svg"></div>
<img src="/resource/logo.png" alt="logo" height="128" />
<div class="loading-spin__container">
<div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div>
@@ -30,7 +29,6 @@
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -1,10 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
"@/*": ["src/*"],
"~/*": ["./*"]
},
"jsx": "preserve"
"jsx": "preserve",
"allowJs": true
},
"exclude": ["node_modules", "dist"]
}

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 = {
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]

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

@@ -0,0 +1,138 @@
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,24 +1,24 @@
import { resolveToken } from '../_utils'
import { resolveToken } from '../utils'
const users = {
admin: {
id: 1,
name: '大脸怪(admin)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['admin'],
},
editor: {
id: 2,
name: '大脸怪(editor)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['editor'],
},
guest: {
id: 3,
name: '访客(guest)',
avatar: 'https://assets.qszone.com/images/avatar.jpg',
avatar: 'https://static.isme.top/images/avatar.jpg',
role: [],
},
}

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)
}

View File

@@ -1,87 +0,0 @@
export default [
{
url: '/api/posts',
method: 'get',
response: () => {
return {
code: 0,
message: 'ok',
data: [
{
id: 36,
title: '使用纯css优雅配置移动端rem布局',
author: 'Ronnie',
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',
},
{
id: 35,
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',
},
{
id: 28,
title: '如何优雅的给图片添加水印',
author: '张传龙',
category: 'JavaScript',
description: '优雅的给图片添加水印',
content: '我之前写过一篇文章记录了一次上传图片的优化史',
isRecommend: true,
isPublish: true,
createDate: '2021-06-24T18:46:19.000Z',
updateDate: '2021-09-23T07:51:22.000Z',
},
{
id: 26,
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',
},
{
id: 24,
title: '使用jQuery的load方法帮女朋友实现套娃Html',
author: '张传龙',
category: 'JavaScript',
description: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
content: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
isRecommend: true,
isPublish: true,
createDate: '2021-05-26T15:26:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
id: 18,
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',
},
],
}
},
},
]

View File

@@ -1,49 +1,82 @@
{
"name": "vue-naive-admin",
"version": "0.4.0",
"version": "1.0.0",
"scripts": {
"build": "vite build",
"build:github": "vite build --mode github && esno ./build/script",
"build:test": "vite build --mode test",
"cz": "cz",
"dev": "vite",
"lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .",
"build": "vite build",
"build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script",
"lint:staged": "lint-staged",
"prepare": "husky install",
"preview": "vite preview"
},
"lint-staged": {
"*.{js,vue}": [
"eslint --ext .js,.vue ."
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
},
"eslintConfig": {
"extends": [
"@zclzone",
"@unocss",
".eslint-global-variables.json"
]
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"axios": "^0.21.4",
"dayjs": "^1.11.0",
"md-editor-v3": "^1.11.4",
"@unocss/eslint-config": "^0.55.7",
"@vueuse/core": "^10.4.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "5.1.12",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.7.0",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"vue": "^3.2.31",
"vue-router": "^4.0.15"
"pinia": "^2.1.6",
"vite": "^4.4.11",
"vue": "3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@iconify/json": "^2.1.63",
"@iconify/vue": "^3.2.1",
"@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31",
"chalk": "^5.0.1",
"dotenv": "^10.0.0",
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.5.0",
"esno": "^0.13.0",
"fs-extra": "^10.0.1",
"naive-ui": "^2.30.3",
"prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10",
"unocss": "^0.38.2",
"unplugin-auto-import": "^0.8.8",
"unplugin-icons": "^0.14.1",
"unplugin-vue-components": "^0.17.21",
"vite": "^2.9.9",
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.125",
"@iconify/vue": "^4.1.1",
"@unocss/preset-rem-to-px": "^0.55.7",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"@zclzone/eslint-config": "^0.0.5",
"chalk": "^5.3.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"dotenv": "^16.3.1",
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"naive-ui": "^2.35.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.69.0",
"unocss": "0.55.3",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-vue-setup-extend": "^0.3.0"
"vite-plugin-mock": "2.9.6",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "1.0.0-rc.7"
}
}

7622
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
module.exports = {
printWidth: 120,
singleQuote: true,
semi: false,
endOfLine: 'lf',
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -10,12 +10,6 @@
height: 100%;
}
.loading-svg {
width: 128px;
height: 128px;
color: var(--primaryColor);
}
.loading-spin__container {
width: 56px;
height: 56px;
@@ -45,7 +39,7 @@
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primaryColor);
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;

View File

@@ -1,25 +1,9 @@
/**
* 初始化加载效果的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 = `--primaryColor: ${themeColor}`;
document.documentElement.style.cssText = cssVars;
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');
addThemeColorCssVars()

BIN
public/resource/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

1
settings/index.js Normal file
View File

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

37
settings/theme.json Normal file
View File

@@ -0,0 +1,37 @@
{
"header": {
"height": 60
},
"tags": {
"visible": true,
"height": 50
},
"naiveThemeOverrides": {
"common": {
"primaryColor": "#316C72FF",
"primaryColorHover": "#316C72E3",
"primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#316C72E3",
"infoColor": "#2080F0FF",
"infoColorHover": "#4098FCFF",
"infoColorPressed": "#1060C9FF",
"infoColorSuppl": "#4098FCFF",
"successColor": "#18A058FF",
"successColorHover": "#36AD6AFF",
"successColorPressed": "#0C7A43FF",
"successColorSuppl": "#36AD6AFF",
"warningColor": "#F0A020FF",
"warningColorHover": "#FCB040FF",
"warningColorPressed": "#C97C10FF",
"warningColorSuppl": "#FCB040FF",
"errorColor": "#D03050FF",
"errorColorHover": "#DE576DFF",
"errorColorPressed": "#AB1F3FFF",
"errorColorSuppl": "#DE576DFF"
}
}
}

View File

@@ -9,12 +9,3 @@
<script setup>
import AppProvider from '@/components/common/AppProvider.vue'
</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'
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,
})
}

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 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

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

View File

@@ -1,55 +1,30 @@
<template>
<n-config-provider :theme-overrides="themStore.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
wh-full
:locale="zhCN"
:date-locale="dateZhCN"
:theme="appStore.isDark ? darkTheme : undefined"
:theme-overrides="naiveThemeOverrides"
>
<slot />
</n-config-provider>
</template>
<script setup>
import { defineComponent, h } from 'vue'
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { useCssVar } from '@vueuse/core'
import { useThemeStore } from '@/store/modules/theme'
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
import { kebabCase } from 'lodash-es'
import { naiveThemeOverrides } from '~/settings'
import { useAppStore } from '@/store'
const themStore = useThemeStore()
watch(
() => themStore.naiveThemeOverrides.common,
(vars) => {
for (const key in vars) {
useCssVar(`--${key}`, document.documentElement).value = vars[key]
if (key === 'primaryColor') {
window.localStorage.setItem('__THEME_COLOR__', vars[key])
}
}
},
{ immediate: true }
)
const appStore = useAppStore()
// 挂载naive组件的方法至window, 以便在全局使用
function setupNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$notification = useNotification()
window.$message = setupMessage(useMessage())
window.$dialog = setupDialog(useDialog())
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] || '')
}
}
const NaiveProviderContent = defineComponent({
setup() {
setupNaiveTools()
},
render() {
return h('div')
},
})
setupCssVar()
</script>

View File

@@ -1,20 +1,19 @@
<template>
<div ref="wrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 50 })">
<div class="left dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: 120 })">
<icon-ic:baseline-keyboard-arrow-left />
</div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -50 })">
<div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right />
</div>
</template>
<div
ref="content"
class="tags-content"
class="content"
:class="{ overflow: isOverflow && showArrow }"
:style="{
height: height + 'px',
transform: `translateX(${translateX}px)`,
}"
>
@@ -24,57 +23,46 @@
</template>
<script setup>
import { debounce } from '@/utils'
import { isNullOrUndef } from '@/utils/is'
import { debounce, useResize } from '@/utils'
defineProps({
height: {
type: Number,
default: 50,
},
showArrow: {
type: Boolean,
default: true,
},
})
onMounted(() => {
refreshIsOverflow()
})
const translateX = ref(0)
const content = ref(null)
const wrapper = ref(null)
const isOverflow = ref(false)
function refreshIsOverflow(isIncrease) {
isOverflow.value = content.value.offsetWidth > wrapper.value.offsetWidth
if (isNullOrUndef(isIncrease)) return
if (isOverflow.value) {
handleMouseWheel({ wheelDelta: isIncrease ? -100 : 100 })
} else if (!isIncrease && translateX.value < 0) {
handleMouseWheel({ wheelDelta: 100 })
}
}
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
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
/**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值
* @wrapperWidth 容器的宽度
* @contentWidth 内容的宽度
*/
if (wheelDelta < 0 && -translateX.value > contentWidth - wrapperWidth + 10) {
return
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)
}
@@ -88,20 +76,51 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
}
}, 200)
const observers = ref([])
onMounted(() => {
refreshIsOverflow()
observers.value.push(useResize(document.body, refreshIsOverflow))
observers.value.push(useResize(content.value, refreshIsOverflow))
})
onBeforeUnmount(() => {
observers.value.forEach((item) => {
item?.disconnect()
})
})
function handleScroll(x, width) {
const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value?.offsetWidth
if (contentWidth <= wrapperWidth) return
// 当 x 小于可视范围的最小值时
if (x < -translateX.value + 150) {
translateX.value = -(x - 150)
resetTranslateX(wrapperWidth, contentWidth)
}
// 当 x 大于可视范围的最大值时
if (x + width > -translateX.value + wrapperWidth) {
translateX.value = wrapperWidth - (x + width)
resetTranslateX(wrapperWidth, contentWidth)
}
}
defineExpose({
refreshIsOverflow,
handleScroll,
})
</script>
<style lang="scss" scoped>
.tags-wrapper {
.wrapper {
display: flex;
background-color: #fff;
position: sticky;
top: 0;
z-index: 9;
overflow: hidden;
.tags-content {
position: relative;
.content {
padding: 0 10px;
display: flex;
align-items: center;

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'
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,18 @@
<template>
<transition name="fade-slide" mode="out-in" appear>
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
<slot />
<AppFooter v-if="showFooter" mt-15 />
<n-back-top :bottom="20" />
</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" mb-15 min-h-45 flex items-center justify-between px-15>
<slot v-if="$slots.header" name="header" />
<template v-else>
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
<slot name="action" />
</template>
</header>
<n-card flex-1 rounded-10>
<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,27 @@
<template>
<div
bg="#fafafc"
min-h-60
flex
items-start
justify-between
b-1
rounded-8
p-15
bc-ccc
dark:bg-black
>
<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,34 @@
<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'
defineProps({
label: {
type: String,
default: '',
},
labelWidth: {
type: Number,
default: 80,
},
contentWidth: {
type: Number,
default: 220,
},
})
</script>

View File

@@ -0,0 +1,55 @@
<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,149 @@
<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>
import { utils, writeFile } from 'xlsx'
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', 'onDataChange'])
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 {
emit('onDataChange', tableData.value)
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)
}
}
function handleExport(columns = props.columns, data = tableData.value) {
if (!data?.length) return $message.warning('没有数据')
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
const thKeys = columnsData.map((item) => item.key)
const thData = columnsData.map((item) => item.title)
const trData = data.map((item) => thKeys.map((key) => item[key]))
const sheet = utils.aoa_to_sheet([thData, ...trData])
const workBook = utils.book_new()
utils.book_append_sheet(workBook, sheet, '数据报表')
writeFile(workBook, '数据报表.xlsx')
}
defineExpose({
handleSearch,
handleReset,
handleExport,
})
</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'
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,20 +1,16 @@
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade-slide" mode="out-in" appear>
<keep-alive :include="keepAliveRouteNames">
<component :is="Component" v-if="appStore.reloadFlag" :key="route.path" />
</keep-alive>
</transition>
<KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
</KeepAlive>
</router-view>
</template>
<script setup>
import { useAppStore } from '@/store/modules/app'
import { useTagsStore } from '@/store'
const tagStore = useTagsStore()
const appStore = useAppStore()
const router = useRouter()
const allRoutes = router.getRoutes()
const keepAliveRouteNames = computed(() => {
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
const keepAliveNames = computed(() => {
return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
})
</script>

View File

@@ -1,12 +1,19 @@
<template>
<n-breadcrumb>
<n-breadcrumb-item v-for="item in route.matched" :key="item.path" @click="handleBreadClick(item.path)">
<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'
const router = useRouter()
const route = useRoute()
@@ -14,4 +21,10 @@ 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

@@ -1,6 +1,6 @@
<template>
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
<icon-ant-design:fullscreen-outlined v-if="isFullscreen" />
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
<icon-ant-design:fullscreen-outlined v-else />
</n-icon>
</template>

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<template>
<n-icon size="20" style="cursor: pointer" @click="appStore.switchCollapsed">
<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'
import { useAppStore } from '@/store'
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,82 @@
<template>
<n-popover trigger="click" placement="bottom" @update:show="handlePopoverShow">
<template #trigger>
<n-badge :value="count" mr-20 cursor-pointer>
<n-icon size="18" color-black dark="color-hex-fff">
<icon-material-symbols:notifications-outline />
</n-icon>
</n-badge>
</template>
<n-tabs v-model:value="activeTab" type="line" justify-content="space-around" animated>
<n-tab-pane
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
:tab="tab.title + `(${tab.messages.length})`"
>
<ul class="cus-scroll-y max-h-200 w-220">
<li
v-for="(item, index) in tab.messages"
:key="index"
class="flex-col py-12"
border-t="1 solid gray-200"
:style="index > 0 ? '' : 'border: none;'"
>
<span mb-4 text-ellipsis>{{ item.content }}</span>
<span text-hex-bbb>{{ item.time }}</span>
</li>
</ul>
</n-tab-pane>
</n-tabs>
</n-popover>
</template>
<script setup>
import { formatDateTime } from '@/utils'
const activeTab = ref('')
const tabs = [
{
name: 'zan',
title: '点赞',
messages: [
{ content: '你的文章《XX》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《YY》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《AA》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《BB》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《CC》收到一条点赞', time: formatDateTime() },
{ content: '你的文章《DD》收到一条点赞', time: formatDateTime() },
],
},
{
name: 'star',
title: '关注',
messages: [
{ content: '张三 关注了你', time: formatDateTime() },
{ content: '王五 关注了你', time: formatDateTime() },
],
},
{
name: 'comment',
title: '评论',
messages: [
{ content: '张三 评论了你的文章《XX》"学到了"', time: formatDateTime() },
{ content: '李四 评论了你的文章《YY》"不如Vue"', time: formatDateTime() },
],
},
]
const count = ref(tabs.map((item) => item.messages).flat().length)
watch(activeTab, (v) => {
if (count === 0) return
const tabIndex = tabs.findIndex((item) => item.name === v)
count.value -= tabs[tabIndex].messages.length
if (count.value < 0) count.value = 0
})
function handlePopoverShow(show) {
if (show && !activeTab.value) {
activeTab.value = tabs[0]?.name
}
}
</script>

View File

@@ -0,0 +1,18 @@
<script setup>
import { useAppStore } from '@/store'
import { useDark, useToggle } from '@vueuse/core'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
appStore.toggleDark()
useToggle(isDark)()
}
</script>
<template>
<n-icon mr-20 cursor-pointer size="18" @click="toggleDark">
<icon-mdi-moon-waning-crescent v-if="isDark" />
<icon-mdi-white-balance-sunny v-else />
</n-icon>
</template>

View File

@@ -1,15 +1,15 @@
<template>
<n-dropdown :options="options" @select="handleSelect">
<div class="avatar">
<img :src="userStore.avatar" />
<div flex cursor-pointer items-center>
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
<span>{{ userStore.name }}</span>
</div>
</n-dropdown>
</template>
<script setup>
import { useUserStore } from '@/store/modules/user'
import { renderIcon } from '@/utils/icon'
import { useUserStore } from '@/store'
import { renderIcon } from '@/utils'
const userStore = useUserStore()
@@ -35,18 +35,3 @@ function handleSelect(key) {
}
}
</script>
<style lang="scss" scoped>
.avatar {
display: flex;
align-items: center;
cursor: pointer;
img {
width: 100%;
width: 35px;
height: 35px;
border-radius: 50%;
margin-right: 10px;
}
}
</style>

View File

@@ -1,15 +1,16 @@
<template>
<header class="header">
<div class="h-left">
<MenuCollapse />
<BreadCrumb ml-15 />
</div>
<div class="h-right">
<GithubSite />
<FullScreen />
<UserAvatar />
</div>
</header>
<div flex items-center>
<MenuCollapse />
<BreadCrumb ml-15 hidden sm:block />
</div>
<div ml-auto flex items-center>
<MessageNotification />
<ThemeMode />
<GiteeSite />
<GithubSite />
<FullScreen />
<UserAvatar />
</div>
</template>
<script setup>
@@ -18,22 +19,7 @@ import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.vue'
import GiteeSite from './components/GiteeSite.vue'
import ThemeMode from './components/ThemeMode.vue'
import MessageNotification from './components/MessageNotification.vue'
</script>
<style lang="scss" scoped>
.header {
padding: 0 15px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.h-left {
display: flex;
align-items: center;
}
.h-right {
display: flex;
align-items: center;
}
}
</style>

View File

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

View File

@@ -1,30 +1,35 @@
<template>
<n-menu
ref="menu"
class="side-menu"
accordion
:indent="18"
:collapsed-icon-size="22"
:collapsed-width="64"
:options="menuOptions"
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
:value="activeKey"
@update:value="handleMenuSelect"
/>
</template>
<script setup>
import { usePermissionStore } from '@/store/modules/permission'
import { isExternal } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { renderIcon } from '@/utils/icon'
import { usePermissionStore } from '@/store'
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
const router = useRouter()
const curRoute = useRoute()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const { currentRoute } = router
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
const menuOptions = computed(() => {
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.index - b.index)
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
})
const menu = ref(null)
watch(curRoute, async () => {
await nextTick()
menu.value?.showOption()
})
function resolvePath(basePath, path) {
@@ -43,11 +48,13 @@ function getMenuItem(route, basePath = '') {
label: (route.meta && route.meta.title) || route.name,
key: route.name,
path: resolvePath(basePath, route.path),
icon: route.meta?.icon ? renderIcon(route.meta?.icon, { size: 16 }) : renderIcon('mdi:circle-outline', { size: 8 }),
index: route.meta?.index || 0,
icon: getIcon(route.meta),
order: route.meta?.order || 0,
}
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : []
const visibleChildren = route.children
? route.children.filter((item) => item.name && !item.isHidden)
: []
if (!visibleChildren.length) return menuItem
@@ -55,39 +62,42 @@ function getMenuItem(route, basePath = '') {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem = {
...menuItem,
label: singleRoute.meta?.title || singleRoute.name,
key: singleRoute.name,
path: resolvePath(menuItem.path, singleRoute.path),
icon: singleRoute.meta?.icon
? renderIcon(singleRoute.meta?.icon, { size: 16 })
: renderIcon('mdi:circle-outline', { size: 8 }),
index: menuItem.index,
icon: getIcon(singleRoute.meta),
}
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : []
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.index - b.index)
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.index - b.index)
.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 === currentRoute.value.path && !currentRoute.value.meta?.keepAlive) {
appStore.reloadPage()
} else {
router.push(item.path)
}
router.push(item.path)
}
}
</script>
@@ -102,7 +112,7 @@ function handleMenuSelect(key, item) {
&.n-menu-item-content--selected,
&:hover {
&::before {
border-left: 4px solid var(--primaryColor);
border-left: 4px solid var(--primary-color);
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<n-dropdown
:show="dropdownShow"
:show="show"
:options="options"
:x="x"
:y="y"
@@ -11,9 +11,8 @@
</template>
<script setup>
import { useTagsStore } from '@/store/modules/tags'
import { renderIcon } from '@/utils/icon'
import { useAppStore } from '@/store/modules/app'
import { useTagsStore } from '@/store'
import { renderIcon } from '@/utils'
const props = defineProps({
show: {
@@ -37,7 +36,6 @@ const props = defineProps({
const emit = defineEmits(['update:show'])
const tagsStore = useTagsStore()
const appStore = useAppStore()
const options = computed(() => [
{
@@ -67,25 +65,19 @@ const options = computed(() => [
{
label: '关闭右侧',
key: 'close-right',
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
disabled:
tagsStore.tags.length <= 1 ||
props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
},
])
const dropdownShow = computed({
get() {
return props.show
},
set(show) {
emit('update:show', show)
},
})
const route = useRoute()
const actionMap = new Map([
[
'reload',
() => {
appStore.reloadPage()
tagsStore.reloadTag(route.path, route.meta?.keepAlive)
},
],
[
@@ -115,7 +107,7 @@ const actionMap = new Map([
])
function handleHideDropdown() {
dropdownShow.value = false
emit('update:show', false)
}
function handleSelect(key) {

View File

@@ -1,36 +1,41 @@
<template>
<ScrollX ref="scrollX" :height="useTheme.tags.height">
<ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
<n-tag
v-for="tag in tagsStore.tags"
ref="tabRefs"
:key="tag.path"
class="mx-5 cursor-pointer rounded-4 px-15 hover:color-primary"
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
:closable="tagsStore.tags.length > 1"
@click="handleTagClick(tag.path)"
@close.stop="tagsStore.removeTag(tag.path)"
@contextmenu.prevent="handleContextMenu($event, tag)"
>
<template v-if="tag.icon" #icon>
<TheIcon :icon="tag.icon" class="mr-4" />
</template>
{{ tag.title }}
</n-tag>
<ContextMenu
v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</ScrollX>
<ContextMenu
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</template>
<script setup name="Tags">
<script setup>
import ContextMenu from './ContextMenu.vue'
import { useTagsStore } from '@/store/modules/tags'
import { useThemeStore } from '@/store/modules/theme'
import { useTagsStore } from '@/store'
import ScrollX from '@/components/common/ScrollX.vue'
const route = useRoute()
const router = useRouter()
const tagsStore = useTagsStore()
const useTheme = useThemeStore()
const tabRefs = ref([])
const scrollXRef = ref(null)
const contextMenuOption = reactive({
show: false,
@@ -42,20 +47,25 @@ const contextMenuOption = reactive({
watch(
() => route.path,
() => {
const { name, path } = route
const { name, fullPath: path } = route
const title = route.meta?.title
tagsStore.addTag({ name, path, title })
const icon = route.meta?.icon
const keepAlive = route.meta?.keepAlive
tagsStore.addTag({ name, path, title, icon, keepAlive })
},
{ immediate: true }
)
const scrollX = ref(null)
watch(
() => tagsStore.tags,
async (newVal, oldVal) => {
() => tagsStore.activeIndex,
async (activeIndex) => {
await nextTick()
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
}
const activeTabElement = tabRefs.value[activeIndex]?.$el
if (!activeTabElement) return
const { offsetLeft: x, offsetWidth: width } = activeTabElement
scrollXRef.value?.handleScroll(x + width, width)
},
{ immediate: true }
)
const handleTagClick = (path) => {
@@ -83,25 +93,14 @@ async function handleContextMenu(e, tagItem) {
}
</script>
<style lang="scss">
.n-tag {
padding: 0 15px;
margin: 0 5px;
cursor: pointer;
.n-tag__close {
margin-left: 5px;
box-sizing: content-box;
font-size: 12px;
padding: 2px;
border-radius: 50%;
transition: all 0.7s;
&:hover {
color: #fff;
background-color: var(--primaryColor);
}
}
&:hover {
color: var(--primaryColor);
}
<style>
.n-tag__close {
box-sizing: content-box;
border-radius: 50%;
font-size: 12px;
padding: 2px;
transform: scale(0.9);
transform: translateX(5px);
transition: all 0.3s;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<n-layout has-sider style="height: 100%">
<n-layout has-sider wh-full>
<n-layout-sider
bordered
collapse-mode="width"
@@ -10,22 +10,23 @@
>
<SideBar />
</n-layout-sider>
<n-layout>
<n-layout-header :style="{ height: useTheme.header.height + 'px' }">
<AppHeader />
</n-layout-header>
<n-layout style="background-color: #f5f6fb" :style="`height: calc(100% - ${useTheme.header.height}px)`">
<AppTags v-if="useTheme.tags.visible" />
<AppMain
class="cur-scroll border-t bc-eee"
:style="{
height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`,
overflow: 'auto',
}"
/>
</n-layout>
</n-layout>
<article flex-col flex-1 overflow-hidden>
<header
border-b="1 solid #eee"
class="flex items-center bg-white px-15"
dark="bg-dark border-0"
:style="`height: ${header.height}px`"
>
<AppHeader />
</header>
<section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
<AppTags :style="{ height: `${tags.height}px` }" />
</section>
<section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
<AppMain />
</section>
</article>
</n-layout>
</template>
@@ -34,17 +35,8 @@ import AppHeader from './components/header/index.vue'
import SideBar from './components/sidebar/index.vue'
import AppMain from './components/AppMain.vue'
import AppTags from './components/tags/index.vue'
import { useThemeStore } from '@/store/modules/theme'
import { useAppStore } from '@/store/modules/app'
import { useAppStore } from '@/store'
import { header, tags } from '~/settings'
const useTheme = useThemeStore()
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
.n-layout-header {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #eee;
}
</style>

View File

@@ -1,21 +1,21 @@
/** 重置样式 */
import '@/styles/reset.css'
import '@/styles/variables.css'
import '@/styles/index.scss'
import 'uno.css'
import '@/styles/global.scss'
import 'virtual:svg-icons-register'
import { createApp } from 'vue'
import { setupRouter } from '@/router'
import { setupStore } from '@/store'
import App from './App.vue'
import { setupNaiveDiscreteApi } from './utils'
function setupApp() {
async function setupApp() {
const app = createApp(App)
setupStore(app)
setupRouter(app)
await setupRouter(app)
app.mount('#app')
setupNaiveDiscreteApi()
}
setupApp()

View File

@@ -1,4 +1,4 @@
const baseTitle = import.meta.env.VITE_APP_TITLE
const baseTitle = import.meta.env.VITE_TITLE
export function createPageTitleGuard(router) {
router.afterEach((to) => {

View File

@@ -1,44 +1,20 @@
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE } from '@/router/routes'
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
import { toLogin } from '@/utils/auth'
import { getToken, refreshAccessToken, isNullOrWhitespace } from '@/utils'
const WHITE_LIST = ['/login', '/redirect']
const WHITE_LIST = ['/login', '/404']
export function createPermissionGuard(router) {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
router.beforeEach(async (to, from, next) => {
router.beforeEach(async (to) => {
const token = getToken()
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (userStore.userId) {
// 已经拿到用户信息
refreshAccessToken()
next()
} else {
await userStore.getUserInfo().catch((error) => {
removeToken()
toLogin()
$message.error(error.message || '获取用户信息失败!')
return
})
const accessRoutes = permissionStore.generateRoutes(userStore.role)
accessRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
router.addRoute(NOT_FOUND_ROUTE)
next({ ...to, replace: true })
}
}
} else {
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next({ path: '/login', query: { ...to.query, redirect: to.path } })
}
/** 没有token的情况 */
if (isNullOrWhitespace(token)) {
if (WHITE_LIST.includes(to.path)) return true
return { path: 'login', query: { ...to.query, redirect: to.path } }
}
/** 有token的情况 */
if (to.path === '/login') return { path: '/' }
refreshAccessToken()
return true
})
}

View File

@@ -1,28 +1,83 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { setupRouterGuard } from './guard'
import { basicRoutes } from './routes'
import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
import { getToken, isNullOrWhitespace } from '@/utils'
import { useUserStore, usePermissionStore } from '@/store'
const isHash = !!import.meta.env.VITE_APP_USE_HASH
const isHash = import.meta.env.VITE_USE_HASH === 'true'
export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes: [],
routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
export function resetRouter() {
export async function setupRouter(app) {
await addDynamicRoutes()
setupRouterGuard(router)
app.use(router)
}
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => {
const { name } = route
router.hasRoute(name) && router.removeRoute(name)
})
basicRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
const name = route.name
if (!basicRouteNames.includes(name)) {
router.removeRoute(name)
}
})
}
export function setupRouter(app) {
basicRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
app.use(router)
setupRouterGuard(router)
export async function addDynamicRoutes() {
// return Promise.reject('123')
const token = getToken()
// 没有token情况
if (isNullOrWhitespace(token)) {
router.addRoute(EMPTY_ROUTE)
return
}
// 有token的情况
const userStore = useUserStore()
try {
const permissionStore = usePermissionStore()
!userStore.userId && (await userStore.getUserInfo())
const accessRoutes = permissionStore.generateRoutes(userStore.role)
accessRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
router.addRoute(NOT_FOUND_ROUTE)
window.$notification?.success({
title: '🎉🎉🎉 2.0 全栈版本开源了!',
content: () =>
h(
'span',
{},
'2.0为全栈版本,提供前端+后端,全新重构,全面简化,',
h(
'a',
{ href: 'https://admin.isme.top', target: '__blank' },
'👉体验 https://admin.isme.top'
)
),
})
} catch (error) {
console.error(error)
$message.error('初始化用户信息失败: ' + error)
userStore.logout()
}
}
export function getRouteNames(routes) {
return routes.map((route) => getRouteName(route)).flat(1)
}
function getRouteName(route) {
const names = [route.name]
if (route.children && route.children.length) {
names.push(...route.children.map((item) => getRouteName(item)).flat(1))
}
return names
}

View File

@@ -1,5 +1,4 @@
import Layout from '@/layout/index.vue'
import Home from '@/views/dashboard/index.vue'
const Layout = () => import('@/layout/index.vue')
export const basicRoutes = [
{
@@ -8,21 +7,9 @@ export const basicRoutes = [
component: () => import('@/views/error-page/404.vue'),
isHidden: true,
},
{
name: 'REDIRECT',
path: '/redirect',
component: Layout,
isHidden: true,
children: [
{
name: 'REDIRECT_NAME',
path: '',
component: () => import('@/views/redirect/index.vue'),
},
],
},
{
name: 'LOGIN',
name: 'Login',
path: '/login',
component: () => import('@/views/login/index.vue'),
isHidden: true,
@@ -31,97 +18,6 @@ export const basicRoutes = [
},
},
{
name: 'Dashboard',
path: '/',
component: Layout,
redirect: '/home',
meta: {
title: 'Dashboard',
icon: 'mdi:chart-bar',
},
children: [
{
name: 'Home',
path: 'home',
component: Home,
meta: {
title: '首页',
icon: 'mdi:home',
},
},
],
},
{
name: 'ErrorPage',
path: '/error-page',
component: Layout,
redirect: '/error-page/404',
meta: {
title: '错误页',
icon: 'mdi:alert-circle-outline',
index: 4,
},
children: [
{
name: 'ERROR-404',
path: '404',
component: () => import('@/views/error-page/404.vue'),
meta: {
title: '404',
icon: 'mdi:alert-circle-outline',
},
},
],
},
{
name: 'Test',
path: '/test',
component: Layout,
redirect: '/test/unocss',
meta: {
title: '基础功能测试',
icon: 'mdi:menu',
},
children: [
{
name: 'Unocss',
path: 'unocss',
component: () => import('@/views/test-page/unocss/index.vue'),
meta: {
title: '测试unocss',
},
},
{
name: 'Message',
path: 'message',
component: () => import('@/views/test-page/message/index.vue'),
meta: {
title: '测试Message',
},
},
{
name: 'Dialog',
path: 'dialog',
component: () => import('@/views/test-page/dialog/index.vue'),
meta: {
title: '测试Dialog',
},
},
{
name: 'TestKeepAlive',
path: 'keep-alive',
component: () => import('@/views/test-page/keep-alive/index.vue'),
meta: {
title: '测试Keep-Alive',
keepAlive: true,
},
},
],
},
{
name: 'ExternalLink',
path: '/external-link',
@@ -129,6 +25,7 @@ export const basicRoutes = [
meta: {
title: '外部链接',
icon: 'mdi:link-variant',
order: 4,
},
children: [
{
@@ -166,10 +63,16 @@ export const NOT_FOUND_ROUTE = {
isHidden: true,
}
const modules = import.meta.globEager('./modules/*.js')
export const EMPTY_ROUTE = {
name: 'Empty',
path: '/:pathMatch(.*)*',
component: null,
}
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
const asyncRoutes = []
Object.keys(modules).forEach((key) => {
asyncRoutes.push(...modules[key].default)
asyncRoutes.push(modules[key].default)
})
export { asyncRoutes }

View File

@@ -1,47 +0,0 @@
import Layout from '@/layout/index.vue'
export default [
{
name: 'Example',
path: '/example',
component: Layout,
redirect: '/example/table',
meta: {
title: '组件示例',
role: ['admin'],
},
children: [
{
name: 'Table',
path: 'table',
component: () => import('@/views/examples/table/index.vue'),
redirect: '/example/table/post',
meta: {
title: '表格',
role: ['admin'],
icon: 'mdi:table',
},
children: [
{
name: 'PostList',
path: 'post',
component: () => import('@/views/examples/table/post/index.vue'),
meta: {
title: '文章列表',
role: ['admin'],
},
},
{
name: 'PostCreate',
path: 'post-create',
component: () => import('@/views/examples/table/post/PostCreate.vue'),
meta: {
title: '创建文章',
role: ['admin'],
},
},
],
},
],
},
]

View File

@@ -1 +0,0 @@
export { default as themeSettings } from './theme.json'

View File

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

View File

@@ -3,3 +3,5 @@ import { createPinia } from 'pinia'
export function setupStore(app) {
app.use(createPinia())
}
export * from './modules'

View File

@@ -1,29 +1,28 @@
import { defineStore } from 'pinia'
import { useDark } from '@vueuse/core'
const isDark = useDark()
export const useAppStore = defineStore('app', {
state() {
return {
reloadFlag: true,
collapsed: false,
isDark,
}
},
actions: {
async reloadPage() {
$loadingBar.start()
this.reloadFlag = false
await nextTick()
this.reloadFlag = true
setTimeout(() => {
document.documentElement.scrollTo({ left: 0, top: 0 })
$loadingBar.finish()
}, 100)
},
switchCollapsed() {
this.collapsed = !this.collapsed
},
setCollapsed(collapsed) {
this.collapsed = collapsed
},
/** 设置暗黑模式 */
setDark(isDark) {
this.isDark = isDark
},
/** 切换/关闭 暗黑模式 */
toggleDark() {
this.isDark = !this.isDark
},
},
})

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