Compare commits

...

71 Commits
master ... main

Author SHA1 Message Date
Maple 895bfcbeaf fix:将岗位与服务器的内容匹配(目前是岗位中的部门/所属部门) 2025-08-06 16:02:00 +08:00
Maple 7f654625aa fix:修改了一部分菜单栏的问题,但是更多的还没有修复 2025-08-06 15:20:23 +08:00
Maple 864f926f2e fix:菜单栏问题 2025-08-06 10:05:40 +08:00
wuxueyu 69b4bf86bd 修复各种打包错误 2025-08-06 01:01:43 +08:00
wuxueyu 48f4020fc8 Merge branch 'main' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web
# Conflicts:
#	.env.development
#	src/views/project-management/bidding/information-retrieval/index.vue
2025-08-06 00:05:20 +08:00
wuxueyu 0eb12b4df9 信息检索模块新增爬虫设置、检索信息列表展示、信息详情查看 2025-08-06 00:03:43 +08:00
chabai 77238c3d03 route 商务添加 2025-08-05 22:50:37 +08:00
chabai 7e006acaa6 business 2025-08-05 22:44:39 +08:00
chabai 2065389094 Merge remote-tracking branch 'origin/main' 2025-08-05 21:37:05 +08:00
chabai 4972dcaf4d fix 2025-08-05 21:36:46 +08:00
Mr.j c7043dbdb1 备注 2025-08-05 21:21:06 +08:00
Maple 02c705ae3d fix:优化了岗位详情信息 2025-08-05 17:16:45 +08:00
Maple 69ff1bba7d add:添加了岗位信息,实现了通过岗位查询到该岗位的角色的功能 2025-08-05 15:40:37 +08:00
wangna0328 c47c8e48aa 制度代码修改版 2025-08-04 22:15:48 +08:00
Maple c64166a7c0 add:添加了岗位信息,实现了通过岗位查询到该岗位的角色的功能 2025-08-04 17:27:42 +08:00
wxy 1307eaf651 Revert "Merge remote-tracking branch 'origin/jgqweb'"
This reverts commit 1648f3df96, reversing
changes made to c76d6beeb0.
2025-08-04 17:10:11 +08:00
wxy d25a34e579 Revert "add:添加了岗位信息,实现了通过岗位查询到该岗位的角色的功能"
This reverts commit a72aaafdfb.
2025-08-04 17:07:06 +08:00
wxy fcd9604272 Revert "Merge branch 'ljy/master'"
This reverts commit 2492a57b88, reversing
changes made to 1648f3df96.
2025-08-04 16:59:54 +08:00
wxy 4ca2696896 Revert "信息检索、爬虫设置相关组件提交"
This reverts commit 11c9d33f94.
2025-08-04 16:57:59 +08:00
wxy 11c9d33f94 信息检索、爬虫设置相关组件提交 2025-08-04 16:25:43 +08:00
Maple 2492a57b88 Merge branch 'ljy/master'
# Conflicts:
#	src/apis/system/post.ts
2025-08-04 14:55:23 +08:00
Maple a72aaafdfb add:添加了岗位信息,实现了通过岗位查询到该岗位的角色的功能 2025-08-04 14:50:06 +08:00
Mr.j 1648f3df96 Merge remote-tracking branch 'origin/jgqweb' 2025-08-04 11:54:12 +08:00
Maple c76d6beeb0 Merge remote-tracking branch 'origin/main' 2025-08-04 10:51:45 +08:00
Maple 2bf9c6a5eb none 2025-08-04 10:51:14 +08:00
chabai ca1c3947c1 Merge remote-tracking branch 'refs/remotes/origin/main' into ccd 2025-08-04 09:42:01 +08:00
Mr.j 7246e8dc99 页面美化,隐藏搜索字段成弹窗形式 2025-08-01 17:59:51 +08:00
chabai 011e1c5337 完成制度管理模块开发,完善搜索功能 2025-08-01 17:22:47 +08:00
Mr.j eaec8547aa 优化分页查询无法绑定数据模型问题 2025-08-01 16:24:33 +08:00
chabai 7f8ccb76cb 完成制度管理模块开发 2025-08-01 15:37:53 +08:00
Mr.j 543ab3d027 实现设备中心模块编辑,删除,查看详情,查看 2025-08-01 15:14:24 +08:00
Vic b8ffc08513 Merge remote-tracking branch 'origin/main' 2025-07-31 22:20:22 +08:00
Vic 23fb041a08 信息检索模块新增爬虫设置、检索信息列表展示、信息详情查看 2025-07-31 22:16:28 +08:00
Mr.j 1d16db937c 实现设备中心模块的页面搭建和分页查询以及手动新增弹窗页面 2025-07-31 17:32:21 +08:00
chabai cc65882b8d 优化制度管理模块,新建制度提案页面 2025-07-31 17:00:32 +08:00
Maple 54d9557a89 fix:修复了个人中心,将前端的字段与后端对应,确保数据能够完全显示 2025-07-31 15:26:28 +08:00
chabai 327064b7e5 完成制度管理模块的制度公告,制度公示,制度管理 2025-07-31 09:11:39 +08:00
Maple 1d96aad163 change: 将菜单栏优化了一下,添加了个人信息,用来查看个人信息,但是并没有调用对应的api接口 2025-07-31 09:09:37 +08:00
chabai 64a82a6775 修改下载文件类型的备份 2025-07-30 15:58:23 +08:00
Maple 124a04c43d change: 将菜单栏优化了一下
delete: 删除了一些多余的菜单栏(注释掉了)
2025-07-30 14:31:18 +08:00
chabai 1dc321ba5c 完成制度管理模块页面搭建,包含发布流程等功能 2025-07-30 10:21:02 +08:00
Mr.j 7fe24ac2cf 培训计划实现查询 2025-07-30 09:13:52 +08:00
wangna0328 1feb4832c5 Merge branch 'main' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web 2025-07-29 20:16:19 +08:00
wangna0328 1f56b0a30f 制度相关页面开发初步实现 2025-07-29 20:12:55 +08:00
Maple db047b4e60 init:添加了岗位管理中的岗位详情按钮,可以点击查看岗位详情
fix: 修复了角色功能,可以通过角色检索该角色的用户
2025-07-29 17:53:31 +08:00
Maple 9741192bee fix:将部门管理中的树形结构改为受控的模式,不在是点一个而全部展开或者收缩 2025-07-28 15:12:10 +08:00
Maple 8c202c45dc Merge remote-tracking branch 'origin/main' 2025-07-28 12:39:07 +08:00
Maple 38d888f24e fix:任务管理 2025-07-26 21:32:37 +08:00
Maple fae3b7b05d fix:任务管理 2025-07-26 21:32:21 +08:00
Maple 236cf1489b style 2025-07-26 19:35:14 +08:00
wxy 0108dd6068 完成原始数据回流后查看界面开发和功能开发,支持对所选图片的关联音频进行播放|修复典型图片设置不生效的问题 2025-07-26 16:58:21 +08:00
Maple 5b673f9525 style 2025-07-26 11:24:29 +08:00
Maple 708e8b47d1 style 2025-07-26 11:14:22 +08:00
何德超 7728cfd8aa logo修改 2025-07-25 21:34:29 +08:00
Vic fe6e84f5f7 新增缺陷增加轴向和弦向字段|缺陷检测界面新增图像预览功能和增加典型图片设置功能|去除可视化菜单 2025-07-25 00:13:51 +08:00
何德超 70fca26e10 证书类型改为与后端枚举类型匹配的下拉框选择 2025-07-24 22:02:13 +08:00
何德超 7f24af3272 立项添加项目经理,风场名称和业主名称绑定一致 2025-07-24 20:56:11 +08:00
何德超 cf780323e0 人员资质修改正常接收数据 2025-07-24 20:53:50 +08:00
何德超 b028f2a7bd 上传 2025-07-23 22:46:47 +08:00
何德超 15d50b997d 项目管理菜单修改 2025-07-22 16:43:51 +08:00
何德超 c3fb37aa6b test connect 2025-07-22 09:38:23 +08:00
何德超 b9b1da2186 fix 2025-07-21 17:51:21 +08:00
何德超 b7e5a4a82f 保险与健康档案分开 2025-07-21 16:29:17 +08:00
何德超 d669624fbe route fix 2025-07-21 14:35:34 +08:00
何德超 f8aeb0caac 按吴总要求不要市场商务管理了,直接把项目来源提上去 2025-07-21 13:21:28 +08:00
何德超 afe4a6d5ba xlsx添加"xlsx": "^0.18.5" 2025-07-21 12:35:34 +08:00
何德超 ab8afb02eb 市场商务管理项目菜单修改 2025-07-21 12:31:26 +08:00
wxy 30eca5da60 Merge branch 'main' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web 2025-07-21 10:52:21 +08:00
wxy a6b7b343a9 侧边栏结构修改 2025-07-21 10:50:02 +08:00
何德超 253b6ffcca 工资单 2025-07-21 10:49:05 +08:00
何德超 a3b11c9971 hdc,绩效和工资单 2025-07-21 10:04:56 +08:00
111 changed files with 33568 additions and 2666 deletions

View File

@ -3,7 +3,8 @@
VITE_API_PREFIX = '/dev-api'
# 接口地址
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
VITE_API_BASE_URL = 'http://localhost:8888/'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'

View File

@ -4,8 +4,8 @@
VITE_BUILD_MOCK = false
# 接口地址
VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
VITE_API_WS_URL = 'ws://localhost:8000'
# 地址前缀
VITE_BASE = '/'

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -2,6 +2,5 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Industrial-image-management-system---web" vcs="Git" />
</component>
</project>

91
BACKEND_SEARCH_API.md Normal file
View File

@ -0,0 +1,91 @@
# 制度类型搜索接口实现指南
## 接口定义
### 请求接口
```
GET /api/regulation/types
Content-Type: application/json
```
### 请求参数
```json
{
"page": number, // 页码可选默认1
"size": number, // 每页大小可选默认10
"typeName": "string", // 类型名称(模糊搜索,可选)
"status": "string", // 状态筛选("1"启用,"0"禁用,可选)
"remark": "string" // 备注内容(模糊搜索,可选)
}
```
### 响应格式
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"typeId": "string",
"typeName": "string",
"sortOrder": number,
"isEnabled": "string",
"remark": "string",
"createBy": "string",
"createTime": "string",
"updateBy": "string",
"updateTime": "string",
"delFlag": "string"
}
],
"total": number, // 总记录数
"current": number, // 当前页码
"size": number, // 每页大小
"pages": number // 总页数
}
}
```
## 后端实现说明
后端已实现以下接口:
```java
@ApiOperation(value = "获取制度类型列表", httpMethod = "GET")
@GetMapping
public Result getRegulationTypes(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String typeName,
@RequestParam(required = false) String status,
@RequestParam(required = false) String remark
) {
return regulationTypeService.getRegulationTypes(page, size, typeName, status, remark);
}
```
## 前端集成说明
前端已完成以下功能:
1. ✅ 调整为GET请求接口
2. ✅ 参数名匹配后端接口isEnabled → status
3. ✅ 移除排序参数(后端不支持)
4. ✅ 简化搜索表单,只支持手动搜索
5. ✅ 保持原有功能不受影响
## 搜索流程
1. 用户在搜索表单中输入条件
2. 点击"搜索"按钮触发搜索
3. 调用后端GET接口 `/api/regulation/types`
4. 后端返回搜索结果
5. 前端展示搜索结果
## 参数说明
- **page**: 页码默认1
- **size**: 每页大小默认10
- **typeName**: 类型名称,支持模糊搜索
- **status**: 状态筛选,"1"表示启用,"0"表示禁用
- **remark**: 备注内容,支持模糊搜索

View File

@ -0,0 +1,125 @@
# 制度公告搜索接口实现指南
## 接口定义
### 请求接口
```
GET /api/regulation
Content-Type: application/json
```
### 请求参数
```json
{
"page": number, // 页码可选默认1
"size": number, // 每页大小可选默认10
"status": "string", // 状态筛选(精确匹配,固定为"PUBLISHED"
"title": "string", // 制度标题(模糊搜索,可选)
"proposer": "string", // 公示人(模糊搜索,可选)
"confirmStatus": "string" // 确认状态(精确匹配,可选)
}
```
### 响应格式
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"regulationId": "string",
"title": "string",
"content": "string",
"regulationType": "string",
"status": "string",
"publishTime": "string",
"effectiveTime": "string",
"expireTime": "string",
"scope": "string",
"level": "string",
"version": "string",
"remark": "string",
"createBy": "string",
"updateBy": "string",
"createTime": "string",
"updateTime": "string",
"delFlag": "string",
"confirmStatus": "string"
}
],
"total": number, // 总记录数
"current": number, // 当前页码
"size": number, // 每页大小
"pages": number // 总页数
}
}
```
## 后端实现说明
后端已实现以下接口:
```java
@ApiOperation(value = "获取制度列表", httpMethod = "GET")
@GetMapping
public Result getRegulationList(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String type,
@RequestParam(required = false) String title,
@RequestParam(required = false) String proposer,
@RequestParam(required = false) String confirmStatus
) {
return regulationService.getRegulationList(page, size, status, type, title, proposer, confirmStatus);
}
```
## 前端集成说明
前端已完成以下功能:
1. ✅ 删除前端搜索逻辑(防抖、实时搜索等)
2. ✅ 调整为GET请求接口
3. ✅ 参数名匹配后端接口createByName → proposer
4. ✅ 简化搜索表单,只支持手动搜索
5. ✅ 移除前端过滤逻辑,由后端处理状态筛选
6. ✅ 保持原有功能不受影响
## 搜索流程
1. 用户在搜索表单中输入条件
2. 点击"搜索"按钮触发搜索
3. 调用后端GET接口 `/api/regulation`
4. 后端返回搜索结果已过滤为PUBLISHED状态的制度
5. 前端展示搜索结果
## 参数说明
- **page**: 页码默认1
- **size**: 每页大小默认10
- **status**: 状态筛选,固定为"PUBLISHED"(已公告状态)
- **title**: 制度标题,支持模糊搜索
- **proposer**: 公示人,支持模糊搜索
- **confirmStatus**: 确认状态精确匹配confirmed、pending
## 搜索功能特性
- **模糊搜索**title 和 proposer 字段支持模糊匹配
- **精确筛选**confirmStatus 字段精确匹配
- **分页查询**:支持分页和排序
- **状态过滤**后端自动过滤为PUBLISHED状态的制度
- **性能优化**:使用数据库索引提升查询性能
## 业务逻辑说明
制度公告页面专门用于展示已经公告的制度,因此:
- 后端需要自动过滤为 `status = 'PUBLISHED'` 的制度
- 支持按确认状态confirmStatus进行筛选
- 前端不再需要手动过滤,完全依赖后端处理
- 用户可以查看制度详情、下载PDF文件、确认知晓制度
## 确认状态说明
- **confirmed**: 已确认 - 用户已确认知晓并遵守该制度
- **pending**: 待确认 - 用户尚未确认知晓该制度
- 空值: 全部 - 显示所有确认状态的制度

View File

@ -0,0 +1,117 @@
# 制度公示搜索接口实现指南
## 接口定义
### 请求接口
```
GET /api/regulation
Content-Type: application/json
```
### 请求参数
```json
{
"page": number, // 页码可选默认1
"size": number, // 每页大小可选默认10
"status": "string", // 状态筛选(精确匹配,可选)
"type": "string", // 提案类型(精确匹配,可选)
"title": "string", // 提案标题(模糊搜索,可选)
"proposer": "string" // 提案人(模糊搜索,可选)
}
```
### 响应格式
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"regulationId": "string",
"title": "string",
"content": "string",
"regulationType": "string",
"status": "string",
"publishTime": "string",
"effectiveTime": "string",
"expireTime": "string",
"scope": "string",
"level": "string",
"version": "string",
"remark": "string",
"createBy": "string",
"updateBy": "string",
"createTime": "string",
"updateTime": "string",
"delFlag": "string",
"confirmStatus": "string"
}
],
"total": number, // 总记录数
"current": number, // 当前页码
"size": number, // 每页大小
"pages": number // 总页数
}
}
```
## 后端实现说明
后端已实现以下接口:
```java
@ApiOperation(value = "获取制度列表", httpMethod = "GET")
@GetMapping
public Result getRegulationList(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String type,
@RequestParam(required = false) String title,
@RequestParam(required = false) String proposer
) {
return regulationService.getRegulationList(page, size, status, type, title, proposer);
}
```
## 前端集成说明
前端已完成以下功能:
1. ✅ 删除前端搜索逻辑(防抖、实时搜索等)
2. ✅ 调整为GET请求接口
3. ✅ 参数名匹配后端接口createByName → proposer, regulationType → type
4. ✅ 简化搜索表单,只支持手动搜索
5. ✅ 移除前端过滤逻辑,由后端处理状态筛选
6. ✅ 保持原有功能不受影响
## 搜索流程
1. 用户在搜索表单中输入条件
2. 点击"搜索"按钮触发搜索
3. 调用后端GET接口 `/api/regulation`
4. 后端返回搜索结果(已过滤掉草稿状态的提案)
5. 前端展示搜索结果
## 参数说明
- **page**: 页码默认1
- **size**: 每页大小默认10
- **status**: 状态筛选精确匹配PUBLISHED、APPROVED等
- **type**: 提案类型,精确匹配(如:管理规范、操作流程、安全制度、其他)
- **title**: 提案标题,支持模糊搜索
- **proposer**: 提案人,支持模糊搜索
## 搜索功能特性
- **模糊搜索**title 和 proposer 字段支持模糊匹配
- **精确筛选**type 和 status 字段精确匹配
- **分页查询**:支持分页和排序
- **状态过滤**:后端自动过滤掉草稿状态的提案,只显示已公示及以上的提案
- **性能优化**:使用数据库索引提升查询性能
## 业务逻辑说明
制度公示页面专门用于展示已经公示或已通过的制度提案,因此:
- 后端需要自动过滤掉 `status = 'DRAFT'` 的提案
- 只返回 `status = 'PUBLISHED'``status = 'APPROVED'` 的提案
- 前端不再需要手动过滤,完全依赖后端处理

View File

@ -0,0 +1,108 @@
# 制度提案搜索接口实现指南
## 接口定义
### 请求接口
```
GET /api/regulation
Content-Type: application/json
```
### 请求参数
```json
{
"page": number, // 页码可选默认1
"size": number, // 每页大小可选默认10
"status": "string", // 状态筛选(精确匹配,可选)
"type": "string", // 提案类型(精确匹配,可选)
"title": "string", // 提案标题(模糊搜索,可选)
"proposer": "string" // 提案人(模糊搜索,可选)
}
```
### 响应格式
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"regulationId": "string",
"title": "string",
"content": "string",
"regulationType": "string",
"status": "string",
"publishTime": "string",
"effectiveTime": "string",
"expireTime": "string",
"scope": "string",
"level": "string",
"version": "string",
"remark": "string",
"createBy": "string",
"updateBy": "string",
"createTime": "string",
"updateTime": "string",
"delFlag": "string",
"confirmStatus": "string"
}
],
"total": number, // 总记录数
"current": number, // 当前页码
"size": number, // 每页大小
"pages": number // 总页数
}
}
```
## 后端实现说明
后端已实现以下接口:
```java
@ApiOperation(value = "获取制度列表", httpMethod = "GET")
@GetMapping
public Result getRegulationList(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String type,
@RequestParam(required = false) String title,
@RequestParam(required = false) String proposer
) {
return regulationService.getRegulationList(page, size, status, type, title, proposer);
}
```
## 前端集成说明
前端已完成以下功能:
1. ✅ 删除前端搜索逻辑(防抖、实时搜索等)
2. ✅ 调整为GET请求接口
3. ✅ 参数名匹配后端接口createByName → proposer, regulationType → type
4. ✅ 简化搜索表单,只支持手动搜索
5. ✅ 保持原有功能不受影响
## 搜索流程
1. 用户在搜索表单中输入条件
2. 点击"搜索"按钮触发搜索
3. 调用后端GET接口 `/api/regulation`
4. 后端返回搜索结果
5. 前端展示搜索结果
## 参数说明
- **page**: 页码默认1
- **size**: 每页大小默认10
- **status**: 状态筛选精确匹配DRAFT、PUBLISHED、APPROVED等
- **type**: 提案类型,精确匹配(如:管理规范、操作流程、安全制度、其他)
- **title**: 提案标题,支持模糊搜索
- **proposer**: 提案人,支持模糊搜索
## 搜索功能特性
- **模糊搜索**title 和 proposer 字段支持模糊匹配
- **精确筛选**type 和 status 字段精确匹配
- **分页查询**:支持分页和排序
- **性能优化**:使用数据库索引提升查询性能

17038
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,9 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.4",
"echarts": "^5.4.2",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"jspdf": "^3.0.1",
"lint-staged": "^15.2.10",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
@ -58,7 +60,8 @@
"vue-router": "^4.3.3",
"vue3-tree-org": "^4.2.2",
"xe-utils": "^3.5.7",
"xgplayer": "^2.31.6"
"xgplayer": "^2.31.6",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@antfu/eslint-config": "^2.16.3",

View File

