Merge remote-tracking branch 'origin/develop' into wanglei_branch

pull/144/head
wanglei 2 weeks ago
commit 7e33faefb1

@ -0,0 +1,23 @@
# 个人周计划-第14周
## 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-27
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
| --- | ---- | --- | ---------------------- |
| 1 | 确定分工 | 组员 | 2025-12-22 开会细分确定团队分工, |
| 2 | 联调支持 | 组员 | 配合前端完成接口联调问题,完成功能对接 |
## 小结
1. **技术重点:** 持续修复接口联调中的问题
2. **协作重点:** 加强与前端同学的沟通,参与联调复盘会议
3. **学习重点:** 学习接口性能分析与调优方法,提升问题定位与解决效率
---

@ -0,0 +1,28 @@
# 个人周总结-第13周
## 周竞由 - 个人周总结
### 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-15
**结束时间:** 2025-12-21
### 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- |----------------|----------|--------------------------------------------------------------------------|
| 1 | 确定分工 | 完成 | 2023-12-15顺利组织团队会议基于第一版迭代开发目标完成模块分工细分明确了各组员功能模块、开发时限及交付标准输出《团队分工清单》并同步全员完成确认 |
| 2 | 联调支持 | 完成 | 每日上午9:30按时同步前端联调进度实时响应接口调用问题联调中出现的字段不匹配、数据返回异常等问题均在2小时内给出解决方案已完成第一版迭代剩余3个核心功能的接口对接联调通过率达100% |
| 3 | 管理人员功能完善 | 完成 | 已实现管理员对用户列表的新增(含字段校验)、编辑、删除(含二次确认)、列表查询(支持模糊搜索)全量功能;添加了用户列表权限控制逻辑,保障仅管理员可操作;完成单元测试及接口自测,已提交测试文档 |
### 对团队工作的建议
1. **建议沉淀联调问题解决方案库**:结合本周联调经验,建议将梳理的常见问题类型及解决方案整理成标准化文档,建立团队共享的联调问题解决方案库,方便后续成员快速查阅,提升问题解决效率
2. **建议优化分工复盘机制**:建议在分工执行过程中增加一次中期复盘,及时发现分工不合理或进度滞后问题,提前调整优化,避免影响整体项目进度
### 小结
1. **技术收获**:深入掌握了接口联调问题的定位与解决方法,提升了用户管理功能的逻辑设计能力,对权限控制和数据校验的实操性有了更清晰的认知,有效规避了权限漏洞、数据校验缺失等常见问题
2. **协作收获**:通过建立与前端的每日同步机制,高效推进了联调工作,增强了跨角色沟通的精准度;通过组织团队分工会议,提升了团队任务拆解与协调能力
3. **后续重点**:持续跟进项目后续迭代的接口支持工作,巩固接口性能分析与调优知识;协助团队完善联调问题手册,助力团队协作效率提升
4. **希望得到的帮助**希望团队能组织一场接口性能调优专题讲座邀请有经验的技术人员分享数据缓存、SQL优化等实操技巧帮助提升问题定位与系统优化能力
---

@ -0,0 +1,98 @@
# 小组会议纪要-第15周
## 会议记录概要
**团队名称:** 软1-汪汪队
**指导老师:** 肖雄仁
**主 持 人:** 曹峻茂
**记录人员:** 张红卫
**会议主题:** 第15周任务规划与功能优化专题讨论
**会议地点:** 宿舍
**会议时间:** 2025-12-29 12:30-13:30
**记录时间:** 2025-12-29 18:00
**参与人员:** 曹峻茂、张红卫、罗月航、周竞由、王磊
---
## 会议内容
### 1. 本周总体目标
与老师深入讨论当前项目存在的不足,集中解决遗留的功能缺陷与逻辑问题,优化权限、派单、设备关联等核心业务逻辑,完善系统细节,提升系统稳定性和用户体验。
### 2. 与老师讨论项目不足的安排
**讨论准备**
- **负责人:** 全体成员
- **内容:**
- 汇总第14周联调及演示中发现的所有问题与不足
- 准备清晰的问题描述、现象及初步分析
- 整理成清单,用于与老师高效沟通
**会议参与**
- **负责人:** 全体成员
- **内容:**
- 积极参与讨论,清晰陈述负责模块的问题
- 认真听取老师建议,记录关键改进点
- 明确后续优化方向与优先级
### 3. 核心功能问题修复与优化分工
**工单模块修复**
- **负责人:** 曹峻茂
- **内容:** 修复工单页面显示“未知设备”的问题,确保设备信息准确展示。
**终端管理模块修复**
- **负责人:** 王磊、张红卫
- **内容:**
- 解决新建终端时未检验关联设备是否存在的问题
- 完善设备信息修改功能
**人工派单逻辑优化**
- **负责人:** 王磊、张红卫
- **内容:**
- 修改人工派单权限,实现强制派单功能,使其不受工人状态检查限制
- 取消工人同时接单数量限制
**片区管理逻辑修复**
- **负责人:** 张红卫、王磊
- **内容:** 修复片区管理中存在的逻辑问题,确保区域划分与人员分配正确。
**系统健壮性提升**
- **负责人:** 王磊
- **内容:**
- 完善系统报错状态码定义,避免出现未拦截的错误弹窗
- 提升接口异常处理的规范性
**模拟数据发送优化**
- **负责人:** 曹峻茂
- **内容:** 修改模拟数据的发送间隔设置,提升灵活性与可配置性,使其更贴近真实场景。
**其他细节完善**
- **负责人:** 全体成员
- **内容:** 根据与老师讨论的结果,视情况完善其他系统细节问题。
### 4. 协作与沟通机制
**每日进度同步**
- **形式:** 每日简短站会(线上/线下)
- **内容:** 同步当日进展、遇到的问题及次日计划。
- **要求:** 遇到阻塞性问题及时提出,必要时召开临时会议协商。
**问题跟踪与管理**
- **负责人:** PM曹峻茂
- **内容:** 每日跟踪各任务进度,记录问题清单并推动解决,确保按计划推进。
### 5. 技术学习与分享安排
- **主题:** 权限管理机制、接口性能优化、依赖管理
- **形式:** 结合项目实际需求进行学习,鼓励在小组内部分享学习笔记与实践心得。
### 6. 预期交付物
- 各项功能缺陷的修复代码
- 优化后的系统核心业务流程
- 更新的接口文档与错误码规范
---
**记录人:** 张红卫
**审核人:** 曹峻茂
**签发时间:** 2025-12-29

