mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-28 20:10:22 +08:00
Compare commits
97 Commits
7ed9a3540d
...
2.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e03a4b1a6 | ||
|
|
a5a20a30c9 | ||
|
|
9a7170f10c | ||
|
|
fb4d18cc0a | ||
|
|
d078bba63e | ||
|
|
5663f58b6e | ||
|
|
32700aee17 | ||
|
|
e593ba3ca5 | ||
|
|
d9de814074 | ||
|
|
5457c4e70b | ||
|
|
6757e47fb1 | ||
|
|
3fbcfdf0e1 | ||
|
|
d6764158f3 | ||
|
|
4785842629 | ||
|
|
5b4b0bd9ad | ||
|
|
3c04bbce8e | ||
|
|
0b69d55765 | ||
|
|
91739a0cd5 | ||
|
|
c925ca3e00 | ||
|
|
11b0f45381 | ||
|
|
0fe0b9d41b | ||
|
|
7243f4f38e | ||
|
|
5178b70aca | ||
|
|
1934776063 | ||
|
|
c675a4d420 | ||
|
|
e29343586f | ||
|
|
6fea2a1b69 | ||
|
|
2513e475be | ||
|
|
dde25dd7ab | ||
|
|
1eb59b7cad | ||
|
|
27a50a99b4 | ||
|
|
cf0ba0a2bf | ||
|
|
70c2098d2e | ||
|
|
82748abcc3 | ||
|
|
e5fd1405da | ||
|
|
833321d31d | ||
|
|
53afc9b531 | ||
|
|
0733b81f82 | ||
|
|
26ce0bd551 | ||
|
|
1201e8e172 | ||
|
|
78d9bb99f2 | ||
|
|
9b510e8350 | ||
|
|
7ce6cf1f7c | ||
|
|
30ca69cf16 | ||
|
|
c34c3646aa | ||
|
|
6563797afc | ||
|
|
6af6eb3c99 | ||
|
|
69e6e9a14a | ||
|
|
d066c999cc | ||
|
|
97fb638ac2 | ||
|
|
db800f60a9 | ||
|
|
304b74bb49 | ||
|
|
359f683955 | ||
|
|
d7a4034c69 | ||
|
|
520db7ec6e | ||
|
|
2264512a8b | ||
|
|
3359b0f2b5 | ||
|
|
615c3b63f7 | ||
|
|
51f2d5d22c | ||
|
|
02e614e903 | ||
|
|
fdb6ad5f1d | ||
|
|
723d141705 | ||
|
|
7b1b990d77 | ||
|
|
36ce5c34d0 | ||
|
|
1e0e1f2c48 | ||
|
|
491237919a | ||
|
|
0c0e7a465b | ||
|
|
0a0c970f5c | ||
|
|
c9e0feea47 | ||
|
|
f28e3a946e | ||
|
|
7dde0bbfc6 | ||
|
|
cf76f2177b | ||
|
|
d0a99dcf12 | ||
|
|
6ab97f511f | ||
|
|
2384113120 | ||
|
|
8cb3d2cf19 | ||
|
|
c5d93628f0 | ||
|
|
4d494801e3 | ||
|
|
e14bd2084a | ||
|
|
3c2e120e34 | ||
|
|
c230e86767 | ||
|
|
b464c70ae3 | ||
|
|
c87f56814e | ||
|
|
fd4582fc0c | ||
|
|
c2ff434b9d | ||
|
|
c98e489063 | ||
|
|
0ab029a124 | ||
|
|
e30e1fa27f | ||
|
|
3c64aca739 | ||
|
|
995175a8a8 | ||
|
|
eca0859f66 | ||
|
|
38fbbfd376 | ||
|
|
54c134c879 | ||
|
|
6b060291bb | ||
|
|
a67510fe34 | ||
|
|
98f9d5893a | ||
|
|
004ef366f2 |
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = unset
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
@@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = '/'
|
|||||||
|
|
||||||
# Axios 基础路径
|
# Axios 基础路径
|
||||||
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||||
VITE_AXIOS_BASE_URL = 'https://mock.apipark.cn/m1/3776410-0-default' # apifox云端mock
|
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
|
||||||
|
|
||||||
# 代理配置-target
|
# 代理配置-target
|
||||||
VITE_PROXY_TARGET = 'http://localhost:8085'
|
VITE_PROXY_TARGET = 'http://localhost:8085'
|
||||||
|
|||||||
@@ -4,7 +4,5 @@ VITE_USE_HASH = 'false'
|
|||||||
# 资源公共路径,需要以 /开头和结尾
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
VITE_PUBLIC_PATH = '/'
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||||
|
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
|
||||||
# 代理配置-target
|
|
||||||
VITE_PROXY_TARGET = 'http://localhost:8085'
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"globals": {
|
|
||||||
"$loadingBar": true,
|
|
||||||
"$message": true,
|
|
||||||
"$dialog": true,
|
|
||||||
"$notification": true,
|
|
||||||
"$modal": true,
|
|
||||||
"defineOptions": true,
|
|
||||||
"EffectScope": true,
|
|
||||||
"computed": true,
|
|
||||||
"createApp": true,
|
|
||||||
"customRef": true,
|
|
||||||
"defineAsyncComponent": true,
|
|
||||||
"defineComponent": true,
|
|
||||||
"effectScope": true,
|
|
||||||
"getCurrentInstance": true,
|
|
||||||
"getCurrentScope": true,
|
|
||||||
"h": true,
|
|
||||||
"inject": true,
|
|
||||||
"isProxy": true,
|
|
||||||
"isReactive": true,
|
|
||||||
"isReadonly": true,
|
|
||||||
"isRef": true,
|
|
||||||
"markRaw": true,
|
|
||||||
"nextTick": true,
|
|
||||||
"onActivated": true,
|
|
||||||
"onBeforeMount": true,
|
|
||||||
"onBeforeUnmount": true,
|
|
||||||
"onBeforeUpdate": true,
|
|
||||||
"onDeactivated": true,
|
|
||||||
"onErrorCaptured": true,
|
|
||||||
"onMounted": true,
|
|
||||||
"onRenderTracked": true,
|
|
||||||
"onRenderTriggered": true,
|
|
||||||
"onScopeDispose": true,
|
|
||||||
"onServerPrefetch": true,
|
|
||||||
"onUnmounted": true,
|
|
||||||
"onUpdated": true,
|
|
||||||
"provide": true,
|
|
||||||
"reactive": true,
|
|
||||||
"readonly": true,
|
|
||||||
"ref": true,
|
|
||||||
"resolveComponent": true,
|
|
||||||
"shallowReactive": true,
|
|
||||||
"shallowReadonly": true,
|
|
||||||
"shallowRef": true,
|
|
||||||
"toRaw": true,
|
|
||||||
"toRef": true,
|
|
||||||
"toRefs": true,
|
|
||||||
"triggerRef": true,
|
|
||||||
"unref": true,
|
|
||||||
"useAttrs": true,
|
|
||||||
"useCssModule": true,
|
|
||||||
"useCssVars": true,
|
|
||||||
"useRoute": true,
|
|
||||||
"useRouter": true,
|
|
||||||
"useSlots": true,
|
|
||||||
"watch": true,
|
|
||||||
"watchEffect": true,
|
|
||||||
"watchPostEffect": true,
|
|
||||||
"watchSyncEffect": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
public
|
|
||||||
package.json
|
|
||||||
1
.npmrc
1
.npmrc
@@ -1,2 +1,3 @@
|
|||||||
registry=https://registry.npmmirror.com
|
registry=https://registry.npmmirror.com
|
||||||
shamefully-hoist=true
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
/node_modules/**
|
|
||||||
/dist/*
|
|
||||||
/public/*
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": false,
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"htmlWhitespaceSensitivity": "ignore"
|
|
||||||
}
|
|
||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -6,6 +6,7 @@
|
|||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"sdras.vue-vscode-snippets",
|
"sdras.vue-vscode-snippets",
|
||||||
"mikestead.dotenv"
|
"mikestead.dotenv",
|
||||||
|
"tu6ge.naive-ui-intelligence"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
40
.vscode/settings.json
vendored
40
.vscode/settings.json
vendored
@@ -1,25 +1,23 @@
|
|||||||
{
|
{
|
||||||
"files.eol": "\n",
|
"prettier.enable": false,
|
||||||
"files.associations": {
|
"editor.formatOnSave": false,
|
||||||
"*.env.*": "dotenv",
|
|
||||||
"*.svg": "html",
|
// Auto fix
|
||||||
"*.css": "scss"
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
|
|
||||||
"editor.formatOnSave": false,
|
// Enable eslint for all supported languages
|
||||||
"[html][css][less][scss][sass][yaml][yml][jsonc][json]": {
|
"eslint.validate": [
|
||||||
"editor.formatOnSave": true,
|
"javascript",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"javascriptreact",
|
||||||
},
|
"typescript",
|
||||||
"[markdown]": {
|
"typescriptreact",
|
||||||
"editor.formatOnSave": true,
|
"vue",
|
||||||
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
"html",
|
||||||
},
|
"markdown",
|
||||||
"editor.codeActionsOnSave": {
|
"json",
|
||||||
"source.fixAll.eslint": "explicit"
|
"jsonc"
|
||||||
},
|
]
|
||||||
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
|
|
||||||
"eslint.options": {
|
|
||||||
"overrideConfigFile": "package.json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -5,9 +5,16 @@
|
|||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://gitcode.com/zclzone/vue-naive-admin"><img alt="GitCode" src="https://gitcode.com/zclzone/vue-naive-admin/star/badge.svg
|
||||||
|
"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href="https://hellogithub.com/repository/54f19ba1f9ae4238b3cbd111f3c428b0" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=54f19ba1f9ae4238b3cbd111f3c428b0&claim_uid=jXGayRdJZScqMNr" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss,后端使用 Nestjs + TypeOrm + MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss,后端使用 Nestjs + TypeOrm + MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
||||||
|
|
||||||
## 设计理念
|
## 设计理念
|
||||||
@@ -50,18 +57,24 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
|
|||||||
|
|
||||||
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve)
|
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve)
|
||||||
- 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
|
- 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
|
||||||
|
- 源码-gitcode: [isme-nest-serve | gitcode](https://gitcode.com/zclzone/isme-nest-serve)
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- 项目文档: [docs | vue-naive-admin](https://docs.isme.top/web/#/624306705/188522224)
|
- 项目文档: [docs | vue-naive-admin](https://isme.top)
|
||||||
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
|
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
|
||||||
|
|
||||||
> 注:有个比较常见的问题,就是如何添加菜单和修改菜单,由于项目是由后端控制菜单资源的,所以需要对接后端后在资源管理功能对菜单进行增删改,然后在角色管理功能给对应角色进行授权。具体如何对接后端,请参考 [项目文档](https://docs.isme.top/web/#/624306705/188522224)。当然,可能有些菜单你不想通过权限控制,那么你可以在 `/src/settings.js` 文件添加 basePermissions,只需对齐菜单资源的结构即可,结构可以参照 [接口文档](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a/api-134536978)。
|
> 注:有个比较常见的问题,就是如何添加菜单和修改菜单,由于项目是由后端控制菜单资源的,所以需要对接后端后在资源管理功能对菜单进行增删改,然后在角色管理功能给对应角色进行授权。具体如何对接后端,请参考 [项目文档](https://isme.top)。当然,可能有些菜单你不想通过权限控制,那么你可以在 `/src/settings.js` 文件添加 basePermissions,只需对齐菜单资源的结构即可,结构可以参照 [接口文档](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a/api-134536978)。
|
||||||
|
|
||||||
## 使用这个模板创建 Github 仓库
|
## 使用这个模板开始你的项目
|
||||||
|
|
||||||
[使用这个模板创建仓库](https://github.com/zclzone/vue-naive-admin/generate).
|
[使用这个模板创建Github仓库](https://github.com/zclzone/vue-naive-admin/generate).
|
||||||
|
|
||||||
|
或者使用 `degit` 克隆此仓库,这样将没有任何历史提交记录:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
npx degit zclzone/vue-naive-admin
|
||||||
|
```
|
||||||
|
|
||||||
## 版权说明
|
## 版权说明
|
||||||
|
|
||||||
@@ -75,10 +88,8 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
|
|||||||
|
|
||||||
- [isme-java-serve](https://github.com/DHBin/isme-java-serve): 一个轻量级的Java后端服务,基于SpringBoot、MybatisPlus、SaToken、MapStruct等实现,已对接 Vue Naive Admin 2.0。
|
- [isme-java-serve](https://github.com/DHBin/isme-java-serve): 一个轻量级的Java后端服务,基于SpringBoot、MybatisPlus、SaToken、MapStruct等实现,已对接 Vue Naive Admin 2.0。
|
||||||
- [naive-admin-go](https://github.com/ituserxxx/naive-admin-go): 一个 Go 后端服务,基于 gin、gorm、mysql、jwt和session,已对接 Vue Naive Admin 2.0。
|
- [naive-admin-go](https://github.com/ituserxxx/naive-admin-go): 一个 Go 后端服务,基于 gin、gorm、mysql、jwt和session,已对接 Vue Naive Admin 2.0。
|
||||||
|
- [isme-java](https://github.com/AllenDengMs/isme-java): 一个轻量且完成度高的Java后端服务,基于Springboot 3 + JDK21,层次结构严谨,注释齐全,避免过度封装,代码可读性度高,依赖最简化,上手成本低,已集成 账号管理、权限管理、API鉴权、消息国际化等功能。
|
||||||
|
|
||||||
## 入群交流
|
## 联系作者 or 进交流群
|
||||||
|
|
||||||
添加微信,拉你进群
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
[https://www.isme.top/contact.html](https://www.isme.top/contact.html)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
|
import path from 'node:path'
|
||||||
import { globSync } from 'glob'
|
import { globSync } from 'glob'
|
||||||
import path from 'path'
|
import dynamicIcons from '../src/assets/icons/dynamic-icons.js'
|
||||||
import dynamicIcons from '../src/assets/icons/dynamic-icons'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @usage 生成icons, 用于 unocss safelist,以支持页面动态渲染自定义图标
|
* @usage 生成icons, 用于 unocss safelist,以支持页面动态渲染自定义图标
|
||||||
@@ -35,5 +35,5 @@ export function getIcons() {
|
|||||||
*/
|
*/
|
||||||
export function getPagePathes() {
|
export function getPagePathes() {
|
||||||
const files = globSync('src/views/**/*.vue')
|
const files = globSync('src/views/**/*.vue')
|
||||||
return files.map((item) => '/' + path.normalize(item).replace(/\\/g, '/'))
|
return files.map(item => `/${path.normalize(item).replace(/\\/g, '/')}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ export function pluginIcons() {
|
|||||||
return {
|
return {
|
||||||
name: 'isme:icons',
|
name: 'isme:icons',
|
||||||
resolveId(id) {
|
resolveId(id) {
|
||||||
if (id === PLUGIN_ICONS_ID) return '\0' + PLUGIN_ICONS_ID
|
if (id === PLUGIN_ICONS_ID)
|
||||||
|
return `\0${PLUGIN_ICONS_ID}`
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === '\0' + PLUGIN_ICONS_ID) {
|
if (id === `\0${PLUGIN_ICONS_ID}`) {
|
||||||
return `export default ${JSON.stringify(getIcons())}`
|
return `export default ${JSON.stringify(getIcons())}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
export { pluginPagePathes } from './page-pathes'
|
|
||||||
export { pluginIcons } from './icons'
|
export { pluginIcons } from './icons'
|
||||||
|
export { pluginPagePathes } from './page-pathes'
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ export function pluginPagePathes() {
|
|||||||
return {
|
return {
|
||||||
name: 'isme:page-pathes',
|
name: 'isme:page-pathes',
|
||||||
resolveId(id) {
|
resolveId(id) {
|
||||||
if (id === PLUGIN_PAGE_PATHES_ID) return '\0' + PLUGIN_PAGE_PATHES_ID
|
if (id === PLUGIN_PAGE_PATHES_ID)
|
||||||
|
return `\0${PLUGIN_PAGE_PATHES_ID}`
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === '\0' + PLUGIN_PAGE_PATHES_ID) {
|
if (id === `\0${PLUGIN_PAGE_PATHES_ID}`) {
|
||||||
return `export default ${JSON.stringify(getPagePathes())}`
|
return `export default ${JSON.stringify(getPagePathes())}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
34
eslint.config.js
Normal file
34
eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
unocss: true,
|
||||||
|
formatters: true,
|
||||||
|
stylistic: true,
|
||||||
|
rules: {
|
||||||
|
'n/prefer-global/process': 'off',
|
||||||
|
'no-undef': 'error',
|
||||||
|
'no-fallthrough': 'off',
|
||||||
|
'vue/block-order': 'off',
|
||||||
|
'@typescript-eslint/no-this-alias': 'off',
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
h: 'readonly',
|
||||||
|
unref: 'readonly',
|
||||||
|
provide: 'readonly',
|
||||||
|
inject: 'readonly',
|
||||||
|
markRaw: 'readonly',
|
||||||
|
defineAsyncComponent: 'readonly',
|
||||||
|
nextTick: 'readonly',
|
||||||
|
useRoute: 'readonly',
|
||||||
|
useRouter: 'readonly',
|
||||||
|
Message: 'readonly',
|
||||||
|
$loadingBar: 'readonly',
|
||||||
|
$message: 'readonly',
|
||||||
|
$dialog: 'readonly',
|
||||||
|
$notification: 'readonly',
|
||||||
|
$modal: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<link rel="stylesheet" href="/resource/loading.css" />
|
|
||||||
<title>%VITE_TITLE%</title>
|
<title>%VITE_TITLE%</title>
|
||||||
<style>
|
<style>
|
||||||
.loading-container {
|
.loading-container {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"~/*": ["./*"]
|
"~/*": ["./*"]
|
||||||
},
|
},
|
||||||
"jsx": "preserve",
|
|
||||||
"allowJs": true
|
"allowJs": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
86
package.json
86
package.json
@@ -1,58 +1,60 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-naive-admin",
|
"name": "vue-naive-admin",
|
||||||
"private": true,
|
|
||||||
"version": "2.0.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint:fix": "eslint --fix --ext .js,.vue ."
|
"lint:fix": "eslint --fix",
|
||||||
|
"postinstall": "npx simple-git-hooks",
|
||||||
|
"up": "taze major -I"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/color": "^0.4.0",
|
"@arco-design/color": "^0.4.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.13.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.19",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^6.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.22",
|
||||||
"naive-ui": "^2.38.2",
|
"naive-ui": "^2.43.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"vue": "^3.4.27",
|
"vue": "^3.5.26",
|
||||||
"vue-echarts": "^6.7.2",
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.6.4",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.2.210",
|
"@antfu/eslint-config": "^6.7.3",
|
||||||
"@iconify/utils": "^2.1.23",
|
"@iconify/json": "^2.2.417",
|
||||||
"@unocss/eslint-config": "^0.58.9",
|
"@unocss/eslint-config": "^66.5.10",
|
||||||
"@unocss/preset-rem-to-px": "^0.59.4",
|
"@unocss/eslint-plugin": "^66.5.10",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@unocss/preset-rem-to-px": "^66.5.10",
|
||||||
"@zclzone/eslint-config": "^0.0.5",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"esno": "^4.7.0",
|
"@vitejs/plugin-vue-jsx": "^5.1.2",
|
||||||
"fs-extra": "^11.2.0",
|
"eslint": "^9.39.2",
|
||||||
"glob": "^10.3.15",
|
"eslint-plugin-format": "^1.1.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"esno": "^4.8.0",
|
||||||
"sass": "^1.77.1",
|
"fs-extra": "^11.3.3",
|
||||||
"unocss": "^0.59.4",
|
"glob": "^13.0.0",
|
||||||
"unplugin-auto-import": "^0.17.6",
|
"lint-staged": "^16.2.7",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"vite": "^5.2.11",
|
"simple-git-hooks": "^2.13.1",
|
||||||
"vite-plugin-router-warn": "^1.0.0",
|
"taze": "^19.9.2",
|
||||||
"vite-plugin-vue-devtools": "^7.2.0"
|
"unocss": "^66.5.10",
|
||||||
|
"unplugin-auto-import": "^20.3.0",
|
||||||
|
"unplugin-vue-components": "^30.0.0",
|
||||||
|
"vite": "^7.3.0",
|
||||||
|
"vite-plugin-router-warn": "^2.0.0",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"vue3-intro-step": "^1.0.5"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"simple-git-hooks": {
|
||||||
"extends": [
|
"pre-commit": "npx lint-staged"
|
||||||
"@zclzone",
|
},
|
||||||
"@unocss",
|
"lint-staged": {
|
||||||
".eslint-global-variables.json"
|
"*": "eslint --fix"
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8751
pnpm-lock.yaml
generated
8751
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
src/App.vue
18
src/App.vue
@@ -16,25 +16,29 @@
|
|||||||
>
|
>
|
||||||
<router-view v-if="Layout" v-slot="{ Component, route: curRoute }">
|
<router-view v-if="Layout" v-slot="{ Component, route: curRoute }">
|
||||||
<component :is="Layout">
|
<component :is="Layout">
|
||||||
|
<transition name="fade-slide" mode="out-in" appear>
|
||||||
<KeepAlive :include="keepAliveNames">
|
<KeepAlive :include="keepAliveNames">
|
||||||
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
|
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
|
</transition>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
<LayoutSetting class="fixed right-12 top-1/2 z-999" />
|
<LayoutSetting v-if="layoutSettingVisible" class="fixed right-12 top-1/2 z-999" />
|
||||||
</router-view>
|
</router-view>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
|
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
|
||||||
import { LayoutSetting } from '@/components'
|
import { LayoutSetting } from '@/components'
|
||||||
import { useAppStore, useTabStore } from '@/store'
|
import { useAppStore, useTabStore } from '@/store'
|
||||||
|
import { layoutSettingVisible } from './settings'
|
||||||
|
|
||||||
const layouts = new Map()
|
const layouts = new Map()
|
||||||
function getLayout(name) {
|
function getLayout(name) {
|
||||||
// 利用map将加载过的layout缓存起来,防止重新加载layout导致页面闪烁
|
// 利用map将加载过的layout缓存起来,防止重新加载layout导致页面闪烁
|
||||||
if (layouts.get(name)) return layouts.get(name)
|
if (layouts.get(name))
|
||||||
|
return layouts.get(name)
|
||||||
const layout = markRaw(defineAsyncComponent(() => import(`@/layouts/${name}/index.vue`)))
|
const layout = markRaw(defineAsyncComponent(() => import(`@/layouts/${name}/index.vue`)))
|
||||||
layouts.set(name, layout)
|
layouts.set(name, layout)
|
||||||
return layout
|
return layout
|
||||||
@@ -42,15 +46,17 @@ function getLayout(name) {
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
if (appStore.layout === 'default') appStore.setLayout('')
|
if (appStore.layout === 'default')
|
||||||
|
appStore.setLayout('')
|
||||||
const Layout = computed(() => {
|
const Layout = computed(() => {
|
||||||
if (!route.matched?.length) return null
|
if (!route.matched?.length)
|
||||||
|
return null
|
||||||
return getLayout(route.meta?.layout || appStore.layout)
|
return getLayout(route.meta?.layout || appStore.layout)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const keepAliveNames = computed(() => {
|
const keepAliveNames = computed(() => {
|
||||||
return tabStore.tabs.filter((item) => item.keepAlive).map((item) => item.name)
|
return tabStore.tabs.filter(item => item.keepAlive).map(item => item.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ export default {
|
|||||||
// 刷新token
|
// 刷新token
|
||||||
refreshToken: () => request.get('/auth/refresh/token'),
|
refreshToken: () => request.get('/auth/refresh/token'),
|
||||||
// 登出
|
// 登出
|
||||||
logout: () => request.post('/auth/logout'),
|
logout: () => request.post('/auth/logout', {}, { needTip: false }),
|
||||||
// 切换当前角色
|
// 切换当前角色
|
||||||
switchCurrentRole: (role) => request.post(`/auth/current-role/switch/${role}`),
|
switchCurrentRole: role => request.post(`/auth/current-role/switch/${role}`),
|
||||||
// 获取角色权限
|
// 获取角色权限
|
||||||
getRolePermissions: () => request.get('/role/permissions/tree'),
|
getRolePermissions: () => request.get('/role/permissions/tree'),
|
||||||
// 验证菜单路径
|
// 验证菜单路径
|
||||||
validateMenuPath: (path) => request.get(`/permission/menu/validate?path=${path}`),
|
validateMenuPath: path => request.get(`/permission/menu/validate?path=${path}`),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="cus-scroll h-full flex-col flex-1 bg-#f5f6fb dark:bg-#121212">
|
<main class="cus-scroll h-full flex-col flex-1 bg-#f5f6fb dark:bg-#121212">
|
||||||
<transition name="fade-slide" mode="out-in" appear>
|
<main :class="{ 'flex-1': full }" class="m-12">
|
||||||
<main :class="{ 'flex-1': full }" class="m-12"><slot /></main>
|
<slot />
|
||||||
</transition>
|
</main>
|
||||||
<slot v-if="$slots.footer" name="footer" />
|
<slot name="footer">
|
||||||
<TheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
|
<TheFooter v-if="showFooter" class="mb-12 mt-auto" />
|
||||||
|
</slot>
|
||||||
<n-back-top :bottom="20" />
|
<n-back-top :bottom="20" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
--------------------------------->
|
--------------------------------->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="h-full flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
|
<main class="h-full flex-col flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
|
||||||
<div class="h-full flex-col">
|
|
||||||
<AppCard
|
<AppCard
|
||||||
v-if="showHeader"
|
v-if="showHeader"
|
||||||
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
|
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
|
||||||
@@ -17,8 +16,8 @@
|
|||||||
<slot v-if="$slots.header" name="header" />
|
<slot v-if="$slots.header" name="header" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<slot v-if="$slots['title-prefix']" name="title-prefix" />
|
<slot name="title-prefix">
|
||||||
<template v-else-if="back">
|
<template v-if="back">
|
||||||
<div
|
<div
|
||||||
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
||||||
@click="router.back()"
|
@click="router.back()"
|
||||||
@@ -27,27 +26,27 @@
|
|||||||
<span class="ml-4">返回</span>
|
<span class="ml-4">返回</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary"></div>
|
</slot>
|
||||||
<h2 class="font-normal">{{ title ?? route.meta?.title }}</h2>
|
|
||||||
|
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary" />
|
||||||
|
<h2 class="font-normal">
|
||||||
|
{{ title ?? route.meta?.title }}
|
||||||
|
</h2>
|
||||||
<slot name="title-suffix" />
|
<slot name="title-suffix" />
|
||||||
</div>
|
</div>
|
||||||
<slot name="action" />
|
<slot name="action" />
|
||||||
</template>
|
</template>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
<transition name="fade-slide" mode="out-in" appear>
|
|
||||||
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
|
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
|
||||||
<slot />
|
<slot />
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="bg-#f5f6fb dark:bg-#121212">
|
<slot name="footer">
|
||||||
<slot v-if="$slots.footer" name="footer" />
|
<AppCard v-if="showFooter" class="flex-shrink-0 py-12">
|
||||||
<AppCard v-else-if="showFooter" class="py-12">
|
|
||||||
<TheFooter />
|
<TheFooter />
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</footer>
|
</slot>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<n-tooltip trigger="hover" placement="left">
|
<n-tooltip trigger="hover" placement="left">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
|
<div id="layout-setting" class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
|
||||||
<i class="i-fe:settings cursor-pointer bg-white text-20" />
|
<i class="i-fe:settings cursor-pointer bg-white text-20" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -94,8 +94,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { MeModal } from '@/components'
|
import { MeModal } from '@/components'
|
||||||
import { useAppStore } from '@/store'
|
|
||||||
import { useModal } from '@/composables'
|
import { useModal } from '@/composables'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const [modalRef] = useModal()
|
const [modalRef] = useModal()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
--------------------------------->
|
--------------------------------->
|
||||||
<template>
|
<template>
|
||||||
<div class="h-40 w-40 rounded-4 bg-primary p-1/100">
|
<div class="h-32 w-32 rounded-4 bg-primary">
|
||||||
<img src="@/assets/images/isme.png" alt="Logo" />
|
<img src="@/assets/images/isme.png" alt="Logo">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover">
|
<div class="f-c-c">
|
||||||
|
<n-tooltip trigger="hover" placement="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-color-picker
|
<n-color-picker
|
||||||
class="mr-16 h-32 w-32"
|
id="theme-setting"
|
||||||
|
class="h-32 w-32"
|
||||||
:value="appStore.primaryColor"
|
:value="appStore.primaryColor"
|
||||||
:swatches="primaryColors"
|
:swatches="primaryColors"
|
||||||
:on-update:value="(v) => appStore.setPrimaryColor(v)"
|
:on-update:value="(v) => appStore.setPrimaryColor(v)"
|
||||||
@@ -11,11 +13,13 @@
|
|||||||
</template>
|
</template>
|
||||||
设置主题色
|
设置主题色
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { getPresetColors } from '@arco-design/color'
|
import { getPresetColors } from '@arco-design/color'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
|
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
|
||||||
|
|||||||
46
src/components/common/ToggleTheme.vue
Normal file
46
src/components/common/ToggleTheme.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<i
|
||||||
|
id="toggleTheme"
|
||||||
|
class="mr-16 cursor-pointer"
|
||||||
|
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
||||||
|
@click="toggleDark"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const isDark = useDark()
|
||||||
|
async function toggleDark({ clientX, clientY }) {
|
||||||
|
function handler() {
|
||||||
|
appStore.toggleDark()
|
||||||
|
useToggle(isDark)()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
return handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipPath = [
|
||||||
|
`circle(0px at ${clientX}px ${clientY}px)`,
|
||||||
|
`circle(${Math.hypot(
|
||||||
|
Math.max(clientX, window.innerWidth - clientX),
|
||||||
|
Math.max(clientY, window.innerHeight - clientY),
|
||||||
|
)}px at ${clientX}px ${clientY}px)`,
|
||||||
|
]
|
||||||
|
|
||||||
|
await document.startViewTransition(handler).ready
|
||||||
|
|
||||||
|
document.documentElement.animate(
|
||||||
|
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
|
||||||
|
{
|
||||||
|
duration: 500,
|
||||||
|
easing: 'ease-in',
|
||||||
|
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
|
||||||
|
fill: 'both',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as AppCard } from './AppCard.vue'
|
export { default as AppCard } from './AppCard.vue'
|
||||||
export { default as TheFooter } from './TheFooter.vue'
|
|
||||||
export { default as AppPage } from './AppPage.vue'
|
export { default as AppPage } from './AppPage.vue'
|
||||||
export { default as CommonPage } from './CommonPage.vue'
|
export { default as CommonPage } from './CommonPage.vue'
|
||||||
export { default as LayoutSetting } from './LayoutSetting.vue'
|
export { default as LayoutSetting } from './LayoutSetting.vue'
|
||||||
|
export { default as TheFooter } from './TheFooter.vue'
|
||||||
|
export { default as ToggleTheme } from './ToggleTheme.vue'
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<label v-if="label || label === 0" class="flex-shrink-0" :style="{ width: labelWidth + 'px' }">
|
<label v-if="label || label === 0" class="flex-shrink-0" :style="{ width: `${labelWidth}px` }">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<div :style="{ width: contentWidth + 'px' }" class="flex-shrink-0">
|
<div :style="{ width: `${contentWidth}px` }" class="flex-shrink-0">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
--------------------------------->
|
--------------------------------->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
|
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
|
||||||
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
|
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
|
||||||
<n-space wrap :size="[32, 16]">
|
<n-scrollbar x-scrollable>
|
||||||
|
<n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10">
|
||||||
<slot />
|
<slot />
|
||||||
</n-space>
|
</n-space>
|
||||||
<div class="flex-shrink-0">
|
</n-scrollbar>
|
||||||
|
<div class="flex-shrink-0 p-10">
|
||||||
<n-button ghost type="primary" @click="handleReset">
|
<n-button ghost type="primary" @click="handleReset">
|
||||||
<i class="i-fe:rotate-ccw mr-4" />
|
<i class="i-fe:rotate-ccw mr-4" />
|
||||||
重置
|
重置
|
||||||
@@ -21,11 +24,22 @@
|
|||||||
<i class="i-fe:search mr-4" />
|
<i class="i-fe:search mr-4" />
|
||||||
搜索
|
搜索
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
|
<template v-if="expand">
|
||||||
|
<n-button v-if="!isExpanded" type="primary" text @click="toggleExpand">
|
||||||
|
<i class="i-fe:chevrons-down ml-4" />
|
||||||
|
展开
|
||||||
|
</n-button>
|
||||||
|
<n-button v-else text type="primary" @click="toggleExpand">
|
||||||
|
<i class="i-fe:chevrons-up ml-4" />
|
||||||
|
收起
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
<n-data-table
|
<NDataTable
|
||||||
:remote="remote"
|
:remote="remote"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:scroll-x="scrollX"
|
:scroll-x="scrollX"
|
||||||
@@ -33,9 +47,12 @@
|
|||||||
:data="tableData"
|
:data="tableData"
|
||||||
:row-key="(row) => row[rowKey]"
|
:row-key="(row) => row[rowKey]"
|
||||||
:pagination="isPagination ? pagination : false"
|
:pagination="isPagination ? pagination : false"
|
||||||
|
flex-height
|
||||||
|
class="flex-1"
|
||||||
@update:checked-row-keys="onChecked"
|
@update:checked-row-keys="onChecked"
|
||||||
@update:page="onPageChange"
|
@update:page="onPageChange"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -78,10 +95,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* ! 约定接口入参出参
|
* ! 约定接口入参出参
|
||||||
* * 分页模式需约定分页接口入参
|
* 分页模式需约定分页接口入参
|
||||||
* @pageSize 分页参数:一页展示多少条,默认10
|
* @pageSize 分页参数:一页展示多少条,默认10
|
||||||
* @pageNo 分页参数:页码,默认1
|
* @pageNo 分页参数:页码,默认1
|
||||||
* * 需约定接口出参
|
* 需约定接口出参
|
||||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||||
*/
|
*/
|
||||||
@@ -89,13 +106,28 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
/** 是否支持展开 */
|
||||||
|
expand: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const initQuery = { ...props.queryItems }
|
const initQuery = { ...props.queryItems }
|
||||||
const tableData = ref([])
|
const tableData = ref([])
|
||||||
const pagination = reactive({ page: 1, pageSize: 10 })
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
prefix({ itemCount }) {
|
||||||
|
return `共 ${itemCount} 条数据`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否展开
|
||||||
|
const isExpanded = ref(false)
|
||||||
|
|
||||||
|
function toggleExpand() {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
async function handleQuery() {
|
async function handleQuery() {
|
||||||
try {
|
try {
|
||||||
@@ -111,18 +143,30 @@ async function handleQuery() {
|
|||||||
})
|
})
|
||||||
tableData.value = data?.pageData || data
|
tableData.value = data?.pageData || data
|
||||||
pagination.itemCount = data.total ?? data.length
|
pagination.itemCount = data.total ?? data.length
|
||||||
} catch (error) {
|
if (pagination.itemCount && !tableData.value.length && pagination.page > 1) {
|
||||||
|
// 如果当前页数据为空,且总条数不为0,则返回上一页数据
|
||||||
|
onPageChange(pagination.page - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
pagination.itemCount = 0
|
pagination.itemCount = 0
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
emit('onDataChange', tableData.value)
|
emit('onDataChange', tableData.value)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleSearch() {
|
|
||||||
pagination.page = 1
|
function handleSearch(keepCurrentPage = false) {
|
||||||
|
if (keepCurrentPage || !props.remote) {
|
||||||
handleQuery()
|
handleQuery()
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
onPageChange(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
async function handleReset() {
|
async function handleReset() {
|
||||||
const queryItems = { ...props.queryItems }
|
const queryItems = { ...props.queryItems }
|
||||||
for (const key in queryItems) {
|
for (const key in queryItems) {
|
||||||
@@ -140,16 +184,17 @@ function onPageChange(currentPage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onChecked(rowKeys) {
|
function onChecked(rowKeys) {
|
||||||
if (props.columns.some((item) => item.type === 'selection')) {
|
if (props.columns.some(item => item.type === 'selection')) {
|
||||||
emit('onChecked', rowKeys)
|
emit('onChecked', rowKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleExport(columns = props.columns, data = tableData.value) {
|
function handleExport(columns = props.columns, data = tableData.value) {
|
||||||
if (!data?.length) return $message.warning('没有数据')
|
if (!data?.length)
|
||||||
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
|
return $message.warning('没有数据')
|
||||||
const thKeys = columnsData.map((item) => item.key)
|
const columnsData = columns.filter(item => !!item.title && !item.hideInExcel)
|
||||||
const thData = columnsData.map((item) => item.title)
|
const thKeys = columnsData.map(item => item.key)
|
||||||
const trData = data.map((item) => thKeys.map((key) => item[key]))
|
const thData = columnsData.map(item => item.title)
|
||||||
|
const trData = data.map(item => thKeys.map(key => item[key]))
|
||||||
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
||||||
const workBook = utils.book_new()
|
const workBook = utils.book_new()
|
||||||
utils.book_append_sheet(workBook, sheet, '数据报表')
|
utils.book_append_sheet(workBook, sheet, '数据报表')
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { default as MeModal } from './modal/index.vue'
|
|
||||||
export { default as MeCrud } from './crud/index.vue'
|
export { default as MeCrud } from './crud/index.vue'
|
||||||
export { default as MeQueryItem } from './crud/QueryItem.vue'
|
export { default as MeQueryItem } from './crud/QueryItem.vue'
|
||||||
|
export { default as MeModal } from './modal/index.vue'
|
||||||
|
|||||||
@@ -18,14 +18,16 @@
|
|||||||
>
|
>
|
||||||
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
|
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<header class="modal-header">{{ modalOptions.title }}</header>
|
<header class="modal-header">
|
||||||
|
{{ modalOptions.title }}
|
||||||
|
</header>
|
||||||
</template>
|
</template>
|
||||||
<slot></slot>
|
<slot />
|
||||||
|
|
||||||
<!-- 底部按钮 -->
|
<!-- 底部按钮 -->
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot v-if="$slots.footer" name="footer" />
|
<slot name="footer">
|
||||||
<footer v-else-if="modalOptions.showFooter" class="flex justify-end">
|
<footer v-if="modalOptions.showFooter" class="flex justify-end">
|
||||||
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
||||||
{{ modalOptions.cancelText }}
|
{{ modalOptions.cancelText }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
{{ modalOptions.okText }}
|
{{ modalOptions.okText }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</footer>
|
</footer>
|
||||||
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { initDrag } from './utils'
|
import { initDrag } from './utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
width: {
|
width: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -101,6 +105,17 @@ const show = ref(false)
|
|||||||
// 声明一个modalOptions变量,用于存储模态框的配置信息
|
// 声明一个modalOptions变量,用于存储模态框的配置信息
|
||||||
const modalOptions = ref({})
|
const modalOptions = ref({})
|
||||||
|
|
||||||
|
const okLoading = computed({
|
||||||
|
get() {
|
||||||
|
return !!modalOptions.value?.okLoading
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
if (modalOptions.value) {
|
||||||
|
modalOptions.value.okLoading = v
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 打开模态框
|
// 打开模态框
|
||||||
async function open(options = {}) {
|
async function open(options = {}) {
|
||||||
// 将props和options合并赋值给modalOptions
|
// 将props和options合并赋值给modalOptions
|
||||||
@@ -111,7 +126,7 @@ async function open(options = {}) {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
initDrag(
|
initDrag(
|
||||||
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
||||||
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
|
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +145,12 @@ async function handleOk(data) {
|
|||||||
// 调用onOk函数,传入data参数
|
// 调用onOk函数,传入data参数
|
||||||
const res = await modalOptions.value.onOk(data)
|
const res = await modalOptions.value.onOk(data)
|
||||||
// 如果onOk函数的返回值不为false,则关闭模态框
|
// 如果onOk函数的返回值不为false,则关闭模态框
|
||||||
res !== false && close()
|
if (res !== false)
|
||||||
} catch (error) {
|
close()
|
||||||
okLoading.value = false
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
okLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,10 +165,12 @@ async function handleCancel(data) {
|
|||||||
const res = await modalOptions.value.onCancel(data)
|
const res = await modalOptions.value.onCancel(data)
|
||||||
|
|
||||||
// 如果onCancel函数的返回值不为false,则关闭模态框
|
// 如果onCancel函数的返回值不为false,则关闭模态框
|
||||||
res !== false && close()
|
if (res !== false)
|
||||||
} catch (error) {
|
close()
|
||||||
okLoading.value = false
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
okLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,21 +178,10 @@ async function onAfterLeave() {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
initDrag(
|
initDrag(
|
||||||
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
||||||
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
|
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const okLoading = computed({
|
|
||||||
get() {
|
|
||||||
return !!modalOptions.value?.okLoading
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
if (modalOptions.value) {
|
|
||||||
modalOptions.value.okLoading = v
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义一个defineExpose函数,用于暴露open、close、handleOk、handleCancel函数
|
// 定义一个defineExpose函数,用于暴露open、close、handleOk、handleCancel函数
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open,
|
open,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ function getCss(element, key) {
|
|||||||
|
|
||||||
// 初始化拖拽
|
// 初始化拖拽
|
||||||
export function initDrag(bar, box) {
|
export function initDrag(bar, box) {
|
||||||
if (!bar || !box) return
|
if (!bar || !box)
|
||||||
|
return
|
||||||
const params = {
|
const params = {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -50,7 +51,8 @@ export function initDrag(bar, box) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.onmousemove = function (e) {
|
document.onmousemove = function (e) {
|
||||||
if (e.target !== bar && !params.flag) return
|
if (e.target !== bar && !params.flag)
|
||||||
|
return
|
||||||
|
|
||||||
e.preventDefault() // 阻止默认事件
|
e.preventDefault() // 阻止默认事件
|
||||||
// 如果拖拽标志为true
|
// 如果拖拽标志为true
|
||||||
@@ -60,11 +62,11 @@ export function initDrag(bar, box) {
|
|||||||
const disX = nowX - params.currentX // 鼠标移动的X距离
|
const disX = nowX - params.currentX // 鼠标移动的X距离
|
||||||
const disY = nowY - params.currentY // 鼠标移动的Y距离
|
const disY = nowY - params.currentY // 鼠标移动的Y距离
|
||||||
|
|
||||||
let left = parseInt(params.left) + disX // 盒子元素的新left值
|
const left = Number.parseInt(params.left) + disX // 盒子元素的新left值
|
||||||
let top = parseInt(params.top) + disY // 盒子元素的新top值
|
const top = Number.parseInt(params.top) + disY // 盒子元素的新top值
|
||||||
|
|
||||||
box.style.left = left + 'px'
|
box.style.left = `${left}px`
|
||||||
box.style.top = top + 'px'
|
box.style.top = `${top}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
export * from './useAliveData'
|
||||||
export * from './useCrud'
|
export * from './useCrud'
|
||||||
export * from './useForm'
|
export * from './useForm'
|
||||||
export * from './useModal'
|
export * from './useModal'
|
||||||
export * from './useAliveData'
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
const lastDataMap = new Map()
|
const lastDataMap = new Map()
|
||||||
export const useAliveData = (initData = {}, key) => {
|
export function useAliveData(initData = {}, key) {
|
||||||
key = key ?? useRoute().name
|
key = key ?? useRoute().name
|
||||||
const lastData = lastDataMap.get(key)
|
const lastData = lastDataMap.get(key)
|
||||||
const aliveData = ref(lastData || { ...initData })
|
const aliveData = ref(lastData || { ...initData })
|
||||||
@@ -17,7 +17,7 @@ export const useAliveData = (initData = {}, key) => {
|
|||||||
(v) => {
|
(v) => {
|
||||||
lastDataMap.set(key, v)
|
lastDataMap.set(key, v)
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { useModal, useForm } from '.'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import { useForm, useModal } from '.'
|
||||||
|
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
view: '查看',
|
view: '查看',
|
||||||
@@ -14,14 +15,14 @@ const ACTIONS = {
|
|||||||
add: '新增',
|
add: '新增',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) => {
|
export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||||
const modalAction = ref('')
|
const modalAction = ref('')
|
||||||
const [modalRef, okLoading] = useModal()
|
const [modalRef, okLoading] = useModal()
|
||||||
const [modalFormRef, modalForm, validation] = useForm(initForm)
|
const [modalFormRef, modalForm, validation] = useForm(initForm)
|
||||||
|
|
||||||
/** 新增 */
|
/** 新增 */
|
||||||
function handleAdd(row = {}, title) {
|
function handleAdd(row = {}, title) {
|
||||||
handleOpen({ action: 'add', title, row: { ...initForm, ...row } })
|
handleOpen({ action: 'add', title, row: Object.assign({}, cloneDeep(initForm), cloneDeep(row)) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改 */
|
/** 修改 */
|
||||||
@@ -44,7 +45,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
|||||||
async onOk() {
|
async onOk() {
|
||||||
if (typeof onOk === 'function') {
|
if (typeof onOk === 'function') {
|
||||||
return await onOk()
|
return await onOk()
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return await handleSave()
|
return await handleSave()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,7 +79,9 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
|||||||
action.cb()
|
action.cb()
|
||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
data && refresh(data)
|
data && refresh(data)
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -85,7 +89,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
|||||||
|
|
||||||
/** 删除 */
|
/** 删除 */
|
||||||
function handleDelete(id, confirmOptions) {
|
function handleDelete(id, confirmOptions) {
|
||||||
if (!id && id !== 0) return
|
if (!id && id !== 0)
|
||||||
|
return
|
||||||
const d = $dialog.warning({
|
const d = $dialog.warning({
|
||||||
content: '确定删除?',
|
content: '确定删除?',
|
||||||
title: '提示',
|
title: '提示',
|
||||||
@@ -97,8 +102,10 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
|||||||
const data = await doDelete(id)
|
const data = await doDelete(id)
|
||||||
$message.success('删除成功')
|
$message.success('删除成功')
|
||||||
d.loading = false
|
d.loading = false
|
||||||
refresh(data)
|
refresh(data, true)
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
d.loading = false
|
d.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
export const useForm = (initFormData = {}) => {
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
|
export function useForm(initFormData = {}) {
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const formModel = ref({ ...initFormData })
|
const formModel = ref(cloneDeep(initFormData))
|
||||||
const rules = {
|
const rules = {
|
||||||
required: {
|
required: {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
export const useModal = () => {
|
export function useModal() {
|
||||||
const modalRef = ref(null)
|
const modalRef = ref(null)
|
||||||
const okLoading = computed({
|
const okLoading = computed({
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
|
import { withDirectives } from 'vue'
|
||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
|
|
||||||
const permission = {
|
const permission = {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
const currentRoute = unref(router.currentRoute)
|
const currentRoute = unref(router.currentRoute)
|
||||||
const btns = currentRoute.meta?.btns?.map((item) => item.code) || []
|
const btns = currentRoute.meta?.btns?.map(item => item.code) || []
|
||||||
if (!btns.includes(binding.value)) {
|
if (!btns.includes(binding.value)) {
|
||||||
el.remove()
|
el.remove()
|
||||||
}
|
}
|
||||||
@@ -21,3 +22,17 @@ const permission = {
|
|||||||
export function setupDirectives(app) {
|
export function setupDirectives(app) {
|
||||||
app.directive('permission', permission)
|
app.directive('permission', permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于h函数使用自定义权限指令
|
||||||
|
*
|
||||||
|
* @param {*} vnode 虚拟节点
|
||||||
|
* @param {*} code 权限码
|
||||||
|
* @returns 返回一个包含权限指令的vnode
|
||||||
|
*
|
||||||
|
* 使用示例:withPermission(h('button', {class: 'text-red-500'}, '删除'), 'user:delete')
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function withPermission(vnode, code) {
|
||||||
|
return withDirectives(vnode, [[permission, code]])
|
||||||
|
}
|
||||||
|
|||||||
120
src/layouts/components/BeginnerGuide.vue
Normal file
120
src/layouts/components/BeginnerGuide.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<i
|
||||||
|
class="i-fe:beginner mr-16 cursor-pointer text-20"
|
||||||
|
@click="show = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
操作指引
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<Vue3IntroStep
|
||||||
|
ref="myIntroStep"
|
||||||
|
v-model:show="show"
|
||||||
|
:config="config"
|
||||||
|
>
|
||||||
|
<template #prev="{ tipItem, index }">
|
||||||
|
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="prev(tipItem, index)">
|
||||||
|
上一步
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<template #next="{ tipItem }">
|
||||||
|
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="next(tipItem)">
|
||||||
|
下一步
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #skip>
|
||||||
|
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="skip">
|
||||||
|
跳过
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #done>
|
||||||
|
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="done">
|
||||||
|
完成
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</Vue3IntroStep>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Vue3IntroStep from 'vue3-intro-step'
|
||||||
|
|
||||||
|
const myIntroStep = shallowRef(null)
|
||||||
|
const show = shallowRef(false)
|
||||||
|
const config = {
|
||||||
|
backgroundOpacity: 0.8,
|
||||||
|
titleStyle: {
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: '18px',
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
el: '#toggleTheme',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '切换系统主题',
|
||||||
|
content: '一键开启护眼模式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#fullscreen',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '全屏/退出全屏',
|
||||||
|
content: '一键开启全屏',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#theme-setting',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '设置主题色',
|
||||||
|
content: '调整为你喜欢的主题色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#user-dropdown',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '个人中心',
|
||||||
|
content: '查看个人资料和退出系统',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#menu-collapse',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '展开/收起菜单',
|
||||||
|
content: '一键展开/收起菜单',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#top-tab',
|
||||||
|
tipPosition: 'bottom',
|
||||||
|
title: '标签栏',
|
||||||
|
content: '鼠标滚轮滑动可调整至最佳视野',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: '#layout-setting',
|
||||||
|
tipPosition: 'left',
|
||||||
|
title: '调整系统布局',
|
||||||
|
content: '将系统布局调整为你喜欢的样子',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip() {
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
// tipItem当前的提示项信息
|
||||||
|
// 调用vue3-intro-step的next方法 手动触发下一步
|
||||||
|
myIntroStep.value.next()
|
||||||
|
}
|
||||||
|
function prev() {
|
||||||
|
// 调用vue3-intro-step的prev方法 手动触发上一步
|
||||||
|
myIntroStep.value.prev()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { usePermissionStore } from '@/store'
|
import { usePermissionStore } from '@/store'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
@@ -43,7 +44,7 @@ watch(
|
|||||||
(v) => {
|
(v) => {
|
||||||
breadItems.value = findMatchs(permissionStore.permissions, v)
|
breadItems.value = findMatchs(permissionStore.permissions, v)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function findMatchs(tree, code, parents = []) {
|
function findMatchs(tree, code, parents = []) {
|
||||||
@@ -69,8 +70,8 @@ function handleItemClick(item) {
|
|||||||
|
|
||||||
function getDropOptions(list = []) {
|
function getDropOptions(list = []) {
|
||||||
return list
|
return list
|
||||||
.filter((item) => item.show)
|
.filter(item => item.show)
|
||||||
.map((child) => ({
|
.map(child => ({
|
||||||
label: child.name,
|
label: child.name,
|
||||||
key: child.code,
|
key: child.code,
|
||||||
icon: () => h('i', { class: child.icon }),
|
icon: () => h('i', { class: child.icon }),
|
||||||
|
|||||||
14
src/layouts/components/Fullscreen.vue
Normal file
14
src/layouts/components/Fullscreen.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<i
|
||||||
|
id="fullscreen"
|
||||||
|
class="mr-16 cursor-pointer"
|
||||||
|
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
||||||
|
@click="toggle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
|
||||||
|
const { isFullscreen, toggle } = useFullscreen()
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover"
|
id="menu-collapse"
|
||||||
|
class="f-c-c cursor-pointer rounded-4 auto-bg-hover p-6 text-22 transition-all-300"
|
||||||
@click="appStore.switchCollapsed"
|
@click="appStore.switchCollapsed"
|
||||||
>
|
>
|
||||||
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />
|
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<n-button class="flex-1" size="large" @click="logout()">退出登录</n-button>
|
<n-button class="flex-1" size="large" @click="logout()">
|
||||||
|
退出登录
|
||||||
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
:loading="okLoading"
|
:loading="okLoading"
|
||||||
class="ml-20 flex-1"
|
class="ml-20 flex-1"
|
||||||
@@ -44,13 +46,13 @@
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { MeModal } from '@/components'
|
import { MeModal } from '@/components'
|
||||||
import { useModal } from '@/composables'
|
import { useModal } from '@/composables'
|
||||||
import { useUserStore, useAuthStore } from '@/store'
|
import { useAuthStore, useUserStore } from '@/store'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const roles = ref(userStore.roles || [])
|
const roles = ref(userStore.roles || [])
|
||||||
const roleCode = ref(userStore.currentRole?.code ?? roles[0]?.code ?? '')
|
const roleCode = ref(userStore.currentRole?.code ?? roles.value[0]?.code ?? '')
|
||||||
|
|
||||||
const [modalRef, okLoading] = useModal()
|
const [modalRef, okLoading] = useModal()
|
||||||
function open(options) {
|
function open(options) {
|
||||||
@@ -67,7 +69,8 @@ async function setCurrentRole() {
|
|||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
$message.success('切换成功')
|
$message.success('切换成功')
|
||||||
modalRef.value?.handleOk()
|
modalRef.value?.handleOk()
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
const title = import.meta.env.VITE_TITLE
|
const title = import.meta.env.VITE_TITLE
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|||||||
@@ -52,13 +52,16 @@ function handleMenuSelect(key, item) {
|
|||||||
router.push(item.path)
|
router.push(item.path)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
|
if (!item.path)
|
||||||
|
return
|
||||||
router.push(item.path)
|
router.push(item.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style>
|
||||||
.side-menu:not(.n-menu--collapsed) {
|
.side-menu:not(.n-menu--collapsed) {
|
||||||
.n-menu-item-content {
|
.n-menu-item-content {
|
||||||
&::before {
|
&::before {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-dropdown :options="options" @select="handleSelect">
|
<n-dropdown :options="options" @select="handleSelect">
|
||||||
<div class="flex cursor-pointer items-center">
|
<div id="user-dropdown" class="flex cursor-pointer items-center">
|
||||||
<n-avatar round :size="36" :src="userStore.avatar" />
|
<n-avatar round :size="36" :src="userStore.avatar" />
|
||||||
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
|
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
|
||||||
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
|
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
|
|
||||||
import { RoleSelect } from '@/layouts/components'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { RoleSelect } from '@/layouts/components'
|
||||||
|
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -35,7 +35,7 @@ const options = reactive([
|
|||||||
label: '个人资料',
|
label: '个人资料',
|
||||||
key: 'profile',
|
key: 'profile',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:person-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:person-outline text-14' }),
|
||||||
show: computed(() => permissionStore.accessRoutes?.some((item) => item.path === '/profile')),
|
show: computed(() => permissionStore.accessRoutes?.some(item => item.path === '/profile')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '切换角色',
|
label: '切换角色',
|
||||||
@@ -71,7 +71,8 @@ function handleSelect(key) {
|
|||||||
async confirm() {
|
async confirm() {
|
||||||
try {
|
try {
|
||||||
await api.logout()
|
await api.logout()
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
authStore.logout()
|
authStore.logout()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export { default as RoleSelect } from './RoleSelect.vue'
|
export { default as BeginnerGuide } from './BeginnerGuide.vue'
|
||||||
export { default as UserAvatar } from './UserAvatar.vue'
|
|
||||||
export { default as MenuCollapse } from './MenuCollapse.vue'
|
|
||||||
export { default as BreadCrumb } from './BreadCrumb.vue'
|
export { default as BreadCrumb } from './BreadCrumb.vue'
|
||||||
export { default as AppTab } from './tab/index.vue'
|
export { default as Fullscreen } from './Fullscreen.vue'
|
||||||
|
export { default as MenuCollapse } from './MenuCollapse.vue'
|
||||||
|
export { default as RoleSelect } from './RoleSelect.vue'
|
||||||
export { default as SideLogo } from './SideLogo.vue'
|
export { default as SideLogo } from './SideLogo.vue'
|
||||||
export { default as SideMenu } from './SideMenu.vue'
|
export { default as SideMenu } from './SideMenu.vue'
|
||||||
|
export { default as AppTab } from './tab/index.vue'
|
||||||
|
export { default as UserAvatar } from './UserAvatar.vue'
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ const options = computed(() => [
|
|||||||
label: '关闭右侧',
|
label: '关闭右侧',
|
||||||
key: 'close-right',
|
key: 'close-right',
|
||||||
disabled:
|
disabled:
|
||||||
tabStore.tabs.length <= 1 ||
|
tabStore.tabs.length <= 1
|
||||||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
|
|| props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
|
||||||
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
|
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -119,7 +119,8 @@ function handleHideDropdown() {
|
|||||||
|
|
||||||
function handleSelect(key) {
|
function handleSelect(key) {
|
||||||
const actionFn = actionMap.get(key)
|
const actionFn = actionMap.get(key)
|
||||||
actionFn && actionFn()
|
if (typeof actionFn === 'function')
|
||||||
|
actionFn()
|
||||||
handleHideDropdown()
|
handleHideDropdown()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
--------------------------------->
|
--------------------------------->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div id="top-tab">
|
||||||
<n-tabs
|
<n-tabs
|
||||||
:value="tabStore.activeTab"
|
:value="tabStore.activeTab"
|
||||||
:closable="tabStore.tabs.length > 1"
|
:closable="tabStore.tabs.length > 1"
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from './ContextMenu.vue'
|
|
||||||
import { useTabStore } from '@/store'
|
import { useTabStore } from '@/store'
|
||||||
|
import ContextMenu from './ContextMenu.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
@@ -49,7 +49,7 @@ const contextMenuOption = reactive({
|
|||||||
currentPath: '',
|
currentPath: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleItemClick = (path) => {
|
function handleItemClick(path) {
|
||||||
tabStore.setActiveTab(path)
|
tabStore.setActiveTab(path)
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ async function handleContextMenu(e, tagItem) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
:deep(.n-tabs) {
|
:deep(.n-tabs) {
|
||||||
.n-tabs-tab {
|
.n-tabs-tab {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
|||||||
@@ -13,16 +13,11 @@
|
|||||||
<BreadCrumb />
|
<BreadCrumb />
|
||||||
|
|
||||||
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
|
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
|
||||||
<i
|
<BeginnerGuide />
|
||||||
class="mr-16 cursor-pointer"
|
|
||||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
<ToggleTheme />
|
||||||
@click="toggleDark"
|
|
||||||
/>
|
<Fullscreen />
|
||||||
<i
|
|
||||||
class="mr-16 cursor-pointer"
|
|
||||||
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
|
||||||
@click="toggle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<i
|
<i
|
||||||
class="i-fe:github mr-16 cursor-pointer"
|
class="i-fe:github mr-16 cursor-pointer"
|
||||||
@@ -41,18 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { MenuCollapse, UserAvatar, BreadCrumb } from '@/layouts/components'
|
import { ToggleTheme } from '@/components'
|
||||||
import { useAppStore } from '@/store'
|
import { BeginnerGuide, BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
|
||||||
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const isDark = useDark()
|
|
||||||
const toggleDark = () => {
|
|
||||||
appStore.toggleDark()
|
|
||||||
useToggle(isDark)()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isFullscreen, toggle } = useFullscreen()
|
|
||||||
|
|
||||||
function handleLinkClick(link) {
|
function handleLinkClick(link) {
|
||||||
window.open(link)
|
window.open(link)
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAppStore } from '@/store'
|
|
||||||
import { AppTab } from '@/layouts/components'
|
import { AppTab } from '@/layouts/components'
|
||||||
import SideBar from './sidebar/index.vue'
|
import { useAppStore } from '@/store'
|
||||||
import AppHeader from './header/index.vue'
|
import AppHeader from './header/index.vue'
|
||||||
|
import SideBar from './sidebar/index.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,16 +15,11 @@
|
|||||||
<span class="mx-6 opacity-20">|</span>
|
<span class="mx-6 opacity-20">|</span>
|
||||||
|
|
||||||
<div class="flex flex-shrink-0 items-center px-12 text-18">
|
<div class="flex flex-shrink-0 items-center px-12 text-18">
|
||||||
<i
|
<BeginnerGuide />
|
||||||
class="mr-16 cursor-pointer"
|
|
||||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
<ToggleTheme />
|
||||||
@click="toggleDark"
|
|
||||||
/>
|
<Fullscreen />
|
||||||
<i
|
|
||||||
class="mr-16 cursor-pointer"
|
|
||||||
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
|
||||||
@click="toggle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<i
|
<i
|
||||||
class="i-fe:github mr-16 cursor-pointer"
|
class="i-fe:github mr-16 cursor-pointer"
|
||||||
@@ -43,24 +38,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { UserAvatar, MenuCollapse, AppTab } from '@/layouts/components'
|
import { ToggleTheme } from '@/components'
|
||||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
import { AppTab, BeginnerGuide, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||||
import { useAppStore } from '@/store'
|
|
||||||
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const isDark = useDark()
|
|
||||||
const toggleDark = () => {
|
|
||||||
appStore.toggleDark()
|
|
||||||
useToggle(isDark)()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isFullscreen, toggle } = useFullscreen()
|
|
||||||
|
|
||||||
function handleLinkClick(link) {
|
function handleLinkClick(link) {
|
||||||
window.open(link)
|
window.open(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import SideBar from './sidebar/index.vue'
|
|
||||||
import AppHeader from './header/index.vue'
|
import AppHeader from './header/index.vue'
|
||||||
|
import SideBar from './sidebar/index.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { UserAvatar, MenuCollapse, SideLogo, SideMenu } from '@/layouts/components'
|
import { MenuCollapse, SideLogo, SideMenu, UserAvatar } from '@/layouts/components'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|||||||
10
src/main.js
10
src/main.js
@@ -8,16 +8,16 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import '@/styles/reset.css'
|
|
||||||
import '@/styles/global.scss'
|
|
||||||
import 'uno.css'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { setupDirectives } from './directives'
|
||||||
|
|
||||||
import { setupRouter } from './router'
|
import { setupRouter } from './router'
|
||||||
import { setupStore } from './store'
|
import { setupStore } from './store'
|
||||||
import { setupNaiveDiscreteApi } from './utils'
|
import { setupNaiveDiscreteApi } from './utils'
|
||||||
import { setupDirectives } from './directives'
|
import '@/styles/reset.css'
|
||||||
|
import '@/styles/global.css'
|
||||||
|
import 'uno.css'
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export function createPageTitleGuard(router) {
|
|||||||
const pageTitle = to.meta?.title
|
const pageTitle = to.meta?.title
|
||||||
if (pageTitle) {
|
if (pageTitle) {
|
||||||
document.title = `${pageTitle} | ${baseTitle}`
|
document.title = `${pageTitle} | ${baseTitle}`
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
document.title = baseTitle
|
document.title = baseTitle
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||||
import { getPermissions, getUserInfo } from '@/store/helper'
|
import { getPermissions, getUserInfo } from '@/store/helper'
|
||||||
|
|
||||||
const WHITE_LIST = ['/login', '/404']
|
const WHITE_LIST = ['/login', '/404']
|
||||||
@@ -18,13 +18,16 @@ export function createPermissionGuard(router) {
|
|||||||
|
|
||||||
/** 没有token */
|
/** 没有token */
|
||||||
if (!token) {
|
if (!token) {
|
||||||
if (WHITE_LIST.includes(to.path)) return true
|
if (WHITE_LIST.includes(to.path))
|
||||||
|
return true
|
||||||
return { path: 'login', query: { ...to.query, redirect: to.path } }
|
return { path: 'login', query: { ...to.query, redirect: to.path } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有token的情况
|
// 有token的情况
|
||||||
if (to.path === '/login') return { path: '/' }
|
if (to.path === '/login')
|
||||||
if (WHITE_LIST.includes(to.path)) return true
|
return { path: '/' }
|
||||||
|
if (WHITE_LIST.includes(to.path))
|
||||||
|
return true
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
@@ -41,7 +44,8 @@ export function createPermissionGuard(router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const routes = router.getRoutes()
|
const routes = router.getRoutes()
|
||||||
if (routes.find((route) => route.name === to.name)) return true
|
if (routes.find(route => route.name === to.name))
|
||||||
|
return true
|
||||||
|
|
||||||
// 判断是无权限还是404
|
// 判断是无权限还是404
|
||||||
const { data: hasMenu } = await api.validateMenuPath(to.path)
|
const { data: hasMenu } = await api.validateMenuPath(to.path)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export const EXCLUDE_TAB = ['/404', '/403', '/login']
|
|||||||
|
|
||||||
export function createTabGuard(router) {
|
export function createTabGuard(router) {
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
if (EXCLUDE_TAB.includes(to.path)) return
|
if (EXCLUDE_TAB.includes(to.path))
|
||||||
|
return
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const { name, fullPath: path } = to
|
const { name, fullPath: path } = to
|
||||||
const title = to.meta?.title
|
const title = to.meta?.title
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||||
import { setupRouterGuards } from './guards'
|
|
||||||
import { basicRoutes } from './basic-routes'
|
import { basicRoutes } from './basic-routes'
|
||||||
|
import { setupRouterGuards } from './guards'
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history:
|
history:
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const defaultLayout = 'normal'
|
|||||||
|
|
||||||
export const defaultPrimaryColor = '#316C72'
|
export const defaultPrimaryColor = '#316C72'
|
||||||
|
|
||||||
|
// 控制 LayoutSetting 组件是否可见
|
||||||
|
export const layoutSettingVisible = true
|
||||||
|
|
||||||
export const naiveThemeOverrides = {
|
export const naiveThemeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
primaryColor: '#316C72FF',
|
primaryColor: '#316C72FF',
|
||||||
@@ -33,7 +36,7 @@ export const basePermissions = [
|
|||||||
code: 'ShowDocs',
|
code: 'ShowDocs',
|
||||||
name: '项目文档',
|
name: '项目文档',
|
||||||
type: 'MENU',
|
type: 'MENU',
|
||||||
path: 'https://docs.isme.top/web/#/624306705/188522224',
|
path: 'https://isme.top',
|
||||||
icon: 'i-me:docs',
|
icon: 'i-me:docs',
|
||||||
order: 1,
|
order: 1,
|
||||||
enable: true,
|
enable: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { basePermissions } from '@/settings'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { basePermissions } from '@/settings'
|
||||||
|
|
||||||
export async function getUserInfo() {
|
export async function getUserInfo() {
|
||||||
const res = await api.getUser()
|
const res = await api.getUser()
|
||||||
@@ -22,8 +23,9 @@ export async function getPermissions() {
|
|||||||
try {
|
try {
|
||||||
const res = await api.getRolePermissions()
|
const res = await api.getRolePermissions()
|
||||||
asyncPermissions = res?.data || []
|
asyncPermissions = res?.data || []
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
return basePermissions.concat(asyncPermissions)
|
return cloneDeep(basePermissions).concat(asyncPermissions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { useDark } from '@vueuse/core'
|
|
||||||
import { generate, getRgbStr } from '@arco-design/color'
|
import { generate, getRgbStr } from '@arco-design/color'
|
||||||
|
import { useDark } from '@vueuse/core'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
@@ -50,7 +50,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
paths: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
pick: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useUserStore, usePermissionStore, useTabStore, useRouterStore } from '@/store'
|
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './app'
|
export * from './app'
|
||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './permission'
|
export * from './permission'
|
||||||
|
export * from './router'
|
||||||
export * from './tab'
|
export * from './tab'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './router'
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { isExternal } from '@/utils'
|
|
||||||
import { hyphenate } from '@vueuse/core'
|
import { hyphenate } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { isExternal } from '@/utils'
|
||||||
|
|
||||||
export const usePermissionStore = defineStore('permission', {
|
export const usePermissionStore = defineStore('permission', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -20,15 +20,15 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
setPermissions(permissions) {
|
setPermissions(permissions) {
|
||||||
this.permissions = permissions
|
this.permissions = permissions
|
||||||
this.menus = this.permissions
|
this.menus = this.permissions
|
||||||
.filter((item) => item.type === 'MENU')
|
.filter(item => item.type === 'MENU')
|
||||||
.map((item) => this.getMenuItem(item))
|
.map(item => this.getMenuItem(item))
|
||||||
.filter((item) => !!item)
|
.filter(item => !!item)
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
},
|
},
|
||||||
getMenuItem(item, parent) {
|
getMenuItem(item, parent) {
|
||||||
const route = this.generateRoute(item, item.show ? null : parent?.key)
|
const route = this.generateRoute(item, item.show ? null : parent?.key)
|
||||||
if (item.enable && route.path && !route.path.startsWith('http')) this.accessRoutes.push(route)
|
if (item.enable && route.path && !route.path.startsWith('http'))
|
||||||
if (!item.show) return null
|
this.accessRoutes.push(route)
|
||||||
const menuItem = {
|
const menuItem = {
|
||||||
label: route.meta.title,
|
label: route.meta.title,
|
||||||
key: route.name,
|
key: route.name,
|
||||||
@@ -37,18 +37,21 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
icon: () => h('i', { class: `${route.meta.icon} text-16` }),
|
icon: () => h('i', { class: `${route.meta.icon} text-16` }),
|
||||||
order: item.order ?? 0,
|
order: item.order ?? 0,
|
||||||
}
|
}
|
||||||
const children = item.children?.filter((item) => item.type === 'MENU') || []
|
const children = item.children?.filter(item => item.type === 'MENU') || []
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
menuItem.children = children
|
menuItem.children = children
|
||||||
.map((child) => this.getMenuItem(child, menuItem))
|
.map(child => this.getMenuItem(child, menuItem))
|
||||||
.filter((item) => !!item)
|
.filter(item => !!item)
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
if (!menuItem.children.length) delete menuItem.children
|
if (!menuItem.children.length)
|
||||||
|
delete menuItem.children
|
||||||
}
|
}
|
||||||
|
if (!item.show)
|
||||||
|
return null
|
||||||
return menuItem
|
return menuItem
|
||||||
},
|
},
|
||||||
generateRoute(item, parentKey) {
|
generateRoute(item, parentKey) {
|
||||||
let originPath = undefined
|
let originPath
|
||||||
if (isExternal(item.path)) {
|
if (isExternal(item.path)) {
|
||||||
originPath = item.path
|
originPath = item.path
|
||||||
item.component = '/src/views/iframe/index.vue'
|
item.component = '/src/views/iframe/index.vue'
|
||||||
@@ -61,14 +64,14 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
component: item.component,
|
component: item.component,
|
||||||
meta: {
|
meta: {
|
||||||
originPath,
|
originPath,
|
||||||
icon: item.icon + '?mask',
|
icon: `${item.icon}?mask`,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
layout: item.layout,
|
layout: item.layout,
|
||||||
keepAlive: !!item.keepAlive,
|
keepAlive: !!item.keepAlive,
|
||||||
parentKey,
|
parentKey,
|
||||||
btns: item.children
|
btns: item.children
|
||||||
?.filter((item) => item.type === 'BUTTON')
|
?.filter(item => item.type === 'BUTTON')
|
||||||
.map((item) => ({ code: item.code, name: item.name })),
|
.map(item => ({ code: item.code, name: item.name })),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const useTabStore = defineStore('tab', {
|
|||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
activeIndex() {
|
activeIndex() {
|
||||||
return this.tabs.findIndex((item) => item.path === this.activeTab)
|
return this.tabs.findIndex(item => item.path === this.activeTab)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@@ -29,19 +29,22 @@ export const useTabStore = defineStore('tab', {
|
|||||||
this.tabs = tabs
|
this.tabs = tabs
|
||||||
},
|
},
|
||||||
addTab(tab = {}) {
|
addTab(tab = {}) {
|
||||||
const findIndex = this.tabs.findIndex((item) => item.path === tab.path)
|
const findIndex = this.tabs.findIndex(item => item.path === tab.path)
|
||||||
if (findIndex !== -1) {
|
if (findIndex !== -1) {
|
||||||
this.tabs.splice(findIndex, 1, tab)
|
this.tabs.splice(findIndex, 1, tab)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
this.setTabs([...this.tabs, tab])
|
this.setTabs([...this.tabs, tab])
|
||||||
}
|
}
|
||||||
this.setActiveTab(tab.path)
|
this.setActiveTab(tab.path)
|
||||||
},
|
},
|
||||||
async reloadTab(path, keepAlive) {
|
async reloadTab(path, keepAlive) {
|
||||||
const findItem = this.tabs.find((item) => item.path === path)
|
const findItem = this.tabs.find(item => item.path === path)
|
||||||
if (!findItem) return
|
if (!findItem)
|
||||||
|
return
|
||||||
// 更新key可让keepAlive失效
|
// 更新key可让keepAlive失效
|
||||||
if (keepAlive) findItem.keepAlive = false
|
if (keepAlive)
|
||||||
|
findItem.keepAlive = false
|
||||||
$loadingBar.start()
|
$loadingBar.start()
|
||||||
this.reloading = true
|
this.reloading = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -53,30 +56,30 @@ export const useTabStore = defineStore('tab', {
|
|||||||
}, 100)
|
}, 100)
|
||||||
},
|
},
|
||||||
async removeTab(path) {
|
async removeTab(path) {
|
||||||
this.setTabs(this.tabs.filter((tab) => tab.path !== path))
|
this.setTabs(this.tabs.filter(tab => tab.path !== path))
|
||||||
if (path === this.activeTab) {
|
if (path === this.activeTab) {
|
||||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeOther(curPath = this.activeTab) {
|
removeOther(curPath = this.activeTab) {
|
||||||
this.setTabs(this.tabs.filter((tab) => tab.path === curPath))
|
this.setTabs(this.tabs.filter(tab => tab.path === curPath))
|
||||||
if (curPath !== this.activeTab) {
|
if (curPath !== this.activeTab) {
|
||||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeLeft(curPath) {
|
removeLeft(curPath) {
|
||||||
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
|
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
||||||
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
|
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
|
||||||
this.setTabs(filterTabs)
|
this.setTabs(filterTabs)
|
||||||
if (!filterTabs.find((item) => item.path === this.activeTab)) {
|
if (!filterTabs.find(item => item.path === this.activeTab)) {
|
||||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeRight(curPath) {
|
removeRight(curPath) {
|
||||||
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
|
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
||||||
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
|
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
|
||||||
this.setTabs(filterTabs)
|
this.setTabs(filterTabs)
|
||||||
if (!filterTabs.find((item) => item.path === this.activeTab.value)) {
|
if (!filterTabs.find(item => item.path === this.activeTab.value)) {
|
||||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -85,7 +88,7 @@ export const useTabStore = defineStore('tab', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
paths: ['tabs'],
|
pick: ['tabs'],
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ body {
|
|||||||
|
|
||||||
.fade-slide-enter-from {
|
.fade-slide-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-30px);
|
transform: translateX(-2%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-leave-to {
|
.fade-slide-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(30px);
|
transform: translateX(2%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义滚动条样式 */
|
/* 自定义滚动条样式 */
|
||||||
@@ -72,3 +72,20 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 切换主题的动画效果 */
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
.dark::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root),
|
||||||
|
.dark::view-transition-old(root) {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
@@ -1,49 +1,389 @@
|
|||||||
/**********************************
|
/*
|
||||||
* @Author: Ronnie Zhang
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
* @LastEditor: Ronnie Zhang
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
* @LastEditTime: 2023/12/05 21:26:38
|
2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`
|
||||||
* @Email: zclzone@outlook.com
|
*/
|
||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
|
||||||
**********************************/
|
|
||||||
|
|
||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::before,
|
::before,
|
||||||
::after {
|
::after {
|
||||||
margin: 0;
|
box-sizing: border-box; /* 1 */
|
||||||
padding: 0;
|
border-width: 0; /* 2 */
|
||||||
box-sizing: inherit;
|
border-style: solid; /* 2 */
|
||||||
|
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--un-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-moz-tab-size: 4; /* 3 */
|
||||||
|
tab-size: 4; /* 3 */
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */
|
||||||
|
font-feature-settings: normal; /* 5 */
|
||||||
|
font-variation-settings: normal; /* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent; /* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0; /* 1 */
|
||||||
|
line-height: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
border-top-width: 1px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
/*
|
||||||
a:link,
|
Add the correct font weight in Edge and Safari.
|
||||||
a:visited,
|
*/
|
||||||
a:active {
|
|
||||||
text-decoration: none;
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
|
||||||
|
font-feature-settings: normal; /* 2 */
|
||||||
|
font-variation-settings: normal; /* 3 */
|
||||||
|
font-size: 1em; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0; /* 1 */
|
||||||
|
border-color: inherit; /* 2 */
|
||||||
|
border-collapse: collapse; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-feature-settings: inherit; /* 1 */
|
||||||
|
font-variation-settings: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
font-weight: inherit; /* 1 */
|
||||||
|
line-height: inherit; /* 1 */
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
/* background-color: transparent; */
|
||||||
|
background-image: none; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol,
|
ol,
|
||||||
ul {
|
ul,
|
||||||
|
menu {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
dialog {
|
||||||
textarea {
|
padding: 0;
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
resize: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1; /* 1 */
|
||||||
|
color: #9ca3af; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role='button'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block; /* 1 */
|
||||||
|
vertical-align: middle; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make elements with the HTML hidden attribute stay hidden by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 格式化时间
|
* @param {(object | string | number)} time
|
||||||
* @param {(Object|string|number)} time
|
|
||||||
* @param {string} format
|
* @param {string} format
|
||||||
* @returns {string | null}
|
* @returns {string | null} 格式化后的时间字符串
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
|
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
return dayjs(time).format(format)
|
return dayjs(time).format(format)
|
||||||
@@ -24,19 +24,19 @@ export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 函数节流
|
|
||||||
* @param {Function} fn
|
* @param {Function} fn
|
||||||
* @param {Number} wait
|
* @param {number} wait
|
||||||
* @returns {Function}
|
* @returns {Function} 节流函数
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export function throttle(fn, wait) {
|
export function throttle(fn, wait) {
|
||||||
var context, args
|
let context, args
|
||||||
var previous = 0
|
let previous = 0
|
||||||
|
|
||||||
return function () {
|
return function (...argArr) {
|
||||||
var now = +new Date()
|
const now = +new Date()
|
||||||
context = this
|
context = this
|
||||||
args = arguments
|
args = argArr
|
||||||
if (now - previous > wait) {
|
if (now - previous > wait) {
|
||||||
fn.apply(context, args)
|
fn.apply(context, args)
|
||||||
previous = now
|
previous = now
|
||||||
@@ -45,16 +45,15 @@ export function throttle(fn, wait) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 函数防抖
|
* @param {Function} method
|
||||||
* @param {Function} func
|
|
||||||
* @param {number} wait
|
* @param {number} wait
|
||||||
* @param {boolean} immediate
|
* @param {boolean} immediate
|
||||||
* @return {*}
|
* @return {*} 防抖函数
|
||||||
*/
|
*/
|
||||||
export function debounce(method, wait, immediate) {
|
export function debounce(method, wait, immediate) {
|
||||||
let timeout
|
let timeout
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
let context = this
|
const context = this
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
@@ -64,14 +63,15 @@ export function debounce(method, wait, immediate) {
|
|||||||
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
|
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
|
||||||
* 这样确保立即执行后wait毫秒内不会被再次触发
|
* 这样确保立即执行后wait毫秒内不会被再次触发
|
||||||
*/
|
*/
|
||||||
let callNow = !timeout
|
const callNow = !timeout
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
timeout = null
|
timeout = null
|
||||||
}, wait)
|
}, wait)
|
||||||
if (callNow) {
|
if (callNow) {
|
||||||
method.apply(context, args)
|
method.apply(context, args)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// 如果immediate为false,则函数wait毫秒后执行
|
// 如果immediate为false,则函数wait毫秒后执行
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
/**
|
/**
|
||||||
@@ -85,12 +85,11 @@ export function debounce(method, wait, immediate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 睡一会儿,让子弹暂停一下
|
|
||||||
* @param {number} time 毫秒数
|
* @param {number} time 毫秒数
|
||||||
* @returns
|
* @returns 睡一会儿,让子弹暂停一下
|
||||||
*/
|
*/
|
||||||
export function sleep(time) {
|
export function sleep(time) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, time))
|
return new Promise(resolve => setTimeout(resolve, time))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,43 +10,34 @@
|
|||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
|
|
||||||
let isConfirming = false
|
let isConfirming = false
|
||||||
export function resolveResError(code, message) {
|
|
||||||
|
function handleAuthExpired(content, needTip) {
|
||||||
|
if (isConfirming || !needTip)
|
||||||
|
return
|
||||||
|
isConfirming = true
|
||||||
|
$dialog.confirm({
|
||||||
|
title: '提示',
|
||||||
|
type: 'info',
|
||||||
|
content,
|
||||||
|
confirm() {
|
||||||
|
useAuthStore().logout()
|
||||||
|
window.$message?.success('已退出登录')
|
||||||
|
isConfirming = false
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
isConfirming = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveResError(code, message, needTip = true) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 401:
|
case 401:
|
||||||
if (isConfirming) return
|
return handleAuthExpired('登录已过期,是否重新登录?', needTip)
|
||||||
isConfirming = true
|
|
||||||
$dialog.confirm({
|
|
||||||
title: '提示',
|
|
||||||
type: 'info',
|
|
||||||
content: '登录已过期,是否重新登录?',
|
|
||||||
confirm() {
|
|
||||||
useAuthStore().logout()
|
|
||||||
window.$message?.success('已退出登录')
|
|
||||||
isConfirming = false
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
isConfirming = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
case 11007:
|
case 11007:
|
||||||
case 11008:
|
case 11008:
|
||||||
if (isConfirming) return
|
return handleAuthExpired(`${message},是否重新登录?`, needTip)
|
||||||
isConfirming = true
|
|
||||||
$dialog.confirm({
|
|
||||||
title: '提示',
|
|
||||||
type: 'info',
|
|
||||||
content: `${message},是否重新登录?`,
|
|
||||||
confirm() {
|
|
||||||
useAuthStore().logout()
|
|
||||||
window.$message?.success('已退出登录')
|
|
||||||
isConfirming = false
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
isConfirming = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
case 403:
|
case 403:
|
||||||
message = '请求被拒绝'
|
message = '请求被拒绝'
|
||||||
break
|
break
|
||||||
@@ -60,5 +51,6 @@ export function resolveResError(code, message) {
|
|||||||
message = message ?? `【${code}】: 未知异常!`
|
message = message ?? `【${code}】: 未知异常!`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
needTip && window.$message?.error(message)
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,10 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { resolveResError } from './helpers'
|
|
||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
|
import { resolveResError } from './helpers'
|
||||||
|
|
||||||
export function setupInterceptors(axiosInstance) {
|
export function setupInterceptors(axiosInstance) {
|
||||||
function reqResolve(config) {
|
|
||||||
// 处理不需要token的请求
|
|
||||||
if (config.noNeedToken) {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken } = useAuthStore()
|
|
||||||
if (accessToken) {
|
|
||||||
// token: Bearer + xxx
|
|
||||||
config.headers.Authorization = 'Bearer ' + accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
function reqReject(error) {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUCCESS_CODES = [0, 200]
|
const SUCCESS_CODES = [0, 200]
|
||||||
function resResolve(response) {
|
function resResolve(response) {
|
||||||
const { data, status, config, statusText, headers } = response
|
const { data, status, config, statusText, headers } = response
|
||||||
@@ -39,34 +20,51 @@ export function setupInterceptors(axiosInstance) {
|
|||||||
}
|
}
|
||||||
const code = data?.code ?? status
|
const code = data?.code ?? status
|
||||||
|
|
||||||
// 根据code处理对应的操作,并返回处理后的message
|
const needTip = config?.needTip !== false
|
||||||
const message = resolveResError(code, data?.message ?? statusText)
|
|
||||||
|
// 根据code处理对应的操作,并返回处理后的message
|
||||||
|
const message = resolveResError(code, data?.message ?? statusText, needTip)
|
||||||
|
|
||||||
//需要错误提醒
|
|
||||||
!config?.noNeedTip && message && window.$message?.error(message)
|
|
||||||
return Promise.reject({ code, message, error: data ?? response })
|
return Promise.reject({ code, message, error: data ?? response })
|
||||||
}
|
}
|
||||||
return Promise.resolve(data ?? response)
|
return Promise.resolve(data ?? response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(reqResolve, reqReject)
|
||||||
|
axiosInstance.interceptors.response.use(resResolve, resReject)
|
||||||
|
}
|
||||||
|
|
||||||
|
function reqResolve(config) {
|
||||||
|
// 处理不需要token的请求
|
||||||
|
if (config.needToken === false) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = useAuthStore()
|
||||||
|
if (accessToken) {
|
||||||
|
// token: Bearer + xxx
|
||||||
|
config.headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
function reqReject(error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
async function resReject(error) {
|
async function resReject(error) {
|
||||||
if (!error || !error.response) {
|
if (!error || !error.response) {
|
||||||
const code = error?.code
|
const code = error?.code
|
||||||
/** 根据code处理对应的操作,并返回处理后的message */
|
/** 根据code处理对应的操作,并返回处理后的message */
|
||||||
const message = resolveResError(code, error.message)
|
const message = resolveResError(code, error.message)
|
||||||
window.$message?.error(message)
|
|
||||||
return Promise.reject({ code, message, error })
|
return Promise.reject({ code, message, error })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, status, config } = error.response
|
const { data, status, config } = error.response
|
||||||
const code = data?.code ?? status
|
const code = data?.code ?? status
|
||||||
|
|
||||||
const message = resolveResError(code, data?.message ?? error.message)
|
const needTip = config?.needTip !== false
|
||||||
/** 需要错误提醒 */
|
const message = resolveResError(code, data?.message ?? error.message, needTip)
|
||||||
!config?.noNeedTip && message && window.$message?.error(message)
|
|
||||||
return Promise.reject({ code, message, error: error.response?.data || error.response })
|
return Promise.reject({ code, message, error: error.response?.data || error.response })
|
||||||
}
|
}
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use(reqResolve, reqReject)
|
|
||||||
axiosInstance.interceptors.response.use(resResolve, resReject)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -99,28 +99,27 @@ export function isEmpty(val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* * 类似mysql的IFNULL函数
|
* 类似mysql的IFNULL函数
|
||||||
* * 第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数
|
*
|
||||||
* @param {Number|Boolean|String} val
|
* @param {number | boolean | string} val
|
||||||
* @param {Number|Boolean|String} def
|
* @param {number | boolean | string} def
|
||||||
* @returns
|
* @returns 第一个参数为null | undefined | '' 则返回第二个参数作为备用值,否则返回第一个参数
|
||||||
*/
|
*/
|
||||||
export function ifNull(val, def = '') {
|
export function ifNull(val, def = '') {
|
||||||
return isNullOrWhitespace(val) ? def : val
|
return isNullOrWhitespace(val) ? def : val
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUrl(path) {
|
export function isUrl(path) {
|
||||||
const reg =
|
const reg = /^https?:\/\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#/%=~|]$/
|
||||||
/(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
|
|
||||||
return reg.test(path)
|
return reg.test(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {Boolean}
|
* @returns {boolean} 是否是外部链接
|
||||||
*/
|
*/
|
||||||
export function isExternal(path) {
|
export function isExternal(path) {
|
||||||
return /^(https?:|mailto:|tel:)/.test(path)
|
return /^https?:|mailto:|tel:/.test(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isServer = typeof window === 'undefined'
|
export const isServer = typeof window === 'undefined'
|
||||||
|
|||||||
@@ -8,15 +8,16 @@
|
|||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import * as NaiveUI from 'naive-ui'
|
import * as NaiveUI from 'naive-ui'
|
||||||
import { isNullOrUndef } from '@/utils'
|
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
import { isNullOrUndef } from '@/utils'
|
||||||
|
|
||||||
export function setupMessage(NMessage) {
|
export function setupMessage(NMessage) {
|
||||||
class Message {
|
class Message {
|
||||||
static instance
|
static instance
|
||||||
constructor() {
|
constructor() {
|
||||||
// 单例模式
|
// 单例模式
|
||||||
if (Message.instance) return Message.instance
|
if (Message.instance)
|
||||||
|
return Message.instance
|
||||||
Message.instance = this
|
Message.instance = this
|
||||||
this.message = {}
|
this.message = {}
|
||||||
this.removeTimer = {}
|
this.removeTimer = {}
|
||||||
@@ -37,7 +38,7 @@ export function setupMessage(NMessage) {
|
|||||||
|
|
||||||
showMessage(type, content, option = {}) {
|
showMessage(type, content, option = {}) {
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content.forEach((msg) => NMessage[type](msg, option))
|
return content.forEach(msg => NMessage[type](msg, option))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!option.key) {
|
if (!option.key) {
|
||||||
@@ -48,7 +49,8 @@ export function setupMessage(NMessage) {
|
|||||||
if (currentMessage) {
|
if (currentMessage) {
|
||||||
currentMessage.type = type
|
currentMessage.type = type
|
||||||
currentMessage.content = content
|
currentMessage.content = content
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
this.message[option.key] = NMessage[type](content, {
|
this.message[option.key] = NMessage[type](content, {
|
||||||
...option,
|
...option,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -109,7 +111,7 @@ export function setupNaiveDiscreteApi() {
|
|||||||
}))
|
}))
|
||||||
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
|
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
|
||||||
['message', 'dialog', 'notification', 'loadingBar'],
|
['message', 'dialog', 'notification', 'loadingBar'],
|
||||||
{ configProviderProps }
|
{ configProviderProps },
|
||||||
)
|
)
|
||||||
|
|
||||||
window.$loadingBar = loadingBar
|
window.$loadingBar = loadingBar
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import { createStorage } from './storage'
|
|||||||
|
|
||||||
const prefixKey = 'vue-naive-admin_'
|
const prefixKey = 'vue-naive-admin_'
|
||||||
|
|
||||||
export const createLocalStorage = function (option = {}) {
|
export function createLocalStorage(option = {}) {
|
||||||
return createStorage({
|
return createStorage({
|
||||||
prefixKey: option.prefixKey || '',
|
prefixKey: option.prefixKey || '',
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSessionStorage = function (option = {}) {
|
export function createSessionStorage(option = {}) {
|
||||||
return createStorage({
|
return createStorage({
|
||||||
prefixKey: option.prefixKey || '',
|
prefixKey: option.prefixKey || '',
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class Storage {
|
|||||||
|
|
||||||
getItem(key, def = null) {
|
getItem(key, def = null) {
|
||||||
const val = this.storage.getItem(this.getKey(key))
|
const val = this.storage.getItem(this.getKey(key))
|
||||||
if (!val) return def
|
if (!val)
|
||||||
|
return def
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(val)
|
const data = JSON.parse(val)
|
||||||
const { value, time, expire } = data
|
const { value, time, expire } = data
|
||||||
@@ -44,7 +45,9 @@ class Storage {
|
|||||||
}
|
}
|
||||||
this.remove(key)
|
this.remove(key)
|
||||||
return def
|
return def
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
this.remove(key)
|
this.remove(key)
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,24 @@
|
|||||||
<n-card title="按钮 Button">
|
<n-card title="按钮 Button">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button>Default</n-button>
|
<n-button>Default</n-button>
|
||||||
<n-button type="tertiary">Tertiary</n-button>
|
<n-button type="tertiary">
|
||||||
<n-button type="primary">Primary</n-button>
|
Tertiary
|
||||||
<n-button type="info">Info</n-button>
|
</n-button>
|
||||||
<n-button type="success">Success</n-button>
|
<n-button type="primary">
|
||||||
<n-button type="warning">Warning</n-button>
|
Primary
|
||||||
<n-button type="error">Error</n-button>
|
</n-button>
|
||||||
|
<n-button type="info">
|
||||||
|
Info
|
||||||
|
</n-button>
|
||||||
|
<n-button type="success">
|
||||||
|
Success
|
||||||
|
</n-button>
|
||||||
|
<n-button type="warning">
|
||||||
|
Warning
|
||||||
|
</n-button>
|
||||||
|
<n-button type="error">
|
||||||
|
Error
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -46,10 +58,18 @@
|
|||||||
<n-space size="large" mt-30>
|
<n-space size="large" mt-30>
|
||||||
<n-card min-w-340 title="通知 Notification">
|
<n-card min-w-340 title="通知 Notification">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button @click="notify('info')">信息</n-button>
|
<n-button @click="notify('info')">
|
||||||
<n-button @click="notify('success')">成功</n-button>
|
信息
|
||||||
<n-button @click="notify('warning')">警告</n-button>
|
</n-button>
|
||||||
<n-button @click="notify('error')">错误</n-button>
|
<n-button @click="notify('success')">
|
||||||
|
成功
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="notify('warning')">
|
||||||
|
警告
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="notify('error')">
|
||||||
|
错误
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -66,7 +86,9 @@
|
|||||||
<i v-show="!loading" class="i-mdi:login mr-4" />
|
<i v-show="!loading" class="i-mdi:login mr-4" />
|
||||||
登录
|
登录
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="error" @click="handleMultiMessage">多个错误提醒</n-button>
|
<n-button type="error" @click="handleMultiMessage">
|
||||||
|
多个错误提醒
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -75,7 +97,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { sleep } from '@/utils'
|
import { sleep } from '@/utils'
|
||||||
const handleDelete = function () {
|
|
||||||
|
function handleDelete() {
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
content: '确认删除?',
|
content: '确认删除?',
|
||||||
confirm() {
|
confirm() {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
<div w-350>
|
<div w-350>
|
||||||
<n-input v-model:value="inputVal" />
|
<n-input v-model:value="inputVal" />
|
||||||
<n-input-number v-model:value="number" mt-30 />
|
<n-input-number v-model:value="number" mt-30 />
|
||||||
<p mt-20 text-center text-14 color-gray>注:右击标签重新加载可重置keep-alive</p>
|
<p mt-20 text-center text-14 color-gray>
|
||||||
|
注:右击标签重新加载可重置keep-alive
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CommonPage>
|
</CommonPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPage show-footer>
|
<CommonPage show-footer>
|
||||||
<n-button type="primary" @click="openModal1">打开第一个弹个窗</n-button>
|
<n-button type="primary" @click="openModal1">
|
||||||
|
打开第一个弹个窗
|
||||||
|
</n-button>
|
||||||
<MeModal ref="$modal1">
|
<MeModal ref="$modal1">
|
||||||
<n-input v-model:value="text" />
|
<n-input v-model:value="text" />
|
||||||
</MeModal>
|
</MeModal>
|
||||||
@@ -20,8 +22,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { MeModal } from '@/components'
|
import { MeModal } from '@/components'
|
||||||
import { sleep } from '@/utils'
|
|
||||||
import { useModal } from '@/composables'
|
import { useModal } from '@/composables'
|
||||||
|
import { sleep } from '@/utils'
|
||||||
|
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const [$modal1, okLoading1] = useModal()
|
const [$modal1, okLoading1] = useModal()
|
||||||
|
|||||||
@@ -9,16 +9,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<CommonPage show-footer>
|
<CommonPage show-footer>
|
||||||
<template #title-suffix>
|
<template #title-suffix>
|
||||||
<n-tag class="ml-12" type="primary">feather图标集 + isme自定义图标</n-tag>
|
<n-tooltip
|
||||||
|
placement="bottom"
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<a href="https://juejin.cn/post/7394789388154241033" target="_blank" class="ml-12 flex cursor-pointer items-center hover:underline">
|
||||||
|
<i class="i-simple-icons:juejin text-#1E80FF" />
|
||||||
|
<span class="ml-4">Unocss 图标</span>
|
||||||
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<ul class="flex flex-wrap justify-between">
|
点击查看如何使用 Unocss 图标
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
<ul class="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] justify-items-center gap-16">
|
||||||
<li
|
<li
|
||||||
v-for="item in icons"
|
v-for="item in icons"
|
||||||
:key="item"
|
:key="item"
|
||||||
class="m-16 w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg"
|
class="w-160 f-c-c flex-col cursor-pointer card-border rounded-12 auto-bg px-12 py-24"
|
||||||
@click="copy(`<i class="${item}" />`)"
|
@click="copy(`<i class="${item}" />`)"
|
||||||
>
|
>
|
||||||
<i :class="item + '?mask'" class="text-28 text-gray-600 hover:bg-primary" />
|
<i :class="`${item}?mask`" class="text-28 text-gray-600 hover:bg-primary" />
|
||||||
<span
|
<span
|
||||||
class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
|
class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
|
||||||
@click.stop="copy(item)"
|
@click.stop="copy(item)"
|
||||||
@@ -26,13 +37,6 @@
|
|||||||
{{ item }}
|
{{ item }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
<li class="mx-16 h-0 w-160"></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</CommonPage>
|
</CommonPage>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,6 +48,7 @@ import icons from 'isme:icons'
|
|||||||
const { copy, copied } = useClipboard()
|
const { copy, copied } = useClipboard()
|
||||||
|
|
||||||
watch(copied, (val) => {
|
watch(copied, (val) => {
|
||||||
val && $message.success('已复制到剪切板')
|
if (val)
|
||||||
|
$message.success('已复制到剪切板')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div mt-20 w-350 f-c-c flex-col>
|
<div mt-20 w-350 f-c-c flex-col>
|
||||||
<div flex flex-wrap justify-around rounded-10 p-10 card-border>
|
<div flex flex-wrap justify-around card-border rounded-10 p-10>
|
||||||
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
||||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
<n-upload-dragger>
|
<n-upload-dragger>
|
||||||
<div class="h-150 f-c-c flex-col">
|
<div class="h-150 f-c-c flex-col">
|
||||||
<i class="i-mdi:upload mb-12 text-68 color-primary" />
|
<i class="i-mdi:upload mb-12 text-68 color-primary" />
|
||||||
<n-text class="text-14 color-gray">点击或者拖动文件到该区域来上传</n-text>
|
<n-text class="text-14 color-gray">
|
||||||
|
点击或者拖动文件到该区域来上传
|
||||||
|
</n-text>
|
||||||
</div>
|
</div>
|
||||||
</n-upload-dragger>
|
</n-upload-dragger>
|
||||||
</n-upload>
|
</n-upload>
|
||||||
@@ -31,7 +33,9 @@
|
|||||||
<n-image width="200" :src="item.url" />
|
<n-image width="200" :src="item.url" />
|
||||||
</div>
|
</div>
|
||||||
<n-space class="mt-16" justify="space-evenly">
|
<n-space class="mt-16" justify="space-evenly">
|
||||||
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
|
<n-button dashed type="primary" @click="copy(item.url)">
|
||||||
|
url
|
||||||
|
</n-button>
|
||||||
<n-button dashed type="primary" @click="copy(``)">
|
<n-button dashed type="primary" @click="copy(``)">
|
||||||
MD
|
MD
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -53,19 +57,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
defineOptions({ name: 'ImgUpload' })
|
defineOptions({ name: 'ImgUpload' })
|
||||||
|
|
||||||
const { copy, copied } = useClipboard()
|
const { copy, copied } = useClipboard()
|
||||||
|
|
||||||
const imgList = reactive([
|
const imgList = reactive([])
|
||||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
|
||||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
|
||||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
|
||||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
|
||||||
])
|
|
||||||
|
|
||||||
watch(copied, (val) => {
|
watch(copied, (val) => {
|
||||||
val && $message.success('已复制到剪切板')
|
if (val)
|
||||||
|
$message.success('已复制到剪切板')
|
||||||
})
|
})
|
||||||
|
|
||||||
function onBeforeUpload({ file }) {
|
function onBeforeUpload({ file }) {
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
<n-button v-if="back" type="primary" ghost @click="router.replace(back)">
|
<n-button v-if="back" type="primary" ghost @click="router.replace(back)">
|
||||||
返回上一页
|
返回上一页
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="primary" class="ml-20" @click="router.replace('/')">返回首页</n-button>
|
<n-button type="primary" class="ml-20" @click="router.replace('/')">
|
||||||
|
返回首页
|
||||||
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-result>
|
</n-result>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +37,8 @@ const back = history.state.back
|
|||||||
|
|
||||||
if (history.state.from === 'permission-guard') {
|
if (history.state.from === 'permission-guard') {
|
||||||
delete history.state.from
|
delete history.state.from
|
||||||
} else if (route.query.path) {
|
}
|
||||||
|
else if (route.query.path) {
|
||||||
router.replace(route.query.path)
|
router.replace(route.query.path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<n-button type="primary" ghost @click="replace('/')">返回首页</n-button>
|
<n-button type="primary" ghost @click="replace('/')">
|
||||||
|
返回首页
|
||||||
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-result>
|
</n-result>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,16 +20,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功。</p>
|
<p class="mt-28 text-14 opacity-60">
|
||||||
<p class="mt-12 text-right text-12 opacity-40">—— 查尔斯·史考伯</p>
|
一个人几乎可以在任何他怀有无限热忱的事情上成功。
|
||||||
|
</p>
|
||||||
|
<p class="mt-12 text-right text-12 opacity-40">
|
||||||
|
—— 查尔斯·史考伯
|
||||||
|
</p>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<a
|
<a
|
||||||
class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
|
class="text-highlight text-14 text-primary hover:underline hover:opacity-80"
|
||||||
href="https://isme.top"
|
href="https://isme.top"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click.prevent="message?.info('官网正在火速开发中...')"
|
|
||||||
>
|
>
|
||||||
isme.top
|
isme.top
|
||||||
</a>
|
</a>
|
||||||
@@ -45,7 +48,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
ghost
|
ghost
|
||||||
tag="a"
|
tag="a"
|
||||||
href="https://docs.isme.top/web/#/624306705/188522224"
|
href="https://isme.top"
|
||||||
target="__blank"
|
target="__blank"
|
||||||
>
|
>
|
||||||
开发文档
|
开发文档
|
||||||
@@ -65,7 +68,7 @@
|
|||||||
<div class="mt-12 flex">
|
<div class="mt-12 flex">
|
||||||
<n-card class="w-50%" title="💯 特性" segmented>
|
<n-card class="w-50%" title="💯 特性" segmented>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
|
<span class="text-highlight opacity-90">👏 历经十几次重构和细节打磨</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="opacity-90">
|
<ul class="opacity-90">
|
||||||
@@ -156,13 +159,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useUserStore } from '@/store'
|
|
||||||
import * as echarts from 'echarts/core'
|
|
||||||
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
|
|
||||||
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
||||||
|
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
|
||||||
|
import * as echarts from 'echarts/core'
|
||||||
import { UniversalTransition } from 'echarts/features'
|
import { UniversalTransition } from 'echarts/features'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -247,7 +250,7 @@ const skillOption = {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
top: '12%',
|
bottom: '12%',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['35%', '90%'],
|
radius: ['35%', '90%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
@@ -280,6 +283,4 @@ const skillOption = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = $message
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppPage full>
|
<AppPage full>
|
||||||
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full"></iframe>
|
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full" />
|
||||||
</AppPage>
|
</AppPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
toggleRole: (data) => request.post('/auth/role/toggle', data),
|
toggleRole: data => request.post('/auth/role/toggle', data),
|
||||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
login: data => request.post('/auth/login', data, { needToken: false }),
|
||||||
getUser: () => request.get('/user/detail'),
|
getUser: () => request.get('/user/detail'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
|
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
|
||||||
<div
|
<div
|
||||||
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 bg-opacity-20 bg-cover p-12 card-shadow auto-bg"
|
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 auto-bg bg-opacity-20 bg-cover p-12 card-shadow"
|
||||||
>
|
>
|
||||||
<div class="hidden w-380 px-20 py-35 md:block">
|
<div class="hidden w-380 px-20 py-35 md:block">
|
||||||
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner" />
|
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-320 flex-col px-20 py-32">
|
<div class="w-320 flex-col px-20 py-32">
|
||||||
<h2 class="f-c-c text-24 text-#6a6a6a font-normal">
|
<h2 class="f-c-c text-24 text-#6a6a6a font-normal">
|
||||||
<img src="@/assets/images/logo.png" class="mr-12 h-50" />
|
<img src="@/assets/images/logo.png" class="mr-12 h-50">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<n-input
|
<n-input
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
height="40"
|
height="40"
|
||||||
class="ml-12 w-80 cursor-pointer"
|
class="ml-12 w-80 cursor-pointer"
|
||||||
@click="initCaptcha"
|
@click="initCaptcha"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-checkbox
|
<n-checkbox
|
||||||
@@ -101,10 +101,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { throttle, lStorage } from '@/utils'
|
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import api from './api'
|
|
||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
|
import { lStorage, throttle } from '@/utils'
|
||||||
|
import api from './api'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -118,7 +118,7 @@ const loginInfo = ref({
|
|||||||
|
|
||||||
const captchaUrl = ref('')
|
const captchaUrl = ref('')
|
||||||
const initCaptcha = throttle(() => {
|
const initCaptcha = throttle(() => {
|
||||||
captchaUrl.value = import.meta.env.VITE_AXIOS_BASE_URL + '/auth/captcha?' + Date.now()
|
captchaUrl.value = `${import.meta.env.VITE_AXIOS_BASE_URL}/auth/captcha?${Date.now()}`
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
const localLoginInfo = lStorage.get('loginInfo')
|
const localLoginInfo = lStorage.get('loginInfo')
|
||||||
@@ -138,19 +138,23 @@ const isRemember = useStorage('isRemember', true)
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
async function handleLogin(isQuick) {
|
async function handleLogin(isQuick) {
|
||||||
const { username, password, captcha } = loginInfo.value
|
const { username, password, captcha } = loginInfo.value
|
||||||
if (!username || !password) return $message.warning('请输入用户名和密码')
|
if (!username || !password)
|
||||||
if (!isQuick && !captcha) return $message.warning('请输入验证码')
|
return $message.warning('请输入用户名和密码')
|
||||||
|
if (!isQuick && !captcha)
|
||||||
|
return $message.warning('请输入验证码')
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
$message.loading('正在验证,请稍后...', { key: 'login' })
|
$message.loading('正在验证,请稍后...', { key: 'login' })
|
||||||
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
|
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
|
||||||
if (isRemember.value) {
|
if (isRemember.value) {
|
||||||
lStorage.set('loginInfo', { username, password })
|
lStorage.set('loginInfo', { username, password })
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
lStorage.remove('loginInfo')
|
lStorage.remove('loginInfo')
|
||||||
}
|
}
|
||||||
onLoginSuccess(data)
|
onLoginSuccess(data)
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
// 10003为验证码错误专属业务码
|
// 10003为验证码错误专属业务码
|
||||||
if (error?.code === 10003) {
|
if (error?.code === 10003) {
|
||||||
// 为防止爆破,验证码错误则刷新验证码
|
// 为防止爆破,验证码错误则刷新验证码
|
||||||
@@ -171,10 +175,12 @@ async function onLoginSuccess(data = {}) {
|
|||||||
const path = route.query.redirect
|
const path = route.query.redirect
|
||||||
delete route.query.redirect
|
delete route.query.redirect
|
||||||
router.push({ path, query: route.query })
|
router.push({ path, query: route.query })
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$message.destroy('login')
|
$message.destroy('login')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default {
|
|||||||
getMenuTree: () => request.get('/permission/menu/tree'),
|
getMenuTree: () => request.get('/permission/menu/tree'),
|
||||||
getButtons: ({ parentId }) => request.get(`/permission/button/${parentId}`),
|
getButtons: ({ parentId }) => request.get(`/permission/button/${parentId}`),
|
||||||
getComponents: () => axios.get(`${import.meta.env.VITE_PUBLIC_PATH}components.json`),
|
getComponents: () => axios.get(`${import.meta.env.VITE_PUBLIC_PATH}components.json`),
|
||||||
addPermission: (data) => request.post('/permission', data),
|
addPermission: data => request.post('/permission', data),
|
||||||
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
|
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
|
||||||
deletePermission: (id) => request.delete(`permission/${id}`),
|
deletePermission: id => request.delete(`permission/${id}`),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
<h3>菜单</h3>
|
<h3>菜单</h3>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<n-input v-model:value="pattern" placeholder="搜索" clearable />
|
<n-input v-model:value="pattern" placeholder="搜索" clearable />
|
||||||
<n-button class="ml-12" type="primary" @click="handleAdd()">
|
<NButton class="ml-12" type="primary" @click="handleAdd()">
|
||||||
<i class="i-material-symbols:add mr-4 text-14" />
|
<i class="i-material-symbols:add mr-4 text-14" />
|
||||||
新增
|
新增
|
||||||
</n-button>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-tree
|
<n-tree
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
:on-update:selected-keys="onSelect"
|
:on-update:selected-keys="onSelect"
|
||||||
key-field="code"
|
key-field="code"
|
||||||
label-field="name"
|
label-field="name"
|
||||||
block-line
|
|
||||||
default-expand-all
|
block-line default-expand-all
|
||||||
/>
|
/>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { withModifiers } from 'vue'
|
|
||||||
import ResAddOrEdit from './ResAddOrEdit.vue'
|
|
||||||
import { NButton } from 'naive-ui'
|
import { NButton } from 'naive-ui'
|
||||||
|
import { withModifiers } from 'vue'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
|
import ResAddOrEdit from './ResAddOrEdit.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
treeData: {
|
treeData: {
|
||||||
@@ -86,7 +86,7 @@ function renderSuffix({ option }) {
|
|||||||
size: 'tiny',
|
size: 'tiny',
|
||||||
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
|
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
|
||||||
},
|
},
|
||||||
{ default: () => '新增' }
|
{ default: () => '新增' },
|
||||||
),
|
),
|
||||||
|
|
||||||
h(
|
h(
|
||||||
@@ -98,7 +98,7 @@ function renderSuffix({ option }) {
|
|||||||
style: 'margin-left: 12px;',
|
style: 'margin-left: 12px;',
|
||||||
onClick: withModifiers(() => handleDelete(option), ['stop']),
|
onClick: withModifiers(() => handleDelete(option), ['stop']),
|
||||||
},
|
},
|
||||||
{ default: () => '删除' }
|
{ default: () => '删除' },
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,9 @@ function handleDelete(item) {
|
|||||||
$message.success('删除成功', { key: 'deleteMenu' })
|
$message.success('删除成功', { key: 'deleteMenu' })
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
emit('update:currentMenu', null)
|
emit('update:currentMenu', null)
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
$message.destroy('deleteMenu')
|
$message.destroy('deleteMenu')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -99,8 +99,12 @@
|
|||||||
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
|
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
|
||||||
</template>
|
</template>
|
||||||
<n-switch v-model:value="modalForm.show">
|
<n-switch v-model:value="modalForm.show">
|
||||||
<template #checked>显示</template>
|
<template #checked>
|
||||||
<template #unchecked>隐藏</template>
|
显示
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
隐藏
|
||||||
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="12" path="enable">
|
<n-form-item-gi :span="12" path="enable">
|
||||||
@@ -111,8 +115,12 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-switch v-model:value="modalForm.enable">
|
<n-switch v-model:value="modalForm.enable">
|
||||||
<template #checked>启用</template>
|
<template #checked>
|
||||||
<template #unchecked>禁用</template>
|
启用
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
禁用
|
||||||
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="keepAlive">
|
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="keepAlive">
|
||||||
@@ -123,8 +131,12 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-switch v-model:value="modalForm.keepAlive">
|
<n-switch v-model:value="modalForm.keepAlive">
|
||||||
<template #checked>是</template>
|
<template #checked>
|
||||||
<template #unchecked>否</template>
|
是
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
否
|
||||||
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi
|
<n-form-item-gi
|
||||||
@@ -147,12 +159,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import icons from 'isme:icons'
|
||||||
|
import pagePathes from 'isme:page-pathes'
|
||||||
import { MeModal } from '@/components'
|
import { MeModal } from '@/components'
|
||||||
import QuestionLabel from './QuestionLabel.vue'
|
|
||||||
import { useForm, useModal } from '@/composables'
|
import { useForm, useModal } from '@/composables'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
import pagePathes from 'isme:page-pathes'
|
import QuestionLabel from './QuestionLabel.vue'
|
||||||
import icons from 'isme:icons'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
menus: {
|
menus: {
|
||||||
@@ -165,10 +177,10 @@ const emit = defineEmits(['refresh'])
|
|||||||
const menuOptions = computed(() => {
|
const menuOptions = computed(() => {
|
||||||
return [{ name: '根菜单', id: '', children: props.menus || [] }]
|
return [{ name: '根菜单', id: '', children: props.menus || [] }]
|
||||||
})
|
})
|
||||||
const componentOptions = pagePathes.map((path) => ({ label: path, value: path }))
|
const componentOptions = pagePathes.map(path => ({ label: path, value: path }))
|
||||||
const iconOptions = icons.map((item) => ({
|
const iconOptions = icons.map(item => ({
|
||||||
label: () =>
|
label: () =>
|
||||||
h('span', { class: 'flex items-center' }, [h('i', { class: item + ' text-18 mr-8' }), item]),
|
h('span', { class: 'flex items-center' }, [h('i', { class: `${item} text-18 mr-8` }), item]),
|
||||||
value: item,
|
value: item,
|
||||||
}))
|
}))
|
||||||
const layoutOptions = [
|
const layoutOptions = [
|
||||||
@@ -203,17 +215,20 @@ async function onSave() {
|
|||||||
okLoading.value = true
|
okLoading.value = true
|
||||||
try {
|
try {
|
||||||
let newFormData
|
let newFormData
|
||||||
if (!modalForm.value.parentId) modalForm.value.parentId = null
|
if (!modalForm.value.parentId)
|
||||||
|
modalForm.value.parentId = null
|
||||||
if (modalAction.value === 'add') {
|
if (modalAction.value === 'add') {
|
||||||
const res = await api.addPermission(modalForm.value)
|
const res = await api.addPermission(modalForm.value)
|
||||||
newFormData = res.data
|
newFormData = res.data
|
||||||
} else if (modalAction.value === 'edit') {
|
}
|
||||||
|
else if (modalAction.value === 'edit') {
|
||||||
await api.savePermission(modalForm.value.id, modalForm.value)
|
await api.savePermission(modalForm.value.id, modalForm.value)
|
||||||
}
|
}
|
||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
$message.success('保存成功')
|
$message.success('保存成功')
|
||||||
emit('refresh', modalAction.value === 'add' ? newFormData : modalForm.value)
|
emit('refresh', modalAction.value === 'add' ? newFormData : modalForm.value)
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
okLoading.value = false
|
okLoading.value = false
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<n-spin size="small" :show="treeLoading">
|
<n-spin size="small" :show="treeLoading">
|
||||||
<MenuTree
|
<MenuTree
|
||||||
v-model:currentMenu="currentMenu"
|
v-model:current-menu="currentMenu"
|
||||||
class="w-320 shrink-0"
|
class="w-320 shrink-0"
|
||||||
:tree-data="treeData"
|
:tree-data="treeData"
|
||||||
@refresh="initData"
|
@refresh="initData"
|
||||||
@@ -21,15 +21,21 @@
|
|||||||
<div class="ml-40 w-0 flex-1">
|
<div class="ml-40 w-0 flex-1">
|
||||||
<template v-if="currentMenu">
|
<template v-if="currentMenu">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<h3 class="mb-12">{{ currentMenu.name }}</h3>
|
<h3 class="mb-12">
|
||||||
<n-button size="small" type="primary" @click="handleEdit(currentMenu)">
|
{{ currentMenu.name }}
|
||||||
|
</h3>
|
||||||
|
<NButton size="small" type="primary" @click="handleEdit(currentMenu)">
|
||||||
<i class="i-material-symbols:edit-outline mr-4 text-14" />
|
<i class="i-material-symbols:edit-outline mr-4 text-14" />
|
||||||
编辑
|
编辑
|
||||||
</n-button>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
<n-descriptions label-placement="left" bordered :column="2">
|
<n-descriptions label-placement="left" bordered :column="2">
|
||||||
<n-descriptions-item label="编码">{{ currentMenu.code }}</n-descriptions-item>
|
<n-descriptions-item label="编码">
|
||||||
<n-descriptions-item label="名称">{{ currentMenu.name }}</n-descriptions-item>
|
{{ currentMenu.code }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="名称">
|
||||||
|
{{ currentMenu.name }}
|
||||||
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="路由地址">
|
<n-descriptions-item label="路由地址">
|
||||||
{{ currentMenu.path ?? '--' }}
|
{{ currentMenu.path ?? '--' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
@@ -61,11 +67,13 @@
|
|||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|
||||||
<div class="mt-32 flex justify-between">
|
<div class="mt-32 flex justify-between">
|
||||||
<h3 class="mb-12">按钮</h3>
|
<h3 class="mb-12">
|
||||||
<n-button size="small" type="primary" @click="handleAddBtn">
|
按钮
|
||||||
|
</h3>
|
||||||
|
<NButton size="small" type="primary" @click="handleAddBtn">
|
||||||
<i class="i-fe:plus mr-4 text-14" />
|
<i class="i-fe:plus mr-4 text-14" />
|
||||||
新增
|
新增
|
||||||
</n-button>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MeCrud
|
<MeCrud
|
||||||
@@ -74,7 +82,7 @@
|
|||||||
:scroll-x="-1"
|
:scroll-x="-1"
|
||||||
:get-data="api.getButtons"
|
:get-data="api.getButtons"
|
||||||
:query-items="{ parentId: currentMenu.id }"
|
:query-items="{ parentId: currentMenu.id }"
|
||||||
></MeCrud>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
|
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
|
||||||
</div>
|
</div>
|
||||||
@@ -85,13 +93,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NSwitch } from 'naive-ui'
|
import { NButton, NSwitch } from 'naive-ui'
|
||||||
import MenuTree from './components/MenuTree.vue'
|
|
||||||
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
|
||||||
import { MeCrud } from '@/components'
|
import { MeCrud } from '@/components'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
import MenuTree from './components/MenuTree.vue'
|
||||||
|
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
||||||
|
|
||||||
const treeData = ref([])
|
const treeData = ref([])
|
||||||
const treeLoading = ref(false)
|
const treeLoading = ref(false)
|
||||||
|
const $table = ref(null)
|
||||||
|
const currentMenu = ref(null)
|
||||||
async function initData(data) {
|
async function initData(data) {
|
||||||
if (data?.type === 'BUTTON') {
|
if (data?.type === 'BUTTON') {
|
||||||
$table.value.handleSearch()
|
$table.value.handleSearch()
|
||||||
@@ -102,17 +112,16 @@ async function initData(data) {
|
|||||||
treeData.value = res?.data || []
|
treeData.value = res?.data || []
|
||||||
treeLoading.value = false
|
treeLoading.value = false
|
||||||
|
|
||||||
if (data) currentMenu.value = data
|
if (data)
|
||||||
|
currentMenu.value = data
|
||||||
}
|
}
|
||||||
initData()
|
initData()
|
||||||
|
|
||||||
const currentMenu = ref(null)
|
|
||||||
|
|
||||||
const modalRef = ref(null)
|
const modalRef = ref(null)
|
||||||
function handleEdit(item = {}) {
|
function handleEdit(item = {}) {
|
||||||
modalRef.value?.handleOpen({
|
modalRef.value?.handleOpen({
|
||||||
action: 'edit',
|
action: 'edit',
|
||||||
title: '编辑菜单 - ' + item.name,
|
title: `编辑菜单 - ${item.name}`,
|
||||||
row: item,
|
row: item,
|
||||||
okText: '保存',
|
okText: '保存',
|
||||||
})
|
})
|
||||||
@@ -124,7 +133,7 @@ const btnsColumns = [
|
|||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
render: (row) =>
|
render: row =>
|
||||||
h(
|
h(
|
||||||
NSwitch,
|
NSwitch,
|
||||||
{
|
{
|
||||||
@@ -137,7 +146,7 @@ const btnsColumns = [
|
|||||||
{
|
{
|
||||||
checked: () => '启用',
|
checked: () => '启用',
|
||||||
unchecked: () => '停用',
|
unchecked: () => '停用',
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -159,7 +168,7 @@ const btnsColumns = [
|
|||||||
{
|
{
|
||||||
default: () => '编辑',
|
default: () => '编辑',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
h(
|
h(
|
||||||
@@ -173,20 +182,20 @@ const btnsColumns = [
|
|||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const $table = ref(null)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentMenu.value,
|
() => currentMenu.value,
|
||||||
async (v) => {
|
async (v) => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (v) $table.value.handleSearch()
|
if (v)
|
||||||
}
|
$table.value.handleSearch()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleAddBtn() {
|
function handleAddBtn() {
|
||||||
@@ -201,7 +210,7 @@ function handleAddBtn() {
|
|||||||
function handleEditBtn(row) {
|
function handleEditBtn(row) {
|
||||||
modalRef.value?.handleOpen({
|
modalRef.value?.handleOpen({
|
||||||
action: 'edit',
|
action: 'edit',
|
||||||
title: '编辑按钮 - ' + row.name,
|
title: `编辑按钮 - ${row.name}`,
|
||||||
row,
|
row,
|
||||||
okText: '保存',
|
okText: '保存',
|
||||||
})
|
})
|
||||||
@@ -220,7 +229,9 @@ function handleDeleteBtn(id) {
|
|||||||
$message.success('删除成功')
|
$message.success('删除成功')
|
||||||
$table.value.handleSearch()
|
$table.value.handleSearch()
|
||||||
d.loading = false
|
d.loading = false
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
d.loading = false
|
d.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -236,7 +247,9 @@ async function handleEnable(item) {
|
|||||||
$message.success('操作成功')
|
$message.success('操作成功')
|
||||||
$table.value?.handleSearch()
|
$table.value?.handleSearch()
|
||||||
item.enableLoading = false
|
item.enableLoading = false
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
item.enableLoading = false
|
item.enableLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
create: (data) => request.post('/role', data),
|
create: data => request.post('/role', data),
|
||||||
read: (params = {}) => request.get('/role/page', { params }),
|
read: (params = {}) => request.get('/role/page', { params }),
|
||||||
update: (data) => request.patch(`/role/${data.id}`, data),
|
update: data => request.patch(`/role/${data.id}`, data),
|
||||||
delete: (id) => request.delete(`/role/${id}`),
|
delete: id => request.delete(`/role/${id}`),
|
||||||
|
|
||||||
getAllPermissionTree: () => request.get('/permission/tree'),
|
getAllPermissionTree: () => request.get('/permission/tree'),
|
||||||
getAllUsers: (params = {}) => request.get('/user', { params }),
|
getAllUsers: (params = {}) => request.get('/user', { params }),
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<CommonPage>
|
<CommonPage>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button type="primary" @click="handleAdd()">
|
<NButton type="primary" @click="handleAdd()">
|
||||||
<i class="i-material-symbols:add mr-4 text-18" />
|
<i class="i-material-symbols:add mr-4 text-18" />
|
||||||
新增角色
|
新增角色
|
||||||
</n-button>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<MeCrud
|
<MeCrud
|
||||||
@@ -74,17 +74,20 @@
|
|||||||
:data="permissionTree"
|
:data="permissionTree"
|
||||||
:checked-keys="modalForm.permissionIds"
|
:checked-keys="modalForm.permissionIds"
|
||||||
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
|
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
|
||||||
default-expand-all
|
|
||||||
checkable
|
checkable check-on-click default-expand-all
|
||||||
check-on-click
|
|
||||||
class="cus-scroll max-h-200 w-full"
|
class="cus-scroll max-h-200 w-full"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="状态" path="enable">
|
<n-form-item label="状态" path="enable">
|
||||||
<n-switch v-model:value="modalForm.enable">
|
<NSwitch v-model:value="modalForm.enable">
|
||||||
<template #checked>启用</template>
|
<template #checked>
|
||||||
<template #unchecked>停用</template>
|
启用
|
||||||
</n-switch>
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
停用
|
||||||
|
</template>
|
||||||
|
</NSwitch>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</MeModal>
|
</MeModal>
|
||||||
@@ -93,7 +96,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NSwitch } from 'naive-ui'
|
import { NButton, NSwitch } from 'naive-ui'
|
||||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||||
import { useCrud } from '@/composables'
|
import { useCrud } from '@/composables'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
@@ -109,13 +112,23 @@ onMounted(() => {
|
|||||||
$table.value?.handleSearch()
|
$table.value?.handleSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit }
|
||||||
|
= useCrud({
|
||||||
|
name: '角色',
|
||||||
|
doCreate: api.create,
|
||||||
|
doDelete: api.delete,
|
||||||
|
doUpdate: api.update,
|
||||||
|
initForm: { enable: true },
|
||||||
|
refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
|
||||||
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '角色名', key: 'name' },
|
{ title: '角色名', key: 'name' },
|
||||||
{ title: '角色编码', key: 'code' },
|
{ title: '角色编码', key: 'code' },
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
render: (row) =>
|
render: row =>
|
||||||
h(
|
h(
|
||||||
NSwitch,
|
NSwitch,
|
||||||
{
|
{
|
||||||
@@ -129,7 +142,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
checked: () => '启用',
|
checked: () => '启用',
|
||||||
unchecked: () => '停用',
|
unchecked: () => '停用',
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,7 +165,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '分配用户',
|
default: () => '分配用户',
|
||||||
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
|
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
@@ -166,7 +179,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '编辑',
|
default: () => '编辑',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
h(
|
h(
|
||||||
@@ -181,7 +194,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -195,21 +208,13 @@ async function handleEnable(row) {
|
|||||||
row.enableLoading = false
|
row.enableLoading = false
|
||||||
$message.success('操作成功')
|
$message.success('操作成功')
|
||||||
$table.value?.handleSearch()
|
$table.value?.handleSearch()
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
row.enableLoading = false
|
row.enableLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit } =
|
|
||||||
useCrud({
|
|
||||||
name: '角色',
|
|
||||||
doCreate: api.create,
|
|
||||||
doDelete: api.delete,
|
|
||||||
doUpdate: api.update,
|
|
||||||
initForm: { enable: true },
|
|
||||||
refresh: () => $table.value?.handleSearch(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const permissionTree = ref([])
|
const permissionTree = ref([])
|
||||||
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
|
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,15 +9,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<CommonPage back>
|
<CommonPage back>
|
||||||
<template #title-suffix>
|
<template #title-suffix>
|
||||||
<n-tag class="ml-12" type="warning">{{ route.query.roleName }}</n-tag>
|
<NTag class="ml-12" type="warning">
|
||||||
|
{{ route.query.roleName }}
|
||||||
|
</NTag>
|
||||||
</template>
|
</template>
|
||||||
<template #action>
|
<template #action>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<n-button :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
|
<NButton :disabled="!userIds.length" type="error" @click="handleBatchRemove()">
|
||||||
<i v-if="userIds.length" class="i-material-symbols:delete-outline mr-4 text-18" />
|
<i v-if="userIds.length" class="i-material-symbols:delete-outline mr-4 text-18" />
|
||||||
批量取消授权
|
批量取消授权
|
||||||
</n-button>
|
</NButton>
|
||||||
<n-button
|
<NButton
|
||||||
class="ml-12"
|
class="ml-12"
|
||||||
:disabled="!userIds.length"
|
:disabled="!userIds.length"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
>
|
>
|
||||||
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
|
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
|
||||||
批量授权
|
批量授权
|
||||||
</n-button>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,10 +68,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||||
|
import { h } from 'vue'
|
||||||
import { MeCrud, MeQueryItem } from '@/components'
|
import { MeCrud, MeQueryItem } from '@/components'
|
||||||
import { formatDateTime } from '@/utils'
|
import { formatDateTime } from '@/utils'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
import { h } from 'vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'RoleUser' })
|
defineOptions({ name: 'RoleUser' })
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -111,8 +113,8 @@ const columns = [
|
|||||||
h(
|
h(
|
||||||
NTag,
|
NTag,
|
||||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||||
{ default: () => item.name }
|
{ default: () => item.name },
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return '暂无角色'
|
return '暂无角色'
|
||||||
@@ -122,14 +124,14 @@ const columns = [
|
|||||||
title: '性别',
|
title: '性别',
|
||||||
key: 'gender',
|
key: 'gender',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
|
render: ({ gender }) => genders.find(item => gender === item.value)?.label ?? '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
key: 'createDate',
|
key: 'createDate',
|
||||||
width: 180,
|
width: 180,
|
||||||
render(row) {
|
render(row) {
|
||||||
return h('span', formatDateTime(row['createTime']))
|
return h('span', formatDateTime(row.createTime))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,7 +139,7 @@ const columns = [
|
|||||||
key: 'enable',
|
key: 'enable',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
|
||||||
render: (row) =>
|
render: row =>
|
||||||
h(
|
h(
|
||||||
NSwitch,
|
NSwitch,
|
||||||
{
|
{
|
||||||
@@ -148,7 +150,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
checked: () => '启用',
|
checked: () => '启用',
|
||||||
unchecked: () => '停用',
|
unchecked: () => '停用',
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -159,7 +161,7 @@ const columns = [
|
|||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
hideInExcel: true,
|
hideInExcel: true,
|
||||||
render(row) {
|
render(row) {
|
||||||
return row.roles?.some((item) => item.id === +route.params.roleId)
|
return row.roles?.some(item => item.id === +route.params.roleId)
|
||||||
? h(
|
? h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
@@ -171,7 +173,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '取消授权',
|
default: () => '取消授权',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
: h(
|
: h(
|
||||||
NButton,
|
NButton,
|
||||||
@@ -184,7 +186,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '授权',
|
default: () => '授权',
|
||||||
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
|
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -197,8 +199,10 @@ function onChecked(rowKeys) {
|
|||||||
|
|
||||||
function handleBatchAdd(ids = userIds.value) {
|
function handleBatchAdd(ids = userIds.value) {
|
||||||
const roleId = route.params.roleId
|
const roleId = route.params.roleId
|
||||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
if (!roleId)
|
||||||
if (!ids.length) return $message.error('请先选择用户')
|
return $message.error('角色异常,请重新选择角色')
|
||||||
|
if (!ids.length)
|
||||||
|
return $message.error('请先选择用户')
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
content: `确认分配【${route.query.roleName}】?`,
|
content: `确认分配【${route.query.roleName}】?`,
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -209,8 +213,10 @@ function handleBatchAdd(ids = userIds.value) {
|
|||||||
}
|
}
|
||||||
function handleBatchRemove(ids = userIds.value) {
|
function handleBatchRemove(ids = userIds.value) {
|
||||||
const roleId = route.params.roleId
|
const roleId = route.params.roleId
|
||||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
if (!roleId)
|
||||||
if (!ids.length) return $message.error('请先选择用户')
|
return $message.error('角色异常,请重新选择角色')
|
||||||
|
if (!ids.length)
|
||||||
|
return $message.error('请先选择用户')
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
content: `确认取消分配【${route.query.roleName}】?`,
|
content: `确认取消分配【${route.query.roleName}】?`,
|
||||||
async confirm() {
|
async confirm() {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
create: (data) => request.post('/user', data),
|
create: data => request.post('/user', data),
|
||||||
read: (params = {}) => request.get('/user', { params }),
|
read: (params = {}) => request.get('/user', { params }),
|
||||||
update: (data) => request.patch(`/user/${data.id}`, data),
|
update: data => request.patch(`/user/${data.id}`, data),
|
||||||
delete: (id) => request.delete(`/user/${id}`),
|
delete: id => request.delete(`/user/${id}`),
|
||||||
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
|
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
|
||||||
|
|
||||||
getAllRoles: () => request.get('/role?enable=1'),
|
getAllRoles: () => request.get('/role?enable=1'),
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<CommonPage>
|
<CommonPage>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button v-permission="'AddUser'" type="primary" @click="handleAdd()">
|
<NButton v-permission="'AddUser'" type="primary" @click="handleAdd()">
|
||||||
<i class="i-material-symbols:add mr-4 text-18" />
|
<i class="i-material-symbols:add mr-4 text-18" />
|
||||||
创建新用户
|
创建新用户
|
||||||
</n-button>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<MeCrud
|
<MeCrud
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
trigger: ['input', 'blur'],
|
trigger: ['input', 'blur'],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<n-input v-model:value="modalForm.password" />
|
<n-input v-model:value="modalForm.password" type="password" show-password-on="mousedown" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
|
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
|
||||||
@@ -92,10 +92,14 @@
|
|||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
|
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
|
||||||
<n-switch v-model:value="modalForm.enable">
|
<NSwitch v-model:value="modalForm.enable">
|
||||||
<template #checked>启用</template>
|
<template #checked>
|
||||||
<template #unchecked>停用</template>
|
启用
|
||||||
</n-switch>
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
停用
|
||||||
|
</template>
|
||||||
|
</NSwitch>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<n-alert v-if="modalAction === 'add'" type="warning" closable>
|
<n-alert v-if="modalAction === 'add'" type="warning" closable>
|
||||||
@@ -107,9 +111,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||||
import { formatDateTime } from '@/utils'
|
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
|
||||||
import { useCrud } from '@/composables'
|
import { useCrud } from '@/composables'
|
||||||
|
import { withPermission } from '@/directives'
|
||||||
|
import { formatDateTime } from '@/utils'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
defineOptions({ name: 'UserMgt' })
|
defineOptions({ name: 'UserMgt' })
|
||||||
@@ -129,6 +134,24 @@ const genders = [
|
|||||||
const roles = ref([])
|
const roles = ref([])
|
||||||
api.getAllRoles().then(({ data = [] }) => (roles.value = data))
|
api.getAllRoles().then(({ data = [] }) => (roles.value = data))
|
||||||
|
|
||||||
|
const {
|
||||||
|
modalRef,
|
||||||
|
modalFormRef,
|
||||||
|
modalForm,
|
||||||
|
modalAction,
|
||||||
|
handleAdd,
|
||||||
|
handleDelete,
|
||||||
|
handleOpen,
|
||||||
|
handleSave,
|
||||||
|
} = useCrud({
|
||||||
|
name: '用户',
|
||||||
|
initForm: { enable: true },
|
||||||
|
doCreate: api.create,
|
||||||
|
doDelete: api.delete,
|
||||||
|
doUpdate: api.update,
|
||||||
|
refresh: () => $table.value?.handleSearch(),
|
||||||
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '头像',
|
title: '头像',
|
||||||
@@ -152,8 +175,8 @@ const columns = [
|
|||||||
h(
|
h(
|
||||||
NTag,
|
NTag,
|
||||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||||
{ default: () => item.name }
|
{ default: () => item.name },
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return '暂无角色'
|
return '暂无角色'
|
||||||
@@ -163,7 +186,7 @@ const columns = [
|
|||||||
title: '性别',
|
title: '性别',
|
||||||
key: 'gender',
|
key: 'gender',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
|
render: ({ gender }) => genders.find(item => gender === item.value)?.label ?? '',
|
||||||
},
|
},
|
||||||
{ title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } },
|
{ title: '邮箱', key: 'email', width: 150, ellipsis: { tooltip: true } },
|
||||||
{
|
{
|
||||||
@@ -171,14 +194,14 @@ const columns = [
|
|||||||
key: 'createDate',
|
key: 'createDate',
|
||||||
width: 180,
|
width: 180,
|
||||||
render(row) {
|
render(row) {
|
||||||
return h('span', formatDateTime(row['createTime']))
|
return h('span', formatDateTime(row.createTime))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (row) =>
|
render: row =>
|
||||||
h(
|
h(
|
||||||
NSwitch,
|
NSwitch,
|
||||||
{
|
{
|
||||||
@@ -191,30 +214,42 @@ const columns = [
|
|||||||
{
|
{
|
||||||
checked: () => '启用',
|
checked: () => '启用',
|
||||||
unchecked: () => '停用',
|
unchecked: () => '停用',
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 320,
|
width: 420,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
hideInExcel: true,
|
hideInExcel: true,
|
||||||
render(row) {
|
render(row) {
|
||||||
return [
|
return [
|
||||||
|
withPermission(
|
||||||
|
h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'primary',
|
||||||
|
secondary: true,
|
||||||
|
}, {
|
||||||
|
default: () => '超管专属',
|
||||||
|
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
||||||
|
}),
|
||||||
|
'SuperAdmin',
|
||||||
|
),
|
||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
|
class: 'ml-12px',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
onClick: () => handleOpenRolesSet(row),
|
onClick: () => handleOpenRolesSet(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => '分配角色',
|
default: () => '分配角色',
|
||||||
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
@@ -227,7 +262,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '重置密码',
|
default: () => '重置密码',
|
||||||
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
|
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
h(
|
h(
|
||||||
@@ -241,7 +276,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
default: () => '删除',
|
default: () => '删除',
|
||||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -255,13 +290,15 @@ async function handleEnable(row) {
|
|||||||
row.enableLoading = false
|
row.enableLoading = false
|
||||||
$message.success('操作成功')
|
$message.success('操作成功')
|
||||||
$table.value?.handleSearch()
|
$table.value?.handleSearch()
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
row.enableLoading = false
|
row.enableLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenRolesSet(row) {
|
function handleOpenRolesSet(row) {
|
||||||
const roleIds = row.roles.map((item) => item.id)
|
const roleIds = row.roles.map(item => item.id)
|
||||||
handleOpen({
|
handleOpen({
|
||||||
action: 'setRole',
|
action: 'setRole',
|
||||||
title: '分配角色',
|
title: '分配角色',
|
||||||
@@ -270,31 +307,14 @@ function handleOpenRolesSet(row) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
modalRef,
|
|
||||||
modalFormRef,
|
|
||||||
modalForm,
|
|
||||||
modalAction,
|
|
||||||
handleAdd,
|
|
||||||
handleDelete,
|
|
||||||
handleOpen,
|
|
||||||
handleSave,
|
|
||||||
} = useCrud({
|
|
||||||
name: '用户',
|
|
||||||
initForm: { enable: true },
|
|
||||||
doCreate: api.create,
|
|
||||||
doDelete: api.delete,
|
|
||||||
doUpdate: api.update,
|
|
||||||
refresh: () => $table.value?.handleSearch(),
|
|
||||||
})
|
|
||||||
|
|
||||||
function onSave() {
|
function onSave() {
|
||||||
if (modalAction.value === 'setRole') {
|
if (modalAction.value === 'setRole') {
|
||||||
return handleSave({
|
return handleSave({
|
||||||
api: () => api.update(modalForm.value),
|
api: () => api.update(modalForm.value),
|
||||||
cb: () => $message.success('分配成功'),
|
cb: () => $message.success('分配成功'),
|
||||||
})
|
})
|
||||||
} else if (modalAction.value === 'reset') {
|
}
|
||||||
|
else if (modalAction.value === 'reset') {
|
||||||
return handleSave({
|
return handleSave({
|
||||||
api: () => api.resetPwd(modalForm.value.id, modalForm.value),
|
api: () => api.resetPwd(modalForm.value.id, modalForm.value),
|
||||||
cb: () => $message.success('密码重置成功'),
|
cb: () => $message.success('密码重置成功'),
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
changePassword: (data) => request.post('/auth/password', data),
|
changePassword: data => request.post('/auth/password', data),
|
||||||
updateProfile: (data) => request.patch(`/user/profile/${data.id}`, data),
|
updateProfile: data => request.patch(`/user/profile/${data.id}`, data),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-16 flex items-center">
|
<div class="mt-16 flex items-center">
|
||||||
<n-button type="primary" ghost @click="avatarModalRef.open()">更改头像</n-button>
|
<n-button type="primary" ghost @click="avatarModalRef.open()">
|
||||||
|
更改头像
|
||||||
|
</n-button>
|
||||||
<span class="ml-12 opacity-60">
|
<span class="ml-12 opacity-60">
|
||||||
修改头像只支持在线链接,不提供上传图片功能,如有需要可自行对接!
|
修改头像只支持在线链接,不提供上传图片功能,如有需要可自行对接!
|
||||||
</span>
|
</span>
|
||||||
@@ -44,12 +46,18 @@
|
|||||||
:column="1"
|
:column="1"
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<n-descriptions-item label="昵称">{{ userStore.nickName }}</n-descriptions-item>
|
<n-descriptions-item label="昵称">
|
||||||
|
{{ userStore.nickName }}
|
||||||
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="性别">
|
<n-descriptions-item label="性别">
|
||||||
{{ genders.find((item) => item.value === userStore.userInfo?.gender)?.label ?? '未知' }}
|
{{ genders.find((item) => item.value === userStore.userInfo?.gender)?.label ?? '未知' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="地址">{{ userStore.userInfo?.address }}</n-descriptions-item>
|
<n-descriptions-item label="地址">
|
||||||
<n-descriptions-item label="邮箱">{{ userStore.userInfo?.email }}</n-descriptions-item>
|
{{ userStore.userInfo?.address }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="邮箱">
|
||||||
|
{{ userStore.userInfo?.email }}
|
||||||
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -65,10 +73,10 @@
|
|||||||
require-mark-placement="left"
|
require-mark-placement="left"
|
||||||
>
|
>
|
||||||
<n-form-item label="原密码" path="oldPassword" :rule="required">
|
<n-form-item label="原密码" path="oldPassword" :rule="required">
|
||||||
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" />
|
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" show-password-on="mousedown" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="新密码" path="newPassword" :rule="required">
|
<n-form-item label="新密码" path="newPassword" :rule="required">
|
||||||
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" />
|
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" show-password-on="mousedown" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</MeModal>
|
</MeModal>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user