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

154 Commits
v1.0.3 ... 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
91 changed files with 6007 additions and 3243 deletions

View File

@@ -5,10 +5,7 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_MOCK = true VITE_USE_MOCK = true
# 是否启用MOCK # 是否启用MOCK
VITE_USE_PROXY = false VITE_USE_PROXY = true
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
VITE_PROXY_TYPE = 'dev'
# base api # base api
VITE_BASE_API = '/api' VITE_BASE_API = '/api'

View File

@@ -1,5 +1,5 @@
# 自定义域名CNAME # 自定义域名CNAME
# VITE_CNAME = 'template.qszone.com' # VITE_CNAME = 'template.isme.top'
# 资源公共路径,需要以 /开头和结尾 # 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/vue-naive-admin/' VITE_PUBLIC_PATH = '/vue-naive-admin/'
@@ -10,4 +10,4 @@ VITE_USE_HASH = true
VITE_USE_MOCK = true VITE_USE_MOCK = true
# base api # base api
VITE_BASE_API = '/api' VITE_BASE_API = '/api'

View File

@@ -8,7 +8,7 @@ VITE_USE_MOCK = true
VITE_BASE_API = '/api' VITE_BASE_API = '/api'
# 是否启用压缩 # 是否启用压缩
VITE_USE_COMPRESS = true VITE_USE_COMPRESS = false
# 压缩类型 # 压缩类型
VITE_COMPRESS_TYPE = gzip VITE_COMPRESS_TYPE = gzip

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': 'error',
'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

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>/**"]
}
]
}

View File

@@ -1,8 +1,4 @@
{ {
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src",
"~/": "${workspaceRoot}"
},
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.printWidth": 120, "prettier.printWidth": 120,
@@ -25,7 +21,7 @@
}, },
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"files.associations": { "files.associations": {
"*.env.*": "dotenv", "*.env.*": "dotenv",

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>

218
README.md
View File

@@ -1,115 +1,229 @@
<p align="center"> <p align="center">
<a href="https://github.com/zclzone/vue-naive-admin"> <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> </a>
</p> </p>
<p align="center"> <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="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p> </p>
<p align='center'> <p align='center'>
<b>English</b> | <b>中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a> <a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.EN.md">English</a>
</p> </p>
### Introduction > 🎉🎉🎉 2.0 已开源,全新重构,全面简化,后端使用 nestjs + mysql + typeOrm[👉点击前往2.0版本 | 分支 2.x](https://github.com/zclzone/vue-naive-admin/tree/2.x),
[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. - 体验地址: [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)
### Features ### 简介
- 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You. [Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite4、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,上手成本非常低,非常适合中小型项目或者个人项目。
- 🍑 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.qszone.com](https://template.qszone.com) - 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin) > ✨✨ 双十一香港特惠服务器推荐,~~**2C4G 100M** `71/年` `142/两年`~~[👉点击前往](https://blog.isme.top/vps-recommend/)
### Docs ### 预览
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs) [https://template.isme.top](https://template.isme.top)
[https://base.isme.top](https://base.isme.top)
### Getting Started ### 文档
项目文档: [Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
从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 ```shell
# Recommended setup git autocrlf 为 false # 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false git config --global core.autocrlf false
# Clone Project # 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin cd vue-naive-admin
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation) # 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored npm i -g pnpm # 装了可忽略
pnpm i # or npm i pnpm i # 或者 npm i
# Start # 启动
pnpm dev pnpm dev
``` ```
### Build and Release ### 构建发布
```shell ```shell
# Test Environment # 构建测试环境
pnpm build:test pnpm build:test
# Github Environment # 构建github pages环境
pnpm build:github pnpm build:github
# Prod Environment # 构建生产环境
pnpm build pnpm build
``` ```
### Other ### 其他指令
```shell ```shell
# eslint check # eslint代码格式检查
pnpm lint pnpm lint
# eslint check and fix # 代码检查并修复
pnpm lint:fix pnpm lint:fix
# PreviewNeed to build first # 预览发布包效果(需先执行构建指令
pnpm preview pnpm preview
# Commithusky+commitlint # 提交代码husky+commitlint
pnpm cz pnpm cz
``` ```
### TS version: Qs Admin
#### source code ### 目录说明
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin) ```
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
#### 源码
- 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) - gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### preview #### 预览
- [https://admin.qszone.com](https://admin.qszone.com) - [https://admin.isme.top](https://admin.isme.top)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin) - [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Communication group & About the author ### 使用该项目的开源项目
<a href="https://blog.qszone.com/about/"> - [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin对接真实后端服务实现了后端控制路由等特性。
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" /> - [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> </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,115 +0,0 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="https://assets.qszone.com/images/logo_qs.svg">
</a>
</p>
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>简体中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">English</a>
</p>
### 简介
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,学习成本非常低,非常适合中小型项目或者个人项目。
### 功能
- 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产和github pages环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
### 预览
[https://template.qszone.com](https://template.qszone.com)
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
### 文档
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
[羽雀文档Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
### 快速开始
```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false
# 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # 装了可忽略
pnpm i # 或者 npm i
# 启动
pnpm dev
```
### 构建发布
```shell
# 构建测试环境
pnpm build:test
# 构建github pages环境
pnpm build:github
# 构建生产环境
pnpm build
```
### 其他指令
```shell
# eslint代码格式检查
pnpm lint
# 代码检查并修复
pnpm lint:fix
# 预览发布包效果(需先执行构建指令)
pnpm preview
# 提交代码husky+commitlint
pnpm cz
```
### TS 版本: Qs Admin
#### 源码
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### 预览
- [https://admin.qszone.com](https://admin.qszone.com)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### 入群交流 & 关于作者
<a href="https://blog.qszone.com/about/">
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
</a>

View File

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

View File

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

View File

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

View File

@@ -1 +1,33 @@
export const OUTPUT_DIR = 'dist' 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

@@ -10,13 +10,15 @@ import Unocss from 'unocss/vite'
import visualizer from 'rollup-plugin-visualizer' import visualizer from 'rollup-plugin-visualizer'
// 压缩 // 压缩
import viteCompression from 'vite-plugin-compression' import viteCompression from 'vite-plugin-compression'
// vite-vuedevtool
import VueDevTools from 'vite-plugin-vue-devtools'
import { configHtmlPlugin } from './html' import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock' import { configMockPlugin } from './mock'
import unplugin from './unplugin' import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) { export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()] const plugins = [VueDevTools(), vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv?.VITE_USE_MOCK) { if (viteEnv?.VITE_USE_MOCK) {
plugins.push(configMockPlugin(isBuild)) plugins.push(configMockPlugin(isBuild))

View File

@@ -1,5 +1,4 @@
import { resolve } from 'path' import { resolve } from 'path'
import DefineOptions from 'unplugin-vue-define-options/vite'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
@@ -19,7 +18,6 @@ import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg') const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [ export default [
DefineOptions(),
AutoImport({ AutoImport({
imports: ['vue', 'vue-router'], imports: ['vue', 'vue-router'],
dts: false, dts: false,
@@ -33,7 +31,10 @@ export default [
defaultClass: 'inline-block', defaultClass: 'inline-block',
}), }),
Components({ Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })], resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
],
dts: false, dts: false,
}), }),
createSvgIconsPlugin({ createSvgIconsPlugin({

View File

@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
/** /**
* * 项目根路径 * * 项目根路径
* @descrition 结尾不带/ * @description 结尾不带/
*/ */
export function getRootPath() { export function getRootPath() {
return path.resolve(process.cwd()) return path.resolve(process.cwd())
@@ -13,14 +13,12 @@ export function getRootPath() {
/** /**
* * 项目src路径 * * 项目src路径
* @param srcName src目录名称(默认: "src") * @param srcName src目录名称(默认: "src")
* @descrition 结尾不带斜杠 * @description 结尾不带斜杠
*/ */
export function getSrcPath(srcName = 'src') { export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName) return path.resolve(getRootPath(), srcName)
} }
const httpsReg = /^https:\/\//
export function convertEnv(envOptions) { export function convertEnv(envOptions) {
const result = {} const result = {}
if (!envOptions) return result if (!envOptions) return result

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="Cache-control" content="no-cache" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/resource/loading.css" /> <link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title> <title><%= title %></title>
@@ -17,7 +17,7 @@
<div id="app"> <div id="app">
<!-- 白屏时的loading效果 --> <!-- 白屏时的loading效果 -->
<div class="loading-container"> <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__container">
<div class="loading-spin"> <div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div> <div class="left-0 top-0 loading-spin-item"></div>

View File

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

View File

@@ -38,7 +38,8 @@ const posts = [
author: '大脸怪', author: '大脸怪',
category: 'Http', category: 'Http',
description: '谈谈前端缓存的理解', description: '谈谈前端缓存的理解',
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存', content:
'> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true, isRecommend: true,
isPublish: true, isPublish: true,
createDate: '2021-06-10T18:51:19.000Z', createDate: '2021-06-10T18:51:19.000Z',
@@ -49,7 +50,8 @@ const posts = [
author: '大脸怪', author: '大脸怪',
category: 'JavaScript', category: 'JavaScript',
description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景', description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景',
content: '## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。', content:
'## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true, isRecommend: true,
isPublish: true, isPublish: true,
createDate: '2021-02-22T22:37:06.000Z', createDate: '2021-02-22T22:37:06.000Z',
@@ -65,7 +67,9 @@ export default [
const { title, pageNo, pageSize } = data.query const { title, pageNo, pageSize } = data.query
let pageData = [] let pageData = []
let total = 60 let total = 60
const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0)) const filterData = posts.filter(
(item) => item.title.includes(title) || (!title && title !== 0)
)
if (filterData.length) { if (filterData.length) {
if (pageSize) { if (pageSize) {
while (pageData.length < pageSize) { while (pageData.length < pageSize) {

View File

@@ -4,21 +4,21 @@ const users = {
admin: { admin: {
id: 1, id: 1,
name: '大脸怪(admin)', name: '大脸怪(admin)',
avatar: 'https://assets.qszone.com/images/avatar.jpg', avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com', email: 'Ronnie@123.com',
role: ['admin'], role: ['admin'],
}, },
editor: { editor: {
id: 2, id: 2,
name: '大脸怪(editor)', name: '大脸怪(editor)',
avatar: 'https://assets.qszone.com/images/avatar.jpg', avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com', email: 'Ronnie@123.com',
role: ['editor'], role: ['editor'],
}, },
guest: { guest: {
id: 3, id: 3,
name: '访客(guest)', name: '访客(guest)',
avatar: 'https://assets.qszone.com/images/avatar.jpg', avatar: 'https://static.isme.top/images/avatar.jpg',
role: [], role: [],
}, },
} }

View File

@@ -2,71 +2,81 @@
"name": "vue-naive-admin", "name": "vue-naive-admin",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"build": "vite build",
"build:github": "vite build --mode github && esno ./build/script",
"build:test": "vite build --mode test",
"cz": "cz",
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .js,.vue .", "lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .", "lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"build": "vite build",
"build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script",
"preview": "vite preview",
"prepare": "husky install", "prepare": "husky install",
"cz": "cz" "preview": "vite preview"
}, },
"dependencies": { "lint-staged": {
"@vueuse/core": "^8.4.2", "*.{js,vue}": [
"axios": "^0.21.4", "eslint --ext .js,.vue ."
"dayjs": "^1.11.0", ]
"lodash-es": "^4.17.21",
"md-editor-v3": "^1.11.4",
"mockjs": "^1.1.0",
"pinia": "^2.0.13",
"vue": "^3.2.39",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.99",
"@iconify/vue": "^3.2.1",
"@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31",
"chalk": "^5.0.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.9.0",
"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",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"naive-ui": "^2.33.3",
"prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10",
"unocss": "^0.43.2",
"unplugin-auto-import": "^0.9.2",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.17.21",
"unplugin-vue-define-options": "^0.11.2",
"vite": "^3.1.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-svg-icons": "^2.0.1"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "node_modules/cz-customizable" "path": "node_modules/cz-customizable"
} }
}, },
"lint-staged": { "eslintConfig": {
"*.{js,vue}": [ "extends": [
"eslint --ext .js,.vue ." "@zclzone",
"@unocss",
".eslint-global-variables.json"
] ]
},
"dependencies": {
"@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.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": {
"@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-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "1.0.0-rc.7"
} }
} }

6909
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%; height: 100%;
} }
.loading-svg {
width: 128px;
height: 128px;
color: var(--primary-color);
}
.loading-spin__container { .loading-spin__container {
width: 56px; width: 56px;
height: 56px; height: 56px;

View File

@@ -1,17 +1,3 @@
/**
* 初始化加载效果的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() { function addThemeColorCssVars() {
const key = '__THEME_COLOR__' const key = '__THEME_COLOR__'
const defaultColor = '#316c72' const defaultColor = '#316c72'
@@ -21,5 +7,3 @@ function addThemeColorCssVars() {
} }
addThemeColorCssVars() addThemeColorCssVars()
initSvgLogo('#loadingLogo')

BIN
public/resource/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 811 B

View File

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

View File

@@ -1,25 +1,23 @@
<template> <template>
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides"> <n-config-provider
<n-loading-bar-provider> wh-full
<n-dialog-provider> :locale="zhCN"
<n-notification-provider> :date-locale="dateZhCN"
<n-message-provider> :theme="appStore.isDark ? darkTheme : undefined"
<slot></slot> :theme-overrides="naiveThemeOverrides"
<NaiveProviderContent /> >
</n-message-provider> <slot />
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider> </n-config-provider>
</template> </template>
<script setup> <script setup>
import { defineComponent, h } from 'vue' import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
import { useCssVar } from '@vueuse/core' import { useCssVar } from '@vueuse/core'
import { kebabCase } from 'lodash-es' import { kebabCase } from 'lodash-es'
import { setupMessage, setupDialog } from '@/utils'
import { naiveThemeOverrides } from '~/settings' import { naiveThemeOverrides } from '~/settings'
import { useAppStore } from '@/store'
const appStore = useAppStore()
function setupCssVar() { function setupCssVar() {
const common = naiveThemeOverrides.common const common = naiveThemeOverrides.common
@@ -28,23 +26,5 @@ function setupCssVar() {
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '') if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
} }
} }
setupCssVar()
// 挂载naive组件的方法至window, 以便在全局使用
function setupNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$notification = useNotification()
window.$message = setupMessage(useMessage())
window.$dialog = setupDialog(useDialog())
}
const NaiveProviderContent = defineComponent({
setup() {
setupCssVar()
setupNaiveTools()
},
render() {
return h('div')
},
})
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel"> <div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow"> <template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })"> <div class="left dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: 120 })">
<icon-ic:baseline-keyboard-arrow-left /> <icon-ic:baseline-keyboard-arrow-left />
</div> </div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })"> <div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
<icon-ic:baseline-keyboard-arrow-right /> <icon-ic:baseline-keyboard-arrow-right />
</div> </div>
</template> </template>
@@ -23,7 +23,7 @@
</template> </template>
<script setup> <script setup>
import { debounce } from '@/utils' import { debounce, useResize } from '@/utils'
defineProps({ defineProps({
showArrow: { showArrow: {
@@ -76,17 +76,39 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
} }
}, 200) }, 200)
const observer = new MutationObserver(refreshIsOverflow) const observers = ref([])
onMounted(() => { onMounted(() => {
refreshIsOverflow() refreshIsOverflow()
window.addEventListener('resize', refreshIsOverflow) observers.value.push(useResize(document.body, refreshIsOverflow))
// 监听内容宽度刷新是否超出 observers.value.push(useResize(content.value, refreshIsOverflow))
observer.observe(content.value, { childList: true })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', refreshIsOverflow) observers.value.forEach((item) => {
observer.disconnect() 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({
handleScroll,
}) })
</script> </script>

View File

@@ -1,8 +1,9 @@
<template> <template>
<transition name="fade-slide" mode="out-in" appear> <transition name="fade-slide" mode="out-in" appear>
<section class="cus-scroll-y wh-full p-15 flex-col bg-[#f5f6fb]"> <section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
<slot /> <slot />
<AppFooter v-if="showFooter" mt-15 /> <AppFooter v-if="showFooter" mt-15 />
<n-back-top :bottom="20" />
</section> </section>
</transition> </transition>
</template> </template>

View File

@@ -1,14 +1,14 @@
<template> <template>
<AppPage :show-footer="showFooter"> <AppPage :show-footer="showFooter">
<header v-if="showHeader" px-15 mb-15 min-h-45 flex justify-between items-center> <header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
<slot v-if="$slots.header" name="header" /> <slot v-if="$slots.header" name="header" />
<template v-else> <template v-else>
<h2 color="#333" text-22 font-normal>{{ title || route.meta?.title }}</h2> <h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
<slot name="action" /> <slot name="action" />
</template> </template>
</header> </header>
<n-card rounded-10 flex-1> <n-card flex-1 rounded-10>
<slot /> <slot />
</n-card> </n-card>
</AppPage> </AppPage>

View File

@@ -1,5 +1,16 @@
<template> <template>
<div min-h-60 p-15 flex items-start justify-between b-1 bc-ccc rounded-8 bg="#fafafc"> <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]"> <n-space wrap :size="[35, 15]">
<slot /> <slot />
</n-space> </n-space>

View File

@@ -1,6 +1,11 @@
<template> <template>
<div flex items-center> <div flex items-center>
<label v-if="!isNullOrWhitespace(label)" w-80 flex-shrink-0 :style="{ width: labelWidth + 'px' }"> <label
v-if="!isNullOrWhitespace(label)"
w-80
flex-shrink-0
:style="{ width: labelWidth + 'px' }"
>
{{ label }} {{ label }}
</label> </label>
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0> <div :style="{ width: contentWidth + 'px' }" flex-shrink-0>

View File

@@ -1,5 +1,12 @@
<template> <template>
<n-modal v-model:show="show" :style="{ width }" preset="card" :title="title" size="huge" :bordered="false"> <n-modal
v-model:show="show"
:style="{ width }"
preset="card"
:title="title"
size="huge"
:bordered="false"
>
<slot /> <slot />
<template v-if="showFooter" #footer> <template v-if="showFooter" #footer>
<footer flex justify-end> <footer flex justify-end>

View File

@@ -17,6 +17,8 @@
</template> </template>
<script setup> <script setup>
import { utils, writeFile } from 'xlsx'
const props = defineProps({ const props = defineProps({
/** /**
* @remote true: 后端分页 false 前端分页 * @remote true: 后端分页 false 前端分页
@@ -73,7 +75,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:queryItems', 'onChecked']) const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
const loading = ref(false) const loading = ref(false)
const initQuery = { ...props.queryItems } const initQuery = { ...props.queryItems }
const tableData = ref([]) const tableData = ref([])
@@ -87,13 +89,18 @@ async function handleQuery() {
if (props.isPagination && props.remote) { if (props.isPagination && props.remote) {
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize } paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
} }
const { data } = await props.getData({ ...props.queryItems, ...props.extraParams, ...paginationParams }) const { data } = await props.getData({
...props.queryItems,
...props.extraParams,
...paginationParams,
})
tableData.value = data?.pageData || data tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length pagination.itemCount = data.total ?? data.length
} catch (error) { } catch (error) {
tableData.value = [] tableData.value = []
pagination.itemCount = 0 pagination.itemCount = 0
} finally { } finally {
emit('onDataChange', tableData.value)
loading.value = false loading.value = false
} }
} }
@@ -122,9 +129,21 @@ function onChecked(rowKeys) {
emit('onChecked', rowKeys) 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({ defineExpose({
handleSearch, handleSearch,
handleReset, handleReset,
handleExport,
}) })
</script> </script>

View File

@@ -1,19 +1,16 @@
<template> <template>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<KeepAlive :include="keepAliveRouteNames"> <KeepAlive :include="keepAliveNames">
<component :is="Component" v-if="appStore.reloadFlag" :key="appStore.aliveKeys[route.name] || route.fullPath" /> <component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
</KeepAlive> </KeepAlive>
</router-view> </router-view>
</template> </template>
<script setup> <script setup>
import { useAppStore } from '@/store' import { useTagsStore } from '@/store'
import { useRouter } from 'vue-router' const tagStore = useTagsStore()
const appStore = useAppStore()
const router = useRouter()
const allRoutes = router.getRoutes() const keepAliveNames = computed(() => {
const keepAliveRouteNames = computed(() => { return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
}) })
</script> </script>

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

@@ -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,7 +1,7 @@
<template> <template>
<n-dropdown :options="options" @select="handleSelect"> <n-dropdown :options="options" @select="handleSelect">
<div flex items-center cursor-pointer> <div flex cursor-pointer items-center>
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full /> <img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
<span>{{ userStore.name }}</span> <span>{{ userStore.name }}</span>
</div> </div>
</n-dropdown> </n-dropdown>

View File

@@ -1,9 +1,12 @@
<template> <template>
<div flex items-center> <div flex items-center>
<MenuCollapse /> <MenuCollapse />
<BreadCrumb ml-15 /> <BreadCrumb ml-15 hidden sm:block />
</div> </div>
<div ml-auto flex items-center> <div ml-auto flex items-center>
<MessageNotification />
<ThemeMode />
<GiteeSite />
<GithubSite /> <GithubSite />
<FullScreen /> <FullScreen />
<UserAvatar /> <UserAvatar />
@@ -16,4 +19,7 @@ import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue' import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue' import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.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> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<router-link h-60 f-c-c to="/"> <router-link h-60 f-c-c to="/">
<icon-custom-logo text-36 color-primary></icon-custom-logo> <img src="@/assets/images/logo.png" height="42" />
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0> <h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
{{ title }} {{ title }}
</h2> </h2>
</router-link> </router-link>

View File

@@ -1,29 +1,37 @@
<template> <template>
<n-menu <n-menu
ref="menu"
class="side-menu" class="side-menu"
accordion accordion
:indent="18" :indent="18"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:collapsed-width="64" :collapsed-width="64"
:options="menuOptions" :options="menuOptions"
:value="curRoute.meta?.activeMenu || curRoute.name" :value="activeKey"
@update:value="handleMenuSelect" @update:value="handleMenuSelect"
/> />
</template> </template>
<script setup> <script setup>
import { usePermissionStore, useAppStore } from '@/store' import { usePermissionStore } from '@/store'
import { renderCustomIcon, renderIcon, isExternal } from '@/utils' import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
const router = useRouter() const router = useRouter()
const curRoute = useRoute() const curRoute = useRoute()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const appStore = useAppStore()
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
const menuOptions = computed(() => { const menuOptions = computed(() => {
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order) 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) { function resolvePath(basePath, path) {
if (isExternal(path)) return path if (isExternal(path)) return path
return ( return (
@@ -44,7 +52,9 @@ function getMenuItem(route, basePath = '') {
order: route.meta?.order || 0, 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 if (!visibleChildren.length) return menuItem
@@ -52,25 +62,28 @@ function getMenuItem(route, basePath = '') {
// 单个子路由处理 // 单个子路由处理
const singleRoute = visibleChildren[0] const singleRoute = visibleChildren[0]
menuItem = { menuItem = {
...menuItem,
label: singleRoute.meta?.title || singleRoute.name, label: singleRoute.meta?.title || singleRoute.name,
key: singleRoute.name, key: singleRoute.name,
path: resolvePath(menuItem.path, singleRoute.path), path: resolvePath(menuItem.path, singleRoute.path),
icon: getIcon(singleRoute.meta), icon: getIcon(singleRoute.meta),
order: menuItem.order,
} }
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) { if (visibleItems.length === 1) {
menuItem = getMenuItem(visibleItems[0], menuItem.path) menuItem = getMenuItem(visibleItems[0], menuItem.path)
} else if (visibleItems.length > 1) { } else if (visibleItems.length > 1) {
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.order - b.order) menuItem.children = visibleItems
.map((item) => getMenuItem(item, menuItem.path))
.sort((a, b) => a.order - b.order)
} }
} else { } else {
menuItem.children = visibleChildren menuItem.children = visibleChildren
.map((item) => getMenuItem(item, menuItem.path)) .map((item) => getMenuItem(item, menuItem.path))
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
} }
return menuItem return menuItem
} }
@@ -84,11 +97,7 @@ function handleMenuSelect(key, item) {
if (isExternal(item.path)) { if (isExternal(item.path)) {
window.open(item.path) window.open(item.path)
} else { } else {
if (item.path === curRoute.path) { router.push(item.path)
appStore.reloadPage()
} else {
router.push(item.path)
}
} }
} }
</script> </script>

View File

@@ -11,9 +11,8 @@
</template> </template>
<script setup> <script setup>
import { useTagsStore, useAppStore } from '@/store' import { useTagsStore } from '@/store'
import { renderIcon } from '@/utils' import { renderIcon } from '@/utils'
import { useLocalStorage } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -37,7 +36,6 @@ const props = defineProps({
const emit = defineEmits(['update:show']) const emit = defineEmits(['update:show'])
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const appStore = useAppStore()
const options = computed(() => [ const options = computed(() => [
{ {
@@ -67,7 +65,9 @@ const options = computed(() => [
{ {
label: '关闭右侧', label: '关闭右侧',
key: 'close-right', 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' }), icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
}, },
]) ])
@@ -77,11 +77,7 @@ const actionMap = new Map([
[ [
'reload', 'reload',
() => { () => {
if (route.meta?.keepAlive) { tagsStore.reloadTag(route.path, route.meta?.keepAlive)
// 重置keepAlive
appStore.setAliveKeys(route.name, +new Date())
}
appStore.reloadPage()
}, },
], ],
[ [

View File

@@ -1,15 +1,19 @@
<template> <template>
<ScrollX> <ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
<n-tag <n-tag
v-for="tag in tagsStore.tags" v-for="tag in tagsStore.tags"
ref="tabRefs"
:key="tag.path" :key="tag.path"
class="px-15 mx-5 rounded-4 cursor-pointer hover:color-primary" class="mx-5 cursor-pointer rounded-4 px-15 hover:color-primary"
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'" :type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
:closable="tagsStore.tags.length > 1" :closable="tagsStore.tags.length > 1"
@click="handleTagClick(tag.path)" @click="handleTagClick(tag.path)"
@close.stop="tagsStore.removeTag(tag.path)" @close.stop="tagsStore.removeTag(tag.path)"
@contextmenu.prevent="handleContextMenu($event, tag)" @contextmenu.prevent="handleContextMenu($event, tag)"
> >
<template v-if="tag.icon" #icon>
<TheIcon :icon="tag.icon" class="mr-4" />
</template>
{{ tag.title }} {{ tag.title }}
</n-tag> </n-tag>
<ContextMenu <ContextMenu
@@ -30,6 +34,8 @@ import ScrollX from '@/components/common/ScrollX.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const tagsStore = useTagsStore() const tagsStore = useTagsStore()
const tabRefs = ref([])
const scrollXRef = ref(null)
const contextMenuOption = reactive({ const contextMenuOption = reactive({
show: false, show: false,
@@ -41,9 +47,23 @@ const contextMenuOption = reactive({
watch( watch(
() => route.path, () => route.path,
() => { () => {
const { name, path } = route const { name, fullPath: path } = route
const title = route.meta?.title 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 }
)
watch(
() => tagsStore.activeIndex,
async (activeIndex) => {
await nextTick()
const activeTabElement = tabRefs.value[activeIndex]?.$el
if (!activeTabElement) return
const { offsetLeft: x, offsetWidth: width } = activeTabElement
scrollXRef.value?.handleScroll(x + width, width)
}, },
{ immediate: true } { immediate: true }
) )

View File

@@ -11,14 +11,19 @@
<SideBar /> <SideBar />
</n-layout-sider> </n-layout-sider>
<article flex-1 flex-col overflow-hidden> <article flex-col flex-1 overflow-hidden>
<header bg-white px-15 border-b bc-eee flex items-center :style="`height: ${header.height}px`"> <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 /> <AppHeader />
</header> </header>
<section v-if="tags.visible" border-b bc-eee> <section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
<AppTags :style="{ height: `${tags.height}px` }" /> <AppTags :style="{ height: `${tags.height}px` }" />
</section> </section>
<section flex-1 overflow-hidden> <section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
<AppMain /> <AppMain />
</section> </section>
</article> </article>

View File

@@ -8,15 +8,14 @@ import { createApp } from 'vue'
import { setupRouter } from '@/router' import { setupRouter } from '@/router'
import { setupStore } from '@/store' import { setupStore } from '@/store'
import App from './App.vue' import App from './App.vue'
import { setupNaiveDiscreteApi } from './utils'
async function setupApp() { async function setupApp() {
const app = createApp(App) const app = createApp(App)
setupStore(app) setupStore(app)
await setupRouter(app) await setupRouter(app)
app.mount('#app') app.mount('#app')
setupNaiveDiscreteApi()
} }
setupApp() setupApp()

View File

@@ -28,6 +28,7 @@ export async function resetRouter() {
} }
export async function addDynamicRoutes() { export async function addDynamicRoutes() {
// return Promise.reject('123')
const token = getToken() const token = getToken()
// 没有token情况 // 没有token情况
@@ -37,8 +38,8 @@ export async function addDynamicRoutes() {
} }
// 有token的情况 // 有token的情况
const userStore = useUserStore()
try { try {
const userStore = useUserStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
!userStore.userId && (await userStore.getUserInfo()) !userStore.userId && (await userStore.getUserInfo())
const accessRoutes = permissionStore.generateRoutes(userStore.role) const accessRoutes = permissionStore.generateRoutes(userStore.role)
@@ -47,8 +48,25 @@ export async function addDynamicRoutes() {
}) })
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name) router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
router.addRoute(NOT_FOUND_ROUTE) 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) { } catch (error) {
console.error(error) console.error(error)
$message.error('初始化用户信息失败: ' + error)
userStore.logout()
} }
} }

View File

@@ -1,34 +1,28 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useDark } from '@vueuse/core'
const isDark = useDark()
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state() { state() {
return { return {
reloadFlag: true,
collapsed: false, collapsed: false,
/** keepAlive路由的key重新赋值可重置keepAlive */ isDark,
aliveKeys: {},
} }
}, },
actions: { 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() { switchCollapsed() {
this.collapsed = !this.collapsed this.collapsed = !this.collapsed
}, },
setCollapsed(collapsed) { setCollapsed(collapsed) {
this.collapsed = collapsed this.collapsed = collapsed
}, },
setAliveKeys(key, val) { /** 设置暗黑模式 */
this.aliveKeys[key] = val setDark(isDark) {
this.isDark = isDark
},
/** 切换/关闭 暗黑模式 */
toggleDark() {
this.isDark = !this.isDark
}, },
}, },
}) })

