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

76 Commits

Author SHA1 Message Date
zclzone
54c134c879 style: lint 2024-06-06 18:11:10 +08:00
zclzone
6b060291bb chore: 移除无用依赖 2024-06-06 18:08:05 +08:00
zclzone
a67510fe34 style: lint 2024-06-06 18:05:36 +08:00
zclzone
98f9d5893a chore: eslint 替换为 @antfu/eslint-config,集成 lint-staged 和 simple-git-hooks 2024-06-06 17:26:55 +08:00
zclzone
004ef366f2 fix: ref value 2024-06-06 16:42:07 +08:00
zclzone
7ed9a3540d chore: 更新 apifox mock 域名 2024-06-05 09:15:44 +08:00
zclzone
5766510ad9 feat: 补充production环境变量 2024-05-29 21:54:41 +08:00
zclzone
905476abf7 feat: 本地环境改用直调apifox云端mock接口 2024-05-29 21:48:05 +08:00
zclzone
160910bb85 style: 删除无用代码 2024-05-29 18:22:56 +08:00
zclzone
d3d002770b Merge branch '2.x' of https://gitee.com/isme-admin/vue-naive-admin into 2.x 2024-05-29 16:50:43 +08:00
zclzone
98f3648f9f docs: 注释 2024-05-29 16:50:29 +08:00
zclzone
6cdf905cd4 build: remove vite-plugin-simple-html 2024-05-25 22:11:57 +08:00
zclzone
f1661731da docs: update README 2024-05-24 23:44:27 +08:00
zclzone
87dce667cf docs: update README 2024-05-24 18:03:42 +08:00
zclzone
ea440e48bd chore: update deps 2024-05-17 09:49:13 +08:00
zclzone
0ac55503b7 refactor: 设置主题色样式 2024-05-12 14:01:35 +08:00
zclzone
769fd86d30 chore: 删除无用依赖 2024-05-12 13:55:54 +08:00
zclzone
fd34922acc fix: 修复简约布局下不初始化主题色问题 2024-05-10 20:18:15 +08:00
zclzone
cf3c4b9020 feat: 增加设置主题色功能 2024-05-10 20:02:55 +08:00
zclzone
621c34a1fb Merge branch '2.x' of https://gitee.com/isme-admin/vue-naive-admin into 2.x 2024-05-10 16:53:07 +08:00
zclzone
88288bc2c4 style: 调整logo样式 2024-05-10 16:52:49 +08:00
zclzone
e73c138892 style: 修改菜单选中样式 2024-05-10 16:51:14 +08:00
大脸怪
005aa60982 docs: Update README 2024-05-08 09:50:27 +08:00
大脸怪
4f637d76e6 docs: Update README, close #81 2024-05-06 08:56:53 +08:00
zclzone
4eb15744a6 feat: 调整主题设置样式 2024-04-24 21:50:55 +08:00
zclzone
ddcbb83574 feat: add VueDevTools plugin 2024-04-24 21:41:40 +08:00
zclzone
2edc6537c1 chore: update deps 2024-04-23 17:22:59 +08:00
zclzone
26afddc559 fix: 修复首页echarts图高度问题 2024-04-23 17:22:43 +08:00
zclzone
0e16cbb0a3 chore: update deps 2024-04-22 18:21:57 +08:00
zclzone
5629e80822 chore: 移除推荐插件中废弃的插件 2024-04-10 21:19:40 +08:00
zclzone
5b798d7db2 chore: 修改尾行风格为自动 2024-04-10 21:17:05 +08:00
zclzone
9615ec9aa8 style: 修改菜单选中样式 2024-04-03 17:58:41 +08:00
zclzone
e135be93af fix: 修改parentIdDisabled判断 & 修复defaultForm问题 2024-04-01 22:12:02 +08:00
zclzone
369ff0a68f fix: 删除菜单时取消选中 2024-04-01 21:45:34 +08:00
大脸怪
eb3c56f5af Merge pull request #75 from HadeAs/2.x
fix: 修复新增菜单报错并且选中新增的菜单节点
2024-04-01 21:38:32 +08:00
hadeas
008bed05a9 fix: 修复新增菜单报错并且选中新增的菜单节点 2024-04-01 12:36:32 +00:00
zclzone
0141c0287e Merge branch '2.x' of https://gitee.com/isme-admin/vue-naive-admin into 2.x 2024-04-01 16:00:52 +08:00
zclzone
8f715925c7 feat: 资源管理新增支持按钮权限 2024-04-01 16:00:44 +08:00
zclzone
763b5f1295 refactor: 取消级联权限树 2024-04-01 15:55:23 +08:00
zclzone
961ad6af7b chore: 重构权限路由守卫 2024-03-31 18:33:58 +08:00
zclzone
c754d02dc0 fix: 修复MeModal拖拽导致不能拖选文本问题,close#74 2024-03-25 22:42:01 +08:00
zclzone
2599ea2060 feat: 支持菜单多色图标 2024-03-20 09:46:56 +08:00
zclzone
a63e72bc2f style: 移除白屏loading的logo 2024-03-15 16:24:38 +08:00
zclzone
04723ffbfa feat: 外链可内嵌打开 2024-03-15 16:20:38 +08:00
zclzone
fd9480e92f fix: logo 宽高异常 2024-03-12 09:38:08 +08:00
大脸怪
0520cd015a Merge pull request #73 from hanfengcan/2.x
feat: 增加必要的校验提示
2024-03-11 21:54:59 +08:00
hanfengcan
f5a26c32e9 U:增加必要的校验提示 2024-03-10 18:21:04 +08:00
zclzone
886ef9e11c feat: logo背景色根据主题色自动调整 2024-03-04 16:33:35 +08:00
zclzone
7d0a17b2b5 chore: update deps, close #I93HBV 2024-02-29 17:23:00 +08:00
zclzone
fa4967efc3 chore: 修改eslint设配置 2024-02-29 17:01:43 +08:00
zclzone
207150623e fix: 修复创建路由没有考虑 base 路径问题, close #70 2024-02-21 10:04:21 +08:00
zclzone
6981692c54 mod: appStore持久化存储改用sessionStorage 2024-02-21 09:06:04 +08:00
zclzone
b64e1c7595 chore: 移除 vite-plugin-vue-devtools 2024-02-11 17:00:25 +08:00
zclzone
efcbe3bea4 mod: 移除多余图标 2024-02-11 16:59:27 +08:00
zclzone
2c808c6d8b chore: 修改npm淘宝镜像地址 2024-02-01 11:19:32 +08:00
zclzone
447db11c52 chore: update eslintConfig 2024-02-01 10:24:52 +08:00
zclzone
a3d0e863cc fix: 修复本地代理apiFox云端mock接口不通问题 2024-01-30 22:51:50 +08:00
zclzone
a2827e4c0d Merge branch '2.x' of https://gitee.com/isme-admin/vue-naive-admin into 2.x 2024-01-25 15:54:05 +08:00
zclzone
f888c2fbfd fix: 移除跳转role-select页面逻辑 2024-01-25 15:53:35 +08:00
zclzone
1c37a38b92 chore: update deps 2024-01-21 18:06:25 +08:00
zclzone
f2eb40357d fix: submit导致reload问题 2024-01-21 18:01:05 +08:00
zclzone
58de3c1ad6 perf: 优化 crud query 2024-01-21 17:52:50 +08:00
zclzone
567e306a5c Merge branch '2.x' of https://github.com/zclzone/vue-naive-admin into 2.x 2024-01-21 17:26:00 +08:00
zclzone
065c6b50c6 fix: enter -> handleQuery 2024-01-21 16:53:43 +08:00
大脸怪
95a1ef654c !2 update src/views/pms/user/index.vue.
Merge pull request !2 from aiden/N/A
2024-01-21 08:51:51 +00:00
大脸怪
346da8772a Update README.md 2024-01-13 18:18:44 +08:00
zclzone
a6773cbfec feat: 增加MeModal拖拽功能 2024-01-13 17:43:48 +08:00
aiden
4c337d3aa8 update src/views/pms/user/index.vue.
修正用户管理回车不执行搜索的bug

