最终版 #176

Merged
p95fco63j merged 62 commits from develop into main 20 hours ago

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: com.campus.water.CampusWaterApplication

@ -9,19 +9,20 @@
## 本周任务计划安排
| 序号 | 计划内容 | 执行人 | 情况说明 |
|----|------------------------|--------|-------------------------|
| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 |
|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 |
|3| 解决工单显示未知设备问题 | 曹峻茂 | 解决工单页面的显示错误 |
|4| 解决新建终端关联没有检验关联设备是否存在问题 | 王磊,张红卫 | 解决新建终端的问题 |
|5| 修改人工派单权限,强制派单 |王磊,张红卫|保证人工派单不会受工人状态检查|
|6| 取消工人同时接单数量限制 |王磊|取消工人同时接单数量限制|
|7| 修复片区管理逻辑 |张红卫,王磊|修复片区管理问题|
|8| 完善报错状态码定义 |王磊|避免未拦截弹窗|
|9| 修改发送间隔设置 |曹峻茂|模拟数据发送间隔固定,灵活性差|
|10|完善设备信息修改|王磊,张红卫|补充设备信息修改功能|
|11|根据老师建议完善其他细节问题|全体成员|视情况完善相关细节|
| 序号 | 计划内容 | 执行人 | 情况说明 |
|----|------------------------|-------------|-------------------------|
| 1 | 确定本周计划分工 | 全体组员 | 2023-12-29 开会确定计划以及团队分工 |
|2| 和老师开会讨论当前项目的不足 | 全体组员 | 和老师讨论当前开发状况 |
|3| 解决工单显示未知设备问题 | 曹峻茂 | 解决工单页面的显示错误 |
|4| 解决新建终端关联没有检验关联设备是否存在问题 | 王磊,张红卫 | 解决新建终端的问题 |
|5| 修改人工派单权限,强制派单 | 王磊,张红卫 |保证人工派单不会受工人状态检查|
|6| 取消工人同时接单数量限制 | 王磊 |取消工人同时接单数量限制|
|7| 修复片区管理逻辑 | 张红卫,王磊 |修复片区管理问题|
|8| 完善报错状态码定义 | 王磊 |避免未拦截弹窗|
|9| 修改发送间隔设置 | 曹峻茂 |模拟数据发送间隔固定,灵活性差|
|10|完善设备信息修改| 王磊,张红卫 |补充设备信息修改功能|
|11|根据老师建议完善其他细节问题| 全体成员 |视情况完善相关细节|
|12|部署云服务器后端程序web端服务打包apk| 曹峻茂,罗月航,张红卫 | |
## 小结
1. **沟通协作:** 每日简短同步进度,遇到阻塞问题及时召开临时会议协商解决,可向指导老师及研究生学长寻求技术支持。

@ -0,0 +1,23 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-03
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|----|----------|-------|-------------------------------------------------------------------------------|
| 1 | 模块功能开发 | 组员 | 基于第14周确定的分工完成个人负责模块的核心功能开发含接口实现、数据校验逻辑编写及单元测试覆盖率不低于80%),每日下班前同步开发进度至团队群 |
| 2 | 联调问题迭代修复 | 前端/组员 | 跟进第14周未解决的联调问题完成剩余2个非核心接口的对接实时响应新联调过程中出现的功能异常、数据返回错误等问题确保核心业务流程联调通过率100% |
| 3 | 中期进度复盘 | 团队全员 | 2026-01-02组织团队中期复盘会议同步各模块开发进度、联调情况及当前存在的阻塞问题协商解决方案调整后续任务优先级输出《第15周中期复盘报告》 |
| 4 | 接口文档完善 | 组员 | 基于开发和联调实际情况,补充接口文档的参数示例、异常响应说明及调用注意事项,确保文档与实际接口一致,便于前端查阅和后续维护 |
## 小结
1. **技术重点:** 聚焦模块核心功能实现与单元测试编写,熟练运用数据校验、异常处理技巧;针对性解决联调中的复杂问题,提升接口兼容性设计能力
2. **协作重点:** 保持与前端的实时沟通,及时同步开发进度与接口变更;通过中期复盘会议,高效协调团队资源,解决项目阻塞问题
3. **学习重点:** 学习单元测试框架如JUnit的高级用法提升测试用例设计合理性研究接口文档标准化编写规范提高文档可读性与实用性

@ -0,0 +1,25 @@
# 个人周总结-第14周
## 周竞由 - 个人周总结
### 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
### 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|------|------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | 确定分工 | 完成 | 2025-12-22按时组织团队线下会议结合第14周迭代开发需求基于组员技能特长与过往任务完成情况细分8个功能模块的开发职责明确各模块交付物含接口文档、测试用例及截止时间输出《第14周团队分工明细表》组织组员逐一确认职责无分工异议 |
| 2 | 联调支持 | 完成 | 全程配合前端开展接口联调工作建立实时沟通群响应问题累计处理字段类型不匹配、跨域配置异常、分页参数错误等联调问题12个平均问题解决时长1.5小时协助前端完成6个核心功能模块的接口对接含用户操作日志同步、数据统计报表生成等关键场景联调通过率达98%剩余2个非核心接口问题已记录并计划下周优先处理 |
### 对团队工作的建议
1. **建议优化接口文档维护机制**本周联调中发现部分接口文档字段说明不清晰、参数示例缺失导致沟通成本增加建议建立接口文档“谁开发谁维护”的责任制要求更新接口后2小时内同步更新文档每周安排专人抽检文档完整性确保前后端信息一致
2. **建议建立联调问题实时同步看板**建议使用团队协作工具如飞书看板、Trello搭建联调问题跟踪看板按“待处理/处理中/已解决/复盘”分类管理问题,标注问题责任人、预计解决时间,方便团队实时掌握联调进度,避免重复沟通
### 小结
1. **技术收获**通过处理多样化的联调问题深化了对HTTP请求参数校验、跨域配置、接口兼容性处理的理解在分工规划中提升了基于项目需求拆解任务、匹配团队资源的能力学会结合组员优势合理分配职责以提升整体开发效率
2. **协作收获**:通过建立实时沟通机制,缩短了跨角色问题响应周期,提升了与前端团队的协作默契;在分工会议中,通过充分听取组员意见,锻炼了协调不同诉求、达成共识的沟通能力
3. **后续重点**跟进剩余2个接口的联调收尾工作确保功能全量对接系统学习接口性能分析工具如JMeter与调优方法重点攻克SQL查询优化、数据缓存设计等知识点协助团队搭建联调问题解决方案库与接口文档抽检机制
4. **希望得到的帮助**:希望能获取团队过往项目的接口性能调优案例集,或邀请技术导师针对“高并发场景下接口优化技巧”进行专项指导,帮助快速掌握实操方法,提升系统性能优化能力

@ -0,0 +1,42 @@
# 小组周总结-第15周
## 团队名称和起止时间
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-1-4
## 本周任务计划安排
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|--------------|------|----------------------------|
| 1 | 确定本周计划分工 | 完成 | 2023-12-29 开会确定计划以及团队分工 |
|2| 和老师开会讨论当前项目的不足 | 完成 | 和老师讨论当前开发状况 |
|3| 解决工单显示未知设备问题 | 完成 | 解决工单页面的显示错误 |
|4| 解决新建终端关联没有检验关联设备是否存在问题 | 完成 | 解决新建终端的问题 |
|5| 修改人工派单权限,强制派单 | 完成 |保证人工派单不会受工人状态检查|
|6| 取消工人同时接单数量限制 | 完成 |取消工人同时接单数量限制|
|7| 修复片区管理逻辑 | 完成 |修复片区管理问题|
|8| 完善报错状态码定义 | 完成 |避免未拦截弹窗|
|9| 修改发送间隔设置 | 完成 |模拟数据发送间隔固定,灵活性差|
|10|完善设备信息修改| 完成 |补充设备信息修改功能|
|11|根据老师建议完善其他细节问题| 完成 |视情况完善相关细节|
|12|部署云服务器后端程序web端服务打包apk|完成| |
## 小结
1. **沟通协作:** 小组成员应积极主动沟通,遇到困难及时寻求帮助,也可以主动向指导老师及研究生学长寻求建议。
2. **学习安排:** 小组成员仍处于软件开发专业知识的初步学习阶段,应合理安排自主学习时间,以便后续开发的顺利进行。
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. PM综合本小组成员工作情况提交小组周计划、周总结报告按时上传至代码托管平台。
---

@ -0,0 +1,37 @@
# 个人周总结-第15周
## 姓名和起止时间
**姓  名:** 曹峻茂
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2025-1-4
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|----------------|------|--------------------------------------------------------------|
| 1 | 确定本周计划分工 | 完成 | 2023-12-29 开会确定计划以及团队分工 |
| 2 | 和老师开会讨论当前项目的不足 | 完成 | 和老师讨论当前开发状况 |
| 3 | 解决工单显示未知设备问题 | 完成 | 解决工单页面的显示错误 |
| 4 | 修改发送间隔设置 | 完成 |模拟数据发送间隔固定,灵活性差|
| 5 | 部署云服务器后端程序 | 完成 | |
## 对团队工作的建议
1. **互助学习:** 小组成员应该根据自身的技能长短开展互帮互助的活动,共同努力提高小组成员的专业水平;
2. **进度统一:** 团队成员尽量统一项目进度;
## 小结
1. **项目管理:** 协调开发进度和前后端同步
2. **团队协作**:与团队成员保持良好的沟通协作,确保设计方向与产品需求一致
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. 所有组员都需提交个人周计划、周总结文档,上传至代码托管平台;

@ -0,0 +1,36 @@
# 个人周总结-第15周
## 姓名和起止时间
**姓  名:** 罗月航
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- | -------- | -------- | -------- |
| 1 | 与老师项目讨论会议 | 完成 | 成功召开项目讨论会,老师对项目成果给予肯定,并提出宝贵的改进建议 |
| 2 | 项目不足分析整理 | 完成 | 系统整理了老师提出的改进建议 |
| 3 | 业务流程梳理优化 | 完成 | 重新梳理了扫码用水和工单处理的核心业务流程 |
| 5 | 代码质量二次审查 | 完成 | 对修改后的代码进行了全面审查,确保代码质量和规范符合要求 |
| 6 | 性能优化持续跟进 | 完成 | 针对老师提出的性能问题进行了进一步优化关键页面加载速度提升25% |
| 7 | 学生端APK打包 | 完成 | 成功将学生端项目打包为APK安装包经过测试可在Android设备上正常运行 |
| 8 | 维修人员APP APK打包 | 完成 | 成功将维修人员APP打包为APK安装包功能完整运行稳定 |
## 对团队工作的建议
1. **安装包管理**建议建立APK版本管理机制方便跟踪不同版本的更新内容
2. **用户分发渠道**建议规划APK的分发渠道和安装指南便于用户获取和安装
3. **更新策略**:建议制定后续版本更新策略和维护计划;
## 小结
1. **专业指导收获**:通过与老师的深入交流,获得了宝贵的专业指导,项目质量得到进一步提升;
2. **改进及时有效**:对老师提出的建议进行了快速响应和改进,问题解决及时有效;
3. **技术成果显著**成功将两个应用打包为APK
4. **打包流程掌握**掌握了Vue项目打包为移动应用的技术流程和配置要点
5. **测试验证充分**对打包后的APK进行了全面测试确保功能完整、运行稳定
6. **项目里程碑**APK打包完成是项目的重要里程碑为后续的演示和交付奠定了基础

@ -0,0 +1,39 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 王磊
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2025-1-4
## 本周任务计划安排
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| --- | --------- | ---- | ------------------------ |
| 1 | 流程闭环所有子流程 | 完成 | 告警→派单→接单→维修→审核→结单,流程形成闭环 |
| 2 | 管理员人工派单 | 完成 | 管理员人工派单是强制的、不受限制的 |
| 3 | 维修工接单 | 完成 | 维修人员能同时接许多工单 |
| 4 | 片区管理 | 完成 | 给片区的分级关联上 |
## 对团队工作的建议
1.****建立项目复盘与知识沉淀机制**** 建议在项目关键节点或里程碑结束后,组织团队进行简短复盘,总结技术实现、协作流程中的亮点与可优化点。
2.****推行轻量级代码评审流程**** 为提升代码质量与团队技术一致性,建议在合并重要功能前,开展轻量级代码评审。可结对或小组内轮换评审,重点关注逻辑清晰度、代码规范与潜在风险。
## 小结
1. **技术收获** 通过完整参与项目流程闭环的实现,深入理解了系统模块间的数据流转与状态控制,并在权限设计、接口优化等方面积累了实战经验。
2. **协作收获** 在与产品、前端、测试等多角色协作中,逐步形成了“需求对齐-接口约定-同步验证”的高效协作模式,增强了跨职能沟通与任务协同能力。
3. **后续重点** 对项目的细节方面进行优化完善。
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台;

@ -0,0 +1,69 @@
# 个人周总结-第十五周
## 基本信息
**姓  名:** 张红卫
**团队名称:** 软1-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-1-4
---
## 本周任务完成情况
| 序号 | 计划内容 | 完成情况 | 说明与成果 |
|----|-------------------------|-------|-----------------------------------------|
| 1 | 参与“解决新建终端关联未检验设备是否存在”问题 | 基本已完成 | 与王磊合作完成前后端校验逻辑,确保新建终端时关联设备必先存在,提升系统可靠性。 |
| 2 | 参与“修改人工派单权限,强制派单”任务 | 基本已完成 | 实现人工派单不受工人状态检查限制的功能,优化了派单流程与权限控制逻辑。 |
| 3 | 参与“修复片区管理逻辑”任务 | 基本已完成 | 修复了片区管理中的业务逻辑错误,确保数据关联与权限验证符合业务流程。 |
| 4 | 参与“完善设备信息修改”功能 | 基本已完成 | 补充并完善设备信息修改功能,加入数据一致性校验与事务处理,保障系统数据完整性。 |
| 5 | 根据老师建议完善其他细节问题 | 基本已完成 | 结合团队反馈与会议建议,对系统中若干交互与显示细节进行优化,提升用户体验。 |
---
## 技术学习与实践总结
### 1. 终端关联与数据校验
- 掌握了前后端协同实现设备关联实时校验的方法,提升了系统数据验证的准确性。
### 2. 权限控制与业务流程优化
- 学习了强制派单场景下的异常处理机制与数据一致性保障方法。
### 3. 片区管理与业务逻辑修复
- 深入理解了片区管理的核心业务逻辑与数据关联,提升了复杂业务调试与修复能力。
### 4. 数据操作完整性与一致性
- 学习了设备信息修改场景下的数据验证策略与事务处理机制。
- 掌握了前后端数据同步与状态管理的实现方法,确保业务操作的数据一致性。
---
## 交付物完成情况
- “新建终端设备校验”功能开发与测试完成
- “强制派单权限修改”功能开发与测试完成
- “片区管理逻辑修复”任务完成
- “设备信息修改功能完善”开发与测试完成
- 系统细节优化与问题修复(根据老师建议)
---
## 协作与沟通情况
- 与王磊在多任务协作中保持高效同步。
- 在权限逻辑、片区修复等复杂问题上,积极与团队成员进行技术方案讨论。
- 参与团队代码审查,并采纳建议优化了部分实现逻辑。
---
## 遇到的问题与解决方案
### 问题1权限逻辑修改后影响原有审批流程
- **解决方案:** 与王磊共同梳理原有流程,设计兼容性方案,增加开关控制,确保新旧功能平滑过渡。
### 问题2片区管理修复过程中发现关联数据异常
- **解决方案:** 深入分析数据表关联关系,编写数据修复脚本,并与后端确认修复逻辑,确保数据一致性。
### 问题3多任务并行导致时间紧张
- **解决方案:** 根据任务优先级重新规划时间,聚焦核心功能开发,利用零散时间处理细节优化。

@ -0,0 +1,27 @@
# 个人周总结-第15周
## 周竞由 - 个人周总结
### 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
### 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- |--------------------|----------|--------------------------------------------------------------------------|
| 1 | 模块功能开发 | 完成 | 按分工完成个人负责的“数据统计分析模块”核心功能开发实现3个核心接口数据查询、统计报表生成、导出编写数据校验逻辑20+条单元测试覆盖率达85%通过本地自测及组员交叉测试未出现功能性bug已提交代码至Git仓库并标注版本号 |
| 2 | 联调问题迭代修复 | 完成 | 已解决第14周遗留的2个非核心接口对接问题字段格式兼容、分页参数逻辑调整本周新联调过程中响应前端问题8个含报表数据渲染异常、导出文件格式错误等平均解决时长1小时核心业务流程联调通过率100%非核心功能联调完成率95% |
| 3 | 中期进度复盘 | 完成 | 2026-01-02如期组织团队中期复盘会议各模块负责人同步进度8个模块中6个已完成核心开发2个模块滞后1天梳理出“第三方接口依赖阻塞”“部分测试用例缺失”等3个关键问题协商确定解决方案协调后端协助对接第三方接口、共享测试用例模板输出《第15周中期复盘报告》并同步全员 |
| 4 | 接口文档完善 | 完成 | 基于开发和联调实际情况补充3个核心接口的参数示例含正常/异常场景、异常响应码说明新增5类常见异常及调用注意事项如请求频率限制、权限要求优化文档排版结构通过团队文档抽检确保与实际接口100%一致 |
### 对团队工作的建议
1. **建议建立测试用例共享库**:本周交叉测试中发现部分组员测试用例设计不全面,建议搭建团队共享测试用例库,按模块分类存储,要求每人提交功能开发时同步上传测试用例,便于交叉测试和后续回归测试,提升测试效率
2. **建议优化第三方接口对接协作**针对本周出现的第三方接口依赖阻塞问题建议指定1名技术负责人统筹对接第三方同步接口文档、调试进度及问题反馈避免多成员重复沟通缩短对接周期
### 小结
1. **技术收获**熟练运用MyBatis-Plus实现复杂数据查询与统计逻辑提升了单元测试用例设计的全面性在联调问题修复中深化了对接口兼容性、异常场景处理的理解掌握了报表导出功能的常见问题解决方案
2. **协作收获**:通过组织中期复盘会议,提升了问题梳理、方案协商的组织协调能力;在交叉测试和联调过程中,加强了与组员、前端的高效沟通,学会通过共享文档、实时同步等方式减少信息差
3. **后续重点**:针对模块功能进行优化迭代(如查询性能优化、界面交互细节调整);配合团队完成系统集成测试,及时修复测试反馈的问题;协助搭建团队测试用例共享库
4. **希望得到的帮助**:希望能获取数据统计模块的性能优化案例(如大数据量下查询提速技巧),或邀请技术导师指导第三方接口对接的容错处理方法,提升系统稳定性设计能力

@ -25,6 +25,7 @@
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>

@ -0,0 +1,46 @@
package com.campus.water.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.io.IOException;
@Configuration
public class HttpsConfig {
// 优先级最高的过滤器实现HTTP→HTTPS跳转
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Filter httpToHttpsRedirectFilter() {
return new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 仅处理HTTP请求跳转到HTTPS
if ("http".equals(req.getScheme())) {
String httpsUrl = "https://" + req.getServerName() + ":" + 8081 + req.getRequestURI();
if (req.getQueryString() != null) {
httpsUrl += "?" + req.getQueryString();
}
res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
res.setHeader("Location", httpsUrl);
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
};
}
}

