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

87 Commits

Author SHA1 Message Date
张传龙
100b91a118 style: reset.css 2022-09-25 17:14:01 +08:00
张传龙
26b71f0ec6 perf: unocss demo 2022-09-25 17:13:35 +08:00
张传龙
92e7ada37b fix: toLogin 2022-09-24 15:40:34 +08:00
张传龙
2d879d0592 refactor: http interceptors 2022-09-24 15:27:26 +08:00
张传龙
8806a6cb43 fix: keep-alive key 2022-09-24 14:44:59 +08:00
张传龙
85a04fd06d fix: logout 2022-09-21 17:14:41 +08:00
张传龙
4a5b8dd005 fix: fix 2022-09-21 17:13:39 +08:00
张传龙
2f7da255e5 style: naive theme 2022-09-18 20:18:55 +08:00
张传龙
6664ae8f7b refactor: folders 2022-09-18 20:05:40 +08:00
张传龙
bdbe9b8483 refactor: dynamic routes 2022-09-18 15:10:22 +08:00
张传龙
30211e14ea chore: update deps 2022-09-18 15:08:06 +08:00
张传龙
e7b1896d9e chore: update deps 2022-09-15 21:44:58 +08:00
张传龙
a5c1046e67 mod: remove 2022-09-14 09:14:37 +08:00
张传龙
31670cd671 mod: base demo 2022-09-11 17:08:36 +08:00
张传龙
b0e3a94e12 style: remove extra code 2022-09-10 16:00:39 +08:00
张传龙
2b2a324a62 refactor: simplify noNeedToken judge 2022-09-10 15:56:53 +08:00
张传龙
40483e09e6 refactor: keep alive 2022-09-09 09:53:49 +08:00
张传龙
a5a3472486 refactor: setupExtend replace with defineOptions 2022-09-08 15:08:28 +08:00
张传龙
fd1752693a chore: update deps 2022-09-08 15:06:23 +08:00
张传龙
2f3a83758a mod: reload page 2022-09-08 09:00:28 +08:00
张传龙
738212c84b Merge branch 'main' of https://github.com/zclzone/vue-naive-admin 2022-09-07 16:13:50 +08:00
张传龙
a4f3e16007 style: update icon and title 2022-09-07 16:12:38 +08:00
Ronnie Zhang
5b2d1c68dd Merge pull request #26 from haichao0817/main
style: change 'loging' to 'loading'
2022-09-07 14:56:17 +08:00
wukang
7b8b50322c change 'loging' to 'loading' 2022-09-07 11:01:02 +08:00
张传龙
bb171866b6 refactor: routes and file 2022-09-04 12:18:47 +08:00
张传龙
f1bc9edbac refactor: request error tip 2022-09-04 11:35:30 +08:00
张传龙
3a38adc71e fix: mock api error 2022-09-03 22:43:57 +08:00
Ronnie Zhang
b760cc34dd Merge pull request #25 from zclzone/feature/crud-table
Feature/crud table
2022-09-03 22:34:49 +08:00
张传龙
b59e47b5dd feat: finish curd table 2022-09-03 22:28:37 +08:00
张传龙
d1dd58215d wip: crud table 2022-09-03 17:33:20 +08:00
张传龙
661aed1a94 style: add annotation 2022-09-03 17:32:30 +08:00
张传龙
f2e2fc6819 wip: crud table 2022-09-01 14:53:18 +08:00
张传龙
9ea8ffd7fd wip: crud table 2022-08-31 10:16:38 +08:00
张传龙
af983d16b9 wip: commonPage 2022-08-29 10:08:18 +08:00
张传龙
079761b6fd feat: add page components 2022-08-28 19:37:23 +08:00
张传龙
841bab0d63 release: release v1.0.0 2022-08-27 14:51:08 +08:00
张传龙
453148fc8d build: update commitlint 2022-08-27 14:50:34 +08:00
张传龙
7ec078bd7a Revert "mod: gh-pages gzip"
This reverts commit dd0bc3e6e8.
2022-08-27 14:47:18 +08:00
张传龙
dd0bc3e6e8 mod: gh-pages gzip 2022-08-27 14:36:21 +08:00
张传龙
8c665c727b style: format 2022-08-27 14:26:14 +08:00
张传龙
da98aa1c7d docs: update readme 2022-08-27 14:23:12 +08:00
张传龙
51b47ea722 chore: upgrade to vite3 2022-08-27 14:22:09 +08:00
张传龙
220a7800f7 refactor: refactor 2022-08-27 14:09:32 +08:00
张传龙
230e3a72d9 chore: update deps 2022-08-27 12:03:58 +08:00
张传龙
0cefadc2a5 refactor: custom icon 2022-08-27 11:46:34 +08:00
张传龙
2f1b747243 feat: add compress plugin 2022-08-27 11:04:07 +08:00
张传龙
296d5ea6f0 perf: png replace with webp 2022-08-27 10:58:22 +08:00
张传龙
3a415703d4 perf: table demo 2022-08-27 10:36:07 +08:00
张传龙
006f730457 perf: table demo 2022-08-26 22:48:03 +08:00
张传龙
606c5a2df0 docs: update readme 2022-08-25 18:26:29 +08:00
张传龙
30c375cc1d fix(components): fix tags contenxtmenu
ISSUES CLOSED: #23
2022-08-18 10:56:28 +08:00
张传龙
ddf14053da chore: update deps 2022-08-18 09:42:08 +08:00
张传龙
38edbcb68a mod: update mock data 2022-08-18 09:39:09 +08:00
Ronnie Zhang
3e54a82abb Merge pull request #22 from amplest/main
fix: get请求无法接收参数
2022-08-12 22:59:43 +08:00
Xiongxing
df6225a752 fix: get请求无法接收参数 2022-08-12 21:10:39 +08:00
张传龙
63c1f2f132 refactor: routes sort 2022-08-08 15:44:16 +08:00
张传龙
0bb2a904e7 refactor: adjust routes 2022-08-08 15:36:43 +08:00
张传龙
ef3aaa5be5 refactor: refactor async routes 2022-08-07 22:25:28 +08:00
张传龙
869a68812c chore: adjust commitlink config 2022-08-04 18:04:08 +08:00
张传龙
fd0032e0e9 docs: update readme 2022-08-02 09:34:25 +08:00
张传龙
b53d7daaa1 docs: update readme 2022-08-02 09:31:17 +08:00
张传龙
856bdfd0ee docs: update readme 2022-07-31 18:08:13 +08:00
张传龙
9f9884759c refactor: simplify permission-guard 2022-07-30 22:11:53 +08:00
张传龙
7dad43d003 docs: update readme 2022-07-29 16:48:58 +08:00
张传龙
7762e02b31 refactor: refactor api usage 2022-07-25 18:36:22 +08:00
张传龙
e5768fa1e3 style: modify login page 2022-07-23 22:17:23 +08:00
张传龙
7ee613d8cf style: modify footer 2022-07-23 22:10:01 +08:00
张传龙
80a5b7f053 style: modify custom scrollbar 2022-07-21 17:47:11 +08:00
张传龙
eb160731da feat: login page compatible mobile 2022-07-20 18:26:38 +08:00
张传龙
789231a7f4 style: format 2022-07-20 09:13:07 +08:00
张传龙
6ea6e1c267 chore: update settings.json 2022-07-20 09:10:40 +08:00
张传龙
d971e7e4ba fix: fix incorrent judgment of isHash 2022-07-19 16:29:07 +08:00
张传龙
215998dc66 perf: optimize login page 2022-07-17 20:37:44 +08:00
张传龙
40f9ac1a6b fix: fix incorrent usage of vue router 2022-07-17 14:54:45 +08:00
张传龙
6ec5588ed4 chore: setup lint-staged 2022-07-15 14:48:40 +08:00
张传龙
380e5768c4 chore: update settings.json 2022-07-15 14:06:57 +08:00
张传龙
5856f601fa style: use unocss rewrite css 2022-07-14 18:05:47 +08:00
张传龙
d10b8f0e96 chore(prettier): update prettier config 2022-07-14 18:04:00 +08:00
张传龙
3860cf9ebb style: simplify unocss test page 2022-07-14 16:40:25 +08:00
张传龙
94b46d9bf6 chore(unocss): update unocss config 2022-07-14 16:39:24 +08:00
张传龙
4df7d44bf1 docs: modify annotation 2022-07-13 22:58:09 +08:00
张传龙
42b8aca37b docs(readme): update readme 2022-07-11 16:07:07 +08:00
张传龙
0c96d0e937 docs(readme): update readme 2022-07-11 12:29:48 +08:00
张传龙
b540f5599f fix: fix incorrect text 2022-07-11 12:28:33 +08:00
张传龙
18b8a81640 fix(other): disabled unplugin generate .d.ts 2022-07-10 22:24:23 +08:00
张传龙
06b3afc2de build(deps): update unplugin deps 2022-07-10 22:20:21 +08:00
92376
3088773ebe fix: modify exit full screen icon
* !1 退出全屏 icon
* 退出全屏 icon
2022-07-10 12:55:45 +00:00
134 changed files with 4142 additions and 1813 deletions