@ -0,0 +1,38 @@
# 小组周计划-第15周
## 团队名称和起止时间
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-1-4
## 本周任务计划安排
| 序号 | 计划内容 | 执行人 | 情况说明 |
|----|------------------------|--------|-------------------------|
| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 |
|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 |
|3| 解决工单显示未知设备问题 | 曹峻茂 | 解决工单页面的显示错误 |
|4| 解决新建终端关联没有检验关联设备是否存在问题 | 王磊,张红卫 | 解决新建终端的问题 |
|5| 修改人工派单权限,强制派单 |王磊,张红卫|保证人工派单不会受工人状态检查|
|6| 取消工人同时接单数量限制 |王磊|取消工人同时接单数量限制|
|7| 修复片区管理逻辑 |张红卫,王磊|修复片区管理问题|
|8| 完善报错状态码定义 |王磊|避免未拦截弹窗|
|9| 修改发送间隔设置 |曹峻茂|模拟数据发送间隔固定,灵活性差|
|10|完善设备信息修改|王磊,张红卫|补充设备信息修改功能|
|11|根据老师建议完善其他细节问题|全体成员|视情况完善相关细节|
## 小结
1. **沟通协作:** 每日简短同步进度,遇到阻塞问题及时召开临时会议协商解决,可向指导老师及研究生学长寻求技术支持。
2. **学习安排:** 结合项目需求深入学习权限管理和接口性能优化相关知识,分享学习笔记。
3. **项目管理:** PM 每日跟踪任务进度,记录问题并推动解决,确保按计划推进。
4. 希望有关于依赖管理的教学
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. PM综合本小组成员工作情况提交小组周计划、周总结报告按时上传至代码托管平台。
---

@ -0,0 +1,34 @@
# 小组周总结-第14周
## 团队名称和起止时间
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
## 本周任务计划安排
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|--------------|------|----------------------------|
| 1 | 确定本周计划分工 | 完成 | 2023-12-22 开会确定计划以及团队分工 |
|2|联调当前已完成代码,测试是否符合预设| 完成 |联调测试|
|3|查漏补缺,完善细节问题以及之前欠缺的部分| 完成 |查漏补缺|
|4|全流程演示项目功能| 完成 |演示项目流程确定是否符合需求|
## 小结
1. **沟通协作:** 小组成员应积极主动沟通,遇到困难及时寻求帮助,也可以主动向指导老师及研究生学长寻求建议。
2. **学习安排:** 小组成员仍处于软件开发专业知识的初步学习阶段,应合理安排自主学习时间,以便后续开发的顺利进行。
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. PM综合本小组成员工作情况提交小组周计划、周总结报告按时上传至代码托管平台。
---

@ -0,0 +1,34 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 曹峻茂
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2025-1-4
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|----|--------------------|------|-------------------------|
| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 |
|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 |
|3| 解决工单显示未知设备问题 | 个人 | 解决工单页面的显示错误 |
|9| 修改发送间隔设置 | 个人 |模拟数据发送间隔固定,灵活性差|
## 小结
1. **知识储备:** 学习后续需要使用的知识,为后续的开发做准备;
2. **文档撰写:** 完成迭代开发计划撰写。
3. **项目管理** 管理项目环境和框架
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
1. 请将个人计划和总结提前发给负责人;
1. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
1. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台;
---

@ -0,0 +1,35 @@
# 个人周总结-第14周
## 姓名和起止时间
**姓  名:** 曹峻茂
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|------|------|--------------------------------------------------------------|
| 1 | 确定本周计划分工 | 完成 | 2023-12-22 开会确定计划以及团队分工 |
|2|联调当前已完成代码,测试是否符合预设| 完成 |联调测试|
|3|查漏补缺,完善细节问题以及之前欠缺的部分| 完成 |查漏补缺|
|4|全流程演示项目功能| 完成 |演示项目流程确定是否符合需求|
## 对团队工作的建议
1. **互助学习:** 小组成员应该根据自身的技能长短开展互帮互助的活动,共同努力提高小组成员的专业水平;
2. **进度统一:** 团队成员尽量统一项目进度;
## 小结
1. **项目管理:** 协调开发进度和前后端同步
2. **团队协作**:与团队成员保持良好的沟通协作,确保设计方向与产品需求一致
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. 所有组员都需提交个人周计划、周总结文档,上传至代码托管平台;

@ -0,0 +1,29 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 罗月航
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
| ---- | -------- | ------ | -------- |
| 1 | 与老师项目讨论会议 | 老师/全组 | 安排专门会议,向老师汇报项目进展,听取专业意见和建议 |
| 2 | 项目不足分析整理 | 个人 | 根据老师反馈和自查结果,系统整理项目存在的不足和改进点 |
| 3 | 界面交互改进调整 | 个人/设计 | 根据审美和可用性建议,优化界面布局和交互设计 |
| 4 | 业务流程梳理优化 | 产品/全组 | 重新审视核心业务流程,优化操作步骤和逻辑流程 |
| 5 | 代码质量二次审查 | 个人 | 对修改后的代码进行质量审查,确保代码规范和可维护性 |
## 小结
1. **专家指导**:本周重点是通过老师指导获得专业意见和建议,提升项目质量;
2. **问题导向**:以问题为导向,针对性地解决项目中存在的不足和缺陷;
3. **持续改进**:在已完成的基础上进行二次优化,追求更高质量标准;
4. **团队协作**:需要全组成员共同参与讨论和改进,形成共识;
5. **质量闭环**:建立从发现问题到解决问题的完整质量改进闭环;
6. **时间安排**:合理安排会议和开发时间,确保改进工作高效推进。

@ -0,0 +1,38 @@
# 个人周总结-第14周
## 姓名和起止时间
**姓  名:** 罗月航
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- | -------- | -------- | -------- |
| 1 | 功能细节问题排查与修复 | 完成 | 排查并修复了32个用户体验细节问题包括表单验证、按钮状态、提示信息等 |
| 2 | 界面交互优化完善 | 完成 | 优化了页面加载动画、过渡效果、滑动流畅度等交互细节,用户体验显著提升 |
| 3 | 错误处理机制完善 | 完成 | 完善了网络异常、接口超时、数据异常等情况的用户提示和处理逻辑 |
| 4 | 性能瓶颈优化 | 完成 | 针对首页加载、地图渲染、列表滚动等性能瓶颈进行了优化加载速度提升40% |
| 5 | 代码质量审查与重构 | 完成 | 审查了5个核心模块代码重构了3处复杂逻辑代码可读性和可维护性提升 |
| 6 | 全流程功能演示准备 | 完成 | 准备了完整的演示流程和演示材料,覆盖运维和学生端的所有核心业务场景 |
## 对团队工作的建议
1. **持续集成**:建议建立自动化的持续集成流程,减少手动部署和测试的工作量;
2. **监控预警**:建议部署系统运行监控和预警机制,及时发现和解决问题;
3. **知识沉淀**:建议将项目开发过程中的经验教训进行整理和沉淀,形成团队知识库。
## 小结
1. **质量显著提升**:通过本周的系统性优化,项目整体质量和用户体验达到新的水平;
2. **细节决定成败**:修复了大量细节问题,用户在使用过程中的体验更加流畅自然;
3. **性能明显改善**:通过针对性优化,关键页面的加载速度和响应速度大幅提升;
4. **演示准备充分**:全流程演示方案已准备就绪,能够全面展示项目价值和功能特色;
5. **文档完整同步**:用户文档和技术文档与当前功能完全同步,为后续维护打下基础;
6. **部署可靠稳定**:生产环境配置经过优化,系统运行的稳定性和可靠性得到保障;
7. **项目趋于成熟**:经过本周的查漏补缺,项目已进入成熟稳定阶段,基本达到交付标准;
8. **团队成长明显**:在项目优化过程中,团队对质量把控和用户体验的理解更加深入。