@ -98,7 +98,7 @@ public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
configuration.setAllowedOriginPatterns(Arrays.asList("**"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setAllowCredentials(true);

@ -41,6 +41,25 @@ public class AdminController {
}
/**
* ID
*/
@GetMapping("/{adminId}")
@PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN')") // 超级/区域管理员可查看
@Operation(summary = "按ID查询单个管理员", description = "根据管理员ID返回完整的管理员信息")
public ResponseEntity<ResultVO<Admin>> getAdminById(@PathVariable String adminId) {
try {
Optional<Admin> adminOpt = adminService.getAdminById(adminId);
if (adminOpt.isPresent()) {
return ResponseEntity.ok(ResultVO.success(adminOpt.get()));
} else {
return ResponseEntity.ok(ResultVO.error(404, "管理员不存在ID" + adminId));
}
} catch (Exception e) {
return ResponseEntity.ok(ResultVO.error(500, "查询管理员失败:" + e.getMessage()));
}
}
/**
*
*/

@ -2,6 +2,7 @@ package com.campus.water.controller.web;
import com.campus.water.entity.Area;
import com.campus.water.service.AreaService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@ -164,4 +165,30 @@ public class AreaController {
.body(buildResponse(500, "查询区域详情失败:" + e.getMessage(), null));
}
}
/**
* ID
*/
@GetMapping("/campus/{areaId}")
@PreAuthorize("hasAnyRole('SUPER_ADMIN', 'AREA_ADMIN', 'REPAIRMAN')") // 与普通区域查询权限一致
@Operation(summary = "按ID查询单个校区", description = "根据区域ID返回校区信息非校区类型返回错误")
public ResponseEntity<Map<String, Object>> getCampusById(@PathVariable String areaId) {
try {
// 先调用已有的service方法查询区域
Area area = areaService.getAreaById(areaId);
// 核心过滤校区类型校验是否为campus
if (Area.AreaType.campus.equals(area.getAreaType())) {
return ResponseEntity.ok(buildResponse(200, "查询校区成功", area));
} else {
return ResponseEntity.badRequest().body(buildResponse(400, "该区域不是校区ID" + areaId, null));
}
} catch (RuntimeException e) {
// 捕获service中抛出的"区域不存在"异常
return ResponseEntity.badRequest().body(buildResponse(400, e.getMessage(), null));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(buildResponse(500, "查询校区失败:" + e.getMessage(), null));
}
}
}

@ -8,7 +8,7 @@ import java.util.List;
@Repository
public interface DeviceRepository extends JpaRepository<Device, String> {
// 根据区域ID查询设备
// 根据区域名称查询设备
List<Device> findByAreaId(String areaId);
// 根据设备类型(枚举)查询

@ -98,7 +98,7 @@ public class DeviceService {
if (areaId != null && deviceType != null) {
return deviceRepository.findByAreaIdAndDeviceType(areaId, deviceType);
} else if (areaId != null) {
return deviceRepository.findByAreaId(areaId);
return deviceRepository.findByAreaId (areaId);
} else if (deviceType != null) {
return deviceRepository.findByDeviceType(deviceType);
} else if (status != null) {

File diff suppressed because it is too large Load Diff

@ -10,13 +10,19 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest"
"test:unit": "vitest",
"cap:sync": "npx cap sync android",
"cap:open": "npx cap open android"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@capacitor/android": "^8.0.0",
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/android": "8.0.0",
"@capacitor/barcode-scanner": "3.0.0",
"@capacitor/camera": "8.0.0",
"@capacitor/cli": "8.0.0",
"@capacitor/core": "8.0.0",
"axios": "^1.7.9",
"jsqr": "^1.4.0",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
@ -29,4 +35,4 @@
"vite-plugin-vue-devtools": "^8.0.5",
"vitest": "^4.0.14"
}
}
}

@ -2,7 +2,7 @@
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'http://120.46.151.248:8080',
baseURL: 'https://120.46.151.248:8081',
headers: {
'Content-Type': 'application/json'
}