View File

@@ -11,9 +11,12 @@ module.exports = {
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' }, { value: 'ci', name:'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' }, { value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name:'revert: 回滚 commit' }, { value: 'revert', name:'revert: 回滚 commit' },
{ value: 'wip', name:'wip: 开发中' },
{ value: 'mod', name:'mod: 不确定分类的修改' },
{ value: 'release', name:'release: 发布' },
], ],
scopes: [ scopes: [
['custom', '自定义3'], ['custom', '自定义'],
['projects', '项目搭建'], ['projects', '项目搭建'],
['components', '组件相关'], ['components', '组件相关'],
['utils', 'utils 相关'], ['utils', 'utils 相关'],

2
.env
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

29
.vscode/settings.json vendored
View File

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

131
README.md
View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { viteMockServe } from 'vite-plugin-mock'
export function configMockPlugin(isBuild) { export function configMockPlugin(isBuild) {
return viteMockServe({ return viteMockServe({
mockPath: 'mock/modules', mockPath: 'mock/api',
localEnabled: !isBuild, localEnabled: !isBuild,
prodEnabled: isBuild, prodEnabled: isBuild,
injectCode: ` injectCode: `

View File

@@ -1,3 +1,5 @@
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'
@@ -10,13 +12,17 @@ import IconsResolver from 'unplugin-icons/resolver'
* 图标库: https://icones.js.org/ * 图标库: https://icones.js.org/
*/ */
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getRootPath } from '../utils' import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
const customIconPath = getRootPath('src', 'assets/icons')
export default [ export default [
DefineOptions(),
AutoImport({ AutoImport({
imports: ['vue', 'vue-router'], imports: ['vue', 'vue-router'],
dts: false,
}), }),
Icons({ Icons({
compiler: 'vue3', compiler: 'vue3',
@@ -28,5 +34,12 @@ export default [
}), }),
Components({ Components({
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })], resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
dts: false,
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}), }),
] ]

View File

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

View File

@@ -2,53 +2,38 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import dotenv from 'dotenv' import dotenv from 'dotenv'
const httpsReg = /^https:\/\// /**
* * 项目根路径
export function wrapperEnv(envOptions) { * @descrition 结尾不带/
if (!envOptions) return {} */
const ret = {} export function getRootPath() {
return path.resolve(process.cwd())
for (const key in envOptions) {
let val = envOptions[key]
if (['true', 'false'].includes(val)) {
val = val === 'true'
}
if (['VITE_PORT'].includes(key)) {
val = +val
}
if (key === 'VITE_PROXY' && val && typeof val === 'string') {
try {
val = JSON.parse(val.replace(/'/g, '"'))
} catch (error) {
val = ''
}
}
ret[key] = val
if (typeof val === 'string') {
process.env[key] = val
} else if (typeof val === 'object') {
process.env[key] = JSON.stringify(val)
}
}
return ret
} }
export function createProxy(list = []) { /**
const ret = {} * * 项目src路径
for (const [prefix, target] of list) { * @param srcName src目录名称(默认: "src")
const isHttps = httpsReg.test(target) * @descrition 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
}
// https://github.com/http-party/node-http-proxy#options const httpsReg = /^https:\/\//
ret[prefix] = {
target: target, export function convertEnv(envOptions) {
changeOrigin: true, const result = {}
ws: true, if (!envOptions) return result
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false for (const envKey in envOptions) {
...(isHttps ? { secure: false } : {}), let envVal = envOptions[envKey]
} if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
result[envKey] = envVal
} }
return ret return result
} }
/** /**
@@ -65,7 +50,7 @@ function getConfFiles() {
return ['.env', '.env.local', '.env.production'] return ['.env', '.env.local', '.env.production']
} }
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) { export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
let envConfig = {} let envConfig = {}
confFiles.forEach((item) => { confFiles.forEach((item) => {
try { try {
@@ -85,7 +70,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
}) })
return envConfig return envConfig
} }
export function getRootPath(...dir) {
return path.resolve(process.cwd(), ...dir)
}

View File

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

View File

@@ -15,7 +15,6 @@
<body> <body>
<div id="app"> <div id="app">
<!-- 白屏时的loading效果 --> <!-- 白屏时的loading效果 -->
<div class="loading-container"> <div class="loading-container">
<div id="loadingLogo" class="loading-svg"></div> <div id="loadingLogo" class="loading-svg"></div>
@@ -30,7 +29,6 @@
<div class="loading-title"><%= title %></div> <div class="loading-title"><%= title %></div>
</div> </div>
<script src="/resource/loading.js"></script> <script src="/resource/loading.js"></script>
</div> </div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

View File

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

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

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

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

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

View File

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

View File

@@ -1,87 +0,0 @@
export default [
{
url: '/api/posts',
method: 'get',
response: () => {
return {
code: 0,
message: 'ok',
data: [
{
id: 36,
title: '使用纯css优雅配置移动端rem布局',
author: 'Ronnie',
category: '移动端,Css',
description: '通常配置rem布局会使用js进行处理比如750的设计稿会这样...',
content: '通常配置rem布局会使用js进行处理比如750的设计稿会这样',
isRecommend: true,
isPublish: true,
createDate: '2021-11-04T04:03:36.000Z',
updateDate: '2021-11-04T04:03:36.000Z',
},
{
id: 35,
title: 'Vue2&Vue3项目风格指南',
author: 'Ronnie',
category: 'Vue',
description: '总结的Vue2和Vue3的项目风格',
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
isRecommend: true,
isPublish: true,
createDate: '2021-10-25T08:57:47.000Z',
updateDate: '2022-02-28T04:02:39.000Z',
},
{
id: 28,
title: '如何优雅的给图片添加水印',
author: '张传龙',
category: 'JavaScript',
description: '优雅的给图片添加水印',
content: '我之前写过一篇文章记录了一次上传图片的优化史',
isRecommend: true,
isPublish: true,
createDate: '2021-06-24T18:46:19.000Z',
updateDate: '2021-09-23T07:51:22.000Z',
},
{
id: 26,
title: '前端缓存的理解',
author: '张传龙',
category: 'Http',
description: '谈谈前端缓存的理解',
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true,
isPublish: true,
createDate: '2021-06-10T18:51:19.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
id: 24,
title: '使用jQuery的load方法帮女朋友实现套娃Html',
author: '张传龙',
category: 'JavaScript',
description: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
content: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
isRecommend: true,
isPublish: true,
createDate: '2021-05-26T15:26:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
id: 18,
title: 'Promise的五个静态方法',
author: '张传龙',
category: 'JavaScript',
description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景',
content: '## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true,
isPublish: true,
createDate: '2021-02-22T22:37:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
],
}
},
},
]

View File

@@ -1,10 +1,11 @@
{ {
"name": "vue-naive-admin", "name": "vue-naive-admin",
"version": "0.4.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .js,.vue .", "lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .", "lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged",
"build": "vite build", "build": "vite build",
"build:test": "vite build --mode test", "build:test": "vite build --mode test",
"build:github": "vite build --mode github && esno ./build/script", "build:github": "vite build --mode github && esno ./build/script",
@@ -16,16 +17,17 @@
"@vueuse/core": "^8.4.2", "@vueuse/core": "^8.4.2",
"axios": "^0.21.4", "axios": "^0.21.4",
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
"lodash-es": "^4.17.21",
"md-editor-v3": "^1.11.4", "md-editor-v3": "^1.11.4",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"pinia": "^2.0.13", "pinia": "^2.0.13",
"vue": "^3.2.31", "vue": "^3.2.39",
"vue-router": "^4.0.15" "vue-router": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.3", "@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3", "@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.63", "@iconify/json": "^2.1.99",
"@iconify/vue": "^3.2.1", "@iconify/vue": "^3.2.1",
"@vitejs/plugin-vue": "^1.10.2", "@vitejs/plugin-vue": "^1.10.2",
"@vue/compiler-sfc": "^3.2.31", "@vue/compiler-sfc": "^3.2.31",
@@ -41,22 +43,30 @@
"esno": "^0.13.0", "esno": "^0.13.0",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"husky": "^8.0.1", "husky": "^8.0.1",
"naive-ui": "^2.30.3", "lint-staged": "^13.0.3",
"naive-ui": "^2.33.3",
"prettier": "^2.6.1", "prettier": "^2.6.1",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.49.10", "sass": "^1.49.10",
"unocss": "^0.43.2", "unocss": "^0.43.2",
"unplugin-auto-import": "^0.8.8", "unplugin-auto-import": "^0.9.2",
"unplugin-icons": "^0.14.1", "unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.17.21", "unplugin-vue-components": "^0.17.21",
"vite": "^2.9.9", "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-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-vue-setup-extend-plus": "^0.1.0" "vite-plugin-svg-icons": "^2.0.1"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "node_modules/cz-customizable" "path": "node_modules/cz-customizable"
} }
},
"lint-staged": {
"*.{js,vue}": [
"eslint --ext .js,.vue ."
]
} }
} }

2196
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,4 @@ module.exports = {
singleQuote: true, singleQuote: true,
semi: false, semi: false,
endOfLine: 'lf', endOfLine: 'lf',
bracketSameLine: true,
} }

View File

@@ -13,7 +13,7 @@
.loading-svg { .loading-svg {
width: 128px; width: 128px;
height: 128px; height: 128px;
color: var(--primaryColor); color: var(--primary-color);
} }
.loading-spin__container { .loading-spin__container {
@@ -45,7 +45,7 @@
position: absolute; position: absolute;
height: 16px; height: 16px;
width: 16px; width: 16px;
background-color: var(--primaryColor); background-color: var(--primary-color);
border-radius: 8px; border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;

View File

@@ -2,24 +2,24 @@
* 初始化加载效果的svg格式logo * 初始化加载效果的svg格式logo
* @param {string} id - 元素id * @param {string} id - 元素id
*/ */
function initSvgLogo(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 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 appEl = document.querySelector(id)
const div = document.createElement('div'); const div = document.createElement('div')
div.innerHTML = svgStr; div.innerHTML = svgStr
if (appEl) { if (appEl) {
appEl.appendChild(div); appEl.appendChild(div)
} }
} }
function addThemeColorCssVars() { function addThemeColorCssVars() {
const key = '__THEME_COLOR__' const key = '__THEME_COLOR__'
const defaultColor = '#316c72'; const defaultColor = '#316c72'
const themeColor = window.localStorage.getItem(key) || defaultColor; const themeColor = window.localStorage.getItem(key) || defaultColor
const cssVars = `--primaryColor: ${themeColor}`; const cssVars = `--primary-color: ${themeColor}`
document.documentElement.style.cssText = cssVars; document.documentElement.style.cssText = cssVars
} }
addThemeColorCssVars(); addThemeColorCssVars()
initSvgLogo('#loadingLogo'); initSvgLogo('#loadingLogo')

2
settings/index.js Normal file
View File

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

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

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

37
settings/theme.json Normal file
View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

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

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

After

Width:  |  Height:  |  Size: 811 B

View File

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

View File

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

View File

@@ -15,7 +15,8 @@
:class="{ overflow: isOverflow && showArrow }" :class="{ overflow: isOverflow && showArrow }"
:style="{ :style="{
transform: `translateX(${translateX}px)`, transform: `translateX(${translateX}px)`,
}"> }"
>
<slot /> <slot />
</div> </div>
</div> </div>
@@ -37,16 +38,16 @@ const wrapper = ref(null)
const isOverflow = ref(false) const isOverflow = ref(false)
const refreshIsOverflow = debounce(() => { const refreshIsOverflow = debounce(() => {
const wrapperWidth = wrapper.value.offsetWidth const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value.offsetWidth const contentWidth = content.value?.offsetWidth
isOverflow.value = contentWidth > wrapperWidth isOverflow.value = contentWidth > wrapperWidth
resetTranslateX(wrapperWidth, contentWidth) resetTranslateX(wrapperWidth, contentWidth)
}, 200) }, 200)
function handleMouseWheel(e) { function handleMouseWheel(e) {
const { wheelDelta } = e const { wheelDelta } = e
const wrapperWidth = wrapper.value.offsetWidth const wrapperWidth = wrapper.value?.offsetWidth
const contentWidth = content.value.offsetWidth const contentWidth = content.value?.offsetWidth
/** /**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移 * @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值 * @translateX 内容translateX的值

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
<template> <template>
<header class="header"> <div flex items-center>
<div class="h-left"> <MenuCollapse />
<MenuCollapse /> <BreadCrumb ml-15 />
<BreadCrumb ml-15 /> </div>
</div> <div ml-auto flex items-center>
<div class="h-right"> <GithubSite />
<GithubSite /> <FullScreen />
<FullScreen /> <UserAvatar />
<UserAvatar /> </div>
</div>
</header>
</template> </template>
<script setup> <script setup>
@@ -19,21 +17,3 @@ 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'
</script> </script>
<style lang="scss" scoped>
.header {
padding: 0 15px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.h-left {
display: flex;
align-items: center;
}
.h-right {
display: flex;
align-items: center;
}
}
</style>

View File

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

View File

@@ -6,24 +6,22 @@
:collapsed-icon-size="22" :collapsed-icon-size="22"
:collapsed-width="64" :collapsed-width="64"
:options="menuOptions" :options="menuOptions"
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name" :value="curRoute.meta?.activeMenu || curRoute.name"
@update:value="handleMenuSelect" /> @update:value="handleMenuSelect"
/>
</template> </template>
<script setup> <script setup>
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore, useAppStore } from '@/store'
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
import { isExternal } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { renderIcon } from '@/utils/icon'
const router = useRouter() const router = useRouter()
const curRoute = useRoute()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const appStore = useAppStore() const appStore = useAppStore()
const { currentRoute } = router
const menuOptions = computed(() => { const menuOptions = computed(() => {
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.index - b.index) return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
}) })
function resolvePath(basePath, path) { function resolvePath(basePath, path) {
@@ -42,8 +40,8 @@ function getMenuItem(route, basePath = '') {
label: (route.meta && route.meta.title) || route.name, label: (route.meta && route.meta.title) || route.name,
key: route.name, key: route.name,
path: resolvePath(basePath, route.path), path: resolvePath(basePath, route.path),
icon: route.meta?.icon ? renderIcon(route.meta?.icon, { size: 16 }) : renderIcon('mdi:circle-outline', { size: 8 }), icon: getIcon(route.meta),
index: route.meta?.index || 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) : []
@@ -57,32 +55,36 @@ function getMenuItem(route, basePath = '') {
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: singleRoute.meta?.icon icon: getIcon(singleRoute.meta),
? renderIcon(singleRoute.meta?.icon, { size: 16 }) order: menuItem.order,
: renderIcon('mdi:circle-outline', { size: 8 }),
index: menuItem.index,
} }
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.index - b.index) 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.index - b.index) .sort((a, b) => a.order - b.order)
} }
return menuItem return menuItem
} }
function getIcon(meta) {
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
return null
}
function handleMenuSelect(key, item) { 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 === currentRoute.value.path && !currentRoute.value.meta?.keepAlive) { if (item.path === curRoute.path) {
appStore.reloadPage() appStore.reloadPage()
} else { } else {
router.push(item.path) router.push(item.path)
@@ -101,7 +103,7 @@ function handleMenuSelect(key, item) {
&.n-menu-item-content--selected, &.n-menu-item-content--selected,
&:hover { &:hover {
&::before { &::before {
border-left: 4px solid var(--primaryColor); border-left: 4px solid var(--primary-color);
} }
} }
} }

View File

@@ -1,18 +1,19 @@
<template> <template>
<n-dropdown <n-dropdown
:show="dropdownShow" :show="show"
:options="options" :options="options"
:x="x" :x="x"
:y="y" :y="y"
placement="bottom-start" placement="bottom-start"
@clickoutside="handleHideDropdown" @clickoutside="handleHideDropdown"
@select="handleSelect" /> @select="handleSelect"
/>
</template> </template>
<script setup> <script setup>
import { useTagsStore } from '@/store/modules/tags' import { useTagsStore, useAppStore } from '@/store'
import { renderIcon } from '@/utils/icon' import { renderIcon } from '@/utils'
import { useAppStore } from '@/store/modules/app' import { useLocalStorage } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -71,19 +72,15 @@ const options = computed(() => [
}, },
]) ])
const dropdownShow = computed({ const route = useRoute()
get() {
return props.show
},
set(show) {
emit('update:show', show)
},
})
const actionMap = new Map([ const actionMap = new Map([
[ [
'reload', 'reload',
() => { () => {
if (route.meta?.keepAlive) {
// 重置keepAlive
appStore.setAliveKeys(route.name, +new Date())
}
appStore.reloadPage() appStore.reloadPage()
}, },
], ],
@@ -114,7 +111,7 @@ const actionMap = new Map([
]) ])
function handleHideDropdown() { function handleHideDropdown() {
dropdownShow.value = false emit('update:show', false)
} }
function handleSelect(key) { function handleSelect(key) {

View File

@@ -1,34 +1,35 @@
<template> <template>
<ScrollX :class="`h-${useTheme.tags.height}`"> <ScrollX>
<n-tag <n-tag
v-for="tag in tagsStore.tags" v-for="tag in tagsStore.tags"
:key="tag.path" :key="tag.path"
class="px-15 mx-5 rounded-4 cursor-pointer 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)"
>
{{ tag.title }} {{ tag.title }}
</n-tag> </n-tag>
<ContextMenu
v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</ScrollX> </ScrollX>
<ContextMenu
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y" />
</template> </template>
<script setup name="Tags"> <script setup>
import ContextMenu from './ContextMenu.vue' import ContextMenu from './ContextMenu.vue'
import { useTagsStore } from '@/store/modules/tags' import { useTagsStore } from '@/store'
import { useThemeStore } from '@/store/modules/theme'
import ScrollX from '@/components/common/ScrollX.vue' 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 useTheme = useThemeStore()
const contextMenuOption = reactive({ const contextMenuOption = reactive({
show: false, show: false,
@@ -72,25 +73,14 @@ async function handleContextMenu(e, tagItem) {
} }
</script> </script>
<style lang="scss"> <style>
.n-tag { .n-tag__close {
padding: 0 15px; box-sizing: content-box;
margin: 0 5px; border-radius: 50%;
cursor: pointer; font-size: 12px;
.n-tag__close { padding: 2px;
margin-left: 5px; transform: scale(0.9);
box-sizing: content-box; transform: translateX(5px);
font-size: 12px; transition: all 0.3s;
padding: 2px;
border-radius: 50%;
transition: all 0.7s;
&:hover {
color: #fff;
background-color: var(--primaryColor);
}
}
&:hover {
color: var(--primaryColor);
}
} }
</style> </style>

View File

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

View File

@@ -1,19 +1,20 @@
/** 重置样式 */
import '@/styles/reset.css' import '@/styles/reset.css'
import '@/styles/variables.css'
import '@/styles/index.scss'
import 'uno.css' import 'uno.css'
import '@/styles/global.scss'
import 'virtual:svg-icons-register'
import { createApp } from 'vue' 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'
function setupApp() { async function setupApp() {
const app = createApp(App) const app = createApp(App)
setupStore(app) setupStore(app)
setupRouter(app) await setupRouter(app)
app.mount('#app') app.mount('#app')
} }

View File

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

View File

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

View File

@@ -1,28 +1,65 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router' import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { setupRouterGuard } from './guard' import { setupRouterGuard } from './guard'
import { basicRoutes } from './routes' import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
import { getToken, isNullOrWhitespace } from '@/utils'
import { useUserStore, usePermissionStore } from '@/store'
const isHash = !!import.meta.env.VITE_APP_USE_HASH const isHash = import.meta.env.VITE_USE_HASH === 'true'
export const router = createRouter({ export const router = createRouter({
history: isHash ? createWebHashHistory('/') : createWebHistory('/'), history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
routes: [], routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}) })
export function resetRouter() { export async function setupRouter(app) {
await addDynamicRoutes()
setupRouterGuard(router)
app.use(router)
}
export async function resetRouter() {
const basicRouteNames = getRouteNames(basicRoutes)
router.getRoutes().forEach((route) => { router.getRoutes().forEach((route) => {
const { name } = route const name = route.name
router.hasRoute(name) && router.removeRoute(name) if (!basicRouteNames.includes(name)) {
}) router.removeRoute(name)
basicRoutes.forEach((route) => { }
!router.hasRoute(route.name) && router.addRoute(route)
}) })
} }
export function setupRouter(app) { export async function addDynamicRoutes() {
basicRoutes.forEach((route) => { const token = getToken()
!router.hasRoute(route.name) && router.addRoute(route)
}) // 没有token情况
app.use(router) if (isNullOrWhitespace(token)) {
setupRouterGuard(router) router.addRoute(EMPTY_ROUTE)
return
}
// 有token的情况
try {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
!userStore.userId && (await userStore.getUserInfo())
const accessRoutes = permissionStore.generateRoutes(userStore.role)
accessRoutes.forEach((route) => {
!router.hasRoute(route.name) && router.addRoute(route)
})
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
router.addRoute(NOT_FOUND_ROUTE)
} catch (error) {
console.error(error)
}
}
export function getRouteNames(routes) {
return routes.map((route) => getRouteName(route)).flat(1)
}
function getRouteName(route) {
const names = [route.name]
if (route.children && route.children.length) {
names.push(...route.children.map((item) => getRouteName(item)).flat(1))
}
return names
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,8 @@ export const useAppStore = defineStore('app', {
return { return {
reloadFlag: true, reloadFlag: true,
collapsed: false, collapsed: false,
/** keepAlive路由的key重新赋值可重置keepAlive */
aliveKeys: {},
} }
}, },
actions: { actions: {
@@ -25,5 +27,8 @@ export const useAppStore = defineStore('app', {
setCollapsed(collapsed) { setCollapsed(collapsed) {
this.collapsed = collapsed this.collapsed = collapsed
}, },
setAliveKeys(key, val) {
this.aliveKeys[key] = val
},
}, },
}) })

View File

@@ -0,0 +1,4 @@
export * from './app'
export * from './permission'
export * from './tags'
export * from './user'

View File

@@ -2,10 +2,15 @@ import { defineStore } from 'pinia'
import { asyncRoutes, basicRoutes } from '@/router/routes' import { asyncRoutes, basicRoutes } from '@/router/routes'
function hasPermission(route, role) { function hasPermission(route, role) {
// * 不需要权限直接返回true
if (!route.meta?.requireAuth) return true
const routeRole = route.meta?.role ? route.meta.role : [] const routeRole = route.meta?.role ? route.meta.role : []
if (!role.length || !routeRole.length) {
return false // * 登录用户没有角色或者路由没有设置角色判定为没有权限
} if (!role.length || !routeRole.length) return false
// * 路由指定的角色包含任一登录用户角色则判定有权限
return role.some((item) => routeRole.includes(item)) return role.some((item) => routeRole.includes(item))
} }
@@ -48,5 +53,8 @@ export const usePermissionStore = defineStore('permission', {
this.accessRoutes = accessRoutes this.accessRoutes = accessRoutes
return accessRoutes return accessRoutes
}, },
resetPermission() {
this.$reset()
},
}, },
}) })

View File

@@ -1,6 +1,6 @@
import { sStorage } from '@/utils/cache' import { sStorage } from '@/utils'
export const activeTag = sStorage.get('activeTag') export const activeTag = sStorage.get('activeTag')
export const tags = sStorage.get('tags') export const tags = sStorage.get('tags')
export const WITHOUT_TAG_PATHS = ['/404', '/login', '/redirect'] export const WITHOUT_TAG_PATHS = ['/404', '/login']

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers' import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
import { router } from '@/router' import { router } from '@/router'
import { sStorage } from '@/utils/cache' import { sStorage } from '@/utils'
export const useTagsStore = defineStore('tag', { export const useTagsStore = defineStore('tag', {
state() { state() {
@@ -57,5 +57,9 @@ export const useTagsStore = defineStore('tag', {
router.push(filterTags[filterTags.length - 1].path) router.push(filterTags[filterTags.length - 1].path)
} }
}, },
resetTags() {
this.setTags([])
this.setActiveTag('')
},
}, },
}) })

View File

@@ -1,23 +0,0 @@
import { defineStore } from 'pinia'
import { themeSettings } from '@/settings'
export const useThemeStore = defineStore('theme', {
state() {
return {
tags: themeSettings.tag || { visible: true, height: 50 },
header: themeSettings.header || { height: 60 },
naiveThemeOverrides: themeSettings.naiveThemeOverrides || {
common: {
primaryColor: '#316C72FF',
primaryColorHover: '#316C72E3',
primaryColorPressed: '#2B4C59FF',
primaryColorSuppl: '#316C7263',
},
},
}
},
actions: {
setTabVisible(visible) {
this.tags.visible = visible
},
},
})

View File

@@ -1,7 +1,8 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUser } from '@/api/user' import { resetRouter } from '@/router'
import { removeToken } from '@/utils/token' import { useTagsStore, usePermissionStore } from '@/store'
import { toLogin } from '@/utils/auth' import { removeToken, toLogin } from '@/utils'
import api from '@/api'
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
state() { state() {
@@ -26,21 +27,22 @@ export const useUserStore = defineStore('user', {
actions: { actions: {
async getUserInfo() { async getUserInfo() {
try { try {
const res = await getUser() const res = await api.getUser()
if (res.code === 0) { const { id, name, avatar, role } = res.data
const { id, name, avatar, role } = res.data this.userInfo = { id, name, avatar, role }
this.userInfo = { id, name, avatar, role } return Promise.resolve(res.data)
return Promise.resolve(res.data)
} else {
return Promise.reject(res)
}
} catch (error) { } catch (error) {
return Promise.reject(error) return Promise.reject(error)
} }
}, },
async logout() { async logout() {
const { resetTags } = useTagsStore()
const { resetPermission } = usePermissionStore()
removeToken() removeToken()
this.userInfo = {} resetTags()
resetPermission()
resetRouter()
this.$reset()
toLogin() toLogin()
}, },
setUserInfo(userInfo = {}) { setUserInfo(userInfo = {}) {

View File

@@ -1,17 +1,24 @@
html {
font-size: 4px; // * 1rem = 4px 方便unocss计算在unocss中 1字体单位 = 0.25rem相当于 1等份 = 1px
}
html, html,
body { body {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background-color: #f2f2f2;
font-family: 'Encode Sans Condensed', sans-serif;
} }
/* router view transition fade-slide */ html {
font-size: 4px; // * 1rem = 4px 方便unocss计算在unocss中 1字体单位 = 0.25rem相当于 1等份 = 1px
}
body {
font-size: 16px;
}
#app {
width: 100%;
height: 100%;
}
/* transition fade-slide */
.fade-slide-leave-active, .fade-slide-leave-active,
.fade-slide-enter-active { .fade-slide-enter-active {
transition: all 0.3s; transition: all 0.3s;
@@ -28,27 +35,40 @@ body {
} }
/* 自定义滚动条样式 */ /* 自定义滚动条样式 */
.cur-scroll { .cus-scroll {
&::-webkit-scrollbar{ overflow: auto;
width: 6px; &::-webkit-scrollbar {
height: 6px; width: 8px;
height: 8px;
} }
&::-webkit-scrollbar-thumb{ }
.cus-scroll-x {
overflow-x: auto;
&::-webkit-scrollbar {
width: 0;
height: 8px;
}
}
.cus-scroll-y {
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
height: 0;
}
}
.cus-scroll,
.cus-scroll-x,
.cus-scroll-y {
&::-webkit-scrollbar-thumb {
background-color: transparent; background-color: transparent;
border-radius: 3px; border-radius: 4px;
}
&::-webkit-scrollbar-corner{
background: #f6f6f6;
} }
&:hover { &:hover {
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #bfbfbf; background: #bfbfbf;
} }
&::-webkit-scrollbar-thumb:hover{ &::-webkit-scrollbar-thumb:hover {
background: #999999; background: var(--primary-color);
} }
} }
} }

View File

@@ -12,7 +12,7 @@ html {
a { a {
text-decoration: none; text-decoration: none;
color: #333; color: inherit;
} }
a:hover, a:hover,
@@ -33,8 +33,3 @@ textarea {
border: none; border: none;
resize: none; resize: none;
} }
body {
font-size: 14px;
font-weight: 400;
}

View File

@@ -1,3 +0,0 @@
:root {
--primaryColor: #316c72;
}

View File

@@ -1,8 +0,0 @@
import { router } from '@/router'
export function toLogin() {
router.replace({
path: '/login',
query: { ...router.currentRoute.query, redirect: router.currentRoute.path },
})
}

16
src/utils/auth/auth.js Normal file
View File

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

2
src/utils/auth/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './auth'
export * from './token'

View File

@@ -1,5 +1,5 @@
import { lStorage } from './cache' import { lStorage } from '@/utils'
import { refreshToken } from '@/api/auth' import api from '@/api'
const TOKEN_CODE = 'access_token' const TOKEN_CODE = 'access_token'
const DURATION = 6 * 60 * 60 const DURATION = 6 * 60 * 60
@@ -25,9 +25,7 @@ export async function refreshAccessToken() {
// token生成或者刷新后30分钟内不执行刷新 // token生成或者刷新后30分钟内不执行刷新
if (new Date().getTime() - time <= 1000 * 60 * 30) return if (new Date().getTime() - time <= 1000 * 60 * 30) return
try { try {
const res = await refreshToken() const res = await api.refreshToken()
if (res.code === 0) { setToken(res.data.token)
setToken(res.data.token)
}
} catch (error) {} } catch (error) {}
} }

View File

@@ -0,0 +1,76 @@
import dayjs from 'dayjs'
/**
* @desc 格式化时间
* @param {(Object|string|number)} time
* @param {string} format
* @returns {string | null}
*/
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(time).format(format)
}
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
return formatDateTime(date, format)
}
/**
* @desc 函数节流
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
var context, args
var previous = 0
return function () {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
}
}
}
/**
* @desc 函数防抖
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(method, wait, immediate) {
let timeout
return function (...args) {
let context = this
if (timeout) {
clearTimeout(timeout)
}
// 立即执行需要两个条件一是immediate为true二是timeout未被赋值或被置为null
if (immediate) {
/**
* 如果定时器不存在则立即执行并设置一个定时器wait毫秒后将定时器置为null
* 这样确保立即执行后wait毫秒内不会被再次触发
*/
let callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) {
method.apply(context, args)
}
} else {
// 如果immediate为false则函数wait毫秒后执行
timeout = setTimeout(() => {
/**
* args是一个类数组对象所以使用fn.apply
* 也可写作method.call(context, ...args)
*/
method.apply(context, args)
}, wait)
}
}
}

View File

@@ -1,7 +1,12 @@
import { h } from 'vue' import { h } from 'vue'
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
import SvgIcon from '@/components/icon/SvgIcon.vue'
export function renderIcon(icon, props = { size: 12 }) { export function renderIcon(icon, props = { size: 12 }) {
return () => h(NIcon, props, { default: () => h(Icon, { icon }) }) return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
} }
export function renderCustomIcon(icon, props = { size: 12 }) {
return () => h(NIcon, props, { default: () => h(SvgIcon, { icon }) })
}

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