Compare commits

...

3 Commits

Author SHA1 Message Date
6b699a0590 Merge remote-tracking branch 'origin/dev' into dev 2025-05-04 23:09:56 +08:00
d5be70128c new Files 2025-05-04 23:07:09 +08:00
fe6fc6576d new Files 2025-04-29 19:11:52 +08:00
108 changed files with 8148 additions and 1619 deletions

15
front/.env.development Normal file
View 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
View 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
View File

@ -0,0 +1,23 @@
Login Page: - [*] 登录页面设计
- [ ] 输入合法性检查
- [ ] 忘记密码
- [ ] 验证码验证
- [ ] SSO设计
Permissions: - [ ] 呈现界面权限设计
- [ ]
Router: - [ ] 路由设计
COOKIES: - [ ] 记录登录状态的cookies设计
- [ ] 记录侧边栏状态
- [ ] 记录主题状态
- [ ] 记录患者信息
API: - [ ] 登录接口设计
- [ ] 患者信息接口设计
- [ ] 搜索接口设计
- [ ] 用户管理接口设计
- [ ] 数据统计接口设计
- [ ] 其他接口设计
OTHERS: - [ ] UI统一化设计
- [ ] 显示异常处理
- [ ] 性能优化设计
- [ ]

View File

@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

44
front/mock/auth.mock.ts Normal file
View 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
View 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
View 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",
},
},
]);

312
front/mock/menu.mock.ts Normal file
View File

@ -0,0 +1,312 @@
import { defineMock } from "./base";
export default defineMock([
{
url: "menus/routes",
method: ["GET"],
body: {
code: "00000",
data: [
{
path: "/Home",
component: "@/pages/HomePage.vue",
name: "Home",
meta: {
title: "首页",
icon: "home-icon",
hidden: false,
alwaysShow: true,
keepAlive: true,
params: null
},
children: [
{
path: "/menjizhen-item",
component: "@/pages/zl-station/menjizhenItemView.vue",
name: "MenjizhenItem",
meta: {
title: "门急诊医生站",
icon: "doctor-icon",
hidden: false,
alwaysShow: true,
keepAlive: true,
params: null
}
},
{
path: "/zhuyuan-item",
component: "@/pages/zl-station/zhuyuanItemView.vue",
name: "ZhuyuanItem",
meta: {
title: "住院医生站",
icon: "hospital-icon",
hidden: false,
alwaysShow: true,
keepAlive: true,
params: null
},
}
]
},
{
path: "/login",
component: "@/pages/LoginPage.vue",
name: "LoginView",
meta: {
title: "登录",
icon: "login-icon",
hidden: true,
alwaysShow: false,
keepAlive: false,
params: null
},
children: []
},
{
path: "/:pathMatch(.*)*",
component: "@/pages/404/notFoundPage.vue",
name: "NotFound",
meta: {
title: "404",
icon: "error-icon",
hidden: true,
alwaysShow: false,
keepAlive: false,
params: null
},
children: []
}
],
msg: "获取菜单路由成功"
}
},
// 获取菜单树形表格列表
{
url: "menus",
method: ["GET"],
body: {
code: "00000",
"data": [
{
"id": 1,
"parentId": 0,
"name": "首页",
"type": "CATALOG",
"routeName": "Home",
"routePath": "/Home",
"component": "@/pages/HomePage.vue",
"sort": 1,
"visible": 1,
"icon": "home-icon",
"redirect": null,
"perm": null,
"children": [
{
"id": 11,
"parentId": 1,
"name": "门急诊医生站",
"type": "MENU",
"routeName": "MenjizhenItem",
"routePath": "/menjizhen-item",
"component": "@/pages/zl-station/menjizhenItemView.vue",
"sort": 1,
"visible": 1,
"icon": "doctor-icon",
"redirect": null,
"perm": null,
"children": []
},
{
"id": 12,
"parentId": 1,
"name": "住院医生站",
"type": "MENU",
"routeName": "ZhuyuanItem",
"routePath": "/zhuyuan-item",
"component": "@/pages/zl-station/zhuyuanItemView.vue",
"sort": 2,
"visible": 1,
"icon": "hospital-icon",
"redirect": null,
"perm": null,
"children": []
}
]
},
{
"id": 2,
"parentId": 0,
"name": "登录",
"type": "MENU",
"routeName": "LoginView",
"routePath": "/login",
"component": "@/pages/LoginPage.vue",
"sort": 99,
"visible": 0, // 对应hidden: true
"icon": "login-icon",
"redirect": null,
"perm": null,
"children": []
}
],
msg: "一切ok",
},
},
// 获取菜单树形下拉列表
{
url: "menus/options",
method: ["GET"],
body: {
code: "00000",
"data": [
{
"value": 1,
"label": "首页",
"children": [
{
"value": 11,
"label": "门急诊医生站",
},
{
"value": 12,
"label": "住院医生站",
}
]
},
{
"value": 2,
"label": "登录",
"children": []
}
],
msg: "一切ok",
}
},
// 新增菜单
{
url: "menus",
method: ["POST"],
body({ body }) {
return {
code: "00000",
data: null,
msg: "新增菜单" + body.name + "成功",
};
},
},
// 获取菜单表单数据
{
url: "menus/:id/form",
method: ["GET"],
body: ({ params }) => {
return {
code: "00000",
data: menuMap[params.id],
msg: "一切ok",
};
},
},
// 修改菜单
{
url: "menus/:id",
method: ["PUT"],
body({ body }) {
return {
code: "00000",
data: null,
msg: "修改菜单" + body.name + "成功",
};
},
},
// 删除菜单
{
url: "menus/:id",
method: ["DELETE"],
body({ params }) {
return {
code: "00000",
data: null,
msg: "删除菜单" + params.id + "成功",
};
},
},
]);
// 菜单映射表数据
const menuMap: Record<string, any> = {
1: {
id: 1,
parentId: 0,
name: "首页",
type: "CATALOG",
routeName: "Home",
routePath: "/Home",
component: "@/pages/HomePage.vue",
perm: null,
visible: 1,
sort: 1,
icon: "home-icon",
redirect: null,
keepAlive: 1,
alwaysShow: 1,
params: null
},
11: {
id: 11,
parentId: 1,
name: "门急诊医生站",
type: "MENU",
routeName: "MenjizhenItem",
routePath: "/menjizhen-item",
component: "@/pages/zl-station/menjizhenItemView.vue",
perm: null,
visible: 1,
sort: 1,
icon: "doctor-icon",
redirect: null,
keepAlive: 1,
alwaysShow: 1,
params: null
},
12: {
id: 12,
parentId: 1,
name: "住院医生站",
type: "MENU",
routeName: "ZhuyuanItem",
routePath: "/zhuyuan-item",
component: "@/pages/zl-station/zhuyuanItemView.vue",
perm: null,
visible: 1,
sort: 2,
icon: "hospital-icon",
redirect: null,
keepAlive: 1,
alwaysShow: 1,
params: null
},
2: {
id: 2,
parentId: 0,
name: "登录",
type: "MENU",
routeName: "LoginView",
routePath: "/login",
component: "@/pages/LoginPage.vue",
perm: null,
visible: 0,
sort: 99,
icon: "login-icon",
redirect: null,
keepAlive: 0,
alwaysShow: 0,
params: null
}
};