View File

@@ -8,8 +8,14 @@ export const useTagsStore = defineStore('tag', {
return { return {
tags: tags || [], tags: tags || [],
activeTag: activeTag || '', activeTag: activeTag || '',
reloading: false,
} }
}, },
getters: {
activeIndex() {
return this.tags.findIndex((item) => item.path === this.activeTag)
},
},
actions: { actions: {
setActiveTag(path) { setActiveTag(path) {
this.activeTag = path this.activeTag = path
@@ -20,20 +26,32 @@ export const useTagsStore = defineStore('tag', {
sStorage.set('tags', tags) sStorage.set('tags', tags)
}, },
addTag(tag = {}) { addTag(tag = {}) {
if (WITHOUT_TAG_PATHS.includes(tag.path)) return
let findItem = this.tags.find((item) => item.path === tag.path)
if (findItem) findItem = tag
else this.setTags([...this.tags, tag])
this.setActiveTag(tag.path) this.setActiveTag(tag.path)
if (WITHOUT_TAG_PATHS.includes(tag.path) || this.tags.some((item) => item.path === tag.path)) return },
this.setTags([...this.tags, tag]) async reloadTag(path, keepAlive) {
const findItem = this.tags.find((item) => item.path === path)
// 更新key可让keepAlive失效
if (findItem && keepAlive) findItem.keepAlive = false
$loadingBar.start()
this.reloading = true
await nextTick()
this.reloading = false
findItem.keepAlive = keepAlive
setTimeout(() => {
document.documentElement.scrollTo({ left: 0, top: 0 })
$loadingBar.finish()
}, 100)
}, },
removeTag(path) { removeTag(path) {
if (path === this.activeTag) {
const activeIndex = this.tags.findIndex((item) => item.path === path)
if (activeIndex > 0) {
router.push(this.tags[activeIndex - 1].path)
} else {
router.push(this.tags[activeIndex + 1].path)
}
}
this.setTags(this.tags.filter((tag) => tag.path !== path)) this.setTags(this.tags.filter((tag) => tag.path !== path))
if (path === this.activeTag) {
router.push(this.tags[this.tags.length - 1].path)
}
}, },
removeOther(curPath = this.activeTag) { removeOther(curPath = this.activeTag) {
this.setTags(this.tags.filter((tag) => tag.path === curPath)) this.setTags(this.tags.filter((tag) => tag.path === curPath))

View File

@@ -5,14 +5,6 @@ body {
overflow: hidden; overflow: hidden;
} }
html {
font-size: 4px; // * 1rem = 4px 方便unocss计算在unocss中 1字体单位 = 0.25rem,相当于 1等份 = 1px
}
body {
font-size: 16px;
}
#app { #app {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -2,7 +2,8 @@ import { router } from '@/router'
export function toLogin() { export function toLogin() {
const currentRoute = unref(router.currentRoute) const currentRoute = unref(router.currentRoute)
const needRedirect = !currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path) const needRedirect =
!currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path)
router.replace({ router.replace({
path: '/login', path: '/login',
query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {}, query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {},

View File

@@ -23,9 +23,11 @@ export async function refreshAccessToken() {
} }
const { time } = tokenItem const { time } = tokenItem
// token生成或者刷新后30分钟内不执行刷新 // token生成或者刷新后30分钟内不执行刷新
if (new Date().getTime() - time <= 1000 * 60 * 30) return if (Date.now() - time <= 1000 * 60 * 30) return
try { try {
const res = await api.refreshToken() const res = await api.refreshToken()
setToken(res.data.token) setToken(res.data.token)
} catch (error) {} } catch (error) {
console.error(error)
}
} }

View File

@@ -74,3 +74,17 @@ export function debounce(method, wait, immediate) {
} }
} }
} }
/**
*
* @param {HTMLElement} el
* @param {Function} cb
* @return {ResizeObserver}
*/
export function useResize(el, cb) {
const observer = new ResizeObserver((entries) => {
cb(entries[0].contentRect)
})
observer.observe(el)
return observer
}

View File

@@ -102,7 +102,7 @@ export function ifNull(val, def = '') {
export function isUrl(path) { export function isUrl(path) {
const reg = const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/ /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path) return reg.test(path)
} }

View File

@@ -1,4 +1,7 @@
import * as NaiveUI from 'naive-ui'
import { isNullOrUndef } from '@/utils' import { isNullOrUndef } from '@/utils'
import { naiveThemeOverrides as themeOverrides } from '~/settings'
import { useAppStore } from '@/store/modules/app'
export function setupMessage(NMessage) { export function setupMessage(NMessage) {
let loadingMessage = null let loadingMessage = null
@@ -77,3 +80,20 @@ export function setupDialog(NDialog) {
return NDialog return NDialog
} }
export function setupNaiveDiscreteApi() {
const appStore = useAppStore()
const configProviderProps = computed(() => ({
theme: appStore.isDark ? NaiveUI.darkTheme : undefined,
themeOverrides,
}))
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{ configProviderProps }
)
window.$loadingBar = loadingBar
window.$notification = notification
window.$message = setupMessage(message)
window.$dialog = setupDialog(dialog)
}

View File

@@ -35,7 +35,7 @@ export function resResolve(response) {
const message = resolveResError(code, data?.message ?? statusText) const message = resolveResError(code, data?.message ?? statusText)
/** 需要错误提醒 */ /** 需要错误提醒 */
!config.noNeedTip && $message.error(message) !config.noNeedTip && window.$message?.error(message)
return Promise.reject({ code, message, error: data || response }) return Promise.reject({ code, message, error: data || response })
} }
return Promise.resolve(data) return Promise.resolve(data)
@@ -46,13 +46,13 @@ export function resReject(error) {
const code = error?.code const code = error?.code
/** 根据code处理对应的操作并返回处理后的message */ /** 根据code处理对应的操作并返回处理后的message */
const message = resolveResError(code, error.message) const message = resolveResError(code, error.message)
$message?.error(message) window.$message?.error(message)
return Promise.reject({ code, message, error }) return Promise.reject({ code, message, error })
} }
const { data, status, config } = error.response const { data, status, config } = error.response
const code = data?.code ?? status const code = data?.code ?? status
const message = resolveResError(code, data?.message ?? error.message) const message = resolveResError(code, data?.message ?? error.message)
/** 需要错误提醒 */ /** 需要错误提醒 */
!config?.noNeedTip && $message.error(message) !config?.noNeedTip && window.$message?.error(message)
return Promise.reject({ code, message, error: error.response?.data || error.response }) return Promise.reject({ code, message, error: error.response?.data || error.response })
} }