Signed-off-by: aiden <792000767@qq.com>
2024-01-08 11:33:47 +00:00
zclzone
2c9604a829 fix: 解决vite5循环引用导致热更新失效问题 2024-01-06 18:47:40 +08:00
大脸怪
c4fd0459ab Update README.md 2024-01-04 09:12:51 +08:00
zclzone
aa92455dbb chore: update deps about vite & vue 2024-01-02 22:29:01 +08:00
zclzone
857381471e chore: update deps about unocss 2024-01-02 22:04:38 +08:00
zclzone
b780113f18 fix: message判空 2023-12-29 15:59:04 +08:00
zclzone
fe4bbded53 fix: 重置空字符 -> null 2023-12-28 17:09:12 +08:00
zclzone
d801cf28cc fix: 报错未及时清除loading message问题 2023-12-23 20:31:52 +08:00
大脸怪
334174d442 Update README.md 2023-12-21 14:26:12 +08:00
103 changed files with 6973 additions and 5746 deletions

View File

@@ -2,4 +2,8 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -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'

View File

@@ -4,5 +4,7 @@ VITE_USE_HASH = 'false'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
VITE_AXIOS_BASE_URL = '/api' # 用于代理
# 代理配置-target
VITE_PROXY_TARGET = 'http://localhost:8085'

