This commit is contained in:
weizhong committed 2025-08-26 18:24:45 +08:00
commit 742adbf553
4 files changed
+487 -960

No files matched your search

@@ -1,811 +0,0 @@
# 物业报修管理 API
## 📋 文档说明
本文档记录物业报修管理相关 API 接口,包括维修分类管理、报修工单管理、工单进度跟踪等功能。
**依赖文档**: 请先阅读 [API 文档基础规范.md](./API文档基础规范.md) 了解通用规范。
---
## 🔧 维修分类管理
### 查询维修分类列表
**接口地址**: `GET /hq/pm/repairCategory/list`
**接口描述**: 分页查询维修分类列表,支持权限过滤
**权限要求**: `pm:category:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------------ | ------ | ---- | --------------------- |
| pageNum | int | 否 | 页码,默认 1 |
| pageSize | int | 否 | 页大小,默认 10 |
| categoryName | string | 否 | 分类名称 |
| categoryCode | string | 否 | 分类编码 |
| parentId | long | 否 | 父级分类 ID |
| skillLevel | string | 否 | 技能等级要求 |
| status | string | 否 | 状态(0-停用,1-正常) |
**响应示例**:
```json
{
"total": 6,
"rows": [
{
"id": 1,
"categoryName": "电路维修",
"categoryCode": "ELECTRIC",
"categoryDescription": "电路、照明、开关、插座等电气设备维修",
"parentId": 0,
"ancestorIds": "0",
"levelDepth": 1,
"childrenCount": 0,
"requiredSkills": "电工技能、安全操作",
"skillLevel": "2",
"estimatedDuration": 120,
"standardCost": 200.0,
"iconUrl": null,
"colorCode": "#FF6B35",
"sortOrder": 1,
"status": "1",
"usageCount": 0,
"lastUsedTime": null,
"remark": null,
"createTime": "2025-01-23 14:30:00"
}
],
"code": 200,
"msg": "查询成功"
}
```
### 获取维修分类树形结构
**接口地址**: `GET /hq/pm/repairCategory/tree`
**接口描述**: 获取维修分类的树形结构数据
**权限要求**: `pm:category:tree`
**请求参数**: 无
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1,
"categoryName": "电路维修",
"categoryCode": "ELECTRIC",
"categoryDescription": "电路、照明、开关、插座等电气设备维修",
"parentId": 0,
"children": []
}
]
}
```
### 获取维修分类下拉选项
**接口地址**: `GET /hq/pm/repairCategory/options`
**接口描述**: 获取维修分类的下拉选项数据,用于表单选择器
**权限要求**: `pm:category:list`
**请求参数**: 无
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "电路维修"
},
{
"value": 2,
"label": "水管维修"
},
{
"value": 3,
"label": "门窗维修"
}
]
}
```
**数据格式说明**:
- **value**: 分类 ID,用于表单提交
- **label**: 分类名称,用于显示
- **扁平化处理**: 将树形结构扁平化,便于下拉选择
**使用场景**:
- 报修工单创建时选择维修类型
- 表单编辑时的分类选择
- 搜索条件中的分类筛选
### 获取维修分类详情
**接口地址**: `GET /hq/pm/repairCategory/{id}`
**接口描述**: 根据 ID 获取维修分类详细信息
**权限要求**: `pm:category:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ---- | ---- | ------- |
| id | long | 是 | 分类 ID |
### 新增维修分类
**接口地址**: `POST /hq/pm/repairCategory`
**接口描述**: 创建新的维修分类
**权限要求**: `pm:category:add`
**请求参数**:
```json
{
"categoryName": "测试维修类型",
"categoryCode": "TEST_REPAIR",
"categoryDescription": "这是一个测试的维修类型",
"parentId": 0,
"skillLevel": "1",
"estimatedDuration": 60,
"standardCost": 100.0,
"colorCode": "#1890FF",
"sortOrder": 99,
"status": "1",
"remark": "测试数据"
}
```
---
## 🛠️ 报修工单管理
### 查询报修工单列表
**接口地址**: `GET /hq/pm/repairOrder/list`
**接口描述**: 分页查询报修工单列表,支持多种条件筛选,根据用户角色进行数据权限过滤
**权限要求**: `pm:repairOrder:list`
**数据权限说明**:
- **超级管理员**: 可查看所有报修工单
- **物业维修管理员** (role_key: property_maintenance_admin): 可查看所有报修工单
- **维修人员** (role_key: maintenance_worker): 只能查看自己创建的工单和分配给自己的工单
- **普通用户**: 只能查看自己创建的工单
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | --------------- |
| pageNum | int | 否 | 页码,默认 1 |
| pageSize | int | 否 | 页大小,默认 10 |
| orderNo | string | 否 | 工单编号 |
| title | string | 否 | 报修标题 |
| repairLocation | string | 否 | 报修地点 |
| categoryId | long | 否 | 维修类型 ID |
| priorityLevel | int | 否 | 紧急程度 |
| reporterName | string | 否 | 报修人姓名 |
| reporterPhone | string | 否 | 报修人电话 |
| orderStatus | string | 否 | 工单状态 |
| assigneeId | long | 否 | 分派维修员 ID |
| assigneeName | string | 否 | 分派维修员姓名 |
| createTimeRange | array | 否 | 创建时间范围 |
| finishTimeRange | array | 否 | 完成时间范围 |
**响应示例**:
```json
{
"total": 1,
"rows": [
{
"id": "1939510596285673473",
"tenantId": "000000",
"orderNo": "RO202506300002",
"title": "详细测试工单 - 会议室投影仪故障",
"description": "会议室投影仪无法正常启动,显示器黑屏,影响会议正常进行。设备型号:EPSON EB-U05,购买时间:2023年6月。",
"repairLocation": "行政楼三楼305会议室",
"categoryId": 1,
"categoryName": "电路维修",
"priorityLevel": 2,
"expectedFinishTime": "2025-07-02 02:25:34",
"reporterId": 1,
"reporterName": "测试用户",
"reporterPhone": "13900139001",
"reporterDept": "信息技术部",
"orderStatus": "1",
"assignTime": null,
"assigneeId": null,
"assigneeName": null,
"assignRemark": null,
"startTime": null,
"finishTime": null,
"totalDuration": null,
"repairResult": null,
"usedMaterials": null,
"followSuggestions": null,
"evaluationScore": null,
"evaluationTime": null,
"attachmentCount": 0,
"progressCount": 1,
"cancelReason": null,
"remark": "设备在保修期内,请联系厂家技术支持",
"createTime": "2025-06-30 02:25:34",
"updateTime": "2025-06-30 02:25:34"
}
],
"code": 200,
"msg": "查询成功"
}
```
### 生成工单编号
**接口地址**: `GET /hq/pm/repairOrder/generateOrderNo`
**接口描述**: 自动生成报修工单编号
**权限要求**: 无需特殊权限
**请求参数**: 无
**响应示例**:
```json
{
"code": 200,
"msg": "RO202506300002",
"data": null
}
```
**说明**: 工单编号格式为`RO + 年月日 + 序号`,如`RO202506300002`
### 获取报修统计数据
**接口地址**: `GET /hq/pm/repairOrder/statistics`
**接口描述**: 获取报修工单的统计数据,用于仪表板展示
**权限要求**: `pm:repairOrder:statistics`
**访问权限说明**:
- **超级管理员**: 可以访问统计数据
- **物业维修管理员** (role_key: property_maintenance_admin): 可以访问统计数据
- **维修人员** (role_key: maintenance_worker): **无权限访问**
- **普通用户**: **无权限访问**
**请求参数**: 无
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"totalOrders": 156,
"pendingAssign": 23,
"inProgress": 45,
"completed": 88,
"cancelled": 0,
"avgProcessTime": 2.5,
"avgEvaluationScore": 4.3,
"monthlyNewOrders": 67,
"monthlyCompletedOrders": 52,
"statusDistribution": [
{
"status": "待分派",
"count": 23,
"percentage": 14.7
},
{
"status": "进行中",
"count": 45,
"percentage": 28.8
},
{
"status": "已完成",
"count": 88,
"percentage": 56.4
}
],
"categoryDistribution": [
{
"categoryName": "电路维修",
"count": 45,
"percentage": 28.8
},
{
"categoryName": "水管维修",
"count": 38,
"percentage": 24.4
}
],
"priorityDistribution": [
{
"priorityLevel": 1,
"count": 12,
"percentage": 7.7
},
{
"priorityLevel": 2,
"count": 34,
"percentage": 21.8
}
]
}
}
```
**统计字段说明**:
| 字段名 | 类型 | 说明 |
| ---------------------- | ----- | ---------------------- |
| totalOrders | int | 总工单数量 |
| pendingAssign | int | 待分派工单数 |
| inProgress | int | 进行中工单数 |
| completed | int | 已完成工单数 |
| cancelled | int | 已取消工单数 |
| avgProcessTime | float | 平均处理时长(小时) |
| avgEvaluationScore | float | 平均评价得分(1-5 分) |
| monthlyNewOrders | int | 本月新增工单数 |
| monthlyCompletedOrders | int | 本月完成工单数 |
| statusDistribution | array | 各状态分布统计 |
| categoryDistribution | array | 各类型分布统计 |
| priorityDistribution | array | 紧急程度分布统计 |
### 分派工单
**接口地址**: `PUT /hq/pm/repairOrder/{orderId}/assign`
**接口描述**: 分派工单给维修员
**权限要求**: `pm:repairOrder:assign`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------- | ---- | ---- | ------- | ------ |
| orderId | Long | 是 | 工单 ID | 1 |
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------------ | ------ | ---- | --------- | ---------- |
| assigneeId | Long | 是 | 维修员 ID | 2 |
| assignRemark | String | 否 | 分派备注 | "紧急处理" |
**响应示例**:
```json
{
"code": 200,
"msg": "分派成功",
"data": null
}
```
**错误码**:
| 错误码 | 说明 | 解决方案 |
| ------ | ------------------ | ------------- |
| 400001 | 工单不存在 | 检查工单 ID |
| 400002 | 维修员不存在 | 检查维修员 ID |
| 400003 | 工单状态不允许分派 | 检查工单状态 |
### 获取工单进度列表
**接口地址**: `GET /hq/pm/repairOrder/{orderId}/progress`
**接口描述**: 获取指定工单的进度记录列表
**权限要求**: `pm:repairOrder:progress`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------- | ---- | ---- | ------- | ------ |
| orderId | Long | 是 | 工单 ID | 1 |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"orderId": 1,
"progressType": "5",
"progressStatus": "正在维修",
"progressDetail": "已到达现场,开始检查设备",
"operatorId": 2,
"operatorName": "张维修",
"operatorRole": "worker",
"locationInfo": "教学楼A座301",
"estimatedDuration": 60,
"operationTime": "2025-06-30 14:30:00",
"nextStep": "更换零件",
"issuesEncountered": "发现零件老化",
"materialsNeeded": "电路板x1",
"remark": "需要采购新零件"
}
]
}
```
### 更新维修进度
**接口地址**: `POST /hq/pm/repairOrder/progress`
**接口描述**: 添加工单维修进度记录
**权限要求**: `pm:repairOrder:updateProgress`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ----------------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------- | -------------------------- |
| orderId | Long | 是 | 工单 ID | 1 |
| progressType | String | 是 | 进度类型:1-工单分派,2-接收确认,3-到达现场,4-开始维修,5-进度更新,6-完成维修,7-用户评价,8-工单归档,9-其他 | "5" |
| progressStatus | String | 是 | 进度状态描述 | "正在维修" |
| progressDetail | String | 是 | 进度详细描述 | "已到达现场,开始检查设备" |
| locationInfo | String | 否 | 操作位置信息 | "教学楼 A 座 301" |
| estimatedDuration | Integer | 否 | 预计耗时(分钟) | 60 |
| nextStep | String | 否 | 下一步计划 | "更换零件" |
| issuesEncountered | String | 否 | 遇到的问题 | "发现零件老化" |
| materialsNeeded | String | 否 | 需要的材料 | "电路板 x1" |
| remark | String | 否 | 备注信息 | "需要采购新零件" |
**响应示例**:
```json
{
"code": 200,
"msg": "进度更新成功",
"data": null
}
```
### 完成工单
**接口地址**: `PUT /hq/pm/repairOrder/{orderId}/finish`
**接口描述**: 完成工单维修,填写维修结果
**权限要求**: `pm:repairOrder:complete`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------- | ---- | ---- | ------- | ------ |
| orderId | Long | 是 | 工单 ID | 1 |
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ----------------- | ------ | ---- | ------------ | -------------------------------- |
| repairResult | String | 是 | 维修结果描述 | "已更换故障电路板,设备恢复正常" |
| usedMaterials | String | 否 | 使用材料清单 | "电路板 x1, 螺丝 x4" |
| followSuggestions | String | 否 | 后续建议 | "建议定期检查电路板状态" |
**响应示例**:
```json
{
"code": 200,
"msg": "工单完成成功",
"data": null
}
```
---
## ⭐ 服务评价管理
### 创建服务评价
**接口地址**: `POST /hq/pm/repairOrder/evaluation`
**接口描述**: 创建工单服务评价
**权限要求**: `pm:repairOrder:evaluate`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ---------------------- | ------- | ---- | -------------------------------------------------------------- | ---------------------------- |
| orderId | Long | 是 | 工单 ID | 1 |
| evaluatedWorkerId | Long | 是 | 被评价维修员 ID | 2 |
| overallScore | Number | 是 | 综合评价得分(1.0-5.0) | 4.5 |
| qualityScore | Number | 是 | 维修质量得分(1.0-5.0) | 4.8 |
| attitudeScore | Number | 是 | 服务态度得分(1.0-5.0) | 4.5 |
| responseSpeedScore | Number | 是 | 响应速度得分(1.0-5.0) | 4.2 |
| communicationScore | Number | 是 | 沟通效果得分(1.0-5.0) | 4.6 |
| evaluationContent | String | 是 | 文字评价内容 | "维修师傅技术过硬,态度很好" |
| satisfactionLevel | String | 是 | 满意度等级:1-非常满意,2-满意,3-一般,4-不满意,5-非常不满意 | "1" |
| isAnonymous | Integer | 否 | 是否匿名评价:0-否,1-是 | 0 |
| isPublic | Integer | 否 | 是否公开显示:0-否,1-是 | 1 |
| positiveTags | String | 否 | 正面标签 | "技术专业,态度友好" |
| negativeTags | String | 否 | 负面标签 | "" |
| improvementSuggestions | String | 否 | 改进建议 | "希望能提前通知到达时间" |
| remark | String | 否 | 备注信息 | "" |
**响应示例**:
```json
{
"code": 200,
"msg": "评价提交成功",
"data": null
}
```
### 获取工单评价
**接口地址**: `GET /hq/pm/repairOrder/{orderId}/evaluation`
**接口描述**: 获取指定工单的服务评价信息
**权限要求**: `pm:repairOrder:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------- | ---- | ---- | ------- | ------ |
| orderId | Long | 是 | 工单 ID | 1 |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1,
"orderId": 1,
"evaluatorId": 3,
"evaluatorName": "张老师",
"evaluatedWorkerId": 2,
"evaluatedWorkerName": "张维修",
"overallScore": 4.5,
"qualityScore": 4.8,
"attitudeScore": 4.5,
"responseSpeedScore": 4.2,
"communicationScore": 4.6,
"evaluationContent": "维修师傅技术过硬,态度很好",
"satisfactionLevel": "1",
"isAnonymous": 0,
"isPublic": 1,
"positiveTags": "技术专业,态度友好",
"improvementSuggestions": "希望能提前通知到达时间",
"evaluationTime": "2025-06-30 16:30:00"
}
}
```
---
## 📊 数据字典
### 工单状态
| 值 | 说明 | 备注 |
| --- | ------ | ------------ |
| 1 | 待分派 | 已创建待分派 |
| 2 | 进行中 | 已分派进行中 |
| 3 | 已完成 | 维修完成 |
| 4 | 已取消 | 工单取消 |
### 紧急程度
| 值 | 说明 | 备注 |
| --- | ---- | ------------ |
| 1 | 低 | 一般维修 |
| 2 | 中 | 较重要维修 |
| 3 | 高 | 紧急维修 |
| 4 | 紧急 | 非常紧急维修 |
### 进度类型
| 值 | 说明 | 备注 |
| --- | -------- | -------------- |
| 1 | 工单分派 | 分派给维修员 |
| 2 | 接收确认 | 维修员确认接收 |
| 3 | 到达现场 | 到达维修现场 |
| 4 | 开始维修 | 开始进行维修 |
| 5 | 进度更新 | 维修进度更新 |
| 6 | 完成维修 | 维修工作完成 |
| 7 | 用户评价 | 用户评价完成 |
| 8 | 工单归档 | 工单归档完成 |
| 9 | 其他 | 其他操作 |
### 满意度等级
| 值 | 说明 | 备注 |
| --- | ---------- | ---- |
| 1 | 非常满意 | 5 分 |
| 2 | 满意 | 4 分 |
| 3 | 一般 | 3 分 |
| 4 | 不满意 | 2 分 |
| 5 | 非常不满意 | 1 分 |
---
## 📞 联系方式
如有物业报修相关问题,请联系开发团队或查看项目仓库的 Issues。
---
**📝 维护**: 物业报修管理 API 文档
**🔄 更新**: 2025-07-01
**�� 文件**: 物业报修管理 API.md
## 📊 统计分析接口
**详细信息参考**: `docs/业务模块/物业报修管理/统计分析功能.md`
---
## 🔧 培训申请待审批接口 (新增 2025-01-03)
### 查询待审批培训申请列表
**接口地址**: `GET /rs/training/application/pending/list`
**权限要求**: `rs:trainingapproval:process`
**接口说明**: 专门为审批人员提供的待审批培训申请列表接口,解决了rs-Approver角色权限问题
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | number | 否 | 页码,默认1 |
| pageSize | number | 否 | 每页数量,默认10 |
| courseName | string | 否 | 课程名称(模糊查询) |
| location | string | 否 | 培训地点(模糊查询) |
| status | string | 否 | 申请状态 |
| excludeStatus | string | 否 | 排除状态(默认排除已通过状态"1") |
#### 响应参数
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"applicationId": 1234567890123456789,
"courseId": 1,
"courseName": "Spring Boot开发培训",
"userId": 123,
"userName": "张三",
"department": "技术部",
"position": "开发工程师",
"reason": "提升技术能力",
"status": "0",
"applyTime": "2025-01-03 10:00:00",
"location": "培训中心A座",
"duration": 3,
"startTime": "2025-01-10 09:00:00",
"endTime": "2025-01-12 17:00:00"
}
],
"total": 15
}
```
#### 状态码说明
| 状态 | 含义 | 类型 |
|------|------|------|
| 0 | 待审核 | warning |
| 1 | 已通过 | success |
| 2 | 已拒绝 | danger |
| 3 | 已取消 | info |
| 4 | 已完成 | success |
#### 权限控制
- **接口权限**: `rs:trainingapproval:process`
- **角色支持**: rs-Approver, rs-admin
- **数据过滤**:
- rs-Approver角色:可查看所有待审批申请
- rs-admin角色:可查看所有申请
- 自动排除已通过状态的申请
#### 使用示例
**前端调用**:
```typescript
import { listPendingTrainingApplication } from '@/api/rs/training'
// 查询待审批列表
const getPendingList = async () => {
const params = {
pageNum: 1,
pageSize: 10,
excludeStatus: '1' // 排除已通过状态
}
const response = await listPendingTrainingApplication(params)
return response
}
```
**Curl示例**:
```bash
curl -X GET "http://localhost:8080/rs/training/application/pending/list?pageNum=1&pageSize=10" \
-H "Authorization: Bearer your-token" \
-H "clientid: e5cd7e4891bf95d1d19206ce24a7b32e" \
-H "referer: http://localhost"
```
#### 与原接口对比
| 项目 | 原接口 | 新接口 |
|------|--------|--------|
| 接口地址 | `/rs/training/application/list` | `/rs/training/application/pending/list` |
| 权限要求 | `rs:trainingapplication:list` | `rs:trainingapproval:process` |
| 角色支持 | rs-admin | rs-Approver, rs-admin |
| 设计目的 | 申请管理 | 审批处理 |
| 权限独立性 | ❌ 权限混合 | ✅ 权限分离 |
#### 技术实现
**后端Controller**:
```java
@SaCheckPermission("rs:trainingapproval:process")
@GetMapping("/pending/list")
public TableDataInfo<RsTrainingApplication> pendingList(
RsTrainingApplication application,
PageQuery pageQuery) {
// 设置租户ID和排除状态
application.setTenantId(LoginHelper.getTenantId());
application.setExcludeStatus("1"); // 排除已通过状态
return trainingApplicationService.selectPageRsTrainingApplicationList(
application, pageQuery);
}
```
**权限过滤逻辑**:
- Service层根据rs-Approver角色自动放开数据权限
- Mapper层支持excludeStatus参数过滤
- 前端调用专门的审批接口,权限控制更精确
---
**接口文档最后更新**: 2025-01-03
**维护责任人**: 系统架构师
**相关文档**:
- `docs/开发文档/培训申请权限一致性修复报告.md`
- `.cursor/cursorrules-backend.md` - 后端开发规范
+453 -140
View File
@@ -10,6 +10,12 @@
<!-- 加载中状态 -->
<u-loading-page :loading="loading" loading-text="加载中..."></u-loading-page>
<!-- 后台数据加载提示 -->
<view v-if="!loading && !backgroundDataLoaded" class="background-loading-tip">
<u-icon name="clock" size="16" color="#909399"></u-icon>
<text class="tip-text">部分辅助数据正在后台加载中...</text>
</view>
<!-- 内容区域 -->
<view v-if="!loading" class="content-wrapper">
<!-- 学生基本信息卡片 -->
@@ -95,7 +101,7 @@
</view>
<view class="info-item">
<text class="label">籍贯</text>
<text class="value">{{ getRegionFullPath(form.nativePlace) || '-' }}</text>
<text class="value">{{ nativePlaceDisplayName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">政治面貌</text>
@@ -172,7 +178,7 @@
<view class="info-item long-content">
<text class="label">生源地区</text>
<text class="value long-text">
{{ getRegionFullPath(form.sourceRegion) || '-' }}
{{ sourceRegionDisplayName || '-' }}
</text>
</view>
<view class="info-item long-content">
@@ -782,38 +788,32 @@ export default {
// 显示名称(异步加载)
roomDisplayName: '',
bedDisplayName: '',
nativePlaceDisplayName: '', // 籍贯显示名称
sourceRegionDisplayName: '', // 生源地区显示名称
// 加载状态
regionLoadingPromise: null,
backgroundDataLoaded: false, // 后台数据是否加载完成
};
},
async created() {
console.log('=== 学生信息页面初始化开始 ===');
try {
await this.loadDictData();
console.log('✓ 字典数据加载完成');
} catch (error) {
console.error('字典数据加载出错(不影响后续流程):', error);
}
try {
await this.loadRegionAndDormData();
console.log('✓ 区域和宿舍数据加载完成');
} catch (error) {
console.error('区域和宿舍数据加载出错:', error);
}
try {
await this.loadDormRoomAndBedData();
console.log('✓ 房间和床位数据加载完成');
} catch (error) {
console.error('房间和床位数据加载出错:', error);
}
// 第一阶段:立即加载核心个人信息,让页面内容快速显示
try {
this.loading = true;
await this.getPersonalInfoData();
console.log('✓ 个人信息加载完成');
console.log('✓ 核心个人信息加载完成,页面可以显示');
} catch (error) {
console.error('个人信息加载出错:', error);
console.error('个人信息加载失败:', error);
// 即使个人信息加载失败,也要设置loading为false,避免页面一直loading
this.loading = false;
}
// 第二阶段:并行加载辅助数据(不阻塞页面显示)
this.loadSupportingDataInBackground();
console.log('=== 学生信息页面初始化完成 ===');
},
@@ -1054,6 +1054,256 @@ export default {
return getDictTagType(this.studentStatusOptions, this.form.studentStatus, fallbackMap);
},
// 后台并行加载辅助数据
async loadSupportingDataInBackground() {
console.log('开始后台加载辅助数据...');
// 第一阶段:并行加载所有非核心数据,互不阻塞
const loadPromises = [
this.loadDictDataSafely(),
this.loadDormDataSafely(),
this.loadDormRoomAndBedDataSafely(),
];
// 使用Promise.allSettled确保即使某些请求失败也不影响其他
const results = await Promise.allSettled(loadPromises);
results.forEach((result, index) => {
const taskNames = ['字典数据', '宿舍数据', '房间床位数据'];
if (result.status === 'fulfilled') {
console.log(`✓ ${taskNames[index]}后台加载完成`);
} else {
console.warn(`✗ ${taskNames[index]}后台加载失败:`, result.reason);
}
});
// 第二阶段:更新宿舍相关显示名称(不依赖区域数据)
try {
await this.updateDisplayNamesAfterBackgroundLoad();
console.log('✓ 后台数据加载后显示名称更新完成');
} catch (error) {
console.warn('后台数据加载后显示名称更新失败:', error);
}
// 第三阶段:尝试加载区域数据并更新区域显示名称(可选,不阻塞整体完成)
this.loadRegionDataAndUpdateDisplay();
console.log('=== 后台辅助数据加载完成 ===');
this.backgroundDataLoaded = true;
},
// 安全加载字典数据(出错不影响页面)
async loadDictDataSafely() {
try {
await this.loadDictData();
} catch (error) {
console.warn('字典数据加载失败,使用默认值:', error);
// 确保字典数组不为undefined
this.ensureDictArraysExist();
}
},
// 安全加载宿舍数据(出错不影响页面)
async loadDormDataSafely() {
try {
await this.loadDormBuildingData();
} catch (error) {
console.warn('宿舍数据加载失败,使用默认值:', error);
this.dormBuildingOptions = [];
}
},
// 安全加载房间床位数据(出错不影响页面)
async loadDormRoomAndBedDataSafely() {
try {
await this.loadDormRoomAndBedData();
} catch (error) {
console.warn('房间床位数据加载失败,使用默认值:', error);
this.dormRoomOptions = [];
this.dormBedOptions = [];
}
},
// 设置临时显示名称,避免页面空白
setTemporaryDisplayNames() {
// 设置临时的区域显示名称
if (this.form.nativePlace) {
this.nativePlaceDisplayName = `区域${this.form.nativePlace}`;
}
if (this.form.sourceRegion) {
this.sourceRegionDisplayName = `区域${this.form.sourceRegion}`;
}
// 房间和床位显示名称暂时留空,将在后台数据加载完成后更新
console.log('临时显示名称已设置');
},
// 异步加载区域数据并更新显示名称(不阻塞主流程)
async loadRegionDataAndUpdateDisplay() {
try {
console.log('开始异步加载区域数据...');
await this.loadRegionDataLazily();
// 区域数据加载完成后,更新区域显示名称
if (this.form.nativePlace) {
const nativePlaceName = this.buildRegionPath(this.form.nativePlace);
if (nativePlaceName !== `区域${this.form.nativePlace}`) {
this.nativePlaceDisplayName = nativePlaceName;
console.log('✓ 异步更新籍贯名称:', nativePlaceName);
}
}
if (this.form.sourceRegion) {
const sourceRegionName = this.buildRegionPath(this.form.sourceRegion);
if (sourceRegionName !== `区域${this.form.sourceRegion}`) {
this.sourceRegionDisplayName = sourceRegionName;
console.log('✓ 异步更新生源地区名称:', sourceRegionName);
}
}
console.log('✓ 区域数据加载和显示名称更新完成');
} catch (error) {
console.warn('区域数据加载失败,保持临时显示名称:', error);
}
},
// 后台数据加载完成后更新显示名称(仅处理宿舍相关)
async updateDisplayNamesAfterBackgroundLoad() {
const updatePromises = [];
// 更新房间和床位名称(需要宿舍数据)
if (this.form.dormitoryRoom) {
updatePromises.push(
this.getRoomName(this.form.dormitoryRoom)
.then((name) => {
this.roomDisplayName = name;
console.log('✓ 后台更新房间名称:', name);
})
.catch((error) => {
console.warn('后台更新房间名称失败:', error);
}),
);
}
if (this.form.extField1) {
updatePromises.push(
this.getBedName(this.form.extField1)
.then((name) => {
this.bedDisplayName = name;
console.log('✓ 后台更新床位名称:', name);
})
.catch((error) => {
console.warn('后台更新床位名称失败:', error);
}),
);
}
// 等待所有宿舍相关更新完成
await Promise.allSettled(updatePromises);
console.log('宿舍相关显示名称更新完成');
},
// 确保字典数组存在,避免页面报错
ensureDictArraysExist() {
const dictArrays = [
'genderOptions',
'nationalityOptions',
'politicalStatusOptions',
'educationLevelOptions',
'householdRegisterTypeOptions',
'maritalStatusOptions',
'yesNoOptions',
'familyEconomicOptions',
'bloodTypeOptions',
'disabilityTypeOptions',
'academicLevelOptions',
'trainingModeOptions',
'admissionCategoryOptions',
'studentStatusOptions',
];
dictArrays.forEach((arrayName) => {
if (!this[arrayName] || !Array.isArray(this[arrayName])) {
this[arrayName] = [];
}
});
},
// 单独加载宿舍楼栋数据
async loadDormBuildingData() {
console.log('开始加载宿舍楼栋数据...');
try {
const dormRes = await request({
url: '/xgss/dormBuildingTree/listAll',
method: 'GET',
});
if (dormRes && dormRes.data) {
this.dormBuildingOptions = this.processDormTreeData(dormRes.data);
console.log('宿舍楼栋数据处理完成,条目数:', this.dormBuildingOptions.length);
} else {
console.warn('宿舍楼栋数据加载失败或为空');
this.dormBuildingOptions = [];
}
} catch (error) {
console.error('加载宿舍楼栋数据失败:', error);
this.dormBuildingOptions = [];
throw error;
}
},
// 懒加载行政区划数据(仅在需要时加载)
async loadRegionDataLazily() {
// 如果已经加载过,直接返回
if (this.regionOptions && this.regionOptions.length > 0) {
return this.regionOptions;
}
// 如果正在加载中,等待加载完成
if (this.regionLoadingPromise) {
return await this.regionLoadingPromise;
}
// 开始加载
this.regionLoadingPromise = this.doLoadRegionData();
try {
await this.regionLoadingPromise;
return this.regionOptions;
} finally {
this.regionLoadingPromise = null;
}
},
// 实际执行行政区划数据加载
async doLoadRegionData() {
console.log('懒加载行政区划数据...');
try {
const regionRes = await request({
url: '/system/SysRegion/tree',
method: 'GET',
});
if (regionRes && regionRes.data) {
this.regionOptions = regionRes.data;
console.log('行政区划数据懒加载完成,条目数:', regionRes.data.length);
} else {
console.warn('行政区划数据懒加载失败或为空');
this.regionOptions = [];
}
} catch (error) {
console.error('懒加载行政区划数据失败:', error);
this.regionOptions = [];
// 显示用户友好的错误提示
uni.showToast({
title: '区域数据加载失败',
icon: 'none',
duration: 2000,
});
throw error;
}
},
// 加载字典数据
async loadDictData() {
console.log('开始加载字典数据...');
@@ -1163,105 +1413,6 @@ export default {
}
},
// 加载行政区划和宿舍楼栋数据
async loadRegionAndDormData() {
console.log('开始加载区域和宿舍数据...');
try {
console.log('发起并行请求: 行政区划 & 宿舍楼栋');
const [regionRes, dormRes] = await Promise.all([
request({
url: '/system/SysRegion/tree',
method: 'GET',
})
.then((res) => {
console.log('✓ 行政区划请求完成');
return res;
})
.catch((err) => {
console.error('✗ 行政区划请求失败:', err);
throw err;
}),
request({
url: '/xgss/dormBuildingTree/listAll',
method: 'GET',
})
.then((res) => {
console.log('✓ 宿舍楼栋请求完成');
return res;
})
.catch((err) => {
console.error('✗ 宿舍楼栋请求失败:', err);
throw err;
}),
]);
// 处理行政区划数据
if (regionRes && regionRes.data) {
this.regionOptions = regionRes.data;
console.log('行政区划数据处理完成,条目数:', regionRes.data.length);
} else {
console.warn('行政区划数据加载失败或为空');
}
// 处理宿舍楼栋数据,添加性别标识
if (dormRes && dormRes.data) {
console.log('dormRes', dormRes);
this.dormBuildingOptions = this.processDormTreeData(dormRes.data);
console.log('宿舍楼栋数据处理完成,条目数:', this.dormBuildingOptions.length);
} else {
console.warn('宿舍楼栋数据加载失败或为空');
this.dormBuildingOptions = [];
}
} catch (error) {
console.error('加载区域和宿舍数据失败:', error);
uni.showToast({
title: '加载基础数据失败,部分功能可能受影响',
icon: 'none',
duration: 3000,
});
}
},
// 处理宿舍楼栋树形数据,为楼栋添加性别标识
processDormTreeData(nodes) {
console.log('processDormTreeData - nodes', nodes);
if (!nodes || !Array.isArray(nodes)) {
console.warn('宿舍楼栋数据格式不正确');
return [];
}
return nodes.map((node) => {
const processedNode = { ...node };
// 如果是楼栋类型,添加性别标识
if (node.type === 'building' && node.buildingType) {
console.log('node.buildingType', node.buildingType);
const genderSuffix =
node.buildingType === '1'
? '(男)'
: node.buildingType === '2'
? '(女)'
: '';
// 确保 label 和 name 存在后再添加后缀
if (node.label) {
processedNode.label = node.label + genderSuffix;
}
if (node.name) {
processedNode.name = node.name + genderSuffix;
}
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
processedNode.children = this.processDormTreeData(node.children);
}
console.log('processedNode', processedNode);
return processedNode;
});
},
// 加载房间和床位数据(改为按需加载)
async loadDormRoomAndBedData() {
console.log('房间和床位数据改为按需加载模式,不再预加载所有数据');
@@ -1270,29 +1421,70 @@ export default {
this.dormBedOptions = [];
},
// 异步加载房间和床位显示名称
async loadRoomAndBedDisplayNames() {
// 异步加载各种显示名称(房间、床位、区域)
async loadDisplayNames() {
try {
// 加载房间名称
// 并行加载房间和床位名称
const loadPromises = [];
if (this.form.dormitoryRoom) {
this.roomDisplayName = await this.getRoomName(this.form.dormitoryRoom);
console.log('房间名称加载完成:', this.roomDisplayName);
loadPromises.push(
this.getRoomName(this.form.dormitoryRoom).then((name) => {
this.roomDisplayName = name;
console.log('房间名称加载完成:', name);
}),
);
}
// 加载床位名称
if (this.form.extField1) {
this.bedDisplayName = await this.getBedName(this.form.extField1);
console.log('床位名称加载完成:', this.bedDisplayName);
loadPromises.push(
this.getBedName(this.form.extField1).then((name) => {
this.bedDisplayName = name;
console.log('床位名称加载完成:', name);
}),
);
}
// 并行加载区域显示名称(不阻塞房间床位显示)
if (this.form.nativePlace) {
loadPromises.push(
this.getRegionFullPathAsync(this.form.nativePlace)
.then((name) => {
this.nativePlaceDisplayName = name;
console.log('籍贯显示名称加载完成:', name);
})
.catch((error) => {
console.warn('籍贯显示名称加载失败:', error);
this.nativePlaceDisplayName = `区域${this.form.nativePlace}`;
}),
);
}
if (this.form.sourceRegion) {
loadPromises.push(
this.getRegionFullPathAsync(this.form.sourceRegion)
.then((name) => {
this.sourceRegionDisplayName = name;
console.log('生源地区显示名称加载完成:', name);
})
.catch((error) => {
console.warn('生源地区显示名称加载失败:', error);
this.sourceRegionDisplayName = `区域${this.form.sourceRegion}`;
}),
);
}
// 等待所有显示名称加载完成(使用 allSettled 确保部分失败不影响其他)
await Promise.allSettled(loadPromises);
console.log('所有显示名称加载完成');
} catch (error) {
console.error('加载房间和床位显示名称失败:', error);
console.error('加载显示名称失败:', error);
}
},
// 获取学生个人信息
async getPersonalInfoData() {
try {
this.loading = true;
const response = await request({
url: '/studentMsg/studentMsg/personalInfo',
method: 'GET',
@@ -1331,8 +1523,10 @@ export default {
extField1Type: typeof this.form.extField1,
});
// 异步加载房间和床位信息
await this.loadRoomAndBedDisplayNames();
// 立即设置临时显示名称,避免空白
this.setTemporaryDisplayNames();
console.log('✓ 个人信息加载完成,页面可以显示');
} else {
uni.showToast({
title: response.msg || '获取个人信息失败',
@@ -1437,9 +1631,63 @@ export default {
}
},
// 根据行政区划代码获取完整路径(省/市/区)
getRegionFullPath(code) {
if (!code || !this.regionOptions) return '';
// 处理宿舍楼栋树形数据,为楼栋添加性别标识
processDormTreeData(nodes) {
console.log('processDormTreeData - nodes', nodes);
if (!nodes || !Array.isArray(nodes)) {
console.warn('宿舍楼栋数据格式不正确');
return [];
}
return nodes.map((node) => {
const processedNode = { ...node };
// 如果是楼栋类型,添加性别标识
if (node.type === 'building' && node.buildingType) {
console.log('node.buildingType', node.buildingType);
const genderSuffix =
node.buildingType === '1'
? '(男)'
: node.buildingType === '2'
? '(女)'
: '';
// 确保 label 和 name 存在后再添加后缀
if (node.label) {
processedNode.label = node.label + genderSuffix;
}
if (node.name) {
processedNode.name = node.name + genderSuffix;
}
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
processedNode.children = this.processDormTreeData(node.children);
}
console.log('processedNode', processedNode);
return processedNode;
});
},
// 异步获取区域完整路径(用于后台加载)
async getRegionFullPathAsync(code) {
if (!code) return '';
// 懒加载区域数据
await this.loadRegionDataLazily();
// 获取完整路径
return this.buildRegionPath(code);
},
// 构建区域路径(纯计算,不触发加载)
buildRegionPath(code) {
if (!code || !this.regionOptions || this.regionOptions.length === 0) {
return `区域${code}`;
}
const path = [];
const findPath = (nodes, targetCode) => {
for (const node of nodes) {
@@ -1456,8 +1704,58 @@ export default {
}
return false;
};
findPath(this.regionOptions, code);
return path.join(' / ');
if (findPath(this.regionOptions, code)) {
return path.join(' / ');
}
return `区域${code}`;
},
// 根据行政区划代码获取完整路径(省/市/区)- 同步版本,用于模板(已弃用)
getRegionFullPath(code) {
if (!code) return '';
// 如果行政区划数据未加载,触发懒加载并返回临时显示
if (!this.regionOptions || this.regionOptions.length === 0) {
// 异步加载区域数据,不阻塞当前显示
this.loadRegionDataLazily()
.then(() => {
// 加载完成后触发页面更新
this.$forceUpdate();
})
.catch(() => {
// 加载失败时显示代码
console.warn('行政区划数据懒加载失败,显示原始代码');
});
// 返回代码作为临时显示
return `区域${code}`;
}
const path = [];
const findPath = (nodes, targetCode) => {
for (const node of nodes) {
path.push(node.name);
if (node.code === targetCode) {
return true;
}
if (node.children && node.children.length > 0) {
if (findPath(node.children, targetCode)) {
return true;
}
}
path.pop();
}
return false;
};
if (findPath(this.regionOptions, code)) {
return path.join(' / ');
}
// 如果找不到,返回代码
return `区域${code}`;
},
// 根据楼栋ID获取楼栋名称(含性别标识)
@@ -1726,6 +2024,21 @@ export default {
min-height: 100vh;
}
.background-loading-tip {
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 30rpx;
background-color: #f0f9ff;
border-bottom: 1px solid #e1ecf4;
gap: 8rpx;
}
.tip-text {
font-size: 24rpx;
color: #606266;
}
.content-wrapper {
padding: 20rpx;
padding-bottom: 40rpx;
+9 -1
View File
@@ -58,7 +58,7 @@
<el-table-column label="报告厅图片" align="center" prop="hallImage" width="200">
<template #default="scope">
<div v-if="scope.row.hallImage" class="table-image">
<image-preview :src="scope.row.hallImage" :width="50" :height="50" />
<image-preview :src="getFirstImage(scope.row.hallImage)" :width="50" :height="50" />
</div>
<div v-else class="table-no-image">
<span class="table-no-image-text">暂无图片</span>
@@ -382,6 +382,14 @@ const cancelView = () => {
viewDialog.visible = false;
};
/** 获取第一张图片 */
const getFirstImage = (hallImage: string) => {
if (!hallImage) return '';
// 如果有多个图片用逗号分隔,只取第一张
const images = hallImage.split(',').filter((img) => img.trim() !== '');
return images.length > 0 ? images[0].trim() : '';
};
onMounted(() => {
getList();
});
+25 -8
View File
@@ -136,6 +136,7 @@ interface MenuAction {
status?: string;
moduleOrder?: number; // 新增:用于模块排序
menuOrder?: number; // 新增:用于菜单项排序
orderNum?: number; // 新增:用于存储菜单的原始排序号
logoUrl?: string; // 新增:用于存储随机logo图片路径
}
@@ -379,10 +380,11 @@ const flattenMenus = (menus: any[], parentChain: string[] = [], parentPath = '',
// 1. 组件为Layout或ParentView字符串
// 2. 组件为Vue对象且没有子菜单(可能是目录)
// 3. menuType为'M'(目录类型)
const isDirectoryMenu = menu.component === 'Layout' ||
menu.component === 'ParentView' ||
(typeof menu.component === 'object' && !hasVisibleChildren) ||
menu.menuType === 'M';
const isDirectoryMenu =
menu.component === 'Layout' ||
menu.component === 'ParentView' ||
(typeof menu.component === 'object' && !hasVisibleChildren) ||
menu.menuType === 'M';
// 添加工作流菜单的详细调试信息
if (menu.meta?.title === '工作流' || menu.name === 'Workflow11616') {
@@ -424,12 +426,13 @@ const flattenMenus = (menus: any[], parentChain: string[] = [], parentPath = '',
status: menu.status || '0',
moduleOrder: moduleOrder, // 添加模块顺序
menuOrder: menuIndex, // 添加菜单在当前层级的顺序
orderNum: menu.orderNum || 0, // 添加菜单的原始排序号
logoUrl: getIconPath(iconKey) // 根据icon名称获取对应图标
};
// 添加调试日志来验证路径构建
// 添加调试日志来验证路径构建和排序
console.log(
`菜单项路径构建: ${menu.meta.title} - 原始path: ${menu.path}, 构建后fullPath: ${fullPath}, parentPath: ${parentPath}, level: ${level}`
`菜单项路径构建: ${menu.meta.title} - 原始path: ${menu.path}, 构建后fullPath: ${fullPath}, orderNum: ${menu.orderNum}, parentPath: ${parentPath}, level: ${level}`
);
result.push(menuAction);
@@ -498,13 +501,27 @@ const groupMenusByModule = (menus: MenuAction[]): MenuModule[] => {
return;
}
// 按orderNum排序前的日志
console.log(
`${moduleName}模块排序前:`,
moduleMenus.map((m) => `${m.title}(${m.orderNum})`)
);
const sortedMenus = moduleMenus.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
// 按orderNum排序后的日志
console.log(
`${moduleName}模块排序后:`,
sortedMenus.map((m) => `${m.title}(${m.orderNum})`)
);
const module: MenuModule = {
id: `module-${moduleName}`,
title: moduleName,
description: `${moduleName}模块包含${moduleMenus.length}个功能`,
color: moduleColors[colorIndex % moduleColors.length],
// 菜单项按原始顺序排序,而不是按名称排序
menus: moduleMenus.sort((a, b) => (a.menuOrder || 0) - (b.menuOrder || 0))
// 菜单项按原始orderNum排序,确保显示顺序正确
menus: sortedMenus
};
modules.push(module);
colorIndex++;