mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-12-25 22:30:19 +08:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34ffd9c1f3 | ||
|
|
3131e00f0f | ||
|
|
f71812d622 | ||
|
|
0c12665fda | ||
|
|
458b7adb29 | ||
|
|
264da00e5d | ||
|
|
0882c25034 | ||
|
|
a3562d9212 | ||
|
|
b08c389e4a | ||
|
|
91bc1519f1 | ||
|
|
58b27c9693 | ||
|
|
25daa23606 | ||
|
|
9110d87580 | ||
|
|
c097b5681d | ||
|
|
beb705f8a9 | ||
|
|
fd9488673c | ||
|
|
70aeefea02 | ||
|
|
1b3d2a6168 | ||
|
|
fcb7ad965d | ||
|
|
828a2f5b60 | ||
|
|
be6d431485 | ||
|
|
61a43b8efd | ||
|
|
e3a9c77fd1 | ||
|
|
e772ff05fb | ||
|
|
d064f6285a | ||
|
|
091ca1a4fe | ||
|
|
9cec6a31a5 | ||
|
|
bb8af263e1 | ||
|
|
7211a17a81 | ||
|
|
810398abb8 | ||
|
|
f2b580fc06 | ||
|
|
3bd8858121 | ||
|
|
5efd1dbec4 | ||
|
|
971915948b | ||
|
|
a9a37036d5 | ||
|
|
7a58035514 | ||
|
|
57bfe27819 | ||
|
|
fcc65c3751 | ||
|
|
f2d8dfc3ef | ||
|
|
b5c570adf5 | ||
|
|
94098d02e8 | ||
|
|
436b15f010 | ||
|
|
0282feb173 | ||
|
|
8456750901 | ||
|
|
1a02cab97c | ||
|
|
1bdd81a1d8 | ||
|
|
a9d58f88aa | ||
|
|
6a344ff2c7 |
2
.env
2
.env
@@ -7,7 +7,7 @@ VITE_APP_TITLE=Soybean管理系统
|
|||||||
VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
VITE_APP_DESC=SoybeanAdmin是一个中后台管理系统模版
|
||||||
|
|
||||||
# 权限路由模式: static | dynamic
|
# 权限路由模式: static | dynamic
|
||||||
VITE_AUTH_ROUTE_MODE=dynamic
|
VITE_AUTH_ROUTE_MODE=static
|
||||||
|
|
||||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||||
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ VITE_COMPRESS=N
|
|||||||
|
|
||||||
# gzip | brotliCompress | deflate | deflateRaw
|
# gzip | brotliCompress | deflate | deflateRaw
|
||||||
VITE_COMPRESS_TYPE=gzip
|
VITE_COMPRESS_TYPE=gzip
|
||||||
|
|
||||||
|
VITE_PWA=N
|
||||||
|
|||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -14,3 +14,4 @@
|
|||||||
"*.scss" eol=lf
|
"*.scss" eol=lf
|
||||||
"*.sass" eol=lf
|
"*.sass" eol=lf
|
||||||
"*.styl" eol=lf
|
"*.styl" eol=lf
|
||||||
|
"*.md" eol=lf
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
dist.zip
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
stats.html
|
stats.html
|
||||||
@@ -21,6 +22,7 @@ stats.html
|
|||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
!.vscode/launch.json
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
.idea
|
.idea
|
||||||
*.suo
|
*.suo
|
||||||
@@ -30,6 +32,5 @@ stats.html
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
/src/typings/components.d.ts
|
/src/typings/components.d.ts
|
||||||
/src/typings/router-page.d.ts
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
pnpm soybean git-commit-verify
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
pnpm lint && pnpm typecheck
|
|
||||||
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Vue debugger",
|
||||||
|
"url": "http://localhost:3200",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.9.8](https://github.com/honghuangdc/soybean-admin/compare/v0.9.7...v0.9.8) (2023-01-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 新增 affix 属性,用于将其固定在tab卡 ([e772ff0](https://github.com/honghuangdc/soybean-admin/commit/e772ff05fb6ef513bd37cd9b1e245ea72f0ad6d2))
|
||||||
|
* **projects:** add compress script [添加压缩命令] ([be6d431](https://github.com/honghuangdc/soybean-admin/commit/be6d431485a5688bd1ed567e11c1ca35ab9259b0))
|
||||||
|
* **projects:** add generate logo script ([25daa23](https://github.com/honghuangdc/soybean-admin/commit/25daa236064c9a76677dbf16bc6d9717b1e0040f))
|
||||||
|
* **projects:** add new route plugin @soybeanjs/vite-plugin-vue-page-route [集成新的路由插件] ([3131e00](https://github.com/honghuangdc/soybean-admin/commit/3131e00f0f4a66756f547892a8d312cde3aaf868))
|
||||||
|
* **projects:** add script about generating png logo from [添加根据svg生成png图标的命令] ([70aeefe](https://github.com/honghuangdc/soybean-admin/commit/70aeefea02fcc13c152ee9d7a1197381bac724b9))
|
||||||
|
* setting 页面新增 是否显示footer的开关 ([d064f62](https://github.com/honghuangdc/soybean-admin/commit/d064f6285a6d1616d7606f1390d5f01819b258bb))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **components:** 修复路由在path中包含重复路单词径菜单时,被激活会错误展开 ([264da00](https://github.com/honghuangdc/soybean-admin/commit/264da00e5d2cd8139907c2ac11a046649d942f4b))
|
||||||
|
* count can't display when endValue is 0. ([0282feb](https://github.com/honghuangdc/soybean-admin/commit/0282feb1730724ba66c2e57ab354099afa074e81))
|
||||||
|
* **projects:** 修复动态路由模式下路由不排序的问题 ([58b27c9](https://github.com/honghuangdc/soybean-admin/commit/58b27c96932ba89b362138a6056a82c25a7be282))
|
||||||
|
* **projects:** 修复tabs在static路由模式下可以关闭首页 ([7211a17](https://github.com/honghuangdc/soybean-admin/commit/7211a17a8158b01a1f6dd6c83591f86d76633de0))
|
||||||
|
* **projects:** add router-page.d.ts to git [将router-page.d.ts添加git提交] ([7a58035](https://github.com/honghuangdc/soybean-admin/commit/7a5803551419f65ca55ba797b49273b3a0dc6067))
|
||||||
|
* **projects:** fix login success message [修复登录成功的消息提示] ([810398a](https://github.com/honghuangdc/soybean-admin/commit/810398abb882613f82ba385e8a7666cf8b86d92d))
|
||||||
|
* **projects:** fix router when the dynamic routes api was failed [修复当动态路由接口失败后路由异常问题] ([f2b580f](https://github.com/honghuangdc/soybean-admin/commit/f2b580fc067e81202238bf8079a13ff014b0b329))
|
||||||
|
* **projects:** fix vite-pwa plugin config ([94098d0](https://github.com/honghuangdc/soybean-admin/commit/94098d02e8cec12e3c9f9331ece154b65d1c9150))
|
||||||
|
* remove height limit h-360px ([b5c570a](https://github.com/honghuangdc/soybean-admin/commit/b5c570adf55fd235dcec49bc20837623b1d5d3c4))
|
||||||
|
* set password attributes ([a9a3703](https://github.com/honghuangdc/soybean-admin/commit/a9a37036d58274385a779e6460f7be281dafdcaf))
|
||||||
|
|
||||||
### [0.9.7](https://github.com/honghuangdc/soybean-admin/compare/v0.9.6...v0.9.7) (2022-11-07)
|
### [0.9.7](https://github.com/honghuangdc/soybean-admin/compare/v0.9.6...v0.9.7) (2022-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://i.loli.net/2021/11/24/x5lLfuSnEawBAgi.png"/>
|
<img src="./public/logo.png" style="width: 240px;"/>
|
||||||
<h1>Soybean Admin</h1>
|
<h1>Soybean Admin</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **最新技术栈**:使用 Vue3/Vite3 等前端前沿技术开发, 使用高效率的 npm 包管理器 pnpm
|
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发, 使用高效率的 npm 包管理器 pnpm
|
||||||
- **TypeScript**:应用程序级 JavaScript 的语言
|
- **TypeScript**: 应用程序级 JavaScript 的语言
|
||||||
- **主题**:丰富可配置的主题、暗黑模式,基于原子 css 框架 - UnoCSS 的动态主题颜色
|
- **主题**:丰富可配置的主题、暗黑模式,基于原子 css 框架 - UnoCss 的动态主题颜色
|
||||||
- **代码规范**:丰富的规范插件及极高的代码规范
|
- **代码规范**:丰富的规范插件及极高的代码规范
|
||||||
- **权限路由**:简易的路由配置、基于 mock 的动态路由能快速实现后端动态路由
|
- **权限路由**:基于文件的路由系统、基于 mock 的动态路由能快速实现后端动态路由
|
||||||
- **请求函数**:基于 axios 的完善的请求函数封装,提供 Promise 和 hooks 两种请求函数,加入请求结果数据转换的适配器
|
- **请求函数**:基于 axios 的完善的请求函数封装,提供 Promise 和 hooks 两种请求函数,加入请求结果数据转换的适配器适配器
|
||||||
|
|
||||||
## 预览
|
## 预览
|
||||||
|
|
||||||
@@ -29,7 +29,11 @@
|
|||||||
## 代码仓库
|
## 代码仓库
|
||||||
|
|
||||||
- [github](https://github.com/honghuangdc/soybean-admin)
|
- [github](https://github.com/honghuangdc/soybean-admin)
|
||||||
|
- [tauri 版](https://github.com/honghuangdc/soybean-admin/tree/tauri)
|
||||||
|
- [精简版](https://github.com/honghuangdc/soybean-admin/tree/thin)
|
||||||
- [gitee](https://gitee.com/honghuangdc/soybean-admin)
|
- [gitee](https://gitee.com/honghuangdc/soybean-admin)
|
||||||
|
- [tauri 版](https://gitee.com/honghuangdc/soybean-admin/tree/tauri)
|
||||||
|
- [精简版](https://gitee.com/honghuangdc/soybean-admin/tree/thin)
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
@@ -37,9 +41,8 @@
|
|||||||
|
|
||||||
## 后端服务
|
## 后端服务
|
||||||
|
|
||||||
- [soybean-admin-java(开发中)](https://github.com/honghuangdc/soybean-admin-java)
|
- [soybean-admin-java](https://github.com/honghuangdc/soybean-admin-java)
|
||||||
- [soybean-admin-go(开发中)](https://github.com/honghuangdc/soybean-admin-go)
|
- [soybean-admin-go](https://github.com/honghuangdc/soybean-admin-go)
|
||||||
- [soybean-admin-nestjs(开发中)](https://github.com/honghuangdc/soybean-admin-nestjs)
|
|
||||||
|
|
||||||
## 项目示例图
|
## 项目示例图
|
||||||
|
|
||||||
@@ -112,13 +115,9 @@ docker run --name soybean -p 80:80 -d soybeanjs/soybean-admin:v0.9.6
|
|||||||
|
|
||||||
## Git 贡献提交规范
|
## Git 贡献提交规范
|
||||||
|
|
||||||
项目已经内置 angular 提交规范,通过 git cz 代替 git commit 命令即可。
|
项目已经内置 angular 提交规范,直接执行 commit 命令即可。
|
||||||
|
|
||||||
git cz 命令需要全局安装 commitizen
|
项目已用 simple-git-hooks 代替了 husky, 旧版本用了 husky,执行 pnpm soy init-git-hooks 进行初始化配置
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm i -g commitizen
|
|
||||||
```
|
|
||||||
|
|
||||||
## 浏览器支持
|
## 浏览器支持
|
||||||
|
|
||||||
|
|||||||
@@ -2,31 +2,21 @@ import type { PluginOption } from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import unocss from '@unocss/vite';
|
import unocss from '@unocss/vite';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
|
||||||
import progress from 'vite-plugin-progress';
|
import progress from 'vite-plugin-progress';
|
||||||
|
import pageRoute from '@soybeanjs/vite-plugin-vue-page-route';
|
||||||
import html from './html';
|
import html from './html';
|
||||||
import unplugin from './unplugin';
|
import unplugin from './unplugin';
|
||||||
import mock from './mock';
|
import mock from './mock';
|
||||||
import visualizer from './visualizer';
|
import visualizer from './visualizer';
|
||||||
import compress from './compress';
|
import compress from './compress';
|
||||||
import soybeanjs from './soybeanjs';
|
import pwa from './pwa';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vite插件
|
* vite插件
|
||||||
* @param viteEnv - 环境变量配置
|
* @param viteEnv - 环境变量配置
|
||||||
*/
|
*/
|
||||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
||||||
const plugins = [
|
const plugins = [vue(), vueJsx(), html(viteEnv), ...unplugin(viteEnv), unocss(), mock, progress(), pageRoute()];
|
||||||
vue(),
|
|
||||||
vueJsx(),
|
|
||||||
VitePWA(),
|
|
||||||
html(viteEnv),
|
|
||||||
...unplugin(viteEnv),
|
|
||||||
unocss(),
|
|
||||||
mock,
|
|
||||||
progress(),
|
|
||||||
soybeanjs()
|
|
||||||
];
|
|
||||||
|
|
||||||
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||||
plugins.push(visualizer as PluginOption);
|
plugins.push(visualizer as PluginOption);
|
||||||
@@ -34,6 +24,9 @@ export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugin
|
|||||||
if (viteEnv.VITE_COMPRESS === 'Y') {
|
if (viteEnv.VITE_COMPRESS === 'Y') {
|
||||||
plugins.push(compress(viteEnv));
|
plugins.push(compress(viteEnv));
|
||||||
}
|
}
|
||||||
|
if (viteEnv.VITE_PWA === 'Y' || viteEnv.VITE_VERCEL === 'Y') {
|
||||||
|
plugins.push(pwa());
|
||||||
|
}
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|||||||
31
build/plugins/pwa.ts
Normal file
31
build/plugins/pwa.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
export default function setupVitePwa() {
|
||||||
|
return VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['favicon.ico'],
|
||||||
|
manifest: {
|
||||||
|
name: 'SoybeanAdmin',
|
||||||
|
short_name: 'SoybeanAdmin',
|
||||||
|
theme_color: '#fff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/logo.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import routerPage from '@soybeanjs/router-page';
|
|
||||||
|
|
||||||
export default function createSoybeanjsPlugin() {
|
|
||||||
return routerPage({
|
|
||||||
pagesFormatter: names => {
|
|
||||||
/** 系统的内置路由,该文件夹名称不作为RouteKey */
|
|
||||||
const SYSTEM_VIEW = 'system-view';
|
|
||||||
|
|
||||||
const result = names
|
|
||||||
.filter(name => name !== SYSTEM_VIEW)
|
|
||||||
.map(name => {
|
|
||||||
return name.replace(`${SYSTEM_VIEW}_`, '');
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ export default function unplugin(viteEnv: ImportMetaEnv) {
|
|||||||
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
|
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
VueMacros(),
|
VueMacros({}),
|
||||||
Icons({
|
Icons({
|
||||||
compiler: 'vue3',
|
compiler: 'vue3',
|
||||||
customCollections: {
|
customCollections: {
|
||||||
|
|||||||
17
index.html
17
index.html
@@ -2,26 +2,13 @@
|
|||||||
<html lang="zh-cmn-Hans">
|
<html lang="zh-cmn-Hans">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/logo.png" />
|
||||||
<link rel="stylesheet" href="/resource/loading.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title><%= appName %></title>
|
<title><%= appName %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="loading-container">
|
<div id="appLoading"></div>
|
||||||
<div id="loadingLogo" class="loading-svg"></div>
|
|
||||||
<div class="loading-spin__container">
|
|
||||||
<div class="loading-spin">
|
|
||||||
<div class="left-0 top-0 loading-spin-item"></div>
|
|
||||||
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
|
||||||
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
|
||||||
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="loading-title"><%= appTitle %></div>
|
|
||||||
</div>
|
|
||||||
<script src="/resource/loading.js"></script>
|
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
88
package.json
88
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "soybean-admin",
|
"name": "soybean-admin",
|
||||||
"version": "0.9.7",
|
"version": "0.9.8",
|
||||||
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
"description": "A fresh and elegant admin template, based on Vue3、Vite3、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite3、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Soybean",
|
"name": "Soybean",
|
||||||
@@ -45,13 +45,14 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
"typecheck": "vue-tsc --noEmit --skipLibCheck",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"commit": "soybean git-commit",
|
"commit": "soy git-commit",
|
||||||
"esno": "esno",
|
"cleanup": "soy cleanup",
|
||||||
"cleanup": "esno ./scripts/cleanup.ts",
|
"update-pkg": "soy update-pkg",
|
||||||
"update-pkg": "ncu --deep -u",
|
"tsx": "tsx",
|
||||||
|
"logo": "tsx ./scripts/logo.ts",
|
||||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"prepare": "husky install"
|
"prepare": "simple-git-hooks"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antv/data-set": "^0.11.8",
|
"@antv/data-set": "^0.11.8",
|
||||||
@@ -59,75 +60,80 @@
|
|||||||
"@better-scroll/core": "^2.5.0",
|
"@better-scroll/core": "^2.5.0",
|
||||||
"@soybeanjs/vue-admin-layout": "^1.1.1",
|
"@soybeanjs/vue-admin-layout": "^1.1.1",
|
||||||
"@soybeanjs/vue-admin-tab": "^1.0.5",
|
"@soybeanjs/vue-admin-tab": "^1.0.5",
|
||||||
"@vueuse/core": "^9.4.0",
|
"@vueuse/core": "^9.10.0",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.7",
|
||||||
"echarts": "^5.4.0",
|
"echarts": "^5.4.1",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"naive-ui": "2.33.5",
|
"naive-ui": "2.34.3",
|
||||||
"pinia": "^2.0.23",
|
"pinia": "^2.0.28",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"swiper": "^8.4.4",
|
"swiper": "^8.4.5",
|
||||||
"ua-parser-js": "^1.0.32",
|
"ua-parser-js": "^1.0.32",
|
||||||
"vditor": "^3.8.18",
|
"vditor": "^3.9.0",
|
||||||
"vue": "3.2.41",
|
"vue": "3.2.45",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"wangeditor": "^4.7.15",
|
"wangeditor": "^4.7.15",
|
||||||
"xgplayer": "^2.32.1"
|
"xgplayer": "^2.32.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@amap/amap-jsapi-types": "^0.0.10",
|
"@amap/amap-jsapi-types": "^0.0.10",
|
||||||
"@iconify/json": "^2.1.133",
|
"@iconify/json": "^2.2.7",
|
||||||
"@iconify/vue": "^4.0.0",
|
"@iconify/vue": "^4.0.2",
|
||||||
"@soybeanjs/cli": "^0.1.2",
|
"@soybeanjs/cli": "^0.1.6",
|
||||||
"@soybeanjs/router-page": "1.0.3",
|
"@soybeanjs/vite-plugin-vue-page-route": "^0.0.5",
|
||||||
"@tauri-apps/cli": "^1.1.1",
|
|
||||||
"@types/bmapgl": "^0.0.5",
|
"@types/bmapgl": "^0.0.5",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/node": "18.8.3",
|
"@types/node": "18.11.18",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@unocss/preset-uno": "^0.46.3",
|
"@unocss/preset-uno": "^0.48.4",
|
||||||
"@unocss/vite": "^0.46.3",
|
"@unocss/vite": "^0.48.4",
|
||||||
"@vitejs/plugin-vue": "^3.2.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^2.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
"conventional-changelog": "^3.1.25",
|
"conventional-changelog": "^3.1.25",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.27.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-config-soybeanjs-vue": "^0.1.2",
|
"eslint-config-soybeanjs-vue": "^0.2.2",
|
||||||
"esno": "^0.16.3",
|
"lint-staged": "12.5.0",
|
||||||
"husky": "^8.0.1",
|
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"npm-check-updates": "^16.3.16",
|
"node-html-to-image": "^3.2.4",
|
||||||
"rimraf": "^3.0.2",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"rollup-plugin-visualizer": "^5.8.3",
|
"sass": "^1.57.1",
|
||||||
"sass": "^1.56.0",
|
"simple-git-hooks": "^2.8.1",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"typescript": "4.8.4",
|
"tsx": "^3.12.2",
|
||||||
"unplugin-icons": "^0.14.13",
|
"typescript": "4.9.4",
|
||||||
"unplugin-vue-components": "0.22.8",
|
"unplugin-icons": "^0.15.1",
|
||||||
"unplugin-vue-macros": "^0.16.0",
|
"unplugin-vue-components": "0.22.12",
|
||||||
|
"unplugin-vue-macros": "^1.3.3",
|
||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.10.0",
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.0.4",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
"vite-plugin-progress": "^0.0.6",
|
"vite-plugin-progress": "^0.0.6",
|
||||||
"vite-plugin-pwa": "^0.13.2",
|
"vite-plugin-pwa": "^0.14.1",
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vue-tsc": "^1.0.9",
|
"vue-tsc": "^1.0.24"
|
||||||
"zx": "^7.1.1"
|
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
"mockjs@1.1.0": "patches/mockjs@1.1.0.patch"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"commit-msg": "pnpm soy git-commit-verify",
|
||||||
|
"pre-commit": "pnpm typecheck && pnpm lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint . --fix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4269
pnpm-lock.yaml
generated
4269
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,91 +0,0 @@
|
|||||||
.loading-container {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-svg {
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: #646464;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* 初始化加载效果的svg格式logo
|
|
||||||
* @param {string} id - 元素id
|
|
||||||
*/
|
|
||||||
function initSvgLogo(id) {
|
|
||||||
const svgStr = `<svg width="128px" height="128px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
|
||||||
y="0px" viewBox="0 0 158.9 158.9" style="enable-background:new 0 0 158.9 158.9;" xml:space="preserve">
|
|
||||||
<path style="fill:none" d="M0,158.9C0,106.3,0,53.7,0,1.1C0,0.2,0.2,0,1.1,0c52.2,0,104.5,0,156.7,0c0.9,0,1.1,0.2,1.1,1.1
|
|
||||||
c0,52.2,0,104.5,0,156.7c0,0.9-0.2,1.1-1.1,1.1C105.2,158.8,52.6,158.8,0,158.9z" />
|
|
||||||
<path style="fill:currentColor" d="M81.3,55.9c-0.1-11.7-2.9-22.5-9.4-32.4c-1-1.5-2.1-2.9-2.5-4.7c-0.7-3.4,0.9-6.9,4-8.6c3-1.7,6.8-1.2,9.3,1.2
|
|
||||||
c2.4,2.6,4.4,5.6,5.9,8.8c4.7,8.9,7.6,18.6,8.4,28.6c1,12.5-0.7,25-5.2,36.7c-0.9,2.5-1.9,4.9-3,7.3c-0.3,0.4-0.3,1,0,1.4
|
|
||||||
c9.6,13.3,21.8,23,37.8,27.2c6.4,1.7,13.1,2.3,19.7,1.6c4.2-0.4,7.9,2.7,8.4,6.9c0.7,4.3-2.3,8.3-6.6,9c0,0,0,0-0.1,0
|
|
||||||
c-7.7,0.9-15.5,0.5-23-1.3c-13.9-3.1-26.7-10-36.9-19.9c-4.4-4.2-8.4-8.8-11.9-13.7c-0.5-0.8-1.4-1.2-2.3-1.1
|
|
||||||
c-9.5,0.7-18.8,3.3-27.4,7.6c-11.6,6-20.7,14.6-26.4,26.4c-0.7,1.9-2,3.5-3.7,4.7c-2.9,1.7-6.6,1.5-9.2-0.7c-2.8-2.2-3.8-6-2.4-9.3
|
|
||||||
c2.2-5.2,5.1-10.1,8.7-14.5c12.2-15.4,28.2-24.6,47.3-28.6c4-0.8,8.1-1.4,12.2-1.6c0.5,0,1-0.3,1.2-0.8c3.3-7.1,5.5-14.6,6.5-22.3
|
|
||||||
C81.1,61.2,81.3,58.6,81.3,55.9z" />
|
|
||||||
<path style="fill:currentColor" d="M136.3,108.3c-3.8-0.5-7.6-1.4-11.1-2.9c-7.7-2.8-14.4-7.5-19.7-13.8c-2.9-3.3-2.5-8.4,0.8-11.3
|
|
||||||
c1.4-1.2,3.1-1.9,4.9-1.9c2.5-0.1,5,1,6.5,2.9c4.9,5.6,11.6,9.4,18.9,10.8c1.5,0.2,3.1,0.6,4.5,1.2c3.2,1.8,4.8,5.6,3.8,9.2
|
|
||||||
C144,106.1,140.8,108.4,136.3,108.3z" />
|
|
||||||
<path style="fill:currentColor" d="M55.7,33.3c3,0.2,5.6,2.2,6.6,5c2.2,5.4,3.4,11.2,3.6,17c0.3,5.9-0.6,11.7-2.5,17.3c-2,5.8-8.2,7.8-12.9,4.2
|
|
||||||
c-2.6-2.2-3.6-5.8-2.4-9c1.4-4,1.9-8.2,1.7-12.4c-0.2-3.8-1-7.5-2.4-11C45.3,38.9,49.2,33.3,55.7,33.3z" />
|
|
||||||
<path style="fill:currentColor" d="M77.9,126.6c0,3.9-2.8,7.2-6.7,7.9c-7.8,1.5-14.8,5.9-19.7,12.2c-2.7,3.5-7.6,4.2-11.2,1.6
|
|
||||||
c-3.6-2.6-4.3-7.6-1.7-11.2c0.1-0.1,0.2-0.3,0.3-0.4c4.1-5.2,9.3-9.6,15.1-12.8c4.4-2.5,9.1-4.2,14-5.1
|
|
||||||
C73.3,117.7,77.9,121.3,77.9,126.6z" />
|
|
||||||
</svg>`;
|
|
||||||
const appEl = document.querySelector(id);
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = svgStr;
|
|
||||||
if (appEl) {
|
|
||||||
appEl.appendChild(div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addThemeColorCssVars() {
|
|
||||||
const key = '__THEME_COLOR__';
|
|
||||||
const defaultColor = '#1890ff';
|
|
||||||
const themeColor = window.localStorage.getItem(key) || defaultColor;
|
|
||||||
const cssVars = `--primary-color: ${themeColor}`;
|
|
||||||
document.documentElement.style.cssText = cssVars;
|
|
||||||
}
|
|
||||||
|
|
||||||
addThemeColorCssVars();
|
|
||||||
|
|
||||||
initSvgLogo('#loadingLogo');
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env zx
|
|
||||||
import { $ } from 'zx';
|
|
||||||
|
|
||||||
$`pnpm rimraf node_modules dist pnpm-lock.yaml`;
|
|
||||||
17
scripts/logo.ts
Normal file
17
scripts/logo.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import nodeHtmlToImage from 'node-html-to-image';
|
||||||
|
import themeSettings from '../src/settings/theme.json';
|
||||||
|
|
||||||
|
async function generatePngLogoFromSvg(svgPath: string, color: string) {
|
||||||
|
const svgStr = await readFile(svgPath, 'utf-8');
|
||||||
|
|
||||||
|
const svgStrWithColor = svgStr.replace(/currentColor/g, color);
|
||||||
|
|
||||||
|
await nodeHtmlToImage({
|
||||||
|
output: './public/logo.png',
|
||||||
|
html: svgStrWithColor,
|
||||||
|
transparent: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePngLogoFromSvg('./src/assets/svg-icon/logo.svg', themeSettings.themeColor);
|
||||||
41
src/components/common/AppLoading.vue
Normal file
41
src/components/common/AppLoading.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed-center flex-col">
|
||||||
|
<system-logo class="text-128px text-primary" />
|
||||||
|
<div class="w-56px h-56px my-36px">
|
||||||
|
<div class="relative h-full animate-spin">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in lodingClasses"
|
||||||
|
:key="index"
|
||||||
|
class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse"
|
||||||
|
:class="item"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-28px font-500 text-#646464">{{ title }}</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppInfo } from '@/composables';
|
||||||
|
import { localStg } from '@/utils';
|
||||||
|
|
||||||
|
const { title } = useAppInfo();
|
||||||
|
|
||||||
|
const lodingClasses = [
|
||||||
|
'left-0 top-0',
|
||||||
|
'left-0 bottom-0 animate-delay-500',
|
||||||
|
'right-0 top-0 animate-delay-1000',
|
||||||
|
'right-0 bottom-0 animate-delay-1500'
|
||||||
|
];
|
||||||
|
|
||||||
|
function addThemeColorCssVars() {
|
||||||
|
const defaultColor = '#1890ff';
|
||||||
|
const themeColor = localStg.get('themeColor') || defaultColor;
|
||||||
|
const cssVars = `--primary-color: ${themeColor}`;
|
||||||
|
document.documentElement.style.cssText = cssVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
addThemeColorCssVars();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -77,7 +77,7 @@ function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(num: number | string) {
|
function formatNumber(num: number | string) {
|
||||||
if (!num) {
|
if (num !== 0 && !num) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Icon :icon="icon" v-bind="bindAttrs" />
|
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nextTick, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, effectScope, onScopeDispose, ref, watch } from 'vue';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
|
||||||
@@ -98,6 +98,7 @@ export function useEcharts(
|
|||||||
|
|
||||||
function update(updateOptions: ECOption) {
|
function update(updateOptions: ECOption) {
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
|
chart?.clear();
|
||||||
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
|
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,38 +128,44 @@ export function useEcharts(
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopSizeWatch = watch([width, height], ([newWidth, newHeight]) => {
|
const scope = effectScope();
|
||||||
initialSize.width = newWidth;
|
|
||||||
initialSize.height = newHeight;
|
scope.run(() => {
|
||||||
if (newWidth === 0 && newHeight === 0) {
|
watch([width, height], ([newWidth, newHeight]) => {
|
||||||
// 节点被删除 将chart置为空
|
initialSize.width = newWidth;
|
||||||
chart = null;
|
initialSize.height = newHeight;
|
||||||
}
|
if (newWidth === 0 && newHeight === 0) {
|
||||||
if (canRender()) {
|
// 节点被删除 将chart置为空
|
||||||
if (!isRendered()) {
|
chart = null;
|
||||||
render();
|
|
||||||
} else {
|
|
||||||
resize();
|
|
||||||
}
|
}
|
||||||
}
|
if (canRender()) {
|
||||||
|
if (!isRendered()) {
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
options,
|
||||||
|
newValue => {
|
||||||
|
update(newValue);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => theme.darkMode,
|
||||||
|
() => {
|
||||||
|
updateTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const stopOptionWatch = watch(options, newValue => {
|
onScopeDispose(() => {
|
||||||
update(newValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
const stopDarkModeWatch = watch(
|
|
||||||
() => theme.darkMode,
|
|
||||||
() => {
|
|
||||||
updateTheme();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
destroy();
|
destroy();
|
||||||
stopSizeWatch();
|
scope.stop();
|
||||||
stopOptionWatch();
|
|
||||||
stopDarkModeWatch();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,22 +5,6 @@ export enum EnumContentType {
|
|||||||
formData = 'multipart/form-data'
|
formData = 'multipart/form-data'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 缓存的key */
|
|
||||||
export enum EnumStorageKey {
|
|
||||||
/** 主题颜色 */
|
|
||||||
'theme-color' = '__THEME_COLOR__',
|
|
||||||
/** 用户token */
|
|
||||||
'token' = '__TOKEN__',
|
|
||||||
/** 用户刷新token */
|
|
||||||
'refresh-token' = '__REFRESH_TOKEN__',
|
|
||||||
/** 用户信息 */
|
|
||||||
'user-info' = '__USER_INFO__',
|
|
||||||
/** 主题配置 */
|
|
||||||
'theme-settings' = '__THEME_SETTINGS__',
|
|
||||||
/** 多页签路由信息 */
|
|
||||||
'multi-tab-routes' = '__MULTI_TAB_ROUTES__'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 数据类型 */
|
/** 数据类型 */
|
||||||
export enum EnumDataType {
|
export enum EnumDataType {
|
||||||
number = '[object Number]',
|
number = '[object Number]',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, onScopeDispose, ref } from 'vue';
|
||||||
import { useBoolean } from '../common';
|
import { useBoolean } from '../common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,6 +42,8 @@ export default function useCountDown(second: number) {
|
|||||||
counts.value = 0;
|
counts.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onScopeDispose(stop);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
counts,
|
counts,
|
||||||
isCounting,
|
isCounting,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
:sider-width="siderWidth"
|
:sider-width="siderWidth"
|
||||||
:sider-collapsed-width="siderCollapsedWidth"
|
:sider-collapsed-width="siderCollapsedWidth"
|
||||||
:sider-collapse="app.siderCollapse"
|
:sider-collapse="app.siderCollapse"
|
||||||
:add-main-overflow-hidden="addMainOverflowHidden"
|
|
||||||
:fixed-footer="theme.footer.fixed"
|
:fixed-footer="theme.footer.fixed"
|
||||||
|
:footer-visible="theme.footer.visible"
|
||||||
@update:sider-collapse="app.setSiderCollapse"
|
@update:sider-collapse="app.setSiderCollapse"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<template #sider>
|
<template #sider>
|
||||||
<global-sider />
|
<global-sider />
|
||||||
</template>
|
</template>
|
||||||
<global-content @hide-main-overflow="setAddMainOverflowHidden" />
|
<global-content />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<global-footer />
|
<global-footer />
|
||||||
</template>
|
</template>
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
import AdminLayout from '@soybeanjs/vue-admin-layout';
|
import AdminLayout from '@soybeanjs/vue-admin-layout';
|
||||||
import { useAppStore, useThemeStore } from '@/store';
|
import { useAppStore, useThemeStore } from '@/store';
|
||||||
import { useBasicLayout } from '@/composables';
|
import { useBasicLayout } from '@/composables';
|
||||||
import { useBoolean } from '@/hooks';
|
|
||||||
import {
|
import {
|
||||||
GlobalBackTop,
|
GlobalBackTop,
|
||||||
GlobalContent,
|
GlobalContent,
|
||||||
@@ -53,8 +52,6 @@ const app = useAppStore();
|
|||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
|
|
||||||
const { mode, isMobile, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout();
|
const { mode, isMobile, headerProps, siderVisible, siderWidth, siderCollapsedWidth } = useBasicLayout();
|
||||||
|
|
||||||
const { bool: addMainOverflowHidden, setBool: setAddMainOverflowHidden } = useBoolean();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -4,13 +4,7 @@
|
|||||||
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
|
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition
|
<transition :name="theme.pageAnimateMode" mode="out-in" :appear="true">
|
||||||
:name="theme.pageAnimateMode"
|
|
||||||
mode="out-in"
|
|
||||||
:appear="true"
|
|
||||||
@before-leave="handleBeforeLeave"
|
|
||||||
@after-enter="handleAfterEnter"
|
|
||||||
>
|
|
||||||
<keep-alive :include="routeStore.cacheRoutes">
|
<keep-alive :include="routeStore.cacheRoutes">
|
||||||
<component :is="Component" v-if="app.reloadFlag" :key="route.fullPath" />
|
<component :is="Component" v-if="app.reloadFlag" :key="route.fullPath" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
@@ -33,23 +27,9 @@ withDefaults(defineProps<Props>(), {
|
|||||||
showPadding: true
|
showPadding: true
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
/** 禁止主体溢出 */
|
|
||||||
(e: 'hide-main-overflow', hidden: boolean): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
|
|
||||||
function handleBeforeLeave() {
|
|
||||||
emit('hide-main-overflow', true);
|
|
||||||
}
|
|
||||||
function handleAfterEnter() {
|
|
||||||
emit('hide-main-overflow', false);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ interface Props {
|
|||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
/** 当前路由路径 */
|
/** 当前路由路径 */
|
||||||
currentPath?: string;
|
currentPath?: string;
|
||||||
|
/** 是否固定在tab卡不可关闭 */
|
||||||
|
affix?: boolean;
|
||||||
/** 鼠标x坐标 */
|
/** 鼠标x坐标 */
|
||||||
x: number;
|
x: number;
|
||||||
/** 鼠标y坐标 */
|
/** 鼠标y坐标 */
|
||||||
@@ -72,7 +74,7 @@ const options = computed<Option[]>(() => [
|
|||||||
{
|
{
|
||||||
label: '关闭',
|
label: '关闭',
|
||||||
key: 'close-current',
|
key: 'close-current',
|
||||||
disabled: props.currentPath === tab.homeTab.fullPath,
|
disabled: props.currentPath === tab.homeTab.fullPath || Boolean(props.affix),
|
||||||
icon: iconRender({ icon: 'ant-design:close-outlined' })
|
icon: iconRender({ icon: 'ant-design:close-outlined' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
:key="item.fullPath"
|
:key="item.fullPath"
|
||||||
:is-active="tab.activeTab === item.fullPath"
|
:is-active="tab.activeTab === item.fullPath"
|
||||||
:primary-color="theme.themeColor"
|
:primary-color="theme.themeColor"
|
||||||
:closable="item.name !== tab.homeTab.name"
|
:closable="!(item.name === tab.homeTab.name || item.meta.affix)"
|
||||||
:dark-mode="theme.darkMode"
|
:dark-mode="theme.darkMode"
|
||||||
:class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }"
|
:class="{ '!mr-0': isChromeMode && index === tab.tabs.length - 1, 'mr-10px': !isChromeMode }"
|
||||||
@click="tab.handleClickTab(item.fullPath)"
|
@click="tab.handleClickTab(item.fullPath)"
|
||||||
@close="tab.removeTab(item.fullPath)"
|
@close="tab.removeTab(item.fullPath)"
|
||||||
@contextmenu="handleContextMenu($event, item.fullPath)"
|
@contextmenu="handleContextMenu($event, item.fullPath, item.meta.affix)"
|
||||||
>
|
>
|
||||||
<svg-icon
|
<svg-icon
|
||||||
:icon="item.meta.icon"
|
:icon="item.meta.icon"
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<context-menu
|
<context-menu
|
||||||
:visible="dropdown.visible"
|
:visible="dropdown.visible"
|
||||||
:current-path="dropdown.currentPath"
|
:current-path="dropdown.currentPath"
|
||||||
|
:affix="dropdown.affix"
|
||||||
:x="dropdown.x"
|
:x="dropdown.x"
|
||||||
:y="dropdown.y"
|
:y="dropdown.y"
|
||||||
@update:visible="handleDropdownVisible"
|
@update:visible="handleDropdownVisible"
|
||||||
@@ -64,32 +65,36 @@ async function getActiveTabClientX() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdown = reactive({
|
interface DropdownConfig {
|
||||||
|
visible: boolean;
|
||||||
|
affix: boolean;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
currentPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdown: DropdownConfig = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
|
affix: false,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
currentPath: ''
|
currentPath: ''
|
||||||
});
|
});
|
||||||
function showDropdown() {
|
|
||||||
dropdown.visible = true;
|
function setDropdown(config: Partial<DropdownConfig>) {
|
||||||
}
|
Object.assign(dropdown, config);
|
||||||
function hideDropdown() {
|
|
||||||
dropdown.visible = false;
|
|
||||||
}
|
|
||||||
function setDropdown(x: number, y: number, currentPath: string) {
|
|
||||||
Object.assign(dropdown, { x, y, currentPath });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let isClickContextMenu = false;
|
let isClickContextMenu = false;
|
||||||
|
|
||||||
function handleDropdownVisible(visible: boolean) {
|
function handleDropdownVisible(visible: boolean) {
|
||||||
if (!isClickContextMenu) {
|
if (!isClickContextMenu) {
|
||||||
dropdown.visible = visible;
|
setDropdown({ visible });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 点击右键菜单 */
|
/** 点击右键菜单 */
|
||||||
async function handleContextMenu(e: MouseEvent, fullPath: string) {
|
async function handleContextMenu(e: MouseEvent, currentPath: string, affix?: boolean) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
@@ -98,11 +103,16 @@ async function handleContextMenu(e: MouseEvent, fullPath: string) {
|
|||||||
|
|
||||||
const DURATION = dropdown.visible ? 150 : 0;
|
const DURATION = dropdown.visible ? 150 : 0;
|
||||||
|
|
||||||
hideDropdown();
|
setDropdown({ visible: false });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDropdown(clientX, clientY, fullPath);
|
setDropdown({
|
||||||
showDropdown();
|
visible: true,
|
||||||
|
x: clientX,
|
||||||
|
y: clientY,
|
||||||
|
currentPath,
|
||||||
|
affix
|
||||||
|
});
|
||||||
isClickContextMenu = false;
|
isClickContextMenu = false;
|
||||||
}, DURATION);
|
}, DURATION);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
<setting-menu label="固定底部">
|
<setting-menu label="固定底部">
|
||||||
<n-switch :value="theme.footer.fixed" @update:value="theme.setFooterIsFixed" />
|
<n-switch :value="theme.footer.fixed" @update:value="theme.setFooterIsFixed" />
|
||||||
</setting-menu>
|
</setting-menu>
|
||||||
|
<setting-menu label="显示底部">
|
||||||
|
<n-switch :value="theme.footer.visible" @update:value="theme.setFooterVisible" />
|
||||||
|
</setting-menu>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
import AppLoading from './components/common/AppLoading.vue';
|
||||||
import { setupDirectives } from './directives';
|
import { setupDirectives } from './directives';
|
||||||
import { setupRouter } from './router';
|
import { setupRouter } from './router';
|
||||||
import { setupAssets } from './plugins';
|
import { setupAssets } from './plugins';
|
||||||
@@ -10,6 +11,11 @@ async function setupApp() {
|
|||||||
// import assets: js、css
|
// import assets: js、css
|
||||||
setupAssets();
|
setupAssets();
|
||||||
|
|
||||||
|
// app loading
|
||||||
|
const appLoading = createApp(AppLoading);
|
||||||
|
|
||||||
|
appLoading.mount('#appLoading');
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// store plugin: pinia
|
// store plugin: pinia
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
||||||
import { routeName } from '@/router';
|
import { routeName } from '@/router';
|
||||||
import { useRouteStore } from '@/store';
|
import { useRouteStore } from '@/store';
|
||||||
import { getToken } from '@/utils';
|
import { localStg } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态路由
|
* 动态路由
|
||||||
@@ -12,7 +12,7 @@ export async function createDynamicRouteGuard(
|
|||||||
next: NavigationGuardNext
|
next: NavigationGuardNext
|
||||||
) {
|
) {
|
||||||
const route = useRouteStore();
|
const route = useRouteStore();
|
||||||
const isLogin = Boolean(getToken());
|
const isLogin = Boolean(localStg.get('token'));
|
||||||
|
|
||||||
// 初始化权限路由
|
// 初始化权限路由
|
||||||
if (!route.isInitAuthRoute) {
|
if (!route.isInitAuthRoute) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
||||||
import { routeName } from '@/router';
|
import { routeName } from '@/router';
|
||||||
import { useAuthStore } from '@/store';
|
import { useAuthStore } from '@/store';
|
||||||
import { exeStrategyActions, getToken } from '@/utils';
|
import { exeStrategyActions, localStg } from '@/utils';
|
||||||
import { createDynamicRouteGuard } from './dynamic';
|
import { createDynamicRouteGuard } from './dynamic';
|
||||||
|
|
||||||
/** 处理路由页面的权限 */
|
/** 处理路由页面的权限 */
|
||||||
@@ -22,7 +22,7 @@ export async function createPermissionGuard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const isLogin = Boolean(getToken());
|
const isLogin = Boolean(localStg.get('token'));
|
||||||
const permissions = to.meta.permissions || [];
|
const permissions = to.meta.permissions || [];
|
||||||
const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length);
|
const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length);
|
||||||
const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole);
|
const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const about: AuthRoute.Route = {
|
const about1: AuthRoute.Route = {
|
||||||
name: 'about',
|
name: 'about',
|
||||||
path: '/about',
|
path: '/about',
|
||||||
component: 'self',
|
component: 'self',
|
||||||
@@ -12,4 +12,4 @@ const about: AuthRoute.Route = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default about;
|
export default about1;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import { useAuthStore } from '@/store';
|
import { useAuthStore } from '@/store';
|
||||||
import { getRefreshToken, setRefreshToken, setToken } from '@/utils';
|
import { localStg } from '@/utils';
|
||||||
import { fetchUpdateToken } from '../api';
|
import { fetchUpdateToken } from '../api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,11 +9,12 @@ import { fetchUpdateToken } from '../api';
|
|||||||
*/
|
*/
|
||||||
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
|
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
|
||||||
const { resetAuthStore } = useAuthStore();
|
const { resetAuthStore } = useAuthStore();
|
||||||
const refreshToken = getRefreshToken();
|
const refreshToken = localStg.get('refreshToken') || '';
|
||||||
const { data } = await fetchUpdateToken(refreshToken);
|
const { data } = await fetchUpdateToken(refreshToken);
|
||||||
if (data) {
|
if (data) {
|
||||||
setToken(data.token);
|
localStg.set('token', data.token);
|
||||||
setRefreshToken(data.refreshToken);
|
localStg.set('refreshToken', data.refreshToken);
|
||||||
|
|
||||||
const config = { ...axiosConfig };
|
const config = { ...axiosConfig };
|
||||||
if (config.headers) {
|
if (config.headers) {
|
||||||
config.headers.Authorization = data.token;
|
config.headers.Authorization = data.token;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
|||||||
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';
|
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
import { REFRESH_TOKEN_CODE } from '@/config';
|
import { REFRESH_TOKEN_CODE } from '@/config';
|
||||||
import {
|
import {
|
||||||
getToken,
|
localStg,
|
||||||
handleAxiosError,
|
handleAxiosError,
|
||||||
handleBackendError,
|
handleBackendError,
|
||||||
handleResponseError,
|
handleResponseError,
|
||||||
@@ -49,7 +49,7 @@ export default class CustomAxiosInstance {
|
|||||||
const contentType = handleConfig.headers['Content-Type'] as string;
|
const contentType = handleConfig.headers['Content-Type'] as string;
|
||||||
handleConfig.data = await transformRequestData(handleConfig.data, contentType);
|
handleConfig.data = await transformRequestData(handleConfig.data, contentType);
|
||||||
// 设置token
|
// 设置token
|
||||||
handleConfig.headers.Authorization = getToken();
|
handleConfig.headers.Authorization = localStg.get('token') || '';
|
||||||
}
|
}
|
||||||
return handleConfig;
|
return handleConfig;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -110,7 +110,8 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"fixed": false,
|
"fixed": false,
|
||||||
"height": 48
|
"height": 48,
|
||||||
|
"visible": true
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"animate": true,
|
"animate": true,
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ const defaultThemeSetting: Theme.Setting = {
|
|||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
fixed: false,
|
fixed: false,
|
||||||
height: 48
|
height: 48,
|
||||||
|
visible: true
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
animate: true,
|
animate: true,
|
||||||
|
|||||||
25
src/store/modules/auth/helpers.ts
Normal file
25
src/store/modules/auth/helpers.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { localStg } from '@/utils';
|
||||||
|
|
||||||
|
/** 获取token */
|
||||||
|
export function getToken() {
|
||||||
|
return localStg.get('token') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户信息 */
|
||||||
|
export function getUserInfo() {
|
||||||
|
const emptyInfo: Auth.UserInfo = {
|
||||||
|
userId: '',
|
||||||
|
userName: '',
|
||||||
|
userRole: 'user'
|
||||||
|
};
|
||||||
|
const userInfo: Auth.UserInfo = localStg.get('userInfo') || emptyInfo;
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 去除用户相关缓存 */
|
||||||
|
export function clearAuthStorage() {
|
||||||
|
localStg.remove('token');
|
||||||
|
localStg.remove('refreshToken');
|
||||||
|
localStg.remove('userInfo');
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { unref } from 'vue';
|
import { unref, nextTick } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { router } from '@/router';
|
import { router } from '@/router';
|
||||||
import { fetchLogin, fetchUserInfo } from '@/service';
|
import { fetchLogin, fetchUserInfo } from '@/service';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
import { clearAuthStorage, getToken, getUserInfo, setRefreshToken, setToken, setUserInfo } from '@/utils';
|
import { localStg } from '@/utils';
|
||||||
import { useTabStore } from '../tab';
|
import { useTabStore } from '../tab';
|
||||||
import { useRouteStore } from '../route';
|
import { useRouteStore } from '../route';
|
||||||
|
import { getToken, getUserInfo, clearAuthStorage } from './helpers';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
@@ -39,32 +40,39 @@ export const useAuthStore = defineStore('auth-store', {
|
|||||||
clearAuthStorage();
|
clearAuthStorage();
|
||||||
this.$reset();
|
this.$reset();
|
||||||
|
|
||||||
resetTabStore();
|
|
||||||
resetRouteStore();
|
|
||||||
|
|
||||||
if (route.meta.requiresAuth) {
|
if (route.meta.requiresAuth) {
|
||||||
toLogin();
|
toLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
resetTabStore();
|
||||||
|
resetRouteStore();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 处理登录后成功或失败的逻辑
|
* 处理登录后成功或失败的逻辑
|
||||||
* @param backendToken - 返回的token
|
* @param backendToken - 返回的token
|
||||||
*/
|
*/
|
||||||
async handleActionAfterLogin(backendToken: ApiAuth.Token) {
|
async handleActionAfterLogin(backendToken: ApiAuth.Token) {
|
||||||
|
const route = useRouteStore();
|
||||||
const { toLoginRedirect } = useRouterPush(false);
|
const { toLoginRedirect } = useRouterPush(false);
|
||||||
|
|
||||||
const loginSuccess = await this.loginByToken(backendToken);
|
const loginSuccess = await this.loginByToken(backendToken);
|
||||||
|
|
||||||
if (loginSuccess) {
|
if (loginSuccess) {
|
||||||
|
await route.initAuthRoute();
|
||||||
|
|
||||||
// 跳转登录后的地址
|
// 跳转登录后的地址
|
||||||
toLoginRedirect();
|
toLoginRedirect();
|
||||||
|
|
||||||
// 登录成功弹出欢迎提示
|
// 登录成功弹出欢迎提示
|
||||||
window.$notification?.success({
|
if (route.isInitAuthRoute) {
|
||||||
title: '登录成功!',
|
window.$notification?.success({
|
||||||
content: `欢迎回来,${this.userInfo.userName}!`,
|
title: '登录成功!',
|
||||||
duration: 3000
|
content: `欢迎回来,${this.userInfo.userName}!`,
|
||||||
});
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,14 +89,14 @@ export const useAuthStore = defineStore('auth-store', {
|
|||||||
|
|
||||||
// 先把token存储到缓存中(后面接口的请求头需要token)
|
// 先把token存储到缓存中(后面接口的请求头需要token)
|
||||||
const { token, refreshToken } = backendToken;
|
const { token, refreshToken } = backendToken;
|
||||||
setToken(token);
|
localStg.set('token', token);
|
||||||
setRefreshToken(refreshToken);
|
localStg.set('refreshToken', refreshToken);
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const { data } = await fetchUserInfo();
|
const { data } = await fetchUserInfo();
|
||||||
if (data) {
|
if (data) {
|
||||||
// 成功后把用户信息存储到缓存中
|
// 成功后把用户信息存储到缓存中
|
||||||
setUserInfo(data);
|
localStg.set('userInfo', data);
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
this.userInfo = data;
|
this.userInfo = data;
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { defineStore } from 'pinia';
|
|||||||
import { ROOT_ROUTE, constantRoutes, router, routes as staticRoutes } from '@/router';
|
import { ROOT_ROUTE, constantRoutes, router, routes as staticRoutes } from '@/router';
|
||||||
import { fetchUserRoutes } from '@/service';
|
import { fetchUserRoutes } from '@/service';
|
||||||
import {
|
import {
|
||||||
|
localStg,
|
||||||
filterAuthRoutesByUserPermission,
|
filterAuthRoutesByUserPermission,
|
||||||
getCacheRoutes,
|
getCacheRoutes,
|
||||||
getConstantRouteNames,
|
getConstantRouteNames,
|
||||||
getUserInfo,
|
transformAuthRouteToVueRoutes,
|
||||||
|
transformAuthRouteToVueRoute,
|
||||||
transformAuthRouteToMenu,
|
transformAuthRouteToMenu,
|
||||||
transformAuthRouteToSearchMenus,
|
transformAuthRouteToSearchMenus,
|
||||||
transformRouteNameToRoutePath,
|
transformRouteNameToRoutePath,
|
||||||
transformRoutePathToRouteName
|
transformRoutePathToRouteName,
|
||||||
|
sortRoutes
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { transformAuthRouteToVueRoutes, transformAuthRouteToVueRoute } from '@/utils/router/transform';
|
|
||||||
import { useAuthStore } from '../auth';
|
import { useAuthStore } from '../auth';
|
||||||
import { useTabStore } from '../tab';
|
import { useTabStore } from '../tab';
|
||||||
|
|
||||||
@@ -105,37 +107,45 @@ export const useRouteStore = defineStore('route-store', {
|
|||||||
},
|
},
|
||||||
/** 初始化动态路由 */
|
/** 初始化动态路由 */
|
||||||
async initDynamicRoute() {
|
async initDynamicRoute() {
|
||||||
const { userId } = getUserInfo();
|
const { initHomeTab } = useTabStore();
|
||||||
const { data } = await fetchUserRoutes(userId);
|
|
||||||
if (data) {
|
const { userId } = localStg.get('userInfo') || {};
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('userId 不能为空!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error, data } = await fetchUserRoutes(userId);
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
this.routeHomeName = data.home;
|
this.routeHomeName = data.home;
|
||||||
this.handleUpdateRootRedirect(data.home);
|
this.handleUpdateRootRedirect(data.home);
|
||||||
this.handleAuthRoute(data.routes);
|
this.handleAuthRoute(sortRoutes(data.routes));
|
||||||
|
|
||||||
|
initHomeTab(data.home, router);
|
||||||
|
|
||||||
|
this.isInitAuthRoute = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 初始化静态路由 */
|
/** 初始化静态路由 */
|
||||||
async initStaticRoute() {
|
async initStaticRoute() {
|
||||||
|
const { initHomeTab } = useTabStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|
||||||
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
|
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
|
||||||
this.handleAuthRoute(routes);
|
this.handleAuthRoute(routes);
|
||||||
},
|
|
||||||
/** 初始化权限路由 */
|
|
||||||
async initAuthRoute() {
|
|
||||||
const { initHomeTab } = useTabStore();
|
|
||||||
const { userId } = getUserInfo();
|
|
||||||
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
const isDynamicRoute = this.authRouteMode === 'dynamic';
|
|
||||||
if (isDynamicRoute) {
|
|
||||||
await this.initDynamicRoute();
|
|
||||||
} else {
|
|
||||||
await this.initStaticRoute();
|
|
||||||
}
|
|
||||||
|
|
||||||
initHomeTab(this.routeHomeName, router);
|
initHomeTab(this.routeHomeName, router);
|
||||||
|
|
||||||
this.isInitAuthRoute = true;
|
this.isInitAuthRoute = true;
|
||||||
|
},
|
||||||
|
/** 初始化权限路由 */
|
||||||
|
async initAuthRoute() {
|
||||||
|
if (this.authRouteMode === 'dynamic') {
|
||||||
|
await this.initDynamicRoute();
|
||||||
|
} else {
|
||||||
|
await this.initStaticRoute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { RouteLocationNormalizedLoaded, RouteRecordNormalized } from 'vue-router';
|
import type { RouteLocationNormalizedLoaded, RouteRecordNormalized } from 'vue-router';
|
||||||
import { EnumStorageKey } from '@/enum';
|
import { localStg } from '@/utils';
|
||||||
import { getLocal, setLocal } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据vue路由获取tab路由
|
* 根据vue路由获取tab路由
|
||||||
@@ -58,15 +57,10 @@ function hasFullPath(
|
|||||||
return Boolean((route as RouteLocationNormalizedLoaded).fullPath);
|
return Boolean((route as RouteLocationNormalizedLoaded).fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 缓存多页签数据 */
|
|
||||||
export function setTabRoutes(data: App.GlobalTabRoute[]) {
|
|
||||||
setLocal(EnumStorageKey['multi-tab-routes'], data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取缓存的多页签数据 */
|
/** 获取缓存的多页签数据 */
|
||||||
export function getTabRoutes() {
|
export function getTabRoutes() {
|
||||||
const routes: App.GlobalTabRoute[] = [];
|
const routes: App.GlobalTabRoute[] = [];
|
||||||
const data = getLocal<App.GlobalTabRoute[]>(EnumStorageKey['multi-tab-routes']);
|
const data = localStg.get('multiTabRoutes');
|
||||||
if (data) {
|
if (data) {
|
||||||
const defaultTabRoutes = data.map(item => ({
|
const defaultTabRoutes = data.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -82,5 +76,5 @@ export function getTabRoutes() {
|
|||||||
|
|
||||||
/** 清空多页签数据 */
|
/** 清空多页签数据 */
|
||||||
export function clearTabRoutes() {
|
export function clearTabRoutes() {
|
||||||
setTabRoutes([]);
|
localStg.set('multiTabRoutes', []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
|
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useRouterPush } from '@/composables';
|
import { useRouterPush } from '@/composables';
|
||||||
|
import { localStg } from '@/utils';
|
||||||
import { useThemeStore } from '../theme';
|
import { useThemeStore } from '../theme';
|
||||||
import {
|
import {
|
||||||
clearTabRoutes,
|
clearTabRoutes,
|
||||||
@@ -8,8 +9,7 @@ import {
|
|||||||
getIndexInTabRoutesByRouteName,
|
getIndexInTabRoutesByRouteName,
|
||||||
getTabRouteByVueRoute,
|
getTabRouteByVueRoute,
|
||||||
getTabRoutes,
|
getTabRoutes,
|
||||||
isInTabRoutes,
|
isInTabRoutes
|
||||||
setTabRoutes
|
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
interface TabState {
|
interface TabState {
|
||||||
@@ -52,7 +52,7 @@ export const useTabStore = defineStore('tab-store', {
|
|||||||
},
|
},
|
||||||
/** 缓存页签路由数据 */
|
/** 缓存页签路由数据 */
|
||||||
cacheTabRoutes() {
|
cacheTabRoutes() {
|
||||||
setTabRoutes(this.tabs);
|
localStg.set('multiTabRoutes', this.tabs);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 设置当前路由对应的页签为激活状态
|
* 设置当前路由对应的页签为激活状态
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { themeSetting } from '@/settings';
|
import { themeSetting } from '@/settings';
|
||||||
import { EnumStorageKey } from '@/enum';
|
import { localStg, addColorAlpha, getColorPalette } from '@/utils';
|
||||||
import { addColorAlpha, getColorPalette, getLocal, getThemeColor, removeLocal, setLocal } from '@/utils';
|
|
||||||
|
|
||||||
/** 初始化主题配置 */
|
/** 初始化主题配置 */
|
||||||
export function initThemeSettings() {
|
export function initThemeSettings() {
|
||||||
const isProd = import.meta.env.PROD;
|
const isProd = import.meta.env.PROD;
|
||||||
// 生产环境才缓存主题配置,本地开发实时调整配置更改配置的json
|
// 生产环境才缓存主题配置,本地开发实时调整配置更改配置的json
|
||||||
const storageSettings = getThemeSettings();
|
const storageSettings = localStg.get('themeSettings');
|
||||||
if (isProd && storageSettings) {
|
if (isProd && storageSettings) {
|
||||||
return storageSettings;
|
return storageSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeColor = getThemeColor() || themeSetting.themeColor;
|
const themeColor = localStg.get('themeColor') || themeSetting.themeColor;
|
||||||
const info = themeSetting.isCustomizeInfoColor ? themeSetting.otherColor.info : getColorPalette(themeColor, 7);
|
const info = themeSetting.isCustomizeInfoColor ? themeSetting.otherColor.info : getColorPalette(themeColor, 7);
|
||||||
const otherColor = { ...themeSetting.otherColor, info };
|
const otherColor = { ...themeSetting.otherColor, info };
|
||||||
const setting = cloneDeep({ ...themeSetting, themeColor, otherColor });
|
const setting = cloneDeep({ ...themeSetting, themeColor, otherColor });
|
||||||
@@ -78,18 +77,3 @@ export function getNaiveThemeOverrides(colors: Record<ColorType, string>): Globa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取缓存中的主题配置 */
|
|
||||||
function getThemeSettings() {
|
|
||||||
return getLocal<Theme.Setting>(EnumStorageKey['theme-settings']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取缓存中的主题配置 */
|
|
||||||
export function setThemeSettings(settings: Theme.Setting) {
|
|
||||||
return setLocal(EnumStorageKey['theme-settings'], settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清除缓存配置 */
|
|
||||||
export function clearThemeSettings() {
|
|
||||||
removeLocal(EnumStorageKey['theme-settings']);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { darkTheme } from 'naive-ui';
|
import { darkTheme } from 'naive-ui';
|
||||||
import { clearThemeSettings, getNaiveThemeOverrides, initThemeSettings, setThemeSettings } from './helpers';
|
import { localStg } from '@/utils';
|
||||||
|
import { getNaiveThemeOverrides, initThemeSettings } from './helpers';
|
||||||
|
|
||||||
type ThemeState = Theme.Setting;
|
type ThemeState = Theme.Setting;
|
||||||
|
|
||||||
@@ -24,14 +25,14 @@ export const useThemeStore = defineStore('theme-store', {
|
|||||||
actions: {
|
actions: {
|
||||||
/** 重置theme状态 */
|
/** 重置theme状态 */
|
||||||
resetThemeStore() {
|
resetThemeStore() {
|
||||||
clearThemeSettings();
|
localStg.remove('themeSettings');
|
||||||
this.$reset();
|
this.$reset();
|
||||||
},
|
},
|
||||||
/** 缓存主题配置 */
|
/** 缓存主题配置 */
|
||||||
cacheThemeSettings() {
|
cacheThemeSettings() {
|
||||||
const isProd = import.meta.env.PROD;
|
const isProd = import.meta.env.PROD;
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
setThemeSettings(this.$state);
|
localStg.set('themeSettings', this.$state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 设置暗黑模式 */
|
/** 设置暗黑模式 */
|
||||||
@@ -148,6 +149,10 @@ export const useThemeStore = defineStore('theme-store', {
|
|||||||
setFooterHeight(height: number) {
|
setFooterHeight(height: number) {
|
||||||
this.footer.height = height;
|
this.footer.height = height;
|
||||||
},
|
},
|
||||||
|
/** 设置底部是否显示 */
|
||||||
|
setFooterVisible(isVisible: boolean) {
|
||||||
|
this.footer.visible = isVisible;
|
||||||
|
},
|
||||||
/** 设置切换页面时是否过渡动画 */
|
/** 设置切换页面时是否过渡动画 */
|
||||||
setPageIsAnimate(animate: boolean) {
|
setPageIsAnimate(animate: boolean) {
|
||||||
this.page.animate = animate;
|
this.page.animate = animate;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { onUnmounted, watch } from 'vue';
|
import { effectScope, onScopeDispose, watch } from 'vue';
|
||||||
import { useOsTheme } from 'naive-ui';
|
import { useOsTheme } from 'naive-ui';
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { kebabCase } from 'lodash-es';
|
import { kebabCase } from 'lodash-es';
|
||||||
import { setThemeColor } from '@/utils';
|
import { localStg } from '@/utils';
|
||||||
import { useThemeStore } from '../modules';
|
import { useThemeStore } from '../modules';
|
||||||
|
|
||||||
/** 订阅theme store */
|
/** 订阅theme store */
|
||||||
@@ -12,67 +12,66 @@ export default function subscribeThemeStore() {
|
|||||||
const osTheme = useOsTheme();
|
const osTheme = useOsTheme();
|
||||||
const { width } = useElementSize(document.documentElement);
|
const { width } = useElementSize(document.documentElement);
|
||||||
const { addDarkClass, removeDarkClass } = handleCssDarkMode();
|
const { addDarkClass, removeDarkClass } = handleCssDarkMode();
|
||||||
|
const scope = effectScope();
|
||||||
|
|
||||||
// 监听主题颜色
|
scope.run(() => {
|
||||||
const stopThemeColor = watch(
|
// 监听主题颜色
|
||||||
() => theme.themeColor,
|
watch(
|
||||||
newValue => {
|
() => theme.themeColor,
|
||||||
setThemeColor(newValue);
|
newValue => {
|
||||||
},
|
localStg.set('themeColor', newValue);
|
||||||
{ immediate: true }
|
},
|
||||||
);
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
// 监听naiveUI themeOverrides
|
// 监听naiveUI themeOverrides
|
||||||
const stopThemeOverrides = watch(
|
watch(
|
||||||
() => theme.naiveThemeOverrides,
|
() => theme.naiveThemeOverrides,
|
||||||
newValue => {
|
newValue => {
|
||||||
if (newValue.common) {
|
if (newValue.common) {
|
||||||
addThemeCssVarsToHtml(newValue.common);
|
addThemeCssVarsToHtml(newValue.common);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听暗黑模式
|
||||||
|
watch(
|
||||||
|
() => theme.darkMode,
|
||||||
|
newValue => {
|
||||||
|
if (newValue) {
|
||||||
|
addDarkClass();
|
||||||
|
} else {
|
||||||
|
removeDarkClass();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听暗黑模式
|
// 监听操作系统主题模式
|
||||||
const stopDarkMode = watch(
|
watch(
|
||||||
() => theme.darkMode,
|
osTheme,
|
||||||
newValue => {
|
newValue => {
|
||||||
if (newValue) {
|
const isDark = newValue === 'dark';
|
||||||
addDarkClass();
|
theme.setAutoFollowSystemMode(isDark);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 禁用横向滚动(页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止)
|
||||||
|
watch(width, newValue => {
|
||||||
|
if (newValue < theme.layout.minWidth) {
|
||||||
|
document.documentElement.style.overflowX = 'auto';
|
||||||
} else {
|
} else {
|
||||||
removeDarkClass();
|
document.documentElement.style.overflowX = 'hidden';
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
{
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听操作系统主题模式
|
|
||||||
const stopOsTheme = watch(
|
|
||||||
osTheme,
|
|
||||||
newValue => {
|
|
||||||
const isDark = newValue === 'dark';
|
|
||||||
theme.setAutoFollowSystemMode(isDark);
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 禁用横向滚动(页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止)
|
|
||||||
const stopWidth = watch(width, newValue => {
|
|
||||||
if (newValue < theme.layout.minWidth) {
|
|
||||||
document.documentElement.style.overflowX = 'auto';
|
|
||||||
} else {
|
|
||||||
document.documentElement.style.overflowX = 'hidden';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onScopeDispose(() => {
|
||||||
stopThemeColor();
|
scope.stop();
|
||||||
stopThemeOverrides();
|
|
||||||
stopDarkMode();
|
|
||||||
stopOsTheme();
|
|
||||||
stopWidth();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import './transition.css';
|
@import './transition.css';
|
||||||
@import './reset.css';
|
@import './reset.css';
|
||||||
|
@import './scrollbar.css';
|
||||||
|
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,26 +1,53 @@
|
|||||||
/*---滚动条默认显示样式--*/
|
html {
|
||||||
::-webkit-scrollbar-thumb {
|
scrollbar-width: thin;
|
||||||
background-color: #e6e6e6;
|
scrollbar-color: #e1e1e1 transparent;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
/*---鼠标点击滚动条显示样式--*/
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
/*---滚动条大小--*/
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*---滚动条默认显示样式--*/
|
||||||
|
html::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
/*---鼠标点击滚动条显示样式--*/
|
||||||
|
html::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
/*---滚动条大小--*/
|
||||||
|
html::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
/*---滚动框背景样式--*/
|
/*---滚动框背景样式--*/
|
||||||
::-webkit-scrollbar-track-piece {
|
html::-webkit-scrollbar-track-piece {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
|
||||||
|
html.dark {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: e6e6e6 transparent;
|
scrollbar-color: #555 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*---滚动条默认显示样式--*/
|
||||||
|
html.dark::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #555;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
/*---鼠标点击滚动条显示样式--*/
|
||||||
|
html.dark::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #555;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
/*---滚动条大小--*/
|
||||||
|
html.dark::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*---滚动框背景样式--*/
|
||||||
|
html.dark::-webkit-scrollbar-track-piece {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/typings/env.d.ts
vendored
2
src/typings/env.d.ts
vendored
@@ -53,6 +53,8 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_COMPRESS?: 'Y' | 'N';
|
readonly VITE_COMPRESS?: 'Y' | 'N';
|
||||||
/** 压缩算法类型 */
|
/** 压缩算法类型 */
|
||||||
readonly VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw';
|
readonly VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw';
|
||||||
|
/** 是否应用pwa */
|
||||||
|
readonly VITE_PWA?: 'Y' | 'N';
|
||||||
/** hash路由模式 */
|
/** hash路由模式 */
|
||||||
readonly VITE_HASH_ROUTE?: 'Y' | 'N';
|
readonly VITE_HASH_ROUTE?: 'Y' | 'N';
|
||||||
/** 是否是部署的vercel */
|
/** 是否是部署的vercel */
|
||||||
|
|||||||
122
src/typings/page-route.d.ts
vendored
Normal file
122
src/typings/page-route.d.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
declare namespace PageRoute {
|
||||||
|
/**
|
||||||
|
* the root route key
|
||||||
|
* @translate 根路由
|
||||||
|
*/
|
||||||
|
type RootRouteKey = 'root';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the not found route, which catch the invalid route path
|
||||||
|
* @translate 未找到路由(捕获无效路径的路由)
|
||||||
|
*/
|
||||||
|
type NotFoundRouteKey = 'not-found';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the route key
|
||||||
|
* @translate 页面路由
|
||||||
|
*/
|
||||||
|
type RouteKey =
|
||||||
|
| '403'
|
||||||
|
| '404'
|
||||||
|
| '500'
|
||||||
|
| 'constant-page'
|
||||||
|
| 'login'
|
||||||
|
| 'not-found'
|
||||||
|
| 'about'
|
||||||
|
| 'auth-demo'
|
||||||
|
| 'auth-demo_permission'
|
||||||
|
| 'auth-demo_super'
|
||||||
|
| 'component'
|
||||||
|
| 'component_button'
|
||||||
|
| 'component_card'
|
||||||
|
| 'component_table'
|
||||||
|
| 'dashboard'
|
||||||
|
| 'dashboard_analysis'
|
||||||
|
| 'dashboard_workbench'
|
||||||
|
| 'document'
|
||||||
|
| 'document_naive'
|
||||||
|
| 'document_project-link'
|
||||||
|
| 'document_project'
|
||||||
|
| 'document_vite'
|
||||||
|
| 'document_vue'
|
||||||
|
| 'exception'
|
||||||
|
| 'exception_403'
|
||||||
|
| 'exception_404'
|
||||||
|
| 'exception_500'
|
||||||
|
| 'function'
|
||||||
|
| 'function_tab-detail'
|
||||||
|
| 'function_tab-multi-detail'
|
||||||
|
| 'function_tab'
|
||||||
|
| 'management'
|
||||||
|
| 'management_auth'
|
||||||
|
| 'management_role'
|
||||||
|
| 'management_route'
|
||||||
|
| 'management_user'
|
||||||
|
| 'multi-menu'
|
||||||
|
| 'multi-menu_first'
|
||||||
|
| 'multi-menu_first_second-new'
|
||||||
|
| 'multi-menu_first_second-new_third'
|
||||||
|
| 'multi-menu_first_second'
|
||||||
|
| 'plugin'
|
||||||
|
| 'plugin_charts'
|
||||||
|
| 'plugin_charts_antv'
|
||||||
|
| 'plugin_charts_echarts'
|
||||||
|
| 'plugin_copy'
|
||||||
|
| 'plugin_editor'
|
||||||
|
| 'plugin_editor_markdown'
|
||||||
|
| 'plugin_editor_quill'
|
||||||
|
| 'plugin_icon'
|
||||||
|
| 'plugin_map'
|
||||||
|
| 'plugin_print'
|
||||||
|
| 'plugin_swiper'
|
||||||
|
| 'plugin_video';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* last degree route key, which has the page file
|
||||||
|
* @translate 最后一级路由(该级路有对应的页面文件)
|
||||||
|
*/
|
||||||
|
type LastDegreeRouteKey = Extract<
|
||||||
|
RouteKey,
|
||||||
|
| '403'
|
||||||
|
| '404'
|
||||||
|
| '500'
|
||||||
|
| 'constant-page'
|
||||||
|
| 'login'
|
||||||
|
| 'not-found'
|
||||||
|
| 'about'
|
||||||
|
| 'auth-demo_permission'
|
||||||
|
| 'auth-demo_super'
|
||||||
|
| 'component_button'
|
||||||
|
| 'component_card'
|
||||||
|
| 'component_table'
|
||||||
|
| 'dashboard_analysis'
|
||||||
|
| 'dashboard_workbench'
|
||||||
|
| 'document_naive'
|
||||||
|
| 'document_project-link'
|
||||||
|
| 'document_project'
|
||||||
|
| 'document_vite'
|
||||||
|
| 'document_vue'
|
||||||
|
| 'exception_403'
|
||||||
|
| 'exception_404'
|
||||||
|
| 'exception_500'
|
||||||
|
| 'function_tab-detail'
|
||||||
|
| 'function_tab-multi-detail'
|
||||||
|
| 'function_tab'
|
||||||
|
| 'management_auth'
|
||||||
|
| 'management_role'
|
||||||
|
| 'management_route'
|
||||||
|
| 'management_user'
|
||||||
|
| 'multi-menu_first_second-new_third'
|
||||||
|
| 'multi-menu_first_second'
|
||||||
|
| 'plugin_charts_antv'
|
||||||
|
| 'plugin_charts_echarts'
|
||||||
|
| 'plugin_copy'
|
||||||
|
| 'plugin_editor_markdown'
|
||||||
|
| 'plugin_editor_quill'
|
||||||
|
| 'plugin_icon'
|
||||||
|
| 'plugin_map'
|
||||||
|
| 'plugin_print'
|
||||||
|
| 'plugin_swiper'
|
||||||
|
| 'plugin_video'
|
||||||
|
>;
|
||||||
|
}
|
||||||
10
src/typings/route.d.ts
vendored
10
src/typings/route.d.ts
vendored
@@ -5,13 +5,13 @@ declare namespace AuthRoute {
|
|||||||
/** 捕获无效路由的路由路径 */
|
/** 捕获无效路由的路由路径 */
|
||||||
type NotFoundRoutePath = '/:pathMatch(.*)*';
|
type NotFoundRoutePath = '/:pathMatch(.*)*';
|
||||||
|
|
||||||
type RootRouteKey = RouterPage.RootRouteKey;
|
type RootRouteKey = PageRoute.RootRouteKey;
|
||||||
|
|
||||||
type NotFoundRouteKey = RouterPage.NotFoundRouteKey;
|
type NotFoundRouteKey = PageRoute.NotFoundRouteKey;
|
||||||
|
|
||||||
type RouteKey = RouterPage.RouteKey;
|
type RouteKey = PageRoute.RouteKey;
|
||||||
|
|
||||||
type LastDegreeRouteKey = RouterPage.LastDegreeRouteKey;
|
type LastDegreeRouteKey = PageRoute.LastDegreeRouteKey;
|
||||||
|
|
||||||
type AllRouteKey = RouteKey | RootRouteKey | NotFoundRouteKey;
|
type AllRouteKey = RouteKey | RootRouteKey | NotFoundRouteKey;
|
||||||
|
|
||||||
@@ -60,6 +60,8 @@ declare namespace AuthRoute {
|
|||||||
activeMenu?: RouteKey;
|
activeMenu?: RouteKey;
|
||||||
/** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */
|
/** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */
|
||||||
multi?: boolean;
|
multi?: boolean;
|
||||||
|
/** 是否固定在tab卡不可关闭 */
|
||||||
|
affix?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Route<K extends AllRouteKey = AllRouteKey> = K extends AllRouteKey
|
type Route<K extends AllRouteKey = AllRouteKey> = K extends AllRouteKey
|
||||||
|
|||||||
22
src/typings/storage.d.ts
vendored
Normal file
22
src/typings/storage.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
declare namespace StorageInterface {
|
||||||
|
/** localStorage的存储数据的类型 */
|
||||||
|
interface Session {
|
||||||
|
demoKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** localStorage的存储数据的类型 */
|
||||||
|
interface Local {
|
||||||
|
/** 主题颜色 */
|
||||||
|
themeColor: string;
|
||||||
|
/** 用户token */
|
||||||
|
token: string;
|
||||||
|
/** 用户刷新token */
|
||||||
|
refreshToken: string;
|
||||||
|
/** 用户信息 */
|
||||||
|
userInfo: Auth.UserInfo;
|
||||||
|
/** 主题配置 */
|
||||||
|
themeSettings: Theme.Setting;
|
||||||
|
/** 多页签路由信息 */
|
||||||
|
multiTabRoutes: App.GlobalTabRoute[];
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/typings/system.d.ts
vendored
2
src/typings/system.d.ts
vendored
@@ -236,6 +236,8 @@ declare namespace Theme {
|
|||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
/** 底部高度 */
|
/** 底部高度 */
|
||||||
height: number;
|
height: number;
|
||||||
|
/* 底部是否可见 */
|
||||||
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页面样式 */
|
/** 页面样式 */
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './user';
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { EnumStorageKey } from '@/enum';
|
|
||||||
import { getLocal, removeLocal, setLocal } from '../storage';
|
|
||||||
|
|
||||||
/** 设置token */
|
|
||||||
export function setToken(token: string) {
|
|
||||||
setLocal(EnumStorageKey.token, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取token */
|
|
||||||
export function getToken() {
|
|
||||||
return getLocal<string>(EnumStorageKey.token) || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 去除token */
|
|
||||||
export function removeToken() {
|
|
||||||
removeLocal(EnumStorageKey.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取refresh token */
|
|
||||||
export function getRefreshToken() {
|
|
||||||
return getLocal<string>(EnumStorageKey['refresh-token']) || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 设置refresh token */
|
|
||||||
export function setRefreshToken(token: string) {
|
|
||||||
setLocal(EnumStorageKey['refresh-token'], token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 去除refresh token */
|
|
||||||
export function removeRefreshToken() {
|
|
||||||
removeLocal(EnumStorageKey['refresh-token']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取用户信息 */
|
|
||||||
export function getUserInfo() {
|
|
||||||
const emptyInfo: Auth.UserInfo = {
|
|
||||||
userId: '',
|
|
||||||
userName: '',
|
|
||||||
userRole: 'user'
|
|
||||||
};
|
|
||||||
const userInfo: Auth.UserInfo = getLocal<Auth.UserInfo>(EnumStorageKey['user-info']) || emptyInfo;
|
|
||||||
return userInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 设置用户信息 */
|
|
||||||
export function setUserInfo(userInfo: Auth.UserInfo) {
|
|
||||||
setLocal(EnumStorageKey['user-info'], userInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 去除用户信息 */
|
|
||||||
export function removeUserInfo() {
|
|
||||||
removeLocal(EnumStorageKey['user-info']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 去除用户相关缓存 */
|
|
||||||
export function clearAuthStorage() {
|
|
||||||
removeToken();
|
|
||||||
removeRefreshToken();
|
|
||||||
removeUserInfo();
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
export * from './typeof';
|
export * from './typeof';
|
||||||
export * from './color';
|
export * from './color';
|
||||||
export * from './number';
|
export * from './number';
|
||||||
export * from './object';
|
|
||||||
export * from './pattern';
|
export * from './pattern';
|
||||||
export * from './theme';
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
/** 设置对象数据 */
|
|
||||||
export function objectAssign<T extends Record<string, any>>(target: T, source: Partial<T>) {
|
|
||||||
Object.assign(target, source);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { EnumStorageKey } from '@/enum';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存主题颜色
|
|
||||||
* @param color
|
|
||||||
*/
|
|
||||||
export function setThemeColor(color: string) {
|
|
||||||
window.localStorage.setItem(EnumStorageKey['theme-color'], color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存的主题颜色
|
|
||||||
*/
|
|
||||||
export function getThemeColor() {
|
|
||||||
return window.localStorage.getItem(EnumStorageKey['theme-color']);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './service';
|
export * from './service';
|
||||||
export * from './auth';
|
|
||||||
export * from './router';
|
export * from './router';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
|
|||||||
54
src/utils/router/component.ts
Normal file
54
src/utils/router/component.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { RouteComponent } from 'vue-router';
|
||||||
|
import { BasicLayout, BlankLayout } from '@/layouts';
|
||||||
|
import { views } from '@/views';
|
||||||
|
import { isFunction } from '../common';
|
||||||
|
|
||||||
|
type Lazy<T> = () => Promise<T>;
|
||||||
|
|
||||||
|
interface ModuleComponent {
|
||||||
|
default: RouteComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutComponent = Record<EnumType.LayoutComponentName, Lazy<ModuleComponent>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取布局的vue文件(懒加载的方式)
|
||||||
|
* @param layoutType - 布局类型
|
||||||
|
*/
|
||||||
|
export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) {
|
||||||
|
const layoutComponent: LayoutComponent = {
|
||||||
|
basic: BasicLayout,
|
||||||
|
blank: BlankLayout
|
||||||
|
};
|
||||||
|
return layoutComponent[layoutType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取页面导入的vue文件
|
||||||
|
* @param routeKey - 路由key
|
||||||
|
*/
|
||||||
|
export function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) {
|
||||||
|
if (!views[routeKey]) {
|
||||||
|
throw new Error(`路由“${routeKey}”没有对应的组件文件!`);
|
||||||
|
}
|
||||||
|
return setViewComponentName(views[routeKey], routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 给页面组件设置名称 */
|
||||||
|
function setViewComponentName(component: RouteComponent | Lazy<ModuleComponent>, name: string) {
|
||||||
|
if (isAsyncComponent(component)) {
|
||||||
|
return async () => {
|
||||||
|
const result = await component();
|
||||||
|
Object.assign(result.default, { name });
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(component, { name });
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAsyncComponent(component: RouteComponent | Lazy<ModuleComponent>): component is Lazy<ModuleComponent> {
|
||||||
|
return isFunction(component);
|
||||||
|
}
|
||||||
@@ -6,64 +6,14 @@ export function getConstantRouteNames(routes: AuthRoute.Route[]) {
|
|||||||
return routes.map(route => getConstantRouteName(route)).flat(1);
|
return routes.map(route => getConstantRouteName(route)).flat(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将权限路由转换成搜索的菜单数据
|
|
||||||
* @param routes - 权限路由
|
|
||||||
* @param treeMap
|
|
||||||
*/
|
|
||||||
export function transformAuthRouteToSearchMenus(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = []) {
|
|
||||||
if (routes && routes.length === 0) return [];
|
|
||||||
return routes.reduce((acc, cur) => {
|
|
||||||
if (!cur.meta?.hide) {
|
|
||||||
acc.push(cur);
|
|
||||||
}
|
|
||||||
if (cur.children && cur.children.length > 0) {
|
|
||||||
transformAuthRouteToSearchMenus(cur.children, treeMap);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, treeMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将路由名字转换成路由路径 */
|
|
||||||
export function transformRouteNameToRoutePath(name: Exclude<AuthRoute.AllRouteKey, 'not-found'>): AuthRoute.RoutePath {
|
|
||||||
const rootPath: AuthRoute.RoutePath = '/';
|
|
||||||
if (name === 'root') return rootPath;
|
|
||||||
|
|
||||||
const splitMark = '_';
|
|
||||||
const pathSplitMark = '/';
|
|
||||||
const path = name.split(splitMark).join(pathSplitMark);
|
|
||||||
|
|
||||||
return (pathSplitMark + path) as AuthRoute.RoutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将路由路径转换成路由名字 */
|
|
||||||
export function transformRoutePathToRouteName<K extends AuthRoute.RoutePath>(path: K) {
|
|
||||||
if (path === '/') return 'root';
|
|
||||||
|
|
||||||
const pathSplitMark = '/';
|
|
||||||
const routeSplitMark = '_';
|
|
||||||
|
|
||||||
const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.AllRouteKey;
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有固定路由的名称集合
|
* 获取所有固定路由的名称集合
|
||||||
* @param route - 固定路由
|
* @param route - 固定路由
|
||||||
*/
|
*/
|
||||||
function getConstantRouteName(route: AuthRoute.Route) {
|
function getConstantRouteName(route: AuthRoute.Route) {
|
||||||
const names = [route.name];
|
const names = [route.name];
|
||||||
if (hasChildren(route)) {
|
if (route.children?.length) {
|
||||||
names.push(...route.children!.map(item => getConstantRouteName(item)).flat(1));
|
names.push(...route.children!.map(item => getConstantRouteName(item)).flat(1));
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有子路由
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function hasChildren(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.children && item.children.length);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './auth';
|
|||||||
export * from './menu';
|
export * from './menu';
|
||||||
export * from './breadcrumb';
|
export * from './breadcrumb';
|
||||||
export * from './regexp';
|
export * from './regexp';
|
||||||
|
export * from './transform';
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
import { useIconRender } from '@/composables';
|
import { useIconRender } from '@/composables';
|
||||||
|
|
||||||
/** 路由不转换菜单 */
|
|
||||||
function hideInMenu(route: AuthRoute.Route) {
|
|
||||||
return Boolean(route.meta.hide);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 给菜单添加可选属性 */
|
|
||||||
function addPartialProps(config: {
|
|
||||||
menu: App.GlobalMenuOption;
|
|
||||||
icon?: string;
|
|
||||||
localIcon?: string;
|
|
||||||
children?: App.GlobalMenuOption[];
|
|
||||||
}) {
|
|
||||||
const { iconRender } = useIconRender();
|
|
||||||
|
|
||||||
const item = { ...config.menu };
|
|
||||||
|
|
||||||
const { icon, localIcon, children } = config;
|
|
||||||
|
|
||||||
if (localIcon) {
|
|
||||||
Object.assign(item, { icon: iconRender({ localIcon }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
Object.assign(item, { icon: iconRender({ icon }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (children) {
|
|
||||||
Object.assign(item, { children });
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将权限路由转换成菜单
|
* 将权限路由转换成菜单
|
||||||
* @param routes - 路由
|
* @param routes - 路由
|
||||||
@@ -77,7 +45,7 @@ export function getActiveKeyPathsOfMenus(activeKey: string, menus: App.GlobalMen
|
|||||||
|
|
||||||
function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenuOption) {
|
function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenuOption) {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
if (activeKey.includes(menu.routeName)) {
|
if (activeKey.startsWith(menu.routeName)) {
|
||||||
keys.push(menu.routeName);
|
keys.push(menu.routeName);
|
||||||
}
|
}
|
||||||
if (menu.children) {
|
if (menu.children) {
|
||||||
@@ -85,3 +53,35 @@ function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenuOption)
|
|||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 路由不转换菜单 */
|
||||||
|
function hideInMenu(route: AuthRoute.Route) {
|
||||||
|
return Boolean(route.meta.hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 给菜单添加可选属性 */
|
||||||
|
function addPartialProps(config: {
|
||||||
|
menu: App.GlobalMenuOption;
|
||||||
|
icon?: string;
|
||||||
|
localIcon?: string;
|
||||||
|
children?: App.GlobalMenuOption[];
|
||||||
|
}) {
|
||||||
|
const { iconRender } = useIconRender();
|
||||||
|
|
||||||
|
const item = { ...config.menu };
|
||||||
|
|
||||||
|
const { icon, localIcon, children } = config;
|
||||||
|
|
||||||
|
if (localIcon) {
|
||||||
|
Object.assign(item, { icon: iconRender({ localIcon }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
Object.assign(item, { icon: iconRender({ icon }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
Object.assign(item, { children });
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 权限路由排序
|
* 权限路由排序
|
||||||
* @param routes - 权限路由
|
* @param routes - 权限路由
|
||||||
*/
|
*/
|
||||||
function sortRoutes(routes: AuthRoute.Route[]) {
|
export function sortRoutes(routes: AuthRoute.Route[]) {
|
||||||
return routes.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
|
return routes.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +1,5 @@
|
|||||||
import type { RouteComponent, RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import { BasicLayout, BlankLayout } from '@/layouts';
|
import { getLayoutComponent, getViewComponent } from './component';
|
||||||
import { views } from '@/views';
|
|
||||||
import { isFunction } from '@/utils';
|
|
||||||
|
|
||||||
type Lazy<T> = () => Promise<T>;
|
|
||||||
|
|
||||||
type LayoutComponent = Record<EnumType.LayoutComponentName, Lazy<RouteComponent>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取布局的vue文件(懒加载的方式)
|
|
||||||
* @param layoutType - 布局类型
|
|
||||||
*/
|
|
||||||
export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) {
|
|
||||||
const layoutComponent: LayoutComponent = {
|
|
||||||
basic: BasicLayout,
|
|
||||||
blank: BlankLayout
|
|
||||||
};
|
|
||||||
return layoutComponent[layoutType];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取页面导入的vue文件
|
|
||||||
* @param routeKey - 路由key
|
|
||||||
*/
|
|
||||||
export function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) {
|
|
||||||
if (!views[routeKey]) {
|
|
||||||
throw new Error(`路由“${routeKey}”没有对应的组件文件!`);
|
|
||||||
}
|
|
||||||
return setViewComponentName(views[routeKey], routeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModuleComponent {
|
|
||||||
default: RouteComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 给页面组件设置名称 */
|
|
||||||
function setViewComponentName(component: RouteComponent | Lazy<ModuleComponent>, name: string) {
|
|
||||||
if (isAsyncComponent(component)) {
|
|
||||||
return async () => {
|
|
||||||
const result = await component();
|
|
||||||
Object.assign(result.default, { name });
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(component, { name });
|
|
||||||
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAsyncComponent(component: RouteComponent | Lazy<ModuleComponent>): component is Lazy<ModuleComponent> {
|
|
||||||
return isFunction(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有外链
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function hasHref(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.meta.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有动态路由path
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function hasDynamicPath(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.meta.dynamicPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有路由组件
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function hasComponent(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.component);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有子路由
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function hasChildren(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.children && item.children.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否是单层级路由
|
|
||||||
* @param item - 权限路由
|
|
||||||
*/
|
|
||||||
function isSingleRoute(item: AuthRoute.Route) {
|
|
||||||
return Boolean(item.meta.singleLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将权限路由转换成vue路由
|
* 将权限路由转换成vue路由
|
||||||
@@ -212,3 +120,85 @@ export function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
|
|||||||
|
|
||||||
return resultRoute;
|
return resultRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将权限路由转换成搜索的菜单数据
|
||||||
|
* @param routes - 权限路由
|
||||||
|
* @param treeMap
|
||||||
|
*/
|
||||||
|
export function transformAuthRouteToSearchMenus(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = []) {
|
||||||
|
if (routes && routes.length === 0) return [];
|
||||||
|
return routes.reduce((acc, cur) => {
|
||||||
|
if (!cur.meta?.hide) {
|
||||||
|
acc.push(cur);
|
||||||
|
}
|
||||||
|
if (cur.children && cur.children.length > 0) {
|
||||||
|
transformAuthRouteToSearchMenus(cur.children, treeMap);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, treeMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将路由名字转换成路由路径 */
|
||||||
|
export function transformRouteNameToRoutePath(name: Exclude<AuthRoute.AllRouteKey, 'not-found'>): AuthRoute.RoutePath {
|
||||||
|
const rootPath: AuthRoute.RoutePath = '/';
|
||||||
|
if (name === 'root') return rootPath;
|
||||||
|
|
||||||
|
const splitMark = '_';
|
||||||
|
const pathSplitMark = '/';
|
||||||
|
const path = name.split(splitMark).join(pathSplitMark);
|
||||||
|
|
||||||
|
return (pathSplitMark + path) as AuthRoute.RoutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将路由路径转换成路由名字 */
|
||||||
|
export function transformRoutePathToRouteName<K extends AuthRoute.RoutePath>(path: K) {
|
||||||
|
if (path === '/') return 'root';
|
||||||
|
|
||||||
|
const pathSplitMark = '/';
|
||||||
|
const routeSplitMark = '_';
|
||||||
|
|
||||||
|
const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.AllRouteKey;
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有外链
|
||||||
|
* @param item - 权限路由
|
||||||
|
*/
|
||||||
|
function hasHref(item: AuthRoute.Route) {
|
||||||
|
return Boolean(item.meta.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有动态路由path
|
||||||
|
* @param item - 权限路由
|
||||||
|
*/
|
||||||
|
function hasDynamicPath(item: AuthRoute.Route) {
|
||||||
|
return Boolean(item.meta.dynamicPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有路由组件
|
||||||
|
* @param item - 权限路由
|
||||||
|
*/
|
||||||
|
function hasComponent(item: AuthRoute.Route) {
|
||||||
|
return Boolean(item.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有子路由
|
||||||
|
* @param item - 权限路由
|
||||||
|
*/
|
||||||
|
function hasChildren(item: AuthRoute.Route) {
|
||||||
|
return Boolean(item.children && item.children.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是单层级路由
|
||||||
|
* @param item - 权限路由
|
||||||
|
*/
|
||||||
|
function isSingleRoute(item: AuthRoute.Route) {
|
||||||
|
return Boolean(item.meta.singleLayout);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +1,57 @@
|
|||||||
import { decrypto, encrypto } from '../crypto';
|
import { decrypto, encrypto } from '../crypto';
|
||||||
|
interface StorageData<T> {
|
||||||
interface StorageData {
|
value: T;
|
||||||
value: unknown;
|
|
||||||
expire: number | null;
|
expire: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认缓存期限为7天 */
|
function createLocalStorage<T extends StorageInterface.Local = StorageInterface.Local>() {
|
||||||
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
|
/** 默认缓存期限为7天 */
|
||||||
|
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
export function setLocal(key: string, value: unknown, expire: number | null = DEFAULT_CACHE_TIME) {
|
function set<K extends keyof T>(key: K, value: T[K], expire: number | null = DEFAULT_CACHE_TIME) {
|
||||||
const storageData: StorageData = { value, expire: expire !== null ? new Date().getTime() + expire * 1000 : null };
|
const storageData: StorageData<T[K]> = {
|
||||||
const json = encrypto(storageData);
|
value,
|
||||||
window.localStorage.setItem(key, json);
|
expire: expire !== null ? new Date().getTime() + expire * 1000 : null
|
||||||
}
|
};
|
||||||
|
const json = encrypto(storageData);
|
||||||
|
window.localStorage.setItem(key as string, json);
|
||||||
|
}
|
||||||
|
|
||||||
export function getLocal<T>(key: string) {
|
function get<K extends keyof T>(key: K) {
|
||||||
const json = window.localStorage.getItem(key);
|
const json = window.localStorage.getItem(key as string);
|
||||||
if (json) {
|
if (json) {
|
||||||
let storageData: StorageData | null = null;
|
let storageData: StorageData<T[K]> | null = null;
|
||||||
try {
|
try {
|
||||||
storageData = decrypto(json);
|
storageData = decrypto(json);
|
||||||
} catch {
|
} catch {
|
||||||
// 防止解析失败
|
// 防止解析失败
|
||||||
}
|
|
||||||
if (storageData) {
|
|
||||||
const { value, expire } = storageData;
|
|
||||||
// 在有效期内直接返回
|
|
||||||
if (expire === null || expire >= Date.now()) {
|
|
||||||
return value as T;
|
|
||||||
}
|
}
|
||||||
|
if (storageData) {
|
||||||
|
const { value, expire } = storageData;
|
||||||
|
// 在有效期内直接返回
|
||||||
|
if (expire === null || expire >= Date.now()) {
|
||||||
|
return value as T[K];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove(key);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
removeLocal(key);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
function remove(key: keyof T) {
|
||||||
|
window.localStorage.removeItem(key as string);
|
||||||
|
}
|
||||||
|
function clear() {
|
||||||
|
window.localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set,
|
||||||
|
get,
|
||||||
|
remove,
|
||||||
|
clear
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeLocal(key: string) {
|
export const localStg = createLocalStorage();
|
||||||
window.localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearLocal() {
|
|
||||||
window.localStorage.clear();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,3 +25,37 @@ export function removeSession(key: string) {
|
|||||||
export function clearSession() {
|
export function clearSession() {
|
||||||
window.sessionStorage.clear();
|
window.sessionStorage.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSessionStorage<T extends StorageInterface.Session = StorageInterface.Session>() {
|
||||||
|
function set<K extends keyof T>(key: K, value: T[K]) {
|
||||||
|
const json = encrypto(value);
|
||||||
|
sessionStorage.setItem(key as string, json);
|
||||||
|
}
|
||||||
|
function get<K extends keyof T>(key: K) {
|
||||||
|
const json = sessionStorage.getItem(key as string);
|
||||||
|
let data: T[K] | null = null;
|
||||||
|
if (json) {
|
||||||
|
try {
|
||||||
|
data = decrypto(json);
|
||||||
|
} catch {
|
||||||
|
// 防止解析失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function remove(key: keyof T) {
|
||||||
|
window.sessionStorage.removeItem(key as string);
|
||||||
|
}
|
||||||
|
function clear() {
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set,
|
||||||
|
get,
|
||||||
|
remove,
|
||||||
|
clear
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionStg = createSessionStorage();
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="pwd">
|
<n-form-item path="pwd">
|
||||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
<n-input v-model:value="model.pwd" type="password" show-password-on="click" placeholder="密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="confirmPwd">
|
<n-form-item path="confirmPwd">
|
||||||
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
|
<n-input v-model:value="model.confirmPwd" type="password" show-password-on="click" placeholder="确认密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space :vertical="true" :size="18">
|
<n-space :vertical="true" :size="18">
|
||||||
<login-agreement v-model:value="agreement" />
|
<login-agreement v-model:value="agreement" />
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="pwd">
|
<n-form-item path="pwd">
|
||||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
<n-input v-model:value="model.pwd" type="password" show-password-on="click" placeholder="密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item path="confirmPwd">
|
<n-form-item path="confirmPwd">
|
||||||
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
|
<n-input v-model:value="model.confirmPwd" type="password" show-password-on="click" placeholder="确认密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space :vertical="true" size="large">
|
<n-space :vertical="true" size="large">
|
||||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
||||||
|
|||||||
@@ -2,18 +2,14 @@
|
|||||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||||
<n-grid-item span="0:24 640:24 1024:8">
|
<n-grid-item span="0:24 640:24 1024:8">
|
||||||
<n-card title="时间线" :bordered="false" class="rounded-16px shadow-sm">
|
<n-card title="时间线" :bordered="false" class="rounded-16px shadow-sm">
|
||||||
<div class="h-360px">
|
<n-timeline>
|
||||||
<n-timeline>
|
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
|
||||||
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
|
</n-timeline>
|
||||||
</n-timeline>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item span="0:24 640:24 1024:16">
|
<n-grid-item span="0:24 640:24 1024:16">
|
||||||
<n-card title="表格" :bordered="false" class="rounded-16px shadow-sm">
|
<n-card title="表格" :bordered="false" class="rounded-16px shadow-sm">
|
||||||
<div class="h-360px">
|
<n-data-table size="small" :columns="columns" :data="tableData" />
|
||||||
<n-data-table size="small" :columns="columns" :data="tableData" />
|
|
||||||
</div>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { RouteComponent } from 'vue-router';
|
import type { RouteComponent } from 'vue-router';
|
||||||
|
|
||||||
export const views: Record<RouterPage.LastDegreeRouteKey, RouteComponent | (() => Promise<RouteComponent>)> = {
|
export const views: Record<
|
||||||
|
PageRoute.LastDegreeRouteKey,
|
||||||
|
RouteComponent | (() => Promise<{ default: RouteComponent }>)
|
||||||
|
> = {
|
||||||
403: () => import('./_builtin/403/index.vue'),
|
403: () => import('./_builtin/403/index.vue'),
|
||||||
404: () => import('./_builtin/404/index.vue'),
|
404: () => import('./_builtin/404/index.vue'),
|
||||||
500: () => import('./_builtin/500/index.vue'),
|
500: () => import('./_builtin/500/index.vue'),
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export default defineConfig(configEnv => {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'~': rootPath,
|
'~': rootPath,
|
||||||
'@': srcPath
|
'@': srcPath,
|
||||||
|
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
define: viteDefine,
|
define: viteDefine,
|
||||||
@@ -30,6 +31,7 @@ export default defineConfig(configEnv => {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
port: 3200,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: createViteProxy(isOpenProxy, envConfig)
|
proxy: createViteProxy(isOpenProxy, envConfig)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user