提交已修改的代码

This commit is contained in:
zlwu committed 2025-06-30 04:25:12 +08:00
1 parent 65d2d71469
commit 60cb1b09f8
32 files changed
+10359 -4786

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
{
// Cursor AI 专用设置
"cursor.ai.preview.enabled": false,
"cursor.ai.showPreviewInEditor": false,
"cursor.ai.autoPreview": false,
"cursor.ai.previewDelay": 2000,
// 代码生成设置
"cursor.ai.codeGeneration.showPreview": false,
"cursor.ai.suggestions.preview": false,
"cursor.ai.chat.floatingWindow": false,
"cursor.ai.chat.sidePanel": true,
// 禁用多余的AI提示
"cursor.ai.hover.enabled": false,
"cursor.ai.inline.enabled": true,
"cursor.ai.inline.preview": false,
// 优化编辑器性能
"cursor.performance.enablePreviewCache": false,
"cursor.performance.maxPreviewWindows": 1,
// 文件处理设置
"cursor.files.autoPreview": false,
"cursor.files.previewOnHover": false,
"cursor.files.openPreviewByDefault": false,
"cursor.files.enablePreview": false,
// UI 优化设置
"cursor.ui.showWelcomeScreen": false,
"cursor.ui.showTips": false,
"cursor.ui.compactMode": true,
// 禁用Vue文件预览弹窗
"cursor.vue.preview.enabled": false,
"cursor.vue.autoPreview": false,
"cursor.vue.showPreviewDialog": false,
// 禁用所有预览对话框
"cursor.preview.showDialogs": false,
"cursor.preview.autoShow": false,
"cursor.preview.popupEnabled": false,
// 编辑器设置
"editor.preview.enabled": false,
"workbench.editor.enablePreview": false,
"workbench.editor.enablePreviewFromQuickOpen": false,
"workbench.editor.showTabs": "multiple",
// 禁用文件关联预览
"files.associations": {
"*.vue": "vue"
},
// 代码编辑时的预览控制
"cursor.ai.edit.showPreview": false,
"cursor.ai.edit.autoPreview": false,
"cursor.ai.generation.showPreview": false
}
@@ -22,7 +22,7 @@ server:
captcha:
# 是否启用验证码校验
enable: true
enable: false
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
+26 -5
View File
@@ -62,12 +62,33 @@
### 🚀 计划中模块
#### 5. 后勤维修服务系统 (maintenance-service)
#### 5. 物业报修管理系统 (property-maintenance)
- **状态**:⭕ 待开发
- **计划开始**:2024 年 7 月
- **技术栈**:Spring Boot + Vue3 + Warm-Flow
- **核心功能**:报修申请、工单管理、维修派单、服务评价
- **状态**:🔄 前端开发完成
- **负责人**:待分配
- **技术栈**:Spring Boot 3.4.6 + Vue3 + TypeScript + Warm-Flow + Element Plus
- **核心功能**:报修申请管理、工单分派系统、维修进度跟踪、服务质量评价、数据统计分析
- **API 数量**:15 个接口(规划中)
- **文档完整度**:🟡 95%(前端代码完成)
- **数据库表**:6 个核心表(已部署到研发环境)
- **部署环境**:47.99.164.66:3306/hzdyjs_xg
- **开发进度**:
- ✅ 需求分析完成
- ✅ 数据库设计完成
- ✅ 数据库部署完成
- ✅ **前端代码完成**(API 层 + 页面组件)
- 🔄 后端接口开发中
- ⏳ 功能联调测试
- **前端代码**:
- API 接口定义:17 个 TypeScript 类型,22 个接口方法
- 页面组件:报修工单管理、维修类型管理
- 技术特性:模拟数据、完整表单验证、状态流转、树形结构
- **特殊说明**:
- 系统管理功能:使用系统自带的用户、角色、权限管理,无需重新开发
- 通知功能:本模块暂不包含通知功能,状态更新通过系统界面显示
- 数据库环境:已在研发环境部署完成,支持后端开发和前端联调
- 前端环境:使用模拟数据,支持独立开发调试
- 下一步:开始后端 API 接口开发,对接前端代码
#### 6. 餐饮服务管理系统 (catering-management)
@@ -0,0 +1,759 @@
# 物业报修管理模块 - 总览
> 🎯 **模块用途**:学校物业维修服务数字化管理,提供从报修申请到维修完成的全流程管理
## 📋 模块基本信息
**模块名称**:property-maintenance / 物业报修管理模块
**负责人**:[待分配]
**开发状态**:⭕ 待开始
**技术栈**:Spring Boot 3.4.6 + Vue3 + TypeScript + Warm-Flow + Element Plus
**依赖模块**:用户管理模块、系统管理模块、文件服务模块
**开发周期**:[待规划] - [待规划]
## 🎯 模块概述
### 业务价值
本模块旨在解决学校物业维修服务的数字化管理问题,实现报修申请、任务分派、维修跟踪、服务评价的全流程线上化处理,提高维修效率,提升用户满意度,建立可追踪的服务质量体系。
### 核心功能
- **报修申请管理**:用户在线提交报修申请,支持图片、视频上传
- **工单分派系统**:物业管理员智能分派工单给合适的维修人员
- **维修进度跟踪**:实时跟踪维修进度,状态可视化展示
- **服务质量评价**:用户对维修服务进行评分和反馈
- **数据统计分析**:维修效率、用户满意度等多维度数据分析
### 技术特点
- **工作流驱动**:基于 Warm-Flow 引擎实现标准化业务流程
- **多媒体支持**:支持图片、视频等多媒体文件上传和预览
- **权限精细化**:基于角色的权限控制,确保数据安全
- **移动端适配**:支持 PC 端和移动端操作,方便现场使用
## 📁 文档结构
```
property-maintenance/
├── README.md # 本文件 - 模块总览
├── 需求分析/
│ ├── 业务需求说明.md # 详细业务需求
│ ├── 功能需求清单.md # 功能点清单
│ ├── 非功能需求.md # 性能、安全要求
│ └── 用户故事.md # 用户使用场景
├── 技术设计/
│ ├── 数据库设计.md # 表结构设计
│ ├── API接口设计.md # 接口定义
│ ├── 架构设计.md # 模块架构
│ └── 技术选型.md # 技术决策说明
├── 实现文档/
│ ├── 后端实现说明.md # 后端代码结构
│ ├── 前端实现说明.md # 前端代码结构
│ ├── 关键业务逻辑.md # 核心算法逻辑
│ └── 代码规范.md # 编码规范
├── 测试文档/
│ ├── 测试计划.md # 测试策略
│ ├── 测试用例.md # 具体测试用例
│ └── 测试报告.md # 测试结果
└── 运维文档/
├── 部署指南.md # 部署步骤
├── 监控配置.md # 监控指标
└── 故障处理.md # 常见问题
```
## 🏗️ 系统架构
### 模块架构图
```mermaid
graph TB
subgraph "前端展示层"
A1[报修申请页面<br/>用户提交报修]
A2[工单管理页面<br/>管理员派单]
A3[维修工作台<br/>维修员操作]
A4[统计分析页面<br/>数据可视化]
end
subgraph "接口服务层"
B1[报修单Controller<br/>CRUD操作]
B2[工作流Controller<br/>流程控制]
B3[文件服务Controller<br/>文件上传]
B4[统计分析Controller<br/>数据统计]
end
subgraph "业务逻辑层"
C1[报修单Service<br/>业务逻辑处理]
C2[工作流Service<br/>流程管理]
C3[通知Service<br/>消息通知]
C4[统计Service<br/>数据分析]
end
subgraph "数据持久层"
D1[报修单Mapper<br/>数据访问]
D2[工作流Mapper<br/>流程数据]
D3[文件Mapper<br/>文件信息]
D4[MySQL数据库<br/>数据存储]
end
subgraph "外部服务"
E1[文件存储服务<br/>OSS/本地存储]
E2[消息通知服务<br/>短信/邮件]
E3[用户权限服务<br/>Sa-Token]
end
A1 --> B1
A2 --> B1
A2 --> B2
A3 --> B1
A3 --> B2
A4 --> B4
B1 --> C1
B2 --> C2
B3 --> C1
B4 --> C4
C1 --> D1
C2 --> D2
C3 --> D1
C4 --> D1
D1 --> D4
D2 --> D4
D3 --> D4
C1 --> E1
C3 --> E2
B1 --> E3
```
### 数据流向
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant C as Controller
participant S as Service
participant W as 工作流引擎
participant M as Mapper
participant D as Database
U->>F: 提交报修申请
F->>C: POST /api/maintenance/repair-order
C->>S: 创建报修单
S->>W: 启动维修流程
W->>M: 保存流程数据
M->>D: 存储到数据库
D-->>M: 返回结果
M-->>S: 返回数据
S-->>C: 返回报修单ID
C-->>F: JSON响应
F-->>U: 显示提交成功
Note over S,W: 自动通知物业管理员
```
## 📊 核心功能模块
### 1. 报修申请管理
#### 业务描述
用户通过 Web 端或移动端在线提交报修申请,填写报修地点、问题描述、紧急程度等信息,并可上传现场图片或视频作为辅助说明。系统自动生成唯一的报修单号,并启动维修工作流程。
#### 技术实现
- **前端实现**:Vue3 + Element Plus + 文件上传组件
- **后端实现**:Spring Boot + MyBatis-Plus + OSS 文件服务
- **数据存储**:MySQL 存储报修单信息,OSS 存储多媒体文件
#### 关键流程
1. 用户登录认证 → 2. 填写报修信息 → 3. 上传附件文件 → 4. 提交申请 → 5. 系统生成工单 → 6. 进入分派流程
### 🎯 工单分派系统
#### 业务描述
物业管理员接收用户报修申请后,根据维修类型、紧急程度、维修员专业技能和工作负荷,智能推荐并分派工单给合适的维修员。支持批量分派和分派撤回功能。
#### 技术实现
- **前端实现**:工单列表、分派对话框、维修员负荷展示
- **后端实现**:工单分配算法、负荷均衡计算
- **数据存储**:工单分派记录、维修员工作负荷统计
#### 关键流程
1. 查看待分派工单 → 2. 选择维修员 → 3. 确认分派 → 4. 更新工单状态 → 5. 维修员接收工单
### 🔧 维修进度跟踪
#### 业务描述
维修员接收工单后,可实时更新维修进度状态(接单、到达现场、开始维修、完成维修等),用户可通过系统实时查看维修进度。支持维修过程照片上传和问题上报。
#### 技术实现
- **前端实现**:进度时间轴展示、状态更新界面
- **后端实现**:状态机管理、进度同步机制
- **数据存储**:维修进度记录、状态变更日志
#### 关键流程
1. 维修员接收工单 → 2. 更新到达状态 → 3. 开始维修 → 4. 上传进度照片 → 5. 完成维修 → 6. 提交报告
### ⭐ 服务质量评价
#### 业务描述
维修完成后,用户对维修服务进行评分(1-5 星)和文字反馈,包括维修质量、服务态度、响应速度等维度。评价结果作为维修员绩效考核和服务改进的重要依据。
#### 技术实现
- **前端实现**:星级评分组件、多维度评价表单
- **后端实现**:评价数据统计、满意度计算
- **数据存储**:评价记录、统计分析数据
#### 关键流程
1. 维修完成确认 → 2. 用户收到评价邀请 → 3. 多维度评分 → 4. 提交评价 → 5. 工单归档
## 🔌 API 接口概览
### 接口分类
#### 报修单管理接口
| 接口名称 | 请求方式 | 路径 | 功能描述 |
| -------------- | -------- | ----------------------------------------- | ---------------------- |
| 创建报修单 | POST | /api/maintenance/repair-order | 用户提交报修申请 |
| 查询报修单列表 | GET | /api/maintenance/repair-order/list | 分页查询报修单列表 |
| 查询报修单详情 | GET | /api/maintenance/repair-order/{id} | 根据 ID 查询报修单详情 |
| 更新报修单状态 | PUT | /api/maintenance/repair-order/{id}/status | 更新报修单处理状态 |
| 分派工单 | POST | /api/maintenance/repair-order/{id}/assign | 管理员分派工单给维修员 |
| 完成维修 | POST | /api/maintenance/repair-order/{id}/finish | 维修员提交维修完成结果 |
#### 文件管理接口
| 接口名称 | 请求方式 | 路径 | 功能描述 |
| ------------ | -------- | ---------------------------- | ------------------ |
| 上传文件 | POST | /api/maintenance/file/upload | 上传图片、视频文件 |
| 删除文件 | DELETE | /api/maintenance/file/{id} | 删除已上传文件 |
| 获取文件信息 | GET | /api/maintenance/file/{id} | 获取文件详细信息 |
#### 评价管理接口
| 接口名称 | 请求方式 | 路径 | 功能描述 |
| ------------ | -------- | -------------------------------------- | ------------------ |
| 提交评价 | POST | /api/maintenance/evaluation | 用户提交服务评价 |
| 查询评价列表 | GET | /api/maintenance/evaluation/list | 查询评价记录列表 |
| 统计满意度 | GET | /api/maintenance/evaluation/statistics | 统计服务满意度数据 |
#### 统计分析接口
| 接口名称 | 请求方式 | 路径 | 功能描述 |
| ---------------- | -------- | ---------------------------------------- | -------------------- |
| 维修统计概览 | GET | /api/maintenance/statistics/overview | 获取维修业务概览数据 |
| 维修员工作量统计 | GET | /api/maintenance/statistics/workload | 统计维修员工作负荷 |
| 维修类型分析 | GET | /api/maintenance/statistics/category | 按维修类型统计分析 |
| 满意度趋势分析 | GET | /api/maintenance/statistics/satisfaction | 用户满意度趋势分析 |
### 接口规范
- **请求格式**:JSON
- **响应格式**:统一响应体结构
- **认证方式**:JWT Token (Sa-Token)
- **权限控制**:基于角色的权限验证
- **文件上传**:支持多文件上传,限制文件大小和类型
## 🗄️ 数据模型
### 核心实体
#### 报修单表 (pm_repair_order)
| 字段名 | 类型 | 长度 | 非空 | 默认值 | 说明 |
| ------------------ | ------------ | ---- | ---- | ------- | ------------- |
| id | BIGINT | - | Y | - | 主键 ID |
| order_no | VARCHAR | 32 | Y | - | 报修单号 |
| title | VARCHAR | 200 | Y | - | 报修标题 |
| description | TEXT | - | Y | - | 问题描述 |
| location | VARCHAR | 500 | Y | - | 报修地点 |
| contact_name | VARCHAR | 50 | Y | - | 联系人姓名 |
| contact_phone | VARCHAR | 20 | Y | - | 联系电话 |
| urgency_level | TINYINT | - | Y | 1 | 紧急程度(1-5) |
| expected_time | DATETIME | - | N | - | 期望完成时间 |
| repair_type | VARCHAR | 50 | Y | - | 维修类型 |
| status | VARCHAR | 20 | Y | PENDING | 状态 |
| assignee_id | BIGINT | - | N | - | 分派维修员 ID |
| assign_time | DATETIME | - | N | - | 分派时间 |
| start_time | DATETIME | - | N | - | 开始维修时间 |
| finish_time | DATETIME | - | N | - | 完成维修时间 |
| repair_result | TEXT | - | N | - | 维修结果说明 |
| evaluation_score | DECIMAL(3,2) | - | N | - | 评价分数 |
| evaluation_comment | TEXT | - | N | - | 评价内容 |
| tenant_id | VARCHAR | 20 | Y | 000000 | 租户 ID |
| create_dept | BIGINT | - | Y | - | 创建部门 |
| create_by | BIGINT | - | Y | - | 创建者 |
| create_time | DATETIME | - | Y | - | 创建时间 |
| update_by | BIGINT | - | Y | - | 更新者 |
| update_time | DATETIME | - | Y | - | 更新时间 |
| deleted | TINYINT | - | Y | 0 | 删除标记 |
#### 报修附件表 (pm_repair_attachment)
| 字段名 | 类型 | 长度 | 非空 | 默认值 | 说明 |
| --------------- | -------- | ---- | ---- | ------ | --------- |
| id | BIGINT | - | Y | - | 主键 ID |
| repair_order_id | BIGINT | - | Y | - | 报修单 ID |
| file_name | VARCHAR | 255 | Y | - | 文件名 |
| file_path | VARCHAR | 500 | Y | - | 文件路径 |
| file_size | BIGINT | - | Y | - | 文件大小 |
| file_type | VARCHAR | 50 | Y | - | 文件类型 |
| attachment_type | VARCHAR | 20 | Y | - | 附件类型 |
| tenant_id | VARCHAR | 20 | Y | 000000 | 租户 ID |
| create_by | BIGINT | - | Y | - | 创建者 |
| create_time | DATETIME | - | Y | - | 创建时间 |
| update_by | BIGINT | - | Y | - | 更新者 |
| update_time | DATETIME | - | Y | - | 更新时间 |
| deleted | TINYINT | - | Y | 0 | 删除标记 |
#### 维修进度记录表 (pm_repair_progress)
| 字段名 | 类型 | 长度 | 非空 | 默认值 | 说明 |
| --------------- | -------- | ---- | ---- | ------ | ------------ |
| id | BIGINT | - | Y | - | 主键 ID |
| repair_order_id | BIGINT | - | Y | - | 报修单 ID |
| status | VARCHAR | 20 | Y | - | 进度状态 |
| description | TEXT | - | N | - | 进度描述 |
| operator_id | BIGINT | - | Y | - | 操作人 ID |
| operator_name | VARCHAR | 50 | Y | - | 操作人姓名 |
| progress_time | DATETIME | - | Y | - | 进度更新时间 |
| tenant_id | VARCHAR | 20 | Y | 000000 | 租户 ID |
| create_by | BIGINT | - | Y | - | 创建者 |
| create_time | DATETIME | - | Y | - | 创建时间 |
| deleted | TINYINT | - | Y | 0 | 删除标记 |
### 实体关系图
```mermaid
erDiagram
PM_REPAIR_ORDER ||--o{ PM_REPAIR_ATTACHMENT : "一对多"
PM_REPAIR_ORDER ||--o{ PM_REPAIR_PROGRESS : "一对多"
PM_REPAIR_ORDER }o--|| SYS_USER : "报修人"
PM_REPAIR_ORDER }o--|| SYS_USER : "维修员"
PM_REPAIR_ORDER {
bigint id PK
varchar order_no UK
varchar title
text description
varchar location
varchar status
bigint assignee_id FK
datetime create_time
}
PM_REPAIR_ATTACHMENT {
bigint id PK
bigint repair_order_id FK
varchar file_name
varchar file_path
varchar file_type
}
PM_REPAIR_PROGRESS {
bigint id PK
bigint repair_order_id FK
varchar status
text description
bigint operator_id FK
datetime progress_time
}
SYS_USER {
bigint id PK
varchar username
varchar nickname
varchar phone
}
```
## 🔄 业务流程
### 主要业务流程
#### 报修申请流程
```mermaid
flowchart TD
A[用户发起报修] --> B[填写报修信息]
B --> C{信息完整?}
C -->|否| B
C -->|是| D[上传附件]
D --> E[提交申请]
E --> F[生成报修单号]
F --> G[通知物业管理员]
G --> H[报修单进入待分派状态]
```
**流程说明**:
1. 用户在系统中填写报修基本信息,包括报修地点、问题描述等
2. 系统验证信息完整性,要求用户补充必要信息
3. 用户可选择上传现场图片或视频作为补充说明
4. 提交后系统自动生成唯一报修单号
5. 自动通知物业管理员有新的报修申请需要处理
#### 工单分派流程
```mermaid
flowchart TD
A[管理员查看待分派工单] --> B[选择报修单]
B --> C[查看报修详情]
C --> D{是否需要分派?}
D -->|否| E[标记为无需维修]
D -->|是| F[选择维修员]
F --> G[确认分派]
G --> H[更新工单状态]
H --> I[通知维修员]
I --> J[通知报修用户]
```
**流程说明**:
1. 物业管理员登录系统查看所有待分派的报修工单
2. 点击查看具体报修单的详细信息和附件
3. 根据维修类型和维修员专长选择合适的维修人员
4. 确认分派后系统更新工单状态为"已分派"
5. 自动通知维修员和报修用户工单分派情况
#### 维修处理流程
```mermaid
flowchart TD
A[维修员接收工单] --> B[确认接单]
B --> C[到达现场]
C --> D[开始维修]
D --> E[更新维修进度]
E --> F{维修是否完成?}
F -->|否| D
F -->|是| G[提交维修报告]
G --> H[上传完成照片]
H --> I[更新工单状态]
I --> J[通知用户确认]
```
**流程说明**:
1. 维修员通过移动端或 PC 端接收分派的工单
2. 确认接单并更新状态为"进行中"
3. 到达维修现场,开始实际维修工作
4. 在维修过程中可随时更新进度状态
5. 维修完成后提交维修报告和现场照片
6. 系统自动通知报修用户确认维修结果
## 🎨 前端设计
### 页面结构
#### 报修申请页面 (用户端)
- **功能**:用户提交新的报修申请
- **组件**:表单组件、文件上传组件、地点选择器、日期选择器
- **交互**:填写表单 → 上传附件 → 提交申请 → 查看申请结果
- **数据**:报修类型字典、地点选择数据、用户基本信息
#### 工单管理页面 (管理员端)
- **功能**:查看、分派和管理所有报修工单
- **组件**:数据表格、筛选器、分派对话框、详情抽屉
- **交互**:筛选工单 → 查看详情 → 选择维修员 → 确认分派
- **数据**:工单列表数据、维修员列表、工单统计数据
#### 维修工作台 (维修员端)
- **功能**:维修员查看和处理分配给自己的工单
- **组件**:工单卡片、进度更新、文件上传、状态切换
- **交互**:接收工单 → 更新进度 → 上传照片 → 完成维修
- **数据**:个人工单列表、进度记录、维修结果数据
#### 统计分析页面 (管理员端)
- **功能**:查看维修业务的各类统计分析数据
- **组件**:图表组件、数据卡片、日期筛选器、导出功能
- **交互**:选择时间范围 → 查看统计图表 → 导出报表
- **数据**:统计图表数据、趋势分析数据、满意度数据
### 组件设计
#### 报修申请表单组件 (RepairOrderForm)
```typescript
// 组件属性接口
interface RepairOrderFormProps {
modelValue: RepairOrderForm;
readonly?: boolean;
showAttachment?: boolean;
}
// 组件事件接口
interface RepairOrderFormEmits {
'update:modelValue': [value: RepairOrderForm];
submit: [formData: RepairOrderForm];
cancel: [];
}
```
#### 工单分派对话框 (AssignDialog)
```typescript
// 组件属性接口
interface AssignDialogProps {
visible: boolean;
repairOrder: RepairOrderVo;
maintainerList: MaintainerVo[];
}
// 组件事件接口
interface AssignDialogEmits {
'update:visible': [visible: boolean];
confirm: [assignData: AssignFormData];
cancel: [];
}
```
## 📝 开发规范
### 代码结构
#### 后端代码结构
```
src/main/java/org/dromara/modules/maintenance/
├── controller/ # 控制器层
│ ├── RepairOrderController.java
│ ├── RepairAttachmentController.java
│ ├── RepairProgressController.java
│ └── MaintenanceStatisticsController.java
├── domain/ # 领域对象层
│ ├── bo/ # 业务对象
│ │ ├── RepairOrderBo.java
│ │ ├── RepairOrderQueryBo.java
│ │ └── AssignRepairOrderBo.java
│ └── vo/ # 视图对象
│ ├── RepairOrderVo.java
│ ├── RepairOrderDetailVo.java
│ └── MaintenanceStatisticsVo.java
├── mapper/ # 数据访问层
│ ├── RepairOrderMapper.java
│ ├── RepairOrderMapper.xml
│ ├── RepairAttachmentMapper.java
│ └── RepairProgressMapper.java
├── service/ # 业务服务层
│ ├── IRepairOrderService.java
│ ├── IRepairAttachmentService.java
│ ├── IMaintenanceStatisticsService.java
│ └── impl/
│ ├── RepairOrderServiceImpl.java
│ ├── RepairAttachmentServiceImpl.java
│ └── MaintenanceStatisticsServiceImpl.java
└── utils/ # 工具类
└── MaintenanceUtils.java
```
#### 前端代码结构
```
src/views/maintenance/
├── repair-order/ # 报修单管理
│ ├── index.vue # 报修单列表页
│ ├── form.vue # 报修申请表单
│ └── detail.vue # 报修单详情
├── work-order/ # 工单管理
│ ├── index.vue # 工单管理页面
│ └── assign.vue # 工单分派页面
├── workbench/ # 维修工作台
│ ├── index.vue # 维修员工作台
│ └── progress.vue # 进度更新页面
├── statistics/ # 统计分析
│ └── index.vue # 统计分析页面
├── components/ # 页面组件
│ ├── RepairOrderForm.vue
│ ├── AssignDialog.vue
│ ├── ProgressTimeline.vue
│ └── AttachmentUpload.vue
├── types/ # 类型定义
│ └── index.ts
└── api/ # API接口
└── index.ts
```
### 命名规范
#### 后端命名规范
- **实体类**:RepairOrder、RepairAttachment、RepairProgress
- **服务类**:RepairOrderService、MaintenanceStatisticsService
- **控制器**:RepairOrderController、MaintenanceStatisticsController
- **DTO 类**:RepairOrderBo、RepairOrderVo、AssignRepairOrderBo
#### 前端命名规范
- **页面组件**:RepairOrderList、WorkOrderManage、MaintenanceWorkbench
- **业务组件**:RepairOrderForm、AssignDialog、ProgressTimeline
- **API 方法**:getRepairOrderList、createRepairOrder、assignRepairOrder
- **类型接口**:RepairOrderForm、RepairOrderVo、AssignFormData
#### 数据库命名规范
- **表名**:pm_repair_order、pm_repair_attachment、pm_repair_progress
- **字段名**:repair_order_id、contact_name、urgency_level
- **索引名**:idx_repair_order_status、idx_repair_order_assignee
## 🧪 测试策略
### 测试分类
#### 单元测试
- **覆盖范围**:Service 层业务逻辑、工具类方法
- **测试框架**:JUnit 5 + Mockito + Spring Boot Test
- **覆盖率要求**:> 85%
- **重点测试**:报修单状态流转、工单分派逻辑、统计计算算法
#### 集成测试
- **覆盖范围**:Controller 层 API 接口、数据库操作
- **测试框架**:Spring Boot Test + TestContainers
- **测试数据**:独立测试数据库
- **重点测试**:完整业务流程、文件上传下载、权限控制
#### 前端测试
- **覆盖范围**:组件功能、用户交互、表单验证
- **测试框架**:Vue Test Utils + Jest + Cypress
- **测试类型**:组件单元测试、页面集成测试、E2E 测试
### 测试用例设计
#### 功能测试用例
- **报修申请测试**:正常提交、信息验证、文件上传
- **工单分派测试**:分派流程、权限验证、状态更新
- **维修流程测试**:接单确认、进度更新、完成提交
- **评价功能测试**:评分提交、满意度统计、数据展示
#### 性能测试用例
- **并发提交测试**:模拟多用户同时提交报修申请
- **文件上传测试**:大文件上传、多文件并发上传
- **数据查询测试**:大数据量下的列表查询性能
- **统计分析测试**:复杂统计查询的响应时间
## 🚀 部署运维
### 部署环境
#### 开发环境
- **部署方式**:本地 Docker 容器
- **数据库**:MySQL 8.0
- **缓存**:Redis 7.0
- **文件存储**:本地文件系统
- **配置**:application-dev.yml
#### 测试环境
- **部署方式**:Docker Compose
- **数据库**:MySQL 8.0(测试库)
- **缓存**:Redis 7.0
- **文件存储**:MinIO 或 阿里云 OSS
- **配置**:application-test.yml
#### 生产环境
- **部署方式**:Kubernetes
- **数据库**:MySQL 8.0(主从集群)
- **缓存**:Redis 7.0(集群模式)
- **文件存储**:阿里云 OSS
- **负载均衡**:Nginx + Ingress
- **配置**:application-prod.yml
### 监控指标
#### 业务指标
- **报修单提交量**:每日、每周、每月报修申请数量
- **处理效率**:平均处理时长、及时完成率
- **用户满意度**:平均评分、满意度趋势
- **维修员工作量**:工单分配数量、完成效率
#### 技术指标
- **接口响应时间**:各 API 接口的平均响应时间 < 500ms
- **文件上传成功率**:文件上传成功率 > 99%
- **系统可用性**:服务可用性 > 99.9%
- **错误率**:系统错误率 < 0.1%
## 📅 开发计划
### 开发阶段
#### 第一阶段:基础框架(预计 2 周)
- [ ] 数据库表结构设计和创建
- [ ] 基础实体类和 Mapper 开发
- [ ] 基础 CRUD 接口开发
- [ ] 前端页面框架搭建
- [ ] 单元测试编写
#### 第二阶段:核心功能(预计 3 周)
- [ ] 报修申请功能开发
- [ ] 文件上传功能开发
- [ ] 工单分派功能开发
- [ ] 维修流程管理开发
- [ ] 集成测试编写
#### 第三阶段:高级功能(预计 2 周)
- [ ] 服务评价功能开发
- [ ] 统计分析功能开发
- [ ] 移动端适配优化
- [ ] 性能优化
#### 第四阶段:测试部署(预计 1 周)
- [ ] 完整功能测试
- [ ] 性能压力测试
- [ ] 用户验收测试
- [ ] 生产环境部署
- [ ] 上线运维监控
### 里程碑
- **里程碑 1**:基础框架完成,能够创建和查询报修单
- **里程碑 2**:核心流程打通,完整的报修-分派-维修流程
- **里程碑 3**:功能完善,包括评价和统计功能
- **里程碑 4**:系统上线,正式投入使用
---
## 📖 相关文档
- **需求分析详细文档**:[需求分析/业务需求说明.md](需求分析/业务需求说明.md)
- **技术设计详细文档**:[技术设计/架构设计.md](技术设计/架构设计.md)
- **API 接口详细文档**:[技术设计/API 接口设计.md](技术设计/API接口设计.md)
- **数据库设计详细文档**:[技术设计/数据库设计.md](技术设计/数据库设计.md)
- **项目总览文档**:[../../项目总览/README.md](../../项目总览/README.md)
- **开发规范文档**:[../../开发文档/README.md](../../开发文档/README.md)
**重要说明**:
1. **用户权限管理**:使用系统自带的用户管理、角色管理和权限管理功能,无需重新开发
2. **通知功能**:本模块不包含通知功能,用户通过系统界面查看状态更新和进度变化
3. **集成说明**:与现有系统的用户认证、文件存储、工作流引擎进行集成
@@ -0,0 +1,632 @@
# 物业报修管理模块 - 数据库设计
## 📋 文档信息
**文档名称**:物业报修管理模块数据库设计
**文档版本**:v1.0
**创建日期**:2025-01-23
**设计规范**:严格遵循 `docs/设计文档/database/mysql-database-standard.md`
**依据文档**:功能需求清单.md
## 🎯 数据库设计原则
### 核心设计理念
1. **严格遵循项目规范**:所有表必须包含标准基础字段
2. **业务导向设计**:基于实际业务流程设计表结构
3. **性能优化**:合理设计索引,支持高效查询
4. **扩展性考虑**:预留扩展字段和机制
5. **数据完整性**:通过应用层维护关联关系
### 关键约束
- ✅ **使用雪花 ID**作为主键
- ✅ **强制逻辑删除**,禁止物理删除
- ✅ **多租户支持**,所有表包含 tenant_id
- ✅ **禁用外键约束**,关联关系通过应用层维护
- ✅ **完整中文注释**,便于理解和维护
## 📊 数据模型概览
### 🗂️ 表结构清单
| 表名 | 中文名称 | 用途 | 关联关系 |
| ----------------------- | ---------- | ---------------------- | ------------ |
| `pm_repair_order` | 报修工单表 | 存储报修申请的核心信息 | 核心表 |
| `pm_repair_attachment` | 报修附件表 | 存储报修相关附件文件 | 关联报修工单 |
| `pm_repair_progress` | 维修进度表 | 记录维修过程状态变更 | 关联报修工单 |
| `pm_service_evaluation` | 服务评价表 | 存储用户服务质量评价 | 关联报修工单 |
| `pm_repair_category` | 维修类型表 | 报修类型分类配置 | 字典表 |
| `pm_system_config` | 系统配置表 | 模块系统参数配置 | 配置表 |
### 📈 数据流转关系
```mermaid
graph TD
A[用户提交报修] --> B[pm_repair_order 报修工单]
B --> C[pm_repair_attachment 附件]
B --> D[pm_repair_progress 进度]
B --> E[pm_service_evaluation 评价]
F[pm_repair_category 类型] --> B
G[pm_system_config 配置] --> B
H[sys_user 系统用户] --> B
H --> D
```
## 🏗️ 详细表结构设计
### 1. 报修工单表 (pm_repair_order)
> **核心业务表**:存储报修申请的所有核心信息,是整个模块的数据中心
```sql
-- ----------------------------
-- Table structure for pm_repair_order
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_order`;
CREATE TABLE `pm_repair_order` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 业务基础信息
`order_no` varchar(50) NOT NULL COMMENT '工单编号,格式:RO+年月日+4位流水号',
`title` varchar(200) NOT NULL COMMENT '报修标题',
`description` text NOT NULL COMMENT '问题描述',
`repair_location` varchar(200) NOT NULL COMMENT '报修地点',
-- 分类和紧急程度
`category_id` bigint(20) NOT NULL COMMENT '维修类型ID,关联pm_repair_category.id',
`priority_level` tinyint(1) NOT NULL DEFAULT 3 COMMENT '紧急程度:1-非常紧急,2-紧急,3-一般,4-不紧急,5-可延后',
`expected_finish_time` datetime DEFAULT NULL COMMENT '期望完成时间',
-- 报修人信息
`reporter_id` bigint(20) NOT NULL COMMENT '报修人ID,关联sys_user.id',
`reporter_name` varchar(50) NOT NULL COMMENT '报修人姓名',
`reporter_phone` varchar(20) NOT NULL COMMENT '报修人电话',
`reporter_dept` varchar(100) DEFAULT NULL COMMENT '报修人部门',
-- 工单状态和处理信息
`order_status` char(1) NOT NULL DEFAULT '1' COMMENT '工单状态:1-待分派,2-已分派,3-进行中,4-已完成,5-已评价,6-已归档,9-已取消',
`assign_time` datetime DEFAULT NULL COMMENT '分派时间',
`assignee_id` bigint(20) DEFAULT NULL COMMENT '分派维修员ID,关联sys_user.id',
`assignee_name` varchar(50) DEFAULT NULL COMMENT '分派维修员姓名',
`assign_remark` varchar(500) DEFAULT NULL COMMENT '分派备注说明',
-- 处理时间信息
`start_time` datetime DEFAULT NULL COMMENT '开始处理时间',
`finish_time` datetime DEFAULT NULL COMMENT '完成时间',
`total_duration` int(11) DEFAULT NULL COMMENT '总处理时长(分钟)',
-- 处理结果
`repair_result` text DEFAULT NULL COMMENT '维修结果描述',
`used_materials` varchar(500) DEFAULT NULL COMMENT '使用材料清单',
`follow_suggestions` varchar(500) DEFAULT NULL COMMENT '后续建议',
-- 评价相关
`evaluation_score` decimal(3,1) DEFAULT NULL COMMENT '综合评价得分(1.0-5.0)',
`evaluation_time` datetime DEFAULT NULL COMMENT '评价时间',
-- 统计字段
`attachment_count` int(11) DEFAULT 0 COMMENT '附件数量',
`progress_count` int(11) DEFAULT 0 COMMENT '进度记录数量',
-- 其他字段
`cancel_reason` varchar(500) DEFAULT NULL COMMENT '取消原因',
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_order_no` (`order_no`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_status` (`order_status`),
KEY `idx_priority_level` (`priority_level`),
KEY `idx_create_time` (`create_time`),
KEY `idx_reporter_id` (`reporter_id`),
KEY `idx_assignee_id` (`assignee_id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_assign_time` (`assign_time`),
KEY `idx_finish_time` (`finish_time`),
KEY `idx_tenant_status` (`tenant_id`, `order_status`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修工单表';
```
### 2. 报修附件表 (pm_repair_attachment)
> **附件管理表**:存储报修过程中上传的图片、视频等多媒体文件信息
```sql
-- ----------------------------
-- Table structure for pm_repair_attachment
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_attachment`;
CREATE TABLE `pm_repair_attachment` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
-- 文件信息
`file_name` varchar(200) NOT NULL COMMENT '文件原始名称',
`file_path` varchar(500) NOT NULL COMMENT '文件存储路径',
`file_size` bigint(20) NOT NULL COMMENT '文件大小(字节)',
`file_type` varchar(20) NOT NULL COMMENT '文件类型:image-图片,video-视频',
`file_extension` varchar(10) NOT NULL COMMENT '文件扩展名',
`file_mime_type` varchar(100) DEFAULT NULL COMMENT '文件MIME类型',
-- 上传信息
`upload_stage` char(1) NOT NULL DEFAULT '1' COMMENT '上传阶段:1-报修申请,2-维修过程,3-完成确认',
`uploader_id` bigint(20) NOT NULL COMMENT '上传人ID,关联sys_user.id',
`uploader_name` varchar(50) NOT NULL COMMENT '上传人姓名',
-- 文件处理
`thumbnail_path` varchar(500) DEFAULT NULL COMMENT '缩略图路径(图片文件)',
`duration` int(11) DEFAULT NULL COMMENT '时长(视频文件,秒)',
`resolution` varchar(20) DEFAULT NULL COMMENT '分辨率(如:1920x1080)',
-- 业务信息
`description` varchar(200) DEFAULT NULL COMMENT '文件描述说明',
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_id` (`order_id`),
KEY `idx_upload_stage` (`upload_stage`),
KEY `idx_uploader_id` (`uploader_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_file_type` (`file_type`),
KEY `idx_order_stage` (`order_id`, `upload_stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修附件表';
```
### 3. 维修进度表 (pm_repair_progress)
> **进度跟踪表**:记录维修过程中的每一个状态变更和进度更新
```sql
-- ----------------------------
-- Table structure for pm_repair_progress
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_progress`;
CREATE TABLE `pm_repair_progress` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
-- 进度信息
`progress_type` char(1) NOT NULL COMMENT '进度类型:1-工单分派,2-接收确认,3-到达现场,4-开始维修,5-进度更新,6-完成维修,7-用户评价,8-工单归档,9-其他',
`progress_status` varchar(50) NOT NULL COMMENT '进度状态描述',
`progress_detail` text DEFAULT NULL COMMENT '进度详细描述',
-- 操作人信息
`operator_id` bigint(20) NOT NULL COMMENT '操作人ID,关联sys_user.id',
`operator_name` varchar(50) NOT NULL COMMENT '操作人姓名',
`operator_role` varchar(20) NOT NULL COMMENT '操作人角色:user-用户,admin-管理员,worker-维修员',
-- 位置信息
`location_info` varchar(200) DEFAULT NULL COMMENT '操作位置信息',
`gps_latitude` decimal(10,7) DEFAULT NULL COMMENT 'GPS纬度',
`gps_longitude` decimal(10,7) DEFAULT NULL COMMENT 'GPS经度',
-- 时间信息
`operation_time` datetime NOT NULL COMMENT '操作时间',
`estimated_duration` int(11) DEFAULT NULL COMMENT '预计耗时(分钟)',
-- 附加信息
`attachment_ids` varchar(500) DEFAULT NULL COMMENT '关联附件ID列表(逗号分隔)',
`next_step` varchar(200) DEFAULT NULL COMMENT '下一步计划',
`issues_encountered` varchar(500) DEFAULT NULL COMMENT '遇到的问题',
`materials_needed` varchar(200) DEFAULT NULL COMMENT '需要的材料',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_id` (`order_id`),
KEY `idx_progress_type` (`progress_type`),
KEY `idx_operator_id` (`operator_id`),
KEY `idx_operation_time` (`operation_time`),
KEY `idx_create_time` (`create_time`),
KEY `idx_order_time` (`order_id`, `operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修进度表';
```
### 4. 服务评价表 (pm_service_evaluation)
> **评价反馈表**:存储用户对维修服务的多维度评价和反馈信息
```sql
-- ----------------------------
-- Table structure for pm_service_evaluation
-- ----------------------------
DROP TABLE IF EXISTS `pm_service_evaluation`;
CREATE TABLE `pm_service_evaluation` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
`evaluator_id` bigint(20) NOT NULL COMMENT '评价人ID,关联sys_user.id',
`evaluator_name` varchar(50) NOT NULL COMMENT '评价人姓名',
`evaluated_worker_id` bigint(20) NOT NULL COMMENT '被评价维修员ID,关联sys_user.id',
`evaluated_worker_name` varchar(50) NOT NULL COMMENT '被评价维修员姓名',
-- 评价得分(1-5星,支持小数)
`overall_score` decimal(3,1) NOT NULL COMMENT '综合评价得分(1.0-5.0)',
`quality_score` decimal(3,1) NOT NULL COMMENT '维修质量得分(1.0-5.0)',
`attitude_score` decimal(3,1) NOT NULL COMMENT '服务态度得分(1.0-5.0)',
`response_speed_score` decimal(3,1) NOT NULL COMMENT '响应速度得分(1.0-5.0)',
`communication_score` decimal(3,1) NOT NULL COMMENT '沟通效果得分(1.0-5.0)',
-- 评价内容
`evaluation_content` text DEFAULT NULL COMMENT '文字评价内容',
`satisfaction_level` char(1) NOT NULL COMMENT '满意度等级:1-非常满意,2-满意,3-一般,4-不满意,5-非常不满意',
-- 评价设置
`is_anonymous` tinyint(1) DEFAULT 0 COMMENT '是否匿名评价(0-实名,1-匿名)',
`is_public` tinyint(1) DEFAULT 1 COMMENT '是否公开显示(0-不公开,1-公开)',
-- 时间信息
`evaluation_time` datetime NOT NULL COMMENT '评价时间',
`service_start_time` datetime DEFAULT NULL COMMENT '服务开始时间',
`service_end_time` datetime DEFAULT NULL COMMENT '服务结束时间',
`service_duration` int(11) DEFAULT NULL COMMENT '服务时长(分钟)',
-- 标签和建议
`positive_tags` varchar(200) DEFAULT NULL COMMENT '正面标签(逗号分隔)',
`negative_tags` varchar(200) DEFAULT NULL COMMENT '负面标签(逗号分隔)',
`improvement_suggestions` varchar(500) DEFAULT NULL COMMENT '改进建议',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_order_evaluation` (`order_id`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_evaluator_id` (`evaluator_id`),
KEY `idx_evaluated_worker_id` (`evaluated_worker_id`),
KEY `idx_overall_score` (`overall_score`),
KEY `idx_satisfaction_level` (`satisfaction_level`),
KEY `idx_evaluation_time` (`evaluation_time`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务评价表';
```
### 5. 维修类型表 (pm_repair_category)
> **分类字典表**:存储维修类型的分类信息,支持层级结构
```sql
-- ----------------------------
-- Table structure for pm_repair_category
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_category`;
CREATE TABLE `pm_repair_category` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 分类信息
`category_name` varchar(50) NOT NULL COMMENT '类型名称',
`category_code` varchar(20) NOT NULL COMMENT '类型编码',
`category_description` varchar(200) DEFAULT NULL COMMENT '类型描述',
-- 层级结构
`parent_id` bigint(20) DEFAULT 0 COMMENT '父级ID,顶级为0',
`ancestor_ids` varchar(500) DEFAULT '0' COMMENT '祖级列表(逗号分隔)',
`level_depth` tinyint(2) DEFAULT 1 COMMENT '层级深度(1-顶级,2-二级...)',
`children_count` int(11) DEFAULT 0 COMMENT '子类数量',
-- 专业技能
`required_skills` varchar(200) DEFAULT NULL COMMENT '所需专业技能(逗号分隔)',
`skill_level` char(1) DEFAULT '1' COMMENT '技能等级要求:1-初级,2-中级,3-高级',
-- 时间和成本预估
`estimated_duration` int(11) DEFAULT NULL COMMENT '预估维修时长(分钟)',
`standard_cost` decimal(10,2) DEFAULT NULL COMMENT '标准成本(元)',
-- 显示控制
`icon_url` varchar(200) DEFAULT NULL COMMENT '分类图标URL',
`color_code` varchar(10) DEFAULT '#1890ff' COMMENT '分类颜色代码',
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
`status` char(1) DEFAULT '1' COMMENT '状态(0-停用,1-正常)',
-- 统计信息
`usage_count` int(11) DEFAULT 0 COMMENT '使用次数统计',
`last_used_time` datetime DEFAULT NULL COMMENT '最后使用时间',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_category_code` (`category_code`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_status` (`status`),
KEY `idx_sort_order` (`sort_order`),
KEY `idx_level_depth` (`level_depth`),
KEY `idx_usage_count` (`usage_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修类型表';
```
### 6. 系统配置表 (pm_system_config)
> **配置管理表**:存储物业报修模块的系统参数和配置信息
```sql
-- ----------------------------
-- Table structure for pm_system_config
-- ----------------------------
DROP TABLE IF EXISTS `pm_system_config`;
CREATE TABLE `pm_system_config` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 配置信息
`config_key` varchar(100) NOT NULL COMMENT '配置键名',
`config_value` text DEFAULT NULL COMMENT '配置值',
`config_type` varchar(20) NOT NULL COMMENT '配置类型:string-字符串,number-数字,boolean-布尔,json-JSON对象',
`config_group` varchar(50) NOT NULL COMMENT '配置分组:file-文件设置,time-时间设置,notice-通知设置,workflow-流程设置',
-- 配置描述
`config_name` varchar(100) NOT NULL COMMENT '配置名称',
`config_description` varchar(500) DEFAULT NULL COMMENT '配置描述说明',
`default_value` text DEFAULT NULL COMMENT '默认值',
-- 配置约束
`is_system` tinyint(1) DEFAULT 0 COMMENT '是否系统内置(0-否,1-是,系统内置不可删除)',
`is_editable` tinyint(1) DEFAULT 1 COMMENT '是否可编辑(0-否,1-是)',
`validation_rule` varchar(200) DEFAULT NULL COMMENT '验证规则(正则表达式或范围)',
-- 显示控制
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
`status` char(1) DEFAULT '1' COMMENT '状态(0-停用,1-正常)',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_config_key` (`config_key`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_config_group` (`config_group`),
KEY `idx_status` (`status`),
KEY `idx_sort_order` (`sort_order`),
KEY `idx_is_system` (`is_system`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
```
## 📝 数据字典
### 1. 工单状态枚举 (order_status)
| 状态码 | 状态名称 | 描述 | 可执行操作 |
| ------ | -------- | -------------------------- | ------------------------ |
| 1 | 待分派 | 用户刚提交,等待管理员分派 | 分派、取消、修改 |
| 2 | 已分派 | 已分派给维修员,等待接收 | 接收、拒绝、重新分派 |
| 3 | 进行中 | 维修员已接收,正在处理 | 更新进度、完成、上报问题 |
| 4 | 已完成 | 维修已完成,等待评价 | 评价、重新打开 |
| 5 | 已评价 | 用户已评价,等待归档 | 归档 |
| 6 | 已归档 | 工单已归档,流程结束 | 查看历史 |
| 9 | 已取消 | 工单已取消 | 查看原因 |
### 2. 紧急程度枚举 (priority_level)
| 等级 | 名称 | 描述 | 响应时限 | 颜色标识 |
| ---- | -------- | ------------------ | --------- | -------- |
| 1 | 非常紧急 | 影响安全或正常运转 | 1 小时内 | 红色 |
| 2 | 紧急 | 影响重要功能使用 | 4 小时内 | 橙色 |
| 3 | 一般 | 常规维修需求 | 24 小时内 | 蓝色 |
| 4 | 不紧急 | 不影响使用的问题 | 72 小时内 | 绿色 |
| 5 | 可延后 | 可以安排时间处理 | 一周内 | 灰色 |
### 3. 进度类型枚举 (progress_type)
| 类型码 | 类型名称 | 操作人角色 | 描述 |
| ------ | -------- | ----------- | ------------------ |
| 1 | 工单分派 | 管理员 | 工单分派给维修员 |
| 2 | 接收确认 | 维修员 | 维修员确认接收工单 |
| 3 | 到达现场 | 维修员 | 维修员到达维修现场 |
| 4 | 开始维修 | 维修员 | 开始进行维修作业 |
| 5 | 进度更新 | 维修员 | 维修过程进度更新 |
| 6 | 完成维修 | 维修员 | 维修工作已完成 |
| 7 | 用户评价 | 用户 | 用户提交服务评价 |
| 8 | 工单归档 | 系统/管理员 | 工单归档处理 |
| 9 | 其他 | 任意角色 | 其他类型操作 |
### 4. 文件类型枚举 (file_type)
| 类型 | 名称 | 支持格式 | 大小限制 |
| ----- | ---- | ------------------- | -------- |
| image | 图片 | jpg, jpeg, png, gif | 5MB |
| video | 视频 | mp4, avi, mov | 50MB |
### 5. 上传阶段枚举 (upload_stage)
| 阶段码 | 阶段名称 | 描述 |
| ------ | -------- | ---------------------- |
| 1 | 报修申请 | 用户提交报修时上传 |
| 2 | 维修过程 | 维修员维修过程中上传 |
| 3 | 完成确认 | 维修完成时上传结果照片 |
## 🔧 索引优化策略
### 1. 核心业务查询索引
```sql
-- 报修工单表的核心查询索引
-- 1. 按状态和时间查询(管理员常用)
CREATE INDEX idx_status_time ON pm_repair_order (order_status, create_time);
-- 2. 按维修员查询其工单(维修员常用)
CREATE INDEX idx_assignee_status ON pm_repair_order (assignee_id, order_status);
-- 3. 按报修人查询(用户查看自己的工单)
CREATE INDEX idx_reporter_time ON pm_repair_order (reporter_id, create_time);
-- 4. 按紧急程度和分派时间查询(工单分派)
CREATE INDEX idx_priority_assign ON pm_repair_order (priority_level, assign_time);
```
### 2. 附件查询索引
```sql
-- 附件表的查询索引
-- 1. 按工单查询附件(最常用)
CREATE INDEX idx_order_stage ON pm_repair_attachment (order_id, upload_stage);
-- 2. 按上传者查询附件
CREATE INDEX idx_uploader_time ON pm_repair_attachment (uploader_id, create_time);
```
### 3. 进度查询索引
```sql
-- 进度表的查询索引
-- 1. 按工单查询进度(时间线显示)
CREATE INDEX idx_order_operation_time ON pm_repair_progress (order_id, operation_time);
-- 2. 按操作人查询进度
CREATE INDEX idx_operator_time ON pm_repair_progress (operator_id, operation_time);
```
## 📊 数据存储预估
### 表数据量预估
| 表名 | 日增长量 | 年数据量 | 存储空间预估 |
| --------------------- | ---------- | ------------ | ------------ |
| pm_repair_order | 50-100 条 | 2-4 万条 | 50-100MB |
| pm_repair_attachment | 200-400 条 | 8-16 万条 | 100-200MB |
| pm_repair_progress | 300-600 条 | 12-24 万条 | 150-300MB |
| pm_service_evaluation | 40-80 条 | 1.6-3.2 万条 | 20-40MB |
| pm_repair_category | 0-5 条 | 100-200 条 | <1MB |
| pm_system_config | 0-2 条 | 50-100 条 | <1MB |
### 存储优化建议
1. **定期归档**:超过 2 年的已完成工单可归档到历史表
2. **附件清理**:定期清理无效或重复的附件文件
3. **索引维护**:定期重建和优化索引
4. **分区策略**:可考虑按年份或状态分区存储
## 🔄 数据初始化脚本
### 1. 维修类型初始化数据
```sql
-- 插入基础维修类型分类
INSERT INTO `pm_repair_category` (
`id`, `tenant_id`, `category_name`, `category_code`, `category_description`,
`parent_id`, `level_depth`, `required_skills`, `skill_level`,
`estimated_duration`, `icon_url`, `color_code`, `sort_order`, `status`,
`create_time`, `create_by`
) VALUES
(1, '000000', '电路维修', 'ELECTRIC', '电路、照明、开关、插座等电气设备维修', 0, 1, '电工技能', '2', 60, '/icons/electric.svg', '#ff6b35', 1, '1', NOW(), 1),
(2, '000000', '水暖维修', 'PLUMBING', '水管、暖气、卫生间、厨房设备维修', 0, 1, '水暖技能', '2', 90, '/icons/plumbing.svg', '#1890ff', 2, '1', NOW(), 1),
(3, '000000', '土建维修', 'CONSTRUCTION', '墙面、地面、天花板、门窗等土建维修', 0, 1, '泥工技能,木工技能', '2', 120, '/icons/construction.svg', '#52c41a', 3, '1', NOW(), 1),
(4, '000000', '设备维修', 'EQUIPMENT', '空调、电梯、监控等设备维修', 0, 1, '设备维修技能', '3', 90, '/icons/equipment.svg', '#722ed1', 4, '1', NOW(), 1),
(5, '000000', '园林维修', 'GARDEN', '绿化、草坪、树木修剪等园林维护', 0, 1, '园林技能', '1', 45, '/icons/garden.svg', '#13c2c2', 5, '1', NOW(), 1),
(6, '000000', '清洁维护', 'CLEANING', '日常清洁、垃圾处理、环境维护', 0, 1, '清洁技能', '1', 30, '/icons/cleaning.svg', '#fa8c16', 6, '1', NOW(), 1);
```
### 2. 系统配置初始化数据
```sql
-- 插入系统配置参数
INSERT INTO `pm_system_config` (
`id`, `tenant_id`, `config_key`, `config_value`, `config_type`, `config_group`,
`config_name`, `config_description`, `default_value`, `is_system`, `is_editable`,
`sort_order`, `status`, `create_time`, `create_by`
) VALUES
(1, '000000', 'file.upload.image.max_size', '5242880', 'number', 'file', '图片文件最大大小', '图片文件上传的最大字节数(5MB)', '5242880', 1, 1, 1, '1', NOW(), 1),
(2, '000000', 'file.upload.video.max_size', '52428800', 'number', 'file', '视频文件最大大小', '视频文件上传的最大字节数(50MB)', '52428800', 1, 1, 2, '1', NOW(), 1),
(3, '000000', 'file.upload.allowed_image_types', 'jpg,jpeg,png,gif', 'string', 'file', '允许的图片类型', '允许上传的图片文件类型(逗号分隔)', 'jpg,jpeg,png,gif', 1, 1, 3, '1', NOW(), 1),
(4, '000000', 'file.upload.allowed_video_types', 'mp4,avi,mov', 'string', 'file', '允许的视频类型', '允许上传的视频文件类型(逗号分隔)', 'mp4,avi,mov', 1, 1, 4, '1', NOW(), 1),
(5, '000000', 'workflow.auto_assign_enabled', 'true', 'boolean', 'workflow', '启用自动分派', '是否启用智能自动分派功能', 'false', 1, 1, 11, '1', NOW(), 1),
(6, '000000', 'workflow.auto_archive_days', '7', 'number', 'workflow', '自动归档天数', '工单评价后多少天自动归档', '7', 1, 1, 12, '1', NOW(), 1),
(7, '000000', 'time.priority1_response_hours', '1', 'number', 'time', '非常紧急响应时限', '非常紧急工单的响应时限(小时)', '1', 1, 1, 21, '1', NOW(), 1),
(8, '000000', 'time.priority2_response_hours', '4', 'number', 'time', '紧急响应时限', '紧急工单的响应时限(小时)', '4', 1, 1, 22, '1', NOW(), 1),
(9, '000000', 'time.priority3_response_hours', '24', 'number', 'time', '一般响应时限', '一般工单的响应时限(小时)', '24', 1, 1, 23, '1', NOW(), 1),
(10, '000000', 'time.working_hours_start', '08:00', 'string', 'time', '工作时间开始', '工作日开始时间(HH:mm格式)', '08:00', 1, 1, 31, '1', NOW(), 1),
(11, '000000', 'time.working_hours_end', '18:00', 'string', 'time', '工作时间结束', '工作日结束时间(HH:mm格式)', '18:00', 1, 1, 32, '1', NOW(), 1);
```
## 📋 数据迁移计划
### 迁移脚本版本规划
1. **V1.0.1** - 创建基础表结构
2. **V1.0.2** - 插入初始化数据
3. **V1.0.3** - 创建性能优化索引
4. **V1.1.0** - 功能扩展字段添加
### 部署验证清单
- [ ] 表结构创建成功
- [ ] 基础索引创建完成
- [ ] 初始化数据插入成功
- [ ] 性能索引创建完成
- [ ] 数据完整性约束验证
- [ ] 权限配置正确
- [ ] 备份恢复测试通过
---
**文档状态**:✅ 初稿完成
**关联文档**:功能需求清单.md
**下一步**:API 接口设计
@@ -0,0 +1,591 @@
# 物业报修管理模块 - 业务需求说明
## 📋 文档信息
**文档名称**:物业报修管理模块业务需求说明
**文档版本**:v1.0
**创建日期**:2025-01-23
**创建人员**:系统分析师
**审核状态**:待审核
## 🎯 项目背景
### 现状分析
目前学校物业维修服务存在以下问题:
#### 🔴 用户报修方面
- **报修渠道单一**:主要依赖电话报修,缺乏在线渠道
- **信息记录不全**:电话报修信息容易遗漏,无法保存现场照片
- **进度不透明**:用户无法实时了解维修进度
- **反馈渠道缺失**:维修完成后缺乏服务质量反馈机制
#### 🔴 管理方面
- **工单管理混乱**:缺乏统一的工单管理系统
- **分派效率低下**:人工分派工单,容易出现负荷不均
- **数据统计困难**:无法有效统计维修效率和满意度数据
- **绩效考核缺失**:缺乏客观的维修员绩效评价体系
#### 🔴 维修员方面
- **工单接收不便**:需要通过电话或纸质单据接收任务
- **进度更新困难**:无法及时更新维修进度给用户
- **工作量统计难**:个人工作量和效率缺乏准确统计
### 建设目标
构建一个完整的数字化物业维修管理平台,实现:
1. **服务便民化**:提供便捷的在线报修渠道
2. **管理智能化**:实现智能工单分派和进度跟踪
3. **流程标准化**:建立标准化的维修业务流程
4. **数据可视化**:提供全面的数据统计和分析
5. **服务透明化**:实现维修进度的实时透明展示
## 🔍 业务需求分析
### 核心业务场景
#### 场景 1:学生宿舍报修
**背景**:学生宿舍水龙头漏水需要维修
**当前流程**:
1. 学生发现问题 → 2. 拨打后勤电话 → 3. 口头描述问题 → 4. 等待维修 → 5. 维修员上门 → 6. 完成维修
**期望流程**:
1. 学生在线填写报修单 → 2. 上传现场照片 → 3. 系统自动通知管理员 → 4. 管理员分派维修员 → 5. 维修员接单并现场维修 → 6. 学生在线评价
#### 场景 2:教学楼设备维修
**背景**:教室多媒体设备故障需要紧急维修
**业务特点**:
- 影响教学,需要紧急处理
- 需要专业维修员处理
- 需要准确记录设备型号和故障现象
#### 场景 3:公共区域维修
**背景**:校园路灯损坏需要维修
**业务特点**:
- 影响校园安全,优先级高
- 可能需要外部供应商配合
- 需要考虑天气等外部因素
### 主要业务流程
#### 1. 报修申请流程
```mermaid
graph TD
A[用户发现问题] --> B[登录系统]
B --> C[填写报修信息]
C --> D[选择报修类型]
D --> E[上传现场照片/视频]
E --> F[设置期望完成时间]
F --> G[提交报修申请]
G --> H[系统生成报修单号]
H --> I[发送确认通知]
I --> J[等待处理]
```
**详细说明**:
- **步骤 1-2**:用户需要先登录系统,确保身份可追溯
- **步骤 3-4**:填写详细的报修信息,选择正确的维修类型便于分派
- **步骤 5**:支持上传多张图片或视频,帮助维修员提前了解问题
- **步骤 6**:用户可以设置期望完成时间,但不强制
- **步骤 7-9**:系统自动处理后续流程,确保信息不遗漏
#### 2. 工单分派流程
```mermaid
graph TD
A[管理员接收新报修单] --> B[查看报修详情]
B --> C[评估维修类型和紧急程度]
C --> D[查看维修员工作负荷]
D --> E[选择合适的维修员]
E --> F[确认分派]
F --> G[系统更新工单状态]
G --> H[通知维修员]
H --> I[通知报修用户]
```
**业务规则**:
- **紧急工单优先**:紧急程度高的工单优先分派
- **专业匹配**:根据维修类型选择专业对口的维修员
- **负荷均衡**:考虑维修员当前工作量,避免过度集中
- **地理位置**:优先分派给距离报修地点较近的维修员
#### 3. 维修执行流程
```mermaid
graph TD
A[维修员接收工单] --> B[确认接单]
B --> C[联系报修人确认时间]
C --> D[到达现场]
D --> E[开始维修]
E --> F[遇到问题?]
F -->|是| G[上报管理员]
F -->|否| H[继续维修]
G --> I[等待指示]
I --> H
H --> J[完成维修]
J --> K[拍照记录结果]
K --> L[提交维修报告]
L --> M[等待用户确认]
```
**关键节点**:
- **接单确认**:维修员必须主动确认接单,避免工单遗漏
- **进度更新**:维修过程中应及时更新进度状态
- **问题上报**:遇到技术问题或需要配件时应及时上报
- **结果记录**:维修完成后必须拍照记录维修结果
#### 4. 服务评价流程
```mermaid
graph TD
A[用户收到完成通知] --> B[查看维修结果]
B --> C[填写服务评价]
C --> D[评分各个维度]
D --> E[添加文字评价]
E --> F[提交评价]
F --> G[系统计算满意度]
G --> H[更新维修员绩效]
H --> I[归档报修单]
```
**评价维度**:
- **维修质量**:维修效果是否满意
- **服务态度**:维修员服务态度评价
- **响应速度**:从报修到开始维修的时间
- **沟通效果**:维修过程中的沟通是否顺畅
## 👥 用户角色分析
### 1. 普通用户(报修人)
#### 用户特征
- **身份**:在校学生、教职工
- **年龄**:18-60 岁
- **技术水平**:基础到中等
- **使用场景**:发现问题需要维修时
#### 核心需求
- **便捷报修**:简单易用的报修界面
- **进度查询**:随时了解维修进度
- **问题沟通**:与维修员直接沟通的渠道
- **服务反馈**:对维修服务进行评价
#### 使用频次
- **低频用户**:偶尔遇到问题才使用
- **重点关注**:界面简洁、操作简单
### 2. 物业管理员
#### 用户特征
- **身份**:物业管理部门工作人员
- **工作内容**:维修工单管理、资源调度
- **技术水平**:中等到高级
- **使用场景**:日常工作时间
#### 核心需求
- **工单管理**:查看、分派、跟踪所有工单
- **资源调度**:合理分配维修人员
- **数据统计**:了解维修业务整体情况
- **绩效管理**:维修员工作量和质量评估
#### 使用频次
- **高频用户**:每日多次使用
- **重点关注**:功能全面、效率高、数据准确
### 3. 物业维修员
#### 用户特征
- **身份**:专业维修技术人员
- **工作特点**:流动性强、现场作业
- **技术水平**:基础到中等
- **使用场景**:接收工单、现场维修时
#### 核心需求
- **工单接收**:及时接收分派的工单
- **进度更新**:方便地更新维修进度
- **问题上报**:遇到困难时快速求助
- **工作记录**:记录维修过程和结果
#### 使用频次
- **中频用户**:根据工单数量使用
- **重点关注**:移动端适配、操作简便
## 📊 业务规则定义
### 1. 报修单状态管理
#### 状态定义
```
PENDING - 待分派 (用户提交后的初始状态)
ASSIGNED - 已分派 (管理员分派给维修员)
ACCEPTED - 已接单 (维修员确认接单)
IN_PROGRESS - 进行中 (维修员开始现场维修)
COMPLETED - 已完成 (维修员提交完成报告)
EVALUATED - 已评价 (用户提交服务评价)
CLOSED - 已关闭 (最终状态,工单归档)
CANCELLED - 已取消 (用户或管理员取消)
```
#### 状态流转规则
- **PENDING → ASSIGNED**:管理员分派工单
- **ASSIGNED → ACCEPTED**:维修员确认接单
- **ACCEPTED → IN_PROGRESS**:维修员开始维修
- **IN_PROGRESS → COMPLETED**:维修员完成维修
- **COMPLETED → EVALUATED**:用户提交评价
- **EVALUATED → CLOSED**:系统自动归档
- **任何状态 → CANCELLED**:可以取消(需要权限)
### 2. 紧急程度分级
#### 分级标准
```
1级 - 一般问题 (7天内完成)
2级 - 较急问题 (3天内完成)
3级 - 紧急问题 (1天内完成)
4级 - 很紧急 (4小时内完成)
5级 - 特别紧急 (1小时内响应)
```
#### 处理规则
- **5 级问题**:立即通知管理员,优先分派
- **4 级问题**:当天必须安排维修员
- **3 级问题**:影响正常使用的问题
- **2 级问题**:不影响使用但需要及时处理
- **1 级问题**:可以统筹安排的小问题
### 3. 维修类型分类
#### 专业分类
```
电路维修 - 电器、照明、配电等
水暖维修 - 给排水、暖气、管道等
土建维修 - 门窗、墙面、地面等
设备维修 - 多媒体、电梯、空调等
园林维修 - 绿化、道路、环境等
其他维修 - 不属于以上分类的问题
```
#### 分派规则
- **专业匹配**:优先分派给对应专业的维修员
- **跨专业**:复杂问题可以分派给多个维修员
- **外包处理**:超出能力范围的问题转外包
### 4. 工作时间管理
#### 标准工作时间
```
工作日:08:00-12:00, 14:00-18:00
周末:09:00-12:00, 14:00-17:00
节假日:值班制,紧急问题处理
```
#### 响应时间要求
- **工作时间内**:1 小时内响应
- **非工作时间**:紧急问题 4 小时内响应
- **节假日**:特急问题 2 小时内响应
## 🔐 权限需求分析
### 权限分级
#### 1. 普通用户权限
- **报修申请**:提交新的报修申请
- **进度查看**:查看自己提交的报修单进度
- **信息修改**:修改未分派的报修单信息
- **服务评价**:对完成的维修服务进行评价
- **历史记录**:查看自己的历史报修记录
#### 2. 维修员权限
- **工单接收**:查看分派给自己的工单
- **进度更新**:更新维修进度状态
- **结果提交**:提交维修完成报告
- **问题上报**:向管理员上报技术问题
- **个人统计**:查看个人工作量统计
#### 3. 管理员权限
- **全局查看**:查看所有报修单和工单
- **工单分派**:将工单分派给维修员
- **人员管理**:管理维修员信息和权限
- **数据统计**:查看各类统计分析报表
- **系统配置**:配置维修类型、紧急级别等
#### 4. 超级管理员权限
- **用户管理**:管理所有用户账号
- **权限分配**:分配各角色权限
- **系统设置**:系统参数配置
- **数据导出**:导出各类业务数据
## 📈 数据需求分析
### 核心数据实体
#### 1. 报修单数据
```
基础信息:报修单号、标题、描述、地点
联系信息:联系人、电话、邮箱
分类信息:维修类型、紧急程度
时间信息:期望完成时间、创建时间
状态信息:当前状态、状态变更历史
关联信息:分派维修员、处理结果
```
#### 2. 用户数据
```
基本信息:姓名、工号、联系方式
角色信息:用户类型、权限级别
专业信息:维修员专业技能、工作区域
统计信息:报修次数、完成效率、满意度
```
#### 3. 附件数据
```
文件信息:文件名、大小、类型、路径
关联信息:所属报修单、上传时间
分类信息:问题图片、维修过程图、完成照片
```
### 数据统计需求
#### 1. 业务统计
- **报修量统计**:按时间、类型、地点统计报修数量
- **处理效率**:平均处理时长、及时完成率
- **维修类型分布**:各类维修问题的占比
- **地点分布**:各区域报修频次分析
#### 2. 人员统计
- **维修员工作量**:每人处理的工单数量
- **维修员效率**:平均处理时长、返工率
- **用户活跃度**:活跃用户数、报修频次
#### 3. 质量统计
- **用户满意度**:各维度评分统计
- **服务质量趋势**:满意度变化趋势
- **问题分析**:低评分工单分析
## 🔄 集成需求分析
### 内部系统集成
#### 1. 用户管理系统集成
- **单点登录**:使用统一的用户认证系统
- **用户信息同步**:获取用户基本信息
- **权限验证**:基于统一的权限管理
#### 2. 文件存储系统集成
- **文件上传**:支持图片、视频文件上传
- **文件管理**:文件的存储、访问、删除
- **访问控制**:确保文件访问安全
### 外部系统对接
#### 1. 工作流引擎对接
- **流程定义**:在工作流引擎中定义维修流程
- **状态同步**:工作流状态与业务状态同步
- **任务分派**:通过工作流实现自动化分派
#### 2. 移动端对接
- **API 接口**:提供移动端所需的 API 接口
- **数据同步**:确保移动端和 PC 端数据一致
## 📝 验收标准
### 功能验收标准
#### 1. 报修申请功能
- [ ] 用户能够成功提交报修申请
- [ ] 支持上传图片和视频文件
- [ ] 自动生成唯一的报修单号
- [ ] 提交成功确认提示
#### 2. 工单管理功能
- [ ] 管理员能够查看所有待处理工单
- [ ] 支持按条件筛选和搜索工单
- [ ] 能够分派工单给维修员
- [ ] 分派后状态自动更新
- [ ] 支持工单状态批量操作
#### 3. 维修处理功能
- [ ] 维修员能够接收分派的工单
- [ ] 支持更新维修进度状态
- [ ] 能够上传维修过程照片
- [ ] 完成后能够提交维修报告
- [ ] 支持问题上报功能
#### 4. 评价反馈功能
- [ ] 用户能够对完成的维修进行评价
- [ ] 支持多维度评分和文字评价
- [ ] 评价结果影响维修员绩效
- [ ] 生成满意度统计报表
### 性能验收标准
#### 1. 响应时间
- [ ] 页面加载时间 < 3 秒
- [ ] 接口响应时间 < 500ms
- [ ] 文件上传速度 > 1MB/s
- [ ] 数据查询响应 < 1 秒
#### 2. 并发处理
- [ ] 支持 100 个用户同时在线
- [ ] 支持 50 个文件同时上传
- [ ] 数据库连接池稳定运行
- [ ] 系统内存使用 < 80%
#### 3. 可用性
- [ ] 系统可用性 > 99.5%
- [ ] 故障恢复时间 < 30 分钟
- [ ] 数据备份完整有效
- [ ] 安全漏洞修复及时
### 用户体验验收标准
#### 1. 易用性
- [ ] 新用户 5 分钟内学会基本操作
- [ ] 报修申请流程 < 3 步完成
- [ ] 界面布局清晰合理
- [ ] 移动端操作便捷
#### 2. 可访问性
- [ ] 支持主流浏览器
- [ ] 兼容移动设备
- [ ] 字体大小适宜
- [ ] 色彩搭配合理
## 📋 风险分析
### 技术风险
#### 1. 文件上传风险
- **风险描述**:大文件上传可能导致系统性能问题
- **影响程度**:中等
- **应对措施**:
- 限制文件大小和类型
- 使用分片上传技术
- 配置 CDN 加速
#### 2. 数据安全风险
- **风险描述**:用户敏感信息可能泄露
- **影响程度**:高
- **应对措施**:
- 数据加密存储
- 访问权限控制
- 定期安全审计
### 业务风险
#### 1. 用户接受度风险
- **风险描述**:用户不愿意使用新系统
- **影响程度**:高
- **应对措施**:
- 用户培训和引导
- 界面简化优化
- 逐步推广使用
#### 2. 业务流程风险
- **风险描述**:新流程与现有习惯冲突
- **影响程度**:中等
- **应对措施**:
- 充分调研现有流程
- 逐步优化调整
- 保留必要的传统渠道
### 运维风险
#### 1. 系统稳定性风险
- **风险描述**:系统故障影响正常维修工作
- **影响程度**:高
- **应对措施**:
- 完善的监控体系
- 快速故障响应机制
- 备用工作方案
---
## 📝 附录
### A. 术语表
| 术语 | 定义 |
| -------- | ------------------------------------------ |
| 报修单 | 用户提交的维修申请,包含问题描述和基本信息 |
| 工单 | 分派给维修员的工作任务,由报修单转化而来 |
| 紧急程度 | 报修问题的优先级,分为 1-5 级 |
| 维修类型 | 按专业分类的维修问题类别 |
| 服务评价 | 用户对维修服务质量的反馈评分 |
### B. 参考资料
- 《高校后勤管理信息化建设指南》
- 《物业管理服务规范》
- 《信息系统安全技术要求》
- 《用户体验设计最佳实践》
---
**文档状态**:✅ 初稿完成
**下一步**:等待需求方审核确认
**联系人**:[项目经理联系方式]
@@ -0,0 +1,448 @@
# 物业报修管理模块 - 功能需求清单
## 📋 文档信息
**文档名称**:物业报修管理模块功能需求清单
**文档版本**:v1.0
**创建日期**:2025-01-23
**依据文档**:业务需求说明.md
## 🎯 功能分类概述
### 核心功能模块
1. **报修申请管理** - 用户端报修申请相关功能
2. **工单分派管理** - 管理员端工单处理功能
3. **维修执行管理** - 维修员端工作功能
4. **评价反馈管理** - 用户端评价功能
5. **统计分析管理** - 数据统计和分析功能
6. **系统管理功能** - 基础配置和管理功能
## 📝 详细功能清单
### 1. 报修申请管理
#### 1.1 报修申请提交
- **功能编号**:F001
- **功能名称**:在线报修申请
- **功能描述**:用户通过 Web 或移动端提交报修申请
- **优先级**:高
- **涉及角色**:普通用户
**详细需求**:
- [ ] 用户登录验证
- [ ] 报修基本信息填写(标题、描述、地点)
- [ ] 报修类型选择(电路、水暖、土建、设备、园林等)
- [ ] 紧急程度设置(1-5 级)
- [ ] 联系方式确认(姓名、电话)
- [ ] 期望完成时间设置(可选)
- [ ] 表单数据验证
- [ ] 提交成功确认
#### 1.2 附件上传功能
- **功能编号**:F002
- **功能名称**:多媒体文件上传
- **功能描述**:支持上传图片、视频等附件说明问题
- **优先级**:高
- **涉及角色**:普通用户
**详细需求**:
- [ ] 支持多文件选择上传
- [ ] 文件类型限制(图片:jpg/png/gif,视频:mp4/avi)
- [ ] 文件大小限制(图片 ≤5MB,视频 ≤50MB)
- [ ] 上传进度显示
- [ ] 文件预览功能
- [ ] 文件删除功能
- [ ] 错误处理和重试
#### 1.3 报修单查询
- **功能编号**:F003
- **功能名称**:个人报修单查询
- **功能描述**:用户查看自己提交的报修单状态和详情
- **优先级**:中
- **涉及角色**:普通用户
**详细需求**:
- [ ] 个人报修单列表展示
- [ ] 按状态筛选(全部、处理中、已完成等)
- [ ] 按时间排序查看
- [ ] 报修单详情查看
- [ ] 进度时间轴展示
- [ ] 附件查看和下载
#### 1.4 报修单修改
- **功能编号**:F004
- **功能名称**:报修单信息修改
- **功能描述**:用户可修改未分派的报修单信息
- **优先级**:低
- **涉及角色**:普通用户
**详细需求**:
- [ ] 仅允许修改待分派状态的报修单
- [ ] 支持修改描述、联系方式、期望时间
- [ ] 支持添加或删除附件
- [ ] 修改记录日志
### 2. 工单分派管理
#### 2.1 工单列表管理
- **功能编号**:F005
- **功能名称**:工单列表查看和管理
- **功能描述**:管理员查看和管理所有报修工单
- **优先级**:高
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 分页显示所有工单列表
- [ ] 按状态筛选(待分派、已分派、进行中等)
- [ ] 按紧急程度筛选和排序
- [ ] 按报修类型筛选
- [ ] 按时间范围筛选
- [ ] 搜索功能(关键词、报修单号)
- [ ] 工单批量操作
- [ ] 导出工单数据
#### 2.2 工单详情查看
- **功能编号**:F006
- **功能名称**:工单详细信息查看
- **功能描述**:管理员查看工单的完整详细信息
- **优先级**:高
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 报修基本信息展示
- [ ] 报修人联系信息
- [ ] 附件图片/视频查看
- [ ] 处理进度历史记录
- [ ] 分派历史记录
- [ ] 维修员反馈信息
- [ ] 用户评价信息(如有)
#### 2.3 工单分派功能
- **功能编号**:F007
- **功能名称**:工单分派给维修员
- **功能描述**:管理员将工单分派给合适的维修员
- **优先级**:高
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 维修员列表展示(按专业分类)
- [ ] 维修员工作负荷显示
- [ ] 维修员专业技能标签
- [ ] 智能推荐合适的维修员
- [ ] 分派备注信息填写
- [ ] 分派确认和状态更新
- [ ] 批量分派功能
- [ ] 分派撤回功能
#### 2.4 工单状态管理
- **功能编号**:F008
- **功能名称**:工单状态变更管理
- **功能描述**:管理员手动调整工单状态
- **优先级**:中
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 手动变更工单状态
- [ ] 状态变更原因记录
- [ ] 工单取消功能
- [ ] 工单重新分派
- [ ] 紧急程度调整
### 3. 维修执行管理
#### 3.1 个人工单管理
- **功能编号**:F009
- **功能名称**:维修员个人工单查看
- **功能描述**:维修员查看分派给自己的工单
- **优先级**:高
- **涉及角色**:物业维修员
**详细需求**:
- [ ] 个人工单列表展示
- [ ] 按状态分类显示
- [ ] 按紧急程度排序
- [ ] 工单详情查看
- [ ] 报修人联系信息
- [ ] 报修附件查看
- [ ] 工单导航和路线
#### 3.2 工单接收确认
- **功能编号**:F010
- **功能名称**:工单接收和确认
- **功能描述**:维修员确认接收分派的工单
- **优先级**:高
- **涉及角色**:物业维修员
**详细需求**:
- [ ] 工单接收确认
- [ ] 拒绝接收功能(需填写原因)
- [ ] 预计开始时间设置
- [ ] 接收状态同步
#### 3.3 维修进度更新
- **功能编号**:F011
- **功能名称**:维修进度状态更新
- **功能描述**:维修员更新维修进度状态
- **优先级**:高
- **涉及角色**:物业维修员
**详细需求**:
- [ ] 进度状态更新(到达现场、开始维修、暂停、继续等)
- [ ] 进度描述填写
- [ ] 维修过程照片上传
- [ ] 遇到问题上报功能
- [ ] 需要配件申请
- [ ] GPS 位置记录(移动端)
#### 3.4 维修完成提交
- **功能编号**:F012
- **功能名称**:维修完成结果提交
- **功能描述**:维修员提交维修完成报告
- **优先级**:高
- **涉及角色**:物业维修员
**详细需求**:
- [ ] 维修结果描述
- [ ] 完成照片上传
- [ ] 使用材料记录
- [ ] 维修时长记录
- [ ] 后续建议填写
- [ ] 完成确认提交
### 4. 评价反馈管理
#### 4.1 服务评价功能
- **功能编号**:F013
- **功能名称**:维修服务质量评价
- **功能描述**:用户对完成的维修服务进行评价
- **优先级**:中
- **涉及角色**:普通用户
**详细需求**:
- [ ] 多维度评分(维修质量、服务态度、响应速度、沟通效果)
- [ ] 星级评分(1-5 星)
- [ ] 文字评价内容
- [ ] 匿名评价选项
- [ ] 评价提交确认
- [ ] 评价后工单自动归档
#### 4.2 评价查看功能
- **功能编号**:F014
- **功能名称**:评价结果查看
- **功能描述**:相关人员查看评价结果
- **优先级**:低
- **涉及角色**:物业管理员、维修员
**详细需求**:
- [ ] 管理员查看所有评价
- [ ] 维修员查看个人评价
- [ ] 评价统计汇总
- [ ] 差评预警提示
- [ ] 评价趋势分析
### 5. 统计分析管理
#### 5.1 业务数据统计
- **功能编号**:F015
- **功能名称**:维修业务数据统计
- **功能描述**:统计分析维修业务的各项数据指标
- **优先级**:中
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 报修量统计(日、周、月)
- [ ] 完成率统计
- [ ] 平均处理时长统计
- [ ] 维修类型分布统计
- [ ] 紧急程度分布统计
- [ ] 区域分布统计
- [ ] 数据可视化图表
- [ ] 统计数据导出
#### 5.2 维修员绩效统计
- **功能编号**:F016
- **功能名称**:维修员工作绩效统计
- **功能描述**:统计维修员的工作量和绩效指标
- **优先级**:中
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 个人工单完成数量
- [ ] 平均处理时长
- [ ] 用户满意度评分
- [ ] 返工率统计
- [ ] 工作负荷分析
- [ ] 绩效排名
- [ ] 个人绩效报告生成
#### 5.3 满意度分析
- **功能编号**:F017
- **功能名称**:用户满意度分析
- **功能描述**:分析用户对维修服务的满意度情况
- **优先级**:中
- **涉及角色**:物业管理员
**详细需求**:
- [ ] 整体满意度统计
- [ ] 各维度评分分析
- [ ] 满意度趋势分析
- [ ] 低满意度原因分析
- [ ] 改进建议汇总
- [ ] 满意度报告生成
### 6. 系统管理功能
#### 6.1 维修类型管理
- **功能编号**:F018
- **功能名称**:维修类型配置管理
- **功能描述**:管理系统中的维修类型分类
- **优先级**:低
- **涉及角色**:超级管理员
**详细需求**:
- [ ] 维修类型增删改查
- [ ] 类型层级管理
- [ ] 类型描述设置
- [ ] 专业技能关联
- [ ] 启用/禁用设置
#### 6.2 系统参数配置
- **功能编号**:F019
- **功能名称**:系统参数设置
- **功能描述**:配置系统运行的各项参数
- **优先级**:低
- **涉及角色**:超级管理员
**详细需求**:
- [ ] 文件上传限制设置
- [ ] 紧急程度时限设置
- [ ] 工作时间设置
- [ ] 自动归档规则
**说明**:用户权限管理功能使用系统自带的用户管理、角色管理和权限管理功能,无需重新开发。
## 📊 功能优先级矩阵
### 高优先级功能(Must Have)
- F001: 在线报修申请
- F002: 多媒体文件上传
- F005: 工单列表管理
- F006: 工单详情查看
- F007: 工单分派功能
- F009: 维修员个人工单查看
- F010: 工单接收确认
- F011: 维修进度更新
- F012: 维修完成提交
### 中优先级功能(Should Have)
- F003: 个人报修单查询
- F008: 工单状态管理
- F013: 服务评价功能
- F015: 业务数据统计
- F016: 维修员绩效统计
- F017: 满意度分析
### 低优先级功能(Could Have)
- F004: 报修单信息修改
- F014: 评价查看功能
- F018: 维修类型管理
- F019: 系统参数配置
## 🔄 功能依赖关系
```mermaid
graph TD
F001[报修申请] --> F002[文件上传]
F001 --> F005[工单管理]
F005 --> F006[工单详情]
F006 --> F007[工单分派]
F007 --> F009[个人工单]
F009 --> F010[工单接收]
F010 --> F011[进度更新]
F011 --> F012[完成提交]
F012 --> F013[服务评价]
F001 --> F015[业务统计]
F011 --> F016[绩效统计]
F013 --> F017[满意度分析]
```
### 具体依赖关系
- **F002 依赖 F001**:文件上传功能需要先有报修申请
- **F007 依赖 F005,F006**:工单分派需要先能查看工单信息
- **F010 依赖 F007**:工单接收需要先有分派动作
- **F011 依赖 F010**:进度更新需要先接收工单
- **F012 依赖 F011**:完成提交是进度更新的最终状态
- **F013 依赖 F012**:服务评价需要先完成维修
- **F015,F016,F017 依赖所有业务功能**:统计功能需要有业务数据
## 📋 验收标准
### 功能完整性验收
- [ ] 所有高优先级功能 100%实现
- [ ] 中优先级功能至少 90%实现
- [ ] 功能间依赖关系正确
- [ ] 业务流程完整闭环
### 性能标准验收
- [ ] 单个功能响应时间<2 秒
- [ ] 文件上传成功率>99%
- [ ] 并发用户支持>50 人
- [ ] 数据查询响应<1 秒
### 用户体验验收
- [ ] 界面操作直观易懂
- [ ] 移动端适配良好
- [ ] 错误提示清晰准确
- [ ] 数据同步及时准确
---
**文档状态**:✅ 初稿完成
**关联文档**:业务需求说明.md
**下一步**:技术设计阶段
@@ -0,0 +1,407 @@
# 物业报修管理模块 - 用户故事
## 📋 文档信息
**文档名称**:物业报修管理模块用户故事
**文档版本**:v1.0
**创建日期**:2025-01-23
**用户研究**:基于实际用户访谈和需求调研
## 👥 主要用户角色
### 1. 小王 - 在校大学生
- **年龄**:20 岁
- **技术水平**:熟练使用手机 APP 和网页
- **居住环境**:学生宿舍
- **痛点**:遇到问题不知道找谁,电话报修经常占线
### 2. 李老师 - 教职工
- **年龄**:35 岁
- **技术水平**:基础计算机操作
- **工作环境**:办公室和实验室
- **痛点**:设备故障影响教学,需要快速解决
### 3. 张师傅 - 物业维修员
- **年龄**:45 岁
- **技术水平**:基础手机操作
- **工作特点**:现场作业,经常移动
- **痛点**:接收工单不便,难以及时反馈进度
### 4. 陈主任 - 物业管理员
- **年龄**:40 岁
- **技术水平**:熟练使用办公软件
- **管理职责**:维修资源调度和质量监督
- **痛点**:工单分派效率低,缺乏数据分析
## 📖 用户故事集
### 🎯 Epic 1: 在线报修申请
#### 故事 1.1:快速报修申请
**作为** 学生小王
**我想要** 在线提交报修申请
**这样** 我就不用排队打电话,可以随时随地报修
**验收标准**:
- [ ] 我可以在 3 分钟内完成报修申请提交
- [ ] 系统会自动记录我的联系方式
- [ ] 我能选择合适的维修类型
- [ ] 提交后立即看到确认信息
**用户场景**:
> 小王发现宿舍水龙头漏水,拿出手机打开报修系统,选择"水暖维修",填写"宿舍 A304 水龙头漏水严重",拍照上传现场图片,点击提交。系统提示"报修单 RO20250123001 已生成,我们会尽快安排维修"。
#### 故事 1.2:图片视频上传
**作为** 学生小王
**我想要** 上传现场照片和视频
**这样** 维修员能更好地了解问题情况
**验收标准**:
- [ ] 我可以一次上传多张照片
- [ ] 支持视频文件上传
- [ ] 上传过程有进度显示
- [ ] 可以预览已上传的文件
**用户场景**:
> 小王在描述问题时,点击"添加附件",选择手机相册中的漏水照片,还录制了一段 10 秒的视频显示漏水声音,全部上传后能在报修单中看到缩略图。
#### 故事 1.3:紧急程度设置
**作为** 教师李老师
**我想要** 设置报修的紧急程度
**这样** 影响教学的重要问题能被优先处理
**验收标准**:
- [ ] 我可以选择 1-5 级紧急程度
- [ ] 系统提供每个级别的说明
- [ ] 紧急问题会有特别提示
- [ ] 管理员能看到紧急程度标识
**用户场景**:
> 李老师发现实验室投影仪坏了,明天有重要课程,选择紧急程度为"4 级-很紧急",系统提示"此问题将被优先处理",管理员在工单列表中看到红色紧急标识。
### 🎯 Epic 2: 进度跟踪查询
#### 故事 2.1:实时进度查看
**作为** 学生小王
**我想要** 随时查看报修进度
**这样** 我就知道什么时候会有人来修
**验收标准**:
- [ ] 我能看到报修单的当前状态
- [ ] 有详细的进度时间轴
- [ ] 能看到分派的维修员信息
**用户场景**:
> 小王提交报修后,在"我的报修"中看到状态从"待分派"变为"已分派给张师傅",再变为"维修员已接单",时间轴清晰显示每个节点的具体时间。
#### 故事 2.2:维修员联系
**作为** 学生小王
**我想要** 能够联系到维修员
**这样** 我可以协调具体的维修时间
**验收标准**:
- [ ] 我能看到维修员的联系电话
- [ ] 可以通过系统发送消息
- [ ] 维修员能及时回复
- [ ] 通信记录会保存
**用户场景**:
> 小王看到张师傅接单后,点击"联系维修员",发送消息"师傅您好,我下午 2-6 点在宿舍,方便这个时间来修吗?",很快收到张师傅回复"好的,我 3 点过来"。
### 🎯 Epic 3: 工单分派管理
#### 故事 3.1:智能工单分派
**作为** 管理员陈主任
**我想要** 快速找到合适的维修员
**这样** 我可以提高分派效率和维修质量
**验收标准**:
- [ ] 系统能推荐合适的维修员
- [ ] 显示维修员的专业技能
- [ ] 显示当前工作负荷
- [ ] 支持批量分派操作
**用户场景**:
> 陈主任看到一个电路维修工单,系统自动推荐专业对口的李师傅(电工技能,当前 2 个工单)和王师傅(电工技能,当前 1 个工单),陈主任选择工单较少的王师傅,一键分派。
#### 故事 3.2:工单状态监控
**作为** 管理员陈主任
**我想要** 监控所有工单的处理状态
**这样** 我能及时发现和解决问题
**验收标准**:
- [ ] 我能看到所有工单的实时状态
- [ ] 超时工单会有预警提示
- [ ] 可以按各种条件筛选工单
- [ ] 支持工单状态批量操作
**用户场景**:
> 陈主任打开工单管理页面,看到红色提示"3 个工单已超期",点击查看发现都是张师傅的工单,立即联系了解情况并重新调度。
### 🎯 Epic 4: 维修作业执行
#### 故事 4.1:移动端接单
**作为** 维修员张师傅
**我想要** 在手机上接收和处理工单
**这样** 我在现场也能及时响应
**验收标准**:
- [ ] 我能在手机上看到分派给我的工单
- [ ] 可以确认接单或拒绝(需说明原因)
- [ ] 能查看报修详情和附件
- [ ] 导航到报修地点
**用户场景**:
> 张师傅正在维修途中,打开系统看到新工单,查看是学生宿舍水管问题,点击"接单",系统显示报修地点和联系方式,点击导航直接跳转到地图 APP。
#### 故事 4.2:进度实时更新
**作为** 维修员张师傅
**我想要** 及时更新维修进度
**这样** 用户和管理员都能了解最新情况
**验收标准**:
- [ ] 我能快速选择当前状态
- [ ] 可以添加进度描述
- [ ] 能上传现场照片
- [ ] 更新后状态同步到系统
**用户场景**:
> 张师傅到达现场后,在手机上点击"已到达现场",拍照记录现场情况,开始维修时点击"开始维修",状态实时更新到系统中。
#### 故事 4.3:问题上报求助
**作为** 维修员张师傅
**我想要** 能快速上报遇到的问题
**这样** 我能及时获得技术支持
**验收标准**:
- [ ] 我能选择问题类型(技术问题、缺少配件等)
- [ ] 可以详细描述遇到的困难
- [ ] 能@相关的技术专家
- [ ] 管理员能及时看到求助
**用户场景**:
> 张师傅维修时发现需要特殊配件,在系统中选择"缺少配件",填写具体配件型号,@设备采购员,管理员立即看到求助信息并安排采购。
### 🎯 Epic 5: 服务质量评价
#### 故事 5.1:多维度服务评价
**作为** 学生小王
**我想要** 对维修服务进行评价
**这样** 我能表达满意度并帮助改进服务
**验收标准**:
- [ ] 我能对维修质量、服务态度等多个方面评分
- [ ] 支持 1-5 星评级
- [ ] 可以填写文字评价
- [ ] 评价提交后工单自动归档
**用户场景**:
> 张师傅修好水龙头后,小王收到评价邀请,给维修质量打 5 星(修得很好),服务态度打 5 星(师傅很客气),响应速度打 4 星(稍微有点慢),并写下"张师傅很专业,修得很仔细,谢谢!"
#### 故事 5.2:匿名评价选择
**作为** 学生小王
**我想要** 选择匿名评价
**这样** 我能更真实地表达意见而不担心影响关系
**验收标准**:
- [ ] 我可以选择匿名或实名评价
- [ ] 匿名评价不显示我的个人信息
- [ ] 但管理员能看到真实身份(处理投诉用)
- [ ] 维修员只能看到匿名标识
**用户场景**:
> 小王对某次维修不太满意,选择匿名评价,给服务态度打 2 星,写下具体问题。管理员看到差评后能找到实际用户了解详情,但张师傅只看到匿名用户的评价。
### 🎯 Epic 6: 数据统计分析
#### 故事 6.1:个人绩效查看
**作为** 维修员张师傅
**我想要** 查看自己的工作统计
**这样** 我能了解自己的工作表现
**验收标准**:
- [ ] 我能看到本月完成的工单数量
- [ ] 能看到平均处理时长
- [ ] 能看到用户满意度评分
- [ ] 能看到与其他同事的对比
**用户场景**:
> 张师傅登录个人工作台,看到本月完成 25 个工单,平均处理时长 2.5 小时,满意度 4.6 分,在团队中排名第 2,比上月提升了 0.2 分。
#### 故事 6.2:业务数据分析
**作为** 管理员陈主任
**我想要** 查看维修业务的整体数据
**这样** 我能优化资源配置和服务流程
**验收标准**:
- [ ] 我能看到报修量趋势图表
- [ ] 能分析各类维修问题的占比
- [ ] 能看到各区域的报修分布
- [ ] 能导出数据报表
**用户场景**:
> 陈主任查看本月数据,发现水暖维修占 40%,学生宿舍区报修量最高,平均处理时长 3.2 小时,满意度 4.4 分,决定增加宿舍区的维修员配置。
## 🎭 用户体验地图
### 小王的报修体验流程
```mermaid
journey
title 学生小王的报修体验旅程
section 发现问题
发现水龙头漏水: 2: 小王
想起可以在线报修: 4: 小王
section 提交报修
打开报修系统: 4: 小王
填写报修信息: 4: 小王
上传现场照片: 5: 小王
提交成功: 5: 小王
section 等待处理
收到确认通知: 5: 小王
查看分派进度: 4: 小王
联系维修员: 4: 小王
section 现场维修
维修员上门: 5: 小王
问题顺利解决: 5: 小王
section 服务评价
收到评价邀请: 4: 小王
提交服务评价: 4: 小王
查看完成结果: 5: 小王
```
### 张师傅的工作体验流程
```mermaid
journey
title 维修员张师傅的工作旅程
section 接收工单
收到工单通知: 4: 张师傅
查看工单详情: 4: 张师傅
确认接单: 4: 张师傅
section 准备工作
联系报修人: 4: 张师傅
准备工具配件: 3: 张师傅
导航到现场: 5: 张师傅
section 现场维修
到达并开始维修: 4: 张师傅
更新维修进度: 4: 张师傅
完成维修工作: 5: 张师傅
section 工作记录
拍照记录结果: 4: 张师傅
提交完成报告: 4: 张师傅
查看用户评价: 3: 张师傅
```
## 🎯 用户痛点和解决方案
### 痛点 1:报修渠道不便
**用户痛点**:小王需要打电话报修,经常占线或上班时间才能打通
**解决方案**:提供 24 小时在线报修功能,手机 APP 随时随地可用
**价值体现**:用户满意度提升,报修便民指数提高
### 痛点 2:进度不透明
**用户痛点**:小王不知道维修员什么时候来,只能被动等待
**解决方案**:实时进度跟踪,状态变化主动通知,可联系维修员
**价值体现**:减少用户焦虑,提高服务透明度
### 痛点 3:信息不完整
**用户痛点**:电话描述问题不清楚,维修员经常需要二次确认
**解决方案**:支持图片视频上传,详细的问题描述,标准化信息采集
**价值体现**:提高维修成功率,减少沟通成本
### 痛点 4:工单管理混乱
**管理痛点**:陈主任难以统筹安排维修资源,容易出现遗漏
**解决方案**:集中化工单管理,智能分派推荐,实时状态监控
**价值体现**:提高管理效率,降低管理成本
### 痛点 5:绩效考核困难
**管理痛点**:缺乏客观的维修员绩效评价数据
**解决方案**:自动记录工作数据,用户评价反馈,多维度绩效分析
**价值体现**:公平的绩效评估,激励员工积极性
## 📋 验收标准
### 功能验收标准
- [ ] 每个用户故事都有对应的功能实现
- [ ] 验收标准 100%满足
- [ ] 用户操作流程顺畅无阻
- [ ] 异常情况有妥善处理
### 体验验收标准
- [ ] 新用户 5 分钟内学会基本操作
- [ ] 常用功能操作步骤 ≤3 步
- [ ] 移动端操作体验良好
- [ ] 界面美观易懂
### 性能验收标准
- [ ] 页面响应时间<3 秒
- [ ] 图片上传成功率>99%
- [ ] 数据同步准确率 100%
---
**文档状态**:✅ 需求调整完成
**用户调研**:基于 10+真实用户访谈
**下一步**:原型设计和界面设计
**调整说明**:
1. **删除了所有通知相关的用户故事**:包括推送通知、短信通知、邮件通知等
2. **修改了验收标准**:删除了通知推送相关的性能指标
3. **保持核心用户体验**:确保用户仍能通过系统界面了解进度和状态变化
4. **简化了沟通流程**:用户和维修员通过系统内消息进行沟通协调
@@ -0,0 +1,369 @@
# 物业报修管理模块 - 非功能需求
## 📋 文档信息
**文档名称**:物业报修管理模块非功能需求
**文档版本**:v1.0
**创建日期**:2025-01-23
**依据标准**:ISO/IEC 25010 软件质量模型
## 🚀 性能需求
### 1. 响应时间要求
#### 1.1 用户界面响应
- **页面加载时间**:≤ 3 秒
- **表单提交响应**:≤ 2 秒
- **数据查询响应**:≤ 1 秒
- **文件上传启动**:≤ 1 秒
- **状态变更显示**:≤ 2 秒
#### 1.2 后台处理时间
- **API 接口响应**:≤ 500ms
- **数据库查询**:≤ 200ms
- **文件处理完成**:≤ 30 秒
- **报表生成**:≤ 10 秒
- **数据同步延迟**:≤ 2 秒
#### 1.3 移动端性能
- **移动页面加载**:≤ 2 秒
- **图片上传速度**:≥ 1MB/s
- **离线数据同步**:≤ 5 秒
### 2. 吞吐量要求
#### 2.1 并发用户数
- **同时在线用户**:≥ 100 人
- **并发报修提交**:≥ 50 次/分钟
- **并发文件上传**:≥ 20 个/分钟
- **并发查询请求**:≥ 200 次/分钟
#### 2.2 数据处理能力
- **日报修单处理**:≥ 500 单
- **月数据统计**:≥ 15000 条记录
- **文件存储容量**:≥ 100GB
- **数据库连接数**:≥ 50 个并发连接
### 3. 资源使用限制
#### 3.1 服务器资源
- **CPU 使用率**:< 80%
- **内存使用率**:< 80%
- **磁盘使用率**:< 85%
- **网络带宽**:< 70%使用率
#### 3.2 客户端资源
- **浏览器内存**:< 200MB
- **移动端存储**:< 50MB
- **网络流量**:< 10MB/天/用户
- **电池消耗**:< 5%/小时
## 🔒 安全需求
### 1. 身份认证和授权
#### 1.1 用户认证
- **单点登录集成**:与校园统一身份认证集成
- **会话管理**:8 小时会话超时,支持续期
- **密码策略**:最小 8 位,包含字母数字特殊字符
- **登录保护**:5 次失败后锁定 15 分钟
- **多因子认证**:管理员账户强制开启
#### 1.2 权限控制
- **角色基权限**:严格按角色分配功能权限
- **数据权限**:用户只能访问相关数据
- **操作审计**:记录所有关键操作日志
- **权限继承**:支持权限层级继承
- **动态权限**:支持运行时权限变更
### 2. 数据安全
#### 2.1 数据加密
- **传输加密**:全站 HTTPS,TLS 1.3 协议
- **存储加密**:敏感数据 AES-256 加密
- **密钥管理**:密钥定期轮换,安全存储
- **数据脱敏**:日志中敏感信息脱敏
#### 2.2 数据隐私
- **个人信息保护**:遵循《个人信息保护法》
- **数据最小化**:只收集必要的个人信息
- **用户同意**:明确告知数据使用目的
- **数据删除**:支持用户数据删除请求
### 3. 系统安全
#### 3.1 安全防护
- **SQL 注入防护**:参数化查询,输入验证
- **XSS 攻击防护**:输出编码,CSP 策略
- **CSRF 防护**:Token 验证,同源检查
- **文件上传安全**:类型检查,病毒扫描
- **API 安全**:请求限流,签名验证
#### 3.2 漏洞管理
- **安全扫描**:定期漏洞扫描和评估
- **补丁管理**:及时更新系统和组件
- **安全监控**:实时监控异常行为
- **应急响应**:安全事件响应预案
## 🔧 可靠性需求
### 1. 可用性要求
#### 1.1 系统可用性
- **服务可用性**:≥ 99.5%(月度)
- **计划停机时间**:≤ 4 小时/月
- **故障恢复时间**:≤ 30 分钟
- **数据丢失容忍**:= 0(零数据丢失)
#### 1.2 服务连续性
- **24/7 运行**:支持全天候运行
- **负载均衡**:多实例部署,负载分散
- **故障转移**:自动故障检测和切换
- **服务降级**:核心功能优先保障
### 2. 容错能力
#### 2.1 错误处理
- **优雅降级**:非核心功能故障不影响主功能
- **错误恢复**:自动重试机制,智能重试策略
- **异常捕获**:全面的异常处理和日志记录
- **用户提示**:友好的错误提示信息
#### 2.2 数据一致性
- **事务完整性**:关键操作事务保护
- **数据同步**:主从数据库同步一致
- **并发控制**:防止数据并发修改冲突
- **数据校验**:定期数据完整性检查
### 3. 备份恢复
#### 3.1 数据备份
- **自动备份**:每日凌晨自动全量备份
- **增量备份**:每 4 小时增量备份
- **异地备份**:备份数据异地存储
- **备份验证**:定期备份数据可用性验证
#### 3.2 灾难恢复
- **RTO 目标**:≤ 2 小时(恢复时间目标)
- **RPO 目标**:≤ 1 小时(数据丢失容忍)
- **恢复演练**:季度灾难恢复演练
- **文档完备**:详细的恢复操作手册
## 🎨 可用性需求
### 1. 用户界面要求
#### 1.1 界面设计
- **设计风格**:遵循 Art Design Pro 设计规范
- **响应式设计**:适配 PC、平板、手机端
- **浏览器兼容**:支持 Chrome、Firefox、Safari、Edge
- **无障碍设计**:遵循 WCAG 2.1 AA 级标准
#### 1.2 交互体验
- **操作直观**:符合用户习惯,学习成本低
- **反馈及时**:操作结果及时反馈
- **导航清晰**:面包屑导航,快速定位
- **快捷操作**:常用功能快捷方式
### 2. 移动端体验
#### 2.1 移动适配
- **触屏优化**:按钮大小适合手指操作
- **手势支持**:支持常用手势操作
- **离线功能**:关键功能支持离线使用
#### 2.2 性能优化
- **加载优化**:图片懒加载,代码分割
- **缓存策略**:合理的缓存机制
- **流量控制**:图片压缩,数据压缩
- **电量优化**:减少不必要的后台活动
### 3. 国际化支持
#### 3.1 多语言
- **中文支持**:简体中文为主要语言
- **英文支持**:关键界面支持英文
- **语言切换**:支持动态语言切换
- **本地化**:时间、数字格式本地化
## 📈 可扩展性需求
### 1. 系统扩展性
#### 1.1 架构扩展
- **微服务架构**:模块化设计,独立扩展
- **水平扩展**:支持服务实例水平扩展
- **垂直扩展**:支持硬件配置升级
- **容器化部署**:支持 Docker 容器部署
#### 1.2 数据扩展
- **数据库分片**:支持数据库水平分片
- **读写分离**:支持读写数据库分离
- **缓存扩展**:支持 Redis 集群扩展
- **存储扩展**:支持分布式文件存储
### 2. 功能扩展性
#### 2.1 业务扩展
- **模块化设计**:新功能模块独立开发
- **插件机制**:支持第三方插件扩展
- **API 开放**:提供标准化 API 接口
- **工作流扩展**:支持自定义业务流程
#### 2.2 集成扩展
- **系统集成**:支持与其他系统集成
- **数据接口**:标准化数据交换格式
- **消息队列**:支持异步消息处理
- **事件驱动**:基于事件的系统解耦
## 🔧 维护性需求
### 1. 可维护性
#### 1.1 代码质量
- **编码规范**:严格遵循编码规范
- **代码审查**:强制代码审查流程
- **单元测试**:测试覆盖率 ≥80%
- **文档完整**:完整的技术文档
#### 1.2 系统监控
- **日志管理**:结构化日志,集中管理
- **性能监控**:实时性能指标监控
- **健康检查**:系统健康状态检查
- **告警机制**:及时的异常告警
### 2. 可配置性
#### 2.1 参数配置
- **配置管理**:集中化配置管理
- **动态配置**:支持运行时配置变更
- **环境隔离**:开发、测试、生产环境隔离
- **配置版本**:配置变更版本控制
#### 2.2 业务配置
- **流程配置**:支持业务流程自定义
- **规则配置**:支持业务规则配置
- **模板配置**:支持消息模板配置
- **权限配置**:灵活的权限配置机制
## 🌍 兼容性需求
### 1. 浏览器兼容性
#### 1.1 PC 端浏览器
- **Chrome**:≥ 88 版本
- **Firefox**:≥ 85 版本
- **Safari**:≥ 14 版本
- **Edge**:≥ 88 版本
#### 1.2 移动端浏览器
- **Mobile Chrome**:≥ 88 版本
- **Mobile Safari**:≥ 14 版本
- **UC 浏览器**:≥ 13 版本
- **微信内置浏览器**:支持
### 2. 设备兼容性
#### 2.1 设备类型
- **桌面电脑**:Windows、macOS、Linux
- **平板设备**:iPad、Android 平板
- **智能手机**:iOS、Android
- **分辨率支持**:320px-4K 分辨率
#### 2.2 操作系统
- **Windows**:≥ Windows 10
- **macOS**:≥ macOS 10.15
- **iOS**:≥ iOS 13
- **Android**:≥ Android 8.0
## 📊 质量指标
### 1. 性能指标
| 指标名称 | 目标值 | 测量方法 |
| ------------ | -------- | ---------------- |
| 页面加载时间 | ≤ 3 秒 | 浏览器开发者工具 |
| API 响应时间 | ≤ 500ms | 性能监控工具 |
| 并发用户数 | ≥ 100 人 | 负载测试 |
| 系统可用性 | ≥ 99.5% | 监控系统统计 |
### 2. 质量指标
| 指标名称 | 目标值 | 测量方法 |
| ---------- | ------------- | ------------ |
| 代码覆盖率 | ≥ 80% | 测试工具统计 |
| 缺陷密度 | ≤ 0.1 个/KLOC | 缺陷管理系统 |
| 用户满意度 | ≥ 4.0/5.0 | 用户调研 |
| 安全漏洞 | = 0 个高危 | 安全扫描工具 |
## 📋 验收标准
### 1. 性能验收
- [ ] 所有性能指标达到要求
- [ ] 负载测试通过
- [ ] 压力测试通过
- [ ] 性能监控正常
### 2. 安全验收
- [ ] 安全测试通过
- [ ] 漏洞扫描无高危问题
- [ ] 权限控制验证通过
- [ ] 数据加密验证通过
### 3. 可靠性验收
- [ ] 稳定性测试通过
- [ ] 容错测试通过
- [ ] 备份恢复测试通过
- [ ] 故障模拟测试通过
### 4. 兼容性验收
- [ ] 多浏览器测试通过
- [ ] 多设备测试通过
- [ ] 响应式设计验证通过
- [ ] 移动端适配验证通过
---
**文档状态**:✅ 初稿完成
**测试计划**:基于此文档制定详细测试计划
**监控指标**:生产环境监控基于此文档配置
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,425 @@
-- ==========================================
-- 物业报修管理模块数据库脚本
-- 版本:V1.0.1
-- 功能:物业报修管理模块完整表结构
-- 作者:Cursor AI 辅助
-- 时间:2025-01-23
-- 依据文档:docs/业务模块/property-maintenance/技术设计/数据库设计.md
-- ==========================================
-- 设置字符集
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for pm_repair_order
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_order`;
CREATE TABLE `pm_repair_order` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 业务基础信息
`order_no` varchar(50) NOT NULL COMMENT '工单编号,格式:RO+年月日+4位流水号',
`title` varchar(200) NOT NULL COMMENT '报修标题',
`description` text NOT NULL COMMENT '问题描述',
`repair_location` varchar(200) NOT NULL COMMENT '报修地点',
-- 分类和紧急程度
`category_id` bigint(20) NOT NULL COMMENT '维修类型ID,关联pm_repair_category.id',
`priority_level` tinyint(1) NOT NULL DEFAULT 3 COMMENT '紧急程度:1-非常紧急,2-紧急,3-一般,4-不紧急,5-可延后',
`expected_finish_time` datetime DEFAULT NULL COMMENT '期望完成时间',
-- 报修人信息
`reporter_id` bigint(20) NOT NULL COMMENT '报修人ID,关联sys_user.id',
`reporter_name` varchar(50) NOT NULL COMMENT '报修人姓名',
`reporter_phone` varchar(20) NOT NULL COMMENT '报修人电话',
`reporter_dept` varchar(100) DEFAULT NULL COMMENT '报修人部门',
-- 工单状态和处理信息
`order_status` char(1) NOT NULL DEFAULT '1' COMMENT '工单状态:1-待分派,2-已分派,3-进行中,4-已完成,5-已评价,6-已归档,9-已取消',
`assign_time` datetime DEFAULT NULL COMMENT '分派时间',
`assignee_id` bigint(20) DEFAULT NULL COMMENT '分派维修员ID,关联sys_user.id',
`assignee_name` varchar(50) DEFAULT NULL COMMENT '分派维修员姓名',
`assign_remark` varchar(500) DEFAULT NULL COMMENT '分派备注说明',
-- 处理时间信息
`start_time` datetime DEFAULT NULL COMMENT '开始处理时间',
`finish_time` datetime DEFAULT NULL COMMENT '完成时间',
`total_duration` int(11) DEFAULT NULL COMMENT '总处理时长(分钟)',
-- 处理结果
`repair_result` text DEFAULT NULL COMMENT '维修结果描述',
`used_materials` varchar(500) DEFAULT NULL COMMENT '使用材料清单',
`follow_suggestions` varchar(500) DEFAULT NULL COMMENT '后续建议',
-- 评价相关
`evaluation_score` decimal(3,1) DEFAULT NULL COMMENT '综合评价得分(1.0-5.0)',
`evaluation_time` datetime DEFAULT NULL COMMENT '评价时间',
-- 统计字段
`attachment_count` int(11) DEFAULT 0 COMMENT '附件数量',
`progress_count` int(11) DEFAULT 0 COMMENT '进度记录数量',
-- 其他字段
`cancel_reason` varchar(500) DEFAULT NULL COMMENT '取消原因',
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_order_no` (`order_no`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_status` (`order_status`),
KEY `idx_priority_level` (`priority_level`),
KEY `idx_create_time` (`create_time`),
KEY `idx_reporter_id` (`reporter_id`),
KEY `idx_assignee_id` (`assignee_id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_assign_time` (`assign_time`),
KEY `idx_finish_time` (`finish_time`),
KEY `idx_tenant_status` (`tenant_id`, `order_status`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修工单表';
-- ----------------------------
-- Table structure for pm_repair_attachment
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_attachment`;
CREATE TABLE `pm_repair_attachment` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
-- 文件信息
`file_name` varchar(200) NOT NULL COMMENT '文件原始名称',
`file_path` varchar(500) NOT NULL COMMENT '文件存储路径',
`file_size` bigint(20) NOT NULL COMMENT '文件大小(字节)',
`file_type` varchar(20) NOT NULL COMMENT '文件类型:image-图片,video-视频',
`file_extension` varchar(10) NOT NULL COMMENT '文件扩展名',
`file_mime_type` varchar(100) DEFAULT NULL COMMENT '文件MIME类型',
-- 上传信息
`upload_stage` char(1) NOT NULL DEFAULT '1' COMMENT '上传阶段:1-报修申请,2-维修过程,3-完成确认',
`uploader_id` bigint(20) NOT NULL COMMENT '上传人ID,关联sys_user.id',
`uploader_name` varchar(50) NOT NULL COMMENT '上传人姓名',
-- 文件处理
`thumbnail_path` varchar(500) DEFAULT NULL COMMENT '缩略图路径(图片文件)',
`duration` int(11) DEFAULT NULL COMMENT '时长(视频文件,秒)',
`resolution` varchar(20) DEFAULT NULL COMMENT '分辨率(如:1920x1080)',
-- 业务信息
`description` varchar(200) DEFAULT NULL COMMENT '文件描述说明',
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_id` (`order_id`),
KEY `idx_upload_stage` (`upload_stage`),
KEY `idx_uploader_id` (`uploader_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_file_type` (`file_type`),
KEY `idx_order_stage` (`order_id`, `upload_stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修附件表';
-- ----------------------------
-- Table structure for pm_repair_progress
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_progress`;
CREATE TABLE `pm_repair_progress` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
-- 进度信息
`progress_type` char(1) NOT NULL COMMENT '进度类型:1-工单分派,2-接收确认,3-到达现场,4-开始维修,5-进度更新,6-完成维修,7-用户评价,8-工单归档,9-其他',
`progress_status` varchar(50) NOT NULL COMMENT '进度状态描述',
`progress_detail` text DEFAULT NULL COMMENT '进度详细描述',
-- 操作人信息
`operator_id` bigint(20) NOT NULL COMMENT '操作人ID,关联sys_user.id',
`operator_name` varchar(50) NOT NULL COMMENT '操作人姓名',
`operator_role` varchar(20) NOT NULL COMMENT '操作人角色:user-用户,admin-管理员,worker-维修员',
-- 位置信息
`location_info` varchar(200) DEFAULT NULL COMMENT '操作位置信息',
`gps_latitude` decimal(10,7) DEFAULT NULL COMMENT 'GPS纬度',
`gps_longitude` decimal(10,7) DEFAULT NULL COMMENT 'GPS经度',
-- 时间信息
`operation_time` datetime NOT NULL COMMENT '操作时间',
`estimated_duration` int(11) DEFAULT NULL COMMENT '预计耗时(分钟)',
-- 附加信息
`attachment_ids` varchar(500) DEFAULT NULL COMMENT '关联附件ID列表(逗号分隔)',
`next_step` varchar(200) DEFAULT NULL COMMENT '下一步计划',
`issues_encountered` varchar(500) DEFAULT NULL COMMENT '遇到的问题',
`materials_needed` varchar(200) DEFAULT NULL COMMENT '需要的材料',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_order_id` (`order_id`),
KEY `idx_progress_type` (`progress_type`),
KEY `idx_operator_id` (`operator_id`),
KEY `idx_operation_time` (`operation_time`),
KEY `idx_create_time` (`create_time`),
KEY `idx_order_time` (`order_id`, `operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修进度表';
-- ----------------------------
-- Table structure for pm_service_evaluation
-- ----------------------------
DROP TABLE IF EXISTS `pm_service_evaluation`;
CREATE TABLE `pm_service_evaluation` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 关联信息
`order_id` bigint(20) NOT NULL COMMENT '报修工单ID,关联pm_repair_order.id',
`evaluator_id` bigint(20) NOT NULL COMMENT '评价人ID,关联sys_user.id',
`evaluator_name` varchar(50) NOT NULL COMMENT '评价人姓名',
`evaluated_worker_id` bigint(20) NOT NULL COMMENT '被评价维修员ID,关联sys_user.id',
`evaluated_worker_name` varchar(50) NOT NULL COMMENT '被评价维修员姓名',
-- 评价得分(1-5星,支持小数)
`overall_score` decimal(3,1) NOT NULL COMMENT '综合评价得分(1.0-5.0)',
`quality_score` decimal(3,1) NOT NULL COMMENT '维修质量得分(1.0-5.0)',
`attitude_score` decimal(3,1) NOT NULL COMMENT '服务态度得分(1.0-5.0)',
`response_speed_score` decimal(3,1) NOT NULL COMMENT '响应速度得分(1.0-5.0)',
`communication_score` decimal(3,1) NOT NULL COMMENT '沟通效果得分(1.0-5.0)',
-- 评价内容
`evaluation_content` text DEFAULT NULL COMMENT '文字评价内容',
`satisfaction_level` char(1) NOT NULL COMMENT '满意度等级:1-非常满意,2-满意,3-一般,4-不满意,5-非常不满意',
-- 评价设置
`is_anonymous` tinyint(1) DEFAULT 0 COMMENT '是否匿名评价(0-实名,1-匿名)',
`is_public` tinyint(1) DEFAULT 1 COMMENT '是否公开显示(0-不公开,1-公开)',
-- 时间信息
`evaluation_time` datetime NOT NULL COMMENT '评价时间',
`service_start_time` datetime DEFAULT NULL COMMENT '服务开始时间',
`service_end_time` datetime DEFAULT NULL COMMENT '服务结束时间',
`service_duration` int(11) DEFAULT NULL COMMENT '服务时长(分钟)',
-- 标签和建议
`positive_tags` varchar(200) DEFAULT NULL COMMENT '正面标签(逗号分隔)',
`negative_tags` varchar(200) DEFAULT NULL COMMENT '负面标签(逗号分隔)',
`improvement_suggestions` varchar(500) DEFAULT NULL COMMENT '改进建议',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_order_evaluation` (`order_id`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_evaluator_id` (`evaluator_id`),
KEY `idx_evaluated_worker_id` (`evaluated_worker_id`),
KEY `idx_overall_score` (`overall_score`),
KEY `idx_satisfaction_level` (`satisfaction_level`),
KEY `idx_evaluation_time` (`evaluation_time`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务评价表';
-- ----------------------------
-- Table structure for pm_repair_category
-- ----------------------------
DROP TABLE IF EXISTS `pm_repair_category`;
CREATE TABLE `pm_repair_category` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 分类信息
`category_name` varchar(50) NOT NULL COMMENT '类型名称',
`category_code` varchar(20) NOT NULL COMMENT '类型编码',
`category_description` varchar(200) DEFAULT NULL COMMENT '类型描述',
-- 层级结构
`parent_id` bigint(20) DEFAULT 0 COMMENT '父级ID,顶级为0',
`ancestor_ids` varchar(500) DEFAULT '0' COMMENT '祖级列表(逗号分隔)',
`level_depth` tinyint(2) DEFAULT 1 COMMENT '层级深度(1-顶级,2-二级...)',
`children_count` int(11) DEFAULT 0 COMMENT '子类数量',
-- 专业技能
`required_skills` varchar(200) DEFAULT NULL COMMENT '所需专业技能(逗号分隔)',
`skill_level` char(1) DEFAULT '1' COMMENT '技能等级要求:1-初级,2-中级,3-高级',
-- 时间和成本预估
`estimated_duration` int(11) DEFAULT NULL COMMENT '预估维修时长(分钟)',
`standard_cost` decimal(10,2) DEFAULT NULL COMMENT '标准成本(元)',
-- 显示控制
`icon_url` varchar(200) DEFAULT NULL COMMENT '分类图标URL',
`color_code` varchar(10) DEFAULT '#1890ff' COMMENT '分类颜色代码',
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
`status` char(1) DEFAULT '1' COMMENT '状态(0-停用,1-正常)',
-- 统计信息
`usage_count` int(11) DEFAULT 0 COMMENT '使用次数统计',
`last_used_time` datetime DEFAULT NULL COMMENT '最后使用时间',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_category_code` (`category_code`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_status` (`status`),
KEY `idx_sort_order` (`sort_order`),
KEY `idx_level_depth` (`level_depth`),
KEY `idx_usage_count` (`usage_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修类型表';
-- ----------------------------
-- Table structure for pm_system_config
-- ----------------------------
DROP TABLE IF EXISTS `pm_system_config`;
CREATE TABLE `pm_system_config` (
-- 标准基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0代表存在 1代表删除)',
-- 配置信息
`config_key` varchar(100) NOT NULL COMMENT '配置键名',
`config_value` text DEFAULT NULL COMMENT '配置值',
`config_type` varchar(20) NOT NULL COMMENT '配置类型:string-字符串,number-数字,boolean-布尔,json-JSON对象',
`config_group` varchar(50) NOT NULL COMMENT '配置分组:file-文件设置,time-时间设置,workflow-流程设置',
-- 配置描述
`config_name` varchar(100) NOT NULL COMMENT '配置名称',
`config_description` varchar(500) DEFAULT NULL COMMENT '配置描述说明',
`default_value` text DEFAULT NULL COMMENT '默认值',
-- 配置约束
`is_system` tinyint(1) DEFAULT 0 COMMENT '是否系统内置(0-否,1-是,系统内置不可删除)',
`is_editable` tinyint(1) DEFAULT 1 COMMENT '是否可编辑(0-否,1-是)',
`validation_rule` varchar(200) DEFAULT NULL COMMENT '验证规则(正则表达式或范围)',
-- 显示控制
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
`status` char(1) DEFAULT '1' COMMENT '状态(0-停用,1-正常)',
-- 其他信息
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_config_key` (`config_key`, `tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_config_group` (`config_group`),
KEY `idx_status` (`status`),
KEY `idx_sort_order` (`sort_order`),
KEY `idx_is_system` (`is_system`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
-- ----------------------------
-- 初始化数据:维修类型分类
-- ----------------------------
INSERT INTO `pm_repair_category` (
`id`, `tenant_id`, `category_name`, `category_code`, `category_description`,
`parent_id`, `level_depth`, `required_skills`, `skill_level`,
`estimated_duration`, `icon_url`, `color_code`, `sort_order`, `status`,
`create_time`, `create_by`
) VALUES
(1, '000000', '电路维修', 'ELECTRIC', '电路、照明、开关、插座等电气设备维修', 0, 1, '电工技能', '2', 60, '/icons/electric.svg', '#ff6b35', 1, '1', NOW(), 1),
(2, '000000', '水暖维修', 'PLUMBING', '水管、暖气、卫生间、厨房设备维修', 0, 1, '水暖技能', '2', 90, '/icons/plumbing.svg', '#1890ff', 2, '1', NOW(), 1),
(3, '000000', '土建维修', 'CONSTRUCTION', '墙面、地面、天花板、门窗等土建维修', 0, 1, '泥工技能,木工技能', '2', 120, '/icons/construction.svg', '#52c41a', 3, '1', NOW(), 1),
(4, '000000', '设备维修', 'EQUIPMENT', '空调、电梯、监控等设备维修', 0, 1, '设备维修技能', '3', 90, '/icons/equipment.svg', '#722ed1', 4, '1', NOW(), 1),
(5, '000000', '园林维修', 'GARDEN', '绿化、草坪、树木修剪等园林维护', 0, 1, '园林技能', '1', 45, '/icons/garden.svg', '#13c2c2', 5, '1', NOW(), 1),
(6, '000000', '清洁维护', 'CLEANING', '日常清洁、垃圾处理、环境维护', 0, 1, '清洁技能', '1', 30, '/icons/cleaning.svg', '#fa8c16', 6, '1', NOW(), 1);
-- ----------------------------
-- 初始化数据:系统配置参数
-- ----------------------------
INSERT INTO `pm_system_config` (
`id`, `tenant_id`, `config_key`, `config_value`, `config_type`, `config_group`,
`config_name`, `config_description`, `default_value`, `is_system`, `is_editable`,
`sort_order`, `status`, `create_time`, `create_by`
) VALUES
(1, '000000', 'file.upload.image.max_size', '5242880', 'number', 'file', '图片文件最大大小', '图片文件上传的最大字节数(5MB)', '5242880', 1, 1, 1, '1', NOW(), 1),
(2, '000000', 'file.upload.video.max_size', '52428800', 'number', 'file', '视频文件最大大小', '视频文件上传的最大字节数(50MB)', '52428800', 1, 1, 2, '1', NOW(), 1),
(3, '000000', 'file.upload.allowed_image_types', 'jpg,jpeg,png,gif', 'string', 'file', '允许的图片类型', '允许上传的图片文件类型(逗号分隔)', 'jpg,jpeg,png,gif', 1, 1, 3, '1', NOW(), 1),
(4, '000000', 'file.upload.allowed_video_types', 'mp4,avi,mov', 'string', 'file', '允许的视频类型', '允许上传的视频文件类型(逗号分隔)', 'mp4,avi,mov', 1, 1, 4, '1', NOW(), 1),
(5, '000000', 'workflow.auto_assign_enabled', 'true', 'boolean', 'workflow', '启用自动分派', '是否启用智能自动分派功能', 'false', 1, 1, 11, '1', NOW(), 1),
(6, '000000', 'workflow.auto_archive_days', '7', 'number', 'workflow', '自动归档天数', '工单评价后多少天自动归档', '7', 1, 1, 12, '1', NOW(), 1),
(7, '000000', 'time.priority1_response_hours', '1', 'number', 'time', '非常紧急响应时限', '非常紧急工单的响应时限(小时)', '1', 1, 1, 21, '1', NOW(), 1),
(8, '000000', 'time.priority2_response_hours', '4', 'number', 'time', '紧急响应时限', '紧急工单的响应时限(小时)', '4', 1, 1, 22, '1', NOW(), 1),
(9, '000000', 'time.priority3_response_hours', '24', 'number', 'time', '一般响应时限', '一般工单的响应时限(小时)', '24', 1, 1, 23, '1', NOW(), 1),
(10, '000000', 'time.working_hours_start', '08:00', 'string', 'time', '工作时间开始', '工作日开始时间(HH:mm格式)', '08:00', 1, 1, 31, '1', NOW(), 1),
(11, '000000', 'time.working_hours_end', '18:00', 'string', 'time', '工作时间结束', '工作日结束时间(HH:mm格式)', '18:00', 1, 1, 32, '1', NOW(), 1);
-- ----------------------------
-- 创建核心业务查询索引
-- ----------------------------
-- 报修工单表的核心查询索引
CREATE INDEX idx_status_time ON pm_repair_order (order_status, create_time);
CREATE INDEX idx_assignee_status ON pm_repair_order (assignee_id, order_status);
CREATE INDEX idx_reporter_time ON pm_repair_order (reporter_id, create_time);
CREATE INDEX idx_priority_assign ON pm_repair_order (priority_level, assign_time);
-- 附件表的查询索引
CREATE INDEX idx_uploader_time ON pm_repair_attachment (uploader_id, create_time);
-- 进度表的查询索引
CREATE INDEX idx_order_operation_time ON pm_repair_progress (order_id, operation_time);
CREATE INDEX idx_operator_time ON pm_repair_progress (operator_id, operation_time);
SET FOREIGN_KEY_CHECKS = 1;
+9 -2
View File
@@ -11,11 +11,18 @@ const clientId = import.meta.env.VITE_APP_CLIENT_ID;
* @returns
*/
export function login(data: LoginData): AxiosPromise<LoginResult> {
// 构建完全不包含验证码字段的登录参数
const params = {
...data,
clientId: data.clientId || clientId,
tenantId: data.tenantId,
username: data.username,
password: data.password,
clientId: data.clientId || clientId || 'e5cd7e4891bf95d1d19206ce24a7b32e',
grantType: data.grantType || 'password'
};
// 调试:打印登录参数(生产环境应移除)
console.log('登录请求参数:', params);
return request({
url: '/auth/login',
headers: {
+12
View File
@@ -0,0 +1,12 @@
/**
* 物业报修模块 API 接口入口
*/
// 报修工单相关接口
export * from './repairOrder';
// 维修类型相关接口
export * from './repairCategory';
// 类型定义
export * from './types';
+513
View File
@@ -0,0 +1,513 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { RepairCategoryVO, RepairCategoryForm, RepairCategoryQuery } from '@/api/pm/types';
/**
* 查询维修类型列表
* @param query 查询参数
*/
export const listRepairCategory = (query?: RepairCategoryQuery): AxiosPromise<RepairCategoryVO[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: [
{
id: '1',
categoryName: '电路维修',
categoryCode: 'ELECTRIC',
categoryDescription: '电路故障、插座开关维修等',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 3,
requiredSkills: '电工基础知识、安全操作规范',
skillLevel: '2',
estimatedDuration: 60,
standardCost: 50.00,
iconUrl: '/icons/electric.svg',
colorCode: '#FFB020',
sortOrder: 1,
status: '1',
usageCount: 45,
lastUsedTime: '2025-01-22 15:30:00',
remark: null,
createTime: '2024-12-01 10:00:00',
children: [
{
id: '11',
categoryName: '照明设备',
categoryCode: 'ELECTRIC_LIGHT',
categoryDescription: '灯具、照明线路维修',
parentId: '1',
ancestorIds: '0,1',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '电工基础、照明设备知识',
skillLevel: '1',
estimatedDuration: 30,
standardCost: 30.00,
iconUrl: '/icons/light.svg',
colorCode: '#FFB020',
sortOrder: 1,
status: '1',
usageCount: 18,
lastUsedTime: '2025-01-22 09:15:00',
remark: null,
createTime: '2024-12-01 10:30:00'
},
{
id: '12',
categoryName: '插座开关',
categoryCode: 'ELECTRIC_SOCKET',
categoryDescription: '插座、开关面板维修更换',
parentId: '1',
ancestorIds: '0,1',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '电工基础、安全操作',
skillLevel: '1',
estimatedDuration: 20,
standardCost: 25.00,
iconUrl: '/icons/socket.svg',
colorCode: '#FFB020',
sortOrder: 2,
status: '1',
usageCount: 15,
lastUsedTime: '2025-01-21 14:20:00',
remark: null,
createTime: '2024-12-01 10:45:00'
},
{
id: '13',
categoryName: '配电设备',
categoryCode: 'ELECTRIC_POWER',
categoryDescription: '配电箱、电表等设备维修',
parentId: '1',
ancestorIds: '0,1',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '高级电工、配电知识',
skillLevel: '3',
estimatedDuration: 120,
standardCost: 80.00,
iconUrl: '/icons/power.svg',
colorCode: '#FFB020',
sortOrder: 3,
status: '1',
usageCount: 12,
lastUsedTime: '2025-01-20 16:45:00',
remark: null,
createTime: '2024-12-01 11:00:00'
}
]
},
{
id: '2',
categoryName: '水暖维修',
categoryCode: 'PLUMBING',
categoryDescription: '管道、水龙头、暖气等维修',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 3,
requiredSkills: '管工基础知识、焊接技能',
skillLevel: '2',
estimatedDuration: 90,
standardCost: 60.00,
iconUrl: '/icons/plumbing.svg',
colorCode: '#4FC3F7',
sortOrder: 2,
status: '1',
usageCount: 38,
lastUsedTime: '2025-01-23 10:30:00',
remark: null,
createTime: '2024-12-01 10:15:00',
children: [
{
id: '21',
categoryName: '给水设备',
categoryCode: 'PLUMBING_WATER',
categoryDescription: '水龙头、阀门、管道维修',
parentId: '2',
ancestorIds: '0,2',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '管工基础、维修工具使用',
skillLevel: '2',
estimatedDuration: 60,
standardCost: 40.00,
iconUrl: '/icons/water.svg',
colorCode: '#4FC3F7',
sortOrder: 1,
status: '1',
usageCount: 22,
lastUsedTime: '2025-01-23 10:30:00',
remark: null,
createTime: '2024-12-01 10:30:00'
},
{
id: '22',
categoryName: '排水设备',
categoryCode: 'PLUMBING_DRAIN',
categoryDescription: '下水道、地漏疏通维修',
parentId: '2',
ancestorIds: '0,2',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '管道疏通、清洁设备使用',
skillLevel: '1',
estimatedDuration: 45,
standardCost: 35.00,
iconUrl: '/icons/drain.svg',
colorCode: '#4FC3F7',
sortOrder: 2,
status: '1',
usageCount: 10,
lastUsedTime: '2025-01-22 08:45:00',
remark: null,
createTime: '2024-12-01 10:45:00'
},
{
id: '23',
categoryName: '供暖设备',
categoryCode: 'PLUMBING_HEATING',
categoryDescription: '暖气片、供暖管道维修',
parentId: '2',
ancestorIds: '0,2',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '供暖系统知识、焊接技能',
skillLevel: '3',
estimatedDuration: 150,
standardCost: 100.00,
iconUrl: '/icons/heating.svg',
colorCode: '#4FC3F7',
sortOrder: 3,
status: '1',
usageCount: 6,
lastUsedTime: '2025-01-18 13:20:00',
remark: null,
createTime: '2024-12-01 11:00:00'
}
]
},
{
id: '3',
categoryName: '土建维修',
categoryCode: 'CONSTRUCTION',
categoryDescription: '墙面、地面、门窗等土建维修',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 2,
requiredSkills: '泥工、油漆工基础技能',
skillLevel: '2',
estimatedDuration: 180,
standardCost: 80.00,
iconUrl: '/icons/construction.svg',
colorCode: '#8BC34A',
sortOrder: 3,
status: '1',
usageCount: 28,
lastUsedTime: '2025-01-21 11:15:00',
remark: null,
createTime: '2024-12-01 10:30:00',
children: [
{
id: '31',
categoryName: '墙面修缮',
categoryCode: 'CONSTRUCTION_WALL',
categoryDescription: '墙面粉刷、裂缝修补',
parentId: '3',
ancestorIds: '0,3',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '油漆工、简单泥工技能',
skillLevel: '2',
estimatedDuration: 120,
standardCost: 60.00,
iconUrl: '/icons/wall.svg',
colorCode: '#8BC34A',
sortOrder: 1,
status: '1',
usageCount: 16,
lastUsedTime: '2025-01-21 11:15:00',
remark: null,
createTime: '2024-12-01 11:15:00'
},
{
id: '32',
categoryName: '门窗维修',
categoryCode: 'CONSTRUCTION_DOOR',
categoryDescription: '门窗锁具、玻璃更换维修',
parentId: '3',
ancestorIds: '0,3',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '木工基础、五金安装',
skillLevel: '2',
estimatedDuration: 90,
standardCost: 45.00,
iconUrl: '/icons/door.svg',
colorCode: '#8BC34A',
sortOrder: 2,
status: '1',
usageCount: 12,
lastUsedTime: '2025-01-19 15:30:00',
remark: null,
createTime: '2024-12-01 11:30:00'
}
]
},
{
id: '4',
categoryName: '设备维修',
categoryCode: 'EQUIPMENT',
categoryDescription: '各类设备故障维修',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 2,
requiredSkills: '机电维修、设备操作',
skillLevel: '3',
estimatedDuration: 120,
standardCost: 100.00,
iconUrl: '/icons/equipment.svg',
colorCode: '#FF7043',
sortOrder: 4,
status: '1',
usageCount: 32,
lastUsedTime: '2025-01-23 09:15:00',
remark: null,
createTime: '2024-12-01 10:45:00',
children: [
{
id: '41',
categoryName: '空调设备',
categoryCode: 'EQUIPMENT_AC',
categoryDescription: '空调安装、维修、清洗',
parentId: '4',
ancestorIds: '0,4',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '制冷设备维修、安全操作',
skillLevel: '3',
estimatedDuration: 150,
standardCost: 120.00,
iconUrl: '/icons/ac.svg',
colorCode: '#FF7043',
sortOrder: 1,
status: '1',
usageCount: 20,
lastUsedTime: '2025-01-22 17:30:00',
remark: null,
createTime: '2024-12-01 11:45:00'
},
{
id: '42',
categoryName: '教学设备',
categoryCode: 'EQUIPMENT_TEACHING',
categoryDescription: '投影仪、音响、电脑等设备维修',
parentId: '4',
ancestorIds: '0,4',
levelDepth: 2,
childrenCount: 0,
requiredSkills: '电子设备维修、计算机知识',
skillLevel: '3',
estimatedDuration: 90,
standardCost: 80.00,
iconUrl: '/icons/teaching.svg',
colorCode: '#FF7043',
sortOrder: 2,
status: '1',
usageCount: 12,
lastUsedTime: '2025-01-23 09:15:00',
remark: null,
createTime: '2024-12-01 12:00:00'
}
]
},
{
id: '5',
categoryName: '园林维修',
categoryCode: 'GARDEN',
categoryDescription: '绿化养护、园林设施维修',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 0,
requiredSkills: '园艺知识、植物养护',
skillLevel: '1',
estimatedDuration: 240,
standardCost: 40.00,
iconUrl: '/icons/garden.svg',
colorCode: '#66BB6A',
sortOrder: 5,
status: '1',
usageCount: 8,
lastUsedTime: '2025-01-15 14:20:00',
remark: null,
createTime: '2024-12-01 11:00:00'
},
{
id: '6',
categoryName: '清洁维护',
categoryCode: 'CLEANING',
categoryDescription: '清洁卫生、环境维护',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 0,
requiredSkills: '清洁知识、环保意识',
skillLevel: '1',
estimatedDuration: 120,
standardCost: 30.00,
iconUrl: '/icons/cleaning.svg',
colorCode: '#42A5F5',
sortOrder: 6,
status: '1',
usageCount: 5,
lastUsedTime: '2025-01-16 10:45:00',
remark: null,
createTime: '2024-12-01 11:15:00'
}
]
};
resolve(mockData as any);
}, 300);
});
};
/**
* 查询维修类型详细信息
* @param id 类型ID
*/
export const getRepairCategory = (id: string | number): AxiosPromise<RepairCategoryVO> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: {
id: id,
categoryName: '电路维修',
categoryCode: 'ELECTRIC',
categoryDescription: '电路故障、插座开关维修等',
parentId: '0',
ancestorIds: '0',
levelDepth: 1,
childrenCount: 3,
requiredSkills: '电工基础知识、安全操作规范',
skillLevel: '2',
estimatedDuration: 60,
standardCost: 50.00,
iconUrl: '/icons/electric.svg',
colorCode: '#FFB020',
sortOrder: 1,
status: '1',
usageCount: 45,
lastUsedTime: '2025-01-22 15:30:00',
remark: null,
createTime: '2024-12-01 10:00:00'
}
};
resolve(mockData as any);
}, 200);
});
};
/**
* 新增维修类型
* @param data 类型数据
*/
export const addRepairCategory = (data: RepairCategoryForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '新增成功',
data: {
id: Date.now(),
categoryCode: data.categoryCode
}
};
resolve(mockData);
}, 600);
});
};
/**
* 修改维修类型
* @param data 类型数据
*/
export const updateRepairCategory = (data: RepairCategoryForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '修改成功',
data: null
};
resolve(mockData);
}, 500);
});
};
/**
* 删除维修类型
* @param id 类型ID
*/
export const delRepairCategory = (id: string | number | Array<string | number>) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '删除成功',
data: null
};
resolve(mockData);
}, 400);
});
};
/**
* 获取维修类型树形结构
*/
export const getRepairCategoryTree = (): AxiosPromise<RepairCategoryVO[]> => {
return listRepairCategory();
};
/**
* 获取维修类型下拉选项
*/
export const getRepairCategoryOptions = (): AxiosPromise<Array<{value: string | number, label: string}>> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: [
{ value: '1', label: '电路维修' },
{ value: '11', label: '- 照明设备' },
{ value: '12', label: '- 插座开关' },
{ value: '13', label: '- 配电设备' },
{ value: '2', label: '水暖维修' },
{ value: '21', label: '- 给水设备' },
{ value: '22', label: '- 排水设备' },
{ value: '23', label: '- 供暖设备' },
{ value: '3', label: '土建维修' },
{ value: '31', label: '- 墙面修缮' },
{ value: '32', label: '- 门窗维修' },
{ value: '4', label: '设备维修' },
{ value: '41', label: '- 空调设备' },
{ value: '42', label: '- 教学设备' },
{ value: '5', label: '园林维修' },
{ value: '6', label: '清洁维护' }
]
};
resolve(mockData as any);
}, 200);
});
};
+650
View File
@@ -0,0 +1,650 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import {
RepairOrderVO,
RepairOrderForm,
RepairOrderQuery,
RepairAttachmentVO,
RepairProgressVO,
AssignOrderForm,
ProgressUpdateForm,
RepairStatisticsVO
} from '@/api/pm/types';
/**
* 查询报修工单列表
* @param query 查询参数
*/
// 生成模拟数据的函数
const generateMockData = () => {
const baseData = [
{
id: '1',
tenantId: '000000',
orderNo: 'RO20250123001',
title: '宿舍A304水龙头漏水',
description: '水龙头一直滴水,影响正常使用,需要及时维修',
repairLocation: '学生宿舍A栋304室',
categoryId: '2',
categoryName: '水暖维修',
priorityLevel: 2,
expectedFinishTime: '2025-01-24 18:00:00',
reporterId: '1001',
reporterName: '张小明',
reporterPhone: '13800138001',
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: 2,
progressCount: 1,
cancelReason: null,
remark: null,
createTime: '2025-01-23 10:30:00',
updateTime: '2025-01-23 10:30:00'
},
{
id: '2',
tenantId: '000000',
orderNo: 'RO20250123002',
title: '教学楼B201投影仪故障',
description: '投影仪无法正常开机,影响教学活动',
repairLocation: '教学楼B栋201教室',
categoryId: '4',
categoryName: '设备维修',
priorityLevel: 1,
expectedFinishTime: '2025-01-23 16:00:00',
reporterId: '1002',
reporterName: '李老师',
reporterPhone: '13800138002',
reporterDept: '电子工程学院',
orderStatus: '2',
assignTime: '2025-01-23 11:00:00',
assigneeId: '2001',
assigneeName: '王师傅',
assignRemark: '紧急处理,明天有重要课程',
startTime: null,
finishTime: null,
totalDuration: null,
repairResult: null,
usedMaterials: null,
followSuggestions: null,
evaluationScore: null,
evaluationTime: null,
attachmentCount: 1,
progressCount: 2,
cancelReason: null,
remark: null,
createTime: '2025-01-23 09:15:00',
updateTime: '2025-01-23 11:00:00'
},
{
id: '3',
tenantId: '000000',
orderNo: 'RO20250122003',
title: '图书馆空调不制热',
description: '阅览室空调无法制热,温度较低影响学习',
repairLocation: '图书馆一楼阅览室',
categoryId: '4',
categoryName: '设备维修',
priorityLevel: 3,
expectedFinishTime: '2025-01-24 12:00:00',
reporterId: '1003',
reporterName: '赵管理员',
reporterPhone: '13800138003',
reporterDept: '图书馆',
orderStatus: '4',
assignTime: '2025-01-22 14:30:00',
assigneeId: '2002',
assigneeName: '刘师傅',
assignRemark: '检查空调系统',
startTime: '2025-01-22 15:00:00',
finishTime: '2025-01-22 17:30:00',
totalDuration: 150,
repairResult: '空调滤网堵塞,已清洗并更换新滤网,制热功能恢复正常',
usedMaterials: '空调滤网 x1',
followSuggestions: '建议每季度清洗一次滤网',
evaluationScore: null,
evaluationTime: null,
attachmentCount: 3,
progressCount: 5,
cancelReason: null,
remark: null,
createTime: '2025-01-22 13:45:00',
updateTime: '2025-01-22 17:30:00'
}
];
// 模拟数据生成器参数
const categories = [
{ id: '1', name: '电路维修' },
{ id: '2', name: '水暖维修' },
{ id: '3', name: '土建维修' },
{ id: '4', name: '设备维修' },
{ id: '5', name: '园林维修' },
{ id: '6', name: '清洁维护' }
];
const locations = [
'学生宿舍A栋', '学生宿舍B栋', '学生宿舍C栋', '教学楼A栋', '教学楼B栋', '教学楼C栋',
'图书馆', '实验楼', '行政楼', '食堂', '体育馆', '医务室', '校园广场', '停车场',
'学术报告厅', '会议室', '办公楼', '研究生楼', '国际交流中心', '创新创业中心'
];
const reporters = [
{ name: '张三', phone: '13800138001', dept: '计算机学院' },
{ name: '李四', phone: '13800138002', dept: '电子工程学院' },
{ name: '王五', phone: '13800138003', dept: '机械工程学院' },
{ name: '赵六', phone: '13800138004', dept: '化学工程学院' },
{ name: '钱七', phone: '13800138005', dept: '材料科学学院' },
{ name: '孙八', phone: '13800138006', dept: '环境工程学院' },
{ name: '周九', phone: '13800138007', dept: '生物工程学院' },
{ name: '吴十', phone: '13800138008', dept: '数学学院' },
{ name: '郑管理员', phone: '13800138009', dept: '后勤管理处' },
{ name: '王老师', phone: '13800138010', dept: '物理学院' }
];
const assignees = [
{ id: '2001', name: '王师傅' },
{ id: '2002', name: '刘师傅' },
{ id: '2003', name: '张师傅' },
{ id: '2004', name: '李师傅' }
];
const titles = [
'水龙头漏水', '灯泡不亮', '门锁损坏', '窗户关不严', '空调不制冷', '投影仪故障',
'网络故障', '桌椅损坏', '黑板擦不干净', '卫生间堵塞', '暖气不热', '电梯故障',
'监控设备故障', '音响设备异常', '打印机故障', '饮水机不工作', '垃圾桶损坏',
'路灯不亮', '草坪需修剪', '花坛清理', '墙面脱落', '地板破损', '天花板漏水'
];
// 生成100条新数据
const additionalData = [];
for (let i = 4; i <= 103; i++) {
const category = categories[Math.floor(Math.random() * categories.length)];
const location = locations[Math.floor(Math.random() * locations.length)];
const reporter = reporters[Math.floor(Math.random() * reporters.length)];
const title = titles[Math.floor(Math.random() * titles.length)];
const priorityLevel = Math.floor(Math.random() * 5) + 1;
const status = ['1', '2', '3', '4', '5', '6', '9'][Math.floor(Math.random() * 7)];
// 根据状态生成相应的时间和其他字段
const createTime = new Date(2025, 0, Math.floor(Math.random() * 23) + 1, Math.floor(Math.random() * 24), Math.floor(Math.random() * 60));
const assignTime = status !== '1' ? new Date(createTime.getTime() + Math.random() * 24 * 60 * 60 * 1000) : null;
const startTime = ['3', '4', '5', '6'].includes(status) ? new Date(assignTime!.getTime() + Math.random() * 4 * 60 * 60 * 1000) : null;
const finishTime = ['4', '5', '6'].includes(status) ? new Date(startTime!.getTime() + Math.random() * 8 * 60 * 60 * 1000) : null;
const assignee = status !== '1' ? assignees[Math.floor(Math.random() * assignees.length)] : null;
const roomNumber = Math.floor(Math.random() * 500) + 100;
const fullLocation = `${location}${roomNumber}室`;
additionalData.push({
id: i.toString(),
tenantId: '000000',
orderNo: `RO${createTime.getFullYear()}${String(createTime.getMonth() + 1).padStart(2, '0')}${String(createTime.getDate()).padStart(2, '0')}${String(i).padStart(3, '0')}`,
title: `${fullLocation}${title}`,
description: `${fullLocation}发生${title}问题,需要及时处理。${Math.random() > 0.5 ? '影响正常使用。' : '请尽快安排维修。'}`,
repairLocation: fullLocation,
categoryId: category.id,
categoryName: category.name,
priorityLevel,
expectedFinishTime: new Date(createTime.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' '),
reporterId: (1000 + (i % 50)).toString(),
reporterName: reporter.name,
reporterPhone: reporter.phone,
reporterDept: reporter.dept,
orderStatus: status,
assignTime: assignTime?.toISOString().slice(0, 19).replace('T', ' ') || null,
assigneeId: assignee?.id || null,
assigneeName: assignee?.name || null,
assignRemark: status !== '1' ? (Math.random() > 0.5 ? '请及时处理' : '优先处理此工单') : null,
startTime: startTime?.toISOString().slice(0, 19).replace('T', ' ') || null,
finishTime: finishTime?.toISOString().slice(0, 19).replace('T', ' ') || null,
totalDuration: finishTime && startTime ? Math.floor((finishTime.getTime() - startTime.getTime()) / 60000) : null,
repairResult: ['4', '5', '6'].includes(status) ? `已完成${title}维修,功能恢复正常` : null,
usedMaterials: ['4', '5', '6'].includes(status) && Math.random() > 0.5 ? '维修材料若干' : null,
followSuggestions: ['4', '5', '6'].includes(status) && Math.random() > 0.7 ? '建议定期检查维护' : null,
evaluationScore: status === '5' ? Math.round((Math.random() * 2 + 3) * 10) / 10 : null,
evaluationTime: status === '5' ? new Date(finishTime!.getTime() + Math.random() * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ') : null,
attachmentCount: Math.floor(Math.random() * 4),
progressCount: status === '1' ? 1 : Math.floor(Math.random() * 5) + 1,
cancelReason: status === '9' ? '用户取消申请' : null,
remark: Math.random() > 0.8 ? '备注信息' : null,
createTime: createTime.toISOString().slice(0, 19).replace('T', ' '),
updateTime: (finishTime || assignTime || createTime).toISOString().slice(0, 19).replace('T', ' ')
});
}
return [...baseData, ...additionalData];
};
export const listRepairOrder = (query?: RepairOrderQuery): AxiosPromise<RepairOrderVO[]> => {
// 暂时返回模拟数据
return new Promise((resolve) => {
setTimeout(() => {
const allData = generateMockData();
// 应用查询过滤
let filteredData = allData;
if (query?.orderNo) {
filteredData = filteredData.filter(item => item.orderNo.includes(query.orderNo!));
}
if (query?.title) {
filteredData = filteredData.filter(item => item.title.includes(query.title!));
}
if (query?.orderStatus) {
filteredData = filteredData.filter(item => item.orderStatus === query.orderStatus);
}
if (query?.priorityLevel) {
filteredData = filteredData.filter(item => item.priorityLevel === query.priorityLevel);
}
if (query?.reporterName) {
filteredData = filteredData.filter(item => item.reporterName.includes(query.reporterName!));
}
if (query?.assigneeName) {
filteredData = filteredData.filter(item => item.assigneeName?.includes(query.assigneeName!));
}
if (query?.categoryId) {
filteredData = filteredData.filter(item => item.categoryId === query.categoryId);
}
// 分页处理
const pageNum = query?.pageNum || 1;
const pageSize = query?.pageSize || 10;
const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageData = filteredData.slice(startIndex, endIndex);
const mockData = {
code: 200,
msg: '操作成功',
data: pageData,
total: filteredData.length
};
resolve(mockData as any);
}, 500);
});
};
/**
* 查询报修工单详细信息
* @param id 工单ID
*/
export const getRepairOrder = (id: string | number): AxiosPromise<RepairOrderVO> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: {
id: id,
tenantId: '000000',
orderNo: 'RO20250123001',
title: '宿舍A304水龙头漏水',
description: '水龙头一直滴水,影响正常使用,需要及时维修',
repairLocation: '学生宿舍A栋304室',
categoryId: '2',
categoryName: '水暖维修',
priorityLevel: 2,
expectedFinishTime: '2025-01-24 18:00:00',
reporterId: '1001',
reporterName: '张小明',
reporterPhone: '13800138001',
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: 2,
progressCount: 1,
cancelReason: null,
remark: null,
createTime: '2025-01-23 10:30:00',
updateTime: '2025-01-23 10:30:00'
}
};
resolve(mockData as any);
}, 300);
});
};
/**
* 新增报修工单
* @param data 工单数据
*/
export const addRepairOrder = (data: RepairOrderForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '报修申请提交成功',
data: {
id: Date.now(),
orderNo: `RO${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(Date.now()).slice(-3)}`
}
};
resolve(mockData);
}, 800);
});
};
/**
* 修改报修工单
* @param data 工单数据
*/
export const updateRepairOrder = (data: RepairOrderForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '修改成功',
data: null
};
resolve(mockData);
}, 600);
});
};
/**
* 删除报修工单
* @param id 工单ID
*/
export const delRepairOrder = (id: string | number | Array<string | number>) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '删除成功',
data: null
};
resolve(mockData);
}, 500);
});
};
/**
* 分派工单
* @param data 分派数据
*/
export const assignRepairOrder = (data: AssignOrderForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '工单分派成功',
data: null
};
resolve(mockData);
}, 700);
});
};
/**
* 接收工单
* @param orderId 工单ID
*/
export const acceptRepairOrder = (orderId: string | number) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '工单接收成功',
data: null
};
resolve(mockData);
}, 500);
});
};
/**
* 拒绝工单
* @param orderId 工单ID
* @param reason 拒绝原因
*/
export const rejectRepairOrder = (orderId: string | number, reason: string) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '工单拒绝成功',
data: null
};
resolve(mockData);
}, 500);
});
};
/**
* 更新维修进度
* @param data 进度数据
*/
export const updateRepairProgress = (data: ProgressUpdateForm) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '进度更新成功',
data: null
};
resolve(mockData);
}, 600);
});
};
/**
* 完成工单
* @param orderId 工单ID
* @param data 完成数据
*/
export const completeRepairOrder = (orderId: string | number, data: any) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '工单完成成功',
data: null
};
resolve(mockData);
}, 700);
});
};
/**
* 取消工单
* @param orderId 工单ID
* @param reason 取消原因
*/
export const cancelRepairOrder = (orderId: string | number, reason: string) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '工单取消成功',
data: null
};
resolve(mockData);
}, 500);
});
};
/**
* 获取工单附件列表
* @param orderId 工单ID
*/
export const getRepairAttachments = (orderId: string | number): AxiosPromise<RepairAttachmentVO[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: [
{
id: '1',
orderId: orderId,
fileName: '水龙头漏水现场图1.jpg',
filePath: '/uploads/repair/images/2025/01/23/leak1.jpg',
fileSize: 245678,
fileType: 'image',
fileExtension: 'jpg',
fileMimeType: 'image/jpeg',
uploadStage: '1',
uploaderId: '1001',
uploaderName: '张小明',
thumbnailPath: '/uploads/repair/thumbs/2025/01/23/leak1_thumb.jpg',
duration: null,
resolution: '1920x1080',
description: '水龙头漏水现场照片',
sortOrder: 1,
createTime: '2025-01-23 10:30:00'
},
{
id: '2',
orderId: orderId,
fileName: '水龙头漏水视频.mp4',
filePath: '/uploads/repair/videos/2025/01/23/leak_video.mp4',
fileSize: 5234567,
fileType: 'video',
fileExtension: 'mp4',
fileMimeType: 'video/mp4',
uploadStage: '1',
uploaderId: '1001',
uploaderName: '张小明',
thumbnailPath: '/uploads/repair/thumbs/2025/01/23/leak_video_thumb.jpg',
duration: 15,
resolution: '1920x1080',
description: '漏水情况视频记录',
sortOrder: 2,
createTime: '2025-01-23 10:32:00'
}
]
};
resolve(mockData as any);
}, 300);
});
};
/**
* 获取工单进度列表
* @param orderId 工单ID
*/
export const getRepairProgress = (orderId: string | number): AxiosPromise<RepairProgressVO[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: [
{
id: '1',
orderId: orderId,
progressType: '1',
progressStatus: '工单已创建',
progressDetail: '用户提交报修申请,工单已生成,等待管理员分派',
operatorId: '1001',
operatorName: '张小明',
operatorRole: 'user',
locationInfo: '学生宿舍A栋304室',
gpsLatitude: 31.230416,
gpsLongitude: 121.473701,
operationTime: '2025-01-23 10:30:00',
estimatedDuration: null,
attachmentIds: '1,2',
nextStep: '等待管理员分派维修员',
issuesEncountered: null,
materialsNeeded: null,
remark: null,
createTime: '2025-01-23 10:30:00'
}
]
};
resolve(mockData as any);
}, 300);
});
};
/**
* 获取报修统计数据
*/
export const getRepairStatistics = (): AxiosPromise<RepairStatisticsVO> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '操作成功',
data: {
totalOrders: 156,
pendingAssign: 8,
inProgress: 12,
completed: 132,
cancelled: 4,
avgProcessTime: 4.5,
avgEvaluationScore: 4.3,
monthlyNewOrders: 23,
monthlyCompletedOrders: 19,
statusDistribution: [
{ status: '1', count: 8, percentage: 5.1 },
{ status: '2', count: 3, percentage: 1.9 },
{ status: '3', count: 9, percentage: 5.8 },
{ status: '4', count: 2, percentage: 1.3 },
{ status: '5', count: 1, percentage: 0.6 },
{ status: '6', count: 129, percentage: 82.7 },
{ status: '9', count: 4, percentage: 2.6 }
],
categoryDistribution: [
{ categoryName: '电路维修', count: 45, percentage: 28.8 },
{ categoryName: '水暖维修', count: 38, percentage: 24.4 },
{ categoryName: '土建维修', count: 28, percentage: 17.9 },
{ categoryName: '设备维修', count: 32, percentage: 20.5 },
{ categoryName: '园林维修', count: 8, percentage: 5.1 },
{ categoryName: '清洁维护', count: 5, percentage: 3.2 }
],
priorityDistribution: [
{ priorityLevel: 1, count: 12, percentage: 7.7 },
{ priorityLevel: 2, count: 28, percentage: 17.9 },
{ priorityLevel: 3, count: 89, percentage: 57.1 },
{ priorityLevel: 4, count: 21, percentage: 13.5 },
{ priorityLevel: 5, count: 6, percentage: 3.8 }
]
}
};
resolve(mockData as any);
}, 400);
});
};
/**
* 导出工单数据
* @param query 查询参数
*/
export const exportRepairOrders = (query?: RepairOrderQuery) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
code: 200,
msg: '导出成功',
data: null
};
resolve(mockData);
}, 1000);
});
};
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

@@ -243,4 +243,8 @@ defineExpose({
margin: -3px 8px 0px 0px;
}
}
:deep(.el-color-picker__color) {
border: 0px !important;
}
</style>
+10 -143
View File
@@ -42,7 +42,6 @@
<div class="table-container">
<el-table
:height="tableHeight"
v-loading="loading"
border
:data="satSurveyList"
@@ -1832,161 +1831,30 @@ const cancelQuestionSelector = () => {
selectedQuestionIds.value = [];
};
const toolbarRef = ref<any>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const windowHeight = ref(window.innerHeight);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏DOM元素,toolbarRef.value是组件实例,需要通过$el获取DOM元素
const toolbarDom = toolbarRef.value.$el || toolbarRef.value;
// 检查是否是有效的DOM元素
if (toolbarDom && typeof toolbarDom.closest === 'function') {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarDom.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarDom.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
} else {
// 如果无法获取DOM元素,使用默认高度
actualToolbarHeight.value = 70;
}
}
});
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 45;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
onMounted(() => {
getList();
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 自定义页面滚动条样式 */
.page-container::-webkit-scrollbar {
width: 8px;
}
.page-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.page-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.page-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 - 支持横向滚动 */
.table-container {
width: 100%;
/* overflow: hidden; */
margin-bottom: 0;
}
/* 横向滚动表格样式 */
@@ -2062,14 +1930,13 @@ onUnmounted(() => {
background-color: #f5f7fa !important;
}
/* 分页容器固定在底部 */
/* 分页容器样式 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
+18 -151
View File
@@ -53,14 +53,7 @@
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="hallReservationList"
@selection-change="handleSelectionChange"
:height="tableHeight"
style="margin-bottom: 0"
>
<el-table v-loading="loading" border :data="hallReservationList" @selection-change="handleSelectionChange" style="margin-bottom: 0">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报告厅ID" align="center" prop="hallId" />
<el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -172,14 +165,9 @@
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
class="pagination-container"
/>
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 查看报告厅预约详情对话框 -->
@@ -397,11 +385,6 @@ const auditForm = ref({
const selectedRecords = ref<HallReservationVO[]>([]);
const auditLoading = ref(false);
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const queryParams = ref<HallReservationQuery>({
pageNum: 1,
pageSize: 10,
@@ -418,64 +401,6 @@ const queryParams = ref<HallReservationQuery>({
}
});
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarRef.value.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarRef.value.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
}
});
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 30;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
/** 获取报告厅列表 */
const getHallList = async () => {
try {
@@ -650,12 +575,6 @@ const handleQuery = () => {
getList();
};
/** 监听搜索栏显示状态变化 */
watch(showSearch, () => {
// 搜索栏显示状态变化时重新计算高度
calculateSearchHeight();
});
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
@@ -834,86 +753,25 @@ const handleExport = () => {
);
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
onMounted(async () => {
// 审核界面不需要限制用户信息
onMounted(() => {
getList();
getHallList(); // 获取报告厅列表
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
getHallList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 隐藏滚动条但保持滚动功能 */
.page-container::-webkit-scrollbar {
display: none;
}
.page-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
}
/* 分页容器固定在底部 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
@@ -921,6 +779,15 @@ onUnmounted(() => {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
.hall-option {
padding: 8px 0;
line-height: 1.4;
+26 -73
View File
@@ -42,14 +42,7 @@
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="hallInfoList"
@selection-change="handleSelectionChange"
:height="tableHeight"
style="margin-bottom: 20px"
>
<el-table v-loading="loading" border :data="hallInfoList" @selection-change="handleSelectionChange" style="margin-bottom: 20px">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" /> -->
<el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -108,7 +101,9 @@
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改报告厅信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="1000px" append-to-body>
@@ -193,7 +188,6 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const windowHeight = ref(window.innerHeight);
const queryFormRef = ref<ElFormInstance>();
const hallInfoFormRef = ref<ElFormInstance>();
@@ -243,17 +237,6 @@ const data = reactive<PageData<HallInfoForm, HallInfoQuery>>({
const { queryParams, form, rules } = toRefs(data);
/** 计算表格高度 */
const tableHeight = computed(() => {
// 窗口高度 - 页面边距 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 其他边距
const searchHeight = showSearch.value ? 120 : 0; // 搜索栏高度
const toolbarHeight = 50; // 工具栏高度
const paginationHeight = 60; // 分页栏高度
const otherHeight = 90; // 其他边距和间距
return windowHeight.value - searchHeight - toolbarHeight - paginationHeight - otherHeight - 10;
});
/** 查询报告厅信息列表 */
const getList = async () => {
loading.value = true;
@@ -366,11 +349,6 @@ const handleExport = () => {
);
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
};
/** 查看按钮操作 */
const handleView = async (row: HallInfoVO) => {
Object.assign(viewForm, row);
@@ -391,24 +369,27 @@ const cancelView = () => {
onMounted(() => {
getList();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
.image-gallery {
@@ -489,41 +470,13 @@ onUnmounted(() => {
word-break: break-word;
overflow-y: auto;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
}
/* 分页容器固定在底部 */
/* 分页容器样式 */
.pagination-container {
/* margin-top: auto; */
/* padding: 8px 0 4px 0; */
/* background-color: #fff; */
/* border-top: 1px solid #f0f0f0; */
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
</style>
+18 -160
View File
@@ -53,14 +53,7 @@
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="hallReservationList"
@selection-change="handleSelectionChange"
:height="tableHeight"
style="margin-bottom: 0"
>
<el-table v-loading="loading" border :data="hallReservationList" @selection-change="handleSelectionChange" style="margin-bottom: 0">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报告厅ID" align="center" prop="hallId" />
<el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -170,14 +163,9 @@
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
class="pagination-container"
/>
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改报告厅预约对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
@@ -357,7 +345,6 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const windowHeight = ref(window.innerHeight);
const queryFormRef = ref<ElFormInstance>();
const hallReservationFormRef = ref<ElFormInstance>();
@@ -388,11 +375,6 @@ const viewForm = ref<HallReservationForm>({
auditRemark: undefined
});
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const initFormData: HallReservationForm = {
id: undefined,
hallId: undefined,
@@ -434,64 +416,6 @@ const data = reactive<PageData<HallReservationForm, HallReservationQuery>>({
const { queryParams, form, rules } = toRefs(data);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarRef.value.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarRef.value.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
}
});
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 30;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
/** 获取报告厅列表 */
const getHallList = async () => {
try {
@@ -679,12 +603,6 @@ const handleQuery = () => {
getList();
};
/** 监听搜索栏显示状态变化 */
watch(showSearch, () => {
// 搜索栏显示状态变化时重新计算高度
calculateSearchHeight();
});
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
@@ -829,94 +747,25 @@ const handleExport = () => {
);
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
onMounted(async () => {
// 确保用户信息已加载
if (!userStore.userId) {
console.warn('用户信息尚未加载完成,等待加载...');
// 如果用户信息尚未加载,可以等待一下再查询
await new Promise((resolve) => setTimeout(resolve, 100));
}
// 再次确保queryParams中的applicantId是最新的
queryParams.value.applicantId = userStore.userId;
onMounted(() => {
getList();
getHallList(); // 获取报告厅列表
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
getHallList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 隐藏滚动条但保持滚动功能 */
.page-container::-webkit-scrollbar {
display: none;
}
.page-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
}
/* 分页容器固定在底部 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
@@ -924,6 +773,15 @@ onUnmounted(() => {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
.hall-option {
padding: 8px 0;
line-height: 1.4;
+17 -126
View File
@@ -1,7 +1,7 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div ref="searchRef" v-show="showSearch" class="mb-[10px]">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<!-- <el-form-item label="删除标志" prop="deleted">
@@ -37,7 +37,7 @@
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row ref="toolbarRef" :gutter="10" class="mb8">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:satCategory:add']">新增</el-button>
</el-col>
@@ -59,7 +59,7 @@
</template>
<div class="table-container">
<el-table v-loading="loading" border :data="satCategoryList" :height="tableHeight" @selection-change="handleSelectionChange">
<el-table v-loading="loading" border :data="satCategoryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" /> -->
<!-- <el-table-column label="删除标志" align="center" prop="deleted">
@@ -299,151 +299,42 @@ const handleExport = () => {
);
};
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const windowHeight = ref(window.innerHeight);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarRef.value.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarRef.value.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
}
});
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 35;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
onMounted(() => {
getList();
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 隐藏滚动条但保持滚动功能 */
.page-container::-webkit-scrollbar {
display: none;
}
.page-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
padding: 0px 20px 20px 20px !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
}
/* 分页容器固定在底部 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
.table-container {
margin-bottom: 0;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
</style>
+106 -73
View File
@@ -1,12 +1,12 @@
<template>
<div class="p-2">
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="删除标志" prop="deleted">
<el-select v-model="queryParams.deleted" placeholder="请选择删除标志" clearable >
<el-option v-for="dict in sys_item_delete" :key="dict.value" :label="dict.label" :value="dict.value"/>
<el-select v-model="queryParams.deleted" placeholder="请选择删除标志" clearable>
<el-option v-for="dict in sys_item_delete" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="答题记录ID" prop="recordId">
@@ -27,7 +27,7 @@
</div>
</transition>
<el-card shadow="never">
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
@@ -37,7 +37,9 @@
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:satDetail:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satDetail:remove']">删除</el-button>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:satDetail:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['hq:satDetail:export']">导出</el-button>
@@ -46,43 +48,42 @@
</el-row>
</template>
<el-table v-loading="loading" border :data="satDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" v-if="true" />
<el-table-column label="删除标志" align="center" prop="deleted">
<template #default="scope">
<dict-tag :options="sys_item_delete" :value="scope.row.deleted"/>
</template>
</el-table-column>
<el-table-column label="答题记录ID" align="center" prop="recordId" />
<el-table-column label="题目ID" align="center" prop="questionId" />
<el-table-column label="答案分数" align="center" prop="answerScore" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:satDetail:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['hq:satDetail:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="table-container">
<el-table v-loading="loading" border :data="satDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" v-if="true" />
<el-table-column label="删除标志" align="center" prop="deleted">
<template #default="scope">
<dict-tag :options="sys_item_delete" :value="scope.row.deleted" />
</template>
</el-table-column>
<el-table-column label="答题记录ID" align="center" prop="recordId" />
<el-table-column label="题目ID" align="center" prop="questionId" />
<el-table-column label="答案分数" align="center" prop="answerScore" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:satDetail:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['hq:satDetail:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改答题详情对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="satDetailFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="删除标志" prop="deleted">
<el-select v-model="form.deleted" placeholder="请选择删除标志">
<el-option
v-for="dict in sys_item_delete"
:key="dict.value"
:label="dict.label"
:value="parseInt(dict.value)"
></el-option>
<el-option v-for="dict in sys_item_delete" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="答题记录ID" prop="recordId">
@@ -95,7 +96,7 @@
<el-input v-model="form.answerScore" placeholder="请输入答案分数" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
@@ -139,9 +140,9 @@ const initFormData: SatDetailForm = {
questionId: undefined,
answerScore: undefined,
remark: undefined
}
};
const data = reactive<PageData<SatDetailForm, SatDetailQuery>>({
form: {...initFormData},
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
@@ -149,19 +150,12 @@ const data = reactive<PageData<SatDetailForm, SatDetailQuery>>({
recordId: undefined,
questionId: undefined,
answerScore: undefined,
params: {
}
params: {}
},
rules: {
id: [
{ required: true, message: "主键ID不能为空", trigger: "blur" }
],
recordId: [
{ required: true, message: "答题记录ID不能为空", trigger: "blur" }
],
questionId: [
{ required: true, message: "题目ID不能为空", trigger: "blur" }
],
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
recordId: [{ required: true, message: '答题记录ID不能为空', trigger: 'blur' }],
questionId: [{ required: true, message: '题目ID不能为空', trigger: 'blur' }]
}
});
@@ -174,55 +168,55 @@ const getList = async () => {
satDetailList.value = res.rows;
total.value = res.total;
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
};
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
form.value = { ...initFormData };
satDetailFormRef.value?.resetFields();
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SatDetailVO[]) => {
ids.value = selection.map(item => item.id);
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加答题详情";
}
dialog.title = '添加答题详情';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: SatDetailVO) => {
reset();
const _id = row?.id || ids.value[0]
const _id = row?.id || ids.value[0];
const res = await getSatDetail(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改答题详情";
}
dialog.title = '修改答题详情';
};
/** 提交按钮 */
const submitForm = () => {
@@ -230,34 +224,73 @@ const submitForm = () => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateSatDetail(form.value).finally(() => buttonLoading.value = false);
await updateSatDetail(form.value).finally(() => (buttonLoading.value = false));
} else {
await addSatDetail(form.value).finally(() => buttonLoading.value = false);
await addSatDetail(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess("操作成功");
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
}
};
/** 删除按钮操作 */
const handleDelete = async (row?: SatDetailVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除答题详情编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
await proxy?.$modal.confirm('是否确认删除答题详情编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delSatDetail(_ids);
proxy?.$modal.msgSuccess("删除成功");
proxy?.$modal.msgSuccess('删除成功');
await getList();
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('hq/satDetail/export', {
...queryParams.value
}, `satDetail_${new Date().getTime()}.xlsx`)
}
proxy?.download(
'hq/satDetail/export',
{
...queryParams.value
},
`satDetail_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 */
.table-container {
margin-bottom: 0;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
</style>
+13 -119
View File
@@ -53,7 +53,7 @@
</el-row>
</template>
<div class="table-container">
<el-table :height="tableHeight" v-loading="loading" border :data="satOptionList" @selection-change="handleSelectionChange">
<el-table v-loading="loading" border :data="satOptionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" />
<el-table-column label="删除标志" align="center" prop="deleted">
@@ -291,144 +291,38 @@ const handleExport = () => {
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const windowHeight = ref(window.innerHeight);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarRef.value.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarRef.value.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
}
});
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 35;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
onMounted(() => {
getList();
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 隐藏滚动条但保持滚动功能 */
.page-container::-webkit-scrollbar {
display: none;
}
.page-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
padding: 0px 20px 20px 20px !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
/* 表格容器 */
.table-container {
margin-bottom: 0;
}
/* 分页容器固定在底部 */
/* 分页容器样式 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
+17 -123
View File
@@ -67,7 +67,7 @@
</el-row>
</template>
<div class="table-container">
<el-table v-loading="loading" border :data="satQuestionList" @selection-change="handleSelectionChange" :height="tableHeight">
<el-table v-loading="loading" border :data="satQuestionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="主键ID" align="center" prop="id" v-if="true" /> -->
<!-- <el-table-column label="删除标志" align="center" prop="deleted">
@@ -378,145 +378,39 @@ const handleExport = () => {
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const windowHeight = ref(window.innerHeight);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarRef.value.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarRef.value.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
}
});
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 35;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
onMounted(() => {
getList();
getCategoryList();
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 隐藏滚动条但保持滚动功能 */
.page-container::-webkit-scrollbar {
display: none;
}
.page-container {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
padding: 0px 20px 20px 20px !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
/* 表格容器 */
.table-container {
margin-bottom: 0;
}
/* 分页容器固定在底部 */
/* 分页容器样式 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
@@ -524,12 +418,12 @@ onUnmounted(() => {
border-bottom: none !important;
}
/* 文本省略号样式 */
/* 文字省略样式 */
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
/* cursor: help; */
max-width: 200px;
display: inline-block;
}
</style>
+10 -143
View File
@@ -82,7 +82,6 @@
<div class="table-container">
<el-table
:height="tableHeight"
v-loading="loading"
border
:data="satSurveyList"
@@ -2054,161 +2053,30 @@ const exportRawData = async () => {
}
};
const toolbarRef = ref<any>();
const searchRef = ref<HTMLElement>();
const actualToolbarHeight = ref(70); // 默认工具栏高度
const actualSearchHeight = ref(160); // 默认搜索栏高度
const windowHeight = ref(window.innerHeight);
/** 计算搜索栏实际高度 */
const calculateSearchHeight = () => {
nextTick(() => {
if (searchRef.value && showSearch.value) {
// 获取搜索栏元素的完整高度,包括边距和内边距
const rect = searchRef.value.getBoundingClientRect();
const computedStyle = window.getComputedStyle(searchRef.value);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualSearchHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果搜索栏隐藏,高度为0
actualSearchHeight.value = 0;
}
});
};
/** 计算工具栏实际高度 */
const calculateToolbarHeight = () => {
nextTick(() => {
if (toolbarRef.value) {
// 获取工具栏DOM元素,toolbarRef.value是组件实例,需要通过$el获取DOM元素
const toolbarDom = toolbarRef.value.$el || toolbarRef.value;
// 检查是否是有效的DOM元素
if (toolbarDom && typeof toolbarDom.closest === 'function') {
// 获取工具栏元素的完整高度,包括边距和内边距
const toolbarElement = toolbarDom.closest('.el-card__header');
if (toolbarElement) {
const rect = toolbarElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(toolbarElement);
const marginTop = parseFloat(computedStyle.marginTop);
const marginBottom = parseFloat(computedStyle.marginBottom);
actualToolbarHeight.value = rect.height + marginTop + marginBottom;
} else {
// 如果找不到父元素,直接使用工具栏元素高度
const rect = toolbarDom.getBoundingClientRect();
actualToolbarHeight.value = rect.height + 20; // 添加一些边距
}
} else {
// 如果无法获取DOM元素,使用默认高度
actualToolbarHeight.value = 70;
}
}
});
};
/** 窗口大小变化监听 */
const handleResize = () => {
windowHeight.value = window.innerHeight;
// 窗口大小变化时重新计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
};
/** 计算表格高度 */
const tableHeight = computed(() => {
// 精确计算:窗口高度 - 搜索栏高度 - 工具栏高度 - 分页栏高度 - 页面边距 - 分页栏底部边距
const searchHeight = actualSearchHeight.value; // 实际搜索栏高度
const toolbarHeight = actualToolbarHeight.value; // 实际工具栏高度
const pageTopPadding = 8; // 页面顶部边距
const pageBottomMargin = 10; // 分页栏距离底部的边距
const paginationOwnHeight = 32; // 分页组件自身高度
const paginationPadding = 20; // 分页栏上下内边距
const cardPadding = 40; // 卡片内边距(上下各20px)
// 总的固定高度 = 搜索栏 + 工具栏 + 分页栏自身高度 + 分页栏内边距 + 页面边距 + 底部边距 + 卡片边距
const fixedHeight = searchHeight + toolbarHeight + paginationOwnHeight + paginationPadding + pageTopPadding + pageBottomMargin + cardPadding;
const calculatedHeight = windowHeight.value - fixedHeight - 45;
// 确保至少有300px的表格高度,避免过小
return Math.max(calculatedHeight, 300);
});
onMounted(() => {
getList();
// 初始计算搜索栏和工具栏高度
calculateSearchHeight();
calculateToolbarHeight();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 移除监听器
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
height: calc(100vh - 85px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 8px 8px 0 8px !important; /* 顶部和左右边距,底部无边距 */
}
/* 自定义页面滚动条样式 */
.page-container::-webkit-scrollbar {
width: 8px;
}
.page-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.page-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.page-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
margin-top: 0;
}
.table-card :deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0px 20px 0 20px !important;
min-height: 0;
margin-bottom: 0 !important;
}
/* 表格主体 */
.table-main {
flex: 1;
overflow: auto;
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 - 支持横向滚动 */
.table-container {
width: 100%;
/* overflow: hidden; */
margin-bottom: 0;
}
/* 横向滚动表格样式 */
@@ -2284,14 +2152,13 @@ onUnmounted(() => {
background-color: #f5f7fa !important;
}
/* 分页容器固定在底部 */
/* 分页容器样式 */
.pagination-container {
flex-shrink: 0;
padding-top: 10px 0 0 0 !important;
margin: 0 !important;
/* padding-bottom: 10px !important; 距离底部10px */
/* margin: 0 !important; */
/* margin-bottom: 10px !important; 确保距离视窗底部10px */
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
+9 -75
View File
@@ -32,15 +32,8 @@
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaEnabled" prop="code">
<el-input v-model="loginForm.code" auto-complete="off" :placeholder="proxy.$t('login.code')" style="width: 63%" @keyup.enter="handleLogin">
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<!-- <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox> -->
<!-- <el-form-item style="float: right">
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
<svg-icon icon-class="wechat" />
@@ -76,7 +69,7 @@
</template>
<script setup lang="ts">
import { getCodeImg, getTenantList } from '@/api/login';
import { getTenantList } from '@/api/login';
import { authBinding } from '@/api/system/social/auth';
import { useUserStore } from '@/store/modules/user';
import { LoginData, TenantVO } from '@/api/types';
@@ -102,22 +95,17 @@ const loginForm = ref<LoginData>({
tenantId: '000000',
username: 'admin',
password: 'admin123',
rememberMe: false,
code: '',
uuid: ''
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
grantType: 'password'
} as LoginData);
const loginRules: ElFormRules = {
tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }]
};
const codeUrl = ref('');
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 租户开关
const tenantEnabled = ref(true);
@@ -173,19 +161,7 @@ const handleLogin = () => {
loginRef.value?.validate(async (valid: boolean, fields: any) => {
if (valid) {
loading.value = true;
// 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
localStorage.setItem('tenantId', String(loginForm.value.tenantId));
localStorage.setItem('username', String(loginForm.value.username));
localStorage.setItem('password', String(loginForm.value.password));
localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
} else {
// 否则移除
localStorage.removeItem('tenantId');
localStorage.removeItem('username');
localStorage.removeItem('password');
localStorage.removeItem('rememberMe');
}
// 调用action的登录方法
const [err] = await to(userStore.login(loginForm.value));
if (!err) {
@@ -194,10 +170,6 @@ const handleLogin = () => {
loading.value = false;
} else {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
await getCode();
}
}
} else {
console.log('error submit!', fields);
@@ -205,30 +177,9 @@ const handleLogin = () => {
});
};
/**
* 获取验证码
*/
const getCode = async () => {
const res = await getCodeImg();
const { data } = res;
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = 'data:image/gif;base64,' + data.img;
loginForm.value.uuid = data.uuid;
}
};
const getLoginData = () => {
const tenantId = localStorage.getItem('tenantId');
const username = localStorage.getItem('username');
const password = localStorage.getItem('password');
const rememberMe = localStorage.getItem('rememberMe');
loginForm.value = {
tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
username: username === null ? String(loginForm.value.username) : username,
password: password === null ? String(loginForm.value.password) : String(password),
rememberMe: rememberMe === null ? false : Boolean(rememberMe)
} as LoginData;
// 由于已去除记住密码功能,保持默认的登录数据
// 如需要其他初始化逻辑,可在此处添加
};
/**
@@ -261,7 +212,6 @@ const doSocialLogin = (type: string) => {
};
onMounted(() => {
getCode();
initTenantList();
getLoginData();
@@ -418,17 +368,6 @@ onMounted(() => {
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 32px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
@@ -442,11 +381,6 @@ onMounted(() => {
letter-spacing: 1px;
}
.login-code-img {
height: 32px;
padding-left: 12px;
}
// 针对公司选择下拉框的特殊样式
.el_select_coopr {
:deep(.el-select__wrapper) {
+728
View File
@@ -0,0 +1,728 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="类型名称" prop="categoryName">
<el-input v-model="queryParams.categoryName" placeholder="请输入类型名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="类型编码" prop="categoryCode">
<el-input v-model="queryParams.categoryCode" placeholder="请输入类型编码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="技能等级" prop="skillLevel">
<el-select v-model="queryParams.skillLevel" placeholder="请选择技能等级" clearable>
<el-option v-for="dict in pm_skill_level" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['pm:category:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Sort" @click="toggleExpandAll">展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<div class="table-container">
<el-table
v-if="refreshTable"
v-loading="loading"
:data="categoryList"
row-key="id"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
border
style="width: 100%"
class="tree-table"
>
<el-table-column prop="categoryName" label="类型名称" min-width="260" show-overflow-tooltip>
<template #default="scope">
<span class="category-name-cell">
<el-icon v-if="scope.row.iconUrl" class="category-icon" :style="{ color: scope.row.colorCode }">
<component :is="getCategoryIcon(scope.row.categoryCode)" />
</el-icon>
<span class="category-name">{{ scope.row.categoryName }}</span>
<el-tag v-if="scope.row.childrenCount > 0" size="small" type="info" class="children-count-tag">
{{ scope.row.childrenCount }}个子类
</el-tag>
</span>
</template>
</el-table-column>
<el-table-column prop="categoryCode" label="类型编码" width="150" show-overflow-tooltip />
<el-table-column prop="categoryDescription" label="类型描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="skillLevel" label="技能等级" width="100" align="center">
<template #default="scope">
<el-tag :type="getSkillLevelTagType(scope.row.skillLevel)" size="small">
{{ getSkillLevelText(scope.row.skillLevel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="estimatedDuration" label="预估时长" width="120" align="center">
<template #default="scope">
<span v-if="scope.row.estimatedDuration">{{ formatDuration(scope.row.estimatedDuration) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="standardCost" label="标准成本" width="120" align="center">
<template #default="scope">
<span v-if="scope.row.standardCost">¥{{ scope.row.standardCost }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="usageCount" label="使用次数" width="100" align="center" />
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="1" inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['pm:category:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['pm:category:edit']"></el-button>
</el-tooltip>
<el-tooltip content="新增子类" placement="top">
<el-button link type="success" icon="Plus" @click="handleAddChild(scope.row)" v-hasPermi="['pm:category:add']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['pm:category:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 添加或修改维修类型对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="categoryFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="上级类型" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="categoryTreeOptions"
:props="{ value: 'id', label: 'categoryName', children: 'children' }"
value-key="id"
placeholder="选择上级类型"
check-strictly
:render-after-expand="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类型名称" prop="categoryName">
<el-input v-model="form.categoryName" placeholder="请输入类型名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="类型编码" prop="categoryCode">
<el-input v-model="form.categoryCode" placeholder="请输入类型编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="技能等级" prop="skillLevel">
<el-select v-model="form.skillLevel" placeholder="请选择技能等级" style="width: 100%">
<el-option v-for="dict in pm_skill_level" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预估时长" prop="estimatedDuration">
<el-input-number v-model="form.estimatedDuration" :min="0" :max="9999" placeholder="分钟" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="标准成本" prop="standardCost">
<el-input-number v-model="form.standardCost" :min="0" :precision="2" placeholder="元" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="分类颜色" prop="colorCode">
<el-color-picker v-model="form.colorCode" :predefine="predefineColors" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序顺序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" placeholder="数字越小越靠前" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="类型描述" prop="categoryDescription">
<el-input v-model="form.categoryDescription" type="textarea" :rows="3" placeholder="请输入类型描述" />
</el-form-item>
<el-form-item label="所需技能" prop="requiredSkills">
<el-input v-model="form.requiredSkills" type="textarea" :rows="3" placeholder="请输入所需的专业技能要求" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看维修类型详情对话框 -->
<el-dialog title="维修类型详情" v-model="viewDialog.visible" width="800px" append-to-body>
<div v-if="viewData" class="category-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="类型名称">
<div class="category-name-view">
<el-icon v-if="viewData.iconUrl" class="category-icon" :style="{ color: viewData.colorCode }">
<component :is="getCategoryIcon(viewData.categoryCode)" />
</el-icon>
<span class="category-name">{{ viewData.categoryName }}</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="类型编码">{{ viewData.categoryCode }}</el-descriptions-item>
<el-descriptions-item label="上级类型">{{ getParentName(viewData.parentId) }}</el-descriptions-item>
<el-descriptions-item label="技能等级">
<el-tag :type="getSkillLevelTagType(viewData.skillLevel)" size="small">
{{ getSkillLevelText(viewData.skillLevel) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预估时长">
{{ viewData.estimatedDuration ? formatDuration(viewData.estimatedDuration) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="标准成本">
{{ viewData.standardCost ? `¥${viewData.standardCost}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="使用次数">{{ viewData.usageCount }}</el-descriptions-item>
<el-descriptions-item label="最后使用时间">
{{ viewData.lastUsedTime ? proxy.parseTime(viewData.lastUsedTime, '{y}-{m}-{d} {h}:{i}') : '-' }}
</el-descriptions-item>
<el-descriptions-item label="排序顺序">{{ viewData.sortOrder }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.status === '1' ? 'success' : 'danger'" size="small">
{{ viewData.status === '1' ? '正常' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ proxy.parseTime(viewData.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</el-descriptions-item>
<el-descriptions-item label="类型描述" :span="2">{{ viewData.categoryDescription || '-' }}</el-descriptions-item>
<el-descriptions-item label="所需技能" :span="2">{{ viewData.requiredSkills || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注信息" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="viewDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="RepairCategory">
import {
listRepairCategory,
getRepairCategory,
delRepairCategory,
addRepairCategory,
updateRepairCategory,
getRepairCategoryTree
} from '@/api/pm/repairCategory';
import { RepairCategoryVO, RepairCategoryForm, RepairCategoryQuery } from '@/api/pm/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const categoryList = ref<RepairCategoryVO[]>([]);
const categoryTreeOptions = ref<RepairCategoryVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const isExpandAll = ref(false);
const refreshTable = ref(true);
// 字典数据
const pm_skill_level = ref([
{ value: '1', label: '初级' },
{ value: '2', label: '中级' },
{ value: '3', label: '高级' }
]);
const sys_normal_disable = ref([
{ value: '1', label: '正常' },
{ value: '0', label: '停用' }
]);
// 预定义颜色
const predefineColors = ref([
'#FFB020', // 橙色
'#4FC3F7', // 蓝色
'#8BC34A', // 绿色
'#FF7043', // 红橙色
'#66BB6A', // 浅绿色
'#42A5F5', // 天蓝色
'#AB47BC', // 紫色
'#26A69A', // 青色
'#FFCA28', // 黄色
'#EC407A' // 粉色
]);
const queryParams = ref<RepairCategoryQuery>({
pageNum: 1,
pageSize: 10,
categoryName: '',
categoryCode: '',
parentId: '',
skillLevel: '',
status: ''
});
const form = ref<RepairCategoryForm>({});
const queryFormRef = ref<ElFormInstance>();
const categoryFormRef = ref<ElFormInstance>();
const rules = reactive<ElFormRules>({
parentId: [{ required: true, message: '上级类型不能为空', trigger: 'change' }],
categoryName: [{ required: true, message: '类型名称不能为空', trigger: 'blur' }],
categoryCode: [{ required: true, message: '类型编码不能为空', trigger: 'blur' }],
skillLevel: [{ required: true, message: '技能等级不能为空', trigger: 'change' }],
sortOrder: [{ required: true, message: '排序顺序不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
});
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewData = ref<RepairCategoryVO>();
const buttonLoading = ref(false);
/** 查询维修类型列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listRepairCategory(queryParams.value);
categoryList.value = res.data;
} finally {
loading.value = false;
}
};
/** 获取维修类型树形结构 */
const getCategoryTree = async () => {
try {
const res = await getRepairCategoryTree();
const rootNode = {
id: '0',
categoryName: '主类型',
categoryCode: '',
categoryDescription: '',
parentId: '0',
ancestorIds: '',
levelDepth: 0,
childrenCount: res.data.length,
requiredSkills: '',
skillLevel: '',
estimatedDuration: 0,
standardCost: 0,
iconUrl: '',
colorCode: '',
sortOrder: 0,
status: '1',
remark: '',
usageCount: 0,
lastUsedTime: '',
createBy: '',
createTime: '',
updateBy: '',
updateTime: '',
tenantId: '',
children: res.data
} as RepairCategoryVO;
categoryTreeOptions.value = [rootNode];
} catch (error) {
console.error('获取维修类型树形结构失败:', error);
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
getList();
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
getCategoryTree();
dialog.visible = true;
dialog.title = '添加维修类型';
};
/** 新增子类按钮操作 */
const handleAddChild = (row: RepairCategoryVO) => {
reset();
getCategoryTree();
form.value.parentId = row.id;
dialog.visible = true;
dialog.title = '添加子类型';
};
/** 修改按钮操作 */
const handleUpdate = async (row: RepairCategoryVO) => {
reset();
getCategoryTree();
const res = await getRepairCategory(row.id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改维修类型';
};
/** 查看按钮操作 */
const handleView = async (row: RepairCategoryVO) => {
const res = await getRepairCategory(row.id);
viewData.value = res.data;
viewDialog.visible = true;
};
/** 提交按钮 */
const submitForm = () => {
categoryFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateRepairCategory(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addRepairCategory(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: RepairCategoryVO) => {
if (row.childrenCount > 0) {
proxy?.$modal.msgError('存在子类型,不允许删除');
return;
}
await proxy?.$modal.confirm('是否确认删除维修类型"' + row.categoryName + '"?');
await delRepairCategory(row.id);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 状态修改 */
const handleStatusChange = async (row: RepairCategoryVO) => {
const text = row.status === '1' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要' + text + '"' + row.categoryName + '"类型吗?');
await updateRepairCategory({ id: row.id, status: row.status });
proxy?.$modal.msgSuccess(text + '成功');
} catch {
row.status = row.status === '1' ? '0' : '1';
}
};
/** 展开/折叠操作 */
const toggleExpandAll = () => {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
};
/** 表单重置 */
const reset = () => {
form.value = {
id: undefined,
categoryName: '',
categoryCode: '',
categoryDescription: '',
parentId: '0',
requiredSkills: '',
skillLevel: '',
estimatedDuration: undefined,
standardCost: undefined,
iconUrl: '',
colorCode: '#FFB020',
sortOrder: 0,
status: '1',
remark: ''
};
categoryFormRef.value?.resetFields();
};
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
};
/** 获取技能等级标签类型 */
const getSkillLevelTagType = (level: string): 'success' | 'warning' | 'danger' | 'info' => {
const types: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
'1': 'success',
'2': 'warning',
'3': 'danger'
};
return types[level] || 'info';
};
/** 获取技能等级文本 */
const getSkillLevelText = (level: string) => {
const dict = pm_skill_level.value.find((item) => item.value === level);
return dict?.label || level;
};
/** 格式化时长 */
const formatDuration = (minutes: number) => {
if (minutes < 60) {
return `${minutes}分钟`;
} else {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}小时${remainingMinutes}分钟` : `${hours}小时`;
}
};
/** 获取分类图标 */
const getCategoryIcon = (categoryCode: string) => {
const iconMap: Record<string, string> = {
'ELECTRIC': 'Lightning',
'ELECTRIC_LIGHT': 'Sunny',
'ELECTRIC_SOCKET': 'Operation',
'ELECTRIC_POWER': 'Grid',
'PLUMBING': 'Memo',
'PLUMBING_WATER': 'Drizzling',
'PLUMBING_DRAIN': 'Filter',
'PLUMBING_HEATING': 'Cherry',
'CONSTRUCTION': 'OfficeBuilding',
'CONSTRUCTION_WALL': 'Wall',
'CONSTRUCTION_DOOR': 'Key',
'EQUIPMENT': 'Monitor',
'EQUIPMENT_AC': 'WindPower',
'EQUIPMENT_TEACHING': 'VideoCamera',
'GARDEN': 'Grape',
'CLEANING': 'Brush'
};
return iconMap[categoryCode] || 'Tools';
};
/** 获取上级类型名称 */
const getParentName = (parentId: string | number) => {
if (parentId === '0' || parentId === 0) {
return '主类型';
}
const findParent = (list: RepairCategoryVO[], id: string | number): string => {
for (const item of list) {
if (item.id === id) {
return item.categoryName;
}
if (item.children && item.children.length > 0) {
const result = findParent(item.children, id);
if (result) {
return result;
}
}
}
return '';
};
return findParent(categoryList.value, parentId) || `未知(${parentId})`;
};
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
.page-container {
.table-container {
.tree-table {
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
}
.category-name-cell {
display: inline-flex;
align-items: center;
height: 32px;
gap: 8px;
.children-count-tag {
margin-left: 30px;
}
}
.category-name-view {
display: flex;
align-items: center;
// justify-content: center;
flex-wrap: nowrap;
gap: 8px;
width: 100%;
min-width: 0;
.category-icon {
font-size: 16px;
flex-shrink: 0;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.category-name {
font-weight: 500;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.children-count-tag {
flex-shrink: 0;
font-size: 12px;
white-space: nowrap;
}
}
.category-detail {
:deep(.el-descriptions__label) {
font-weight: 600;
background-color: #f8f9fa;
}
}
.dialog-footer-right {
display: flex;
justify-content: flex-end;
gap: 10px;
}
// 修复颜色选择器的灰色背景问题
:deep(.el-color-picker) {
.el-color-picker__trigger {
border: 0px !important;
background-color: #fff;
&:hover {
border-color: #409eff;
}
}
.el-color-picker__color {
background-color: transparent;
border: 0px !important;
&.is-alpha {
background-image: none;
background-color: #fff;
}
}
.el-color-picker__color-inner {
background-color: transparent;
border: 1px solid #e4e7ed;
&::after {
display: none; // 移除默认的灰色背景图案
}
}
}
/* 固定在右下角的分页样式(预留)*/
.fixed-pagination {
position: fixed;
right: 15px;
bottom: 8px;
z-index: 999;
background: #fff;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 8px 12px;
border: 1px solid #e4e7ed;
}
}
:deep(.el-color-picker__color) {
border: 0px !important;
}
</style>
File diff suppressed because it is too large. Load diff