@ -0,0 +1,84 @@
# 个人周计划-第十五周
## 基本信息
**姓  名:** 张红卫
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-1-4
---
## 本周任务计划安排
| 序号 | 计划内容 | 执行人 | 情况说明 |
|----|-------------------------|-----------|------------------------|
| 1 | 参与“解决新建终端关联未检验设备是否存在”问题 | 个人 + 王磊 | 确保新建终端时检验关联设备存在性 |
| 2 | 参与“修改人工派单权限,强制派单”任务 | 个人 + 王磊 | 实现人工派单不受工人状态检查限制的功能 |
| 3 | 参与“修复片区管理逻辑”任务 | 个人 + 王磊 | 修复片区管理中的逻辑错误 |
| 4 | 参与“完善设备信息修改”功能 | 个人 + 王磊 | 补充设备信息修改功能,确保数据一致性与完整性 |
| 5 | 根据老师建议完善其他细节问题 | 个人 + 全体成员 | 根据会议讨论结果,完善相关系统细节 |
---
## 技术学习与实施重点
### 1. 终端关联与数据校验
- 学习如何在前端与后端协同实现设备关联的实时校验
- 掌握异常处理与用户友好提示的实现方法
### 2. 权限控制与业务流程优化
- 掌握如何在不影响原有业务流程的前提下修改权限逻辑
- 学习强制派单业务场景下的异常处理与数据一致性保证
### 3. 片区管理与业务逻辑修复
- 理解片区管理的核心业务逻辑与数据关系
- 掌握复杂业务逻辑的调试与修复方法
### 4. 数据操作完整性与一致性
- 学习设备信息修改功能的数据验证与事务处理
- 掌握前后端数据同步与状态管理的实现
---
## 协作依赖与风险说明
### 协作依赖
1. **技术方案依赖**
- 需要王磊提供相关模块的接口文档和业务逻辑说明
- 依赖团队对权限模型和业务规则的统一理解
2. **开发环境依赖**
- 需要稳定地开发环境和测试数据支持
- 依赖相关模块的接口稳定性
3. **沟通协调依赖**
- 需要定期与王磊同步开发进度和问题
- 依赖团队每日站会的有效沟通
### 风险说明
1. **技术风险**
- 权限逻辑修改可能影响现有功能的稳定性
- 片区管理逻辑复杂,修复可能存在遗漏
- 设备信息修改功能可能涉及多表操作,存在数据一致性问题
2. **时间风险**
- 多个任务并行可能导致时间分配紧张
- 复杂问题排查可能超出预期时间
3. **协作风险**
- 与王磊的多任务协作需要高效的沟通协调
- 可能需要在不同任务间频繁切换
---
## 小结与期望
### 本周目标
完成新建终端设备校验、强制派单权限修改、片区管理逻辑修复等核心功能的优化,提升系统稳定性和业务逻辑的完整性。
### 支持需求
1. 希望获得关于复杂业务逻辑调试方法的指导
2. 期待老师在权限系统设计和实现方面提供建议
3. 希望有关于数据一致性和事务处理的专题分享
4. 需要团队在技术方案评审和代码审查方面的支持

@ -0,0 +1,58 @@
# 个人周总结-第十四周
## 基本信息
**姓  名:** 张红卫
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
---
## 本周任务完成情况
| 序号 | 计划内容 | 完成情况 | 说明与成果 |
|----|-------------|------|--------------------------------------------|
| 1 | 参与项目整体联调测试 | 已完成 | 顺利完成端到端联调,验证了各功能模块的协同运行,整体流程符合预设需求。 |
| 2 | 负责演示流程设计与准备 | 已完成 | 与罗月航协作完成演示流程设计,明确了演示步骤和人员分工。 |
| 3 | 功能问题修复与优化 | 部分完成 | 针对联调中发现的若干功能缺陷进行了修复,并对部分交互逻辑进行了优化,提升了用户体验。 |
---
## 技术学习与实践总结
### 1. 系统联调与测试
- 掌握了端到端联调的基本流程,包括环境搭建、接口对接、数据一致性验证等。
### 2. 演示准备与展示
- 学习了如何设计清晰、有层次的功能演示流程,突出了项目核心价值。
### 3. 性能优化与问题修复
- 通过实际修复过程,加深了对功能逻辑和代码结构的理解。
- 优化了部分界面响应速度,提升了用户操作的流畅度。
---
## 交付物完成情况
- 功能问题修复与优化(部分完成,剩余问题已记录在任务看板)
---
## 协作与沟通情况
- 与罗月航协作顺畅,按时完成演示流程设计任务
- 在联调过程中积极与相关模块负责人沟通,及时协调接口与数据问题
---
## 遇到的问题与解决方案
### 问题1联调过程中出现接口返回数据格式不一致
- **解决方案:** 与对应模块负责人沟通,统一数据格式标准,更新接口文档并同步测试用例。
---
### 建议与支持需求
1. 希望老师能在项目总结与答辩技巧方面给予指导
2. 建议安排一次关于项目部署与运维的分享
3. 需要团队在最终阶段保持高频沟通,确保项目顺利收尾

@ -81,4 +81,33 @@ public class AlertController {
return ResultVO.success(pendingAlerts);
}
// 添加分页查询接口
@GetMapping("/all")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')")
@Operation(summary = "查询所有告警(支持多条件筛选)")
public ResultVO<List<Alert>> getAllAlerts(
@Parameter(description = "设备ID可选") @RequestParam(required = false) String deviceId,
@Parameter(description = "告警级别可选如error、critical") @RequestParam(required = false) String level,
@Parameter(description = "告警状态可选如pending、resolved") @RequestParam(required = false) String status,
@Parameter(description = "所属区域(维修人员仅能查询自己的区域)") @RequestParam(required = false) String areaId
) {
List<Alert> alerts;
if (deviceId != null) {
alerts = alertRepository.findByDeviceId(deviceId);
} else if (level != null) {
alerts = alertRepository.findByAlertLevel(Alert.AlertLevel.valueOf(level));
} else if (status != null) {
alerts = alertRepository.findByStatus(Alert.AlertStatus.valueOf(status));
} else if (areaId != null) {
alerts = alertRepository.findByAreaId(areaId);
} else {
alerts = alertRepository.findAll();
}
return ResultVO.success(alerts);
}
}