334
front/mock/role.mock.ts Normal file
View 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
View 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],
},
};

1972
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,17 @@
},
"dependencies": {
"@devui-design/icons": "^1.4.0",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@fullcalendar/vue3": "^6.1.17",
"@vue/compiler-sfc": "^3.5.13",
"axios": "^1.9.0",
"bootstrap": "^5.3.5",
"chart.js": "^4.4.9",
"devui-theme": "^0.0.7",
"jquery": "^3.7.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"qs": "^6.14.0",
"stompjs": "^2.3.3",
"vue": "^3.5.13",
"vue-devui": "^1.6.32",
"vue-router": "^4.5.0",
@ -25,9 +28,20 @@
},
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@types/node": "^22.15.3",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@vitejs/plugin-vue": "^5.2.3",
"esbuild-plugin-vue": "^0.2.4",
"sass-embedded": "^1.86.3",
"vite": "^6.2.4",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.5.0",
"vite": "6.3.4",
"vite-plugin-mock-dev-server": "^1.8.5",
"vite-plugin-vue-devtools": "^7.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -1,32 +0,0 @@
<template>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="appendHtml()">添加病程</el-button>
</el-button-group>
</el-row>
<Source src="/code/AppendDoc.vue"></Source>
<Editor doc="https://www.x-emr.cn/doc/233.html" @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import axios from 'axios'//引入axios
let editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//添加日常病程
const appendHtml = function() {
axios.get('https://www.x-emr.cn/doc/233.html').then((result) => {
editor.appendHtml(result.data)
})
}
</script>

View File

