new Files
15
front/.env.development
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# 应用端口
|
||||||
|
VITE_APP_PORT=5173
|
||||||
|
|
||||||
|
# 代理前缀
|
||||||
|
VITE_APP_BASE_API=/dev-api
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
# VITE_APP_API_URL=https://api.youlai.tech # 线上
|
||||||
|
VITE_APP_API_URL=http://localhost:8989 # 本地
|
||||||
|
|
||||||
|
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
|
||||||
|
VITE_APP_WS_ENDPOINT=
|
||||||
|
|
||||||
|
# 启用 Mock 服务
|
||||||
|
VITE_MOCK_DEV_SERVER=true
|
6
front/.env.production
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# 代理前缀
|
||||||
|
VITE_APP_BASE_API = '/prod-api'
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket端点(可选)
|
||||||
|
#VITE_APP_WS_ENDPOINT=wss://api.youlai.tech/ws
|
23
front/TODO.txt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Login Page: - [*] 登录页面设计
|
||||||
|
- [ ] 输入合法性检查
|
||||||
|
- [ ] 忘记密码
|
||||||
|
- [ ] 验证码验证
|
||||||
|
- [ ] SSO设计
|
||||||
|
|
||||||
|
Permissions: - [ ] 呈现界面权限设计
|
||||||
|
- [ ]
|
||||||
|
Router: - [ ] 路由设计
|
||||||
|
COOKIES: - [ ] 记录登录状态的cookies设计
|
||||||
|
- [ ] 记录侧边栏状态
|
||||||
|
- [ ] 记录主题状态
|
||||||
|
- [ ] 记录患者信息
|
||||||
|
API: - [ ] 登录接口设计
|
||||||
|
- [ ] 患者信息接口设计
|
||||||
|
- [ ] 搜索接口设计
|
||||||
|
- [ ] 用户管理接口设计
|
||||||
|
- [ ] 数据统计接口设计
|
||||||
|
- [ ] 其他接口设计
|
||||||
|
OTHERS: - [ ] UI统一化设计
|
||||||
|
- [ ] 显示异常处理
|
||||||
|
- [ ] 性能优化设计
|
||||||
|
- [ ]
|
@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
44
front/mock/auth.mock.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { defineMock } from "./base";
|
||||||
|
|
||||||
|
export default defineMock([
|
||||||
|
{
|
||||||
|
url: "auth/captcha",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
captchaKey: "534b8ef2b0a24121bec76391ddd159f9",
|
||||||
|
captchaBase64:
|
||||||
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAAkCAIAAADNSmkJAAAFKUlEQVR4Xu2ZXUwcVRiGV70wMWo08V5NvPXCrDbFaGpMaZW2hqQxaoiJTRsaMBCNSYtpa2JTKiFSelFa+Q/QZcMWqEhBlh+htbEpZhMrBQrlJ0hBywLLyrJ0WZbje3bqOvPNLHPWrDvdOE9ONmfe78zkzMs335wzWJhJQrBQweS/wTQ6QWgYHdoIOcecOe05O+t2WkutO+p2ZF3Ksg/YV9ZW6FATYajR3nveg60H9327r3O8c35lHgp+r05dPdJzBL73TPSQ8SaCKIxGLsPlop+K0JHrEkPuoT31e5qGmmjARACF0agYyGVNlyVm/pzZXrN9fHGcBkz0UBid+31u93i3XFFT80vN8cvHqWqih8Lo1NpUqS5vwh3vnd223VQ10UNh9NbyrcFQUK6oCawHUipSqGqiB83oBf+CXFGDMp1mS6OqiR4Ko7FexkpOrqhpHGw82nOUqiZ6KIzGrkRuorW0dJMmOy+hOCfYGzb2RBFv6HRO0gEJw/U7y+pgL1bwmTxexN6sZ31TdEwEhdG+gA+7EqyXpUO1uZH20cWL8hMTRt1N9tBXzCJrOIRoCPJpSO2RAp4HmtCdIfZ+2JWgEBN9LbR28seTGU0Zue1tMLp+YIAMSADzfvbkKX4/eb28j4YODiGin3heqmIlLja5hAUCu+nmGY3JWKvpMAlqNGgebsauBOvlqSX+JEx7p7EbTLen53XlzfmWUioqXikrc68Y8N2juJ/fyVsNChGHEE//rBANYWaZz+TRQqpLaBgNsPfDrgSpbS21YtV87IdjrlkX9JZbt5DOma2t9ITo5F+5glN22WwL/n+yDv00mw06orKxOqQ5+J04hhViwzAXETIcJDVm8uxZqktoGx2Nj9t43Wgaul/ERQiGQvtbWnDWgZYW9CXlQFjZ/7ciyHNn+Z2MexTimIeLz59TiIln0M1e+IbPpOAaDUnEYPTi6iqKxpbycs/qKo1tCslfKcffPn9enuMiPPY1vxO/ckeFQ4h46cdGqUWoidE/y54q5tPY5WDrGzQqIXot4BgchEE57e00IMCw2/1qZSVO/7SjA78o9INzcxsbrL+fnTnDDh9mmZn8F30oG1Hm+nABv5mQMopDS/h1HxtqTzWbABMe9sxpPoe9zezeOo1GELqWhPS8t46M0IAYHbdvR1aHbaOjbjfLz2eFhez6dba4yAfgF30o0BFVE8+Mjh/wFxPI+I5mAEHU6Ls+38vhTFwOBGhMDF8gkFpbC5ffsdv/uBs6dIj19dExEtARVXv9YNbop8NFY3aZ6gRRo+tu3IBHnzmdNCBMXldXJKPfL74WzWUJRE+coDUknqsOdZXQbAJYwluVTbOZI3Qt8GFzMwxyjo3RgBiN4fr+elXVpZGRLWXl6PdOTtJBSlBDUK/lnIrjOlrtqWYTQDJaF6FrTXu9sOa1ysrVoM5HVE1GFxZQcyJ/p+xzv6K/rbr6N6+XDpUBl0tKFIrbz78qWB6YnWFMCBld4XLBms+7df75ook/GNzb0GCV7U1Qfz9p64TyQWNjYD3qe9rj4SMJtQP3MyjSDPzWIRHPjH7X4YAvfXoPuyZf9Pbi3PcuXIh4mp3NllYC6XY79C+jl2o8PBipxjnBttn4MgMNnWgfcRJGPI2OL8hTj3LloIlmRicvBhiNykvecpqoa3RSY4DRcLAwyicuOepVR1JjgNFYHWONHL04czTX0UmNAUYD7Pr+xc4wqTHGaBb2OtZvHUmNYUazcA2J6etdUmOk0f8rTKMTxF91RG0D1SwYGwAAAABJRU5ErkJggg==",
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "auth/login",
|
||||||
|
method: ["POST"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
accessToken:
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImRlcHRJZCI6MSwiZGF0YVNjb3BlIjoxLCJ1c2VySWQiOjIsImlhdCI6MTcyODE5MzA1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhZDg3NzlhZDZlYWY0OWY3OTE4M2ZmYmI5OWM4MjExMSJ9.58YHwL3sNNC22jyAmOZeSm-7MITzfHb_epBIz7LvWeA",
|
||||||
|
tokenType: "Bearer",
|
||||||
|
refreshToken:
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImRlcHRJZCI6MSwiZGF0YVNjb3BlIjoxLCJ1c2VySWQiOjIsImlhdCI6MTcyODE5MzA1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhZDg3NzlhZDZlYWY0OWY3OTE4M2ZmYmI5OWM4MjExMSJ9.58YHwL3sNNC22jyAmOZeSm-7MITzfHb_epBIz7LvWeA",
|
||||||
|
expires: null,
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "auth/logout",
|
||||||
|
method: ["DELETE"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {},
|
||||||
|
msg: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
9
front/mock/base.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { createDefineMock } from "vite-plugin-mock-dev-server";
|
||||||
|
|
||||||
|
export const defineMock = createDefineMock((mock) => {
|
||||||
|
// 拼接url
|
||||||
|
mock.url = path.join(import.meta.env.VITE_APP_BASE_API + "/api/v1/", mock.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
207
front/mock/log.mock.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { defineMock } from "./base";
|
||||||
|
|
||||||
|
export default defineMock([
|
||||||
|
{
|
||||||
|
url: "logs/page",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 36192,
|
||||||
|
module: "菜单",
|
||||||
|
content: "菜单列表",
|
||||||
|
requestUri: "/api/v1/menus",
|
||||||
|
method: null,
|
||||||
|
ip: "183.156.148.241",
|
||||||
|
region: "浙江省 杭州市",
|
||||||
|
browser: "Chrome 109.0.0.0",
|
||||||
|
os: "OSX",
|
||||||
|
executionTime: 5,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:47",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36190,
|
||||||
|
module: "字典",
|
||||||
|
content: "字典分页列表",
|
||||||
|
requestUri: "/api/v1/dict/page",
|
||||||
|
method: null,
|
||||||
|
ip: "183.156.148.241",
|
||||||
|
region: "浙江省 杭州市",
|
||||||
|
browser: "Chrome 109.0.0.0",
|
||||||
|
os: "OSX",
|
||||||
|
executionTime: 9,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:45",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36193,
|
||||||
|
module: "部门",
|
||||||
|
content: "部门列表",
|
||||||
|
requestUri: "/api/v1/dept",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 27,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:45",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36191,
|
||||||
|
module: "菜单",
|
||||||
|
content: "菜单列表",
|
||||||
|
requestUri: "/api/v1/menus",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 39,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:44",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36189,
|
||||||
|
module: "角色",
|
||||||
|
content: "角色分页列表",
|
||||||
|
requestUri: "/api/v1/roles/page",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 55,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:43",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36188,
|
||||||
|
module: "用户",
|
||||||
|
content: "用户分页列表",
|
||||||
|
requestUri: "/api/v1/users/page",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 92,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:42",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36187,
|
||||||
|
module: "登录",
|
||||||
|
content: "登录",
|
||||||
|
requestUri: "/api/v1/auth/login",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 19340,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:38:09",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36186,
|
||||||
|
module: "登录",
|
||||||
|
content: "登录",
|
||||||
|
requestUri: "/api/v1/auth/login",
|
||||||
|
method: null,
|
||||||
|
ip: "192.168.31.134",
|
||||||
|
region: "0 内网IP",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 19869,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:37:59",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36185,
|
||||||
|
module: "登录",
|
||||||
|
content: "登录",
|
||||||
|
requestUri: "/api/v1/auth/login",
|
||||||
|
method: null,
|
||||||
|
ip: "112.103.111.59",
|
||||||
|
region: "黑龙江省 哈尔滨市",
|
||||||
|
browser: "Chrome 97.0.4692.98",
|
||||||
|
os: "Android",
|
||||||
|
executionTime: 96,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:37:21",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36184,
|
||||||
|
module: "登录",
|
||||||
|
content: "登录",
|
||||||
|
requestUri: "/api/v1/auth/login",
|
||||||
|
method: null,
|
||||||
|
ip: "114.86.204.190",
|
||||||
|
region: "上海 上海市",
|
||||||
|
browser: "Chrome 125.0.0.0",
|
||||||
|
os: "Windows 10 or Windows Server 2016",
|
||||||
|
executionTime: 89,
|
||||||
|
createBy: null,
|
||||||
|
createTime: "2024-07-07 20:29:37",
|
||||||
|
operator: "系统管理员",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 36188,
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "logs/visit-trend",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
dates: [
|
||||||
|
"2024-06-30",
|
||||||
|
"2024-07-01",
|
||||||
|
"2024-07-02",
|
||||||
|
"2024-07-03",
|
||||||
|
"2024-07-04",
|
||||||
|
"2024-07-05",
|
||||||
|
"2024-07-06",
|
||||||
|
"2024-07-07",
|
||||||
|
],
|
||||||
|
pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003],
|
||||||
|
uvList: null,
|
||||||
|
ipList: [207, 566, 565, 631, 579, 496, 222, 152],
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "logs/visit-stats",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
todayPvCount: 1629,
|
||||||
|
totalPvCount: 286086,
|
||||||
|
pvGrowthRate: -0.65,
|
||||||
|
todayIpCount: 169,
|
||||||
|
totalIpCount: 19985,
|
||||||
|
ipGrowthRate: -0.57,
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
2082
front/mock/menu.mock.ts
Normal file
334
front/mock/role.mock.ts
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
import { defineMock } from "./base";
|
||||||
|
|
||||||
|
export default defineMock([
|
||||||
|
{
|
||||||
|
url: "roles/options",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: "系统管理员",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: "系统管理员1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: "系统管理员2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 6,
|
||||||
|
label: "系统管理员3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 7,
|
||||||
|
label: "系统管理员4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 8,
|
||||||
|
label: "系统管理员5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: "系统管理员6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 10,
|
||||||
|
label: "系统管理员7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 11,
|
||||||
|
label: "系统管理员8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 12,
|
||||||
|
label: "系统管理员9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: "访问游客",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "roles/page",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "系统管理员",
|
||||||
|
code: "ADMIN",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "访问游客",
|
||||||
|
code: "GUEST",
|
||||||
|
status: 1,
|
||||||
|
sort: 3,
|
||||||
|
createTime: "2021-05-26 15:49:05",
|
||||||
|
updateTime: "2019-05-05 16:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "系统管理员1",
|
||||||
|
code: "ADMIN1",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "系统管理员2",
|
||||||
|
code: "ADMIN2",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "系统管理员3",
|
||||||
|
code: "ADMIN3",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: "系统管理员4",
|
||||||
|
code: "ADMIN4",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: "系统管理员5",
|
||||||
|
code: "ADMIN5",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: "系统管理员6",
|
||||||
|
code: "ADMIN6",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: "2023-12-04 11:43:15",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: "系统管理员7",
|
||||||
|
code: "ADMIN7",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: "系统管理员8",
|
||||||
|
code: "ADMIN8",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 10,
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增角色
|
||||||
|
{
|
||||||
|
url: "roles",
|
||||||
|
method: ["POST"],
|
||||||
|
body({ body }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "新增角色" + body.name + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取角色表单数据
|
||||||
|
{
|
||||||
|
url: "roles/:id/form",
|
||||||
|
method: ["GET"],
|
||||||
|
body: ({ params }) => {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: roleMap[params.id],
|
||||||
|
msg: "一切ok",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 修改角色
|
||||||
|
{
|
||||||
|
url: "roles/:id",
|
||||||
|
method: ["PUT"],
|
||||||
|
body({ body }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "修改角色" + body.name + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除角色
|
||||||
|
{
|
||||||
|
url: "roles/:id",
|
||||||
|
method: ["DELETE"],
|
||||||
|
body({ params }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "删除角色" + params.id + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 获取角色拥有的菜单ID
|
||||||
|
{
|
||||||
|
url: "roles/:id/menuIds",
|
||||||
|
method: ["GET"],
|
||||||
|
body: ({}) => {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: [
|
||||||
|
1, 2, 31, 32, 33, 88, 3, 70, 71, 72, 4, 73, 75, 74, 5, 76, 77, 78, 6, 79, 81, 84, 85, 86,
|
||||||
|
87, 40, 41, 26, 30, 20, 21, 22, 23, 24, 89, 90, 91, 36, 37, 38, 39, 93, 94, 95, 97, 102,
|
||||||
|
89, 90, 91, 93, 94, 95, 97, 102, 103, 104,
|
||||||
|
],
|
||||||
|
msg: "一切ok",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 保存角色菜单
|
||||||
|
{
|
||||||
|
url: "roles/:id/menus",
|
||||||
|
method: ["PUT"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 角色映射表数据
|
||||||
|
const roleMap: Record<string, any> = {
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
name: "系统管理员",
|
||||||
|
code: "ADMIN",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
id: 3,
|
||||||
|
name: "访问游客",
|
||||||
|
code: "GUEST",
|
||||||
|
status: 1,
|
||||||
|
sort: 3,
|
||||||
|
createTime: "2021-05-26 15:49:05",
|
||||||
|
updateTime: "2019-05-05 16:00:00",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
id: 4,
|
||||||
|
name: "系统管理员1",
|
||||||
|
code: "ADMIN1",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
id: 5,
|
||||||
|
name: "系统管理员2",
|
||||||
|
code: "ADMIN2",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
6: {
|
||||||
|
id: 6,
|
||||||
|
name: "系统管理员3",
|
||||||
|
code: "ADMIN3",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
id: 7,
|
||||||
|
name: "系统管理员4",
|
||||||
|
code: "ADMIN4",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
8: {
|
||||||
|
id: 8,
|
||||||
|
name: "系统管理员5",
|
||||||
|
code: "ADMIN5",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
id: 9,
|
||||||
|
name: "系统管理员6",
|
||||||
|
code: "ADMIN6",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: "2023-12-04 11:43:15",
|
||||||
|
},
|
||||||
|
10: {
|
||||||
|
id: 10,
|
||||||
|
name: "系统管理员7",
|
||||||
|
code: "ADMIN7",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
11: {
|
||||||
|
id: 11,
|
||||||
|
name: "系统管理员8",
|
||||||
|
code: "ADMIN8",
|
||||||
|
status: 1,
|
||||||
|
sort: 2,
|
||||||
|
createTime: "2021-03-25 12:39:54",
|
||||||
|
updateTime: null,
|
||||||
|
},
|
||||||
|
};
|
251
front/mock/user.mock.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { defineMock } from "./base";
|
||||||
|
|
||||||
|
export default defineMock([
|
||||||
|
{
|
||||||
|
url: "users/me",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
userId: 2,
|
||||||
|
username: "admin",
|
||||||
|
nickname: "系统管理员",
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
roles: ["ADMIN"],
|
||||||
|
perms: [
|
||||||
|
"sys:user:query",
|
||||||
|
"sys:user:add",
|
||||||
|
"sys:user:edit",
|
||||||
|
"sys:user:delete",
|
||||||
|
"sys:user:import",
|
||||||
|
"sys:user:export",
|
||||||
|
"sys:user:reset-password",
|
||||||
|
|
||||||
|
"sys:role:query",
|
||||||
|
"sys:role:add",
|
||||||
|
"sys:role:edit",
|
||||||
|
"sys:role:delete",
|
||||||
|
|
||||||
|
"sys:dept:query",
|
||||||
|
"sys:dept:add",
|
||||||
|
"sys:dept:edit",
|
||||||
|
"sys:dept:delete",
|
||||||
|
|
||||||
|
"sys:menu:query",
|
||||||
|
"sys:menu:add",
|
||||||
|
"sys:menu:edit",
|
||||||
|
"sys:menu:delete",
|
||||||
|
|
||||||
|
"sys:dict:query",
|
||||||
|
"sys:dict:add",
|
||||||
|
"sys:dict:edit",
|
||||||
|
"sys:dict:delete",
|
||||||
|
"sys:dict:delete",
|
||||||
|
|
||||||
|
"sys:dict-item:query",
|
||||||
|
"sys:dict-item:add",
|
||||||
|
"sys:dict-item:edit",
|
||||||
|
"sys:dict-item:delete",
|
||||||
|
|
||||||
|
"sys:notice:query",
|
||||||
|
"sys:notice:add",
|
||||||
|
"sys:notice:edit",
|
||||||
|
"sys:notice:delete",
|
||||||
|
"sys:notice:revoke",
|
||||||
|
"sys:notice:publish",
|
||||||
|
|
||||||
|
"sys:config:query",
|
||||||
|
"sys:config:add",
|
||||||
|
"sys:config:update",
|
||||||
|
"sys:config:delete",
|
||||||
|
"sys:config:refresh",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "users/page",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "admin",
|
||||||
|
nickname: "系统管理员",
|
||||||
|
mobile: "17621210366",
|
||||||
|
gender: 1,
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
email: "",
|
||||||
|
status: 1,
|
||||||
|
deptId: 1,
|
||||||
|
roleIds: [2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
username: "test",
|
||||||
|
nickname: "测试小用户",
|
||||||
|
mobile: "17621210366",
|
||||||
|
gender: 1,
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
email: "youlaitech@163.com",
|
||||||
|
status: 1,
|
||||||
|
deptId: 3,
|
||||||
|
roleIds: [3],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
msg: "一切ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
{
|
||||||
|
url: "users",
|
||||||
|
method: ["POST"],
|
||||||
|
body({ body }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "新增用户" + body.nickname + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取用户表单数据
|
||||||
|
{
|
||||||
|
url: "users/:userId/form",
|
||||||
|
method: ["GET"],
|
||||||
|
body: ({ params }) => {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: userMap[params.userId],
|
||||||
|
msg: "一切ok",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 修改用户
|
||||||
|
{
|
||||||
|
url: "users/:userId",
|
||||||
|
method: ["PUT"],
|
||||||
|
body({ body }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "修改用户" + body.nickname + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
{
|
||||||
|
url: "users/:userId",
|
||||||
|
method: ["DELETE"],
|
||||||
|
body({ params }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "删除用户" + params.id + "成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
{
|
||||||
|
url: "users/:userId/password/reset",
|
||||||
|
method: ["PUT"],
|
||||||
|
body({ query }) {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "重置密码成功,新密码为:" + query.password,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导出Excel
|
||||||
|
{
|
||||||
|
url: "users/_export",
|
||||||
|
method: ["GET"],
|
||||||
|
headers: {
|
||||||
|
"Content-Disposition": "attachment; filename=%E7%94%A8%E6%88%B7%E5%88%97%E8%A1%A8.xlsx",
|
||||||
|
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "users/profile",
|
||||||
|
method: ["GET"],
|
||||||
|
body: {
|
||||||
|
code: "00000",
|
||||||
|
data: {
|
||||||
|
id: 2,
|
||||||
|
username: "admin",
|
||||||
|
nickname: "系统管理员",
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
gender: 1,
|
||||||
|
mobile: "17621210366",
|
||||||
|
email: null,
|
||||||
|
deptName: "有来技术",
|
||||||
|
roleNames: "系统管理员",
|
||||||
|
createTime: "2019-10-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "users/profile",
|
||||||
|
method: ["PUT"],
|
||||||
|
body() {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "修改个人信息成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
url: "users/password",
|
||||||
|
method: ["PUT"],
|
||||||
|
body() {
|
||||||
|
return {
|
||||||
|
code: "00000",
|
||||||
|
data: null,
|
||||||
|
msg: "修改密码成功",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 用户映射表数据
|
||||||
|
const userMap: Record<string, any> = {
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
username: "admin",
|
||||||
|
nickname: "系统管理员",
|
||||||
|
mobile: "17621210366",
|
||||||
|
gender: 1,
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
email: "",
|
||||||
|
status: 1,
|
||||||
|
deptId: 1,
|
||||||
|
roleIds: [2],
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
id: 3,
|
||||||
|
username: "test",
|
||||||
|
nickname: "测试小用户",
|
||||||
|
mobile: "17621210366",
|
||||||
|
gender: 1,
|
||||||
|
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||||
|
email: "youlaitech@163.com",
|
||||||
|
status: 1,
|
||||||
|
deptId: 3,
|
||||||
|
roleIds: [3],
|
||||||
|
},
|
||||||
|
};
|
1847
front/package-lock.json
generated
@ -10,14 +10,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@devui-design/icons": "^1.4.0",
|
"@devui-design/icons": "^1.4.0",
|
||||||
"@fullcalendar/core": "^6.1.17",
|
"axios": "^1.9.0",
|
||||||
"@fullcalendar/daygrid": "^6.1.17",
|
|
||||||
"@fullcalendar/interaction": "^6.1.17",
|
|
||||||
"@fullcalendar/timegrid": "^6.1.17",
|
|
||||||
"@fullcalendar/vue3": "^6.1.17",
|
|
||||||
"bootstrap": "^5.3.5",
|
"bootstrap": "^5.3.5",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"devui-theme": "^0.0.7",
|
"devui-theme": "^0.0.7",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^3.0.2",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"stompjs": "^2.3.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-devui": "^1.6.32",
|
"vue-devui": "^1.6.32",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
@ -25,9 +26,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@types/node": "^22.15.3",
|
||||||
|
"@types/qs": "^6.9.18",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"sass-embedded": "^1.86.3",
|
"sass-embedded": "^1.86.3",
|
||||||
|
"unplugin-auto-import": "^19.1.2",
|
||||||
"vite": "^6.2.4",
|
"vite": "^6.2.4",
|
||||||
|
"vite-plugin-mock-dev-server": "^1.8.5",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2"
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<script src = "./vender/codemirror.js"></script>
|
<script src = "./vender/codemirror.js"></script>
|
||||||
<script src = "./vender/fabric.js"></script>
|
<script src = "./vender/fabric.js"></script>
|
||||||
<!-- 局域网环境, 请下载 editor.js替换以下路径 -->
|
<!-- 局域网环境, 请下载 editor.js替换以下路径 -->
|
||||||
<script src="https://www.x-emr.cn/js/editor.js"></script>
|
<script src="./vender/editor.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
//配置项可以不设置,会使用默认设置
|
//配置项可以不设置,会使用默认设置
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
baseUrl: 'http://localhost:81/src',
|
baseUrl: 'http://localhost:81/src',
|
||||||
paths: {'editor': 'editor'}
|
paths: {'editor': 'vender/editor'}
|
||||||
})
|
})
|
||||||
|
|
||||||
require(['editor'], function () {
|
require(['public/vender/editor.js'], function () {
|
||||||
|
|
||||||
//配置项可以不设置,会使用默认设置
|
//配置项可以不设置,会使用默认设置
|
||||||
let option = {
|
let option = {
|
||||||
|
1
front/public/vender/editor.js
Normal file
@ -1,247 +1,11 @@
|
|||||||
<script setup>
|
|
||||||
import {reactive, ref, shallowRef} from 'vue'
|
|
||||||
import { ThemeServiceInit, infinityTheme } from 'devui-theme';
|
|
||||||
|
|
||||||
import Menu from './Menu.vue'
|
|
||||||
|
|
||||||
|
|
||||||
// 使用无限主题
|
|
||||||
ThemeServiceInit({ infinityTheme }, 'infinityTheme');
|
|
||||||
|
|
||||||
// // 当前激活的菜单项
|
|
||||||
// const activeMenu = ref('item1')
|
|
||||||
|
|
||||||
// // 当前显示的组件
|
|
||||||
// const currentComponent = shallowRef(HomePage)
|
|
||||||
|
|
||||||
// 组件映射表
|
|
||||||
// const componentMap = {
|
|
||||||
// 'item1': HomePage,
|
|
||||||
// 'menjizhen-item': menjizhenItemView,
|
|
||||||
// 'notFoundPage': NotFoundPage,
|
|
||||||
// // 其他菜单项对应的组件...
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 处理菜单选择
|
|
||||||
// const handleMenuSelect = (key) => {
|
|
||||||
// activeMenu.value = key["key"]
|
|
||||||
// if (componentMap[key["key"]]) {
|
|
||||||
// currentComponent.value = componentMap[key["key"]]
|
|
||||||
// } else {
|
|
||||||
// console.warn(`未找到菜单项 ${key} 对应的组件`)
|
|
||||||
// console.warn(key["key"])
|
|
||||||
// currentComponent.value = NotFoundPage
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
const menu = ref(null)
|
|
||||||
const toggleClick = function() {
|
|
||||||
let menu = document.querySelector('.menu-aside')
|
|
||||||
|
|
||||||
menu.style.transition = menu.style.transition || 'all 0.3s ease-in-out'
|
|
||||||
|
|
||||||
if (menu.style.display == 'none') {
|
|
||||||
menu.style.display = 'block'
|
|
||||||
} else {
|
|
||||||
menu.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<template>
|
<template>
|
||||||
<d-layout style="width: 100vw; ">
|
<div id="app">
|
||||||
<!-- 顶栏 -->
|
<router-view />
|
||||||
<d-header style="position: fixed; width: 100%; z-index: 100;background: #fff;height: 56px;box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
|
||||||
<div class="nav-collapse i icon-nav-collapse me-4 ms-4" @click="toggleClick()"></div>
|
|
||||||
<span class="avatar-demo-2" style="position: relative;
|
|
||||||
text-align: right;">
|
|
||||||
<d-avatar name="张医生" :width="28" :height="28" class="profile"/>
|
|
||||||
<span class="name" style="margin-left: 10px;">张医生</span>
|
|
||||||
</span>
|
|
||||||
<!-- <d-breadcrumb :source="source" style="display: inline-flex;text-align: center; position: relative; overflow: hidden; width: auto; margin-left:180px" />-->
|
|
||||||
|
|
||||||
<div style="float: right;
|
|
||||||
text-align: right;
|
|
||||||
position: fixed;
|
|
||||||
display: inline-block;
|
|
||||||
right: 4rem;">
|
|
||||||
<span>
|
|
||||||
<d-search class="mt-3"
|
|
||||||
size="sm"
|
|
||||||
style="
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: right;
|
|
||||||
width: 200px"
|
|
||||||
is-keyup-search
|
|
||||||
:delay="1000"
|
|
||||||
@search="onSearch">
|
|
||||||
</d-search>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
position: relative;
|
|
||||||
text-align: right;
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-top: 1rem;"
|
|
||||||
>
|
|
||||||
<d-icon name="feedback"/>
|
|
||||||
<a style="color:#54dc35 ;">
|
|
||||||
<d-badge :count="100" status="info" class="badge-item">未读消息</d-badge>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
position: relative;
|
|
||||||
text-align: right;
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-top: 1rem;">
|
|
||||||
<a href="/logout" style="color:rgba(0,0,0,0.63)">注销</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</d-header>
|
|
||||||
<!-- 内层布局:侧边 + 主区 -->
|
|
||||||
<d-layout style=" margin-top: 64px; width: 100%;" > <!-- 添加顶部间距,避免被header遮挡 -->
|
|
||||||
<!-- 侧边栏 - 添加fixed样式 -->
|
|
||||||
<d-aside style="
|
|
||||||
position: relative;
|
|
||||||
z-index: 99;"
|
|
||||||
class="menu-aside"
|
|
||||||
>
|
|
||||||
<!-- <d-menu-->
|
|
||||||
<!-- mode="vertical"-->
|
|
||||||
<!-- :default-select-keys="['item1']"-->
|
|
||||||
<!-- width="256px"-->
|
|
||||||
<!-- @select="handleMenuSelect"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <!– 首页 –>-->
|
|
||||||
<!-- <d-menu-item key="item1">-->
|
|
||||||
<!-- <template #icon><i class="icon-homepage"></i></template>-->
|
|
||||||
<!-- <span>首页</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- <!– 诊疗工作站 –>-->
|
|
||||||
<!-- <d-sub-menu title="诊疗工作站" key="zl-station">-->
|
|
||||||
<!-- <template #icon><i class="icon-system"></i></template>-->
|
|
||||||
<!-- <d-menu-item key="menjizhen-item"><span>门急诊医生站</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="zhuyuan-item"><span>住院医生站</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="rjssmz-item"><span>日间手术门诊工作站</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="rjsszy-item"><span>日间手术住院工作站</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="hzjlgl-item"><span>会诊记录管理</span></d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <!– 病历管理 –>-->
|
|
||||||
<!-- <d-sub-menu title="病历管理" key="bl-manage">-->
|
|
||||||
<!-- <template #icon><i class="icon-system"></i></template>-->
|
|
||||||
<!-- <d-menu-item key="cyblbj-item"><span>出院病历编辑</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="cybldy-item"><span>出院病历打印</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="mzbljs-item"><span>门诊病历检索</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="zybljs-item"><span>住院病历检索</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="blxgjl-item"><span>病历修改记录</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="blfcxf-item"><span>病历封存解封</span></d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="bljs-item"><span>病历解锁</span></d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <d-sub-menu title="我的申请" key="my-apply">-->
|
|
||||||
<!-- <template #icon>-->
|
|
||||||
<!-- <i class="icon-system"></i>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- <d-menu-item key="my-apply-item">-->
|
|
||||||
<!-- <span>我的申请</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <d-sub-menu title="我的审批" key="my-approve">-->
|
|
||||||
<!-- <template #icon>-->
|
|
||||||
<!-- <i class="icon-system"></i>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- <d-menu-item key="my-approve-item">-->
|
|
||||||
<!-- <span>我的审批</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <d-sub-menu title="个人质检" key="personal-check">-->
|
|
||||||
<!-- <template #icon>-->
|
|
||||||
<!-- <i class="icon-system"></i>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <d-sub-menu title="质控管理" key="quality-control">-->
|
|
||||||
<!-- <template #icon>-->
|
|
||||||
<!-- <i class="icon-system"></i>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- <d-menu-item key="quality-control-item">-->
|
|
||||||
<!-- <span>质控管理</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- <d-sub-menu title="系统管理" key="system-manage">-->
|
|
||||||
<!-- <template #icon>-->
|
|
||||||
<!-- <i class="icon-system"></i>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- <d-menu-item key="user-manage">-->
|
|
||||||
<!-- <span>用户管理</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="role-manage">-->
|
|
||||||
<!-- <span>角色管理</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- <d-menu-item key="permission-manage">-->
|
|
||||||
<!-- <span>权限管理</span>-->
|
|
||||||
<!-- </d-menu-item>-->
|
|
||||||
<!-- </d-sub-menu>-->
|
|
||||||
<!-- </d-menu>-->
|
|
||||||
<!-- 使用独立组件-->
|
|
||||||
<Menu></Menu>
|
|
||||||
</d-aside>
|
|
||||||
<!-- 主显示区 - 添加左侧边距 -->
|
|
||||||
<d-content
|
|
||||||
style="
|
|
||||||
padding: 16px;
|
|
||||||
overflow-x: hidden; /* 禁止内容溢出 */
|
|
||||||
">
|
|
||||||
<!-- <component :is="currentComponent"/>-->
|
|
||||||
<router-view></router-view>
|
|
||||||
</d-content>
|
|
||||||
</d-layout>
|
|
||||||
<d-footer style="position: fixed; bottom: 0; width: 100%; z-index: 100;background: #fff;height: 16px;">
|
|
||||||
<div style="text-align: center; padding: 8px;">
|
|
||||||
<img src="../assets/logo.png" alt="Logo" class="logo" />
|
|
||||||
<p style="font-size: 12px; color: #999;">© 2023 Your Company. All rights reserved.</p>
|
|
||||||
</div>
|
|
||||||
</d-footer>
|
|
||||||
</d-layout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.profile {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 在CSS中定义可变字体 */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Sans SC';
|
|
||||||
src: local('src/assets/NotoSansSC-VariableFont_wght.ttf');/* 先尝试本地已有字体 */
|
|
||||||
font-weight: 100 900; /* 定义支持的权重范围 */
|
|
||||||
font-stretch: 75% 125%; /* 定义支持的宽度范围 */
|
|
||||||
font-style: oblique 0deg 20deg; /* 可选的斜体范围 */
|
|
||||||
font-display: swap; /* 优化加载体验 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-collapse{
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.nav-collapse:hover{
|
|
||||||
cursor: pointer;
|
|
||||||
color: rgb(126, 126, 126);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
86
front/src/api/auth.api.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const AUTH_BASE_URL = "/api/v1/auth";
|
||||||
|
|
||||||
|
const AuthAPI = {
|
||||||
|
/** 登录接口*/
|
||||||
|
login(data: LoginFormData) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("username", data.username);
|
||||||
|
formData.append("password", data.password);
|
||||||
|
formData.append("captchaKey", data.captchaKey);
|
||||||
|
formData.append("captchaCode", data.captchaCode);
|
||||||
|
return request<any, LoginResult>({
|
||||||
|
url: `${AUTH_BASE_URL}/login`,
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 刷新 token 接口*/
|
||||||
|
refreshToken(refreshToken: string) {
|
||||||
|
return request<any, LoginResult>({
|
||||||
|
url: `${AUTH_BASE_URL}/refresh-token`,
|
||||||
|
method: "post",
|
||||||
|
params: { refreshToken: refreshToken },
|
||||||
|
headers: {
|
||||||
|
Authorization: "no-auth",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 注销登录接口 */
|
||||||
|
logout() {
|
||||||
|
return request({
|
||||||
|
url: `${AUTH_BASE_URL}/logout`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取验证码接口*/
|
||||||
|
getCaptcha() {
|
||||||
|
return request<any, CaptchaInfo>({
|
||||||
|
url: `${AUTH_BASE_URL}/captcha`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthAPI;
|
||||||
|
|
||||||
|
/** 登录表单数据 */
|
||||||
|
export interface LoginFormData {
|
||||||
|
/** 用户名 */
|
||||||
|
username: string;
|
||||||
|
/** 密码 */
|
||||||
|
password: string;
|
||||||
|
/** 验证码缓存key */
|
||||||
|
captchaKey: string;
|
||||||
|
/** 验证码 */
|
||||||
|
captchaCode: string;
|
||||||
|
/** 记住我 */
|
||||||
|
rememberMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录响应 */
|
||||||
|
export interface LoginResult {
|
||||||
|
/** 访问令牌 */
|
||||||
|
accessToken: string;
|
||||||
|
/** 刷新令牌 */
|
||||||
|
refreshToken: string;
|
||||||
|
/** 令牌类型 */
|
||||||
|
tokenType: string;
|
||||||
|
/** 过期时间(秒) */
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码信息 */
|
||||||
|
export interface CaptchaInfo {
|
||||||
|
/** 验证码缓存key */
|
||||||
|
captchaKey: string;
|
||||||
|
/** 验证码图片Base64字符串 */
|
||||||
|
captchaBase64: string;
|
||||||
|
}
|
191
front/src/api/codegen.api.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const GENERATOR_BASE_URL = "/api/v1/codegen";
|
||||||
|
|
||||||
|
const GeneratorAPI = {
|
||||||
|
/** 获取数据表分页列表 */
|
||||||
|
getTablePage(params: TablePageQuery) {
|
||||||
|
return request<any, PageResult<TablePageVO[]>>({
|
||||||
|
url: `${GENERATOR_BASE_URL}/table/page`,
|
||||||
|
method: "get",
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取代码生成配置 */
|
||||||
|
getGenConfig(tableName: string) {
|
||||||
|
return request<any, GenConfigForm>({
|
||||||
|
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取代码生成配置 */
|
||||||
|
saveGenConfig(tableName: string, data: GenConfigForm) {
|
||||||
|
return request({
|
||||||
|
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取代码生成预览数据 */
|
||||||
|
getPreviewData(tableName: string) {
|
||||||
|
return request<any, GeneratorPreviewVO[]>({
|
||||||
|
url: `${GENERATOR_BASE_URL}/${tableName}/preview`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 重置代码生成配置 */
|
||||||
|
resetGenConfig(tableName: string) {
|
||||||
|
return request({
|
||||||
|
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 ZIP 文件
|
||||||
|
* @param url
|
||||||
|
* @param fileName
|
||||||
|
*/
|
||||||
|
download(tableName: string) {
|
||||||
|
return request({
|
||||||
|
url: `${GENERATOR_BASE_URL}/${tableName}/download`,
|
||||||
|
method: "get",
|
||||||
|
responseType: "blob",
|
||||||
|
}).then((response) => {
|
||||||
|
const fileName = decodeURI(
|
||||||
|
response.headers["content-disposition"].split(";")[1].split("=")[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], { type: "application/zip" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneratorAPI;
|
||||||
|
|
||||||
|
/** 代码生成预览对象 */
|
||||||
|
export interface GeneratorPreviewVO {
|
||||||
|
/** 文件生成路径 */
|
||||||
|
path: string;
|
||||||
|
/** 文件名称 */
|
||||||
|
fileName: string;
|
||||||
|
/** 文件内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据表分页查询参数 */
|
||||||
|
export interface TablePageQuery extends PageQuery {
|
||||||
|
/** 关键字(表名) */
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据表分页对象 */
|
||||||
|
export interface TablePageVO {
|
||||||
|
/** 表名称 */
|
||||||
|
tableName: string;
|
||||||
|
|
||||||
|
/** 表描述 */
|
||||||
|
tableComment: string;
|
||||||
|
|
||||||
|
/** 存储引擎 */
|
||||||
|
engine: string;
|
||||||
|
|
||||||
|
/** 字符集排序规则 */
|
||||||
|
tableCollation: string;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代码生成配置表单 */
|
||||||
|
export interface GenConfigForm {
|
||||||
|
/** 主键 */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** 表名 */
|
||||||
|
tableName?: string;
|
||||||
|
|
||||||
|
/** 业务名 */
|
||||||
|
businessName?: string;
|
||||||
|
|
||||||
|
/** 模块名 */
|
||||||
|
moduleName?: string;
|
||||||
|
|
||||||
|
/** 包名 */
|
||||||
|
packageName?: string;
|
||||||
|
|
||||||
|
/** 实体名 */
|
||||||
|
entityName?: string;
|
||||||
|
|
||||||
|
/** 作者 */
|
||||||
|
author?: string;
|
||||||
|
|
||||||
|
/** 上级菜单 */
|
||||||
|
parentMenuId?: string;
|
||||||
|
|
||||||
|
/** 后端应用名 */
|
||||||
|
backendAppName?: string;
|
||||||
|
/** 前端应用名 */
|
||||||
|
frontendAppName?: string;
|
||||||
|
|
||||||
|
/** 字段配置列表 */
|
||||||
|
fieldConfigs?: FieldConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字段配置 */
|
||||||
|
export interface FieldConfig {
|
||||||
|
/** 主键 */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** 列名 */
|
||||||
|
columnName?: string;
|
||||||
|
|
||||||
|
/** 列类型 */
|
||||||
|
columnType?: string;
|
||||||
|
|
||||||
|
/** 字段名 */
|
||||||
|
fieldName?: string;
|
||||||
|
|
||||||
|
/** 字段类型 */
|
||||||
|
fieldType?: string;
|
||||||
|
|
||||||
|
/** 字段描述 */
|
||||||
|
fieldComment?: string;
|
||||||
|
|
||||||
|
/** 是否在列表显示 */
|
||||||
|
isShowInList?: number;
|
||||||
|
|
||||||
|
/** 是否在表单显示 */
|
||||||
|
isShowInForm?: number;
|
||||||
|
|
||||||
|
/** 是否在查询条件显示 */
|
||||||
|
isShowInQuery?: number;
|
||||||
|
|
||||||
|
/** 是否必填 */
|
||||||
|
isRequired?: number;
|
||||||
|
|
||||||
|
/** 表单类型 */
|
||||||
|
formType?: number;
|
||||||
|
|
||||||
|
/** 查询类型 */
|
||||||
|
queryType?: number;
|
||||||
|
|
||||||
|
/** 字段长度 */
|
||||||
|
maxLength?: number;
|
||||||
|
|
||||||
|
/** 字段排序 */
|
||||||
|
fieldSort?: number;
|
||||||
|
|
||||||
|
/** 字典类型 */
|
||||||
|
dictType?: string;
|
||||||
|
}
|
81
front/src/api/file.api.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const FileAPI = {
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*
|
||||||
|
* @param formData
|
||||||
|
*/
|
||||||
|
upload(formData: FormData) {
|
||||||
|
return request<any, FileInfo>({
|
||||||
|
url: "/api/v1/files",
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*/
|
||||||
|
uploadFile(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
return request<any, FileInfo>({
|
||||||
|
url: "/api/v1/files",
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
*
|
||||||
|
* @param filePath 文件完整路径
|
||||||
|
*/
|
||||||
|
delete(filePath?: string) {
|
||||||
|
return request({
|
||||||
|
url: "/api/v1/files",
|
||||||
|
method: "delete",
|
||||||
|
params: { filePath: filePath },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
* @param url
|
||||||
|
* @param fileName
|
||||||
|
*/
|
||||||
|
download(url: string, fileName?: string) {
|
||||||
|
return request({
|
||||||
|
url: url,
|
||||||
|
method: "get",
|
||||||
|
responseType: "blob",
|
||||||
|
}).then((res) => {
|
||||||
|
const blob = new Blob([res.data]);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName || "下载文件";
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileAPI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件API类型声明
|
||||||
|
*/
|
||||||
|
export interface FileInfo {
|
||||||
|
/** 文件名 */
|
||||||
|
name: string;
|
||||||
|
/** 文件路径 */
|
||||||
|
url: string;
|
||||||
|
}
|
95
front/src/api/system/config.api.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const CONFIG_BASE_URL = "/api/v1/config";
|
||||||
|
|
||||||
|
const ConfigAPI = {
|
||||||
|
/** 系统配置分页 */
|
||||||
|
getPage(queryParams?: ConfigPageQuery) {
|
||||||
|
return request<any, PageResult<ConfigPageVO[]>>({
|
||||||
|
url: `${CONFIG_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 系统配置表单数据 */
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, ConfigForm>({
|
||||||
|
url: `${CONFIG_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 新增系统配置 */
|
||||||
|
create(data: ConfigForm) {
|
||||||
|
return request({
|
||||||
|
url: `${CONFIG_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 更新系统配置 */
|
||||||
|
update(id: string, data: ConfigForm) {
|
||||||
|
return request({
|
||||||
|
url: `${CONFIG_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除系统配置
|
||||||
|
*
|
||||||
|
* @param ids 系统配置ID
|
||||||
|
*/
|
||||||
|
deleteById(id: string) {
|
||||||
|
return request({
|
||||||
|
url: `${CONFIG_BASE_URL}/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshCache() {
|
||||||
|
return request({
|
||||||
|
url: `${CONFIG_BASE_URL}/refresh`,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigAPI;
|
||||||
|
|
||||||
|
/** $系统配置分页查询参数 */
|
||||||
|
export interface ConfigPageQuery extends PageQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统配置表单对象 */
|
||||||
|
export interface ConfigForm {
|
||||||
|
/** 主键 */
|
||||||
|
id?: string;
|
||||||
|
/** 配置名称 */
|
||||||
|
configName?: string;
|
||||||
|
/** 配置键 */
|
||||||
|
configKey?: string;
|
||||||
|
/** 配置值 */
|
||||||
|
configValue?: string;
|
||||||
|
/** 描述、备注 */
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统配置分页对象 */
|
||||||
|
export interface ConfigPageVO {
|
||||||
|
/** 主键 */
|
||||||
|
id?: string;
|
||||||
|
/** 配置名称 */
|
||||||
|
configName?: string;
|
||||||
|
/** 配置键 */
|
||||||
|
configKey?: string;
|
||||||
|
/** 配置值 */
|
||||||
|
configValue?: string;
|
||||||
|
/** 描述、备注 */
|
||||||
|
remark?: string;
|
||||||
|
}
|
130
front/src/api/system/dept.api.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const DEPT_BASE_URL = "/api/v1/dept";
|
||||||
|
|
||||||
|
const DeptAPI = {
|
||||||
|
/**
|
||||||
|
* 获取部门列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数(可选)
|
||||||
|
* @returns 部门树形表格数据
|
||||||
|
*/
|
||||||
|
getList(queryParams?: DeptQuery) {
|
||||||
|
return request<any, DeptVO[]>({
|
||||||
|
url: `${DEPT_BASE_URL}`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取部门下拉列表 */
|
||||||
|
getOptions() {
|
||||||
|
return request<any, OptionType[]>({
|
||||||
|
url: `${DEPT_BASE_URL}/options`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门表单数据
|
||||||
|
*
|
||||||
|
* @param id 部门ID
|
||||||
|
* @returns 部门表单数据
|
||||||
|
*/
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, DeptForm>({
|
||||||
|
url: `${DEPT_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增部门
|
||||||
|
*
|
||||||
|
* @param data 部门表单数据
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
create(data: DeptForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DEPT_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改部门
|
||||||
|
*
|
||||||
|
* @param id 部门ID
|
||||||
|
* @param data 部门表单数据
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
update(id: string, data: DeptForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DEPT_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除部门
|
||||||
|
*
|
||||||
|
* @param ids 部门ID,多个以英文逗号(,)分隔
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
deleteByIds(ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DEPT_BASE_URL}/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeptAPI;
|
||||||
|
|
||||||
|
/** 部门查询参数 */
|
||||||
|
export interface DeptQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
/** 状态 */
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门类型 */
|
||||||
|
export interface DeptVO {
|
||||||
|
/** 子部门 */
|
||||||
|
children?: DeptVO[];
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: Date;
|
||||||
|
/** 部门ID */
|
||||||
|
id?: string;
|
||||||
|
/** 部门名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 部门编号 */
|
||||||
|
code?: string;
|
||||||
|
/** 父部门ID */
|
||||||
|
parentid?: string;
|
||||||
|
/** 排序 */
|
||||||
|
sort?: number;
|
||||||
|
/** 状态(1:启用;0:禁用) */
|
||||||
|
status?: number;
|
||||||
|
/** 修改时间 */
|
||||||
|
updateTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门表单类型 */
|
||||||
|
export interface DeptForm {
|
||||||
|
/** 部门ID(新增不填) */
|
||||||
|
id?: string;
|
||||||
|
/** 部门名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 部门编号 */
|
||||||
|
code?: string;
|
||||||
|
/** 父部门ID */
|
||||||
|
parentId: string;
|
||||||
|
/** 排序 */
|
||||||
|
sort?: number;
|
||||||
|
/** 状态(1:启用;0:禁用) */
|
||||||
|
status?: number;
|
||||||
|
}
|
310
front/src/api/system/dict.api.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const DICT_BASE_URL = "/api/v1/dicts";
|
||||||
|
|
||||||
|
const DictAPI = {
|
||||||
|
//---------------------------------------------------
|
||||||
|
// 字典相关接口
|
||||||
|
//---------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典分页列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
* @returns 字典分页结果
|
||||||
|
*/
|
||||||
|
getPage(queryParams: DictPageQuery) {
|
||||||
|
return request<any, PageResult<DictPageVO[]>>({
|
||||||
|
url: `${DICT_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典列表
|
||||||
|
*
|
||||||
|
* @returns 字典列表
|
||||||
|
*/
|
||||||
|
getList() {
|
||||||
|
return request<any, OptionType[]>({
|
||||||
|
url: `${DICT_BASE_URL}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典表单数据
|
||||||
|
*
|
||||||
|
* @param id 字典ID
|
||||||
|
* @returns 字典表单数据
|
||||||
|
*/
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, DictForm>({
|
||||||
|
url: `${DICT_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增字典
|
||||||
|
*
|
||||||
|
* @param data 字典表单数据
|
||||||
|
*/
|
||||||
|
create(data: DictForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改字典
|
||||||
|
*
|
||||||
|
* @param id 字典ID
|
||||||
|
* @param data 字典表单数据
|
||||||
|
*/
|
||||||
|
update(id: string, data: DictForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除字典
|
||||||
|
*
|
||||||
|
* @param ids 字典ID,多个以英文逗号(,)分隔
|
||||||
|
*/
|
||||||
|
deleteByIds(ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//---------------------------------------------------
|
||||||
|
// 字典项相关接口
|
||||||
|
//---------------------------------------------------
|
||||||
|
/**
|
||||||
|
* 获取字典分页列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
* @returns 字典分页结果
|
||||||
|
*/
|
||||||
|
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
|
||||||
|
return request<any, PageResult<DictItemPageVO[]>>({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典项列表
|
||||||
|
*/
|
||||||
|
getDictItems(dictCode: string) {
|
||||||
|
return request<any, DictItemOption[]>({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增字典项
|
||||||
|
*/
|
||||||
|
createDictItem(dictCode: string, data: DictItemForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典项表单数据
|
||||||
|
*
|
||||||
|
* @param id 字典项ID
|
||||||
|
* @returns 字典项表单数据
|
||||||
|
*/
|
||||||
|
getDictItemFormData(dictCode: string, id: string) {
|
||||||
|
return request<any, DictItemForm>({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改字典项
|
||||||
|
*/
|
||||||
|
updateDictItem(dictCode: string, id: string, data: DictItemForm) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除字典项
|
||||||
|
*/
|
||||||
|
deleteDictItems(dictCode: string, ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DictAPI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典查询参数
|
||||||
|
*/
|
||||||
|
export interface DictPageQuery extends PageQuery {
|
||||||
|
/**
|
||||||
|
* 关键字(字典名称/编码)
|
||||||
|
*/
|
||||||
|
keywords?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典状态(1:启用,0:禁用)
|
||||||
|
*/
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典分页对象
|
||||||
|
*/
|
||||||
|
export interface DictPageVO {
|
||||||
|
/**
|
||||||
|
* 字典ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 字典名称
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
dictCode: string;
|
||||||
|
/**
|
||||||
|
* 字典状态(1:启用,0:禁用)
|
||||||
|
*/
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典
|
||||||
|
*/
|
||||||
|
export interface DictForm {
|
||||||
|
/**
|
||||||
|
* 字典ID
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* 字典名称
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
dictCode?: string;
|
||||||
|
/**
|
||||||
|
* 字典状态(1-启用,0-禁用)
|
||||||
|
*/
|
||||||
|
status?: number;
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典查询参数
|
||||||
|
*/
|
||||||
|
export interface DictItemPageQuery extends PageQuery {
|
||||||
|
/** 关键字(字典数据值/标签) */
|
||||||
|
keywords?: string;
|
||||||
|
|
||||||
|
/** 字典编码 */
|
||||||
|
dictCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典分页对象
|
||||||
|
*/
|
||||||
|
export interface DictItemPageVO {
|
||||||
|
/**
|
||||||
|
* 字典ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
dictCode: string;
|
||||||
|
/**
|
||||||
|
* 字典数据值
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
/**
|
||||||
|
* 字典数据标签
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* 状态(1:启用,0:禁用)
|
||||||
|
*/
|
||||||
|
status: number;
|
||||||
|
/**
|
||||||
|
* 字典排序
|
||||||
|
*/
|
||||||
|
sort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典
|
||||||
|
*/
|
||||||
|
export interface DictItemForm {
|
||||||
|
/**
|
||||||
|
* 字典ID
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* 字典编码
|
||||||
|
*/
|
||||||
|
dictCode?: string;
|
||||||
|
/**
|
||||||
|
* 字典数据值
|
||||||
|
*/
|
||||||
|
value?: string;
|
||||||
|
/**
|
||||||
|
* 字典数据标签
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
/**
|
||||||
|
* 状态(1:启用,0:禁用)
|
||||||
|
*/
|
||||||
|
status?: number;
|
||||||
|
/**
|
||||||
|
* 字典排序
|
||||||
|
*/
|
||||||
|
sort?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签类型
|
||||||
|
*/
|
||||||
|
tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典项下拉选项
|
||||||
|
*/
|
||||||
|
export interface DictItemOption {
|
||||||
|
value: number | string;
|
||||||
|
label: string;
|
||||||
|
tagType?: "" | "success" | "info" | "warning" | "danger";
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
121
front/src/api/system/log.api.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const LOG_BASE_URL = "/api/v1/logs";
|
||||||
|
|
||||||
|
const LogAPI = {
|
||||||
|
/**
|
||||||
|
* 获取日志分页列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
*/
|
||||||
|
getPage(queryParams: LogPageQuery) {
|
||||||
|
return request<any, PageResult<LogPageVO[]>>({
|
||||||
|
url: `${LOG_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取访问趋势
|
||||||
|
*
|
||||||
|
* @param queryParams
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getVisitTrend(queryParams: VisitTrendQuery) {
|
||||||
|
return request<any, VisitTrendVO>({
|
||||||
|
url: `${LOG_BASE_URL}/visit-trend`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取访问统计
|
||||||
|
*
|
||||||
|
* @param queryParams
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getVisitStats() {
|
||||||
|
return request<any, VisitStatsVO>({
|
||||||
|
url: `${LOG_BASE_URL}/visit-stats`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogAPI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志分页查询对象
|
||||||
|
*/
|
||||||
|
export interface LogPageQuery extends PageQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
/** 操作时间 */
|
||||||
|
createTime?: [string, string];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统日志分页VO
|
||||||
|
*/
|
||||||
|
export interface LogPageVO {
|
||||||
|
/** 主键 */
|
||||||
|
id: string;
|
||||||
|
/** 日志模块 */
|
||||||
|
module: string;
|
||||||
|
/** 日志内容 */
|
||||||
|
content: string;
|
||||||
|
/** 请求路径 */
|
||||||
|
requestUri: string;
|
||||||
|
/** 请求方法 */
|
||||||
|
method: string;
|
||||||
|
/** IP 地址 */
|
||||||
|
ip: string;
|
||||||
|
/** 地区 */
|
||||||
|
region: string;
|
||||||
|
/** 浏览器 */
|
||||||
|
browser: string;
|
||||||
|
/** 终端系统 */
|
||||||
|
os: string;
|
||||||
|
/** 执行时间(毫秒) */
|
||||||
|
executionTime: number;
|
||||||
|
/** 操作人 */
|
||||||
|
operator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 访问趋势视图对象 */
|
||||||
|
export interface VisitTrendVO {
|
||||||
|
/** 日期列表 */
|
||||||
|
dates: string[];
|
||||||
|
/** 浏览量(PV) */
|
||||||
|
pvList: number[];
|
||||||
|
/** 访客数(UV) */
|
||||||
|
uvList: number[];
|
||||||
|
/** IP数 */
|
||||||
|
ipList: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 访问趋势查询参数 */
|
||||||
|
export interface VisitTrendQuery {
|
||||||
|
/** 开始日期 */
|
||||||
|
startDate: string;
|
||||||
|
/** 结束日期 */
|
||||||
|
endDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 访问统计 */
|
||||||
|
export interface VisitStatsVO {
|
||||||
|
/** 今日访客数(UV) */
|
||||||
|
todayUvCount: number;
|
||||||
|
/** 总访客数 */
|
||||||
|
totalUvCount: number;
|
||||||
|
/** 访客数同比增长率(相对于昨天同一时间段的增长率) */
|
||||||
|
uvGrowthRate: number;
|
||||||
|
/** 今日浏览量(PV) */
|
||||||
|
todayPvCount: number;
|
||||||
|
/** 总浏览量 */
|
||||||
|
totalPvCount: number;
|
||||||
|
/** 同比增长率(相对于昨天同一时间段的增长率) */
|
||||||
|
pvGrowthRate: number;
|
||||||
|
}
|
209
front/src/api/system/menu.api.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
// 菜单基础URL
|
||||||
|
const MENU_BASE_URL = "/api/v1/menus";
|
||||||
|
|
||||||
|
const MenuAPI = {
|
||||||
|
/**
|
||||||
|
* 获取当前用户的路由列表
|
||||||
|
* <p/>
|
||||||
|
* 无需传入角色,后端解析token获取角色自行判断是否拥有路由的权限
|
||||||
|
*
|
||||||
|
* @returns 路由列表
|
||||||
|
*/
|
||||||
|
getRoutes() {
|
||||||
|
return request<any, RouteVO[]>({
|
||||||
|
url: `${MENU_BASE_URL}/routes`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单树形列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
* @returns 菜单树形列表
|
||||||
|
*/
|
||||||
|
getList(queryParams: MenuQuery) {
|
||||||
|
return request<any, MenuVO[]>({
|
||||||
|
url: `${MENU_BASE_URL}`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单下拉数据源
|
||||||
|
*
|
||||||
|
* @returns 菜单下拉数据源
|
||||||
|
*/
|
||||||
|
getOptions(onlyParent?: boolean) {
|
||||||
|
return request<any, OptionType[]>({
|
||||||
|
url: `${MENU_BASE_URL}/options`,
|
||||||
|
method: "get",
|
||||||
|
params: { onlyParent: onlyParent },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单表单数据
|
||||||
|
*
|
||||||
|
* @param id 菜单ID
|
||||||
|
*/
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, MenuForm>({
|
||||||
|
url: `${MENU_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加菜单
|
||||||
|
*
|
||||||
|
* @param data 菜单表单数据
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
create(data: MenuForm) {
|
||||||
|
return request({
|
||||||
|
url: `${MENU_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改菜单
|
||||||
|
*
|
||||||
|
* @param id 菜单ID
|
||||||
|
* @param data 菜单表单数据
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
update(id: string, data: MenuForm) {
|
||||||
|
return request({
|
||||||
|
url: `${MENU_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除菜单
|
||||||
|
*
|
||||||
|
* @param id 菜单ID
|
||||||
|
* @returns 请求结果
|
||||||
|
*/
|
||||||
|
deleteById(id: string) {
|
||||||
|
return request({
|
||||||
|
url: `${MENU_BASE_URL}/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuAPI;
|
||||||
|
|
||||||
|
import type { MenuTypeEnum } from "@/enums/system/menu.enum";
|
||||||
|
|
||||||
|
/** 菜单查询参数 */
|
||||||
|
export interface MenuQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 菜单视图对象 */
|
||||||
|
export interface MenuVO {
|
||||||
|
/** 子菜单 */
|
||||||
|
children?: MenuVO[];
|
||||||
|
/** 组件路径 */
|
||||||
|
component?: string;
|
||||||
|
/** ICON */
|
||||||
|
icon?: string;
|
||||||
|
/** 菜单ID */
|
||||||
|
id?: string;
|
||||||
|
/** 菜单名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 父菜单ID */
|
||||||
|
parentId?: string;
|
||||||
|
/** 按钮权限标识 */
|
||||||
|
perm?: string;
|
||||||
|
/** 跳转路径 */
|
||||||
|
redirect?: string;
|
||||||
|
/** 路由名称 */
|
||||||
|
routeName?: string;
|
||||||
|
/** 路由相对路径 */
|
||||||
|
routePath?: string;
|
||||||
|
/** 菜单排序(数字越小排名越靠前) */
|
||||||
|
sort?: number;
|
||||||
|
/** 菜单 */
|
||||||
|
type?: MenuTypeEnum;
|
||||||
|
/** 菜单是否可见(1:显示;0:隐藏) */
|
||||||
|
visible?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 菜单表单对象 */
|
||||||
|
export interface MenuForm {
|
||||||
|
/** 菜单ID */
|
||||||
|
id?: string;
|
||||||
|
/** 父菜单ID */
|
||||||
|
parentId?: string;
|
||||||
|
/** 菜单名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 菜单是否可见(1-是 0-否) */
|
||||||
|
visible: number;
|
||||||
|
/** ICON */
|
||||||
|
icon?: string;
|
||||||
|
/** 排序 */
|
||||||
|
sort?: number;
|
||||||
|
/** 路由名称 */
|
||||||
|
routeName?: string;
|
||||||
|
/** 路由路径 */
|
||||||
|
routePath?: string;
|
||||||
|
/** 组件路径 */
|
||||||
|
component?: string;
|
||||||
|
/** 跳转路由路径 */
|
||||||
|
redirect?: string;
|
||||||
|
/** 菜单 */
|
||||||
|
type?: MenuTypeEnum;
|
||||||
|
/** 权限标识 */
|
||||||
|
perm?: string;
|
||||||
|
/** 【菜单】是否开启页面缓存 */
|
||||||
|
keepAlive?: number;
|
||||||
|
/** 【目录】只有一个子路由是否始终显示 */
|
||||||
|
alwaysShow?: number;
|
||||||
|
/** 参数 */
|
||||||
|
params?: KeyValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyValue {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RouteVO,路由对象 */
|
||||||
|
export interface RouteVO {
|
||||||
|
/** 子路由列表 */
|
||||||
|
children: RouteVO[];
|
||||||
|
/** 组件路径 */
|
||||||
|
component?: string;
|
||||||
|
/** 路由属性 */
|
||||||
|
meta?: Meta;
|
||||||
|
/** 路由名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 路由路径 */
|
||||||
|
path?: string;
|
||||||
|
/** 跳转链接 */
|
||||||
|
redirect?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Meta,路由属性 */
|
||||||
|
export interface Meta {
|
||||||
|
/** 【目录】只有一个子路由是否始终显示 */
|
||||||
|
alwaysShow?: boolean;
|
||||||
|
/** 是否隐藏(true-是 false-否) */
|
||||||
|
hidden?: boolean;
|
||||||
|
/** ICON */
|
||||||
|
icon?: string;
|
||||||
|
/** 【菜单】是否开启页面缓存 */
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/** 路由title */
|
||||||
|
title?: string;
|
||||||
|
}
|
199
front/src/api/system/notice.api.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const NOTICE_BASE_URL = "/api/v1/notices";
|
||||||
|
|
||||||
|
const NoticeAPI = {
|
||||||
|
/** 获取通知公告分页数据 */
|
||||||
|
getPage(queryParams?: NoticePageQuery) {
|
||||||
|
return request<any, PageResult<NoticePageVO[]>>({
|
||||||
|
url: `${NOTICE_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通知公告表单数据
|
||||||
|
*
|
||||||
|
* @param id 通知
|
||||||
|
* @returns 通知表单数据
|
||||||
|
*/
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, NoticeForm>({
|
||||||
|
url: `${NOTICE_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加通知公告
|
||||||
|
*
|
||||||
|
* @param data 通知表单数据
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
create(data: NoticeForm) {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新通知公告
|
||||||
|
*
|
||||||
|
* @param id 通知ID
|
||||||
|
* @param data 通知表单数据
|
||||||
|
*/
|
||||||
|
update(id: string, data: NoticeForm) {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除通知公告,多个以英文逗号(,)分割
|
||||||
|
*
|
||||||
|
* @param ids 通知公告ID字符串,多个以英文逗号(,)分割
|
||||||
|
*/
|
||||||
|
deleteByIds(ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布通知
|
||||||
|
*
|
||||||
|
* @param id 被发布的通知公告id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
publish(id: string) {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/${id}/publish`,
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回通知
|
||||||
|
*
|
||||||
|
* @param id 撤回的通知id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
revoke(id: string) {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/${id}/revoke`,
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 查看通知
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
getDetail(id: string) {
|
||||||
|
return request<any, NoticeDetailVO>({
|
||||||
|
url: `${NOTICE_BASE_URL}/${id}/detail`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 全部已读 */
|
||||||
|
readAll() {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/read-all`,
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取我的通知分页列表 */
|
||||||
|
getMyNoticePage(queryParams?: NoticePageQuery) {
|
||||||
|
return request<any, PageResult<NoticePageVO[]>>({
|
||||||
|
url: `${NOTICE_BASE_URL}/my-page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoticeAPI;
|
||||||
|
|
||||||
|
/** 通知公告分页查询参数 */
|
||||||
|
export interface NoticePageQuery extends PageQuery {
|
||||||
|
/** 标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 发布状态(0:未发布,1:已发布,-1:已撤回) */
|
||||||
|
publishStatus?: number;
|
||||||
|
|
||||||
|
isRead?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通知公告表单对象 */
|
||||||
|
export interface NoticeForm {
|
||||||
|
id?: string;
|
||||||
|
/** 通知标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 通知内容 */
|
||||||
|
content?: string;
|
||||||
|
/** 通知类型 */
|
||||||
|
type?: number;
|
||||||
|
/** 优先级(L:低,M:中,H:高) */
|
||||||
|
level?: string;
|
||||||
|
/** 目标类型(1-全体 2-指定) */
|
||||||
|
targetType?: number;
|
||||||
|
/** 目标ID合集,以,分割 */
|
||||||
|
targetUserIds?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通知公告分页对象 */
|
||||||
|
export interface NoticePageVO {
|
||||||
|
id: string;
|
||||||
|
/** 通知标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 通知内容 */
|
||||||
|
content?: string;
|
||||||
|
/** 通知类型 */
|
||||||
|
type?: number;
|
||||||
|
/** 发布人 */
|
||||||
|
publisherId?: bigint;
|
||||||
|
/** 优先级(0-低 1-中 2-高) */
|
||||||
|
priority?: number;
|
||||||
|
/** 目标类型(0-全体 1-指定) */
|
||||||
|
targetType?: number;
|
||||||
|
/** 发布状态(0-未发布 1已发布 2已撤回) */
|
||||||
|
publishStatus?: number;
|
||||||
|
/** 发布时间 */
|
||||||
|
publishTime?: Date;
|
||||||
|
/** 撤回时间 */
|
||||||
|
revokeTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeDetailVO {
|
||||||
|
/** 通知ID */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** 通知标题 */
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/** 通知内容 */
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
/** 通知类型 */
|
||||||
|
type?: number;
|
||||||
|
|
||||||
|
/** 发布人 */
|
||||||
|
publisherName?: string;
|
||||||
|
|
||||||
|
/** 优先级(L-低 M-中 H-高) */
|
||||||
|
level?: string;
|
||||||
|
|
||||||
|
/** 发布时间 */
|
||||||
|
publishTime?: Date;
|
||||||
|
|
||||||
|
/** 发布状态 */
|
||||||
|
publishStatus?: number;
|
||||||
|
}
|
138
front/src/api/system/role.api.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const ROLE_BASE_URL = "/api/v1/roles";
|
||||||
|
|
||||||
|
const RoleAPI = {
|
||||||
|
/** 获取角色分页数据 */
|
||||||
|
getPage(queryParams?: RolePageQuery) {
|
||||||
|
return request<any, PageResult<RolePageVO[]>>({
|
||||||
|
url: `${ROLE_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取角色下拉数据源 */
|
||||||
|
getOptions() {
|
||||||
|
return request<any, OptionType[]>({
|
||||||
|
url: `${ROLE_BASE_URL}/options`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取角色的菜单ID集合
|
||||||
|
*
|
||||||
|
* @param roleId 角色ID
|
||||||
|
* @returns 角色的菜单ID集合
|
||||||
|
*/
|
||||||
|
getRoleMenuIds(roleId: string) {
|
||||||
|
return request<any, string[]>({
|
||||||
|
url: `${ROLE_BASE_URL}/${roleId}/menuIds`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配菜单权限
|
||||||
|
*
|
||||||
|
* @param roleId 角色ID
|
||||||
|
* @param data 菜单ID集合
|
||||||
|
*/
|
||||||
|
updateRoleMenus(roleId: string, data: number[]) {
|
||||||
|
return request({
|
||||||
|
url: `${ROLE_BASE_URL}/${roleId}/menus`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色表单数据
|
||||||
|
*
|
||||||
|
* @param id 角色ID
|
||||||
|
* @returns 角色表单数据
|
||||||
|
*/
|
||||||
|
getFormData(id: string) {
|
||||||
|
return request<any, RoleForm>({
|
||||||
|
url: `${ROLE_BASE_URL}/${id}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 添加角色 */
|
||||||
|
create(data: RoleForm) {
|
||||||
|
return request({
|
||||||
|
url: `${ROLE_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
*
|
||||||
|
* @param id 角色ID
|
||||||
|
* @param data 角色表单数据
|
||||||
|
*/
|
||||||
|
update(id: string, data: RoleForm) {
|
||||||
|
return request({
|
||||||
|
url: `${ROLE_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除角色,多个以英文逗号(,)分割
|
||||||
|
*
|
||||||
|
* @param ids 角色ID字符串,多个以英文逗号(,)分割
|
||||||
|
*/
|
||||||
|
deleteByIds(ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${ROLE_BASE_URL}/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoleAPI;
|
||||||
|
|
||||||
|
/** 角色分页查询参数 */
|
||||||
|
export interface RolePageQuery extends PageQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 角色分页对象 */
|
||||||
|
export interface RolePageVO {
|
||||||
|
/** 角色ID */
|
||||||
|
id?: string;
|
||||||
|
/** 角色编码 */
|
||||||
|
code?: string;
|
||||||
|
/** 角色名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 排序 */
|
||||||
|
sort?: number;
|
||||||
|
/** 角色状态 */
|
||||||
|
status?: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: Date;
|
||||||
|
/** 修改时间 */
|
||||||
|
updateTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 角色表单对象 */
|
||||||
|
export interface RoleForm {
|
||||||
|
/** 角色ID */
|
||||||
|
id?: string;
|
||||||
|
/** 角色编码 */
|
||||||
|
code?: string;
|
||||||
|
/** 数据权限 */
|
||||||
|
dataScope?: number;
|
||||||
|
/** 角色名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 排序 */
|
||||||
|
sort?: number;
|
||||||
|
/** 角色状态(1-正常;0-停用) */
|
||||||
|
status?: number;
|
||||||
|
}
|
384
front/src/api/system/user.api.ts
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const USER_BASE_URL = "/api/v1/users";
|
||||||
|
|
||||||
|
const UserAPI = {
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
*
|
||||||
|
* @returns 登录用户昵称、头像信息,包括角色和权限
|
||||||
|
*/
|
||||||
|
getInfo() {
|
||||||
|
return request<any, UserInfo>({
|
||||||
|
url: `${USER_BASE_URL}/me`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户分页列表
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
*/
|
||||||
|
getPage(queryParams: UserPageQuery) {
|
||||||
|
return request<any, PageResult<UserPageVO[]>>({
|
||||||
|
url: `${USER_BASE_URL}/page`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户表单详情
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 用户表单详情
|
||||||
|
*/
|
||||||
|
getFormData(userId: string) {
|
||||||
|
return request<any, UserForm>({
|
||||||
|
url: `${USER_BASE_URL}/${userId}/form`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加用户
|
||||||
|
*
|
||||||
|
* @param data 用户表单数据
|
||||||
|
*/
|
||||||
|
create(data: UserForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}`,
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param data 用户表单数据
|
||||||
|
*/
|
||||||
|
update(id: string, data: UserForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户密码
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param password 新密码
|
||||||
|
*/
|
||||||
|
resetPassword(id: string, password: string) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/${id}/password/reset`,
|
||||||
|
method: "put",
|
||||||
|
params: { password: password },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除用户,多个以英文逗号(,)分割
|
||||||
|
*
|
||||||
|
* @param ids 用户ID字符串,多个以英文逗号(,)分割
|
||||||
|
*/
|
||||||
|
deleteByIds(ids: string) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/${ids}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 下载用户导入模板 */
|
||||||
|
downloadTemplate() {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/template`,
|
||||||
|
method: "get",
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出用户
|
||||||
|
*
|
||||||
|
* @param queryParams 查询参数
|
||||||
|
*/
|
||||||
|
export(queryParams: UserPageQuery) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/export`,
|
||||||
|
method: "get",
|
||||||
|
params: queryParams,
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入用户
|
||||||
|
*
|
||||||
|
* @param deptId 部门ID
|
||||||
|
* @param file 导入文件
|
||||||
|
*/
|
||||||
|
import(deptId: string, file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
return request<any, ExcelResult>({
|
||||||
|
url: `${USER_BASE_URL}/import`,
|
||||||
|
method: "post",
|
||||||
|
params: { deptId: deptId },
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取个人中心用户信息 */
|
||||||
|
getProfile() {
|
||||||
|
return request<any, UserProfileVO>({
|
||||||
|
url: `${USER_BASE_URL}/profile`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 修改个人中心用户信息 */
|
||||||
|
updateProfile(data: UserProfileForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/profile`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 修改个人中心用户密码 */
|
||||||
|
changePassword(data: PasswordChangeForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/password`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 发送短信验证码(绑定或更换手机号)*/
|
||||||
|
sendMobileCode(mobile: string) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/mobile/code`,
|
||||||
|
method: "post",
|
||||||
|
params: { mobile: mobile },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 绑定或更换手机号 */
|
||||||
|
bindOrChangeMobile(data: MobileUpdateForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/mobile`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 发送邮箱验证码(绑定或更换邮箱)*/
|
||||||
|
sendEmailCode(email: string) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/email/code`,
|
||||||
|
method: "post",
|
||||||
|
params: { email: email },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 绑定或更换邮箱 */
|
||||||
|
bindOrChangeEmail(data: EmailUpdateForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/email`,
|
||||||
|
method: "put",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户下拉列表
|
||||||
|
*/
|
||||||
|
getOptions() {
|
||||||
|
return request<any, OptionType[]>({
|
||||||
|
url: `${USER_BASE_URL}/options`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserAPI;
|
||||||
|
|
||||||
|
/** 登录用户信息 */
|
||||||
|
export interface UserInfo {
|
||||||
|
/** 用户ID */
|
||||||
|
userId?: string;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/** 昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
/** 头像URL */
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
/** 角色 */
|
||||||
|
roles: string[];
|
||||||
|
|
||||||
|
/** 权限 */
|
||||||
|
perms: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户分页查询对象
|
||||||
|
*/
|
||||||
|
export interface UserPageQuery extends PageQuery {
|
||||||
|
/** 搜索关键字 */
|
||||||
|
keywords?: string;
|
||||||
|
|
||||||
|
/** 用户状态 */
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
/** 部门ID */
|
||||||
|
deptId?: string;
|
||||||
|
|
||||||
|
/** 开始时间 */
|
||||||
|
createTime?: [string, string];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户分页对象 */
|
||||||
|
export interface UserPageVO {
|
||||||
|
/** 用户ID */
|
||||||
|
id: string;
|
||||||
|
/** 用户头像URL */
|
||||||
|
avatar?: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: Date;
|
||||||
|
/** 部门名称 */
|
||||||
|
deptName?: string;
|
||||||
|
/** 用户邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/** 性别 */
|
||||||
|
gender?: number;
|
||||||
|
/** 手机号 */
|
||||||
|
mobile?: string;
|
||||||
|
/** 用户昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||||
|
roleNames?: string;
|
||||||
|
/** 用户状态(1:启用;0:禁用) */
|
||||||
|
status?: number;
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户表单类型 */
|
||||||
|
export interface UserForm {
|
||||||
|
/** 用户ID */
|
||||||
|
id?: string;
|
||||||
|
/** 用户头像 */
|
||||||
|
avatar?: string;
|
||||||
|
/** 部门ID */
|
||||||
|
deptId?: string;
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/** 性别 */
|
||||||
|
gender?: number;
|
||||||
|
/** 手机号 */
|
||||||
|
mobile?: string;
|
||||||
|
/** 昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
/** 角色ID集合 */
|
||||||
|
roleIds?: number[];
|
||||||
|
/** 用户状态(1:正常;0:禁用) */
|
||||||
|
status?: number;
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人中心用户信息 */
|
||||||
|
export interface UserProfileVO {
|
||||||
|
/** 用户ID */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/** 昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
/** 头像URL */
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
/** 性别 */
|
||||||
|
gender?: number;
|
||||||
|
|
||||||
|
/** 手机号 */
|
||||||
|
mobile?: string;
|
||||||
|
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
/** 部门名称 */
|
||||||
|
deptName?: string;
|
||||||
|
|
||||||
|
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||||
|
roleNames?: string;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人中心用户信息表单 */
|
||||||
|
export interface UserProfileForm {
|
||||||
|
/** 用户ID */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/** 昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
/** 头像URL */
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
/** 性别 */
|
||||||
|
gender?: number;
|
||||||
|
|
||||||
|
/** 手机号 */
|
||||||
|
mobile?: string;
|
||||||
|
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改密码表单 */
|
||||||
|
export interface PasswordChangeForm {
|
||||||
|
/** 原密码 */
|
||||||
|
oldPassword?: string;
|
||||||
|
/** 新密码 */
|
||||||
|
newPassword?: string;
|
||||||
|
/** 确认新密码 */
|
||||||
|
confirmPassword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改手机表单 */
|
||||||
|
export interface MobileUpdateForm {
|
||||||
|
/** 手机号 */
|
||||||
|
mobile?: string;
|
||||||
|
/** 验证码 */
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改邮箱表单 */
|
||||||
|
export interface EmailUpdateForm {
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/** 验证码 */
|
||||||
|
code?: string;
|
||||||
|
}
|
BIN
front/src/assets/images/QQ.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
front/src/assets/images/alipay.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
front/src/assets/images/background.jpg
Normal file
After Width: | Height: | Size: 420 KiB |
1
front/src/assets/images/signin.svg
Normal file
After Width: | Height: | Size: 34 KiB |
1
front/src/assets/images/signup.svg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
front/src/assets/images/water.jpeg
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
front/src/assets/images/wechat.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 根据实际部署环境修改 editor.html 的路径 -->
|
<!-- 根据实际部署环境修改 editor.html 的路径 -->
|
||||||
<iframe src="public/editor.html" v-bind="objectOfAttrs"></iframe>
|
<iframe src="/public/editor.html" v-bind="objectOfAttrs"></iframe>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
msg: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
position: relative;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
421
front/src/components/LoginView.vue
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div class="body">
|
||||||
|
<div class="main-box">
|
||||||
|
<div :class="['container', 'container-register', { 'is-txl': isLogin }]">
|
||||||
|
<d-form>
|
||||||
|
<h2 class="title">注册</h2>
|
||||||
|
<!-- <div class="form__icons">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/wechat.png" alt="微信登录">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/alipay.png" alt="支付宝登录">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/QQ.png" alt="QQ登录">-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <span class="text">或使用邮箱进行注册</span>-->
|
||||||
|
<input class="form__input" type="text" placeholder="请输入用户名"/>
|
||||||
|
<input class="form__input" type="text" placeholder="请输入工号"/>
|
||||||
|
<input class="form__input" type="password" placeholder="请输入密码"/>
|
||||||
|
<input class="form__input" type="password" placeholder="请输入密码"/>
|
||||||
|
<div class="form__button">立即注册</div>
|
||||||
|
</d-form>
|
||||||
|
</div>
|
||||||
|
<div :class="['container', 'container-login', { 'is-txl is-z200': isLogin }]">
|
||||||
|
<d-form ref="loginFormRef" layout="vertical" :data="loginFormData" :rules="loginRules" :pop-position="['right']" hide-required-mark="true">
|
||||||
|
<h2 class="title">登录</h2>
|
||||||
|
<!-- <div class="form__icons">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/wechat.png" alt="微信登录">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/alipay.png" alt="支付宝登录">-->
|
||||||
|
<!-- <img class="form__icon" src="@/assets/images/QQ.png" alt="QQ登录">-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <span class="text">或使用用户名登录</span>-->
|
||||||
|
<d-form-item
|
||||||
|
field="username"
|
||||||
|
:show-feedback="false"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<input class="form__input" v-model="loginFormData.username" type="text" placeholder="用户名/邮箱/手机号"/>
|
||||||
|
</d-form-item>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="input-login">
|
||||||
|
<d-form-item
|
||||||
|
field="password"
|
||||||
|
label="密码">
|
||||||
|
<input class="form__input" v-model="loginFormData.password" :type="ShowPassword? 'text' : 'password'" placeholder="请输入密码"
|
||||||
|
style="display: inline-block; " @keyup="checkCapsLock" @keyup.enter="handleLoginSubmit"/>
|
||||||
|
</d-form-item>
|
||||||
|
<d-icon name="view-2" class="icon" style="display: inline-block;" @click="ShowPassword = !ShowPassword" ></d-icon>
|
||||||
|
<div :hidden="!isCapsLock" style="position:absolute;display: inline-flex; margin-top: -20px; color: rgba(255,0,0,0.6);">
|
||||||
|
大写锁定已开启
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form__button" @click="handleLoginSubmit()">立即登录</div>
|
||||||
|
</d-form>
|
||||||
|
</div>
|
||||||
|
<div :class="['switch', { 'login': isLogin }]">
|
||||||
|
<div class="switch__circle"></div>
|
||||||
|
<div class="switch__circle switch__circle_top"></div>
|
||||||
|
<div class="switch__container">
|
||||||
|
<h2>{{ isLogin ? '您好 !' : '欢迎回来 !' }}</h2>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
isLogin
|
||||||
|
? '如果您还没有账号,请点击下方立即注册按钮进行账号注册'
|
||||||
|
: '如果您已经注册过账号,请点击下方立即登录按钮进行登录'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<div class="form__button" @click="isLogin = !isLogin">
|
||||||
|
{{ isLogin ? '立即注册' : '立即登录' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import { LoginFormData } from "@/api/auth.api";
|
||||||
|
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
|
||||||
|
import router from "@/router";
|
||||||
|
import { useUserStore } from "@/store";
|
||||||
|
import {Form} from "vue-devui";
|
||||||
|
|
||||||
|
const loginFormRef = ref<InstanceType<typeof Form>>();
|
||||||
|
const loginFormData = ref<LoginFormData>({
|
||||||
|
username: "admin",
|
||||||
|
password: "123456",
|
||||||
|
captchaKey: "",
|
||||||
|
captchaCode: "",
|
||||||
|
rememberMe: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false); // 按钮 loading 状态
|
||||||
|
const isCapsLock = ref(false); // 是否大写锁定
|
||||||
|
|
||||||
|
const loginRules = computed(() => {
|
||||||
|
return {
|
||||||
|
username: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "用户名不能为空",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "密码不能为空",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 6,
|
||||||
|
message: "密码长度不小于6位",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
captchaCode: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "需要输入验证码",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const route = useRoute();
|
||||||
|
let isLogin = ref(true);
|
||||||
|
let ShowPassword = ref(false);
|
||||||
|
|
||||||
|
|
||||||
|
// 登录提交处理
|
||||||
|
async function handleLoginSubmit() {
|
||||||
|
try {
|
||||||
|
// 1. 表单验证
|
||||||
|
|
||||||
|
const valid = await loginFormRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// 2. 执行登录
|
||||||
|
await userStore.login(loginFormData.value);
|
||||||
|
|
||||||
|
// 3. 获取用户信息
|
||||||
|
await userStore.getUserInfo();
|
||||||
|
|
||||||
|
// 4. 解析并跳转目标地址
|
||||||
|
const redirect = resolveRedirectTarget(route.query);
|
||||||
|
await router.push(redirect);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 5. 统一错误处理
|
||||||
|
// getCaptcha(); // 刷新验证码
|
||||||
|
console.error("登录失败:", error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析重定向目标
|
||||||
|
* @param query 路由查询参数
|
||||||
|
* @returns 标准化后的路由地址对象
|
||||||
|
*/
|
||||||
|
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
|
||||||
|
// 默认跳转路径
|
||||||
|
const defaultPath = "/";
|
||||||
|
|
||||||
|
// 获取原始重定向路径
|
||||||
|
const rawRedirect = (query.redirect as string) || defaultPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 6. 使用Vue Router解析路径
|
||||||
|
const resolved = router.resolve(rawRedirect);
|
||||||
|
return {
|
||||||
|
path: resolved.path,
|
||||||
|
query: resolved.query,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// 7. 异常处理:返回安全路径
|
||||||
|
return { path: defaultPath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查输入大小写
|
||||||
|
function checkCapsLock(event: KeyboardEvent) {
|
||||||
|
// 防止浏览器密码自动填充时报错
|
||||||
|
if (event instanceof KeyboardEvent) {
|
||||||
|
isCapsLock.value = event.getModifierState("CapsLock");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: "Montserrat", sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
background-image: url("@/assets/images/background.jpg");
|
||||||
|
color: #a0a5a8;
|
||||||
|
|
||||||
|
}
|
||||||
|
.main-box {
|
||||||
|
position: relative;
|
||||||
|
width: 800px;
|
||||||
|
min-width: 800px;
|
||||||
|
min-height: 500px;
|
||||||
|
height: 500px;
|
||||||
|
padding: 25px;
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
box-shadow: 1px 1px 100px 10PX #ecf0f3;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 400px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 25px;
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
transition: all 1.25s;
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #a0a5a8;
|
||||||
|
|
||||||
|
.form__icon {
|
||||||
|
object-fit: contain;
|
||||||
|
width: 30px;
|
||||||
|
margin: 0 5px;
|
||||||
|
opacity: .5;
|
||||||
|
transition: .15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transition: .15s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 3;
|
||||||
|
color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__input {
|
||||||
|
width: 350px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 4px 0;
|
||||||
|
padding-left: 25px;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.15px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
// font-family: 'Montserrat', sans-serif;
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
transition: 0.25s ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: inset 2px 2px 4px #d1d9e6, inset -2px -2px 4px #f9f9f9;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #a0a5a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-register {
|
||||||
|
z-index: 100;
|
||||||
|
left: calc(100% - 450px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-login {
|
||||||
|
left: calc(100% - 450px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-txl {
|
||||||
|
left: 50px;
|
||||||
|
transition: 1.25s;
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-z200 {
|
||||||
|
z-index: 200;
|
||||||
|
transition: 1.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 300px;
|
||||||
|
padding: 50px;
|
||||||
|
z-index: 200;
|
||||||
|
transition: 1.25s;
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 4px 4px 10px #d1d9e6, -4px -4px 10px #f9f9f9;
|
||||||
|
color: #a0a5a8;
|
||||||
|
|
||||||
|
.switch__circle {
|
||||||
|
position: absolute;
|
||||||
|
width: 330px;
|
||||||
|
height: 330px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
box-shadow: inset 8px 8px 12px #d1d9e6, inset -8px -8px 12px #f9f9f9;
|
||||||
|
bottom: -50%;
|
||||||
|
left: -30%;
|
||||||
|
transition: 1.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch__circle_top {
|
||||||
|
top: -30%;
|
||||||
|
left: 60%;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch__container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
width: 400px;
|
||||||
|
padding: 50px 55px;
|
||||||
|
transition: 1.25s;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 3;
|
||||||
|
color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
left: calc(100% - 300px);
|
||||||
|
|
||||||
|
.switch__circle {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__button {
|
||||||
|
width: 180px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 25px;
|
||||||
|
margin-top: 50px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
background-color: #4b70e2;
|
||||||
|
color: #f9f9f9;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 8px 8px 16px #d1d9e6, -8px -8px 16px #f9f9f9;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 2px 2px 3px 0 rgba(255, 255, 255, 50%),
|
||||||
|
-2px -2px 3px 0 rgba(116, 125, 136, 50%),
|
||||||
|
inset -2px -2px 3px 0 rgba(255, 255, 255, 20%),
|
||||||
|
inset 2px 2px 3px 0 rgba(0, 0, 0, 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.input-login{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.input-login .icon{
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 16px;
|
||||||
|
color: #a0a5a8;
|
||||||
|
}
|
||||||
|
.input-login .icon:hover{
|
||||||
|
color: #4b70e2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,94 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
|
|
||||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
|
||||||
+
|
|
||||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
|
||||||
and
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
|
||||||
/
|
|
||||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in
|
|
||||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
|
||||||
>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
|
||||||
(our official Discord server), or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also follow the official
|
|
||||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
|
||||||
Bluesky account or the
|
|
||||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
X account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
@ -1,87 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
23
front/src/enums/api/result.enum.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 响应码枚举
|
||||||
|
*/
|
||||||
|
export const enum ResultEnum {
|
||||||
|
/**
|
||||||
|
* 成功
|
||||||
|
*/
|
||||||
|
SUCCESS = "00000",
|
||||||
|
/**
|
||||||
|
* 错误
|
||||||
|
*/
|
||||||
|
ERROR = "B0001",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌无效或过期
|
||||||
|
*/
|
||||||
|
ACCESS_TOKEN_INVALID = "A0230",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌无效或过期
|
||||||
|
*/
|
||||||
|
REFRESH_TOKEN_INVALID = "A0231",
|
||||||
|
}
|
15
front/src/enums/codegen/form.enum.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 表单类型枚举
|
||||||
|
*/
|
||||||
|
export const FormTypeEnum: Record<string, OptionType> = {
|
||||||
|
INPUT: { value: 1, label: "输入框" },
|
||||||
|
SELECT: { value: 2, label: "下拉框" },
|
||||||
|
RADIO: { value: 3, label: "单选框" },
|
||||||
|
CHECK_BOX: { value: 4, label: "复选框" },
|
||||||
|
INPUT_NUMBER: { value: 5, label: "数字输入框" },
|
||||||
|
SWITCH: { value: 6, label: "开关" },
|
||||||
|
TEXT_AREA: { value: 7, label: "文本域" },
|
||||||
|
DATE: { value: 8, label: "日期框" },
|
||||||
|
DATE_TIME: { value: 9, label: "日期时间框" },
|
||||||
|
HIDDEN: { value: 10, label: "隐藏域" },
|
||||||
|
};
|
37
front/src/enums/codegen/query.enum.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 查询类型枚举
|
||||||
|
*/
|
||||||
|
export const QueryTypeEnum: Record<string, OptionType> = {
|
||||||
|
/** 等于 */
|
||||||
|
EQ: { value: 1, label: "=" },
|
||||||
|
|
||||||
|
/** 模糊匹配 */
|
||||||
|
LIKE: { value: 2, label: "LIKE '%s%'" },
|
||||||
|
|
||||||
|
/** 包含 */
|
||||||
|
IN: { value: 3, label: "IN" },
|
||||||
|
|
||||||
|
/** 范围 */
|
||||||
|
BETWEEN: { value: 4, label: "BETWEEN" },
|
||||||
|
|
||||||
|
/** 大于 */
|
||||||
|
GT: { value: 5, label: ">" },
|
||||||
|
|
||||||
|
/** 大于等于 */
|
||||||
|
GE: { value: 6, label: ">=" },
|
||||||
|
|
||||||
|
/** 小于 */
|
||||||
|
LT: { value: 7, label: "<" },
|
||||||
|
|
||||||
|
/** 小于等于 */
|
||||||
|
LE: { value: 8, label: "<=" },
|
||||||
|
|
||||||
|
/** 不等于 */
|
||||||
|
NE: { value: 9, label: "!=" },
|
||||||
|
|
||||||
|
/** 左模糊匹配 */
|
||||||
|
LIKE_LEFT: { value: 10, label: "LIKE '%s'" },
|
||||||
|
|
||||||
|
/** 右模糊匹配 */
|
||||||
|
LIKE_RIGHT: { value: 11, label: "LIKE 's%'" },
|
||||||
|
};
|
11
front/src/enums/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export * from "./api/result.enum";
|
||||||
|
|
||||||
|
export * from "./codegen/form.enum";
|
||||||
|
export * from "./codegen/query.enum";
|
||||||
|
|
||||||
|
export * from "./settings/layout.enum";
|
||||||
|
export * from "./settings/theme.enum";
|
||||||
|
export * from "./settings/locale.enum";
|
||||||
|
export * from "./settings/device.enum";
|
||||||
|
|
||||||
|
export * from "./system/menu.enum";
|
14
front/src/enums/settings/device.enum.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 设备枚举
|
||||||
|
*/
|
||||||
|
export const enum DeviceEnum {
|
||||||
|
/**
|
||||||
|
* 宽屏设备
|
||||||
|
*/
|
||||||
|
DESKTOP = "desktop",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窄屏设备
|
||||||
|
*/
|
||||||
|
MOBILE = "mobile",
|
||||||
|
}
|
53
front/src/enums/settings/layout.enum.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 菜单布局枚举
|
||||||
|
*/
|
||||||
|
export const enum LayoutMode {
|
||||||
|
/**
|
||||||
|
* 左侧菜单布局
|
||||||
|
*/
|
||||||
|
LEFT = "left",
|
||||||
|
/**
|
||||||
|
* 顶部菜单布局
|
||||||
|
*/
|
||||||
|
TOP = "top",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混合菜单布局
|
||||||
|
*/
|
||||||
|
MIX = "mix",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏状态枚举
|
||||||
|
*/
|
||||||
|
export const enum SidebarStatus {
|
||||||
|
/**
|
||||||
|
* 展开
|
||||||
|
*/
|
||||||
|
OPENED = "opened",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭
|
||||||
|
*/
|
||||||
|
CLOSED = "closed",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件尺寸枚举
|
||||||
|
*/
|
||||||
|
export const enum ComponentSize {
|
||||||
|
/**
|
||||||
|
* 默认
|
||||||
|
*/
|
||||||
|
DEFAULT = "default",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大型
|
||||||
|
*/
|
||||||
|
LARGE = "large",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小型
|
||||||
|
*/
|
||||||
|
SMALL = "small",
|
||||||
|
}
|
14
front/src/enums/settings/locale.enum.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 语言枚举
|
||||||
|
*/
|
||||||
|
export const enum LanguageEnum {
|
||||||
|
/**
|
||||||
|
* 中文
|
||||||
|
*/
|
||||||
|
ZH_CN = "zh-cn",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 英文
|
||||||
|
*/
|
||||||
|
EN = "en",
|
||||||
|
}
|
32
front/src/enums/settings/theme.enum.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 主题枚举
|
||||||
|
*/
|
||||||
|
export const enum ThemeMode {
|
||||||
|
/**
|
||||||
|
* 明亮主题
|
||||||
|
*/
|
||||||
|
LIGHT = "light",
|
||||||
|
/**
|
||||||
|
* 暗黑主题
|
||||||
|
*/
|
||||||
|
DARK = "dark",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统自动
|
||||||
|
*/
|
||||||
|
AUTO = "auto",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏配色方案枚举
|
||||||
|
*/
|
||||||
|
export const enum SidebarColor {
|
||||||
|
/**
|
||||||
|
* 经典蓝
|
||||||
|
*/
|
||||||
|
CLASSIC_BLUE = "classic-blue",
|
||||||
|
/**
|
||||||
|
* 极简白
|
||||||
|
*/
|
||||||
|
MINIMAL_WHITE = "minimal-white",
|
||||||
|
}
|
7
front/src/enums/system/menu.enum.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// 核心枚举定义
|
||||||
|
export enum MenuTypeEnum {
|
||||||
|
CATALOG = 2, // 目录
|
||||||
|
MENU = 1, // 菜单
|
||||||
|
BUTTON = 4, // 按钮
|
||||||
|
EXTLINK = 3, // 外链
|
||||||
|
}
|
7
front/src/hook/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 全局Hooks入口文件
|
||||||
|
* 导出所有可用的Hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导出WebSocket相关Hook
|
||||||
|
export * from "./websocket";
|
294
front/src/hook/websocket/core/useStomp.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { Client, IMessage, StompSubscription } from "@stomp/stompjs";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
export interface UseStompOptions {
|
||||||
|
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
|
||||||
|
brokerURL?: string;
|
||||||
|
/** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */
|
||||||
|
token?: string;
|
||||||
|
/** 重连延迟,单位毫秒,默认为 8000 */
|
||||||
|
reconnectDelay?: number;
|
||||||
|
/** 连接超时时间,单位毫秒,默认为 10000 */
|
||||||
|
connectionTimeout?: number;
|
||||||
|
/** 是否开启指数退避重连策略 */
|
||||||
|
useExponentialBackoff?: boolean;
|
||||||
|
/** 最大重连次数,默认为 5 */
|
||||||
|
maxReconnectAttempts?: number;
|
||||||
|
/** 最大重连延迟,单位毫秒,默认为 60000 */
|
||||||
|
maxReconnectDelay?: number;
|
||||||
|
/** 是否开启调试日志 */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STOMP WebSocket连接Hook
|
||||||
|
* 用于管理WebSocket连接的建立、断开、重连和消息订阅
|
||||||
|
*/
|
||||||
|
export function useStomp(options: UseStompOptions = {}) {
|
||||||
|
// 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取
|
||||||
|
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
|
||||||
|
// 不再使用defaultToken,每次连接时直接获取最新token
|
||||||
|
|
||||||
|
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
|
||||||
|
// 不再存储token,改为在初始化时获取
|
||||||
|
const reconnectDelay = options.reconnectDelay ?? 8000;
|
||||||
|
const connectionTimeout = options.connectionTimeout ?? 10000;
|
||||||
|
const useExponentialBackoff = options.useExponentialBackoff ?? false;
|
||||||
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
||||||
|
const maxReconnectDelay = options.maxReconnectDelay ?? 60000;
|
||||||
|
|
||||||
|
// 连接状态标记
|
||||||
|
const isConnected = ref(false);
|
||||||
|
// 重连尝试次数
|
||||||
|
const reconnectCount = ref(0);
|
||||||
|
// 重连计时器
|
||||||
|
let reconnectTimer: any = null;
|
||||||
|
// 连接超时计时器
|
||||||
|
let connectionTimeoutTimer: any = null;
|
||||||
|
// 存储所有订阅
|
||||||
|
const subscriptions = new Map<string, StompSubscription>();
|
||||||
|
|
||||||
|
// 用于保存 STOMP 客户端的实例
|
||||||
|
let client = ref<Client | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 STOMP 客户端
|
||||||
|
*/
|
||||||
|
const initializeClient = () => {
|
||||||
|
if (client.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次连接前重新获取最新令牌,不依赖之前的token值
|
||||||
|
const currentToken = getAccessToken();
|
||||||
|
|
||||||
|
// 检查令牌是否为空,如果为空则不进行连接
|
||||||
|
if (!currentToken) {
|
||||||
|
console.error("WebSocket连接失败:授权令牌为空,请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 STOMP 客户端
|
||||||
|
client.value = new Client({
|
||||||
|
brokerURL: brokerURL.value,
|
||||||
|
connectHeaders: {
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
debug: options.debug ? console.log : () => {},
|
||||||
|
reconnectDelay: useExponentialBackoff ? 0 : reconnectDelay, // 使用自定义退避策略时禁用内置重连
|
||||||
|
heartbeatIncoming: 4000,
|
||||||
|
heartbeatOutgoing: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置连接监听器
|
||||||
|
client.value.onConnect = () => {
|
||||||
|
isConnected.value = true;
|
||||||
|
reconnectCount.value = 0;
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
console.log("WebSocket连接已建立");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置断开连接监听器
|
||||||
|
client.value.onDisconnect = () => {
|
||||||
|
isConnected.value = false;
|
||||||
|
console.log("WebSocket连接已断开");
|
||||||
|
|
||||||
|
// 如果使用自定义指数退避重连策略,则在这里处理
|
||||||
|
if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) {
|
||||||
|
handleReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置 Web Socket 关闭监听器
|
||||||
|
client.value.onWebSocketClose = (event) => {
|
||||||
|
isConnected.value = false;
|
||||||
|
console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`);
|
||||||
|
|
||||||
|
// 如果是授权问题导致的关闭,尝试重新获取令牌
|
||||||
|
if (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) {
|
||||||
|
console.log("可能是授权问题导致连接关闭,尝试重新建立连接");
|
||||||
|
|
||||||
|
// 等待一段时间后再尝试重连,避免立即重连
|
||||||
|
setTimeout(() => {
|
||||||
|
// 强制重新初始化客户端,获取最新令牌
|
||||||
|
client.value = null;
|
||||||
|
|
||||||
|
// 检查当前是否有有效令牌
|
||||||
|
const freshToken = getAccessToken();
|
||||||
|
if (freshToken) {
|
||||||
|
initializeClient();
|
||||||
|
connect();
|
||||||
|
} else {
|
||||||
|
console.warn("没有有效令牌,暂不重连WebSocket");
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置错误监听器
|
||||||
|
client.value.onStompError = (frame) => {
|
||||||
|
console.error("STOMP错误:", frame.headers, frame.body);
|
||||||
|
|
||||||
|
// 检查是否是授权错误
|
||||||
|
if (
|
||||||
|
frame.headers?.message?.includes("Unauthorized") ||
|
||||||
|
frame.body?.includes("Unauthorized") ||
|
||||||
|
frame.body?.includes("Token")
|
||||||
|
) {
|
||||||
|
console.warn("WebSocket授权错误,请检查登录状态");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理重连逻辑
|
||||||
|
*/
|
||||||
|
const handleReconnect = () => {
|
||||||
|
if (reconnectCount.value >= maxReconnectAttempts) {
|
||||||
|
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectCount.value++;
|
||||||
|
console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
|
||||||
|
|
||||||
|
// 使用指数退避策略增加重连间隔
|
||||||
|
const delay = useExponentialBackoff
|
||||||
|
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
|
||||||
|
: reconnectDelay;
|
||||||
|
|
||||||
|
// 清除之前的计时器
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置重连计时器
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
if (!isConnected.value && client.value) {
|
||||||
|
client.value.activate();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 brokerURL 的变化,若地址改变则重新初始化
|
||||||
|
watch(brokerURL, (newURL, oldURL) => {
|
||||||
|
if (newURL !== oldURL) {
|
||||||
|
console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
|
||||||
|
// 断开当前连接,重新激活客户端
|
||||||
|
if (client.value && client.value.connected) {
|
||||||
|
client.value.deactivate();
|
||||||
|
}
|
||||||
|
brokerURL.value = newURL;
|
||||||
|
initializeClient(); // 重新初始化客户端
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化客户端
|
||||||
|
initializeClient();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活连接(如果已经连接或正在激活则直接返回)
|
||||||
|
*/
|
||||||
|
const connect = () => {
|
||||||
|
if (!client.value) {
|
||||||
|
initializeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.value && (client.value.connected || client.value.active)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.value) {
|
||||||
|
console.error("STOMP客户端初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接超时
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
connectionTimeoutTimer = setTimeout(() => {
|
||||||
|
if (!isConnected.value) {
|
||||||
|
console.warn("WebSocket连接超时");
|
||||||
|
if (useExponentialBackoff) {
|
||||||
|
handleReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, connectionTimeout);
|
||||||
|
|
||||||
|
client.value.activate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅指定主题
|
||||||
|
* @param destination 目标主题地址
|
||||||
|
* @param callback 接收到消息时的回调函数
|
||||||
|
* @returns 返回订阅 id,用于后续取消订阅
|
||||||
|
*/
|
||||||
|
const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
|
||||||
|
if (!client.value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.value.connected) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscription = client.value.subscribe(destination, callback);
|
||||||
|
subscriptions.set(subscription.id, subscription);
|
||||||
|
console.log(`订阅成功: ${destination}, ID: ${subscription.id}`);
|
||||||
|
return subscription.id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`订阅失败(${destination}):`, error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定订阅
|
||||||
|
* @param subscriptionId 要取消的订阅 id
|
||||||
|
*/
|
||||||
|
const unsubscribe = (subscriptionId: string) => {
|
||||||
|
const subscription = subscriptions.get(subscriptionId);
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscriptions.delete(subscriptionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动断开连接(如果未连接则不执行)
|
||||||
|
*/
|
||||||
|
const disconnect = () => {
|
||||||
|
if (client.value && !(client.value.connected || client.value.active)) {
|
||||||
|
console.log("Already disconnected, skipping disconnect() call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有计时器
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionTimeoutTimer) {
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
connectionTimeoutTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.value?.deactivate();
|
||||||
|
isConnected.value = false;
|
||||||
|
reconnectCount.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
isConnected,
|
||||||
|
reconnectCount,
|
||||||
|
connect,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
disconnect,
|
||||||
|
brokerURL,
|
||||||
|
};
|
||||||
|
}
|
11
front/src/hook/websocket/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket相关Hook入口文件
|
||||||
|
* 统一导出所有WebSocket相关Hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 核心基础Hook
|
||||||
|
export { useStomp } from "./core/useStomp";
|
||||||
|
|
||||||
|
// 业务服务Hook
|
||||||
|
export { useOnlineCount } from "./services/useOnlineCount";
|
||||||
|
export { useDictSync } from "./services/useDictSync";
|
189
front/src/hook/websocket/services/useDictSync.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { useDictStoreHook } from "@/store/modules/dict.store";
|
||||||
|
import { useStomp } from "../core/useStomp";
|
||||||
|
import { IMessage } from "@stomp/stompjs";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
// 字典消息类型
|
||||||
|
export interface DictMessage {
|
||||||
|
dictCode: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典事件回调类型
|
||||||
|
export type DictMessageCallback = (message: DictMessage) => void;
|
||||||
|
|
||||||
|
// 全局单例实例
|
||||||
|
let instance: ReturnType<typeof createDictSyncHook> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建字典同步Hook
|
||||||
|
* 负责监听后端字典变更并同步到前端
|
||||||
|
*/
|
||||||
|
function createDictSyncHook() {
|
||||||
|
const dictStore = useDictStoreHook();
|
||||||
|
|
||||||
|
// 使用现有的useStomp,配置适合字典场景的重连参数
|
||||||
|
const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({
|
||||||
|
reconnectDelay: 10000, // 使用更长的重连延迟 - 10秒
|
||||||
|
connectionTimeout: 15000, // 更长的连接超时时间 - 15秒
|
||||||
|
useExponentialBackoff: false, // 字典数据不需要指数退避策略
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储订阅ID
|
||||||
|
const subscriptionIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
// 已订阅的主题
|
||||||
|
const subscribedTopics = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 消息回调函数列表
|
||||||
|
const messageCallbacks = ref<DictMessageCallback[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册字典消息回调
|
||||||
|
* @param callback 回调函数
|
||||||
|
*/
|
||||||
|
const onDictMessage = (callback: DictMessageCallback) => {
|
||||||
|
messageCallbacks.value.push(callback);
|
||||||
|
return () => {
|
||||||
|
// 返回取消注册的函数
|
||||||
|
const index = messageCallbacks.value.indexOf(callback);
|
||||||
|
if (index !== -1) {
|
||||||
|
messageCallbacks.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WebSocket
|
||||||
|
*/
|
||||||
|
const initWebSocket = async () => {
|
||||||
|
try {
|
||||||
|
// 连接WebSocket
|
||||||
|
connect();
|
||||||
|
|
||||||
|
// 设置字典订阅
|
||||||
|
setupDictSubscription();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WebSocket] 初始化失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭WebSocket
|
||||||
|
*/
|
||||||
|
const closeWebSocket = () => {
|
||||||
|
// 取消所有订阅
|
||||||
|
subscriptionIds.value.forEach((id) => {
|
||||||
|
unsubscribe(id);
|
||||||
|
});
|
||||||
|
subscriptionIds.value = [];
|
||||||
|
subscribedTopics.value.clear();
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置字典订阅
|
||||||
|
*/
|
||||||
|
const setupDictSubscription = () => {
|
||||||
|
const topic = "/topic/dict";
|
||||||
|
|
||||||
|
// 防止重复订阅
|
||||||
|
if (subscribedTopics.value.has(topic)) {
|
||||||
|
console.log(`跳过重复订阅: ${topic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始尝试订阅字典主题: ${topic}`);
|
||||||
|
|
||||||
|
// 使用简化的重试逻辑,依赖useStomp的连接管理
|
||||||
|
const attemptSubscribe = () => {
|
||||||
|
if (!isConnected.value) {
|
||||||
|
console.log("等待WebSocket连接建立...");
|
||||||
|
// 3秒后再次尝试
|
||||||
|
setTimeout(attemptSubscribe, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已订阅
|
||||||
|
if (subscribedTopics.value.has(topic)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`连接已建立,开始订阅: ${topic}`);
|
||||||
|
|
||||||
|
// 订阅字典更新
|
||||||
|
const subId = subscribe(topic, (message: IMessage) => {
|
||||||
|
handleDictEvent(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subId) {
|
||||||
|
subscriptionIds.value.push(subId);
|
||||||
|
subscribedTopics.value.add(topic);
|
||||||
|
console.log(`字典主题订阅成功: ${topic}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`字典主题订阅失败: ${topic}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始尝试订阅
|
||||||
|
attemptSubscribe();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理字典事件
|
||||||
|
* @param message STOMP消息
|
||||||
|
*/
|
||||||
|
const handleDictEvent = (message: IMessage) => {
|
||||||
|
if (!message.body) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 记录接收到的消息
|
||||||
|
console.log(`收到字典更新消息: ${message.body}`);
|
||||||
|
|
||||||
|
// 尝试解析消息
|
||||||
|
const parsedData = JSON.parse(message.body) as DictMessage;
|
||||||
|
const dictCode = parsedData.dictCode;
|
||||||
|
|
||||||
|
if (!dictCode) return;
|
||||||
|
|
||||||
|
// 清除缓存,等待按需加载
|
||||||
|
dictStore.removeDictItem(dictCode);
|
||||||
|
console.log(`字典缓存已清除: ${dictCode}`);
|
||||||
|
|
||||||
|
// 调用所有注册的回调函数
|
||||||
|
messageCallbacks.value.forEach((callback) => {
|
||||||
|
try {
|
||||||
|
callback(parsedData);
|
||||||
|
} catch (callbackError) {
|
||||||
|
console.error("[WebSocket] 回调执行失败:", callbackError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示提示消息
|
||||||
|
console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WebSocket] 解析消息失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
initWebSocket,
|
||||||
|
closeWebSocket,
|
||||||
|
handleDictEvent,
|
||||||
|
onDictMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典同步Hook
|
||||||
|
* 用于监听后端字典变更并同步到前端
|
||||||
|
*/
|
||||||
|
export function useDictSync() {
|
||||||
|
if (!instance) {
|
||||||
|
instance = createDictSyncHook();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
169
front/src/hook/websocket/services/useOnlineCount.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||||
|
import { useStomp } from "../core/useStomp";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户计数Hook
|
||||||
|
* 用于订阅后端推送的在线用户数量变化
|
||||||
|
*/
|
||||||
|
export function useOnlineCount() {
|
||||||
|
// 在线用户数量
|
||||||
|
const onlineUserCount = ref(0);
|
||||||
|
|
||||||
|
// 最后更新时间戳
|
||||||
|
const lastUpdateTime = ref(0);
|
||||||
|
|
||||||
|
// 连接状态
|
||||||
|
const isConnected = ref(false);
|
||||||
|
|
||||||
|
// 连接正在尝试中
|
||||||
|
const isConnecting = ref(false);
|
||||||
|
|
||||||
|
// 使用Stomp客户端 - 配置使用指数退避策略
|
||||||
|
const {
|
||||||
|
connect,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
disconnect,
|
||||||
|
isConnected: stompConnected,
|
||||||
|
} = useStomp({
|
||||||
|
reconnectDelay: 5000, // 初始重连延迟5秒
|
||||||
|
maxReconnectAttempts: 3, // 最大重连3次
|
||||||
|
connectionTimeout: 10000, // 连接超时10秒
|
||||||
|
useExponentialBackoff: true, // 启用指数退避
|
||||||
|
});
|
||||||
|
|
||||||
|
// 订阅ID
|
||||||
|
let subscriptionId = "";
|
||||||
|
|
||||||
|
// 连接超时计时器
|
||||||
|
let connectionTimeoutTimer: any = null;
|
||||||
|
|
||||||
|
// 监听Stomp连接状态
|
||||||
|
watch(stompConnected, (connected) => {
|
||||||
|
if (connected && isConnecting.value) {
|
||||||
|
isConnected.value = true;
|
||||||
|
isConnecting.value = false;
|
||||||
|
|
||||||
|
// 一旦连接成功,立即订阅主题
|
||||||
|
subscribeToOnlineCount();
|
||||||
|
console.log("WebSocket连接成功,已订阅在线用户计数主题");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅在线用户计数主题
|
||||||
|
*/
|
||||||
|
const subscribeToOnlineCount = () => {
|
||||||
|
if (!stompConnected.value) return;
|
||||||
|
|
||||||
|
// 如果已经订阅,先取消订阅
|
||||||
|
if (subscriptionId) {
|
||||||
|
unsubscribe(subscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅在线用户计数主题
|
||||||
|
subscriptionId = subscribe("/topic/online-count", (message) => {
|
||||||
|
try {
|
||||||
|
const data = message.body;
|
||||||
|
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
const count = typeof jsonData === "number" ? jsonData : jsonData.count;
|
||||||
|
|
||||||
|
if (!isNaN(count)) {
|
||||||
|
onlineUserCount.value = count;
|
||||||
|
lastUpdateTime.value = Date.now();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析在线用户数量失败:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WebSocket连接并订阅在线用户主题
|
||||||
|
*/
|
||||||
|
const initWebSocket = () => {
|
||||||
|
if (isConnecting.value) return;
|
||||||
|
|
||||||
|
// 检查是否有可用的令牌
|
||||||
|
const hasToken = !!getAccessToken();
|
||||||
|
if (!hasToken) {
|
||||||
|
console.log("没有检测到有效令牌,不尝试WebSocket连接");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnecting.value = true;
|
||||||
|
|
||||||
|
// 连接WebSocket
|
||||||
|
connect();
|
||||||
|
|
||||||
|
// 设置连接超时显示UI提示
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
connectionTimeoutTimer = setTimeout(() => {
|
||||||
|
if (!isConnected.value) {
|
||||||
|
console.warn("WebSocket连接超时,将自动尝试重连");
|
||||||
|
ElMessage.warning("正在尝试连接服务器,请稍候...");
|
||||||
|
|
||||||
|
// 超时后尝试重新连接
|
||||||
|
closeWebSocket();
|
||||||
|
setTimeout(() => {
|
||||||
|
// 再次检查令牌有效性
|
||||||
|
if (getAccessToken()) {
|
||||||
|
initWebSocket();
|
||||||
|
} else {
|
||||||
|
console.log("令牌无效,放弃重连");
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, 10000); // 较长的UI提示超时
|
||||||
|
|
||||||
|
// 监听连接状态变化,连接成功后清除超时计时器
|
||||||
|
const unwatch = watch(stompConnected, (connected) => {
|
||||||
|
if (connected) {
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
unwatch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭WebSocket连接
|
||||||
|
*/
|
||||||
|
const closeWebSocket = () => {
|
||||||
|
if (subscriptionId) {
|
||||||
|
unsubscribe(subscriptionId);
|
||||||
|
subscriptionId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除连接超时计时器
|
||||||
|
if (connectionTimeoutTimer) {
|
||||||
|
clearTimeout(connectionTimeoutTimer);
|
||||||
|
connectionTimeoutTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
isConnected.value = false;
|
||||||
|
isConnecting.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时初始化WebSocket
|
||||||
|
onMounted(() => {
|
||||||
|
initWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时关闭WebSocket
|
||||||
|
onUnmounted(() => {
|
||||||
|
closeWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
onlineUserCount,
|
||||||
|
lastUpdateTime,
|
||||||
|
isConnected,
|
||||||
|
isConnecting,
|
||||||
|
initWebSocket,
|
||||||
|
closeWebSocket,
|
||||||
|
};
|
||||||
|
}
|
11
front/src/layout/components/Navbar/Navbar.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import router from "@/router/index.js";
|
import router from "@/router/index.ts";
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const handleMenuSelect = (key) => {
|
const handleMenuSelect = (key) => {
|
||||||
router.push(`/${key["key"]}`)
|
router.push(`/${key["key"]}`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前激活的菜单项
|
// 当前激活的菜单项
|
162
front/src/layout/index.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<script setup>
|
||||||
|
import {reactive, ref, shallowRef} from 'vue'
|
||||||
|
|
||||||
|
// // 当前激活的菜单项
|
||||||
|
// const activeMenu = ref('item1')
|
||||||
|
|
||||||
|
// // 当前显示的组件
|
||||||
|
// const currentComponent = shallowRef(HomePage)
|
||||||
|
|
||||||
|
// 组件映射表
|
||||||
|
// const componentMap = {
|
||||||
|
// 'item1': HomePage,
|
||||||
|
// 'menjizhen-item': menjizhenItemView,
|
||||||
|
// 'notFoundPage': NotFoundPage,
|
||||||
|
// // 其他菜单项对应的组件...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 处理菜单选择
|
||||||
|
// const handleMenuSelect = (key) => {
|
||||||
|
// activeMenu.value = key["key"]
|
||||||
|
// if (componentMap[key["key"]]) {
|
||||||
|
// currentComponent.value = componentMap[key["key"]]
|
||||||
|
// } else {
|
||||||
|
// console.warn(`未找到菜单项 ${key} 对应的组件`)
|
||||||
|
// console.warn(key["key"])
|
||||||
|
// currentComponent.value = NotFoundPage
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
const toggleClick = function() {
|
||||||
|
let menu = document.querySelector('.menu-aside')
|
||||||
|
|
||||||
|
menu.style.transition = menu.style.transition || 'all 0.3s ease-in-out'
|
||||||
|
|
||||||
|
if (menu.style.display == 'none') {
|
||||||
|
menu.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
menu.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<d-layout style="width: 100vw; ">
|
||||||
|
<!-- 顶栏 -->
|
||||||
|
<d-header style="position: fixed; width: 100%; z-index: 100;background: #fff;height: 56px;box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
||||||
|
<div class="nav-collapse i icon-nav-collapse me-4 ms-4" @click="toggleClick()"></div>
|
||||||
|
<span class="avatar-demo-2" style="position: relative;
|
||||||
|
text-align: right;">
|
||||||
|
<d-avatar name="张医生" :width="28" :height="28" class="profile"/>
|
||||||
|
<span class="name" style="margin-left: 10px;">张医生</span>
|
||||||
|
</span>
|
||||||
|
<!-- <d-breadcrumb :source="source" style="display: inline-flex;text-align: center; position: relative; overflow: hidden; width: auto; margin-left:180px" />-->
|
||||||
|
|
||||||
|
<div style="float: right;
|
||||||
|
text-align: right;
|
||||||
|
position: fixed;
|
||||||
|
display: inline-block;
|
||||||
|
right: 4rem;">
|
||||||
|
<span>
|
||||||
|
<d-search class="mt-3"
|
||||||
|
size="sm"
|
||||||
|
style="
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
width: 200px"
|
||||||
|
is-keyup-search
|
||||||
|
:delay="1000"
|
||||||
|
@search="onSearch">
|
||||||
|
</d-search>
|
||||||
|
</span>
|
||||||
|
<span style="
|
||||||
|
position: relative;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 1rem;"
|
||||||
|
>
|
||||||
|
<d-icon name="feedback"/>
|
||||||
|
<a style="color:#54dc35 ;">
|
||||||
|
<d-badge :count="100" status="info" class="badge-item">未读消息</d-badge>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span style="
|
||||||
|
position: relative;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-top: 1rem;">
|
||||||
|
<a href="/logout" style="color:rgba(0,0,0,0.63)">注销</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</d-header>
|
||||||
|
<!-- 内层布局:侧边 + 主区 -->
|
||||||
|
<d-layout style=" margin-top: 64px; width: 100%;" > <!-- 添加顶部间距,避免被header遮挡 -->
|
||||||
|
<!-- 侧边栏 - 添加fixed样式 -->
|
||||||
|
<d-aside style="
|
||||||
|
position: relative;
|
||||||
|
z-index: 99;"
|
||||||
|
class="menu-aside"
|
||||||
|
>
|
||||||
|
|
||||||
|
</d-aside>
|
||||||
|
<!-- 主显示区 - 添加左侧边距 -->
|
||||||
|
<d-content
|
||||||
|
style="
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: hidden; /* 禁止内容溢出 */
|
||||||
|
">
|
||||||
|
<!-- <component :is="currentComponent"/>-->
|
||||||
|
<router-view></router-view>
|
||||||
|
</d-content>
|
||||||
|
</d-layout>
|
||||||
|
<d-footer style="position: fixed; bottom: 0; width: 100%; z-index: 100;background: #fff;height: 16px;">
|
||||||
|
<div style="text-align: center; padding: 8px;">
|
||||||
|
<img src="../assets/logo.png" alt="Logo" class="logo" />
|
||||||
|
<p style="font-size: 12px; color: #999;">© 2023 Your Company. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</d-footer>
|
||||||
|
</d-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.profile {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在CSS中定义可变字体 */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans SC';
|
||||||
|
src: local('src/assets/NotoSansSC-VariableFont_wght.ttf');/* 先尝试本地已有字体 */
|
||||||
|
font-weight: 100 900; /* 定义支持的权重范围 */
|
||||||
|
font-stretch: 75% 125%; /* 定义支持的宽度范围 */
|
||||||
|
font-style: oblique 0deg 20deg; /* 可选的斜体范围 */
|
||||||
|
font-display: swap; /* 优化加载体验 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-collapse{
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.nav-collapse:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgb(126, 126, 126);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
@ -1,27 +1,22 @@
|
|||||||
import './assets/main.css'
|
|
||||||
import DevUI from 'vue-devui';
|
|
||||||
import 'vue-devui/style.css';
|
import 'vue-devui/style.css';
|
||||||
import '@devui-design/icons/icomoon/devui-icon.css';
|
import '@devui-design/icons/icomoon/devui-icon.css';
|
||||||
import { ThemeServiceInit, infinityTheme } from 'devui-theme';
|
import { ThemeServiceInit, infinityTheme } from 'devui-theme';
|
||||||
|
ThemeServiceInit({ infinityTheme }, 'infinityTheme');
|
||||||
import router from './router/index'
|
|
||||||
|
|
||||||
import 'devui-theme/styles-var/devui-var.scss'
|
import 'devui-theme/styles-var/devui-var.scss'
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import 'bootstrap/dist/js/bootstrap.min.js'
|
import 'bootstrap/dist/js/bootstrap.min.js'
|
||||||
|
|
||||||
//
|
import { createApp } from 'vue';
|
||||||
import Editor from '@/components/Editor.vue'
|
import App from './App.vue';
|
||||||
|
import setupPlugins from "@/plugins";
|
||||||
|
import setupEditor from "@/components/Editor.vue";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
ThemeServiceInit({ infinityTheme }, 'infinityTheme');
|
// 病历编辑器
|
||||||
|
app.component('Editor', setupEditor)
|
||||||
import { createApp } from 'vue'
|
app.use(setupPlugins);
|
||||||
import App from './App.vue'
|
|
||||||
const app = createApp(App)
|
|
||||||
|
|
||||||
app.component('Editor', Editor)
|
|
||||||
app.use(DevUI)
|
|
||||||
app.use(router)
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
@ -70,8 +70,16 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
</d-card>
|
</d-card>
|
||||||
|
<d-card shadow="never" class="item-card container-right-card">
|
||||||
|
<template #title>统计面板</template>
|
||||||
|
<template #content>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</d-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</d-content>
|
</d-content>
|
||||||
|
12
front/src/pages/LoginPage.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import LoginView from "@/components/LoginView.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LoginView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
21
front/src/plugins/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { App } from "vue";
|
||||||
|
|
||||||
|
import VueDevUI from 'vue-devui';
|
||||||
|
|
||||||
|
import { setupRouter } from "@/router";
|
||||||
|
import { setupStore } from "@/store";
|
||||||
|
import { setupPermission } from "./permission";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(app: App<Element>) {
|
||||||
|
// 路由(router)
|
||||||
|
setupRouter(app);
|
||||||
|
// 状态管理(store)
|
||||||
|
setupStore(app);
|
||||||
|
// 路由守卫
|
||||||
|
setupPermission();
|
||||||
|
|
||||||
|
app.use(VueDevUI);
|
||||||
|
},
|
||||||
|
};
|
88
front/src/plugins/permission.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
|
||||||
|
import NProgress from "@/utils/nprogress";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
import router from "@/router";
|
||||||
|
import { usePermissionStore, useUserStore } from "@/store";
|
||||||
|
|
||||||
|
export function setupPermission() {
|
||||||
|
// 白名单路由
|
||||||
|
const whiteList = ["/login"];
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
NProgress.start();
|
||||||
|
|
||||||
|
const isLogin = !!getAccessToken(); // 判断是否登录
|
||||||
|
if (isLogin) {
|
||||||
|
if (to.path === "/login") {
|
||||||
|
// 已登录,访问登录页,跳转到首页
|
||||||
|
next({ path: "/" });
|
||||||
|
} else {
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
// 判断路由是否加载完成
|
||||||
|
if (permissionStore.isRoutesLoaded) {
|
||||||
|
if (to.matched.length === 0) {
|
||||||
|
// 路由未匹配,跳转到404
|
||||||
|
next("/404");
|
||||||
|
} else {
|
||||||
|
// 动态设置页面标题
|
||||||
|
const title = (to.params.title as string) || (to.query.title as string);
|
||||||
|
if (title) {
|
||||||
|
to.meta.title = title;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// 生成动态路由
|
||||||
|
const dynamicRoutes = await permissionStore.generateRoutes();
|
||||||
|
dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route));
|
||||||
|
next({ ...to, replace: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
// 路由加载失败,重置 token 并重定向到登录页
|
||||||
|
await useUserStore().clearSessionAndCache();
|
||||||
|
redirectToLogin(to, next);
|
||||||
|
NProgress.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未登录,判断是否在白名单中
|
||||||
|
if (whiteList.includes(to.path)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
// 不在白名单,重定向到登录页
|
||||||
|
redirectToLogin(to, next);
|
||||||
|
NProgress.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 后置守卫,保证每次路由跳转结束时关闭进度条
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向到登录页
|
||||||
|
function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||||
|
const params = new URLSearchParams(to.query as Record<string, string>);
|
||||||
|
const queryString = params.toString();
|
||||||
|
const redirect = queryString ? `${to.path}?${queryString}` : to.path;
|
||||||
|
next(`/login?redirect=${encodeURIComponent(redirect)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否有权限 */
|
||||||
|
export function hasAuth(value: string | string[], type: "button" | "role" = "button") {
|
||||||
|
const { roles, perms } = useUserStore().userInfo;
|
||||||
|
|
||||||
|
// 超级管理员 拥有所有权限
|
||||||
|
if (type === "button" && roles.includes("ROOT")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auths = type === "button" ? perms : roles;
|
||||||
|
return typeof value === "string"
|
||||||
|
? auths.includes(value)
|
||||||
|
: value.some((perm) => auths.includes(perm));
|
||||||
|
}
|
39
front/src/plugins/websocket.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useDictSync } from "@/hooks/websocket/services/useDictSync";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WebSocket服务
|
||||||
|
*/
|
||||||
|
export function setupWebSocket() {
|
||||||
|
console.log("[WebSocketPlugin] 开始初始化WebSocket服务...");
|
||||||
|
|
||||||
|
// 检查token是否存在
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (!token) {
|
||||||
|
console.warn(
|
||||||
|
"[WebSocketPlugin] 未找到访问令牌,WebSocket初始化已跳过。用户登录后将自动重新连接。"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 延迟初始化,确保应用完全启动
|
||||||
|
setTimeout(() => {
|
||||||
|
const dictWebSocket = useDictSync();
|
||||||
|
|
||||||
|
// 初始化字典WebSocket服务
|
||||||
|
dictWebSocket.initWebSocket();
|
||||||
|
console.log("[WebSocketPlugin] 字典WebSocket初始化完成");
|
||||||
|
|
||||||
|
// 在窗口关闭前断开WebSocket连接
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
console.log("[WebSocketPlugin] 窗口即将关闭,断开WebSocket连接");
|
||||||
|
dictWebSocket.closeWebSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[WebSocketPlugin] WebSocket服务初始化完成");
|
||||||
|
}, 1000); // 延迟1秒初始化
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WebSocketPlugin] 初始化WebSocket服务失败:", error);
|
||||||
|
}
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
import * as VueRouter from 'vue-router'
|
|
||||||
import Home from '@/pages/HomePage.vue'
|
|
||||||
import menjizhenItemView from '@/pages/zl-station/menjizhenItemView.vue'
|
|
||||||
import Page404 from '@/pages/404/notFoundPage.vue'
|
|
||||||
|
|
||||||
function generateRoutes() {
|
|
||||||
//const pages = import.meta.glob('@/pages/*.vue', { eager: true })
|
|
||||||
let routers = [
|
|
||||||
//默认路由
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: Home
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/HomePage',
|
|
||||||
component: Home
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/menjizhen-item',
|
|
||||||
component: menjizhenItemView
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/zhuyuan-item',
|
|
||||||
component: () => import('@/pages/zl-station/zhuyuanItemView.vue')
|
|
||||||
},
|
|
||||||
// 通配符路由 - 必须放在最后
|
|
||||||
{
|
|
||||||
path: '/:pathMatch(.*)*', // 匹配所有路径
|
|
||||||
name: 'NotFound',
|
|
||||||
component: Page404
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// //自动发现路由
|
|
||||||
// Object.keys(pages).forEach(path => {
|
|
||||||
// routers.push({
|
|
||||||
// path: path.replace('/src/pages', '').replace('.vue', ''),
|
|
||||||
// component: pages[path].default
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
return routers
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = VueRouter.createRouter({
|
|
||||||
history: VueRouter.createWebHistory(),
|
|
||||||
routes: generateRoutes()
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
54
front/src/router/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import type { App } from "vue";
|
||||||
|
import * as VueRouter from 'vue-router'
|
||||||
|
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
|
||||||
|
import Home from '@/pages/HomePage.vue'
|
||||||
|
import mjz from '@/pages/zl-station/menjizhenItemView.vue'
|
||||||
|
import zy from '@/pages/zl-station/zhuyuanItemView.vue'
|
||||||
|
import Login from '@/pages/LoginPage.vue'
|
||||||
|
|
||||||
|
import Page404 from '@/pages/404/notFoundPage.vue'
|
||||||
|
|
||||||
|
export const constantRoutes : RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/HomePage',
|
||||||
|
redirect: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/menjizhen-item',
|
||||||
|
component: () => import('@/pages/zl-station/menjizhenItemView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/zhuyuan-item',
|
||||||
|
component: zy
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'LoginView',
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
|
// 通配符路由 - 必须放在最后
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*', // 匹配所有路径
|
||||||
|
name: 'NotFound',
|
||||||
|
component: Page404
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: VueRouter.createWebHistory(),
|
||||||
|
routes: constantRoutes,
|
||||||
|
// 刷新时,滚动条位置还原
|
||||||
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 全局注册 router
|
||||||
|
export function setupRouter(app: App<Element>) {
|
||||||
|
app.use(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
37
front/src/settings.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from "@/enums";
|
||||||
|
|
||||||
|
const { pkg } = __APP_INFO__;
|
||||||
|
|
||||||
|
// 检查用户的操作系统是否使用深色模式
|
||||||
|
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
const defaultSettings: AppSettings = {
|
||||||
|
// 系统Title
|
||||||
|
title: pkg.name,
|
||||||
|
// 系统版本
|
||||||
|
version: pkg.version,
|
||||||
|
// 是否显示设置
|
||||||
|
showSettings: true,
|
||||||
|
// 是否显示标签视图
|
||||||
|
tagsView: true,
|
||||||
|
// 是否显示侧边栏Logo
|
||||||
|
sidebarLogo: true,
|
||||||
|
// 布局方式,默认为左侧布局
|
||||||
|
layout: LayoutMode.LEFT,
|
||||||
|
// 主题,根据操作系统的色彩方案自动选择
|
||||||
|
theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT,
|
||||||
|
// 组件大小 default | medium | small | large
|
||||||
|
size: ComponentSize.DEFAULT,
|
||||||
|
// 语言
|
||||||
|
language: LanguageEnum.ZH_CN,
|
||||||
|
// 主题颜色
|
||||||
|
themeColor: "#4080FF",
|
||||||
|
// 是否开启水印
|
||||||
|
watermarkEnabled: false,
|
||||||
|
// 水印内容
|
||||||
|
watermarkContent: pkg.name,
|
||||||
|
// 侧边栏配色方案
|
||||||
|
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defaultSettings;
|
17
front/src/store/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { App } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
const store = createPinia();
|
||||||
|
|
||||||
|
// 全局注册 store
|
||||||
|
export function setupStore(app: App<Element>) {
|
||||||
|
app.use(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./modules/app.store";
|
||||||
|
export * from "./modules/permission.store";
|
||||||
|
export * from "./modules/settings.store";
|
||||||
|
export * from "./modules/tags-view.store";
|
||||||
|
export * from "./modules/user.store";
|
||||||
|
export * from "./modules/dict.store";
|
||||||
|
export { store };
|
81
front/src/store/modules/app.store.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import defaultSettings from "@/settings";
|
||||||
|
|
||||||
|
import { store } from "@/store";
|
||||||
|
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||||
|
import { SidebarStatus } from "@/enums/settings/layout.enum";
|
||||||
|
|
||||||
|
export const useAppStore = defineStore("app", () => {
|
||||||
|
// 设备类型
|
||||||
|
const device = useStorage("device", DeviceEnum.DESKTOP);
|
||||||
|
// 布局大小
|
||||||
|
const size = useStorage("size", defaultSettings.size);
|
||||||
|
// 侧边栏状态
|
||||||
|
const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED);
|
||||||
|
const sidebar = reactive({
|
||||||
|
opened: sidebarStatus.value === SidebarStatus.OPENED,
|
||||||
|
withoutAnimation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 顶部菜单激活路径
|
||||||
|
const activeTopMenuPath = useStorage("activeTopMenuPath", "");
|
||||||
|
|
||||||
|
// 切换侧边栏
|
||||||
|
function toggleSidebar() {
|
||||||
|
sidebar.opened = !sidebar.opened;
|
||||||
|
sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭侧边栏
|
||||||
|
function closeSideBar() {
|
||||||
|
sidebar.opened = false;
|
||||||
|
sidebarStatus.value = SidebarStatus.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开侧边栏
|
||||||
|
function openSideBar() {
|
||||||
|
sidebar.opened = true;
|
||||||
|
sidebarStatus.value = SidebarStatus.OPENED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换设备
|
||||||
|
function toggleDevice(val: string) {
|
||||||
|
device.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改变布局大小
|
||||||
|
*
|
||||||
|
* @param val 布局大小 default | small | large
|
||||||
|
*/
|
||||||
|
function changeSize(val: string) {
|
||||||
|
size.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混合模式顶部切换
|
||||||
|
*/
|
||||||
|
function activeTopMenu(val: string) {
|
||||||
|
activeTopMenuPath.value = val;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
device,
|
||||||
|
sidebar,
|
||||||
|
size,
|
||||||
|
activeTopMenu,
|
||||||
|
toggleDevice,
|
||||||
|
changeSize,
|
||||||
|
toggleSidebar,
|
||||||
|
closeSideBar,
|
||||||
|
openSideBar,
|
||||||
|
activeTopMenuPath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
|
||||||
|
* 官方文档解释了如何在组件外部使用 Pinia Store:
|
||||||
|
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
|
||||||
|
*/
|
||||||
|
export function useAppStoreHook() {
|
||||||
|
return useAppStore(store);
|
||||||
|
}
|
72
front/src/store/modules/dict.store.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { store } from "@/store";
|
||||||
|
import DictAPI, { type DictItemOption } from "@/api/system/dict.api";
|
||||||
|
|
||||||
|
export const useDictStore = defineStore("dict", () => {
|
||||||
|
// 字典数据缓存
|
||||||
|
const dictCache = useStorage<Record<string, DictItemOption[]>>("dict_cache", {});
|
||||||
|
|
||||||
|
// 请求队列(防止重复请求)
|
||||||
|
const requestQueue: Record<string, Promise<void>> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存字典数据
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
* @param data 字典项列表
|
||||||
|
*/
|
||||||
|
const cacheDictItems = (dictCode: string, data: DictItemOption[]) => {
|
||||||
|
dictCache.value[dictCode] = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载字典数据(如果缓存中没有则请求)
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
const loadDictItems = async (dictCode: string) => {
|
||||||
|
if (dictCache.value[dictCode]) return;
|
||||||
|
// 防止重复请求
|
||||||
|
if (!requestQueue[dictCode]) {
|
||||||
|
requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => {
|
||||||
|
cacheDictItems(dictCode, data);
|
||||||
|
Reflect.deleteProperty(requestQueue, dictCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await requestQueue[dictCode];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典项列表
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
* @returns 字典项列表
|
||||||
|
*/
|
||||||
|
const getDictItems = (dictCode: string): DictItemOption[] => {
|
||||||
|
return dictCache.value[dictCode] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定字典项
|
||||||
|
* @param dictCode 字典编码
|
||||||
|
*/
|
||||||
|
const removeDictItem = (dictCode: string) => {
|
||||||
|
if (dictCache.value[dictCode]) {
|
||||||
|
Reflect.deleteProperty(dictCache.value, dictCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空字典缓存
|
||||||
|
*/
|
||||||
|
const clearDictCache = () => {
|
||||||
|
dictCache.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadDictItems,
|
||||||
|
getDictItems,
|
||||||
|
removeDictItem,
|
||||||
|
clearDictCache,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useDictStoreHook() {
|
||||||
|
return useDictStore(store);
|
||||||
|
}
|
111
front/src/store/modules/permission.store.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
import { constantRoutes } from "@/router";
|
||||||
|
import { store } from "@/store";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
import MenuAPI, { type RouteVO } from "@/api/system/menu.api";
|
||||||
|
const modules = import.meta.glob("../../views/**/**.vue");
|
||||||
|
const Layout = () => import("@/layout/index.vue");
|
||||||
|
|
||||||
|
export const usePermissionStore = defineStore("permission", () => {
|
||||||
|
// 储所有路由,包括静态路由和动态路由
|
||||||
|
const routes = ref<RouteRecordRaw[]>([]);
|
||||||
|
// 混合模式左侧菜单路由
|
||||||
|
const mixedLayoutLeftRoutes = ref<RouteRecordRaw[]>([]);
|
||||||
|
// 路由是否加载完成
|
||||||
|
const isRoutesLoaded = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后台动态路由数据,解析并注册到全局路由
|
||||||
|
*
|
||||||
|
* @returns Promise<RouteRecordRaw[]> 解析后的动态路由列表
|
||||||
|
*/
|
||||||
|
function generateRoutes() {
|
||||||
|
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
|
||||||
|
MenuAPI.getRoutes()
|
||||||
|
.then((data) => {
|
||||||
|
const dynamicRoutes = parseDynamicRoutes(data);
|
||||||
|
routes.value = [...constantRoutes, ...dynamicRoutes];
|
||||||
|
isRoutesLoaded.value = true;
|
||||||
|
resolve(dynamicRoutes);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据父菜单路径设置混合模式左侧菜单
|
||||||
|
*
|
||||||
|
* @param parentPath 父菜单的路径,用于查找对应的菜单项
|
||||||
|
*/
|
||||||
|
const setMixedLayoutLeftRoutes = (parentPath: string) => {
|
||||||
|
const matchedItem = routes.value.find((item) => item.path === parentPath);
|
||||||
|
if (matchedItem && matchedItem.children) {
|
||||||
|
mixedLayoutLeftRoutes.value = matchedItem.children;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置路由
|
||||||
|
*/
|
||||||
|
const resetRouter = () => {
|
||||||
|
// 从 router 实例中移除动态路由
|
||||||
|
routes.value.forEach((route) => {
|
||||||
|
if (route.name && !constantRoutes.find((r) => r.name === route.name)) {
|
||||||
|
router.removeRoute(route.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空本地存储的路由和菜单数据
|
||||||
|
routes.value = [];
|
||||||
|
mixedLayoutLeftRoutes.value = [];
|
||||||
|
isRoutesLoaded.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
routes,
|
||||||
|
mixedLayoutLeftRoutes,
|
||||||
|
isRoutesLoaded,
|
||||||
|
generateRoutes,
|
||||||
|
setMixedLayoutLeftRoutes,
|
||||||
|
resetRouter,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置
|
||||||
|
*
|
||||||
|
* @param rawRoutes 后端返回的原始路由数据
|
||||||
|
* @returns 解析后的路由配置数组
|
||||||
|
*/
|
||||||
|
const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
||||||
|
const parsedRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
|
rawRoutes.forEach((route) => {
|
||||||
|
const normalizedRoute = { ...route } as RouteRecordRaw;
|
||||||
|
|
||||||
|
// 处理组件路径
|
||||||
|
normalizedRoute.component =
|
||||||
|
normalizedRoute.component?.toString() === "Layout"
|
||||||
|
? Layout
|
||||||
|
: modules[`../../views/${normalizedRoute.component}.vue`] ||
|
||||||
|
modules["../../views/error-page/404.vue"];
|
||||||
|
|
||||||
|
// 递归解析子路由
|
||||||
|
if (normalizedRoute.children) {
|
||||||
|
normalizedRoute.children = parseDynamicRoutes(route.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedRoutes.push(normalizedRoute);
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedRoutes;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 在组件外使用 Pinia store 实例 @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
|
||||||
|
*/
|
||||||
|
export function usePermissionStoreHook() {
|
||||||
|
return usePermissionStore(store);
|
||||||
|
}
|
97
front/src/store/modules/settings.store.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import defaultSettings from "@/settings";
|
||||||
|
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
|
||||||
|
import { LayoutMode } from "@/enums/settings/layout.enum";
|
||||||
|
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
|
||||||
|
|
||||||
|
type SettingsValue = boolean | string;
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore("setting", () => {
|
||||||
|
// 基本设置
|
||||||
|
const settingsVisible = ref(false);
|
||||||
|
// 标签视图
|
||||||
|
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
|
||||||
|
// 侧边栏 Logo
|
||||||
|
const sidebarLogo = useStorage<boolean>("sidebarLogo", defaultSettings.sidebarLogo);
|
||||||
|
// 侧边栏配色方案 (经典蓝/极简白)
|
||||||
|
const sidebarColorScheme = useStorage<string>(
|
||||||
|
"sidebarColorScheme",
|
||||||
|
defaultSettings.sidebarColorScheme
|
||||||
|
);
|
||||||
|
// 布局
|
||||||
|
const layout = useStorage<LayoutMode>("layout", defaultSettings.layout as LayoutMode);
|
||||||
|
// 水印
|
||||||
|
const watermarkEnabled = useStorage<boolean>(
|
||||||
|
"watermarkEnabled",
|
||||||
|
defaultSettings.watermarkEnabled
|
||||||
|
);
|
||||||
|
|
||||||
|
// 主题
|
||||||
|
const themeColor = useStorage<string>("themeColor", defaultSettings.themeColor);
|
||||||
|
const theme = useStorage<ThemeMode>("theme", defaultSettings.theme);
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
watch(
|
||||||
|
[theme, themeColor],
|
||||||
|
([newTheme, newThemeColor]) => {
|
||||||
|
toggleDarkMode(newTheme === ThemeMode.DARK);
|
||||||
|
const colors = generateThemeColors(newThemeColor, newTheme);
|
||||||
|
applyTheme(colors);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听浅色侧边栏配色方案变化
|
||||||
|
watch(
|
||||||
|
[sidebarColorScheme],
|
||||||
|
([newSidebarColorScheme]) => {
|
||||||
|
toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置映射
|
||||||
|
const settingsMap: Record<string, Ref<SettingsValue>> = {
|
||||||
|
tagsView,
|
||||||
|
sidebarLogo,
|
||||||
|
sidebarColorScheme,
|
||||||
|
layout,
|
||||||
|
watermarkEnabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
function changeSetting({ key, value }: { key: string; value: SettingsValue }) {
|
||||||
|
const setting = settingsMap[key];
|
||||||
|
if (setting) setting.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTheme(val: ThemeMode) {
|
||||||
|
theme.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSidebarColor(val: string) {
|
||||||
|
sidebarColorScheme.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeThemeColor(color: string) {
|
||||||
|
themeColor.value = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeLayout(val: LayoutMode) {
|
||||||
|
layout.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
settingsVisible,
|
||||||
|
tagsView,
|
||||||
|
sidebarLogo,
|
||||||
|
sidebarColorScheme,
|
||||||
|
layout,
|
||||||
|
themeColor,
|
||||||
|
theme,
|
||||||
|
watermarkEnabled,
|
||||||
|
changeSetting,
|
||||||
|
changeTheme,
|
||||||
|
changeThemeColor,
|
||||||
|
changeLayout,
|
||||||
|
changeSidebarColor,
|
||||||
|
};
|
||||||
|
});
|
257
front/src/store/modules/tags-view.store.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||||
|
const visitedViews = ref<TagView[]>([]);
|
||||||
|
const cachedViews = ref<string[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加已访问视图到已访问视图列表中
|
||||||
|
*/
|
||||||
|
function addVisitedView(view: TagView) {
|
||||||
|
// 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加
|
||||||
|
if (view.path.startsWith("/redirect")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (visitedViews.value.some((v) => v.name === view.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
|
||||||
|
if (view.affix) {
|
||||||
|
visitedViews.value.unshift(view);
|
||||||
|
} else {
|
||||||
|
// 如果视图不是固定的,则在已访问的视图列表的末尾添加
|
||||||
|
visitedViews.value.push(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加缓存视图到缓存视图列表中
|
||||||
|
*/
|
||||||
|
function addCachedView(view: TagView) {
|
||||||
|
const viewName = view.name;
|
||||||
|
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
|
||||||
|
if (cachedViews.value.includes(viewName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
||||||
|
if (view.keepAlive) {
|
||||||
|
cachedViews.value.push(viewName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从已访问视图列表中删除指定的视图
|
||||||
|
*/
|
||||||
|
function delVisitedView(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
for (const [i, v] of visitedViews.value.entries()) {
|
||||||
|
// 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图
|
||||||
|
if (v.path === view.path) {
|
||||||
|
visitedViews.value.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve([...visitedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delCachedView(view: TagView) {
|
||||||
|
const viewName = view.name;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = cachedViews.value.indexOf(viewName);
|
||||||
|
if (index > -1) {
|
||||||
|
cachedViews.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
resolve([...cachedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function delOtherVisitedViews(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
visitedViews.value = visitedViews.value.filter((v) => {
|
||||||
|
return v?.affix || v.path === view.path;
|
||||||
|
});
|
||||||
|
resolve([...visitedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delOtherCachedViews(view: TagView) {
|
||||||
|
const viewName = view.name as string;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = cachedViews.value.indexOf(viewName);
|
||||||
|
if (index > -1) {
|
||||||
|
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||||
|
} else {
|
||||||
|
// if index = -1, there is no cached tags
|
||||||
|
cachedViews.value = [];
|
||||||
|
}
|
||||||
|
resolve([...cachedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVisitedView(view: TagView) {
|
||||||
|
for (let v of visitedViews.value) {
|
||||||
|
if (v.path === view.path) {
|
||||||
|
v = Object.assign(v, view);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addView(view: TagView) {
|
||||||
|
addVisitedView(view);
|
||||||
|
addCachedView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delView(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
delVisitedView(view);
|
||||||
|
delCachedView(view);
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...visitedViews.value],
|
||||||
|
cachedViews: [...cachedViews.value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delOtherViews(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
delOtherVisitedViews(view);
|
||||||
|
delOtherCachedViews(view);
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...visitedViews.value],
|
||||||
|
cachedViews: [...cachedViews.value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delLeftViews(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
|
||||||
|
if (currIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||||
|
if (index >= currIndex || item?.affix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheIndex = cachedViews.value.indexOf(item.name);
|
||||||
|
if (cacheIndex > -1) {
|
||||||
|
cachedViews.value.splice(cacheIndex, 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...visitedViews.value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delRightViews(view: TagView) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
|
||||||
|
if (currIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||||
|
if (index <= currIndex || item?.affix) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...visitedViews.value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delAllViews() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
|
||||||
|
visitedViews.value = affixTags;
|
||||||
|
cachedViews.value = [];
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...visitedViews.value],
|
||||||
|
cachedViews: [...cachedViews.value],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delAllVisitedViews() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
|
||||||
|
visitedViews.value = affixTags;
|
||||||
|
resolve([...visitedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delAllCachedViews() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
cachedViews.value = [];
|
||||||
|
resolve([...cachedViews.value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭当前tagView
|
||||||
|
*/
|
||||||
|
function closeCurrentView() {
|
||||||
|
const tags: TagView = {
|
||||||
|
name: route.name as string,
|
||||||
|
title: route.meta.title as string,
|
||||||
|
path: route.path,
|
||||||
|
fullPath: route.fullPath,
|
||||||
|
affix: route.meta?.affix,
|
||||||
|
keepAlive: route.meta?.keepAlive,
|
||||||
|
query: route.query,
|
||||||
|
};
|
||||||
|
delView(tags).then((res: any) => {
|
||||||
|
if (isActive(tags)) {
|
||||||
|
toLastView(res.visitedViews, tags);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(tag: TagView) {
|
||||||
|
return tag.path === route.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLastView(visitedViews: TagView[], view?: TagView) {
|
||||||
|
const latestView = visitedViews.slice(-1)[0];
|
||||||
|
if (latestView && latestView.fullPath) {
|
||||||
|
router.push(latestView.fullPath);
|
||||||
|
} else {
|
||||||
|
// now the default is to redirect to the home page if there is no tags-view,
|
||||||
|
// you can adjust it according to your needs.
|
||||||
|
if (view?.name === "Dashboard") {
|
||||||
|
// to reload home page
|
||||||
|
router.replace("/redirect" + view.fullPath);
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitedViews,
|
||||||
|
cachedViews,
|
||||||
|
addVisitedView,
|
||||||
|
addCachedView,
|
||||||
|
delVisitedView,
|
||||||
|
delCachedView,
|
||||||
|
delOtherVisitedViews,
|
||||||
|
delOtherCachedViews,
|
||||||
|
updateVisitedView,
|
||||||
|
addView,
|
||||||
|
delView,
|
||||||
|
delOtherViews,
|
||||||
|
delLeftViews,
|
||||||
|
delRightViews,
|
||||||
|
delAllViews,
|
||||||
|
delAllVisitedViews,
|
||||||
|
delAllCachedViews,
|
||||||
|
closeCurrentView,
|
||||||
|
isActive,
|
||||||
|
toLastView,
|
||||||
|
};
|
||||||
|
});
|
122
front/src/store/modules/user.store.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { store } from "@/store";
|
||||||
|
import { usePermissionStoreHook } from "@/store/modules/permission.store";
|
||||||
|
import { useDictStoreHook } from "@/store/modules/dict.store";
|
||||||
|
|
||||||
|
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
|
||||||
|
import UserAPI, { type UserInfo } from "@/api/system/user.api";
|
||||||
|
|
||||||
|
import { setAccessToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
|
||||||
|
|
||||||
|
export const useUserStore = defineStore("user", () => {
|
||||||
|
const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* @param LoginFormData
|
||||||
|
*/
|
||||||
|
function login(LoginFormData: LoginFormData) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
AuthAPI.login(LoginFormData)
|
||||||
|
.then((data) => {
|
||||||
|
const { accessToken, refreshToken } = data;
|
||||||
|
setAccessToken(accessToken); // eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||||
|
setRefreshToken(refreshToken);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*
|
||||||
|
* @returns {UserInfo} 用户信息
|
||||||
|
*/
|
||||||
|
function getUserInfo() {
|
||||||
|
return new Promise<UserInfo>((resolve, reject) => {
|
||||||
|
UserAPI.getInfo()
|
||||||
|
.then((data) => {
|
||||||
|
if (!data) {
|
||||||
|
reject("Verification failed, please Login again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(userInfo.value, { ...data });
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
AuthAPI.logout()
|
||||||
|
.then(() => {
|
||||||
|
clearSessionAndCache();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新 token
|
||||||
|
*/
|
||||||
|
function refreshToken() {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
AuthAPI.refreshToken(refreshToken)
|
||||||
|
.then((data) => {
|
||||||
|
const { accessToken, refreshToken } = data;
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
setRefreshToken(refreshToken);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(" refreshToken 刷新失败", error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户会话和缓存
|
||||||
|
*/
|
||||||
|
function clearSessionAndCache() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
clearToken();
|
||||||
|
usePermissionStoreHook().resetRouter();
|
||||||
|
useDictStoreHook().clearDictCache();
|
||||||
|
userInfo.value = {} as UserInfo;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
getUserInfo,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
clearSessionAndCache,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
|
||||||
|
* 官方文档解释了如何在组件外部使用 Pinia Store:
|
||||||
|
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
|
||||||
|
*/
|
||||||
|
export function useUserStoreHook() {
|
||||||
|
return useUserStore(store);
|
||||||
|
}
|
1780
front/src/types/auto-imports.d.ts
vendored
Normal file
100
front/src/types/components.d.ts
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module "vue" {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
AppLink: (typeof import("./../components/AppLink/index.vue"))["default"];
|
||||||
|
AppMain: (typeof import("./../layout/components/AppMain/index.vue"))["default"];
|
||||||
|
Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"];
|
||||||
|
CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"];
|
||||||
|
CURD: (typeof import("./../components/CURD/index.vue"))["default"];
|
||||||
|
Dict: (typeof import("./../components/Dict/index.vue"))["default"];
|
||||||
|
DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"];
|
||||||
|
ECharts: (typeof import("./../components/ECharts/index.vue"))["default"];
|
||||||
|
ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];
|
||||||
|
ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"];
|
||||||
|
ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"];
|
||||||
|
ElButton: (typeof import("element-plus/es"))["ElButton"];
|
||||||
|
ElCard: (typeof import("element-plus/es"))["ElCard"];
|
||||||
|
ElCascader: (typeof import("element-plus/es"))["ElCascader"];
|
||||||
|
ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"];
|
||||||
|
ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"];
|
||||||
|
ElCol: (typeof import("element-plus/es"))["ElCol"];
|
||||||
|
ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"];
|
||||||
|
ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"];
|
||||||
|
ElDatePicker: (typeof import("element-plus/es"))["ElDatePicker"];
|
||||||
|
ElDialog: (typeof import("element-plus/es"))["ElDialog"];
|
||||||
|
ElDivider: (typeof import("element-plus/es"))["ElDivider"];
|
||||||
|
ElDrawer: (typeof import("element-plus/es"))["ElDrawer"];
|
||||||
|
ElDropdown: (typeof import("element-plus/es"))["ElDropdown"];
|
||||||
|
ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"];
|
||||||
|
ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"];
|
||||||
|
ElForm: (typeof import("element-plus/es"))["ElForm"];
|
||||||
|
ElFormItem: (typeof import("element-plus/es"))["ElFormItem"];
|
||||||
|
ElIcon: (typeof import("element-plus/es"))["ElIcon"];
|
||||||
|
ElImage: (typeof import("element-plus/es"))["ElImage"];
|
||||||
|
ElInput: (typeof import("element-plus/es"))["ElInput"];
|
||||||
|
ElInputTag: (typeof import("element-plus/es"))["ElInputTag"];
|
||||||
|
ElInputNumber: (typeof import("element-plus/es"))["ElInputNumber"];
|
||||||
|
ElLink: (typeof import("element-plus/es"))["ElLink"];
|
||||||
|
ElMenu: (typeof import("element-plus/es"))["ElMenu"];
|
||||||
|
ElMenuItem: (typeof import("element-plus/es"))["ElMenuItem"];
|
||||||
|
ElOption: (typeof import("element-plus/es"))["ElOption"];
|
||||||
|
ElPagination: (typeof import("element-plus/es"))["ElPagination"];
|
||||||
|
ElPopover: (typeof import("element-plus/es"))["ElPopover"];
|
||||||
|
ElRadio: (typeof import("element-plus/es"))["ElRadio"];
|
||||||
|
ElRadioGroup: (typeof import("element-plus/es"))["ElRadioGroup"];
|
||||||
|
ElRow: (typeof import("element-plus/es"))["ElRow"];
|
||||||
|
ElScrollbar: (typeof import("element-plus/es"))["ElScrollbar"];
|
||||||
|
ElSelect: (typeof import("element-plus/es"))["ElSelect"];
|
||||||
|
ElStatistic: (typeof import("element-plus/es"))["ElStatistic"];
|
||||||
|
ElSubMenu: (typeof import("element-plus/es"))["ElSubMenu"];
|
||||||
|
ElSwitch: (typeof import("element-plus/es"))["ElSwitch"];
|
||||||
|
ElTable: (typeof import("element-plus/es"))["ElTable"];
|
||||||
|
ElTableColumn: (typeof import("element-plus/es"))["ElTableColumn"];
|
||||||
|
ElTag: (typeof import("element-plus/es"))["ElTag"];
|
||||||
|
ElText: (typeof import("element-plus/es"))["ElText"];
|
||||||
|
ElTimeSelect: (typeof import("element-plus/es"))["ElTimeSelect"];
|
||||||
|
ElTooltip: (typeof import("element-plus/es"))["ElTooltip"];
|
||||||
|
ElTree: (typeof import("element-plus/es"))["ElTree"];
|
||||||
|
ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"];
|
||||||
|
ElUpload: (typeof import("element-plus/es"))["ElUpload"];
|
||||||
|
ElWatermark: (typeof import("element-plus/es"))["ElWatermark"];
|
||||||
|
ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"];
|
||||||
|
FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"];
|
||||||
|
Form: (typeof import("./../components/CURD/Form.vue"))["default"];
|
||||||
|
Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"];
|
||||||
|
GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"];
|
||||||
|
Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"];
|
||||||
|
IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"];
|
||||||
|
LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"];
|
||||||
|
MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"];
|
||||||
|
MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"];
|
||||||
|
NoticeDropdown: (typeof import("./../components/Notice/NoticeDropdown.vue"))["default"];
|
||||||
|
LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"];
|
||||||
|
PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"];
|
||||||
|
PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"];
|
||||||
|
PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"];
|
||||||
|
Pagination: (typeof import("./../components/Pagination/index.vue"))["default"];
|
||||||
|
RouterLink: (typeof import("vue-router"))["RouterLink"];
|
||||||
|
RouterView: (typeof import("vue-router"))["RouterView"];
|
||||||
|
Settings: (typeof import("./../layout/components/Settings/index.vue"))["default"];
|
||||||
|
Sidebar: (typeof import("./../layout/components/Sidebar/index.vue"))["default"];
|
||||||
|
SidebarLogo: (typeof import("./../layout/components/Sidebar/components/SidebarLogo.vue"))["default"];
|
||||||
|
SidebarMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMenu.vue"))["default"];
|
||||||
|
SidebarMenuItem: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItem.vue"))["default"];
|
||||||
|
SidebarMenuItemTitle: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItemTitle.vue"))["default"];
|
||||||
|
SidebarMixTopMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue"))["default"];
|
||||||
|
SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"];
|
||||||
|
SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"];
|
||||||
|
TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"];
|
||||||
|
TagsView: (typeof import("./../layout/components/TagsView/index.vue"))["default"];
|
||||||
|
WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"];
|
||||||
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"];
|
||||||
|
}
|
||||||
|
}
|
33
front/src/types/env.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// https://cn.vitejs.dev/guide/env-and-mode
|
||||||
|
|
||||||
|
// TypeScript 类型提示都为 string: https://github.com/vitejs/vite/issues/6930
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
/** 应用端口 */
|
||||||
|
VITE_APP_PORT: number;
|
||||||
|
/** API 基础路径(代理前缀) */
|
||||||
|
VITE_APP_BASE_API: string;
|
||||||
|
/** API 地址 */
|
||||||
|
VITE_APP_API_URL: string;
|
||||||
|
/** 是否开启 Mock 服务 */
|
||||||
|
VITE_MOCK_DEV_SERVER: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示
|
||||||
|
*/
|
||||||
|
declare const __APP_INFO__: {
|
||||||
|
pkg: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
engines: {
|
||||||
|
node: string;
|
||||||
|
};
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
devDependencies: Record<string, string>;
|
||||||
|
};
|
||||||
|
buildTimestamp: number;
|
||||||
|
};
|
109
front/src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* 响应数据
|
||||||
|
*/
|
||||||
|
interface ResponseData<T = any> {
|
||||||
|
code: string;
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询参数
|
||||||
|
*/
|
||||||
|
interface PageQuery {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页响应对象
|
||||||
|
*/
|
||||||
|
interface PageResult<T> {
|
||||||
|
/** 数据列表 */
|
||||||
|
list: T;
|
||||||
|
/** 总数 */
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页签对象
|
||||||
|
*/
|
||||||
|
interface TagView {
|
||||||
|
/** 页签名称 */
|
||||||
|
name: string;
|
||||||
|
/** 页签标题 */
|
||||||
|
title: string;
|
||||||
|
/** 页签路由路径 */
|
||||||
|
path: string;
|
||||||
|
/** 页签路由完整路径 */
|
||||||
|
fullPath: string;
|
||||||
|
/** 页签图标 */
|
||||||
|
icon?: string;
|
||||||
|
/** 是否固定页签 */
|
||||||
|
affix?: boolean;
|
||||||
|
/** 是否开启缓存 */
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/** 路由查询参数 */
|
||||||
|
query?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统设置
|
||||||
|
*/
|
||||||
|
interface AppSettings {
|
||||||
|
/** 系统标题 */
|
||||||
|
title: string;
|
||||||
|
/** 系统版本 */
|
||||||
|
version: string;
|
||||||
|
/** 是否显示设置 */
|
||||||
|
showSettings: boolean;
|
||||||
|
/** 是否显示多标签导航 */
|
||||||
|
tagsView: boolean;
|
||||||
|
/** 是否显示侧边栏Logo */
|
||||||
|
sidebarLogo: boolean;
|
||||||
|
/** 导航栏布局(left|top|mix) */
|
||||||
|
layout: "left" | "top" | "mix";
|
||||||
|
/** 主题颜色 */
|
||||||
|
themeColor: string;
|
||||||
|
/** 主题模式(dark|light) */
|
||||||
|
theme: import("@/enums/settings/theme.enum").ThemeMode;
|
||||||
|
/** 布局大小(default |large |small) */
|
||||||
|
size: string;
|
||||||
|
/** 语言( zh-cn| en) */
|
||||||
|
language: string;
|
||||||
|
/** 是否开启水印 */
|
||||||
|
watermarkEnabled: boolean;
|
||||||
|
/** 水印内容 */
|
||||||
|
watermarkContent: string;
|
||||||
|
/** 侧边栏配色方案 */
|
||||||
|
sidebarColorScheme: "classic-blue" | "minimal-white";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下拉选项数据类型
|
||||||
|
*/
|
||||||
|
interface OptionType {
|
||||||
|
/** 值 */
|
||||||
|
value: string | number;
|
||||||
|
/** 文本 */
|
||||||
|
label: string;
|
||||||
|
/** 子列表 */
|
||||||
|
children?: OptionType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入结果
|
||||||
|
*/
|
||||||
|
interface ExcelResult {
|
||||||
|
/** 状态码 */
|
||||||
|
code: string;
|
||||||
|
/** 无效数据条数 */
|
||||||
|
invalidCount: number;
|
||||||
|
/** 有效数据条数 */
|
||||||
|
validCount: number;
|
||||||
|
/** 错误信息 */
|
||||||
|
messageList: Array<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export {};
|
54
front/src/types/router.d.ts
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import "vue-router";
|
||||||
|
|
||||||
|
declare module "vue-router" {
|
||||||
|
// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
|
||||||
|
// 可以通过扩展 RouteMeta 接口来输入 meta 字段
|
||||||
|
interface RouteMeta {
|
||||||
|
/**
|
||||||
|
* 菜单名称
|
||||||
|
* @example 'Dashboard'
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单图标
|
||||||
|
* @example 'el-icon-edit'
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否隐藏菜单
|
||||||
|
* true 隐藏, false 显示
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hidden?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 始终显示父级菜单,即使只有一个子菜单
|
||||||
|
* true 显示父级菜单, false 隐藏父级菜单,显示唯一子节点
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
alwaysShow?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否固定在页签上
|
||||||
|
* true 固定, false 不固定
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
affix?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否缓存页面
|
||||||
|
* true 缓存, false 不缓存
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
keepAlive?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在面包屑导航中隐藏
|
||||||
|
* true 隐藏, false 显示
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
breadcrumb?: boolean;
|
||||||
|
}
|
||||||
|
}
|
5
front/src/types/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
6
front/src/types/socket.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// https://github.com/sockjs/sockjs-client/issues/565
|
||||||
|
|
||||||
|
declare module "sockjs-client/dist/sockjs.min.js" {
|
||||||
|
import Client from "sockjs-client";
|
||||||
|
export default Client;
|
||||||
|
}
|
15
front/src/types/websocket.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典WebSocket事件类型
|
||||||
|
*/
|
||||||
|
export interface DictWebSocketEvent {
|
||||||
|
/** 事件类型:更新或删除 */
|
||||||
|
type: "DICT_UPDATED" | "DICT_DELETED";
|
||||||
|
/** 字典编码 */
|
||||||
|
dictCode: string;
|
||||||
|
/** 时间戳 */
|
||||||
|
timestamp: number;
|
||||||
|
}
|
27
front/src/utils/auth.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 访问 token 缓存的 key
|
||||||
|
const ACCESS_TOKEN_KEY = "access_token";
|
||||||
|
// 刷新 token 缓存的 key
|
||||||
|
const REFRESH_TOKEN_KEY = "refresh_token";
|
||||||
|
|
||||||
|
function getAccessToken(): string {
|
||||||
|
return localStorage.getItem(ACCESS_TOKEN_KEY) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccessToken(token: string) {
|
||||||
|
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRefreshToken(): string {
|
||||||
|
return localStorage.getItem(REFRESH_TOKEN_KEY) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRefreshToken(token: string) {
|
||||||
|
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getAccessToken, setAccessToken, clearToken, getRefreshToken, setRefreshToken };
|
12
front/src/utils/i18n.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// translate router.meta.title, be used in breadcrumb sidebar tagsview
|
||||||
|
import i18n from "@/lang/index";
|
||||||
|
|
||||||
|
export function translateRouteTitle(title: any) {
|
||||||
|
// 判断是否存在国际化配置,如果没有原生返回
|
||||||
|
const hasKey = i18n.global.te("route." + title);
|
||||||
|
if (hasKey) {
|
||||||
|
const translatedTitle = i18n.global.t("route." + title);
|
||||||
|
return translatedTitle;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
58
front/src/utils/index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Check if an element has a class
|
||||||
|
* @param {HTMLElement} ele
|
||||||
|
* @param {string} cls
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function hasClass(ele: HTMLElement, cls: string) {
|
||||||
|
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add class to element
|
||||||
|
* @param {HTMLElement} ele
|
||||||
|
* @param {string} cls
|
||||||
|
*/
|
||||||
|
export function addClass(ele: HTMLElement, cls: string) {
|
||||||
|
if (!hasClass(ele, cls)) ele.className += " " + cls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove class from element
|
||||||
|
* @param {HTMLElement} ele
|
||||||
|
* @param {string} cls
|
||||||
|
*/
|
||||||
|
export function removeClass(ele: HTMLElement, cls: string) {
|
||||||
|
if (hasClass(ele, cls)) {
|
||||||
|
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
|
||||||
|
ele.className = ele.className.replace(reg, " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是外部链接
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isExternal(path: string) {
|
||||||
|
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||||
|
return isExternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值
|
||||||
|
*
|
||||||
|
* @param growthRate
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function formatGrowthRate(growthRate: number) {
|
||||||
|
if (growthRate === 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedRate = Math.abs(growthRate * 100)
|
||||||
|
.toFixed(2)
|
||||||
|
.replace(/\.?0+$/, "");
|
||||||
|
return formattedRate + "%";
|
||||||
|
}
|
18
front/src/utils/nprogress.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import NProgress from "nprogress";
|
||||||
|
import "nprogress/nprogress.css";
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
NProgress.configure({
|
||||||
|
// 动画方式
|
||||||
|
easing: "ease",
|
||||||
|
// 递增进度条的速度
|
||||||
|
speed: 500,
|
||||||
|
// 是否显示加载ico
|
||||||
|
showSpinner: false,
|
||||||
|
// 自动递增间隔
|
||||||
|
trickleSpeed: 200,
|
||||||
|
// 初始化时的最小百分比
|
||||||
|
minimum: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NProgress;
|
106
front/src/utils/request.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
|
||||||
|
import qs from "qs";
|
||||||
|
import { useUserStoreHook } from "@/store/modules/user.store";
|
||||||
|
import { ResultEnum } from "@/enums/api/result.enum";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
import router from "@/router";
|
||||||
|
import {Message} from "vue-devui";
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||||
|
timeout: 50000,
|
||||||
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||||
|
paramsSerializer: (params) => qs.stringify(params),
|
||||||
|
});
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
const accessToken = getAccessToken();
|
||||||
|
// 如果 Authorization 设置为 no-auth,则不携带 Token
|
||||||
|
if (config.headers.Authorization !== "no-auth" && accessToken) {
|
||||||
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
} else {
|
||||||
|
delete config.headers.Authorization;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
// 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等
|
||||||
|
if (response.config.responseType === "blob") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const { code, data, msg } = response.data;
|
||||||
|
if (code === ResultEnum.SUCCESS) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
Message.error(msg || "系统出错");
|
||||||
|
return Promise.reject(new Error(msg || "Error"));
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
console.error("request error", error); // for debug
|
||||||
|
const { config, response } = error;
|
||||||
|
if (response) {
|
||||||
|
const { code, msg } = response.data;
|
||||||
|
if (code === ResultEnum.ACCESS_TOKEN_INVALID) {
|
||||||
|
// Token 过期,刷新 Token
|
||||||
|
return handleTokenRefresh(config);
|
||||||
|
} else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
|
||||||
|
// 刷新 Token 过期,跳转登录页
|
||||||
|
await handleSessionExpired();
|
||||||
|
return Promise.reject(new Error(msg || "Error"));
|
||||||
|
} else {
|
||||||
|
Message.error(msg || "系统出错");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default service;
|
||||||
|
// 是否正在刷新标识,避免重复刷新
|
||||||
|
let isRefreshing = false;
|
||||||
|
// 因 Token 过期导致的请求等待队列
|
||||||
|
const waitingQueue: Array<() => void> = [];
|
||||||
|
// 刷新 Token 处理
|
||||||
|
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 封装需要重试的请求
|
||||||
|
const retryRequest = () => {
|
||||||
|
config.headers.Authorization = `Bearer ${getAccessToken()}`;
|
||||||
|
resolve(service(config));
|
||||||
|
};
|
||||||
|
waitingQueue.push(retryRequest);
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
useUserStoreHook()
|
||||||
|
.refreshToken()
|
||||||
|
.then(() => {
|
||||||
|
// 依次重试队列中所有请求, 重试后清空队列
|
||||||
|
waitingQueue.forEach((callback) => callback());
|
||||||
|
waitingQueue.length = 0;
|
||||||
|
})
|
||||||
|
.catch(async (error) => {
|
||||||
|
console.error("handleTokenRefresh error", error);
|
||||||
|
// 刷新 Token 失败,跳转登录页
|
||||||
|
await handleSessionExpired();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRefreshing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 处理会话过期
|
||||||
|
async function handleSessionExpired() {
|
||||||
|
ElNotification({
|
||||||
|
title: "提示",
|
||||||
|
message: "您的会话已过期,请重新登录",
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
await useUserStoreHook().clearSessionAndCache();
|
||||||
|
router.push("/login");
|
||||||
|
}
|
106
front/src/utils/theme.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { ThemeMode } from "@/enums";
|
||||||
|
|
||||||
|
// 辅助函数:将十六进制颜色转换为 RGB
|
||||||
|
function hexToRgb(hex: string): [number, number, number] {
|
||||||
|
const bigint = parseInt(hex.slice(1), 16);
|
||||||
|
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将 RGB 转换为十六进制颜色
|
||||||
|
function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:调整颜色亮度
|
||||||
|
/** function adjustBrightness(hex: string, factor: number, theme: string): string {
|
||||||
|
const rgb = hexToRgb(hex);
|
||||||
|
// 是否是暗黑模式
|
||||||
|
const isDarkMode = theme === "dark" ? 0 : 255;
|
||||||
|
const newRgb = rgb.map((val) =>
|
||||||
|
Math.max(0, Math.min(255, Math.round(val + (isDarkMode - val) * factor)))
|
||||||
|
) as [number, number, number];
|
||||||
|
return rgbToHex(...newRgb);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加深颜色值
|
||||||
|
* @param {String} color 颜色值字符串
|
||||||
|
* @param {Number} level 加深的程度,限0-1之间
|
||||||
|
* @returns {String} 返回处理后的颜色值
|
||||||
|
*/
|
||||||
|
export function getDarkColor(color: string, level: number): string {
|
||||||
|
const rgb = hexToRgb(color);
|
||||||
|
for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level));
|
||||||
|
return rgbToHex(rgb[0], rgb[1], rgb[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变浅颜色值
|
||||||
|
* @param {String} color 颜色值字符串
|
||||||
|
* @param {Number} level 加深的程度,限0-1之间
|
||||||
|
* @returns {String} 返回处理后的颜色值
|
||||||
|
*/
|
||||||
|
export const getLightColor = (color: string, level: number): string => {
|
||||||
|
const rgb = hexToRgb(color);
|
||||||
|
for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level));
|
||||||
|
return rgbToHex(rgb[0], rgb[1], rgb[2]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成主题色
|
||||||
|
* @param primary 主题色
|
||||||
|
* @param theme 主题类型
|
||||||
|
*/
|
||||||
|
export function generateThemeColors(primary: string, theme: ThemeMode) {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
primary,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成浅色变体
|
||||||
|
for (let i = 1; i <= 9; i++) {
|
||||||
|
colors[`primary-light-${i}`] =
|
||||||
|
theme === ThemeMode.LIGHT
|
||||||
|
? `${getLightColor(primary, i / 10)}`
|
||||||
|
: `${getDarkColor(primary, i / 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成深色变体
|
||||||
|
colors["primary-dark-2"] =
|
||||||
|
theme === ThemeMode.LIGHT ? `${getLightColor(primary, 0.2)}` : `${getDarkColor(primary, 0.3)}`;
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTheme(colors: Record<string, string>) {
|
||||||
|
const el = document.documentElement;
|
||||||
|
|
||||||
|
Object.entries(colors).forEach(([key, value]) => {
|
||||||
|
el.style.setProperty(`--el-color-${key}`, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换暗黑模式
|
||||||
|
*
|
||||||
|
* @param isDark 是否启用暗黑模式
|
||||||
|
*/
|
||||||
|
export function toggleDarkMode(isDark: boolean) {
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add(ThemeMode.DARK);
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove(ThemeMode.DARK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换浅色主题下的侧边栏颜色方案
|
||||||
|
*
|
||||||
|
* @param isBlue 布尔值,表示是否开启深蓝色侧边栏颜色方案
|
||||||
|
*/
|
||||||
|
export function toggleSidebarColor(isBuleSidebar: boolean) {
|
||||||
|
if (isBuleSidebar) {
|
||||||
|
document.documentElement.classList.add("sidebar-color-blue");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("sidebar-color-blue");
|
||||||
|
}
|
||||||
|
}
|
33
front/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 严格性和类型检查相关配置
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
// 模块和兼容性相关配置
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
// 调试和兼容性相关配置
|
||||||
|
"sourceMap": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// 类型声明相关配置
|
||||||
|
"types": ["node", "vite/client"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["mock/**/*.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
vueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0', // 允许外部访问
|
|
||||||
port: 5173, // 可选:指定端口
|
|
||||||
},
|
|
||||||
})
|
|
106
front/vite.config.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { type ConfigEnv, loadEnv, defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
import mockDevServerPlugin from "vite-plugin-mock-dev-server";
|
||||||
|
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
|
||||||
|
import { name, version, engines, dependencies, devDependencies } from "./package.json";
|
||||||
|
|
||||||
|
// 平台的名称、版本、运行所需的 node 版本、依赖、构建时间的类型提示
|
||||||
|
const __APP_INFO__ = {
|
||||||
|
pkg: { name, version, engines, dependencies, devDependencies },
|
||||||
|
buildTimestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const pathSrc = resolve(__dirname, "src");
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(({ mode }: ConfigEnv) => {
|
||||||
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
env.VITE_MOCK_DEV_SERVER === "true" ? mockDevServerPlugin() : null,
|
||||||
|
AutoImport({
|
||||||
|
// 导入 Vue 函数,如:ref, reactive, toRef 等
|
||||||
|
imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
|
||||||
|
eslintrc: {
|
||||||
|
enabled: false,
|
||||||
|
filepath: "./.eslintrc-auto-import.json",
|
||||||
|
globalsPropValue: true,
|
||||||
|
},
|
||||||
|
vueTemplate: true,
|
||||||
|
// 导入函数类型声明文件路径 (false:关闭自动生成)
|
||||||
|
dts: false,
|
||||||
|
// dts: "src/types/auto-imports.d.ts",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': pathSrc
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0', // 允许外部访问
|
||||||
|
port: +env.VITE_APP_PORT, // 可选:指定端口\
|
||||||
|
open: true, // 启动时自动打开浏览器
|
||||||
|
proxy: {
|
||||||
|
// 代理 /dev-api 的请求
|
||||||
|
[env.VITE_APP_BASE_API]: {
|
||||||
|
changeOrigin: true,
|
||||||
|
target: env.VITE_APP_API_URL,
|
||||||
|
rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 构建配置
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 2000, // 消除打包大小超过500kb警告
|
||||||
|
minify: "terser", // Vite 2.6.x 以上需要配置 minify: "terser", terserOptions 才能生效
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
|
||||||
|
drop_console: true, // 生产环境去除 console
|
||||||
|
drop_debugger: true, // 生产环境去除 debugger
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
comments: false, // 删除注释
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// manualChunks: {
|
||||||
|
// "vue-i18n": ["vue-i18n"],
|
||||||
|
// },
|
||||||
|
// 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
|
||||||
|
entryFileNames: "js/[name].[hash].js",
|
||||||
|
// 用于命名代码拆分时创建的共享块的输出命名
|
||||||
|
chunkFileNames: "js/[name].[hash].js",
|
||||||
|
// 用于输出静态资源的命名,[ext]表示文件扩展名
|
||||||
|
assetFileNames: (assetInfo: any) => {
|
||||||
|
const info = assetInfo.name.split(".");
|
||||||
|
let extType = info[info.length - 1];
|
||||||
|
// console.log('文件信息', assetInfo.name)
|
||||||
|
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
|
||||||
|
extType = "media";
|
||||||
|
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
|
||||||
|
extType = "img";
|
||||||
|
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
|
||||||
|
extType = "fonts";
|
||||||
|
}
|
||||||
|
return `${extType}/[name].[hash].[ext]`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__APP_INFO__: JSON.stringify(__APP_INFO__),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|