Compare commits

..

1 Commits
main ... hdc

Author SHA1 Message Date
何德超 8f7591589b 净空形变监测相关原型 2025-08-01 15:57:09 +08:00
61 changed files with 4965 additions and 28505 deletions

View File

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

1
.gitignore vendored
View File

@ -1 +0,0 @@
node_modules

View File

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

View File

@ -1,91 +0,0 @@
# 制度类型搜索接口实现指南
## 接口定义
### 请求接口
```
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

@ -1,125 +0,0 @@
# 制度公告搜索接口实现指南
## 接口定义
### 请求接口
```
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

@ -1,117 +0,0 @@
# 制度公示搜索接口实现指南
## 接口定义
### 请求接口
```
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

@ -1,108 +0,0 @@
# 制度提案搜索接口实现指南
## 接口定义
### 请求接口
```
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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -68,15 +68,9 @@ 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
@ -149,9 +143,6 @@ 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
@ -469,10 +460,6 @@ 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'}
@ -1202,18 +1189,12 @@ 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==}
@ -1500,10 +1481,6 @@ 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==}
@ -1624,10 +1601,6 @@ 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'}
@ -1672,11 +1645,6 @@ 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==}
@ -1717,17 +1685,9 @@ 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'}
@ -1799,10 +1759,6 @@ 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'}
@ -1893,11 +1849,6 @@ 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==}
@ -1912,9 +1863,6 @@ 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==}
@ -2091,9 +2039,6 @@ 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==}
@ -2647,9 +2592,6 @@ 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'}
@ -2713,10 +2655,6 @@ 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'}
@ -2903,10 +2841,6 @@ 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==}
@ -3210,9 +3144,6 @@ 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==}
@ -3678,9 +3609,6 @@ 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==}
@ -3862,9 +3790,6 @@ 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==}
@ -3888,9 +3813,6 @@ 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==}
@ -3965,10 +3887,6 @@ 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'}
@ -4188,18 +4106,10 @@ 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'}
@ -4299,10 +4209,6 @@ 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==}
@ -4340,9 +4246,6 @@ 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==}
@ -4560,9 +4463,6 @@ 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:
@ -4787,18 +4687,10 @@ 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'}
@ -4821,11 +4713,6 @@ 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'}
@ -5143,8 +5030,6 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.2': {}
'@babel/template@7.24.0':
dependencies:
'@babel/code-frame': 7.24.2
@ -5871,18 +5756,12 @@ 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': {}
@ -6287,8 +6166,6 @@ 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)
@ -6427,8 +6304,6 @@ snapshots:
balanced-match@1.0.2: {}
base64-arraybuffer@1.0.2: {}
base@0.11.2:
dependencies:
cache-base: 1.0.1
@ -6497,8 +6372,6 @@ 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: {}
@ -6542,29 +6415,12 @@ 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
@ -6665,8 +6521,6 @@ snapshots:
transitivePeerDependencies:
- '@lezer/common'
codepage@1.15.0: {}
collection-visit@1.0.0:
dependencies:
map-visit: 1.0.0
@ -6754,8 +6608,6 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crc-32@1.2.2: {}
crelt@1.0.6: {}
cron-parser@4.9.0:
@ -6770,10 +6622,6 @@ 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
@ -6943,11 +6791,6 @@ 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
@ -7635,8 +7478,6 @@ snapshots:
dependencies:
reusify: 1.0.4
fflate@0.8.2: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -7707,8 +7548,6 @@ 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
@ -7884,11 +7723,6 @@ 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
@ -8151,18 +7985,6 @@ 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
@ -8672,9 +8494,6 @@ snapshots:
perfect-debounce@1.0.0: {}
performance-now@2.1.0:
optional: true
picocolors@1.0.1: {}
picocolors@1.1.1: {}
@ -8891,11 +8710,6 @@ 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
@ -8927,9 +8741,6 @@ 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:
@ -8990,9 +8801,6 @@ snapshots:
rfdc@1.4.1: {}
rgbcolor@1.0.1:
optional: true
rollup@4.17.2:
dependencies:
'@types/estree': 1.0.5
@ -9241,15 +9049,8 @@ 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
@ -9366,9 +9167,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
svg-pathdata@6.0.3:
optional: true
svg-tags@1.0.0: {}
svgo@2.8.0:
@ -9403,10 +9201,6 @@ 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:
@ -9654,10 +9448,6 @@ 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
@ -9927,12 +9717,8 @@ 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
@ -9969,16 +9755,6 @@ 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: {}

View File