@ -1,96 +0,0 @@
<template>
<Source src="/code/BindData.vue"></Source>
<el-row>
<el-col :span="16">
<Editor @load="onLoad" style="margin: 10px 0;"></Editor>
</el-col>
<el-col :span="8">
<el-card style="margin: 10px;">
<el-form @change="bindData()" label-width="auto">
<el-form-item label="姓名">
<el-input v-model="patient.pat_name" ></el-input>
</el-form-item>
<el-form-item label="性别">
<el-input v-model="patient.pat_sex"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input v-model="patient.pat_age"></el-input>
</el-form-item>
<el-form-item label="就诊科室">
<el-input v-model="patient.visit_dept"></el-input>
</el-form-item>
<el-form-item label="就诊号">
<el-input v-model="patient.pat_id"></el-input>
</el-form-item>
<el-form-item label="就诊时间">
<el-date-picker v-model="patient.visit_time" type="datetime" value-format="YYYY-MM-DD hh:mm:ss"></el-date-picker>
<el-radio-group v-model="patient.firstcall" style="margin-left: 10px;">
<el-radio value="1">初诊</el-radio>
<el-radio value="2">复诊</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="patient.pat_phone"></el-input>
</el-form-item>
<el-form-item label=" 家庭住址">
<el-input v-model="patient.pat_address"></el-input>
</el-form-item>
<el-form-item label=" 主诉">
<el-input v-model="patient.pat_appeal"></el-input>
</el-form-item>
<el-form-item label=" 现病史">
<el-input v-model="patient.pat_now_history"></el-input>
</el-form-item>
<el-form-item label=" 既往史">
<el-input v-model="patient.pat_past_history"></el-input>
</el-form-item>
<el-form-item label=" 过敏史">
<el-input v-model="patient.pat_allergy_history"></el-input>
</el-form-item>
<el-form-item label=" 诊断">
<el-input v-model="patient.diagnosis"></el-input>
</el-form-item>
<el-form-item label=" 处方">
<el-input v-model="patient.presc" type="textarea"></el-input>
</el-form-item>
<el-form-item label=" 建议">
<el-input v-model="patient.advice"></el-input>
</el-form-item>
<el-form-item label=" 医生签字">
<el-input v-model="patient.doctor_name"></el-input>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
const patient = ref({})
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
setTimeout(()=>{
//异步加载文档
editor.loadUrl('/mock/bind_data.html').then(()=>{
patient.value = editor.getBindObject()
})
//文档输入后表单值随着变化
editor.document.addEventListener('input', ()=>{
patient.value = editor.getBindObject()
})
}, 0)
}
//表单数据改变
const bindData = () => {
editor.setBindObject(patient.value)
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<Source src="/code/Calculate.vue"></Source>
<el-row>
<Editor doc="/mock/assess_table.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
</script>

View File

@ -1,47 +0,0 @@
<template>
<Source src="/code/Command.vue"></Source>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="execCommand('form')">表单模式</el-button>
<el-button plain type="primary" @click="execCommand('design')">设计模式</el-button>
<el-button plain type="primary" @click="execCommand('readonly')">只读模式</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
<el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button>
<el-button plain type="primary" @click="execCommand('previewPdf')">预览PDF</el-button>
<el-button plain type="primary" @click="execCommand('previewHtml')">预览HTML</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('exportHtml')">导出模板</el-button>
<el-button plain type="primary" @click="execCommand('exportJson')">导出数据</el-button>
<el-button plain type="primary" @click="execCommand('exportPdf')">导出PDF</el-button>
<el-button plain type="primary" @click="execCommand('exportWord')">导出Word</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('mobile')">移动填报</el-button>
</el-button-group>
</el-row>
<el-row>
<Editor doc="https://www.x-emr.cn/doc/999.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
var editor
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//文档命令I
const execCommand = (cmd) => {
let param = {fileName:'病案首页'}
editor.execCommand(cmd, param)
}
const current = ref("1")
</script>

View File

@ -1,60 +0,0 @@
<template>
<Source src="/code/DataTable.vue"></Source>
<el-row>
<el-col :span="2" style="vertical-align: center;">服务器地址</el-col>
<el-col :span="8"><el-input v-model="dataUrl" ></el-input></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="bindDataForTable">填充数据</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="clearDataTable">清除数据</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button></el-col>
<el-col :span="1"></el-col>
<el-col :span="1"><el-button plain type="primary" @click="execCommand('print')">打印</el-button></el-col>
</el-row>
<Editor @load="onLoad" doc="/mock/data_table.html" mode="design" style="margin: 10px 0;"></Editor>
</template>
<script>
import axios from 'axios'
export default{
data(){
return{
editor:null,
//服务端地址
dataUrl:'https://www.x-emr.cn/doc/list.json'
}
},
methods:{
//加载编辑器
onLoad: function(e) {
this.editor = e.target.contentWindow.editor
},
//获取数据到表格
bindDataForTable: function() {
axios.get(this.dataUrl).then(res=>{
this.editor.bindDataList('list', res.data)
let html = `<field tabindex="0" id="" type="DropdownList" contenteditable="false" class="blank input" title="请选择" data-list="[{&quot;value&quot;:&quot;0&quot;,&quot;text&quot;:&quot;选项1&quot;},{&quot;value&quot;:&quot;1&quot;,&quot;text&quot;:&quot;选项2&quot;}]" name="" data-code="" data-expression="" event="undefined" multi="false" validate="false" data-show-vaule="" data-show-id="">请选择</field>`
this.editor.$('#list tr:not(:first) td:nth-child(3)').html(html)
})
},
//清除数据表格
clearDataTable: function() {
this.editor.bindDataList('list', [])
},
execCommand : function(cmd){
this.editor.execCommand(cmd)
}
}
}
</script>

View File

@ -1,27 +0,0 @@
<template>
<Source src="/code/DocLang.vue"></Source>
<el-tabs v-model="activeName">
<el-tab-pane label="英文" name="en-us" >
<Editor mode="design" lang="en-us" doc="/mock/en_us.html"></Editor>
</el-tab-pane>
<el-tab-pane label="简体中文" name="zh-cn">
<Editor mode="design" lang="zh-cn" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="繁体中文" name="zh-tw">
<Editor mode="design" lang="zh-tw" doc="/mock/zh-tw.html"></Editor>
</el-tab-pane>
<el-tab-pane label="藏文" name="zh-bo">
<Editor mode="design" lang="zh-bo" doc="/mock/zh-bo.html"></Editor>
</el-tab-pane>
<el-tab-pane label="维文" name="zh-ug">
<Editor mode="design" lang="zh-ug" doc="/mock/zh-ug.html"></Editor>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue'
const activeName = ref('en-us')
</script>

View File

@ -1,20 +0,0 @@
<template>
<Source src="/code/DocMode.vue"></Source>
<el-tabs v-model="activeName">
<el-tab-pane label="设计模式" name="design">
<Editor mode="design" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="表单模式" name="form">
<Editor mode="form" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
<el-tab-pane label="阅读模式" name="readonly">
<Editor mode="readonly" doc="https://www.x-emr.cn/doc/999.html"></Editor>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { ref } from 'vue'
const activeName = ref('design')
</script>

View File

@ -1,153 +0,0 @@
<template>
<h2>echarts 代码参考 https://echarts.apache.org/examples/zh/index.html</h2>
<el-row>
<el-button-group>
<el-button plain type="primary" @click="addSvgEChart()">添加图表(SVG)</el-button>
<el-button plain type="primary" @click="addCanvasEChart()">添加图表(Canvas)</el-button>
</el-button-group>
</el-row>
<Source src="/code/EChart.vue"></Source>
<Editor doc="https://www.x-emr.cn/doc/asdf1.html" @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import * as echarts from 'echarts';
let editor = null
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//添加图表
const addSvgEChart = function () {
var _body = editor.document.getElementById('_body')
let chartDom = editor.document.createElement('div')
chartDom.style.width = '100%'
chartDom.style.height = '500px'
_body.appendChild(chartDom)
var myChart = echarts.init(chartDom, null, { renderer: 'svg' });
var option = {
title: {
text: 'Referer of a Website',
subtext: 'Fake Data',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
option && myChart.setOption(option);
}
//添加图表
const addCanvasEChart = function () {
var _body = editor.document.getElementById('_body')
let chartDom = editor.document.createElement('div')
chartDom.style.width = '100%'
chartDom.style.height = '500px'
_body.appendChild(chartDom)
var myChart = echarts.init(chartDom, null, { renderer: 'canvas' });
var option = {
title: {
text: 'Rainfall vs Evaporation',
subtext: 'Fake Data'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Rainfall', 'Evaporation']
},
toolbox: {
show: true,
feature: {
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
calculable: true,
xAxis: [
{
type: 'category',
// prettier-ignore
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Rainfall',
type: 'bar',
data: [
2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3
],
markPoint: {
data: [
{ type: 'max', name: 'Max' },
{ type: 'min', name: 'Min' }
]
},
markLine: {
data: [{ type: 'average', name: 'Avg' }]
}
},
{
name: 'Evaporation',
type: 'bar',
data: [
2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3
],
markPoint: {
data: [
{ name: 'Max', value: 182.2, xAxis: 7, yAxis: 183 },
{ name: 'Min', value: 2.3, xAxis: 11, yAxis: 3 }
]
},
markLine: {
data: [{ type: 'average', name: 'Avg' }]
}
}
]
};
option && myChart.setOption(option);
}
</script>

View File

@ -1,18 +0,0 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="./editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script>
export default {
data() {
return {
objectOfAttrs:{
width:'100%',
height:'800vh',
frameborder: 0
}
}
}
}
</script>

View File

@ -1,15 +0,0 @@
<template>
<h3>
Vue 集成X-EMR 编辑器步骤
</h3>
<h4>1.新建组件 Editor.vue</h4>
<Source src="/code/Editor.vue" open="true"></Source>
<h4>2.模块中使用@load获取组件的editor实例</h4>
<Source src="/code/Simple.vue" open="true"></Source>
<h4>3.自定义修改配置 editor.html</h4>
<Source src="/editor.html" open="true"></Source>
</template>
<script setup>
</script>

View File

@ -1,76 +0,0 @@
<template>
<el-row>
<el-col :span="2" style="vertical-align: center;">服务器地址</el-col>
<el-col :span="10"><el-input v-model="serverUrl" ></el-input></el-col>
<el-col :span="1"></el-col>
<el-col :span="2"><el-button plain type="primary" @click="saveHtml">保存文档</el-button></el-col>
<el-col :span="2"> <el-button plain type="primary" @click="saveHtmlAndData">保存数据&文档</el-button></el-col>
</el-row>
<Source src="/code/SaveDoc.vue"></Source>
<Editor @load="onLoad" doc="https://www.x-emr.cn/doc/999.html" style="margin: 10px 0;"></Editor>
</template>
<script>
import axios from 'axios'
export default{
data(){
return{
editor:null,
//服务端地址
serverUrl:'http://localhost/post'
}
},
methods:{
//初始化
onLoad: function(e) {
this.editor = e.target.contentWindow.editor
},
//仅保存HTML文档
saveHtml: function() {
// 若文档未修改,则无需保存
if(this.editor.edited == false){
this.$message.error('文档未修改,无需保存');
return;
}
// 若文档校验不通过,则无法保存
if(this.editor.validate() == false){
this.$message.error('请查看文档是否有未填项或不合规内容');
return;
}
let data = {'doc': this.editor.getHtml()}
axios.post(this.serverUrl, data).then(res=>{
this.$message.success('保存成功')
})
},
//保存文档及机构化数据
saveHtmlAndData: function() {
// 若文档未修改,则无需保存
if(this.editor.edited == false){
this.$message.error('文档未修改,无需保存');
return;
}
// 若文档校验不通过,则无法保存
if(this.editor.validate() == false){
this.$message.error('请查看文档是否有未填项或不合规内容');
return;
}
let data = {
'doc': this.editor.getHtml(),
'data': this.editor.getBindObject()
}
axios.post(this.serverUrl, data).then(res=>{
this.$message.success('保存成功')
})
},
}
}
</script>

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
<template>
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
<Editor doc="https://www.x-emr.cn/doc/999.html" @load="onLoad" style="margin: 10px 0;" mode="design"></Editor>
</template>
<script setup>
var editor = null
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor
}
//处理文档命令
const execCommand = (cmd) => {
editor.execCommand(cmd)
}
</script>

View File

@ -1,134 +0,0 @@
<template>
<Source src="/code/VitalSigns.vue"></Source>
<el-row>
<el-form-item label="体温单ID">
<el-col :span="24"><el-input v-model="vitalSignsId" ></el-input></el-col>
</el-form-item>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="createVitalSigns()">创建体温单</el-button>
<el-button plain type="primary" @click="updateVitalSigns()">更新体温单</el-button>
</el-button-group>
<el-button-group style="margin: 0 20px;">
<el-button plain type="primary" @click="createBabyVitalSigns()">新生儿体温单</el-button>
</el-button-group>
<el-button-group style="margin-left: 20px;">
<el-button plain type="primary" @click="execCommand('preview')">打印预览</el-button>
<el-button plain type="primary" @click="execCommand('print')">打印</el-button>
</el-button-group>
</el-row>
<Editor @load="onLoad" style="margin: 10px 0;"></Editor>
</template>
<script setup>
import { ref } from 'vue'
const vitalSignsId = ref('')
let editor = null
//初始化后
const onLoad = function(e) {
editor = e.target.contentWindow.editor
}
//文档命令
const execCommand = (cmd) => {
editor.execCommand(cmd)
}
//添加体温单
const createVitalSigns = () => {
let data = {
"id": vitalSignsId.value,
"name": "吴晓莉",
"inDate": "2023-08-01",
"diag": "新型冠状病毒肺炎",
"dept": "呼吸内科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-08-01",
"operateDate": "2023-08-03",
"notes": "入院-十时二十分,,转入ICU,,,,,,,手术,,,,,,,,,,,,,,,出院,死亡于×时×分",
"sphygmus": "112,110,109,103,108,85,90,83,90,103,108,85,90,83,90,,90,83,90,103,108,85,90,83,90",
"heart": "112,120,118,111,,,,,,,112,120,118,111",
"tempType": "0,1,2,3,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": "38.7,38.9,38.5,39.1|37.1,38.5,38.5,,,38.5,38.5|36.9,38.4,38.8,,38.5,38.6,38.8,,38.9,38.8,38.5,38.6,38.8,,38.9,38.8,34,34.0",
"breath": "30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)|身高(cm)|体重(kg)|过敏药",
"data1": "120/85,121/84,,110/75,",
"data2": "1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
"data6": "167cm,,,,,,,,,164cm",
"data7": "95kg,,,,,90kg",
"data8": "青霉素,",
"data9": "测试,"
}
vitalSignsId.value = editor.createVitalSigns(data)
}
//更新体温单
const updateVitalSigns = () => {
let data = {
"id": vitalSignsId.value,
"name": "吴晓莉",
"inDate": "2023-08-01",
"diag": "新型冠状病毒肺炎",
"dept": "呼吸内科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-09-01",
"operateDate": "2023-08-03",
"notes": ",,,,,,,,,,,,,,出院,死亡于×时×分",
"sphygmus": "112,110,109,103,108,85,90,83,90,103,108,85,90,83,90,90,90,83,90,103,108,85,90,83,90",
"heart": ",,,,,,112,120,118,111,,,110,120,120,118,111,100",
"tempType": ",,,,,,0,1,2,3,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": ",,,,,,38.7,38.9,38.5,39.1,38.5,38.5,,,38.5,38.5,38.4,38.8,,38.5,38.6,38.8,,38.9,38.838.5,38.6,38.8,,38.9,38.8,34,34.0",
"breath": ",,,,,,30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)|身高(cm)|体重(kg)|过敏药",
"data1": ",,,,,,120/85,121/84,,110/75,",
"data2": ",,,,,,1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
"data6": "167cm,,,,,,,,,164cm",
"data7": "95kg,,,,,90kg",
"data8": "青霉素,",
"data9": "测试,",
"pain":"2,2,4,5,8,8,9|2,6|3,,,4,4,4"
}
vitalSignsId.value = editor.createVitalSigns(data)
}
//添加新生儿体温单
const createBabyVitalSigns = () => {
let data =
{
"id": vitalSignsId.value,
"type": "baby",
"name": "吴晓莉",
"inDate": "2023-08-01",
"sex": "男",
"dept": "妇产科",
"bed": "801",
"medicalNo": "202300991",
"begin": "2023-08-01",
"notes": "出生-十时二十分",
"weight": "3200,,,,3300,,,,3400,,,,3400,,,,3500,,,,3400,,,,",
"heart": "112,120,118,111,,,,,,,112,120,118,111",
"tempType": "2,2,2,3,2,1,2,2,2,1,2,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1",
"temperature": "37.7,37.9,38.5,37.1,37.5,38.5,,,37.5,38.5,37.4,37.8,,37.5,37.6,37.8,,37.9,37.8,37.5,38.6,37.8,,37.9,37.8",
"physicalcool": ",,,37.6,,,,,,,,37.5,,,,,,,,,37.4,,",
"breath": "30,30,R,,35,35,35,35,35,35,35,35,,R,R,35,35,,R,,,35,35,,R",
"labels": "血压(mmHg)|入水量(ml)|出水量(ml)|大便(次)|小便(次)",
"data1": "120/85,121/84,,110/75,",
"data2": "1180ml,,,500ml,,40ml",
"data3": "500ml,,,,500ml,,67ml",
"data4": "2,4,5,3,3,3,2,,2",
"data5": "2,4,5,3,3,3,2,,2",
}
//第二个参数isBaby:true
editor.createVitalSigns(data)
}
</script>

View File

@ -2,17 +2,16 @@
<html>
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="./favicon.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="./vender/jquery/zTreeStyle/zTreeStyle.css" type="text/css">
<script src = "./vender/jquery/jquery.js"></script>
<script src = "./vender/date97/WdatePicker.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exedit.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exhide.min.js"></script>
<script src = "./vender/codemirror.js"></script>
<script src = "./vender/fabric.js"></script>
<!-- 局域网环境, 请下载 editor.js替换以下路径 -->
<script src="https://www.x-emr.cn/js/editor.js"></script>
<script src="js/editor.js"></script>
<script>
$(function() {
//配置项可以不设置,会使用默认设置

View File

@ -1,54 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="./favicon.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="./vender/jquery/zTreeStyle/zTreeStyle.css" type="text/css">
<script src = "./vender/jquery/jquery.js"></script>
<script src = "./vender/date97/WdatePicker.js"></script>
<script src = "./vender/jquery/jquery.ztree.core.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exedit.min.js"></script>
<script src = "./vender/jquery/jquery.ztree.exhide.min.js"></script>
<script src = "./vender/codemirror.js"></script>
<script src = "./vender/fabric.js"></script>
<script src="./vender/requirejs/require.js"></script>
<script>
require.config({
baseUrl: 'http://localhost:81/src',
paths: {'editor': 'editor'}
})
require(['editor'], function () {
//配置项可以不设置,会使用默认设置
let option = {
license:'gaR8jJur/A30SFnd5RHwJT4vNz7zuTe+5UVjd3EztbJyrcUa2ZMAc0WXBJZMJs5D+lpGh+a7p49pT8G1di9alwDwAzKnsz0BTNhHKUsLrU4uMy6I5iQ6l0OMB76w/VP2u1Qf8PCJQiO388mrc8dEcEZjeSVls1O3GedGINg3Od0=',
mode:'form', //默认模式 form:表单模式design:设计模式
pdfUrl:'https://www.x-emr.cn/pdf/post', //pdf生成服务
dictionary: [ //知识库
{name: '体征', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=20', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '症状', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=20', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '卫生信息数据元', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=40', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '电子病历数据集', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=50', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '国家医保标准', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=80', itemUrl:'https://www.x-emr.cn/dictitem'},
{name: '省数据平台标准', isParent:true, treeUrl:'https://www.x-emr.cn/dict?category=90', itemUrl:'https://www.x-emr.cn/dictitem'},
]
}
//从模块的Editor组件中获取modedoclang属性 (该段代码可删除)
let mode = window.frameElement.getAttribute('mode')
let doc = window.frameElement.getAttribute('doc')
let lang = window.frameElement.getAttribute('lang')
mode? option.mode = mode: null
lang? option.lang = lang: null
//初始化编辑器初始化代码可以放入组件的load事件中
editor.init(option).then(()=>{
doc? editor.loadUrl(doc) : null
})
})
</script>
</head>
<body></body>
</html>

File diff suppressed because one or more lines are too long

View 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>
<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-menu-->
<!-- mode="vertical"-->
<!-- :default-select-keys="['item1']"-->
<!-- width="256px"-->
<!-- @select="handleMenuSelect"-->
<!-- >-->
<!-- &lt;!&ndash; 首页 &ndash;&gt;-->
<!-- <d-menu-item key="item1">-->
<!-- <template #icon><i class="icon-homepage"></i></template>-->
<!-- <span>首页</span>-->
<!-- </d-menu-item>-->
<!-- &lt;!&ndash; 诊疗工作站 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 病历管理 &ndash;&gt;-->
<!-- <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>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts">
</script>
<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>

View File

@ -1,95 +0,0 @@
<script setup>
import router from "@/router/index.js";
import { ref } from 'vue'
const handleMenuSelect = (key) => {
router.push(`/${key["key"]}`)
}
// 当前激活的菜单项
const activeMenu = ref('HomePage')
</script>
<template>
<d-menu
mode="vertical"
:default-select-keys="['HomePage']"
width="256px"
@select="handleMenuSelect"
>
<!-- 首页 -->
<d-menu-item key="HomePage">
<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>
</template>
<style scoped>
</style>

86
front/src/api/auth.api.ts Normal file
View 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;
}

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

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

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

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

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

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

View 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;
/** 优先级(LMH高) */
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;
}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,40 @@
<template>
<component :is="linkType" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script setup lang="ts">
import { computed } from "vue";
defineOptions({
name: "AppLink",
inheritAttrs: false,
});
import { isExternal } from "@/utils/index";
const props = defineProps({
to: {
type: Object,
required: true,
},
});
const isExternalLink = computed(() => {
return isExternal(props.to.path || "");
});
const linkType = computed(() => (isExternalLink.value ? "a" : "router-link"));
const linkProps = (to: any) => {
if (isExternalLink.value) {
return {
href: to.path,
target: "_blank",
rel: "noopener noreferrer",
};
}
return { to: to };
};
</script>

View File

@ -1,18 +0,0 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="public/editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script>
export default {
data() {
return {
objectOfAttrs:{
width:'100%',
height:'800vh',
frameborder: 0
}
}
}
}
</script>

View File

@ -1,44 +0,0 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>

View File

@ -0,0 +1,422 @@
<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 { computed } 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>

View File

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

View File

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

View File

@ -0,0 +1,14 @@
<template>
<!-- 根据实际部署环境修改 editor.html 的路径 -->
<iframe src="/editor.html" v-bind="objectOfAttrs"></iframe>
</template>
<script setup>
const objectOfAttrs = {
width: '100%',
height: '800vh',
frameborder: 0
}
</script>

View File

@ -0,0 +1,9 @@
import type { App } from "vue";
import { hasPerm } from "./permission";
// 全局注册 directive
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
app.directive("hasPerm", hasPerm);
}

View File

@ -0,0 +1,64 @@
import type { Directive, DirectiveBinding } from "vue";
import { useUserStore } from "@/store";
/**
* 按钮权限
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredPerms = binding.value;
// 校验传入的权限值是否合法
if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) {
throw new Error(
"需要提供权限标识例如v-has-perm=\"'sys:user:add'\" 或 v-has-perm=\"['sys:user:add', 'sys:user:edit']\""
);
}
const { roles, perms } = useUserStore().userInfo;
// 超级管理员拥有所有权限,如果是”*:*:*”权限标识,则不需要进行权限校验
if (roles.includes("ROOT") || requiredPerms.includes("*:*:*")) {
return;
}
// 检查权限
const hasAuth = Array.isArray(requiredPerms)
? requiredPerms.some((perm) => perms.includes(perm))
: perms.includes(requiredPerms);
// 如果没有权限,移除该元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};
/**
* 角色权限指令
*/
export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredRoles = binding.value;
// 校验传入的角色值是否合法
if (!requiredRoles || (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))) {
throw new Error(
"需要提供角色标识例如v-has-role=\"'ADMIN'\" 或 v-has-role=\"['ADMIN', 'TEST']\""
);
}
const { roles } = useUserStore().userInfo;
// 检查是否有对应角色权限
const hasAuth = Array.isArray(requiredRoles)
? requiredRoles.some((role) => roles.includes(role))
: roles.includes(requiredRoles);
// 如果没有权限,移除元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};

View File

@ -0,0 +1,23 @@
/**
* 响应码枚举
*/
export const enum ResultEnum {
/**
* 成功
*/
SUCCESS = "00000",
/**
* 错误
*/
ERROR = "B0001",
/**
* 访问令牌无效或过期
*/
ACCESS_TOKEN_INVALID = "A0230",
/**
* 刷新令牌无效或过期
*/
REFRESH_TOKEN_INVALID = "A0231",
}

View 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: "隐藏域" },
};

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

View File

@ -0,0 +1,14 @@
/**
* 设备枚举
*/
export const enum DeviceEnum {
/**
* 宽屏设备
*/
DESKTOP = "desktop",
/**
* 窄屏设备
*/
MOBILE = "mobile",
}

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

View File

@ -0,0 +1,14 @@
/**
* 语言枚举
*/
export const enum LanguageEnum {
/**
* 中文
*/
ZH_CN = "zh-cn",
/**
* 英文
*/
EN = "en",
}

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

View File

@ -0,0 +1,7 @@
// 核心枚举定义
export enum MenuTypeEnum {
CATALOG = 2, // 目录
MENU = 1, // 菜单
BUTTON = 4, // 按钮
EXTLINK = 3, // 外链
}

7
front/src/hook/index.ts Normal file
View File

@ -0,0 +1,7 @@
/**
* 全局Hooks入口文件
* 导出所有可用的Hooks
*/
// 导出WebSocket相关Hook
export * from "./websocket";

View File

@ -0,0 +1,36 @@
<template>
<section class="app-main">
<router-view>
<template #default="{ Component, route }">
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
<!-- <keep-alive :include="cachedViews">-->
<component :is="Component" :key="route.path" />
<!-- </keep-alive>-->
</transition>
</template>
</router-view>
</section>
</template>
<script setup lang="ts">
import { useSettingsStore, useTagsViewStore } from "@/store";
// import variables from "@/styles/variables.module.scss";
// 缓存页面集合
const cachedViews = computed(() => useTagsViewStore().cachedViews);
// const appMainHeight = computed(() => {
// if (useSettingsStore().tagsView) {
// return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
// } else {
// return `calc(100vh - ${variables["navbar-height"]})`;
// }
// });
</script>
<style lang="scss" scoped>
.app-main {
position: relative;
overflow-y: auto;
background-color: var(--el-bg-color-page);
}
</style>

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,52 @@
<template>
<!-- 菜单图标 -->
<template v-if="icon">
<d-icon v-if="isElIcon" class="el-icon">
<component :is="iconComponent" />
</d-icon>
<div v-else :class="`i-svg:${icon}`" />
</template>
<template v-else>
<div class="i-svg:menu" />
</template>
<!-- 菜单标题 -->
<span v-if="title" class="ml-1">{{ title }}</span>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
icon?: string;
title?: string;
}>();
const isElIcon = computed(() => props.icon?.startsWith("icon"));
const iconComponent = computed(() => props.icon?.replace("icon-", ""));
</script>
<style lang="scss" scoped>
.el-icon {
width: 14px !important;
margin-right: 0 !important;
color: currentcolor;
}
[class^="i-svg:"] {
width: 14px;
height: 14px;
color: currentcolor !important;
}
.hideSidebar {
.el-sub-menu,
.el-menu-item {
.el-icon {
margin-left: 20px;
}
}
[class^="i-svg:"] {
margin-left: 20px;
}
}
</style>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import SidebarMenuItem from "./SidebarMenuItem.vue";
import path from "path-browserify";
import type { RouteRecordRaw } from "vue-router";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, useAppStore } from "@/store";
import { isExternal } from "@/utils/index";
import router from "@/router";
import variables from "@/styles/variables.module.scss";
import {Form, Menu} from "vue-devui";
const props = defineProps({
data: {
type: Array<RouteRecordRaw>,
default: () => [],
},
basePath: {
type: String,
required: true,
example: "/system",
},
});
const menuRef = ref<InstanceType<typeof Menu>>();
const settingsStore = useSettingsStore();
const appStore = useAppStore();
const currentRoute = useRoute();
// 存储已展开的菜单项索引
const expandedMenuIndexes = ref<string[]>([]);
const handleMenuSelect = (key: any) => {
router.push(`/${key["key"]}`)
}
/**
* 获取完整路径
*
* @param routePath 当前路由的相对路径 /user
* @returns 完整的绝对路径 D://vue3-element-admin/system/user
*/
function resolveFullPath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
// 解析路径,生成完整的绝对路径
return path.resolve(props.basePath, routePath);
}
</script>
<template>
<d-menu
mode="vertical"
:default-select-keys="currentRoute.path"
width="256px"
@select="handleMenuSelect"
>
<SidebarMenuItem
v-for="route in data"
:key="route.path"
:item="route"
:base-path="resolveFullPath(route.path)"
/>
</d-menu>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,142 @@
<script setup lang="ts">
import {PropType} from "vue";
import SidebarMenuItemTitle from "@/layout/components/SideBar/components/SideBarMenuItemTitle.vue";
import AppLink from "@/components/AppLink/index.vue";
defineOptions({
name: "SidebarMenuItem",
inheritAttrs: false,
});
import path from "path-browserify";
import { RouteRecordRaw } from "vue-router";
import { isExternal } from "@/utils";
const props = defineProps({
/**
* 当前路由对象
*/
item: {
type: Object as PropType<RouteRecordRaw>,
required: true,
},
/**
* 父级完整路径
*/
basePath: {
type: String,
required: true,
},
/**
* 是否为嵌套路由
*/
isNest: {
type: Boolean,
default: false,
},
});
// 可见的唯一子节点
const onlyOneChild = ref();
/**
* 检查是否仅有一个可见子节点
*
* @param children 子路由数组
* @param parent 父级路由
* @returns 是否仅有一个可见子节点
*/
function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw) {
// 过滤出可见子节点
const showingChildren = children.filter((route: RouteRecordRaw) => {
if (!route.meta?.hidden) {
onlyOneChild.value = route;
return true;
}
return false;
});
// 仅有一个节点
if (showingChildren.length === 1) {
return true;
}
// 无子节点时
if (showingChildren.length === 0) {
// 父节点设置为唯一显示节点,并标记为无子节点
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
/**
* 获取完整路径,适配外部链接
*
* @param routePath 路由路径
* @returns 绝对路径
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) return routePath;
if (isExternal(props.basePath)) return props.basePath;
// 拼接父路径和当前路径
return path.resolve(props.basePath, routePath);
}
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden">
<!--叶子节点显示叶子节点或唯一子节点且父节点未配置始终显示 -->
<template
v-if="
// 未配置始终显示,使用唯一子节点替换父节点显示为叶子节点
(hasOneShowingChild(item.children, item) &&
!item.meta?.alwaysShow &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)) ||
// 即使配置了始终显示,但无子节点,也显示为叶子节点
(item.meta?.alwaysShow && !item.children)
"
>
<AppLink
v-if="onlyOneChild.meta"
:to="{
path: resolvePath(onlyOneChild.path),
query: onlyOneChild.meta.params,
}"
>
<d-menu-item
:key="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<SidebarMenuItemTitle
:icon="onlyOneChild.meta.icon || item.meta?.icon"
:title="onlyOneChild.meta.title"
/>
</d-menu-item>
</AppLink>
</template>
<!--【非叶子节点】显示含多个子节点的父菜单,或始终显示的单子节点 -->
<d-sub-menu v-else :index="resolvePath(item.path)" teleported>
<template #title>
<SidebarMenuItemTitle v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
</template>
<SidebarMenuItem
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
/>
</d-sub-menu>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import SidebarMenu from './components/SidebarMenu.vue'
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import { ref } from 'vue'
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const layout = computed(() => settingsStore.layout);
const isSidebarCollapsed = computed(() => !appStore.sidebar.opened);
// 当前激活的菜单项
const activeMenu = ref('HomePage')
</script>
<template>
<SidebarMenu :data="permissionStore.routes" base-path="" />
<!-- <d-menu-->
<!-- mode="vertical"-->
<!-- :default-select-keys="['HomePage']"-->
<!-- width="256px"-->
<!-- @select="handleMenuSelect"-->
<!-- >-->
<!-- &lt;!&ndash; 首页 &ndash;&gt;-->
<!-- <d-menu-item key="HomePage">-->
<!-- <template #icon><i class="icon-homepage"></i></template>-->
<!-- <span>首页</span>-->
<!-- </d-menu-item>-->
<!-- &lt;!&ndash; 诊疗工作站 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; 病历管理 &ndash;&gt;-->
<!-- <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>-->
</template>
<style scoped>
</style>

182
front/src/layout/index.vue Normal file
View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import AppMain from "@/layout/components/AppMain/index.vue";
import Sidebar from "@/layout/components/Sidebar/index.vue"; // 根据你的实际路径调整
import { useAppStore, useSettingsStore, usePermissionStore } from "@/store";
// 配置
import defaultSettings from "@/settings";
// 枚举
import { DeviceEnum } from "@/enums/settings/device.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const width = useWindowSize().width;
// 常量
const WIDTH_DESKTOP = 992; // 响应式布局容器固定宽度(大屏 >=1200px中屏 >=992px小屏 >=768px
// 计算属性
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE); // 是否为移动设备
const isSidebarOpen = computed(() => appStore.sidebar.opened); // 侧边栏是否展开
const isShowTagsView = computed(() => settingsStore.tagsView); // 是否显示标签视图
const layout = computed(() => settingsStore.layout); // 当前布局模式left、top、mix
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活路径
const mixedLayoutLeftRoutes = computed(() => permissionStore.mixedLayoutLeftRoutes); // 混合布局左侧菜单路由
//设置侧边栏的显示和隐藏
const toggleClick = (): void => {
const menu = document.querySelector('.menu-aside') as HTMLElement | null;
if (menu) {
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';
}
} else {
console.warn('Menu element not found');
}
};
// 监听路由变化,如果是移动设备且侧边栏展开,则关闭侧边栏
const route = useRoute();
watch(route, () => {
if (isMobile.value && isSidebarOpen.value) {
appStore.closeSideBar();
}
});
// 计算属性:布局样式
const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
mobile: appStore.device === DeviceEnum.MOBILE,
[`layout-${settingsStore.layout}`]: true,
}));
</script>
<template>
<d-layout class="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"
>
<Sidebar class="layout__sidebar" />
</d-aside>
<!-- 主显示区 - 添加左侧边距 -->
<d-content
style="
padding: 16px;
overflow-x: hidden; /* 禁止内容溢出 */
">
<!-- <component :is="currentComponent"/>-->
<AppMain />
</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>

View File

@ -1,27 +0,0 @@
import './assets/main.css'
import DevUI from 'vue-devui';
import 'vue-devui/style.css';
import '@devui-design/icons/icomoon/devui-icon.css';
import { ThemeServiceInit, infinityTheme } from 'devui-theme';
import router from './router/index'
import 'devui-theme/styles-var/devui-var.scss'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.min.js'
//
import Editor from '@/components/Editor.vue'
ThemeServiceInit({ infinityTheme }, 'infinityTheme');
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.component('Editor', Editor)
app.use(DevUI)
app.use(router)
app.mount('#app')

25
front/src/main.ts Normal file
View File

@ -0,0 +1,25 @@
import 'vue-devui/style.css';
import '@devui-design/icons/icomoon/devui-icon.css';
// import { ThemeServiceInit, infinityTheme } from 'devui-theme';
// ThemeServiceInit({ infinityTheme }, 'infinityTheme');
import DevUI from 'vue-devui'
import 'devui-theme/styles-var/devui-var.scss'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.min.js'
import { createApp } from 'vue';
import App from './App.vue';
import setupPlugins from "@/plugins";
import Editor from '@/components/X-EMR/Editor.vue'
const app = createApp(App);
app.use(setupPlugins);
app.use(DevUI)
// 病历编辑器
app.component('Editor', Editor)
app.mount('#app')

View File

@ -70,7 +70,15 @@
</template>
</d-card>
</div>
<d-card shadow="never" class="item-card container-right-card">
<template #title>统计面板</template>
<template #content>
</template>
</d-card>
</div>
</div>

View File

@ -0,0 +1,12 @@
<script setup>
import LoginView from "@/components/LoginView.vue";
</script>
<template>
<LoginView />
</template>
<style scoped>
</style>

View File

@ -66,10 +66,10 @@ const sizeChange = (size) => {
const id = ref('tab1');
//X-EMR-VUE
const patient = ref({})
var editor = null
var editor = null;
//加载编辑器
const onLoad = (e) => {
editor = e.target.contentWindow.editor

View File

@ -0,0 +1,16 @@
import type { App } from "vue";
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();
},
};

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

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

View File

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

59
front/src/router/index.ts Normal file
View File

@ -0,0 +1,59 @@
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: '/',
name: 'main',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/Home',
name: 'Home',
component: Home
},
{
path:'menjizhen-item',
name: 'MenjizhenItem',
component: mjz
},
{
path: 'zhuyuan-item',
name: 'ZhuyuanItem',
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
View 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
View 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 };

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

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

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

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

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

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

33
front/src/types/env.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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;
}

View File

@ -0,0 +1,15 @@
/**
* WebSocket相关类型定义
*/
/**
* 字典WebSocket事件类型
*/
export interface DictWebSocketEvent {
/** 事件类型:更新或删除 */
type: "DICT_UPDATED" | "DICT_DELETED";
/** 字典编码 */
dictCode: string;
/** 时间戳 */
timestamp: number;
}

Some files were not shown because too many files have changed in this diff Show More