View File

@@ -14,7 +14,7 @@ class Storage {
const stringData = JSON.stringify({ const stringData = JSON.stringify({
value, value,
time: Date.now(), time: Date.now(),
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null, expire: !isNullOrUndef(expire) ? Date.now() + expire * 1000 : null,
}) })
this.storage.setItem(this.getKey(key), stringData) this.storage.setItem(this.getKey(key), stringData)
} }
@@ -30,7 +30,7 @@ class Storage {
try { try {
const data = JSON.parse(val) const data = JSON.parse(val)
const { value, time, expire } = data const { value, time, expire } = data
if (isNullOrUndef(expire) || expire > new Date().getTime()) { if (isNullOrUndef(expire) || expire > Date.now()) {
return { value, time } return { value, time }
} }
this.remove(key) this.remove(key)

View File

@@ -4,25 +4,33 @@
<n-card title="按钮 Button"> <n-card title="按钮 Button">
<n-space> <n-space>
<n-button>Default</n-button> <n-button>Default</n-button>
<n-button type="tertiary"> Tertiary </n-button> <n-button type="tertiary">Tertiary</n-button>
<n-button type="primary"> Primary </n-button> <n-button type="primary">Primary</n-button>
<n-button type="info"> Info </n-button> <n-button type="info">Info</n-button>
<n-button type="success"> Success </n-button> <n-button type="success">Success</n-button>
<n-button type="warning"> Warning </n-button> <n-button type="warning">Warning</n-button>
<n-button type="error"> Error </n-button> <n-button type="error">Error</n-button>
</n-space> </n-space>
</n-card> </n-card>
<n-card title="带 Icon 的按钮"> <n-card title="带 Icon 的按钮">
<n-space> <n-space>
<n-button type="info"> <TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新增 </n-button> <n-button type="info">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新增
</n-button>
<n-button type="error"> <n-button type="error">
<TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" /> 删除 <TheIcon icon="material-symbols:delete-outline" :size="18" class="mr-5" />
删除
</n-button> </n-button>
<n-button type="warning"> <n-button type="warning">
<TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" /> 编辑 <TheIcon icon="material-symbols:edit-outline" :size="18" class="mr-5" />
编辑
</n-button>
<n-button type="primary">
<TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" />
查看
</n-button> </n-button>
<n-button type="primary"> <TheIcon icon="majesticons:eye-line" :size="18" class="mr-5" /> 查看 </n-button>
</n-space> </n-space>
</n-card> </n-card>
</n-space> </n-space>
@@ -30,15 +38,18 @@
<n-space size="large" mt-30> <n-space size="large" mt-30>
<n-card min-w-340 title="通知 Notification"> <n-card min-w-340 title="通知 Notification">
<n-space> <n-space>
<n-button @click="notify('info')"> 信息 </n-button> <n-button @click="notify('info')">信息</n-button>
<n-button @click="notify('success')"> 成功 </n-button> <n-button @click="notify('success')">成功</n-button>
<n-button @click="notify('warning')"> 警告 </n-button> <n-button @click="notify('warning')">警告</n-button>
<n-button @click="notify('error')"> 错误 </n-button> <n-button @click="notify('error')">错误</n-button>
</n-space> </n-space>
</n-card> </n-card>
<n-card min-w-340 title="确认弹窗 Dialog"> <n-card min-w-340 title="确认弹窗 Dialog">
<n-button type="error" @click="handleDelete"> <icon-mi:delete mr-5 />删除</n-button> <n-button type="error" @click="handleDelete">
<icon-mi:delete mr-5 />
删除
</n-button>
</n-card> </n-card>
<n-card min-w-340 title="消息提醒 Message"> <n-card min-w-340 title="消息提醒 Message">

View File

@@ -3,7 +3,7 @@
<div w-350> <div w-350>
<n-input v-model:value="inputVal" /> <n-input v-model:value="inputVal" />
<n-input-number v-model:value="number" mt-30 /> <n-input-number v-model:value="number" mt-30 />
<p mt-20 text-center color-gray text-14>右击标签重新加载可重置keep-alive</p> <p mt-20 text-center text-14 color-gray>右击标签重新加载可重置keep-alive</p>
</div> </div>
</CommonPage> </CommonPage>
</template> </template>

View File

@@ -13,7 +13,7 @@ export default {
children: [ children: [
{ {
name: 'BaseComponents', name: 'BaseComponents',
path: 'idnex', path: 'index',
component: () => import('./index.vue'), component: () => import('./index.vue'),
meta: { meta: {
title: '基础组件', title: '基础组件',

View File

@@ -1,7 +1,10 @@
<template> <template>
<CommonPage show-footer> <CommonPage show-footer>
<p> <p>
文档<a hover-decoration-underline c-blue href="https://uno.antfu.me/" target="_blank">https://uno.antfu.me/</a> 文档
<a c-blue hover-decoration-underline href="https://uno.antfu.me/" target="_blank">
https://uno.antfu.me/
</a>
</p> </p>
<p> <p>
playground playground
@@ -10,57 +13,57 @@
</a> </a>
</p> </p>
<div f-c-c flex-col mt-20 w-350> <div mt-20 w-350 f-c-c flex-col>
<div flex flex-wrap justify-around p-10 rounded-10 b-1 bc-ccc> <div flex flex-wrap justify-around rounded-10 p-10 border="1 solid #ccc">
<div w-50 h-50 b-1 rounded-5 f-c-c p-10 m-20> <div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-end></span> <span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-center></span> <span h-6 w-6 self-center rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black self-end></span> <span h-6 w-6 self-end rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex justify-between p-10 m-20> <div m-20 h-50 w-50 flex justify-between rounded-5 p-10 border="1 solid">
<div flex-col justify-between> <div flex-col justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex-col justify-between> <div flex-col justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex-col justify-between items-center p-10 m-20> <div m-20 h-50 w-50 flex-col items-center justify-between rounded-5 p-10 border="1 solid">
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div w-6 h-6 rounded-3 bg-black></div> <div h-6 w-6 rounded-3 bg-black dark:bg-white />
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
<div w-50 h-50 b-1 rounded-5 flex-col justify-between p-10 m-20> <div m-20 h-50 w-50 flex-col justify-between rounded-5 p-10 border="1 solid">
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
<div flex w-full justify-between> <div w-full flex justify-between>
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
<span w-6 h-6 rounded-3 bg-black></span> <span h-6 w-6 rounded-3 bg-black dark:bg-white />
</div> </div>
</div> </div>
</div> </div>
<h2 font-normal text-14 mt-10 color-gray>Flex 骰子</h2> <h2 mt-10 text-14 font-normal color-gray>Flex 骰子</h2>
</div> </div>
</CommonPage> </CommonPage>
</template> </template>

View File

@@ -1,31 +1,40 @@
<template> <template>
<CommonPage :show-header="false" title="32323"> <CommonPage>
<div h-60 pl-20 pr-20 flex items-center bg-white> <div h-60 flex items-center bg-white pl-20 pr-20 dark:bg-dark>
<input <input
v-model="post.title" v-model="post.title"
class="flex-1 pt-15 pb-15 mr-20 text-20 font-bold color-primary" class="mr-20 flex-1 pb-15 pt-15 text-20 font-bold color-primary"
dark:bg-dark
type="text" type="text"
placeholder="输入文章标题..." placeholder="输入文章标题..."
/> />
<n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost"> <n-button type="primary" style="width: 80px" :loading="btnLoading" @click="handleSavePost">
<TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" /> 保存 <TheIcon v-if="!btnLoading" icon="line-md:confirm-circle" class="mr-5" :size="18" />
保存
</n-button> </n-button>
</div> </div>
<MdEditor v-model="post.content" style="height: calc(100vh - 250px)" /> <MdEditor
v-model="post.content"
:theme="appStore.isDark ? 'dark' : 'light'"
style="height: calc(100vh - 305px)"
/>
</CommonPage> </CommonPage>
</template> </template>
<script setup> <script setup>
import MdEditor from 'md-editor-v3' import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css' import 'md-editor-v3/lib/style.css'
import { useAppStore } from '@/store'
defineOptions({ name: 'MDEditor' }) defineOptions({ name: 'MDEditor' })
const appStore = useAppStore()
// refs // refs
let post = ref({}) let post = ref({})
let btnLoading = ref(false) let btnLoading = ref(false)
function handleSavePost(e) { function handleSavePost() {
btnLoading.value = true btnLoading.value = true
$message.loading('正在保存...') $message.loading('正在保存...')
setTimeout(() => { setTimeout(() => {

View File

@@ -0,0 +1,46 @@
<template>
<AppPage>
<div class="h-full flex-col" border="1 solid #ccc" dark:bg-dark>
<WangToolbar
border-b="1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
/>
<WangEditor
v-model="valueHtml"
style="flex: 1; overflow-y: hidden"
:default-config="editorConfig"
mode="default"
@on-created="handleCreated"
/>
</div>
</AppPage>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor as WangEditor, Toolbar as WangToolbar } from '@wangeditor/editor-for-vue'
defineOptions({ name: 'RichTextEditor' })
const editorRef = shallowRef()
const toolbarConfig = { excludeKeys: 'fullScreen' }
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} }
const valueHtml = ref('')
const handleCreated = (editor) => {
editorRef.value = editor
}
</script>
<style>
html.dark {
--w-e-textarea-bg-color: #333;
--w-e-textarea-color: #fff;
--w-e-toolbar-bg-color: #333;
--w-e-toolbar-color: #fff;
--w-e-toolbar-active-bg-color: #666;
--w-e-toolbar-active-color: #fff;
/* ...其他... */
}
</style>

View File

@@ -4,18 +4,18 @@ export default {
name: 'Demo', name: 'Demo',
path: '/demo', path: '/demo',
component: Layout, component: Layout,
redirect: '/demo/crud-table', redirect: '/demo/crud',
meta: { meta: {
title: '示例页面', title: '示例页面',
customIcon: 'logo', icon: 'uil:pagelines',
role: ['admin'], role: ['admin'],
requireAuth: true, requireAuth: true,
order: 3, order: 3,
}, },
children: [ children: [
{ {
name: 'CrudTable', name: 'Crud',
path: 'crud-table', path: 'crud',
component: () => import('./table/index.vue'), component: () => import('./table/index.vue'),
meta: { meta: {
title: 'CRUD表格', title: 'CRUD表格',
@@ -37,5 +37,29 @@ export default {
keepAlive: true, keepAlive: true,
}, },
}, },
{
name: 'RichTextEditor',
path: 'rich-text',
component: () => import('./editor/rich-text.vue'),
meta: {
title: '富文本编辑器',
icon: 'ic:sharp-text-rotation-none',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
{
name: 'Upload',
path: 'upload',
component: () => import('./upload/index.vue'),
meta: {
title: '图片上传',
icon: 'mdi:upload',
role: ['admin'],
requireAuth: true,
keepAlive: true,
},
},
], ],
} }

View File

@@ -1,9 +1,16 @@
<template> <template>
<CommonPage show-footer title="文章"> <CommonPage show-footer title="文章">
<template #action> <template #action>
<n-button type="primary" @click="handleAdd"> <div>
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" /> 新建文章 <n-button type="primary" secondary @click="$table?.handleExport()">
</n-button> <TheIcon icon="mdi:download" :size="18" class="mr-5" />
导出
</n-button>
<n-button type="primary" class="ml-16" @click="handleAdd">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />
新建文章
</n-button>
</div>
</template> </template>
<CrudTable <CrudTable
@@ -14,6 +21,7 @@
:columns="columns" :columns="columns"
:get-data="api.getPosts" :get-data="api.getPosts"
@on-checked="onChecked" @on-checked="onChecked"
@on-data-change="(data) => (tableData = data)"
> >
<template #queryBar> <template #queryBar>
<QueryBarItem label="标题" :label-width="50"> <QueryBarItem label="标题" :label-width="50">
@@ -21,7 +29,7 @@
v-model:value="queryItems.title" v-model:value="queryItems.title"
type="text" type="text"
placeholder="请输入标题" placeholder="请输入标题"
@keydown.enter="$table?.handleSearch" @keypress.enter="$table?.handleSearch"
/> />
</QueryBarItem> </QueryBarItem>
</template> </template>
@@ -86,15 +94,17 @@ import { formatDateTime, renderIcon, isNullOrUndef } from '@/utils'
import { useCRUD } from '@/composables' import { useCRUD } from '@/composables'
import api from './api' import api from './api'
defineOptions({ name: 'CrudTable' }) defineOptions({ name: 'Crud' })
const $table = ref(null) const $table = ref(null)
/** 表格数据,触发搜索的时候会更新这个值 */
const tableData = ref([])
/** QueryBar筛选参数可选 */ /** QueryBar筛选参数可选 */
const queryItems = ref({}) const queryItems = ref({})
/** 补充参数(可选) */ /** 补充参数(可选) */
const extraParams = ref({}) const extraParams = ref({})
onMounted(() => { onActivated(() => {
$table.value?.handleSearch() $table.value?.handleSearch()
}) })
@@ -141,6 +151,7 @@ const columns = [
width: 240, width: 240,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
hideInExcel: true,
render(row) { render(row) {
return [ return [
h( h(
@@ -172,7 +183,10 @@ const columns = [
style: 'margin-left: 15px;', style: 'margin-left: 15px;',
onClick: () => handleDelete(row.id), onClick: () => handleDelete(row.id),
}, },
{ default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) } {
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 14 }),
}
), ),
] ]
}, },

View File

@@ -0,0 +1,84 @@
<template>
<CommonPage>
<n-upload
class="mx-auto w-[75%] p-20 text-center"
:custom-request="handleUpload"
:show-file-list="false"
accept=".png,.jpg,.jpeg"
@before-upload="onBeforeUpload"
>
<n-upload-dragger>
<div class="h-150 f-c-c flex-col">
<TheIcon icon="mdi:upload" :size="68" class="mb-12 c-primary" />
<n-text class="text-14 c-gray">点击或者拖动文件到该区域来上传</n-text>
</div>
</n-upload-dragger>
</n-upload>
<n-card v-if="imgList && imgList.length" class="mt-16 items-center">
<n-image-group>
<n-space justify="space-between" align="center">
<n-card v-for="(item, index) in imgList" :key="index" class="w-280 hover:card-shadow">
<div class="h-160 f-c-c">
<n-image width="200" :src="item.url" />
</div>
<n-space class="mt-16" justify="space-evenly">
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
<n-button dashed type="primary" @click="copy(`![${item.fileName}](${item.url})`)">
MD
</n-button>
<n-button
dashed
type="primary"
@click="copy(`&lt;img src=&quot;${item.url}&quot; /&gt;`)"
>
img
</n-button>
</n-space>
</n-card>
<div v-for="i in 4" :key="i" class="w-280" />
</n-space>
</n-image-group>
</n-card>
</CommonPage>
</template>
<script setup>
import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'Upload' })
const { copy, copied } = useClipboard()
const imgList = reactive([
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
])
watch(copied, (val) => {
val && $message.success('已复制到剪切板')
})
function onBeforeUpload({ file }) {
if (!file.file?.type.startsWith('image/')) {
$message.error('只能上传图片')
return false
}
return true
}
async function handleUpload({ file, onFinish }) {
if (!file || !file.type) {
$message.error('请选择文件')
}
// 模拟上传
$message.loading('上传中...')
setTimeout(() => {
$message.success('上传成功')
imgList.push({ fileName: file.name, url: URL.createObjectURL(file.file) })
onFinish()
}, 1500)
}
</script>

View File

@@ -2,41 +2,65 @@
<AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }"> <AppPage :show-footer="true" bg-cover :style="{ backgroundImage: `url(${bgImg})` }">
<div <div
style="transform: translateY(25px)" style="transform: translateY(25px)"
class="m-auto p-15 f-c-c min-w-345 max-w-700 rounded-10 card-shadow bg-white bg-opacity-60" class="m-auto max-w-700 min-w-345 f-c-c rounded-10 bg-white bg-opacity-60 p-15 card-shadow"
dark:bg-dark
> >
<div w-380 hidden md:block px-20 py-35> <div hidden w-380 px-20 py-35 md:block>
<img src="@/assets/images/login_banner.webp" w-full alt="login_banner" /> <img src="@/assets/images/login_banner.webp" w-full alt="login_banner" />
</div> </div>
<div w-320 flex-col px-20 py-35> <div w-320 flex-col px-20 py-35>
<h5 f-c-c text-24 font-normal color="#6a6a6a"><icon-custom-logo mr-10 text-50 color-primary />{{ title }}</h5> <h5 f-c-c text-24 font-normal color="#6a6a6a">
<div mt-30> <img src="@/assets/images/logo.png" height="50" class="mr-10" />
{{ title }}
</h5>
<div mt-32>
<n-input <n-input
v-model:value="loginInfo.name" v-model:value="loginInfo.name"
autofocus autofocus
class="text-16 items-center h-50 pl-10" class="h-48 items-center text-16"
placeholder="admin" placeholder="name"
:maxlength="20" :maxlength="20"
/> >
<template #prefix>
<icon-material-symbols:account-circle-outline class="mr-8 text-20 opacity-40" />
</template>
</n-input>
</div> </div>
<div mt-30> <div mt-32>
<n-input <n-input
v-model:value="loginInfo.password" v-model:value="loginInfo.password"
class="text-16 items-center h-50 pl-10" class="h-48 items-center text-16"
type="password" type="password"
show-password-on="mousedown" show-password-on="mousedown"
placeholder="123456" placeholder="password"
:maxlength="20" :maxlength="20"
@keydown.enter="handleLogin" @keydown.enter="handleLogin"
>
<template #prefix>
<icon-ri:lock-password-line class="mr-8 text-20 opacity-40" />
</template>
</n-input>
</div>
<div mt-20>
<n-checkbox
:checked="isRemember"
label="记住我"
:on-update:checked="(val) => (isRemember = val)"
/> />
</div> </div>
<div mt-20> <div mt-20>
<n-checkbox :checked="isRemember" label="记住我" :on-update:checked="(val) => (isRemember = val)" /> <n-button
</div> h-50
w-full
<div mt-20> rounded-5
<n-button w-full h-50 rounded-5 text-16 type="primary" :loading="loading" @click="handleLogin"> text-16
type="primary"
:loading="loading"
@click="handleLogin"
>
登录 登录
</n-button> </n-button>
</div> </div>

View File

@@ -0,0 +1,3 @@
<template>
<div>a-1-1</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<div>a-1-2</div>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<CommonPage>
<div>a-1</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<div>a-2-1</div>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<CommonPage>
<div>a-2</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<CommonPage>
<div>a</div>
<div pl-20>
<RouterView />
</div>
</CommonPage>
</template>

View File

@@ -0,0 +1,75 @@
const Layout = () => import('@/layout/index.vue')
export default {
name: 'MultipleMenu',
path: '/multi-menu',
component: Layout,
meta: {
title: '多级菜单',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
order: 4,
},
children: [
{
name: 'a-1',
path: 'multiple-menu',
component: () => import('./a-1/index.vue'),
meta: {
title: 'a-1',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
children: [
{
name: 'a-1-1',
path: 'a-1-1',
component: () => import('./a-1/a-1-1/index.vue'),
meta: {
title: 'a-1-1',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
{
name: 'a-1-2',
path: 'a-1-2',
component: () => import('./a-1/a-1-2/index.vue'),
meta: {
title: 'a-1-2',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
],
},
{
name: 'a-2',
path: 'a-2',
component: () => import('./a-2/index.vue'),
meta: {
title: 'a-2',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
children: [
{
name: 'a-2-1',
path: 'a-2-1',
component: () => import('./a-2/a-2-1/index.vue'),
meta: {
title: 'a-2-1单个子菜单',
icon: 'ic:baseline-menu',
role: ['admin'],
requireAuth: true,
},
},
],
},
],
}

View File

@@ -1,58 +1,201 @@
<template> <template>
<AppPage :show-footer="true"> <AppPage :show-footer="true">
<div flex-1> <div class="flex">
<n-card rounded-10> <n-card class="w-30%">
<div flex items-center> <div class="flex items-center">
<img rounded-full width="60" :src="userStore.avatar" /> <n-avatar round :size="60" :src="userStore.avatar" />
<div ml-20> <div class="ml-20 flex-col">
<p text-16>Hello, {{ userStore.name }}</p> <span class="text-20 opacity-80">Hello, {{ userStore.name }}</span>
<p mt-5 text-12 op-60>今天又是元气满满的一天</p> <span class="mt-4 opacity-50">今日事今日毕</span>
</div>
<div ml-auto flex items-center>
<n-statistic label="待办" :value="4">
<template #suffix> / 10 </template>
</n-statistic>
<n-statistic label="Stars" w-100 ml-80>
<a href="https://github.com/zclzone/vue-naive-admin">
<img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin" />
</a>
</n-statistic>
<n-statistic label="Forks" w-100 ml-80>
<a href="https://github.com/zclzone/vue-naive-admin">
<img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin" />
</a>
</n-statistic>
</div> </div>
</div> </div>
</n-card>
<n-card title="项目" size="small" :segmented="true" mt-15 rounded-10> <p class="mt-20 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功</p>
<p class="mt-12 text-right text-12 opacity-40"> 查尔斯·史考伯</p>
</n-card>
<n-card class="ml-12 w-70%">
<h3 class="text-20 font-normal opacity-90">欢迎使用 Vue Naive Admin</h3>
<p class="mt-8 opacity-60">
这是一款基于 Vue3 + Vite + Pinia + Unocss + Naive UI 的轻量级后台管理模板
</p>
<footer class="mt-24 flex items-center justify-end">
<n-button
tag="a"
href="https://zclzone.github.io/vue-naive-admin-docs"
target="_blank"
type="primary"
ghost
>
开发文档
</n-button>
<n-button
tag="a"
href="https://github.com/zclzone/vue-naive-admin"
target="_blank"
type="primary"
class="ml-12"
>
代码仓库
</n-button>
</footer>
</n-card>
</div>
<div class="mt-12 flex">
<n-card title="项目" segmented>
<template #header-extra> <template #header-extra>
<n-button text type="primary">更多</n-button> <n-button text type="primary">更多</n-button>
</template> </template>
<div flex flex-wrap justify-between> <div class="flex flex-wrap justify-between">
<n-card <n-card
v-for="i in 10" v-for="i in 6"
:key="i" :key="i"
class="w-300 flex-shrink-0 mt-10 mb-10 cursor-pointer"
hover:card-shadow
title="Vue Naive Admin"
size="small" size="small"
class="my-6 w-320 flex-shrink-0 cursor-pointer hover:card-shadow"
title="Vue Naive Admin"
> >
<p op-60>一个基于 Vue3.0ViteNaive UI 的轻量级后台管理模板</p> <p class="op-60">一个基于 Vue3.0ViteNaive UI 的轻量级后台管理模板</p>
</n-card> </n-card>
<div w-300 h-0></div> <div h-0 w-300></div>
<div w-300 h-0></div> <div h-0 w-300></div>
<div w-300 h-0></div> <div h-0 w-300></div>
<div w-300 h-0></div> <div h-0 w-300></div>
</div> </div>
</n-card> </n-card>
<n-card class="ml-12" title="技术栈" segmented>
<VChart :option="skillsOption" class="wh-full" autoresize />
</n-card>
</div> </div>
<n-card class="mt-12" title="趋势" segmented>
<VChart :option="trendOption" class="h-480 w-full" autoresize />
</n-card>
</AppPage> </AppPage>
</template> </template>
<script setup> <script setup>
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import * as echarts from 'echarts/core'
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import VChart from 'vue-echarts'
const userStore = useUserStore() const userStore = useUserStore()
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
LineChart,
CanvasRenderer,
UniversalTransition,
PieChart,
])
const trendOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
},
legend: {
top: '5%',
data: ['star', 'fork'],
},
xAxis: [
{
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
axisPointer: {
type: 'shadow',
},
},
],
yAxis: [
{
type: 'value',
min: 0,
max: 3000,
interval: 500,
axisLabel: {
formatter: '{value}',
},
},
{
type: 'value',
min: 0,
max: 500,
interval: 100,
axisLabel: {
formatter: '{value}',
},
},
],
series: [
{
name: 'star',
type: 'line',
data: [200, 320, 520, 550, 600, 805, 888, 950, 1300, 2503, 2702, 2712],
},
{
name: 'fork',
yAxisIndex: 1,
type: 'bar',
data: [40, 72, 110, 115, 121, 175, 180, 201, 260, 398, 423, 455],
},
],
}
const skillsOption = {
tooltip: {
trigger: 'item',
formatter({ name, value }) {
return `${name} ${value}%`
},
},
legend: {
left: 'center',
},
series: [
{
top: '7%',
type: 'pie',
radius: ['40%', '85%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 36,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 38.5, name: 'Vue' },
{ value: 37.0, name: 'JavaScript' },
{ value: 6.5, name: 'CSS' },
{ value: 6.2, name: 'HTML' },
{ value: 1.8, name: 'Other' },
],
},
],
}
</script> </script>

View File

@@ -1,8 +1,13 @@
import { defineConfig, presetAttributify, presetUno } from 'unocss' import { defineConfig, presetAttributify, presetUno } from 'unocss'
import presetRemToPx from '@unocss/preset-rem-to-px'
export default defineConfig({ export default defineConfig({
exclude: ['node_modules', '.git', '.github', '.husky', '.vscode', 'build', 'dist', 'mock', 'public', './stats.html'], content: {
presets: [presetUno(), presetAttributify()], pipeline: {
include: [/\.(vue|svelte|[jt]sx|mdx?|html)($|\?)/],
},
},
presets: [presetUno(), presetAttributify(), presetRemToPx({ baseFontSize: 4 })],
shortcuts: [ shortcuts: [
['wh-full', 'w-full h-full'], ['wh-full', 'w-full h-full'],
['f-c-c', 'flex justify-center items-center'], ['f-c-c', 'flex justify-center items-center'],
@@ -16,7 +21,10 @@ export default defineConfig({
], ],
rules: [ rules: [
[/^bc-(.+)$/, ([, color]) => ({ 'border-color': `#${color}` })], [/^bc-(.+)$/, ([, color]) => ({ 'border-color': `#${color}` })],
['card-shadow', { 'box-shadow': '0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017' }], [
'card-shadow',
{ 'box-shadow': '0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017' },
],
], ],
theme: { theme: {
colors: { colors: {
@@ -40,6 +48,7 @@ export default defineConfig({
error_hover: 'var(--error-color-hover)', error_hover: 'var(--error-color-hover)',
error_pressed: 'var(--error-color-pressed)', error_pressed: 'var(--error-color-pressed)',
error_active: 'var(--error-color-active)', error_active: 'var(--error-color-active)',
dark: '#18181c',
}, },
}, },
}) })

8
vercel.json Normal file
View File

@@ -0,0 +1,8 @@
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}

View File

@@ -1,9 +1,8 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import { convertEnv, getSrcPath, getRootPath } from './build/utils' import { convertEnv, getSrcPath, getRootPath } from './build/utils'
import { createViteProxy, viteDefine } from './build/config'
import { createVitePlugins } from './build/plugin' import { createVitePlugins } from './build/plugin'
import { OUTPUT_DIR } from './build/constant' import { OUTPUT_DIR, PROXY_CONFIG } from './build/constant'
export default defineConfig(({ command, mode }) => { export default defineConfig(({ command, mode }) => {
const srcPath = getSrcPath() const srcPath = getSrcPath()
@@ -12,7 +11,7 @@ export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) const env = loadEnv(mode, process.cwd())
const viteEnv = convertEnv(env) const viteEnv = convertEnv(env)
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_PROXY_TYPE } = viteEnv const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_BASE_API } = viteEnv
return { return {
base: VITE_PUBLIC_PATH || '/', base: VITE_PUBLIC_PATH || '/',
@@ -22,17 +21,21 @@ export default defineConfig(({ command, mode }) => {
'@': srcPath, '@': srcPath,
}, },
}, },
define: viteDefine,
plugins: createVitePlugins(viteEnv, isBuild), plugins: createVitePlugins(viteEnv, isBuild),
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: VITE_PORT, port: VITE_PORT,
open: false, open: false,
proxy: createViteProxy(VITE_USE_PROXY, VITE_PROXY_TYPE), proxy: VITE_USE_PROXY
? {
[VITE_BASE_API]: PROXY_CONFIG[VITE_BASE_API],
'/api/v2': PROXY_CONFIG['/api/v2'],
}
: undefined,
}, },
build: { build: {
target: 'es2015', target: 'es2015',
outDir: OUTPUT_DIR, outDir: OUTPUT_DIR || 'dist',
reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告 reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告
chunkSizeWarningLimit: 1024, // chunk 大小警告的限制单位kb chunkSizeWarningLimit: 1024, // chunk 大小警告的限制单位kb
}, },