@ -68,9 +68,15 @@ importers:
echarts:
specifier: ^5.4.2
version: 5.5.0
html2canvas:
specifier: ^1.4.1
version: 1.4.1
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
jspdf:
specifier: ^3.0.1
version: 3.0.1
lint-staged:
specifier: ^15.2.10
version: 15.2.10
@ -143,6 +149,9 @@ importers:
xgplayer:
specifier: ^2.31.6
version: 2.32.6
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies:
'@antfu/eslint-config':
specifier: ^2.16.3
@ -460,6 +469,10 @@ packages:
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.2':
resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.0':
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
engines: {node: '>=6.9.0'}
@ -1189,12 +1202,18 @@ packages:
resolution: {integrity: sha512-yuIv/WRffRzL7cBW+sla4HwBZrEXRNf1MKQ5SklPEadth+BKbDxiVG8A3iISN5B3yC4EeSCzMZP8llHTcUhOzQ==}
deprecated: This is a stub types definition. query-string provides its own type definitions, so you do not need this installed.
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
'@types/sortablejs@1.15.8':
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
'@types/svgo@2.6.4':
resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.10':
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
@ -1481,6 +1500,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
aieditor@1.0.13:
resolution: {integrity: sha512-A1NIydCJgno3VvEKWPyHZlS7IF5FwBO1X4QO3GEKNcs8wMmmVGbcoVDPHON3uo9bTKaxuuIiONyfLCGHLBpW2Q==}
@ -1601,6 +1624,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
engines: {node: '>=0.10.0'}
@ -1645,6 +1672,11 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
btoa@1.2.1:
resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
engines: {node: '>= 0.4.0'}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -1685,9 +1717,17 @@ packages:
caniuse-lite@1.0.30001620:
resolution: {integrity: sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==}
canvg@3.0.11:
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
engines: {node: '>=10.0.0'}
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@ -1759,6 +1799,10 @@ packages:
codemirror@6.0.1:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collection-visit@1.0.0:
resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
engines: {node: '>=0.10.0'}
@ -1849,6 +1893,11 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@ -1863,6 +1912,9 @@ packages:
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
@ -2039,6 +2091,9 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@ -2592,6 +2647,9 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -2655,6 +2713,10 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'}
@ -2841,6 +2903,10 @@ packages:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
htmlparser2@3.10.1:
resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
@ -3144,6 +3210,9 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jspdf@3.0.1:
resolution: {integrity: sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -3609,6 +3678,9 @@ packages:
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@ -3790,6 +3862,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@ -3813,6 +3888,9 @@ packages:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@ -3887,6 +3965,10 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rgbcolor@1.0.1:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
rollup@4.17.2:
resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -4106,10 +4188,18 @@ packages:
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
engines: {node: '>=0.10.0'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stable@0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'}
@ -4209,6 +4299,10 @@ packages:
svg-baker@1.7.0:
resolution: {integrity: sha512-nibslMbkXOIkqKVrfcncwha45f97fGuAOn1G99YwnwTj8kF9YiM6XexPcUso97NxOm6GsP0SIvYVIosBis1xLg==}
svg-pathdata@6.0.3:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
svg-tags@1.0.0:
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
@ -4246,6 +4340,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -4463,6 +4560,9 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
v-viewer@3.0.13:
resolution: {integrity: sha512-T8pgGzlF0ZCHVpD/32OKsD8MlpI6tqYP3n1XLcSjvGQMc0ABn8nJ4AumxvzAKVQrLRWtDTG6qRGAyCPCmi7ceA==}
peerDependencies:
@ -4687,10 +4787,18 @@ packages:
resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
engines: {node: '>=12'}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -4713,6 +4821,11 @@ packages:
resolution: {integrity: sha512-ESwYYcG8SQciPaN43tZkN3r0dS/jQ5RtyxyGbxn2+qcKgZQ861M899xq8Cab/z6qVVX+/4eIsxDbm3lfYGYzvA==}
hasBin: true
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
@ -5030,6 +5143,8 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.2': {}
'@babel/template@7.24.0':
dependencies:
'@babel/code-frame': 7.24.2
@ -5756,12 +5871,18 @@ snapshots:
dependencies:
query-string: 9.0.0
'@types/raf@3.4.3':
optional: true
'@types/sortablejs@1.15.8': {}
'@types/svgo@2.6.4':
dependencies:
'@types/node': 20.12.12
'@types/trusted-types@2.0.7':
optional: true
'@types/unist@2.0.10': {}
'@types/web-bluetooth@0.0.20': {}
@ -6166,6 +6287,8 @@ snapshots:
acorn@8.11.3: {}
adler-32@1.3.1: {}
aieditor@1.0.13(@tiptap/extension-code-block@2.5.8(@tiptap/core@2.5.8(@tiptap/pm@2.5.8))(@tiptap/pm@2.5.8)):
dependencies:
'@tiptap/core': 2.5.8(@tiptap/pm@2.5.8)
@ -6304,6 +6427,8 @@ snapshots:
balanced-match@1.0.2: {}
base64-arraybuffer@1.0.2: {}
base@0.11.2:
dependencies:
cache-base: 1.0.1
@ -6372,6 +6497,8 @@ snapshots:
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0)
btoa@1.2.1: {}
buffer-from@1.1.2: {}
builtin-modules@3.3.0: {}
@ -6415,12 +6542,29 @@ snapshots:
caniuse-lite@1.0.30001620: {}
canvg@3.0.11:
dependencies:
'@babel/runtime': 7.28.2
'@types/raf': 3.4.3
core-js: 3.40.0
raf: 3.4.1
regenerator-runtime: 0.13.11
rgbcolor: 1.0.1
stackblur-canvas: 2.7.0
svg-pathdata: 6.0.3
optional: true
capital-case@1.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.2
upper-case-first: 2.0.2
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@1.1.3:
dependencies:
ansi-styles: 2.2.1
@ -6521,6 +6665,8 @@ snapshots:
transitivePeerDependencies:
- '@lezer/common'
codepage@1.15.0: {}
collection-visit@1.0.0:
dependencies:
map-visit: 1.0.0
@ -6608,6 +6754,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crc-32@1.2.2: {}
crelt@1.0.6: {}
cron-parser@4.9.0:
@ -6622,6 +6770,10 @@ snapshots:
crypto-js@4.2.0: {}
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
css-select@4.3.0:
dependencies:
boolbase: 1.0.0
@ -6791,6 +6943,11 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.6:
optionalDependencies:
'@types/trusted-types': 2.0.7
optional: true
domutils@1.7.0:
dependencies:
dom-serializer: 0.2.2
@ -7478,6 +7635,8 @@ snapshots:
dependencies:
reusify: 1.0.4
fflate@0.8.2: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -7548,6 +7707,8 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
frac@1.1.2: {}
fragment-cache@0.2.1:
dependencies:
map-cache: 0.2.2
@ -7723,6 +7884,11 @@ snapshots:
html-tags@3.3.1: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
htmlparser2@3.10.1:
dependencies:
domelementtype: 1.3.1
@ -7985,6 +8151,18 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jspdf@3.0.1:
dependencies:
'@babel/runtime': 7.28.2
atob: 2.1.2
btoa: 1.2.1
fflate: 0.8.2
optionalDependencies:
canvg: 3.0.11
core-js: 3.40.0
dompurify: 3.2.6
html2canvas: 1.4.1
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@ -8494,6 +8672,9 @@ snapshots:
perfect-debounce@1.0.0: {}
performance-now@2.1.0:
optional: true
picocolors@1.0.1: {}
picocolors@1.1.1: {}
@ -8710,6 +8891,11 @@ snapshots:
queue-microtask@1.2.3: {}
raf@3.4.1:
dependencies:
performance-now: 2.1.0
optional: true
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@ -8741,6 +8927,9 @@ snapshots:
dependencies:
'@eslint-community/regexpp': 4.10.0
regenerator-runtime@0.13.11:
optional: true
regenerator-runtime@0.14.1: {}
regex-not@1.0.2:
@ -8801,6 +8990,9 @@ snapshots:
rfdc@1.4.1: {}
rgbcolor@1.0.1:
optional: true
rollup@4.17.2:
dependencies:
'@types/estree': 1.0.5
@ -9049,8 +9241,15 @@ snapshots:
dependencies:
extend-shallow: 3.0.2
ssf@0.11.2:
dependencies:
frac: 1.1.2
stable@0.1.8: {}
stackblur-canvas@2.7.0:
optional: true
static-extend@0.1.2:
dependencies:
define-property: 0.2.5
@ -9167,6 +9366,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
svg-pathdata@6.0.3:
optional: true
svg-tags@1.0.0: {}
svgo@2.8.0:
@ -9201,6 +9403,10 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
text-table@0.2.0: {}
tippy.js@6.3.7:
@ -9448,6 +9654,10 @@ snapshots:
utils-merge@1.0.1: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
v-viewer@3.0.13(viewerjs@1.11.6)(vue@3.5.12(typescript@5.0.4)):
dependencies:
lodash-es: 4.17.21
@ -9717,8 +9927,12 @@ snapshots:
dependencies:
string-width: 5.1.2
wmf@1.0.2: {}
word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@ -9755,6 +9969,16 @@ snapshots:
fs-extra: 5.0.0
xgplayer-subtitles: 1.0.19
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-name-validator@4.0.0: {}
y18n@5.0.8: {}

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,20 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- 风机塔筒 -->
<rect x="47" y="40" width="6" height="45" fill="#4a5568" rx="3"/>
<!-- 风机机舱 -->
<ellipse cx="50" cy="40" rx="8" ry="5" fill="#2d3748"/>
<!-- 风机叶片 -->
<g transform="translate(50, 40)">
<!-- 叶片1 -->
<path d="M0,0 L-25,-15 Q-35,-20 -40,-30" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片2 -->
<path d="M0,0 L15,-30 Q20,-40 30,-45" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片3 -->
<path d="M0,0 L30,10 Q40,15 45,25" stroke="#4a5568" stroke-width="3" fill="none"/>
</g>
<!-- 风机底座 -->
<ellipse cx="50" cy="85" rx="12" ry="3" fill="#2d3748"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@ -17,14 +17,13 @@
<script setup lang="ts">
import { useAppStore, useUserStore } from '@/stores'
// 1
defineOptions({ name: 'App' })
const userStore = useUserStore()
const appStore = useAppStore()
appStore.initTheme()
appStore.initSiteConfig()
</script>
<style scoped lang="scss">
.loading-icon {
animation: arco-loading-circle 1s infinite cubic-bezier(0,0,1,1);

View File

@ -6,6 +6,7 @@ import type { AttachInfoData, BusinessTypeResult } from './type'
*
* @param businessType
* @param files
* @returns
*/
export function batchAddAttachment(businessType: string, formData: FormData) {
return request<AttachInfoData[]>({
@ -23,7 +24,7 @@ export function batchAddAttachment(businessType: string, formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachment(formData: FormData) {
export function addAttachment(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/model`,
method: 'post',
@ -38,7 +39,7 @@ export function addAttachment(formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachmentByDefectMarkPic(formData: FormData) {
export function addAttachmentByDefectMarkPic(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/defect_mark_pic`,
method: 'post',
@ -53,7 +54,7 @@ export function addAttachmentByDefectMarkPic(formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachInsurance(formData: FormData) {
export function addAttachInsurance(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/insurance_file`,
method: 'post',

View File

@ -0,0 +1,13 @@
import http from '@/utils/http'
import type { AttendanceRecordReq, AttendanceRecordResp } from './type'
const BASE_URL = '/attendance-record'
/** 新增考勤记录 */
export function addAttendanceRecord(data: AttendanceRecordReq) {
return http.post<AttendanceRecordResp>(BASE_URL, data, {
headers: {
'Content-Type': 'application/json'
}
})
}

View File

@ -0,0 +1,15 @@
/** 新增考勤记录请求体 */
export interface AttendanceRecordReq {
recordImage?: string
recordPosition?: string
recordPositionLabel?: string
}
/** 新增考勤记录响应体 */
export interface AttendanceRecordResp {
code: number
data: object
msg: string
status: number
success: boolean
}

View File

@ -1,64 +1,64 @@
/** 用户信息响应类型 */
export interface UserInfoResponse {
code: number;
status: number;
success: boolean;
msg: string;
code: number
status: number
success: boolean
msg: string
data: {
user: UserDetail;
dept: DeptDetail;
roles: RoleDetail[];
posts: any[];
};
user: UserDetail
dept: DeptDetail
roles: RoleDetail[]
posts: any[]
}
}
/** 用户详细信息 */
export interface UserDetail {
userId: string;
account: string;
name: string;
status: number;
userCode: string;
userStatus: string;
userType: string;
mobile: string;
createTime: string;
avatar?: string;
userId: string
account: string
name: string
status: number
userCode: string
userStatus: string
userType: string
mobile: string
createTime: string
avatar?: string
}
/** 部门详细信息 */
export interface DeptDetail {
deptId: string;
deptName: string;
parentId: string;
orderNum: number;
leaderId: string;
status: string;
deptId: string
deptName: string
parentId: string
orderNum: number
leaderId: string
status: string
}
/** 角色详细信息 */
export interface RoleDetail {
roleId: string;
roleName: string;
roleCode: string | null;
roleKey: string;
roleId: string
roleName: string
roleCode: string | null
roleKey: string
}
/** 用户类型 - 兼容旧版本 */
export interface UserInfo {
id: string;
username: string;
nickname: string;
gender: 0 | 1 | 2;
email: string;
phone: string;
avatar: string;
pwdResetTime: string;
pwdExpired: boolean;
registrationDate: string;
deptName: string;
roles: string[];
permissions: string[];
id: string
username: string
nickname: string
gender: 0 | 1 | 2
email: string
phone: string
avatar: string
pwdResetTime: string
pwdExpired: boolean
registrationDate: string
deptName: string
roles: string[]
permissions: string[]
}
/** 路由类型 */

View File

@ -0,0 +1,62 @@
import http from '@/utils/http'
const { request } = http
// 文件信息
export interface KnowledgeFile {
id: string
name: string
size: string
type: string
uploadTime: string
}
// 文件夹信息
export interface KnowledgeFolder {
id: string
name: string
children?: KnowledgeFolder[]
}
// 获取文件夹树
export function getFolderTreeApi() {
return request<KnowledgeFolder[]>({
url: '/knowledge/folders',
method: 'get',
})
}
// 获取文件列表(按文件夹)
export function getFilesApi(folderId: string) {
return request<KnowledgeFile[]>({
url: '/knowledge/files',
method: 'get',
params: { folderId },
})
}
// 创建文件夹
export function createFolderApi(data: { name: string; parentId?: string }) {
return request({
url: '/knowledge/create-folder',
method: 'post',
data,
})
}
// 删除文件
export function deleteFileApi(fileId: string) {
return request({
url: `/knowledge/delete-file/${fileId}`,
method: 'delete',
})
}
// 下载文件
export function downloadFileApi(fileId: string) {
return request<Blob>({
url: `/knowledge/download/${fileId}`,
method: 'get',
responseType: 'blob',
})
}

View File

@ -1,5 +1,5 @@
import http from '@/utils/http'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo } from './type'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo,CertificationPageResponse, CertificationReq } from './type'
const { request } = http
@ -7,7 +7,7 @@ const { request } = http
export type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo }
// 新增人员资质
export function createCertification(data: CertificationInfo) {
export function createCertification(data: CertificationReq) {
return request({
url: '/certification',
method: 'post',
@ -74,4 +74,12 @@ export function getUserList() {
url: '/user/list',
method: 'get'
})
}
}
// 查询人员资质信息分页列表(新接口)
export function getCertificationPage(params: CertificationListParams) {
return request<CertificationPageResponse>({
url: '/certification/page',
method: 'get',
params
})
}

View File

@ -11,19 +11,25 @@ export interface CertificationInfo {
createTime?: string
updateTime?: string
}
export interface CertificationReq {
certificationCode: string
certificationImage: string
certificationName: string
certificationType: string
userId: string
validityDateBegin: string
validityDateEnd: string
}
/** 人员资质列表查询参数 */
export interface CertificationListParams {
certificationName?: string
certificationType?: string
userName?: string
current?: number
size?: number
}
/** 人员资质列表响应 */
export interface CertificationListResponse {
records: CertificationInfo[]
data: CertificationInfo[]
total: number
current: number
size: number
@ -32,7 +38,22 @@ export interface CertificationListResponse {
/** 用户信息简化版 */
export interface SimpleUserInfo {
userId: string
userName: string
account: string
name: string
}
account: string
}
export interface CertificationInfo {
certificationId?: string
certificationCode: string
certificationName: string
userId: string
userName?: string // 新增,后端会返回
}
// 分页响应
export interface CertificationPageResponse {
rows: CertificationInfo[]
total: number
code?: number
msg?: string
[key: string]: any
}

View File

@ -0,0 +1,49 @@
import http from '@/utils/http'
import type * as T from '@/types/equipment.d'
const BASE_URL = '/equipment'
/** @desc 分页查询设备列表 */
export function pageEquipment(query: T.EquipmentPageQuery) {
return http.get<T.EquipmentResp[]>(`${BASE_URL}/page`, query)
}
/** @desc 查询设备列表 */
export function listEquipment(query?: T.EquipmentPageQuery) {
return http.get<T.EquipmentResp[]>(`${BASE_URL}/list`, query)
}
/** @desc 查询设备详情 */
export function getEquipmentDetail(equipmentId: string) {
return http.get<T.EquipmentResp>(`${BASE_URL}/detail/${equipmentId}`)
}
/** @desc 新增设备 */
export function createEquipment(data: T.EquipmentReq) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 更新设备 */
export function updateEquipment(equipmentId: string, data: T.EquipmentReq) {
return http.put(`${BASE_URL}/${equipmentId}`, data)
}
/** @desc 删除设备 */
export function deleteEquipment(equipmentId: string) {
return http.del(`${BASE_URL}/${equipmentId}`)
}
/** @desc 设备状态变更 */
export function changeEquipmentStatus(equipmentId: string, status: string) {
return http.put(`${BASE_URL}/${equipmentId}/status`, { status })
}
/** @desc 设备分配 */
export function assignEquipment(equipmentId: string, userId: string) {
return http.put(`${BASE_URL}/${equipmentId}/assign`, { userId })
}
/** @desc 设备归还 */
export function returnEquipment(equipmentId: string) {
return http.put(`${BASE_URL}/${equipmentId}/return`)
}

277
src/apis/equipment/type.ts Normal file
View File

@ -0,0 +1,277 @@
/**
*
*/
export interface EquipmentListReq {
/** 最低价格 */
minPrice?: number
/** 最高价格 */
maxPrice?: number
/** 设备名称 */
equipmentName?: string
/** 设备类型 */
equipmentType?: string
/** 设备状态 */
equipmentStatus?: string
/** 设备序列号 */
equipmentSn?: string
/** 资产编号 */
assetCode?: string
/** 品牌 */
brand?: string
/** 位置状态 */
locationStatus?: string
/** 健康状态 */
healthStatus?: string
/** 负责人 */
responsiblePerson?: string
/** 使用状态 */
useStatus?: string
/** 项目ID */
projectId?: string
/** 使用人ID */
userId?: string
/** 设备型号 */
equipmentModel?: string
/** 配置规格/参数 */
specification?: string
/** 设备当前物理位置 */
physicalLocation?: string
/** 供应商名称 */
supplierName?: string
/** 采购订单号 */
purchaseOrder?: string
/** 维护人员 */
maintenancePerson?: string
/** 次户号 */
accountNumber?: string
/** 数量 */
quantity?: number
/** 单价 */
unitPrice?: number
/** 总价 */
totalPrice?: number
/** 盘点依据 */
inventoryBasis?: string
/** 动态记录 */
dynamicRecord?: string
/** 资产备注 */
assetRemark?: string
/** 采购时间开始 */
purchaseTimeStart?: string
/** 采购时间结束 */
purchaseTimeEnd?: string
/** 入库时间开始 */
inStockTimeStart?: string
/** 入库时间结束 */
inStockTimeEnd?: string
/** 启用时间开始 */
activationTimeStart?: string
/** 启用时间结束 */
activationTimeEnd?: string
/** 当前页码 */
pageNum?: number
/** 每页大小 */
pageSize?: number
/** 排序字段 */
orderBy?: string
/** 排序方向 */
orderDirection?: string
/** 页码 */
page?: number
/** 库存条码 */
inventoryBarcode?: string
}
/**
*
*/
export interface PageResult<T> {
code: number
msg: string
rows: T[]
total: number
}
/**
*
*/
export interface EquipmentResp {
/** 设备ID */
equipmentId: string
/** 资产编号 */
assetCode?: string
/** 设备名称 */
equipmentName: string
/** 设备类型 */
equipmentType: string
/** 设备类型描述 */
equipmentTypeLabel?: string
/** 设备型号 */
equipmentModel: string
/** 设备SN */
equipmentSn: string
/** 品牌 */
brand?: string
/** 配置规格/参数 */
specification?: string
/** 设备状态 */
equipmentStatus: string
/** 设备状态描述 */
equipmentStatusLabel?: string
/** 使用状态 */
useStatus: string
/** 位置状态 */
locationStatus?: string
/** 位置状态描述 */
locationStatusLabel?: string
/** 设备当前物理位置 */
physicalLocation?: string
/** 负责人 */
responsiblePerson?: string
/** 健康状态 */
healthStatus?: string
/** 健康状态描述 */
healthStatusLabel?: string
/** 采购时间 */
purchaseTime?: string
/** 入库时间 */
inStockTime?: string
/** 启用时间 */
activationTime?: string
/** 预计报废时间 */
expectedScrapTime?: string
/** 实际报废时间 */
actualScrapTime?: string
/** 状态变更时间 */
statusChangeTime?: string
/** 采购订单 */
purchaseOrder?: string
/** 供应商名称 */
supplierName?: string
/** 采购价格 */
purchasePrice?: number
/** 当前净值 */
currentNetValue?: number
/** 折旧方法 */
depreciationMethod?: string
/** 折旧年限 */
depreciationYears?: number
/** 残值 */
salvageValue?: number
/** 保修截止日期 */
warrantyExpireDate?: string
/** 上次维护日期 */
lastMaintenanceDate?: string
/** 下次维护日期 */
nextMaintenanceDate?: string
/** 维护人员 */
maintenancePerson?: string
/** 库存条码 */
inventoryBarcode?: string
/** 资产备注 */
assetRemark?: string
/** 项目ID */
projectId?: string
/** 项目名称 */
projectName?: string
/** 使用人ID */
userId?: string
/** 使用人 */
name?: string
/** 创建时间 */
createTime?: string
/** 更新时间 */
updateTime?: string
/** 次户号 */
accountNumber?: string
/** 数量 */
quantity?: number
/** 单价 */
unitPrice?: number
/** 总价 */
totalPrice?: number
/** 盘点依据 */
inventoryBasis?: string
/** 动态记录 */
dynamicRecord?: string
}
/**
*
*/
export interface EquipmentReq {
/** 设备名称 */
equipmentName: string
/** 设备型号 */
equipmentModel: string
/** 设备类型 */
equipmentType: string
/** 设备状态 */
equipmentStatus: string
/** 使用状态 */
useStatus: string
/** 设备序列号 */
equipmentSn: string
/** 资产编号 */
assetCode?: string
/** 品牌 */
brand?: string
/** 配置规格/参数 */
specification?: string
/** 位置状态 */
locationStatus?: string
/** 设备当前物理位置 */
physicalLocation?: string
/** 负责人 */
responsiblePerson?: string
/** 健康状态 */
healthStatus?: string
/** 采购时间 */
purchaseTime?: string
/** 入库时间 */
inStockTime?: string
/** 启用时间 */
activationTime?: string
/** 预计报废时间 */
expectedScrapTime?: string
/** 实际报废时间 */
actualScrapTime?: string
/** 采购订单 */
purchaseOrder?: string
/** 供应商名称 */
supplierName?: string
/** 采购价格 */
purchasePrice?: number
/** 当前净值 */
currentNetValue?: number
/** 折旧方法 */
depreciationMethod?: string
/** 折旧年限 */
depreciationYears?: number
/** 残值 */
salvageValue?: number
/** 保修截止日期 */
warrantyExpireDate?: string
/** 上次维护日期 */
lastMaintenanceDate?: string
/** 下次维护日期 */
nextMaintenanceDate?: string
/** 维护人员 */
maintenancePerson?: string
/** 库存条码 */
inventoryBarcode?: string
/** 资产备注 */
assetRemark?: string
/** 次户号 */
accountNumber?: string
/** 数量 */
quantity?: number
/** 单价 */
unitPrice?: number
/** 总价 */
totalPrice?: number
/** 盘点依据 */
inventoryBasis?: string
/** 动态记录 */
dynamicRecord?: string
}

View File

@ -16,6 +16,7 @@ export * as InsuranceTypeAPI from './insurance-type'
export * as HealthRecordAPI from './health-record'
export * as InsuranceFileAPI from './insurance-file'
export * as EmployeeAPI from './employee'
export * as RegulationAPI from './regulation'
export * from './area/type'
export * from './auth/type'

View File

@ -0,0 +1,40 @@
import http from '@/utils/http'
import type { PerformanceDimension, PerformanceRule, DimensionQuery, RuleQuery } from './type'
/** 维度相关 */
export function getDimensionList(params?: DimensionQuery) {
return http.get<PerformanceDimension[]>('/performance-dimension/list', params)
}
export function getDimensionDetail(id: string) {
return http.get<PerformanceDimension>(`/performance-dimension/${id}`)
}
export function addDimension(data: Partial<PerformanceDimension>) {
return http.post('/performance-dimension', data)
}
export function updateDimension(id: string, data: Partial<PerformanceDimension>) {
return http.put(`/performance-dimension/${id}`, data)
}
export function deleteDimension(id: string) {
return http.del(`/performance-dimension/${id}`)
}
/** 细则相关 */
export function getRuleList(params?: RuleQuery) {
return http.get<PerformanceRule[]>('/performance-rule/list', params)
}
export function getRuleDetail(id: string) {
return http.get<PerformanceRule>(`/performance-rule/${id}`)
}
export function addRule(data: Partial<PerformanceRule>) {
return http.post('/performance-rule', data)
}
export function updateRule(id: string, data: Partial<PerformanceRule>) {
return http.put(`/performance-rule/${id}`, data)
}
export function deleteRule(id: string) {
return http.del(`/performance-rule/${id}`)
}
// 我的绩效
export function getMyEvaluation() {
return http.get('/performance-evaluation/my')
}

View File

@ -0,0 +1,39 @@
/** 绩效维度 */
export interface PerformanceDimension {
dimensionId: string
dimensionName: string
description?: string
deptName: string
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 绩效细则 */
export interface PerformanceRule {
ruleId: string
ruleName: string
description?: string
dimensionName: string
bonus?: string
score?: number
weight?: number
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 查询参数 */
export interface DimensionQuery {
dimensionName?: string
status?: 0 | 1
}
export interface RuleQuery {
dimensionName?: string
ruleName?: string
status?: 0 | 1
}

View File

@ -0,0 +1,59 @@
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/performance'
/* ===== 维度 ===== */
export const getDimensionList = () => http.get<T.DimensionResp[]>(`${BASE_URL}/dimension`)
export const addDimension = (data: T.DimensionAddReq) =>
http.post<boolean>(`${BASE_URL}/dimension`, data)
export const updateDimension = (id: string, data: T.DimensionUpdateReq) =>
http.put<boolean>(`${BASE_URL}/dimension/${id}`, data)
export const deleteDimension = (id: string) =>
http.del<boolean>(`${BASE_URL}/dimension/${id}`)
/** 维度详情RuleDrawer.vue 需要) */
export const getDimensionDetail = (id: string) =>
http.get<T.DimensionResp>(`${BASE_URL}/dimension/${id}`)
/* ===== 细则 ===== */
export const getRuleList = (dimensionId: string) =>
http.get<T.RuleResp[]>(`${BASE_URL}/rule`, { dimensionId })
export const addRule = (data: T.RuleAddReq) =>
http.post<boolean>(`${BASE_URL}/rule`, data)
export const updateRule = (id: string, data: T.RuleUpdateReq) =>
http.put<boolean>(`${BASE_URL}/rule/${id}`, data)
export const deleteRule = (id: string) =>
http.del<boolean>(`${BASE_URL}/rule/${id}`)
/** 细则详情RuleDrawer.vue 需要) */
export const getRuleDetail = (id: string) =>
http.get<T.RuleResp>(`${BASE_URL}/rule/${id}`)
/* ===== 周期 ===== */
export const getPeriodList = () => http.get<T.PeriodResp[]>(`${BASE_URL}/period`)
export const addPeriod = (data: T.PeriodResp) => http.post<boolean>(`${BASE_URL}/period`, data)
export const deletePeriod = (id: string) => http.del<boolean>(`${BASE_URL}/period/${id}`)
export const updataPeriod = (id: string, data: T.PeriodResp) => http.post<boolean>(`${BASE_URL}/period/${id}`, data)
/* ===== 评估 ===== */
export const startEvaluate = (data: T.EvaluateReq) =>
http.post<boolean>(`${BASE_URL}/evaluate`, data)
export const getEvaluatePage = (query?: T.EvaluateQuery) =>
http.get<PageRes<T.EvaluateResp[]>>(`${BASE_URL}/evaluate/page`, query)
// 同样的修改和删除评估接口
/** 员工查看自己的绩效 */
export const getMyEvaluate = (query?: T.EvaluateQuery) =>
http.get<PageRes<T.EvaluateResp[]>>(`${BASE_URL}/evaluate/my`, query)
/* ===== 反馈 ===== */
export const submitFeedback = (data: T.FeedbackReq) =>
http.post<boolean>(`${BASE_URL}/feedback`, data)

View File

@ -0,0 +1,87 @@
// ========== 维度 ==========
export interface DimensionResp {
id: string
name: string
desc: string
weight: number
status: 0 | 1
ruleCount: number
deptId: string
}
export interface DimensionAddReq {
name: string
desc: string
weight: number
deptId: string
}
export interface DimensionUpdateReq extends DimensionAddReq {
status: 0 | 1
}
// ========== 细则 ==========
export interface RuleResp {
id: string
dimensionId: string
name: string
ruleDesc: string
score: number
weight: number
isExtra: boolean
}
export interface RuleAddReq {
dimensionId: string
name: string
ruleDesc: string
score: number
weight: number
isExtra: boolean
}
export interface RuleUpdateReq extends RuleAddReq {}
// ========== 绩效周期 ==========
export interface PeriodResp {
id: string
name: string
startDate: string
endDate: string
}
// ========== 评估 ==========
export interface EvaluateReq {
userId: string
dimensionId: string
ruleId: string
periodId: string
}
export interface EvaluateResp {
id: string // 细则id
userId: string
userName: string
dimensionName: string
ruleName: string
periodName: string
score: number
aiComment: string
status: 0 | 1 // 0待反馈 1已反馈
}
// ========== 查询 ==========
export interface EvaluateQuery {
keyword?: string
periodId?: string
dimensionId?: string
status?: 0 | 1
}
// ========== 反馈 ==========
export interface FeedbackReq {
evaluateId: string
level: 0 | 1 | 2
content: string
userId: string
}

View File

@ -23,9 +23,9 @@ export function deleteTaskGroup(id: number) {
return http.del(`${BASE_URL}/group/${id}`)
}
/** @desc 查询任务列表 */
export function listTask(query: T.TaskPageQuery) {
return http.get<PageRes<T.TaskResp[]>>(`${BASE_URL}`, query)
/** @desc 查询任务列表(标准导出) */
export const listTask = (params: any) => {
return http.get('/project-task/list', params)
}
/** @desc 获取任务详情 */

View File

@ -0,0 +1,108 @@
import http from '@/utils/http'
import {
type RegulationTypeSearchRequest,
type RegulationTypeSearchResponse,
type RegulationProposalSearchRequest,
type RegulationProposalSearchResponse
} from './type'
// 制度管理API接口
export const regulationApi = {
// 获取制度列表
getRegulationList: (params: {
page?: number
size?: number
status?: string
type?: string
title?: string
proposer?: string
}): Promise<RegulationProposalSearchResponse> => {
return http.get('/regulation', params)
},
// 获取制度详情
getRegulationDetail: (regulationId: string) => {
return http.get(`/regulation/${regulationId}`)
},
// 创建制度提案
createProposal: (data: {
title: string
content: string
regulationType: string
scope: string
level: string
remark?: string
createBy?: string
}) => {
return http.post('/regulation/proposal', data)
},
// 更新制度提案
updateProposal: (regulationId: string, data: any) => {
return http.put(`/regulation/proposal/${regulationId}`, data)
},
// 公示提案
publishProposal: (regulationId: string) => {
return http.post(`/regulation/proposal/${regulationId}/publish`)
},
// 删除制度提案
deleteProposal: (regulationId: string) => {
return http.del(`/regulation/proposal/${regulationId}`)
},
// 公示制度
publishRegulation: (regulationId: string) => {
return http.post(`/regulation/${regulationId}/approve`)
},
// 获取已公示制度列表
getPublishedRegulationList: (params: {
page: number
size: number
status: string
}) => {
return http.get('/regulation', params)
},
// 确认制度知晓
confirmRegulation: (regulationId: string) => {
return http.post(`/regulation/${regulationId}/confirm`)
},
// 搜索制度类型(后端搜索接口)
searchRegulationTypes: (params: {
page?: number
size?: number
typeName?: string
status?: string
remark?: string
}) => {
return http.get('/regulation/types', params)
},
// 创建制度类型
createRegulationType: (data: {
typeName: string
sortOrder?: number
isEnabled?: string
}) => {
return http.post('/regulation/types', data)
},
// 更新制度类型
updateRegulationType: (typeId: string, data: {
typeName: string
sortOrder?: number
isEnabled?: string
}) => {
return http.put(`/regulation/types/${typeId}`, data)
},
// 删除制度类型
deleteRegulationType: (typeId: string) => {
return http.del(`/regulation/types/${typeId}`)
}
}

121
src/apis/regulation/type.ts Normal file
View File

@ -0,0 +1,121 @@
// 制度状态枚举
export enum RegulationStatus {
DRAFT = 'DRAFT', // 草稿
APPROVED = 'APPROVED', // 已通过
PUBLISHED = 'PUBLISHED', // 已公示
}
// 制度级别枚举
export enum RegulationLevel {
LOW = 'LOW', // 低
MEDIUM = 'MEDIUM', // 中
HIGH = 'HIGH' // 高
}
// 制度信息接口
export interface Regulation {
regulationId: string
title: string
content: string
regulationType: string
status: RegulationStatus
publishTime: string // 公示时间
effectiveTime: string
expireTime: string
scope: string
level: RegulationLevel
version: string
remark?: string
createBy: string
updateBy: string
createTime: string
updateTime: string
page: number
pageSize: number
delFlag: string
confirmStatus?: string
}
// 创建提案请求接口
export interface CreateProposalRequest {
title: string
content: string
regulationType: string
scope: string
level: RegulationLevel
remark?: string
createBy?: string
}
// 分页参数接口
export interface PaginationParams {
page: number
size: number
}
// 制度类型接口
export interface RegulationType {
typeId: string
typeName: string
sortOrder: number
isEnabled: string
remark?: string
createBy: string
createTime: string
updateBy: string
updateTime: string
delFlag: string
}
// 创建制度类型请求接口
export interface CreateRegulationTypeRequest {
typeName: string
sortOrder?: number
isEnabled?: string
remark?: string
}
// 更新制度类型请求接口
export interface UpdateRegulationTypeRequest {
typeName: string
sortOrder?: number
isEnabled?: string
remark?: string
}
// 制度类型搜索请求接口
export interface RegulationTypeSearchRequest {
page?: number
size?: number
typeName?: string
status?: string
remark?: string
}
// 制度类型搜索响应接口
export interface RegulationTypeSearchResponse {
records: RegulationType[]
total: number
current: number
size: number
pages: number
}
// 制度提案搜索请求接口
export interface RegulationProposalSearchRequest {
page?: number
size?: number
status?: string
type?: string
title?: string
proposer?: string
}
// 制度提案搜索响应接口
export interface RegulationProposalSearchResponse {
records: Regulation[]
total: number
current: number
size: number
pages: number
}

64
src/apis/salary/index.ts Normal file
View File

@ -0,0 +1,64 @@
import type { SalaryRecord, SalaryQuery, SalaryCreateRequest } from '@/views/salary-management/types'
import http from '@/utils/http'
const BASE_URL = '/salary'
// 获取工资单列表
export const getSalaryList = (query?: SalaryQuery) => {
return http.get<PageRes<SalaryRecord[]>>(`${BASE_URL}/list`, query)
}
// 获取工资单详情
export const getSalaryDetail = (id: string) => {
return http.get<SalaryRecord>(`${BASE_URL}/${id}`)
}
// 创建工资单
export const createSalary = (data: SalaryCreateRequest) => {
return http.post<SalaryRecord>(`${BASE_URL}`, data)
}
// 更新工资单
export const updateSalary = (id: string, data: Partial<SalaryRecord>) => {
return http.put<SalaryRecord>(`${BASE_URL}/${id}`, data)
}
// 删除工资单
export const deleteSalary = (id: string) => {
return http.del<boolean>(`${BASE_URL}/${id}`)
}
// 提交审批
export const submitApproval = (id: string) => {
return http.post<boolean>(`${BASE_URL}/${id}/submit`)
}
// 审批工资单
export const approveSalary = (id: string, data: { status: string; comment?: string }) => {
return http.put<boolean>(`${BASE_URL}/${id}/approve`, data)
}
// 导出工资单
export const exportSalary = (id: string, format: 'excel' | 'pdf' = 'excel') => {
return http.download(`${BASE_URL}/${id}/export`, { format })
}
// 批量导出
export const exportSalaryBatch = (ids: string[], format: 'excel' | 'pdf' = 'excel') => {
return http.download(`${BASE_URL}/export/batch`, { ids, format })
}
// 获取实习生配置
export const getInternConfig = () => {
return http.get<any>(`${BASE_URL}/config/intern`)
}
// 获取员工列表
export const getEmployeeList = (type?: string) => {
return http.get<any[]>(`${BASE_URL}/employees`, { type })
}
// 获取项目列表
export const getProjectList = () => {
return http.get<any[]>(`${BASE_URL}/projects`)
}

View File

@ -42,5 +42,11 @@ export function updatePost(postId: string, data: T.PostUpdateReq) {
*
*/
export function deletePost(postId: string) {
return http.del<any>(`${BASE_URL}/${postId}`);
}
}
/**
*
*/
export function getPostUsers(postId: string) {
return http.get<T.UserNewResp[]>(`${BASE_URL}/${postId}/user`);
}

View File

@ -4,6 +4,7 @@ import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/role'
const BASE_URL_NEW = '/role'
/** @desc 查询角色列表(已废弃) */
export function listRole(query: T.RoleQuery) {
@ -72,7 +73,7 @@ export function updateRolePermission(id: string, data: any) {
/** @desc 查询角色关联用户 */
export function listRoleUser(id: string, query: T.RoleUserPageQuery) {
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL_NEW}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
@ -87,5 +88,5 @@ export function unassignFromUsers(userRoleIds: Array<string | number>) {
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL}/${id}/user/id`)
return http.get(`${BASE_URL_NEW}/${id}/user`)
}

View File

@ -0,0 +1,234 @@
<template>
<div class="turbine-grid-container">
<div class="turbine-grid">
<div v-for="turbine in turbines" :key="turbine.id" class="turbine-card"
:class="getStatusClass(turbine.status)">
<div class="turbine-status-badge" :class="`status-${turbine.status}`">
{{ getStatusText(turbine.status) }}
</div>
<div class="turbine-icon">
<img src="/static/images/wind-turbine-icon.svg" alt="风机图标" class="turbine-image" />
</div>
<div class="turbine-info">
<div class="turbine-number">
<a-input v-model="turbine.turbineNo" size="small" class="turbine-input" placeholder="请输入机组编号"
@change="handleTurbineNoChange(turbine)" />
</div>
</div>
<div class="turbine-actions">
<a-button type="text" size="mini" @click="openMapModal(turbine)" title="地图选点">
<template #icon><icon-location /></template>
</a-button>
<a-button type="text" size="mini" @click="editTurbine(turbine)" title="编辑">
<template #icon><icon-edit /></template>
</a-button>
</div>
</div>
</div>
<!-- 添加新机组按钮 -->
<div v-if="showAddButton" class="turbine-card add-turbine-card" @click="addTurbine">
<div class="add-icon">
<icon-plus />
</div>
<div class="add-text">添加机组</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0: , 1: , 2:
lat?: number
lng?: number
}
interface Props {
turbines: Turbine[]
showAddButton?: boolean
}
interface Emits {
(e: 'update:turbines', turbines: Turbine[]): void
(e: 'turbine-change', turbine: Turbine): void
(e: 'add-turbine'): void
}
const props = withDefaults(defineProps<Props>(), {
showAddButton: false
})
const emit = defineEmits<Emits>()
const getStatusText = (status: number) => {
const statusMap = {
0: '待施工',
1: '施工中',
2: '已完成'
}
return statusMap[status] || '未知状态'
}
const getStatusClass = (status: number) => {
return `status-${status}`
}
const handleTurbineNoChange = (turbine: Turbine) => {
emit('turbine-change', turbine)
emit('update:turbines', props.turbines)
}
const openMapModal = (turbine: Turbine) => {
Message.info(`地图选点功能待开发,当前机组编号:${turbine.turbineNo}`)
}
const editTurbine = (turbine: Turbine) => {
//
Message.info(`编辑机组:${turbine.turbineNo}`)
}
const addTurbine = () => {
emit('add-turbine')
}
</script>
<style scoped>
.turbine-grid-container {
padding: 16px;
}
.turbine-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.turbine-card {
position: relative;
background: white;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px;
transition: all 0.3s ease;
cursor: pointer;
}
.turbine-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.turbine-card.status-0 {
border-left: 4px solid #ff7d00;
}
.turbine-card.status-1 {
border-left: 4px solid #165dff;
}
.turbine-card.status-2 {
border-left: 4px solid #00b42a;
}
.turbine-status-badge {
position: absolute;
top: 8px;
left: 8px;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
color: white;
}
.turbine-status-badge.status-0 {
background-color: #ff7d00;
}
.turbine-status-badge.status-1 {
background-color: #165dff;
}
.turbine-status-badge.status-2 {
background-color: #00b42a;
}
.turbine-icon {
display: flex;
justify-content: center;
margin: 20px 0;
}
.turbine-image {
width: 80px;
height: 80px;
object-fit: contain;
}
.turbine-info {
text-align: center;
margin-bottom: 12px;
}
.turbine-input {
text-align: center;
font-weight: 500;
}
.turbine-actions {
display: flex;
justify-content: center;
gap: 8px;
}
.add-turbine-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #d9d9d9;
background-color: #fafafa;
color: #666;
min-height: 200px;
}
.add-turbine-card:hover {
border-color: #165dff;
color: #165dff;
background-color: #f0f7ff;
}
.add-icon {
font-size: 32px;
margin-bottom: 8px;
}
.add-text {
font-size: 14px;
}
@media (max-width: 768px) {
.turbine-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.turbine-card {
padding: 12px;
}
.turbine-image {
width: 60px;
height: 60px;
}
}
</style>

View File

@ -4,7 +4,7 @@
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<a-layout-sider
class="menu" collapsible breakpoint="xl" hide-trigger :width="300"
class="menu" collapsible breakpoint="xl" hide-trigger :width="250"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>

View File

@ -1,8 +1,12 @@
<template>
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
<img v-if="logo" class="logo" :src="logo" alt="logo" />
<img v-else class="logo" src="/logo.svg" alt="logo" />
<span class="system-name gi_line_1">{{ title }}</span>
<row>
<img v-if="logo" class="logo" :src="logo" alt="logo" />
<img v-else class="logo" src="/logo.svg" alt="logo" />
</row>
<row>
<span class="system-name gi_line_1">{{ title }}</span>
</row>
</section>
</template>
@ -14,8 +18,10 @@ const props = withDefaults(defineProps<Props>(), {
})
const appStore = useAppStore()
// const title = computed(() => appStore.getTitle())
const title = "武汉迪特聚能有限公司管理平台"
const logo = computed(() => appStore.getLogo())
const title = "数智平台"
const logo = "/logo.png"
//computed(() => appStore.getLogo())
interface Props {
collapsed?: boolean
@ -29,50 +35,46 @@ const toHome = () => {
<style scoped lang="scss">
.system-logo {
height: 56px;
padding: 0 12px;
color: var(--color-text-1);
font-size: 16px;
line-height: 1;
height: 64px; //
padding: 8px 12px;
display: flex;
font-weight: 600;
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
cursor: pointer;
user-select: none;
box-sizing: border-box;
&.collapsed {
height: 56px;
flex-direction: row;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
.system-name {
display: none;
}
}
.logo {
width: 32px;
height: 32px;
border-radius: 6px;
transition: all 0.2s;
overflow: hidden;
flex-shrink: 0;
}
.system-name {
padding-left: 6px;
white-space: nowrap;
transition: color 0.3s;
line-height: 1.5;
display: inline-flex;
align-items: center;
&:hover {
color: $color-theme !important;
cursor: pointer;
.logo {
margin-bottom: 0;
}
}
}
.logo {
height: 32px;
width: auto;
max-width: 140px; // logo
object-fit: contain;
margin-bottom: 4px;
}
.system-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
text-align: center;
line-height: 1.2;
white-space: nowrap;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface Regulation {
id: string
name: string
type: string
typeName: string
publisher: string
publishTime: string
effectiveDate: string
confirmStatus: 'pending' | 'confirmed'
content: string
scope: string
requirements: string
notes: string
}
export const useRegulationStore = defineStore('regulation', () => {
// 已发布的制度列表
const publishedRegulations = ref<Regulation[]>([])
// 添加新发布的制度
const addPublishedRegulation = (regulation: Regulation) => {
publishedRegulations.value.unshift(regulation)
}
// 更新制度确认状态
const updateRegulationConfirmStatus = (id: string, status: 'pending' | 'confirmed') => {
const regulation = publishedRegulations.value.find(item => item.id === id)
if (regulation) {
regulation.confirmStatus = status
}
}
// 批量确认所有制度
const confirmAllRegulations = () => {
publishedRegulations.value.forEach(regulation => {
regulation.confirmStatus = 'confirmed'
})
}
return {
publishedRegulations,
addPublishedRegulation,
updateRegulationConfirmStatus,
confirmAllRegulations
}
})

View File

@ -4,10 +4,9 @@ import type { RouteRecordRaw } from 'vue-router'
import { mapTree, toTreeArray } from 'xe-utils'
import { cloneDeep, omit } from 'lodash-es'
import { constantRoutes, systemRoutes } from '@/router/route'
import { type RouteItem, getUserRouteWithAdapter } from '@/apis'
import type { RouteItem } from '@/apis'
import { transformPathToName } from '@/utils'
import { asyncRouteModules } from '@/router/asyncModules'
import { convertMenuData, type ApiMenuItem } from '@/utils/menuConverter'
const layoutComponentMap = {
Layout: () => import('@/layout/index.vue'),
@ -94,91 +93,77 @@ const storeSetup = () => {
// 获取路由数据并已通过适配器转换
// const { data } = await getUserRouteWithAdapter()
const data = [{
"id": 1000,
"parentId": 0,
"title": "系统管理",
"type": 1,
"path": "/system",
"name": "System",
"component": "Layout",
"redirect": "/system/user",
"icon": "settings",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 1,
"children": [
{
"id": 1010,
"parentId": 1000,
"title": "用户管理",
"type": 2,
"path": "/system/user",
"name": "SystemUser",
"component": "system/user/index",
"icon": "user",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 1
},
{
"id": 1030,
"parentId": 1000,
"title": "角色管理",
"type": 2,
"path": "/system/role",
"name": "SystemRole",
"component": "system/role/index",
"icon": "user-group",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 2
},
{
"id": 1050,
"parentId": 1000,
"title": "菜单管理",
"type": 2,
"path": "/system/menu",
"name": "SystemMenu",
"component": "system/menu/index",
"icon": "menu",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 3
},
{
"id": 1070,
"parentId": 1000,
"title": "部门管理",
"type": 2,
"path": "/system/dept",
"name": "SystemDept",
"component": "system/dept/index",
"icon": "mind-mapping",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 4
},
{
"id": 1090,
"parentId": 1000,
"title": "岗位管理",
"type": 2,
"path": "/system/post",
"name": "SystemPost",
"component": "system/post/index",
"icon": "settings",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 5
}
]
id: 1000,
parentId: 0,
title: '系统管理',
type: 1,
path: '/system',
name: 'System',
component: 'Layout',
redirect: '/system/user',
icon: 'settings',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
children: [
{
id: 1010,
parentId: 1000,
title: '用户管理',
type: 2,
path: '/system/user',
name: 'SystemUser',
component: 'system/user/index',
icon: 'user',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
},
{
id: 1030,
parentId: 1000,
title: '角色管理',
type: 2,
path: '/system/role',
name: 'SystemRole',
component: 'system/role/index',
icon: 'user-group',
isExternal: false,
isCache: false,
isHidden: false,
sort: 2,
},
{
id: 1050,
parentId: 1000,
title: '菜单管理',
type: 2,
path: '/system/menu',
name: 'SystemMenu',
component: 'system/menu/index',
icon: 'menu',
isExternal: false,
isCache: false,
isHidden: false,
sort: 3,
},
{
id: 1070,
parentId: 1000,
title: '个人中心',
type: 2,
path: '/user/profile',
name: 'UserProfile',
component: 'user/profile/index',
icon: 'user',
isExternal: false,
isCache: false,
isHidden: false,
sort: 4,
},
],
}]
// 使用已转换的数据生成路由
const asyncRoutes = formatAsyncRoutes(data as unknown as RouteItem[])

View File

@ -57,6 +57,7 @@ declare global {
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']

View File

@ -61,6 +61,7 @@ declare module 'vue' {
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']

179
src/types/equipment.d.ts vendored Normal file
View File

@ -0,0 +1,179 @@
export interface EquipmentPageQuery {
equipmentName?: string
equipmentType?: string
equipmentStatus?: string
equipmentSn?: string
assetCode?: string
brand?: string
locationStatus?: string
healthStatus?: string
responsiblePerson?: string
useStatus?: string
projectId?: string
userId?: string
equipmentModel?: string
specification?: string
physicalLocation?: string
supplierName?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增搜索字段
usingDepartment?: string
invoice?: string
barcode?: string
importer?: string
page?: number
pageSize?: number
orderBy?: string
orderDirection?: string
}
export interface EquipmentReq {
equipmentName: string
equipmentModel: string
equipmentType: string
equipmentStatus: string
useStatus: string
equipmentSn: string
assetCode?: string
brand?: string
specification?: string
locationStatus?: string
physicalLocation?: string
responsiblePerson?: string
healthStatus?: string
purchaseTime?: string
inStockTime?: string
activationTime?: string
expectedScrapTime?: string
actualScrapTime?: string
statusChangeTime?: string
purchaseOrder?: string
supplierName?: string
purchasePrice?: number
currentNetValue?: number
depreciationMethod?: string
depreciationYears?: number
salvageValue?: number
warrantyExpireDate?: string
lastMaintenanceDate?: string
nextMaintenanceDate?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增字段
usingDepartment?: string
borrowingTime?: string
returnTime?: string
outStockTime?: string
totalUsageTime?: string
depreciationRate?: number
depreciationMethodDesc?: string
invoice?: string
invoiceStatus?: string
attachments?: string
photos?: string
barcode?: string
importer?: string
inventoryTimeStatus1?: string
inventoryTimeStatus2?: string
inventoryTimeStatus3?: string
inventoryCheckTimeStatus1?: string
inventoryCheckTimeStatus2?: string
inventoryCheckTimeStatus3?: string
}
export interface EquipmentResp {
equipmentId: string
assetCode?: string
equipmentName: string
equipmentType: string
equipmentTypeLabel?: string
equipmentModel: string
equipmentSn: string
brand?: string
specification?: string
equipmentStatus: string
equipmentStatusLabel?: string
useStatus: string
locationStatus?: string
locationStatusLabel?: string
physicalLocation?: string
responsiblePerson?: string
healthStatus?: string
healthStatusLabel?: string
purchaseTime?: string
inStockTime?: string
activationTime?: string
expectedScrapTime?: string
actualScrapTime?: string
statusChangeTime?: string
purchaseOrder?: string
supplierName?: string
purchasePrice?: number
currentNetValue?: number
depreciationMethod?: string
depreciationYears?: number
salvageValue?: number
warrantyExpireDate?: string
lastMaintenanceDate?: string
nextMaintenanceDate?: string
maintenancePerson?: string
inventoryBarcode?: string
assetRemark?: string
// 新增字段
usingDepartment?: string
borrowingTime?: string
returnTime?: string
outStockTime?: string
totalUsageTime?: string
depreciationRate?: number
depreciationMethodDesc?: string
invoice?: string
invoiceStatus?: string
attachments?: string
photos?: string
barcode?: string
importer?: string
inventoryTimeStatus1?: string
inventoryTimeStatus2?: string
inventoryTimeStatus3?: string
inventoryCheckTimeStatus1?: string
inventoryCheckTimeStatus2?: string
inventoryCheckTimeStatus3?: string
projectId?: string
projectName?: string
userId?: string
name?: string
createTime?: string
updateTime?: string
}
export interface EquipmentTypeOption {
label: string
value: string
}
export interface EquipmentStatusOption {
label: string
value: string
color: string
}
export interface LocationStatusOption {
label: string
value: string
color: string
}
export interface HealthStatusOption {
label: string
value: string
color: string
}
export interface DepreciationMethodOption {
label: string
value: string
}

169
src/utils/pdfGenerator.ts Normal file
View File

@ -0,0 +1,169 @@
import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
// PDF生成器类
export class PDFGenerator {
// 生成制度PDF
static async generateRegulationPDF(regulation: any): Promise<Blob> {
// 创建临时HTML元素
const tempDiv = document.createElement('div')
tempDiv.style.position = 'absolute'
tempDiv.style.left = '-9999px'
tempDiv.style.top = '-9999px'
tempDiv.style.width = '800px'
tempDiv.style.padding = '40px'
tempDiv.style.backgroundColor = 'white'
tempDiv.style.fontFamily = 'Arial, sans-serif'
tempDiv.style.fontSize = '14px'
tempDiv.style.lineHeight = '1.6'
tempDiv.style.color = '#333'
// 添加水印样式
tempDiv.style.position = 'relative'
tempDiv.style.overflow = 'hidden'
// 生成HTML内容
tempDiv.innerHTML = `
<div style="position: relative; z-index: 1;">
<!-- -->
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
font-size: 48px;
color: rgba(200, 200, 200, 0.3);
font-weight: bold;
z-index: 0;
pointer-events: none;
white-space: nowrap;
"></div>
<!-- -->
<h1 style="
text-align: center;
font-size: 24px;
font-weight: bold;
margin-bottom: 30px;
color: #333;
">${regulation.title || '制度文档'}</h1>
<!-- -->
<div style="margin-bottom: 30px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.regulationType || '-'}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.createByName || regulation.createBy || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.publishTime || '-'}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.effectiveTime || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.scope || '-'}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.level || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"><strong></strong>${regulation.version || '1.0'}</td>
<td style="padding: 8px 0; border-bottom: 1px solid #eee;"></td>
</tr>
</table>
</div>
<!-- 线 -->
<hr style="border: none; border-top: 2px solid #ddd; margin: 30px 0;">
<!-- -->
<div style="margin-bottom: 30px;">
<h2 style="font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #333;"></h2>
<div style="
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.8;
text-align: justify;
">${regulation.content || ''}</div>
</div>
${regulation.remark ? `
<!-- -->
<div style="margin-bottom: 30px;">
<h2 style="font-size: 18px; font-weight: bold; margin-bottom: 15px; color: #333;"></h2>
<div style="
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.8;
text-align: justify;
">${regulation.remark}</div>
</div>
` : ''}
<!-- -->
<div style="
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid #ddd;
text-align: center;
font-size: 12px;
color: #666;
">
${new Date().toLocaleString('zh-CN')} | ID${regulation.regulationId || '-'}
</div>
</div>
`
// 添加到DOM
document.body.appendChild(tempDiv)
try {
// 使用html2canvas转换为图片
const canvas = await html2canvas(tempDiv, {
scale: 2, // 提高清晰度
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: 800,
height: tempDiv.scrollHeight
})
// 创建PDF
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF('p', 'mm', 'a4')
const imgWidth = 210 // A4宽度
const pageHeight = 295 // A4高度
const imgHeight = (canvas.height * imgWidth) / canvas.width
let heightLeft = imgHeight
let position = 0
// 添加第一页
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
// 添加后续页面
while (heightLeft >= 0) {
position = heightLeft - imgHeight
pdf.addPage()
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
}
// 返回PDF blob
return pdf.output('blob')
} finally {
// 清理临时元素
document.body.removeChild(tempDiv)
}
}
// 下载PDF文件
static downloadPDF(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -105,8 +105,16 @@ const formData = reactive({
const fetchBusinessTypes = async () => {
try {
const res = await getAttachBusinessTypes()
console.log("res:",res);
if (res.data) {
businessTypes.value = res.data
res.data.forEach(item => {
const key = Object.keys(item)[0];
const value = item[key];
businessTypes.value.push({
name: value,
code:key
});
});
}
} catch (error) {
console.error('获取业务类型失败:', error)

View File

@ -0,0 +1,603 @@
<template>
<GiPageLayout>
<div class="data-preprocessing-container">
<!-- 上传进度框 -->
<div v-if="uploadState.visible" class="upload-progress-fixed">
<a-card :title="`上传进度 (${uploadState.percent}%)`" :bordered="false">
<a-progress
:percent="uploadState.percent"
:status="uploadState.status"
:stroke-width="16"
/>
<div class="progress-details">
<p><icon-file /> {{ uploadState.currentFile || '准备中...' }}</p>
<p><icon-check-circle /> 已完成 {{ uploadState.uploadedCount }}/{{ uploadState.totalCount }}</p>
<p><icon-clock-circle /> 状态: {{ getStatusText(uploadState.status) }}</p>
</div>
</a-card>
</div>
<div class="step-panel">
<div class="step-header">
<h3>批量上传图片</h3>
</div>
<div class="data-selection">
<a-form :model="form" layout="vertical">
<!-- 项目选择 -->
<a-form-item label="所属项目" required>
<a-select
v-model="form.projectId"
placeholder="请选择项目"
allow-search
:filter-option="filterProjectOption"
>
<a-option
v-for="project in projectList"
:key="project.id"
:value="project.id"
:label="project.name"
/>
</a-select>
</a-form-item>
<!-- 图片来源选择 -->
<a-form-item label="图片来源" required>
<a-select
v-model="form.imageSource"
placeholder="请选择图片来源"
allow-search
:filter-option="filterSourceOption"
>
<a-option
v-for="source in imageSources"
:key="source.value"
:value="source.value"
:label="source.label"
/>
</a-select>
</a-form-item>
<!-- 文件夹操作 -->
<a-form-item label="文件操作">
<div class="folder-actions">
<a-upload
ref="uploadRef"
directory
:multiple="true"
:show-file-list="false"
accept="image/*"
:key="uploadKey"
@change="handleFolderSelect"
>
<template #upload-button>
<a-button type="outline">
<template #icon>
<icon-folder />
</template>
选择文件夹
</a-button>
</template>
</a-upload>
<a-button
type="outline"
status="warning"
@click="clearFileList"
:disabled="selectedFiles.length === 0"
>
<template #icon>
<icon-delete />
</template>
清空列表
</a-button>
<a-button
type="primary"
:loading="uploading"
:disabled="!canUpload"
@click="handleUpload"
>
<template #icon>
<icon-upload />
</template>
开始上传
</a-button>
</div>
</a-form-item>
<!-- 文件列表 -->
<a-form-item v-if="selectedFiles.length > 0">
<div class="file-list-container">
<div class="file-list-header">
<span>已选择 {{ selectedFiles.length }} 个文件选中 {{ checkedFiles.length }} </span>
<a-checkbox
v-model="selectAll"
:indeterminate="indeterminate"
@change="handleSelectAllChange"
>
全选
</a-checkbox>
</div>
<div class="file-table-wrapper">
<a-table
:data="selectedFiles"
:columns="fileColumns"
:row-selection="rowSelection"
:pagination="false"
row-key="uid"
size="small"
bordered
>
<!-- 表格列模板 -->
<template #thumbnail="{ record }">
<div class="thumbnail-cell">
<img
v-if="isImage(record.type)"
:src="record.preview"
class="thumbnail-image"
alt="预览"
/>
<div v-else class="file-icon">
<icon-file />
</div>
</div>
</template>
<template #fileType="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<template #fileSize="{ record }">
{{ formatFileSize(record.size) }}
</template>
</a-table>
</div>
</div>
</a-form-item>
</a-form>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { UploadItem } from '@arco-design/web-vue/es/upload'
import type { SelectOptionData, TableColumnData, TableRowSelection } from '@arco-design/web-vue'
import axios from 'axios'
import {
getProjectList,
getImageSources
} from '@/apis/industrial-image'
//
interface FileItem {
uid: string
name: string
type: string
size: number
file: File
preview?: string
}
type UploadStatus = 'waiting' | 'uploading' | 'success' | 'error'
//
const projectList = ref([])
const imageSources = ref([])
//
const fetchProjectList = async () => {
try {
const res = await getProjectList({ page: 1, pageSize: 1000 });
projectList.value = res.data.map(item => ({
name: item.projectName,
id: item.projectId
}))
} catch (error) {
Message.error('获取项目列表失败')
} finally {
}
}
//
const fetchImageSourceList = async () => {
try {
const res = await getImageSources();
res.data.forEach(item => {
const key = Object.keys(item)[0];
const value = item[key];
imageSources.value.push({
label: value,
value:key
});
});
} catch (error) {
Message.error('获取项目列表失败')
} finally {
}
}
const form = reactive({
projectId: undefined as number | undefined,
imageSource: undefined as string | undefined
})
const selectedFiles = ref<FileItem[]>([])
const checkedFiles = ref<string[]>([])
const uploading = ref(false)
const uploadRef = ref()
const uploadKey = ref(0)
const uploadState = reactive({
visible: false,
percent: 0,
status: 'waiting' as UploadStatus,
currentFile: '',
uploadedCount: 0,
totalCount: 0
})
//
const canUpload = computed(() => {
return checkedFiles.value.length > 0
&& !!form.projectId
&& !!form.imageSource
&& !uploading.value
})
const selectAll = ref(false)
const indeterminate = computed(() => {
return checkedFiles.value.length > 0 &&
checkedFiles.value.length < selectedFiles.value.length
})
//
const fileColumns: TableColumnData[] = [
{
title: '选择',
dataIndex: 'selection',
type: 'selection',
width: 60,
align: 'center'
},
{
title: '预览',
dataIndex: 'thumbnail',
slotName: 'thumbnail',
width: 100,
align: 'center'
},
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
tooltip: true,
width: 300
},
{
title: '类型',
dataIndex: 'type',
slotName: 'fileType',
width: 100,
align: 'center'
},
{
title: '大小',
dataIndex: 'size',
slotName: 'fileSize',
width: 100,
align: 'center'
}
]
//
const rowSelection = reactive<TableRowSelection>({
type: 'checkbox',
showCheckedAll: false,
selectedRowKeys: checkedFiles,
onChange: (rowKeys: string[]) => {
checkedFiles.value = rowKeys
selectAll.value = rowKeys.length === selectedFiles.value.length
}
})
//
const filterProjectOption = (inputValue: string, option: SelectOptionData) => {
return option.label.toLowerCase().includes(inputValue.toLowerCase())
}
const filterSourceOption = (inputValue: string, option: SelectOptionData) => {
return option.label.toLowerCase().includes(inputValue.toLowerCase())
}
//
const handleFolderSelect = async (fileList: UploadItem[]) => {
// 1.
clearFileList()
// 2.
const newFiles: FileItem[] = []
for (const item of fileList) {
const file = item.file
if (!file) continue
const fileType = file.type.split('/')[0] || 'unknown'
const preview = fileType === 'image' ? URL.createObjectURL(file) : undefined
newFiles.push({
uid: item.uid,
name: file.name,
type: fileType,
size: file.size,
file: file,
preview: preview
})
}
// 3.
selectedFiles.value = newFiles
checkedFiles.value = newFiles.map(f => f.uid)
selectAll.value = true
// 4. key
uploadKey.value++
}
//
const clearFileList = () => {
// URL
selectedFiles.value.forEach(file => {
if (file.preview) URL.revokeObjectURL(file.preview)
})
//
selectedFiles.value = []
checkedFiles.value = []
selectAll.value = false
}
//
const handleSelectAllChange = (checked: boolean) => {
checkedFiles.value = checked ? selectedFiles.value.map(f => f.uid) : []
}
//
const handleUpload = async () => {
if (!canUpload.value) {
Message.error('请完成所有必填项并选择文件')
return
}
//
Object.assign(uploadState, {
visible: true,
percent: 0,
status: 'uploading',
currentFile: '',
uploadedCount: 0,
totalCount: checkedFiles.value.length
})
uploading.value = true
try {
const filesToUpload = selectedFiles.value.filter(f => checkedFiles.value.includes(f.uid))
const formData = new FormData()
// FormData
filesToUpload.forEach(file => {
formData.append('files', file);
});
let url =`http://pms.dtyx.net:9158/image/${form.projectId}/${form.imageSource}/upload-batch`;
let res = await axios.post(
url,
formData,
{
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
uploadedBytes = progressEvent.loaded
const elapsedTime = (Date.now() - startTime) / 1000
const speed = uploadedBytes / elapsedTime
uploadState.speed = `${formatFileSize(speed)}/s`
const remainingBytes = totalBytes - uploadedBytes
uploadState.remainingTime = formatTime(remainingBytes / speed)
uploadState.percent = Math.round((uploadedBytes / totalBytes) * 100)
}
},
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
console.log("res:",res);
uploadState.status = 'success'
Message.success(`成功上传 ${filesToUpload.length} 个文件`)
} catch (error) {
uploadState.status = 'error'
Message.error('上传失败: ' + (error as Error).message)
} finally {
uploading.value = false
// 5
setTimeout(() => uploadState.visible = false, 5000)
}
}
//
const getStatusText = (status: UploadStatus) => {
const statusMap = {
waiting: '等待上传',
uploading: '上传中',
success: '上传成功',
error: '上传失败'
}
return statusMap[status] || status
}
const getFileTypeColor = (type: string) => {
const colors: Record<string, string> = {
image: 'arcoblue',
video: 'green',
audio: 'orange',
document: 'purple'
}
return colors[type.toLowerCase()] || 'gray'
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const isImage = (type: string) => type === 'image'
//
onMounted(() => {
fetchProjectList()
fetchImageSourceList()
})
</script>
<style scoped lang="less">
.data-preprocessing-container {
position: relative;
padding: 20px;
min-height: calc(100vh - 40px);
}
.step-panel {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.step-header {
margin-bottom: 24px;
border-bottom: 1px solid var(--color-border);
padding-bottom: 16px;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
}
.folder-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
> * {
flex-shrink: 0;
}
}
.file-list-container {
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 12px;
margin-top: 16px;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 0 8px;
}
.file-table-wrapper {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 4px;
}
.file-table {
width: 100%;
:deep(.arco-table-th) {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--color-fill-2);
}
}
.thumbnail-cell {
display: flex;
justify-content: center;
align-items: center;
height: 60px;
.thumbnail-image {
max-height: 60px;
max-width: 80px;
object-fit: contain;
border-radius: 2px;
}
.file-icon {
font-size: 24px;
color: var(--color-text-3);
}
}
/* 上传进度框样式 */
.upload-progress-fixed {
position: fixed;
bottom: 24px;
right: 24px;
width: 360px;
z-index: 1000;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border-radius: 8px;
animation: fadeIn 0.3s ease;
:deep(.arco-card-header) {
border-bottom: 1px solid var(--color-border);
padding-bottom: 12px;
}
:deep(.arco-progress-text) {
font-size: 14px;
}
}
.progress-details {
margin-top: 12px;
font-size: 13px;
color: var(--color-text-2);
p {
display: flex;
align-items: center;
margin: 6px 0;
.arco-icon {
margin-right: 8px;
font-size: 16px;
}
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -12,6 +12,14 @@
<a-form-item label="缺陷名称" field="defectName">
<a-input v-model="form.defectName" placeholder="请输入缺陷名称" />
</a-form-item>
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!-- 缺陷类型 -->
<a-form-item
@ -27,19 +35,47 @@
<a-option v-for="type in defectTypes" :key="type.code" :value="type.code">{{ type.label }}</a-option>
</a-select>
</a-form-item>
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!--轴向尺寸-->
<div class="dimension-fields">
<a-form-item
field="axialDimension"
label="轴向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.axialDimension"
:min="0"
:step="1"
placeholder="请输入轴向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
<!--弦向尺寸 -->
<a-form-item
field="chordDimension"
label="弦向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.chordDimension"
:min="0"
:step="1"
placeholder="请输入弦向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
</div>
<!-- 缺陷等级 -->
<a-form-item
field="defectLevel"
label="缺陷等级"
label="严重程度"
:rules="[{ required: true, message: '请选择缺陷等级' }]"
>
<a-select
@ -104,6 +140,8 @@ interface DefectFormData {
defectPosition: string
description: string
repairIdea: string
axialDimension: number | 0
chordDimension: number | 0
}
const props = defineProps<{
@ -135,7 +173,9 @@ const form = reactive<DefectFormData>({
defectLevelLabel: '',
defectPosition: '',
description: '',
repairIdea: '建议进行进一步检查'
repairIdea: '建议进行进一步检查',
axialDimension: 0, // null
chordDimension: 0 // null
})
//
@ -326,6 +366,7 @@ const handleSubmit = async () => {
}
//
console.log("form:",form);
emit('submit', form, props.annotation)
} catch (error) {
@ -434,6 +475,25 @@ onMounted(() => {
}
}
.dimension-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
:deep(.arco-input-number) {
width: 100%;
.arco-input-number-step {
width: 32px;
}
.arco-input-number-input {
text-align: center;
}
}
}
:deep(.arco-form-item) {
margin-bottom: 16px;
}

View File

@ -0,0 +1,242 @@
<template>
<a-modal
:visible="previewModalVisible"
title="图像详情"
width="80%"
:footer="footerButtons"
:mask-closable="true"
@update:visible="emit('update:previewModalVisible', $event)"
:confirm-loading="loading"
>
<div v-if="previewImage" class="modal-image-viewer">
<img :src="getImageUrl(previewImage.imagePath)" :alt="editingData.imageName" class="preview-image" />
<div class="image-details">
<a-form layout="vertical" :model="editingData">
<a-form-item label="图片名称" field="imageName">
<a-input v-model="editingData.imageName" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="大小" field="size">
<a-input :model-value="formatFileSize(editingData.size || 0)" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="尺寸" field="imageResolution">
<a-input v-model="editingData.imageResolution" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="采集员ID" field="collectId">
<a-input v-model="editingData.collectId" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="采集员姓名" field="collectorName">
<a-input v-model="editingData.collectorName" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="来源" field="imageSource">
<a-input v-model="editingData.imageSource" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="图片类型" field="imageType">
<a-select v-model="editingData.imageType">
<a-option value="DEFECT">缺陷影像</a-option>
<a-option value="TYPICAL">典型影像</a-option>
<a-option value="OTHER">其他影像</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="图片类型描述" field="imageTypeLabel">
<a-textarea
v-model="editingData.imageTypeLabel"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-form>
</div>
</div>
<template #footer>
<a-button @click="emit('update:previewModalVisible', false)">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleSave">保存</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import axios from 'axios'
const props = defineProps<{
previewModalVisible: boolean
previewImage: {
imagePath: string
imageName: string
size?: number
imageResolution?: string
collectId?: string
collectorName?: string
imageSource?: string
imageType?: string
imageTypeLabel?: string
partId?: string
} | null
selectedImage: any | null
}>()
const emit = defineEmits<{
'update:previewModalVisible': [visible: boolean]
'save': [data: any]
}>()
const loading = ref(false)
const editingData = ref<{
imagePath: string
imageName: string
size?: number
imageResolution?: string
collectId?: string
collectorName?: string
imageSource?: string
imageType?: string
imageTypeLabel?: string
partId?: string
imageId?:string
}>({
imagePath: '',
imageName: '',
size: 0,
imageResolution: '',
collectId: '',
collectorName: '',
imageSource: '',
imageType: '',
imageTypeLabel: '',
partId: '',
imageId:''
})
//
watch(() => props.previewImage, (newVal) => {
if (newVal) {
editingData.value = { ...newVal }
}
}, { immediate: true, deep: true })
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
}
const formatFileSize = (size: number): string => {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
const handleSave = async () => {
if (!editingData.value.imageName?.trim()) {
Message.error('图片名称不能为空')
return
}
if (!editingData.value.partId) {
Message.error('缺少部件ID无法保存')
return
}
try {
loading.value = true
//
const requestData = {
collectId: editingData.value.collectId,
collectorName: editingData.value.collectorName,
imageSource: editingData.value.imageSource,
imageType: editingData.value.imageType,
imageTypeLabel: editingData.value.imageTypeLabel,
imageList: [
{
imageId: editingData.value.imageId,
imageName: editingData.value.imageName,
imageResolution: editingData.value.imageResolution,
}
],
}
console.log("requestData:",requestData);
//
const response = await axios.post(
`http://pms.dtyx.net:9158/image/setting-info/${editingData.value.partId}`,
requestData
)
if (response.data && response.data.code === 200 && response.data.success) {
Message.success('图片信息保存成功')
emit('save', editingData.value)
emit('update:previewModalVisible', false)
} else {
Message.error(response.data?.message || '保存失败')
}
} catch (error) {
console.error('保存图片信息失败:', error)
Message.error('保存失败,请稍后重试')
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
.modal-image-viewer {
display: flex;
gap: 24px;
align-items: flex-start;
.preview-image {
flex: 1;
max-width: 60%;
max-height: 500px;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.image-details {
flex: 1;
padding: 16px;
:deep(.arco-form) {
.arco-form-item {
margin-bottom: 16px;
}
}
}
}
@media (max-width: 768px) {
.modal-image-viewer {
flex-direction: column;
.preview-image {
max-width: 100%;
}
}
}
</style>

View File

@ -349,6 +349,7 @@ export function useIndustrialImage() {
}
const handleImagePreview = (image: any) => {
console.log("image:",image);
previewImage.value = image
previewModalVisible.value = true
}

View File

@ -145,7 +145,14 @@
</div>
<!-- 模态框 -->
<ImageModals
:preview-modal-visible="previewModalVisible"
:preview-image="previewImage"
:process-image="processImage"
:selected-image="selectedImage"
@update:preview-modal-visible="previewModalVisible = $event"
@save="handleSaveImageSuccess"
/>
</div>
</template>
@ -158,6 +165,7 @@ import ProjectTree from './components/ProjectTree.vue'
import ImagePreview from './components/ImagePreview.vue'
import ImageCanvas from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import IndustrialImageList from '@/components/IndustrialImageList/index.vue'
import ImageModals from './components/ImageModals.vue'
import RecognitionResults from './components/RecognitionResults.vue'
import DefectListPanel from './components/DefectListPanel.vue'
import DefectDetailsForm from './components/DefectDetailsForm.vue'
@ -212,6 +220,19 @@ const loadDefectLevels = async () => {
}
}
//
const handleSaveImageSuccess = (updatedData: any) => {
//
Message.success('图片信息已更新')
// selectedImagepreviewImage
if (selectedImage.value && selectedImage.value.id === updatedData.id) {
selectedImage.value = { ...selectedImage.value, ...updatedData }
}
if (previewImage.value && previewImage.value.id === updatedData.id) {
previewImage.value = { ...previewImage.value, ...updatedData }
}
}
// 使
const {
//
@ -594,8 +615,8 @@ const handleDefectFormSubmit = async (formData: any, annotation: Annotation) =>
const defectData = {
attachId: attachId,
attachPath: '',
// axial: 0,
// chordwise: 0,
axial: formData.axialDimension,
chordwise: formData.chordDimension,
// defectCode: `MANUAL_${Date.now()}`,
defectId: '',
defectLevel: formData.defectLevel,

View File

@ -38,7 +38,7 @@ const IconMap: Record<number, Component> = {
const router = useRouter()
//
const back = () => {
router.replace({ path: '/project-management/bidding/tender-documents' })
router.replace({ path: '/asset-management/intellectual-property' })
}
</script>
@ -58,6 +58,7 @@ const back = () => {
flex-direction: column;
align-items: center;
}
&__img {
width: 100%;
position: relative;
@ -66,14 +67,17 @@ const back = () => {
justify-content: center;
align-items: center;
}
&__icon {
max-width: 90%;
height: 50vh;
}
&__tip {
display: flex;
flex-direction: column;
align-items: center;
&--a {
margin-bottom: 20px;
font-size: 32px;
@ -85,6 +89,7 @@ const back = () => {
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&--b {
margin-bottom: 10px;
font-size: 20px;
@ -97,6 +102,7 @@ const back = () => {
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&--c {
padding: 0 30px;
margin-bottom: 20px;
@ -112,11 +118,13 @@ const back = () => {
}
}
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);

View File

@ -323,138 +323,138 @@
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
// 访
// 访
if (appVisitChart.value) {
const visitChart = echarts.init(appVisitChart.value)
visitChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Web端', '移动端', '小程序']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'Web端',
type: 'line',
data: [2500, 2800, 3200, 3100, 2950, 1800, 1200]
},
{
name: '移动端',
type: 'line',
data: [3200, 3500, 3800, 3600, 3400, 2800, 2500]
},
{
name: '小程序',
type: 'line',
data: [4500, 4800, 5200, 4900, 4700, 3900, 3500]
}
]
})
const visitChart = echarts.init(appVisitChart.value)
visitChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Web端', '移动端', '小程序']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'Web端',
type: 'line',
data: [2500, 2800, 3200, 3100, 2950, 1800, 1200]
},
{
name: '移动端',
type: 'line',
data: [3200, 3500, 3800, 3600, 3400, 2800, 2500]
},
{
name: '小程序',
type: 'line',
data: [4500, 4800, 5200, 4900, 4700, 3900, 3500]
}
]
})
}
// 使
// 使
if (appTimeChart.value) {
const timeChart = echarts.init(appTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '使用时长分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: 'Web端' },
{ value: 45, name: '移动端' },
{ value: 20, name: '小程序' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
const timeChart = echarts.init(appTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '使用时长分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: 'Web端' },
{ value: 45, name: '移动端' },
{ value: 20, name: '小程序' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
]
})
}
}
]
})
}
//
//
if (deviceDistributionChart.value) {
const deviceChart = echarts.init(deviceDistributionChart.value)
deviceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
const deviceChart = echarts.init(deviceDistributionChart.value)
deviceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['Windows PC', 'Mac', 'iOS', 'Android', '微信']
},
series: [
{
name: '访问量',
type: 'bar',
stack: 'total',
label: {
show: true
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
emphasis: {
focus: 'series'
},
xAxis: {
type: 'value'
data: [5200, 3800, 6500, 8200, 9500]
},
{
name: '用户数',
type: 'bar',
stack: 'total',
label: {
show: true
},
yAxis: {
type: 'category',
data: ['Windows PC', 'Mac', 'iOS', 'Android', '微信']
emphasis: {
focus: 'series'
},
series: [
{
name: '访问量',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [5200, 3800, 6500, 8200, 9500]
},
{
name: '用户数',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [280, 220, 320, 380, 420]
}
]
})
data: [280, 220, 320, 380, 420]
}
]
})
}
//
window.addEventListener('resize', () => {
//
window.addEventListener('resize', () => {
if (appVisitChart.value) {
const visitChart = echarts.getInstanceByDom(appVisitChart.value)
visitChart?.resize()

View File

@ -222,135 +222,135 @@
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
// 使
// 使
if (moduleUsageChart.value) {
const moduleChart = echarts.init(moduleUsageChart.value)
moduleChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['使用次数', '使用人数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['组织架构', '资产管理', '产品与服务', '项目管理', '施工操作台', '聊天平台', '企业设置', '系统资源管理']
},
series: [
{
name: '使用次数',
type: 'bar',
data: [2110, 1988, 2106, 2470, 2460, 1420, 720, 650]
},
{
name: '使用人数',
type: 'bar',
data: [320, 302, 315, 335, 340, 356, 120, 85]
}
]
})
const moduleChart = echarts.init(moduleUsageChart.value)
moduleChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['使用次数', '使用人数']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['组织架构', '资产管理', '产品与服务', '项目管理', '施工操作台', '聊天平台', '企业设置', '系统资源管理']
},
series: [
{
name: '使用次数',
type: 'bar',
data: [2110, 1988, 2106, 2470, 2460, 1420, 720, 650]
},
{
name: '使用人数',
type: 'bar',
data: [320, 302, 315, 335, 340, 356, 120, 85]
}
]
})
}
// 使
// 使
if (departmentUsageChart.value) {
const departmentChart = echarts.init(departmentUsageChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '部门使用分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: '技术部' },
{ value: 25, name: '市场部' },
{ value: 20, name: '销售部' },
{ value: 10, name: '人事部' },
{ value: 10, name: '财务部' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
const departmentChart = echarts.init(departmentUsageChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '部门使用分布',
type: 'pie',
radius: '70%',
data: [
{ value: 35, name: '技术部' },
{ value: 25, name: '市场部' },
{ value: 20, name: '销售部' },
{ value: 10, name: '人事部' },
{ value: 10, name: '财务部' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
]
})
}
}
]
})
}
// 使
// 使
if (usageTimeChart.value) {
const timeChart = echarts.init(usageTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
const timeChart = echarts.init(usageTimeChart.value)
timeChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '使用时长占比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
legend: {
orient: 'vertical',
left: 'left'
label: {
show: false,
position: 'center'
},
series: [
{
name: '使用时长占比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 20, name: '组织架构' },
{ value: 15, name: '资产管理' },
{ value: 15, name: '产品与服务' },
{ value: 20, name: '项目管理' },
{ value: 20, name: '施工操作台' },
{ value: 5, name: '聊天平台' },
{ value: 3, name: '企业设置' },
{ value: 2, name: '系统资源管理' }
]
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 20, name: '组织架构' },
{ value: 15, name: '资产管理' },
{ value: 15, name: '产品与服务' },
{ value: 20, name: '项目管理' },
{ value: 20, name: '施工操作台' },
{ value: 5, name: '聊天平台' },
{ value: 3, name: '企业设置' },
{ value: 2, name: '系统资源管理' }
]
})
}
]
})
}
//
window.addEventListener('resize', () => {
//
window.addEventListener('resize', () => {
if (moduleUsageChart.value) {
const moduleChart = echarts.getInstanceByDom(moduleUsageChart.value)
moduleChart?.resize()

View File

@ -322,150 +322,150 @@
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
//
//
if (departmentActivityChart.value) {
const departmentChart = echarts.init(departmentActivityChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value'
},
series: [
{
name: '活跃度',
type: 'bar',
data: [92, 85, 88, 79, 82]
},
{
name: '登录次数',
type: 'bar',
data: [320, 280, 310, 240, 260]
},
{
name: '操作次数',
type: 'bar',
data: [2800, 2100, 2400, 1800, 2000]
}
]
})
const departmentChart = echarts.init(departmentActivityChart.value)
departmentChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value'
},
series: [
{
name: '活跃度',
type: 'bar',
data: [92, 85, 88, 79, 82]
},
{
name: '登录次数',
type: 'bar',
data: [320, 280, 310, 240, 260]
},
{
name: '操作次数',
type: 'bar',
data: [2800, 2100, 2400, 1800, 2000]
}
]
})
}
//
//
if (dailyActiveUsersChart.value) {
const dailyActiveChart = echarts.init(dailyActiveUsersChart.value)
dailyActiveChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 132, 145, 135, 128, 68, 42],
type: 'line',
areaStyle: {}
}
]
})
const dailyActiveChart = echarts.init(dailyActiveUsersChart.value)
dailyActiveChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 132, 145, 135, 128, 68, 42],
type: 'line',
areaStyle: {}
}
]
})
}
// 线
// 线
if (onlineTimeChart.value) {
const onlineChart = echarts.init(onlineTimeChart.value)
onlineChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value} 小时'
}
},
series: [
{
name: '平均在线时长',
type: 'bar',
data: [7.5, 6.8, 7.2, 6.5, 6.9]
}
]
})
const onlineChart = echarts.init(onlineTimeChart.value)
onlineChart.setOption({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['技术部', '市场部', '销售部', '人事部', '财务部']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value} 小时'
}
},
series: [
{
name: '平均在线时长',
type: 'bar',
data: [7.5, 6.8, 7.2, 6.5, 6.9]
}
]
})
}
//
//
if (attendanceTrendChart.value) {
const attendanceChart = echarts.init(attendanceTrendChart.value)
attendanceChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['出勤率', '迟到率', '早退率']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%'
}
},
series: [
{
name: '出勤率',
type: 'line',
data: [96.2, 97.1, 96.8, 97.5, 98.2, 97.8]
},
{
name: '迟到率',
type: 'line',
data: [2.8, 2.2, 2.5, 1.8, 1.2, 1.5]
},
{
name: '早退率',
type: 'line',
data: [1.0, 0.7, 0.7, 0.7, 0.6, 0.7]
}
]
})
const attendanceChart = echarts.init(attendanceTrendChart.value)
attendanceChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['出勤率', '迟到率', '早退率']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}%'
}
},
series: [
{
name: '出勤率',
type: 'line',
data: [96.2, 97.1, 96.8, 97.5, 98.2, 97.8]
},
{
name: '迟到率',
type: 'line',
data: [2.8, 2.2, 2.5, 1.8, 1.2, 1.5]
},
{
name: '早退率',
type: 'line',
data: [1.0, 0.7, 0.7, 0.7, 0.6, 0.7]
}
]
})
}
//
window.addEventListener('resize', () => {
//
window.addEventListener('resize', () => {
if (departmentActivityChart.value) {
const departmentChart = echarts.getInstanceByDom(departmentActivityChart.value)
departmentChart?.resize()

View File

@ -105,139 +105,139 @@
onMounted(() => {
// 使 nextTick DOM
nextTick(() => {
//
//
if (projectProgressChart.value) {
const projectChart = echarts.init(projectProgressChart.value)
projectChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '项目状态',
type: 'pie',
radius: '70%',
data: [
{ value: 48, name: '进行中' },
{ value: 65, name: '已完成' },
{ value: 12, name: '已暂停' },
{ value: 3, name: '已取消' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
const projectChart = echarts.init(projectProgressChart.value)
projectChart.setOption({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '项目状态',
type: 'pie',
radius: '70%',
data: [
{ value: 48, name: '进行中' },
{ value: 65, name: '已完成' },
{ value: 12, name: '已暂停' },
{ value: 3, name: '已取消' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
]
})
}
}
]
})
}
// 使
// 使
if (resourceUsageChart.value) {
const resourceChart = echarts.init(resourceUsageChart.value)
resourceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
const resourceChart = echarts.init(resourceUsageChart.value)
resourceChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['服务器', '存储空间', '带宽', '设备使用率', '人力资源']
},
series: [
{
name: '已使用',
type: 'bar',
stack: 'total',
label: {
show: true
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
emphasis: {
focus: 'series'
},
xAxis: {
type: 'value'
data: [65, 72, 58, 80, 75]
},
{
name: '剩余',
type: 'bar',
stack: 'total',
label: {
show: true
},
yAxis: {
type: 'category',
data: ['服务器', '存储空间', '带宽', '设备使用率', '人力资源']
emphasis: {
focus: 'series'
},
series: [
{
name: '已使用',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [65, 72, 58, 80, 75]
},
{
name: '剩余',
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [35, 28, 42, 20, 25]
}
]
})
data: [35, 28, 42, 20, 25]
}
]
})
}
//
//
if (businessTrendChart.value) {
const businessChart = echarts.init(businessTrendChart.value)
businessChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['项目数量', '营业收入', '新增客户']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '项目数量',
type: 'line',
data: [10, 12, 15, 18, 22, 24]
},
{
name: '营业收入',
type: 'line',
data: [120, 132, 145, 160, 178, 190]
},
{
name: '新增客户',
type: 'line',
data: [5, 7, 8, 10, 12, 15]
}
]
})
const businessChart = echarts.init(businessTrendChart.value)
businessChart.setOption({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['项目数量', '营业收入', '新增客户']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '项目数量',
type: 'line',
data: [10, 12, 15, 18, 22, 24]
},
{
name: '营业收入',
type: 'line',
data: [120, 132, 145, 160, 178, 190]
},
{
name: '新增客户',
type: 'line',
data: [5, 7, 8, 10, 12, 15]
}
]
})
}
//
window.addEventListener('resize', () => {
//
window.addEventListener('resize', () => {
if (projectProgressChart.value) {
const projectChart = echarts.getInstanceByDom(projectProgressChart.value)
projectChart?.resize()

View File

@ -0,0 +1,73 @@
<template>
<GiPageLayout :margin="true">
<template #header>
<h2>新增考勤记录</h2>
</template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
@submit="onSubmit"
>
<a-form-item label="打卡照片" name="recordImage">
<a-input v-model="form.recordImage" placeholder="请输入打卡照片URL" />
<!-- 你可以替换为图片上传组件 -->
</a-form-item>
<a-form-item label="打卡地点(经纬度)" name="recordPosition">
<a-input v-model="form.recordPosition" placeholder="请输入经纬度" />
</a-form-item>
<a-form-item label="打卡地点(中文描述)" name="recordPositionLabel">
<a-input v-model="form.recordPositionLabel" placeholder="请输入地点描述" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading">提交</a-button>
</a-form-item>
</a-form>
<a-alert v-if="resultMsg" :type="resultType" :show-icon="true" style="margin-top: 16px;">
{{ resultMsg }}
</a-alert>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addAttendanceRecord } from '@/apis/attendance-record/index'
import type { AttendanceRecordReq } from '@/apis/attendance-record/type'
const formRef = ref()
const form = ref<AttendanceRecordReq>({
recordImage: '',
recordPosition: '',
recordPositionLabel: '',
})
const rules = {
//
}
const loading = ref(false)
const resultMsg = ref('')
const resultType = ref<'success' | 'error'>('success')
const onSubmit = async () => {
loading.value = true
resultMsg.value = ''
try {
const res = await addAttendanceRecord(form.value)
if (res.success) {
resultMsg.value = '考勤记录提交成功'
resultType.value = 'success'
form.value = { recordImage: '', recordPosition: '', recordPositionLabel: '' }
} else {
resultMsg.value = res.msg || '提交失败'
resultType.value = 'error'
}
} catch (e: any) {
resultMsg.value = e?.message || '提交异常'
resultType.value = 'error'
} finally {
loading.value = false
}
}
</script>

View File

@ -1,3 +1,4 @@
<!-- 考勤统计 -->
<template>
<GiPageLayout>
<GiTable

View File

@ -15,30 +15,15 @@
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item label="证书名称" field="certificationName">
<a-input
v-model="searchForm.certificationName"
placeholder="请输入证书名称"
allow-clear
style="width: 200px"
/>
<a-input v-model="searchForm.certificationName" placeholder="请输入证书名称" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item label="证书类型" field="certificationType">
<a-input
v-model="searchForm.certificationType"
placeholder="请输入证书类型"
allow-clear
style="width: 200px"
/>
<a-input v-model="searchForm.certificationType" placeholder="请输入证书类型" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item label="用户姓名" field="userName">
<a-input
v-model="searchForm.userName"
placeholder="请输入用户姓名"
allow-clear
style="width: 200px"
/>
<a-input v-model="searchForm.userName" placeholder="请输入用户姓名" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item>
@ -62,28 +47,15 @@
<!-- 人员资质表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="certificationList"
:pagination="paginationConfig"
:loading="loading"
row-key="certificationId"
@page-change="handlePageChange"
>
<a-table :columns="columns" :data="certificationList" :pagination="paginationConfig" :loading="loading"
row-key="certificationId" @page-change="handlePageChange">
<template #userName="{ record }">
<span>{{ getUserName(record.userId) }}</span>
</template>
<template #certificationImage="{ record }">
<a-image
v-if="record.certificationImage"
:src="record.certificationImage"
width="60"
height="40"
fit="cover"
show-loader
preview
/>
<a-image v-if="record.certificationImage" :src="record.certificationImage" width="60" height="40"
fit="cover" show-loader preview />
<span v-else>-</span>
</template>
@ -105,19 +77,9 @@
</a-card>
<!-- 新增/编辑资质信息模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑资质信息' : '新增资质信息'"
width="700px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-modal v-model:visible="modalVisible" :title="isEdit ? '编辑资质信息' : '新增资质信息'" width="700px" @ok="handleSubmit"
@cancel="handleCancel">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书编号" field="certificationCode">
@ -132,75 +94,47 @@
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书类型" field="certificationType">
<a-input v-model="formData.certificationType" placeholder="请输入证书类型" />
</a-form-item>
</a-col>
<a-form-item label="证书类型" field="certificationType">
<a-select v-model="formData.certificationType" placeholder="请选择证书类型">
<a-option v-for="item in CERTIFICATION_TYPE_OPTIONS" :key="item.value" :value="item.value"
:label="item.label">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
<a-col :span="12">
<a-form-item label="持证人" field="userId">
<a-select
v-model="formData.userId"
placeholder="请选择持证人"
:loading="loadingUsers"
allow-search
:filter-option="filterUserOption"
>
<a-option
v-for="user in userList"
:key="user.userId"
:value="user.userId"
:label="user.name"
>
{{ user.name }}({{ user.account }})
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-col :span="12">
<a-form-item label="持证人" field="userId">
<a-select v-model="formData.userId" placeholder="请选择持证人" :loading="loadingUsers" allow-search
:filter-option="filterUserOption" @="console.log('d', formData)">
<a-option v-for="user in userList" :key="user.userId" :value="user.userId" :label="user.name">
{{ user.name }}({{ user.account }})
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="有效期开始" field="validityDateBegin">
<a-date-picker
v-model="formData.validityDateBegin"
placeholder="请选择有效期开始日期"
style="width: 100%"
/>
<a-date-picker v-model="formData.validityDateBegin" placeholder="请选择有效期开始日期" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="有效期结束" field="validityDateEnd">
<a-date-picker
v-model="formData.validityDateEnd"
placeholder="请选择有效期结束日期"
style="width: 100%"
/>
<a-date-picker v-model="formData.validityDateEnd" placeholder="请选择有效期结束日期" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="证书图片" field="certificationImage">
<a-upload
:custom-request="handleUpload"
:show-file-list="false"
accept="image/*"
:before-upload="beforeUpload"
>
<a-upload :custom-request="handleUpload" :show-file-list="false" accept="image/*"
:before-upload="beforeUpload">
<template #upload-button>
<div class="upload-wrapper">
<a-image
v-if="formData.certificationImage"
:src="formData.certificationImage"
width="200"
height="120"
fit="cover"
show-loader
preview
/>
<a-image v-if="formData.certificationImage" :src="formData.certificationImage" width="200"
height="120" fit="cover" show-loader preview />
<div v-else class="upload-placeholder">
<icon-plus />
<div>点击上传证书图片</div>
@ -233,7 +167,7 @@ const isEdit = ref(false)
const formRef = ref()
//
const certificationList = ref<CertificationInfo[]>([])
const certificationList = ref<CertificationAPI.CertificationListResponse>()
const userList = ref<SimpleUserInfo[]>([])
//
@ -254,14 +188,14 @@ const paginationConfig = reactive({
//
const formData = reactive<CertificationInfo>({
certificationId: '',
certificationCode: '',
certificationImage: '',
certificationImage: 'n',
certificationName: '',
certificationType: '',
userId: '',
validityDateBegin: '',
validityDateEnd: ''
validityDateEnd: '',
// certificationTypeLabel: ''
})
//
@ -285,7 +219,15 @@ const formRules = {
{ required: true, message: '请选择有效期结束日期' }
]
}
const CERTIFICATION_TYPE_OPTIONS = [
{ label: '高处作业', value: 'height-operation' },
{ label: '低压电工', value: 'low-voltage-operation' },
{ label: '高压电工', value: 'high-voltage-operation' },
{ label: '海上交通安全', value: 'maritime-traffic-safety' },
{ label: '驾驶证', value: 'driving-license' },
{ label: '防雷检测', value: 'lightning-protection-detection' },
{ label: '无人机驾驶', value: 'drone-driving' }
]
//
const columns = [
{
@ -301,11 +243,15 @@ const columns = [
{
title: '证书类型',
dataIndex: 'certificationType',
width: 150
width: 150,
render: ({ record }) => {
const type = CERTIFICATION_TYPE_OPTIONS.find(item => item.value === record.certificationType)
return type ? type.label : '-'
}
},
{
title: '持证人',
dataIndex: 'userId',
dataIndex: 'userName',
slotName: 'userName',
width: 120
},
@ -339,10 +285,10 @@ const getUserName = (userId: string) => {
const filterUserOption = (inputValue: string, option: any) => {
const user = userList.value.find(u => u.userId === option.value)
if (!user) return false
const searchText = inputValue.toLowerCase()
return user.name.toLowerCase().includes(searchText) ||
user.account.toLowerCase().includes(searchText)
return user.name.toLowerCase().includes(searchText) ||
user.account.toLowerCase().includes(searchText)
}
//
@ -350,11 +296,16 @@ const getUserList = async () => {
try {
loadingUsers.value = true
const response = await CertificationAPI.getUserList()
console.log('用户列表响应:', response)
if (response.data) {
userList.value = response.data || []
userList.value = response.data.map(item => ({
userId: item.userId,
name: item.name,
account: item.account,
})) || []
console.log('用户列表响应2:', userList.value)
}
} catch (error) {
console.error('获取用户列表失败:', error)
@ -370,20 +321,15 @@ const getCertificationList = async () => {
loading.value = true
const params: CertificationListParams = {
...searchForm,
current: paginationConfig.current,
size: paginationConfig.pageSize
}
const response = await CertificationAPI.getCertificationList(params)
console.log('资质信息列表响应:', response)
if (response.data) {
certificationList.value = response.data.records || []
certificationList.value = response.data || []
paginationConfig.total = response.data.total || 0
console.log('cc', certificationList.value)
console.log('cd', response.data[0])
}
} catch (error) {
console.error('获取资质信息列表失败:', error)
Message.error('获取资质信息列表失败')
} finally {
loading.value = false
@ -397,13 +343,13 @@ const beforeUpload = (file: File) => {
Message.error('只能上传图片文件')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
Message.error('图片大小不能超过 2MB')
return false
}
return true
}
@ -412,9 +358,9 @@ const handleUpload = async (option: any) => {
try {
const uploadFormData = new FormData()
uploadFormData.append('file', option.fileItem.file)
const response = await uploadFile(uploadFormData)
if (response.data && response.data.url) {
formData.certificationImage = response.data.url
Message.success('图片上传成功')
@ -491,7 +437,7 @@ const deleteRecord = (record: CertificationInfo) => {
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (isEdit.value) {
await CertificationAPI.updateCertification(formData.certificationId!, formData)
Message.success('更新成功')
@ -499,7 +445,7 @@ const handleSubmit = async () => {
await CertificationAPI.createCertification(formData)
Message.success('创建成功')
}
modalVisible.value = false
resetForm()
await getCertificationList()
@ -598,4 +544,4 @@ onMounted(() => {
font-size: 24px;
margin-bottom: 8px;
}
</style>
</style>

View File

@ -1,8 +1,10 @@
<!--任务管理-->
<template>
<GiPageLayout>
<a-button type="primary" style="margin-bottom: 16px" @click="openAddModal">发布任务</a-button>
<GiTable
row-key="id"
title="工作量管理"
row-key="taskId"
title="任务记录"
:data="dataList"
:columns="tableColumns"
:loading="loading"
@ -11,38 +13,74 @@
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
@row-click="onRowClick"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增工作量记录</template>
</a-button>
<template #status="{ record }">
<a-tag :color="getTaskStatusColor(record.status)">{{ getTaskStatusText(record.status) }}</a-tag>
</template>
<!-- 工作量显示 -->
<template #workload="{ record }">
<span class="font-medium text-blue-600">{{ record.workload }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link status="danger" @click="deleteRecord(record)">删除</a-link>
<a-link @click.stop="onRowClick(record)">详情</a-link>
<a-link status="danger" @click.stop="deleteRecord(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 发布任务弹窗 -->
<a-modal v-model:visible="showAddModal" title="发布任务" @ok="handleAddTask" @cancel="showAddModal = false">
<a-form :model="addForm" label-width="90px">
<a-form-item label="任务ID">
<a-input v-model="addForm.taskId" disabled />
</a-form-item>
<a-form-item label="任务描述">
<a-input v-model="addForm.remark" placeholder="请输入任务描述" />
</a-form-item>
<a-form-item label="创建时间">
<a-input v-model="addForm.createTime" disabled />
</a-form-item>
</a-form>
</a-modal>
<!-- 任务详情弹窗 -->
<a-modal v-model:visible="showDetailModal" title="任务详情" @ok="handleUpdateTask" @cancel="showDetailModal = false" :footer="false">
<a-form :model="detailForm" label-width="90px">
<a-form-item label="任务ID">
<a-input v-model="detailForm.taskId" disabled />
</a-form-item>
<a-form-item label="任务描述">
<a-input v-model="detailForm.remark" />
</a-form-item>
<a-form-item label="分配岗位">
<a-select v-model="detailForm.deptName" :options="deptOptions" allow-clear placeholder="请选择岗位" />
</a-form-item>
<a-form-item label="分配人员">
<a-select v-model="detailForm.mainUserId" :options="userOptions" allow-clear placeholder="请选择人员" />
</a-form-item>
<a-form-item label="任务状态">
<a-input :value="getTaskStatusText(detailForm.status)" disabled />
</a-form-item>
<a-form-item label="创建时间">
<a-input v-model="detailForm.createTime" disabled />
</a-form-item>
<a-form-item label="完成时间">
<a-input v-model="detailForm.finishTime" disabled />
</a-form-item>
</a-form>
<template #footer>
<a-space style="float: right">
<a-button status="danger" @click="handleDeleteTask">删除</a-button>
<a-button type="primary" @click="handleUpdateTask">确定</a-button>
</a-space>
</template>
</a-modal>
</GiPageLayout>
</template>
@ -50,13 +88,52 @@
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
// import { listTask } from '@/apis/project/task' //
//
// 0123
const statusOptions = [
{ label: '未分配', value: 0 },
{ label: '已分配', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已取消', value: 3 }
]
const getTaskStatusColor = (status: number) => {
const colorMap: Record<number, string> = {
0: 'gray',
1: 'blue',
2: 'green',
3: 'red'
}
return colorMap[status] || 'gray'
}
const getTaskStatusText = (status: number) => {
const textMap: Record<number, string> = {
0: '未分配',
1: '已分配',
2: '已完成',
3: '已取消'
}
return textMap[status] || status
}
//
const deptOptions = [
{ label: '开发', value: '开发' },
{ label: '测试', value: '测试' },
{ label: '运维', value: '运维' }
]
const userOptions = [
{ label: '张三', value: '张三' },
{ label: '李四', value: '李四' },
{ label: '王五', value: '王五' }
]
//
let searchForm = reactive({
userName: '',
projectName: '',
startDate: '',
endDate: '',
taskName: '',
responsiblePerson: '', // mainUserId
status: '',
page: 1,
size: 10
})
@ -64,119 +141,241 @@ let searchForm = reactive({
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
field: 'taskName',
label: '任务标题',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
placeholder: '请输入任务标题',
style: { width: '200px' }
}
},
{
field: 'projectName',
label: '项目名称',
field: 'responsiblePerson',
label: '分配用户',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
placeholder: '请输入分配用户',
style: { width: '200px' }
}
},
{
field: 'status',
label: '任务状态',
type: 'select' as const,
props: {
placeholder: '请选择任务状态',
options: statusOptions,
style: { width: '200px' }
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '项目名称', dataIndex: 'projectName', width: 200, ellipsis: true, tooltip: true },
{ title: '工作内容', dataIndex: 'workContent', width: 250, ellipsis: true, tooltip: true },
{ title: '工作量(小时)', dataIndex: 'workload', slotName: 'workload', width: 120 },
{ title: '工作日期', dataIndex: 'workDate', width: 120 },
{ title: '任务ID', dataIndex: 'taskId', width: 80 },
{ title: '任务标题', dataIndex: 'taskName', width: 200, ellipsis: true, tooltip: true },
{ title: '分配用户', dataIndex: 'mainUserId', width: 120 },
{ title: '分配部门', dataIndex: 'deptName', width: 120 },
{ title: '任务内容', dataIndex: 'remark', width: 250, ellipsis: true, tooltip: true },
{ title: '任务状态', dataIndex: 'status', slotName: 'status', width: 120 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
deptName: '技术部',
projectName: '企业管理系统',
workContent: '前端开发',
workload: 8,
workDate: '2024-01-15',
createTime: '2024-01-15 10:30:00'
},
{
id: 2,
userName: '李四',
deptName: '技术部',
projectName: '移动端应用',
workContent: '后端接口开发',
workload: 6,
workDate: '2024-01-15',
createTime: '2024-01-15 11:20:00'
}
])
const dataList = ref<any[]>([])
const allData = ref<any[]>([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 2,
total: 0,
showTotal: true,
showPageSize: true
})
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
//
const showAddModal = ref(false)
const addForm = reactive({
taskId: '',
remark: '',
createTime: ''
})
const openAddModal = () => {
addForm.taskId = Date.now().toString()
addForm.remark = ''
addForm.createTime = new Date().toLocaleString()
showAddModal.value = true
}
const handleAddTask = () => {
if (!addForm.remark) {
Message.warning('请输入任务描述')
return
}
const newTask = {
taskId: addForm.taskId,
taskName: addForm.remark,
mainUserId: '',
deptName: '',
remark: addForm.remark,
status: 0, //
createTime: addForm.createTime,
finishTime: ''
}
allData.value.unshift(newTask)
filterAndPaginate()
showAddModal.value = false
Message.success('任务发布成功')
}
const reset = () => {
//
const showDetailModal = ref(false)
const detailForm = reactive({
taskId: '',
taskName: '',
mainUserId: '',
deptName: '',
remark: '',
status: 0,
createTime: '',
finishTime: ''
})
let detailIndex = -1
const onRowClick = (record: any) => {
Object.assign(detailForm, record)
detailIndex = allData.value.findIndex(item => item.taskId === record.taskId)
showDetailModal.value = true
}
const handleUpdateTask = () => {
if (detailIndex !== -1) {
allData.value[detailIndex] = { ...detailForm }
filterAndPaginate()
showDetailModal.value = false
Message.success('任务更新成功')
}
}
const handleDeleteTask = () => {
if (detailIndex !== -1) {
allData.value.splice(detailIndex, 1)
filterAndPaginate()
showDetailModal.value = false
Message.success('任务已删除')
}
}
//
const fetchTaskList = async () => {
loading.value = true
try {
// const res = await listTask(params)
// const rows = res?.rows || res?.data?.rows || []
// allData.value = rows
//
allData.value = [
{
taskId: '1710000000000',
taskName: '示例任务A',
mainUserId: '张三',
deptName: '开发',
remark: '开发新功能',
status: 1,
createTime: '2024-05-01 10:00:00',
finishTime: ''
},
{
taskId: '1710000000001',
taskName: '示例任务B',
mainUserId: '',
deptName: '',
remark: '待分配任务',
status: 0,
createTime: '2024-05-02 11:00:00',
finishTime: ''
},
{
taskId: '1710000000002',
taskName: '示例任务C',
mainUserId: '李四',
deptName: '测试',
remark: '测试任务',
status: 2,
createTime: '2024-05-03 09:00:00',
finishTime: '2024-05-05 18:00:00'
}
]
filterAndPaginate()
pagination.total = allData.value.length
} catch (e) {
Message.error('获取任务数据失败')
} finally {
loading.value = false
}
}
//
const filterAndPaginate = () => {
let filtered = allData.value
if (searchForm.taskName) {
filtered = filtered.filter((item: any) =>
item.taskName?.toLowerCase().includes(searchForm.taskName.toLowerCase())
)
}
if (searchForm.responsiblePerson) {
filtered = filtered.filter((item: any) =>
item.mainUserId?.toLowerCase().includes(searchForm.responsiblePerson.toLowerCase())
)
}
if (searchForm.status !== '' && searchForm.status !== undefined) {
filtered = filtered.filter((item: any) => String(item.status) === String(searchForm.status))
}
//
const start = (pagination.current - 1) * pagination.pageSize
const end = start + pagination.pageSize
dataList.value = filtered.slice(start, end)
pagination.total = filtered.length
}
//
const search = async () => {
pagination.current = 1
filterAndPaginate()
}
const reset = async () => {
Object.assign(searchForm, {
userName: '',
projectName: '',
startDate: '',
endDate: '',
taskName: '',
responsiblePerson: '',
status: '',
page: 1,
size: 10
})
pagination.current = 1
search()
filterAndPaginate()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
filterAndPaginate()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增工作量记录功能开发中...')
}
const editRecord = (record: any) => {
Message.info(`编辑工作量记录: ${record.userName}`)
filterAndPaginate()
}
//
const deleteRecord = (record: any) => {
Message.info(`删除工作量记录: ${record.userName}`)
const idx = allData.value.findIndex(item => item.taskId === record.taskId)
if (idx !== -1) {
allData.value.splice(idx, 1)
filterAndPaginate()
Message.success('任务已删除')
}
}
onMounted(() => {
search()
fetchTaskList()
})
</script>

View File

@ -1,13 +1,6 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin">
<a-form-item field="account" hide-label>
<a-input v-model="form.account" placeholder="请输入用户名" allow-clear />
</a-form-item>
@ -108,7 +101,7 @@ const handleLogin = async () => {
const { rememberMe } = loginConfig.value
loginConfig.value.account = rememberMe ? form.account : ''
await router.push({
path: (redirect as string) || '/project-management/projects/initiation',
path: (redirect as string) || '/asset-management/intellectual-property',
query: {
...othersQuery,
},

View File

@ -0,0 +1,337 @@
<template>
<!-- 图片预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
:width="1000"
:footer="false"
title="多媒体预览"
class="preview-modal"
@close="handleModalClose"
>
<div class="preview-container">
<!-- 图片预览区域 -->
<div class="image-section">
<img
:src="currentPreviewItem?.url"
:alt="currentPreviewItem?.name"
class="preview-image"
/>
</div>
<!-- 音频列表区域 -->
<div class="audio-section">
<h3 class="audio-title">关联音频 ({{ audioList.length }})</h3>
<div class="audio-list">
<div
v-for="(audio, index) in audioList"
:key="index"
class="audio-item"
:class="{ 'active': currentAudioIndex === index }"
>
<div class="audio-info">
<span class="audio-name">{{ audio.audioId || `音频 ${index + 1}` }}</span>
<span class="audio-duration">{{ formatDuration(audio.duration) }}</span>
</div>
<div class="audio-controls">
<a-button
size="mini"
@click="toggleAudio(index)"
:loading="audioLoading && currentAudioIndex === index"
>
{{ currentAudioIndex === index && isPlaying ? '暂停' : '播放' }}
</a-button>
<a-progress
v-if="currentAudioIndex === index"
:percent="playProgress"
:show-text="false"
size="small"
class="progress-bar"
/>
</div>
<!-- 隐藏的audio元素 -->
<audio
ref="audioPlayers"
:src="audio.url"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
preload="metadata"
/>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { Message } from '@arco-design/web-vue'
interface AudioItem {
url: string
name?: string
duration?: number //
}
interface PreviewItem {
url: string
name: string
audios?: AudioItem[]
}
//
const previewModalVisible = ref(false)
const currentPreviewItem = ref<PreviewItem | null>(null)
//
const audioList = ref<AudioItem[]>([])
const currentAudioIndex = ref(-1)
const isPlaying = ref(false)
const audioLoading = ref(false)
const playProgress = ref(0)
const audioPlayers = ref<HTMLAudioElement[]>([])
const getAudioUrl = (filePath: string): string => {
if (!filePath) return ''
if (filePath.startsWith('http')) return filePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${filePath}`
}
//
const openPreview = (item: PreviewItem) => {
currentPreviewItem.value = item
audioList.value = []
if(item.audios){
for (const audio of item.audios) {
let temp={
audioId:audio.audioId,
url:getAudioUrl(audio.filePath)
}
audioList.value.push(temp)
}
}
previewModalVisible.value = true
resetAudioState()
//
nextTick(() => {
loadAudioDurations()
})
}
//
const handleModalClose = () => {
stopAudio()
previewModalVisible.value = false
}
//
const resetAudioState = () => {
currentAudioIndex.value = -1
isPlaying.value = false
playProgress.value = 0
}
//
const loadAudioDurations = () => {
audioPlayers.value.forEach((player, index) => {
player.onloadedmetadata = () => {
if (!audioList.value[index].duration) {
audioList.value[index].duration = player.duration
}
}
})
}
//
const toggleAudio = (index: number) => {
if (currentAudioIndex.value === index) {
// /
if (isPlaying.value) {
pauseAudio()
} else {
playAudio(index)
}
} else {
//
stopAudio()
playAudio(index)
}
}
//
const playAudio = (index: number) => {
audioLoading.value = true
currentAudioIndex.value = index
const player = audioPlayers.value[index]
player.play()
.then(() => {
isPlaying.value = true
audioLoading.value = false
})
.catch(err => {
console.error('播放失败:', err)
Message.error('音频播放失败')
audioLoading.value = false
})
}
//
const pauseAudio = () => {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
isPlaying.value = false
}
//
const stopAudio = () => {
if (currentAudioIndex.value !== -1) {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
player.currentTime = 0
}
isPlaying.value = false
currentAudioIndex.value = -1
playProgress.value = 0
}
//
const handleTimeUpdate = (e: Event) => {
const player = e.target as HTMLAudioElement
if (player.duration) {
playProgress.value = (player.currentTime / player.duration) * 100
}
}
//
const handleAudioEnded = () => {
isPlaying.value = false
playProgress.value = 100
}
//
const formatDuration = (seconds?: number) => {
if (!seconds) return '--:--'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs < 10 ? '0' : ''}${secs}`
}
//
defineExpose({
openPreview
})
</script>
<style scoped lang="less">
.preview-modal {
:deep(.arco-modal-content) {
padding: 0;
}
}
.preview-container {
display: flex;
height: 70vh;
max-height: 800px;
}
.image-section {
flex: 1;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
overflow: hidden;
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.audio-section {
width: 350px;
border-left: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
.audio-title {
padding: 16px;
margin: 0;
font-size: 16px;
border-bottom: 1px solid #e5e5e5;
}
.audio-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.audio-item {
padding: 12px 16px;
transition: background-color 0.2s;
&.active {
background-color: #f0f7ff;
}
&:hover {
background-color: #f9f9f9;
}
}
.audio-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.audio-name {
font-weight: 500;
}
.audio-duration {
color: #8c8c8c;
font-size: 12px;
}
}
.audio-controls {
display: flex;
align-items: center;
gap: 12px;
.progress-bar {
flex: 1;
}
}
}
@media (max-width: 1200px) {
.preview-container {
flex-direction: column;
height: 80vh;
}
.image-section {
height: 60%;
}
.audio-section {
width: 100%;
height: 40%;
border-left: none;
border-top: 1px solid #e5e5e5;
}
}
</style>

View File

@ -1,151 +1,297 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">数据入库 - 1号机组叶片A型检查</h2>
</div>
<!-- 页面标题 -->
<!-- <div class="page-header">
<h2 class="page-title">图像音频关联查看</h2>
</div>-->
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="图片" title="图片">
<div class="tab-content">
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
:custom-request="customUpload"
:show-file-list="false"
multiple
:accept="getAcceptType()"
@change="handleFileChange"
drag
class="upload-dragger"
>
<div class="upload-content">
<div class="upload-icon">
<icon-upload :size="48" />
</div>
<div class="upload-text">
<p class="primary-text">将文件拖拽到此处<span class="link-text">点击上传</span></p>
<p class="secondary-text">支持上传任意不超过100MB格式文件</p>
</div>
</div>
</a-upload>
</div>
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="列表" title="列表">
<div class="tab-content">
<!-- 操作按钮区域 -->
<div class="action-buttons">
<a-button
type="primary"
@click="startUpload"
:loading="uploading"
:disabled="uploadQueue.length === 0"
>
开始上传
</a-button>
<a-button @click="clearFiles">清空文件</a-button>
<a-button @click="batchImport">批量导入...</a-button>
</div>
<!-- 筛选条件区域 -->
<div class="filter-section">
<a-space size="large">
<!-- 项目选择 -->
<div class="filter-item">
<span class="filter-label">项目</span>
<a-select
v-model="filterParams.project"
placeholder="请选择项目"
:options="projectOptions"
allow-search
allow-clear
:loading="loading.project"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<h3 class="section-title">已上传数据</h3>
<a-table
<!-- 机组选择 -->
<div class="filter-item">
<span class="filter-label">机组</span>
<a-select
v-model="filterParams.unit"
placeholder="请先选择项目"
:options="unitOptions"
allow-search
allow-clear
:disabled="!filterParams.project"
:loading="loading.unit"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 部件选择 -->
<div class="filter-item">
<span class="filter-label">部件</span>
<a-select
v-model="filterParams.component"
placeholder="请先选择机组"
:options="componentOptions"
allow-search
allow-clear
:disabled="!filterParams.unit"
:loading="loading.component"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
</a-space>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<a-table
:columns="fileColumns"
:data="uploadedFiles"
:data="imageList"
:pagination="false"
:scroll="{ x: '100%' }"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }"
:loading="loading.image"
class="scrollable-table"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ record.imageTypeLabel}}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)" size="small">
{{ record.status }}
</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.preTreatment)" size="small">
{{ record.preTreatment }}
</a-tag>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">预览</a-button>
<a-button size="small" status="danger" @click="deleteFile(record)">删除</a-button>
</a-space>
</template>
</a-table>
</div>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">预览</a-button>
<a-button size="small" status="danger" @click="deleteFile(record)">删除</a-button>
</a-space>
</template>
</a-table>
</div>
</a-tab-pane>
<a-tab-pane key="video" tab="视频" title="视频">
<div class="tab-content">
<a-empty description="视频上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="audio" tab="语音" title="语音">
<div class="tab-content">
<a-empty description="语音上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="document" tab="文档" title="文档">
<div class="tab-content">
<a-empty description="文档上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="other" tab="其他" title="其他">
<div class="tab-content">
<a-empty description="其他文件上传功能开发中..." />
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 文件预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
title="文件预览"
:width="800"
:footer="false"
>
<div class="preview-container">
<div v-if="previewFileData && previewFileData.type === 'image'" class="image-preview">
<img :src="previewFileData.url" :alt="previewFileData.name" style="max-width: 100%; height: auto;" />
</div>
<div v-else-if="previewFileData && previewFileData.type === 'video'" class="video-preview">
<video :src="previewFileData.url" controls style="max-width: 100%; height: auto;" />
</div>
<div v-else class="file-info">
<p>文件名{{ previewFileData?.name }}</p>
<p>文件类型{{ previewFileData?.type }}</p>
<p>文件大小{{ formatFileSize(previewFileData?.size) }}</p>
</div>
</div>
</a-modal>
<PreviewModal ref="previewModal" />
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ref, reactive, computed,onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import PreviewModal from './components/PreviewModal.vue'
import {
getProjectList,
getTurbineList,
getPartList,
getImageList,
deleteImage,
autoAnnotateImage,
generateReport,
batchUploadImages,
uploadImageToPartV2
} from '@/apis/industrial-image'
const previewModal = ref()
//
const activeTab = ref('image')
//
const filterParams = reactive({
project: null,
unit: null,
component: null
})
//
const projectOptions = ref<Array<{label: string, value: string}>>([])
const unitOptions = ref<Array<{label: string, value: string}>>([])
const componentOptions = ref<Array<{label: string, value: string}>>([])
//
const imageList = ref<Array<{
id: number
name: string
type: string
imageTypeLabel: string
shootingTime: string
preTreatment: string
url: string
}>>([])
//
const loading = reactive({
project: false,
unit: false,
component: false,
image: false
})
//
onMounted(() => {
fetchProjectList()
})
//
if (watch) {
watch(() => filterParams.project, (newVal) => {
//
filterParams.unit = null
filterParams.component = null
unitOptions.value = []
componentOptions.value = []
imageList.value = []
if (newVal) {
fetchTurbineList(newVal)
}
})
}
//
if (watch) {
watch(() => filterParams.unit, (newVal) => {
//
filterParams.component = null
componentOptions.value = []
imageList.value = []
if (newVal && filterParams.project) {
fetchPartList(filterParams.project, newVal)
}
})
}
//
const fetchProjectList = async () => {
loading.project = true
try {
const res = await getProjectList({ page: 1, pageSize: 1000 });
projectOptions.value = res.data.map(item => ({
label: item.projectName,
value: item.projectId
}))
} catch (error) {
Message.error('获取项目列表失败')
} finally {
loading.project = false
}
}
//
const fetchTurbineList = async (projectId: string) => {
loading.unit = true
try {
const res = await getTurbineList({ projectId })
unitOptions.value = res.data.map(item => ({
label: item.turbineName,
value: item.turbineId
}))
} catch (error) {
console.error('获取机组列表失败:', error)
Message.error('获取机组列表失败')
} finally {
loading.unit = false
}
}
//
const fetchPartList = async (projectId: string, turbineId: string) => {
loading.component = true
try {
const res = await getPartList({ projectId, turbineId })
componentOptions.value = res.data.map(item => ({
label: item.partName, //
value: item.partId //
}))
} catch (error) {
console.error('获取部件列表失败:', error)
Message.error('获取部件列表失败')
} finally {
loading.component = false
}
}
//
const handleFilterChange = async () => {
// if (!filterParams.project) return
loading.image = true
try {
let params = {
projectId: filterParams.project
}
if(filterParams.unit){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit
}
}
if(filterParams.component){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit,
partId: filterParams.component
}
}
const res = await getImageList(params)
imageList.value = res.data.map((item: any) => ({
id: item.imageId,
name: item.imageName,
type: item.imageType?item.imageType:"未指定类型",
imageTypeLabel: item.imageTypeLabel,
shootingTime: item.shootingTime,
preTreatment: item.preTreatment?"已审核":"未审核",
imagePath: item.imagePath,
audioList:item.audioList
}))
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
} catch (error) {
Message.error('获取图像列表失败')
} finally {
loading.image = false
}
}
// const response = await getTurbineList({ projectId: node.id })
//
const uploading = ref(false)
const uploadQueue = ref<any[]>([])
@ -155,65 +301,32 @@ const previewModalVisible = ref(false)
const previewFileData = ref<any>(null)
//
const uploadedFiles = ref([
{
id: 1,
name: 'IMG_20231105_1430.jpg',
type: 'image',
size: 3355443, // 3.2MB
uploadTime: '2023-11-05 14:32',
status: '成功',
url: '/api/files/IMG_20231105_1430.jpg'
},
{
id: 2,
name: 'VID_20231106_0915.mp4',
type: 'video',
size: 47185920, // 45.6MB
uploadTime: '2023-11-06 09:18',
status: '成功',
url: '/api/files/VID_20231106_0915.mp4'
},
{
id: 3,
name: 'IMG_20231107_1645.jpg',
type: 'image',
size: 2936013, // 2.8MB
uploadTime: '2023-11-07 16:48',
status: '成功',
url: '/api/files/IMG_20231107_1645.jpg'
}
])
const uploadedFiles = ref([])
//
const fileColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'name', width: 250 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 100 },
{ title: '大小', dataIndex: 'size', slotName: 'size', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '类型描述', dataIndex: 'imageTypeLabel', slotName: 'imageTypeLabel', width: 100 },
{ title: '上传时间', dataIndex: 'shootingTime', width: 150 },
{ title: '状态', dataIndex: 'preTreatment', slotName: 'preTreatment', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const getAcceptType = () => {
const typeMap: Record<string, string> = {
'image': 'image/*',
'video': 'video/*',
'audio': 'audio/*',
'document': '.pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx',
'other': '*'
}
return typeMap[activeTab.value] || '*'
}
//
const filteredFiles = computed(() => {
return uploadedFiles.value.filter(file =>
(filterParams.project === null || file.project === filterParams.project) &&
(filterParams.unit === null || file.unit === filterParams.unit) &&
(filterParams.component === null || file.component === filterParams.component)
)
})
//
const getFileTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'image': 'blue',
'video': 'green',
'audio': 'orange',
'document': 'purple',
'TYPICAL': 'blue',
'DEFECT': 'orange',
'other': 'gray'
}
return colorMap[type] || 'gray'
@ -221,11 +334,10 @@ const getFileTypeColor = (type: string) => {
//
const getStatusColor = (status: string) => {
console.log(status);
const colorMap: Record<string, string> = {
'成功': 'green',
'失败': 'red',
'上传中': 'blue',
'等待': 'orange'
'已审核': 'green',
'未审核': 'red'
}
return colorMap[status] || 'gray'
}
@ -239,107 +351,30 @@ const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
}
//
const customUpload = (option: any) => {
const { file } = option
//
if (file.size > 100 * 1024 * 1024) {
Message.error('文件大小不能超过100MB')
return
}
//
uploadQueue.value.push({
file,
name: file.name,
size: file.size,
type: getFileType(file.name),
status: '等待'
})
Message.success(`文件 ${file.name} 已添加到上传队列`)
}
//
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension || '')) {
return 'image'
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(extension || '')) {
return 'video'
} else if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension || '')) {
return 'audio'
} else if (['pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension || '')) {
return 'document'
} else {
return 'other'
}
}
//
const handleFileChange = (fileList: any[]) => {
//
}
//
const startUpload = async () => {
if (uploadQueue.value.length === 0) {
Message.warning('请先选择要上传的文件')
return
}
uploading.value = true
try {
//
for (let i = 0; i < uploadQueue.value.length; i++) {
const queueItem = uploadQueue.value[i]
queueItem.status = '上传中'
//
await new Promise(resolve => setTimeout(resolve, 1000))
//
uploadedFiles.value.push({
id: Date.now() + i,
name: queueItem.name,
type: queueItem.type,
size: queueItem.size,
uploadTime: new Date().toLocaleString(),
status: '成功',
url: `/api/files/${queueItem.name}`
})
}
uploadQueue.value = []
Message.success('所有文件上传完成')
} catch (error) {
Message.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
//
const clearFiles = () => {
uploadQueue.value = []
Message.success('已清空文件队列')
}
//
const batchImport = () => {
Message.info('批量导入功能开发中...')
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
}
//
const previewFile = (file: any) => {
previewFileData.value = file
previewModalVisible.value = true
/* previewFileData.value = file
previewModalVisible.value = true*/
const fileObj ={
id: file.id,
name: file.name,
url: getImageUrl(file.imagePath),
audios: file.audioList
}
previewModal.value.openPreview(fileObj)
}
//
const deleteFile = (file: any) => {
console.log(index);
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
@ -357,7 +392,7 @@ const deleteFile = (file: any) => {
.page-header {
margin-bottom: 16px;
.page-title {
font-size: 20px;
font-weight: 500;
@ -377,65 +412,22 @@ const deleteFile = (file: any) => {
margin-top: 16px;
}
.upload-section {
.filter-section {
margin-bottom: 16px;
.upload-dragger {
:deep(.arco-upload-drag) {
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
padding: 40px;
text-align: center;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
background: #f0f7ff;
}
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.upload-icon {
color: #bfbfbf;
font-size: 48px;
}
.upload-text {
.primary-text {
font-size: 16px;
color: #595959;
margin: 0 0 8px 0;
.link-text {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.secondary-text {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
}
padding: 12px;
background: #fafafa;
border-radius: 4px;
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 24px;
.filter-item {
display: inline-flex;
align-items: center;
.filter-label {
font-size: 14px;
color: #595959;
margin-right: 8px;
}
}
}
.uploaded-files-section {
@ -449,16 +441,16 @@ const deleteFile = (file: any) => {
.preview-container {
text-align: center;
.image-preview,
.video-preview {
max-height: 500px;
overflow: hidden;
}
.file-info {
text-align: left;
p {
margin: 8px 0;
font-size: 14px;
@ -499,4 +491,4 @@ const deleteFile = (file: any) => {
:deep(.arco-upload-drag:hover) {
border-color: #1890ff;
}
</style>
</style>

View File

@ -0,0 +1,75 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '修改维度' : '新增维度'"
@before-ok="save"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="name" required>
<a-input v-model="form.name" />
</a-form-item>
<a-form-item label="权重%" field="weight" required>
<a-input-number v-model="form.weight" :min="1" :max="100" />
</a-form-item>
<a-form-item label="描述" field="desc">
<a-textarea v-model="form.desc" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { addDimension, getDimensionDetail, updateDimension } from '@/apis/performance'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const formRef = ref()
const id = ref('')
const form = reactive({
name: '',
weight: 50,
desc: '',
status: 0 as 0 | 1,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, { name: '', weight: 50, desc: '', status: 0 })
id.value = ''
}
const save = async () => {
if (isUpdate.value) {
await updateDimension(id.value, form)
} else {
await addDimension(form)
}
visible.value = false
emit('success')
}
const onAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const onUpdate = async (dimensionId: string) => {
reset()
isUpdate.value = true
id.value = dimensionId
const data = await getDimensionDetail(dimensionId)
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>

View File

@ -0,0 +1,100 @@
<template>
<a-spin :loading="loading">
<a-descriptions :data="detailData" bordered>
<a-descriptions-item label="员工">
{{ data.userName }}
</a-descriptions-item>
<a-descriptions-item label="维度">
{{ data.dimensionName }}
</a-descriptions-item>
<a-descriptions-item label="周期">
{{ data.periodName }}
</a-descriptions-item>
<a-descriptions-item label="评分">
<a-tag color="blue" size="large">{{ data.score }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="AI评价" :span="2">
{{ data.aiComment }}
</a-descriptions-item>
</a-descriptions>
<!-- 反馈表单 -->
<a-divider orientation="left">评价反馈</a-divider>
<a-form ref="feedbackRef" :model="feedback" :rules="rules" layout="vertical">
<a-form-item label="是否有异议" field="level" required>
<a-radio-group v-model="feedback.level">
<a-radio :value="0">无异议</a-radio>
<a-radio :value="1">部分有异议</a-radio>
<a-radio :value="2">完全不同意</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="请说明理由" field="content" required>
<a-textarea
v-model="feedback.content"
:rows="4"
placeholder="请具体描述您对评价结果的看法..."
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSubmit">
提交反馈
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { submitFeedback } from '@/apis/performance'
import type { EvaluateResp, FeedbackReq } from '@/apis/performance/type'
interface Props {
data: EvaluateResp
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'feedback'): void
}>()
const loading = ref(false)
const submitting = ref(false)
const feedbackRef = ref()
const feedback = reactive<FeedbackReq>({
evaluateId: props.data.id,
level: 0,
content: '',
})
const rules = {
level: [{ required: true, message: '请选择异议等级' }],
content: [{ required: true, message: '请填写反馈内容' }],
}
const detailData = computed(() => [
{ label: '员工', value: props.data.userName },
{ label: '维度', value: props.data.dimensionName },
{ label: '周期', value: props.data.periodName },
{ label: '评分', value: props.data.score },
{ label: 'AI评价', value: props.data.aiComment },
])
const handleSubmit = async () => {
const ok = await feedbackRef.value?.validate()
if (ok) return
submitting.value = true
try {
await submitFeedback(feedback)
Message.success('反馈已提交')
emit('feedback')
} finally {
submitting.value = false
}
}
</script>

View File

@ -0,0 +1,18 @@
<template>
<a-menu
:default-selected-keys="['dimension']"
@menu-item-click="handleClick"
>
<a-menu-item key="dimension">绩效维度</a-menu-item>
<a-menu-item key="evaluate">员工评估</a-menu-item>
<a-menu-item key="my">我的绩效</a-menu-item>
</a-menu>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'select', key: string): void
}>()
const handleClick = (key: string) => emit('select', key)
</script>

View File

@ -0,0 +1,131 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '编辑细则' : '新增细则'"
:width="480"
@before-ok="handleSave"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="所属维度" field="dimensionId" required>
<a-select v-model="form.dimensionId" placeholder="请选择维度">
<a-option
v-for="item in dimensionOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</a-select>
</a-form-item>
<a-form-item label="细则名称" field="name" required>
<a-input v-model="form.name" placeholder="如:持有基础资质证书" />
</a-form-item>
<a-form-item label="评分规则描述" field="ruleDesc" required>
<a-textarea
v-model="form.ruleDesc"
:rows="4"
placeholder="详细说明评分标准"
/>
</a-form-item>
<a-form-item label="满分" field="score" required>
<a-input-number v-model="form.score" :min="0" :precision="0" />
</a-form-item>
<a-form-item label="权重" field="weight" required>
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="是否加分项" field="isExtra">
<a-switch v-model="form.isExtra" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
addRule,
getDimensionList,
getRuleDetail,
updateRule,
} from '@/apis/performance'
import type { DimensionResp, RuleAddReq, RuleUpdateReq } from '@/apis/performance/type'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const id = ref('')
const formRef = ref()
const dimensionOptions = ref<DimensionResp[]>([])
const form = reactive<RuleAddReq>({
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, {
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
id.value = ''
}
const openAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const openUpdate = async (ruleId: string) => {
reset()
isUpdate.value = true
id.value = ruleId
const detail = await getRuleDetail(ruleId)
Object.assign(form, detail)
visible.value = true
}
const handleSave = async () => {
const ok = await formRef.value?.validate()
if (ok) return false
try {
if (isUpdate.value) {
await updateRule(id.value, form as RuleUpdateReq)
Message.success('细则已更新')
} else {
await addRule(form)
Message.success('细则已新增')
}
visible.value = false
emit('success')
return true
} catch {
return false
}
}
onMounted(async () => {
dimensionOptions.value = await getDimensionList()
})
defineExpose({ openAdd, openUpdate })
</script>

View File

@ -0,0 +1,67 @@
<template>
<a-modal
v-model:visible="visible"
title="细则管理"
:width="800"
@before-ok="handleSave"
@cancel="reset"
>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</a-modal>
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import RuleDrawer from './RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
import { Message } from '@arco-design/web-vue'
const visible = ref(false)
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const open = async (dimensionId: string) => {
visible.value = true
rules.value = await getRuleList(dimensionId)
}
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load()
}
const load = async () => {
//
rules.value = await getRuleList(dimensionId.value)
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,72 @@
<template>
<div>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="DimensionDrawerRef?.onAdd()">
<icon-plus /> 添加维度
</a-button>
</template>
<a-table :data="dimensions" :columns="columns" row-key="id">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="DimensionDrawerRef?.onUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
<a-button type="text" @click="openRuleList(record.id)">
管理细则
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="DimensionDrawerRef" @success="load" />
<!-- 细则列表 -->
<RuleList ref="RuleListRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDimensionList, deleteDimension } from '@/apis/performance'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import type { DimensionResp } from '@/apis/performance/type'
const dimensions = ref<DimensionResp[]>([])
const DimensionDrawerRef = ref()
const RuleListRef = ref()
const columns = [
{ title: '维度名称', dataIndex: 'name' },
{ title: '权重', dataIndex: 'weight', render: ({ record }) => `${record.weight}%` },
{ title: '细则数量', dataIndex: 'ruleCount' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const response=await getDimensionList()
dimensions.value = await getDimensionList().data||[]
}
onMounted(load)
const handleDelete = async (id: string) => {
await deleteDimension(id)
load()
}
const openRuleList = (dimensionId: string) => {
RuleListRef.value.open(dimensionId)
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<a-card title="员工绩效评估">
<a-form layout="inline" style="margin-bottom: 16px">
<a-form-item label="绩效周期">
<a-select v-model="query.periodId" placeholder="请选择周期" allow-clear>
<a-option v-for="p in periods" :key="p.id" :value="p.id">{{ p.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="维度">
<a-select v-model="query.dimensionId" placeholder="请选择维度" allow-clear>
<a-option v-for="d in dimensions" :key="d.id" :value="d.id">{{ d.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-input v-model="query.keyword" placeholder="姓名/工号" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="load">查询</a-button>
</a-form-item>
</a-form>
<a-table :data="data" :columns="columns" row-key="id">
<template #action="{ record }">
<a-button type="text" @click="openEvaluate(record)">
开始评估
</a-button>
</template>
</a-table>
</a-card>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getDimensionList, getEvaluatePage, getPeriodList, startEvaluate } from '@/apis/performance'
import type { EvaluateQuery, EvaluateResp } from '@/apis/performance/type'
const periods = ref([])
const dimensions = ref([])
const query = ref<EvaluateQuery>({})
const data = ref<EvaluateResp[]>([])
const columns = [
{ title: '员工', dataIndex: 'userName' },
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
data.value = await getEvaluatePage(query.value)
}
onMounted(async () => {
periods.value = await getPeriodList()
dimensions.value = await getDimensionList()
load()
})
const openEvaluate = async (row: EvaluateResp) => {
await startEvaluate({
userId: row.userId,
dimensionId: row.dimensionId,
ruleId: row.ruleId,
periodId: row.periodId,
})
Message.success('评估已启动')
load()
}
</script>

View File

@ -0,0 +1,19 @@
<template>
<GiPageLayout>
<template #left>
<PerformanceMenu @select="handleSelectMenu" />
</template>
<router-view />
</GiPageLayout>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import PerformanceMenu from './components/PerformanceMenu.vue'
const router = useRouter()
const handleSelectMenu = (key: string) => {
router.push(`/performance/${key}`)
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<a-card title="我的绩效">
<a-table :data="data" :columns="columns" row-key="evaluationId">
<template #action="{ record }">
<a-button type="text" @click="openDetail(record)">查看详情</a-button>
</template>
</a-table>
</a-card>
<a-modal v-model:visible="visible" title="绩效详情">
<div v-if="selected">
<div>维度{{ selected.dimensionName }}</div>
<div>周期{{ selected.periodName }}</div>
<div>评分{{ selected.score }}</div>
<div>奖金{{ selected.bonus }}</div>
<div>评价{{ selected.comment }}</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getMyEvaluation } from '@/apis/performance-setting'
import type { PerformanceRule } from '@/apis/performance-setting/type'
const data = ref<any[]>([])
const visible = ref(false)
const selected = ref<any>()
const columns = [
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '评价', dataIndex: 'comment' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const res = await getMyEvaluation()
data.value = res.data || []
}
const openDetail = (row: any) => {
selected.value = row
visible.value = true
}
onMounted(load)
</script>

View File

@ -0,0 +1,61 @@
<template>
<div>
<a-card title="绩效细则管理">
<template #extra>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</template>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 细则抽屉 -->
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import RuleDrawer from './components/RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const load = async (dimensionId: string) => {
rules.value = await getRuleList(dimensionId)
}
onMounted(() => {
//
load('default-dimension-id')
})
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load('default-dimension-id') //
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑维度' : '新增维度'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="dimensionName" required>
<a-input v-model="form.dimensionName" />
</a-form-item>
<a-form-item label="岗位" field="deptName" required>
<a-input v-model="form.deptName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addDimension, updateDimension } from '@/apis/performance-setting'
import type { PerformanceDimension } from '@/apis/performance-setting/type'
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceDimension>>({
dimensionName: '',
deptName: '',
description: '',
status: 0,
})
const open = (record?: PerformanceDimension) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, { dimensionName: '', deptName: '', description: '', status: 0 })
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.dimensionId) {
await updateDimension(form.value.dimensionId, form.value)
} else {
await addDimension(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,86 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑细则' : '新增细则'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="细则名称" field="ruleName" required>
<a-input v-model="form.ruleName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="分数" field="score">
<a-input-number v-model="form.score" :min="0" />
</a-form-item>
<a-form-item label="权重" field="weight">
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="奖金" field="bonus">
<a-input v-model="form.bonus" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { addRule, updateRule } from '@/apis/performance-setting'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const props = defineProps<{ dimension?: PerformanceDimension }>()
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceRule>>({
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: '',
})
watch(
() => props.dimension,
(val) => {
if (val && !isEdit.value) {
form.value.dimensionName = val.dimensionName
}
},
{ immediate: true }
)
const open = (record?: PerformanceRule) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, {
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: props.dimension?.dimensionName || '',
})
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.ruleId) {
await updateRule(form.value.ruleId, form.value)
} else {
await addRule(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,64 @@
<template>
<a-drawer v-model:visible="visible" :title="`管理细则 - ${dimension?.dimensionName || ''}`">
<a-button type="primary" @click="openRuleDrawer()">新增细则</a-button>
<a-table :data="ruleList" :columns="ruleColumns" row-key="ruleId" style="margin-top: 16px;">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openRuleDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteRule(record.ruleId)">删除</a-button>
</a-space>
</template>
</a-table>
<RuleDrawer ref="ruleDrawerRef" @success="loadRules" :dimension="dimension" />
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getRuleList, deleteRule } from '@/apis/performance-setting'
import RuleDrawer from './RuleDrawer.vue'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const visible = ref(false)
const dimension = ref<PerformanceDimension>()
const ruleList = ref<PerformanceRule[]>([])
const ruleDrawerRef = ref()
const ruleColumns = [
{ title: '细则名称', dataIndex: 'ruleName' },
{ title: '描述', dataIndex: 'description' },
{ title: '分数', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const open = (dim: PerformanceDimension) => {
dimension.value = dim
visible.value = true
loadRules()
}
const loadRules = async () => {
if (!dimension.value) return
const res = await getRuleList({ dimensionName: dimension.value.dimensionName })
ruleList.value = res.data || []
}
const openRuleDrawer = (record?: PerformanceRule) => {
ruleDrawerRef.value.open(record)
}
const handleDeleteRule = async (id: string) => {
await deleteRule(id)
loadRules()
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,67 @@
<template>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="openDimensionDrawer()">新增维度</a-button>
</template>
<a-table :data="dimensionList" :columns="dimensionColumns" row-key="dimensionId">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openDimensionDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteDimension(record.dimensionId)">删除</a-button>
<a-button type="text" @click="openRuleList(record)">管理细则</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="dimensionDrawerRef" @success="loadDimensions" />
<!-- 细则管理 -->
<RuleList ref="ruleListRef" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import { deleteDimension, getDimensionList } from '@/apis/performance-setting/index'
import type { PerformanceDimension } from '@/apis/performance-setting/type.ts'
const dimensionList = ref<PerformanceDimension[]>([])
const dimensionDrawerRef = ref()
const ruleListRef = ref()
const dimensionColumns = [
{ title: '维度名称', dataIndex: 'dimensionName' },
{ title: '岗位', dataIndex: 'deptName' },
{ title: '描述', dataIndex: 'description' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const loadDimensions = async () => {
const res = await getDimensionList()
dimensionList.value = res.data || []
}
const openDimensionDrawer = (record?: PerformanceDimension) => {
dimensionDrawerRef.value.open(record)
}
const handleDeleteDimension = async (id: string) => {
await deleteDimension(id)
loadDimensions()
}
const openRuleList = (dimension: PerformanceDimension) => {
ruleListRef.value.open(dimension)
}
onMounted(loadDimensions)
</script>

View File

@ -0,0 +1,198 @@
<template>
<a-modal
:visible="visible"
title="招标详情"
width="800px"
:footer="false"
:mask-closable="false"
@update:visible="(val) => $emit('update:visible', val)"
>
<a-descriptions
:column="2"
bordered
:label-style="{ width: '120px', fontWeight: 'bold' }"
>
<a-descriptions-item label="项目名称">{{ detail.projectName }}</a-descriptions-item>
<a-descriptions-item label="招标单位">{{ detail.biddingUnit }}</a-descriptions-item>
<a-descriptions-item label="预算金额">{{ detail.budgetAmount }}万元</a-descriptions-item>
<a-descriptions-item label="截止时间">{{ detail.deadline }}</a-descriptions-item>
<a-descriptions-item label="爬取时间">{{ detail.crawlingTime }}</a-descriptions-item>
<a-descriptions-item label="项目地点">{{ detail.projectLocation }}</a-descriptions-item>
<a-descriptions-item label="项目周期">{{ detail.projectDuration }}</a-descriptions-item>
<a-descriptions-item label="招标范围">{{ detail.biddingScope }}</a-descriptions-item>
<a-descriptions-item label="资质要求">{{ detail.qualificationRequirements }}</a-descriptions-item>
<a-descriptions-item label="来源平台">
<a
:href="getPlatformUrl(detail.sourcePlatform)"
target="_blank"
class="platform-link"
>
{{ detail.sourcePlatform }}
</a>
</a-descriptions-item>
<a-descriptions-item label="招标文件">
<div class="file-display">
<a-link
v-if="detail.biddingDocuments"
:href="detail.biddingDocuments"
target="_blank"
class="file-link"
>
{{ displayedFileName }}
</a-link>
<span v-else class="no-file">暂无文件</span>
<a-upload
:show-file-list="false"
:before-upload="beforeUpload"
accept=".pdf,.doc,.docx"
style="margin-left: 10px"
>
<a-button
type="outline"
size="mini"
:loading="uploading"
>
<template #icon><icon-upload /></template>
{{ detail.biddingDocuments ? '重新上传' : '上传文件' }}
</a-button>
</a-upload>
</div>
</a-descriptions-item>
</a-descriptions>
<div class="content-section">
<h3>招标内容</h3>
<div class="content-text">{{ detail.biddingContent }}</div>
<ul v-if="detail.contentItems && detail.contentItems.length">
<li v-for="(item, index) in detail.contentItems" :key="index">{{ item }}</li>
</ul>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { BiddingDetail } from '../types'
const props = defineProps<{
visible: boolean
detail: BiddingDetail
uploading?: boolean
}>()
const emit = defineEmits(['update:visible', 'upload'])
const displayedFileName = computed(() => {
if (!props.detail?.biddingDocuments) return ''
const url = props.detail.biddingDocuments
return url.split('/').pop() || '招标文件'
})
// URL
const platformUrls: Record<string, string> = {
'中国招标投标网': 'https://www.cebpubservice.com/',
'国能e招': 'https://www.negc.cn/',
'中国节能': 'https://www.cecec.cn/',
'三峡招标': 'https://epp.ctg.com.cn/'
}
const getPlatformUrl = (platformName: string): string => {
return platformUrls[platformName] || '#'
}
const handleUpload = () => {
emit('upload')
}
const beforeUpload = (file: File) => {
//
const isValidType = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
].includes(file.type)
// (10MB)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isValidType) {
Message.error('只能上传PDF或Word文件!')
return false
}
if (!isLt10M) {
Message.error('文件大小不能超过10MB!')
return false
}
//
emit('upload', file)
// false
return false
}
</script>
<style scoped lang="less">
.file-display {
display: flex;
align-items: center;
}
.file-link {
color: #1890ff;
text-decoration: underline;
cursor: pointer;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
&:hover {
color: #40a9ff;
}
}
.no-file {
color: var(--color-text-3);
}
.platform-link {
color: #1890ff; /* 蓝色 */
text-decoration: underline; /* 下划线 */
cursor: pointer;
&:hover {
color: #40a9ff; /* 悬停时变浅蓝色 */
text-decoration: underline;
}
}
.content-section {
margin-top: 20px;
padding: 16px;
background-color: var(--color-fill-2);
border-radius: 4px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.content-text {
margin-bottom: 12px;
line-height: 1.6;
color: var(--color-text-2);
}
ul {
padding-left: 20px;
color: var(--color-text-2);
li {
margin-bottom: 8px;
line-height: 1.6;
}
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<a-modal
:visible="visible"
title="爬虫设置"
width="800px"
@cancel="handleCancel"
@ok="handleOk"
:mask-closable="false"
>
<a-form :model="form" layout="vertical">
<a-form-item label="爬取频率">
<a-select v-model="form.frequency">
<a-option value="hourly">每小时</a-option>
<a-option value="daily">每天</a-option>
<a-option value="weekly">每周</a-option>
<a-option value="monthly">每月</a-option>
</a-select>
</a-form-item>
<a-form-item label="关键词过滤">
<a-input-tag
v-model="form.keywords"
placeholder="输入关键词后回车"
allow-clear
/>
</a-form-item>
<a-form-item label="来源平台">
<a-checkbox-group v-model="form.platforms">
<a-row :gutter="[16, 16]">
<a-col :span="8">
<a-checkbox value="国能e招">国能e招</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="中国节能">中国节能</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="科幻集团">科幻集团</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="三峡招标">三峡招标</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="三峡采购">三峡采购</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="北京京能">北京京能</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="华润守正">华润守正</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
<a-form-item label="自动通知">
<a-switch v-model="form.autoNotify" />
</a-form-item>
<template v-if="form.autoNotify">
<a-form-item label="通知方式">
<a-checkbox-group v-model="form.notifyMethods">
<a-checkbox value="inApp">站内消息</a-checkbox>
<a-checkbox value="email">电子邮件</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item v-if="form.notifyMethods.includes('email')" label="通知邮箱">
<a-input v-model="form.notifyEmail" placeholder="请输入接收通知的邮箱" />
</a-form-item>
</template>
<a-divider />
<a-form-item label="爬虫日志">
<div class="log-container">
<div v-for="(log, index) in logs" :key="index" class="log-item">
[{{ log.time }}] {{ log.message }}
</div>
</div>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleOk">保存设置</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
interface LogEntry {
time: string
message: string
}
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'save'])
const form = reactive({
frequency: 'daily',
keywords: ['风电', '叶片', '检查', '运维'],
platforms: ['国能e招', '中国节能', '三峡招标'],
autoNotify: true,
notifyMethods: ['inApp', 'email'],
notifyEmail: 'user@example.com'
})
const logs = ref<LogEntry[]>([
{ time: '2025-07-30 22:00', message: '爬虫任务已启动' },
{ time: '2025-07-30 21:00', message: '爬虫任务已启动' },
{ time: '2023-11-01 09:30', message: '成功爬取中国招标投标网数据' },
{ time: '2023-11-01 09:32', message: '成功爬取某省公共资源交易中心数据' },
{ time: '2023-11-02 10:15', message: '爬取企业自有招标平台失败:连接超时' }
])
const handleCancel = () => {
emit('update:visible', false)
}
const handleOk = () => {
Message.success('设置保存成功')
emit('save', form)
emit('update:visible', false)
}
// Watch for email notification toggle
watch(() => form.notifyMethods, (newVal) => {
if (!newVal.includes('email')) {
form.notifyEmail = ''
}
})
</script>
<style scoped lang="less">
.log-container {
height: 200px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background-color: var(--color-fill-2);
}
.log-item {
padding: 4px 0;
font-family: monospace;
font-size: 13px;
color: var(--color-text-2);
}
.log-item:not(:last-child) {
border-bottom: 1px dashed var(--color-border-2);
}
</style>

View File

@ -0,0 +1,344 @@
<template>
<div class="project-table-container">
<a-table
:data="data"
:columns="columns"
:loading="loading"
:pagination="false"
:scroll="{ x: '100%', y: '100%' }"
row-key="id"
:stripe="false"
size="large"
>
<!-- 招标项目 -->
<template #projectName="{ record }">
<div class="project-name">
<span class="name-text">{{ record.projectName }}</span>
</div>
</template>
<!-- 招标单位 -->
<template #biddingUnit="{ record }">
<span class="bidding-unit">{{ record.biddingUnit }}</span>
</template>
<!-- 预算金额 -->
<template #budgetAmount="{ record }">
<span class="budget-text">{{ record.budgetAmount }}</span>
</template>
<!-- 截止时间 -->
<template #deadline="{ record }">
<span class="date-text">{{ record.deadline }}</span>
</template>
<!-- 爬取时间 -->
<template #crawlingTime="{ record }">
<span class="date-text">{{ record.crawlingTime }}</span>
</template>
<!-- 来源平台 -->
<template #sourcePlatform="{ record }">
<span class="source-text">{{ record.sourcePlatform }}</span>
</template>
<!-- 招标文件 -->
<template #biddingDocuments="{ record }">
<span class="doc-text">{{ record.biddingDocuments }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作 -->
<template #action="{ record }">
<div class="action-buttons">
<a-button size="small" @click="handleView(record)">
详情
</a-button>
<a-button
type="primary"
size="small"
@click="handleEnter(record)"
>
报名
</a-button>
</div>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectData } from '../types'
interface Props {
data: ProjectData[]
loading?: boolean
}
interface Emits {
(e: 'view', project: ProjectData): void
(e: 'enter', project: ProjectData): void
}
const props = withDefaults(defineProps<Props>(), {
loading: false
})
const emit = defineEmits<Emits>()
//
const columns: TableColumnData[] = [
{
title: '招标项目',
dataIndex: 'projectName',
slotName: 'projectName',
width: 300,
ellipsis: true,
tooltip: true
},
{
title: '招标单位',
dataIndex: ' biddingUnit',
slotName: 'biddingUnit',
width: 120,
align: 'center'
},
{
title: '预算金额',
dataIndex: 'budgetAmount',
slotName: 'budgetAmount',
width: 120,
align: 'center'
},
{
title: '截止时间',
dataIndex: 'deadline',
slotName: 'deadline',
width: 150,
align: 'center'
},
{
title: '爬取时间',
dataIndex: 'crawlingTime',
slotName: 'crawlingTime',
width: 150,
align: 'center'
},
{
title: '来源平台',
dataIndex: 'sourcePlatform',
slotName: 'sourcePlatform',
width: 150,
align: 'center'
},
{
title: '招标文件',
dataIndex: ' biddingDocuments',
slotName: 'biddingDocuments',
width: 150,
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 120,
align: 'center'
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 150,
align: 'center',
fixed: 'right'
}
]
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'not_started': 'gray',
'in_progress': 'blue',
'completed': 'green',
'paused': 'orange',
'cancelled': 'red'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'not_started': '未开始',
'in_progress': '进行中',
'completed': '已完成',
'paused': '已暂停',
'cancelled': '已取消'
}
return textMap[status] || status
}
//
const getProgressColor = (progress: number) => {
if (progress === 0) return '#d9d9d9'
if (progress === 100) return '#52c41a'
if (progress >= 70) return '#1890ff'
if (progress >= 40) return '#faad14'
return '#ff4d4f'
}
//
const handleView = (record: ProjectData) => {
emit('view', record)
}
const handleEnter = (record: ProjectData) => {
emit('enter', record)
}
</script>
<style scoped lang="less">
.project-table-container {
height: 100%;
background: #fff;
border-radius: 8px;
}
.project-name {
.name-text {
font-weight: 500;
color: #333;
}
}
.unit-count {
font-weight: 600;
color: #1890ff;
}
.date-text {
color: #666;
font-size: 13px;
}
.progress-container {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.progress-text {
font-size: 12px;
color: #666;
font-weight: 500;
min-width: 35px;
text-align: right;
}
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
:deep(.arco-table) {
.arco-table-thead {
.arco-table-th {
background-color: #fafafa;
font-weight: 600;
color: #333;
border-bottom: 1px solid #e8e8e8;
}
}
.arco-table-tbody {
.arco-table-tr {
&:hover {
background-color: #f5f5f5;
}
}
.arco-table-td {
border-bottom: 1px solid #f0f0f0;
padding: 16px 12px;
}
}
}
:deep(.arco-tag) {
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
:deep(.arco-progress-line-outer) {
border-radius: 4px;
}
:deep(.arco-progress-line-inner) {
border-radius: 4px;
transition: all 0.3s ease;
}
//
:deep(.arco-tag-gray) {
background-color: #f5f5f5;
border-color: #d9d9d9;
color: #666;
}
:deep(.arco-tag-blue) {
background-color: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
}
:deep(.arco-tag-green) {
background-color: #f6ffed;
border-color: #b7eb8f;
color: #52c41a;
}
:deep(.arco-tag-orange) {
background-color: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
}
:deep(.arco-tag-red) {
background-color: #fff1f0;
border-color: #ffccc7;
color: #ff4d4f;
}
//
@media (max-width: 768px) {
:deep(.arco-table-td) {
padding: 12px 8px;
font-size: 14px;
}
.action-buttons {
flex-direction: column;
gap: 4px;
}
.progress-container {
gap: 8px;
.progress-text {
font-size: 11px;
}
}
}
</style>

View File

@ -0,0 +1,415 @@
<template>
<GiPageLayout>
<div class="project-list-container">
<!-- 顶部搜索和操作区域 -->
<div class="header-section">
<div class="search-area">
<a-input-search
v-model="searchKeyword"
placeholder="搜索项目名称"
allow-clear
@search="handleSearch"
@clear="handleClear"
style="width: 300px"
/>
</div>
<div class="action-area">
<a-space>
<a-button type="primary" @click="startCrawler">
<template #icon><icon-play-arrow /></template>
开始爬虫
</a-button>
<a-button @click="refreshData">
<template #icon><icon-refresh /></template>
刷新数据
</a-button>
<a-button @click="exportData">
<template #icon><icon-download /></template>
导出数据
</a-button>
<a-button @click="openCrawlerSettings">
<template #icon><icon-settings /></template>
爬虫设置
</a-button>
</a-space>
</div>
</div>
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs
v-model:active-key="activeTab"
type="line"
size="large"
@change="handleTabChange"
>
<a-tab-pane key="all" tab="信息检索" title="信息检索">
<ProjectTable
:data="pagedData"
:loading="loading"
@view="handleView"
@enter="handleEnter"
/>
<div class="pagination-wrapper">
<a-pagination
v-model:current="currentPage"
v-model:page-size="pageSize"
:total="totalItems"
show-total
show-jumper
show-page-size
:page-size-options="[10, 20, 50, 100]"
@change="handlePageChange"
/>
</div>
</a-tab-pane>
<a-tab-pane key="my" tab="投标响应" title="投标响应">
<ProjectTable
:data="myPagedData"
:loading="loading"
@view="handleView"
@enter="handleEnter"
/>
<div class="pagination-wrapper">
<a-pagination
v-model:current="myCurrentPage"
v-model:page-size="pageSize"
:total="myTotalItems"
show-total
show-jumper
show-page-size
:page-size-options="[10, 20, 50, 100]"
@change="handleMyPageChange"
/>
</div>
</a-tab-pane>
</a-tabs>
</div>
<CrawlerSettings
v-model:visible="crawlerSettingsVisible"
@save="handleSaveSettings"
/>
</div>
</GiPageLayout>
<BiddingDetailModal
v-model:visible="detailVisible"
:detail="currentDetail"
:uploading="uploading"
@upload="handleUploadDocument"
/>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import ProjectTable from './components/InformationTable.vue'
import CrawlerSettings from './components/CrawlerSettings.vue'
import BiddingDetailModal from './components/BiddingDetailModal.vue'
import type { ProjectData, TabKey,BiddingDetail } from './types'
import { ProjectStatus } from './types'
defineOptions({ name: 'ProjectList' })
//
const loading = ref(false)
const activeTab = ref<TabKey>('all')
const searchKeyword = ref('')
const crawlerSettingsVisible = ref(false)
//
const currentPage = ref(1)
const myCurrentPage = ref(1)
const pageSize = ref(10)
//
const detailVisible = ref(false)
const uploading = ref(false)
const currentDetail = ref<BiddingDetail>({
id: 0,
projectName: '',
biddingUnit: '',
budgetAmount: 0,
deadline: '',
crawlingTime: '',
projectLocation: '',
projectDuration: '',
biddingScope: '',
qualificationRequirements: '',
sourcePlatform: '',
biddingDocuments: '',
biddingContent: '',
contentItems: []
})
//
const projectList = ref<ProjectData[]>([
{
id: 1,
projectName: 'A风场2023年检查',
biddingUnit: 15,
budgetAmount:111,
deadline: '2023-10-01',
crawlingTime: '2023-12-31',
sourcePlatform:"中国招标投标网",
biddingDocuments:"333.pdf",
status: ProjectStatus.IN_PROGRESS,
manager: '张三',
isMyProject: true
},
//
...Array.from({ length: 25 }, (_, i) => ({
id: i + 2,
projectName: `项目${i + 2}`,
biddingUnit: Math.floor(Math.random() * 20) + 1,
budgetAmount: Math.floor(Math.random() * 1000) + 100,
deadline: `2023-${(Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0')}-${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}`,
crawlingTime: `2023-${(Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0')}-${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}`,
sourcePlatform: "中国招标投标网",
biddingDocuments: `文档${i + 2}.pdf`,
status: Math.random() > 0.5 ? ProjectStatus.IN_PROGRESS : ProjectStatus.COMPLETED,
manager: ['张三', '李四', '王五'][Math.floor(Math.random() * 3)],
isMyProject: Math.random() > 0.7
}))
])
const handleSaveSettings = (settings: any) => {
console.log('Saved settings:', settings)
}
//
const handleView = (project: ProjectData) => {
// ID
currentDetail.value = {
id: project.id,
projectName: project.projectName,
biddingUnit: project.biddingUnit.toString(),
budgetAmount: project.budgetAmount,
deadline: project.deadline,
crawlingTime: project.crawlingTime,
projectLocation: '内蒙古自治区', //
projectDuration: '6个月', //
biddingScope: '风电场50台机组叶片检查服务', //
qualificationRequirements: '具备风电叶片检查资质', //
sourcePlatform: project.sourcePlatform,
biddingDocuments: project.biddingDocuments,
biddingContent: '本项目为某风电场2023年叶片检查服务采购包括但不限于',
contentItems: [
'叶片外观检查',
'无损检测',
'缺陷记录与评估',
'检查报告编制'
]
}
detailVisible.value = true
}
// -
const filteredProjects = computed(() => {
if (!searchKeyword.value) {
return projectList.value
}
return projectList.value.filter(project =>
project.projectName.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// -
const myProjects = computed(() => {
return filteredProjects.value.filter(project => project.isMyProject)
})
//
const totalItems = computed(() => filteredProjects.value.length)
const myTotalItems = computed(() => myProjects.value.length)
const pagedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredProjects.value.slice(start, end)
})
const myPagedData = computed(() => {
const start = (myCurrentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return myProjects.value.slice(start, end)
})
//
const handleSearch = (value: string) => {
searchKeyword.value = value
currentPage.value = 1 //
myCurrentPage.value = 1
}
const handleClear = () => {
searchKeyword.value = ''
currentPage.value = 1 //
myCurrentPage.value = 1
}
//
const handleTabChange = (key: string) => {
activeTab.value = key as TabKey
}
//
const handlePageChange = (page: number) => {
currentPage.value = page
}
const handleMyPageChange = (page: number) => {
myCurrentPage.value = page
}
//
const refreshData = async () => {
loading.value = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 800))
Message.success('数据已刷新')
} catch (error) {
Message.error('刷新失败')
} finally {
loading.value = false
}
}
//
const startCrawler = () => {
Message.info('开始爬虫任务')
//
}
//
const exportData = () => {
Message.info('导出数据')
//
}
//
const openCrawlerSettings = () => {
crawlerSettingsVisible.value = true
}
const handleUploadDocument = async (file: File) => {
uploading.value = true
try {
const formData = new FormData()
formData.append('file', file)
formData.append('projectId', currentDetail.value.id.toString())
// API
const response = await uploadBiddingDocument(formData)
Message.success('文件上传成功')
currentDetail.value.biddingDocuments = response.data.fileUrl
} catch (error) {
Message.error(`文件上传失败: ${error.message}`)
} finally {
uploading.value = false
}
}
// API
const uploadBiddingDocument = (formData: FormData) => {
return new Promise((resolve) => {
setTimeout(() => {
const file = formData.get('file') as File
const projectId = formData.get('projectId')
const extension = file.name.split('.').pop()
resolve({
data: {
fileUrl: `https://your-api-domain.com/uploads/${projectId}.${extension}`
}
})
}, 1500)
})
}
onMounted(() => {
//
})
</script>
<style scoped lang="less">
.project-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 4px;
}
.search-area {
flex: 1;
}
.action-area {
display: flex;
gap: 12px;
align-items: center;
}
.tabs-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
:deep(.arco-tabs-content) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.arco-tabs-pane) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.arco-tabs-nav-tab) {
font-weight: 500;
font-size: 16px;
}
:deep(.arco-tabs-nav-tab-list) {
padding: 0 4px;
}
//
@media (max-width: 768px) {
.header-section {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.search-area {
flex: none;
}
.action-area {
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,64 @@
// 项目状态枚举
export enum ProjectStatus {
NOT_STARTED = 'not_started',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
PAUSED = 'paused',
CANCELLED = 'cancelled'
}
// 项目数据接口
export interface ProjectData {
id: number
projectName: string
unitCount: number
startDate: string
endDate: string
status: ProjectStatus
progress: number
manager: string
isMyProject: boolean
description?: string
location?: string
client?: string
budget?: number
team?: string[]
}
// 选项卡类型
export type TabKey = 'all' | 'my' | 'pending'
// 项目筛选条件
export interface ProjectFilter {
keyword?: string
status?: ProjectStatus
manager?: string
dateRange?: [string, string]
}
// 项目统计信息
export interface ProjectStats {
total: number
inProgress: number
completed: number
notStarted: number
myProjects: number
}
// 招标详情
export interface BiddingDetail {
id: number
projectName: string
biddingUnit: string
budgetAmount: number
deadline: string
crawlingTime: string
projectLocation: string
projectDuration: string
biddingScope: string
qualificationRequirements: string
sourcePlatform: string
biddingDocuments: string
biddingContent: string
contentItems?: string[]
}

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import { computed } from 'vue'
import WindTurbine from './icons/WindTurbine.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0 1 2
lat?: number
lng?: number
}
const props = defineProps<{ modelValue: Turbine }>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine): void
(e: 'map'): void
}>()
const turbine = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
/* 状态文字 & 颜色 */
const statusTextMap = { 0: '待施工', 1: '施工中', 2: '已完成' }
const statusColorMap = { 0: '#FF7D00', 1: '#165DFF', 2: '#00B42A' }
/* 点击循环切换 */
function toggleStatus() {
const next = ((turbine.value.status + 1) % 3) as 0 | 1 | 2
turbine.value = { ...turbine.value, status: next }
}
function updateNo(val: string) {
turbine.value = { ...turbine.value, turbineNo: val }
}
</script>
<template>
<div class="turbine-card">
<!-- 可点击的状态标签 -->
<div class="status-tag" :style="{ backgroundColor: statusColorMap[turbine.status] }" @click="toggleStatus">
{{ statusTextMap[turbine.status] }}
</div>
<!-- 风机图标 -->
<WindTurbine />
<!-- 机组编号输入框 -->
<a-input :model-value="turbine.turbineNo" @update:model-value="updateNo" size="small" class="turbine-input"
placeholder="编号" />
<!-- 地图选点按钮 -->
<a-button size="mini" @click="$emit('map')">
<template #icon><icon-location /></template>
</a-button>
</div>
</template>
<style scoped>
.turbine-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: var(--color-bg-2);
}
.status-tag {
position: absolute;
top: 4px;
left: 4px;
font-size: 10px;
color: #fff;
padding: 2px 4px;
border-radius: 2px;
cursor: pointer;
user-select: none;
}
.turbine-input {
margin-top: 8px;
text-align: center;
}
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { computed } from 'vue'
import TurbineCard from './TurbineCard.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2
lat?: number
lng?: number
}
const props = defineProps<{
modelValue: Turbine[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine[]): void
(e: 'map', turbine: Turbine): void
}>()
const turbines = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
function updateTurbine(index: number, t: Turbine) {
const arr = [...turbines.value]
arr.splice(index, 1, t)
turbines.value = arr
}
</script>
<template>
<a-row v-if="turbines.length" :gutter="[16, 16]" wrap>
<a-col v-for="(t, i) in turbines" :key="t.id" :xs="12" :sm="8" :md="6" :lg="4" :xl="3">
<TurbineCard v-model="turbines[i]" @update:model-value="v => updateTurbine(i, v)" @map="$emit('map', t)" />
</a-col>
</a-row>
<a-empty v-else description="请先设置项目规模" />
</template>

View File

@ -7,7 +7,9 @@
<template #icon><icon-arrow-left /></template>
</a-button>
<h2 class="ml-2">{{ projectTitle }}</h2>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{ projectData.status }}</a-tag>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{
projectData.status
}}</a-tag>
</div>
<div class="flex items-center">
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject">
@ -24,17 +26,18 @@
<a-tab-pane key="1" title="详细信息">
<a-card class="general-card" title="项目基本信息">
<a-descriptions :data="projectInfos" layout="horizontal" bordered column="3" />
<a-divider />
<div class="card-header">
<div class="card-title">项目收支情况</div>
</div>
<div class="finance-cards grid grid-cols-3 gap-4 mb-6">
<a-card class="finance-card">
<div class="finance-title">利润</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}
</div>
</a-card>
<a-card class="finance-card">
<div class="finance-title">收入</div>
@ -45,11 +48,11 @@
<div class="finance-amount text-warning">{{ finance.expense.toFixed(2) }}</div>
</a-card>
</div>
<a-descriptions title="合同金额" :data="contractInfos" layout="horizontal" bordered />
</a-card>
</a-tab-pane>
<a-tab-pane key="2" title="执行情况">
<a-card class="general-card" title="项目进度跟踪">
<div class="flex items-center justify-between mb-6">
@ -66,7 +69,7 @@
</a-button>
</div>
</div>
<a-card :bordered="false" class="mt-4">
<a-row :gutter="16" class="task-container">
<a-col :span="6" v-for="(column, index) in taskColumns" :key="index">
@ -75,38 +78,34 @@
<span class="column-title">{{ column.title }}</span>
<a-tag>{{ column.tasks.length }}</a-tag>
</div>
<div class="task-list">
<div
v-for="task in column.tasks"
:key="task.id"
class="task-card"
@click="openTaskDetail(task)"
>
<div v-for="task in column.tasks" :key="task.id" class="task-card" @click="openTaskDetail(task)">
<div class="task-card-title">{{ task.taskName }}</div>
<div class="task-card-desc text-gray-500 text-xs mt-1">{{ task.description || '暂无描述' }}</div>
<div class="task-card-footer flex items-center justify-between mt-2">
<div class="flex items-center">
<icon-calendar class="mr-1 text-gray-500" />
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{ formatDate(task.taskPeriod?.[1]) }}</span>
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{
formatDate(task.taskPeriod?.[1])
}}</span>
</div>
<div class="text-xs">
<a-progress :percent="task.progress" size="small" />
</div>
</div>
<div class="task-card-members mt-2">
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{ user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3 }}</a-avatar>
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{
user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3
}}</a-avatar>
</div>
</div>
<div
class="add-task-placeholder"
@click="handleAddTaskClick(column.status)"
>
<icon-plus />
<span>添加任务</span>
</div>
<div class="add-task-placeholder" @click="handleAddTaskClick(column.status)">
<icon-plus />
<span>添加任务</span>
</div>
</div>
</div>
</a-col>
@ -114,15 +113,11 @@
</a-card>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" title="附件">
<a-card class="general-card" title="项目附件">
<div class="attachment-section">
<a-upload
list-type="picture-card"
:file-list="attachments"
@change="handleAttachmentChange"
>
<a-upload list-type="picture-card" :file-list="attachments" @change="handleAttachmentChange">
<template #upload-button>
<div class="upload-button-content">
<icon-plus />
@ -136,13 +131,9 @@
</a-tabs>
<!-- 新增任务弹窗 -->
<a-modal
v-model:visible="addTaskModalVisible"
title="新增任务"
@cancel="resetTaskForm"
@before-ok="handleTaskSubmit"
>
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-modal v-model:visible="addTaskModalVisible" title="新增任务" @cancel="resetTaskForm" @before-ok="handleTaskSubmit">
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-form-item field="taskName" label="任务名称" required>
<a-input v-model="taskForm.taskName" placeholder="请输入" />
</a-form-item>
@ -172,13 +163,10 @@
</a-modal>
<!-- 新增任务组弹窗 -->
<a-modal
v-model:visible="addTaskGroupModalVisible"
title="新增任务组"
@cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit"
>
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-modal v-model:visible="addTaskGroupModalVisible" title="新增任务组" @cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit">
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-form-item field="groupName" label="组名称" required>
<a-input v-model="taskGroupForm.groupName" placeholder="请输入" />
</a-form-item>
@ -186,18 +174,14 @@
</a-modal>
<!-- 任务详情弹窗 -->
<a-modal
v-model:visible="taskDetailModalVisible"
title="任务详情"
@cancel="closeTaskDetail"
>
<a-modal v-model:visible="taskDetailModalVisible" title="任务详情" @cancel="closeTaskDetail">
<div v-if="currentTask">
<a-descriptions :data="taskDetailInfos" layout="horizontal" bordered />
<div class="mt-4">
<h4>任务进度</h4>
<a-progress :percent="currentTask.progress || 0" />
<a-form class="mt-4">
<a-form-item label="更新进度">
<a-input-number v-model="updateProgress" :min="0" :max="100" style="width: 100%" />
@ -289,7 +273,7 @@ const contractInfos = computed(() => [
const taskDetailInfos = computed(() => {
if (!currentTask.value) return []
return [
{ label: '任务名称', value: currentTask.value.taskName },
{ label: '任务编号', value: currentTask.value.taskCode },
@ -350,19 +334,19 @@ const fetchProjectData = async () => {
const fetchTaskData = async () => {
try {
const res = await listTask({
const res = await listTask({
projectId: projectId.value,
page: 1,
size: 100
})
//
taskColumns.value.forEach(column => {
column.tasks = []
})
const tasks = res.data?.list || []
//
tasks.forEach((task: any) => {
const column = taskColumns.value.find(col => col.status === task.status)
@ -494,10 +478,10 @@ const submitProgressUpdate = async () => {
try {
await updateTaskProgress({ progress: updateProgress.value }, currentTask.value.id)
Message.success('更新进度成功')
//
currentTask.value.progress = updateProgress.value
//
await fetchTaskData()
} catch (error) {
@ -516,33 +500,41 @@ onMounted(() => {
.finance-card {
text-align: center;
}
.finance-title {
color: #7f7f7f;
font-size: 14px;
}
.finance-amount {
font-size: 24px;
font-weight: bold;
}
.text-danger {
color: #f53f3f;
}
.text-success {
color: #00b42a;
}
.text-warning {
color: #ff7d00;
}
.task-container {
overflow-x: auto;
min-height: 600px;
}
.task-column {
background-color: #f5f5f5;
border-radius: 4px;
height: 100%;
min-height: 600px;
}
.task-column-header {
padding: 12px;
border-radius: 4px 4px 0 0;
@ -551,9 +543,11 @@ onMounted(() => {
align-items: center;
font-weight: bold;
}
.task-list {
padding: 8px;
}
.task-card {
background-color: white;
border-radius: 4px;
@ -563,16 +557,20 @@ onMounted(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s;
}
.task-card:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
}
.task-card-title {
font-weight: bold;
}
.task-card-members {
display: flex;
gap: 4px;
}
.add-task-placeholder {
display: flex;
align-items: center;
@ -584,26 +582,31 @@ onMounted(() => {
cursor: pointer;
gap: 8px;
}
.add-task-placeholder:hover {
background-color: #d9d9d9;
}
.upload-button-content {
display: flex;
flex-direction: column;
align-items: center;
color: #7f7f7f;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.general-card {
margin-bottom: 16px;
}
</style>
</style>

View File

@ -0,0 +1,358 @@
<template>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="80px" height="160px" viewBox="0 0 210 234" enable-background="new 0 0 210 234"
xml:space="preserve">
<image id="image0" width="210" height="234" x="0" y="0" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANIAAADqCAMAAADgbuuwAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACc1BMVEXx8fH29vb8+/z////+
/v74+Pjz8/Py8vLv7+/39/fs7Oz6+vrw8PD19fX5+fn9/f3q6up+fn60tLTu7u7p6ekPDw9SUVLr
6+vo6OiZmZkHBwcBAQHMzMwAAAA7Ozvn5+fk5OSrq6vj4+Pl5eUkJCRvb2/c3Nx4eHgFBQWysrIQ
EBANDQ03Nzevr68MDAxHR0cXFxeQkJAKCgoDAwMYGBjFxcUJCQlxcXEICAixsbEVFRVBQUECAgIG
BgaamprV1dVbW1sUFBTJycmNjY0TExO4uLgbGxvd3d2lpaXh4eFjY2O+vr5/f38rKytmZmZFRUUt
LS23t7epqana2tqWlpbGxsaIiIiwsLB5eXmBgYGLi4uSkpKzs7PBwcEREREODg4zMzNWVlYjIyN2
dnZYWFibm5vS0tLb29vPz884ODhAQEDe3t7U1NQcHBx7e3u2trZtbW3T09MpKSlMTEykpKTZ2dks
LCxqamq7u7teXl4/Pz9sbGzNzc1kZGQWFhaPj4+hoaF9fX3IyMgdHR1iYmK9vb2AgIDi4uLCwsLX
19fExMSOjo5LS0tdXV2npqesrKwiIiKoqKg5OTmHh4coKCi/v78vLy/g4OA2NjZoaGgxMTFPT08m
JiYhISFfX19CQkI+Pj5ycnLQ0NBpaWlhYWEeHh7Hx8dwcHBzc3OXl5fAwMA9PT1VVVWUlJR6enpr
a2tERES5ublTU1NaWlqVlZUfHx8aGhpJSUmGhoaTk5Otra2ioqLOzs6dnZ26urpcXFxlZWWgoKBN
TU2FhYWJiYkyMjIwMDDW1tZQUFCEhISMjIxXV1dOTk4lJSVISEiCgoJUVFSQ4NxnAAAAAWJLR0QD
EQxM8gAAAAd0SU1FB+kHFwcmCqQ1L2MAAEkTSURBVHjazb2LnyXbVR62d712PXZVHVxnqEMfFz2a
hqvjO2gQc9CoJbqRr/VCCeAABoRB94JsBEIgE/FKiCwhIFxQYuOA4JpHHBSQkYgBB5PEIQlxiO28
H3+S1/etvetUd5/umXsRmPPTT/f0dNfj24+1vvXcxtgkTbO8cM6Z0uTyQ5pW8jG1aTL5pN4W+Bn/
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" />
</svg>
</template>
<style scoped>
.turbine-svg {
width: 100%;
height: 100%;
pointer-events: none;
}
</style>

View File

@ -12,20 +12,12 @@
-->
<template>
<GiPageLayout>
<GiTable
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['project:create']" type="primary" @click="openAddModal">
@ -71,12 +63,7 @@
<a-space>
<a-link v-permission="['project:detail']" title="详情" @click="viewDetail(record)">详情</a-link>
<a-link v-permission="['project:update']" title="修改" @click="openEditModal(record)">修改</a-link>
<a-link
v-permission="['project:delete']"
status="danger"
title="删除"
@click="confirmDelete(record)"
>
<a-link v-permission="['project:delete']" status="danger" title="删除" @click="confirmDelete(record)">
删除
</a-link>
</a-space>
@ -84,22 +71,10 @@
</GiTable>
<!-- 新增/编辑项目弹窗 -->
<a-modal
v-model:visible="addModalVisible"
:title="modalTitle"
@cancel="resetForm"
:ok-button-props="{ loading: submitLoading }"
@ok="handleSubmit"
width="800px"
modal-class="project-form-modal"
>
<a-form
ref="formRef"
:model="form"
:rules="formRules"
layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
>
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm"
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal">
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }">
<!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16">
@ -108,6 +83,17 @@
<a-input v-model="form.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="地址">
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item>
</a-col>
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }">
<template #icon><icon-location /></template>
地图选点
</a-button></a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectManagerId" label="项目经理" required>
<a-select v-model="form.projectManagerId" placeholder="请选择项目经理" :loading="userLoading">
@ -118,10 +104,54 @@
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态" >
<a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
<!--风场名称同步业主 -->
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="业主单位联系人">
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="业主单位联系电话">
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位">
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人">
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话">
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" />
</a-form-item>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
@ -130,166 +160,57 @@
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="scale" label="项目规模" >
<a-input v-model="form.scale" placeholder="请输入项目规模" />
<a-form-item field="scale" label="项目规模">
<a-input-number v-model="form.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="startDate" label="开始时间" >
<a-form-item field="startDate" label="开始时间">
<a-date-picker v-model="form.startDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="endDate" label="结束时间" >
<a-form-item field="endDate" label="结束时间">
<a-date-picker v-model="form.endDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="coverUrl" label="项目封面">
<a-input v-model="form.coverUrl" placeholder="请输入项目封面URL" />
</a-form-item>
</a-col>
</a-row>
<!-- 委托方信息 -->
<a-divider orientation="left">委托方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位" >
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人" >
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话" >
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 检查方信息 -->
<a-divider orientation="left">检查方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="检查单位" >
<a-input v-model="form.inspectionUnit" placeholder="请输入检查单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="检查单位联系人" >
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="检查单位联系电话" >
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="farmName" label="风场名称" >
<a-input v-model="form.farmName" placeholder="请输入风场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="风场地址" >
<a-input v-model="form.farmAddress" placeholder="请输入风场地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="turbineModel" label="风机型号" >
<a-form-item field="turbineModel" label="风机型号">
<a-input v-model="form.turbineModel" placeholder="请输入风机型号" />
</a-form-item>
</a-col>
</a-row>
<!-- 项目团队 -->
<a-divider orientation="left">项目团队</a-divider>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息可视化</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="constructionTeamLeaderId" label="施工组长" >
<a-select v-model="form.constructionTeamLeaderId" placeholder="请选择施工组长" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="constructorIds" label="施工人员" >
<a-select v-model="form.constructorIds" multiple placeholder="请选择施工人员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="qualityOfficerId" label="质量员" >
<a-select v-model="form.qualityOfficerId" placeholder="请选择质量员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="auditorId" label="安全员" >
<a-select v-model="form.auditorId" placeholder="请选择安全员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
<a-col :span="24">
<a-form-item label="机组网格布局">
<a-space direction="vertical" style="width: 100%">
<TurbineGrid v-model:="form.turbineList"></TurbineGrid>
</a-space>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="middle">地图</a-divider>
</a-form>
</a-modal>
<!-- 导入项目弹窗 -->
<a-modal
v-model:visible="importModalVisible"
title="导入文件"
@cancel="handleCancelImport"
@before-ok="handleImport"
>
<a-modal v-model:visible="importModalVisible" title="导入文件" @cancel="handleCancelImport" @before-ok="handleImport">
<div class="flex flex-col items-center justify-center p-8">
<div class="text-primary text-4xl mb-4">
<icon-file-upload />
</div>
<div class="text-lg font-medium mb-2">批量导入文件</div>
<div class="text-gray-500 mb-4">拖动文件到此处或点击下方按钮上传</div>
<a-upload
:file-list="fileList"
:limit="1"
@change="handleFileChange"
>
<a-upload :file-list="fileList" :limit="1" @change="handleFileChange">
<template #upload-button>
<a-button type="primary">选择文件</a-button>
</template>
@ -311,7 +232,7 @@ import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' })
// (API)
@ -324,7 +245,7 @@ const PROJECT_STATUS = {
//
const PROJECT_STATUS_MAP = {
0: '待施工',
1: '施工中',
1: '施工中',
2: '已完成'
} as const
@ -379,11 +300,11 @@ const queryFormColumns: ColumnItem[] = reactive([
},
{
type: 'input',
label: '风场名称',
field: 'fieldName', // 使fieldNameAPI使
label: '业主',
field: 'inspectionUnit',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入风场名称',
placeholder: '请输入业主名称',
},
},
{
@ -405,21 +326,22 @@ const form = reactive({
client: '', //
clientContact: '', //
clientPhone: '', //
inspectionUnit: '', //
inspectionContact: '', //
inspectionPhone: '', //
farmName: '', //
farmAddress: '', //
scale: '', //
inspectionUnit: '', //
inspectionContact: '', //
inspectionPhone: '', //
farmName: '', //
farmAddress: '', //
scale: '', //
turbineModel: '', //
status: 0, // 01234
status: '', // 01234
startDate: '', //
endDate: '', //
coverUrl: '', //
coverUrl: '', //
constructionTeamLeaderId: '', // id
constructorIds: '', // id
qualityOfficerId: '', // id
auditorId: '' // id
auditorId: '', // id
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[], //
})
const pagination = reactive({
@ -430,7 +352,9 @@ const pagination = reactive({
showJumper: true,
showPageSize: true
})
const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
}
const tableColumns = ref<TableColumnData[]>([
{
title: '序号',
@ -439,91 +363,91 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => rowIndex + 1 + (pagination.current - 1) * pagination.pageSize,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '风场名称/风场地址',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '地点',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
},
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '检查单位',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '业主',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '检查单位联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '业主联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '操作',
@ -533,7 +457,20 @@ const tableColumns = ref<TableColumnData[]>([
fixed: !isMobile() ? 'right' : undefined,
},
])
watch(() => form.scale, (newVal) => {
const count = Number(newVal)
if (count > 0 && count <= 999) {
form.turbineList = Array.from({ length: count }, (_, i) => ({
id: i + 1,
turbineNo: `${String(i + 1).padStart(3, '0')}`,
lat: undefined,
lng: undefined,
status: form.status,
}))
} else {
form.turbineList = []
}
}, { immediate: true })
const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目')
const getStatusColor = (status: number) => {
@ -557,37 +494,37 @@ const fetchData = async () => {
page: pagination.current,
size: pagination.pageSize
}
const res = await listProject(params)
if (res.success && res.data) {
// API
const projects = Array.isArray(res.data) ? res.data : []
//
dataList.value = projects.map((item: any) => {
const mappedItem: T.ProjectResp = {
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
projectScale: item.scale,
//
//
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : []
};
return mappedItem;
})
// APItotal使
// total
pagination.total = projects.length
//
if (projects.length < pagination.pageSize) {
pagination.total = (pagination.current - 1) * pagination.pageSize + projects.length
@ -619,7 +556,7 @@ const reset = () => {
fieldName: '', // 使fieldNameAPI使
status: undefined,
})
//
pagination.current = 1
search()
@ -661,7 +598,7 @@ const resetForm = () => {
qualityOfficerId: '', // id
auditorId: '' // id
})
isEdit.value = false
currentId.value = null
}
@ -674,13 +611,13 @@ const openAddModal = () => {
const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
//
Object.keys(form).forEach(key => {
// @ts-ignore
form[key] = ''
})
//
Object.keys(form).forEach(key => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
@ -688,7 +625,7 @@ const openEditModal = (record: T.ProjectResp) => {
form[key] = record[key as keyof T.ProjectResp]
}
})
//
if (record.farmName) form.farmName = record.farmName
if (record.farmAddress) form.farmAddress = record.farmAddress
@ -697,11 +634,11 @@ const openEditModal = (record: T.ProjectResp) => {
if (record.clientPhone) form.clientPhone = record.clientPhone
if (record.turbineModel) form.turbineModel = record.turbineModel
if (record.scale) form.scale = record.scale
//
if (record.startDate) form.startDate = record.startDate
if (record.endDate) form.endDate = record.endDate
addModalVisible.value = true
}
@ -716,12 +653,12 @@ const submitLoading = ref(false)
const handleSubmit = async () => {
console.log('表单提交开始', form)
submitLoading.value = true
try {
//
console.log('开始验证表单', formRef.value)
await formRef.value.validate()
//
const submitData = {
...form,
@ -735,7 +672,7 @@ const handleSubmit = async () => {
}
console.log('提交数据:', submitData)
let res
if (isEdit.value && currentId.value) {
//
@ -746,18 +683,19 @@ const handleSubmit = async () => {
//
console.log('新增模式提交')
res = await addProject(submitData)
Message.success('添加成功')
}
console.log('API响应结果:', res)
//
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
submitLoading.value = false
return
}
addModalVisible.value = false
fetchData()
} catch (error: any) {
@ -787,16 +725,16 @@ const deleteItem = async (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
try {
const res = await deleteProject(projectId)
//
if (res && res.success === false) {
Message.error(res.msg || '删除失败')
return
}
Message.success('删除成功')
fetchData()
} catch (error) {
@ -811,7 +749,7 @@ const viewDetail = (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
router.push({
name: 'ProjectDetail',
params: {
@ -839,24 +777,24 @@ const handleImport = async () => {
Message.warning('请选择文件')
return false
}
try {
const fileItem = fileList.value[0] as any
const file = fileItem?.file || fileItem
if (!file) {
Message.warning('请选择有效的文件')
return false
}
// API
const res = await importProject(file)
if (res && res.success === false) {
Message.error(res.msg || '导入失败')
return false
}
Message.success('导入成功')
handleCancelImport()
fetchData() //
@ -875,7 +813,7 @@ const exportData = async () => {
status: searchForm.status,
fieldName: searchForm.fieldName, // 使fieldNameAPI使
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
@ -922,11 +860,11 @@ onMounted(() => {
.arco-form-item {
margin-bottom: 16px;
}
.arco-divider {
margin: 16px 0;
font-weight: 500;
color: var(--color-text-2);
}
}
</style>
</style>

View File

@ -0,0 +1,439 @@
<template>
<div class="process-management">
<a-card title="制度公示管理" :bordered="false">
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="提案标题">
<a-input
v-model="searchForm.title"
placeholder="请输入提案标题"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="提案人">
<a-input
v-model="searchForm.proposer"
placeholder="请输入提案人"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 150px"
>
<a-option value="">全部</a-option>
<a-option value="PUBLISHED">已公告</a-option>
<a-option value="APPROVED">已公示</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="search">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #toolbar-left>
<span class="search-result-info">
共找到 <strong>{{ pagination.total }}</strong> 条记录
</span>
</template>
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template #level="{ record }">
<a-tag :color="getLevelColor(record.level)">
{{ getLevelText(record.level) }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button
v-if="record.status === RegulationStatus.PUBLISHED"
type="text"
size="small"
disabled
>
已公告
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 提案详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="提案详情"
width="800px"
:footer="false"
>
<div class="proposal-detail" v-if="currentProposal">
<div class="detail-header">
<h3>{{ currentProposal.title }}</h3>
<div class="detail-meta">
<span>提案人: {{ currentProposal.createByName }}</span>
<span>提案类型: {{ currentProposal.regulationType }}</span>
<span>适用范围: {{ currentProposal.scope }}</span>
<span>级别: <a-tag :color="getLevelColor(currentProposal.level)">{{ getLevelText(currentProposal.level) }}</a-tag></span>
<span>创建时间: {{ formatDate(currentProposal.createTime) }}</span>
<span>状态: <a-tag :color="getStatusColor(currentProposal.status)">{{ getStatusText(currentProposal.status) }}</a-tag></span>
</div>
</div>
<a-divider />
<div class="detail-content">
<h4>提案内容</h4>
<div class="content-text">{{ currentProposal.content }}</div>
<h4>备注</h4>
<div class="content-text">{{ currentProposal.remark || '暂无备注' }}</div>
</div>
<a-divider />
<div class="detail-footer">
<a-button
v-if="currentProposal.status === RegulationStatus.PUBLISHED"
disabled
>
已自动公示
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import { useRegulationStore } from '@/stores/modules/regulation'
import { regulationApi } from '@/apis/regulation'
import {
RegulationStatus,
RegulationLevel,
type Regulation,
type RegulationType
} from '@/apis/regulation/type'
defineOptions({ name: 'ProcessManagement' })
//
const columns = [
{ title: '提案标题', dataIndex: 'title', key: 'title', width: 200 },
{ title: '提案人', dataIndex: 'createByName', key: 'createByName', width: 120 },
{ title: '提案类型', dataIndex: 'regulationType', key: 'regulationType', width: 120 },
{ title: '状态', dataIndex: 'status', key: 'status', slotName: 'status', width: 100 },
{ title: '级别', dataIndex: 'level', key: 'level', slotName: 'level', width: 80 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
{ title: '操作', key: 'operations', slotName: 'operations', width: 150 }
]
//
const searchForm = reactive({
title: '',
proposer: '',
status: ''
})
//
const tableData = ref<Regulation[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
const currentProposal = ref<Regulation | null>(null)
//
const detailModalVisible = ref(false)
//
const currentUser = ref('管理者') // ID
// store
const regulationStore = useRegulationStore()
//
const getStatusColor = (status: RegulationStatus) => {
const colors = {
[RegulationStatus.PUBLISHED]: 'purple',
[RegulationStatus.APPROVED]: 'green',
}
return colors[status] || 'purple'
}
//
const getStatusText = (status: RegulationStatus) => {
const texts = {
[RegulationStatus.PUBLISHED]: '已公告',
[RegulationStatus.APPROVED]: '已公示',
}
return texts[status] || '已公告'
}
//
const formatDate = (dateString: string) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
//
const getLevelColor = (level: RegulationLevel) => {
const colors = {
[RegulationLevel.LOW]: 'blue',
[RegulationLevel.MEDIUM]: 'orange',
[RegulationLevel.HIGH]: 'red'
}
return colors[level] || 'blue'
}
//
const getLevelText = (level: RegulationLevel) => {
const texts = {
[RegulationLevel.LOW]: '低',
[RegulationLevel.MEDIUM]: '中',
[RegulationLevel.HIGH]: '高'
}
return texts[level] || '中'
}
// - 使
const getTableData = async () => {
loading.value = true
try {
//
const requestParams: any = {
page: pagination.current,
size: pagination.pageSize,
title: searchForm.title || undefined,
proposer: searchForm.proposer || undefined
}
if (searchForm.status) {
//
requestParams.status = searchForm.status
const response = await regulationApi.getRegulationList(requestParams)
if (response.status === 200) {
tableData.value = response.data.records
pagination.total = response.data.total
pagination.current = response.data.current
} else {
Message.error('搜索失败')
}
} else {
//
//
const [publishedResponse, approvedResponse] = await Promise.all([
regulationApi.getRegulationList({
...requestParams,
status: RegulationStatus.PUBLISHED
}),
regulationApi.getRegulationList({
...requestParams,
status: RegulationStatus.APPROVED
})
])
if (publishedResponse.status === 200 && approvedResponse.status === 200) {
//
const allRecords = [
...publishedResponse.data.records,
...approvedResponse.data.records
]
//
allRecords.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
tableData.value = allRecords
pagination.total = allRecords.length
pagination.current = 1
} else {
Message.error('搜索失败')
}
}
} catch (error) {
console.error('搜索制度公示失败:', error)
Message.error('搜索失败')
} finally {
loading.value = false
}
}
//
const search = () => {
pagination.current = 1
getTableData()
}
//
const reset = () => {
Object.assign(searchForm, {
title: '',
proposer: '',
status: ''
})
pagination.current = 1
getTableData()
}
//
const handleView = (record: Regulation) => {
currentProposal.value = record
detailModalVisible.value = true
}
//
const handlePageChange = (page: number) => {
pagination.current = page
getTableData()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.process-management {
.arco-card {
margin-bottom: 16px;
}
}
.search-container {
padding: 16px;
background: var(--color-bg-2);
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.arco-form-item {
margin-bottom: 0;
margin-right: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-right: 8px;
}
}
.arco-input,
.arco-select {
border-radius: 4px;
}
.arco-btn {
border-radius: 4px;
}
}
}
.search-result-info {
color: var(--color-text-2);
font-size: 14px;
strong {
color: var(--color-text-1);
font-weight: 600;
}
}
.proposal-detail {
.detail-header {
margin-bottom: 16px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.detail-meta {
display: flex;
gap: 16px;
color: var(--color-text-3);
font-size: 14px;
flex-wrap: wrap;
}
}
.detail-content {
h4 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 500;
}
.content-text {
color: var(--color-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
}
}
.detail-footer {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<GiPageLayout
:margin="true"
:default-collapsed="false"
:header-style="isDesktop ? { padding: 0, borderBottomWidth: 0 } : { borderBottomWidth: '1px' } "
>
<template v-if="isDesktop" #left>
<a-tabs v-model:active-key="activeKey" type="rounded" position="left" hide-content size="large" @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key">
<template #title>
<div style="display: flex; align-items: center">
<GiSvgIcon :name="item.icon" :size="18" style="margin-right: 4px" />
{{ item.name }}
</div>
</template>
</a-tab-pane>
</a-tabs>
</template>
<template #header>
<a-tabs v-if="!isDesktop" v-model:active-key="activeKey" type="rounded" position="top" size="large" @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name">
<template #title>
<div style="display: flex; align-items: center">
<GiSvgIcon :name="item.icon" :size="18" style="margin-right: 4px" />
{{ item.name }}
</div>
</template>
</a-tab-pane>
</a-tabs>
</template>
<transition name="fade-slide" mode="out-in" appear>
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
</transition>
</GiPageLayout>
</template>
<script setup lang="tsx">
import { useRoute, useRouter } from 'vue-router'
import SystemRegulation from './repository.vue'
import RegulationProposal from './proposal/index.vue'
import RegulationType from './type/index.vue'
import ProcessManagement from './confirm.vue'
import { useDevice } from '@/hooks'
defineOptions({ name: 'RegulationManagement' })
const { isDesktop } = useDevice()
const data = [
{ name: '制度公告', key: 'system-regulation', icon: 'file-text', value: SystemRegulation, path: '/regulation/system-regulation' },
{ name: '制度提案', key: 'proposal', icon: 'edit', value: RegulationProposal, path: '/regulation/proposal' },
{ name: '制度公示', key: 'process-management', icon: 'workflow', value: ProcessManagement, path: '/regulation/process-management' },
{ name: '制度类型', key: 'type', icon: 'tag', value: RegulationType, path: '/regulation/type' },
]
const menuList = computed(() => {
return data
})
const route = useRoute()
const router = useRouter()
//
const activeKey = computed(() => {
const currentPath = route.path
const menuItem = menuList.value.find(item => item.path === currentPath)
return menuItem ? menuItem.key : menuList.value[0].key
})
watch(
() => route.path,
() => {
// activeKey
},
{ immediate: true },
)
const change = (key: string | number) => {
const menuItem = menuList.value.find(item => item.key === key)
if (menuItem) {
router.push(menuItem.path)
}
}
</script>
<style scoped lang="scss">
.gi_page {
padding-top: 0;
}
:deep(.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab) {
margin: 0;
padding: 8px 16px;
&:hover {
background: var(--color-fill-1);
.arco-tabs-tab-title {
&::before {
display: none !important;
}
}
}
&.arco-tabs-tab-active {
background: rgba(var(--primary-6), 0.08);
}
}
</style>

View File

@ -0,0 +1,907 @@
<template>
<div class="regulation-proposal">
<a-card title="制度提案管理" :bordered="false">
<template #extra>
<a-button type="primary" @click="handleAdd">
<template #icon>
<GiSvgIcon name="plus" />
</template>
新增制度提案
</a-button>
</template>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="提案标题">
<a-input
v-model="searchForm.title"
placeholder="请输入提案标题"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="提案人">
<a-input
v-model="searchForm.proposer"
placeholder="请输入提案人"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 150px"
>
<a-option value="">全部</a-option>
<a-option value="DRAFT">草稿</a-option>
<a-option value="PUBLISHED">已公告</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="search">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #toolbar-left>
<span class="search-result-info">
共找到 <strong>{{ pagination.total }}</strong> 条记录
</span>
</template>
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template #level="{ record }">
<a-tag :color="getLevelColor(record.level)">
{{ getLevelText(record.level) }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button
v-if="record.status === RegulationStatus.DRAFT"
type="text"
size="small"
@click="handleEdit(record)"
>
编辑
</a-button>
<a-button
v-if="record.status === RegulationStatus.DRAFT"
type="primary"
size="small"
@click="handleConfirm(record)"
>
确认公示
</a-button>
<a-button
v-if="record.status !== RegulationStatus.DRAFT"
type="text"
size="small"
disabled
>
已公示
</a-button>
<a-popconfirm
v-if="record.status === RegulationStatus.DRAFT"
content="确定要删除这个提案吗?"
@ok="handleDelete(record)"
>
<a-button type="text" size="small" status="danger">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-card>
<!-- 提案表单弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="900px"
@ok="handleSubmit"
@cancel="handleCancel"
class="proposal-form-modal"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
class="proposal-form"
>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="提案标题" field="title" class="form-item">
<a-input
v-model="formData.title"
placeholder="请输入提案标题"
class="form-input"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="提案类型" field="regulationType" class="form-item">
<a-select
v-model="formData.regulationType"
placeholder="请选择提案类型"
class="form-select"
allow-clear
>
<a-option
v-for="type in regulationTypes"
:key="type.typeId"
:value="type.typeName"
:disabled="type.isEnabled === '0'"
>
{{ type.typeName }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="适用范围" field="scope" class="form-item">
<a-input
v-model="formData.scope"
placeholder="请输入适用范围"
class="form-input"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="制度级别" field="level" class="form-item">
<a-select
v-model="formData.level"
placeholder="请选择制度级别"
class="form-select"
allow-clear
>
<a-option :value="RegulationLevel.LOW"></a-option>
<a-option :value="RegulationLevel.MEDIUM"></a-option>
<a-option :value="RegulationLevel.HIGH"></a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="提案内容" field="content" class="form-item">
<a-textarea
v-model="formData.content"
placeholder="请详细描述提案的具体内容,包括制度的目的、适用范围、具体规定等"
:rows="8"
class="form-textarea"
allow-clear
/>
</a-form-item>
<a-form-item label="备注" field="remark" class="form-item">
<a-textarea
v-model="formData.remark"
placeholder="请输入其他补充说明(可选)"
:rows="3"
class="form-textarea"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 提案详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="提案详情"
width="800px"
:footer="false"
>
<div class="proposal-detail" v-if="currentProposal">
<div class="detail-header">
<h3>{{ currentProposal.title }}</h3>
<div class="detail-meta">
<span>提案人: {{ currentProposal.createByName }}</span>
<span>提案类型: {{ currentProposal.regulationType }}</span>
<span>适用范围: {{ currentProposal.scope }}</span>
<span>级别: <a-tag :color="getLevelColor(currentProposal.level)">{{ getLevelText(currentProposal.level) }}</a-tag></span>
<span>创建时间: {{ formatDate(currentProposal.createTime) }}</span>
<span>状态: <a-tag :color="getStatusColor(currentProposal.status)">{{ getStatusText(currentProposal.status) }}</a-tag></span>
</div>
</div>
<a-divider />
<div class="detail-content">
<h4>提案内容</h4>
<div class="content-text">{{ currentProposal.content }}</div>
<h4>备注</h4>
<div class="content-text">{{ currentProposal.remark || '暂无备注' }}</div>
</div>
<a-divider />
<div class="detail-footer">
<a-button
v-if="currentProposal.status === RegulationStatus.DRAFT && currentProposal.createByName === currentUser"
type="primary"
@click="handleConfirm(currentProposal)"
>
确认公示
</a-button>
<a-button @click="detailModalVisible = false">
关闭
</a-button>
</div>
</div>
</a-modal>
<!-- 确认公示弹窗 -->
<a-modal
v-model:visible="confirmModalVisible"
title="确认公示提案"
width="600px"
@ok="submitConfirm"
@cancel="confirmModalVisible = false"
>
<div class="confirm-content">
<div class="confirm-header">
<GiSvgIcon name="exclamation-circle" style="color: #faad14; font-size: 24px; margin-right: 8px;" />
<span style="font-weight: 500; font-size: 16px;">公示确认</span>
</div>
<div class="confirm-body">
<p>您即将公示提案<strong>{{ currentProposal?.title }}</strong></p>
<p>公示后</p>
<ul>
<li>该提案将进入制度公示页面</li>
<li>其他用户可以在制度公示页面查看此提案</li>
<li>提案将进入正式的公示流程</li>
<li>您将无法再编辑此提案</li>
</ul>
</div>
<div class="confirm-footer">
<a-checkbox v-model="agreeDisplay">
我已仔细阅读并确认公示此提案
</a-checkbox>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import { useRegulationStore } from '@/stores/modules/regulation'
import { regulationApi } from '@/apis/regulation'
import {
RegulationStatus,
RegulationLevel,
type Regulation,
type RegulationType,
type RegulationProposalSearchRequest
} from '@/apis/regulation/type'
defineOptions({ name: 'RegulationProposal' })
//
const columns = [
{ title: '提案标题', dataIndex: 'title', key: 'title', width: 220 },
{ title: '提案人', dataIndex: 'createByName', key: 'createByName', width: 130 },
{ title: '提案类型', dataIndex: 'regulationType', key: 'regulationType', width: 130 },
{ title: '状态', dataIndex: 'status', key: 'status', slotName: 'status', width: 110 },
{ title: '级别', dataIndex: 'level', key: 'level', slotName: 'level', width: 90 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 },
{ title: '操作', key: 'operations', slotName: 'operations', width: 320 }
]
//
const searchForm = reactive({
title: '',
proposer: '',
status: ''
})
//
const tableData = ref<Regulation[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const modalVisible = ref(false)
const modalTitle = ref('新增制度提案')
const formRef = ref()
const formData = reactive({
regulationId: '',
title: '',
regulationType: '',
content: '',
scope: '',
level: RegulationLevel.MEDIUM,
remark: ''
})
const currentProposal = ref<Regulation | null>(null)
const detailModalVisible = ref(false)
const confirmModalVisible = ref(false)
const agreeDisplay = ref(false)
//
const currentUser = ref('管理者') // ID
// store
const regulationStore = useRegulationStore()
//
const regulationTypes = ref<RegulationType[]>([])
//
const rules = {
title: [{ required: true, message: '请输入提案标题' }],
regulationType: [{ required: true, message: '请选择提案类型' }],
content: [{ required: true, message: '请输入提案内容' }],
scope: [{ required: true, message: '请输入适用范围' }]
}
//
const getStatusColor = (status: RegulationStatus) => {
const colors = {
[RegulationStatus.DRAFT]: 'blue',
[RegulationStatus.VOTING]: 'orange',
[RegulationStatus.REJECTED]: 'red',
[RegulationStatus.PUBLISHED]: 'purple',
[RegulationStatus.APPROVED]: 'green',
[RegulationStatus.ARCHIVED]: 'gray'
}
return colors[status] || 'blue'
}
//
const getStatusText = (status: RegulationStatus) => {
const texts = {
[RegulationStatus.DRAFT]: '草稿',
[RegulationStatus.VOTING]: '投票中',
[RegulationStatus.REJECTED]: '已否决',
[RegulationStatus.PUBLISHED]: '已公告',
[RegulationStatus.APPROVED]: '已公示',
[RegulationStatus.ARCHIVED]: '已归档'
}
return texts[status] || '草稿'
}
//
const formatDate = (dateString: string) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
//
const getLevelColor = (level: RegulationLevel) => {
const colors = {
[RegulationLevel.LOW]: 'blue',
[RegulationLevel.MEDIUM]: 'orange',
[RegulationLevel.HIGH]: 'red'
}
return colors[level] || 'blue'
}
//
const getLevelText = (level: RegulationLevel) => {
const texts = {
[RegulationLevel.LOW]: '低',
[RegulationLevel.MEDIUM]: '中',
[RegulationLevel.HIGH]: '高'
}
return texts[level] || '中'
}
//
const getRegulationTypes = async () => {
try {
const response = await regulationApi.searchRegulationTypes()
if (response.status === 200) {
regulationTypes.value = response.data.records || response.data
}
} catch (error) {
console.error('获取制度类型列表失败:', error)
}
}
// - 使
const getTableData = async () => {
loading.value = true
try {
const response = await regulationApi.getRegulationList({
page: pagination.current,
size: pagination.pageSize,
title: searchForm.title || undefined,
proposer: searchForm.proposer || undefined,
status: searchForm.status || undefined
})
if (response.status === 200) {
tableData.value = response.data.records
pagination.total = response.data.total
pagination.current = response.data.current
} else {
Message.error('搜索失败')
}
} catch (error) {
console.error('搜索制度提案失败:', error)
Message.error('搜索失败')
} finally {
loading.value = false
}
}
//
const search = () => {
pagination.current = 1
getTableData()
}
//
const reset = () => {
Object.assign(searchForm, {
title: '',
proposer: '',
status: ''
})
pagination.current = 1
getTableData()
}
//
const handleAdd = () => {
modalTitle.value = '新增制度提案'
modalVisible.value = true
resetForm()
}
//
const handleEdit = (record: Regulation) => {
modalTitle.value = '编辑制度提案'
modalVisible.value = true
Object.assign(formData, {
regulationId: record.regulationId,
title: record.title,
regulationType: record.regulationType,
content: record.content,
scope: record.scope,
level: record.level,
remark: record.remark
})
}
//
const handleView = (record: Regulation) => {
currentProposal.value = record
detailModalVisible.value = true
}
//
const handleConfirm = (record: Regulation) => {
currentProposal.value = record
confirmModalVisible.value = true
agreeDisplay.value = false
}
//
const handleDelete = async (record: Regulation) => {
try {
await regulationApi.deleteProposal(record.regulationId)
Message.success('删除成功')
getTableData()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
//
const submitConfirm = async () => {
if (!agreeDisplay.value) {
Message.warning('请先确认公示此提案')
return
}
try {
if (currentProposal.value) {
console.log('准备公示提案:', currentProposal.value.regulationId)
// 使API
const response = await regulationApi.publishRegulation(currentProposal.value.regulationId)
console.log('公示响应:', response)
Message.success('提案公示成功,已进入公示流程')
confirmModalVisible.value = false
getTableData() //
//
if (detailModalVisible.value) {
detailModalVisible.value = false
}
}
} catch (error) {
console.error('公示失败:', error)
console.error('错误详情:', error.response || error)
Message.error('公示失败')
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (formData.regulationId) {
//
await regulationApi.updateProposal(formData.regulationId, {
title: formData.title,
regulationType: formData.regulationType,
content: formData.content,
scope: formData.scope,
level: formData.level,
remark: formData.remark
})
Message.success('更新成功')
} else {
//
const createData = {
title: formData.title,
regulationType: formData.regulationType,
content: formData.content,
scope: formData.scope,
level: formData.level,
remark: formData.remark,
createByName: currentUser.value
}
console.log('新增提案数据:', createData)
const response = await regulationApi.createProposal(createData)
console.log('新增提案响应:', response)
Message.success('提案提交成功')
}
modalVisible.value = false
getTableData()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败')
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(formData, {
regulationId: '',
title: '',
regulationType: '',
content: '',
scope: '',
level: RegulationLevel.MEDIUM,
remark: ''
})
formRef.value?.resetFields()
}
//
const handlePageChange = (page: number) => {
pagination.current = page
getTableData()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
onMounted(() => {
getTableData()
getRegulationTypes()
})
</script>
<style scoped lang="scss">
.regulation-proposal {
.arco-card {
margin-bottom: 16px;
}
}
.search-container {
padding: 16px;
background: var(--color-bg-2);
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.arco-form-item {
margin-bottom: 0;
margin-right: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-right: 8px;
}
}
.arco-input,
.arco-select {
border-radius: 4px;
}
.arco-btn {
border-radius: 4px;
}
}
}
.search-result-info {
color: var(--color-text-2);
font-size: 14px;
strong {
color: var(--color-text-1);
font-weight: 600;
}
}
//
.proposal-form-modal {
.arco-modal-body {
padding: 24px 32px;
}
.arco-modal-header {
padding: 20px 32px 16px;
border-bottom: 1px solid var(--color-border);
.arco-modal-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
}
.arco-modal-footer {
padding: 16px 32px 24px;
border-top: 1px solid var(--color-border);
}
}
.proposal-form {
.form-item {
margin-bottom: 24px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-bottom: 8px;
font-size: 14px;
&::before {
color: #ff4d4f;
margin-right: 4px;
}
}
.arco-form-item-error {
margin-top: 6px;
font-size: 12px;
}
}
.form-input,
.form-select {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-input-focus,
&.arco-select-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
.arco-input-inner {
padding: 8px 12px;
font-size: 14px;
}
}
.form-textarea {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-textarea-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
.arco-textarea-inner {
padding: 12px;
font-size: 14px;
line-height: 1.6;
}
}
//
.arco-select {
.arco-select-view {
padding: 8px 12px;
font-size: 14px;
}
.arco-select-arrow {
color: var(--color-text-3);
}
}
//
.arco-input-inner::placeholder,
.arco-textarea-inner::placeholder {
color: var(--color-text-3);
font-size: 14px;
}
//
.arco-input-clear-btn,
.arco-textarea-clear-btn {
color: var(--color-text-3);
&:hover {
color: var(--color-text-2);
}
}
}
.proposal-detail {
.detail-header {
margin-bottom: 16px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.detail-meta {
display: flex;
gap: 16px;
color: var(--color-text-3);
font-size: 14px;
flex-wrap: wrap;
}
}
.detail-content {
h4 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 500;
}
.content-text {
color: var(--color-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
}
}
.detail-footer {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
}
}
.confirm-content {
.confirm-header {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border);
}
.confirm-body {
margin-bottom: 20px;
p {
margin-bottom: 12px;
color: var(--color-text-1);
line-height: 1.6;
}
ul {
margin: 12px 0;
padding-left: 20px;
li {
margin-bottom: 8px;
color: var(--color-text-2);
line-height: 1.5;
}
}
}
.confirm-footer {
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
}
</style>

View File

@ -0,0 +1,467 @@
<template>
<div class="system-regulation">
<a-card title="已公告制度确认" :bordered="false">
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="制度标题">
<a-input
v-model="searchForm.title"
placeholder="请输入制度标题"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="公示人">
<a-input
v-model="searchForm.proposer"
placeholder="请输入公示人"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="确认状态">
<a-select
v-model="searchForm.confirmStatus"
placeholder="请选择状态"
allow-clear
style="width: 150px"
>
<a-option value="">全部</a-option>
<a-option value="CONFIRMED">已确认</a-option>
<a-option value="PENDING">待确认</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="search">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #toolbar-left>
<span class="search-result-info">
共找到 <strong>{{ pagination.total }}</strong> 条记录
</span>
</template>
<template #confirmStatus="{ record }">
<a-tag :color="record.confirmStatus === 'CONFIRMED' ? 'green' : 'orange'">
{{ record.confirmStatus === 'CONFIRMED' ? '已确认' : '待确认' }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button type="text" size="small" @click="handleDownload(record)">
下载
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 制度详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="制度详情"
width="800px"
:footer="false"
>
<div class="regulation-detail" v-if="currentRegulation">
<div class="detail-header">
<h3>{{ currentRegulation.title }}</h3>
<div class="detail-meta">
<span>公示人: {{ currentRegulation.createByName }}</span>
<span>公示时间: {{ currentRegulation.publishTime }}</span>
<span>生效日期: {{ currentRegulation.effectiveTime }}</span>
<span>确认状态:
<a-tag :color="currentRegulation.confirmStatus === 'CONFIRMED' ? 'green' : 'orange'">
{{ currentRegulation.confirmStatus === 'CONFIRMED' ? '已确认' : '待确认' }}
</a-tag>
</span>
</div>
</div>
<a-divider />
<div class="detail-content">
<h4>制度内容</h4>
<div class="content-text">{{ currentRegulation.content }}</div>
<h4>适用范围</h4>
<div class="content-text">{{ currentRegulation.scope }}</div>
<h4>实施要求</h4>
<div class="content-text">{{ currentRegulation.requirements }}</div>
<h4>注意事项</h4>
<div class="content-text">{{ currentRegulation.notes }}</div>
</div>
<a-divider />
<div class="detail-footer">
<a-button
v-if="currentRegulation.confirmStatus !== 'CONFIRMED'"
type="primary"
@click="handleConfirm(currentRegulation)"
>
确认知晓并遵守
</a-button>
<a-button
v-else
type="primary"
disabled
>
已确认知晓
</a-button>
<a-button @click="handleDownload(currentRegulation)">
下载制度文件
</a-button>
</div>
</div>
</a-modal>
<!-- 确认承诺弹窗 -->
<a-modal
v-model:visible="confirmModalVisible"
title="制度确认承诺"
width="600px"
@ok="submitConfirm"
@cancel="confirmModalVisible = false"
>
<div class="confirm-content">
<p>我确认已仔细阅读并理解{{ currentRegulation?.title }}的全部内容承诺</p>
<ul>
<li>严格遵守该制度的各项规定</li>
<li>认真履行相关职责和义务</li>
<li>如有违反愿意承担相应责任</li>
</ul>
<a-checkbox v-model="agreeTerms">
我已阅读并同意上述承诺
</a-checkbox>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { Regulation } from '@/apis/regulation/type'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import { useRegulationStore } from '@/stores/modules/regulation'
import { regulationApi } from '@/apis/regulation'
import { PDFGenerator } from '@/utils/pdfGenerator'
defineOptions({ name: 'SystemRegulation' })
//
const columns = [
{ title: '制度名称', dataIndex: 'title', key: 'title' },
{ title: '制度类型', dataIndex: 'regulationType', key: 'regulationType' },
{ title: '公示人', dataIndex: 'createByName', key: 'createByName' },
{ title: '公示时间', dataIndex: 'publishTime', key: 'publishTime' },
{ title: '生效日期', dataIndex: 'effectiveTime', key: 'effectiveTime' },
{ title: '确认状态', dataIndex: 'confirmStatus', key: 'confirmStatus', slotName: 'confirmStatus' },
{ title: '操作', key: 'operations', slotName: 'operations', width: 250 }
]
//
const tableData = ref<Regulation[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const searchForm = reactive({
title: '',
proposer: '',
confirmStatus: ''
})
//
const detailModalVisible = ref(false)
const confirmModalVisible = ref(false)
const currentRegulation = ref<Regulation | null>(null)
const agreeTerms = ref(false)
// store
const regulationStore = useRegulationStore()
// - 使
const getTableData = async () => {
loading.value = true
try {
const response = await regulationApi.getRegulationList({
page: pagination.current,
size: pagination.pageSize,
status: "PUBLISHED",
title: searchForm.title || undefined,
proposer: searchForm.proposer || undefined,
confirmStatus: searchForm.confirmStatus || undefined
})
if (response.status === 200) {
// confirmStatus
const records = response.data.records.map((record: any) => ({
...record,
confirmStatus: record.confirmStatus || 'PENDING'
}))
tableData.value = records
pagination.pageSize = response.data.size
pagination.current = response.data.current
pagination.total = response.data.total
} else {
Message.error('搜索失败')
}
} catch (error) {
console.error('搜索已公告制度失败:', error)
Message.error('搜索失败')
} finally {
loading.value = false
}
}
//
const search = () => {
pagination.current = 1
getTableData()
}
//
const reset = () => {
Object.assign(searchForm, {
title: '',
proposer: '',
confirmStatus: ''
})
pagination.current = 1
getTableData()
}
//
const onPageChange = (page: number) => {
pagination.current = page
getTableData()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
//
const handleView = (record: any) => {
currentRegulation.value = record
detailModalVisible.value = true
}
//
const handleConfirm = (record: any) => {
currentRegulation.value = record
confirmModalVisible.value = true
agreeTerms.value = false
}
//
const handleDownload = async (record: any) => {
try {
const loadingMessage = Message.loading('正在生成PDF文件...', 0)
// PDF
const pdfBlob = await PDFGenerator.generateRegulationPDF(record)
//
loadingMessage.close()
//
const filename = `${record.title}_${new Date().toISOString().split('T')[0]}.pdf`
// PDF
PDFGenerator.downloadPDF(pdfBlob, filename)
Message.success('PDF文件下载成功')
} catch (error) {
Message.error('PDF生成失败')
console.error('PDF生成错误:', error)
}
}
//
const submitConfirm = async () => {
if (!agreeTerms.value) {
Message.warning('请先同意承诺条款')
return
}
try {
if (currentRegulation.value) {
await regulationApi.confirmRegulation(currentRegulation.value.regulationId)
Message.success('确认成功,您已承诺遵守该制度')
confirmModalVisible.value = false
//
await getTableData()
//
if (detailModalVisible.value && currentRegulation.value) {
const updatedRecord = tableData.value.find(item => item.regulationId === currentRegulation.value.regulationId)
if (updatedRecord) {
currentRegulation.value = updatedRecord
}
}
}
} catch (error) {
console.error('确认失败:', error)
Message.error('确认失败')
}
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.system-regulation {
.arco-card {
margin-bottom: 16px;
}
}
.search-container {
padding: 16px;
background: var(--color-bg-2);
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.arco-form-item {
margin-bottom: 0;
margin-right: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-right: 8px;
}
}
.arco-input,
.arco-select {
border-radius: 4px;
}
.arco-btn {
border-radius: 4px;
}
}
}
.search-result-info {
color: var(--color-text-2);
font-size: 14px;
strong {
color: var(--color-text-1);
font-weight: 600;
}
}
.regulation-detail {
.detail-header {
margin-bottom: 16px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.detail-meta {
display: flex;
gap: 16px;
color: var(--color-text-3);
font-size: 14px;
}
}
.detail-content {
h4 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 500;
}
.content-text {
color: var(--color-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
}
}
.detail-footer {
display: flex;
gap: 12px;
justify-content: center;
}
}
.confirm-content {
p {
margin-bottom: 16px;
color: var(--color-text-1);
line-height: 1.6;
}
ul {
margin-bottom: 20px;
padding-left: 20px;
li {
margin-bottom: 8px;
color: var(--color-text-2);
line-height: 1.5;
}
}
}
</style>

View File

@ -0,0 +1,404 @@
<template>
<GiPageLayout>
<GiTable
row-key="typeId"
title="制度类型管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="类型名称">
<a-input
v-model="searchForm.typeName"
placeholder="请输入类型名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 150px"
>
<a-option value="">全部</a-option>
<a-option value="1">启用</a-option>
<a-option value="0">禁用</a-option>
</a-select>
</a-form-item>
<a-form-item label="备注">
<a-input
v-model="searchForm.remark"
placeholder="请输入备注内容"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="search">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增类型</template>
</a-button>
<span class="search-result-info">
共找到 <strong>{{ pagination.total }}</strong> 条记录
</span>
</a-space>
</template>
<!-- 启用状态 -->
<template #isEnabled="{ record }">
<a-tag :color="record.isEnabled === '1' ? 'green' : 'red'">
{{ record.isEnabled === '1' ? '启用' : '禁用' }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="editRecord(record)">编辑</a-link>
<a-popconfirm
content="确定要删除这个制度类型吗?"
@ok="deleteRecord(record)"
>
<a-link status="danger">删除</a-link>
</a-popconfirm>
</a-space>
</template>
</GiTable>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="600px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="类型名称" field="typeName">
<a-input v-model="formData.typeName" placeholder="请输入制度类型名称" />
</a-form-item>
<a-form-item label="排序" field="sortOrder">
<a-input-number
v-model="formData.sortOrder"
placeholder="请输入排序值"
:min="0"
:max="999"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="状态" field="isEnabled">
<a-radio-group v-model="formData.isEnabled">
<a-radio value="1">启用</a-radio>
<a-radio value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注信息"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import { regulationApi } from '@/apis/regulation'
import {
type RegulationType,
type CreateRegulationTypeRequest,
type UpdateRegulationTypeRequest,
type RegulationTypeSearchRequest
} from '@/apis/regulation/type'
defineOptions({ name: 'RegulationType' })
//
const tableColumns = [
{ title: '类型名称', dataIndex: 'typeName', key: 'typeName', width: 150 },
{ title: '备注', dataIndex: 'remark', key: 'remark', width: 200 },
{ title: '状态', dataIndex: 'isEnabled', key: 'isEnabled', slotName: 'isEnabled', width: 100 },
{ title: '创建人', dataIndex: 'createrName', key: 'createrName', width: 120 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 100 },
{ title: '操作', key: 'action', slotName: 'action', width: 150, fixed: 'right' }
]
//
const dataList = ref<RegulationType[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const searchForm = reactive({
typeName: '',
status: '',
remark: ''
})
//
const modalVisible = ref(false)
const modalTitle = ref('新增制度类型')
const formRef = ref()
const formData = reactive({
typeId: '',
typeName: '',
sortOrder: 0,
isEnabled: '1',
remark: ''
})
//
const rules = {
typeName: [{ required: true, message: '请输入制度类型名称' }],
sortOrder: [{ required: true, message: '请输入排序值' }],
isEnabled: [{ required: true, message: '请选择状态' }]
}
//
const getTableData = async () => {
loading.value = true
try {
// 使
const response = await regulationApi.searchRegulationTypes({
page: pagination.current,
size: pagination.pageSize,
typeName: searchForm.typeName || undefined,
status: searchForm.status || undefined,
remark: searchForm.remark || undefined,
})
if (response.status === 200) {
dataList.value = response.data.records || response.data
pagination.total = response.data.total || response.data.length
pagination.current = response.data.current || 1
} else {
Message.error('搜索失败')
}
} catch (error) {
console.error('搜索制度类型失败:', error)
Message.error('搜索失败')
} finally {
loading.value = false
}
}
//
const search = () => {
pagination.current = 1
getTableData()
}
//
const reset = () => {
Object.assign(searchForm, {
typeName: '',
status: '',
remark: ''
})
pagination.current = 1
getTableData()
}
//
const onPageChange = (page: number) => {
pagination.current = page
getTableData()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
//
const openAddModal = () => {
modalTitle.value = '新增制度类型'
modalVisible.value = true
resetForm()
}
//
const editRecord = (record: RegulationType) => {
modalTitle.value = '编辑制度类型'
modalVisible.value = true
Object.assign(formData, {
typeId: record.typeId,
typeName: record.typeName,
sortOrder: record.sortOrder,
isEnabled: record.isEnabled,
remark: record.remark || ''
})
}
//
const deleteRecord = async (record: RegulationType) => {
try {
await regulationApi.deleteRegulationType(record.typeId)
Message.success('删除成功')
getTableData()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (formData.typeId) {
//
const updateData: UpdateRegulationTypeRequest = {
typeName: formData.typeName,
sortOrder: formData.sortOrder,
isEnabled: formData.isEnabled,
remark: formData.remark
}
await regulationApi.updateRegulationType(formData.typeId, updateData)
Message.success('更新成功')
} else {
//
const createData: CreateRegulationTypeRequest = {
typeName: formData.typeName,
sortOrder: formData.sortOrder,
isEnabled: formData.isEnabled,
remark: formData.remark
}
await regulationApi.createRegulationType(createData)
Message.success('新增成功')
}
modalVisible.value = false
getTableData()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败')
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(formData, {
typeId: '',
typeName: '',
sortOrder: 0,
isEnabled: '1',
remark: ''
})
formRef.value?.resetFields()
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.regulation-type {
.arco-card {
margin-bottom: 16px;
}
}
.search-container {
padding: 16px;
background: var(--color-bg-2);
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.arco-form-item {
margin-bottom: 0;
margin-right: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-right: 8px;
}
}
.arco-input,
.arco-select {
border-radius: 4px;
}
.arco-btn {
border-radius: 4px;
}
}
}
.search-result-info {
color: var(--color-text-2);
font-size: 14px;
strong {
color: var(--color-text-1);
font-weight: 600;
}
}
</style>

View File

@ -0,0 +1,242 @@
<template>
<a-modal
:visible="visible"
@update:visible="val => emit('update:visible', val)"
:title="title"
:width="900"
:footer="true"
@cancel="handleCancel"
>
<a-descriptions :data="descriptionsData" bordered :column="2">
<a-descriptions-item label="员工姓名">{{ data?.employeeName }}</a-descriptions-item>
<a-descriptions-item label="员工类型">{{ employeeTypeText }}</a-descriptions-item>
<a-descriptions-item label="身份证号">{{ data?.idCard }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ data?.phone }}</a-descriptions-item>
<a-descriptions-item label="银行卡号">{{ data?.bankCard }}</a-descriptions-item>
<a-descriptions-item label="开户银行">{{ data?.bankName }}</a-descriptions-item>
<a-descriptions-item label="项目名称">{{ data?.projectName }}</a-descriptions-item>
<a-descriptions-item label="项目周期">{{ data?.projectPeriod }}</a-descriptions-item>
<a-descriptions-item label="工作天数">{{ data?.workDays }}</a-descriptions-item>
<a-descriptions-item label="海上作业天数">{{ data?.offshoreDays }}</a-descriptions-item>
</a-descriptions>
<a-divider orientation="left">薪酬明细</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-statistic
title="基本工资"
:value="data?.baseSalary"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="绩效奖金"
:value="data?.performanceBonus"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic
title="各类补助"
:value="data?.allowances"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="应发总额"
:value="data?.totalPayable"
:precision="2"
:value-style="{ color: '#1890ff' }"
/>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic
title="扣款金额"
:value="data?.deductions"
:precision="2"
:value-style="{ color: '#ff4d4f' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="实发金额"
:value="data?.netPay"
:precision="2"
:value-style="{ color: '#52c41a', fontSize: '24px', fontWeight: 'bold' }"
/>
</a-col>
</a-row>
<a-divider orientation="left">绩效评价</a-divider>
<a-table :data="data?.performanceItems" :columns="performanceColumns" :pagination="false">
<template #score="{ record }">
<a-tag :color="getScoreColor(record.score, record.maxScore)">
{{ record.score }}/{{ record.maxScore }}
</a-tag>
</template>
<template #amount="{ record }">
¥{{ (record.score / record.maxScore * record.amount).toFixed(2) }}
</template>
</a-table>
<a-divider orientation="left">补助明细</a-divider>
<a-table :data="data?.allowanceItems" :columns="allowanceColumns" :pagination="false">
<template #total="{ record }">
¥{{ record.total.toFixed(2) }}
</template>
</a-table>
<a-divider orientation="left">审批流程</a-divider>
<a-steps :current="approvalCurrent" direction="vertical">
<a-step
v-for="step in data?.approvalFlow"
:key="step.step"
:title="step.step"
:description="`${step.role} - ${step.approver}`"
>
<template #icon>
<icon-check-circle v-if="step.status === 'approved'" style="color: #52c41a" />
<icon-close-circle v-else-if="step.status === 'rejected'" style="color: #ff4d4f" />
<icon-clock-circle v-else style="color: #faad14" />
</template>
</a-step>
</a-steps>
<template #footer>
<a-space>
<a-button @click="handleCancel">关闭</a-button>
<a-button type="primary" @click="handleExport">
<template #icon><icon-download /></template>
导出Excel
</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { SalaryRecord } from '../types'
interface Props {
visible: boolean
data: SalaryRecord | null
}
interface Emits {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const title = computed(() => `${props.data?.employeeName} - 工资详情`)
const employeeTypeText = computed(() => {
const typeMap = {
intern: '实习生',
fulltime: '正式员工',
parttime: '兼职员工',
}
return props.data?.employeeType ? typeMap[props.data.employeeType] : ''
})
const approvalCurrent = computed(() => {
if (!props.data?.approvalFlow) return 0
const approvedSteps = props.data.approvalFlow.filter((step) => step.status === 'approved').length
return approvedSteps
})
const descriptionsData = computed(() => [
{ label: '员工姓名', value: props.data?.employeeName },
{ label: '员工类型', value: employeeTypeText.value },
{ label: '身份证号', value: props.data?.idCard },
{ label: '联系电话', value: props.data?.phone },
{ label: '银行卡号', value: props.data?.bankCard },
{ label: '开户银行', value: props.data?.bankName },
{ label: '项目名称', value: props.data?.projectName },
{ label: '项目周期', value: props.data?.projectPeriod },
{ label: '工作天数', value: `${props.data?.workDays}` },
{ label: '海上作业天数', value: `${props.data?.offshoreDays}` },
])
const performanceColumns = [
{ title: '评价项', dataIndex: 'name', width: 200 },
{ title: '权重', dataIndex: 'weight', width: 80 },
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
{ title: '满分', dataIndex: 'maxScore', width: 80 },
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
]
const allowanceColumns = [
{ title: '补助类型', dataIndex: 'name', width: 200 },
{ title: '标准', dataIndex: 'amount', width: 100 },
{ title: '数量', dataIndex: 'quantity', width: 100 },
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
]
const getScoreColor = (score: number, maxScore: number) => {
const ratio = score / maxScore
if (ratio >= 0.9) return 'green'
if (ratio >= 0.7) return 'blue'
if (ratio >= 0.6) return 'orange'
return 'red'
}
const handleCancel = () => {
emit('update:visible', false)
}
const handleExport = () => {
if (!props.data) return
//
const exportData = {
employee: {
name: props.data.employeeName,
type: employeeTypeText.value,
idCard: props.data.idCard,
phone: props.data.phone,
bankCard: props.data.bankCard,
bankName: props.data.bankName,
},
project: {
name: props.data.projectName,
period: props.data.projectPeriod,
workDays: props.data.workDays,
offshoreDays: props.data.offshoreDays,
},
salary: {
baseSalary: props.data.baseSalary,
performanceBonus: props.data.performanceBonus,
allowances: props.data.allowances,
totalPayable: props.data.totalPayable,
deductions: props.data.deductions,
netPay: props.data.netPay,
},
performanceItems: props.data.performanceItems,
allowanceItems: props.data.allowanceItems,
}
//
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${props.data.employeeName}_工资单_${props.data.projectPeriod}.json`
a.click()
URL.revokeObjectURL(url)
}
</script>

View File

@ -0,0 +1,468 @@
<template>
<a-drawer
:visible="visible"
@update:visible="val => emit('update:visible', val)"
:title="title"
:width="800"
:footer="true"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<!-- 基本信息 -->
<a-card title="基本信息" :bordered="false">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="员工姓名" field="employeeName">
<a-input v-model="formData.employeeName" placeholder="请输入员工姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="员工类型" field="employeeType">
<a-select v-model="formData.employeeType" placeholder="请选择员工类型">
<a-option value="intern">实习生</a-option>
<a-option value="fulltime">正式员工</a-option>
<a-option value="parttime">兼职员工</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="身份证号" field="idCard">
<a-input v-model="formData.idCard" placeholder="请输入身份证号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" field="phone">
<a-input v-model="formData.phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="银行卡号" field="bankCard">
<a-input v-model="formData.bankCard" placeholder="请输入银行卡号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开户银行" field="bankName">
<a-input v-model="formData.bankName" placeholder="请输入开户银行" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 项目信息 -->
<a-card title="项目信息" :bordered="false" style="margin-top: 16px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="项目名称" field="projectName">
<a-input v-model="formData.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="项目周期" field="projectPeriod">
<a-input v-model="formData.projectPeriod" placeholder="如2025-07" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="工作天数" field="workDays">
<a-input-number
v-model="formData.workDays"
:min="0"
:precision="0"
placeholder="请输入工作天数"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="海上作业天数" field="offshoreDays">
<a-input-number
v-model="formData.offshoreDays"
:min="0"
:precision="0"
placeholder="请输入海上作业天数"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 薪酬计算 -->
<a-card title="薪酬计算" :bordered="false" style="margin-top: 16px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="基本工资标准" field="baseSalaryStandard">
<a-input-number
v-model="formData.baseSalaryStandard"
:min="0"
:precision="2"
placeholder="请输入基本工资标准"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="基本工资" field="baseSalary">
<a-input-number
v-model="formData.baseSalary"
:min="0"
:precision="2"
placeholder="自动计算"
style="width: 100%"
readonly
/>
</a-form-item>
</a-col>
</a-row>
<!-- 绩效评价 -->
<a-divider orientation="left">绩效评价</a-divider>
<a-table :data="formData.performanceItems" :columns="performanceColumns" :pagination="false">
<template #score="{ record, rowIndex }">
<a-input-number
v-model="record.score"
:min="0"
:max="record.maxScore"
:precision="0"
@change="calculatePerformance"
/>
</template>
<template #amount="{ record }">
{{ record.amount }}
</template>
</a-table>
<!-- 补助明细 -->
<a-divider orientation="left">补助明细</a-divider>
<a-table :data="formData.allowanceItems" :columns="allowanceColumns" :pagination="false">
<template #quantity="{ record, rowIndex }">
<a-input-number
v-model="record.quantity"
:min="0"
:precision="0"
@change="calculateAllowances"
/>
</template>
<template #total="{ record }">
{{ record.total }}
</template>
</a-table>
<!-- 计算结果 -->
<a-divider orientation="left">计算结果</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="应发总额">
<a-input-number
v-model="formData.totalPayable"
:min="0"
:precision="2"
readonly
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="扣款金额">
<a-input-number
v-model="formData.deductions"
:min="0"
:precision="2"
placeholder="请输入扣款金额"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="实发金额">
<a-input-number
v-model="formData.netPay"
:min="0"
:precision="2"
readonly
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSubmit">保存</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { SalaryRecord, AllowanceItem, PerformanceItem } from '../types'
interface Props {
visible: boolean
data: SalaryRecord | null
mode: 'add' | 'edit'
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref()
const formData = reactive<SalaryRecord>({
id: '',
employeeId: '',
employeeName: '',
employeeType: 'intern',
idCard: '',
phone: '',
bankCard: '',
bankName: '',
projectName: '',
projectPeriod: '',
workDays: 0,
offshoreDays: 0,
baseSalary: 0,
baseSalaryStandard: 3500,
performanceBonus: 0,
allowances: 0,
performanceItems: [],
allowanceItems: [],
totalPayable: 0,
deductions: 0,
netPay: 0,
status: 'draft',
approvalFlow: [],
salaryMonth: '',
createdAt: '',
updatedAt: '',
})
const performanceColumns = [
{ title: '评价项', dataIndex: 'name', width: 200 },
{ title: '权重', dataIndex: 'weight', width: 80 },
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
{ title: '满分', dataIndex: 'maxScore', width: 80 },
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
]
const allowanceColumns = [
{ title: '补助类型', dataIndex: 'name', width: 200 },
{ title: '标准', dataIndex: 'amount', width: 100 },
{ title: '单位', dataIndex: 'unit', width: 80 },
{ title: '数量', dataIndex: 'quantity', slotName: 'quantity', width: 100 },
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
]
const title = computed(() => (props.mode === 'add' ? '新建工资单' : '编辑工资单'))
const rules = {
employeeName: [{ required: true, message: '请输入员工姓名' }],
employeeType: [{ required: true, message: '请选择员工类型' }],
idCard: [{ required: true, message: '请输入身份证号' }],
phone: [{ required: true, message: '请输入联系电话' }],
projectName: [{ required: true, message: '请输入项目名称' }],
workDays: [{ required: true, message: '请输入工作天数' }],
}
//
const initInternConfig = () => {
formData.performanceItems = [
{
id: '1',
name: '平时沟通态度',
weight: 30,
score: 0,
maxScore: 100,
amount: 200,
},
{
id: '2',
name: '积极主动性',
weight: 50,
score: 0,
maxScore: 100,
amount: 200,
},
{
id: '3',
name: '成长性心态',
weight: 20,
score: 0,
maxScore: 100,
amount: 100,
},
]
formData.allowanceItems = [
{
type: 'construction',
name: '施工绩效',
amount: 100,
unit: 'day',
quantity: 0,
total: 0,
},
{
type: 'offshore',
name: '海上作业补助',
amount: 30,
unit: 'day',
quantity: 0,
total: 0,
},
{
type: 'meal',
name: '务餐补助',
amount: 45,
unit: 'day',
quantity: 0,
total: 0,
},
]
}
//
const calculateBaseSalary = () => {
formData.baseSalary = (formData.baseSalaryStandard / 30) * formData.workDays
calculateTotal()
}
//
const calculatePerformance = () => {
formData.performanceBonus = formData.performanceItems.reduce(
(sum, item) => sum + (item.score / item.maxScore) * item.amount,
0,
)
calculateTotal()
}
//
const calculateAllowances = () => {
formData.allowanceItems.forEach((item) => {
item.total = item.amount * item.quantity
})
formData.allowances = formData.allowanceItems.reduce((sum, item) => sum + item.total, 0)
calculateTotal()
}
//
const calculateTotal = () => {
formData.totalPayable = formData.baseSalary + formData.performanceBonus + formData.allowances
formData.netPay = formData.totalPayable - formData.deductions
}
//
watch(
() => formData.workDays,
() => {
calculateBaseSalary()
//
const mealItem = formData.allowanceItems.find((item) => item.type === 'meal')
if (mealItem) {
mealItem.quantity = formData.workDays
calculateAllowances()
}
},
)
//
watch(
() => formData.offshoreDays,
() => {
//
const offshoreItem = formData.allowanceItems.find((item) => item.type === 'offshore')
if (offshoreItem) {
offshoreItem.quantity = formData.offshoreDays
calculateAllowances()
}
},
)
//
watch(() => formData.deductions, calculateTotal)
const handleCancel = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
// API
Message.success('保存成功')
emit('success')
} catch (error) {
console.error('表单验证失败:', error)
}
}
//
watch(
() => props.data,
(newData) => {
if (newData) {
Object.assign(formData, newData)
} else {
//
Object.assign(formData, {
id: '',
employeeId: '',
employeeName: '',
employeeType: 'intern',
idCard: '',
phone: '',
bankCard: '',
bankName: '',
projectName: '',
projectPeriod: '',
workDays: 0,
offshoreDays: 0,
baseSalary: 0,
baseSalaryStandard: 3500,
performanceBonus: 0,
allowances: 0,
performanceItems: [],
allowanceItems: [],
totalPayable: 0,
deductions: 0,
netPay: 0,
status: 'draft',
approvalFlow: [],
salaryMonth: '',
createdAt: '',
updatedAt: '',
})
initInternConfig()
}
},
{ immediate: true },
)
//
watch(
() => formData.employeeType,
(newType) => {
if (newType === 'intern') {
initInternConfig()
} else {
//
formData.performanceItems = []
formData.allowanceItems = []
}
},
)
</script>

View File

@ -0,0 +1,229 @@
<template>
<div class="salary-management">
<GiPageLayout>
<template #header>
<a-row justify="space-between" align="center">
<a-col>
<h1>工资管理系统</h1>
</a-col>
<a-col>
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
新建工资单
</a-button>
<a-button @click="handleExport">
<template #icon><icon-download /></template>
导出Excel
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<a-card>
<a-row :gutter="16" class="search-section">
<a-col :span="6">
<a-input
v-model="searchParams.keyword"
placeholder="搜索员工姓名/工号"
allow-clear
/>
</a-col>
<a-col :span="6">
<a-select
v-model="searchParams.employeeType"
placeholder="员工类型"
allow-clear
>
<a-option value="intern">实习生</a-option>
<a-option value="fulltime">正式员工</a-option>
<a-option value="parttime">兼职员工</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model="searchParams.dateRange"
style="width: 100%"
/>
</a-col>
<a-col :span="6">
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-col>
</a-row>
<a-table
:data="tableData"
:columns="columns"
:loading="loading"
row-key="id"
:scroll="{ x: 1500 }"
>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="handleEdit(record)">编辑</a-button>
<a-button type="text" @click="handleDetail(record)">详情</a-button>
<a-button type="text" status="danger" @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
</GiPageLayout>
<SalaryFormDrawer
v-model:visible="drawerVisible"
:data="currentData"
:mode="drawerMode"
@success="handleSuccess"
/>
<SalaryDetailModal
v-model:visible="detailVisible"
:data="currentData"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import SalaryFormDrawer from './components/SalaryFormDrawer.vue'
import SalaryDetailModal from './components/SalaryDetailModal.vue'
import type { SalaryRecord } from './types'
const loading = ref(false)
const drawerVisible = ref(false)
const detailVisible = ref(false)
const drawerMode = ref<'add' | 'edit'>('add')
const currentData = ref<SalaryRecord | null>(null)
const searchParams = reactive({
keyword: '',
employeeType: '',
dateRange: [],
})
const tableData = ref<SalaryRecord[]>([])
const columns = [
{ title: '员工姓名', dataIndex: 'employeeName', fixed: 'left', width: 100 },
{ title: '员工类型', dataIndex: 'employeeType', width: 100 },
{ title: '项目名称', dataIndex: 'projectName', width: 150 },
{ title: '工作天数', dataIndex: 'workDays', width: 80 },
{ title: '基本工资', dataIndex: 'baseSalary', width: 100 },
{ title: '绩效奖金', dataIndex: 'performanceBonus', width: 100 },
{ title: '各类补助', dataIndex: 'allowances', width: 100 },
{ title: '应发总额', dataIndex: 'totalPayable', width: 100 },
{ title: '实发金额', dataIndex: 'netPay', width: 100 },
{ title: '状态', dataIndex: 'status', width: 80 },
{ title: '创建时间', dataIndex: 'createdAt', width: 150 },
{ title: '操作', slotName: 'action', fixed: 'right', width: 150 },
]
const loadData = async () => {
loading.value = true
try {
//
tableData.value = [
{
id: '1',
employeeId: 'E001',
employeeName: '张三',
employeeType: 'intern',
idCard: '123456789012345678',
phone: '13800000000',
bankCard: '6222021234567890',
bankName: '中国银行',
projectName: '海上风电项目A',
projectPeriod: '2025-06',
workDays: 22,
offshoreDays: 5,
baseSalary: 3500,
baseSalaryStandard: 3500,
performanceBonus: 2200,
allowances: 660,
performanceItems: [],
allowanceItems: [],
totalPayable: 6360,
deductions: 0,
netPay: 6360,
status: 'approved',
approvalFlow: [],
salaryMonth: '2025-07',
createdAt: '2025-07-01',
updatedAt: '2025-07-01',
},
]
} finally {
loading.value = false
}
}
const handleAdd = () => {
drawerMode.value = 'add'
currentData.value = null
drawerVisible.value = true
}
const handleEdit = (record: SalaryRecord) => {
drawerMode.value = 'edit'
currentData.value = record
drawerVisible.value = true
}
const handleDetail = (record: SalaryRecord) => {
currentData.value = record
detailVisible.value = true
}
const handleDelete = async (_record: SalaryRecord) => {
try {
await Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?',
})
Message.success('删除成功')
loadData()
} catch {
//
}
}
const handleSearch = () => {
loadData()
}
const handleReset = () => {
searchParams.keyword = ''
searchParams.employeeType = ''
searchParams.dateRange = []
loadData()
}
const handleExport = () => {
// Excel
Message.success('导出功能开发中...')
}
const handleSuccess = () => {
drawerVisible.value = false
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.salary-management {
padding: 16px;
}
.search-section {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,127 @@
// 员工类型
export type EmployeeType = 'intern' | 'fulltime' | 'parttime'
// 工资单状态
export type SalaryStatus = 'draft' | 'pending' | 'approved' | 'rejected' | 'paid'
// 补助类型
export interface AllowanceItem {
type: string
name: string
amount: number
unit: 'day' | 'project' | 'fixed'
quantity: number
total: number
}
// 绩效评价项
export interface PerformanceItem {
id: string
name: string
weight: number
score: number
maxScore: number
amount: number
description?: string
}
// 工资记录
export interface SalaryRecord {
id: string
employeeId: string
employeeName: string
employeeType: EmployeeType
idCard: string
phone: string
bankCard: string
bankName: string
// 项目信息
projectName: string
projectPeriod: string
workDays: number
offshoreDays: number
// 薪酬结构
baseSalary: number
baseSalaryStandard: number
performanceBonus: number
allowances: number
performanceItems: PerformanceItem[]
allowanceItems: AllowanceItem[]
// 计算结果
totalPayable: number
deductions: number
netPay: number
// 状态
status: SalaryStatus
approvalFlow: ApprovalStep[]
// 时间
salaryMonth: string
createdAt: string
updatedAt: string
}
// 审批步骤
export interface ApprovalStep {
step: string
role: string
approver: string
status: 'pending' | 'approved' | 'rejected'
comment?: string
date?: string
}
// 查询参数
export interface SalaryQuery {
keyword?: string
employeeType?: EmployeeType
status?: SalaryStatus
dateRange?: string[]
projectName?: string
}
// 实习生专用配置
export interface InternConfig {
baseSalaryStandard: {
withCertification: number
withoutCertification: number
}
performanceRates: {
construction: number
offshore: number
}
allowances: {
offshoreCert: number
towerWork: {
land: {
bladeInspection: number
lightning: number
}
sea: {
bladeInspection: number
lightning: number
}
}
droneWork: number
meal: number
}
}
// 工资单创建请求
export interface SalaryCreateRequest {
employeeId: string
projectId: string
workDays: number
offshoreDays: number
performanceItems: Omit<PerformanceItem, 'id'>[]
allowanceItems: Omit<AllowanceItem, 'total'>[]
deductions?: {
type: string
amount: number
reason: string
}[]
}

View File

@ -0,0 +1,255 @@
import * as XLSX from 'xlsx'
import type { SalaryRecord } from '../types'
// 导出工资单到Excel
export const exportSalaryToExcel = (records: SalaryRecord[], filename: string) => {
// 创建工作簿
const workbook = XLSX.utils.book_new()
// 主表数据
const mainData = records.map((record) => ({
员工姓名: record.employeeName,
员工类型: getEmployeeTypeText(record.employeeType),
身份证号: record.idCard,
联系电话: record.phone,
银行卡号: record.bankCard,
开户银行: record.bankName,
项目名称: record.projectName,
项目周期: record.projectPeriod,
工作天数: record.workDays,
海上作业天数: record.offshoreDays,
基本工资: record.baseSalary,
绩效奖金: record.performanceBonus,
各类补助: record.allowances,
应发总额: record.totalPayable,
扣款金额: record.deductions,
实发金额: record.netPay,
状态: getStatusText(record.status),
创建时间: record.createdAt,
}))
// 创建主表
const mainSheet = XLSX.utils.json_to_sheet(mainData)
XLSX.utils.book_append_sheet(workbook, mainSheet, '工资汇总')
// 绩效明细表
const performanceData: any[] = []
records.forEach((record) => {
record.performanceItems.forEach((item) => {
performanceData.push({
员工姓名: record.employeeName,
项目名称: record.projectName,
评价项: item.name,
权重: item.weight,
得分: item.score,
满分: item.maxScore,
: (item.score / item.maxScore * item.amount).toFixed(2),
})
})
})
if (performanceData.length > 0) {
const performanceSheet = XLSX.utils.json_to_sheet(performanceData)
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
}
// 补助明细表
const allowanceData: any[] = []
records.forEach((record) => {
record.allowanceItems.forEach((item) => {
allowanceData.push({
员工姓名: record.employeeName,
项目名称: record.projectName,
补助类型: item.name,
标准: item.amount,
单位: getUnitText(item.unit),
数量: item.quantity,
小计: item.total,
})
})
})
if (allowanceData.length > 0) {
const allowanceSheet = XLSX.utils.json_to_sheet(allowanceData)
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
}
// 导出文件
XLSX.writeFile(workbook, filename)
}
// 导出单个工资单详情
export const exportSingleSalaryToExcel = (record: SalaryRecord) => {
const workbook = XLSX.utils.book_new()
// 基本信息
const basicInfo = [{
: '员工姓名',
: record.employeeName,
}, {
: '员工类型',
: getEmployeeTypeText(record.employeeType),
}, {
: '身份证号',
: record.idCard,
}, {
: '联系电话',
: record.phone,
}, {
: '银行卡号',
: record.bankCard,
}, {
: '开户银行',
: record.bankName,
}, {
: '项目名称',
: record.projectName,
}, {
: '项目周期',
: record.projectPeriod,
}, {
: '工作天数',
: record.workDays,
}, {
: '海上作业天数',
: record.offshoreDays,
}]
const basicSheet = XLSX.utils.json_to_sheet(basicInfo)
XLSX.utils.book_append_sheet(workbook, basicSheet, '基本信息')
// 薪酬汇总
const salarySummary = [{
: '基本工资',
金额: record.baseSalary,
}, {
: '绩效奖金',
金额: record.performanceBonus,
}, {
: '各类补助',
金额: record.allowances,
}, {
: '应发总额',
金额: record.totalPayable,
}, {
: '扣款金额',
金额: record.deductions,
}, {
: '实发金额',
金额: record.netPay,
}]
const salarySheet = XLSX.utils.json_to_sheet(salarySummary)
XLSX.utils.book_append_sheet(workbook, salarySheet, '薪酬汇总')
// 绩效明细
if (record.performanceItems.length > 0) {
const performanceSheet = XLSX.utils.json_to_sheet(record.performanceItems.map((item) => ({
评价项: item.name,
权重: item.weight,
得分: item.score,
满分: item.maxScore,
: (item.score / item.maxScore * item.amount).toFixed(2),
})))
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
}
// 补助明细
if (record.allowanceItems.length > 0) {
const allowanceSheet = XLSX.utils.json_to_sheet(record.allowanceItems.map((item) => ({
补助类型: item.name,
标准: item.amount,
单位: getUnitText(item.unit),
数量: item.quantity,
小计: item.total,
})))
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
}
// 导出文件
const filename = `${record.employeeName}_工资单_${record.projectPeriod}.xlsx`
XLSX.writeFile(workbook, filename)
}
// 导出实习报酬申领表格式
export const exportInternCompensationForm = (record: SalaryRecord) => {
const workbook = XLSX.utils.book_new()
// 实习生基本信息
const basicInfo = [
['实习生基本信息', '', '', '', ''],
['类别', '填写内容', '', '备注', ''],
['姓名', record.employeeName, '', '', ''],
['身份证号', record.idCard, '', '', ''],
['联系电话', record.phone, '', '', ''],
['银行卡号', record.bankCard, '', '', ''],
['开户银行', record.bankName, '', '', ''],
['', '', '', '', ''],
['实习项目及项目实习相关信息', '', '', '', ''],
['序号', '项目名称', '在项目时间', '出外业施工作业时间(天)', '备注(时间段)'],
['1', record.projectName, record.workDays, record.workDays - record.offshoreDays, record.projectPeriod],
['', '', '', '', ''],
['薪酬、绩效、补助(一)', '', '', '', ''],
['项目类别', '计算标准(元/天)或(元/台)', '计酬天数统计(日)', '应发金额', '备注'],
['基本工资', record.baseSalaryStandard, record.workDays, record.baseSalary, ''],
['施工绩效', 100, record.workDays - record.offshoreDays, record.performanceBonus, ''],
['海上作业绩效补助', 30, record.offshoreDays, record.offshoreDays * 30, ''],
['务餐补助', 45, record.workDays, record.allowances, ''],
['合计应发金额', '', '', record.totalPayable, ''],
]
const sheet = XLSX.utils.aoa_to_sheet(basicInfo)
// 设置列宽
const cols = [
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 25 },
{ wch: 20 },
]
sheet['!cols'] = cols
// 合并单元格
sheet['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 4 } },
{ s: { r: 8, c: 0 }, e: { r: 8, c: 4 } },
{ s: { r: 12, c: 0 }, e: { r: 12, c: 4 } },
]
XLSX.utils.book_append_sheet(workbook, sheet, '实习报酬申领表')
// 导出文件
const filename = `${record.employeeName}_实习报酬申领表_${record.projectPeriod}.xlsx`
XLSX.writeFile(workbook, filename)
}
// 辅助函数
function getEmployeeTypeText(type: string) {
const typeMap = {
intern: '实习生',
fulltime: '正式员工',
parttime: '兼职员工',
}
return typeMap[type as keyof typeof typeMap] || type
}
function getStatusText(status: string) {
const statusMap = {
draft: '草稿',
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
paid: '已发放',
}
return statusMap[status as keyof typeof statusMap] || status
}
function getUnitText(unit: string) {
const unitMap = {
day: '天',
project: '项目',
fixed: '固定',
}
return unitMap[unit as keyof typeof unitMap] || unit
}

View File

@ -0,0 +1,313 @@
<template>
<div class="device-detail-container">
<a-card title="设备基本信息">
<a-descriptions :data="basicInfo" layout="vertical" bordered />
</a-card>
<a-card title="设备状态信息" style="margin-top: 16px">
<a-descriptions :data="statusInfo" layout="vertical" bordered />
</a-card>
<a-card title="设备详细信息" style="margin-top: 16px">
<a-tabs>
<a-tab-pane key="maintenance" tab="维护记录">
<div class="maintenance-section">
<a-button type="primary" @click="handleAddMaintenance">
添加维护记录
</a-button>
<a-divider />
<a-table :data="maintenanceRecords" :columns="maintenanceColumns" row-key="id" size="small" />
</div>
</a-tab-pane>
<a-tab-pane key="usage" tab="使用记录">
<div class="usage-section">
<a-table :data="usageRecords" :columns="usageColumns" row-key="id" size="small" />
</div>
</a-tab-pane>
<a-tab-pane key="location" tab="位置变更">
<div class="location-section">
<a-table :data="locationRecords" :columns="locationColumns" row-key="id" size="small" />
</div>
</a-tab-pane>
<a-tab-pane key="files" tab="相关文件">
<div class="files-section">
<a-upload
:file-list="fileList"
:custom-request="handleFileUpload"
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传文件
</a-button>
</a-upload>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import { getEquipmentDetail } from '@/apis/equipment'
import type { EquipmentResp } from '@/types/equipment.d'
defineOptions({ name: 'DeviceDetail' })
const route = useRoute()
const deviceId = route.params.id as string
//
const basicInfo = ref([
{ label: '设备名称', value: '' },
{ label: '设备类型', value: '' },
{ label: '设备型号', value: '' },
{ label: '序列号', value: '' },
{ label: '品牌', value: '' },
{ label: '资产编号', value: '' },
{ label: '负责人', value: '' },
{ label: '物理位置', value: '' },
{ label: '配置规格', value: '' },
{ label: '备注', value: '' },
])
//
const statusInfo = ref([
{ label: '设备状态', value: '' },
{ label: '使用状态', value: '' },
{ label: '位置状态', value: '' },
{ label: '健康状态', value: '' },
{ label: '当前使用人', value: '' },
{ label: '所属项目', value: '' },
{ label: '创建时间', value: '' },
{ label: '更新时间', value: '' },
])
//
const maintenanceRecords = ref([
{
id: 1,
maintenanceType: '定期保养',
maintenancePerson: '张师傅',
maintenanceTime: '2024-06-01 10:00:00',
description: '更换机油,检查设备运行状态',
cost: 500,
status: '已完成',
},
{
id: 2,
maintenanceType: '故障维修',
maintenancePerson: '李师傅',
maintenanceTime: '2024-05-15 14:30:00',
description: '修复传感器故障',
cost: 1200,
status: '已完成',
},
])
const maintenanceColumns = [
{ title: '维护类型', dataIndex: 'maintenanceType', key: 'maintenanceType' },
{ title: '维护人员', dataIndex: 'maintenancePerson', key: 'maintenancePerson' },
{ title: '维护时间', dataIndex: 'maintenanceTime', key: 'maintenanceTime' },
{ title: '维护描述', dataIndex: 'description', key: 'description' },
{ title: '维护费用', dataIndex: 'cost', key: 'cost' },
{ title: '状态', dataIndex: 'status', key: 'status' },
]
// 使
const usageRecords = ref([
{
id: 1,
userName: '张三',
projectName: '项目A',
startTime: '2024-06-01 09:00:00',
endTime: '2024-06-01 18:00:00',
usagePurpose: '现场检测',
},
{
id: 2,
userName: '李四',
projectName: '项目B',
startTime: '2024-05-30 08:00:00',
endTime: '2024-05-30 17:00:00',
usagePurpose: '安全巡检',
},
])
const usageColumns = [
{ title: '使用人', dataIndex: 'userName', key: 'userName' },
{ title: '项目名称', dataIndex: 'projectName', key: 'projectName' },
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime' },
{ title: '结束时间', dataIndex: 'endTime', key: 'endTime' },
{ title: '使用目的', dataIndex: 'usagePurpose', key: 'usagePurpose' },
]
//
const locationRecords = ref([
{
id: 1,
oldLocation: '仓库A',
newLocation: '现场B',
changeTime: '2024-06-01 08:00:00',
changeReason: '项目需要',
operator: '管理员',
},
{
id: 2,
oldLocation: '现场B',
newLocation: '仓库A',
changeTime: '2024-05-30 18:00:00',
changeReason: '项目结束',
operator: '管理员',
},
])
const locationColumns = [
{ title: '原位置', dataIndex: 'oldLocation', key: 'oldLocation' },
{ title: '新位置', dataIndex: 'newLocation', key: 'newLocation' },
{ title: '变更时间', dataIndex: 'changeTime', key: 'changeTime' },
{ title: '变更原因', dataIndex: 'changeReason', key: 'changeReason' },
{ title: '操作人', dataIndex: 'operator', key: 'operator' },
]
//
const fileList = ref([
{
uid: '1',
name: '设备说明书.pdf',
status: 'done',
url: 'https://example.com/manual.pdf',
},
{
uid: '2',
name: '维护记录.xlsx',
status: 'done',
url: 'https://example.com/maintenance.xlsx',
},
])
//
const getEquipmentTypeText = (type: string) => {
const typeMap: Record<string, string> = {
1: '检测设备',
2: '安全设备',
3: '车辆',
}
return typeMap[type] || type
}
//
const getEquipmentStatusText = (status: string) => {
const statusMap: Record<string, string> = {
normal: '正常',
maintenance: '维修中',
scrapped: '报废',
}
return statusMap[status] || status
}
// 使
const getUseStatusText = (status: string) => {
return status === '1' ? '使用中' : '空闲'
}
//
const getLocationStatusText = (status: string) => {
const statusMap: Record<string, string> = {
in_stock: '库存中',
allocated: '已分配',
repair: '维修中',
scrap: '待报废',
scrapped: '已报废',
borrowed: '外借中',
lost: '丢失',
}
return statusMap[status] || status
}
//
const getHealthStatusText = (status: string) => {
const statusMap: Record<string, string> = {
excellent: '优秀',
good: '良好',
normal: '一般',
poor: '较差',
bad: '差',
}
return statusMap[status] || status
}
//
const loadDeviceDetail = async () => {
try {
const res = await getEquipmentDetail(deviceId)
const device = res.data as EquipmentResp
//
basicInfo.value = [
{ label: '设备名称', value: device.equipmentName },
{ label: '设备类型', value: getEquipmentTypeText(device.equipmentType) },
{ label: '设备型号', value: device.equipmentModel },
{ label: '序列号', value: device.equipmentSn },
{ label: '品牌', value: device.brand || '-' },
{ label: '资产编号', value: device.assetCode || '-' },
{ label: '负责人', value: device.responsiblePerson || '-' },
{ label: '物理位置', value: device.physicalLocation || '-' },
{ label: '配置规格', value: device.specification || '-' },
{ label: '备注', value: device.assetRemark || '-' },
]
//
statusInfo.value = [
{ label: '设备状态', value: getEquipmentStatusText(device.equipmentStatus) },
{ label: '使用状态', value: getUseStatusText(device.useStatus) },
{ label: '位置状态', value: getLocationStatusText(device.locationStatus || '') },
{ label: '健康状态', value: getHealthStatusText(device.healthStatus || '') },
{ label: '当前使用人', value: device.name || '-' },
{ label: '所属项目', value: device.projectName || '-' },
{ label: '创建时间', value: device.createTime || '-' },
{ label: '更新时间', value: device.updateTime || '-' },
]
} catch (error) {
console.error('加载设备详情失败:', error)
}
}
//
const handleAddMaintenance = () => {
// TODO:
console.log('添加维护记录')
}
//
const handleFileUpload = (options: any) => {
// TODO:
console.log('文件上传:', options)
}
onMounted(() => {
if (deviceId) {
loadDeviceDetail()
}
})
</script>
<style scoped>
.device-detail-container {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.maintenance-section,
.usage-section,
.location-section,
.files-section {
padding: 16px 0;
}
</style>

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