@ -356,8 +356,15 @@ const isMapLoading = ref(false)
const mapConfig = reactive({
center: [112.9375, 28.1655],
zoom: 16,
key: import.meta.env.VITE_AMAP_KEY || '', // 使
viewMode: '3D'
key: import.meta.env.VITE_AMAP_KEY || '7e03ef3b43a8cdbb62e3038fc727e035', // 使
viewMode: '3D',
plugins: [
'AMap.Marker',
'AMap.Geolocation',
'AMap.ToolBar',
'AMap.Scale',
'AMap.ControlBar'
]
})
//
@ -383,6 +390,7 @@ const initMap = async () => {
isMapLoading.value = true
try {
console.log('开始加载高德地图...')
//
const AMap = await AMapLoader.load({
key: mapConfig.key,
@ -395,12 +403,13 @@ const initMap = async () => {
'AMap.ControlBar'
]
})
console.log('高德地图加载成功')
//
mapInstance.value = new AMap.Map('mapContainer', {
zoom: mapConfig.zoom,
center: mapConfig.center,
viewMode: mapConfig.viewMode
viewMode: mapConfig.viewMode,
mapStyle: 'amap://styles/normal'
})
//

@ -1,4 +1,4 @@
<!-- ScanPage.vue - 修复样式版本 -->
<!-- ScanPage.vue - JavaScript 扫码版本 -->
<template>
<div class="scan-page">
<!-- 顶部标题栏 -->
@ -12,28 +12,45 @@
<!-- 扫描区域 -->
<div class="scan-section" v-if="!deviceInfo">
<div class="scan-area">
<!-- 扫描框 -->
<div class="scan-frame">
<div class="scan-lines">
<div class="scan-line"></div>
</div>
<div class="scan-corners">
<div class="corner top-left"></div>
<div class="corner top-right"></div>
<div class="corner bottom-left"></div>
<div class="corner bottom-right"></div>
</div>
<!-- 扫码状态提示 -->
<div v-if="scanStatus" class="scan-status" :class="scanStatus.type">
{{ scanStatus.message }}
</div>
<div class="scan-instruction">
请扫描设备二维码
<!-- 扫码按钮 -->
<div class="scan-button-container">
<button class="scan-btn" @click="startScan" :disabled="isScanning">
<span class="scan-icon">📷</span>
<span class="scan-text">
{{ isScanning ? '正在扫码...' : '点击扫码' }}
</span>
</button>
</div>
<div class="scan-hint">
将二维码放入框内即可自动扫描
<!-- 扫描框样式 -->
<div class="scan-frame-container">
<div class="scan-frame">
<div class="scan-lines">
<div class="scan-line"></div>
</div>
<div class="scan-corners">
<div class="corner top-left"></div>
<div class="corner top-right"></div>
<div class="corner bottom-left"></div>
<div class="corner bottom-right"></div>
</div>
</div>
<div class="scan-instruction">
请扫描设备二维码
</div>
<div class="scan-hint">
点击上方按钮开启摄像头扫码
</div>
</div>
<!-- 开发调试按钮更简洁的样式 -->
<!-- 开发调试按钮 -->
<div class="dev-options" v-if="isDevelopment">
<div class="dev-buttons">
<button class="dev-btn" @click="simulateScan('TERM001')">TERM001</button>
@ -67,7 +84,7 @@
</div>
</div>
<!-- 用户信息简洁样式 -->
<!-- 用户信息 -->
<div class="user-info" v-if="userInfo">
<div class="user-label">当前用户:</div>
<div class="user-name">{{ userInfo.username }}</div>
@ -95,7 +112,7 @@
</div>
</div>
<!-- 自定义输入精简样式 -->
<!-- 自定义输入 -->
<div class="custom-amount">
<input
type="number"
@ -197,7 +214,7 @@
</div>
</div>
<!-- 手动输入弹窗保持原有样式 -->
<!-- 手动输入弹窗 -->
<div v-if="showManualDialog" class="dialog-overlay">
<div class="dialog-content">
<div class="dialog-header">
@ -247,20 +264,14 @@
</div>
</template>
<!-- ScanPage.vue - 将所有设备设置为在线 -->
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { deviceService } from '@/services/deviceService'
import { useUserStore } from '@/stores/user'
import jsQR from 'jsqr'
const router = useRouter()
const userStore = useUserStore()
//
const isDevelopment = ref(true)
//
//
const deviceInfo = ref(null)
const showManualDialog = ref(false)
const manualDeviceId = ref('')
@ -273,6 +284,16 @@ const showResultDialog = ref(false)
const resultStatus = ref('')
const resultTitle = ref('')
const resultMessage = ref('')
const isScanning = ref(false)
const scanStatus = ref(null)
const router = useRouter()
const userStore = useUserStore()
//
const isDevelopment = computed(() => {
return import.meta.env.DEV;
});
//
const userInfo = computed(() => {
@ -280,7 +301,7 @@ const userInfo = computed(() => {
username: userStore.username || '用户',
studentId: userStore.studentId || '未登录'
}
})
});
//
const waterAmounts = [
@ -289,13 +310,13 @@ const waterAmounts = [
{ value: 750, price: '免费' }
]
// 线
//
const mockDevices = {
'TERM001': {
id: 'TERM001',
name: '教学楼饮水机',
deviceId: 'WM001',
status: 'online', // online
status: 'online',
statusText: '在线',
location: '教学楼1F大厅',
waterQuality: {
@ -307,7 +328,7 @@ const mockDevices = {
id: 'TERM002',
name: '学生公寓饮水机',
deviceId: 'WM002',
status: 'online', // online
status: 'online',
statusText: '在线',
location: '天马学生公寓1F',
waterQuality: {
@ -319,7 +340,7 @@ const mockDevices = {
id: 'TERM003',
name: '图书馆饮水机',
deviceId: 'WM003',
status: 'online', // online
status: 'online',
statusText: '在线',
location: '图书馆2F',
waterQuality: {
@ -329,22 +350,319 @@ const mockDevices = {
}
}
onMounted(() => {
console.log('扫码页面加载')
})
//
const checkCameraSupport = () => {
const isSecure = window.isSecureContext || window.location.protocol === 'https:'
const hasMediaDevices = 'mediaDevices' in navigator
const hasGetUserMedia = 'getUserMedia' in navigator.mediaDevices
console.log('摄像头支持检查:', {
isSecure,
hasMediaDevices,
hasGetUserMedia,
protocol: window.location.protocol
})
return isSecure && hasMediaDevices && hasGetUserMedia
}
// JavaScript
const startScan = async () => {
console.log('开始纯 JavaScript 扫码')
if (!checkCameraSupport()) {
alert('您的浏览器不支持摄像头访问,请使用手动输入')
showManualDialog.value = true
return
}
//
resetScan()
isScanning.value = true
scanStatus.value = {
type: 'info',
message: '正在准备扫码...'
}
try {
//
const scannerContainer = document.createElement('div')
scannerContainer.className = 'scanner-container'
scannerContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`
//
const title = document.createElement('div')
title.textContent = '请对准二维码'
title.style.cssText = `
color: white;
font-size: 18px;
margin-bottom: 20px;
font-weight: bold;
`
//
const videoContainer = document.createElement('div')
videoContainer.style.cssText = `
position: relative;
width: 300px;
height: 300px;
border: 2px solid #1890ff;
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
`
//
const video = document.createElement('video')
video.style.cssText = `
width: 100%;
height: 100%;
object-fit: cover;
`
video.setAttribute('playsinline', 'true')
video.setAttribute('autoplay', 'true')
//
const scanFrame = document.createElement('div')
scanFrame.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
`
// 线
const scanLine = document.createElement('div')
scanLine.style.cssText = `
position: absolute;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #1890ff, transparent);
top: 0;
animation: scanLineMove 2s infinite linear;
`
//
const style = document.createElement('style')
style.textContent = `
@keyframes scanLineMove {
0% { top: 0; }
100% { top: 100%; }
}
`
style.id = 'scanner-style'
//
const closeBtn = document.createElement('button')
closeBtn.textContent = '关闭'
closeBtn.style.cssText = `
padding: 12px 40px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
margin-top: 20px;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
transition: all 0.3s;
`
closeBtn.onmouseenter = () => {
closeBtn.style.transform = 'translateY(-2px)'
closeBtn.style.boxShadow = '0 6px 16px rgba(24, 144, 255, 0.4)'
}
closeBtn.onmouseleave = () => {
closeBtn.style.transform = 'translateY(0)'
closeBtn.style.boxShadow = '0 4px 12px rgba(24, 144, 255, 0.3)'
}
//
scanFrame.appendChild(scanLine)
videoContainer.appendChild(video)
videoContainer.appendChild(scanFrame)
scannerContainer.appendChild(title)
scannerContainer.appendChild(videoContainer)
scannerContainer.appendChild(closeBtn)
document.head.appendChild(style)
document.body.appendChild(scannerContainer)
//
scanStatus.value.message = '正在访问摄像头...'
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // 使
width: { ideal: 1280 },
height: { ideal: 720 }
}
})
//
video.srcObject = stream
//
await new Promise((resolve) => {
video.onloadedmetadata = () => {
video.play()
resolve()
}
})
scanStatus.value.message = '请对准二维码...'
// Canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let scanning = true
//
const scanFrameFunc = () => {
if (!scanning) return
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth
canvas.height = video.videoHeight
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// 使 jsQR
try {
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
})
if (code) {
console.log('找到二维码:', code.data)
scanning = false
//
stream.getTracks().forEach(track => track.stop())
//
document.body.removeChild(scannerContainer)
const styleEl = document.getElementById('scanner-style')
if (styleEl) {
document.head.removeChild(styleEl)
}
//
loadDeviceInfo(code.data)
return
}
} catch (error) {
console.warn('二维码分析错误:', error)
}
}
requestAnimationFrame(scanFrameFunc)
}
//
scanFrameFunc()
//
closeBtn.onclick = () => {
scanning = false
stream.getTracks().forEach(track => track.stop())
document.body.removeChild(scannerContainer)
const styleEl = document.getElementById('scanner-style')
if (styleEl) {
document.head.removeChild(styleEl)
}
isScanning.value = false
scanStatus.value = null
}
//
video.onerror = (error) => {
console.error('视频播放错误:', error)
scanning = false
stream.getTracks().forEach(track => track.stop())
document.body.removeChild(scannerContainer)
const styleEl = document.getElementById('scanner-style')
if (styleEl) {
document.head.removeChild(styleEl)
}
isScanning.value = false
scanStatus.value = {
type: 'error',
message: '摄像头访问失败'
}
}
} catch (error) {
console.error('扫码失败:', error)
isScanning.value = false
let errorMessage = '扫码失败'
if (error.name === 'NotAllowedError') {
errorMessage = '摄像头权限被拒绝,请在浏览器设置中开启权限'
} else if (error.name === 'NotFoundError') {
errorMessage = '未找到摄像头设备'
} else if (error.name === 'NotReadableError') {
errorMessage = '摄像头被其他应用占用'
} else if (error.name === 'OverconstrainedError') {
errorMessage = '摄像头配置不满足要求'
} else if (error.name === 'SecurityError') {
errorMessage = '需要在 HTTPS 或 localhost 环境下使用摄像头'
} else {
errorMessage = error.message || '未知错误'
}
scanStatus.value = {
type: 'error',
message: errorMessage
}
// 3
setTimeout(() => {
scanStatus.value = null
}, 3000)
//
setTimeout(() => {
showManualDialog.value = true
}, 1000)
}
}
//
const simulateScan = async (deviceId) => {
console.log('模拟扫码:', deviceId)
//
resetScan()
//
scanStatus.value = {
type: 'info',
message: '模拟扫码中...'
}
setTimeout(async () => {
await loadDeviceInfo(deviceId)
}, 100)
scanStatus.value = null
}, 500)
}
// 线
//
const loadDeviceInfo = async (terminalId) => {
try {
const result = await deviceService.getTerminalInfo(terminalId)
@ -354,30 +672,37 @@ const loadDeviceInfo = async (terminalId) => {
deviceInfo.value = {
id: terminalId,
name: data.terminalName || mockDevices[terminalId]?.name || '饮水机',
deviceId: data.deviceId || mockDevices[terminalId]?.deviceId,
status: 'online', // 线
name: data.terminalName || '饮水机',
deviceId: data.deviceId || '未知',
status: 'online',
statusText: '在线',
location: data.location || mockDevices[terminalId]?.location || '',
location: data.location || '未知位置',
waterQuality: {
tapWater: data.rawWaterTds || mockDevices[terminalId]?.waterQuality.tapWater || '--',
pureWater: data.pureWaterTds || mockDevices[terminalId]?.waterQuality.pureWater || '--'
tapWater: data.rawWaterTds || '--',
pureWater: data.pureWaterTds || '--'
}
}
} else {
// 使线
deviceInfo.value = {
...mockDevices[terminalId],
// 使
deviceInfo.value = mockDevices[terminalId] || {
id: terminalId,
name: `${terminalId}饮水机`,
deviceId: `WM${terminalId.slice(-3)}`,
status: 'online',
statusText: '在线'
statusText: '在线',
location: '校园内',
waterQuality: {
tapWater: '285',
pureWater: '15'
}
}
}
} catch (error) {
console.error('获取设备信息失败,使用模拟数据:', error)
// 使线
console.error('获取设备信息失败:', error)
deviceInfo.value = mockDevices[terminalId] || {
id: terminalId,
name: `${terminalId}饮水机`,
deviceId: `WM${terminalId.slice(-3)}`,
status: 'online',
statusText: '在线',
location: '未知位置',
@ -387,6 +712,8 @@ const loadDeviceInfo = async (terminalId) => {
}
}
}
isScanning.value = false
}
//
@ -407,7 +734,7 @@ const useCustomAmount = () => {
}
}
//
//
const confirmWater = async () => {
if (!selectedAmount.value) {
alert('请选择取水量')
@ -473,7 +800,6 @@ const startWaterProcess = async () => {
// API
const callWaterUsageAPI = async () => {
//
console.log('调用取水API水量:', selectedAmount.value)
try {
@ -502,14 +828,12 @@ const callWaterUsageAPI = async () => {
const completeWaterProcess = async () => {
isProcessing.value = false
showResult(
'success',
'取水成功',
`您已成功取水 ${selectedAmount.value}ml`
)
setTimeout(() => {
resetScan()
showResultDialog.value = false
@ -530,10 +854,8 @@ const closeResultDialog = () => {
resetScan()
}
///
//
const recordWaterHistory = (data) => {
//
if (!deviceInfo.value || !deviceInfo.value.id) {
console.log('设备信息不完整,不保存历史记录')
return
@ -550,45 +872,11 @@ const recordWaterHistory = (data) => {
location: deviceInfo.value.location || ''
}
//
const existingHistory = JSON.parse(localStorage.getItem('waterHistory') || '[]')
// 5
const now = new Date()
const fiveSecondsAgo = new Date(now.getTime() - 5000) // 5
const duplicateIndex = existingHistory.findIndex(record => {
const recordTime = new Date(record.timestamp)
return (
record.deviceId === deviceInfo.value.id &&
recordTime >= fiveSecondsAgo &&
recordTime <= now
)
})
if (duplicateIndex !== -1) {
// 5
console.log('发现重复记录,替换而不是新增')
existingHistory[duplicateIndex] = history
} else {
//
console.log('保存新记录')
existingHistory.unshift(history)
}
// terminalID
const deviceRecords = {}
const finalHistory = existingHistory.filter(record => {
if (!deviceRecords[record.deviceId]) {
deviceRecords[record.deviceId] = true
return true
}
return false
})
existingHistory.unshift(history)
//
const limitedHistory = finalHistory.slice(0, 20)
const limitedHistory = existingHistory.slice(0, 20)
localStorage.setItem('waterHistory', JSON.stringify(limitedHistory))
console.log('更新后的历史记录:', limitedHistory)
}
@ -629,13 +917,13 @@ const submitManualInput = async () => {
closeManualDialog()
}
//
//
const resetScan = () => {
deviceInfo.value = null
selectedAmount.value = null
customAmount.value = ''
isProcessing.value = false
progress.value = 0
isScanning.value = false
scanStatus.value = null
}
//
@ -660,6 +948,12 @@ const goToPage = (page) => {
break
}
}
onMounted(() => {
console.log('扫码页面已加载')
console.log('摄像头支持:', checkCameraSupport())
console.log('开发环境:', isDevelopment.value)
})
</script>
<style scoped>
@ -733,6 +1027,11 @@ const goToPage = (page) => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 扫描框容器 */
.scan-frame-container {
margin-top: 20px;
}
/* 扫描框 */
.scan-frame {
width: 250px;
@ -825,6 +1124,51 @@ const goToPage = (page) => {
color: #666;
}
/* 扫码状态提示 */
.scan-status {
padding: 10px 15px;
margin-bottom: 15px;
border-radius: 8px;
font-size: 14px;
text-align: center;
animation: fadeIn 0.3s ease;
}
.scan-status.error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.scan-status.warning {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffe0b2;
}
.scan-status.info {
background-color: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.scan-status.success {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 开发调试按钮 */
.dev-options {
margin-top: 16px;
@ -852,6 +1196,48 @@ const goToPage = (page) => {
color: white;
}
/* 扫码按钮容器 */
.scan-button-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.scan-btn {
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50px;
padding: 20px 30px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
}
.scan-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}
.scan-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.scan-icon {
font-size: 24px;
margin-bottom: 5px;
}
.scan-text {
font-size: 14px;
}
/* 手动输入 */
.manual-input {
background: white;
@ -1308,7 +1694,7 @@ const goToPage = (page) => {
/* 手动输入弹窗 */
.dialog-overlay {
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
@ -1317,15 +1703,10 @@ const goToPage = (page) => {
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.dialog-content {
background: white;
border-radius: 16px;
@ -1384,7 +1765,6 @@ const goToPage = (page) => {
border: 1px solid #e8e8e8;
border-radius: 6px;
font-size: 14px;
margin-bottom: 8px;
}
.device-input:focus {

@ -16,12 +16,12 @@
</div>
<div class="form-group">
<label for="studentId">学号</label>
<label for="studentId">姓名</label>
<input
type="text"
id="studentId"
v-model="loginForm.studentId"
placeholder="请输入学号"
placeholder="请输入姓名"
@keyup.enter="handleLogin"
/>
</div>
@ -280,7 +280,7 @@ const showConfirmPassword = ref(false)
//
const validateLogin = () => {
if (!loginForm.studentId.trim()) {
alert('请输入学号')
alert('请输入姓名')
return false
}
if (!loginForm.password) {

@ -7,7 +7,7 @@ module.exports = defineConfig({
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
target: 'https://120.46.151.248:8081', // 后端地址
changeOrigin: true
}
}

@ -35,4 +35,12 @@ server:
charset: UTF-8
enabled: true
force: true
port: 8080
port: 8081
ssl:
key-store: classpath:keystore.p12
key-store-password: 123456
key-store-type: PKCS12
key-alias: myhttps
additional-ports:
- port: 8080
protocol: HTTP

Binary file not shown.

@ -1,2 +1,2 @@
VITE_API_BASE_URL=http://localhost:8080
VITE_API_BASE_URL=https://120.46.151.248:8081
VITE_APP_ORIGIN=http://localhost:5173

@ -1 +1 @@
VITE_API_BASE_URL=http://localhost:8080
VITE_API_BASE_URL=https://120.46.151.248:8081

@ -0,0 +1,4 @@
# 生产环境
VITE_API_BASE_URL=/api
VITE_APP_TITLE=饮水机管理系统
NODE_ENV=production

File diff suppressed because it is too large Load Diff

@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vite build",
"type-check": "vue-tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
@ -17,7 +18,7 @@
"devDependencies": {
"@tsconfig/node24": "^24.0.3",
"@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.2.2",
"typescript": "^5.9.3",
"vite": "^5.1.6",
"vue-tsc": "^1.8.27"
}

@ -2,14 +2,16 @@
import type { LoginRequest, LoginResponse, LoginVO } from './types/auth'
// 真实的登录API调用
// auth.ts
export const realLoginApi = async (data: LoginRequest): Promise<LoginResponse> => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
// 使用相对路径让Vite代理处理
const apiUrl = '/api/common/login'
console.log('🌐 调用登录接口:', `${API_BASE_URL}/api/common/login`)
console.log('🌐 调用登录接口:', apiUrl)
console.log('📤 请求数据:', data)
try {
const response = await fetch(`${API_BASE_URL}/api/common/login`, {
const response = await fetch(apiUrl, { // ✅ 使用相对路径
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -22,7 +24,7 @@ export const realLoginApi = async (data: LoginRequest): Promise<LoginResponse> =
if (!response.ok) {
const errorText = await response.text()
console.error('❌ 响应内容:', errorText)
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`)
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result: LoginResponse = await response.json()
@ -32,7 +34,7 @@ export const realLoginApi = async (data: LoginRequest): Promise<LoginResponse> =
} catch (error: any) {
console.error('❌ 登录接口调用失败:', error)
throw new Error(`登录失败: ${error.message}`)
throw error // 直接抛出原错误
}
}

@ -1,12 +1,9 @@
// src/api/request.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
// 统一的 fetch 封装
export async function request<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
// 处理日志数据GET/HEAD 方法不显示 body
// 处理日志数据
const method = options.method?.toUpperCase() || 'GET';
const logData: Record<string, any> = {
method,
@ -18,7 +15,7 @@ export async function request<T>(
logData.body = options.body ? JSON.parse(options.body as string) : undefined;
}
console.log(`🌐 发送请求: ${API_BASE_URL}${url}`, logData)
console.log(`🌐 发送请求: ${url}`, logData) // ✅ 改为相对路径
const defaultOptions: RequestInit = {
headers: {
@ -53,15 +50,15 @@ export async function request<T>(
delete fetchOptions.body;
}
const response = await fetch(`${API_BASE_URL}${url}`, fetchOptions)
// ✅ 使用相对路径,让 Vite 代理处理
const response = await fetch(url, fetchOptions)
console.log('📥 响应状态:', response.status, response.statusText)
// 尝试读取响应文本(无论成功与否)
// 尝试读取响应文本
let responseText = ''
try {
responseText = await response.text()
//console.log('📥 响应内容:', responseText)
} catch (e) {
console.log('📥 无法读取响应文本')
}
@ -92,7 +89,6 @@ export async function request<T>(
throw new Error(`响应不是有效的 JSON: ${responseText}`)
}
} else {
// 没有响应体的情况(如 204 No Content
return {} as T
}
} catch (error: any) {

@ -2,6 +2,8 @@
export interface LoginRequest {
username: string
password: string
userType?: string
rememberMe?: boolean
}
export interface LoginVO {

@ -1,10 +1,21 @@
// src/api/types/workorder.ts
// src/api/types/workorder.ts
export interface WorkOrder {
orderId: string
deviceId: string
areaId: string
orderType: 'repair' | 'maintenance' | 'inspection' // 添加此属性
description: string
priority: 'low' | 'medium' | 'high' | 'urgent' // 添加此属性
status: 'pending' | 'processing' | 'reviewing' | 'completed' | 'timeout'
createdTime?: string
assignedRepairmanId?: string
createdTime?: string
grabbedTime?: string // 添加此属性
deadline?: string // 添加此属性
completedTime?: string // 添加此属性
dealNote?: string // 添加此属性
imgUrl?: string // 添加此属性
createdBy: string // 添加此属性
updatedTime?: string // 添加此属性
alertId?: string // 添加此属性
}

@ -4,28 +4,107 @@
<h1 class="system-title">校园矿化水系统</h1>
</div>
<div class="header-right">
<div class="user-info" @click="goToProfile">
<div class="user-avatar">
<img :src="userAvatar" alt="用户头像" />
<!-- 用户信息下拉菜单 -->
<div class="user-dropdown">
<div class="user-info" @click="toggleDropdown">
<div class="user-avatar">
<img :src="userAvatar" alt="用户头像" />
</div>
<span class="user-name">{{ currentUser?.username || '未登录' }}</span>
<div class="dropdown-arrow"></div>
</div>
<!-- 下拉菜单 -->
<div v-if="showDropdown" class="dropdown-menu" @click="closeDropdown">
<div class="user-menu-header">
<div class="user-menu-avatar">
<img :src="userAvatar" alt="用户头像" />
</div>
<div class="user-menu-info">
<div class="user-menu-name">{{ currentUser?.username || '未登录' }}</div>
<div class="user-menu-role">{{ currentUser?.role || '普通用户' }}</div>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" @click="switchUser">
<i class="icon-switch"></i>
<span>切换用户</span>
</div>
<div class="dropdown-item" @click="logout">
<i class="icon-logout"></i>
<span>退出登录</span>
</div>
</div>
<span class="user-name">张管理员</span>
<div class="dropdown-arrow"></div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const userAvatar = ref('images/用户界面/u106.jpg') //
// 使
const userAvatar = ref('https://ui-avatars.com/api/?name=User&background=667eea&color=fff&size=36')
const router = useRouter()
const authStore = useAuthStore()
const showDropdown = ref(false)
//
const currentUser = computed(() => {
const user = authStore.userInfo
if (user) {
// URL
const name = user.realName || user.username || 'User'
userAvatar.value = `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=667eea&color=fff&size=36`
}
return user
})
//
const toggleDropdown = (event: Event) => {
event.stopPropagation()
showDropdown.value = !showDropdown.value
}
//
const closeDropdown = (event: Event) => {
event.stopPropagation()
//
}
//
const goToProfile = () => {
router.push('/home/profile')
//
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement
if (!target.closest('.user-dropdown')) {
showDropdown.value = false
}
}
//
const switchUser = () => {
showDropdown.value = false
authStore.logout() //
router.push('/')
}
// 退
const logout = () => {
showDropdown.value = false
authStore.logout()
router.push('/')
}
//
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
//
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
@ -39,6 +118,7 @@ const goToProfile = () => {
padding: 0 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
position: relative;
}
.system-title {
@ -47,6 +127,11 @@ const goToProfile = () => {
margin: 0;
}
.user-dropdown {
position: relative;
display: inline-block;
}
.user-info {
display: flex;
align-items: center;
@ -84,4 +169,89 @@ const goToProfile = () => {
font-size: 12px;
opacity: 0.8;
}
</style>
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
min-width: 240px;
z-index: 1001;
margin-top: 8px;
overflow: hidden;
}
.user-menu-header {
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
}
.user-menu-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f0f0f0;
overflow: hidden;
}
.user-menu-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-menu-info {
flex: 1;
}
.user-menu-name {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.user-menu-role {
font-size: 12px;
color: #666;
}
.dropdown-divider {
height: 1px;
background: #f0f0f0;
margin: 4px 0;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.3s;
color: #333;
font-size: 14px;
}
.dropdown-item:hover {
background: #f5f5f5;
}
.dropdown-item i {
width: 16px;
height: 16px;
display: inline-block;
}
.icon-switch::before {
content: "🔄";
}
.icon-logout::before {
content: "🚪";
}
</style>

@ -93,12 +93,7 @@ const menuItems: MenuItem[] = [
{ name: '校区', route: '/home/area/campus' }
]
},
{
id: 6,
name: '个人信息',
icon: '👤',
route: '/home/profile'
}
]
//

@ -216,16 +216,7 @@ const router = createRouter({
title: '校园片区'
}
},
// 个人信息路由
{
path: 'profile',
name: 'profile',
component: () => import('../views/Profile.vue'),
meta: {
title: '个人信息',
requiresAuth: true
}
}
]
},
{

@ -20,37 +20,48 @@ export const useAuthStore = defineStore('auth', () => {
// 真实登录接口调用
const login = async (loginData: LoginRequest) => {
try {
// 调用真实后端接口
const response: ResultVO<LoginVO> = await authApi.login(loginData)
try {
const response: ResultVO<LoginVO> = await authApi.login(loginData)
// 检查响应状态
if (response.code !== 200) {
throw new Error(response.message || '登录失败')
}
if (response.code !== 200 || !response.data) {
throw new Error(response.message || '登录失败')
}
// 保存 token 和用户信息
token.value = response.data.token
userInfo.value = response.data.userInfo
isLoggedIn.value = true
// 存储到 localStorage如果用户选择记住我
if (loginData.rememberMe) {
localStorage.setItem('token', response.data.token)
localStorage.setItem('userInfo', JSON.stringify(response.data.userInfo))
localStorage.setItem('rememberMe', 'true')
} else {
sessionStorage.setItem('token', response.data.token)
sessionStorage.setItem('userInfo', JSON.stringify(response.data.userInfo))
localStorage.removeItem('rememberMe')
}
// 直接使用后端返回的数据结构
const responseData = response.data
if (!responseData.token) {
throw new Error('登录响应数据不完整')
}
return response
} catch (error: any) {
console.error('登录失败:', error)
throw error
// 转换后端数据结构为前端期望的格式
token.value = responseData.token
userInfo.value = {
id: parseInt(responseData.userId) || 0,
username: responseData.username,
role: responseData.userType,
areaId: responseData.areaId
}
isLoggedIn.value = true
// 存储到 localStorage如果用户选择记住我
if (loginData.rememberMe) {
localStorage.setItem('token', responseData.token)
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
localStorage.setItem('rememberMe', 'true')
} else {
sessionStorage.setItem('token', responseData.token)
sessionStorage.setItem('userInfo', JSON.stringify(userInfo.value))
localStorage.removeItem('rememberMe')
}
return response
} catch (error: any) {
console.error('登录失败:', error)
throw error
}
}
// 退出登录
const logout = () => {
@ -103,4 +114,4 @@ export const useAuthStore = defineStore('auth', () => {
initialize,
checkAuth,
}
})
})

@ -7,20 +7,10 @@
</div>
<form class="login-form" @submit.prevent="handleLogin">
<!-- 用户类型选择 -->
<!-- 用户类型显示改为静态文本 -->
<div class="form-group">
<label for="userType">用户类型</label>
<select
id="userType"
v-model="loginForm.userType"
class="form-input"
:disabled="loading"
required
>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="repairer">维修人员</option>
</select>
<label>用户类型</label>
<div class="user-type-display">管理员</div>
</div>
<div class="form-group">
@ -54,7 +44,6 @@
<input type="checkbox" v-model="loginForm.rememberMe" />
<span class="checkbox-text">记住我</span>
</label>
<a href="#" class="forgot-password">忘记密码</a>
</div>
<button type="submit" class="login-button" :disabled="loading">
@ -71,6 +60,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'

@ -1,629 +0,0 @@
<!-- src/views/profile/Profile.vue -->
<template>
<div class="profile-page">
<!-- 页面标题和面包屑 -->
<div class="page-header">
<h2>个人信息</h2>
<div class="breadcrumb">校园矿化水平台 / 个人中心 / 个人信息</div>
</div>
<div class="profile-container">
<!-- 左侧头像和基本信息 -->
<div class="profile-sidebar">
<div class="avatar-container">
<img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="user-avatar">
<button class="change-avatar-btn" @click="triggerAvatarUpload"></button>
<input
type="file"
ref="avatarInput"
class="avatar-input"
accept="image/*"
@change="handleAvatarUpload"
>
</div>
<div class="basic-info">
<div class="info-item">
<span class="label">用户名</span>
<span class="value">{{ userInfo.username }}</span>
</div>
<div class="info-item">
<span class="label">真实姓名</span>
<span class="value">{{ userInfo.realName }}</span>
</div>
<div class="info-item">
<span class="label">身份</span>
<span class="value role-tag">{{ formatRole(userInfo.role) }}</span>
</div>
<div class="info-item">
<span class="label">联系电话</span>
<span class="value">{{ userInfo.phone || '未设置' }}</span>
</div>
<div class="info-item">
<span class="label">账号状态</span>
<span class="value status-tag" :class="userInfo.status">
{{ userInfo.status === 'active' ? '启用' : '禁用' }}
</span>
</div>
<div class="info-item">
<span class="label">最后登录</span>
<span class="value">{{ formatDate(userInfo.lastLoginTime) }}</span>
</div>
</div>
</div>
<!-- 右侧信息编辑和密码修改 -->
<div class="profile-content">
<!-- 个人信息编辑表单 -->
<div class="profile-card">
<div class="card-header">
<h3>编辑个人信息</h3>
</div>
<div class="card-body">
<form @submit.prevent="updateProfile">
<div class="form-row">
<div class="form-item">
<label>真实姓名</label>
<input
type="text"
v-model="profileForm.realName"
placeholder="请输入真实姓名"
required
>
</div>
<div class="form-item">
<label>联系电话</label>
<input
type="tel"
v-model="profileForm.phone"
placeholder="请输入联系电话"
pattern="^1[3-9]\d{9}$"
>
</div>
</div>
<div class="form-row">
<div class="form-item full-width">
<label>备注信息</label>
<textarea
v-model="profileForm.remark"
placeholder="请输入备注信息(选填)"
rows="3"
></textarea>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-save">保存修改</button>
</div>
</form>
</div>
</div>
<!-- 密码修改表单 -->
<div class="profile-card password-card">
<div class="card-header">
<h3>修改密码</h3>
</div>
<div class="card-body">
<form @submit.prevent="changePassword">
<div class="form-row">
<div class="form-item full-width">
<label>当前密码</label>
<input
type="password"
v-model="passwordForm.oldPassword"
placeholder="请输入当前密码"
required
>
</div>
</div>
<div class="form-row">
<div class="form-item full-width">
<label>新密码</label>
<input
type="password"
v-model="passwordForm.newPassword"
placeholder="请输入新密码至少8位包含字母和数字"
pattern="^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$"
required
>
</div>
</div>
<div class="form-row">
<div class="form-item full-width">
<label>确认新密码</label>
<input
type="password"
v-model="passwordForm.confirmPassword"
placeholder="请再次输入新密码"
required
>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-change-pwd">修改密码</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 操作成功提示 -->
<div class="toast" v-if="showToast">
{{ toastMessage }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
// 使Element Plus
//
const defaultAvatar = 'images/avatar/default-avatar.png'
//
interface UserInfo {
id: string
username: string
realName: string
role: 'student' | 'teacher' | 'visitor' | 'admin' | 'maintenance'
phone?: string
avatar?: string
status: 'active' | 'disabled'
lastLoginTime: string
remark?: string
}
//
const avatarInput = ref<HTMLInputElement | null>(null)
const showToast = ref(false)
const toastMessage = ref('')
//
const userInfo = ref<UserInfo>({
id: '1',
username: 'admin01',
realName: '管理员',
role: 'admin',
phone: '13800138000',
avatar: '',
status: 'active',
lastLoginTime: '2025-12-05 09:23:45',
remark: '系统超级管理员'
})
//
const profileForm = reactive({
realName: '',
phone: '',
remark: ''
})
//
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
//
onMounted(() => {
profileForm.realName = userInfo.value.realName
profileForm.phone = userInfo.value.phone || ''
profileForm.remark = userInfo.value.remark || ''
})
//
const formatRole = (role: string) => {
const roleMap: Record<string, string> = {
student: '学生',
teacher: '老师',
visitor: '游客',
admin: '管理员',
maintenance: '维修人员'
}
return roleMap[role] || '未知身份'
}
//
const formatDate = (dateStr: string) => {
if (!dateStr) return '暂无记录'
return new Date(dateStr).toLocaleString('zh-CN')
}
//
const triggerAvatarUpload = () => {
avatarInput.value?.click()
}
//
const handleAvatarUpload = (e: Event) => {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
//
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
showToastMessage('请上传图片格式的文件!')
return
}
if (!isLt2M) {
showToastMessage('头像大小不能超过2MB')
return
}
//
const reader = new FileReader()
reader.onload = (e) => {
userInfo.value.avatar = e.target?.result as string
showToastMessage('头像上传成功!')
}
reader.readAsDataURL(file)
// input便
target.value = ''
}
}
//
const updateProfile = () => {
//
if (!profileForm.realName.trim()) {
showToastMessage('真实姓名不能为空!')
return
}
//
if (profileForm.phone && !/^1[3-9]\d{9}$/.test(profileForm.phone)) {
showToastMessage('请输入正确的手机号码!')
return
}
//
userInfo.value.realName = profileForm.realName
userInfo.value.phone = profileForm.phone
userInfo.value.remark = profileForm.remark
showToastMessage('个人信息修改成功!')
}
//
const changePassword = () => {
//
if (!passwordForm.oldPassword) {
showToastMessage('请输入当前密码!')
return
}
if (passwordForm.newPassword.length < 8) {
showToastMessage('新密码长度不能少于8位')
return
}
if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(passwordForm.newPassword)) {
showToastMessage('新密码必须包含字母和数字!')
return
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
showToastMessage('两次输入的新密码不一致!')
return
}
//
showToastMessage('密码修改成功,请重新登录!')
//
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
}
//
const showToastMessage = (message: string) => {
toastMessage.value = message
showToast.value = true
// 3
setTimeout(() => {
showToast.value = false
}, 3000)
}
</script>
<style scoped>
/* 基础样式 */
.profile-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.breadcrumb {
color: #666;
font-size: 14px;
}
/* 布局容器 */
.profile-container {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
/* 左侧侧边栏 */
.profile-sidebar {
width: 300px;
flex-shrink: 0;
}
.avatar-container {
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 16px;
}
.user-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 12px;
}
.change-avatar-btn {
background: #667eea;
color: white;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.change-avatar-btn:hover {
background: #556cd6;
}
.avatar-input {
display: none;
}
.basic-info {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.info-item {
display: flex;
margin-bottom: 12px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
color: #666;
flex-shrink: 0;
}
.value {
color: #333;
flex: 1;
}
/* 角色和状态标签 */
.role-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.disabled {
background-color: #f5f5f5;
color: #8c8c8c;
}
/* 右侧内容区 */
.profile-content {
flex: 1;
min-width: 500px;
}
.profile-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 24px;
overflow: hidden;
}
.password-card {
margin-bottom: 0;
}
.card-header {
padding: 16px 20px;
background: #f8f9fa;
border-bottom: 1px solid #f0f0f0;
}
.card-header h3 {
margin: 0;
font-size: 16px;
color: #333;
font-weight: 600;
}
.card-body {
padding: 20px;
}
/* 表单样式 */
.form-row {
display: flex;
gap: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.form-item {
flex: 1;
min-width: 200px;
}
.form-item.full-width {
flex: 1 1 100%;
min-width: 100%;
}
.form-item label {
display: block;
margin-bottom: 8px;
color: #666;
font-size: 14px;
font-weight: 500;
}
.form-item input,
.form-item textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.form-item input:focus,
.form-item textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.btn-save, .btn-change-pwd {
background: #42b983;
color: white;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn-save:hover, .btn-change-pwd:hover {
background: #359e75;
}
.btn-change-pwd {
background: #667eea;
}
.btn-change-pwd:hover {
background: #556cd6;
}
/* 提示框样式 */
.toast {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 4px;
z-index: 1000;
animation: fadeIn 0.3s, fadeOut 0.3s 2.7s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-20px); }
}
/* 响应式调整 */
@media (max-width: 900px) {
.profile-container {
flex-direction: column;
}
.profile-sidebar {
width: 100%;
}
.profile-content {
min-width: 100%;
}
}
@media (max-width: 576px) {
.form-row {
flex-direction: column;
gap: 16px;
}
.form-item {
min-width: 100%;
}
}
</style>

@ -27,6 +27,11 @@
</div>
</div>
<!-- 空闲管理员提示 -->
<div v-if="availableAdmins.length === 0" class="no-available-admins">
所有区域管理员都有负责的校区当前无空闲区域管理员
</div>
<!-- 校区表格 -->
<div class="card">
<table class="campus-table">
@ -46,16 +51,14 @@
<td>{{ campus.areaName }}</td>
<td>{{ getCityName(campus.parentAreaId) }}</td> <!-- 显示所属市区名称 -->
<td>{{ campus.address }}</td>
<td>{{ getManagerName(campus.manager) }}</td>
<td>
<span v-if="managerNames[campus.areaId]">{{ managerNames[campus.areaId] }}</span>
<span v-else-if="loadingManagerNames.includes(campus.areaId)">加载中...</span>
<span v-else></span>
</td>
<td>{{ campus.managerPhone }}</td>
<td>{{ formatDate(campus.createdTime) }}</td>
<td class="operation-buttons">
<button
class="btn-stats"
@click="handleViewStats(campus.areaId, campus.areaName)"
>
统计
</button>
<button
class="btn-edit"
@click="handleEdit(campus)"
@ -147,9 +150,19 @@
required
>
<option value="">请选择负责人</option>
<option v-for="admin in adminList" :key="admin.adminId" :value="admin">
{{ admin.adminName }}
</option>
<!-- 显示空闲管理员提示 -->
<optgroup v-if="availableAdmins.length === 0" label="提示">
<option value="" disabled>当前无空闲区域管理员</option>
</optgroup>
<optgroup label="空闲区域管理员" v-else>
<option
v-for="admin in availableAdmins"
:key="admin.adminId"
:value="admin"
>
{{ admin.adminName }} ({{ admin.phone }})
</option>
</optgroup>
</select>
</div>
<div class="form-item">
@ -321,7 +334,7 @@ interface AreaDeviceStatsVO {
//
const campusList = ref<Area[]>([]) //
const cityList = ref<Area[]>([]) //
const adminList = ref<Admin[]>([])
const allAdminList = ref<Admin[]>([]) //
const selectedManager = ref<Admin | null>(null)
const selectedCityFilter = ref('') // ID
const selectedCampus = ref('')
@ -339,6 +352,10 @@ const saving = ref(false) // 添加保存状态
const deleting = ref(false) //
const loadingStats = ref(false) //
//
const managerNames = ref<Record<string, string>>({}) //
const loadingManagerNames = ref<string[]>([]) // ID
//
const formData = ref<Area>({
areaId: '',
@ -352,6 +369,25 @@ const formData = ref<Area>({
updatedTime: undefined
})
// ID
const assignedManagerIds = computed(() => {
return campusList.value.map(campus => campus.manager)
})
//
const availableAdmins = computed(() => {
return allAdminList.value.filter(admin =>
!assignedManagerIds.value.includes(admin.adminId)
)
})
//
const assignedAdmins = computed(() => {
return allAdminList.value.filter(admin =>
assignedManagerIds.value.includes(admin.adminId)
)
})
//
const formatDate = (date: Date | undefined) => {
if (!date) return '-'
@ -392,6 +428,84 @@ const totalPages = computed(() => {
return Math.ceil(filteredCount / pageSize.value)
})
//
const loadManagerName = async (areaId: string, managerId: string) => {
if (!managerId) {
managerNames.value[areaId] = '未分配'
return
}
//
if (loadingManagerNames.value.includes(areaId)) return
loadingManagerNames.value.push(areaId)
try {
const response = await request<{
code: number
msg: string
data: Admin
}>(`/api/web/admin/${managerId}`, {
method: 'GET',
})
if (response.code === 200 && response.data) {
managerNames.value[areaId] = response.data.adminName
} else {
managerNames.value[areaId] = '未知负责人'
}
} catch (error) {
console.error('获取管理员姓名失败:', error)
managerNames.value[areaId] = '未知负责人'
} finally {
loadingManagerNames.value = loadingManagerNames.value.filter(id => id !== areaId)
}
}
//
const loadAllManagerNames = async () => {
for (const campus of campusList.value) {
await loadManagerName(campus.areaId, campus.manager)
}
}
// ID
const updateAdminAreaId = async (adminId: string, areaId: string) => {
try {
const response = await request<{
code: number
msg: string
data: Admin
}>('/api/web/admin/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
},
body: JSON.stringify({
adminId: adminId,
areaId: areaId
})
});
if (response.code === 200) {
//
const adminIndex = allAdminList.value.findIndex(admin => admin.adminId === adminId);
if (adminIndex !== -1) { //
const admin = allAdminList.value[adminIndex];
if (admin) { //
admin.areaId = areaId;
}
}
console.log('管理员区域ID更新成功');
} else {
console.error('更新管理员区域ID失败:', response.msg);
}
} catch (error) {
console.error('更新管理员区域ID请求失败:', error);
}
}
//
const fetchCampusList = async () => {
loading.value = true
@ -444,6 +558,9 @@ const fetchCampusList = async () => {
}
campusList.value = allCampuses
//
await loadAllManagerNames()
} catch (error: any) {
console.error('请求异常:', error)
const errorMsg = error.message.includes('401') || error.message.includes('403')
@ -517,7 +634,7 @@ const fetchAdminList = async () => {
const areaAdmins = response.data.filter(admin =>
admin.role === 'AREA_ADMIN' || admin.role === 'ROLE_AREA_ADMIN'
)
adminList.value = areaAdmins
allAdminList.value = areaAdmins
} else {
console.error('获取可分配校区的区域管理员失败:', response?.msg || '未知错误')
alert(`获取可分配校区的区域管理员失败:${response?.msg || '未知错误'}`)
@ -534,11 +651,14 @@ const fetchAdminList = async () => {
}
// - ID
const onManagerChange = () => {
// - IDareaId
const onManagerChange = async () => {
if (selectedManager.value) {
formData.value.manager = selectedManager.value.adminId // ID
formData.value.managerPhone = selectedManager.value.phone
// areaIdareaId
// areaId
} else {
formData.value.manager = ''
formData.value.managerPhone = ''
@ -575,15 +695,20 @@ const handleEdit = (campus: Area) => {
formData.value = { ...campus }
//
const matchedAdmin = adminList.value.find(admin => admin.adminId === campus.manager)
const matchedAdmin = allAdminList.value.find(admin => admin.adminId === campus.manager)
selectedManager.value = matchedAdmin || null
//
if (!selectedManager.value) {
const matchedByName = adminList.value.find(admin => admin.adminName === campus.manager)
const matchedByName = allAdminList.value.find(admin => admin.adminName === campus.manager)
selectedManager.value = matchedByName || null
}
// areaId
if (selectedManager.value && campus.areaId) {
selectedManager.value.areaId = campus.areaId;
}
showModal.value = true
}
@ -613,7 +738,7 @@ const confirmDelete = async () => {
})
if (response?.code === 200) {
fetchCampusList() //
await fetchCampusList() //
showDeleteConfirm.value = false
} else {
const errorMsg = response?.msg || `删除失败(错误码:${response?.code || '未知'}`
@ -671,6 +796,7 @@ const handleSave = async () => {
'Authorization': `Bearer ${authStore.token}` //
},
body: JSON.stringify({
areaId: formData.value.areaId,
areaName: formData.value.areaName,
areaType: 'campus',
parentAreaId: formData.value.parentAreaId,
@ -681,7 +807,11 @@ const handleSave = async () => {
})
if (response?.code === 200 && response?.data) {
fetchCampusList() //
// areaId
if (selectedManager.value && response.data.areaId) {
await updateAdminAreaId(selectedManager.value.adminId, response.data.areaId);
}
await fetchCampusList() //
showModal.value = false
} else {
const errorMsg = response?.msg || `更新失败(错误码:${response?.code || '未知'}`
@ -731,7 +861,11 @@ const handleSave = async () => {
})
if (response?.code === 200 && response?.data) {
fetchCampusList() //
// areaId - 使areaId
if (selectedManager.value && response.data.areaId) {
await updateAdminAreaId(selectedManager.value.adminId, response.data.areaId);
}
await fetchCampusList() //
showModal.value = false
} else {
const errorMsg = response?.msg || `新增失败(错误码:${response?.code || '未知'}`
@ -755,10 +889,10 @@ const handleSave = async () => {
}
// ID
// ID - 使
const getManagerName = (managerId: string) => {
if (!managerId) return '未分配'
const admin = adminList.value.find(admin => admin.adminId === managerId)
const admin = allAdminList.value.find(admin => admin.adminId === managerId)
return admin ? admin.adminName : '未知负责人'
}
@ -813,7 +947,7 @@ onMounted(async () => {
console.log('Token:', authStore.token)
await fetchCityList() //
await fetchAdminList() //
fetchCampusList() //
await fetchCampusList() //
})
</script>
@ -848,6 +982,16 @@ onMounted(async () => {
gap: 16px;
}
.no-available-admins {
background-color: #fffbe6;
border: 1px solid #ffe58f;
color: #faad14;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
text-align: center;
}
.btn-add {
background: #42b983;
color: white;

@ -10,7 +10,6 @@
<div class="action-bar">
<!-- 新增片区按钮 -->
<button class="btn-add" @click="handleAddArea"></button>
</div>
<!-- 片区表格 -->
@ -19,7 +18,7 @@
<thead>
<tr>
<th>片区</th>
<th>设备数量</th>
<th>关联校区</th>
<th>范围</th>
<th>操作</th>
</tr>
@ -27,7 +26,12 @@
<tbody>
<tr v-for="area in filteredAreas" :key="area.areaId">
<td>{{ area.areaName }}</td>
<td>{{ area.deviceCount || 0 }}</td>
<td>
<span v-if="area.campuses && area.campuses.length > 0">
{{ area.campuses.map((campus: any) => campus.areaName).join(', ') }}
</span>
<span v-else></span>
</td>
<td>{{ area.address || '未设置' }}</td>
<td class="operation-buttons">
<button
@ -101,22 +105,6 @@
rows="3"
></textarea>
</div>
<div class="form-item">
<label>负责人</label>
<input
type="text"
v-model="formData.manager"
placeholder="请输入负责人姓名"
>
</div>
<div class="form-item">
<label>联系电话</label>
<input
type="text"
v-model="formData.managerPhone"
placeholder="请输入负责人联系电话"
>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showModal = false">取消</button>
<button type="submit" class="btn-submit" :disabled="saving">
@ -150,10 +138,10 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request' //
import { useAuthStore } from '@/stores/auth' // authStore
import {computed, onMounted, ref} from 'vue'
import {useRouter} from 'vue-router'
import {request} from '@/api/request' //
import {useAuthStore} from '@/stores/auth' // authStore
//
const router = useRouter()
@ -168,7 +156,7 @@ interface Area {
address: string
manager: string
managerPhone: string
deviceCount?: number
campuses?: any[] //
createdTime?: Date
updatedTime?: Date
}
@ -196,7 +184,7 @@ const formData = ref<Partial<Area>>({
address: '',
manager: '',
managerPhone: '',
deviceCount: 0
campuses: []
})
//
@ -223,31 +211,67 @@ const totalPages = computed(() => {
})
//
//
const fetchAreaList = async () => {
loading.value = true
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
// URL
const url = `/api/web/area/cities`
const response = await request<{
//
const citiesResponse = await request<{
code: number
msg: string
data: Area[]
}>(url, {
}>('/api/web/area/cities', {
method: 'GET',
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (response.code === 200) {
areaList.value = response.data
if (citiesResponse.code === 200) {
//
areaList.value = await Promise.all(
citiesResponse.data.map(async (city) => {
try {
const campusesResponse = await request<{
code: number
msg: string
data: Area[]
}>(`/api/web/area/campuses/${city.areaId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (campusesResponse.code === 200) {
return {
...city,
campuses: campusesResponse.data
}
} else {
console.error(`获取市区 ${city.areaName} 的校区失败:`, campusesResponse.msg)
return {
...city,
campuses: []
}
}
} catch (error) {
console.error(`获取市区 ${city.areaName} 的校区时发生错误:`, error)
return {
...city,
campuses: []
}
}
})
)
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
const errorMsg = citiesResponse.msg || `获取失败(错误码:${citiesResponse.code}`
console.error('获取片区列表失败:', errorMsg)
alert(`获取片区列表失败:${errorMsg}`)
}
@ -264,12 +288,7 @@ const fetchAreaList = async () => {
}
}
//
const handleAreaChange = () => {
currentPage.value = 1 //
}
//
const handleAddArea = () => {
isEdit.value = false
@ -282,7 +301,7 @@ const handleAddArea = () => {
address: '',
manager: '',
managerPhone: '',
deviceCount: 0
campuses: []
}
showModal.value = true
}
@ -311,7 +330,7 @@ const confirmDelete = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -320,10 +339,13 @@ const confirmDelete = async () => {
msg: string
}>(`/api/web/area/delete/${deleteAreaId.value}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (response.code === 200) {
fetchAreaList() //
await fetchAreaList() //
showDeleteConfirm.value = false
} else {
const errorMsg = response.msg || `删除失败(错误码:${response.code}`
@ -350,19 +372,19 @@ const handleSave = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
let response
if (isEdit.value) {
// - URLareaId
//
response = await request<{
code: number
msg: string
data: Area
}>(`/api/web/area/update/${formData.value.areaId}`, { // URL
}>(`/api/web/area/update/${formData.value.areaId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@ -379,7 +401,7 @@ const handleSave = async () => {
})
if (response.code === 200) {
fetchAreaList() //
await fetchAreaList() //
showModal.value = false
} else {
const errorMsg = response.msg || `更新失败(错误码:${response.code}`
@ -387,7 +409,7 @@ const handleSave = async () => {
alert(`更新片区失败:${errorMsg}`)
}
} else {
// - parentAreaId
//
const newArea = {
areaName: formData.value.areaName,
areaType: 'zone' as const,
@ -410,7 +432,7 @@ const handleSave = async () => {
})
if (response.code === 200) {
fetchAreaList() //
await fetchAreaList() //
showModal.value = false
} else {
const errorMsg = response.msg || `新增失败(错误码:${response.code}`
@ -431,9 +453,6 @@ const handleSave = async () => {
}
}
//
onMounted(() => {
console.log('Token:', authStore.token)
@ -441,6 +460,8 @@ onMounted(() => {
})
</script>
<!-- Urban.vue 文件中确保有正确的样式定义 -->
<style scoped>
/* 确保样式规则完整且正确 */
@ -488,21 +509,6 @@ onMounted(() => {
background: #359e75;
}
.filter-box {
display: flex;
align-items: center;
gap: 8px;
color: #666;
}
.area-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 200px;
font-size: 14px;
}
/* 表格样式 */
.area-table {
width: 100%;
@ -718,12 +724,5 @@ onMounted(() => {
align-items: flex-start;
}
.filter-box {
width: 100%;
}
.area-select {
width: 100%;
}
}
</style>

@ -49,6 +49,7 @@
<th>状态</th>
<th>安装日期</th>
<th>关联设备ID</th>
<th>所属片区</th>
<th>操作</th>
</tr>
</thead>
@ -65,6 +66,7 @@
</td>
<td>{{ formatDate(terminal.installDate) }}</td>
<td>{{ terminal.deviceId || '-' }}</td>
<td>{{ terminal.areaId || '-' }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewTerminal(terminal.terminalId)"></button>
<button
@ -82,7 +84,7 @@
</td>
</tr>
<tr v-if="paginatedTerminals.length === 0">
<td colspan="8" class="no-data">暂无终端数据</td>
<td colspan="9" class="no-data">暂无终端数据</td>
</tr>
</tbody>
</table>
@ -145,12 +147,13 @@
</div>
<!-- 市区选择 -->
<div class="form-group" v-if="!isEditing">
<div class="form-group">
<label>选择市区:</label>
<select
v-model="selectedCityId"
@change="onCityChange"
class="select-input"
:disabled="isEditing"
>
<option value="">请选择市区</option>
<option
@ -164,13 +167,13 @@
</div>
<!-- 校区选择 -->
<div class="form-group" v-if="!isEditing && selectedCityId">
<div class="form-group" v-if="selectedCityId">
<label>选择校区:</label>
<select
v-model="selectedCampusId"
@change="onCampusChange"
class="select-input"
:disabled="!campusList.length"
:disabled="!campusList.length || isEditing"
>
<option value="">请选择校区</option>
<option
@ -193,7 +196,7 @@
class="select-input"
:disabled="!filteredSupplyDevices.length"
>
<option value="">请选择供水机</option>
<option value="">无关联设备</option>
<option
v-for="device in filteredSupplyDevices"
:key="device.deviceId"
@ -310,7 +313,7 @@ const loadTerminals = async (): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -334,7 +337,8 @@ const loadTerminals = async (): Promise<void> => {
latitude: item.latitude,
terminalStatus: item.terminalStatus,
installDate: item.installDate,
deviceId: item.deviceId
deviceId: item.deviceId,
areaId: item.areaId
}))
} else {
console.warn('API响应非成功状态或数据格式错误:', result)
@ -351,7 +355,8 @@ const loadTerminals = async (): Promise<void> => {
latitude: item.latitude,
terminalStatus: item.terminalStatus,
installDate: item.installDate,
deviceId: item.deviceId
deviceId: item.deviceId,
areaId: item.areaId
}))
} else {
console.warn('API响应数据格式错误:', result)
@ -367,7 +372,7 @@ const loadTerminals = async (): Promise<void> => {
terminals.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -378,7 +383,7 @@ const loadCityList = async (): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -405,7 +410,7 @@ const loadCityList = async (): Promise<void> => {
cityList.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -416,7 +421,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
}
@ -443,7 +448,7 @@ 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')
}
}
}
@ -454,7 +459,7 @@ const loadAvailableSupplyDevices = async (): Promise<void> => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -474,14 +479,14 @@ const loadAvailableSupplyDevices = async (): Promise<void> => {
if (result.code === 200 && result.data && Array.isArray(result.data)) {
// 使
// loadAvailableSupplyDevices
availableSupplyDevices.value = result.data.map((device: any) => ({
deviceId: device.deviceId,
deviceName: device.deviceName,
installLocation: device.installLocation,
deviceType: device.deviceType,
status: device.status,
areaId: device.areaId ? device.areaId.split('-')[0] : '' //
}))
availableSupplyDevices.value = result.data.map((device: any) => ({
deviceId: device.deviceId,
deviceName: device.deviceName,
installLocation: device.installLocation,
deviceType: device.deviceType,
status: device.status,
areaId: device.areaId ? device.areaId.split('-')[0] : '' //
}))
console.log(`获取到${availableSupplyDevices.value.length}个可用供水机`)
} else {
@ -507,7 +512,7 @@ availableSupplyDevices.value = result.data.map((device: any) => ({
availableSupplyDevices.value = []
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -612,19 +617,40 @@ const viewTerminal = (id: string) => {
}
//
const editTerminal = (terminal: TerminalManageVO) => {
const editTerminal = async (terminal: TerminalManageVO) => {
//
currentTerminal.value = { ...terminal }
isEditing.value = true
showAddModal.value = true
//
if (availableSupplyDevices.value.length === 0) {
loadAvailableSupplyDevices()
await loadAvailableSupplyDevices()
}
//
if (cityList.value.length === 0) {
loadCityList()
await loadCityList()
}
// areaId
if (terminal.areaId) {
//
if (cityList.value.length > 0) {
for (const city of cityList.value) {
//
await loadCampusListByCity(city.areaId)
// areaIdareaName
const matchedCampus = campusList.value.find(campus => campus.areaName === terminal.areaId)
if (matchedCampus) {
//
selectedCityId.value = city.areaId
selectedCampusId.value = matchedCampus.areaId
break
}
}
}
}
}
@ -635,11 +661,10 @@ const deleteTerminal = async (terminalId: string) => {
}
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -647,18 +672,18 @@ const deleteTerminal = async (terminalId: string) => {
method: 'DELETE'
})
// Map
if (result && typeof result === 'object' && result.message) {
if (result.message.includes('成功')) {
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200) {
//
terminals.value = terminals.value.filter(t => t.terminalId !== terminalId)
alert('终端删除成功')
} else {
alert(`删除终端失败: ${result.message}`)
alert(`删除终端失败: ${result.message || '未知错误'}`)
}
} else {
//
//
//
terminals.value = terminals.value.filter(t => t.terminalId !== terminalId)
alert('终端删除成功')
}
@ -667,11 +692,12 @@ const deleteTerminal = async (terminalId: string) => {
alert('删除终端失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
//
const saveTerminal = async () => {
try {
@ -679,7 +705,7 @@ const saveTerminal = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -695,11 +721,12 @@ const saveTerminal = async () => {
return
}
//
if (!currentTerminal.value.deviceId) {
alert('请选择关联的供水机')
return
}
//
//
// if (!currentTerminal.value.deviceId) {
// alert('')
// return
// }
let result: ResultVO<TerminalManageVO> | TerminalManageVO
if (isEditing.value) {
@ -716,12 +743,6 @@ const saveTerminal = async () => {
return
}
//
if (!currentTerminal.value.deviceId) {
alert('请选择关联的供水机')
return
}
result = await request<ResultVO<TerminalManageVO> | TerminalManageVO>('/api/web/terminal/add', {
method: 'POST',
body: JSON.stringify(currentTerminal.value)
@ -825,7 +846,7 @@ const saveTerminal = async () => {
alert(`${isEditing.value ? '更新' : '添加'}终端失败`)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -943,34 +964,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.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.inactive {
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;

@ -51,6 +51,41 @@
</div>
</div>
<!-- 关联设备信息卡片 -->
<div v-if="terminal?.deviceId" class="card">
<h3>关联设备信息</h3>
<div v-if="deviceLoading" class="loading">...</div>
<div v-else-if="relatedDevice" class="detail-grid">
<div class="detail-item">
<label>设备ID:</label>
<span>{{ relatedDevice.deviceId }}</span>
</div>
<div class="detail-item">
<label>设备名称:</label>
<span>{{ relatedDevice.deviceName }}</span>
</div>
<div class="detail-item">
<label>设备类型:</label>
<span>{{ relatedDevice.deviceType }}</span>
</div>
<div class="detail-item">
<label>设备状态:</label>
<span :class="`status-tag ${relatedDevice.status}`">
{{ formatDeviceStatus(relatedDevice.status) }}
</span>
</div>
<div class="detail-item" v-if="relatedDevice.installLocation">
<label>安装位置:</label>
<span>{{ relatedDevice.installLocation }}</span>
</div>
<div class="detail-item" v-if="relatedDevice.areaId">
<label>所属片区:</label>
<span>{{ relatedDevice.areaId }}</span>
</div>
</div>
<div v-else class="no-data">未关联设备或设备信息不存在</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="btn-back" @click="goBack"></button>
@ -129,6 +164,16 @@ interface TerminalManageVO {
deviceId?: string
}
//
interface Device {
deviceId: string
deviceName: string
deviceType: string
status: string
installLocation?: string
areaId?: string
}
//
function isTerminalManageVO(obj: any): obj is TerminalManageVO {
return obj &&
@ -156,6 +201,10 @@ const editingTerminal = ref<TerminalManageVO>({
terminalStatus: 'active'
})
//
const relatedDevice = ref<Device | null>(null)
const deviceLoading = ref(false)
// ID
const terminalId = route.params.id as string
@ -170,6 +219,19 @@ const formatStatus = (status: TerminalStatus): string => {
return statusMap[status] || status
}
//
const formatDeviceStatus = (status: string): string => {
const statusMap: Record<string, string> = {
'online': '在线',
'offline': '离线',
'fault': '故障',
'warning': '警告',
'active': '运行中',
'inactive': '停止'
}
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
@ -177,6 +239,52 @@ const formatDate = (dateString?: string): string => {
return dateString
}
//
const loadRelatedDevice = async (deviceId: string) => {
if (!deviceId) {
return
}
try {
deviceLoading.value = true
const token = authStore.token
if (!token) {
await router.push('/login')
return
}
const result = await request<any>(`/api/web/device/${deviceId}`, {
method: 'GET'
})
if (result && typeof result === 'object' && 'code' in result) {
if (result.code === 200 && result.data) {
// MapdeviceInfo
if (result.data.deviceInfo) {
relatedDevice.value = result.data.deviceInfo as Device
} else {
//
relatedDevice.value = result.data as Device
}
} else {
console.warn('获取关联设备失败:', result.message)
}
} else {
//
if (result && typeof result === 'object' && result.deviceInfo) {
relatedDevice.value = result.deviceInfo as Device
} else if (result && typeof result === 'object') {
relatedDevice.value = result as Device
}
}
} catch (err) {
console.error('获取关联设备失败:', err)
} finally {
deviceLoading.value = false
}
}
//
const loadTerminal = async () => {
if (!terminalId) {
@ -190,7 +298,7 @@ const loadTerminal = async () => {
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -216,6 +324,11 @@ const loadTerminal = async () => {
}
//
editingTerminal.value = { ...terminal.value }
//
if (terminal.value.deviceId) {
await loadRelatedDevice(terminal.value.deviceId)
}
} else {
error.value = result.message || '获取终端信息失败'
console.warn('API响应非成功状态或数据格式错误:', result)
@ -233,6 +346,11 @@ const loadTerminal = async () => {
}
//
editingTerminal.value = { ...terminal.value }
//
if (terminal.value.deviceId) {
await loadRelatedDevice(terminal.value.deviceId)
}
} else {
error.value = '获取终端信息失败'
console.warn('API响应数据格式错误:', result)
@ -242,7 +360,7 @@ const loadTerminal = async () => {
error.value = '加载终端详情失败'
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
@ -250,20 +368,13 @@ const loadTerminal = async () => {
}
//
const editTerminal = () => {
if (terminal.value) {
editingTerminal.value = { ...terminal.value }
showEditModal.value = true
}
}
//
//
const saveTerminal = async () => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -317,46 +428,13 @@ const saveTerminal = async () => {
alert('更新终端信息失败')
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
//
const deleteTerminal = async () => {
if (!confirm(`确定要删除终端 ${terminal.value?.terminalId} 吗?`)) {
return
}
try {
const token = authStore.token
if (!token) {
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/terminal/delete/${terminal.value?.terminalId}`, {
method: 'DELETE'
})
if (result.code === 200 || result.code === 201 || result.code === 204) {
alert('终端删除成功')
router.push('/home/equipment/terminal')
} else {
alert(`删除终端失败: ${result.message}`)
}
} catch (err) {
console.error('删除终端失败:', err)
alert('删除终端失败')
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const goBack = () => {
router.push('/home/equipment/terminal')
@ -439,36 +517,6 @@ onMounted(() => {
font-size: 15px;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
align-self: flex-start;
max-width: fit-content;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.inactive {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault {
background-color: #ffebe6;
color: #cf1322;
}
.action-buttons {
display: flex;
gap: 12px;
@ -483,18 +531,6 @@ onMounted(() => {
font-size: 14px;
}
.btn-edit {
background: #42b983;
color: white;
border: none;
}
.btn-delete {
background: #ff4d4f;
color: white;
border: none;
}
.btn-back {
background: #f5f5f5;
color: #666;
@ -569,4 +605,5 @@ onMounted(() => {
color: white;
border: none;
}
</style>

@ -97,7 +97,6 @@
<button
class="btn-delete"
@click="deleteDevice(device.deviceId)"
:disabled="device.status === 'online'"
>
删除
</button>
@ -130,22 +129,22 @@
</button>
</div>
<!-- 添加设备模态框 -->
<!-- 添加/编辑设备模态框 -->
<div v-if="showAddModal" class="modal-overlay" @click="showAddModal = false">
<div class="modal-content" @click.stop>
<h3>添加制水机</h3>
<form @submit.prevent="addDevice">
<h3>{{ isEditing ? '编辑设备' : '添加制水机' }}</h3>
<form @submit.prevent="saveDevice">
<div class="form-group">
<label>设备ID:</label>
<input v-model="newDevice.deviceId" type="text" required>
<input v-model="currentDevice.deviceId" type="text" :disabled="isEditing" required>
</div>
<div class="form-group">
<label>设备名称:</label>
<input v-model="newDevice.deviceName" type="text" required>
<input v-model="currentDevice.deviceName" type="text" required>
</div>
<!-- 市区选择 -->
<div class="form-group">
<div class="form-group" v-if="!isEditing">
<label>选择市区:</label>
<select
v-model="selectedCityId"
@ -164,7 +163,7 @@
</div>
<!-- 校区选择 -->
<div class="form-group" v-if="selectedCityId">
<div class="form-group" v-if="!isEditing && selectedCityId">
<label>选择校区:</label>
<select
v-model="selectedCampusId"
@ -188,79 +187,19 @@
<div class="form-group">
<label>安装位置:</label>
<input v-model="newDevice.installLocation" type="text" required>
</div>
<div class="form-actions">
<button type="button" @click="showAddModal = false">取消</button>
<button type="submit">添加</button>
</div>
</form>
</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>
<input v-model="currentDevice.installLocation" type="text" required>
</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>
<label>设备类型:</label>
<select v-model="currentDevice.deviceType" required>
<option value="water_maker">制水机</option>
<option value="other">其他</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>
<button type="button" @click="showAddModal = false">取消</button>
<button type="submit">{{ isEditing ? '更新' : '添加' }}</button>
</div>
</form>
</div>
@ -306,6 +245,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
@ -313,18 +253,28 @@ import { useAuthStore } from '@/stores/auth'
import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
//
const isDeviceManageVO = (obj: any): obj is DeviceManageVO => {
return obj &&
typeof obj === 'object' &&
typeof obj.deviceId === 'string' &&
typeof obj.deviceName === 'string' &&
typeof obj.installLocation === 'string' &&
typeof obj.deviceType === 'string' &&
['online', 'offline', 'warning', 'fault'].includes(obj.status)
}
//
type DeviceStatus = 'online' | 'offline' | 'warning' | 'fault'
//
interface WaterMakerDevice {
// - DeviceManageVO
interface DeviceManageVO {
deviceId: string
deviceName: string
deviceType: string
areaId: string
installLocation: string
deviceType: string
status: DeviceStatus
// lastHeartbeatTime
areaId?: string
}
//
@ -339,7 +289,7 @@ interface Area {
}
//
const devices = ref<WaterMakerDevice[]>([])
const devices = ref<DeviceManageVO[]>([])
const searchKeyword = ref('')
const selectedCity = ref('') //
const selectedCampus = ref('') //
@ -351,30 +301,21 @@ const authStore = useAuthStore() // 初始化auth store
//
const showAddModal = ref(false)
const showEditModal = ref(false)
const isEditing = ref(false)
const showOfflineReasonModal = ref(false)
const showFaultModal = ref(false)
// ID
const currentDeviceId = ref('')
//
const newDevice = ref({
deviceId: '',
deviceName: '',
areaId: '',
installLocation: '',
deviceType: 'water_maker'
})
//
const editingDevice = ref<WaterMakerDevice>({
//
const currentDevice = ref<DeviceManageVO>({
deviceId: '',
deviceName: '',
deviceType: '',
areaId: '',
installLocation: '',
status: 'online'
deviceType: 'water_maker',
status: 'online',
areaId: undefined
})
//
@ -383,11 +324,6 @@ 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: '',
@ -424,7 +360,7 @@ const loadDevices = async (): Promise<void> => {
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`
//
const result = await request<ResultVO<WaterMakerDevice[]>>(
const result = await request<ResultVO<DeviceManageVO[]>>(
url,
{ method: 'GET' }
)
@ -440,7 +376,6 @@ const loadDevices = async (): Promise<void> => {
areaId: item.areaId,
installLocation: item.installLocation,
status: item.status
// lastHeartbeatTime
}))
}
@ -534,94 +469,24 @@ const loadCampusListByCity = async (cityId: string): Promise<void> => {
//
const onCityChange = async () => {
//
//
selectedCampus.value = ''
currentDevice.value.areaId = undefined
campusList.value = []
if (selectedCity.value) {
await loadCampusListByCity(selectedCity.value)
} else {
//
campusList.value = []
}
}
// 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')
}
if (selectedCityId.value) {
await loadCampusListByCity(selectedCityId.value)
}
}
//
//
const onCampusChange = () => {
// areaIdareaNameareaId
// areaIdareaName
const selectedCampus = campusList.value.find(campus => campus.areaId === selectedCampusId.value)
if (selectedCampus) {
newDevice.value.areaId = selectedCampus.areaName // 使areaNameareaId
} else {
newDevice.value.areaId = ''
}
}
//
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
currentDevice.value.areaId = selectedCampus.areaName // 使areaNameareaId
} else {
editingDevice.value.areaId = ''
currentDevice.value.areaId = undefined
}
}
@ -632,16 +497,15 @@ const filteredDevices = computed(() => {
device.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.installLocation.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
let areaMatch = true
//
let areaMatch = true;
if (selectedCampus.value && selectedCampus.value !== '') {
areaMatch = device.areaId === selectedCampus.value ||
device.areaId === campusList.value.find(c => c.areaId === selectedCampus.value)?.areaName
} else if (selectedCity.value && selectedCity.value !== '') {
//
areaMatch = campusList.value.some(campus =>
device.areaId === campus.areaId || device.areaId === campus.areaName
) || device.areaId === selectedCity.value
//
const selectedCampusObj = campusList.value.find(c => c.areaId === selectedCampus.value);
if (selectedCampusObj) {
// 使 areaName areaId
areaMatch = device.areaId === selectedCampusObj.areaName;
}
}
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
@ -792,167 +656,162 @@ const deleteDevice = async (deviceId: string) => {
}
//
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 openEditModal = (device: DeviceManageVO) => {
currentDevice.value = { ...device }
isEditing.value = true
showAddModal.value = true
//
if (cityList.value.length === 0) {
loadCityList()
}
}
//
const updateDevice = async () => {
//
const saveDevice = async () => {
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return
}
//
if (!selectedEditCampusId.value) {
//
if (!isEditing.value && !selectedCampusId.value) {
alert('请选择校区')
return
}
// areaId
if (!editingDevice.value.areaId) {
if (!currentDevice.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
let result: ResultVO<DeviceManageVO> | DeviceManageVO
if (isEditing.value) {
//
result = await request<ResultVO<DeviceManageVO> | DeviceManageVO>('/api/web/device/edit', {
method: 'PUT',
body: JSON.stringify(currentDevice.value)
})
} else {
//
//
if (!selectedCampusId.value) {
alert('请选择校区')
return
}
//
if (!currentDevice.value.areaId) {
alert('请选择校区以确定所属片区')
return
}
result = await request<ResultVO<DeviceManageVO> | DeviceManageVO>('/api/web/device/add', {
method: 'POST',
body: JSON.stringify(currentDevice.value)
})
}
const result = await request<ResultVO>('/api/web/device/edit', {
method: 'PUT',
body: JSON.stringify(deviceToUpdate)
})
// ResultVO
if (result && typeof result === 'object' && 'code' in result) {
// ResultVO
if (result.code === 200 && result.data && isDeviceManageVO(result.data)) {
//
await loadDevices()
//
showAddModal.value = false
isEditing.value = false
currentDevice.value = {
deviceId: '',
deviceName: '',
installLocation: '',
deviceType: 'water_maker',
status: 'online',
areaId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
if (result.code === 200) {
//
const index = devices.value.findIndex(d => d.deviceId === editingDevice.value.deviceId)
if (index !== -1) {
devices.value[index] = { ...editingDevice.value }
alert(isEditing.value ? '设备更新成功' : '设备添加成功')
} else if (result.code === 200) {
// code200data
//
await loadDevices()
//
showAddModal.value = false
isEditing.value = false
currentDevice.value = {
deviceId: '',
deviceName: '',
installLocation: '',
deviceType: 'water_maker',
status: 'online',
areaId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert(isEditing.value ? '设备更新成功' : '设备添加成功')
} else {
alert(`${isEditing.value ? '更新' : '添加'}设备失败: ${result.message}`)
}
}
//
else if (isDeviceManageVO(result)) {
//
await loadDevices()
//
showEditModal.value = false
editingDevice.value = {
showAddModal.value = false
isEditing.value = false
currentDevice.value = {
deviceId: '',
deviceName: '',
deviceType: '',
areaId: '',
installLocation: '',
status: 'online'
deviceType: 'water_maker',
status: 'online',
areaId: undefined
}
selectedEditCityId.value = ''
selectedEditCampusId.value = ''
editCampusList.value = []
alert('设备更新成功')
} else {
alert(`设备更新失败: ${result.message}`)
}
} catch (error) {
console.error('更新设备失败:', error)
alert('更新设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
}
}
}
//
const addDevice = async () => {
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return
}
//
if (!selectedCampusId.value) {
alert('请选择校区')
return
}
// areaId
if (!newDevice.value.areaId) {
alert('请先选择校区以确定所属片区')
return
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
//
const deviceToAdd = {
deviceId: newDevice.value.deviceId,
deviceName: newDevice.value.deviceName,
areaId: newDevice.value.areaId,
installLocation: newDevice.value.installLocation,
deviceType: newDevice.value.deviceType
alert(isEditing.value ? '设备更新成功' : '设备添加成功')
}
// 使token
const result = await request<ResultVO>('/api/web/device/add', {
method: 'POST',
body: JSON.stringify(deviceToAdd)
})
if (result.code === 200) {
else {
//
//
await loadDevices()
//
showAddModal.value = false
newDevice.value = {
isEditing.value = false
currentDevice.value = {
deviceId: '',
deviceName: '',
areaId: '',
installLocation: '',
deviceType: 'water_maker'
deviceType: 'water_maker',
status: 'online',
areaId: undefined
}
selectedCityId.value = ''
selectedCampusId.value = ''
campusList.value = []
alert('设备添加成功')
} else {
alert(`设备添加失败: ${result.message}`)
alert(isEditing.value ? '设备更新成功' : '设备添加成功')
}
} catch (error) {
console.error('添加设备失败:', error)
alert('添加设备失败')
console.error(`${isEditing.value ? '更新' : '添加'}设备失败:`, error)
alert(`${isEditing.value ? '更新' : '添加'}设备失败`)
if ((error as Error).message.includes('401')) {
authStore.logout()
await router.push('/login')
@ -968,6 +827,7 @@ onMounted(async () => {
})
</script>
<style scoped>
/* 样式与终端机页面保持一致 */
.water-maker-page {
@ -1079,34 +939,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;

@ -311,7 +311,7 @@ const loadSupplierDevices = async (makerId: string) => {
// token
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -333,7 +333,7 @@ const loadSupplierDevices = async (makerId: string) => {
supplierDevices.value = []
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loadingSuppliers.value = false
@ -415,7 +415,7 @@ const loadDeviceDetail = async () => {
// token
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -435,7 +435,7 @@ const loadDeviceDetail = async () => {
}
//
nextTick(() => {
await nextTick(() => {
initTDSChart()
})
} else {
@ -446,7 +446,7 @@ const loadDeviceDetail = async () => {
error.value = '加载设备详情失败'
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
@ -570,38 +570,6 @@ onUnmounted(() => {
min-height: 24px;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
min-width: 60px;
text-align: center;
}
.status-tag.online,
.status-tag.normal {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault,
.status-tag.error {
background-color: #ffebe6;
color: #cf1322;
}
.loading, .error-message {
text-align: center;
padding: 40px 0;

@ -353,7 +353,7 @@ const loadDeviceDetail = async () => {
// token
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -373,7 +373,7 @@ const loadDeviceDetail = async () => {
}
//
nextTick(() => {
await nextTick(() => {
initPressureChart()
initFlowChart()
initTemperatureChart()
@ -386,7 +386,7 @@ const loadDeviceDetail = async () => {
error.value = '加载设备详情失败'
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
@ -398,7 +398,7 @@ const loadAssociatedMaker = async (makerId: string) => {
try {
const token = authStore.token
if (!token) {
router.push('/login')
await router.push('/login')
return
}
@ -417,7 +417,7 @@ const loadAssociatedMaker = async (makerId: string) => {
console.error('加载关联制水机失败:', err)
if ((err as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
@ -529,38 +529,6 @@ onUnmounted(() => {
min-height: 24px;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
min-width: 60px;
text-align: center;
}
.status-tag.online,
.status-tag.normal {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.offline {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.warning {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.fault,
.status-tag.error {
background-color: #ffebe6;
color: #cf1322;
}
.loading, .error-message {
text-align: center;
padding: 40px 0;

@ -13,7 +13,7 @@
<div class="search-box">
<input
type="text"
placeholder="搜索姓名或账号..."
placeholder="搜索姓名..."
v-model="searchKeyword"
@input="handleSearch"
>
@ -31,7 +31,6 @@
<th>联系电话</th>
<th>身份</th>
<th>关联区域</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
@ -43,15 +42,10 @@
<td>{{ formatRole(admin.role) }}</td>
<td>
<span v-if="admin.role === 'ROLE_AREA_ADMIN'" class="area-list">
{{ admin.areaName || (admin.areaId ? getAreaNameById(admin.areaId) : '') || '未关联区域' }}
{{ getAreaNameFromCache(admin.areaId) || admin.areaName || '未关联区域' }}
</span>
<span v-else>-</span>
</td>
<td>
<span :class="`status-tag ${admin.status}`">
{{ admin.status === 'active' ? '启用' : '禁用' }}
</span>
</td>
<td class="operation-buttons">
<button
class="btn-edit"
@ -65,17 +59,10 @@
>
删除
</button>
<button
class="btn-status"
:class="admin.status === 'active' ? 'btn-disable' : 'btn-enable'"
@click="handleStatusChange(admin.adminId, admin.status)"
>
{{ admin.status === 'active' ? '禁用' : '启用' }}
</button>
</td>
</tr>
<tr v-if="paginatedAdmins.length === 0">
<td colspan="7" class="no-data">暂无管理员数据</td>
<td colspan="6" class="no-data">暂无管理员数据</td>
</tr>
</tbody>
</table>
@ -195,7 +182,7 @@
<div class="form-group" v-if="editFormData.role === 'ROLE_AREA_ADMIN' && originalAdminData?.areaId">
<label class="form-label">关联区域</label>
<div class="readonly-area">
{{ getAreaNameById(originalAdminData.areaId) }}
{{ getAreaNameFromCache(originalAdminData.areaId) || originalAdminData.areaName || '未关联区域' }}
</div>
</div>
@ -220,14 +207,11 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import type { ResultVO } from '@/api/types/auth'
//
type AdminStatus = 'active' | 'disabled'
import {computed, onMounted, ref} from 'vue'
import {useRouter} from 'vue-router'
import {request} from '@/api/request'
import {useAuthStore} from '@/stores/auth'
import type {ResultVO} from '@/api/types/auth'
//
interface Area {
@ -247,7 +231,6 @@ interface Admin {
account: string
phone: string
role: string
status: AdminStatus
areaId?: string
areaName?: string
}
@ -285,6 +268,9 @@ const loading = ref(false)
const showAddModal = ref(false)
const showEditModal = ref(false)
//
const areaNameCache = ref<Record<string, string>>({})
//
const unassignedAreas = ref<Area[]>([])
@ -322,7 +308,7 @@ const loadUnassignedAreas = async () => {
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -339,11 +325,59 @@ const loadUnassignedAreas = async () => {
console.error('请求未设置负责人片区列表异常:', error)
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
}
}
// ID -
const getAreaNameById = async (areaId: string | undefined): Promise<string> => {
if (!areaId) return '未知区域'
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
await router.push('/login')
return '未知区域'
}
const response = await request<ResultVO<Area>>(`/api/web/area/${areaId}`, {
method: 'GET',
})
if (response.code === 200 && response.data) {
const areaName = response.data.areaName || '未知区域'
//
areaNameCache.value[areaId] = areaName
return areaName
} else {
console.error('获取区域信息失败:', response.message)
return '未知区域'
}
} catch (error: any) {
console.error('请求区域信息异常:', error)
return '未知区域'
}
}
//
const getAreaNameFromCache = (areaId: string | undefined): string => {
if (!areaId) return '未知区域'
return areaNameCache.value[areaId] || ''
}
//
const preloadAreaNames = async (adminList: Admin[]) => {
const areaIds = adminList
.filter(admin => admin.role === 'ROLE_AREA_ADMIN' && admin.areaId && !areaNameCache.value[admin.areaId])
.map(admin => admin.areaId!)
for (const areaId of areaIds) {
await getAreaNameById(areaId)
}
}
//
const fetchAdminList = async () => {
loading.value = true
@ -352,7 +386,7 @@ const fetchAdminList = async () => {
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -366,16 +400,20 @@ const fetchAdminList = async () => {
})
if (response.code === 200) {
admins.value = (response.data || []).map((admin: any) => ({
const adminList = (response.data || []).map((admin: any) => ({
adminId: admin.adminId || '',
name: admin.adminName || '未知姓名', //
name: admin.adminName || '未知姓名',
account: admin.adminId || '',
phone: admin.phone || '未知电话',
role: admin.role || '未知角色',
status: 'active' as AdminStatus,
areaId: admin.areaId,
areaName: admin.areaName || undefined //
areaName: admin.areaName || undefined
}))
admins.value = adminList
//
await preloadAreaNames(adminList)
} else {
const errorMsg = response.message || `获取失败(错误码:${response.code}`
console.error('获取管理员列表失败:', errorMsg)
@ -392,7 +430,7 @@ const fetchAdminList = async () => {
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
@ -409,22 +447,12 @@ const formatRole = (role: string): string => {
return roleMap[role] || role
}
// ID
// script
const getAreaNameById = (areaId: string | undefined): string => {
if (!areaId) return '未知区域'
const area = unassignedAreas.value.find(a => a.areaId === areaId)
return area ? area.areaName : '未知区域'
}
//
const filteredAdmins = computed(() => {
return admins.value.filter(admin => {
const keywordMatch = searchKeyword.value.trim() === '' ||
return searchKeyword.value.trim() === '' ||
admin.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
admin.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
return keywordMatch
})
})
@ -463,34 +491,9 @@ const handleEditRoleChange = () => {
//
onMounted(async () => {
await loadUnassignedAreas()
fetchAdminList()
await fetchAdminList()
})
//
const handleStatusChange = async (id: string, currentStatus: AdminStatus) => {
try {
const newStatus: AdminStatus = currentStatus === 'active' ? 'disabled' : 'active'
// API
const response = await request<ResultVO>(`/api/web/admin/status/${id}`, {
method: 'PUT',
body: JSON.stringify({ status: newStatus })
})
if (response.code === 200) {
//
admins.value = admins.value.map(admin =>
admin.adminId === id ? { ...admin, status: newStatus } : admin
)
} else {
alert(`状态更新失败: ${response.message}`)
}
} catch (error: any) {
console.error('更新状态失败:', error)
alert(`更新状态失败: ${error.message}`)
}
}
//
const handleEdit = async (id: string) => {
try {
@ -553,7 +556,7 @@ const handleEditSubmit = async () => {
};
originalAdminData.value = null;
//
fetchAdminList();
await fetchAdminList();
} else {
alert(`更新失败:${response.message}`);
}
@ -577,7 +580,7 @@ const performDelete = async (id: string) => {
if (!token) {
console.warn('未获取到 Token跳转到登录页');
router.push('/login');
await router.push('/login');
return;
}
@ -588,7 +591,7 @@ const performDelete = async (id: string) => {
if (response.code === 200) {
alert('管理员删除成功');
//
fetchAdminList();
await fetchAdminList();
} else {
const errorMsg = response.message || `删除失败(错误码:${response.code}`;
console.error('删除管理员失败:', errorMsg);
@ -605,16 +608,15 @@ const performDelete = async (id: string) => {
if (error.message.includes('401')) {
authStore.logout();
router.push('/login');
await router.push('/login');
}
}
}
//
const campusAreas = computed(() => {
computed(() => {
return unassignedAreas.value.filter(area => area.areaType === '校园')
})
});
//
const handleSubmit = async () => {
try {
@ -654,7 +656,7 @@ const handleSubmit = async () => {
}
selectedAreaId.value = '' //
//
fetchAdminList()
await fetchAdminList()
} else {
alert(`添加失败:${response.message}`)
}
@ -755,24 +757,6 @@ const handleSubmit = async () => {
background-color: #f8f9fa;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.disabled {
background-color: #f5f5f5;
color: #8c8c8c;
}
.area-list {
color: #666;
font-size: 12px;
@ -810,16 +794,6 @@ const handleSubmit = async () => {
background-color: #ffccc7;
}
.btn-enable {
background-color: #e6f7ee;
color: #00875a;
}
.btn-disable {
background-color: #ffebe6;
color: #cf1322;
}
.no-data {
text-align: center;
padding: 40px 0;
@ -940,13 +914,6 @@ const handleSubmit = async () => {
font-size: 14px;
}
.error-message {
color: #cf1322;
font-size: 12px;
margin-top: 4px;
margin-bottom: 0;
}
.form-actions {
display: flex;
gap: 16px;

@ -1,4 +1,4 @@
<!-- src/views/personnel/Maintenance.vue -->
11<!-- src/views/personnel/Maintenance.vue -->
<template>
<div class="maintenance-page">
<!-- 页面标题和面包屑 -->
@ -26,10 +26,13 @@
<div class="filter-item">
<select v-model="searchFilters.areaId" @change="handleSearch">
<option value="">全部区域</option>
<option value="A">A区</option>
<option value="B">B区</option>
<option value="C">C区</option>
<option value="D">D区</option>
<option
v-for="area in uniqueAreas"
:key="area"
:value="area"
>
{{ area }}
</option>
</select>
</div>
@ -128,6 +131,16 @@
</div>
<div class="modal-body">
<form @submit.prevent="saveRepairman">
<!-- ID字段添加时显示 -->
<div v-if="!isEditing" class="form-group">
<label class="form-label required">ID</label>
<input
type="text"
v-model="form.repairmanId"
required
placeholder="请输入ID"
/>
</div>
<!-- ID字段编辑时显示 -->
<div v-if="isEditing" class="form-group">
<label>ID</label>
@ -156,12 +169,32 @@
placeholder="请输入联系电话"
/>
</div>
<!-- 市区选择 -->
<div class="form-group">
<label class="form-label required">维修片区</label>
<label class="form-label required">所属市区</label>
<select v-model="selectedCity" @change="onCityChange" required>
<option value="">请选择市区</option>
<option
v-for="city in cities"
:key="city.areaId"
:value="city.areaId"
>
{{ city.areaName }}
</option>
</select>
</div>
<!-- 校区选择 -->
<div class="form-group">
<label class="form-label required">维修校区</label>
<select v-model="form.areaId" required>
<option value="">请选择片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="">请选择校区</option>
<option
v-for="campus in campuses"
:key="campus.areaId"
:value="campus.areaName"
>
{{ campus.areaName }}
</option>
</select>
</div>
<div class="form-group">
@ -210,6 +243,13 @@ import type { ResultVO } from '@/api/types/auth'
//
type RepairmanStatus = 'idle' | 'busy' | 'vacation'
//
interface Area {
areaId: string
areaName: string
parentAreaId?: string
}
//
interface MaintenanceStaff {
repairmanId: string
@ -238,10 +278,11 @@ interface FormData {
const authStore = useAuthStore()
const router = useRouter()
// ========== ==========
//
const staffList = ref<MaintenanceStaff[]>([])
const cities = ref<Area[]>([]) //
const campuses = ref<Area[]>([]) //
const selectedCity = ref<string>('') //
const searchFilters = ref<SearchFilters>({
name: '',
areaId: '',
@ -267,6 +308,11 @@ const form = ref<FormData>({
status: 'idle'
})
// ID
const generateId = () => {
return 'RM' + Date.now() + Math.floor(Math.random() * 10000)
}
//
const fetchMaintenanceStaff = async () => {
loading.value = true
@ -274,7 +320,7 @@ const fetchMaintenanceStaff = async () => {
// token
if (!authStore.token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
@ -318,13 +364,79 @@ const fetchMaintenanceStaff = async () => {
// Token
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
}
}
//
const fetchCities = async () => {
try {
const response = await request<ResultVO<any>>(
`/api/web/area/cities`,
{
method: 'GET'
}
)
if (response.code === 200) {
cities.value = response.data.map((area: any) => ({
areaId: area.areaId,
areaName: area.areaName || area.name
}))
} else {
console.error('获取市区列表失败:', response.message)
}
} catch (error) {
console.error('请求市区列表失败:', error)
}
}
// ID
const fetchCampusesByCity = async (cityId: string) => {
if (!cityId) {
campuses.value = []
return
}
try {
const response = await request<ResultVO<any>>(
`/api/web/area/campuses/${cityId}`,
{
method: 'GET'
}
)
if (response.code === 200) {
campuses.value = response.data.map((area: any) => ({
areaId: area.areaId,
areaName: area.areaName || area.name
}))
} else {
console.error('获取校区列表失败:', response.message)
}
} catch (error) {
console.error('请求校区列表失败:', error)
}
}
//
const onCityChange = async () => {
await fetchCampusesByCity(selectedCity.value)
//
if (!isEditing.value) {
form.value.areaId = ''
}
}
//
const uniqueAreas = computed(() => {
const areas = [...new Set(staffList.value.map(staff => staff.areaId))]
return areas.filter(area => area && area.trim() !== '') //
})
//
const filteredStaff = computed(() => {
//
@ -380,24 +492,49 @@ const getStatusText = (status: RepairmanStatus) => {
return statusMap[status] || status
}
// ========== ==========
const openEditForm = (staff: MaintenanceStaff) => {
//
const openEditForm = async (staff: MaintenanceStaff) => {
//
form.value = JSON.parse(JSON.stringify(staff))
//
try {
const response = await request<ResultVO<any>>(
`/api/web/area/${staff.areaId}`,
{
method: 'GET'
}
)
if (response.code === 200) {
const campus = response.data
//
if (campus.parentAreaId) {
selectedCity.value = campus.parentAreaId
//
await fetchCampusesByCity(campus.parentAreaId)
}
}
} catch (error) {
console.error('获取区域信息失败:', error)
}
isEditing.value = true
isModalOpen.value = true
}
// ========== ==========
//
const openAddForm = () => {
//
// ID
form.value = {
repairmanId: '',
repairmanId: generateId(), // ID
repairmanName: '',
phone: '',
areaId: '',
status: 'idle'
}
selectedCity.value = '' //
campuses.value = [] //
isEditing.value = false
isModalOpen.value = true
}
@ -412,6 +549,10 @@ const closeModal = () => {
const saveRepairman = async () => {
try {
//
if (!form.value.repairmanId.trim()) {
alert('请输入ID')
return
}
if (!form.value.repairmanName.trim()) {
alert('请输入姓名')
return
@ -421,7 +562,7 @@ const saveRepairman = async () => {
return
}
if (!form.value.areaId) {
alert('请选择维修区')
alert('请选择维修区')
return
}
@ -437,7 +578,7 @@ const saveRepairman = async () => {
if (response.code === 200) {
alert(isEditing.value ? '维修人员更新成功' : '维修人员新增成功')
closeModal()
fetchMaintenanceStaff() //
await fetchMaintenanceStaff() //
} else {
//
alert(`保存失败:${response.message}`)
@ -481,7 +622,7 @@ const deleteRepairman = async () => {
if (response.code === 200) {
alert('删除成功')
closeDeleteConfirm()
fetchMaintenanceStaff() //
await fetchMaintenanceStaff() //
} else {
alert(`删除失败:${response.message}`)
}
@ -499,8 +640,9 @@ const handleViewRecords = (id: string) => {
}
//
onMounted(() => {
fetchMaintenanceStaff()
onMounted(async () => {
await fetchMaintenanceStaff()
await fetchCities() //
})
</script>
@ -838,4 +980,4 @@ onMounted(() => {
.btn-cancel:hover {
background: #e0e0e0;
}
</style>
</style>

@ -1,4 +1,3 @@
<!-- src/views/personnel/MaintenanceRecord.vue -->
<template>
<div class="record-page">
<!-- 页面标题和面包屑 -->
@ -8,12 +7,11 @@
</div>
<!-- 返回按钮 -->
<div class="back-button">
<button @click="goBack" class="btn-back">
返回维修人员列表
</button>
</div>
<div class="back-button">
<button @click="goBack" class="btn-back">
返回维修人员列表
</button>
</div>
<!-- 维修人员信息 -->
<div class="repairman-info card">
@ -105,7 +103,7 @@
</td>
<td>{{ formatDate(order.createdTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewOrderDetail(order.orderId)">
<button class="btn-view" @click="showOrderDetail(order)">
查看详情
</button>
</td>
@ -137,6 +135,71 @@
下一页
</button>
</div>
<!-- 工单详情弹窗 -->
<div v-if="showDetailModal" class="modal-overlay" @click="closeDetailModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>工单详情</h3>
<button class="close-btn" @click="closeDetailModal">×</button>
</div>
<div class="modal-body">
<div class="order-detail-info">
<div class="info-row">
<span class="info-label">工单号</span>
<span>{{ currentOrder.orderId }}</span>
</div>
<div class="info-row">
<span class="info-label">设备ID</span>
<span>{{ currentOrder.deviceId }}</span>
</div>
<div class="info-row">
<span class="info-label">片区</span>
<span>{{ currentOrder.areaId }}</span>
</div>
<div class="info-row">
<span class="info-label">工单类型</span>
<span>{{ formatOrderType(currentOrder.orderType) }}</span>
</div>
<div class="info-row">
<span class="info-label">状态</span>
<span :class="`status-tag ${currentOrder.status}`">
{{ formatOrderStatus(currentOrder.status) }}
</span>
</div>
<div class="info-row">
<span class="info-label">创建时间</span>
<span>{{ formatDate(currentOrder.createdTime) }}</span>
</div>
<div class="info-row" v-if="currentOrder.grabbedTime">
<span class="info-label">抢单时间</span>
<span>{{ formatDate(currentOrder.grabbedTime) }}</span>
</div>
<div class="info-row" v-if="currentOrder.completedTime">
<span class="info-label">完成时间</span>
<span>{{ formatDate(currentOrder.completedTime) }}</span>
</div>
<div class="info-row">
<span class="info-label">问题描述</span>
<div class="description-content">{{ currentOrder.description }}</div>
</div>
<div class="info-row" v-if="currentOrder.dealNote">
<span class="info-label">处理结果</span>
<div class="deal-note-content">{{ currentOrder.dealNote }}</div>
</div>
<div class="info-row" v-if="currentOrder.imgUrl">
<span class="info-label">处理图片</span>
<div class="image-container">
<img :src="currentOrder.imgUrl" alt="维修图片" class="work-image">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-close" @click="closeDetailModal"></button>
</div>
</div>
</div>
</div>
</template>
@ -146,6 +209,7 @@ import { useRoute, useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import type { WorkOrder } from '@/api/types/workorder'
import type {ResultVO} from "@/api/types/auth"
//
interface RepairmanInfo {
@ -156,10 +220,19 @@ interface RepairmanInfo {
status: RepairmanStatus //
}
interface MaintenanceStaff {
repairmanId: string
repairmanName: string
phone: string
areaId: string
status: RepairmanStatus
}
//
type OrderStatus = 'pending' | 'processing' | 'reviewing' | 'completed' | 'timeout'
type RepairmanStatus = 'idle' | 'busy' | 'vacation'
type OrderType = 'repair' | 'maintenance' | 'inspection'
type OrderPriority = 'low' | 'medium' | 'high' | 'urgent'
const route = useRoute()
const router = useRouter()
@ -180,6 +253,28 @@ const currentPage = ref(1)
const pageSize = 10
const loading = ref(false)
//
const showDetailModal = ref(false)
const currentOrder = ref<WorkOrder>({
orderId: '',
deviceId: '',
areaId: '',
orderType: 'repair', //
description: '',
priority: 'medium', //
status: 'pending',
assignedRepairmanId: '',
createdTime: undefined,
grabbedTime: undefined,
deadline: undefined,
completedTime: undefined,
dealNote: '',
imgUrl: '',
createdBy: '',
updatedTime: undefined,
alertId: undefined
})
//
const fetchRepairmanData = async () => {
loading.value = true
@ -187,22 +282,40 @@ const fetchRepairmanData = async () => {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
await router.push('/login')
return
}
const repairmanId = route.params.id as string
//
//
repairmanInfo.value.repairmanId = repairmanId
repairmanInfo.value.repairmanName = '维修人员姓名'
repairmanInfo.value.phone = '13800138000'
repairmanInfo.value.areaId = '市区'
repairmanInfo.value.status = 'idle'
//
const repairmanResponse = await request<ResultVO<MaintenanceStaff>>(
`/api/web/repairman/${repairmanId}`,
{
method: 'GET'
}
)
if (repairmanResponse.code === 200 && repairmanResponse.data) {
// repairmanInfo
repairmanInfo.value = {
repairmanId: repairmanResponse.data.repairmanId,
repairmanName: repairmanResponse.data.repairmanName,
phone: repairmanResponse.data.phone,
areaId: repairmanResponse.data.areaId,
status: repairmanResponse.data.status
}
} else if (repairmanResponse.code === 404) {
alert('维修人员不存在')
router.push('/home/personnel/maintenance')
return
} else {
console.error('获取维修人员信息失败:', repairmanResponse.message)
alert(`获取维修人员信息失败:${repairmanResponse.message}`)
}
//
const response = await request<{
const orderResponse = await request<{
code: number
msg: string
data: WorkOrder[]
@ -210,10 +323,10 @@ const fetchRepairmanData = async () => {
method: 'GET'
})
if (response.code === 200) {
allOrders.value = response.data || []
if (orderResponse.code === 200) {
allOrders.value = orderResponse.data || []
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
const errorMsg = orderResponse.msg || `获取失败(错误码:${orderResponse.code}`
console.error('获取工单列表失败:', errorMsg)
alert(`获取工单列表失败:${errorMsg}`)
}
@ -228,7 +341,7 @@ const fetchRepairmanData = async () => {
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
await router.push('/login')
}
} finally {
loading.value = false
@ -291,15 +404,36 @@ const getStatusText = (status: RepairmanStatus): string => {
//
const formatOrderStatus = (status: OrderStatus): string => {
const statusMap: Record<OrderStatus, string> = {
'pending': '待抢单',
'pending': '待处理',
'processing': '处理中',
'reviewing': '待审核',
'completed': '已完成',
'timeout': '超时未抢'
'timeout': '超时'
}
return statusMap[status] || status
}
//
const formatOrderType = (type: OrderType): string => {
const typeMap: Record<OrderType, string> = {
'repair': '维修',
'maintenance': '保养',
'inspection': '巡检'
}
return typeMap[type] || type
}
//
const formatOrderPriority = (priority: OrderPriority): string => {
const priorityMap: Record<OrderPriority, string> = {
'low': '低',
'medium': '中',
'high': '高',
'urgent': '紧急'
}
return priorityMap[priority] || priority
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
@ -307,10 +441,35 @@ const formatDate = (dateString?: string): string => {
return date.toLocaleString('zh-CN')
}
//
const viewOrderDetail = (orderId: string) => {
//
router.push(`/home/work-order/detail/${orderId}`)
//
const showOrderDetail = (order: WorkOrder) => {
currentOrder.value = { ...order } //
showDetailModal.value = true
}
//
const closeDetailModal = () => {
showDetailModal.value = false
//
currentOrder.value = {
orderId: '',
deviceId: '',
areaId: '',
orderType: 'repair',
description: '',
priority: 'medium',
status: 'pending',
assignedRepairmanId: '',
createdTime: undefined,
grabbedTime: undefined,
deadline: undefined,
completedTime: undefined,
dealNote: '',
imgUrl: '',
createdBy: '',
updatedTime: undefined,
alertId: undefined
}
}
//
@ -318,9 +477,7 @@ const goBack = () => {
router.back()
}
//
// MaintenanceRecord.vue
onMounted(() => {
const repairmanId = route.params.id as string
if (!repairmanId) {
@ -331,7 +488,6 @@ onMounted(() => {
fetchRepairmanData()
})
</script>
<style scoped>
@ -433,11 +589,6 @@ onMounted(() => {
transition: all 0.3s;
}
.order-tabs button.active {
background: #42b983;
color: white;
}
.order-table {
width: 100%;
border-collapse: collapse;
@ -468,54 +619,6 @@ onMounted(() => {
text-overflow: ellipsis;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.pending {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.processing {
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag.reviewing {
background-color: #f6f7ff;
color: #667eea;
}
.status-tag.completed {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.timeout {
background-color: #ffebe6;
color: #cf1322;
}
.status-tag.idle {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.busy {
background-color: #fffbe6;
color: #d48806;
}
.status-tag.vacation {
background-color: #f0f0f0;
color: #8c8c8c;
}
.operation-buttons {
display: flex;
gap: 8px;
@ -565,6 +668,183 @@ onMounted(() => {
cursor: not-allowed;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 600px;
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 4px;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #eee;
text-align: right;
}
.btn-close {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-close:hover {
background: #e0e0e0;
}
.order-detail-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
}
.info-label {
font-weight: 500;
color: #666;
min-width: 100px;
margin-right: 12px;
flex-shrink: 0;
}
.description-content,
.deal-note-content {
flex: 1;
padding: 8px;
background-color: #fafafa;
border-radius: 4px;
line-height: 1.6;
white-space: pre-wrap;
}
.priority-tag,
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.priority-tag.low {
background-color: #f6ffed;
color: #52c41a;
}
.priority-tag.medium {
background-color: #e6f7ff;
color: #1890ff;
}
.priority-tag.high {
background-color: #fffbe6;
color: #d48806;
}
.priority-tag.urgent {
background-color: #fff2f0;
color: #ff4d4f;
}
.status-tag.pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag.processing {
background-color: #fffbe6;
color: #d48806;
}
.status-tag.reviewing {
background-color: #f6ffed;
color: #52c41a;
}
.status-tag.completed {
background-color: #f0f0f0;
color: #8c8c8c;
}
.status-tag.timeout {
background-color: #fff2f0;
color: #ff4d4f;
}
.image-container {
margin-top: 8px;
flex: 1;
}
.work-image {
max-width: 100%;
max-height: 200px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* 响应式调整 */
@media (max-width: 768px) {
.order-stats {
@ -580,22 +860,35 @@ onMounted(() => {
flex-direction: column;
}
.btn-back {
/* 在 MaintenanceRecord.vue 中更新按钮样式 */
.btn-back {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
border: none; /* 移除边框 */
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
transition: background 0.3s;
}
.btn-back:hover {
background: #e0e0e0;
border-color: #bbb;
}
.modal-content {
width: 95%;
margin: 20px;
}
.info-row {
flex-direction: column;
}
.info-label {
min-width: auto;
margin-bottom: 4px;
}
}
</style>

@ -1,4 +1,3 @@
<!-- src/views/personnel/User.vue -->
<template>
<div class="user-page">
<!-- 页面标题和面包屑 -->
@ -50,16 +49,10 @@
</td>
<td class="operation-buttons">
<button
class="btn-view"
@click="handleView(user.studentId)"
class="btn-delete"
@click="handleDelete(user.studentId, user.studentName)"
>
查看
</button>
<button
class="btn-edit"
@click="handleEdit(user.studentId)"
>
编辑
删除
</button>
</td>
</tr>
@ -70,6 +63,26 @@
</table>
</div>
<!-- 删除确认弹窗 -->
<div class="modal" v-if="showDeleteModal">
<div class="modal-content">
<div class="modal-header">
<h3>确认删除</h3>
<button class="close-btn" @click="closeDeleteModal">&times;</button>
</div>
<div class="modal-body">
<p>确定要删除用户 <strong>{{ deleteUserName }}</strong> </p>
<p>此操作不可撤销删除后用户信息将永久丢失</p>
</div>
<div class="modal-footer">
<button class="btn-cancel" @click="closeDeleteModal"></button>
<button class="btn-confirm-delete" @click="confirmDelete" :disabled="deleteLoading">
{{ deleteLoading ? '删除中...' : '确认删除' }}
</button>
</div>
</div>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
@ -94,10 +107,9 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import {computed, onMounted, ref} from 'vue'
import {request} from '@/api/request'
import {useAuthStore} from '@/stores/auth'
//
type UserStatus = 'active' | 'inactive'
@ -111,7 +123,6 @@ interface User {
}
const authStore = useAuthStore()
const router = useRouter()
//
const users = ref<User[]>([])
@ -120,6 +131,12 @@ const currentPage = ref(1)
const pageSize = 10
const loading = ref(false)
//
const showDeleteModal = ref(false)
const deleteUserId = ref('')
const deleteUserName = ref('')
const deleteLoading = ref(false)
//
const fetchUserList = async () => {
loading.value = true
@ -127,7 +144,7 @@ const fetchUserList = async () => {
// token
if (!authStore.token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
window.location.href = '/login' //
return
}
@ -166,7 +183,7 @@ const fetchUserList = async () => {
// Token
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
window.location.href = '/login'
}
} finally {
loading.value = false
@ -176,9 +193,8 @@ const fetchUserList = async () => {
//
const filteredUsers = computed(() => {
return users.value.filter(user => {
const keywordMatch = searchKeyword.value.trim() === '' ||
user.studentName.toLowerCase().includes(searchKeyword.value.toLowerCase())
return keywordMatch
return searchKeyword.value.trim() === '' ||
user.studentName.toLowerCase().includes(searchKeyword.value.toLowerCase())
})
})
@ -204,14 +220,60 @@ onMounted(() => {
fetchUserList()
})
//
const handleView = (id: string) => {
router.push(`/home/personnel/user/view/${id}`)
// -
const handleDelete = (id: string, name: string) => {
deleteUserId.value = id
deleteUserName.value = name
showDeleteModal.value = true
}
//
const confirmDelete = async () => {
deleteLoading.value = true
try {
//
const response = await request<{
code: number
msg: string
data: null
}>(`/api/web/user/${deleteUserId.value}`, {
method: 'DELETE'
})
if (response.code === 200) {
//
showDeleteModal.value = false
await fetchUserList()
alert('用户删除成功')
} else {
const errorMsg = response.msg || `删除失败(错误码:${response.code}`
console.error('删除用户失败:', errorMsg)
alert(`删除用户失败:${errorMsg}`)
}
} catch (error: any) {
console.error('删除请求异常:', error)
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '删除失败,请稍后重试'
alert(`删除用户失败:${errorMsg}`)
// Token
if (error.message.includes('401')) {
authStore.logout()
window.location.href = '/login'
}
} finally {
deleteLoading.value = false
}
}
//
const handleEdit = (id: string) => {
router.push(`/home/personnel/user/edit/${id}`)
//
const closeDeleteModal = () => {
showDeleteModal.value = false
deleteUserId.value = ''
deleteUserName.value = ''
}
</script>
@ -290,23 +352,6 @@ const handleEdit = (id: string) => {
}
/* 状态标签样式 */
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.active {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.inactive {
background-color: #f5f5f5;
color: #8c8c8c;
}
/* 角色标签样式 */
.role-tag {
@ -340,14 +385,9 @@ const handleEdit = (id: string) => {
opacity: 0.9;
}
.btn-view {
background-color: #f6f7ff;
color: #667eea;
}
.btn-edit {
background-color: #e6f7ff;
color: #1890ff;
.btn-delete {
background-color: #ffe6e6;
color: #ff4d4f;
}
.no-data {
@ -379,6 +419,107 @@ const handleEdit = (id: string) => {
cursor: not-allowed;
}
/* 删除确认弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 450px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #999;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.modal-body p {
margin: 0 0 10px 0;
color: #666;
line-height: 1.5;
}
.modal-body p:last-child {
margin-bottom: 0;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
}
.btn-cancel {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
color: #666;
}
.btn-cancel:hover {
background: #f5f5f5;
}
.btn-confirm-delete {
padding: 8px 16px;
border: none;
background: #ff4d4f;
color: white;
border-radius: 4px;
cursor: pointer;
}
.btn-confirm-delete:hover {
background: #ff7875;
}
.btn-confirm-delete:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 响应式调整 */
@media (max-width: 768px) {
.action-bar {
@ -393,5 +534,13 @@ const handleEdit = (id: string) => {
.search-box input {
width: 100%;
}
.operation-buttons {
flex-wrap: wrap;
}
.modal-content {
width: 90%;
}
}
</style>

@ -1,4 +1,3 @@
<!-- src/views/workorder/Completed.vue -->
<template>
<div class="order-completed-page">
<!-- 页面标题和面包屑 -->
@ -21,15 +20,25 @@
>
</div>
<!-- 区筛选 -->
<!-- 区筛选 -->
<div class="filter-item">
<label>所属片区</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<label>所属市区</label>
<select v-model="filterForm.cityId" class="filter-select" @change="onCityChange">
<option value="">全部市区</option>
<option v-for="city in cityList" :key="city.areaId" :value="city.areaId">
{{ city.areaName }}
</option>
</select>
</div>
<!-- 校区筛选 -->
<div class="filter-item" v-if="filterForm.cityId">
<label>所属校区</label>
<select v-model="filterForm.campusId" class="filter-select" @change="handleFilter">
<option value="">全部校区</option>
<option v-for="campus in campusList" :key="campus.areaId" :value="campus.areaId">
{{ campus.areaName }}
</option>
</select>
</div>
@ -55,7 +64,7 @@
<tr>
<th>工单号</th>
<th>设备</th>
<th></th>
<th></th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
@ -67,11 +76,10 @@
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
<td>{{ order.area }}</td>
<td>{{ order.campusName }}</td>
<td class="desc-cell">{{ order.problemDesc }}</td>
<td>
<span :class="`status-tag ${order.status}`">
@ -116,7 +124,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
@ -125,19 +133,30 @@ import { useAuthStore } from '@/stores/auth'
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
//
interface CompletedOrder {
interface WorkOrder {
id: string
orderNo: string
deviceType: string // /
deviceId: string // ID
area: string //
areaId: string // ID
campusName?: string //
problemDesc: string //
status: OrderStatus //
createTime: string //
}
//
interface Area {
areaId: string
areaName: string
areaType: 'zone' | 'campus' // zone=campus=
parentAreaId?: string // ID
}
//
const orders = ref<CompletedOrder[]>([])
const orders = ref<WorkOrder[]>([])
const cityList = ref<Area[]>([]) //
const campusList = ref<Area[]>([]) //
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
@ -149,15 +168,14 @@ const searchKeyword = ref('')
//
const filterForm = ref({
area: '', //
cityId: '', //
campusId: '', //
createDate: '' //
})
//
const loadCompletedOrders = async () => {
loading.value = true
//
const loadCityList = async () => {
try {
// Token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
@ -165,42 +183,119 @@ const loadCompletedOrders = async () => {
return
}
console.log('当前 Token:', token.substring(0, 20) + '...')
console.log('正在加载市区列表...')
//
let url = ''
const params = new URLSearchParams()
const response = await request<{
code: number
msg: string
data: Area[]
}>('/api/web/area/cities', {
method: 'GET',
})
if (response.code === 200) {
cityList.value = response.data || []
console.log('成功加载市区列表:', cityList.value)
} else {
const errorMsg = response.msg || `获取市区失败(错误码:${response.code}`
console.error('获取市区列表失败:', errorMsg)
alert(`获取市区列表失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求市区列表异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取市区列表失败,请稍后重试'
alert(`获取市区列表失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}
}
}
// ID
const loadCampusList = async (cityId: string) => {
if (!cityId) {
campusList.value = []
return
}
if (filterForm.value.createDate) {
// 使 by-time-range
url = '/api/work-orders/by-time-range'
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const startDate = new Date(filterForm.value.createDate)
const endDate = new Date(startDate)
endDate.setDate(endDate.getDate() + 1)
console.log('正在加载校区列表...', cityId)
params.append('startTime', startDate.toISOString())
params.append('endTime', endDate.toISOString())
params.append('status', 'completed') //
const response = await request<{
code: number
msg: string
data: Area[]
}>(`/api/web/area/campuses/${cityId}`, {
method: 'GET',
})
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
if (response.code === 200) {
campusList.value = response.data || []
console.log('成功加载校区列表:', campusList.value)
} else {
// 使 by-status
url = '/api/work-orders/by-status'
params.append('status', 'completed')
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
const errorMsg = response.msg || `获取校区失败(错误码:${response.code}`
console.error('获取校区列表失败:', errorMsg)
alert(`获取校区列表失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求校区列表异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取校区列表失败,请稍后重试'
alert(`获取校区列表失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}
}
}
//
const loadCompletedOrders = async () => {
loading.value = true
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('当前 Token:', token.substring(0, 20) + '...')
//
let url = '/api/work-orders/by-status'
const params = new URLSearchParams()
params.append('status', 'completed')
const queryString = params.toString()
if (queryString) {
url += `?${queryString}`
@ -217,16 +312,23 @@ const loadCompletedOrders = async () => {
//
if (response.code === 200) {
orders.value = (response.data || []).map((order: any) => ({
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status || 'completed',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间'
}))
orders.value = (response.data || []).map((order: any) => {
// areaId
const campus = campusList.value.find(campus => campus.areaName === order.areaId);
const areaId = campus ? campus.areaId : order.areaId; // 使ID使
return {
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
areaId: areaId,
campusName: order.areaId || order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status || 'completed',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间'
}
})
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取已结单工单失败:', errorMsg)
@ -256,6 +358,20 @@ const loadCompletedOrders = async () => {
}
}
//
const onCityChange = async () => {
console.log('市区选择改变:', filterForm.value.cityId)
//
filterForm.value.campusId = ''
//
if (filterForm.value.cityId) {
await loadCampusList(filterForm.value.cityId)
}
//
}
//
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
@ -268,7 +384,7 @@ const formatStatus = (status: OrderStatus): string => {
return statusMap[status] || status
}
// -
//
const filteredOrders = computed(() => {
return orders.value.filter(order => {
// /ID
@ -276,15 +392,15 @@ const filteredOrders = computed(() => {
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
// ID
const campusMatch = filterForm.value.campusId === '' ||
campusList.value.find(campus => campus.areaId === filterForm.value.campusId)?.areaName === order.campusName
//
// const dateMatch = filterForm.value.createDate === '' ||
// order.createTime.split(' ')[0] === filterForm.value.createDate
//
const dateMatch = filterForm.value.createDate === '' ||
order.createTime.split(' ')[0] === filterForm.value.createDate
// dateMatch
return keywordMatch && areaMatch
return keywordMatch && campusMatch && dateMatch
})
})
@ -305,21 +421,23 @@ const handleSearch = () => {
currentPage.value = 1 //
}
// /
// /
const handleFilter = () => {
currentPage.value = 1 //
loadCompletedOrders() //
//
}
//
const resetFilter = () => {
searchKeyword.value = '' //
filterForm.value = {
area: '',
cityId: '',
campusId: '',
createDate: ''
}
campusList.value = [] //
currentPage.value = 1
loadCompletedOrders()
//
}
//
@ -327,10 +445,24 @@ const viewOrderDetail = (id: string) => {
router.push(`/home/work-order/completed/${id}`)
}
//
watch(() => filterForm.value.campusId, () => {
handleFilter()
})
//
onMounted(() => {
onMounted(async () => {
console.log('页面加载开始')
console.log('Token:', authStore.token)
loadCompletedOrders()
try {
await loadCityList()
console.log('市区列表加载完成:', cityList.value)
await loadCompletedOrders()
} catch (error) {
console.error('页面加载失败:', error)
}
})
</script>

@ -20,15 +20,18 @@
>
</div>
<!-- 区筛选 -->
<!-- 区筛选 -->
<div class="filter-item">
<label>所属</label>
<label>所属</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="">全部校区</option>
<option
v-for="areaOption in areaOptions"
:key="areaOption"
:value="areaOption"
>
{{ areaOption }}
</option>
</select>
</div>
@ -54,7 +57,7 @@
<tr>
<th>工单号</th>
<th>设备</th>
<th></th>
<th></th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
@ -66,7 +69,6 @@
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
@ -183,7 +185,7 @@ interface ToClaimOrder {
orderNo: string
deviceType: string // /
deviceId: string // ID
area: string //
area: string //
problemDesc: string //
status: OrderStatus //
createTime: string //
@ -204,7 +206,7 @@ const searchKeyword = ref('')
//
const filterForm = ref({
area: '', //
area: '', //
createDate: '' //
})
@ -212,6 +214,17 @@ const filterForm = ref({
const showDetailModal = ref(false)
const currentOrder = ref<ToClaimOrder | null>(null)
//
const areaOptions = computed(() => {
const areas = new Set<string>()
orders.value.forEach(order => {
if (order.area) {
areas.add(order.area)
}
})
return Array.from(areas).sort()
})
// -
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
@ -241,10 +254,6 @@ const loadAvailableOrders = async () => {
let url = ''
const params = new URLSearchParams()
// areaId
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
//
if (filterForm.value.createDate) {
// 使 by-time-range
@ -257,16 +266,11 @@ const loadAvailableOrders = async () => {
params.append('startTime', startDate.toISOString())
params.append('endTime', endDate.toISOString())
if (areaId) {
params.append('areaId', areaId)
}
params.append('status', 'pending') //
} else {
// 使 available
// 使 available
url = '/api/work-orders/available'
if (areaId) {
params.append('areaId', areaId)
}
params.append('status', 'pending')
}
const queryString = params.toString()
@ -288,7 +292,7 @@ const loadAvailableOrders = async () => {
orderNo: order.orderId || '',
deviceType: '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
area: order.areaName || order.areaId || '', // 使 areaName使 areaId
problemDesc: order.description || '暂无描述',
status: order.status || 'pending',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
@ -324,13 +328,9 @@ const filteredOrders = computed(() => {
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
// const dateMatch = filterForm.value.createDate === '' ||
// order.createTime.split(' ')[0] === filterForm.value.createDate
return keywordMatch && areaMatch
})
})
@ -354,10 +354,14 @@ const handleSearch = () => {
//
}
// /
// /
const handleFilter = () => {
currentPage.value = 1 //
loadAvailableOrders() //
//
if (filterForm.value.createDate) {
loadAvailableOrders() //
}
//
}
//
@ -368,7 +372,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadAvailableOrders()
loadAvailableOrders() //
}
//
@ -733,4 +737,4 @@ onMounted(() => {
flex: 0 0 100px;
}
}
</style>
</style>

@ -20,15 +20,18 @@
>
</div>
<!-- 区筛选 -->
<!-- 区筛选 -->
<div class="filter-item">
<label>所属</label>
<label>所属</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="">全部校区</option>
<option
v-for="areaOption in areaOptions"
:key="areaOption"
:value="areaOption"
>
{{ areaOption }}
</option>
</select>
</div>
@ -54,7 +57,7 @@
<tr>
<th>工单号</th>
<th>设备</th>
<th></th>
<th></th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
@ -66,7 +69,6 @@
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
@ -184,7 +186,7 @@ interface ProcessingOrder {
orderNo: string
deviceType: string // /
deviceId: string // ID
area: string //
area: string //
problemDesc: string //
status: OrderStatus // state
createTime: string //
@ -207,7 +209,7 @@ const searchKeyword = ref('')
//
const filterForm = ref({
area: '', //
area: '', //
createDate: '' //
})
@ -215,6 +217,17 @@ const filterForm = ref({
const showDetailModal = ref(false)
const currentOrder = ref<ProcessingOrder | null>(null)
//
const areaOptions = computed(() => {
const areas = new Set<string>()
orders.value.forEach(order => {
if (order.area) {
areas.add(order.area)
}
})
return Array.from(areas).sort()
})
// state
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
@ -255,18 +268,10 @@ const loadProcessingOrders = async () => {
params.append('startTime', startDate.toISOString())
params.append('endTime', endDate.toISOString())
params.append('status', 'processing') //
if (filterForm.value.area) {
params.append('areaId', filterForm.value.area)
}
} else {
// 使 by-status
// 使 by-status
url = '/api/work-orders/by-status'
params.append('status', 'processing')
if (filterForm.value.area) {
params.append('areaId', filterForm.value.area)
}
}
const queryString = params.toString()
@ -290,14 +295,15 @@ const loadProcessingOrders = async () => {
orderNo: order.orderId,
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId,
area: order.areaId,
area: order.areaName || order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status,
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.updatedTime ? new Date(order.updatedTime).toLocaleString('zh-CN') : '未知时间',
location: order.location || '未知位置',
maintenanceName: order.assignedRepairmanName || '未分配',
maintenancePhone: order.assignedRepairmanPhone || '未知'
// ID
maintenanceName: order.assignedRepairmanId || '未分配',
maintenancePhone: '未知'
}))
} else {
console.error('获取处理中工单失败:', response.msg)
@ -320,13 +326,9 @@ const filteredOrders = computed(() => {
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
//const dateMatch = filterForm.value.createDate === '' ||
// order.createTime.split(' ')[0] === filterForm.value.createDate
return keywordMatch && areaMatch
})
})
@ -348,10 +350,14 @@ const handleSearch = () => {
currentPage.value = 1 //
}
// /
// /
const handleFilter = () => {
currentPage.value = 1 //
loadProcessingOrders()
//
if (filterForm.value.createDate) {
loadProcessingOrders() //
}
//
}
//
@ -362,7 +368,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadProcessingOrders()
loadProcessingOrders() //
}
//

@ -21,15 +21,18 @@
>
</div>
<!-- 区筛选 -->
<!-- 区筛选 -->
<div class="filter-item">
<label>所属</label>
<label>所属</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="">全部校区</option>
<option
v-for="areaOption in areaOptions"
:key="areaOption"
:value="areaOption"
>
{{ areaOption }}
</option>
</select>
</div>
@ -55,7 +58,7 @@
<tr>
<th>工单号</th>
<th>设备</th>
<th></th>
<th></th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
@ -130,7 +133,7 @@ interface ReviewOrder {
orderNo: string
deviceType: string // /
deviceId: string // ID
area: string //
area: string //
problemDesc: string //
status: OrderStatus //
createTime: string //
@ -149,10 +152,21 @@ const searchKeyword = ref('')
//
const filterForm = ref({
area: '', //
area: '', //
createDate: '' //
})
//
const areaOptions = computed(() => {
const areas = new Set<string>()
orders.value.forEach(order => {
if (order.area) {
areas.add(order.area)
}
})
return Array.from(areas).sort()
})
//
// -
const loadReviewOrders = async () => {
@ -184,22 +198,10 @@ const loadReviewOrders = async () => {
params.append('startTime', startDate.toISOString())
params.append('endTime', endDate.toISOString())
params.append('status', 'reviewing') //
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
} else {
// 使 by-status
// 使 by-status
url = '/api/work-orders/by-status'
params.append('status', 'reviewing')
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
}
const queryString = params.toString()
@ -223,7 +225,7 @@ const loadReviewOrders = async () => {
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
area: order.areaName || order.areaId || '', // 使 areaName使 areaId
problemDesc: order.description || '暂无描述',
status: order.status || 'reviewing',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间'
@ -277,14 +279,9 @@ const filteredOrders = computed(() => {
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
// const dateMatch = filterForm.value.createDate === '' ||
// order.createTime.split(' ')[0] === filterForm.value.createDate
// dateMatch
return keywordMatch && areaMatch
})
})
@ -306,10 +303,14 @@ const handleSearch = () => {
currentPage.value = 1 //
}
// /
// /
const handleFilter = () => {
currentPage.value = 1 //
loadReviewOrders() //
//
if (filterForm.value.createDate) {
loadReviewOrders() //
}
//
}
//
@ -320,7 +321,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadReviewOrders()
loadReviewOrders() //
}
//

@ -21,15 +21,18 @@
>
</div>
<!-- 区筛选 -->
<!-- 区筛选 -->
<div class="filter-item">
<label>所属</label>
<label>所属</label>
<select v-model="filterForm.area" class="filter-select" @change="handleFilter">
<option value="">全部片区</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<option value="">全部校区</option>
<option
v-for="areaOption in areaOptions"
:key="areaOption"
:value="areaOption"
>
{{ areaOption }}
</option>
</select>
</div>
@ -55,7 +58,7 @@
<tr>
<th>工单号</th>
<th>设备</th>
<th></th>
<th></th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
@ -67,7 +70,6 @@
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
@ -212,7 +214,7 @@ interface TimeoutOrder {
orderNo: string
deviceType: string // /
deviceId: string // ID
area: string //
area: string //
problemDesc: string //
status: OrderStatus //
createTime: string //
@ -242,7 +244,7 @@ const searchKeyword = ref('')
//
const filterForm = ref({
area: '', //
area: '', //
createDate: '' //
})
@ -253,6 +255,17 @@ const selectedStaffId = ref('')
const assignRemark = ref('')
const allStaff = ref<Repairman[]>([])
//
const areaOptions = computed(() => {
const areas = new Set<string>()
orders.value.forEach(order => {
if (order.area) {
areas.add(order.area)
}
})
return Array.from(areas).sort()
})
// -
const loadTimeoutOrders = async () => {
loading.value = true
@ -283,22 +296,10 @@ const loadTimeoutOrders = async () => {
params.append('startTime', startDate.toISOString())
params.append('endTime', endDate.toISOString())
params.append('status', 'timeout') //
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
} else {
// 使 by-status
// 使 by-status
url = '/api/work-orders/by-status'
params.append('status', 'timeout')
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
}
const queryString = params.toString()
@ -322,7 +323,7 @@ const loadTimeoutOrders = async () => {
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
area: order.areaName || order.areaId || '', // 使 areaName使 areaId
problemDesc: order.description || '暂无描述',
status: order.status || 'timeout',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
@ -421,14 +422,13 @@ const filteredOrders = computed(() => {
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
// const dateMatch = filterForm.value.createDate === '' ||
// order.createTime.split(' ')[0] === filterForm.value.createDate
// dateMatch
return keywordMatch && areaMatch
})
})
@ -461,10 +461,14 @@ const handleSearch = () => {
currentPage.value = 1 //
}
// /
// /
const handleFilter = () => {
currentPage.value = 1 //
loadTimeoutOrders() //
//
if (filterForm.value.createDate) {
loadTimeoutOrders() //
}
//
}
//
@ -475,7 +479,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadTimeoutOrders()
loadTimeoutOrders() //
}
//

@ -15,7 +15,7 @@ export default defineConfig({
proxy: {
// 代理所有以 /api 开头的请求到后端
'/api': {
target: 'http://localhost:8080', // Spring Boot 后端地址
target: 'https://120.46.151.248:8081', // Spring Boot 后端地址
changeOrigin: true, // 改变请求来源
secure: false, // 如果是https可能需要设置为false
// 如果需要重写路径,可以取消下面的注释
@ -31,5 +31,20 @@ export default defineConfig({
port: 5173, // Vite默认端口
// 可选:自动打开浏览器
open: true
},
// ✅ 添加生产配置
base: './', // 使用相对路径,重要!
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false, // 生产环境关闭sourcemap
rollupOptions: {
output: {
// 代码分割配置
manualChunks: {
vendor: ['vue', 'vue-router'], // 第三方库
}
}
}
}
})

File diff suppressed because it is too large Load Diff

@ -16,12 +16,15 @@
"lint": "run-s lint:*"
},
"dependencies": {
"@capacitor/android": "^8.0.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@eslint/js": "^9.39.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vitest/eslint-plugin": "^1.5.0",

@ -4,7 +4,7 @@ import { useAuthStore } from '@/stores/auth'
// 创建最基本的axios实例
const apiClient = axios.create({
baseURL: 'http://120.46.151.248:8080',
baseURL: 'https://120.46.151.248:8081',
headers: {
'Content-Type': 'application/json'
}

@ -19,7 +19,6 @@
<div class="area-card">
<div class="area-info">
<div class="area-name">岳麓片区</div>
<div class="area-details">5所学校 · 12台制水机</div>
</div>
<div class="area-divider"></div>
<button class="view-btn" @click="viewYueluArea"></button>

@ -53,7 +53,7 @@
<button class="action-btn detail" @click="viewDeviceDetail(device.id)">
详情
</button>
<button class="action-btn inspect" @click="viewWaterSupplier(device.id)">
<button class="action-btn inspect" @click="viewRelatedWaterSuppliers(device)">
查看供水机
</button>
</div>
@ -77,6 +77,7 @@
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { deviceService } from '@/services/deviceService'
import api from '@/services/api'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
@ -131,7 +132,44 @@ const fetchWaterMakers = async () => {
}
}
//
//
const fetchRelatedWaterSuppliers = async (makerId) => {
try {
loading.value = true
//
const response = await api.get(`/api/web/device/maker/${makerId}/suppliers`)
if (response.data.code === 200) {
const relatedSuppliers = response.data.data || []
//
if (relatedSuppliers.length === 0) {
alert('该制水机暂未关联供水机')
return
}
// ID
const supplierIds = relatedSuppliers.map(supplier => supplier.deviceId)
router.push({
path: '/inspection/water-supplier',
query: {
makerId: makerId,
relatedSuppliers: JSON.stringify(supplierIds),
fromMaker: true //
}
})
} else {
error.value = response.data.message
}
} catch (err) {
console.error('获取关联供水机失败:', err)
alert('获取关联供水机失败,请稍后重试')
} finally {
loading.value = false
}
}
//
const goBack = () => {
router.back()
}
@ -156,9 +194,9 @@ const viewDeviceDetail = (deviceId) => {
router.push(`/inspection/water-maker/${deviceId}`)
}
const viewWaterSupplier = (deviceId) => {
// ID
router.push(`/inspection/water-supplier?makerId=${deviceId}`)
//
const viewRelatedWaterSuppliers = (device) => {
fetchRelatedWaterSuppliers(device.id)
}
//

@ -1,11 +1,13 @@
<template>
<div class="water-supplier-list">
<!-- 顶部标题栏 -->
<!-- 顶部标题栏 - 添加返回按钮和制水机名称显示 -->
<div class="header">
<div class="header-left">
<span class="back-btn" @click="goBack"></span>
</div>
<div class="header-title">设备巡检</div>
<div class="header-title">
{{ fromMaker ? `制水机#${makerId}的供水机` : '设备巡检' }}
</div>
<div class="header-right">
<span class="nav-text">供水机</span>
</div>
@ -28,7 +30,9 @@
<!-- 正常状态 -->
<div v-else class="device-list">
<div v-for="device in deviceList" :key="device.id" class="device-item">
<div v-for="device in filteredDeviceList" :key="device.id" class="device-item">
<div class="device-info">
<div class="device-name">{{ device.name }}</div>
<div class="device-location">{{ device.location }}</div>
@ -48,6 +52,11 @@
</button>
</div>
</div>
<!-- 没有供水机时的提示 -->
<div v-if="fromMaker && filteredDeviceList.length === 0" class="empty-tip">
该制水机暂未关联任何供水机
</div>
</div>
</div>
@ -62,19 +71,26 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { deviceService } from '@/services/deviceService'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
//
const deviceList = ref([])
const filteredDeviceList = ref([])
const loading = ref(true)
const error = ref(null)
//
const makerId = ref(route.query.makerId || '')
const relatedSuppliers = ref([])
const fromMaker = ref(route.query.fromMaker === 'true')
//
const fetchWaterSuppliers = async () => {
try {
@ -94,6 +110,21 @@ const fetchWaterSuppliers = async () => {
storageCapacity: device.storageCapacity || 0,
areaId: device.areaId
}))
//
if (fromMaker.value && route.query.relatedSuppliers) {
try {
relatedSuppliers.value = JSON.parse(route.query.relatedSuppliers)
filteredDeviceList.value = deviceList.value.filter(device =>
relatedSuppliers.value.includes(device.id)
)
} catch (e) {
console.error('解析关联供水机参数失败:', e)
filteredDeviceList.value = deviceList.value
}
} else {
filteredDeviceList.value = deviceList.value
}
} else {
error.value = response.message
}
@ -412,4 +443,21 @@ onMounted(() => {
color: #999;
margin-top: 4px;
}
.related-tip {
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
padding: 10px 16px;
margin-bottom: 16px;
color: #1890ff;
font-size: 14px;
}
.empty-tip {
text-align: center;
padding: 40px;
color: #999;
font-size: 16px;
}
</style>

Loading…
Cancel
Save