@ -238,7 +238,7 @@ public class DeviceController {
// ========== 新增:管理员编辑设备基本信息接口 ==========
@PutMapping("/edit")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") // 限制仅管理员/超级管理员可访问
@PreAuthorize("hasAnyRole( 'SUPER_ADMIN','AREA_ADMIN')") // 限制仅管理员/超级管理员可访问
@Operation(summary = "编辑设备基本信息", description = "管理员更新设备名称、类型、安装位置等基本信息(不含设备状态、创建时间)")
public ResponseEntity<ResultVO<Device>> editDevice(@Valid @RequestBody Device device) {
try {

@ -47,9 +47,34 @@
<!-- 主要内容区域 - 修改为单列布局 -->
<div class="content-single-column">
<!-- 最新告警 - 居中显示 -->
<!-- 告警表格 - 居中显示 -->
<div class="content-card">
<h3 class="card-title">最新告警</h3>
<div class="card-header">
<h3 class="card-title">告警信息表格</h3>
<div class="table-controls">
<!-- 搜索框 -->
<input
v-model="searchQuery"
placeholder="搜索设备ID或告警信息..."
class="search-input"
/>
<!-- 筛选下拉框 -->
<select v-model="filterStatus" class="filter-select">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="processing">处理中</option>
<option value="resolved">已解决</option>
<option value="closed">已关闭</option>
</select>
<select v-model="filterLevel" class="filter-select">
<option value="">全部级别</option>
<option value="critical">紧急</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
</select>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loadingAlerts" class="loading-state">
@ -67,22 +92,96 @@
</div>
<!-- 空状态 -->
<div v-else-if="recentAlerts.length === 0" class="empty-state">
<div v-else-if="filteredAlerts.length === 0" class="empty-state">
<div class="empty-icon"></div>
<div class="empty-text">暂无告警信息</div>
</div>
<!-- 告警列表 -->
<div v-else class="alert-list">
<!-- 只显示前10条告警按时间倒序排列 -->
<div v-for="(alert, index) in recentAlerts.slice(0, 10)" :key="alert.alertId" class="alert-item">
<div class="alert-text">{{ alert.deviceId }}{{ alert.alertMessage }}</div>
<div class="alert-time">{{ formatDateTime(alert.timestamp) }}</div>
<div class="alert-meta">
<div :class="['alert-level', alert.alertLevel?.toLowerCase()]">
{{ formatAlertLevel(alert.alertLevel) }}
</div>
<div v-if="alert.areaId" class="alert-area">{{ alert.areaId }}</div>
<!-- 告警表格 -->
<div v-else class="table-container">
<table class="alert-table">
<thead>
<tr>
<th>告警ID</th>
<th>设备ID</th>
<th>告警类型</th>
<th>告警级别</th>
<th>告警信息</th>
<th>区域ID</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<tr v-for="alert in filteredAlerts" :key="alert.alertId">
<td>{{ alert.alertId }}</td>
<td>{{ alert.deviceId }}</td>
<td>{{ alert.alertType }}</td>
<td>
<span :class="['alert-level-badge', alert.alertLevel?.toLowerCase()]">
{{ formatAlertLevel(alert.alertLevel) }}
</span>
</td>
<td class="alert-message">{{ alert.alertMessage }}</td>
<td>{{ alert.areaId }}</td>
<td>
<span :class="['status-badge', alert.status?.toLowerCase()]">
{{ formatAlertStatus(alert.status) }}
</span>
</td>
<td>{{ formatDateTime(alert.timestamp) }}</td>
<td>
</td>
</tr>
</tbody>
</table>
<!-- 分页组件 -->
<div class="pagination-controls">
<div class="page-size-selector">
<label>每页显示</label>
<select v-model="pageSize" @change="handlePageSizeChange">
<option :value="5">5</option>
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
</select>
</div>
<div class="pagination">
<button
@click="changePage(1)"
:disabled="currentPage === 1 || totalPages === 0"
class="pagination-btn"
>
首页
</button>
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1 || totalPages === 0"
class="pagination-btn"
>
上一页
</button>
<span class="pagination-info">
{{ currentPage }} / {{ totalPages }} ( {{ totalAlerts }} )
</span>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === totalPages || totalPages === 0"
class="pagination-btn"
>
下一页
</button>
<button
@click="changePage(totalPages)"
:disabled="currentPage === totalPages || totalPages === 0"
class="pagination-btn"
>
末页
</button>
</div>
</div>
</div>
@ -92,7 +191,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
@ -130,16 +229,59 @@ const stats = ref<StatsData>({
pendingWorkOrders: 0
})
const recentAlerts = ref<Alert[]>([])
const latestAlert = ref<Alert | null>(null)
const allAlerts = ref<Alert[]>([])
const latestAlert = ref<Alert | null | undefined>(null)
const loadingAlerts = ref(false)
const showAlertNotification = ref(true)
const alertPermissionError = ref(false)
//
const searchQuery = ref('')
const filterStatus = ref('')
const filterLevel = ref('')
//
const currentPage = ref(1)
const pageSize = ref(10)
// store
const router = useRouter()
const authStore = useAuthStore()
//
const totalFilteredAlerts = computed(() => {
return allAlerts.value.filter(alert => {
// ID
const matchesSearch = !searchQuery.value ||
alert.deviceId.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
alert.alertMessage.toLowerCase().includes(searchQuery.value.toLowerCase())
//
const matchesStatus = !filterStatus.value || alert.status === filterStatus.value
// -
const matchesLevel = !filterLevel.value ||
(alert.alertLevel && alert.alertLevel.toLowerCase() === filterLevel.value.toLowerCase())
return matchesSearch && matchesStatus && matchesLevel
})
})
//
const totalPages = computed(() => {
return Math.ceil(totalFilteredAlerts.value.length / pageSize.value)
})
//
const filteredAlerts = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value
const endIndex = startIndex + pageSize.value
return totalFilteredAlerts.value.slice(startIndex, endIndex)
})
//
const totalAlerts = computed(() => totalFilteredAlerts.value.length)
//
const formatAlertLevel = (level: string): string => {
const levelMap: Record<string, string> = {
@ -151,6 +293,17 @@ const formatAlertLevel = (level: string): string => {
return levelMap[level?.toLowerCase()] || level
}
//
const formatAlertStatus = (status: string): string => {
const statusMap: Record<string, string> = {
'pending': '待处理',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
}
return statusMap[status?.toLowerCase()] || status
}
//
const formatDateTime = (dateTime: string): string => {
if (!dateTime) return ''
@ -165,7 +318,7 @@ const fetchStatsData = async () => {
// token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -230,21 +383,19 @@ const fetchStatsData = async () => {
} catch (error: any) {
console.error('获取统计数据失败:', error)
const errorMsg = error.message?.includes('401')
error.message?.includes('401')
? '登录已过期,请重新登录'
: error.message?.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
//
: error.message || '获取数据失败,请稍后重试';
//
if (error.message?.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
//
//
const fetchAlertData = async () => {
loadingAlerts.value = true
@ -256,11 +407,11 @@ const fetchAlertData = async () => {
// token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
// - 使
// - 使pending
const alertResult = await request<ResultVO<Alert[]>>(
'/api/alerts/pending',
{ method: 'GET' }
@ -280,17 +431,16 @@ const fetchAlertData = async () => {
return timeB - timeA //
})
// 10
recentAlerts.value = sortedAlerts.slice(0, 10)
allAlerts.value = sortedAlerts
//
// 222
if (recentAlerts.value.length > 0 && recentAlerts.value[0]) {
latestAlert.value = recentAlerts.value[0] //
//
if (sortedAlerts.length > 0) {
latestAlert.value = sortedAlerts[0] //
} else {
latestAlert.value = null
}
} else {
latestAlert.value = null
}
} catch (error: any) {
console.error('获取告警数据失败:', error)
@ -299,27 +449,49 @@ const fetchAlertData = async () => {
if (error.message?.includes('403')) {
console.warn('当前用户无权限访问告警数据')
alertPermissionError.value = true
recentAlerts.value = []
allAlerts.value = []
latestAlert.value = null
return
}
const errorMsg = error.message?.includes('401')
error.message?.includes('401')
? '登录已过期,请重新登录'
: error.message?.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
//
: error.message || '获取数据失败,请稍后重试';
//
if (error.message?.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
latestAlert.value = null
} finally {
loadingAlerts.value = false
}
}
//
//
//
const changePage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const changePageSize = (size: number) => {
pageSize.value = size
currentPage.value = 1 //
}
//
const handlePageSizeChange = (event: Event) => {
const target = event.target as HTMLSelectElement
if (target) {
const size = Number(target.value)
changePageSize(size)
}
}
//
onMounted(() => {
@ -439,12 +611,149 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
margin: 0;
}
.table-controls {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.search-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 200px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
}
/* 表格样式 */
.table-container {
overflow-x: auto;
}
.alert-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
margin-top: 16px;
}
.alert-table th,
.alert-table td {
padding: 12px 8px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.alert-table th {
background-color: #f5f5f5;
font-weight: 600;
color: #333;
position: sticky;
top: 0;
}
.alert-table tbody tr:hover {
background-color: #f9f9f9;
}
.alert-message {
max-width: 200px;
word-wrap: break-word;
}
.alert-level-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-align: center;
min-width: 50px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-align: center;
min-width: 60px;
}
/* 分页样式 */
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
}
.page-size-selector select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.pagination-btn {
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.pagination-btn:hover:not(:disabled) {
background: #f0f0f0;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
margin: 0 15px;
font-size: 14px;
color: #666;
}
/* 加载状态 */
@ -527,100 +836,28 @@ onMounted(() => {
color: #666;
}
/* 告警列表样式 */
.alert-list {
display: flex;
flex-direction: column;
}
.alert-item {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.alert-item:last-child {
border-bottom: none;
}
.alert-text {
font-size: 14px;
color: #333;
margin-bottom: 4px;
line-height: 1.4;
}
.alert-time {
font-size: 12px;
color: #666;
margin: 4px 0;
}
.alert-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-level {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
min-width: 50px;
text-align: center;
}
.alert-level.critical {
background: #ffebee;
color: #c62828;
}
.alert-level.error {
background: #ffebee;
color: #c62828;
}
.alert-level.warning {
background: #fff8e1;
color: #ff8f00;
}
.alert-level.info {
background: #e8f5e9;
color: #2e7d32;
}
.alert-area {
font-size: 12px;
color: #666;
background: #f5f5f5;
padding: 4px 8px;
border-radius: 4px;
}
.chart-placeholder {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 4px;
}
.placeholder-text {
text-align: center;
color: #666;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.alert-meta {
.card-header {
flex-direction: column;
align-items: stretch;
}
.table-controls {
width: 100%;
}
.search-input {
min-width: unset;
width: 100%;
}
.pagination-controls {
flex-direction: column;
align-items: flex-start;
gap: 8px;
gap: 15px;
}
}
</style>

@ -1,4 +1,3 @@
<!-- src/views/equipment/WaterMaker.vue -->
<template>
<div class="water-maker-page">
<!-- 页面标题和面包屑 -->
@ -10,8 +9,6 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="showAddModal = true">添加制水机</button>
<!-- 在操作按钮列中添加删除按钮 -->
<div class="filters">
<!-- 搜索框 -->
@ -68,7 +65,7 @@
<tbody>
<tr v-for="device in paginatedDevices" :key="device.deviceId">
<td>{{ device.deviceId }}</td>
<td>{{ device.deviceType === 'WATER_MAKER' ? '制水机' : device.deviceType }}</td>
<td>{{ device.deviceType === 'water_maker' ? '制水机' : device.deviceType }}</td>
<td>{{ device.areaId }}</td>
<td>{{ device.installLocation }}</td>
<td>
@ -79,12 +76,11 @@
<td>{{ formatDate(device.lastHeartbeatTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.deviceId)"></button>
<button class="btn-edit" @click="openEditModal(device)"></button>
<button
class="btn-online"
@click="updateDeviceStatus(device.deviceId, 'online')"
:disabled="device.status === 'online'"
class="btn-delete"
@click="deleteDevice(device.deviceId)"
>
删除
</button>
</td>
@ -185,6 +181,74 @@
</div>
</div>
<!-- 编辑设备模态框 -->
<div v-if="showEditModal" class="modal-overlay" @click="showEditModal = false">
<div class="modal-content" @click.stop>
<h3>编辑制水机</h3>
<form @submit.prevent="updateDevice">
<div class="form-group">
<label>设备ID:</label>
<input v-model="editingDevice.deviceId" type="text" disabled>
</div>
<div class="form-group">
<label>设备名称:</label>
<input v-model="editingDevice.deviceName" type="text" required>
</div>
<!-- 市区选择 -->
<div class="form-group">
<label>选择市区:</label>
<select
v-model="selectedEditCityId"
@change="onEditCityChange"
class="select-input"
>
<option value="">请选择市区</option>
<option
v-for="city in cityList"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
</div>
<!-- 校区选择 -->
<div class="form-group" v-if="selectedEditCityId">
<label>选择校区:</label>
<select
v-model="selectedEditCampusId"
@change="onEditCampusChange"
class="select-input"
:disabled="!editCampusList.length"
>
<option value="">请选择校区</option>
<option
v-for="campus in editCampusList"
:key="campus.areaId"
:value="campus.areaId"
>
{{ campus.areaName }}
</option>
</select>
<p v-if="selectedEditCityId && !editCampusList.length" class="no-data-message">
该市区暂无校区
</p>
</div>
<div class="form-group">
<label>安装位置:</label>
<input v-model="editingDevice.installLocation" type="text" required>
</div>
<div class="form-actions">
<button type="button" @click="showEditModal = false">取消</button>
<button type="submit">更新</button>
</div>
</form>
</div>
</div>
<!-- 离线原因模态框 -->
<div v-if="showOfflineReasonModal" class="modal-overlay" @click="showOfflineReasonModal = false">
<div class="modal-content" @click.stop>
@ -270,6 +334,7 @@ const authStore = useAuthStore() // 初始化auth store
//
const showAddModal = ref(false)
const showEditModal = ref(false)
const showOfflineReasonModal = ref(false)
const showFaultModal = ref(false)
@ -285,12 +350,27 @@ const newDevice = ref({
deviceType: 'water_maker'
})
//
const editingDevice = ref<WaterMakerDevice>({
deviceId: '',
deviceName: '',
deviceType: '',
areaId: '',
installLocation: '',
status: 'online'
})
//
const cityList = ref<Area[]>([]) //
const campusList = ref<Area[]>([]) //
const selectedCityId = ref('') // ID
const selectedCampusId = ref('') // ID
//
const editCampusList = ref<Area[]>([]) //
const selectedEditCityId = ref('') // ID
const selectedEditCampusId = ref('') // ID
const offlineReason = ref('')
const faultInfo = ref({
faultType: '',
@ -304,7 +384,7 @@ const loadDevices = async (): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -338,7 +418,7 @@ const loadDevices = async (): Promise<void> => {
console.error('加载设备数据失败:', error)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -349,7 +429,7 @@ const loadCityList = async (): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -376,7 +456,7 @@ const loadCityList = async (): Promise<void> => {
cityList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -387,7 +467,7 @@ const loadCampusListByCity = async (cityId: string): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -414,7 +494,45 @@ const loadCampusListByCity = async (cityId: string): Promise<void> => {
campusList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
// ID
const loadEditCampusListByCity = async (cityId: string): Promise<void> => {
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return
}
console.log(`开始加载市区 ${cityId} 的校区列表...`)
const result = await request<any>(`/api/web/area/campuses/${cityId}`, { method: 'GET' })
if (result && typeof result === 'object' && 'code' in result) {
if (result.code === 200 && result.data && Array.isArray(result.data)) {
editCampusList.value = result.data
console.log(`获取到${editCampusList.value.length}个校区`)
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
editCampusList.value = []
}
} else if (Array.isArray(result)) {
editCampusList.value = result
} else {
console.warn('API响应数据格式错误:', result)
editCampusList.value = []
}
} catch (error) {
console.error('加载校区列表失败:', error)
editCampusList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
}
}
}
@ -441,6 +559,35 @@ const onCampusChange = () => {
}
}
//
const onEditCityChange = async () => {
//
selectedEditCampusId.value = ''
editCampusList.value = []
if (selectedEditCityId.value) {
await loadEditCampusListByCity(selectedEditCityId.value)
//
const matchingCampus = editCampusList.value.find(campus =>
campus.areaName === editingDevice.value.areaId
)
if (matchingCampus) {
selectedEditCampusId.value = matchingCampus.areaId
}
}
}
//
const onEditCampusChange = () => {
// areaIdareaNameareaId
const selectedCampus = editCampusList.value.find(campus => campus.areaId === selectedEditCampusId.value)
if (selectedCampus) {
editingDevice.value.areaId = selectedCampus.areaName // 使areaNameareaId
} else {
editingDevice.value.areaId = ''
}
}
//
const filteredDevices = computed(() => {
return devices.value.filter(device => {
@ -494,17 +641,7 @@ const viewDevice = (id: string) => {
}
// 线
const showOfflineModal = (deviceId: string) => {
currentDeviceId.value = deviceId
showOfflineReasonModal.value = true
}
//
const showFaultModalFunc = (deviceId: string) => {
currentDeviceId.value = deviceId
showFaultModal.value = true
}
// 线
const confirmOffline = async () => {
try {
@ -512,7 +649,7 @@ const confirmOffline = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -537,7 +674,7 @@ const confirmOffline = async () => {
alert('设置设备离线失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -549,7 +686,7 @@ const confirmFault = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -574,101 +711,146 @@ const confirmFault = async () => {
alert('设置设备故障失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
// 线
const updateDeviceStatus = async (deviceId: string, status: string) => {
//
const deleteDevice = async (deviceId: string) => {
if (!confirm(`确定要删除设备 ${deviceId} 吗?此操作不可恢复。`)) {
return
}
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
let url = ''
let body = {}
switch (status) {
case 'online':
url = `/api/web/device-status/${deviceId}/online`
break
case 'offline':
url = `/api/web/device-status/${deviceId}/offline`
body = { reason: '手动设置为离线' }
break
case 'fault':
url = `/api/web/device-status/${deviceId}/fault`
body = { faultType: 'MANUAL', description: '手动设置为故障' }
break
default:
throw new Error('不支持的状态类型')
}
const result = await request<ResultVO<boolean>>(url, {
method: 'POST',
body: JSON.stringify(body)
const result = await request<ResultVO<boolean>>(`/api/web/device/delete/${deviceId}`, {
method: 'DELETE'
})
if (result.code === 200) {
//
const device = devices.value.find(d => d.deviceId === deviceId)
if (device) {
device.status = status as DeviceStatus
}
alert(`设备已标记为${formatStatus(status as DeviceStatus)}`)
//
devices.value = devices.value.filter(d => d.deviceId !== deviceId)
alert('设备删除成功')
} else {
alert(`更新设备状态失败: ${result.message}`)
alert(`删除设备失败: ${result.message}`)
}
} catch (error) {
console.error('更新设备状态失败:', error)
alert('更新设备状态失败')
console.error('删除设备失败:', error)
alert('删除设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
//
const deleteDevice = async (deviceId: string) => {
if (!confirm(`确定要删除设备 ${deviceId} 吗?此操作不可恢复。`)) {
return
//
const openEditModal = (device: WaterMakerDevice) => {
//
editingDevice.value = { ...device }
//
selectedEditCityId.value = ''
selectedEditCampusId.value = ''
editCampusList.value = []
//
findCurrentAreaInCityList(device.areaId)
showEditModal.value = true
}
//
const findCurrentAreaInCityList = (areaId: string) => {
for (const city of cityList.value) {
loadEditCampusListByCity(city.areaId).then(() => {
const matchingCampus = editCampusList.value.find(campus => campus.areaName === areaId)
if (matchingCampus) {
selectedEditCityId.value = city.areaId
selectedEditCampusId.value = matchingCampus.areaId
return
}
})
}
}
//
const updateDevice = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/device/delete/${deviceId}`, {
method: 'DELETE'
//
if (!selectedEditCampusId.value) {
alert('请选择校区')
return
}
// areaId
if (!editingDevice.value.areaId) {
alert('请先选择校区以确定所属片区')
return
}
//
const deviceToUpdate = {
deviceId: editingDevice.value.deviceId,
deviceName: editingDevice.value.deviceName,
areaId: editingDevice.value.areaId,
installLocation: editingDevice.value.installLocation,
deviceType: editingDevice.value.deviceType
}
const result = await request<ResultVO>('/api/web/device/edit', {
method: 'PUT',
body: JSON.stringify(deviceToUpdate)
})
if (result.code === 200) {
//
devices.value = devices.value.filter(d => d.deviceId !== deviceId)
alert('设备删除成功')
//
const index = devices.value.findIndex(d => d.deviceId === editingDevice.value.deviceId)
if (index !== -1) {
devices.value[index] = { ...editingDevice.value }
}
//
showEditModal.value = false
editingDevice.value = {
deviceId: '',
deviceName: '',
deviceType: '',
areaId: '',
installLocation: '',
status: 'online'
}
selectedEditCityId.value = ''
selectedEditCampusId.value = ''
editCampusList.value = []
alert('设备更新成功')
} else {
alert(`删除设备失败: ${result.message}`)
alert(`设备更新失败: ${result.message}`)
}
} catch (error) {
console.error('删除设备失败:', error)
alert('删除设备失败')
console.error('更新设备失败:', error)
alert('更新设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
//
const addDevice = async () => {
try {
@ -676,7 +858,7 @@ const addDevice = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -702,7 +884,7 @@ const addDevice = async () => {
}
// 使token
const result = await request<ResultVO<any>>('/api/web/device/add', {
const result = await request<ResultVO>('/api/web/device/add', {
method: 'POST',
body: JSON.stringify(deviceToAdd)
})
@ -733,7 +915,7 @@ const addDevice = async () => {
alert('添加设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -850,34 +1032,6 @@ onMounted(async () => {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.online {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault {
background-color: #ffebe6;
color: #cf1322;
}
.operation-buttons {
display: flex;
gap: 8px;
@ -907,19 +1061,14 @@ onMounted(async () => {
color: #1890ff;
}
.btn-online {
background-color: #e6f7ee;
color: #00875a;
}
.btn-offline {
background-color: #f5f5f5;
color: #8c8c8c;
.btn-edit {
background-color: #e6f7ff;
color: #1890ff;
}
.btn-fault {
.btn-delete {
background-color: #ffebe6;
color: #cf1322;
color: #ff4d4f;
}
.no-data {
@ -1066,9 +1215,8 @@ onMounted(async () => {
min-width: auto;
}
.btn-delete {
background-color: #ff4d4f;
color: white;
.operation-buttons {
flex-direction: column;
}
}
</style>

@ -78,6 +78,7 @@
<td>{{ device.lastUploadTime }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.id)"></button>
<button class="btn-edit" @click="openEditModal(device)"></button>
<button class="btn-delete" @click="currentDeviceId = device.id; showDeleteModal = true">删除</button>
</td>
</tr>
@ -154,6 +155,51 @@
</div>
</div>
<!-- 编辑设备模态框 -->
<div v-if="showEditModal" class="modal-overlay" @click="showEditModal = false">
<div class="modal-content" @click.stop>
<h3>编辑供水机</h3>
<form @submit.prevent="updateDevice">
<div class="form-group">
<label>设备ID:</label>
<input v-model="editingDevice.deviceId" type="text" disabled>
</div>
<div class="form-group">
<label>设备名称:</label>
<input v-model="editingDevice.deviceName" type="text" required>
</div>
<div class="form-group">
<label>所属片区:</label>
<select v-model="editingDevice.areaId" @change="loadAvailableMakersForEdit" required>
<option value="">请选择片区</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
</select>
</div>
<div class="form-group">
<label>关联制水机:</label>
<select v-model="editingDevice.parentMakerId" :disabled="!editingDevice.areaId">
<option value="">请选择关联制水机</option>
<option v-for="maker in availableMakersForEdit" :key="maker.id" :value="maker.id">
{{ maker.name }}
</option>
</select>
<p v-if="!editingDevice.areaId" class="help-text"></p>
</div>
<div class="form-group">
<label>安装位置:</label>
<input v-model="editingDevice.installLocation" type="text" required>
</div>
<div class="form-actions">
<button type="button" @click="showEditModal = false">取消</button>
<button type="submit">更新</button>
</div>
</form>
</div>
</div>
<!-- 删除确认模态框 -->
<div v-if="showDeleteModal" class="modal-overlay" @click="showDeleteModal = false">
<div class="modal-content" @click.stop>
@ -187,6 +233,16 @@ interface WaterSupplierDevice {
lastUploadTime: string
}
//
interface DeviceDetail {
deviceId: string
deviceName: string
areaId: string
installLocation: string
deviceType: string
parentMakerId?: string
}
//
const devices = ref<WaterSupplierDevice[]>([])
const searchKeyword = ref('')
@ -208,8 +264,20 @@ const newDevice = ref({
deviceType: 'water_supply'
})
//
const showEditModal = ref(false)
const editingDevice = ref<DeviceDetail>({
deviceId: '',
deviceName: '',
areaId: '',
installLocation: '',
deviceType: 'water_supply',
parentMakerId: ''
})
//
const availableMakers = ref<{id: string, name: string}[]>([])
const availableMakersForEdit = ref<{id: string, name: string}[]>([])
//
const showDeleteModal = ref(false)
@ -222,7 +290,7 @@ const loadWaterSuppliers = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -259,7 +327,7 @@ const loadWaterSuppliers = async () => {
alert('获取供水机列表失败,请检查网络连接');
if (error instanceof Error && error.message.includes('401')) {
authStore.logout();
router.push('/login');
await router.push('/login');
}
}
}
@ -274,7 +342,7 @@ const loadAvailableMakers = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -301,6 +369,50 @@ const loadAvailableMakers = async () => {
} catch (error) {
console.error('加载制水机列表失败:', error);
availableMakers.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
}
}
}
//
const loadAvailableMakersForEdit = async () => {
if (!editingDevice.value.areaId) {
availableMakersForEdit.value = []
return
}
try {
const token = authStore.token
if (!token) {
await router.push('/login')
return
}
//
const params = new URLSearchParams();
params.append('areaId', editingDevice.value.areaId);
params.append('deviceType', 'water_maker');
const queryString = params.toString();
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`;
const response = await request<ResultVO<any[]>>(url, { method: 'GET' });
if (response.code === 200 && response.data && Array.isArray(response.data)) {
// ID
availableMakersForEdit.value = response.data.map((maker: any) => ({
id: maker.deviceId,
name: `${maker.deviceId} - ${maker.installLocation}`
}))
} else {
console.error('获取制水机列表失败:', response.message);
availableMakersForEdit.value = []
}
} catch (error) {
console.error('加载制水机列表失败:', error);
availableMakersForEdit.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
@ -409,6 +521,100 @@ const resetAddForm = () => {
availableMakers.value = []
}
//
const openEditModal = async (device: WaterSupplierDevice) => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const response = await request<ResultVO<{ deviceInfo: DeviceDetail }>>(`/api/web/device/${device.id}`, { method: 'GET' })
if (response.code === 200 && response.data?.deviceInfo) {
const deviceDetail = response.data.deviceInfo // 👈 deviceInfo
editingDevice.value = {
deviceId: deviceDetail.deviceId,
deviceName: deviceDetail.deviceName,
areaId: deviceDetail.areaId,
installLocation: deviceDetail.installLocation,
deviceType: deviceDetail.deviceType,
parentMakerId: deviceDetail.parentMakerId || ''
}
//
await loadAvailableMakersForEdit()
showEditModal.value = true
} else {
alert(`获取设备详情失败: ${response.message}`)
}
} catch (error) {
console.error('获取设备详情失败:', error)
alert('获取设备详情失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const updateDevice = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const deviceToUpdate = {
deviceId: editingDevice.value.deviceId,
deviceName: editingDevice.value.deviceName,
areaId: editingDevice.value.areaId,
installLocation: editingDevice.value.installLocation,
deviceType: editingDevice.value.deviceType,
parentMakerId: editingDevice.value.parentMakerId || null
}
const result = await request<ResultVO<any>>('/api/web/device/edit', {
method: 'PUT',
body: JSON.stringify(deviceToUpdate)
})
if (result.code === 200) {
await loadWaterSuppliers()
showEditModal.value = false
resetEditForm()
alert('设备更新成功')
} else {
alert(`设备更新失败: ${result.message}`)
}
} catch (error) {
console.error('更新设备失败:', error)
alert('更新设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const resetEditForm = () => {
editingDevice.value = {
deviceId: '',
deviceName: '',
areaId: '',
installLocation: '',
deviceType: 'water_supply',
parentMakerId: ''
}
availableMakersForEdit.value = []
}
//
const deleteDevice = async () => {
try {
@ -491,6 +697,21 @@ onMounted(() => {
background: #359e75;
}
.btn-edit {
background: #e6f7ff;
color: #1890ff;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s;
}
.btn-edit:hover {
background: #bae7ff;
}
.btn-delete {
background: #cf1322;
color: white;
@ -564,34 +785,6 @@ onMounted(() => {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.online {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.error {
background-color: #ffebe6;
color: #cf1322;
}
.operation-buttons {
display: flex;
gap: 8px;

@ -46,12 +46,6 @@ const router = createRouter({
component: () => import('../views/InspectionPage.vue'),
meta: { requiresAuth: true }
},
{
path: '/inspection/scan',
name: 'InspectionScan',
component: () => import('../views/InspectionScan.vue'),
meta: { requiresAuth: true }
},
{
path: '/inspection/water-maker',
name: 'WaterMakerList',

@ -70,8 +70,8 @@ const goToProfile = () => {
const viewYueluArea = () => {
console.log('查看岳麓片区详情')
//
router.push('/inspection/scan')
//
router.push('/inspection/water-maker')
}
</script>

@ -1,255 +0,0 @@
<template>
<div class="inspection-scan">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-left">
<span class="back-btn" @click="goBack"></span>
</div>
<div class="header-title">设备巡检</div>
<div class="header-right"></div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<div class="scan-container">
<!-- 扫码框 -->
<div class="scan-box">
<div class="scan-icon">📷</div>
<div class="scan-border">
<div class="scan-line"></div>
</div>
</div>
<div class="scan-text">请扫描设备上二维码以开始巡检</div>
<button class="scan-complete-btn" @click="simulateScanComplete">
扫描完成
</button>
<div class="scan-tips">
<p>💡 提示将二维码放入框内自动扫描</p>
<p>或点击"扫描完成"模拟测试</p>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-item" @click="goToHome"></div>
<div class="nav-item active" @click="goToInspection"></div>
<div class="nav-item" @click="goToWorkOrders"></div>
<div class="nav-item" @click="goToProfile"></div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goBack = () => {
router.back()
}
const goToHome = () => {
router.push('/home')
}
const goToInspection = () => {
router.push('/inspection')
}
const goToWorkOrders = () => {
router.push('/work-orders')
}
const goToProfile = () => {
router.push('/profile')
}
//
const simulateScanComplete = () => {
console.log('模拟扫码完成,跳转到制水机列表')
router.push('/inspection/water-maker')
}
</script>
<style scoped>
.inspection-scan {
width: 100%;
height: 100%;
background: #f8f9fa;
display: flex;
flex-direction: column;
}
/* 顶部标题栏 */
.header {
background: white;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.header-left {
width: 80px;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
flex: 1;
}
.header-right {
width: 80px;
}
.back-btn {
font-size: 14px;
color: #1890ff;
cursor: pointer;
transition: color 0.3s;
}
.back-btn:hover {
color: #096dd9;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
/* 扫码容器 */
.scan-container {
width: 100%;
max-width: 320px;
background: white;
border-radius: 12px;
padding: 30px 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
text-align: center;
}
/* 扫码框 */
.scan-box {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto 30px;
background: #f8f9fa;
border-radius: 8px;
overflow: hidden;
}
.scan-icon {
font-size: 60px;
margin: 40px 0 20px;
opacity: 0.7;
}
.scan-border {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px dashed #1890ff;
border-radius: 8px;
}
.scan-line {
position: absolute;
top: 0;
left: 10px;
right: 10px;
height: 2px;
background: #1890ff;
animation: scanAnimation 2s linear infinite;
}
@keyframes scanAnimation {
0% {
top: 0;
}
100% {
top: 100%;
}
}
/* 扫码文本 */
.scan-text {
font-size: 16px;
color: #333;
margin-bottom: 30px;
font-weight: 500;
}
/* 扫码完成按钮 */
.scan-complete-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
}
.scan-complete-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(24, 144, 255, 0.3);
}
/* 扫码提示 */
.scan-tips {
font-size: 13px;
color: #666;
line-height: 1.5;
}
.scan-tips p {
margin: 5px 0;
}
/* 底部导航栏 */
.bottom-nav {
display: flex;
background: white;
border-top: 1px solid #e8e8e8;
padding: 8px 0;
}
.nav-item {
flex: 1;
text-align: center;
padding: 8px;
font-size: 12px;
color: #666;
cursor: pointer;
transition: color 0.3s;
}
.nav-item.active {
color: #1890ff;
}
.nav-item:hover {
color: #1890ff;
}
</style>
Loading…
Cancel
Save