mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-28 04:00:22 +08:00
Compare commits
71 Commits
v2.0.0
...
7ed9a3540d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a63e72bc2f | ||
|
|
04723ffbfa | ||
|
|
fd9480e92f | ||
|
|
0520cd015a | ||
|
|
f5a26c32e9 | ||
|
|
886ef9e11c | ||
|
|
7d0a17b2b5 | ||
|
|
fa4967efc3 | ||
|
|
207150623e | ||
|
|
6981692c54 | ||
|
|
b64e1c7595 | ||
|
|
efcbe3bea4 | ||
|
|
2c808c6d8b | ||
|
|
447db11c52 | ||
|
|
a3d0e863cc | ||
|
|
a2827e4c0d | ||
|
|
f888c2fbfd | ||
|
|
1c37a38b92 | ||
|
|
f2eb40357d | ||
|
|
58de3c1ad6 | ||
|
|
567e306a5c | ||
|
|
065c6b50c6 | ||
|
|
95a1ef654c | ||
|
|
346da8772a | ||
|
|
a6773cbfec | ||
|
|
4c337d3aa8 | ||
|
|
2c9604a829 | ||
|
|
c4fd0459ab | ||
|
|
aa92455dbb | ||
|
|
857381471e | ||
|
|
b780113f18 | ||
|
|
fe4bbded53 | ||
|
|
d801cf28cc | ||
|
|
334174d442 |
@@ -1,5 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
@@ -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'
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,2 +1,2 @@
|
||||
registry=https://registry.npm.taobao.org
|
||||
registry=https://registry.npmmirror.com
|
||||
shamefully-hoist=true
|
||||
@@ -2,6 +2,6 @@
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"endOfLine": "lf",
|
||||
"endOfLine": "auto",
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"antfu.unocss",
|
||||
"antfu.iconify",
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -18,5 +18,8 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"]
|
||||
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": "package.json"
|
||||
}
|
||||
}
|
||||
|
||||
23
README.md
23
README.md
@@ -12,7 +12,7 @@ Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解
|
||||
|
||||
## 设计理念
|
||||
|
||||
Vue Naive Admin 2022年2月开始开源,从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript(前端),这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱,截至 2023-11-17, github `1.1k+` star,gitee `260+` star。
|
||||
Vue Naive Admin 2022年2月开始开源,从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript(前端),这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱。
|
||||
|
||||
## 特性
|
||||
|
||||
@@ -48,11 +48,21 @@ 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://docs.isme.top/web/#/624306705/188522224)
|
||||
- 接口文档: [apidoc | isme-nest-serve](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a)
|
||||
|
||||
> 注:有个比较常见的问题,就是如何添加菜单和修改菜单,由于项目是由后端控制菜单资源的,所以需要对接后端后在资源管理功能对菜单进行增删改,然后在角色管理功能给对应角色进行授权。具体如何对接后端,请参考 [项目文档](https://docs.isme.top/web/#/624306705/188522224)。当然,可能有些菜单你不想通过权限控制,那么你可以在 `/src/settings.js` 文件添加 basePermissions,只需对齐菜单资源的结构即可,结构可以参照 [接口文档](https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a/api-134536978)。
|
||||
|
||||
## 使用这个模板创建 Github 仓库
|
||||
|
||||
[使用这个模板创建仓库](https://github.com/zclzone/vue-naive-admin/generate).
|
||||
|
||||
## 版权说明
|
||||
|
||||
本项目使用 `MIT协议`,默认授权给任何人,被授权人可免费地无限制的使用、复制、修改、合并、发布、发行、再许可、售卖本软件拷贝、并有权向被供应人授予同等的权利,但必须满足以下条件:
|
||||
@@ -64,4 +74,11 @@ 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。
|
||||
|
||||
## 入群交流
|
||||
|
||||
添加微信,拉你进群
|
||||
|
||||

|
||||
|
||||
|
||||
92
index.html
92
index.html
@@ -1,28 +1,94 @@
|
||||
<!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">
|
||||
<img src="/resource/logo.png" alt="logo" height="128" />
|
||||
<div class="loading-spin__container">
|
||||
<div class="loading-spin">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
<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>
|
||||
|
||||
62
package.json
62
package.json
@@ -10,37 +10,38 @@
|
||||
"lint:fix": "eslint --fix --ext .js,.vue ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/json": "^2.2.129",
|
||||
"@iconify/utils": "^2.1.11",
|
||||
"@unocss/eslint-config": "^0.58.0",
|
||||
"@unocss/preset-rem-to-px": "^0.58.0",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"axios": "^1.5.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"@arco-design/color": "^0.4.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"naive-ui": "^2.35.0",
|
||||
"naive-ui": "^2.38.2",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"sass": "^1.69.3",
|
||||
"unocss": "^0.58.0",
|
||||
"vue": "^3.3.11",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "^3.4.27",
|
||||
"vue-echarts": "^6.7.2",
|
||||
"vue-router": "^4.3.2",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@iconify/json": "^2.2.210",
|
||||
"@iconify/utils": "^2.1.23",
|
||||
"@unocss/eslint-config": "^0.58.9",
|
||||
"@unocss/preset-rem-to-px": "^0.59.4",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@zclzone/eslint-config": "^0.0.5",
|
||||
"esno": "^0.17.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"glob": "^10.3.10",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^5.0.7",
|
||||
"vite-plugin-simple-html": "^0.1.1",
|
||||
"vite-plugin-vue-devtools": "1.0.0-rc.7"
|
||||
"esno": "^4.7.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^10.3.15",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.1",
|
||||
"unocss": "^0.59.4",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-router-warn": "^1.0.0",
|
||||
"vite-plugin-vue-devtools": "^7.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -48,13 +49,10 @@
|
||||
"@unocss",
|
||||
".eslint-global-variables.json"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6649
pnpm-lock.yaml
generated
6649
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()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
15
src/App.vue
15
src/App.vue
@@ -29,8 +29,6 @@
|
||||
<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'
|
||||
|
||||
const layouts = new Map()
|
||||
@@ -50,17 +48,12 @@ const Layout = computed(() => {
|
||||
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)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
|
||||
})
|
||||
</script>
|
||||
|
||||
16
src/assets/icons/isme/apifox.svg
Normal file
16
src/assets/icons/isme/apifox.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
t="1710490574771"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="4141"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M1022.934187 429.44a446.122667 446.122667 0 0 0-19.456-100.437333 175.786667 175.786667 0 0 1-15.530667 31.701333 401.92 401.92 0 0 0-164.224-267.264A399.701333 399.701333 0 0 0 724.566187 41.770667a313.6 313.6 0 0 0-14.421334-4.778667 300.458667 300.458667 0 0 1 60.8 148.096c0.256 1.877333 0.469333 3.754667 0.64 5.674667a2.56 2.56 0 0 1 0 0.426666c0.213333 1.92 0.426667 3.882667 0.554667 5.845334v1.024a223.957333 223.957333 0 0 1 0.426667 5.845333 246.357333 246.357333 0 0 1 0.426666 10.026667 8.277333 8.277333 0 0 0 0 0.682666v6.997334a300.672 300.672 0 0 1-54.101333 172.458666 270.293333 270.293333 0 0 1 3.2 93.653334 263.552 263.552 0 0 0 49.664-17.109334 263.850667 263.850667 0 0 0 34.133333-18.773333 268.074667 268.074667 0 0 1 10.368 74.410667 269.525333 269.525333 0 0 1-2.986666 40.362666 266.197333 266.197333 0 0 0 68.906666-16.64c11.605333-4.522667 22.869333-9.813333 33.706667-15.914666a266.410667 266.410667 0 0 1 4.266667 47.701333 269.141333 269.141333 0 0 1-5.76 55.381333 266.752 266.752 0 0 0 81.322666-16.213333A446.165333 446.165333 0 0 0 1024.000853 464.213333a366.933333 366.933333 0 0 0-1.024-34.816z m-119.808 247.210667a242.56 242.56 0 0 1-16.64 0.512h-5.504a266.666667 266.666667 0 0 0 8.448-66.858667 269.653333 269.653333 0 0 0-1.706667-30.08 265.813333 265.813333 0 0 1-82.944 25.344 257.152 257.152 0 0 1-24.448 2.133333 264.618667 264.618667 0 0 0 4.906667-39.594666 276.352 276.352 0 0 0 0.341333-13.312 268.373333 268.373333 0 0 0-6.058667-56.832 265.984 265.984 0 0 1-90.453333 33.109333l-0.213333-0.298667a263.594667 263.594667 0 0 0 4.736-37.802666 239.658667 239.658667 0 0 0 0.426666-14.506667c0-16.128-1.450667-32.256-4.352-48.128a301.098667 301.098667 0 0 1-218.325333 93.354667 300.501333 300.501333 0 0 1-138.538667-33.621334 267.221333 267.221333 0 0 0 55.210667-29.226666 268.8 268.8 0 0 1-152.448-242.346667 268.629333 268.629333 0 0 1 4.352-48.426667 264.362667 264.362667 0 0 1 12.885333-46.421333 264.832 264.832 0 0 1 20.650667-42.752A267.221333 267.221333 0 0 1 305.664853 37.546667 521.045333 521.045333 0 0 0 191.14752 108.8 520.832 520.832 0 0 0 13.99552 391.722667 520.362667 520.362667 0 0 0 0.000853 512.042667a518.528 518.528 0 0 0 63.573334 249.642666 266.709333 266.709333 0 0 1 18.986666-73.088 497.450667 497.450667 0 0 0 366.293334 298.410667 266.069333 266.069333 0 0 1-34.304-59.050667 469.674667 469.674667 0 0 0 133.376 22.784c4.010667 0 8.021333 0.128 12.074666 0.128a468.650667 468.650667 0 0 0 207.658667-48.170666 263.509333 263.509333 0 0 1-30.890667-20.394667 448.298667 448.298667 0 0 0 184.32-132.010667 446.677333 446.677333 0 0 0 57.856-89.301333 267.008 267.008 0 0 1-75.818666 15.616z"
|
||||
fill="#F44A53"
|
||||
p-id="4142"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
15
src/assets/icons/isme/docs.svg
Normal file
15
src/assets/icons/isme/docs.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
t="1710490291582"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3145"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M476.811454 234.638085S575.875862 38.761663 627.148553 9.152912A275.775 275.775 0 0 1 707.702374 0.204763c22.66728 0.839529 111.186386 4.218121 152.753304 63.804195a84.403367 84.403367 0 0 1 4.914315 7.719571 189.938291 189.938291 0 0 1-16.196764 145.996121 204.517427 204.517427 0 0 1-131.048411 98.900597l-179.822992 34.154493a62.616569 62.616569 0 0 0 51.190785 47.83267l163.953849-2.313824a443.353156 443.353156 0 0 1-66.302306 69.373753c-65.524206 55.081285-131.048411 85.242896-177.77536 91.140075-34.400208 2.3343-68.00184-1.474295-102.012998-2.190966 0 0 17.077246 96.812014 172.00104 79.734768a152.978544 152.978544 0 0 1-17.363915 47.382191 166.861485 166.861485 0 0 1-111.432102 79.243336c-52.624128 4.607171-103.917295 2.047631-155.88618 3.173829 0 0-52.378412 189.037333-84.260034 259.639665H176.956308s14.988662-92.143414 47.976004-262.956828A311.731408 311.731408 0 0 1 170.567698 423.634466s28.032074 159.838109 124.905517 181.481573c-7.350997-129.881261-43.55312-267.420664-15.930573-385.568997a382.907077 382.907077 0 0 1 50.105541-76.294747 400.844328 400.844328 0 0 1 166.533864-115.875463s-87.679578 63.763243-66.05659 214.100342z"
|
||||
p-id="3146"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
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 |
@@ -1,21 +0,0 @@
|
||||
<svg
|
||||
t="1702480351321"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="11122"
|
||||
width="200"
|
||||
height="200"
|
||||
>
|
||||
<path
|
||||
d="M509.9008 519.8336m-450.816 0a450.816 450.816 0 1 0 901.632 0 450.816 450.816 0 1 0-901.632 0Z"
|
||||
fill="#C65EDB"
|
||||
p-id="11123"
|
||||
></path>
|
||||
<path
|
||||
d="M798.1568 512.512l-113.3056-78.3872a47.4112 47.4112 0 0 1-20.4288-39.0656l0.3584-137.7792c0.1024-39.2704-44.9024-61.5936-76.0832-37.7856l-109.568 83.5072a47.2832 47.2832 0 0 1-43.4688 7.3216l-130.9184-42.9056c-37.3248-12.2368-72.448 23.6544-59.4432 60.7232l45.568 129.9968A47.3088 47.3088 0 0 1 284.416 501.76l-81.2544 111.2576c-23.1424 31.6928 0.1024 76.2368 39.3728 75.3152l137.728-3.1744a47.3344 47.3344 0 0 1 39.4752 19.6096l80.6912 111.6672c22.9888 31.8464 72.4992 23.4496 83.7632-14.1824l37.9392-126.6176 126.5664 118.272a27.648 27.648 0 0 0 17.7664 7.4752c7.8848 0.3584 15.872-2.6112 21.7088-8.8064a27.91936 27.91936 0 0 0-1.3312-39.4752l-124.8768-116.6848 123.8016-39.8848c37.376-11.9808 44.6976-61.6448 12.3904-84.0192z m-389.6832-6.5024l-42.8032 77.824a27.86816 27.86816 0 0 1-37.9392 11.008 27.93984 27.93984 0 0 1-11.008-37.9392l40.0384-72.7552-19.0464-63.6928c-4.4032-14.7968 3.9936-30.3616 18.7392-34.7648 14.7456-4.4032 30.3616 3.9936 34.7648 18.7392l20.6848 69.2224c3.2256 10.752 1.9968 22.528-3.4304 32.3584z"
|
||||
fill="#FFFFFF"
|
||||
p-id="11124"
|
||||
></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/isme.png
Normal file
BIN
src/assets/images/isme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -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">
|
||||
|
||||
12
src/components/common/TheLogo.vue
Normal file
12
src/components/common/TheLogo.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2024/03/04 16:09:47
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
<template>
|
||||
<div class="h-40 w-40 rounded-4 bg-primary p-1/100">
|
||||
<img src="@/assets/images/isme.png" alt="Logo" />
|
||||
</div>
|
||||
</template>
|
||||
22
src/components/common/ThemeSetting.vue
Normal file
22
src/components/common/ThemeSetting.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<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 { getPresetColors } from '@arco-design/color'
|
||||
import { useAppStore } from '@/store'
|
||||
const appStore = useAppStore()
|
||||
|
||||
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
|
||||
</script>
|
||||
@@ -7,25 +7,22 @@
|
||||
--------------------------------->
|
||||
|
||||
<template>
|
||||
<AppCard
|
||||
v-if="$slots.default"
|
||||
bordered
|
||||
bg="#fafafc dark:black"
|
||||
class="mb-30 min-h-60 flex justify-between rounded-4 p-16"
|
||||
>
|
||||
<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 class="ml-20" type="primary" @click="handleSearch">
|
||||
<i class="i-fe:search mr-4" />
|
||||
搜索
|
||||
</n-button>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<n-data-table
|
||||
@@ -54,7 +51,7 @@ const props = defineProps({
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @remote 是否分页
|
||||
* @isPagination 是否分页
|
||||
*/
|
||||
isPagination: {
|
||||
type: Boolean,
|
||||
@@ -129,7 +126,7 @@ function handleSearch() {
|
||||
async function handleReset() {
|
||||
const queryItems = { ...props.queryItems }
|
||||
for (const key in queryItems) {
|
||||
queryItems[key] = ''
|
||||
queryItems[key] = null
|
||||
}
|
||||
emit('update:queryItems', { ...queryItems, ...initQuery })
|
||||
await nextTick()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/16 18:50:02
|
||||
- @LastEditTime: 2024/01/13 17:41:38
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
@@ -9,17 +9,17 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
class="modal-box"
|
||||
:style="{ width: modalOptions.width, ...modalOptions.modalStyle }"
|
||||
:preset="undefined"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
@after-leave="onAfterLeave"
|
||||
>
|
||||
<n-card
|
||||
:title="modalOptions.title"
|
||||
:style="modalOptions.contentStyle"
|
||||
:closable="modalOptions.closable"
|
||||
@close="close()"
|
||||
>
|
||||
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
|
||||
<template #header>
|
||||
<header class="modal-header">{{ modalOptions.title }}</header>
|
||||
</template>
|
||||
<slot></slot>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -45,6 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { initDrag } from './utils'
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
@@ -101,12 +102,17 @@ const show = ref(false)
|
||||
const modalOptions = ref({})
|
||||
|
||||
// 打开模态框
|
||||
function open(options = {}) {
|
||||
async function open(options = {}) {
|
||||
// 将props和options合并赋值给modalOptions
|
||||
modalOptions.value = { ...props, ...options }
|
||||
|
||||
// 将show的值设置为true
|
||||
show.value = true
|
||||
await nextTick()
|
||||
initDrag(
|
||||
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
||||
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
|
||||
)
|
||||
}
|
||||
|
||||
// 定义一个close函数,用于关闭模态框
|
||||
@@ -149,6 +155,14 @@ async function handleCancel(data) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onAfterLeave() {
|
||||
await nextTick()
|
||||
initDrag(
|
||||
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
|
||||
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1)
|
||||
)
|
||||
}
|
||||
|
||||
const okLoading = computed({
|
||||
get() {
|
||||
return !!modalOptions.value?.okLoading
|
||||
|
||||
70
src/components/me/modal/utils.js
Normal file
70
src/components/me/modal/utils.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2024/01/13 17:41:26
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
// 获取元素的CSS样式
|
||||
function getCss(element, key) {
|
||||
return element.currentStyle
|
||||
? element.currentStyle[key]
|
||||
: window.getComputedStyle(element, null)[key]
|
||||
}
|
||||
|
||||
// 初始化拖拽
|
||||
export function initDrag(bar, box) {
|
||||
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')
|
||||
}
|
||||
if (getCss(box, 'top') !== 'auto') {
|
||||
params.top = getCss(box, 'top')
|
||||
}
|
||||
|
||||
// 设置触发拖动元素的鼠标样式为移动图标
|
||||
bar.style.cursor = 'move'
|
||||
// 鼠标按下事件处理函数
|
||||
bar.onmousedown = function (e) {
|
||||
params.flag = true // 设置拖拽标志为true
|
||||
e.preventDefault() // 阻止默认事件
|
||||
params.currentX = e.clientX // 鼠标当前位置的X坐标
|
||||
params.currentY = e.clientY // 鼠标当前位置的Y坐标
|
||||
}
|
||||
document.onmouseup = function () {
|
||||
params.flag = false // 设置拖拽标志为false
|
||||
if (getCss(box, 'left') !== 'auto') {
|
||||
params.left = getCss(box, 'left')
|
||||
}
|
||||
if (getCss(box, 'top') !== 'auto') {
|
||||
params.top = getCss(box, 'top')
|
||||
}
|
||||
}
|
||||
document.onmousemove = function (e) {
|
||||
if (e.target !== bar && !params.flag) return
|
||||
|
||||
e.preventDefault() // 阻止默认事件
|
||||
// 如果拖拽标志为true
|
||||
if (params.flag) {
|
||||
const nowX = e.clientX // 鼠标当前位置的X坐标
|
||||
const nowY = e.clientY // 鼠标当前位置的Y坐标
|
||||
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值
|
||||
|
||||
box.style.left = left + 'px'
|
||||
box.style.top = top + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,8 @@ function open(options) {
|
||||
async function setCurrentRole() {
|
||||
try {
|
||||
okLoading.value = true
|
||||
await userStore.switchCurrentRole(roleCode.value)
|
||||
const { data } = await api.switchCurrentRole(roleCode.value)
|
||||
await authStore.switchCurrentRole(data)
|
||||
okLoading.value = false
|
||||
$message.success('切换成功')
|
||||
modalRef.value?.handleOk()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<template>
|
||||
<router-link class="h-60 f-c-c" to="/">
|
||||
<img src="@/assets/images/logo.png" class="h-40" />
|
||||
<TheLogo />
|
||||
<h2
|
||||
v-show="!appStore.collapsed"
|
||||
class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold"
|
||||
|
||||
@@ -39,8 +39,19 @@ watch(route, async () => {
|
||||
})
|
||||
|
||||
function handleMenuSelect(key, item) {
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
if (isExternal(item.originPath)) {
|
||||
$dialog.confirm({
|
||||
type: 'info',
|
||||
title: `请选择打开方式`,
|
||||
positiveText: '外链打开',
|
||||
negativeText: '在本站内嵌打开',
|
||||
confirm() {
|
||||
window.open(item.originPath)
|
||||
},
|
||||
cancel: () => {
|
||||
router.push(item.path)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
router.push(item.path)
|
||||
}
|
||||
@@ -55,7 +66,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<script setup>
|
||||
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
|
||||
import { RoleSelect } from '@/layouts/components'
|
||||
import { initUserAndPermissions } from '@/router'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -60,9 +59,7 @@ function handleSelect(key) {
|
||||
case 'toggleRole':
|
||||
roleSelectRef.value?.open({
|
||||
onOk() {
|
||||
initUserAndPermissions().then(() => {
|
||||
router.replace('/')
|
||||
})
|
||||
location.reload()
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
@@ -38,10 +37,9 @@
|
||||
|
||||
<script setup>
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTabStore, useAppStore } from '@/store'
|
||||
import { useTabStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const tabStore = useTabStore()
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
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>
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
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>
|
||||
@@ -56,4 +59,8 @@ const { isFullscreen, toggle } = useFullscreen()
|
||||
function handleLinkClick(link) {
|
||||
window.open(link)
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -22,10 +22,10 @@ import { setupDirectives } from './directives'
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
setupStore(app)
|
||||
setupNaiveDiscreteApi()
|
||||
setupDirectives(app)
|
||||
await setupRouter(app)
|
||||
app.mount('#app')
|
||||
setupNaiveDiscreteApi()
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
40
src/router/basic-routes.js
Normal file
40
src/router/basic-routes.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export const basicRoutes = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: {
|
||||
title: '登录页',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
meta: {
|
||||
title: '页面飞走了',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: '403',
|
||||
path: '/403',
|
||||
component: () => import('@/views/error-page/403.vue'),
|
||||
meta: {
|
||||
title: '没有权限',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -6,10 +6,11 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { useAuthStore } from '@/store'
|
||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||
import api from '@/api'
|
||||
import { getPermissions, getUserInfo } from '@/store/helper'
|
||||
|
||||
const WHITE_LIST = ['/login', '/404', '/role-select']
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
router.beforeEach(async (to) => {
|
||||
const authStore = useAuthStore()
|
||||
@@ -25,6 +26,20 @@ export function createPermissionGuard(router) {
|
||||
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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useTabStore } from '@/store'
|
||||
|
||||
export const EXCLUDE_TAB = ['/404', '/403', '/login', '/role-select']
|
||||
export const EXCLUDE_TAB = ['/404', '/403', '/login']
|
||||
|
||||
export function createTabGuard(router) {
|
||||
router.afterEach((to) => {
|
||||
|
||||
@@ -8,98 +8,18 @@
|
||||
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuards } from './guards'
|
||||
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: {
|
||||
title: '登录页',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
meta: {
|
||||
title: '页面飞走了',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: '403',
|
||||
path: '/403',
|
||||
component: () => import('@/views/error-page/403.vue'),
|
||||
meta: {
|
||||
title: '没有权限',
|
||||
layout: 'empty',
|
||||
},
|
||||
},
|
||||
]
|
||||
import { basicRoutes } from './basic-routes'
|
||||
|
||||
export const router = createRouter({
|
||||
history:
|
||||
import.meta.env.VITE_USE_HASH === 'true' ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
import.meta.env.VITE_USE_HASH === 'true'
|
||||
? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH || '/')
|
||||
: createWebHistory(import.meta.env.VITE_PUBLIC_PATH || '/'),
|
||||
routes: basicRoutes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
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) {
|
||||
authStore.toLogin()
|
||||
return
|
||||
}
|
||||
await Promise.all([userStore.getUserInfo(), permissionStore.initPermissions()])
|
||||
permissionStore.accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
}
|
||||
|
||||
export async function resetRouter() {
|
||||
const basicRouteNames = getRouteNames(basicRoutes)
|
||||
router.getRoutes().forEach((route) => {
|
||||
const name = route.name
|
||||
if (!basicRouteNames.includes(name)) {
|
||||
router.removeRoute(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getRouteNames(routes) {
|
||||
const names = []
|
||||
for (const route of routes) {
|
||||
names.push(route.name)
|
||||
if (route.children?.length) {
|
||||
names.push(...getRouteNames(route.children))
|
||||
}
|
||||
}
|
||||
return names
|
||||
setupRouterGuards(router)
|
||||
}
|
||||
|
||||
@@ -8,52 +8,64 @@
|
||||
|
||||
export const defaultLayout = 'normal'
|
||||
|
||||
export const defaultPrimaryColor = '#316C72'
|
||||
|
||||
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',
|
||||
},
|
||||
}
|
||||
|
||||
export const basePermissions = [
|
||||
{
|
||||
code: 'ExternalLink',
|
||||
name: '外链',
|
||||
name: '外链(可内嵌打开)',
|
||||
type: 'MENU',
|
||||
icon: 'i-fe:external-link',
|
||||
order: 98,
|
||||
enable: true,
|
||||
show: true,
|
||||
children: [
|
||||
{
|
||||
code: 'ShowDocs',
|
||||
name: '项目文档',
|
||||
type: 'MENU',
|
||||
path: 'https://docs.isme.top/web/#/624306705/188522224',
|
||||
icon: 'i-me:docs',
|
||||
order: 1,
|
||||
enable: true,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
code: 'ApiFoxDocs',
|
||||
name: '接口文档',
|
||||
type: 'MENU',
|
||||
path: 'https://apifox.com/apidoc/shared-ff4a4d32-c0d1-4caf-b0ee-6abc130f734a',
|
||||
icon: 'i-me:apifox',
|
||||
order: 2,
|
||||
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',
|
||||
path: 'https://juejin.cn/user/1961184475483255/posts',
|
||||
icon: 'i-simple-icons:juejin',
|
||||
order: 1,
|
||||
order: 4,
|
||||
enable: true,
|
||||
show: true,
|
||||
},
|
||||
|
||||
29
src/store/helper.js
Normal file
29
src/store/helper.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { basePermissions } from '@/settings'
|
||||
import api from '@/api'
|
||||
|
||||
export async function getUserInfo() {
|
||||
const res = await api.getUser()
|
||||
const { id, username, profile, roles, currentRole } = res.data || {}
|
||||
return {
|
||||
id,
|
||||
username,
|
||||
avatar: profile?.avatar,
|
||||
nickName: profile?.nickName,
|
||||
gender: profile?.gender,
|
||||
address: profile?.address,
|
||||
email: profile?.email,
|
||||
roles,
|
||||
currentRole,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPermissions() {
|
||||
let asyncPermissions = []
|
||||
try {
|
||||
const res = await api.getRolePermissions()
|
||||
asyncPermissions = res?.data || []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return basePermissions.concat(asyncPermissions)
|
||||
}
|
||||
@@ -8,13 +8,15 @@
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { defaultLayout, naiveThemeOverrides } from '@/settings'
|
||||
import { generate, getRgbStr } from '@arco-design/color'
|
||||
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
collapsed: false,
|
||||
isDark: useDark(),
|
||||
layout: defaultLayout,
|
||||
primaryColor: defaultPrimaryColor,
|
||||
naiveThemeOverrides,
|
||||
}),
|
||||
actions: {
|
||||
@@ -30,9 +32,25 @@ 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,
|
||||
})
|
||||
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'],
|
||||
storage: localStorage,
|
||||
paths: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { useUserStore, usePermissionStore, useTabStore } from '@/store'
|
||||
import { resetRouter, router } from '@/router'
|
||||
import { useUserStore, usePermissionStore, useTabStore, useRouterStore } from '@/store'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
@@ -22,24 +21,30 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.$reset()
|
||||
},
|
||||
toLogin() {
|
||||
const currentRoute = unref(router.currentRoute)
|
||||
const { router, route } = useRouterStore()
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: currentRoute.query,
|
||||
query: route.query,
|
||||
})
|
||||
},
|
||||
async switchCurrentRole(data) {
|
||||
this.resetLoginState()
|
||||
await nextTick()
|
||||
this.setToken(data)
|
||||
},
|
||||
resetLoginState() {
|
||||
const { resetUser } = useUserStore()
|
||||
const { resetPermission } = usePermissionStore()
|
||||
const { resetRouter } = useRouterStore()
|
||||
const { resetPermission, accessRoutes } = usePermissionStore()
|
||||
const { resetTabs } = useTabStore()
|
||||
// 重置路由
|
||||
resetRouter(accessRoutes)
|
||||
// 重置用户
|
||||
resetUser()
|
||||
// 重置权限
|
||||
resetPermission()
|
||||
// 重置Tabs
|
||||
resetTabs()
|
||||
// 重置路由
|
||||
resetRouter()
|
||||
// 重置token
|
||||
this.resetToken()
|
||||
},
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './auth'
|
||||
export * from './permission'
|
||||
export * from './tab'
|
||||
export * from './user'
|
||||
export * from './router'
|
||||
|
||||
@@ -6,28 +6,19 @@
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { isExternal } from '@/utils'
|
||||
import { basePermissions } from '@/settings'
|
||||
import api from '@/api'
|
||||
|
||||
const routeComponents = import.meta.glob('@/views/**/*.vue')
|
||||
import { hyphenate } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: () => ({
|
||||
menus: [],
|
||||
accessRoutes: [],
|
||||
asyncPermissions: [],
|
||||
permissions: [],
|
||||
menus: [],
|
||||
}),
|
||||
getters: {
|
||||
permissions() {
|
||||
return basePermissions.concat(this.asyncPermissions)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async initPermissions() {
|
||||
const { data } = (await api.getRolePermissions()) || []
|
||||
this.asyncPermissions = data
|
||||
setPermissions(permissions) {
|
||||
this.permissions = permissions
|
||||
this.menus = this.permissions
|
||||
.filter((item) => item.type === 'MENU')
|
||||
.map((item) => this.getMenuItem(item))
|
||||
@@ -36,13 +27,14 @@ export const usePermissionStore = defineStore('permission', {
|
||||
},
|
||||
getMenuItem(item, parent) {
|
||||
const route = this.generateRoute(item, item.show ? null : parent?.key)
|
||||
if (item.enable && route.path && !isExternal(route.path)) this.accessRoutes.push(route)
|
||||
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,
|
||||
icon: () => h('i', { class: `${route.meta.icon}?mask text-16` }),
|
||||
originPath: route.meta.originPath,
|
||||
icon: () => h('i', { class: `${route.meta.icon} text-16` }),
|
||||
order: item.order ?? 0,
|
||||
}
|
||||
const children = item.children?.filter((item) => item.type === 'MENU') || []
|
||||
@@ -56,13 +48,20 @@ export const usePermissionStore = defineStore('permission', {
|
||||
return menuItem
|
||||
},
|
||||
generateRoute(item, parentKey) {
|
||||
let originPath = undefined
|
||||
if (isExternal(item.path)) {
|
||||
originPath = item.path
|
||||
item.component = '/src/views/iframe/index.vue'
|
||||
item.path = `/iframe/${hyphenate(item.code)}`
|
||||
}
|
||||
return {
|
||||
name: item.code,
|
||||
path: item.path,
|
||||
redirect: item.redirect,
|
||||
component: routeComponents[item.component] || undefined,
|
||||
component: item.component,
|
||||
meta: {
|
||||
icon: item.icon,
|
||||
originPath,
|
||||
icon: item.icon + '?mask',
|
||||
title: item.name,
|
||||
layout: item.layout,
|
||||
keepAlive: !!item.keepAlive,
|
||||
|
||||
26
src/store/modules/router.js
Normal file
26
src/store/modules/router.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**********************************
|
||||
* @Author: Ronnie Zhang
|
||||
* @LastEditor: Ronnie Zhang
|
||||
* @LastEditTime: 2024/01/06 17:18:40
|
||||
* @Email: zclzone@outlook.com
|
||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useRouterStore = defineStore('router', () => {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
function resetRouter(accessRoutes) {
|
||||
accessRoutes.forEach((item) => {
|
||||
router.hasRoute(item.name) && router.removeRoute(item.name)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
router,
|
||||
route,
|
||||
resetRouter,
|
||||
}
|
||||
})
|
||||
@@ -7,7 +7,7 @@
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { router } from '@/router'
|
||||
import { useRouterStore } from './router'
|
||||
|
||||
export const useTabStore = defineStore('tab', {
|
||||
state: () => ({
|
||||
@@ -55,13 +55,13 @@ export const useTabStore = defineStore('tab', {
|
||||
async removeTab(path) {
|
||||
this.setTabs(this.tabs.filter((tab) => tab.path !== path))
|
||||
if (path === this.activeTab) {
|
||||
router.push(this.tabs[this.tabs.length - 1].path)
|
||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeOther(curPath = this.activeTab) {
|
||||
this.setTabs(this.tabs.filter((tab) => tab.path === curPath))
|
||||
if (curPath !== this.activeTab) {
|
||||
router.push(this.tabs[this.tabs.length - 1].path)
|
||||
useRouterStore().router?.push(this.tabs[this.tabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeLeft(curPath) {
|
||||
@@ -69,7 +69,7 @@ export const useTabStore = defineStore('tab', {
|
||||
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
|
||||
this.setTabs(filterTabs)
|
||||
if (!filterTabs.find((item) => item.path === this.activeTab)) {
|
||||
router.push(filterTabs[filterTabs.length - 1].path)
|
||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeRight(curPath) {
|
||||
@@ -77,7 +77,7 @@ export const useTabStore = defineStore('tab', {
|
||||
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
|
||||
this.setTabs(filterTabs)
|
||||
if (!filterTabs.find((item) => item.path === this.activeTab.value)) {
|
||||
router.push(filterTabs[filterTabs.length - 1].path)
|
||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||
}
|
||||
},
|
||||
resetTabs() {
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
**********************************/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
@@ -35,32 +33,8 @@ export const useUserStore = defineStore('user', {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const res = await api.getUser()
|
||||
const { id, username, profile, roles, currentRole } = res.data || {}
|
||||
this.userInfo = {
|
||||
id,
|
||||
username,
|
||||
avatar: profile?.avatar,
|
||||
nickName: profile?.nickName,
|
||||
gender: profile?.gender,
|
||||
address: profile?.address,
|
||||
email: profile?.email,
|
||||
roles,
|
||||
currentRole,
|
||||
}
|
||||
return Promise.resolve(res.data)
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
async switchCurrentRole(roleCode) {
|
||||
const { data } = await api.switchCurrentRole(roleCode)
|
||||
const authStore = useAuthStore()
|
||||
authStore.resetLoginState()
|
||||
await nextTick()
|
||||
authStore.setToken(data)
|
||||
setUser(user) {
|
||||
this.userInfo = user
|
||||
},
|
||||
resetUser() {
|
||||
this.$reset()
|
||||
|
||||
@@ -68,7 +68,7 @@ body {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-color);
|
||||
background: rgb(var(--primary-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,9 @@ textarea {
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -43,7 +43,7 @@ export function setupInterceptors(axiosInstance) {
|
||||
const message = resolveResError(code, data?.message ?? statusText)
|
||||
|
||||
//需要错误提醒
|
||||
!config.noNeedTip && window.$message?.error(message)
|
||||
!config?.noNeedTip && message && window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error: data ?? response })
|
||||
}
|
||||
return Promise.resolve(data ?? response)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import * as NaiveUI from 'naive-ui'
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
export function setupMessage(NMessage) {
|
||||
class Message {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--------------------------------
|
||||
- @Author: Ronnie Zhang
|
||||
- @LastEditor: Ronnie Zhang
|
||||
- @LastEditTime: 2023/12/16 18:51:56
|
||||
- @LastEditTime: 2024/01/13 17:41:47
|
||||
- @Email: zclzone@outlook.com
|
||||
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||
--------------------------------->
|
||||
@@ -55,7 +55,7 @@ function openModal2() {
|
||||
$modal2.value?.open({
|
||||
cancelText: '关闭当前',
|
||||
okText: '关闭所有弹窗',
|
||||
modalStyle: { width: '320px', padding: '12px', top: '100px' },
|
||||
width: '400px',
|
||||
async onOk() {
|
||||
okLoading2.value = true
|
||||
$message.loading('正在关闭...', { key: 'modal2' })
|
||||
|
||||
@@ -148,7 +148,9 @@
|
||||
</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>
|
||||
|
||||
9
src/views/iframe/index.vue
Normal file
9
src/views/iframe/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<AppPage full>
|
||||
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full"></iframe>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
</script>
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<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" height="50" class="mr-12" />
|
||||
<img src="@/assets/images/logo.png" class="mr-12 h-50" />
|
||||
{{ title }}
|
||||
</h2>
|
||||
<n-input
|
||||
@@ -104,42 +104,29 @@
|
||||
import { throttle, lStorage } from '@/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import api from './api'
|
||||
import { useUserStore, useAuthStore } from '@/store'
|
||||
import { initUserAndPermissions } from '@/router'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const isLogined = computed(() => {
|
||||
return authStore.accessToken && userStore.roles
|
||||
})
|
||||
|
||||
const loginInfo = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
function initLoginInfo() {
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
if (localLoginInfo) {
|
||||
loginInfo.value.username = localLoginInfo.username || ''
|
||||
loginInfo.value.password = localLoginInfo.password || ''
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (isLogined.value) {
|
||||
router.push({ path: '/role-select', query: route.query })
|
||||
} else {
|
||||
initLoginInfo()
|
||||
initCaptcha()
|
||||
const localLoginInfo = lStorage.get('loginInfo')
|
||||
if (localLoginInfo) {
|
||||
loginInfo.value.username = localLoginInfo.username || ''
|
||||
loginInfo.value.password = localLoginInfo.password || ''
|
||||
}
|
||||
initCaptcha()
|
||||
|
||||
function quickLogin() {
|
||||
loginInfo.value.username = 'admin'
|
||||
@@ -179,7 +166,6 @@ 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**********************************
|
||||
* @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
|
||||
**********************************/
|
||||
@@ -11,7 +11,7 @@ import { request } from '@/utils'
|
||||
|
||||
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),
|
||||
savePermission: (id, data) => request.patch(`/permission/${id}`, data),
|
||||
|
||||
@@ -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
|
||||
--------------------------------->
|
||||
@@ -22,6 +22,7 @@
|
||||
:show-irrelevant-nodes="false"
|
||||
:pattern="pattern"
|
||||
:data="treeData"
|
||||
:selected-keys="[currentMenu?.code]"
|
||||
:render-prefix="renderPrefix"
|
||||
:render-suffix="renderSuffix"
|
||||
:on-update:selected-keys="onSelect"
|
||||
@@ -106,10 +107,15 @@ function handleDelete(item) {
|
||||
$dialog.confirm({
|
||||
content: `确认删除【${item.name}】?`,
|
||||
async confirm() {
|
||||
$message.loading('正在删除', { key: 'deleteMenu' })
|
||||
await api.deletePermission(item.id)
|
||||
$message.success('删除成功', { key: 'deleteMenu' })
|
||||
emit('refresh')
|
||||
try {
|
||||
$message.loading('正在删除', { key: 'deleteMenu' })
|
||||
await api.deletePermission(item.id)
|
||||
$message.success('删除成功', { key: 'deleteMenu' })
|
||||
emit('refresh')
|
||||
emit('update:currentMenu', null)
|
||||
} catch (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="根菜单"
|
||||
@@ -38,13 +39,28 @@
|
||||
</template>
|
||||
<n-input v-model:value="modalForm.code" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" path="path">
|
||||
<n-form-item-gi
|
||||
v-if="modalForm.type === 'MENU'"
|
||||
:span="12"
|
||||
path="path"
|
||||
:rule="{
|
||||
trigger: ['blur', 'change'],
|
||||
type: 'string',
|
||||
message: '必须是/、http、https开头',
|
||||
validator(rule, value) {
|
||||
if (value) {
|
||||
return /\/|http|https/.test(value)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<QuestionLabel label="路由地址" content="父级菜单可不填" />
|
||||
</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="菜单图标"
|
||||
@@ -53,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"
|
||||
@@ -62,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="组件路径"
|
||||
@@ -78,7 +94,7 @@
|
||||
/>
|
||||
</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>
|
||||
@@ -99,7 +115,7 @@
|
||||
<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"
|
||||
@@ -112,6 +128,7 @@
|
||||
</n-switch>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-if="modalForm.type === 'MENU'"
|
||||
:span="12"
|
||||
label="排序"
|
||||
path="order"
|
||||
@@ -167,15 +184,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 })
|
||||
}
|
||||
|
||||
@@ -183,15 +202,17 @@ async function onSave() {
|
||||
await validation()
|
||||
okLoading.value = true
|
||||
try {
|
||||
let newFormData
|
||||
if (!modalForm.value.parentId) modalForm.value.parentId = null
|
||||
if (modalAction.value === 'add') {
|
||||
await api.addPermission(modalForm.value)
|
||||
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)
|
||||
emit('refresh', modalAction.value === 'add' ? newFormData : modalForm.value)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
okLoading.value = false
|
||||
|
||||
@@ -22,12 +22,7 @@
|
||||
<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)"
|
||||
>
|
||||
<n-button size="small" type="primary" @click="handleEdit(currentMenu)">
|
||||
<i class="i-material-symbols:edit-outline mr-4 text-14" />
|
||||
编辑
|
||||
</n-button>
|
||||
@@ -64,6 +59,22 @@
|
||||
{{ currentMenu.order ?? '--' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="mt-32 flex justify-between">
|
||||
<h3 class="mb-12">按钮</h3>
|
||||
<n-button size="small" type="primary" @click="handleAddBtn">
|
||||
<i class="i-fe:plus mr-4 text-14" />
|
||||
新增
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<MeCrud
|
||||
ref="$table"
|
||||
:columns="btnsColumns"
|
||||
:scroll-x="-1"
|
||||
:get-data="api.getButtons"
|
||||
:query-items="{ parentId: currentMenu.id }"
|
||||
></MeCrud>
|
||||
</template>
|
||||
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
|
||||
</div>
|
||||
@@ -73,13 +84,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NButton, NSwitch } from 'naive-ui'
|
||||
import MenuTree from './components/MenuTree.vue'
|
||||
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
||||
import { MeCrud } from '@/components'
|
||||
import api from './api'
|
||||
|
||||
const treeData = ref([])
|
||||
const treeLoading = ref(false)
|
||||
async function initData(data) {
|
||||
if (data?.type === 'BUTTON') {
|
||||
$table.value.handleSearch()
|
||||
return
|
||||
}
|
||||
treeLoading.value = true
|
||||
const res = await api.getMenuTree()
|
||||
treeData.value = res?.data || []
|
||||
@@ -100,4 +117,127 @@ function handleEdit(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' }),
|
||||
}
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
const $table = ref(null)
|
||||
|
||||
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) {
|
||||
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) {
|
||||
item.enableLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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
|
||||
--------------------------------->
|
||||
@@ -23,13 +23,7 @@
|
||||
:get-data="api.read"
|
||||
>
|
||||
<MeQueryItem label="角色名" :label-width="50">
|
||||
<n-input
|
||||
v-model:value="queryItems.name"
|
||||
type="text"
|
||||
placeholder="请输入角色名"
|
||||
clearable
|
||||
@keydown.enter="() => $table?.handleSearch()"
|
||||
/>
|
||||
<n-input v-model:value="queryItems.name" type="text" placeholder="请输入角色名" clearable />
|
||||
</MeQueryItem>
|
||||
<MeQueryItem label="状态" :label-width="50">
|
||||
<n-select
|
||||
@@ -73,11 +67,16 @@
|
||||
<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>
|
||||
@@ -97,7 +96,6 @@ import { NButton, NSwitch } from 'naive-ui'
|
||||
import { MeCrud, MeQueryItem, MeModal } from '@/components'
|
||||
import { useCrud } from '@/composables'
|
||||
import api from './api'
|
||||
import CascadeTree from './components/CascadeTree.vue'
|
||||
|
||||
defineOptions({ name: 'RoleMgt' })
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keydown.enter="() => $table?.handleSearch"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keydown.enter="() => $table?.handleSearch"
|
||||
/>
|
||||
</MeQueryItem>
|
||||
|
||||
|
||||
@@ -100,9 +100,10 @@
|
||||
import { MeModal } from '@/components'
|
||||
import { useForm, useModal } from '@/composables'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getUserInfo } from '@/store/helper'
|
||||
import api from './api'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const required = {
|
||||
required: true,
|
||||
message: '此为必填项',
|
||||
@@ -116,7 +117,7 @@ async function handlePwdSave() {
|
||||
await pwdValidation()
|
||||
await api.changePassword(pwdForm.value)
|
||||
$message.success('密码修改成功')
|
||||
userStore.getUserInfo()
|
||||
refreshUserInfo()
|
||||
}
|
||||
|
||||
const newAvatar = ref(userStore.avatar)
|
||||
@@ -128,7 +129,7 @@ async function handleAvatarSave() {
|
||||
}
|
||||
await api.updateProfile({ id: userStore.userId, avatar: newAvatar.value })
|
||||
$message.success('头像修改成功')
|
||||
userStore.getUserInfo()
|
||||
refreshUserInfo()
|
||||
}
|
||||
|
||||
const genders = [
|
||||
@@ -148,6 +149,11 @@ async function handleProfileSave() {
|
||||
await profileValidation()
|
||||
await api.updateProfile(profileForm.value)
|
||||
$message.success('资料修改成功')
|
||||
userStore.getUserInfo()
|
||||
refreshUserInfo()
|
||||
}
|
||||
|
||||
async function refreshUserInfo() {
|
||||
const user = await getUserInfo()
|
||||
userStore.setUser(user)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,26 +48,7 @@ export default defineConfig({
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: 'var(--primary-color)',
|
||||
primary_hover: 'var(--primary-color-hover)',
|
||||
primary_pressed: 'var(--primary-color-pressed)',
|
||||
primary_active: 'var(--primary-color-active)',
|
||||
info: 'var(--info-color)',
|
||||
info_hover: 'var(--info-color-hover)',
|
||||
info_pressed: 'var(--info-color-pressed)',
|
||||
info_active: 'var(--info-color-active)',
|
||||
success: 'var(--success-color)',
|
||||
success_hover: 'var(--success-color-hover)',
|
||||
success_pressed: 'var(--success-color-pressed)',
|
||||
success_active: 'var(--success-color-active)',
|
||||
warning: 'var(--warning-color)',
|
||||
warning_hover: 'var(--warning-color-hover)',
|
||||
warning_pressed: 'var(--warning-color-pressed)',
|
||||
warning_active: 'var(--warning-color-active)',
|
||||
error: 'var(--error-color)',
|
||||
error_hover: 'var(--error-color-hover)',
|
||||
error_pressed: 'var(--error-color-pressed)',
|
||||
error_active: 'var(--error-color-active)',
|
||||
primary: 'rgba(var(--primary-color))',
|
||||
dark: '#18181c',
|
||||
light_border: '#efeff5',
|
||||
dark_border: '#2d2d30',
|
||||
|
||||
@@ -9,24 +9,23 @@
|
||||
import path from 'path'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Unocss from 'unocss/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import simpleHtmlPlugin from 'vite-plugin-simple-html'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import removeNoMatch from 'vite-plugin-router-warn'
|
||||
import { pluginPagePathes, pluginIcons } from './build/plugin-isme'
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
const isBuild = command === 'build'
|
||||
export default defineConfig(({ mode }) => {
|
||||
const viteEnv = loadEnv(mode, process.cwd())
|
||||
const { VITE_TITLE, VITE_PUBLIC_PATH, VITE_PROXY_TARGET } = viteEnv
|
||||
const { VITE_PUBLIC_PATH, VITE_PROXY_TARGET } = viteEnv
|
||||
|
||||
return {
|
||||
base: VITE_PUBLIC_PATH || '/',
|
||||
plugins: [
|
||||
VueDevTools(),
|
||||
Vue(),
|
||||
VueDevTools(),
|
||||
Unocss(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router'],
|
||||
@@ -36,18 +35,12 @@ export default defineConfig(({ command, mode }) => {
|
||||
resolvers: [NaiveUiResolver()],
|
||||
dts: false,
|
||||
}),
|
||||
simpleHtmlPlugin({
|
||||
minify: isBuild,
|
||||
inject: {
|
||||
data: {
|
||||
title: VITE_TITLE,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// 自定义插件,用于生成页面文件的path,并添加到虚拟模块
|
||||
pluginPagePathes(),
|
||||
// 自定义插件,用于生成自定义icon,并添加到虚拟模块
|
||||
pluginIcons(),
|
||||
// 移除非必要的vue-router动态路由警告: No match found for location with path
|
||||
removeNoMatch(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -64,6 +57,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
target: VITE_PROXY_TARGET,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
|
||||
secure: false,
|
||||
configure: (proxy, options) => {
|
||||
// 配置此项可在响应头中看到请求的真实地址
|
||||
proxy.on('proxyRes', (proxyRes, req) => {
|
||||
|
||||
Reference in New Issue
Block a user