View File

@@ -1,4 +0,0 @@
node_modules
dist
public
package.json

2
.npmrc
View File

@@ -1,2 +1,2 @@
registry=https://registry.npm.taobao.org
registry=https://registry.npmmirror.com
shamefully-hoist=true

View File

@@ -1,3 +0,0 @@
/node_modules/**
/dist/*
/public/*

View File

@@ -1,7 +0,0 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "ignore"
}

View File

@@ -1,7 +1,6 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"antfu.unocss",
"antfu.iconify",
"dbaeumer.vscode-eslint",

41
.vscode/settings.json vendored
View File

@@ -1,22 +1,27 @@
{
"files.eol": "\n",
"files.associations": {
"*.env.*": "dotenv",
"*.svg": "html",
"*.css": "scss"
// Enable the ESlint flat config support
// (remove this if your ESLint extension above v3.0.5)
"eslint.experimental.useFlatConfig": true,
"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"]
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc"
]
}

View File

@@ -8,11 +8,12 @@
</p>
## 简介
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss后端使用 Nestjs + TypeOrm + MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满
## 设计理念
Vue Naive Admin 2022年2月开始开源从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript前端这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱,截至 2023-11-17 github `1.1k+` stargitee `260+` star
Vue Naive Admin 2022年2月开始开源从 1.0 到现在的 2.0,一直秉持着`简单即正义`的理念,旨在帮助中小企业、在校大学生及个人开发者快速上手开发后台管理项目,为了降低使用者的学习成本,没有使用看似主流的 TypeScript前端这也使得 Vue Naive Admin 成为了市面上少有的 `使用 JavaScript 的 Vue3 后台管理模板`,而且还算优秀,得到了大量朋友的认可和喜爱。
## 特性
@@ -48,11 +49,20 @@ 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,10 @@ 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。
## 入群交流
添加微信,拉你进群
![](https://static.isme.top/image/coder_wx.jpg)

View File

@@ -6,8 +6,8 @@
* 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'
/**
@@ -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, '/')}`)
}

View File

@@ -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())}`
}
},

View File

@@ -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
View 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',
},
},
})

View File

@@ -5,24 +5,90 @@
<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 class="loading">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -1,60 +1,60 @@
{
"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": {
"@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.10.0",
"axios": "^1.7.2",
"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.3",
"vue-router": "^4.3.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"@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"
"@antfu/eslint-config": "^2.20.0",
"@iconify/json": "^2.2.216",
"@iconify/utils": "^2.1.24",
"@unocss/eslint-config": "^0.60.4",
"@unocss/eslint-plugin": "^0.60.4",
"@unocss/preset-rem-to-px": "^0.60.4",
"@vitejs/plugin-vue": "^5.0.5",
"eslint": "^9.4.0",
"eslint-plugin-format": "^0.1.1",
"esno": "^4.7.0",
"fs-extra": "^11.2.0",
"glob": "^10.4.1",
"lint-staged": "^15.2.5",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.4",
"simple-git-hooks": "^2.11.1",
"taze": "^0.13.8",
"unocss": "^0.60.4",
"unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.0",
"vite": "^5.2.12",
"vite-plugin-router-warn": "^1.0.0",
"vite-plugin-vue-devtools": "^7.2.1"
},
"eslintConfig": {
"extends": [
"@zclzone",
"@unocss",
".eslint-global-variables.json"
],
"rules": {
"no-unused-vars": [
"error",
{
"varsIgnorePattern": "^_"
}
]
}
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}

10274
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}

View File

@@ -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

View File

@@ -27,16 +27,15 @@
</template>
<script setup>
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
import { darkTheme, dateZhCN, zhCN } 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()
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 +43,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>

View File

@@ -16,9 +16,9 @@ export default {
// 登出
logout: () => request.post('/auth/logout'),
// 切换当前角色
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}`),
}

View 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

View 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

View 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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -9,7 +9,9 @@
<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>
<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" />

View File

@@ -27,8 +27,10 @@
<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>
<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" />

View File

@@ -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">

View 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>

View 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 { getPresetColors } from '@arco-design/color'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const primaryColors = Object.entries(getPresetColors()).map(([, value]) => value.primary)
</script>

View File

@@ -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>

View File

@@ -7,12 +7,8 @@
--------------------------------->
<template>
<AppCard
v-if="$slots.default"
bordered
bg="#fafafc dark:black"
class="mb-30 min-h-60 flex justify-between rounded-4 p-16"
>
<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>
@@ -21,14 +17,15 @@
<i class="i-fe:rotate-ccw mr-4" />
重置
</n-button>
<n-button class="ml-20" type="primary" @click="handleSearch">
<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
<NDataTable
:remote="remote"
:loading="loading"
:scroll-x="scrollX"
@@ -54,7 +51,7 @@ const props = defineProps({
default: true,
},
/**
* @remote 是否分页
* @isPagination 是否分页
*/
isPagination: {
type: Boolean,
@@ -81,10 +78,10 @@ const props = defineProps({
},
/**
* ! 约定接口入参出参
* * 分页模式需约定分页接口入参
* 分页模式需约定分页接口入参
* @pageSize 分页参数一页展示多少条默认10
* @pageNo 分页参数页码默认1
* * 需约定接口出参
* 需约定接口出参
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
* @total 分页模式必须非分页模式如果没有total则取上一层data.length
*/
@@ -114,10 +111,12 @@ async function handleQuery() {
})
tableData.value = data?.pageData || data
pagination.itemCount = data.total ?? data.length
} catch (error) {
}
catch (error) {
tableData.value = []
pagination.itemCount = 0
} finally {
}
finally {
emit('onDataChange', tableData.value)
loading.value = false
}
@@ -129,7 +128,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()
@@ -143,16 +142,17 @@ function onPageChange(currentPage) {
}
}
function onChecked(rowKeys) {
if (props.columns.some((item) => item.type === 'selection')) {
if (props.columns.some(item => item.type === 'selection')) {
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, '数据报表')

View File

@@ -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,18 +9,20 @@
<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()"
>
<slot></slot>
<n-card :style="modalOptions.contentStyle" :closable="modalOptions.closable" @close="close()">
<template #header>
<header class="modal-header">
{{ modalOptions.title }}
</header>
</template>
<slot />
<!-- 底部按钮 -->
<template #footer>
@@ -45,6 +47,8 @@
</template>
<script setup>
import { initDrag } from './utils'
const props = defineProps({
width: {
type: String,
@@ -100,13 +104,29 @@ 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
}
},
})
// 打开模态框
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函数用于关闭模态框
@@ -125,7 +145,8 @@ async function handleOk(data) {
const res = await modalOptions.value.onOk(data)
// 如果onOk函数的返回值不为false则关闭模态框
res !== false && close()
} catch (error) {
}
catch (error) {
okLoading.value = false
console.error(error)
}
@@ -143,22 +164,20 @@ async function handleCancel(data) {
// 如果onCancel函数的返回值不为false则关闭模态框
res !== false && close()
} catch (error) {
}
catch (error) {
okLoading.value = false
console.error(error)
}
}
const okLoading = computed({
get() {
return !!modalOptions.value?.okLoading
},
set(v) {
if (modalOptions.value) {
modalOptions.value.okLoading = v
}
},
})
async function onAfterLeave() {
await nextTick()
initDrag(
Array.prototype.at.call(document.querySelectorAll('.modal-header'), -1),
Array.prototype.at.call(document.querySelectorAll('.modal-box'), -1),
)
}
// 定义一个defineExpose函数用于暴露open、close、handleOk、handleCancel函数
defineExpose({

View File

@@ -0,0 +1,72 @@
/**********************************
* @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距离
const left = Number.parseInt(params.left) + disX // 盒子元素的新left值
const top = Number.parseInt(params.top) + disY // 盒子元素的新top值
box.style.left = `${left}px`
box.style.top = `${top}px`
}
}
}

View File

@@ -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 {

View File

@@ -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,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
action.cb()
okLoading.value = false
data && refresh(data)
} catch (error) {
}
catch (error) {
okLoading.value = false
return false
}
@@ -85,7 +87,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: '提示',
@@ -98,7 +101,8 @@ export const useCrud = ({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
$message.success('删除成功')
d.loading = false
refresh(data)
} catch (error) {
}
catch (error) {
d.loading = false
}
},

View File

@@ -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 = {

View File

@@ -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() {

View File

@@ -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()
}

View File

@@ -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 }),

View File

@@ -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) {
@@ -62,11 +64,13 @@ 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()
} catch (error) {
}
catch (error) {
console.error(error)
okLoading.value = false
return false

View File

@@ -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"
@@ -20,6 +20,7 @@
<script setup>
import { useAppStore } from '@/store'
const title = import.meta.env.VITE_TITLE
const appStore = useAppStore()

View File

@@ -39,9 +39,21 @@ watch(route, async () => {
})
function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
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 +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));
}
}
}

View File

@@ -21,9 +21,8 @@
</template>
<script setup>
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
import { useAuthStore, usePermissionStore, useUserStore } from '@/store'
import { RoleSelect } from '@/layouts/components'
import { initUserAndPermissions } from '@/router'
import api from '@/api'
const router = useRouter()
@@ -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()

View File

@@ -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' }),
},
])

View File

@@ -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({
@@ -51,7 +49,7 @@ const contextMenuOption = reactive({
currentPath: '',
})
const handleItemClick = (path) => {
function handleItemClick(path) {
tabStore.setActiveTab(path)
router.push(path)
}
@@ -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,

View File

@@ -32,19 +32,22 @@
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 { useDark, useFullscreen, useToggle } from '@vueuse/core'
import { BreadCrumb, MenuCollapse, UserAvatar } from '@/layouts/components'
import { useAppStore } from '@/store'
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
function toggleDark() {
appStore.toggleDark()
useToggle(isDark)()
}

View File

@@ -27,10 +27,10 @@
</template>
<script setup>
import { useAppStore } from '@/store'
import { AppTab } from '@/layouts/components'
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
import { useAppStore } from '@/store'
import { AppTab } from '@/layouts/components'
const appStore = useAppStore()
</script>

View File

@@ -34,19 +34,22 @@
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 { useDark, useFullscreen, useToggle } from '@vueuse/core'
import { AppTab, MenuCollapse, UserAvatar } from '@/layouts/components'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
function toggleDark() {
appStore.toggleDark()
useToggle(isDark)()
}
@@ -56,4 +59,8 @@ const { isFullscreen, toggle } = useFullscreen()
function handleLinkClick(link) {
window.open(link)
}
watchEffect(() => {
appStore.setThemeColor(appStore.primaryColor, appStore.isDark)
})
</script>

View File

@@ -24,9 +24,9 @@
</template>
<script setup>
import { useAppStore } from '@/store'
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
import { useAppStore } from '@/store'
const appStore = useAppStore()
</script>

View File

@@ -23,8 +23,8 @@
</template>
<script setup>
import { useAppStore } from '@/store'
import SideBar from './sidebar/index.vue'
import { useAppStore } from '@/store'
const appStore = useAppStore()
</script>

View File

@@ -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()

View File

@@ -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()

View 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',
},
},
]

View File

@@ -13,7 +13,8 @@ export function createPageTitleGuard(router) {
const pageTitle = to.meta?.title
if (pageTitle) {
document.title = `${pageTitle} | ${baseTitle}`
} else {
}
else {
document.title = baseTitle
}
})

View File

@@ -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()
@@ -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)

View File

@@ -8,11 +8,12 @@
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) => {
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

View File

@@ -6,100 +6,20 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { setupRouterGuards } from './guards'
import { 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)
}

View File

@@ -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,
},

30
src/store/helper.js Normal file
View File

@@ -0,0 +1,30 @@
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)
}

View File

@@ -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,
},
})

View File

@@ -7,8 +7,7 @@
**********************************/
import { defineStore } from 'pinia'
import { useUserStore, usePermissionStore, useTabStore } from '@/store'
import { resetRouter, router } from '@/router'
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } 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()
},

