mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2026-01-22 23:50:22 +08:00
Compare commits
90 Commits
a63e72bc2f
...
2.x_vxe-ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a977ec1f3a | ||
|
|
6d1b3a4159 | ||
|
|
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 | ||
|
|
7ed9a3540d | ||
|
|
5766510ad9 | ||
|
|
905476abf7 | ||
|
|
160910bb85 | ||
|
|
d3d002770b | ||
|
|
98f3648f9f | ||
|
|
6cdf905cd4 | ||
|
|
f1661731da | ||
|
|
87dce667cf | ||
|
|
ea440e48bd | ||
|
|
0ac55503b7 | ||
|
|
769fd86d30 | ||
|
|
fd34922acc | ||
|
|
cf3c4b9020 | ||
|
|
621c34a1fb | ||
|
|
88288bc2c4 | ||
|
|
e73c138892 | ||
|
|
005aa60982 | ||
|
|
4f637d76e6 | ||
|
|
4eb15744a6 | ||
|
|
ddcbb83574 | ||
|
|
2edc6537c1 | ||
|
|
26afddc559 | ||
|
|
0e16cbb0a3 | ||
|
|
5629e80822 | ||
|
|
5b798d7db2 | ||
|
|
9615ec9aa8 | ||
|
|
e135be93af | ||
|
|
369ff0a68f | ||
|
|
eb3c56f5af | ||
|
|
008bed05a9 | ||
|
|
0141c0287e | ||
|
|
8f715925c7 | ||
|
|
763b5f1295 | ||
|
|
961ad6af7b | ||
|
|
c754d02dc0 | ||
|
|
2599ea2060 |
@@ -2,4 +2,8 @@ root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = unset
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -4,6 +4,9 @@ VITE_USE_HASH = 'true'
|
||||
# 资源公共路径,需要以 /开头和结尾
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 代理配置-target 本地服务 | Apifox云端mock
|
||||
# VITE_PROXY_TARGET = 'http://localhost:8085'
|
||||
VITE_PROXY_TARGET = 'https://mock.apifox.com/m1/3776410-0-default/'
|
||||
# Axios 基础路径
|
||||
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||
VITE_AXIOS_BASE_URL = 'https://mock.apipark.cn/m1/3776410-0-default' # apifox云端mock
|
||||
|
||||
# 代理配置-target
|
||||
VITE_PROXY_TARGET = 'http://localhost:8085'
|
||||
|
||||
@@ -4,5 +4,7 @@ VITE_USE_HASH = 'false'
|
||||
# 资源公共路径,需要以 /开头和结尾
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||
|
||||
# 代理配置-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
|
||||
3
.npmrc
3
.npmrc
@@ -1,2 +1,3 @@
|
||||
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": "lf",
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"antfu.unocss",
|
||||
"antfu.iconify",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"sdras.vue-vscode-snippets",
|
||||
"mikestead.dotenv"
|
||||
"mikestead.dotenv",
|
||||
"simonhe.common-intellisense"
|
||||
]
|
||||
}
|
||||
|
||||
46
.vscode/settings.json
vendored
46
.vscode/settings.json
vendored
@@ -1,25 +1,31 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv",
|
||||
"*.svg": "html",
|
||||
"*.css": "scss"
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"[html][css][less][scss][sass][yaml][yml][jsonc][json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": "package.json"
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc"
|
||||
],
|
||||
"common-intellisense.ui": [
|
||||
"naiveUi2"
|
||||
],
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#233212",
|
||||
"titleBar.activeBackground": "#314619",
|
||||
"titleBar.activeForeground": "#F9FCF6"
|
||||
}
|
||||
}
|
||||
|
||||
23
README.md
23
README.md
@@ -8,6 +8,7 @@
|
||||
</p>
|
||||
|
||||
## 简介
|
||||
|
||||
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss,后端使用 Nestjs + TypeOrm + MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
||||
|
||||
## 设计理念
|
||||
@@ -48,12 +49,26 @@ Vue Naive Admin 2022年2月开始开源,从 1.0 到现在的 2.0,一直秉
|
||||
|
||||
Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + MySql,内置 JWT、RABC及模板所需的一些基础接口。
|
||||
|
||||
*后续可能会提供 Java 版和 Go 版的,但由于精力有限,欢迎感兴趣的大佬基于前端提供对接好的后端项目,当然,并不局限于 Java 和 Go,已对接的后端项目会展示到仓库的 README 和 官方文档中*
|
||||
|
||||
- 源码-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)
|
||||
|
||||
## 文档
|
||||
|
||||
- 项目文档: [docs | vue-naive-admin](https://isme.top)
|
||||
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
|
||||
|
||||
> 注:有个比较常见的问题,就是如何添加菜单和修改菜单,由于项目是由后端控制菜单资源的,所以需要对接后端后在资源管理功能对菜单进行增删改,然后在角色管理功能给对应角色进行授权。具体如何对接后端,请参考 [项目文档](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).
|
||||
|
||||
或者使用 `degit` 克隆此仓库,这样将没有任何历史提交记录:
|
||||
|
||||
```cmd
|
||||
npx degit zclzone/vue-naive-admin
|
||||
```
|
||||
|
||||
## 版权说明
|
||||
|
||||
本项目使用 `MIT协议`,默认授权给任何人,被授权人可免费地无限制的使用、复制、修改、合并、发布、发行、再许可、售卖本软件拷贝、并有权向被供应人授予同等的权利,但必须满足以下条件:
|
||||
@@ -66,4 +81,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。
|
||||
- [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
|
||||
**********************************/
|
||||
|
||||
import path from 'node:path'
|
||||
import { globSync } from 'glob'
|
||||
import path from 'path'
|
||||
import dynamicIcons from '../src/assets/icons/dynamic-icons'
|
||||
import dynamicIcons from '../src/assets/icons/dynamic-icons.js'
|
||||
|
||||
/**
|
||||
* @usage 生成icons, 用于 unocss safelist,以支持页面动态渲染自定义图标
|
||||
@@ -35,5 +35,5 @@ export function getIcons() {
|
||||
*/
|
||||
export function getPagePathes() {
|
||||
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 {
|
||||
name: 'isme:icons',
|
||||
resolveId(id) {
|
||||
if (id === PLUGIN_ICONS_ID) return '\0' + PLUGIN_ICONS_ID
|
||||
if (id === PLUGIN_ICONS_ID)
|
||||
return `\0${PLUGIN_ICONS_ID}`
|
||||
},
|
||||
load(id) {
|
||||
if (id === '\0' + PLUGIN_ICONS_ID) {
|
||||
if (id === `\0${PLUGIN_ICONS_ID}`) {
|
||||
return `export default ${JSON.stringify(getIcons())}`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
export { pluginPagePathes } from './page-pathes'
|
||||
export { pluginIcons } from './icons'
|
||||
export { pluginPagePathes } from './page-pathes'
|
||||
|
||||
@@ -13,10 +13,11 @@ export function pluginPagePathes() {
|
||||
return {
|
||||
name: 'isme:page-pathes',
|
||||
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) {
|
||||
if (id === '\0' + PLUGIN_PAGE_PATHES_ID) {
|
||||
if (id === `\0${PLUGIN_PAGE_PATHES_ID}`) {
|
||||
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',
|
||||
},
|
||||
},
|
||||
})
|
||||
92
index.html
92
index.html
@@ -1,27 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/resource/loading.css" />
|
||||
<title><%= title %></title>
|
||||
<title>%VITE_TITLE%</title>
|
||||
<style>
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.dark .loading-container {
|
||||
background-color: #232324;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.loading-container .loading {
|
||||
--speed-of-animation: 0.9s;
|
||||
--gap: 12px;
|
||||
--first-color: #4c86f9;
|
||||
--second-color: #49a84c;
|
||||
--third-color: #f6bb02;
|
||||
--fourth-color: #26a69a;
|
||||
--fifth-color: #2196f3;
|
||||
|
||||
margin: auto;
|
||||
width: 160px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.loading-container .loading span {
|
||||
width: 6px;
|
||||
height: 80px;
|
||||
background: var(--first-color);
|
||||
animation: scale var(--speed-of-animation) ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-container .loading span:nth-child(2) {
|
||||
background: var(--second-color);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
|
||||
.loading-container .loading span:nth-child(3) {
|
||||
background: var(--third-color);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
|
||||
.loading-container .loading span:nth-child(4) {
|
||||
background: var(--fourth-color);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
|
||||
.loading-container .loading span:nth-child(5) {
|
||||
background: var(--fifth-color);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0%,
|
||||
40%,
|
||||
100% {
|
||||
transform: scaleY(0.25);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="dark:text-#e9e9e9 auto-bg">
|
||||
<div id="app">
|
||||
<!-- 白屏时的loading效果 -->
|
||||
<div class="loading-container">
|
||||
<div class="loading-spin__container">
|
||||
<div class="loading-spin">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="loading-title"><%= title %></div>
|
||||
</div>
|
||||
<script src="/resource/loading.js"></script>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
83
package.json
83
package.json
@@ -1,56 +1,63 @@
|
||||
{
|
||||
"name": "vue-naive-admin",
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue ."
|
||||
"lint:fix": "eslint --fix",
|
||||
"postinstall": "npx simple-git-hooks",
|
||||
"up": "taze major -I"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
"@arco-design/color": "^0.4.0",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.38.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-echarts": "^6.6.9",
|
||||
"vue-router": "^4.3.0",
|
||||
"naive-ui": "^2.40.1",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^4.1.1",
|
||||
"vue": "^3.5.12",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-router": "^4.4.5",
|
||||
"vxe-pc-ui": "4.2.5",
|
||||
"vxe-table": "4.8.13",
|
||||
"vxe-table-plugin-export-xlsx": "^4.0.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.188",
|
||||
"@iconify/utils": "^2.1.22",
|
||||
"@unocss/eslint-config": "^0.58.5",
|
||||
"@unocss/preset-rem-to-px": "^0.58.5",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@zclzone/eslint-config": "^0.0.5",
|
||||
"esno": "^4.0.0",
|
||||
"@antfu/eslint-config": "^3.8.0",
|
||||
"@iconify/json": "^2.2.262",
|
||||
"@unocss/eslint-config": "^0.63.6",
|
||||
"@unocss/eslint-plugin": "^0.63.6",
|
||||
"@unocss/preset-rem-to-px": "^0.63.6",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"esno": "^4.8.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^10.3.10",
|
||||
"glob": "^11.0.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.71.1",
|
||||
"unocss": "^0.58.5",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-simple-html": "^0.1.2"
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"taze": "^0.17.2",
|
||||
"unocss": "^0.63.6",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-router-warn": "^1.0.0",
|
||||
"vite-plugin-vue-devtools": "^7.5.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@zclzone",
|
||||
"@unocss",
|
||||
".eslint-global-variables.json"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
|
||||
9032
pnpm-lock.yaml
generated
9032
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/04 22:50:18
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spin__container {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 36px 0;
|
||||
}
|
||||
|
||||
.loading-spin {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
animation: loadingSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0;
|
||||
}
|
||||
.right-0 {
|
||||
right: 0;
|
||||
}
|
||||
.top-0 {
|
||||
top: 0;
|
||||
}
|
||||
.bottom-0 {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.loading-spin-item {
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes loadingSpin {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-delay-500 {
|
||||
-webkit-animation-delay: 500ms;
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
.loading-delay-1000 {
|
||||
-webkit-animation-delay: 1000ms;
|
||||
animation-delay: 1000ms;
|
||||
}
|
||||
.loading-delay-1500 {
|
||||
-webkit-animation-delay: 1500ms;
|
||||
animation-delay: 1500ms;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/04 22:50:27
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
function addThemeColorCssVars() {
|
||||
const key = '__THEME_COLOR__'
|
||||
const defaultColor = '#316c72'
|
||||
const themeColor = localStorage.getItem(key) || defaultColor
|
||||
const cssVars = `--primary-color: ${themeColor}`
|
||||
document.documentElement.style.cssText = cssVars
|
||||
}
|
||||
|
||||
addThemeColorCssVars()
|
||||
|
||||
39
src/App.vue
39
src/App.vue
@@ -16,27 +16,29 @@
|
||||
>
|
||||
<router-view v-if="Layout" v-slot="{ Component, route: curRoute }">
|
||||
<component :is="Layout">
|
||||
<KeepAlive :include="keepAliveNames">
|
||||
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
|
||||
</KeepAlive>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<KeepAlive :include="keepAliveNames">
|
||||
<component :is="Component" v-if="!tabStore.reloading" :key="curRoute.fullPath" />
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
</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>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
|
||||
import { LayoutSetting } from '@/components'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
import { useAppStore, useTabStore } from '@/store'
|
||||
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
|
||||
import { layoutSettingVisible } from './settings'
|
||||
|
||||
const layouts = new Map()
|
||||
function getLayout(name) {
|
||||
// 利用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`)))
|
||||
layouts.set(name, layout)
|
||||
return layout
|
||||
@@ -44,23 +46,20 @@ function getLayout(name) {
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
if (appStore.layout === 'default') appStore.setLayout('')
|
||||
if (appStore.layout === 'default')
|
||||
appStore.setLayout('')
|
||||
const Layout = computed(() => {
|
||||
if (!route.matched?.length) return null
|
||||
if (!route.matched?.length)
|
||||
return null
|
||||
return getLayout(route.meta?.layout || appStore.layout)
|
||||
})
|
||||
|
||||
function setupCssVar() {
|
||||
const common = appStore.naiveThemeOverrides?.common || {}
|
||||
for (const key in common) {
|
||||
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
|
||||
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
|
||||
}
|
||||
}
|
||||
setupCssVar()
|
||||
|
||||
const tabStore = useTabStore()
|
||||
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(() => {
|
||||
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -14,11 +14,11 @@ export default {
|
||||
// 刷新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'),
|
||||
// 验证菜单路径
|
||||
validateMenuPath: (path) => request.get(`/permission/menu/validate?path=${path}`),
|
||||
validateMenuPath: path => request.get(`/permission/menu/validate?path=${path}`),
|
||||
}
|
||||
|
||||
37
src/assets/icons/isme/naiveui.svg
Normal file
37
src/assets/icons/isme/naiveui.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg
|
||||
t="1710898099143"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2321"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M335.657769 831.865977c0 10.134611 0 20.269222 0.202692 30.403833 0 2.189076 0 4.337614-0.202692 6.405074zM688.017924 152.765964v345.387543c-0.243231-110.426721-0.77023-220.893981-0.891846-331.320703a117.561487 117.561487 0 0 1 0.891846-14.06684z"
|
||||
fill="#93CEAA"
|
||||
p-id="2322"
|
||||
></path>
|
||||
<path
|
||||
d="M687.085539 508.49081a68.50997 68.50997 0 0 0 0.932385-10.337303v322.929244l-169.207465-154.73524-85.130733-77.631121-51.240593-47.105671-22.782605-20.796222-8.391458-7.661766-0.608077-0.527c-2.067461-1.662076-4.053844-3.405229-5.959151-4.986228s-3.567383-2.959306-5.837536-2.270153a5.067305 5.067305 0 0 0-3.202537 3.64846v-304.03833l1.743153 1.580999 34.741446 31.417294c18.282838 16.215378 36.72783 32.430755 54.97013 48.646133q38.916906 34.863062 77.590582 70.131508 30.606525 27.809373 61.131974 55.699822c24.323066 22.174529 48.321825 44.592288 72.644891 66.604664 10.94538 9.89138 21.971837 19.580068 33.038832 29.309295 2.35123 2.067461 4.661921 4.053844 7.053689 6.161843a5.959151 5.959151 0 0 0 5.675382 0.729692 4.702459 4.702459 0 0 0 2.270153-2.918768c0.324308-1.540461 0.324308-2.59446 0.567538-3.851152z"
|
||||
fill="#4C9717"
|
||||
p-id="2323"
|
||||
></path>
|
||||
<path
|
||||
d="M335.292923 510.801501a63.523742 63.523742 0 0 0-0.405385 11.107534c0 103.373032 0 206.746064 0.770231 310.078558v36.808907a60.199589 60.199589 0 0 1-23.796067 44.957134c-27.60668 23.066375-53.348592 48.281287-79.98235 72.523276-11.148072 10.134611-22.215067 20.269222-33.687447 30.160603s-40.984367 11.107534-57.321359-2.553922a318.145708 318.145708 0 0 1-34.619832-35.106293 51.727054 51.727054 0 0 1-15.120839-35.227908c0-2.918768-0.243231-6.121305 0-9.648149 0-6.080767 0-12.161533 0.202692-18.282838 0.202692-17.431531 0-34.9036 0-52.335132V294.772133a51.240593 51.240593 0 0 1 1.540461-12.891225 74.671814 74.671814 0 0 1 4.053844-10.702149 89.833192 89.833192 0 0 1 8.188766-14.512763l64.86151-64.86151 41.308675-41.268136a31.78214 31.78214 0 0 1 7.459073-5.594306 32.633447 32.633447 0 0 1 13.012841-4.053844 56.145745 56.145745 0 0 1 39.362829 7.256381 55.091745 55.091745 0 0 1 8.472535 6.364536l56.064668 50.348748v304.038329a13.701994 13.701994 0 0 0-0.364846 1.905307z"
|
||||
fill="#5FBC21"
|
||||
p-id="2324"
|
||||
></path>
|
||||
<path
|
||||
d="M335.292923 510.801501a13.701994 13.701994 0 0 1 0.364846-1.662076v322.726552c-0.608077-103.373032-0.567538-206.746064-0.770231-310.078558a63.523742 63.523742 0 0 1 0.405385-10.985918z"
|
||||
fill="#E8CEAA"
|
||||
opacity=".6"
|
||||
p-id="2325"
|
||||
></path>
|
||||
<path
|
||||
d="M924.357052 758.937317l-119.142487 109.453798a15.201916 15.201916 0 0 1-1.459384 0.972923l-0.445923 0.324308-0.810769 0.486461a56.753822 56.753822 0 0 1-67.942432-6.688843l-7.175304-6.283459-39.524983-36.119754V152.765964c0.648615-5.634844 1.621538-11.269687 2.716076-16.782916 2.837691-14.391148 15.485686-21.525914 25.133835-30.525448 20.958376-19.458453 42.565366-38.187214 63.929126-57.240283 14.715455-13.134456 29.390372-26.30945 44.227442-39.362829 12.931764-11.391303 37.984522-10.823765 50.146056-3.770075a40.984367 40.984367 0 0 1 8.431996 6.526689l13.701994 13.215533 11.066995 10.580534 10.094073 9.769765a46.416518 46.416518 0 0 1 7.215843 8.472535 47.551595 47.551595 0 0 1 6.891535 26.187834c-0.202692 59.429359 0.243231 118.858718 0.364846 178.369154q0.364846 151.411088 0.648615 302.822176 0 77.590582 0.283769 155.140625v7.094228c0.608077 15.404609 0.283769 25.579758-8.350919 35.673831z"
|
||||
fill="#5FBC21"
|
||||
p-id="2326"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -8,11 +8,12 @@
|
||||
|
||||
<template>
|
||||
<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"><slot /></main>
|
||||
</transition>
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<TheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
|
||||
<main :class="{ 'flex-1': full }" class="m-12">
|
||||
<slot />
|
||||
</main>
|
||||
<slot name="footer">
|
||||
<TheFooter v-if="showFooter" class="mb-12 mt-auto" />
|
||||
</slot>
|
||||
<n-back-top :bottom="20" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -7,18 +7,17 @@
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<main class="h-full flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
|
||||
<div class="h-full flex-col">
|
||||
<AppCard
|
||||
v-if="showHeader"
|
||||
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
|
||||
border-b="1px solid light_border dark:dark_border"
|
||||
>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<div class="flex items-center">
|
||||
<slot v-if="$slots['title-prefix']" name="title-prefix" />
|
||||
<template v-else-if="back">
|
||||
<main class="h-full flex-col flex-1 overflow-hidden bg-#f5f6fb dark:bg-#121212">
|
||||
<AppCard
|
||||
v-if="showHeader"
|
||||
class="sticky top-0 z-1 min-h-60 flex items-center justify-between px-24"
|
||||
border-b="1px solid light_border dark:dark_border"
|
||||
>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<div class="flex items-center">
|
||||
<slot name="title-prefix">
|
||||
<template v-if="back">
|
||||
<div
|
||||
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
||||
@click="router.back()"
|
||||
@@ -27,27 +26,27 @@
|
||||
<span class="ml-4">返回</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary"></div>
|
||||
<h2 class="font-normal">{{ title ?? route.meta?.title }}</h2>
|
||||
<slot name="title-suffix" />
|
||||
</div>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</AppCard>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
|
||||
<slot />
|
||||
</AppCard>
|
||||
</transition>
|
||||
</div>
|
||||
</main>
|
||||
</slot>
|
||||
|
||||
<footer class="bg-#f5f6fb dark:bg-#121212">
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<AppCard v-else-if="showFooter" class="py-12">
|
||||
<TheFooter />
|
||||
<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" />
|
||||
</div>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</AppCard>
|
||||
</footer>
|
||||
<AppCard class="cus-scroll m-12 h-0 flex-1 rounded-8 p-24" bordered>
|
||||
<slot />
|
||||
</AppCard>
|
||||
|
||||
<slot name="footer">
|
||||
<AppCard v-if="showFooter" class="flex-shrink-0 py-12">
|
||||
<TheFooter />
|
||||
</AppCard>
|
||||
</slot>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -10,18 +10,14 @@
|
||||
<div>
|
||||
<n-tooltip trigger="hover" placement="left">
|
||||
<template #trigger>
|
||||
<i class="i-fe:settings cursor-pointer text-32 color-primary" @click="modalRef.open()" />
|
||||
<div class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
|
||||
<i class="i-fe:settings cursor-pointer bg-white text-20" />
|
||||
</div>
|
||||
</template>
|
||||
布局设置
|
||||
</n-tooltip>
|
||||
|
||||
<MeModal
|
||||
ref="modalRef"
|
||||
title="布局设置"
|
||||
:show-footer="false"
|
||||
width="600px"
|
||||
:modal-style="{ opacity: 0.85 }"
|
||||
>
|
||||
<MeModal ref="modalRef" title="布局设置" :show-footer="false" width="600px">
|
||||
<n-space justify="space-between">
|
||||
<div class="flex-col cursor-pointer justify-center" @click="appStore.setLayout('simple')">
|
||||
<div class="flex">
|
||||
@@ -98,8 +94,8 @@
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useModal } from '@/composables'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const [modalRef] = useModal()
|
||||
|
||||
240
src/components/common/MeVxeGrid.vue
Normal file
240
src/components/common/MeVxeGrid.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div
|
||||
:style="isComputedHeight ? `height: calc(100% - ${autoHeight}px);` : `height: ${autoHeight}px`"
|
||||
>
|
||||
<vxe-grid
|
||||
ref="gridRef"
|
||||
:columns="vxeColumns"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
height="auto"
|
||||
:stripe="true"
|
||||
:border="true"
|
||||
size="mini"
|
||||
:auto-resize="true"
|
||||
:pager-config="pagerConfig"
|
||||
:export-config="exportConfig"
|
||||
:toolbar-config="vxeToolbarConfig"
|
||||
:proxy-config="proxyConfig"
|
||||
v-on="gridEvent"
|
||||
>
|
||||
<template #loading>
|
||||
<n-spin :show="loading" />
|
||||
</template>
|
||||
<template #total>
|
||||
<span class="font-size-14 line-height-28px">共 {{ pagerConfig.total }} 条数据</span>
|
||||
</template>
|
||||
<template v-for="(c, k) in slots" :key="k" #[c.key]="{ row }">
|
||||
<template v-if="Array.isArray(c.render(row))">
|
||||
<component :is="r" v-for="(r, k1) in c.render(row)" :key="k1" :row="row" :col="c" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<component :is="c.render(row)" :row="row" :col="c" />
|
||||
</template>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({ name: 'MeVxeGrid' })
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @remote 当isComputedHeight为true时为除table外的高度 false时为table高度
|
||||
*/
|
||||
autoHeight: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
/**
|
||||
* @isPagination 是否为自动计算高度
|
||||
*/
|
||||
isComputedHeight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* @remote table的列
|
||||
*/
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
pagination: {
|
||||
type: [Boolean, Object],
|
||||
default: true,
|
||||
},
|
||||
|
||||
exportConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
toolbarConfig: {
|
||||
type: Object,
|
||||
default: () => ({ }),
|
||||
},
|
||||
/**
|
||||
* 是否显示复选框
|
||||
*/
|
||||
showCheck: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowKey: {
|
||||
type: String, // rowKey
|
||||
default: 'id',
|
||||
},
|
||||
|
||||
exportMethod: {
|
||||
type: Function,
|
||||
default: () => null,
|
||||
},
|
||||
remoteMethod: {
|
||||
type: Function,
|
||||
default: () => null,
|
||||
},
|
||||
})
|
||||
const emits = defineEmits(['update:checked-row-keys', 'pageChanged'])
|
||||
const isFirst = shallowRef(true)
|
||||
const gridRef = ref(null)
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(n) => {
|
||||
isFirst.value = false
|
||||
gridRef.value.reloadData(n)
|
||||
},
|
||||
)
|
||||
|
||||
const vxeToolbarConfig = computed(() => {
|
||||
return {
|
||||
size: 'mini',
|
||||
className: 'c-toolbar',
|
||||
perfect: true,
|
||||
refresh: true,
|
||||
export: true,
|
||||
zoom: true,
|
||||
custom: true,
|
||||
...props.toolbarConfig,
|
||||
}
|
||||
})
|
||||
const pagerConfig = computed(() => {
|
||||
if (props.pagination === false) {
|
||||
return {
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
currentPage: props.pagination.pageNo,
|
||||
pageSize: props.pagination.pageSize,
|
||||
total: props.pagination.itemCount || 0,
|
||||
background: true,
|
||||
size: 'small',
|
||||
layouts: [
|
||||
'Home',
|
||||
'PrevPage',
|
||||
'JumpNumber',
|
||||
'NextPage',
|
||||
'End',
|
||||
'Sizes',
|
||||
'FullJump',
|
||||
// 'Total',
|
||||
],
|
||||
slots: {
|
||||
right: 'total',
|
||||
},
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 将naive 转为vxe-table可用的列
|
||||
*/
|
||||
const vxeColumns = computed(() => {
|
||||
if (props.showCheck) {
|
||||
return [{
|
||||
type: 'checkbox',
|
||||
resizable: false,
|
||||
width: 45,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
}, ...props.columns.map((item) => {
|
||||
item.field = item.key
|
||||
if (item.render) {
|
||||
item.slots = { default: item.key }
|
||||
}
|
||||
return item
|
||||
})]
|
||||
}
|
||||
return props.columns.map((item) => {
|
||||
item.field = item.key
|
||||
if (item.render) {
|
||||
item.slots = { default: item.key }
|
||||
}
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 将naive render函数转换为vxeTable的slots
|
||||
*/
|
||||
const slots = computed(() => {
|
||||
return props.columns.filter(item => item.render)
|
||||
})
|
||||
|
||||
const gridEvent = {
|
||||
checkboxAll() {
|
||||
const $grid = gridRef.value
|
||||
if ($grid) {
|
||||
emits(
|
||||
'update:checked-row-keys',
|
||||
$grid.getCheckboxRecords().map(item => item[props.rowKey]),
|
||||
)
|
||||
}
|
||||
},
|
||||
checkboxChange() {
|
||||
const $grid = gridRef.value
|
||||
if ($grid) {
|
||||
emits(
|
||||
'update:checked-row-keys',
|
||||
$grid.getCheckboxRecords().map(item => item[props.rowKey]),
|
||||
)
|
||||
}
|
||||
},
|
||||
pageChange(e) {
|
||||
emits('pageChanged', e)
|
||||
},
|
||||
}
|
||||
|
||||
const proxyConfig = {
|
||||
props: {
|
||||
result: 'data.pageData',
|
||||
total: 'data.total',
|
||||
},
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (isFirst.value === true) {
|
||||
return Promise.reject('')
|
||||
}
|
||||
props.remoteMethod(page.currentPage, page.pageSize)
|
||||
},
|
||||
queryAll: async () => {
|
||||
const res = await props.exportMethod()
|
||||
return res
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
<template>
|
||||
<div class="h-40 w-40 rounded-full bg-primary p-1/100">
|
||||
<img src="@/assets/images/isme.png" alt="Logo" />
|
||||
<div class="h-32 w-32 rounded-4 bg-primary">
|
||||
<img src="@/assets/images/isme.png" alt="Logo">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
23
src/components/common/ThemeSetting.vue
Normal file
23
src/components/common/ThemeSetting.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-color-picker
|
||||
class="mr-16 h-32 w-32"
|
||||
:value="appStore.primaryColor"
|
||||
:swatches="primaryColors"
|
||||
:on-update:value="(v) => appStore.setPrimaryColor(v)"
|
||||
:render-label="() => ''"
|
||||
/>
|
||||
</template>
|
||||
设置主题色
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import { getPresetColors } from '@arco-design/color'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
|
||||
</script>
|
||||
44
src/components/common/ToggleTheme.vue
Normal file
44
src/components/common/ToggleTheme.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<i
|
||||
class="mr-16 cursor-pointer"
|
||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
||||
@click="toggleDark"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
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)`,
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,7 @@
|
||||
export { default as AppCard } from './AppCard.vue'
|
||||
export { default as TheFooter } from './TheFooter.vue'
|
||||
export { default as AppPage } from './AppPage.vue'
|
||||
export { default as CommonPage } from './CommonPage.vue'
|
||||
export { default as LayoutSetting } from './LayoutSetting.vue'
|
||||
export { default as MeVxeGrid } from './MeVxeGrid.vue'
|
||||
export { default as TheFooter } from './TheFooter.vue'
|
||||
export { default as ToggleTheme } from './ToggleTheme.vue'
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<div :style="{ width: contentWidth + 'px' }" class="flex-shrink-0">
|
||||
<div :style="{ width: `${contentWidth}px` }" class="flex-shrink-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,25 +7,59 @@
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<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()">
|
||||
<n-space wrap :size="[32, 16]">
|
||||
<slot />
|
||||
</n-space>
|
||||
<div class="flex-shrink-0">
|
||||
<n-button ghost type="primary" @click="handleReset">
|
||||
<i class="i-fe:rotate-ccw mr-4" />
|
||||
重置
|
||||
</n-button>
|
||||
<n-button attr-type="submit" class="ml-20" type="primary">
|
||||
<i class="i-fe:search mr-4" />
|
||||
搜索
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</AppCard>
|
||||
<div :class="computedHeight === true ? 'h-100%' : ''">
|
||||
<div id="search_main">
|
||||
<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()">
|
||||
<n-scrollbar x-scrollable>
|
||||
<n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10">
|
||||
<slot />
|
||||
</n-space>
|
||||
</n-scrollbar>
|
||||
<div class="flex-shrink-0 p-10">
|
||||
<n-button ghost type="primary" @click="handleReset">
|
||||
<i class="i-fe:rotate-ccw mr-4" />
|
||||
重置
|
||||
</n-button>
|
||||
<n-button attr-type="submit" class="ml-20" type="primary">
|
||||
<i class="i-fe:search mr-4" />
|
||||
搜索
|
||||
</n-button>
|
||||
|
||||
<n-data-table
|
||||
<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>
|
||||
</form>
|
||||
</AppCard>
|
||||
</div>
|
||||
<MeVxeGrid
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:scroll-x="scrollX"
|
||||
:columns="columns"
|
||||
:export-config="{ filename: exportName, modes: ['all', 'current'] }"
|
||||
:toolbar-config="toolbarConfig"
|
||||
:data="tableData"
|
||||
:auto-height="computedHeight === true ? autoHeight : computedHeight"
|
||||
:row-key="rowKey"
|
||||
:is-computed-height="computedHeight === true"
|
||||
:show-check="showCheck"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
:remote-method="remoteMethod"
|
||||
:export-method="exportMethod"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
|
||||
<!-- <NDataTable
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:scroll-x="scrollX"
|
||||
@@ -35,14 +69,39 @@
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NDataTable } from 'naive-ui'
|
||||
import { utils, writeFile } from 'xlsx'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 是否自动计算高度,适用只有search和table
|
||||
*/
|
||||
computedHeight: {
|
||||
type: [Boolean, Number],
|
||||
default: () => true,
|
||||
},
|
||||
showCheck: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
export: true,
|
||||
zoom: true,
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* @remote 导出数据表格名称
|
||||
*/
|
||||
exportName: {
|
||||
type: String,
|
||||
default: '导出数据',
|
||||
},
|
||||
/**
|
||||
* @remote true: 后端分页 false: 前端分页
|
||||
*/
|
||||
@@ -51,7 +110,7 @@ const props = defineProps({
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @remote 是否分页
|
||||
* @isPagination 是否分页
|
||||
*/
|
||||
isPagination: {
|
||||
type: Boolean,
|
||||
@@ -78,10 +137,10 @@ const props = defineProps({
|
||||
},
|
||||
/**
|
||||
* ! 约定接口入参出参
|
||||
* * 分页模式需约定分页接口入参
|
||||
* 分页模式需约定分页接口入参
|
||||
* @pageSize 分页参数:一页展示多少条,默认10
|
||||
* @pageNo 分页参数:页码,默认1
|
||||
* * 需约定接口出参
|
||||
* 需约定接口出参
|
||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||
*/
|
||||
@@ -89,13 +148,54 @@ const props = defineProps({
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
/** 是否支持展开 */
|
||||
expand: Boolean,
|
||||
})
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
|
||||
const autoHeight = ref(0)
|
||||
|
||||
window.onresize = () => {
|
||||
autoHeight.value = document.getElementById('search_main').clientHeight + 24
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
autoHeight.value = document.getElementById('search_main').clientHeight + 24
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
const loading = ref(false)
|
||||
const initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10 })
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
prefix({ itemCount }) {
|
||||
return `共 ${itemCount} 条数据`
|
||||
},
|
||||
})
|
||||
|
||||
// 是否展开
|
||||
const isExpanded = ref(false)
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
async function exportMethod() {
|
||||
const { data } = await props.getData({
|
||||
pageNo: 1,
|
||||
pageSize: 2000,
|
||||
...props.queryItems,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
function remoteMethod(page, pageSize) {
|
||||
pagination.page = page
|
||||
pagination.pageSize = pageSize
|
||||
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
async function handleQuery() {
|
||||
try {
|
||||
@@ -111,17 +211,30 @@ async function handleQuery() {
|
||||
})
|
||||
tableData.value = data?.pageData || data
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
if (pagination.itemCount && !tableData.value.length && pagination.page > 1) {
|
||||
// 如果当前页数据为空,且总条数不为0,则返回上一页数据
|
||||
onPageChange(pagination.page - 1)
|
||||
}
|
||||
return data
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
emit('onDataChange', tableData.value)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
|
||||
function handleSearch(keepCurrentPage = false) {
|
||||
if (keepCurrentPage || !props.remote) {
|
||||
handleQuery()
|
||||
}
|
||||
else {
|
||||
onPageChange(1)
|
||||
}
|
||||
}
|
||||
async function handleReset() {
|
||||
const queryItems = { ...props.queryItems }
|
||||
@@ -140,16 +253,15 @@ function onPageChange(currentPage) {
|
||||
}
|
||||
}
|
||||
function onChecked(rowKeys) {
|
||||
if (props.columns.some((item) => item.type === 'selection')) {
|
||||
emit('onChecked', rowKeys)
|
||||
}
|
||||
emit('onChecked', rowKeys)
|
||||
}
|
||||
function handleExport(columns = props.columns, data = tableData.value) {
|
||||
if (!data?.length) return $message.warning('没有数据')
|
||||
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
|
||||
const thKeys = columnsData.map((item) => item.key)
|
||||
const thData = columnsData.map((item) => item.title)
|
||||
const trData = data.map((item) => thKeys.map((key) => item[key]))
|
||||
if (!data?.length)
|
||||
return $message.warning('没有数据')
|
||||
const columnsData = columns.filter(item => !!item.title && !item.hideInExcel)
|
||||
const thKeys = columnsData.map(item => item.key)
|
||||
const thData = columnsData.map(item => item.title)
|
||||
const trData = data.map(item => thKeys.map(key => item[key]))
|
||||
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
||||
const workBook = utils.book_new()
|
||||
utils.book_append_sheet(workBook, sheet, '数据报表')
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as MeModal } from './modal/index.vue'
|
||||
export { default as MeCrud } from './crud/index.vue'
|
||||
export { default as MeQueryItem } from './crud/QueryItem.vue'
|
||||
export { default as MeModal } from './modal/index.vue'
|
||||
|
||||
@@ -18,27 +18,30 @@
|
||||
>
|
||||
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
|
||||
<template #header>
|
||||
<header class="modal-header">{{ modalOptions.title }}</header>
|
||||
<header class="modal-header">
|
||||
{{ modalOptions.title }}
|
||||
</header>
|
||||
</template>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<template #footer>
|
||||
<slot v-if="$slots.footer" name="footer" />
|
||||
<footer v-else-if="modalOptions.showFooter" class="flex justify-end">
|
||||
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
||||
{{ modalOptions.cancelText }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="modalOptions.showOk"
|
||||
type="primary"
|
||||
:loading="modalOptions.okLoading"
|
||||
class="ml-20"
|
||||
@click="handleOk()"
|
||||
>
|
||||
{{ modalOptions.okText }}
|
||||
</n-button>
|
||||
</footer>
|
||||
<slot name="footer">
|
||||
<footer v-if="modalOptions.showFooter" class="flex justify-end">
|
||||
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
||||
{{ modalOptions.cancelText }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="modalOptions.showOk"
|
||||
type="primary"
|
||||
:loading="modalOptions.okLoading"
|
||||
class="ml-20"
|
||||
@click="handleOk()"
|
||||
>
|
||||
{{ modalOptions.okText }}
|
||||
</n-button>
|
||||
</footer>
|
||||
</slot>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
@@ -46,6 +49,7 @@
|
||||
|
||||
<script setup>
|
||||
import { initDrag } from './utils'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
@@ -101,6 +105,17 @@ const show = ref(false)
|
||||
// 声明一个modalOptions变量,用于存储模态框的配置信息
|
||||
const modalOptions = ref({})
|
||||
|
||||
const okLoading = computed({
|
||||
get() {
|
||||
return !!modalOptions.value?.okLoading
|
||||
},
|
||||
set(v) {
|
||||
if (modalOptions.value) {
|
||||
modalOptions.value.okLoading = v
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 打开模态框
|
||||
async function open(options = {}) {
|
||||
// 将props和options合并赋值给modalOptions
|
||||
@@ -111,7 +126,7 @@ async function open(options = {}) {
|
||||
await nextTick()
|
||||
initDrag(
|
||||
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参数
|
||||
const res = await modalOptions.value.onOk(data)
|
||||
// 如果onOk函数的返回值不为false,则关闭模态框
|
||||
res !== false && close()
|
||||
} catch (error) {
|
||||
okLoading.value = false
|
||||
if (res !== false)
|
||||
close()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +165,12 @@ async function handleCancel(data) {
|
||||
const res = await modalOptions.value.onCancel(data)
|
||||
|
||||
// 如果onCancel函数的返回值不为false,则关闭模态框
|
||||
res !== false && close()
|
||||
} catch (error) {
|
||||
okLoading.value = false
|
||||
if (res !== false)
|
||||
close()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,21 +178,10 @@ async function onAfterLeave() {
|
||||
await nextTick()
|
||||
initDrag(
|
||||
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,
|
||||
|
||||
@@ -13,28 +13,17 @@ function getCss(element, key) {
|
||||
: window.getComputedStyle(element, null)[key]
|
||||
}
|
||||
|
||||
const params = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
flag: false,
|
||||
}
|
||||
|
||||
// 初始化拖拽
|
||||
export function initDrag(bar, box) {
|
||||
if (!bar || !box) return
|
||||
const screenWidth = document.body.clientWidth // 页面宽度
|
||||
const screenHeight = document.documentElement.clientHeight // 页面可见区域高度
|
||||
|
||||
const dragDomWidth = box.offsetWidth // 盒子宽度
|
||||
const dragDomHeight = box.offsetHeight // 盒子高度
|
||||
|
||||
const minDomLeft = box.offsetLeft // 盒子相对于父元素的左偏移量
|
||||
const minDomTop = box.offsetTop // 盒子相对于父元素的上偏移量
|
||||
|
||||
const maxDragDomLeft = screenWidth - minDomLeft - dragDomWidth // 盒子在水平方向上可拖拽的最大距离
|
||||
const maxDragDomTop = screenHeight - minDomTop - dragDomHeight // 盒子在垂直方向上可拖拽的最大距离
|
||||
if (!bar || !box)
|
||||
return
|
||||
const params = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
flag: false,
|
||||
}
|
||||
|
||||
if (getCss(box, 'left') !== 'auto') {
|
||||
params.left = getCss(box, 'left')
|
||||
@@ -45,7 +34,6 @@ export function initDrag(bar, box) {
|
||||
|
||||
// 设置触发拖动元素的鼠标样式为移动图标
|
||||
bar.style.cursor = 'move'
|
||||
|
||||
// 鼠标按下事件处理函数
|
||||
bar.onmousedown = function (e) {
|
||||
params.flag = true // 设置拖拽标志为true
|
||||
@@ -63,6 +51,9 @@ export function initDrag(bar, box) {
|
||||
}
|
||||
}
|
||||
document.onmousemove = function (e) {
|
||||
if (e.target !== bar && !params.flag)
|
||||
return
|
||||
|
||||
e.preventDefault() // 阻止默认事件
|
||||
// 如果拖拽标志为true
|
||||
if (params.flag) {
|
||||
@@ -71,24 +62,11 @@ export function initDrag(bar, box) {
|
||||
const disX = nowX - params.currentX // 鼠标移动的X距离
|
||||
const disY = nowY - params.currentY // 鼠标移动的Y距离
|
||||
|
||||
let left = parseInt(params.left) + disX // 盒子元素的新left值
|
||||
let top = parseInt(params.top) + disY // 盒子元素的新top值
|
||||
const left = Number.parseInt(params.left) + disX // 盒子元素的新left值
|
||||
const top = Number.parseInt(params.top) + disY // 盒子元素的新top值
|
||||
|
||||
// 拖出屏幕边缘
|
||||
if (-left > minDomLeft) {
|
||||
left = -minDomLeft
|
||||
} else if (left > maxDragDomLeft) {
|
||||
left = maxDragDomLeft
|
||||
}
|
||||
|
||||
if (-top > minDomTop) {
|
||||
top = -minDomTop
|
||||
} else if (top > maxDragDomTop) {
|
||||
top = maxDragDomTop
|
||||
}
|
||||
|
||||
box.style.left = left + 'px'
|
||||
box.style.top = top + 'px'
|
||||
box.style.left = `${left}px`
|
||||
box.style.top = `${top}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './useAliveData'
|
||||
export * from './useCrud'
|
||||
export * from './useForm'
|
||||
export * from './useModal'
|
||||
export * from './useAliveData'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
**********************************/
|
||||
|
||||
const lastDataMap = new Map()
|
||||
export const useAliveData = (initData = {}, key) => {
|
||||
export function useAliveData(initData = {}, key) {
|
||||
key = key ?? useRoute().name
|
||||
const lastData = lastDataMap.get(key)
|
||||
const aliveData = ref(lastData || { ...initData })
|
||||
@@ -17,7 +17,7 @@ export const useAliveData = (initData = {}, key) => {
|
||||
(v) => {
|
||||
lastDataMap.set(key, v)
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { useModal, useForm } from '.'
|
||||
import { useForm, useModal } from '.'
|
||||
|
||||
const ACTIONS = {
|
||||
view: '查看',
|
||||
@@ -14,7 +14,7 @@ const ACTIONS = {
|
||||
add: '新增',
|
||||
}
|
||||
|
||||
export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) => {
|
||||
export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||
const modalAction = ref('')
|
||||
const [modalRef, okLoading] = useModal()
|
||||
const [modalFormRef, modalForm, validation] = useForm(initForm)
|
||||
@@ -44,7 +44,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
||||
async onOk() {
|
||||
if (typeof onOk === 'function') {
|
||||
return await onOk()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return await handleSave()
|
||||
}
|
||||
},
|
||||
@@ -77,7 +78,9 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
||||
action.cb()
|
||||
okLoading.value = false
|
||||
data && refresh(data)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
return false
|
||||
}
|
||||
@@ -85,7 +88,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete(id, confirmOptions) {
|
||||
if (!id && id !== 0) return
|
||||
if (!id && id !== 0)
|
||||
return
|
||||
const d = $dialog.warning({
|
||||
content: '确定删除?',
|
||||
title: '提示',
|
||||
@@ -97,8 +101,10 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
||||
const data = await doDelete(id)
|
||||
$message.success('删除成功')
|
||||
d.loading = false
|
||||
refresh(data)
|
||||
} catch (error) {
|
||||
refresh(data, true)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
d.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
export const useForm = (initFormData = {}) => {
|
||||
export function useForm(initFormData = {}) {
|
||||
const formRef = ref(null)
|
||||
const formModel = ref({ ...initFormData })
|
||||
const rules = {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
export const useModal = () => {
|
||||
export function useModal() {
|
||||
const modalRef = ref(null)
|
||||
const okLoading = computed({
|
||||
get() {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { router } from '@/router'
|
||||
const permission = {
|
||||
mounted(el, binding) {
|
||||
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)) {
|
||||
el.remove()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
<script setup>
|
||||
import { usePermissionStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const permissionStore = usePermissionStore()
|
||||
@@ -43,7 +44,7 @@ watch(
|
||||
(v) => {
|
||||
breadItems.value = findMatchs(permissionStore.permissions, v)
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function findMatchs(tree, code, parents = []) {
|
||||
@@ -69,8 +70,8 @@ function handleItemClick(item) {
|
||||
|
||||
function getDropOptions(list = []) {
|
||||
return list
|
||||
.filter((item) => item.show)
|
||||
.map((child) => ({
|
||||
.filter(item => item.show)
|
||||
.map(child => ({
|
||||
label: child.name,
|
||||
key: child.code,
|
||||
icon: () => h('i', { class: child.icon }),
|
||||
|
||||
13
src/layouts/components/Fullscreen.vue
Normal file
13
src/layouts/components/Fullscreen.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<i
|
||||
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>
|
||||
@@ -24,7 +24,9 @@
|
||||
|
||||
<template #footer>
|
||||
<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
|
||||
:loading="okLoading"
|
||||
class="ml-20 flex-1"
|
||||
@@ -44,13 +46,13 @@
|
||||
import api from '@/api'
|
||||
import { MeModal } from '@/components'
|
||||
import { useModal } from '@/composables'
|
||||
import { useUserStore, useAuthStore } from '@/store'
|
||||
import { useAuthStore, useUserStore } from '@/store'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
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()
|
||||
function open(options) {
|
||||
@@ -67,7 +69,8 @@ async function setCurrentRole() {
|
||||
okLoading.value = false
|
||||
$message.success('切换成功')
|
||||
modalRef.value?.handleOk()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
return false
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
<template>
|
||||
<router-link class="h-60 f-c-c" to="/">
|
||||
<!-- <img src="@/assets/images/logo.png" class="h-40" /> -->
|
||||
<TheLogo class="rounded-8!" />
|
||||
<TheLogo />
|
||||
<h2
|
||||
v-show="!appStore.collapsed"
|
||||
class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold"
|
||||
@@ -21,6 +20,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
@@ -52,13 +52,14 @@ function handleMenuSelect(key, item) {
|
||||
router.push(item.path)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style>
|
||||
.side-menu:not(.n-menu--collapsed) {
|
||||
.n-menu-item-content {
|
||||
&::before {
|
||||
@@ -66,7 +67,7 @@ function handleMenuSelect(key, item) {
|
||||
right: 8px;
|
||||
}
|
||||
&.n-menu-item-content--selected::before {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
border-left: 4px solid rgb(var(--primary-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
|
||||
import { RoleSelect } from '@/layouts/components'
|
||||
import { initUserAndPermissions } from '@/router'
|
||||
import api from '@/api'
|
||||
import { RoleSelect } from '@/layouts/components'
|
||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -36,7 +35,7 @@ const options = reactive([
|
||||
label: '个人资料',
|
||||
key: 'profile',
|
||||
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: '切换角色',
|
||||
@@ -60,9 +59,7 @@ function handleSelect(key) {
|
||||
case 'toggleRole':
|
||||
roleSelectRef.value?.open({
|
||||
onOk() {
|
||||
initUserAndPermissions().then(() => {
|
||||
router.replace('/')
|
||||
})
|
||||
location.reload()
|
||||
},
|
||||
})
|
||||
break
|
||||
@@ -74,7 +71,8 @@ function handleSelect(key) {
|
||||
async confirm() {
|
||||
try {
|
||||
await api.logout()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
authStore.logout()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export { default as RoleSelect } from './RoleSelect.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 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 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: '关闭右侧',
|
||||
key: 'close-right',
|
||||
disabled:
|
||||
tabStore.tabs.length <= 1 ||
|
||||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
|
||||
tabStore.tabs.length <= 1
|
||||
|| props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
|
||||
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
|
||||
},
|
||||
])
|
||||
@@ -119,7 +119,8 @@ function handleHideDropdown() {
|
||||
|
||||
function handleSelect(key) {
|
||||
const actionFn = actionMap.get(key)
|
||||
actionFn && actionFn()
|
||||
if (typeof actionFn === 'function')
|
||||
actionFn()
|
||||
handleHideDropdown()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<n-tabs
|
||||
:value="tabStore.activeTab"
|
||||
:closable="tabStore.tabs.length > 1"
|
||||
:style="`--selected-bg: ${appStore.isDark ? '#1b2429' : '#eaf0f1'}`"
|
||||
type="card"
|
||||
@close="(path) => tabStore.removeTab(path)"
|
||||
>
|
||||
@@ -37,11 +36,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTabStore } from '@/store'
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTabStore, useAppStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const tabStore = useTabStore()
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
@@ -51,7 +49,7 @@ const contextMenuOption = reactive({
|
||||
currentPath: '',
|
||||
})
|
||||
|
||||
const handleItemClick = (path) => {
|
||||
function handleItemClick(path) {
|
||||
tabStore.setActiveTab(path)
|
||||
router.push(path)
|
||||
}
|
||||
@@ -76,7 +74,7 @@ async function handleContextMenu(e, tagItem) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
:deep(.n-tabs) {
|
||||
.n-tabs-tab {
|
||||
padding-left: 16px;
|
||||
@@ -85,12 +83,12 @@ async function handleContextMenu(e, tagItem) {
|
||||
border-radius: 4px !important;
|
||||
margin-right: 4px;
|
||||
&:hover {
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
border: 1px solid rgb(var(--primary-color)) !important;
|
||||
}
|
||||
}
|
||||
.n-tabs-tab--active {
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
background-color: var(--selected-bg) !important;
|
||||
border: 1px solid rgb(var(--primary-color)) !important;
|
||||
background-color: rgba(var(--primary-color), 0.1) !important;
|
||||
}
|
||||
.n-tabs-pad,
|
||||
.n-tabs-tab-pad,
|
||||
|
||||
@@ -13,16 +13,9 @@
|
||||
<BreadCrumb />
|
||||
|
||||
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
|
||||
<i
|
||||
class="mr-16 cursor-pointer"
|
||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
||||
@click="toggleDark"
|
||||
/>
|
||||
<i
|
||||
class="mr-16 cursor-pointer"
|
||||
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
||||
@click="toggle"
|
||||
/>
|
||||
<ToggleTheme />
|
||||
|
||||
<Fullscreen />
|
||||
|
||||
<i
|
||||
class="i-fe:github mr-16 cursor-pointer"
|
||||
@@ -32,24 +25,17 @@
|
||||
class="i-me:gitee mr-16 cursor-pointer"
|
||||
@click="handleLinkClick('https://gitee.com/isme-admin/vue-naive-admin/tree/2.x')"
|
||||
/>
|
||||
|
||||
<ThemeSetting class="mr-16" />
|
||||
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</AppCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MenuCollapse, UserAvatar, BreadCrumb } from '@/layouts/components'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
const toggleDark = () => {
|
||||
appStore.toggleDark()
|
||||
useToggle(isDark)()
|
||||
}
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
import { ToggleTheme } from '@/components'
|
||||
import { BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||
|
||||
function handleLinkClick(link) {
|
||||
window.open(link)
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import { AppTab } from '@/layouts/components'
|
||||
import SideBar from './sidebar/index.vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import AppHeader from './header/index.vue'
|
||||
import SideBar from './sidebar/index.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -15,16 +15,9 @@
|
||||
<span class="mx-6 opacity-20">|</span>
|
||||
|
||||
<div class="flex flex-shrink-0 items-center px-12 text-18">
|
||||
<i
|
||||
class="mr-16 cursor-pointer"
|
||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
||||
@click="toggleDark"
|
||||
/>
|
||||
<i
|
||||
class="mr-16 cursor-pointer"
|
||||
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
||||
@click="toggle"
|
||||
/>
|
||||
<ToggleTheme />
|
||||
|
||||
<Fullscreen />
|
||||
|
||||
<i
|
||||
class="i-fe:github mr-16 cursor-pointer"
|
||||
@@ -34,24 +27,17 @@
|
||||
class="i-me:gitee mr-16 cursor-pointer"
|
||||
@click="handleLinkClick('https://gitee.com/isme-admin/vue-naive-admin/tree/2.x')"
|
||||
/>
|
||||
|
||||
<ThemeSetting class="mr-16" />
|
||||
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</AppCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { UserAvatar, MenuCollapse, AppTab } from '@/layouts/components'
|
||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
const toggleDark = () => {
|
||||
appStore.toggleDark()
|
||||
useToggle(isDark)()
|
||||
}
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
import { ToggleTheme } from '@/components'
|
||||
import { AppTab, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||
|
||||
function handleLinkClick(link) {
|
||||
window.open(link)
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import SideBar from './sidebar/index.vue'
|
||||
import AppHeader from './header/index.vue'
|
||||
import SideBar from './sidebar/index.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { UserAvatar, MenuCollapse, SideLogo, SideMenu } from '@/layouts/components'
|
||||
import { MenuCollapse, SideLogo, SideMenu, UserAvatar } from '@/layouts/components'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
28
src/main.js
28
src/main.js
@@ -8,24 +8,40 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import '@/styles/reset.css'
|
||||
import '@/styles/global.scss'
|
||||
import 'uno.css'
|
||||
|
||||
import ExcelJS from 'exceljs'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import { setupDirectives } from './directives'
|
||||
/**
|
||||
* 引入vxe-table 和 vxe-pc-ui excel导出
|
||||
*/
|
||||
import VxeUI from 'vxe-pc-ui'
|
||||
import VXETable from 'vxe-table'
|
||||
import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
|
||||
import { setupRouter } from './router'
|
||||
import { setupStore } from './store'
|
||||
|
||||
import { setupNaiveDiscreteApi } from './utils'
|
||||
import { setupDirectives } from './directives'
|
||||
import 'vxe-pc-ui/lib/style.css'
|
||||
import 'vxe-table/lib/style.css'
|
||||
import '@/styles/reset.css'
|
||||
import '@/styles/global.css'
|
||||
import 'uno.css'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
|
||||
VXETable.use(VXETablePluginExportXLSX, {
|
||||
ExcelJS,
|
||||
})
|
||||
app.use(VxeUI).use(VXETable)
|
||||
|
||||
setupStore(app)
|
||||
setupNaiveDiscreteApi()
|
||||
setupDirectives(app)
|
||||
await setupRouter(app)
|
||||
app.mount('#app')
|
||||
setupNaiveDiscreteApi()
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
@@ -13,7 +13,8 @@ export function createPageTitleGuard(router) {
|
||||
const pageTitle = to.meta?.title
|
||||
if (pageTitle) {
|
||||
document.title = `${pageTitle} | ${baseTitle}`
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
document.title = baseTitle
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { useAuthStore } from '@/store'
|
||||
import api from '@/api'
|
||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||
import { getPermissions, getUserInfo } from '@/store/helper'
|
||||
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
@@ -17,16 +18,34 @@ export function createPermissionGuard(router) {
|
||||
|
||||
/** 没有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 } }
|
||||
}
|
||||
|
||||
// 有token的情况
|
||||
if (to.path === '/login') return { path: '/' }
|
||||
if (WHITE_LIST.includes(to.path)) return true
|
||||
if (to.path === '/login')
|
||||
return { path: '/' }
|
||||
if (WHITE_LIST.includes(to.path))
|
||||
return true
|
||||
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
if (!userStore.userInfo) {
|
||||
const [user, permissions] = await Promise.all([getUserInfo(), getPermissions()])
|
||||
userStore.setUser(user)
|
||||
permissionStore.setPermissions(permissions)
|
||||
const routeComponents = import.meta.glob('@/views/**/*.vue')
|
||||
permissionStore.accessRoutes.forEach((route) => {
|
||||
route.component = routeComponents[route.component] || undefined
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
return { ...to, replace: true }
|
||||
}
|
||||
|
||||
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
|
||||
const { data: hasMenu } = await api.validateMenuPath(to.path)
|
||||
|
||||
@@ -12,7 +12,8 @@ export const EXCLUDE_TAB = ['/404', '/403', '/login']
|
||||
|
||||
export function createTabGuard(router) {
|
||||
router.afterEach((to) => {
|
||||
if (EXCLUDE_TAB.includes(to.path)) return
|
||||
if (EXCLUDE_TAB.includes(to.path))
|
||||
return
|
||||
const tabStore = useTabStore()
|
||||
const { name, fullPath: path } = to
|
||||
const title = to.meta?.title
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuards } from './guards'
|
||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||
import { getPermissions, getUserInfo } from '@/store/helper'
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import { basicRoutes } from './basic-routes'
|
||||
import { setupRouterGuards } from './guards'
|
||||
|
||||
export const router = createRouter({
|
||||
history:
|
||||
@@ -22,36 +20,6 @@ export const router = createRouter({
|
||||
})
|
||||
|
||||
export async function setupRouter(app) {
|
||||
try {
|
||||
await initUserAndPermissions()
|
||||
} catch (error) {
|
||||
console.error('🚀 初始化失败', error)
|
||||
}
|
||||
setupRouterGuards(router)
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
export async function initUserAndPermissions() {
|
||||
const permissionStore = usePermissionStore()
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (!authStore.accessToken) {
|
||||
const route = unref(router.currentRoute)
|
||||
if (route.path !== '/login') {
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: route.query,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const [user, permissions] = await Promise.all([getUserInfo(), getPermissions()])
|
||||
userStore.setUser(user)
|
||||
permissionStore.setPermissions(permissions)
|
||||
const routeComponents = import.meta.glob('@/views/**/*.vue')
|
||||
permissionStore.accessRoutes.forEach((route) => {
|
||||
route.component = routeComponents[route.component] || undefined
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
setupRouterGuards(router)
|
||||
}
|
||||
|
||||
@@ -8,32 +8,17 @@
|
||||
|
||||
export const defaultLayout = 'normal'
|
||||
|
||||
export const defaultPrimaryColor = '#316C72'
|
||||
|
||||
// 控制 LayoutSetting 组件是否可见
|
||||
export const layoutSettingVisible = true
|
||||
|
||||
export const naiveThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#316C72FF',
|
||||
primaryColorHover: '#316C72E3',
|
||||
primaryColorPressed: '#2B4C59FF',
|
||||
primaryColorSuppl: '#316C72E3',
|
||||
|
||||
infoColor: '#2080F0FF',
|
||||
infoColorHover: '#4098FCFF',
|
||||
infoColorPressed: '#1060C9FF',
|
||||
infoColorSuppl: '#4098FCFF',
|
||||
|
||||
successColor: '#18A058FF',
|
||||
successColorHover: '#36AD6AFF',
|
||||
successColorPressed: '#0C7A43FF',
|
||||
successColorSuppl: '#36AD6AFF',
|
||||
|
||||
warningColor: '#F0A020FF',
|
||||
warningColorHover: '#FCB040FF',
|
||||
warningColorPressed: '#C97C10FF',
|
||||
warningColorSuppl: '#FCB040FF',
|
||||
|
||||
errorColor: '#D03050FF',
|
||||
errorColorHover: '#DE576DFF',
|
||||
errorColorPressed: '#AB1F3FFF',
|
||||
errorColorSuppl: '#DE576DFF',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,7 +36,7 @@ export const basePermissions = [
|
||||
code: 'ShowDocs',
|
||||
name: '项目文档',
|
||||
type: 'MENU',
|
||||
path: 'https://docs.isme.top/web/#/624306705/188522224',
|
||||
path: 'https://isme.top',
|
||||
icon: 'i-me:docs',
|
||||
order: 1,
|
||||
enable: true,
|
||||
@@ -67,13 +52,23 @@ export const basePermissions = [
|
||||
enable: true,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
code: 'NaiveUI',
|
||||
name: 'Naive UI',
|
||||
type: 'MENU',
|
||||
path: 'https://www.naiveui.com/zh-CN/os-theme',
|
||||
icon: 'i-me:naiveui',
|
||||
order: 3,
|
||||
enable: true,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
code: 'MyBlog',
|
||||
name: '博客-掘金',
|
||||
type: 'MENU',
|
||||
path: 'https://juejin.cn/user/1961184475483255/posts',
|
||||
icon: 'i-simple-icons:juejin',
|
||||
order: 3,
|
||||
order: 4,
|
||||
enable: true,
|
||||
show: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { basePermissions } from '@/settings'
|
||||
import api from '@/api'
|
||||
import { basePermissions } from '@/settings'
|
||||
|
||||
export async function getUserInfo() {
|
||||
const res = await api.getUser()
|
||||
@@ -22,7 +22,8 @@ export async function getPermissions() {
|
||||
try {
|
||||
const res = await api.getRolePermissions()
|
||||
asyncPermissions = res?.data || []
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return basePermissions.concat(asyncPermissions)
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
||||
import { generate, getRgbStr } from '@arco-design/color'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { defaultLayout, naiveThemeOverrides } from '@/settings'
|
||||
import { defineStore } from 'pinia'
|
||||
import { VxeUI } from 'vxe-pc-ui'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
collapsed: false,
|
||||
isDark: useDark(),
|
||||
layout: defaultLayout,
|
||||
primaryColor: defaultPrimaryColor,
|
||||
naiveThemeOverrides,
|
||||
}),
|
||||
actions: {
|
||||
@@ -30,9 +33,37 @@ export const useAppStore = defineStore('app', {
|
||||
setLayout(v) {
|
||||
this.layout = v
|
||||
},
|
||||
setPrimaryColor(color) {
|
||||
this.primaryColor = color
|
||||
},
|
||||
setThemeColor(color = this.primaryColor, isDark = this.isDark) {
|
||||
const colors = generate(color, {
|
||||
list: true,
|
||||
dark: isDark,
|
||||
})
|
||||
VxeUI.setTheme(isDark ? 'dark' : 'light')
|
||||
if (isDark) {
|
||||
document.body.style.setProperty('--vxe-ui-font-color', '#fff')
|
||||
}
|
||||
else {
|
||||
document.body.style.setProperty('--vxe-ui-font-color', '#000')
|
||||
}
|
||||
|
||||
document.body.style.setProperty('--vxe-ui-font-primary-color', color)
|
||||
document.body.style.setProperty('--vxe-ui-font-primary-lighten-color', color)
|
||||
document.body.style.setProperty('--vxe-ui-font-primary-darken-color', color)
|
||||
|
||||
document.body.style.setProperty('--primary-color', getRgbStr(colors[5]))
|
||||
this.naiveThemeOverrides.common = Object.assign(this.naiveThemeOverrides.common || {}, {
|
||||
primaryColor: colors[5],
|
||||
primaryColorHover: colors[4],
|
||||
primaryColorSuppl: colors[4],
|
||||
primaryColorPressed: colors[6],
|
||||
})
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
paths: ['collapsed', 'naiveThemeOverrides'],
|
||||
paths: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useUserStore, usePermissionStore, useTabStore, useRouterStore } from '@/store'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './app'
|
||||
export * from './auth'
|
||||
export * from './permission'
|
||||
export * from './router'
|
||||
export * from './tab'
|
||||
export * from './user'
|
||||
export * from './router'
|
||||
|
||||
@@ -20,35 +20,38 @@ export const usePermissionStore = defineStore('permission', {
|
||||
setPermissions(permissions) {
|
||||
this.permissions = permissions
|
||||
this.menus = this.permissions
|
||||
.filter((item) => item.type === 'MENU')
|
||||
.map((item) => this.getMenuItem(item))
|
||||
.filter((item) => !!item)
|
||||
.filter(item => item.type === 'MENU')
|
||||
.map(item => this.getMenuItem(item))
|
||||
.filter(item => !!item)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
getMenuItem(item, parent) {
|
||||
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.show) return null
|
||||
if (item.enable && route.path && !route.path.startsWith('http'))
|
||||
this.accessRoutes.push(route)
|
||||
if (!item.show)
|
||||
return null
|
||||
const menuItem = {
|
||||
label: route.meta.title,
|
||||
key: route.name,
|
||||
path: route.path,
|
||||
originPath: route.meta.originPath,
|
||||
icon: () => h('i', { class: `${route.meta.icon}?mask text-16` }),
|
||||
icon: () => h('i', { class: `${route.meta.icon} text-16` }),
|
||||
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) {
|
||||
menuItem.children = children
|
||||
.map((child) => this.getMenuItem(child, menuItem))
|
||||
.filter((item) => !!item)
|
||||
.map(child => this.getMenuItem(child, menuItem))
|
||||
.filter(item => !!item)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
if (!menuItem.children.length) delete menuItem.children
|
||||
if (!menuItem.children.length)
|
||||
delete menuItem.children
|
||||
}
|
||||
return menuItem
|
||||
},
|
||||
generateRoute(item, parentKey) {
|
||||
let originPath = undefined
|
||||
let originPath
|
||||
if (isExternal(item.path)) {
|
||||
originPath = item.path
|
||||
item.component = '/src/views/iframe/index.vue'
|
||||
@@ -61,14 +64,14 @@ export const usePermissionStore = defineStore('permission', {
|
||||
component: item.component,
|
||||
meta: {
|
||||
originPath,
|
||||
icon: item.icon,
|
||||
icon: `${item.icon}?mask`,
|
||||
title: item.name,
|
||||
layout: item.layout,
|
||||
keepAlive: !!item.keepAlive,
|
||||
parentKey,
|
||||
btns: item.children
|
||||
?.filter((item) => item.type === 'BUTTON')
|
||||
.map((item) => ({ code: item.code, name: item.name })),
|
||||
?.filter(item => item.type === 'BUTTON')
|
||||
.map(item => ({ code: item.code, name: item.name })),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useTabStore = defineStore('tab', {
|
||||
}),
|
||||
getters: {
|
||||
activeIndex() {
|
||||
return this.tabs.findIndex((item) => item.path === this.activeTab)
|
||||
return this.tabs.findIndex(item => item.path === this.activeTab)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@@ -29,19 +29,22 @@ export const useTabStore = defineStore('tab', {
|
||||
this.tabs = tabs
|
||||
},
|
||||
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) {
|
||||
this.tabs.splice(findIndex, 1, tab)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.setTabs([...this.tabs, tab])
|
||||
}
|
||||
this.setActiveTab(tab.path)
|
||||
},
|
||||
async reloadTab(path, keepAlive) {
|
||||
const findItem = this.tabs.find((item) => item.path === path)
|
||||
if (!findItem) return
|
||||
const findItem = this.tabs.find(item => item.path === path)
|
||||
if (!findItem)
|
||||
return
|
||||
// 更新key可让keepAlive失效
|
||||
if (keepAlive) findItem.keepAlive = false
|
||||
if (keepAlive)
|
||||
findItem.keepAlive = false
|
||||
$loadingBar.start()
|
||||
this.reloading = true
|
||||
await nextTick()
|
||||
@@ -53,30 +56,30 @@ export const useTabStore = defineStore('tab', {
|
||||
}, 100)
|
||||
},
|
||||
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) {
|
||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
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) {
|
||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
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)
|
||||
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)
|
||||
}
|
||||
},
|
||||
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)
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,12 +26,12 @@ body {
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transform: translateX(-2%);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
transform: translateX(2%);
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
@@ -68,7 +68,24 @@ body {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-color);
|
||||
background: rgb(var(--primary-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 切换主题的动画效果 */
|
||||
::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,388 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:26:38
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
box-sizing: border-box; /* 1 */
|
||||
border-width: 0; /* 2 */
|
||||
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 {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
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,
|
||||
ul {
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
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,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Make elements with the HTML hidden attribute stay hidden by default.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* @desc 格式化时间
|
||||
* @param {(Object|string|number)} time
|
||||
* @param {(object | string | number)} time
|
||||
* @param {string} format
|
||||
* @returns {string | null}
|
||||
* @returns {string | null} 格式化后的时间字符串
|
||||
*
|
||||
*/
|
||||
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
return dayjs(time).format(format)
|
||||
@@ -24,19 +24,19 @@ export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数节流
|
||||
* @param {Function} fn
|
||||
* @param {Number} wait
|
||||
* @returns {Function}
|
||||
* @param {number} wait
|
||||
* @returns {Function} 节流函数
|
||||
*
|
||||
*/
|
||||
export function throttle(fn, wait) {
|
||||
var context, args
|
||||
var previous = 0
|
||||
let context, args
|
||||
let previous = 0
|
||||
|
||||
return function () {
|
||||
var now = +new Date()
|
||||
return function (...argArr) {
|
||||
const now = +new Date()
|
||||
context = this
|
||||
args = arguments
|
||||
args = argArr
|
||||
if (now - previous > wait) {
|
||||
fn.apply(context, args)
|
||||
previous = now
|
||||
@@ -45,16 +45,15 @@ export function throttle(fn, wait) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数防抖
|
||||
* @param {Function} func
|
||||
* @param {Function} method
|
||||
* @param {number} wait
|
||||
* @param {boolean} immediate
|
||||
* @return {*}
|
||||
* @return {*} 防抖函数
|
||||
*/
|
||||
export function debounce(method, wait, immediate) {
|
||||
let timeout
|
||||
return function (...args) {
|
||||
let context = this
|
||||
const context = this
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
@@ -64,14 +63,15 @@ export function debounce(method, wait, immediate) {
|
||||
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
|
||||
* 这样确保立即执行后wait毫秒内不会被再次触发
|
||||
*/
|
||||
let callNow = !timeout
|
||||
const callNow = !timeout
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
}, wait)
|
||||
if (callNow) {
|
||||
method.apply(context, args)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// 如果immediate为false,则函数wait毫秒后执行
|
||||
timeout = setTimeout(() => {
|
||||
/**
|
||||
@@ -85,12 +85,11 @@ export function debounce(method, wait, immediate) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 睡一会儿,让子弹暂停一下
|
||||
* @param {number} time 毫秒数
|
||||
* @returns
|
||||
* @returns 睡一会儿,让子弹暂停一下
|
||||
*/
|
||||
export function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
return new Promise(resolve => setTimeout(resolve, time))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
let isConfirming = false
|
||||
export function resolveResError(code, message) {
|
||||
export function resolveResError(code, message, needTip = true) {
|
||||
switch (code) {
|
||||
case 401:
|
||||
if (isConfirming) return
|
||||
if (isConfirming || !needTip)
|
||||
return
|
||||
isConfirming = true
|
||||
$dialog.confirm({
|
||||
title: '提示',
|
||||
@@ -31,7 +32,8 @@ export function resolveResError(code, message) {
|
||||
return false
|
||||
case 11007:
|
||||
case 11008:
|
||||
if (isConfirming) return
|
||||
if (isConfirming || !needTip)
|
||||
return
|
||||
isConfirming = true
|
||||
$dialog.confirm({
|
||||
title: '提示',
|
||||
@@ -60,5 +62,6 @@ export function resolveResError(code, message) {
|
||||
message = message ?? `【${code}】: 未知异常!`
|
||||
break
|
||||
}
|
||||
needTip && window.$message?.error(message)
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { setupInterceptors } from './interceptors'
|
||||
|
||||
export function createAxios(options = {}) {
|
||||
const defaultOptions = {
|
||||
baseURL: '/api',
|
||||
baseURL: import.meta.env.VITE_AXIOS_BASE_URL,
|
||||
timeout: 12000,
|
||||
}
|
||||
const service = axios.create({
|
||||
|
||||
@@ -7,29 +7,10 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { resolveResError } from './helpers'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { resolveResError } from './helpers'
|
||||
|
||||
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]
|
||||
function resResolve(response) {
|
||||
const { data, status, config, statusText, headers } = response
|
||||
@@ -39,34 +20,51 @@ export function setupInterceptors(axiosInstance) {
|
||||
}
|
||||
const code = data?.code ?? status
|
||||
|
||||
// 根据code处理对应的操作,并返回处理后的message
|
||||
const message = resolveResError(code, data?.message ?? statusText)
|
||||
const needTip = config?.needTip !== false
|
||||
|
||||
// 根据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.resolve(data ?? response)
|
||||
}
|
||||
|
||||
async function resReject(error) {
|
||||
if (!error || !error.response) {
|
||||
const code = error?.code
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
const message = resolveResError(code, error.message)
|
||||
window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error })
|
||||
}
|
||||
|
||||
const { data, status, config } = error.response
|
||||
const code = data?.code ?? status
|
||||
|
||||
const message = resolveResError(code, data?.message ?? error.message)
|
||||
/** 需要错误提醒 */
|
||||
!config?.noNeedTip && message && window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error: error.response?.data || error.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) {
|
||||
if (!error || !error.response) {
|
||||
const code = error?.code
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
const message = resolveResError(code, error.message)
|
||||
return Promise.reject({ code, message, error })
|
||||
}
|
||||
|
||||
const { data, status, config } = error.response
|
||||
const code = data?.code ?? status
|
||||
|
||||
const needTip = config?.needTip !== false
|
||||
const message = resolveResError(code, data?.message ?? error.message, needTip)
|
||||
return Promise.reject({ code, message, error: error.response?.data || error.response })
|
||||
}
|
||||
|
||||
@@ -99,28 +99,27 @@ export function isEmpty(val) {
|
||||
}
|
||||
|
||||
/**
|
||||
* * 类似mysql的IFNULL函数
|
||||
* * 第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数
|
||||
* @param {Number|Boolean|String} val
|
||||
* @param {Number|Boolean|String} def
|
||||
* @returns
|
||||
* 类似mysql的IFNULL函数
|
||||
*
|
||||
* @param {number | boolean | string} val
|
||||
* @param {number | boolean | string} def
|
||||
* @returns 第一个参数为null | undefined | '' 则返回第二个参数作为备用值,否则返回第一个参数
|
||||
*/
|
||||
export function ifNull(val, def = '') {
|
||||
return isNullOrWhitespace(val) ? def : val
|
||||
}
|
||||
|
||||
export function isUrl(path) {
|
||||
const reg =
|
||||
/(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
|
||||
const reg = /^https?:\/\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#/%=~|]$/
|
||||
return reg.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
* @returns {boolean} 是否是外部链接
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
return /^https?:|mailto:|tel:/.test(path)
|
||||
}
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import * as NaiveUI from 'naive-ui'
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
import { useAppStore } from '@/store'
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
import * as NaiveUI from 'naive-ui'
|
||||
|
||||
export function setupMessage(NMessage) {
|
||||
class Message {
|
||||
static instance
|
||||
constructor() {
|
||||
// 单例模式
|
||||
if (Message.instance) return Message.instance
|
||||
if (Message.instance)
|
||||
return Message.instance
|
||||
Message.instance = this
|
||||
this.message = {}
|
||||
this.removeTimer = {}
|
||||
@@ -37,7 +38,7 @@ export function setupMessage(NMessage) {
|
||||
|
||||
showMessage(type, content, option = {}) {
|
||||
if (Array.isArray(content)) {
|
||||
return content.forEach((msg) => NMessage[type](msg, option))
|
||||
return content.forEach(msg => NMessage[type](msg, option))
|
||||
}
|
||||
|
||||
if (!option.key) {
|
||||
@@ -48,7 +49,8 @@ export function setupMessage(NMessage) {
|
||||
if (currentMessage) {
|
||||
currentMessage.type = type
|
||||
currentMessage.content = content
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.message[option.key] = NMessage[type](content, {
|
||||
...option,
|
||||
duration: 0,
|
||||
@@ -109,7 +111,7 @@ export function setupNaiveDiscreteApi() {
|
||||
}))
|
||||
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar'],
|
||||
{ configProviderProps }
|
||||
{ configProviderProps },
|
||||
)
|
||||
|
||||
window.$loadingBar = loadingBar
|
||||
|
||||
@@ -35,7 +35,8 @@ class Storage {
|
||||
|
||||
getItem(key, def = null) {
|
||||
const val = this.storage.getItem(this.getKey(key))
|
||||
if (!val) return def
|
||||
if (!val)
|
||||
return def
|
||||
try {
|
||||
const data = JSON.parse(val)
|
||||
const { value, time, expire } = data
|
||||
@@ -44,7 +45,9 @@ class Storage {
|
||||
}
|
||||
this.remove(key)
|
||||
return def
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
this.remove(key)
|
||||
return def
|
||||
}
|
||||
|
||||
@@ -12,12 +12,24 @@
|
||||
<n-card title="按钮 Button">
|
||||
<n-space>
|
||||
<n-button>Default</n-button>
|
||||
<n-button type="tertiary">Tertiary</n-button>
|
||||
<n-button type="primary">Primary</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-button type="tertiary">
|
||||
Tertiary
|
||||
</n-button>
|
||||
<n-button type="primary">
|
||||
Primary
|
||||
</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-card>
|
||||
|
||||
@@ -46,10 +58,18 @@
|
||||
<n-space size="large" mt-30>
|
||||
<n-card min-w-340 title="通知 Notification">
|
||||
<n-space>
|
||||
<n-button @click="notify('info')">信息</n-button>
|
||||
<n-button @click="notify('success')">成功</n-button>
|
||||
<n-button @click="notify('warning')">警告</n-button>
|
||||
<n-button @click="notify('error')">错误</n-button>
|
||||
<n-button @click="notify('info')">
|
||||
信息
|
||||
</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-card>
|
||||
|
||||
@@ -66,7 +86,9 @@
|
||||
<i v-show="!loading" class="i-mdi:login mr-4" />
|
||||
登录
|
||||
</n-button>
|
||||
<n-button type="error" @click="handleMultiMessage">多个错误提醒</n-button>
|
||||
<n-button type="error" @click="handleMultiMessage">
|
||||
多个错误提醒
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
@@ -75,6 +97,7 @@
|
||||
|
||||
<script setup>
|
||||
import { sleep } from '@/utils'
|
||||
|
||||
const handleDelete = function () {
|
||||
$dialog.confirm({
|
||||
content: '确认删除?',
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
<div w-350>
|
||||
<n-input v-model:value="inputVal" />
|
||||
<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>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<n-button type="primary" @click="openModal1">打开第一个弹个窗</n-button>
|
||||
<n-button type="primary" @click="openModal1">
|
||||
打开第一个弹个窗
|
||||
</n-button>
|
||||
<MeModal ref="$modal1">
|
||||
<n-input v-model:value="text" />
|
||||
</MeModal>
|
||||
@@ -20,8 +22,8 @@
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import { sleep } from '@/utils'
|
||||
import { useModal } from '@/composables'
|
||||
import { sleep } from '@/utils'
|
||||
|
||||
const text = ref('')
|
||||
const [$modal1, okLoading1] = useModal()
|
||||
|
||||
@@ -9,16 +9,27 @@
|
||||
<template>
|
||||
<CommonPage show-footer>
|
||||
<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>
|
||||
点击查看如何使用 Unocss 图标
|
||||
</n-tooltip>
|
||||
</template>
|
||||
<ul class="flex flex-wrap justify-between">
|
||||
<ul class="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] justify-items-center gap-16">
|
||||
<li
|
||||
v-for="item in icons"
|
||||
: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 rounded-12 px-12 py-24 card-border auto-bg"
|
||||
@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
|
||||
class="mt-16 text-center text-14 text-gray-400 hover:color-primary"
|
||||
@click.stop="copy(item)"
|
||||
@@ -26,13 +37,6 @@
|
||||
{{ item }}
|
||||
</span>
|
||||
</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>
|
||||
</CommonPage>
|
||||
</template>
|
||||
@@ -44,6 +48,7 @@ import icons from 'isme:icons'
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
watch(copied, (val) => {
|
||||
val && $message.success('已复制到剪切板')
|
||||
if (val)
|
||||
$message.success('已复制到剪切板')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
<n-upload-dragger>
|
||||
<div class="h-150 f-c-c flex-col">
|
||||
<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>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
@@ -31,7 +33,9 @@
|
||||
<n-image width="200" :src="item.url" />
|
||||
</div>
|
||||
<n-space class="mt-16" justify="space-evenly">
|
||||
<n-button dashed type="primary" @click="copy(item.url)">url</n-button>
|
||||
<n-button dashed type="primary" @click="copy(item.url)">
|
||||
url
|
||||
</n-button>
|
||||
<n-button dashed type="primary" @click="copy(``)">
|
||||
MD
|
||||
</n-button>
|
||||
@@ -53,19 +57,20 @@
|
||||
|
||||
<script setup>
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
defineOptions({ name: 'ImgUpload' })
|
||||
|
||||
const { copy, copied } = useClipboard()
|
||||
|
||||
const imgList = reactive([
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||
{ url: 'https://img.isme.top/isme/67208863145ef.jpg' },
|
||||
{ url: 'https://img.isme.top/isme/67208ab2a9de0.jpg' },
|
||||
{ url: 'https://img.isme.top/isme/67208ab4c6596.jpg' },
|
||||
])
|
||||
|
||||
watch(copied, (val) => {
|
||||
val && $message.success('已复制到剪切板')
|
||||
if (val)
|
||||
$message.success('已复制到剪切板')
|
||||
})
|
||||
|
||||
function onBeforeUpload({ file }) {
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
<n-button v-if="back" type="primary" ghost @click="router.replace(back)">
|
||||
返回上一页
|
||||
</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>
|
||||
</n-result>
|
||||
</div>
|
||||
@@ -35,7 +37,8 @@ const back = history.state.back
|
||||
|
||||
if (history.state.from === 'permission-guard') {
|
||||
delete history.state.from
|
||||
} else if (route.query.path) {
|
||||
}
|
||||
else if (route.query.path) {
|
||||
router.replace(route.query.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
size="large"
|
||||
>
|
||||
<template #footer>
|
||||
<n-button type="primary" ghost @click="replace('/')">返回首页</n-button>
|
||||
<n-button type="primary" ghost @click="replace('/')">
|
||||
返回首页
|
||||
</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功。</p>
|
||||
<p class="mt-12 text-right text-12 opacity-40">—— 查尔斯·史考伯</p>
|
||||
<p class="mt-28 text-14 opacity-60">
|
||||
一个人几乎可以在任何他怀有无限热忱的事情上成功。
|
||||
</p>
|
||||
<p class="mt-12 text-right text-12 opacity-40">
|
||||
—— 查尔斯·史考伯
|
||||
</p>
|
||||
</n-card>
|
||||
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
||||
<template #header-extra>
|
||||
@@ -45,7 +49,7 @@
|
||||
type="primary"
|
||||
ghost
|
||||
tag="a"
|
||||
href="https://docs.isme.top/web/#/624306705/188522224"
|
||||
href="https://isme.top"
|
||||
target="__blank"
|
||||
>
|
||||
开发文档
|
||||
@@ -148,16 +152,18 @@
|
||||
</div>
|
||||
|
||||
<n-card class="mt-12" title="⚡️ 趋势" segmented>
|
||||
<VChart :option="trendOption" :init-options="{ height: 400 }" autoresize />
|
||||
<div class="h-400">
|
||||
<VChart :option="trendOption" autoresize />
|
||||
</div>
|
||||
</n-card>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<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 { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { UniversalTransition } from 'echarts/features'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import VChart from 'vue-echarts'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<AppPage full>
|
||||
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full"></iframe>
|
||||
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full" />
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
toggleRole: (data) => request.post('/auth/role/toggle', data),
|
||||
login: (data) => request.post('/auth/login', data, { noNeedToken: true }),
|
||||
toggleRole: data => request.post('/auth/role/toggle', data),
|
||||
login: data => request.post('/auth/login', data, { needToken: false }),
|
||||
getUser: () => request.get('/user/detail'),
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
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"
|
||||
>
|
||||
<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 class="w-320 flex-col px-20 py-32">
|
||||
<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 }}
|
||||
</h2>
|
||||
<n-input
|
||||
@@ -64,7 +64,7 @@
|
||||
height="40"
|
||||
class="ml-12 w-80 cursor-pointer"
|
||||
@click="initCaptcha"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<n-checkbox
|
||||
@@ -101,11 +101,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { throttle, lStorage } from '@/utils'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { lStorage, throttle } from '@/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import api from './api'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { initUserAndPermissions } from '@/router'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
@@ -119,7 +118,7 @@ const loginInfo = ref({
|
||||
|
||||
const captchaUrl = ref('')
|
||||
const initCaptcha = throttle(() => {
|
||||
captchaUrl.value = '/api/auth/captcha?' + Date.now()
|
||||
captchaUrl.value = `${import.meta.env.VITE_AXIOS_BASE_URL}/auth/captcha?${Date.now()}`
|
||||
}, 500)
|
||||
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
@@ -139,19 +138,23 @@ const isRemember = useStorage('isRemember', true)
|
||||
const loading = ref(false)
|
||||
async function handleLogin(isQuick) {
|
||||
const { username, password, captcha } = loginInfo.value
|
||||
if (!username || !password) return $message.warning('请输入用户名和密码')
|
||||
if (!isQuick && !captcha) return $message.warning('请输入验证码')
|
||||
if (!username || !password)
|
||||
return $message.warning('请输入用户名和密码')
|
||||
if (!isQuick && !captcha)
|
||||
return $message.warning('请输入验证码')
|
||||
try {
|
||||
loading.value = true
|
||||
$message.loading('正在验证,请稍后...', { key: 'login' })
|
||||
const { data } = await api.login({ username, password: password.toString(), captcha, isQuick })
|
||||
if (isRemember.value) {
|
||||
lStorage.set('loginInfo', { username, password })
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
lStorage.remove('loginInfo')
|
||||
}
|
||||
onLoginSuccess(data)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
// 10003为验证码错误专属业务码
|
||||
if (error?.code === 10003) {
|
||||
// 为防止爆破,验证码错误则刷新验证码
|
||||
@@ -167,16 +170,17 @@ async function onLoginSuccess(data = {}) {
|
||||
authStore.setToken(data)
|
||||
$message.loading('登录中...', { key: 'login' })
|
||||
try {
|
||||
await initUserAndPermissions()
|
||||
$message.success('登录成功', { key: 'login' })
|
||||
if (route.query.redirect) {
|
||||
const path = route.query.redirect
|
||||
delete route.query.redirect
|
||||
router.push({ path, query: route.query })
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
router.push('/')
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
$message.destroy('login')
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2023/12/05 21:28:47
|
||||
* @LastEditTime: 2024/04/01 15:52:04
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import axios from 'axios'
|
||||
import { request } from '@/utils'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
getMenuTree: () => request.get('/permission/menu/tree'),
|
||||
getButtonAndApi: (parentId) => request.get(`/permission/button-and-api/${parentId}`),
|
||||
getButtons: ({ parentId }) => request.get(`/permission/button/${parentId}`),
|
||||
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),
|
||||
deletePermission: (id) => request.delete(`permission/${id}`),
|
||||
deletePermission: id => request.delete(`permission/${id}`),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:28:59
|
||||
- @LastEditTime: 2024/04/01 15:51:34
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
@@ -12,23 +12,24 @@
|
||||
<h3>菜单</h3>
|
||||
<div class="flex">
|
||||
<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" />
|
||||
新增
|
||||
</n-button>
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<n-tree
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="pattern"
|
||||
:data="treeData"
|
||||
:selected-keys="[currentMenu?.code]"
|
||||
:render-prefix="renderPrefix"
|
||||
:render-suffix="renderSuffix"
|
||||
:on-update:selected-keys="onSelect"
|
||||
key-field="code"
|
||||
label-field="name"
|
||||
block-line
|
||||
default-expand-all
|
||||
|
||||
block-line default-expand-all
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
@@ -37,10 +38,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { withModifiers } from 'vue'
|
||||
import ResAddOrEdit from './ResAddOrEdit.vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { withModifiers } from 'vue'
|
||||
import api from '../api'
|
||||
import ResAddOrEdit from './ResAddOrEdit.vue'
|
||||
|
||||
defineProps({
|
||||
treeData: {
|
||||
@@ -85,7 +86,7 @@ function renderSuffix({ option }) {
|
||||
size: 'tiny',
|
||||
onClick: withModifiers(() => handleAdd({ parentId: option.id }), ['stop']),
|
||||
},
|
||||
{ default: () => '新增' }
|
||||
{ default: () => '新增' },
|
||||
),
|
||||
|
||||
h(
|
||||
@@ -97,7 +98,7 @@ function renderSuffix({ option }) {
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: withModifiers(() => handleDelete(option), ['stop']),
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -111,7 +112,10 @@ function handleDelete(item) {
|
||||
await api.deletePermission(item.id)
|
||||
$message.success('删除成功', { key: 'deleteMenu' })
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
emit('update:currentMenu', null)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
$message.destroy('deleteMenu')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/12 09:03:43
|
||||
- @LastEditTime: 2024/04/01 15:52:31
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
@@ -20,6 +20,7 @@
|
||||
<n-tree-select
|
||||
v-model:value="modalForm.parentId"
|
||||
:options="menuOptions"
|
||||
:disabled="parentIdDisabled"
|
||||
label-field="name"
|
||||
key-field="id"
|
||||
placeholder="根菜单"
|
||||
@@ -39,6 +40,7 @@
|
||||
<n-input v-model:value="modalForm.code" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-if="modalForm.type === 'MENU'"
|
||||
:span="12"
|
||||
path="path"
|
||||
:rule="{
|
||||
@@ -58,7 +60,7 @@
|
||||
</template>
|
||||
<n-input v-model:value="modalForm.path" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="icon">
|
||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="icon">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="菜单图标"
|
||||
@@ -67,7 +69,7 @@
|
||||
</template>
|
||||
<n-select v-model:value="modalForm.icon" :options="iconOptions" clearable filterable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="layout">
|
||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="layout">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="layout"
|
||||
@@ -76,7 +78,7 @@
|
||||
</template>
|
||||
<n-select v-model:value="modalForm.layout" :options="layoutOptions" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="24" path="component">
|
||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="24" path="component">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="组件路径"
|
||||
@@ -92,13 +94,17 @@
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<n-form-item-gi :span="12" path="show">
|
||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="show">
|
||||
<template #label>
|
||||
<QuestionLabel label="显示状态" content="控制是否在菜单栏显示,不影响路由注册" />
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.show">
|
||||
<template #checked>显示</template>
|
||||
<template #unchecked>隐藏</template>
|
||||
<template #checked>
|
||||
显示
|
||||
</template>
|
||||
<template #unchecked>
|
||||
隐藏
|
||||
</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="enable">
|
||||
@@ -109,11 +115,15 @@
|
||||
/>
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>禁用</template>
|
||||
<template #checked>
|
||||
启用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
禁用
|
||||
</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="enable">
|
||||
<n-form-item-gi v-if="modalForm.type === 'MENU'" :span="12" path="keepAlive">
|
||||
<template #label>
|
||||
<QuestionLabel
|
||||
label="KeepAlive"
|
||||
@@ -121,11 +131,16 @@
|
||||
/>
|
||||
</template>
|
||||
<n-switch v-model:value="modalForm.keepAlive">
|
||||
<template #checked>是</template>
|
||||
<template #unchecked>否</template>
|
||||
<template #checked>
|
||||
是
|
||||
</template>
|
||||
<template #unchecked>
|
||||
否
|
||||
</template>
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-if="modalForm.type === 'MENU'"
|
||||
:span="12"
|
||||
label="排序"
|
||||
path="order"
|
||||
@@ -145,11 +160,11 @@
|
||||
|
||||
<script setup>
|
||||
import { MeModal } from '@/components'
|
||||
import QuestionLabel from './QuestionLabel.vue'
|
||||
import { useForm, useModal } from '@/composables'
|
||||
import api from '../api'
|
||||
import pagePathes from 'isme:page-pathes'
|
||||
import icons from 'isme:icons'
|
||||
import pagePathes from 'isme:page-pathes'
|
||||
import api from '../api'
|
||||
import QuestionLabel from './QuestionLabel.vue'
|
||||
|
||||
const props = defineProps({
|
||||
menus: {
|
||||
@@ -162,10 +177,10 @@ const emit = defineEmits(['refresh'])
|
||||
const menuOptions = computed(() => {
|
||||
return [{ name: '根菜单', id: '', children: props.menus || [] }]
|
||||
})
|
||||
const componentOptions = pagePathes.map((path) => ({ label: path, value: path }))
|
||||
const iconOptions = icons.map((item) => ({
|
||||
const componentOptions = pagePathes.map(path => ({ label: path, value: path }))
|
||||
const iconOptions = icons.map(item => ({
|
||||
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,
|
||||
}))
|
||||
const layoutOptions = [
|
||||
@@ -181,15 +196,17 @@ const required = {
|
||||
trigger: ['blur', 'change'],
|
||||
}
|
||||
|
||||
const defaultForm = { enable: true, show: true }
|
||||
const [modalFormRef, modalForm, validation] = useForm(defaultForm)
|
||||
const defaultForm = { enable: true, show: true, layout: '' }
|
||||
const [modalFormRef, modalForm, validation] = useForm()
|
||||
const [modalRef, okLoading] = useModal()
|
||||
|
||||
const modalAction = ref('')
|
||||
const parentIdDisabled = ref(false)
|
||||
function handleOpen(options = {}) {
|
||||
const { action, row = {}, ...rest } = options
|
||||
modalAction.value = action
|
||||
modalForm.value = { ...row }
|
||||
modalForm.value = { ...defaultForm, ...row }
|
||||
parentIdDisabled.value = !!row.parentId && row.type === 'BUTTON'
|
||||
modalRef.value.open({ ...rest, onOk: onSave })
|
||||
}
|
||||
|
||||
@@ -197,16 +214,21 @@ async function onSave() {
|
||||
await validation()
|
||||
okLoading.value = true
|
||||
try {
|
||||
if (!modalForm.value.parentId) modalForm.value.parentId = null
|
||||
let newFormData
|
||||
if (!modalForm.value.parentId)
|
||||
modalForm.value.parentId = null
|
||||
if (modalAction.value === 'add') {
|
||||
await api.addPermission(modalForm.value)
|
||||
} else if (modalAction.value === 'edit') {
|
||||
const res = await api.addPermission(modalForm.value)
|
||||
newFormData = res.data
|
||||
}
|
||||
else if (modalAction.value === 'edit') {
|
||||
await api.savePermission(modalForm.value.id, modalForm.value)
|
||||
}
|
||||
okLoading.value = false
|
||||
$message.success('保存成功')
|
||||
emit('refresh', modalForm.value)
|
||||
} catch (error) {
|
||||
emit('refresh', modalAction.value === 'add' ? newFormData : modalForm.value)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
return false
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="flex">
|
||||
<n-spin size="small" :show="treeLoading">
|
||||
<MenuTree
|
||||
v-model:currentMenu="currentMenu"
|
||||
v-model:current-menu="currentMenu"
|
||||
class="w-320 shrink-0"
|
||||
:tree-data="treeData"
|
||||
@refresh="initData"
|
||||
@@ -21,20 +21,21 @@
|
||||
<div class="ml-40 w-0 flex-1">
|
||||
<template v-if="currentMenu">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="mb-12">{{ currentMenu.name }}</h3>
|
||||
<n-button
|
||||
:disabled="!currentMenu"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEdit(currentMenu)"
|
||||
>
|
||||
<h3 class="mb-12">
|
||||
{{ currentMenu.name }}
|
||||
</h3>
|
||||
<NButton size="small" type="primary" @click="handleEdit(currentMenu)">
|
||||
<i class="i-material-symbols:edit-outline mr-4 text-14" />
|
||||
编辑
|
||||
</n-button>
|
||||
</NButton>
|
||||
</div>
|
||||
<n-descriptions label-placement="left" bordered :column="2">
|
||||
<n-descriptions-item label="编码">{{ currentMenu.code }}</n-descriptions-item>
|
||||
<n-descriptions-item label="名称">{{ currentMenu.name }}</n-descriptions-item>
|
||||
<n-descriptions-item label="编码">
|
||||
{{ currentMenu.code }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="名称">
|
||||
{{ currentMenu.name }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="路由地址">
|
||||
{{ currentMenu.path ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
@@ -64,6 +65,25 @@
|
||||
{{ currentMenu.order ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="mt-32 flex justify-between">
|
||||
<h3 class="mb-12">
|
||||
按钮
|
||||
</h3>
|
||||
<NButton size="small" type="primary" @click="handleAddBtn">
|
||||
<i class="i-fe:plus mr-4 text-14" />
|
||||
新增
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<MeCrud
|
||||
ref="$table"
|
||||
:columns="btnsColumns"
|
||||
:scroll-x="-1"
|
||||
:get-data="api.getButtons"
|
||||
:computed-height="300"
|
||||
:query-items="{ parentId: currentMenu.id }"
|
||||
/>
|
||||
</template>
|
||||
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
|
||||
</div>
|
||||
@@ -73,31 +93,165 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MeCrud } from '@/components'
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import api from './api'
|
||||
import MenuTree from './components/MenuTree.vue'
|
||||
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
||||
import api from './api'
|
||||
|
||||
const treeData = ref([])
|
||||
const treeLoading = ref(false)
|
||||
const $table = ref(null)
|
||||
const currentMenu = ref(null)
|
||||
async function initData(data) {
|
||||
if (data?.type === 'BUTTON') {
|
||||
$table.value.handleSearch()
|
||||
return
|
||||
}
|
||||
treeLoading.value = true
|
||||
const res = await api.getMenuTree()
|
||||
treeData.value = res?.data || []
|
||||
treeLoading.value = false
|
||||
|
||||
if (data) currentMenu.value = data
|
||||
if (data)
|
||||
currentMenu.value = data
|
||||
}
|
||||
initData()
|
||||
|
||||
const currentMenu = ref(null)
|
||||
|
||||
const modalRef = ref(null)
|
||||
function handleEdit(item = {}) {
|
||||
modalRef.value?.handleOpen({
|
||||
action: 'edit',
|
||||
title: '编辑菜单 - ' + item.name,
|
||||
title: `编辑菜单 - ${item.name}`,
|
||||
row: item,
|
||||
okText: '保存',
|
||||
})
|
||||
}
|
||||
|
||||
const btnsColumns = [
|
||||
{ title: '名称', key: 'name' },
|
||||
{ title: '编码', key: 'code' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
render: row =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row.enable,
|
||||
loading: !!row.enableLoading,
|
||||
onUpdateValue: () => handleEnable(row),
|
||||
},
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 320,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render(row) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: () => handleEditBtn(row),
|
||||
},
|
||||
{
|
||||
default: () => '编辑',
|
||||
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
||||
},
|
||||
),
|
||||
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 12px;',
|
||||
onClick: () => handleDeleteBtn(row.id),
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
},
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
watch(
|
||||
() => currentMenu.value,
|
||||
async (v) => {
|
||||
await nextTick()
|
||||
if (v)
|
||||
$table.value.handleSearch()
|
||||
},
|
||||
)
|
||||
|
||||
function handleAddBtn() {
|
||||
modalRef.value?.handleOpen({
|
||||
action: 'add',
|
||||
title: '新增按钮',
|
||||
row: { type: 'BUTTON', parentId: currentMenu.value.id },
|
||||
okText: '保存',
|
||||
})
|
||||
}
|
||||
|
||||
function handleEditBtn(row) {
|
||||
modalRef.value?.handleOpen({
|
||||
action: 'edit',
|
||||
title: `编辑按钮 - ${row.name}`,
|
||||
row,
|
||||
okText: '保存',
|
||||
})
|
||||
}
|
||||
|
||||
function handleDeleteBtn(id) {
|
||||
const d = $dialog.warning({
|
||||
content: '确定删除?',
|
||||
title: '提示',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
async onPositiveClick() {
|
||||
try {
|
||||
d.loading = true
|
||||
await api.deletePermission(id)
|
||||
$message.success('删除成功')
|
||||
$table.value.handleSearch()
|
||||
d.loading = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
d.loading = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleEnable(item) {
|
||||
try {
|
||||
item.enableLoading = true
|
||||
await api.savePermission(item.id, {
|
||||
enable: !item.enable,
|
||||
})
|
||||
$message.success('操作成功')
|
||||
$table.value?.handleSearch()
|
||||
item.enableLoading = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
item.enableLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
create: (data) => request.post('/role', data),
|
||||
create: data => request.post('/role', data),
|
||||
read: (params = {}) => request.get('/role/page', { params }),
|
||||
update: (data) => request.patch(`/role/${data.id}`, data),
|
||||
delete: (id) => request.delete(`/role/${id}`),
|
||||
update: data => request.patch(`/role/${data.id}`, data),
|
||||
delete: id => request.delete(`/role/${id}`),
|
||||
|
||||
getAllPermissionTree: () => request.get('/permission/tree'),
|
||||
getAllUsers: (params = {}) => request.get('/user', { params }),
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:32
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<n-tree
|
||||
:key-field="keyField"
|
||||
:label-field="labelField"
|
||||
:selectable="false"
|
||||
default-expand-all
|
||||
checkable
|
||||
check-on-click
|
||||
cascade
|
||||
:data="treeData"
|
||||
:checked-keys="checkedKeys"
|
||||
:on-update:checked-keys="(keys) => (checkedKeys = keys)"
|
||||
:on-update:indeterminate-keys="(keys) => (halfCheckedKeys = keys)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'label',
|
||||
},
|
||||
keyField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:value'])
|
||||
|
||||
const halfCheckedKeys = ref([])
|
||||
const checkedKeys = ref([])
|
||||
watch([halfCheckedKeys, checkedKeys], ([v1, v2]) => {
|
||||
emit('update:value', Array.from(new Set([...v1, ...v2])))
|
||||
})
|
||||
onMounted(() => {
|
||||
halfCheckedKeys.value = getHalfCheckedValues(props.value, props.treeData)
|
||||
checkedKeys.value = props.value.filter((item) => !halfCheckedKeys.value.includes(item))
|
||||
})
|
||||
|
||||
// 获取半选状态的值
|
||||
function getHalfCheckedValues(selectedValues, treeData, halfCheckedValues = []) {
|
||||
function isHalfChecked(node) {
|
||||
// 如果存在子节点没有选中或者子节点是半选状态
|
||||
return node.children.some(
|
||||
(item) =>
|
||||
!selectedValues.includes(item[props.keyField]) ||
|
||||
halfCheckedValues.includes(item[props.keyField])
|
||||
)
|
||||
}
|
||||
|
||||
function hasGrandson(node) {
|
||||
return node.children.some((item) => !!item.children?.length)
|
||||
}
|
||||
|
||||
for (const item of treeData) {
|
||||
if (!item.children?.length) continue
|
||||
if (hasGrandson(item)) {
|
||||
// 根据孙节点判断子节点是否是半选
|
||||
getHalfCheckedValues(selectedValues, item.children, halfCheckedValues)
|
||||
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
|
||||
} else {
|
||||
isHalfChecked(item) && halfCheckedValues.push(item[props.keyField])
|
||||
}
|
||||
}
|
||||
|
||||
return halfCheckedValues
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/05 21:29:38
|
||||
- @LastEditTime: 2024/04/01 15:52:40
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
@@ -9,10 +9,10 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<template #action>
|
||||
<n-button type="primary" @click="handleAdd()">
|
||||
<NButton type="primary" @click="handleAdd()">
|
||||
<i class="i-material-symbols:add mr-4 text-18" />
|
||||
新增角色
|
||||
</n-button>
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<MeCrud
|
||||
@@ -67,19 +67,27 @@
|
||||
<n-input v-model:value="modalForm.code" :disabled="modalAction !== 'add'" />
|
||||
</n-form-item>
|
||||
<n-form-item label="权限" path="permissionIds">
|
||||
<CascadeTree
|
||||
v-model:value="modalForm.permissionIds"
|
||||
:tree-data="permissionTree"
|
||||
label-field="name"
|
||||
<n-tree
|
||||
key-field="id"
|
||||
label-field="name"
|
||||
:selectable="false"
|
||||
:data="permissionTree"
|
||||
:checked-keys="modalForm.permissionIds"
|
||||
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
|
||||
|
||||
default-expand-all checkable check-on-click
|
||||
class="cus-scroll max-h-200 w-full"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="状态" path="enable">
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>停用</template>
|
||||
</n-switch>
|
||||
<NSwitch v-model:value="modalForm.enable">
|
||||
<template #checked>
|
||||
启用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
停用
|
||||
</template>
|
||||
</NSwitch>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</MeModal>
|
||||
@@ -87,11 +95,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
||||
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||
import { useCrud } from '@/composables'
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import api from './api'
|
||||
import CascadeTree from './components/CascadeTree.vue'
|
||||
|
||||
defineOptions({ name: 'RoleMgt' })
|
||||
|
||||
@@ -105,13 +112,23 @@ onMounted(() => {
|
||||
$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 = [
|
||||
{ title: '角色名', key: 'name' },
|
||||
{ title: '角色编码', key: 'code' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
render: (row) =>
|
||||
render: row =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
@@ -125,7 +142,7 @@ const columns = [
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -148,7 +165,7 @@ const columns = [
|
||||
{
|
||||
default: () => '分配用户',
|
||||
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
@@ -162,7 +179,7 @@ const columns = [
|
||||
{
|
||||
default: () => '编辑',
|
||||
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
h(
|
||||
@@ -177,7 +194,7 @@ const columns = [
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
},
|
||||
@@ -191,21 +208,13 @@ async function handleEnable(row) {
|
||||
row.enableLoading = false
|
||||
$message.success('操作成功')
|
||||
$table.value?.handleSearch()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
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([])
|
||||
api.getAllPermissionTree().then(({ data = [] }) => (permissionTree.value = data))
|
||||
</script>
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
<template>
|
||||
<CommonPage back>
|
||||
<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 #action>
|
||||
<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" />
|
||||
批量取消授权
|
||||
</n-button>
|
||||
<n-button
|
||||
</NButton>
|
||||
<NButton
|
||||
class="ml-12"
|
||||
:disabled="!userIds.length"
|
||||
type="primary"
|
||||
@@ -25,7 +27,7 @@
|
||||
>
|
||||
<i v-if="userIds.length" class="i-line-md:confirm-circle mr-4 text-18" />
|
||||
批量授权
|
||||
</n-button>
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,6 +37,7 @@
|
||||
:scroll-x="1200"
|
||||
:columns="columns"
|
||||
:get-data="api.getAllUsers"
|
||||
:show-check="true"
|
||||
@on-checked="onChecked"
|
||||
>
|
||||
<MeQueryItem label="用户名" :label-width="50">
|
||||
@@ -65,11 +68,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import { MeCrud, MeQueryItem } from '@/components'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import api from './api'
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import { h } from 'vue'
|
||||
import api from './api'
|
||||
|
||||
defineOptions({ name: 'RoleUser' })
|
||||
const route = useRoute()
|
||||
@@ -88,7 +91,7 @@ const genders = [
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
// { type: 'selection', fixed: 'left' },
|
||||
{
|
||||
title: '头像',
|
||||
key: 'avatar',
|
||||
@@ -99,11 +102,10 @@ const columns = [
|
||||
src: avatar,
|
||||
}),
|
||||
},
|
||||
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '用户名', key: 'username', ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '角色',
|
||||
key: 'roles',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: ({ roles }) => {
|
||||
if (roles?.length) {
|
||||
@@ -111,8 +113,8 @@ const columns = [
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||
{ default: () => item.name }
|
||||
)
|
||||
{ default: () => item.name },
|
||||
),
|
||||
)
|
||||
}
|
||||
return '暂无角色'
|
||||
@@ -122,14 +124,14 @@ const columns = [
|
||||
title: '性别',
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
render: ({ gender }) => genders.find((item) => gender === item.value)?.label ?? '',
|
||||
render: ({ gender }) => genders.find(item => gender === item.value)?.label ?? '',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createDate',
|
||||
width: 180,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createTime']))
|
||||
return h('span', formatDateTime(row.createTime))
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -137,7 +139,7 @@ const columns = [
|
||||
key: 'enable',
|
||||
width: 100,
|
||||
|
||||
render: (row) =>
|
||||
render: row =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
@@ -148,44 +150,44 @@ const columns = [
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
width: 120,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row) {
|
||||
return row.roles?.some((item) => item.id === +route.params.roleId)
|
||||
return row.roles?.some(item => item.id === +route.params.roleId)
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchRemove([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '取消授权',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
)
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchRemove([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '取消授权',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
},
|
||||
)
|
||||
: h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchAdd([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '授权',
|
||||
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
|
||||
}
|
||||
)
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleBatchAdd([row.id]),
|
||||
},
|
||||
{
|
||||
default: () => '授权',
|
||||
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -197,8 +199,10 @@ function onChecked(rowKeys) {
|
||||
|
||||
function handleBatchAdd(ids = userIds.value) {
|
||||
const roleId = route.params.roleId
|
||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length) return $message.error('请先选择用户')
|
||||
if (!roleId)
|
||||
return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length)
|
||||
return $message.error('请先选择用户')
|
||||
$dialog.confirm({
|
||||
content: `确认分配【${route.query.roleName}】?`,
|
||||
async confirm() {
|
||||
@@ -209,8 +213,10 @@ function handleBatchAdd(ids = userIds.value) {
|
||||
}
|
||||
function handleBatchRemove(ids = userIds.value) {
|
||||
const roleId = route.params.roleId
|
||||
if (!roleId) return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length) return $message.error('请先选择用户')
|
||||
if (!roleId)
|
||||
return $message.error('角色异常,请重新选择角色')
|
||||
if (!ids.length)
|
||||
return $message.error('请先选择用户')
|
||||
$dialog.confirm({
|
||||
content: `确认取消分配【${route.query.roleName}】?`,
|
||||
async confirm() {
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
create: (data) => request.post('/user', data),
|
||||
create: data => request.post('/user', data),
|
||||
read: (params = {}) => request.get('/user', { params }),
|
||||
update: (data) => request.patch(`/user/${data.id}`, data),
|
||||
delete: (id) => request.delete(`/user/${id}`),
|
||||
update: data => request.patch(`/user/${data.id}`, data),
|
||||
delete: id => request.delete(`/user/${id}`),
|
||||
resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
|
||||
|
||||
getAllRoles: () => request.get('/role?enable=1'),
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<template>
|
||||
<CommonPage>
|
||||
<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" />
|
||||
创建新用户
|
||||
</n-button>
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<MeCrud
|
||||
@@ -92,10 +92,14 @@
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="modalAction === 'add'" label="状态" path="enable">
|
||||
<n-switch v-model:value="modalForm.enable">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>停用</template>
|
||||
</n-switch>
|
||||
<NSwitch v-model:value="modalForm.enable">
|
||||
<template #checked>
|
||||
启用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
停用
|
||||
</template>
|
||||
</NSwitch>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-alert v-if="modalAction === 'add'" type="warning" closable>
|
||||
@@ -106,10 +110,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
||||
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||
import { useCrud } from '@/composables'
|
||||
import { formatDateTime } from '@/utils'
|
||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||
import api from './api'
|
||||
|
||||
defineOptions({ name: 'UserMgt' })
|
||||
@@ -129,6 +133,24 @@ const genders = [
|
||||
const roles = ref([])
|
||||
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 = [
|
||||
{
|
||||
title: '头像',
|
||||
@@ -140,11 +162,10 @@ const columns = [
|
||||
src: avatar,
|
||||
}),
|
||||
},
|
||||
{ title: '用户名', key: 'username', width: 150, ellipsis: { tooltip: true } },
|
||||
{ title: '用户名', key: 'username', ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '角色',
|
||||
key: 'roles',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: ({ roles }) => {
|
||||
if (roles?.length) {
|
||||
@@ -152,8 +173,8 @@ const columns = [
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
|
||||
{ default: () => item.name }
|
||||
)
|
||||
{ default: () => item.name },
|
||||
),
|
||||
)
|
||||
}
|
||||
return '暂无角色'
|
||||
@@ -163,7 +184,7 @@ const columns = [
|
||||
title: '性别',
|
||||
key: 'gender',
|
||||
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 } },
|
||||
{
|
||||
@@ -171,14 +192,14 @@ const columns = [
|
||||
key: 'createDate',
|
||||
width: 180,
|
||||
render(row) {
|
||||
return h('span', formatDateTime(row['createTime']))
|
||||
return h('span', formatDateTime(row.createTime))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'enable',
|
||||
width: 120,
|
||||
render: (row) =>
|
||||
render: row =>
|
||||
h(
|
||||
NSwitch,
|
||||
{
|
||||
@@ -191,7 +212,7 @@ const columns = [
|
||||
{
|
||||
checked: () => '启用',
|
||||
unchecked: () => '停用',
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -214,7 +235,7 @@ const columns = [
|
||||
{
|
||||
default: () => '分配角色',
|
||||
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
@@ -227,7 +248,7 @@ const columns = [
|
||||
{
|
||||
default: () => '重置密码',
|
||||
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
h(
|
||||
@@ -241,7 +262,7 @@ const columns = [
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
},
|
||||
@@ -255,13 +276,15 @@ async function handleEnable(row) {
|
||||
row.enableLoading = false
|
||||
$message.success('操作成功')
|
||||
$table.value?.handleSearch()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
row.enableLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenRolesSet(row) {
|
||||
const roleIds = row.roles.map((item) => item.id)
|
||||
const roleIds = row.roles.map(item => item.id)
|
||||
handleOpen({
|
||||
action: 'setRole',
|
||||
title: '分配角色',
|
||||
@@ -270,31 +293,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() {
|
||||
if (modalAction.value === 'setRole') {
|
||||
return handleSave({
|
||||
api: () => api.update(modalForm.value),
|
||||
cb: () => $message.success('分配成功'),
|
||||
})
|
||||
} else if (modalAction.value === 'reset') {
|
||||
}
|
||||
else if (modalAction.value === 'reset') {
|
||||
return handleSave({
|
||||
api: () => api.resetPwd(modalForm.value.id, modalForm.value),
|
||||
cb: () => $message.success('密码重置成功'),
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
changePassword: (data) => request.post('/auth/password', data),
|
||||
updateProfile: (data) => request.patch(`/user/profile/${data.id}`, data),
|
||||
changePassword: data => request.post('/auth/password', data),
|
||||
updateProfile: data => request.patch(`/user/profile/${data.id}`, data),
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user