@ -17,13 +17,14 @@
<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,7 +6,6 @@ import type { AttachInfoData, BusinessTypeResult } from './type'
*
* @param businessType
* @param files
* @returns
*/
export function batchAddAttachment(businessType: string, formData: FormData) {
return request<AttachInfoData[]>({
@ -24,7 +23,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',
@ -39,7 +38,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',
@ -54,7 +53,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

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

@ -1,62 +0,0 @@
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,49 +0,0 @@
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`)
}

View File

@ -1,277 +0,0 @@
/**
*
*/
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,7 +16,6 @@ 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

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

View File

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

View File

@ -1,121 +0,0 @@
// 制度状态枚举
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
}

View File

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

View File

@ -4,7 +4,6 @@ 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) {
@ -73,7 +72,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_NEW}/${id}/user`, query)
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
@ -88,5 +87,5 @@ export function unassignFromUsers(userRoleIds: Array<string | number>) {
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL_NEW}/${id}/user`)
return http.get(`${BASE_URL}/${id}/user/id`)
}

View File

@ -26,272 +26,203 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
// ],
// },
{
path: '/regulation',
name: 'Regulation',
component: Layout,
redirect: '/regulation/system-regulation',
meta: { title: '制度管理', icon: 'file-text', hidden: false, sort: 1.5 },
children: [
{
path: '/regulation/system-regulation',
name: 'SystemRegulation',
component: () => import('@/views/regulation/repository.vue'),
meta: { title: '制度公告', icon: 'file-text', hidden: false },
},
{
path: '/regulation/process-management',
name: 'ProcessManagement',
component: () => import('@/views/regulation/confirm.vue'),
meta: { title: '制度公示', icon: 'workflow', hidden: false },
},
{
path: '/regulation/proposal',
name: 'RegulationProposal',
component: () => import('@/views/regulation/proposal/index.vue'),
meta: { title: '制度提案', icon: 'edit', hidden: false },
},
{
path: '/regulation/type',
name: 'RegulationType',
component: () => import('@/views/regulation/type/index.vue'),
meta: { title: '制度类型', icon: 'tag', hidden: false },
},
],
},
{
path: '/organization',
name: 'Organization',
component: Layout,
redirect: '/organization/dept',
meta: { title: '企业管理', icon: 'user-group', hidden: false, sort: 2 },
redirect: '/organization/hr/member',
meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
children: [
{
path: '/organization/user',
name: 'OrganizationUser',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', icon: 'user', hidden: false, sort: 2.25 },
path: '/organization/hr',
name: 'HRManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/member',
meta: { title: '人员管理', icon: 'user', hidden: false },
children: [
{
path: '/organization/hr/member',
name: 'HRMember',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '成员', icon: 'user', hidden: false },
},
{
path: '/organization/hr/dept',
name: 'HRDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门', icon: 'dept', hidden: false },
},
{
path: '/organization/hr/workload',
name: 'HRWorkload',
component: () => import('@/views/hr/workload/index.vue'),
meta: { title: '工作量', icon: 'workload', hidden: false },
},
{
path: '/organization/hr/attendance',
name: 'HRAttendance',
component: () => import('@/views/hr/attendance/index.vue'),
meta: { title: '考勤', icon: 'attendance', hidden: false },
},
{
path: '/organization/hr/performance',
name: 'HRPerformance',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '绩效', icon: 'performance', hidden: false },
children: [
{
path: '/organization/hr/performance/dimention',
name: 'Dimention',
component: () => import('@/views/performance/setting/index.vue'),
meta: { title: '绩效维度', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/rule',
name: 'Rule',
component: () => import('@/views/performance/rule.vue'),
meta: { title: '绩效细则', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/my',
name: 'MyPerformance',
component: () => import('@/views/performance/my.vue'),
meta: { title: '我的绩效', icon: 'performance', hidden: false },
},
],
},
{
path: '/organization/hr/salary',
name: 'HRSalary',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/overview',
meta: { title: '工资', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/overview',
name: 'HRSalaryOverview',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '工资概览', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/payroll',
name: 'Payroll',
component: () => import('@/views/salary-management/index.vue'),
meta: { title: '工资单', icon: 'salary', hidden: false },
},
],
},
],
},
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// }
// ]
// },
{
path: '/organization/hr/salary/system-insurance/health-management',
name: 'HRSystemHealthManagement',
component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
meta: { title: '健康档案管理', icon: 'heart', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance',
name: 'HRSystemInsurance',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/system-insurance/overview',
meta: { title: '人员保险', icon: 'settings', hidden: false },
children: [
{
path: '/organization/hr/salary/system-insurance/overview',
name: 'HRSystemInsuranceOverview',
component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/management',
name: 'HRSystemInsuranceManagement',
component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
meta: { title: '保险管理', icon: 'shield', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/file-management',
name: 'HRSystemFileManagement',
component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
meta: { title: '保单文件管理', icon: 'file', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/company-management',
name: 'HRSystemCompanyManagement',
component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
meta: { title: '保险公司管理', icon: 'building', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/type-management',
name: 'HRSystemTypeManagement',
component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
meta: { title: '保险类型管理', icon: 'category', hidden: false },
},
],
},
{
path: '/organization/hr/salary/certification',
name: 'HRCertification',
component: () => import('@/views/hr/salary/certification/index.vue'),
meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
},
{
path: '/organization/hr/contribution',
name: 'HRContribution',
component: () => import('@/views/hr/contribution/index.vue'),
meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
},
],
},
{
path: '/organization/dept',
name: 'OrganizationDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门管理', icon: 'mind-mapping', hidden: false, sort: 2.5 },
path: '/organization/role',
name: 'OrganizationRole',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role', hidden: false },
},
{
path: '/organization/post',
name: 'OrganizationPost',
component: () => import('@/views/system/post/index.vue'),
meta: { title: '岗位管理', icon: 'nav', hidden: false, sort: 2.75 },
},
// {
// path: '/organization/hr/workload',
// name: 'HRWorkload',
// component: () => import('@/views/hr/workload/index.vue'),
// meta: { title: '任务管理', icon: 'bookmark', hidden: false },
// },
],
},
// {
// path: '/organization',
// name: 'Organization',
// component: Layout,
// redirect: '/organization/hr/member',
// meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
// children: [
// {
// path: '/organization/hr',
// name: 'HRManagement',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/member',
// meta: { title: '人员管理', icon: 'user', hidden: false },
// children: [
// {
// path: '/organization/hr/member',
// name: 'HRMember',
// component: () => import('@/views/system/user/index.vue'),
// meta: { title: '成员', icon: 'user', hidden: false },
// },
// {
// path: '/organization/hr/dept',
// name: 'HRDept',
// component: () => import('@/views/system/dept/index.vue'),
// meta: { title: '部门', icon: 'dept', hidden: false },
// },
// {
// path: '/organization/hr/workload',
// name: 'HRWorkload',
// component: () => import('@/views/hr/workload/index.vue'),
// meta: { title: '任务管理', icon: 'workload', hidden: false },
// },
// {
// path: '/organization/hr/attendance',
// name: 'HRAttendance',
// component: () => import('@/views/hr/attendance/index.vue'),
// meta: { title: '考勤', icon: 'attendance', hidden: false },
// },
// {
// path: '/organization/hr/performance',
// name: 'HRPerformance',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '绩效', icon: 'performance', hidden: false },
// children: [
// {
// path: '/organization/hr/performance/dimention',
// name: 'Dimention',
// component: () => import('@/views/performance/setting/index.vue'),
// meta: { title: '绩效维度', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/rule',
// name: 'Rule',
// component: () => import('@/views/performance/rule.vue'),
// meta: { title: '绩效细则', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/my',
// name: 'MyPerformance',
// component: () => import('@/views/performance/my.vue'),
// meta: { title: '我的绩效', icon: 'performance', hidden: false },
//
// },
// ],
// },
// {
// path: '/organization/hr/salary',
// name: 'HRSalary',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/overview',
// meta: { title: '工资', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/overview',
// name: 'HRSalaryOverview',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '工资概览', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/payroll',
// name: 'Payroll',
// component: () => import('@/views/salary-management/index.vue'),
// meta: { title: '工资单', icon: 'salary', hidden: false },
//
// },
// ],
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/system-insurance/health-management',
// name: 'HRSystemHealthManagement',
// component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
// meta: { title: '健康档案管理', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance',
// name: 'HRSystemInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/system-insurance/overview',
// meta: { title: '人员保险', icon: 'settings', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/system-insurance/overview',
// name: 'HRSystemInsuranceOverview',
// component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/management',
// name: 'HRSystemInsuranceManagement',
// component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
// meta: { title: '保险管理', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/file-management',
// name: 'HRSystemFileManagement',
// component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
// meta: { title: '保单文件管理', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/company-management',
// name: 'HRSystemCompanyManagement',
// component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
// meta: { title: '保险公司管理', icon: 'building', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/type-management',
// name: 'HRSystemTypeManagement',
// component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
// meta: { title: '保险类型管理', icon: 'category', hidden: false },
// },
// ],
// },
// {
// path: '/organization/hr/salary/certification',
// name: 'HRCertification',
// component: () => import('@/views/hr/salary/certification/index.vue'),
// meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
// },
// {
// path: '/organization/hr/contribution',
// name: 'HRContribution',
// component: () => import('@/views/hr/contribution/index.vue'),
// meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
// },
// ],
// },
// {
// path: '/organization/role',
// name: 'OrganizationRole',
// component: () => import('@/views/system/role/index.vue'),
// meta: { title: '角色管理', icon: 'role', hidden: false },
// },
// ],
// },
{
path: '/asset-management',
name: 'AssetManagement',
@ -558,9 +489,9 @@ export const systemRoutes: RouteRecordRaw[] = [
},
},
{
path: '/project-management/project-template/information-retrieval',
path: 'project-management/project-template/information-retrieval',
name: 'InformationRetrieval',
component: () => import ('@/views/project-management/bidding/information-retrieval/index.vue'),
component: () => import ('@/views/default/error/404.vue'),
meta: {
title: '信息检索(N)',
icon: 'trophy',
@ -703,26 +634,6 @@ export const systemRoutes: RouteRecordRaw[] = [
],
},
// 添加商务知识库
{
path: '/bussiness-knowledge',
name: 'bussinesskonwledge',
component: Layout,
redirect: '/bussiness-knowledge/data',
meta: { title: '商务资料知识库', icon: 'message', hidden: false, sort: 6 },
children: [
{
path: '/bussiness-konwledge/data',
name: 'bussiness-knowledge',
component: () => import('@/views/bussiness-data/bussiness.vue'),
meta: {
title: '商务数据库信息',
icon: 'info-circle',
hidden: false,
},
},
],
},
{
path: 'project-management/project-implementation/',
name: 'Project-Implementation',
@ -829,11 +740,11 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
component: () => import('@/views/operation-platform/data-processing/data-storage/index.vue'),
meta: { title: '附件管理', icon: 'attachment', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/model-config',
name: 'ModelConfig',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
meta: { title: '模型配置', icon: 'robot', hidden: false },
@ -844,23 +755,8 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data',
name: 'PreprocessedData',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/data-storage',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '数据预处理', icon: 'filter', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageBatchUpload',
name: 'ImageBatchUpload',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '批量上传', icon: 'file', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageSorting',
name: 'ImageSorting',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/image-sorting/index.vue'),
meta: { title: '图像分拣', icon: 'attachment', hidden: false },
},
],
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection',
@ -1036,30 +932,6 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
],
},
{
path: '/user/profile',
name: 'UserProfile',
component: Layout,
redirect: '/user/profile',
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
sort: 100,
},
children: [
{
path: '/user/profile',
name: 'UsersProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
},
},
],
},
{
path: '/enterprise-settings',
name: 'EnterpriseSettings',
@ -1263,6 +1135,7 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/',
redirect: '/project-management/project-template/project-aproval',
@ -1288,11 +1161,6 @@ export const constantRoutes: RouteRecordRaw[] = [
},
],
},
// {
// path: '/user/profile',
// component: () => import('@/views/user/profile/index.vue'),
// meta: { hidden: true },
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),

View File

@ -1,49 +0,0 @@
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,9 +4,10 @@ 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 } from '@/apis'
import { type RouteItem, getUserRouteWithAdapter } 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'),
@ -93,63 +94,91 @@ 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": 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
}
]
}]
// 使用已转换的数据生成路由
const asyncRoutes = formatAsyncRoutes(data as unknown as RouteItem[])

View File

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

View File

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

View File

@ -1,603 +0,0 @@
<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

@ -183,11 +183,12 @@ const handleSave = async () => {
console.log("requestData:",requestData);
//
const response = await axios.post(
const response = await axios.put(
`http://pms.dtyx.net:9158/image/setting-info/${editingData.value.partId}`,
requestData
)
if (response.data && response.data.code === 200 && response.data.success) {
if (response.data && response.data.code === 0) {
Message.success('图片信息保存成功')
emit('save', editingData.value)
emit('update:previewModalVisible', false)
@ -239,4 +240,4 @@ const handleSave = async () => {
}
}
}
</style>
</style>

View File

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

View File

@ -1,10 +1,8 @@
<!--任务管理-->
<template>
<GiPageLayout>
<a-button type="primary" style="margin-bottom: 16px" @click="openAddModal">发布任务</a-button>
<GiTable
row-key="taskId"
title="任务记录"
row-key="id"
title="工作量管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
@ -13,74 +11,38 @@
@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 #status="{ record }">
<a-tag :color="getTaskStatusColor(record.status)">{{ getTaskStatusText(record.status) }}</a-tag>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增工作量记录</template>
</a-button>
</template>
<!-- 工作量显示 -->
<template #workload="{ record }">
<span class="font-medium text-blue-600">{{ record.workload }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click.stop="onRowClick(record)">详情</a-link>
<a-link status="danger" @click.stop="deleteRecord(record)">删除</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link status="danger" @click="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>
@ -88,52 +50,13 @@
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({
taskName: '',
responsiblePerson: '', // mainUserId
status: '',
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
@ -141,241 +64,119 @@ let searchForm = reactive({
//
const queryFormColumns = [
{
field: 'taskName',
label: '任务标题',
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入任务标题',
style: { width: '200px' }
placeholder: '请输入员工姓名'
}
},
{
field: 'responsiblePerson',
label: '分配用户',
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入分配用户',
style: { width: '200px' }
}
},
{
field: 'status',
label: '任务状态',
type: 'select' as const,
props: {
placeholder: '请选择任务状态',
options: statusOptions,
style: { width: '200px' }
placeholder: '请输入项目名称'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ 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: '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: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref<any[]>([])
const allData = ref<any[]>([])
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 pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
total: 2,
showTotal: true,
showPageSize: true
})
//
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 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()
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = async () => {
const reset = () => {
Object.assign(searchForm, {
taskName: '',
responsiblePerson: '',
status: '',
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
pagination.current = 1
filterAndPaginate()
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
filterAndPaginate()
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
filterAndPaginate()
search()
}
//
const openAddModal = () => {
Message.info('新增工作量记录功能开发中...')
}
const editRecord = (record: any) => {
Message.info(`编辑工作量记录: ${record.userName}`)
}
//
const deleteRecord = (record: any) => {
const idx = allData.value.findIndex(item => item.taskId === record.taskId)
if (idx !== -1) {
allData.value.splice(idx, 1)
filterAndPaginate()
Message.success('任务已删除')
}
Message.info(`删除工作量记录: ${record.userName}`)
}
onMounted(() => {
fetchTaskList()
search()
})
</script>

View File

@ -1,337 +0,0 @@
<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,297 +1,151 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<!-- <div class="page-header">
<h2 class="page-title">图像音频关联查看</h2>
</div>-->
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">数据入库 - 1号机组叶片A型检查</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="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="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="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-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
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<h3 class="section-title">已上传数据</h3>
<a-table
:columns="fileColumns"
:data="imageList"
:data="uploadedFiles"
:pagination="false"
: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>
:scroll="{ x: '100%' }"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ record.imageTypeLabel}}</span>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.preTreatment)" size="small">
{{ record.preTreatment }}
</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)" size="small">
{{ record.status }}
</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>
<!-- 操作 -->
<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>
</div>
</div>
</a-tab-pane>
</a-tabs>
</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>
<!-- 文件预览模态框 -->
<PreviewModal ref="previewModal" />
<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>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed,onMounted } from 'vue'
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PreviewModal from './components/PreviewModal.vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
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[]>([])
@ -301,32 +155,65 @@ const previewModalVisible = ref(false)
const previewFileData = ref<any>(null)
//
const uploadedFiles = ref([])
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 fileColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'name', width: 250 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 100 },
{ title: '类型描述', dataIndex: 'imageTypeLabel', slotName: 'imageTypeLabel', width: 100 },
{ title: '上传时间', dataIndex: 'shootingTime', width: 150 },
{ title: '状态', dataIndex: 'preTreatment', slotName: 'preTreatment', width: 100 },
{ title: '大小', dataIndex: 'size', slotName: 'size', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
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 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 getFileTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'TYPICAL': 'blue',
'DEFECT': 'orange',
'image': 'blue',
'video': 'green',
'audio': 'orange',
'document': 'purple',
'other': 'gray'
}
return colorMap[type] || 'gray'
@ -334,10 +221,11 @@ const getFileTypeColor = (type: string) => {
//
const getStatusColor = (status: string) => {
console.log(status);
const colorMap: Record<string, string> = {
'已审核': 'green',
'未审核': 'red'
'成功': 'green',
'失败': 'red',
'上传中': 'blue',
'等待': 'orange'
}
return colorMap[status] || 'gray'
}
@ -351,30 +239,107 @@ const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
}
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 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 previewFile = (file: any) => {
/* 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)
previewFileData.value = file
previewModalVisible.value = true
}
//
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)
@ -392,7 +357,7 @@ const deleteFile = (file: any) => {
.page-header {
margin-bottom: 16px;
.page-title {
font-size: 20px;
font-weight: 500;
@ -412,22 +377,65 @@ const deleteFile = (file: any) => {
margin-top: 16px;
}
.filter-section {
.upload-section {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.filter-item {
display: inline-flex;
align-items: center;
.filter-label {
font-size: 14px;
color: #595959;
margin-right: 8px;
.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;
}
}
}
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.uploaded-files-section {
@ -441,16 +449,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;
@ -491,4 +499,4 @@ const deleteFile = (file: any) => {
:deep(.arco-upload-drag:hover) {
border-color: #1890ff;
}
</style>
</style>

View File

@ -1,198 +0,0 @@
<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

@ -1,164 +0,0 @@
<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

@ -1,344 +0,0 @@
<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

@ -1,415 +0,0 @@
<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

@ -1,64 +0,0 @@
// 项目状态枚举
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

@ -1,281 +1,409 @@
<template>
<GiPageLayout>
<div class="deformation-detection-container">
<!-- 页面标题 -->
<div class="raw-data-container">
<div class="page-header">
<h2 class="page-title">形变检测</h2>
<div class="page-title">原始数据管理</div>
<div class="page-subtitle">管理和分析原始视频数据</div>
</div>
<!-- 查询表单 -->
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item label="开始日期">
<a-date-picker
v-model="searchForm.startDate"
style="width: 200px"
format="YYYY-MM-DD"
:placeholder="'选择开始日期'"
/>
<div class="action-bar">
<div class="action-buttons">
<a-button type="primary" @click="showUploadModal = true">
<template #icon>
<IconUpload />
</template>
上传视频
</a-button>
<a-button type="primary" @click="handleBatchAnalysis">
<template #icon>
<IconPlayCircle />
</template>
批量分析
</a-button>
<a-button type="primary" @click="handleExportData">
<template #icon>
<IconDownload />
</template>
导出数据
</a-button>
</div>
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目">
<a-select v-model="filterForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号">
<a-input v-model="filterForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model="filterForm.status" placeholder="请选择状态">
<a-option value="completed">已完成</a-option>
<a-option value="pending">待分析</a-option>
<a-option value="analyzing">分析中</a-option>
<a-option value="failed">失败</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleFilterChange">查询</a-button>
</a-form-item>
</a-form>
</div>
</div>
<div class="project-sections">
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
<div class="project-header">
<div class="project-title">{{ project.name }}</div>
<div class="project-stats">
<div class="stat-item">
<IconVideoCamera />
{{ project.totalVideos }} 个视频
</div>
<div class="stat-item">
<IconCheckCircle />
{{ project.completedCount }} 个已完成
</div>
<div class="stat-item">
<IconClockCircle />
{{ project.pendingCount }} 个待分析
</div>
</div>
</div>
<div class="units-grid">
<div v-for="unit in project.units" :key="unit.id" class="unit-card">
<div class="unit-header">
<div class="unit-title">{{ unit.number }}</div>
<div class="unit-actions">
<a-button type="primary" size="small" @click="handleViewUnitVideos(unit)">查看全部</a-button>
<a-button type="primary" size="small" @click="handleAnalyzeUnit(unit)">{{
getAnalysisButtonText(unit.status)
}}</a-button>
</div>
</div>
<div class="videos-list">
<div v-for="video in unit.videos" :key="video.id" class="video-item">
<div class="video-thumbnail">
<img :src="video.thumbnail" alt="Video Thumbnail" />
<div class="video-overlay" @click="handlePlayVideo(video)">
<IconPlayArrowFill style="font-size: 24px; color: #fff;" />
</div>
</div>
<div class="video-info">
<div class="video-name">{{ video.name }}</div>
<div class="video-meta">
<span>{{ video.duration }}</span>
<span>{{ video.angle }}°</span>
</div>
<div class="video-status">
<a-tag :color="getStatusColor(video.status)">{{ getStatusText(video.status) }}</a-tag>
</div>
</div>
</div>
</div>
<div class="analysis-progress">
<div class="progress-info">
<span>分析进度</span>
<span>{{ unit.progress }}%</span>
</div>
<a-progress :percent="unit.progress" :show-text="false" status="normal" />
</div>
</div>
</div>
</div>
</div>
<!-- 视频播放模态框 -->
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false"
@cancel="videoModalVisible = false">
<video v-if="selectedVideo" :src="selectedVideo.url" controls
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video>
<div v-if="selectedVideo" class="video-meta-info">
<p>项目{{ selectedVideo.projectName }}</p>
<p>机组号{{ selectedVideo.unitNumber }}</p>
<p>采集人{{ selectedVideo.collector }}</p>
<p>风速{{ selectedVideo.windSpeed }} m/s</p>
<p>转速{{ selectedVideo.rpm }} rpm</p>
<p>采集时间{{ selectedVideo.time }}</p>
<p>角度{{ selectedVideo.angle }}°</p>
</div>
</a-modal>
<!-- 上传视频模态框 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload"
@cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="至">
<a-date-picker
v-model="searchForm.endDate"
style="width: 200px"
format="YYYY-MM-DD"
:placeholder="'选择结束日期'"
/>
<a-form-item label="机组号" required>
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
查询
</a-button>
<a-form-item label="采集人" required>
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" />
</a-form-item>
<a-form-item>
<a-button @click="handleExport">
<template #icon><icon-download /></template>
导出数据
</a-button>
<a-form-item label="风速 (m/s)">
<a-input-number v-model="uploadForm.windSpeed" :min="0" />
</a-form-item>
<a-form-item label="转速 (rpm)">
<a-input-number v-model="uploadForm.rpm" :min="0" />
</a-form-item>
<a-form-item label="采集时间" required>
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" />
</a-form-item>
<a-form-item label="视频文件可多选建议3个角度" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="true" :limit="3" accept="video/*"
:auto-upload="false" list-type="picture-card">
<template #upload-button>
<a-button>选择视频</a-button>
</template>
</a-upload>
</a-form-item>
</a-form>
</a-card>
<!-- 数据表格 -->
<a-card class="table-card" :bordered="false">
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<!-- 状态列 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-link @click="handleViewDetail(record)">详情</a-link>
</template>
</a-table>
</a-card>
<!-- 详情模态框 -->
<a-modal
v-model:visible="detailModalVisible"
title="形变检测详情"
width="800px"
@ok="detailModalVisible = false"
@cancel="detailModalVisible = false"
>
<div v-if="selectedRecord" class="detail-content">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="日期">
{{ selectedRecord.date }}
</a-descriptions-item>
<a-descriptions-item label="机组编号">
{{ selectedRecord.unitNumber }}
</a-descriptions-item>
<a-descriptions-item label="X方向形变量">
{{ selectedRecord.xDeformation }}mm
</a-descriptions-item>
<a-descriptions-item label="Y方向形变量">
{{ selectedRecord.yDeformation }}mm
</a-descriptions-item>
<a-descriptions-item label="Z方向形变量">
{{ selectedRecord.zDeformation }}mm
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(selectedRecord.status)">
{{ getStatusText(selectedRecord.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="检测时间">
{{ selectedRecord.detectionTime }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ selectedRecord.remark || '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconDownload } from '@arco-design/web-vue/es/icon'
import type { TableColumnData } from '@arco-design/web-vue'
import {
IconUpload,
IconPlayCircle,
IconDownload,
IconVideoCamera,
IconCheckCircle,
IconClockCircle,
IconPlayArrowFill
} from '@arco-design/web-vue/es/icon'
defineOptions({ name: 'DeformationDetection' })
const showUploadModal = ref(false)
const videoModalVisible = ref(false)
const selectedVideo = ref<any>(null)
//
const searchForm = reactive({
startDate: '',
endDate: ''
const filterForm = reactive({
projectId: '',
unitNumber: '',
status: ''
})
//
const tableData = ref([
const uploadForm = reactive({
projectId: '',
unitNumber: '',
collector: '',
windSpeed: null,
rpm: null,
time: '',
fileList: []
})
//
const projects = ref([
{
id: 1,
date: '2023-11-05',
unitNumber: 'A-001',
xDeformation: 2.1,
yDeformation: 1.5,
zDeformation: 0.8,
status: 'normal',
detectionTime: '2023-11-05 14:30:00',
remark: '正常范围内'
},
{
id: 2,
date: '2023-11-06',
unitNumber: 'A-001',
xDeformation: 2.3,
yDeformation: 1.6,
zDeformation: 0.9,
status: 'normal',
detectionTime: '2023-11-06 14:30:00',
remark: '正常范围内'
},
{
id: 3,
date: '2023-11-07',
unitNumber: 'A-001',
xDeformation: 3.5,
yDeformation: 2.8,
zDeformation: 1.2,
status: 'warning',
detectionTime: '2023-11-07 14:30:00',
remark: '超出正常范围,需要关注'
id: 'project-1',
name: '风电场A区',
totalVideos: 6,
completedCount: 4,
pendingCount: 2,
units: [
{
id: 'A-001',
number: 'A-001',
status: 'completed',
progress: 100,
videos: [
{
id: 'v1',
name: 'A-001-正面',
url: '/videos/A-001-front.mp4',
thumbnail: '/images/A-001-front.jpg',
angle: 0,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v2',
name: 'A-001-侧面',
url: '/videos/A-001-side.mp4',
thumbnail: '/images/A-001-side.jpg',
angle: 90,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v3',
name: 'A-001-背面',
url: '/videos/A-001-back.mp4',
thumbnail: '/images/A-001-back.jpg',
angle: 180,
duration: '00:30',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
}
]
},
{
id: 'A-002',
number: 'A-002',
status: 'analyzing',
progress: 60,
videos: [
{
id: 'v4',
name: 'A-002-正面',
url: '/videos/A-002-front.mp4',
thumbnail: '/images/A-002-front.jpg',
angle: 0,
duration: '00:28',
status: 'analyzing',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v5',
name: 'A-002-侧面',
url: '/videos/A-002-side.mp4',
thumbnail: '/images/A-002-side.jpg',
angle: 90,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v6',
name: 'A-002-背面',
url: '/videos/A-002-back.mp4',
thumbnail: '/images/A-002-back.jpg',
angle: 180,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
}
]
}
]
}
// ...
])
//
const columns: TableColumnData[] = [
{
title: '日期',
dataIndex: 'date',
width: 120
},
{
title: '机组编号',
dataIndex: 'unitNumber',
width: 100
},
{
title: 'X方向形变量(mm)',
dataIndex: 'xDeformation',
width: 140
},
{
title: 'Y方向形变量(mm)',
dataIndex: 'yDeformation',
width: 140
},
{
title: 'Z方向形变量(mm)',
dataIndex: 'zDeformation',
width: 140
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 100
},
{
title: '操作',
slotName: 'action',
width: 100,
fixed: 'right'
}
]
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 100,
showTotal: true,
showPageSize: true
const filteredProjects = computed(() => {
//
return projects.value
.filter(p => !filterForm.projectId || p.id === filterForm.projectId)
.map(project => ({
...project,
units: project.units
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber)
.map(unit => ({
...unit,
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
}))
.filter(u => u.videos.length > 0)
}))
.filter(p => p.units.length > 0)
})
//
const loading = ref(false)
function handleFilterChange() {
// API
}
//
const detailModalVisible = ref(false)
const selectedRecord = ref<any>(null)
function handlePlayVideo(video: any) {
selectedVideo.value = video
videoModalVisible.value = true
}
//
const getStatusColor = (status: string) => {
const colorMap = {
normal: 'green',
warning: 'orange',
error: 'red'
function handleViewUnitVideos(unit: any) {
//
Message.info(`查看机组 ${unit.number} 的所有视频`)
}
function handleAnalyzeUnit(unit: any) {
//
Message.success(`已提交机组 ${unit.number} 的分析任务`)
// API
}
function getStatusColor(status: string) {
switch (status) {
case 'completed': return 'green'
case 'pending': return 'gray'
case 'analyzing': return 'blue'
case 'failed': return 'red'
default: return 'gray'
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap = {
normal: '正常',
warning: '警告',
error: '异常'
function getStatusText(status: string) {
switch (status) {
case 'completed': return '已完成'
case 'pending': return '待分析'
case 'analyzing': return '分析中'
case 'failed': return '失败'
default: return '未知'
}
}
function getAnalysisButtonText(status: string) {
switch (status) {
case 'completed': return '重新分析'
case 'pending': return '分析'
case 'analyzing': return '分析中...'
case 'failed': return '重新分析'
default: return '分析'
}
return textMap[status] || '未知'
}
//
const handleSearch = () => {
loading.value = true
// API
setTimeout(() => {
Message.success('查询成功')
loading.value = false
}, 1000)
function handleBatchAnalysis() {
Message.success('批量分析任务已提交')
}
//
const handleExport = () => {
Message.success('导出成功')
function handleExportData() {
Message.success('数据导出成功')
}
//
const handlePageChange = (page: number) => {
pagination.current = page
handleSearch()
function handleUpload() {
Message.success('上传成功')
showUploadModal.value = false
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
handleSearch()
}
//
const handleViewDetail = (record: any) => {
selectedRecord.value = record
detailModalVisible.value = true
}
onMounted(() => {
handleSearch()
})
</script>
<style scoped lang="scss">
.deformation-detection-container {
.raw-data-container {
padding: 20px;
}
@ -284,28 +412,179 @@ onMounted(() => {
}
.page-title {
font-size: 24px;
font-size: 28px;
font-weight: 600;
margin: 0;
margin: 0 0 8px 0;
color: #1d2129;
}
.search-card {
margin-bottom: 20px;
.page-subtitle {
font-size: 14px;
color: #86909c;
margin: 0;
}
.table-card {
.arco-table {
.arco-table-th {
background-color: #f7f8fa;
font-weight: 600;
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.filter-section {
margin-left: 24px;
}
}
.project-sections {
.project-section {
margin-bottom: 32px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px #f0f1f2;
padding: 20px;
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.project-title {
font-size: 20px;
font-weight: 600;
}
.project-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
color: #86909c;
}
}
}
.units-grid {
display: flex;
gap: 24px;
flex-wrap: wrap;
.unit-card {
background: #fafbfc;
border-radius: 8px;
padding: 16px;
width: 360px;
margin-bottom: 16px;
.unit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.unit-title {
font-size: 16px;
font-weight: 600;
}
.unit-actions {
display: flex;
gap: 8px;
}
}
.videos-list {
display: flex;
gap: 12px;
margin-bottom: 8px;
.video-item {
width: 100px;
.video-thumbnail {
position: relative;
width: 100px;
height: 60px;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
.video-info {
margin-top: 4px;
.video-name {
font-size: 12px;
font-weight: 500;
color: #1d2129;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.video-meta {
font-size: 11px;
color: #86909c;
display: flex;
gap: 4px;
}
.video-status {
margin-top: 2px;
}
}
}
}
.analysis-progress {
margin-top: 8px;
.progress-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #86909c;
margin-bottom: 2px;
}
}
}
}
}
}
.detail-content {
.arco-descriptions {
margin-top: 16px;
.video-meta-info {
margin-top: 16px;
font-size: 13px;
color: #4e5969;
p {
margin: 2px 0;
}
}
</style>
</style>

View File

@ -1,17 +1,797 @@
<template>
<div class="WideAngleVideo-container">
<a-card title="广角视频" :bordered="false">
<a-empty description="广角视频模块正在建设中..." />
</a-card>
<GiPageLayout>
<div class="analysis-report-container">
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">检测分析结果报告</h2>
<p class="page-subtitle">基于计算机视觉的风机叶片净空距离与形变状态分析</p>
</div>
<!-- 筛选和操作栏 -->
<div class="action-bar">
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目">
<a-select
v-model="filterForm.projectId"
placeholder="选择项目"
style="width: 150px"
@change="handleFilterChange"
>
<a-option value="">全部项目</a-option>
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号">
<a-input
v-model="filterForm.unitNumber"
placeholder="输入机组号"
style="width: 120px"
@change="handleFilterChange"
/>
</a-form-item>
<a-form-item label="检测状态">
<a-select
v-model="filterForm.status"
placeholder="选择状态"
style="width: 120px"
@change="handleFilterChange"
>
<a-option value="">全部状态</a-option>
<a-option value="normal">正常</a-option>
<a-option value="warning">警告</a-option>
<a-option value="error">异常</a-option>
</a-select>
</a-form-item>
<a-form-item label="检测日期">
<a-date-picker
v-model="filterForm.dateRange"
type="daterange"
style="width: 200px"
@change="handleFilterChange"
/>
</a-form-item>
</a-form>
</div>
<div class="action-buttons">
<a-button @click="handleExportReport">
<template #icon><icon-download /></template>
导出报告
</a-button>
<a-button @click="handleBatchExport">
<template #icon><icon-file-pdf /></template>
批量导出
</a-button>
</div>
</div>
<!-- 统计概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="overview-card">
<div class="overview-item">
<div class="overview-icon normal">
<icon-check-circle />
</div>
<div class="overview-content">
<div class="overview-number">{{ overviewData.normalCount }}</div>
<div class="overview-label">正常叶片</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="overview-card">
<div class="overview-item">
<div class="overview-icon warning">
<icon-exclamation-circle />
</div>
<div class="overview-content">
<div class="overview-number">{{ overviewData.warningCount }}</div>
<div class="overview-label">警告叶片</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="overview-card">
<div class="overview-item">
<div class="overview-icon error">
<icon-close-circle />
</div>
<div class="overview-content">
<div class="overview-number">{{ overviewData.errorCount }}</div>
<div class="overview-label">异常叶片</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="overview-card">
<div class="overview-item">
<div class="overview-icon total">
<icon-file />
</div>
<div class="overview-content">
<div class="overview-number">{{ overviewData.totalCount }}</div>
<div class="overview-label">总检测数</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 检测结果列表 -->
<div class="results-section">
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
<div class="project-header">
<h3 class="project-title">{{ project.name }}</h3>
<div class="project-stats">
<span class="stat-item">
<icon-file />
{{ project.totalUnits }} 个机组
</span>
<span class="stat-item">
<icon-check-circle />
{{ project.completedUnits }} 已完成
</span>
</div>
</div>
<div class="units-list">
<div v-for="unit in project.units" :key="unit.id" class="unit-result-card">
<div class="unit-header">
<div class="unit-info">
<h4 class="unit-title">{{ unit.number }}</h4>
<div class="unit-meta">
<span>检测时间{{ unit.detectionTime }}</span>
<span>检测人员{{ unit.operator }}</span>
</div>
</div>
<div class="unit-status">
<a-tag :color="getStatusColor(unit.status)">
{{ getStatusText(unit.status) }}
</a-tag>
</div>
</div>
<div class="blades-analysis">
<div v-for="(blade, index) in unit.blades" :key="blade.id" class="blade-result">
<!-- 图片区域左右并排 -->
<div class="blade-content">
<div class="result-images">
<div class="image-section">
<h6>净空距离检测</h6>
<div class="image-container large">
<img :src="blade.clearanceImage" />
<div class="image-overlay">
<a-button type="primary" size="small" @click="handleViewImage(blade.clearanceImage)">
<template #icon><icon-eye /></template> 查看大图
</a-button>
</div>
</div>
</div>
<div class="image-section">
<h6>形变状态检测</h6>
<div class="image-container large">
<img :src="blade.deformationImage" />
<div class="image-overlay">
<a-button type="primary" size="small" @click="handleViewImage(blade.deformationImage)">
<template #icon><icon-eye /></template> 查看大图
</a-button>
</div>
</div>
</div>
</div>
<!-- 右侧结果报告 -->
<div class="result-report">
<div class="blade-topbar">
<a-tag :color="getStatusColor(blade.status)" size="small">
叶片{{ index + 1 }} · {{ getStatusText(blade.status) }}
</a-tag>
</div>
<div class="report-section">
<h6>净空距离测量</h6>
<div class="measurement-data">
<div class="data-item">
<span class="label">最小净空距离</span>
<span class="value">{{ blade.clearanceDistance.min }}m</span>
</div>
<div class="data-item">
<span class="label">平均净空距离</span>
<span class="value">{{ blade.clearanceDistance.avg }}m</span>
</div>
<div class="data-item">
<span class="label">安全阈值</span>
<span class="value">{{ blade.clearanceDistance.threshold }}m</span>
</div>
<div class="data-item">
<span class="label">安全系数</span>
<span class="value" :class="getSafetyClass(blade.clearanceDistance.safetyFactor)">
{{ blade.clearanceDistance.safetyFactor }}
</span>
</div>
</div>
</div>
<div class="report-section">
<h6>形变状态分析</h6>
<div class="deformation-data">
<div class="data-item">
<span class="label">最大形变量</span>
<span class="value">{{ blade.deformation.max }}mm</span>
</div>
<div class="data-item">
<span class="label">平均形变量</span>
<span class="value">{{ blade.deformation.avg }}mm</span>
</div>
<div class="data-item">
<span class="label">形变位置</span>
<span class="value">{{ blade.deformation.location }}</span>
</div>
<div class="data-item">
<span class="label">形变等级</span>
<a-tag :color="getDeformationColor(blade.deformation.level)" size="small">
{{ getDeformationText(blade.deformation.level) }}
</a-tag>
</div>
</div>
</div>
<div class="report-section">
<h6>检测结论</h6>
<div class="conclusion">
<p>{{ blade.conclusion }}</p>
</div>
</div>
<div class="report-section">
<h6>建议措施</h6>
<div class="recommendations">
<ul>
<li v-for="(rec, recIndex) in blade.recommendations" :key="recIndex">
{{ rec }}
</li>
</ul>
</div>
</div>
<div class="report-section">
<h6>检测参数</h6>
<div class="detection-params">
<div class="param-item">
<span class="label">风速</span>
<span class="value">{{ blade.params.windSpeed }}m/s</span>
</div>
<div class="param-item">
<span class="label">转速</span>
<span class="value">{{ blade.params.rpm }}rpm</span>
</div>
<div class="param-item">
<span class="label">温度</span>
<span class="value">{{ blade.params.temperature }}°C</span>
</div>
<div class="param-item">
<span class="label">湿度</span>
<span class="value">{{ blade.params.humidity }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图片大图预览模态框 -->
<a-modal
v-model:visible="imageModalVisible"
title="图片预览"
width="800px"
@ok="imageModalVisible = false"
@cancel="imageModalVisible = false"
>
<img :src="previewImage" alt="检测图片" style="width: 100%; border-radius: 8px;" />
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
defineOptions({ name: 'WideAngleVideo' })
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconDownload,
IconFilePdf,
IconCheckCircle,
IconExclamationCircle,
IconCloseCircle,
IconFile,
IconEye
} from '@arco-design/web-vue/es/icon'
const filterForm = reactive({
projectId: '',
unitNumber: '',
status: '',
dateRange: []
})
const imageModalVisible = ref(false)
const previewImage = ref('')
//
const overviewData = reactive({
normalCount: 12,
warningCount: 3,
errorCount: 1,
totalCount: 16
})
//
const projects = ref([
{
id: 'project-1',
name: '风电场A区',
totalUnits: 2,
completedUnits: 2,
units: [
{
id: 'A-001',
number: 'A-001',
detectionTime: '2023-11-05 08:00',
operator: '张三',
status: 'normal',
blades: [
{
id: 'A-001-blade-1',
status: 'normal',
clearanceImage: '/images/blade1_clearance.jpg',
deformationImage: '/images/blade1_deform.jpg',
clearanceDistance: {
min: 12.5,
avg: 13.2,
threshold: 10.0,
safetyFactor: 1.25
},
deformation: {
max: 2.1,
avg: 1.5,
location: '叶尖',
level: 'normal'
},
conclusion: '叶片净空距离充足,形变在正常范围,无需特殊处理。',
recommendations: [
'定期检测叶片净空与形变',
'关注极端天气下的叶片状态'
],
params: {
windSpeed: 8.2,
rpm: 15,
temperature: 15.3,
humidity: 60
}
},
{
id: 'A-001-blade-2',
status: 'warning',
clearanceImage: '/images/blade2_clearance.jpg',
deformationImage: '/images/blade2_deform.jpg',
clearanceDistance: {
min: 10.1,
avg: 10.5,
threshold: 10.0,
safetyFactor: 1.01
},
deformation: {
max: 4.5,
avg: 3.2,
location: '中部',
level: 'warning'
},
conclusion: '叶片净空距离接近安全阈值,形变略大,建议加强监测。',
recommendations: [
'缩短检测周期',
'必要时进行维护'
],
params: {
windSpeed: 8.2,
rpm: 15,
temperature: 15.3,
humidity: 60
}
},
{
id: 'A-001-blade-3',
status: 'normal',
clearanceImage: '/images/blade3_clearance.jpg',
deformationImage: '/images/blade3_deform.jpg',
clearanceDistance: {
min: 12.0,
avg: 12.8,
threshold: 10.0,
safetyFactor: 1.20
},
deformation: {
max: 2.0,
avg: 1.4,
location: '根部',
level: 'normal'
},
conclusion: '叶片状态良好。',
recommendations: [
'保持常规巡检'
],
params: {
windSpeed: 8.2,
rpm: 15,
temperature: 15.3,
humidity: 60
}
}
]
},
{
id: 'A-002',
number: 'A-002',
detectionTime: '2023-11-05 12:00',
operator: '李四',
status: 'error',
blades: [
{
id: 'A-002-blade-1',
status: 'error',
clearanceImage: '/images/blade1_clearance_error.jpg',
deformationImage: '/images/blade1_deform_error.jpg',
clearanceDistance: {
min: 8.5,
avg: 9.0,
threshold: 10.0,
safetyFactor: 0.85
},
deformation: {
max: 8.2,
avg: 6.5,
location: '叶尖',
level: 'error'
},
conclusion: '叶片净空距离低于安全阈值,形变严重,存在安全隐患。',
recommendations: [
'立即停机检查',
'联系技术人员处理'
],
params: {
windSpeed: 10.5,
rpm: 16,
temperature: 18.1,
humidity: 65
}
},
// ...
]
}
]
}
// ...
])
const filteredProjects = computed(() => {
// filterForm
return projects.value
})
function handleFilterChange() {
// API
}
function handleExportReport() {
Message.success('报告导出成功')
}
function handleBatchExport() {
Message.success('批量导出成功')
}
function handleViewImage(url: string) {
previewImage.value = url
imageModalVisible.value = true
}
function getStatusColor(status: string) {
switch (status) {
case 'normal': return 'green'
case 'warning': return 'orange'
case 'error': return 'red'
default: return 'gray'
}
}
function getStatusText(status: string) {
switch (status) {
case 'normal': return '正常'
case 'warning': return '警告'
case 'error': return '异常'
default: return '未知'
}
}
function getDeformationColor(level: string) {
switch (level) {
case 'normal': return 'green'
case 'warning': return 'orange'
case 'error': return 'red'
default: return 'gray'
}
}
function getDeformationText(level: string) {
switch (level) {
case 'normal': return '正常'
case 'warning': return '警告'
case 'error': return '异常'
default: return '未知'
}
}
function getSafetyClass(factor: number) {
if (factor >= 1.2) return 'safe'
if (factor >= 1.0) return 'warning'
return 'danger'
}
</script>
<style scoped lang="scss">
.WideAngleVideo-container {
.analysis-report-container {
padding: 20px;
height: 100vh;
overflow-y: auto;
box-sizing: border-box;
}
</style>
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1d2129;
}
.page-subtitle {
font-size: 14px;
color: #86909c;
margin: 0;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 20px;
.filter-section {
flex: 1;
}
.action-buttons {
display: flex;
gap: 8px;
}
}
.overview-section {
margin-bottom: 20px;
}
.overview-card {
.overview-item {
display: flex;
align-items: center;
padding: 16px;
.overview-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 24px;
color: white;
&.normal { background-color: #00b42a; }
&.warning { background-color: #ff7d00; }
&.error { background-color: #f53f3f; }
&.total { background-color: #165dff; }
}
.overview-content {
.overview-number {
font-size: 24px;
font-weight: 600;
color: #1d2129;
line-height: 1;
}
.overview-label {
font-size: 14px;
color: #86909c;
margin-top: 4px;
}
}
}
}
.results-section {
.project-section {
margin-bottom: 32px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px #f0f1f2;
padding: 20px;
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.project-title {
font-size: 20px;
font-weight: 600;
}
.project-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
color: #86909c;
}
}
}
.units-list {
.unit-result-card {
background: #fafbfc;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
.unit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.unit-info {
.unit-title {
font-size: 16px;
font-weight: 600;
}
.unit-meta {
font-size: 12px;
color: #86909c;
margin-top: 2px;
span {
margin-right: 16px;
}
}
}
}
.blades-analysis {
display: flex;
flex-direction: column;
gap: 24px;
.blade-result {
display: flex;
gap: 32px;
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 4px #f0f1f2;
padding: 16px;
.blade-header {
flex: 0 0 80px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
.blade-title {
font-size: 15px;
font-weight: 600;
}
}
.blade-content {
display: flex;
gap: 24px;
width: 100%;
.result-images {
display: flex;
flex-direction: column;
gap: 16px;
.image-section {
h6 {
font-size: 13px;
color: #4e5969;
margin-bottom: 4px;
}
.blade-topbar {
margin-bottom: 4px;
}
.image-container.large {
width: 220px; // 160px
height: 140px;
}
.image-container {
position: relative;
width: 160px;
height: 100px;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
}
}
.result-report {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
.report-section {
h6 {
font-size: 13px;
color: #165dff;
margin-bottom: 4px;
}
.measurement-data, .deformation-data, .detection-params {
display: flex;
flex-wrap: wrap;
gap: 16px;
.data-item, .param-item {
font-size: 13px;
color: #4e5969;
.label {
color: #86909c;
}
.value.safe { color: #00b42a; }
.value.warning { color: #ff7d00; }
.value.danger { color: #f53f3f; }
}
}
.conclusion {
p {
color: #1d2129;
font-weight: 500;
margin: 0;
}
}
.recommendations {
ul {
margin: 0;
padding-left: 20px;
li {
color: #4e5969;
margin-bottom: 4px;
}
}
}
}
}
}
}
}
}
}
}
}
</style>

View File

@ -1,439 +0,0 @@
<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

@ -1,108 +0,0 @@
<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

@ -1,907 +0,0 @@
<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

@ -1,467 +0,0 @@
<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

@ -1,404 +0,0 @@
<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

@ -1,313 +0,0 @@
<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>

View File

@ -1,5 +1,4 @@
<template>
<GiPageLayout>
<div>
<a-radio-group v-model="viewType" type="button" size="small" style="margin-bottom: 16px;">
@ -10,15 +9,13 @@
<GiTable
v-show="viewType === 'table'"
ref="tableRef"
row-key="deptId"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="false"
:disabled-column-keys="['name']"
:expanded-keys="expandedKeys"
@expand="onExpand"
@refresh="search"
>
<template #expand-icon="{ expanded }">
@ -52,7 +49,6 @@
<a-link v-permission="['system:dept:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:dept:delete']"
:disabled="record.children && record.children.length > 0"
status="danger"
title="删除"
@click="onDelete(record)"
@ -73,14 +69,14 @@
:collapsable="true"
:horizontal="false"
:define-menus="menus"
:expand-all="true"
:default-expand-level="999"
:props="{ id: 'deptId', parentId: 'parentId', label: 'deptName', children: 'children' }"
center
:node-add="handleAdd"
:node-delete="onDelete"
:node-edit="onUpdate"
:expanded-keys="nodeExpandedKeys"
@node-click="(node) => toggleExpand(node.deptId)"
@on-expand-all="bool => nodeExpandAll = bool"
>
</Vue3TreeOrg>
</a-dropdown>
@ -94,7 +90,6 @@
import 'vue3-tree-org/lib/vue3-tree-org.css'
import { Vue3TreeOrg } from 'vue3-tree-org'
import type { TableInstance } from '@arco-design/web-vue'
import { Message } from '@arco-design/web-vue'
import DeptAddModal from './DeptAddModal.vue'
import { type DeptQuery, type DeptResp, deleteDept, getDeptTree } from '@/apis/system/dept'
import type GiTable from '@/components/GiTable/index.vue'
@ -102,8 +97,6 @@ import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
const $message = Message
defineOptions({ name: 'SystemDept' })
const queryForm = reactive<DeptQuery>({})
@ -116,21 +109,9 @@ const {
} = useTable<DeptResp>(() => getDeptTree(queryForm), {
immediate: true,
onSuccess: () => {
if (isFirstLoad.value) {
// id
const collectKeys = (nodes: DeptResp[]): string[] => {
let keys: string[] = []
nodes.forEach(node => {
keys.push(node.deptId)
if (node.children && node.children.length) {
keys = keys.concat(collectKeys(node.children))
}
})
return keys
}
expandedKeys.value = collectKeys(tableData.value)
isFirstLoad.value = false
}
nextTick(() => {
tableRef.value?.tableRef?.expandAll(true)
})
},
})
//
@ -142,9 +123,7 @@ const menus = [
{ name: '删除部门', command: 'delete' },
]
//
//
const expandedKeys = ref<string[]>([])
const isFirstLoad = ref(true)
const nodeExpandAll = ref<boolean>(true)
//
const searchData = (name: string) => {
const loop = (data: DeptResp[]) => {
@ -201,13 +180,6 @@ const reset = () => {
//
const onDelete = (record: DeptResp) => {
//
if (record.children && record.children.length > 0) {
//
$message.warning('该部门存在子部门,无法直接删除')
return Promise.reject()
}
return handleDelete(() => deleteDept(record.deptId), {
content: `是否确定删除部门「${record.deptName}」?`,
showModal: true,
@ -232,31 +204,6 @@ const handleAdd = (record: DeptResp) => {
const onUpdate = (record: DeptResp) => {
DeptAddModalRef.value?.onUpdate(record.deptId)
}
const onExpand = (expanded: boolean, record: DeptResp) => {
const key = record.deptId
if (expanded) {
if (!expandedKeys.value.includes(key)) {
expandedKeys.value.push(key)
}
} else {
expandedKeys.value = expandedKeys.value.filter(k => k !== key)
}
}
// /
const toggleExpandAll = () => {
nodeExpandAll.value = !nodeExpandAll.value
}
// :
const nodeExpandedKeys = ref<string[]>([])
const toggleExpand = (deptId: string) => {
const index = nodeExpandedKeys.value.indexOf(deptId)
index > -1
? nodeExpandedKeys.value.splice(index, 1)
: nodeExpandedKeys.value.push(deptId)
}
</script>
<style scoped lang="scss">

View File

@ -27,8 +27,8 @@
<a-radio :value="0">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="说明">
<a-textarea v-model="formData.remark" placeholder="请输入岗位说明" allow-clear />
<a-form-item field="remark" label="备注">
<a-textarea v-model="formData.remark" placeholder="请输入备注" allow-clear />
</a-form-item>
</a-form>
</a-modal>

View File

@ -2,226 +2,29 @@
<a-drawer
v-model:visible="visible"
title="岗位详情"
width="720px"
width="500px"
:footer="false"
unmount-on-close
class="post-detail-drawer"
@close="() => visible = false"
>
<a-spin :loading="loading" class="detail-container">
<div class="detail-card">
<!-- 岗位标题和状态 -->
<div class="detail-header">
<div class="post-name">{{ primaryInfo?.name || '-' }}</div>
<a-tag class="status-tag" :color="getStatusColor(primaryInfo?.status)">
{{ getStatusText(primaryInfo?.status) }}
</a-tag>
</div>
<!-- 标签页菜单 -->
<a-tabs v-model:active-key="activeTab" class="detail-tabs" type="card">
<!-- 1. 基本信息 -->
<a-tab-pane key="basic" title="基本信息">
<div class="tab-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">岗位名称</div>
<div class="info-value">{{ primaryInfo?.name || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">所属部门</div>
<div class="info-value">{{ primaryInfo?.department || '技术部' }}</div>
</div>
<div class="info-item">
<div class="info-label">直接上级岗位</div>
<div class="info-value">{{ primaryInfo?.superior || '技术总监' }}</div>
</div>
<div class="info-item">
<div class="info-label">岗位等级/职级</div>
<div class="info-value">{{ primaryInfo?.level || 'P5' }}</div>
</div>
<div class="info-item">
<div class="info-label">编写日期/版本号</div>
<div class="info-value">{{ primaryInfo?.version || '2024-06-01 / V1.0' }}</div>
</div>
<div class="info-item">
<div class="info-label">岗位编号</div>
<div class="info-value">{{ primaryInfo?.id || '10001' }}</div>
</div>
<div class="info-item">
<div class="info-label">工作地点</div>
<div class="info-value">{{ primaryInfo?.location || '上海/远程' }}</div>
</div>
</div>
<!-- 岗位目的 -->
<div class="section-title">岗位目的</div>
<div class="content-container">
<div class="content-text">{{ primaryInfo?.summary || '负责公司核心产品开发,支撑业务增长。' }}</div>
</div>
</div>
</a-tab-pane>
<!-- 2. 工作职责 -->
<a-tab-pane key="tasks" title="工作职责">
<div class="tab-content">
<!-- 主要职责与工作任务 -->
<div class="section-title">主要职责与工作任务</div>
<div class="content-container">
<ul>
<li v-for="(task, idx) in (primaryInfo?.tasks || ['负责XXX产品的需求分析、架构设计、核心模块编码和单元测试。','制定并执行季度社交媒体营销计划,提升品牌曝光度和用户互动率。'])" :key="idx">{{ task }}</li>
</ul>
</div>
<!-- 工作权限 -->
<div class="section-title">工作权限</div>
<div class="content-container">
<ul>
<li v-for="(perm, idx) in (primaryInfo?.permissions || ['有权审批部门内5000元以下的采购申请。','有权对项目团队成员的工作任务进行分配和调整。'])" :key="idx">{{ perm }}</li>
</ul>
</div>
<!-- 汇报关系 -->
<div class="section-title">汇报关系</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">直接上级</div>
<div class="info-value">{{ primaryInfo?.superior || '技术总监' }}</div>
</div>
<div class="info-item">
<div class="info-label">直接下级</div>
<div class="info-value">{{ primaryInfo?.subordinates || '无' }}</div>
</div>
<div class="info-item">
<div class="info-label">协作关系</div>
<div class="info-value">{{ primaryInfo?.collaboration || '与产品部、销售部、客服部紧密合作' }}</div>
</div>
</div>
</div>
</a-tab-pane>
<!-- 3. 任职资格 -->
<a-tab-pane key="qualifications" title="任职资格">
<div class="tab-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">教育背景</div>
<div class="info-value">{{ primaryInfo?.education || '本科及以上学历,计算机相关专业' }}</div>
</div>
<div class="info-item">
<div class="info-label">工作经验</div>
<div class="info-value">{{ primaryInfo?.experience || '5年以上相关工作经验' }}</div>
</div>
<div class="info-item">
<div class="info-label">专业知识与技能</div>
<div class="info-value">{{ primaryInfo?.skills || '精通Java/Python良好的沟通与团队协作能力' }}</div>
</div>
<div class="info-item">
<div class="info-label">证书/执照</div>
<div class="info-value">{{ primaryInfo?.certificates || 'PMP、注册会计师等' }}</div>
</div>
<div class="info-item">
<div class="info-label">其他要求</div>
<div class="info-value">{{ primaryInfo?.otherRequirements || '英语流利,能适应短期出差' }}</div>
</div>
</div>
</div>
</a-tab-pane>
<!-- 4. 工作条件 -->
<a-tab-pane key="conditions" title="工作条件">
<div class="tab-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">工作环境</div>
<div class="info-value">{{ primaryInfo?.workEnv || '办公室/远程' }}</div>
</div>
<div class="info-item">
<div class="info-label">工作时间</div>
<div class="info-value">{{ primaryInfo?.workTime || '标准工时,偶有加班' }}</div>
</div>
<div class="info-item">
<div class="info-label">出差要求</div>
<div class="info-value">{{ primaryInfo?.travel || '偶尔出差' }}</div>
</div>
<div class="info-item">
<div class="info-label">体力要求</div>
<div class="info-value">{{ primaryInfo?.physical || '无特殊要求' }}</div>
</div>
<div class="info-item">
<div class="info-label">特殊设备/工具</div>
<div class="info-value">{{ primaryInfo?.tools || '电脑、办公软件' }}</div>
</div>
<div class="info-item">
<div class="info-label">健康与安全</div>
<div class="info-value">{{ primaryInfo?.health || '注意用眼卫生' }}</div>
</div>
</div>
</div>
</a-tab-pane>
<!-- 5. 绩效与薪酬 -->
<a-tab-pane key="performance" title="绩效与薪酬">
<div class="tab-content">
<!-- 绩效衡量标准 -->
<div class="section-title">绩效衡量标准</div>
<div class="content-container">
<div class="content-text">{{ primaryInfo?.performance || '根据项目按时交付率、代码质量、产品性能指标提升情况进行评估。' }}</div>
</div>
<!-- 薪酬范围 -->
<div class="section-title">薪酬范围</div>
<div class="content-container">
<div class="content-text salary-value">{{ primaryInfo?.salaryRange || '20-30万/年' }}</div>
</div>
</div>
</a-tab-pane>
<!-- 6. 企业文化与发展 -->
<a-tab-pane key="culture" title="企业文化与发展">
<div class="tab-content">
<!-- 公司文化与价值观要求 -->
<div class="section-title">公司文化与价值观要求</div>
<div class="content-container">
<div class="content-text">{{ primaryInfo?.culture || '客户第一、创新、诚信、合作' }}</div>
</div>
<!-- 职业发展路径 -->
<div class="section-title">职业发展路径</div>
<div class="content-container">
<div class="content-text">{{ primaryInfo?.careerPath || '可晋升为高级工程师、技术经理,或横向发展至产品/项目管理岗位。' }}</div>
</div>
</div>
</a-tab-pane>
<!-- 7. 时间信息 -->
<a-tab-pane key="time" title="时间信息">
<div class="tab-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">{{ primaryInfo?.createTime || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">更新时间</div>
<div class="info-value">{{ primaryInfo?.updateTime || '-' }}</div>
</div>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-spin>
<template #footer>
<div class="footer-actions">
<a-button type="primary" @click="handleClose">关闭详情</a-button>
</div>
</template>
<a-descriptions
:data="detailData"
:column="1"
:align="{ label: 'right' }"
label-style="width: 120px"
size="medium"
:loading="loading"
border
>
<template #label="{ label }">{{ label }}</template>
<template #value="{ value }">
<span v-if="value !== undefined && value !== null">{{ value }}</span>
<span v-else>-</span>
</template>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getPostDetail } from '@/apis/system/post'
import { useLoading } from '@/hooks'
@ -229,300 +32,35 @@ defineOptions({ name: 'PostDetailDrawer' })
const visible = ref(false)
const { loading, setLoading } = useLoading()
const primaryInfo = ref<any>(null)
const activeTab = ref('basic')
const detailData = ref<Array<{ label: string; value: any }>>([])
//
const getDetail = async (id: string) => {
try {
setLoading(true)
primaryInfo.value = null
const response = await getPostDetail(id)
const data = response?.data || response
if (data && typeof data === 'object') {
primaryInfo.value = {
id: data.postId ?? data.id ?? '10001',
name: data.postName ?? data.name ?? '-',
status: data.status,
department: data.department ?? '技术部',
superior: data.superior ?? '技术总监',
level: data.level ?? 'P5',
version: data.version ?? '2024-06-01 / V1.0',
location: data.location ?? '上海/远程',
summary: data.summary ?? '负责公司核心产品开发,支撑业务增长。',
tasks: data.tasks ?? [
'负责XXX产品的需求分析、架构设计、核心模块编码和单元测试。',
'制定并执行季度社交媒体营销计划,提升品牌曝光度和用户互动率。'
],
permissions: data.permissions ?? [
'有权审批部门内5000元以下的采购申请。',
'有权对项目团队成员的工作任务进行分配和调整。'
],
subordinates: data.subordinates ?? '无',
collaboration: data.collaboration ?? '与产品部、销售部、客服部紧密合作',
education: data.education ?? '本科及以上学历,计算机相关专业',
experience: data.experience ?? '5年以上相关工作经验',
skills: data.skills ?? '精通Java/Python良好的沟通与团队协作能力',
certificates: data.certificates ?? 'PMP、注册会计师等',
otherRequirements: data.otherRequirements ?? '英语流利,能适应短期出差',
workEnv: data.workEnv ?? '办公室/远程',
workTime: data.workTime ?? '标准工时,偶有加班',
travel: data.travel ?? '偶尔出差',
physical: data.physical ?? '无特殊要求',
tools: data.tools ?? '电脑、办公软件',
health: data.health ?? '注意用眼卫生',
performance: data.performance ?? '根据项目按时交付率、代码质量、产品性能指标提升情况进行评估。',
careerPath: data.careerPath ?? '可晋升为高级工程师、技术经理,或横向发展至产品/项目管理岗位。',
salaryRange: data.salaryRange ?? '20-30万/年',
culture: data.culture ?? '客户第一、创新、诚信、合作',
createTime: data.createTime,
updateTime: data.updateTime,
}
const { data } = await getPostDetail(id)
if (data) {
detailData.value = [
{ label: '岗位名称', value: data.postName },
{ label: '岗位排序', value: data.postSort },
{ label: '状态', value: Number(data.status) === 1 ? '正常' : '停用' },
{ label: '备注', value: data.remark },
{ label: '创建时间', value: data.createTime },
{ label: '更新时间', value: data.updateTime },
]
}
} catch (error: any) {
console.error('获取岗位详情失败:', error)
} finally {
setLoading(false)
}
}
//
const getStatusText = (status: number | string) => {
if (status === 1 || status === '1') return '正常'
if (status === 0 || status === '0') return '停用'
return '未知状态'
}
//
const getStatusColor = (status: number | string) => {
if (status === 1 || status === '1') return 'rgb(82, 196, 26)'
if (status === 0 || status === '0') return 'rgb(245, 34, 45)'
return 'rgb(150, 150, 150)'
}
const handleClose = () => {
visible.value = false
}
//
const onDetail = async (id: string) => {
if (!id) return
visible.value = true
activeTab.value = 'basic' //
await nextTick()
await getDetail(id)
}
defineExpose({
onDetail,
})
</script>
<style scoped>
.post-detail-drawer {
--primary-color: #3498db;
--light-bg: #f9fafc;
--border-color: #eaeaea;
--label-color: #666;
--value-color: #333;
--group-title-color: #555;
--group-bg: #f5f7fa;
}
.detail-container {
padding: 16px 24px;
}
.detail-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 20px;
transition: all 0.3s ease;
}
.detail-header {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border-color);
}
.post-name {
font-size: 20px;
font-weight: 600;
color: var(--value-color);
margin-right: 15px;
letter-spacing: 0.5px;
}
.status-tag {
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
border: none;
color: white !important;
}
.detail-tabs {
margin-top: 10px;
}
.tab-content {
padding: 20px 0;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
}
/* 自定义滚动条样式 */
.tab-content::-webkit-scrollbar {
width: 6px;
}
.tab-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.tab-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.tab-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 18px;
}
.info-item {
display: flex;
flex-direction: column;
background: white;
padding: 12px 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
}
.info-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.info-label {
font-size: 14px;
color: var(--label-color);
margin-bottom: 6px;
font-weight: 500;
display: flex;
align-items: center;
}
.info-label::after {
content: ':';
margin-right: 4px;
}
.info-value {
font-size: 16px;
color: var(--value-color);
font-weight: 500;
word-break: break-word;
}
.salary-value {
color: #e74c3c;
font-weight: 600;
}
.content-container {
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.content-text {
color: var(--value-color);
line-height: 1.7;
font-size: 15px;
white-space: pre-wrap;
word-break: break-word;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--group-title-color);
margin-top: 20px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.section-title:first-child {
margin-top: 0;
}
.footer-actions {
display: flex;
justify-content: flex-end;
padding: 16px 24px;
}
/* 标签页样式优化 */
:deep(.arco-tabs-nav) {
background-color: var(--light-bg);
border-radius: 8px;
padding: 4px;
margin-bottom: 16px;
}
:deep(.arco-tabs-nav-tab) {
background-color: transparent;
border: none;
}
:deep(.arco-tabs-tab) {
border-radius: 6px;
margin: 0 2px;
padding: 8px 16px;
font-weight: 400;
font-size: 14px;
transition: all 0.3s ease;
border: 2px solid transparent;
color: #666;
}
:deep(.arco-tabs-tab:hover) {
background-color: rgba(52, 152, 219, 0.1);
color: var(--primary-color);
font-weight: 500;
}
:deep(.arco-tabs-tab-active) {
background-color: var(--primary-color);
color: white !important;
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
font-weight: 700 !important;
font-size: 15px !important;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
:deep(.arco-tabs-content) {
padding: 0;
}
:deep(.arco-tabs-tabpane) {
padding: 0;
}
</style>
</script>

View File

@ -1,14 +1,14 @@
<template>
<GiPageLayout>
<GiTable
row-key="postId"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
:disabled-tools="['size']"
@refresh="search"
row-key="postId"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:pagination="pagination"
:disabled-tools="['size']"
@refresh="search"
>
<template #top>
<GiForm v-model="queryForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm>
@ -24,59 +24,14 @@
{{ Number(record.status) === 1 ? '正常' : '停用' }}
</a-tag>
</template>
<template #postName="{ record }">
<a-dropdown trigger="hover" @visible-change="(visible) => onDropdownVisibleChange(visible, record)">
<a-link type="primary" :title="`点击查询 ${record.postName} 岗位下的用户信息`" @click="onPostClick(record)">
{{ record.postName }}
<template #icon>
<icon-user style="margin-right: 4px; font-size: 12px; color: #666;" />
<icon-down />
</template>
</a-link>
<template #content>
<a-doption
:value="record.postId"
:disabled="userLoadingMap[record.postId]"
>
<div v-if="userLoadingMap[record.postId]" class="loading-container">
<a-spin size="mini" />
<span style="margin-left: 8px;">加载中...</span>
</div>
<div v-else-if="postUsersMap[record.postId]?.length">
<div class="user-list">
<div class="user-list-header">
<span class="user-count">该岗位下有 {{ postUsersMap[record.postId].length }} 个用户</span>
</div>
<div class="user-item" v-for="user in postUsersMap[record.postId]" :key="user.userId">
<a-avatar :size="24" :src="user.avatar">
{{ user.name?.charAt(0) || user.account?.charAt(0) || 'U' }}
</a-avatar>
<div class="user-info">
<div class="user-name clickable" @click="onUserClick(user)" :title="`点击查看 ${user.name || user.account} 的详细信息`">{{ user.name || user.account }}</div>
<div class="user-detail">
<span v-if="user.mobile">手机: {{ user.mobile }}</span>
<span v-if="user.email">邮箱: {{ user.email }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-users">
该岗位暂无用户
</div>
</a-doption>
</template>
</a-dropdown>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:post:query']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:post:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:post:delete']"
status="danger"
title="删除"
@click="onDelete(record)"
v-permission="['system:post:delete']"
status="danger"
title="删除"
@click="onDelete(record)"
>
删除
</a-link>
@ -86,18 +41,15 @@
<PostAddModal ref="PostAddModalRef" @save-success="search" />
<PostDetailDrawer ref="PostDetailDrawerRef" />
<UserDetailDrawer ref="UserDetailDrawerRef" />
</GiPageLayout>
</template>
<script setup lang="ts">
import type { TableColumnData } from '@arco-design/web-vue'
import type { TableColumnData, TableInstance } from '@arco-design/web-vue'
import PostAddModal from './PostAddModal.vue'
import PostDetailDrawer from './PostDetailDrawer.vue'
import UserDetailDrawer from '../user/UserDetailDrawer.vue'
import { deletePost, listPost, getPostUsers } from '@/apis/system/post'
import type { PostVO } from '@/apis/system/type'
import type { UserNewResp } from '@/apis/system/type'
import { deletePost, pagePost,listPost } from '@/apis/system/post'
import type { PostVO, PostPageQuery } from '@/apis/system/type'
import { useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
@ -128,7 +80,7 @@ const queryFormColumns: ColumnItem[] = reactive([
props: {
options: [
{ label: '正常', value: 1 },
{ label: '停用', value: 0 },
{ label: '停用', value: 0 }
],
placeholder: '请选择状态',
},
@ -141,14 +93,10 @@ const {
pagination,
search,
handleDelete,
} = useTable((params) => listPost({
...queryForm,
} = useTable((params) => listPost({
...queryForm,
}), { immediate: true })
//
const postUsersMap = ref<Record<string, UserNewResp[]>>({})
const userLoadingMap = ref<Record<string, boolean>>({})
const tableColumns = ref<TableColumnData[]>([
{
title: '序号',
@ -157,51 +105,50 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '岗位名称',
dataIndex: 'postName',
slotName: 'postName',
minWidth: 140,
ellipsis: true,
{
title: '岗位名称',
dataIndex: 'postName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '岗位排序',
dataIndex: 'postSort',
minWidth: 100,
ellipsis: true,
tooltip: true,
{
title: '岗位排序',
dataIndex: 'postSort',
minWidth: 100,
ellipsis: true,
tooltip: true
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100,
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
},
{
title: '说明',
dataIndex: 'remark',
minWidth: 180,
ellipsis: true,
tooltip: true,
{
title: '备注',
dataIndex: 'remark',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '创建时间',
dataIndex: 'createTime',
minWidth: 180,
ellipsis: true,
{
title: '创建时间',
dataIndex: 'createTime',
minWidth: 180,
ellipsis: true,
tooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
}
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 200,
width: 160,
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:post:update', 'system:post:delete']),
},
@ -221,66 +168,8 @@ const onDelete = (record: PostVO) => {
})
}
//
const onDropdownVisibleChange = async (visible: boolean, record: PostVO) => {
if (visible && !postUsersMap.value[record.postId] && !userLoadingMap.value[record.postId]) {
//
userLoadingMap.value[record.postId] = true
try {
const response = await getPostUsers(record.postId)
if (response.success && response.data) {
postUsersMap.value[record.postId] = response.data
} else {
postUsersMap.value[record.postId] = []
}
} catch (error) {
console.error('获取岗位用户信息失败:', error)
postUsersMap.value[record.postId] = []
} finally {
userLoadingMap.value[record.postId] = false
}
}
}
//
const onPostClick = async (record: PostVO) => {
//
if (postUsersMap.value[record.postId]) {
return
}
//
userLoadingMap.value[record.postId] = true
try {
const response = await getPostUsers(record.postId)
if (response.success && response.data) {
postUsersMap.value[record.postId] = response.data
} else {
postUsersMap.value[record.postId] = []
}
} catch (error) {
console.error('获取岗位用户信息失败:', error)
postUsersMap.value[record.postId] = []
} finally {
userLoadingMap.value[record.postId] = false
}
}
//
const onUserClick = (user: UserNewResp) => {
UserDetailDrawerRef.value?.onOpen(user.userId)
}
const PostAddModalRef = ref<InstanceType<typeof PostAddModal>>()
const PostDetailDrawerRef = ref<InstanceType<typeof PostDetailDrawer>>()
const UserDetailDrawerRef = ref<InstanceType<typeof UserDetailDrawer>>()
//
const onAdd = () => {
@ -298,80 +187,4 @@ const onDetail = (record: PostVO) => {
}
</script>
<style scoped lang="scss">
.loading-container {
display: flex;
align-items: center;
padding: 8px;
}
.user-list {
min-width: 300px;
max-width: 400px;
.user-list-header {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
.user-count {
font-size: 12px;
color: #666;
}
}
.user-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.user-info {
margin-left: 8px;
flex: 1;
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 2px;
&.clickable {
cursor: pointer;
color: #165dff;
transition: color 0.2s ease;
&:hover {
color: #0e42d2;
text-decoration: underline;
}
}
}
.user-detail {
font-size: 12px;
color: #666;
span {
margin-right: 8px;
&:last-child {
margin-right: 0;
}
}
}
}
}
}
.no-users {
padding: 16px;
text-align: center;
color: #999;
font-size: 14px;
}
</style>
<style scoped lang="scss"></style>

View File

@ -1,13 +1,13 @@
<template>
<a-modal
v-model:visible="visible"
title="分配角色"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1100 ? 1100 : '100%'"
draggable
@before-ok="save"
@close="reset"
v-model:visible="visible"
title="分配角色"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1100 ? 1100 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<UserSelect v-if="visible" ref="UserSelectRef" v-model:value="selectedUsers" :role-id="dataId" @select-user="onSelectUser" />
</a-modal>
@ -17,7 +17,6 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { assignToUsers } from '@/apis/system/role'
//
const emit = defineEmits<{
(e: 'save-success'): void

View File

@ -98,11 +98,21 @@ const columns: TableInstance['columns'] = [
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'name', slotName: 'name', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{
title: '昵称',
dataIndex: 'nickname',
slotName: 'nickname',
minWidth: 130,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
{ title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true },
{
title: '操作',
dataIndex: 'action',
@ -132,7 +142,7 @@ const onMulDelete = () => {
content: `是否确定取消分配角色给所选的${selectedKeys.value.length}个用户?`,
hideCancel: false,
onOk: async () => {
await unassignFromUsers(selectedKeys.value.map(id => String(id)))
await unassignFromUsers(selectedKeys.value)
Message.success('取消成功')
search()
},
@ -141,7 +151,7 @@ const onMulDelete = () => {
//
const onDelete = (record: RoleUserResp) => {
return handleDelete(() => unassignFromUsers([String(record.id)]), {
return handleDelete(() => unassignFromUsers([record.id]), {
content: `是否确定取消分配角色给用户「${record.nickname}(${record.username})」?`,
successTip: '取消成功',
showModal: true,

View File

@ -18,7 +18,6 @@
import RoleTree from './tree/index.vue'
import Permission from './components/Permission.vue'
import RoleUser from './components/RoleUser.vue'
import {listRoleUserId} from "@/apis";
defineOptions({ name: 'SystemRole' })

View File

@ -60,7 +60,7 @@ const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
await updateUserRole({ roleIds: form.roleIds.map(id => String(id)) }, dataId.value)
await updateUserRole({ roleIds: form.roleIds }, dataId.value)
Message.success('分配成功')
emit('save-success')
return true

View File

@ -93,7 +93,7 @@ const handleSubmit = async () => {
await bindUserRole({
userId: props.userData.userId,
roleIds: selectedRoles.value.map(id => String(id))
roleIds: selectedRoles.value
})
Message.success('角色分配成功')

View File

@ -313,17 +313,17 @@ const columns: TableInstance['columns'] = [
},
{ title: '账号', dataIndex: 'account', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '员工编码', dataIndex: 'userCode', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center', width: 80 },
{ title: '手机号', dataIndex: 'mobile', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '学历', dataIndex: 'educationLabel', width: 100, ellipsis: true, tooltip: true, show: false },
{ title: '专业', dataIndex: 'majorField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '在职状态', dataIndex: 'userStatus', slotName: 'userStatus', align: 'center', width: 100 },
{ title: '员工性质', dataIndex: 'userType', slotName: 'userType', align: 'center', width: 100 },
{ title: '角色', dataIndex: 'roleName', slotName: 'roleName', minWidth: 185 },
{ title: '邮箱', dataIndex: 'email', slotName: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center', width: 80 },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleIds', slotName: 'roleIds', minWidth: 165 },
{ title: '手机号', dataIndex: 'mobile', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '邮箱', dataIndex: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '入职日期', dataIndex: 'hiredate', width: 120, ellipsis: true, tooltip: true },
{ title: '出生日期', dataIndex: 'birthdate', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '学历', dataIndex: 'educationLabel', width: 100, ellipsis: true, tooltip: true, show: false },
{ title: '专业', dataIndex: 'majorField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '工作方向', dataIndex: 'workField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '身份证', dataIndex: 'identityCard', width: 180, ellipsis: true, tooltip: true, show: false },
{

View File

@ -17,25 +17,25 @@
</template>
</a-upload>
<div class="name">
<span style="margin-right: 10px">{{ userInfo.name }}</span>
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
<icon-edit :size="16" class="btn" @click="onUpdate" />
</div>
<div class="id">
<GiSvgIcon name="id" :size="16" />
<span>{{ userInfo.userId }}</span>
<span>{{ userInfo.id }}</span>
</div>
</section>
<footer>
<a-descriptions :column="4" size="large">
<a-descriptions-item :span="4">
<template #label> <icon-user /><span style="margin-left: 5px">用户名</span></template>
{{ userInfo.account }}
{{ userInfo.username }}
<icon-man v-if="userInfo.gender === 1" style="color: #19bbf1" />
<icon-woman v-else-if="userInfo.gender === 2" style="color: #fa7fa9" />
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-phone /><span style="margin-left: 5px">手机</span></template>
{{ userInfo.mobile || '暂无' }}
{{ userInfo.phone || '暂无' }}
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-email /><span style="margin-left: 5px">邮箱</span></template>
@ -47,12 +47,12 @@
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-user-group /><span style="margin-left: 5px">角色</span></template>
{{ userInfo.roles?.map(role => role.roleName).join(', ') || '暂无' }}
{{ userInfo.roles.join('') }}
</a-descriptions-item>
</a-descriptions>
</footer>
</div>
<div class="footer">注册于 {{ userInfo.createTime }}</div>
<div class="footer">注册于 {{ userInfo.registrationDate }}</div>
</a-card>
<a-modal v-model:visible="visible" title="上传头像" :width="width >= 400 ? 400 : '100%'" :footer="false" draggable @close="reset">
@ -102,22 +102,19 @@ import { uploadAvatar } from '@/apis/system'
import 'vue-cropper/dist/index.css'
import { useUserStore } from '@/stores'
import getAvatar from '@/utils/avatar'
import XG_DEBUG from 'xgplayer/es/utils/debug'
import config = XG_DEBUG.config
const { width } = useWindowSize()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const avatar = computed(() => ({
const avatar = {
uid: '-2',
name: 'avatar.png',
url: userInfo.value.avatar || getAvatar(userInfo.value.avatar, undefined),
}))
const avatarList = computed<FileItem[]>(() => [avatar.value])
const fileRef = ref<File | null>(null)
const options = reactive<cropperOptions>({
url: userInfo.value.avatar,
}
const avatarList = ref<FileItem[]>([avatar])
const fileRef = ref(reactive({ name: 'avatar.png' }))
const options: cropperOptions = reactive({
img: '',
autoCrop: true,
autoCropWidth: 160,
@ -131,14 +128,13 @@ const options = reactive<cropperOptions>({
outputType: 'png',
})
const visible = ref(false)
//
const onBeforeUpload = (file: File): boolean => {
fileRef.value = file
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
options.img = reader.result as string
options.img = reader.result
}
visible.value = true
return false