View File

@@ -3,3 +3,4 @@ export * from './auth'
export * from './permission'
export * from './tab'
export * from './user'
export * from './router'

View File

@@ -6,70 +6,72 @@
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
**********************************/
import { hyphenate } from '@vueuse/core'
import { defineStore } from 'pinia'
import { isExternal } from '@/utils'
import { basePermissions } from '@/settings'
import api from '@/api'
const routeComponents = import.meta.glob('@/views/**/*.vue')
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))
.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 && !isExternal(route.path)) 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,
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') || []
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
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,
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 })),
},
}
},

View 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,
}
})

View File

@@ -7,7 +7,7 @@
**********************************/
import { defineStore } from 'pinia'
import { router } from '@/router'
import { useRouterStore } from './router'
export const useTabStore = defineStore('tab', {
state: () => ({
@@ -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,31 +56,31 @@ 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) {
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))
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) {
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)) {
router.push(filterTabs[filterTabs.length - 1].path)
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)) {
router.push(filterTabs[filterTabs.length - 1].path)
if (!filterTabs.find(item => item.path === this.activeTab.value)) {
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
}
},
resetTabs() {

View File

@@ -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()

View File

@@ -68,7 +68,7 @@ body {
background: #bfbfbf;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
background: rgb(var(--primary-color));
}
}
}

View File

@@ -41,3 +41,9 @@ textarea {
border: none;
resize: none;
}
img,
video {
max-width: 100%;
height: auto;
}

View File

@@ -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))
}
/**

View File

@@ -13,7 +13,8 @@ let isConfirming = false
export function resolveResError(code, message) {
switch (code) {
case 401:
if (isConfirming) return
if (isConfirming)
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)
return
isConfirming = true
$dialog.confirm({
title: '提示',

View File

@@ -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({

View File

@@ -20,7 +20,7 @@ export function setupInterceptors(axiosInstance) {
const { accessToken } = useAuthStore()
if (accessToken) {
// token: Bearer + xxx
config.headers.Authorization = 'Bearer ' + accessToken
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
@@ -42,8 +42,8 @@ export function setupInterceptors(axiosInstance) {
// 根据code处理对应的操作并返回处理后的message
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)

View File

@@ -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'

View File

@@ -9,14 +9,15 @@
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 {
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

View File

@@ -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,8 @@ class Storage {
}
this.remove(key)
return def
} catch (error) {
}
catch (error) {
this.remove(key)
return def
}

View File

@@ -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: '确认删除?',

View File

@@ -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>

View File

@@ -1,14 +1,16 @@
<!--------------------------------
- @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
--------------------------------->
<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>
@@ -55,7 +57,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' })

View File

@@ -9,7 +9,9 @@
<template>
<CommonPage show-footer>
<template #title-suffix>
<n-tag class="ml-12" type="primary">feather图标集 + isme自定义图标</n-tag>
<n-tag class="ml-12" type="primary">
feather图标集 + isme自定义图标
</n-tag>
</template>
<ul class="flex flex-wrap justify-between">
<li
@@ -18,7 +20,7 @@
class="m-16 w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg"
@click="copy(`&lt;i class=&quot;${item}&quot; /&gt;`)"
>
<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 +28,13 @@
{{ 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>
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
<li class="mx-16 h-0 w-160" />
</ul>
</CommonPage>
</template>

View File

@@ -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(`![${item.fileName}](${item.url})`)">
MD
</n-button>
@@ -53,6 +57,7 @@
<script setup>
import { useClipboard } from '@vueuse/core'
defineOptions({ name: 'ImgUpload' })
const { copy, copied } = useClipboard()

View 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>

View File

@@ -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>

View File

@@ -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>
@@ -148,19 +152,21 @@
</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 { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import VChart from 'vue-echarts'
import { useUserStore } from '@/store'
const userStore = useUserStore()

View File

@@ -0,0 +1,9 @@
<template>
<AppPage full>
<iframe :src="route.meta.originPath" frameborder="0" class="wh-full" />
</AppPage>
</template>
<script setup>
const route = useRoute()
</script>

View File

@@ -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, { noNeedToken: true }),
getUser: () => request.get('/user/detail'),
}

View File

@@ -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" height="50" class="mr-12" />
<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,45 +101,32 @@
</template>
<script setup>
import { throttle, lStorage } from '@/utils'
import { useStorage } from '@vueuse/core'
import api from './api'
import { useUserStore, useAuthStore } from '@/store'
import { initUserAndPermissions } from '@/router'
import { lStorage, throttle } from '@/utils'
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'
@@ -151,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) {
// 为防止爆破,验证码错误则刷新验证码
@@ -179,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')
}

View File

@@ -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,9 +11,9 @@ 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),
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}`),
}

View File

@@ -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
/>
</n-space>
@@ -38,9 +39,9 @@
<script setup>
import { withModifiers } from 'vue'
import ResAddOrEdit from './ResAddOrEdit.vue'
import { NButton } from 'naive-ui'
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: () => '删除' },
),
]
}
@@ -106,10 +107,16 @@ function handleDelete(item) {
$dialog.confirm({
content: `确认删除【${item.name}】?`,
async confirm() {
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')
}
},
})
}

View File

@@ -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,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">
@@ -95,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"
@@ -107,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"
@@ -130,12 +159,12 @@
</template>
<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 api from '../api'
import QuestionLabel from './QuestionLabel.vue'
import { MeModal } from '@/components'
import { useForm, useModal } from '@/composables'
const props = defineProps({
menus: {
@@ -148,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 = [
@@ -167,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 })
}
@@ -183,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

View File

@@ -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,24 @@
{{ 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"
:query-items="{ parentId: currentMenu.id }"
/>
</template>
<n-empty v-else class="h-450 f-c-c" size="large" description="请选择菜单查看详情" />
</div>
@@ -73,31 +92,163 @@
</template>
<script setup>
import { NButton, NSwitch } from 'naive-ui'
import MenuTree from './components/MenuTree.vue'
import ResAddOrEdit from './components/ResAddOrEdit.vue'
import api from './api'
import { MeCrud } from '@/components'
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) {
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>

View File

@@ -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 }),

View File

@@ -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>

View File

@@ -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
@@ -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,19 +67,28 @@
<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)"
checkable
check-on-click
default-expand-all
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>
@@ -94,10 +97,9 @@
<script setup>
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'
import { MeCrud, MeModal, MeQueryItem } from '@/components'
import { useCrud } from '@/composables'
defineOptions({ name: 'RoleMgt' })
@@ -111,13 +113,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: () => $table.value?.handleSearch(),
})
const columns = [
{ title: '角色名', key: 'name' },
{ title: '角色编码', key: 'code' },
{
title: '状态',
key: 'enable',
render: (row) =>
render: row =>
h(
NSwitch,
{
@@ -131,7 +143,7 @@ const columns = [
{
checked: () => '启用',
unchecked: () => '停用',
}
},
),
},
{
@@ -154,7 +166,7 @@ const columns = [
{
default: () => '分配用户',
icon: () => h('i', { class: 'i-fe:user-plus text-14' }),
}
},
),
h(
NButton,
@@ -168,7 +180,7 @@ const columns = [
{
default: () => '编辑',
icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
}
},
),
h(
@@ -183,7 +195,7 @@ const columns = [
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
},
),
]
},
@@ -197,21 +209,12 @@ async function handleEnable(row) {
row.enableLoading = false
$message.success('操作成功')
$table.value?.handleSearch()
} catch (error) {
}
catch (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>

View File

@@ -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>
@@ -43,7 +45,6 @@
type="text"
placeholder="请输入用户名"
clearable
@keydown.enter="() => $table?.handleSearch"
/>
</MeQueryItem>
@@ -67,10 +68,10 @@
<script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { h } from 'vue'
import api from './api'
import { MeCrud, MeQueryItem } from '@/components'
import { formatDateTime } from '@/utils'
import api from './api'
import { h } from 'vue'
defineOptions({ name: 'RoleUser' })
const route = useRoute()
@@ -112,8 +113,8 @@ const columns = [
h(
NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name }
)
{ default: () => item.name },
),
)
}
return '暂无角色'
@@ -123,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))
},
},
{
@@ -138,7 +139,7 @@ const columns = [
key: 'enable',
width: 100,
render: (row) =>
render: row =>
h(
NSwitch,
{
@@ -149,7 +150,7 @@ const columns = [
{
checked: () => '启用',
unchecked: () => '停用',
}
},
),
},
{
@@ -160,7 +161,7 @@ const columns = [
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,
{
@@ -172,7 +173,7 @@ const columns = [
{
default: () => '取消授权',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
},
)
: h(
NButton,
@@ -185,7 +186,7 @@ const columns = [
{
default: () => '授权',
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
}
},
)
},
},
@@ -198,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() {
@@ -210,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() {

View File

@@ -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'),

View File

@@ -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
@@ -28,7 +28,6 @@
type="text"
placeholder="请输入用户名"
clearable
@keydown.enter="() => $table?.handleSearch"
/>
</MeQueryItem>
@@ -93,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>
@@ -108,10 +111,10 @@
<script setup>
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
import { formatDateTime } from '@/utils'
import { MeCrud, MeQueryItem, MeModal } from '@/components'
import { useCrud } from '@/composables'
import api from './api'
import { formatDateTime } from '@/utils'
import { MeCrud, MeModal, MeQueryItem } from '@/components'
import { useCrud } from '@/composables'
defineOptions({ name: 'UserMgt' })
@@ -130,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: '头像',
@@ -153,8 +174,8 @@ const columns = [
h(
NTag,
{ type: 'success', style: index > 0 ? 'margin-left: 8px;' : '' },
{ default: () => item.name }
)
{ default: () => item.name },
),
)
}
return '暂无角色'
@@ -164,7 +185,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 } },
{
@@ -172,14 +193,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,
{
@@ -192,7 +213,7 @@ const columns = [
{
checked: () => '启用',
unchecked: () => '停用',
}
},
),
},
{
@@ -215,7 +236,7 @@ const columns = [
{
default: () => '分配角色',
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
}
},
),
h(
NButton,
@@ -228,7 +249,7 @@ const columns = [
{
default: () => '重置密码',
icon: () => h('i', { class: 'i-radix-icons:reset text-14' }),
}
},
),
h(
@@ -242,7 +263,7 @@ const columns = [
{
default: () => '删除',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}
},
),
]
},
@@ -256,13 +277,14 @@ async function handleEnable(row) {
row.enableLoading = false
$message.success('操作成功')
$table.value?.handleSearch()
} catch (error) {
}
catch (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: '分配角色',
@@ -271,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('密码重置成功'),

View File

@@ -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