feat: ODF v1.0.2 功能更新 - 签到、干线故障、光缆管理、用户模块权限

数据库:
- 新增 odf_checkin/odf_cables/odf_cable_faults/odf_cable_fault_images/odf_user_modules 5张表
- 新增菜单权限和角色分配 SQL 脚本

后台 API (.NET/SqlSugar):
- 新增实体模型、DTO、Service、Controller (签到/光缆/故障/图片/用户模块)

前端 APP (UniApp):
- 新增 portal/checkin/trunk/cable/fault-list/fault-detail/fault-add/trunk-search/route-plan 9个页面
- 新增 permission/checkin/trunk 服务层
- 新增 navigation/watermark 工具函数

后台管理前端 (ZR.Vue):
- 新增光缆管理/干线故障管理/签到记录管理/用户模块权限 4个管理页面
- 新增对应 API 模块和表单组件
This commit is contained in:
zpc 2026-03-04 14:08:48 +08:00
parent 13d10f4f9b
commit 7c4d7d5978
58 changed files with 5913 additions and 75 deletions

View File

@ -6,23 +6,23 @@
## 任务
- [ ] 1. 数据库:创建新表和索引
- [ ] 1.1 创建 odf_checkin 签到记录表(含索引 IX_odf_checkin_RoomId、IX_odf_checkin_CheckinTime
- [x] 1. 数据库:创建新表和索引
- [x] 1.1 创建 odf_checkin 签到记录表(含索引 IX_odf_checkin_RoomId、IX_odf_checkin_CheckinTime
- 编写 SQL Server DDL 脚本,创建表和索引
- _需求: 2.6_
- [ ] 1.2 创建 odf_cables 光缆表(含索引 IX_odf_cables_DeptId、IX_odf_cables_CableName
- [x] 1.2 创建 odf_cables 光缆表(含索引 IX_odf_cables_DeptId、IX_odf_cables_CableName
- 编写 SQL Server DDL 脚本,创建表和索引
- _需求: 4.1_
- [ ] 1.3 创建 odf_cable_faults 干线故障表(含索引 IX_odf_cable_faults_CableId、IX_odf_cable_faults_FaultTime
- [x] 1.3 创建 odf_cable_faults 干线故障表(含索引 IX_odf_cable_faults_CableId、IX_odf_cable_faults_FaultTime
- 编写 SQL Server DDL 脚本,创建表和索引
- _需求: 5.1, 7.9_
- [ ] 1.4 创建 odf_cable_fault_images 故障图片表(含索引 IX_odf_cable_fault_images_FaultId
- [x] 1.4 创建 odf_cable_fault_images 故障图片表(含索引 IX_odf_cable_fault_images_FaultId
- 编写 SQL Server DDL 脚本,创建表和索引
- _需求: 7.9_
- [ ] 1.5 创建 odf_user_modules 用户功能版块权限表(含唯一索引和普通索引)
- [x] 1.5 创建 odf_user_modules 用户功能版块权限表(含唯一索引和普通索引)
- 编写 SQL Server DDL 脚本,创建表和索引
- _需求: 8.1_
- [ ] 1.6 插入后台管理菜单和权限 SQL 脚本
- [x] 1.6 插入后台管理菜单和权限 SQL 脚本
- 编写 INSERT INTO sys_menu SQL 脚本添加以下一级菜单parentId=0和按钮权限
- 一级菜单「光缆管理」MenuId=11190OrderNum=5及按钮权限odfcables:query / odfcables:add / odfcables:edit / odfcables:delete / odfcables:export
- 一级菜单「干线故障管理」MenuId=11200OrderNum=6及按钮权限odfcablefaults:query / odfcablefaults:delete / odfcablefaults:export
@ -36,26 +36,26 @@
- Role 1 (admin):超级管理员自动拥有所有权限,无需插入
- _需求: 8.1_
- [ ] 2. 检查点 — 数据库脚本完成
- [x] 2. 检查点 — 数据库脚本完成
- 确认所有 5 张表和索引创建成功,菜单权限 SQL 脚本执行无误,如有问题请告知。
- [ ] 3. 后台 API实体模型层
- [ ] 3.1 创建 OdfCheckin 实体类ZR.Model/Business/OdfCheckin.cs
- [x] 3. 后台 API实体模型层
- [x] 3.1 创建 OdfCheckin 实体类ZR.Model/Business/OdfCheckin.cs
- 使用 [SugarTable("odf_checkin")] 注解,定义 Id、RoomId、Personnel、CheckinTime、WorkContent、UserId、CreatedAt 属性
- _需求: 2.6_
- [ ] 3.2 创建 OdfCables 实体类ZR.Model/Business/OdfCables.cs
- [x] 3.2 创建 OdfCables 实体类ZR.Model/Business/OdfCables.cs
- 使用 [SugarTable("odf_cables")] 注解,定义 Id、CableName、DeptId、DeptName、CreatedAt、UpdatedAt 属性
- _需求: 4.1_
- [ ] 3.3 创建 OdfCableFaults 实体类ZR.Model/Business/OdfCableFaults.cs
- [x] 3.3 创建 OdfCableFaults 实体类ZR.Model/Business/OdfCableFaults.cs
- 使用 [SugarTable("odf_cable_faults")] 注解,定义所有字段属性
- _需求: 5.1, 7.9_
- [ ] 3.4 创建 OdfCableFaultImages 实体类ZR.Model/Business/OdfCableFaultImages.cs
- [x] 3.4 创建 OdfCableFaultImages 实体类ZR.Model/Business/OdfCableFaultImages.cs
- 使用 [SugarTable("odf_cable_fault_images")] 注解
- _需求: 7.9_
- [ ] 3.5 创建 OdfUserModules 实体类ZR.Model/Business/OdfUserModules.cs
- [x] 3.5 创建 OdfUserModules 实体类ZR.Model/Business/OdfUserModules.cs
- 使用 [SugarTable("odf_user_modules")] 注解
- _需求: 8.1_
- [ ] 3.6 创建各实体的 Dto 和 QueryDto 类
- [x] 3.6 创建各实体的 Dto 和 QueryDto 类
- OdfCheckinDto、OdfCheckinQueryDto继承 PagerInfo
- OdfCablesQueryDto继承 PagerInfo
- OdfCableFaultsQueryDto继承 PagerInfo含 beginFaultTime/endFaultTime
@ -63,8 +63,8 @@
- OdfUserModulesSaveDto含 userId、modules 列表)
- _需求: 2.6, 4.1, 5.1, 7.9, 8.1_
- [ ] 4. 后台 APIService 层
- [ ] 4.1 实现 IOdfUserModulesService / OdfUserModulesService
- [x] 4. 后台 APIService 层
- [x] 4.1 实现 IOdfUserModulesService / OdfUserModulesService
- GetUserModules(userId):按 UserId 查询 odf_user_modules返回 ModuleCode 列表
- GetUserList():查询 sys_user 返回用户列表userId, userName
- SaveUserModules(userId, modules):先删后插,事务保证原子性
@ -73,7 +73,7 @@
- **Property 15: 用户模块权限 round-trip**
- 使用 FsCheck 生成随机 UserId 和模块子集,验证插入后查询结果一致
- **验证: 需求 8.1, 8.2**
- [ ] 4.2 实现 IOdfCheckinService / OdfCheckinService
- [x] 4.2 实现 IOdfCheckinService / OdfCheckinService
- AddCheckin(dto):校验 RoomId 存在,插入 odf_checkin 记录
- GetList(queryDto):分页查询,联查 odf_rooms 获取机房名称,联查 sys_user 获取提交人用户名
- Export(queryDto):导出签到记录
@ -82,7 +82,7 @@
- **Property 11: 签到数据持久化 round-trip**
- 使用 FsCheck 生成随机签到数据,验证插入后查询字段一致
- **验证: 需求 2.6**
- [ ] 4.3 实现 IOdfCablesService / OdfCablesService
- [x] 4.3 实现 IOdfCablesService / OdfCablesService
- GetList(queryDto):按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询)
- Search(deptId, keyword):在指定公司范围内搜索光缆和故障
- Add/Update/Delete/GetDetail/Export光缆 CRUD 和导出
@ -95,7 +95,7 @@
- **Property 13: 搜索结果 DeptId 范围限定**
- 使用 FsCheck 生成随机数据集,验证搜索结果均属于指定 DeptId
- **验证: 需求 4.4**
- [ ] 4.4 实现 IOdfCableFaultsService / OdfCableFaultsService
- [x] 4.4 实现 IOdfCableFaultsService / OdfCableFaultsService
- GetList(queryDto):按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序
- GetDetail(id):查询故障详情,联查光缆名称和图片列表
- AddFault(dto):校验 CableId 存在和图片数量,插入故障记录,保存图片文件,插入图片记录
@ -106,25 +106,25 @@
- **Property 14: 故障新增 round-trip含图片**
- 使用 FsCheck 生成随机故障数据和图片数量,验证插入后查询一致
- **验证: 需求 7.9**
- [ ] 4.5 实现 IOdfCableFaultImagesService / OdfCableFaultImagesService
- [x] 4.5 实现 IOdfCableFaultImagesService / OdfCableFaultImagesService
- GetByFaultId(faultId):按故障 ID 查询图片列表
- BatchInsert(faultId, imageUrls):批量插入图片记录
- DeleteByFaultId(faultId):按故障 ID 删除所有图片记录
- _需求: 7.9_
- [ ] 5. 后台 APIController 层
- [ ] 5.1 创建 OdfUserModulesController路由 business/OdfUserModules
- [x] 5. 后台 APIController 层
- [x] 5.1 创建 OdfUserModulesController路由 business/OdfUserModules
- GET list — 获取当前登录用户的功能版块列表APP 端调用,无权限限制,登录即可)
- GET users — 获取用户列表(管理端调用,[ActionPermissionFilter(Permission = "odfusermodules:query")]
- GET list?userId=xxx — 获取指定用户的模块权限(管理端调用,[ActionPermissionFilter(Permission = "odfusermodules:query")]
- POST save — 批量保存用户模块权限(管理端调用,[ActionPermissionFilter(Permission = "odfusermodules:edit")]
- _需求: 8.1, 8.2_
- [ ] 5.2 创建 OdfCheckinController路由 business/OdfCheckin
- [x] 5.2 创建 OdfCheckinController路由 business/OdfCheckin
- POST submit — 提交签到记录APP 端调用,[ActionPermissionFilter(Permission = "odfcheckin:list")]
- GET list — 分页查询签到记录(管理端调用,[ActionPermissionFilter(Permission = "odfcheckin:list")],联查机房名称和提交人)
- GET export — 导出签到记录(管理端调用,[ActionPermissionFilter(Permission = "odfcheckin:export")]
- _需求: 2.6_
- [ ] 5.3 创建 OdfCablesController路由 business/OdfCables
- [x] 5.3 创建 OdfCablesController路由 business/OdfCables
- GET list — 光缆列表([ActionPermissionFilter(Permission = "odfcables:list")]
- GET search — 搜索光缆和故障([ActionPermissionFilter(Permission = "odfcables:query")]
- POST — 新增光缆([ActionPermissionFilter(Permission = "odfcables:add")]
@ -133,7 +133,7 @@
- POST delete/{id} — 删除光缆([ActionPermissionFilter(Permission = "odfcables:delete")]
- GET export — 导出光缆([ActionPermissionFilter(Permission = "odfcables:export")]
- _需求: 4.1, 4.4_
- [ ] 5.4 创建 OdfCableFaultsController路由 business/OdfCableFaults
- [x] 5.4 创建 OdfCableFaultsController路由 business/OdfCableFaults
- GET list — 故障列表分页查询([ActionPermissionFilter(Permission = "odfcablefaults:list")]
- GET {id} — 故障详情含图片([ActionPermissionFilter(Permission = "odfcablefaults:query")]
- POST add — 新增故障含图片上传APP 端调用,[ActionPermissionFilter(Permission = "odfcablefaults:list")]
@ -141,7 +141,7 @@
- GET export — 导出故障列表([ActionPermissionFilter(Permission = "odfcablefaults:export")]
- _需求: 5.1, 6.1, 7.9_
- [ ] 6. 后台 API依赖注入注册
- [x] 6. 后台 API依赖注入注册
- 在 Startup / Program.cs 中注册所有新增 Service 的依赖注入
- IOdfCheckinService → OdfCheckinService
- IOdfCablesService → OdfCablesService
@ -150,36 +150,36 @@
- IOdfUserModulesService → OdfUserModulesService
- _需求: 2.6, 4.1, 5.1, 7.9, 8.1_
- [ ] 7. 检查点 — 后台 API 完成
- [x] 7. 检查点 — 后台 API 完成
- 确认所有 API 编译通过,依赖注入正确,如有问题请告知。
- [ ] 8. 前端 APP基础设施和服务层
- [ ] 8.1 创建 services/permission.js — 权限 API
- [x] 8. 前端 APP基础设施和服务层
- [x] 8.1 创建 services/permission.js — 权限 API
- getUserModules()GET /business/OdfUserModules/list返回模块代码列表
- _需求: 8.2_
- [ ] 8.2 创建 services/checkin.js — 签到 API
- [x] 8.2 创建 services/checkin.js — 签到 API
- submitCheckin(data)POST /business/OdfCheckin/submit
- _需求: 2.6_
- [ ] 8.3 创建 services/trunk.js — 干线版块 API
- [x] 8.3 创建 services/trunk.js — 干线版块 API
- getCableList(deptId)GET /business/OdfCables/list
- getFaultList(cableId, pageNum, pageSize)GET /business/OdfCableFaults/list
- getFaultDetail(id)GET /business/OdfCableFaults/{id}
- addFault(formData)POST /business/OdfCableFaults/addmultipart/form-data
- searchCablesAndFaults(deptId, keyword)GET /business/OdfCables/search
- _需求: 4.1, 5.1, 6.1, 7.9, 4.4_
- [ ] 8.4 扩展 store/index.js — 新增 modules 字段
- [x] 8.4 扩展 store/index.js — 新增 modules 字段
- 添加 modules: [] 字段
- 添加 setModules(modules) 方法
- 在 clearAuth() 中清除 modules
- _需求: 8.2, 8.3_
- [ ] 8.5 创建 utils/navigation.js — 导航工具函数
- [x] 8.5 创建 utils/navigation.js — 导航工具函数
- openNavigation(lat, lng, name):安卓端弹出导航 APP 选择列表H5 端打开地图网页
- _需求: 6.4, 6.5, 10.4_
- [ ]* 8.5.1 编写属性测试 Property 6导航 URL 构建正确性
- **Property 6: 导航 URL 构建正确性**
- 使用 fast-check 生成随机经纬度和地点名,验证 URL 包含正确参数
- **验证: 需求 6.5, 10.4**
- [ ] 8.6 创建 utils/watermark.js — 水印处理工具函数
- [x] 8.6 创建 utils/watermark.js — 水印处理工具函数
- addWatermark(imagePath, text)Canvas 叠加水印文字到照片左下角
- _需求: 7.8_
- [ ]* 8.6.1 编写属性测试 Property 7水印叠加参数正确性
@ -187,12 +187,12 @@
- 使用 fast-check 生成随机时间和人员字符串,验证水印文本包含两者
- **验证: 需求 7.8**
- [ ] 9. 前端 APP登录页改造和功能入口页
- [ ] 9.1 修改 pages/login/index.vue — 登录成功后跳转至 pages/portal/index
- [x] 9. 前端 APP登录页改造和功能入口页
- [x] 9.1 修改 pages/login/index.vue — 登录成功后跳转至 pages/portal/index
- 将 uni.reLaunch 目标从 home 改为 portal
- 登录成功后调用 getUserModules() 获取权限并存入 store.modules
- _需求: 1.1, 8.2_
- [ ] 9.2 新建 pages/portal/index.vue — 功能入口列表页
- [x] 9.2 新建 pages/portal/index.vue — 功能入口列表页
- 自定义导航栏(刷新 + 标题"功能列表" + 设置图标)
- CSS Grid 2 列布局展示功能版块卡片(机房、干线)
- 根据 store.modules 过滤显示的入口卡片
@ -203,17 +203,17 @@
- **Property 1: 权限过滤正确性**
- 使用 fast-check 生成随机权限子集,验证过滤函数只返回有权限的版块
- **验证: 需求 1.1, 1.5, 8.3**
- [ ] 9.3 更新 pages.json — 注册所有新增页面
- [x] 9.3 更新 pages.json — 注册所有新增页面
- 添加 portal、checkin、trunk、cable、fault-list、fault-detail、fault-add、trunk-search、route-plan 页面配置
- 所有页面 navigationStyle 设为 custom
- _需求: 1.1_
- [ ] 10. 前端 APP签到功能
- [ ] 10.1 改造 pages/rack/index.vue — 导航栏右侧添加签到按钮
- [x] 10. 前端 APP签到功能
- [x] 10.1 改造 pages/rack/index.vue — 导航栏右侧添加签到按钮
- 将 nav-icon-placeholder 替换为蓝色【签到】按钮
- 点击签到按钮 → navigateTo checkin 页面,传递 roomId 参数
- _需求: 2.1, 2.2_
- [ ] 10.2 新建 pages/checkin/index.vue — 签到页
- [x] 10.2 新建 pages/checkin/index.vue — 签到页
- 自定义导航栏 + 表单区域(人员输入、时间 picker、工作内容 textarea
- 底部固定提交按钮
- 点击时间字段弹出 picker mode="date" 年月日选择器
@ -225,14 +225,14 @@
- 使用 fast-check 生成随机签到数据,验证请求体包含所有必要字段
- **验证: 需求 2.6**
- [ ] 11. 前端 APP干线版块 — 公司列表和光缆列表
- [ ] 11.1 新建 pages/trunk/index.vue — 干线页(公司列表)
- [x] 11. 前端 APP干线版块 — 公司列表和光缆列表
- [x] 11.1 新建 pages/trunk/index.vue — 干线页(公司列表)
- 自定义导航栏 + section-title "公司列表"
- 复用 getCompanyList() 接口获取公司数据
- 公司卡片列表(白色背景 + 图片占位 + 公司名称居中)
- 点击公司卡片 → navigateTo cable 页面,传递 deptId
- _需求: 3.1, 3.2, 3.3_
- [ ] 11.2 新建 pages/cable/index.vue — 光缆列表页
- [x] 11.2 新建 pages/cable/index.vue — 光缆列表页
- 自定义导航栏 + section-title "光缆列表" + 搜索栏
- 调用 getCableList(deptId) 获取光缆数据
- 光缆卡片列表(同公司卡片样式)
@ -248,8 +248,8 @@
- 使用 fast-check 生成随机列表项数据,验证导航 URL 包含正确参数
- **验证: 需求 3.3, 4.2, 5.2, 9.4, 9.5**
- [ ] 12. 前端 APP干线版块 — 故障管理
- [ ] 12.1 新建 pages/fault-list/index.vue — 故障列表页
- [x] 12. 前端 APP干线版块 — 故障管理
- [x] 12.1 新建 pages/fault-list/index.vue — 故障列表页
- 自定义导航栏 + section-title "故障列表"
- 调用 getFaultList(cableId) 获取故障数据支持分页加载onReachBottom
- 故障卡片列表(显示故障时间、故障原因、表显故障里程、所属光缆)
@ -261,7 +261,7 @@
- **Property 3: 故障记录渲染完整性**
- 使用 fast-check 生成随机故障数据,验证渲染输出包含所有必要字段
- **验证: 需求 5.1, 6.1**
- [ ] 12.2 新建 pages/fault-detail/index.vue — 故障详情页
- [x] 12.2 新建 pages/fault-detail/index.vue — 故障详情页
- 自定义导航栏 + 图片区域(横向滚动,点击预览)+ 信息展示区域
- 调用 getFaultDetail(faultId) 获取完整数据
- 展示所有字段:图片列表、故障时间、人员、故障原因、表显故障里程、所属光缆、地点、备注
@ -269,7 +269,7 @@
- 点击图片 → uni.previewImage 全屏预览
- 点击导航 → 调用 openNavigation(lat, lng, name)
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6_
- [ ] 12.3 新建 pages/fault-add/index.vue — 新增故障页
- [x] 12.3 新建 pages/fault-add/index.vue — 新增故障页
- 自定义导航栏 + 拍照区域(横向滚动,仅相机拍摄)+ 表单区域
- 拍摄第 1 张照片时自动获取时间填充故障时间字段
- 自动填充所属光缆名称(从参数传入,不可编辑)
@ -283,8 +283,8 @@
- 使用 fast-check 生成随机故障表单数据,验证请求体包含所有必要字段
- **验证: 需求 7.9**
- [ ] 13. 前端 APP搜索结果页
- [ ] 13.1 新建 pages/trunk-search/index.vue — 搜索结果页
- [x] 13. 前端 APP搜索结果页
- [x] 13.1 新建 pages/trunk-search/index.vue — 搜索结果页
- 自定义导航栏 + 搜索结果区域
- 页面 onLoad 从参数获取 deptId 和 keyword调用 searchCablesAndFaults 接口
- 按「光缆」和「故障列表」两个分类展示结果
@ -297,8 +297,8 @@
- 使用 fast-check 生成随机搜索结果数据,验证分类展示逻辑正确
- **验证: 需求 9.2, 9.3**
- [ ] 14. 前端 APP路线规划尝试性
- [ ] 14.1 新建 pages/route-plan/index.vue — 路线规划页
- [x] 14. 前端 APP路线规划尝试性
- [x] 14.1 新建 pages/route-plan/index.vue — 路线规划页
- 集成地图 SDK如高德地图 UniApp 插件)
- 输入起终点坐标,调用路线规划 API展示路线总长度
- 输入距离值,计算路线上对应坐标并标注
@ -309,32 +309,32 @@
- 使用 fast-check 生成随机路线点序列和距离值,验证定位计算正确
- **验证: 需求 11.3**
- [ ] 15. 检查点 — 前端 APP 完成
- [x] 15. 检查点 — 前端 APP 完成
- 确认所有新增页面正常渲染页面跳转和参数传递正确API 调用正常,如有问题请告知。
- [ ] 16. 后台管理前端API 层
- [ ] 16.1 创建 api/business/odfcables.js — 光缆管理 API
- [x] 16. 后台管理前端API 层
- [x] 16.1 创建 api/business/odfcables.js — 光缆管理 API
- listOdfCables / addOdfCables / updateOdfCables / getOdfCables / delOdfCables / exportOdfCables
- _需求: 4.1_
- [ ] 16.2 创建 api/business/odfcablefaults.js — 干线故障管理 API
- [x] 16.2 创建 api/business/odfcablefaults.js — 干线故障管理 API
- listOdfCableFaults / getOdfCableFaults / delOdfCableFaults / exportOdfCableFaults
- _需求: 5.1_
- [ ] 16.3 创建 api/business/odfcheckin.js — 签到记录管理 API
- [x] 16.3 创建 api/business/odfcheckin.js — 签到记录管理 API
- listOdfCheckin / exportOdfCheckin
- _需求: 2.6_
- [ ] 16.4 创建 api/business/odfusermodules.js — 用户模块权限管理 API
- [x] 16.4 创建 api/business/odfusermodules.js — 用户模块权限管理 API
- listUsers / getUserModules / saveUserModules
- _需求: 8.1_
- [ ] 17. 后台管理前端:光缆管理页面
- [ ] 17.1 创建 views/business/OdfCables.vue — 光缆管理页面
- [x] 17. 后台管理前端:光缆管理页面
- [x] 17.1 创建 views/business/OdfCables.vue — 光缆管理页面
- 查询表单(光缆名称 el-input + 所属部门 el-tree-select
- 工具栏按钮(新增/编辑/删除/导出v-hasPermi 权限控制)
- el-table 展示光缆列表Id、光缆名称、所属部门、创建时间
- 操作列:详情、编辑、删除
- 分页组件
- _需求: 4.1_
- [ ] 17.2 创建 components/business/OdfCableForm.vue — 光缆表单组件
- [x] 17.2 创建 components/business/OdfCableForm.vue — 光缆表单组件
- 支持 add / edit / view 三种模式
- 表单字段光缆名称必填、所属部门el-tree-select
- v-model:visible 控制弹窗显隐,@success 回调刷新列表
@ -344,8 +344,8 @@
- 使用 fast-check 生成随机光缆名称和部门 ID验证请求体字段完整
- **验证: 后台管理前端 — 光缆管理**
- [ ] 18. 后台管理前端:干线故障管理页面
- [ ] 18.1 创建 views/business/OdfCableFaults.vue — 干线故障管理页面
- [x] 18. 后台管理前端:干线故障管理页面
- [x] 18.1 创建 views/business/OdfCableFaults.vue — 干线故障管理页面
- 查询表单(所属光缆 el-select 远程搜索 + 故障时间范围 el-date-picker + 故障原因 el-input
- 工具栏按钮(删除/导出,无新增/编辑)
- el-table 展示故障列表Id、故障时间、人员、故障原因、表显故障里程、所属光缆、地点、创建时间
@ -358,8 +358,8 @@
- 使用 fast-check 生成随机查询条件组合,验证 API 请求参数完整
- **验证: 后台管理前端 — 光缆管理、故障管理、签到记录管理**
- [ ] 19. 后台管理前端:签到记录管理页面
- [ ] 19.1 创建 views/business/OdfCheckin.vue — 签到记录管理页面
- [x] 19. 后台管理前端:签到记录管理页面
- [x] 19.1 创建 views/business/OdfCheckin.vue — 签到记录管理页面
- 查询表单(机房 el-select 远程搜索 + 人员 el-input + 签到时间范围 el-date-picker
- 工具栏按钮(仅导出,无增删改)
- el-table 展示签到记录Id、机房名称、人员、签到时间、工作内容、提交人、创建时间
@ -367,8 +367,8 @@
- 分页组件
- _需求: 2.6_
- [ ] 20. 后台管理前端:用户模块权限管理页面
- [ ] 20.1 创建 views/business/OdfUserModules.vue — 用户模块权限管理页面
- [x] 20. 后台管理前端:用户模块权限管理页面
- [x] 20.1 创建 views/business/OdfUserModules.vue — 用户模块权限管理页面
- 左右分栏布局(左侧 360px 用户列表 + 右侧模块权限配置)
- 左侧:搜索框 + el-table 用户列表,点击行高亮选中
- 右侧:标题"用户名 - 模块权限配置" + el-checkbox-groupodf/trunk/route+ 保存按钮
@ -386,7 +386,7 @@
- 使用 fast-check 生成随机 userId 和模块子集,验证请求体正确
- **验证: 后台管理前端 — 用户模块权限管理**
- [ ] 21. 后台管理前端:路由配置
- [x] 21. 后台管理前端:路由配置
- ZRAdmin 使用动态路由(从 sys_menu 读取 Component 字段自动生成路由),因此无需手动配置 Vue Router
- 任务 1.6 的菜单 SQL 脚本已配置 Component 指向对应 Vue 文件,执行后即可通过菜单访问
- 仅需确认 4 个 Vue 文件路径与菜单 Component 一致:
@ -396,7 +396,7 @@
- business/OdfUserModules → views/business/OdfUserModules.vue
- _需求: 8.1_
- [ ] 22. 最终检查点 — 全部完成
- [x] 22. 最终检查点 — 全部完成
- 确认所有功能模块正常工作:数据库表、后台 API、前端 APP 页面、后台管理前端页面
- 确认菜单权限 SQL 脚本已执行,后台管理页面可通过菜单访问
- 确认所有测试通过,如有问题请告知。

View File

@ -39,6 +39,42 @@
{
"path": "pages/change-password/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/portal/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/checkin/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/trunk/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/cable/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/fault-list/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/fault-detail/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/fault-add/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/trunk-search/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
},
{
"path": "pages/route-plan/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
}
],
"globalStyle": {

View File

@ -0,0 +1,216 @@
<template>
<view class="cable-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">干线</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 小标题 -->
<text class="section-title">光缆列表</text>
<!-- 搜索栏 -->
<view class="search-bar">
<image class="search-icon" src="/static/images/ic_search.png" mode="aspectFit" />
<input
class="search-input"
v-model="keyword"
placeholder="只支持搜索本公司光缆和故障信息"
placeholder-class="search-placeholder"
confirm-type="search"
@confirm="handleSearch"
/>
</view>
<!-- 光缆列表 -->
<scroll-view class="cable-list" scroll-y>
<view
class="cable-card"
v-for="item in cableList"
:key="item.id"
@click="goFaultList(item)"
>
<view class="cable-image" />
<text class="cable-name">{{ item.cableName }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getCableList } from '@/services/trunk'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const cableList = ref([])
const deptId = ref('')
const keyword = ref('')
async function loadCableList() {
const res = await getCableList(deptId.value)
if (res.code === 200) {
cableList.value = res.data || []
}
}
function goBack() {
uni.navigateBack()
}
function handleSearch() {
const kw = keyword.value.trim()
if (!kw) return
uni.navigateTo({
url: '/pages/trunk-search/index?deptId=' + deptId.value + '&keyword=' + encodeURIComponent(kw)
})
}
function goFaultList(item) {
uni.navigateTo({
url: '/pages/fault-list/index?cableId=' + item.id + '&cableName=' + encodeURIComponent(item.cableName)
})
}
onLoad((options) => {
if (options.deptId) {
deptId.value = options.deptId
}
loadCableList()
})
onPullDownRefresh(() => {
loadCableList().finally(() => {
uni.stopPullDownRefresh()
})
})
</script>
<style scoped>
.cable-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 16rpx 24rpx 8rpx;
display: block;
}
.search-bar {
display: flex;
align-items: center;
margin: 16rpx 24rpx;
padding: 0 24rpx;
height: 72rpx;
background: #fff;
border-radius: 36rpx;
}
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 26rpx;
border: none;
}
.search-placeholder {
color: #999;
font-size: 26rpx;
}
.cable-list {
padding: 0 0 24rpx;
height: calc(100vh - 500rpx);
}
.cable-card {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 24rpx 20rpx;
padding: 24rpx;
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.cable-image {
width: 100%;
height: 160rpx;
background: #F0F0F0;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.cable-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
text-align: center;
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<view class="checkin-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">签到</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 表单区域 -->
<view class="form-area">
<view class="form-group">
<text class="form-label">人员</text>
<input
class="form-input"
v-model="form.personnel"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">时间</text>
<picker mode="date" :value="form.checkinTime" @change="onDateChange">
<view class="form-picker">
<text :class="['picker-text', form.checkinTime ? 'picker-text-active' : '']">
{{ form.checkinTime || '请选择年月日' }}
</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">工作内容</text>
<textarea
class="form-textarea"
v-model="form.workContent"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- 底部固定提交按钮 -->
<view class="bottom-bar">
<view class="submit-btn" @click="handleSubmit">
<text class="submit-btn-text">提交</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { submitCheckin } from '@/services/checkin'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const roomId = ref('')
const submitting = ref(false)
const form = reactive({
personnel: '',
checkinTime: '',
workContent: ''
})
function goBack() {
uni.navigateBack()
}
function onDateChange(e) {
form.checkinTime = e.detail.value
}
async function handleSubmit() {
if (!form.personnel.trim()) {
uni.showToast({ title: '请输入人员', icon: 'none' })
return
}
if (!form.checkinTime) {
uni.showToast({ title: '请选择时间', icon: 'none' })
return
}
if (!form.workContent.trim()) {
uni.showToast({ title: '请输入工作内容', icon: 'none' })
return
}
if (submitting.value) return
submitting.value = true
try {
const res = await submitCheckin({
roomId: roomId.value,
personnel: form.personnel.trim(),
checkinTime: form.checkinTime,
workContent: form.workContent.trim()
})
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
onLoad((options) => {
if (options.roomId) {
roomId.value = options.roomId
}
})
</script>
<style scoped>
.checkin-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.form-area {
padding: 24rpx;
}
.form-group {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
display: block;
}
.form-input {
height: 80rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
}
.form-picker {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
}
.picker-text {
font-size: 28rpx;
color: #999;
}
.picker-text-active {
color: #333;
}
.picker-arrow {
font-size: 24rpx;
color: #999;
}
.form-textarea {
min-height: 200rpx;
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
width: 100%;
box-sizing: border-box;
}
.input-placeholder {
color: #999;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn-text {
color: #fff;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,440 @@
<template>
<view class="fault-add-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">新增故障</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 拍照区域 -->
<view class="photo-area">
<scroll-view class="photo-scroll" scroll-x>
<view class="photo-list">
<view class="photo-add-btn" @click="takePhoto">
<text class="plus-icon">+</text>
<text class="add-text">点击拍摄</text>
</view>
<image
class="photo-thumb"
v-for="(photo, index) in photoList"
:key="index"
:src="photo"
mode="aspectFill"
/>
</view>
</scroll-view>
</view>
<!-- 表单区域 -->
<view class="form-area">
<view class="form-group">
<text class="form-label">故障时间</text>
<view class="form-display">
<text class="display-text">{{ form.faultTime || '拍摄第一张照片后自动填充' }}</text>
</view>
</view>
<view class="form-group">
<text class="form-label">人员</text>
<input
class="form-input"
v-model="form.personnel"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">故障原因</text>
<input
class="form-input"
v-model="form.faultReason"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">表显故障里程</text>
<input
class="form-input"
v-model="form.mileage"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-group">
<text class="form-label">所属光缆</text>
<view class="form-display">
<text class="display-text">{{ form.cableName }}</text>
</view>
</view>
<view class="form-group">
<text class="form-label">地点</text>
<view class="location-btn" @click="getLocation">
<text class="location-btn-text">点击获取当前经纬度</text>
</view>
<text class="location-text">当前经度{{ form.longitude }} 当前纬度{{ form.latitude }}</text>
</view>
<view class="form-group">
<text class="form-label">备注</text>
<textarea
class="form-textarea"
v-model="form.remark"
placeholder="请输入"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- 底部固定提交按钮 -->
<view class="bottom-bar">
<view class="submit-btn" @click="handleSubmit">
<text class="submit-btn-text">提交故障</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { addFault } from '@/services/trunk'
import { addWatermark } from '@/utils/watermark'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const photoList = ref([])
const cableId = ref('')
const submitting = ref(false)
const form = reactive({
faultTime: '',
personnel: '',
faultReason: '',
mileage: '',
cableName: '',
latitude: 0,
longitude: 0,
remark: ''
})
function goBack() {
uni.navigateBack()
}
function takePhoto() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success(res) {
const tempPath = res.tempFilePaths[0]
photoList.value.push(tempPath)
//
if (photoList.value.length === 1) {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
const h = String(now.getHours()).padStart(2, '0')
const min = String(now.getMinutes()).padStart(2, '0')
form.faultTime = `${y}/${m}/${d} ${h}:${min}`
}
}
})
}
function getLocation() {
uni.getLocation({
type: 'gcj02',
success(res) {
form.latitude = res.latitude
form.longitude = res.longitude
uni.showToast({ title: '获取成功', icon: 'success' })
},
fail() {
uni.showToast({ title: '获取位置失败', icon: 'none' })
}
})
}
async function handleSubmit() {
if (photoList.value.length === 0) {
uni.showToast({ title: '请至少拍摄一张照片', icon: 'none' })
return
}
if (submitting.value) return
submitting.value = true
try {
//
const watermarkText = `${form.faultTime} ${form.personnel}`
const watermarkedPhotos = []
for (const photo of photoList.value) {
try {
const result = await addWatermark(photo, watermarkText)
watermarkedPhotos.push(result)
} catch (err) {
// 使
watermarkedPhotos.push(photo)
}
}
//
const files = watermarkedPhotos.map((path, index) => ({
name: 'images',
uri: path
}))
const formData = {
files,
data: {
cableId: cableId.value,
faultTime: form.faultTime,
personnel: form.personnel,
faultReason: form.faultReason,
mileage: form.mileage,
latitude: String(form.latitude),
longitude: String(form.longitude),
remark: form.remark
}
}
const res = await addFault(formData)
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
onLoad((options) => {
if (options.cableId) {
cableId.value = options.cableId
}
if (options.cableName) {
form.cableName = decodeURIComponent(options.cableName)
}
})
</script>
<style scoped>
.fault-add-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 120rpx;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.photo-area {
padding: 24rpx;
}
.photo-scroll {
white-space: nowrap;
}
.photo-list {
display: inline-flex;
align-items: center;
}
.photo-add-btn {
width: 200rpx;
height: 200rpx;
background: #fff;
border: 2rpx dashed #CCCCCC;
border-radius: 12rpx;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.plus-icon {
font-size: 48rpx;
color: #999;
}
.add-text {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.photo-thumb {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.form-area {
padding: 0 24rpx;
}
.form-group {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
display: block;
}
.form-input {
height: 80rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
}
.form-display {
height: 80rpx;
padding: 0 24rpx;
background: #F5F5F5;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
display: flex;
align-items: center;
}
.display-text {
font-size: 28rpx;
color: #333;
}
.form-textarea {
min-height: 200rpx;
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
color: #333;
width: 100%;
box-sizing: border-box;
}
.input-placeholder {
color: #999;
}
.location-btn {
background: #1A73EC;
border-radius: 12rpx;
padding: 16rpx 0;
text-align: center;
width: 100%;
}
.location-btn-text {
color: #fff;
font-size: 28rpx;
}
.location-text {
font-size: 26rpx;
color: #999;
margin-top: 12rpx;
display: block;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn-text {
color: #fff;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<view class="fault-detail-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">故障详情</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 图片区域 -->
<view class="image-area" v-if="imageList.length > 0">
<scroll-view class="image-scroll" scroll-x>
<view class="image-grid">
<image
class="image-item"
v-for="(img, index) in imageList"
:key="index"
:src="img"
mode="aspectFill"
@click="previewImage(index)"
/>
</view>
</scroll-view>
</view>
<!-- 信息展示区域 -->
<view class="info-area">
<view class="info-row">
<text class="info-label">故障时间</text>
<text class="info-value">{{ detail.faultTime }}</text>
</view>
<view class="info-row">
<text class="info-label">人员</text>
<text class="info-value">{{ detail.personnel }}</text>
</view>
<view class="info-row">
<text class="info-label">故障原因</text>
<text class="info-value">{{ detail.faultReason }}</text>
</view>
<view class="info-row">
<text class="info-label">表显故障里程</text>
<text class="info-value">{{ detail.mileage }}</text>
</view>
<view class="info-row">
<text class="info-label">所属光缆</text>
<text class="info-value">{{ detail.cableName }}</text>
</view>
<view class="info-row">
<text class="info-label">地点</text>
<text class="info-value">{{ detail.location }}</text>
</view>
<view class="info-row last-row">
<text class="info-label">备注</text>
<text class="info-value">{{ detail.remark }}</text>
</view>
</view>
</view>
<!-- 底部导航按钮 -->
<view class="bottom-bar" v-if="hasLocation">
<view class="navigate-btn" @click="handleNavigate">
<text class="navigate-btn-text">导航至地点</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getFaultDetail } from '@/services/trunk'
import { openNavigation } from '@/utils/navigation'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const faultId = ref('')
const imageList = ref([])
const detail = reactive({
faultTime: '',
personnel: '',
faultReason: '',
mileage: '',
cableName: '',
location: '',
latitude: 0,
longitude: 0,
remark: ''
})
const hasLocation = computed(() => {
return detail.latitude && detail.longitude &&
Number(detail.latitude) !== 0 && Number(detail.longitude) !== 0
})
async function loadDetail() {
try {
const res = await getFaultDetail(faultId.value)
if (res.code === 200 && res.data) {
const d = res.data
detail.faultTime = d.faultTime || ''
detail.personnel = d.personnel || ''
detail.faultReason = d.faultReason || ''
detail.mileage = d.mileage || ''
detail.cableName = d.cableName || ''
detail.location = d.location || ''
detail.latitude = d.latitude || 0
detail.longitude = d.longitude || 0
detail.remark = d.remark || ''
imageList.value = (d.images || []).map(img => img.url)
}
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
}
function goBack() {
uni.navigateBack()
}
function previewImage(index) {
uni.previewImage({
urls: imageList.value,
current: imageList.value[index]
})
}
function handleNavigate() {
openNavigation(detail.latitude, detail.longitude, detail.location || '故障地点')
}
onLoad((options) => {
if (options.faultId) {
faultId.value = options.faultId
}
loadDetail()
})
</script>
<style scoped>
.fault-detail-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 120rpx;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.image-area {
padding: 24rpx;
}
.image-scroll {
white-space: nowrap;
}
.image-grid {
display: inline-flex;
gap: 16rpx;
}
.image-item {
width: 280rpx;
height: 280rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.info-area {
background-color: #fff;
margin: 0 24rpx;
padding: 24rpx;
border-radius: 12rpx;
}
.info-row {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
}
.info-row.last-row {
margin-bottom: 0;
}
.info-label {
font-size: 26rpx;
color: #999;
flex-shrink: 0;
width: 180rpx;
}
.info-value {
font-size: 26rpx;
color: #333;
flex: 1;
word-break: break-all;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
}
.navigate-btn {
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.navigate-btn-text {
color: #fff;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,245 @@
<template>
<view class="fault-list-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">干线</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 小标题 -->
<text class="section-title">故障列表</text>
<!-- 故障列表 -->
<view class="fault-list">
<view
class="fault-card"
v-for="item in faultList"
:key="item.id"
@click="goFaultDetail(item)"
>
<view class="fault-row">
<text class="fault-label">故障时间</text>
<text class="fault-value">{{ item.faultTime }}</text>
</view>
<view class="fault-row">
<text class="fault-label">故障原因</text>
<text class="fault-value">{{ item.faultReason }}</text>
</view>
<view class="fault-row">
<text class="fault-label">表显故障里程</text>
<text class="fault-value">{{ item.mileage }}</text>
</view>
<view class="fault-row last-row">
<text class="fault-label">所属光缆</text>
<text class="fault-value">{{ item.cableName }}</text>
</view>
</view>
</view>
</view>
<!-- 底部固定按钮 -->
<view class="bottom-bar">
<view class="add-fault-btn" @click="goFaultAdd">
<text class="add-fault-btn-text">新增故障</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getFaultList } from '@/services/trunk'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const faultList = ref([])
const cableId = ref('')
const cableName = ref('')
const pageNum = ref(1)
const pageSize = ref(20)
const totalPage = ref(1)
const loading = ref(false)
async function loadFaultList(isLoadMore = false) {
if (loading.value) return
loading.value = true
try {
const res = await getFaultList(cableId.value, pageNum.value, pageSize.value)
if (res.code === 200) {
const data = res.data || {}
const list = data.result || []
if (isLoadMore) {
faultList.value = [...faultList.value, ...list]
} else {
faultList.value = list
}
totalPage.value = data.totalPage || 1
}
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function goBack() {
uni.navigateBack()
}
function goFaultDetail(item) {
uni.navigateTo({ url: '/pages/fault-detail/index?faultId=' + item.id })
}
function goFaultAdd() {
uni.navigateTo({
url: '/pages/fault-add/index?cableId=' + cableId.value + '&cableName=' + encodeURIComponent(cableName.value)
})
}
onLoad((options) => {
if (options.cableId) {
cableId.value = options.cableId
}
if (options.cableName) {
cableName.value = decodeURIComponent(options.cableName)
}
loadFaultList()
})
onReachBottom(() => {
if (pageNum.value < totalPage.value) {
pageNum.value++
loadFaultList(true)
}
})
</script>
<style scoped>
.fault-list-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
padding-bottom: 120rpx;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 16rpx 24rpx 8rpx;
display: block;
}
.fault-list {
padding: 0 0 24rpx;
}
.fault-card {
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
padding: 24rpx;
margin: 0 24rpx 20rpx;
}
.fault-row {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
}
.fault-row.last-row {
margin-bottom: 0;
}
.fault-label {
font-size: 26rpx;
color: #999;
flex-shrink: 0;
}
.fault-value {
font-size: 26rpx;
color: #333;
flex: 1;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
}
.add-fault-btn {
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.add-fault-btn-text {
color: #fff;
font-size: 32rpx;
}
</style>

View File

@ -31,6 +31,7 @@
import { ref } from 'vue'
import store from '@/store'
import { appLogin, checkPermission } from '@/services/auth'
import { getUserModules } from '@/services/permission'
const username = ref('')
const password = ref('')
@ -42,7 +43,12 @@ async function handleLogin() {
store.setAuth(jwt, userId, userName)
const permRes = await checkPermission()
store.isPermission = permRes.code === 200
uni.reLaunch({ url: '/pages/home/index' })
//
const modulesRes = await getUserModules()
if (modulesRes.code === 200) {
store.setModules(modulesRes.data)
}
uni.reLaunch({ url: '/pages/portal/index' })
} else {
uni.showToast({ title: res.msg, icon: 'none' })
}

View File

@ -0,0 +1,172 @@
<template>
<view class="portal-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_refresh.png"
mode="aspectFit"
@click="handleRefresh"
/>
<text class="nav-title">功能列表</text>
<image
class="nav-icon"
src="/static/images/ic_set.png"
mode="aspectFit"
@click="goSettings"
/>
</view>
</view>
<!-- 功能入口网格 -->
<view class="module-grid" v-if="filteredModules.length > 0">
<view
class="module-card"
v-for="item in filteredModules"
:key="item.code"
@click="handleModuleClick(item)"
>
<image class="module-image" :src="item.icon" mode="aspectFit" />
<text class="module-name">{{ item.name }}</text>
</view>
</view>
<!-- 空权限提示 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无可用功能模块</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import store from '@/store'
import { getUserModules } from '@/services/permission'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
//
const allModules = [
{ code: 'odf', name: '机房', icon: '/static/images/ic_odf.png', url: '/pages/home/index' },
{ code: 'trunk', name: '干线', icon: '/static/images/ic_trunk.png', url: '/pages/trunk/index' }
]
//
const filteredModules = computed(() => {
return allModules.filter(m => store.modules.includes(m.code))
})
function handleModuleClick(item) {
uni.navigateTo({ url: item.url })
}
async function handleRefresh() {
const res = await getUserModules()
if (res.code === 200) {
store.setModules(res.data)
}
}
function goSettings() {
uni.navigateTo({ url: '/pages/change-password/index' })
}
onLoad(() => {
//
handleRefresh()
})
</script>
<style scoped>
.portal-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.module-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
padding: 24rpx;
}
.module-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.module-image {
width: 120rpx;
height: 120rpx;
margin-bottom: 16rpx;
}
.module-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
text-align: center;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@ -13,7 +13,9 @@
@click="goBack"
/>
<text class="nav-title">机房详情</text>
<view class="nav-icon-placeholder" />
<view class="checkin-btn" @click="goCheckin">
<text class="checkin-btn-text">签到</text>
</view>
</view>
</view>
@ -68,6 +70,12 @@ function goBack() {
uni.navigateBack()
}
function goCheckin() {
uni.navigateTo({
url: '/pages/checkin/index?roomId=' + roomIdRef.value
})
}
function goDetail(item) {
uni.navigateTo({
url: '/pages/rack-detail/index?rackId=' + item.id + '&rackName=' + encodeURIComponent(item.rackName) + '&roomName=' + encodeURIComponent(roomName.value)
@ -136,9 +144,15 @@ onReachBottom(() => {
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
.checkin-btn {
background-color: #1A73EC;
border-radius: 8rpx;
padding: 8rpx 24rpx;
}
.checkin-btn-text {
color: #fff;
font-size: 26rpx;
}
.nav-title {

View File

@ -0,0 +1,553 @@
<template>
<view class="route-plan-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">路线规划</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 起终点输入 -->
<view class="form-area">
<view class="form-group">
<text class="form-label">起点经度</text>
<input class="form-input" type="digit" v-model="startLng" placeholder="请输入起点经度" />
</view>
<view class="form-group">
<text class="form-label">起点纬度</text>
<input class="form-input" type="digit" v-model="startLat" placeholder="请输入起点纬度" />
</view>
<view class="form-group">
<text class="form-label">终点经度</text>
<input class="form-input" type="digit" v-model="endLng" placeholder="请输入终点经度" />
</view>
<view class="form-group">
<text class="form-label">终点纬度</text>
<input class="form-input" type="digit" v-model="endLat" placeholder="请输入终点纬度" />
</view>
<view class="plan-btn" @click="planRoute">
<text class="plan-btn-text">规划路线</text>
</view>
</view>
<!-- 路线结果 -->
<view class="result-area" v-if="routeDistance > 0">
<view class="result-card">
<text class="result-label">路线总长度</text>
<text class="result-value">{{ formatDistance(routeDistance) }}</text>
</view>
<!-- 距离定位 -->
<view class="form-group">
<text class="form-label">输入距离</text>
<input class="form-input" type="digit" v-model="targetDistance" placeholder="请输入距离值" />
</view>
<view class="plan-btn" @click="locateByDistance">
<text class="plan-btn-text">定位坐标</text>
</view>
<!-- 定位结果 -->
<view class="result-card" v-if="locatedPoint">
<text class="result-label">定位坐标</text>
<text class="result-value">经度: {{ locatedPoint.lng }}纬度: {{ locatedPoint.lat }}</text>
</view>
</view>
<!-- 地图展示 -->
<view class="map-area" v-if="showMap">
<map
id="routeMap"
class="route-map"
:latitude="mapCenter.lat"
:longitude="mapCenter.lng"
:scale="mapScale"
:markers="markers"
:polyline="polyline"
show-location
/>
</view>
<!-- 导航按钮 -->
<view class="bottom-bar" v-if="locatedPoint">
<view class="navigate-btn" @click="navigateToPoint">
<text class="navigate-btn-text">导航至该位置</text>
</view>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-mask" v-if="loading">
<text class="loading-text">路线规划中...</text>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { openNavigation } from '@/utils/navigation'
// Web Service API Key Key
const AMAP_KEY = 'YOUR_AMAP_WEB_SERVICE_KEY'
const AMAP_DRIVING_URL = 'https://restapi.amap.com/v3/direction/driving'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
//
const startLng = ref('')
const startLat = ref('')
const endLng = ref('')
const endLat = ref('')
// 线
const routeDistance = ref(0) // 线
const routePolyline = ref([]) // 线 [{ lng, lat }, ...]
const targetDistance = ref('')
const locatedPoint = ref(null)
//
const showMap = ref(false)
const loading = ref(false)
const mapCenter = reactive({ lat: 39.9042, lng: 116.4074 })
const mapScale = ref(12)
const markers = ref([])
const polyline = ref([])
function goBack() {
uni.navigateBack()
}
function formatDistance(meters) {
if (meters >= 1000) {
return (meters / 1000).toFixed(2) + ' km'
}
return meters + ' m'
}
/**
* 调用高德地图驾车路线规划 API
*/
async function planRoute() {
if (!startLng.value || !startLat.value || !endLng.value || !endLat.value) {
uni.showToast({ title: '请输入完整的起终点坐标', icon: 'none' })
return
}
loading.value = true
locatedPoint.value = null
try {
const origin = `${startLng.value},${startLat.value}`
const destination = `${endLng.value},${endLat.value}`
const res = await amapRequest(AMAP_DRIVING_URL, {
key: AMAP_KEY,
origin,
destination,
extensions: 'all'
})
if (res.status === '1' && res.route && res.route.paths && res.route.paths.length > 0) {
const path = res.route.paths[0]
routeDistance.value = parseInt(path.distance) || 0
// 线
const points = []
for (const step of path.steps) {
const coords = step.polyline.split(';')
for (const coord of coords) {
const [lng, lat] = coord.split(',')
points.push({ lng: parseFloat(lng), lat: parseFloat(lat) })
}
}
routePolyline.value = points
//
updateMap(points)
} else {
const infocode = res.infocode || ''
const info = res.info || '路线规划失败'
uni.showToast({ title: `路线规划失败: ${info}(${infocode})`, icon: 'none', duration: 3000 })
}
} catch (err) {
uni.showToast({ title: '网络异常或服务不可用,请稍后重试', icon: 'none', duration: 3000 })
} finally {
loading.value = false
}
}
/**
* 更新地图显示
*/
function updateMap(points) {
if (!points || points.length === 0) return
showMap.value = true
// 线
const midIndex = Math.floor(points.length / 2)
mapCenter.lat = points[midIndex].lat
mapCenter.lng = points[midIndex].lng
//
const startPoint = points[0]
const endPoint = points[points.length - 1]
markers.value = [
{
id: 1,
latitude: startPoint.lat,
longitude: startPoint.lng,
title: '起点',
iconPath: '/static/images/ic_back.png',
width: 30,
height: 30,
callout: { content: '起点', display: 'ALWAYS', fontSize: 12, borderRadius: 4, padding: 4 }
},
{
id: 2,
latitude: endPoint.lat,
longitude: endPoint.lng,
title: '终点',
iconPath: '/static/images/ic_back.png',
width: 30,
height: 30,
callout: { content: '终点', display: 'ALWAYS', fontSize: 12, borderRadius: 4, padding: 4 }
}
]
// 线线
polyline.value = [{
points: points.map(p => ({ latitude: p.lat, longitude: p.lng })),
color: '#1A73EC',
width: 6,
arrowLine: true
}]
}
/**
* 根据距离值计算路线上对应坐标
* 沿路线点序列累加距离找到目标距离对应的插值点
*/
function locateByDistance() {
const dist = parseFloat(targetDistance.value)
if (isNaN(dist) || dist < 0) {
uni.showToast({ title: '请输入有效的距离值', icon: 'none' })
return
}
if (dist > routeDistance.value) {
uni.showToast({ title: '距离超出路线总长度', icon: 'none' })
return
}
if (routePolyline.value.length < 2) {
uni.showToast({ title: '路线数据不足', icon: 'none' })
return
}
const point = findPointAtDistance(routePolyline.value, dist)
if (point) {
locatedPoint.value = {
lng: point.lng.toFixed(6),
lat: point.lat.toFixed(6)
}
//
addLocatedMarker(point)
}
}
/**
* 在路线点序列上根据沿线距离找到对应坐标线性插值
*/
export function findPointAtDistance(points, targetDist) {
if (!points || points.length < 2) return null
if (targetDist <= 0) return { lng: points[0].lng, lat: points[0].lat }
let accumulated = 0
for (let i = 0; i < points.length - 1; i++) {
const segDist = haversineDistance(points[i], points[i + 1])
if (accumulated + segDist >= targetDist) {
// 线线
const remaining = targetDist - accumulated
const ratio = remaining / segDist
return {
lng: points[i].lng + (points[i + 1].lng - points[i].lng) * ratio,
lat: points[i].lat + (points[i + 1].lat - points[i].lat) * ratio
}
}
accumulated += segDist
}
//
return { lng: points[points.length - 1].lng, lat: points[points.length - 1].lat }
}
/**
* Haversine 公式计算两点间距离
*/
export function haversineDistance(p1, p2) {
const R = 6371000 //
const toRad = (deg) => deg * Math.PI / 180
const dLat = toRad(p2.lat - p1.lat)
const dLng = toRad(p2.lng - p1.lng)
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(p1.lat)) * Math.cos(toRad(p2.lat)) * Math.sin(dLng / 2) ** 2
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}
/**
* 在地图上添加定位标记
*/
function addLocatedMarker(point) {
//
const existingMarkers = markers.value.filter(m => m.id <= 2)
existingMarkers.push({
id: 3,
latitude: point.lat,
longitude: point.lng,
title: '定位点',
iconPath: '/static/images/ic_back.png',
width: 30,
height: 30,
callout: {
content: `距起点 ${formatDistance(parseFloat(targetDistance.value))}`,
display: 'ALWAYS',
fontSize: 12,
borderRadius: 4,
padding: 4,
bgColor: '#1A73EC',
color: '#fff'
}
})
markers.value = existingMarkers
//
mapCenter.lat = point.lat
mapCenter.lng = point.lng
}
/**
* 导航至定位点
*/
function navigateToPoint() {
if (!locatedPoint.value) return
openNavigation(
parseFloat(locatedPoint.value.lat),
parseFloat(locatedPoint.value.lng),
'路线定位点'
)
}
/**
* 高德地图 API 请求封装
*/
function amapRequest(url, params) {
return new Promise((resolve, reject) => {
const queryStr = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
uni.request({
url: `${url}?${queryStr}`,
method: 'GET',
timeout: 15000,
success(res) {
resolve(res.data)
},
fail(err) {
reject(err)
}
})
})
}
onLoad(() => {
//
})
</script>
<style scoped>
.route-plan-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
padding-bottom: 140rpx;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.form-area {
padding: 24rpx;
}
.form-group {
margin-bottom: 24rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
display: block;
}
.form-input {
height: 80rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
font-size: 28rpx;
}
.plan-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
background: #1A73EC;
border-radius: 12rpx;
margin-top: 16rpx;
margin-bottom: 24rpx;
}
.plan-btn-text {
color: #fff;
font-size: 32rpx;
font-weight: 500;
}
.result-area {
padding: 0 24rpx;
}
.result-card {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.result-label {
font-size: 26rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.result-value {
font-size: 34rpx;
color: #333;
font-weight: 600;
display: block;
}
.map-area {
padding: 0 24rpx;
margin-bottom: 24rpx;
}
.route-map {
width: 100%;
height: 600rpx;
border-radius: 12rpx;
overflow: hidden;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 24rpx;
background: #fff;
box-sizing: border-box;
}
.navigate-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 88rpx;
background: #1A73EC;
border-radius: 20rpx;
}
.navigate-btn-text {
color: #fff;
font-size: 32rpx;
}
.loading-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.loading-text {
background: #fff;
padding: 32rpx 48rpx;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
}
</style>

View File

@ -0,0 +1,259 @@
<template>
<view class="trunk-search-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">搜索结果</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 搜索结果区域 -->
<view class="result-area" v-if="!loading">
<!-- 光缆分类 -->
<view class="section" v-if="cables.length > 0">
<text class="section-title">光缆</text>
<view
class="cable-card"
v-for="item in cables"
:key="item.id"
@click="goCableFaultList(item)"
>
<view class="cable-image" />
<text class="cable-name">{{ item.cableName }}</text>
</view>
</view>
<!-- 故障分类 -->
<view class="section" v-if="faults.length > 0">
<text class="section-title">故障列表</text>
<view
class="fault-card"
v-for="item in faults"
:key="item.id"
@click="goFaultDetail(item)"
>
<view class="fault-row">
<text class="fault-label">故障时间</text>
<text class="fault-value">{{ item.faultTime }}</text>
</view>
<view class="fault-row">
<text class="fault-label">故障原因</text>
<text class="fault-value">{{ item.faultReason }}</text>
</view>
<view class="fault-row">
<text class="fault-label">表显故障里程</text>
<text class="fault-value">{{ item.mileage }}</text>
</view>
<view class="fault-row last-row">
<text class="fault-label">所属光缆</text>
<text class="fault-value">{{ item.cableName }}</text>
</view>
</view>
</view>
<!-- 无结果 -->
<view class="no-result" v-if="cables.length === 0 && faults.length === 0">
<text class="no-result-text">暂无搜索结果</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { searchCablesAndFaults } from '@/services/trunk'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const cables = ref([])
const faults = ref([])
const loading = ref(true)
async function doSearch(deptId, keyword) {
loading.value = true
try {
const res = await searchCablesAndFaults(deptId, keyword)
if (res.code === 200 && res.data) {
cables.value = res.data.cables || []
faults.value = res.data.faults || []
}
} catch (err) {
uni.showToast({ title: '搜索失败', icon: 'none' })
} finally {
loading.value = false
}
}
function goBack() {
uni.navigateBack()
}
function goCableFaultList(item) {
uni.navigateTo({
url: '/pages/fault-list/index?cableId=' + item.id + '&cableName=' + encodeURIComponent(item.cableName)
})
}
function goFaultDetail(item) {
uni.navigateTo({ url: '/pages/fault-detail/index?faultId=' + item.id })
}
onLoad((options) => {
const deptId = options.deptId || ''
const keyword = decodeURIComponent(options.keyword || '')
if (deptId && keyword) {
doSearch(deptId, keyword)
} else {
loading.value = false
}
})
</script>
<style scoped>
.trunk-search-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.result-area {
padding: 16rpx 0;
}
.section {
margin-bottom: 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 16rpx 24rpx 8rpx;
display: block;
}
/* 光缆卡片 — 复用 cable 页样式 */
.cable-card {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 24rpx 20rpx;
padding: 24rpx;
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.cable-image {
width: 100%;
height: 160rpx;
background: #F0F0F0;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.cable-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
text-align: center;
}
/* 故障卡片 — 复用 fault-list 页样式 */
.fault-card {
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
padding: 24rpx;
margin: 0 24rpx 20rpx;
}
.fault-row {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
}
.fault-row.last-row {
margin-bottom: 0;
}
.fault-label {
font-size: 26rpx;
color: #999;
flex-shrink: 0;
}
.fault-value {
font-size: 26rpx;
color: #333;
flex: 1;
}
/* 无结果 */
.no-result {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
}
.no-result-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<view class="trunk-page">
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
<view class="content">
<!-- 顶部导航栏 -->
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar-inner">
<image
class="nav-icon"
src="/static/images/ic_back.png"
mode="aspectFit"
@click="goBack"
/>
<text class="nav-title">干线</text>
<view class="nav-icon-placeholder" />
</view>
</view>
<!-- 小标题 -->
<text class="section-title">公司列表</text>
<!-- 公司列表 -->
<scroll-view class="company-list" scroll-y>
<view
class="company-card"
v-for="item in companyList"
:key="item.deptId"
@click="goCable(item)"
>
<view class="company-image" />
<text class="company-name">{{ item.deptName }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getCompanyList } from '@/services/home'
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
const companyList = ref([])
async function loadCompanyList() {
const res = await getCompanyList()
if (res.code === 200) {
companyList.value = res.data || []
}
}
function goBack() {
uni.navigateBack()
}
function goCable(item) {
uni.navigateTo({ url: '/pages/cable/index?deptId=' + item.deptId })
}
onLoad(() => {
loadCompanyList()
})
onPullDownRefresh(() => {
loadCompanyList().finally(() => {
uni.stopPullDownRefresh()
})
})
</script>
<style scoped>
.trunk-page {
position: relative;
min-height: 100vh;
background-color: #F5F5F5;
}
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
}
.nav-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
}
.nav-icon {
width: 44rpx;
height: 44rpx;
}
.nav-icon-placeholder {
width: 44rpx;
height: 44rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 16rpx 24rpx 8rpx;
display: block;
}
.company-list {
padding: 0 0 24rpx;
height: calc(100vh - 400rpx);
}
.company-card {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 24rpx 20rpx;
padding: 24rpx;
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #E8E8E8;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.company-image {
width: 100%;
height: 160rpx;
background: #F0F0F0;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.company-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
text-align: center;
}
</style>

View File

@ -0,0 +1,3 @@
import { post } from './api'
export const submitCheckin = (data) => post('/business/OdfCheckin/submit', data)

View File

@ -0,0 +1,3 @@
import { get } from './api'
export const getUserModules = () => get('/business/OdfUserModules/list')

View File

@ -0,0 +1,48 @@
import { get } from './api'
import store from '@/store'
const BASE_URL = 'http://49.233.115.141:11082'
export const getCableList = (deptId) =>
get('/business/OdfCables/list', { deptId })
export const getFaultList = (cableId, pageNum, pageSize) =>
get('/business/OdfCableFaults/list', { cableId, pageNum, pageSize })
export const getFaultDetail = (id) =>
get(`/business/OdfCableFaults/${id}`)
/**
* 新增故障multipart/form-data含图片上传
* @param {FormData|object} formData - 包含故障信息和图片的 FormData
* @returns {Promise}
*/
export function addFault(formData) {
return new Promise((resolve, reject) => {
const header = {
'Authorization': `Bearer ${store.token}`,
'Userid': store.userId,
'Username': store.userName
}
uni.uploadFile({
url: BASE_URL + '/business/OdfCableFaults/add',
files: formData.files || [],
formData: formData.data || {},
header,
success(res) {
try {
const result = JSON.parse(res.data)
resolve({ code: result.code, msg: result.msg, data: result.data })
} catch (e) {
reject({ code: -1, msg: '解析响应失败' })
}
},
fail(err) {
reject({ code: -1, msg: err.errMsg || '网络异常' })
}
})
})
}
export const searchCablesAndFaults = (deptId, keyword) =>
get('/business/OdfCables/search', { deptId, keyword })

View File

@ -8,6 +8,9 @@ const store = reactive({
userName: uni.getStorageSync('userName') || '',
isPermission: false,
// 功能版块权限列表
modules: JSON.parse(uni.getStorageSync('modules') || '[]'),
// 字典数据
dictUnitTypes: [], // 设备型号列表
dictBusinessTypes: [], // 业务类型列表
@ -22,15 +25,23 @@ const store = reactive({
uni.setStorageSync('userName', userName)
},
// 设置功能版块权限
setModules(modules) {
this.modules = modules || []
uni.setStorageSync('modules', JSON.stringify(this.modules))
},
// 清除认证信息
clearAuth() {
this.token = ''
this.userId = ''
this.userName = ''
this.isPermission = false
this.modules = []
uni.removeStorageSync('token')
uni.removeStorageSync('userId')
uni.removeStorageSync('userName')
uni.removeStorageSync('modules')
}
})

View File

@ -0,0 +1,54 @@
/**
* 打开导航 APP
* @param {number} lat - 纬度
* @param {number} lng - 经度
* @param {string} name - 地点名称
*/
export function openNavigation(lat, lng, name) {
// #ifdef APP-PLUS
const apps = []
// 检测高德地图
if (plus.runtime.isApplicationExist({ pname: 'com.autonavi.minimap', action: '' })) {
apps.push({
title: '高德地图',
scheme: `androidamap://navi?sourceApplication=odf&lat=${lat}&lon=${lng}&dev=0&style=2`
})
}
// 检测百度地图
if (plus.runtime.isApplicationExist({ pname: 'com.baidu.BaiduMap', action: '' })) {
apps.push({
title: '百度地图',
scheme: `baidumap://map/navi?location=${lat},${lng}&src=odf&coord_type=gcj02`
})
}
// 检测腾讯地图
if (plus.runtime.isApplicationExist({ pname: 'com.tencent.map', action: '' })) {
apps.push({
title: '腾讯地图',
scheme: `qqmap://map/routeplan?type=drive&to=${encodeURIComponent(name)}&tocoord=${lat},${lng}&referer=odf`
})
}
if (apps.length === 0) {
uni.showToast({ title: '未检测到导航应用', icon: 'none' })
return
}
uni.showActionSheet({
itemList: apps.map(a => a.title),
success(res) {
const selected = apps[res.tapIndex]
plus.runtime.openURL(selected.scheme, (err) => {
uni.showToast({ title: '打开导航失败', icon: 'none' })
})
}
})
// #endif
// #ifdef H5
window.open(`https://uri.amap.com/navigation?to=${lng},${lat},${encodeURIComponent(name)}&mode=car&src=odf`)
// #endif
}

View File

@ -0,0 +1,108 @@
/**
* 在照片左下角叠加水印文字
* @param {string} imagePath - 原始图片路径
* @param {string} text - 水印文字 "2025/06/15 12:00 张三"
* @returns {Promise<string>} 带水印的临时文件路径
*/
export function addWatermark(imagePath, text) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imagePath,
success(imgInfo) {
const canvasId = 'watermarkCanvas'
const width = imgInfo.width
const height = imgInfo.height
// 使用 OffscreenCanvasAPP-PLUS 和 H5 均支持)
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('watermark')
bitmap.load(imagePath, () => {
const canvas = new plus.nativeObj.View('watermarkView', {
left: '0px', top: '0px',
width: width + 'px', height: height + 'px'
})
// 绘制原图
canvas.drawBitmap(bitmap, {}, { left: '0px', top: '0px', width: width + 'px', height: height + 'px' })
// 水印参数
const fontSize = Math.max(Math.floor(width * 0.03), 14)
const padding = Math.floor(fontSize * 0.8)
const textX = padding
const textY = height - padding
// 绘制半透明背景
const bgHeight = fontSize + padding * 2
canvas.drawRect(
{ color: 'rgba(0,0,0,0.4)' },
{ left: '0px', top: (height - bgHeight) + 'px', width: width + 'px', height: bgHeight + 'px' }
)
// 绘制水印文字
canvas.drawText(text, {
left: textX + 'px',
top: (height - bgHeight + padding) + 'px',
width: (width - textX * 2) + 'px',
height: fontSize + 'px'
}, {
size: fontSize + 'px',
color: '#ffffff'
})
// 导出
const tempPath = `_doc/watermark_${Date.now()}.jpg`
canvas.toBitmap(tempPath, {}, () => {
bitmap.clear()
resolve(tempPath)
}, (err) => {
bitmap.clear()
reject(err)
})
}, (err) => {
reject(err)
})
// #endif
// #ifndef APP-PLUS
// H5 / 小程序端使用 Canvas 2D
const canvas = uni.createOffscreenCanvas({ type: '2d', width, height })
const ctx = canvas.getContext('2d')
const img = canvas.createImage()
img.onload = () => {
// 绘制原图
ctx.drawImage(img, 0, 0, width, height)
// 水印参数
const fontSize = Math.max(Math.floor(width * 0.03), 14)
const padding = Math.floor(fontSize * 0.8)
// 绘制半透明背景
const bgHeight = fontSize + padding * 2
ctx.fillStyle = 'rgba(0,0,0,0.4)'
ctx.fillRect(0, height - bgHeight, width, bgHeight)
// 绘制水印文字
ctx.fillStyle = '#ffffff'
ctx.font = `${fontSize}px sans-serif`
ctx.textBaseline = 'middle'
ctx.fillText(text, padding, height - bgHeight / 2)
// 导出为临时文件
const tempFilePath = canvas.toDataURL('image/jpeg', 0.9)
resolve(tempFilePath)
}
img.onerror = (err) => {
reject(err || new Error('图片加载失败'))
}
img.src = imagePath
// #endif
},
fail(err) {
reject(err)
}
})
})
}

View File

@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Mvc;
using ZR.Model.Business.Dto;
using ZR.Service.Business.IBusinessService;
//创建时间2025-09-21
namespace ZR.Admin.WebApi.Controllers.Business
{
/// <summary>
/// 干线故障管理
/// </summary>
[Route("business/OdfCableFaults")]
public class OdfCableFaultsController : BaseController
{
/// <summary>
/// 干线故障接口
/// </summary>
private readonly IOdfCableFaultsService _OdfCableFaultsService;
public OdfCableFaultsController(IOdfCableFaultsService OdfCableFaultsService)
{
_OdfCableFaultsService = OdfCableFaultsService;
}
/// <summary>
/// 故障列表分页查询
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
[HttpGet("list")]
[ActionPermissionFilter(Permission = "odfcablefaults:list")]
public IActionResult GetList([FromQuery] OdfCableFaultsQueryDto parm)
{
var response = _OdfCableFaultsService.GetList(parm);
return SUCCESS(response);
}
/// <summary>
/// 故障详情(含图片)
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}")]
[ActionPermissionFilter(Permission = "odfcablefaults:query")]
public IActionResult GetDetail(int id)
{
var response = _OdfCableFaultsService.GetDetail(id);
return SUCCESS(response);
}
/// <summary>
/// 新增故障含图片上传APP端调用
/// </summary>
/// <returns></returns>
[HttpPost("add")]
[ActionPermissionFilter(Permission = "odfcablefaults:list")]
[Log(Title = "干线故障", BusinessType = BusinessType.INSERT)]
public async Task<IActionResult> Add([FromForm] OdfCableFaultAddDto dto)
{
dto.UserId = HttpContext.GetUId();
var response = await _OdfCableFaultsService.AddFault(dto);
return ToResponse(response);
}
/// <summary>
/// 删除故障并级联删除图片
/// </summary>
/// <returns></returns>
[HttpPost("delete/{id}")]
[ActionPermissionFilter(Permission = "odfcablefaults:delete")]
[Log(Title = "干线故障", BusinessType = BusinessType.DELETE)]
public IActionResult Delete(int id)
{
var response = _OdfCableFaultsService.Delete(id);
return ToResponse(response);
}
/// <summary>
/// 导出故障列表
/// </summary>
/// <returns></returns>
[Log(Title = "干线故障", BusinessType = BusinessType.EXPORT, IsSaveResponseData = false)]
[HttpGet("export")]
[ActionPermissionFilter(Permission = "odfcablefaults:export")]
public IActionResult Export([FromQuery] OdfCableFaultsQueryDto parm)
{
var list = _OdfCableFaultsService.ExportList(parm);
if (list == null || list.Result == null || list.Result.Count <= 0)
{
return ToResponse(ResultCode.FAIL, "没有要导出的数据");
}
var result = ExportExcelMini(list.Result, "故障列表", "故障列表");
return ExportExcel(result.Item2, result.Item1);
}
}
}

View File

@ -0,0 +1,123 @@
using Microsoft.AspNetCore.Mvc;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
using ZR.Service.Business.IBusinessService;
//创建时间2025-09-21
namespace ZR.Admin.WebApi.Controllers.Business
{
/// <summary>
/// 光缆管理
/// </summary>
[Route("business/OdfCables")]
public class OdfCablesController : BaseController
{
/// <summary>
/// 光缆管理接口
/// </summary>
private readonly IOdfCablesService _OdfCablesService;
public OdfCablesController(IOdfCablesService OdfCablesService)
{
_OdfCablesService = OdfCablesService;
}
/// <summary>
/// 查询光缆列表
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
[HttpGet("list")]
[ActionPermissionFilter(Permission = "odfcables:list")]
public IActionResult GetList([FromQuery] OdfCablesQueryDto parm)
{
var response = _OdfCablesService.GetList(parm);
return SUCCESS(response);
}
/// <summary>
/// 搜索光缆和故障(限定公司范围)
/// </summary>
/// <returns></returns>
[HttpGet("search")]
[ActionPermissionFilter(Permission = "odfcables:query")]
public IActionResult Search([FromQuery] long deptId, [FromQuery] string keyword)
{
var response = _OdfCablesService.Search(deptId, keyword);
return SUCCESS(response);
}
/// <summary>
/// 新增光缆
/// </summary>
/// <returns></returns>
[HttpPost]
[ActionPermissionFilter(Permission = "odfcables:add")]
[Log(Title = "光缆管理", BusinessType = BusinessType.INSERT)]
public IActionResult Add([FromBody] OdfCables parm)
{
parm.CreatedAt = DateTime.Now;
parm.UpdatedAt = DateTime.Now;
var response = _OdfCablesService.Add(parm);
return SUCCESS(response);
}
/// <summary>
/// 修改光缆
/// </summary>
/// <returns></returns>
[HttpPut]
[ActionPermissionFilter(Permission = "odfcables:edit")]
[Log(Title = "光缆管理", BusinessType = BusinessType.UPDATE)]
public IActionResult Update([FromBody] OdfCables parm)
{
parm.UpdatedAt = DateTime.Now;
var response = _OdfCablesService.Update(parm);
return ToResponse(response);
}
/// <summary>
/// 查询光缆详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}")]
[ActionPermissionFilter(Permission = "odfcables:query")]
public IActionResult GetDetail(int id)
{
var response = _OdfCablesService.GetDetail(id);
return SUCCESS(response);
}
/// <summary>
/// 删除光缆
/// </summary>
/// <returns></returns>
[HttpPost("delete/{id}")]
[ActionPermissionFilter(Permission = "odfcables:delete")]
[Log(Title = "光缆管理", BusinessType = BusinessType.DELETE)]
public IActionResult Delete(int id)
{
var response = _OdfCablesService.Delete(id);
return ToResponse(response);
}
/// <summary>
/// 导出光缆列表
/// </summary>
/// <returns></returns>
[Log(Title = "光缆管理", BusinessType = BusinessType.EXPORT, IsSaveResponseData = false)]
[HttpGet("export")]
[ActionPermissionFilter(Permission = "odfcables:export")]
public IActionResult Export([FromQuery] OdfCablesQueryDto parm)
{
var list = _OdfCablesService.ExportList(parm);
if (list == null || list.Result == null || list.Result.Count <= 0)
{
return ToResponse(ResultCode.FAIL, "没有要导出的数据");
}
var result = ExportExcelMini(list.Result, "光缆列表", "光缆列表");
return ExportExcel(result.Item2, result.Item1);
}
}
}

View File

@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using ZR.Model.Business.Dto;
using ZR.Service.Business.IBusinessService;
//创建时间2025-09-21
namespace ZR.Admin.WebApi.Controllers.Business
{
/// <summary>
/// 签到记录
/// </summary>
[Route("business/OdfCheckin")]
public class OdfCheckinController : BaseController
{
/// <summary>
/// 签到记录接口
/// </summary>
private readonly IOdfCheckinService _OdfCheckinService;
public OdfCheckinController(IOdfCheckinService OdfCheckinService)
{
_OdfCheckinService = OdfCheckinService;
}
/// <summary>
/// 提交签到记录APP端调用
/// </summary>
/// <returns></returns>
[HttpPost("submit")]
[ActionPermissionFilter(Permission = "odfcheckin:list")]
[Log(Title = "签到记录", BusinessType = BusinessType.INSERT)]
public IActionResult Submit([FromBody] OdfCheckinDto dto)
{
dto.UserId = HttpContext.GetUId();
var response = _OdfCheckinService.AddCheckin(dto);
return SUCCESS(response);
}
/// <summary>
/// 分页查询签到记录(管理端调用,联查机房名称和提交人)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
[HttpGet("list")]
[ActionPermissionFilter(Permission = "odfcheckin:list")]
public IActionResult GetList([FromQuery] OdfCheckinQueryDto parm)
{
var response = _OdfCheckinService.GetList(parm);
return SUCCESS(response);
}
/// <summary>
/// 导出签到记录(管理端调用)
/// </summary>
/// <returns></returns>
[Log(Title = "签到记录", BusinessType = BusinessType.EXPORT, IsSaveResponseData = false)]
[HttpGet("export")]
[ActionPermissionFilter(Permission = "odfcheckin:export")]
public IActionResult Export([FromQuery] OdfCheckinQueryDto parm)
{
var list = _OdfCheckinService.ExportList(parm);
if (list == null || list.Result == null || list.Result.Count <= 0)
{
return ToResponse(ResultCode.FAIL, "没有要导出的数据");
}
var result = ExportExcelMini(list.Result, "签到记录", "签到记录");
return ExportExcel(result.Item2, result.Item1);
}
}
}

View File

@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Mvc;
using ZR.Model.Business.Dto;
using ZR.Service.Business.IBusinessService;
//创建时间2025-09-21
namespace ZR.Admin.WebApi.Controllers.Business
{
/// <summary>
/// 用户模块权限
/// </summary>
[Route("business/OdfUserModules")]
public class OdfUserModulesController : BaseController
{
/// <summary>
/// 用户模块权限接口
/// </summary>
private readonly IOdfUserModulesService _OdfUserModulesService;
public OdfUserModulesController(IOdfUserModulesService OdfUserModulesService)
{
_OdfUserModulesService = OdfUserModulesService;
}
/// <summary>
/// 获取当前登录用户的功能版块列表APP端调用登录即可
/// </summary>
/// <returns></returns>
[HttpGet("list")]
public IActionResult GetUserModules([FromQuery] long? userId)
{
// 管理端传 userId 参数时查指定用户APP端不传则查当前登录用户
long uid = userId ?? HttpContext.GetUId();
var modules = _OdfUserModulesService.GetUserModules(uid);
return SUCCESS(modules);
}
/// <summary>
/// 获取用户列表(管理端调用)
/// </summary>
/// <returns></returns>
[HttpGet("users")]
[ActionPermissionFilter(Permission = "odfusermodules:query")]
public IActionResult GetUserList()
{
var list = _OdfUserModulesService.GetUserList();
return SUCCESS(list);
}
/// <summary>
/// 批量保存用户模块权限(管理端调用)
/// </summary>
/// <returns></returns>
[HttpPost("save")]
[ActionPermissionFilter(Permission = "odfusermodules:edit")]
[Log(Title = "用户模块权限", BusinessType = BusinessType.UPDATE)]
public IActionResult SaveUserModules([FromBody] OdfUserModulesSaveDto dto)
{
var response = _OdfUserModulesService.SaveUserModules(dto.UserId, dto.Modules);
return ToResponse(response);
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Http;
namespace ZR.Model.Business.Dto
{
/// <summary>
/// 干线故障查询对象
/// </summary>
public class OdfCableFaultsQueryDto : PagerInfo
{
public int? CableId { get; set; }
/// <summary>
/// 故障时间范围 - 开始
/// </summary>
public DateTime? BeginFaultTime { get; set; }
/// <summary>
/// 故障时间范围 - 结束
/// </summary>
public DateTime? EndFaultTime { get; set; }
public string FaultReason { get; set; }
}
/// <summary>
/// 新增故障输入对象(含图片上传)
/// </summary>
public class OdfCableFaultAddDto
{
public int CableId { get; set; }
public string FaultTime { get; set; }
public string Personnel { get; set; }
public string FaultReason { get; set; }
public string Mileage { get; set; }
public string Location { get; set; }
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
public string Remark { get; set; }
public long? UserId { get; set; }
public IFormFile[] Images { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace ZR.Model.Business.Dto
{
/// <summary>
/// 光缆列表查询对象
/// </summary>
public class OdfCablesQueryDto : PagerInfo
{
public long? DeptId { get; set; }
public string CableName { get; set; }
}
}

View File

@ -0,0 +1,78 @@
using MiniExcelLibs.Attributes;
namespace ZR.Model.Business.Dto
{
/// <summary>
/// 签到记录输入对象
/// </summary>
public class OdfCheckinDto
{
public int RoomId { get; set; }
public string Personnel { get; set; }
public string CheckinTime { get; set; }
public string WorkContent { get; set; }
public long? UserId { get; set; }
}
/// <summary>
/// 签到记录列表/导出对象
/// </summary>
public class OdfCheckinListDto
{
[ExcelColumn(Name = "Id")]
public int Id { get; set; }
[ExcelColumn(Name = "机房ID")]
public int RoomId { get; set; }
/// <summary>
/// 机房名称(联查 odf_rooms
/// </summary>
[ExcelColumn(Name = "机房名称")]
public string RoomName { get; set; }
[ExcelColumn(Name = "人员")]
public string Personnel { get; set; }
[ExcelColumn(Name = "签到时间", Format = "yyyy-MM-dd HH:mm:ss", Width = 20)]
public DateTime CheckinTime { get; set; }
[ExcelColumn(Name = "工作内容")]
public string WorkContent { get; set; }
public long? UserId { get; set; }
/// <summary>
/// 提交人用户名(联查 sys_user.NickName
/// </summary>
[ExcelColumn(Name = "提交人")]
public string UserName { get; set; }
[ExcelColumn(Name = "创建时间", Format = "yyyy-MM-dd HH:mm:ss", Width = 20)]
public DateTime? CreatedAt { get; set; }
}
/// <summary>
/// 签到记录查询对象
/// </summary>
public class OdfCheckinQueryDto : PagerInfo
{
public int? RoomId { get; set; }
public string Personnel { get; set; }
/// <summary>
/// 签到时间范围 - 开始
/// </summary>
public DateTime? BeginCheckinTime { get; set; }
/// <summary>
/// 签到时间范围 - 结束
/// </summary>
public DateTime? EndCheckinTime { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace ZR.Model.Business.Dto
{
/// <summary>
/// 用户模块权限保存对象
/// </summary>
public class OdfUserModulesSaveDto
{
public long UserId { get; set; }
public List<string> Modules { get; set; }
}
}

View File

@ -0,0 +1,30 @@
namespace ZR.Model.Business
{
/// <summary>
/// 故障图片
/// </summary>
[SugarTable("odf_cable_fault_images")]
public class OdfCableFaultImages
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 关联故障ID
/// </summary>
public int FaultId { get; set; }
/// <summary>
/// 图片访问URL
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreatedAt { get; set; }
}
}

View File

@ -0,0 +1,75 @@
namespace ZR.Model.Business
{
/// <summary>
/// 干线故障
/// </summary>
[SugarTable("odf_cable_faults")]
public class OdfCableFaults
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 关联光缆ID
/// </summary>
public int CableId { get; set; }
/// <summary>
/// 故障时间
/// </summary>
public DateTime FaultTime { get; set; }
/// <summary>
/// 人员
/// </summary>
public string? Personnel { get; set; }
/// <summary>
/// 故障原因
/// </summary>
public string? FaultReason { get; set; }
/// <summary>
/// 表显故障里程
/// </summary>
public string? Mileage { get; set; }
/// <summary>
/// 地点描述
/// </summary>
public string? Location { get; set; }
/// <summary>
/// 纬度
/// </summary>
public decimal Latitude { get; set; }
/// <summary>
/// 经度
/// </summary>
public decimal Longitude { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 提交人用户ID
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
}

View File

@ -0,0 +1,40 @@
namespace ZR.Model.Business
{
/// <summary>
/// 光缆
/// </summary>
[SugarTable("odf_cables")]
public class OdfCables
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 光缆名称
/// </summary>
public string CableName { get; set; }
/// <summary>
/// 所属公司/部门ID
/// </summary>
public long DeptId { get; set; }
/// <summary>
/// 部门名称(冗余)
/// </summary>
public string DeptName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
}

View File

@ -0,0 +1,46 @@
namespace ZR.Model.Business
{
/// <summary>
/// 签到记录
/// </summary>
[SugarTable("odf_checkin")]
public class OdfCheckin
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 关联机房ID
/// </summary>
public int RoomId { get; set; }
/// <summary>
/// 签到人员
/// </summary>
public string Personnel { get; set; }
/// <summary>
/// 签到时间
/// </summary>
public DateTime CheckinTime { get; set; }
/// <summary>
/// 工作内容
/// </summary>
public string WorkContent { get; set; }
/// <summary>
/// 提交人用户ID
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 记录创建时间
/// </summary>
public DateTime? CreatedAt { get; set; }
}
}

View File

@ -0,0 +1,30 @@
namespace ZR.Model.Business
{
/// <summary>
/// 用户功能版块权限
/// </summary>
[SugarTable("odf_user_modules")]
public class OdfUserModules
{
/// <summary>
/// Id
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 模块标识
/// </summary>
public string ModuleCode { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreatedAt { get; set; }
}
}

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
<PackageReference Include="MiniExcel" Version="1.41.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SqlSugarCoreNoDrive" Version="5.1.4.193" />

View File

@ -0,0 +1,32 @@
using ZR.Model.Business;
namespace ZR.Service.Business.IBusinessService
{
/// <summary>
/// 故障图片service接口
/// </summary>
public interface IOdfCableFaultImagesService : IBaseService<OdfCableFaultImages>
{
/// <summary>
/// 按故障 ID 查询图片列表
/// </summary>
/// <param name="faultId"></param>
/// <returns></returns>
List<OdfCableFaultImages> GetByFaultId(int faultId);
/// <summary>
/// 批量插入图片记录
/// </summary>
/// <param name="faultId"></param>
/// <param name="imageUrls"></param>
/// <returns></returns>
int BatchInsert(int faultId, List<string> imageUrls);
/// <summary>
/// 按故障 ID 删除所有图片记录
/// </summary>
/// <param name="faultId"></param>
/// <returns></returns>
int DeleteByFaultId(int faultId);
}
}

View File

@ -0,0 +1,37 @@
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
namespace ZR.Service.Business.IBusinessService
{
/// <summary>
/// 干线故障service接口
/// </summary>
public interface IOdfCableFaultsService : IBaseService<OdfCableFaults>
{
/// <summary>
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序
/// </summary>
PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm);
/// <summary>
/// 查询故障详情,联查光缆名称和图片列表
/// </summary>
object GetDetail(int id);
/// <summary>
/// 新增故障(含图片上传)
/// </summary>
Task<int> AddFault(OdfCableFaultAddDto dto);
/// <summary>
/// 删除故障记录并级联删除关联图片
/// </summary>
int Delete(int id);
/// <summary>
/// 导出故障列表
/// </summary>
PagedInfo<dynamic> ExportList(OdfCableFaultsQueryDto parm);
}
}

View File

@ -0,0 +1,62 @@
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
namespace ZR.Service.Business.IBusinessService
{
/// <summary>
/// 光缆管理service接口
/// </summary>
public interface IOdfCablesService : IBaseService<OdfCables>
{
/// <summary>
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm);
/// <summary>
/// 在指定公司范围内搜索光缆和故障
/// </summary>
/// <param name="deptId"></param>
/// <param name="keyword"></param>
/// <returns></returns>
object Search(long deptId, string keyword);
/// <summary>
/// 新增光缆
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
OdfCables Add(OdfCables model);
/// <summary>
/// 修改光缆
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
int Update(OdfCables model);
/// <summary>
/// 删除光缆
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
int Delete(int id);
/// <summary>
/// 获取光缆详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
OdfCables GetDetail(int id);
/// <summary>
/// 导出光缆列表
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm);
}
}

View File

@ -0,0 +1,33 @@
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
namespace ZR.Service.Business.IBusinessService
{
/// <summary>
/// 签到记录service接口
/// </summary>
public interface IOdfCheckinService : IBaseService<OdfCheckin>
{
/// <summary>
/// 新增签到记录
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
OdfCheckin AddCheckin(OdfCheckinDto dto);
/// <summary>
/// 分页查询签到记录(联查机房名称和提交人)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
PagedInfo<OdfCheckinListDto> GetList(OdfCheckinQueryDto parm);
/// <summary>
/// 导出签到记录
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
PagedInfo<OdfCheckinListDto> ExportList(OdfCheckinQueryDto parm);
}
}

View File

@ -0,0 +1,31 @@
using ZR.Model.Business;
namespace ZR.Service.Business.IBusinessService
{
/// <summary>
/// 用户模块权限service接口
/// </summary>
public interface IOdfUserModulesService : IBaseService<OdfUserModules>
{
/// <summary>
/// 获取用户的功能版块列表
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
List<string> GetUserModules(long userId);
/// <summary>
/// 获取用户列表userId, userName
/// </summary>
/// <returns></returns>
List<dynamic> GetUserList();
/// <summary>
/// 保存用户模块权限(先删后插,事务保证原子性)
/// </summary>
/// <param name="userId"></param>
/// <param name="modules"></param>
/// <returns></returns>
int SaveUserModules(long userId, List<string> modules);
}
}

View File

@ -0,0 +1,62 @@
using Infrastructure.Attribute;
using ZR.Model.Business;
using ZR.Repository;
using ZR.Service.Business.IBusinessService;
namespace ZR.Service.Business
{
/// <summary>
/// 故障图片Service业务层处理
/// </summary>
[AppService(ServiceType = typeof(IOdfCableFaultImagesService), ServiceLifetime = LifeTime.Transient)]
public class OdfCableFaultImagesService : BaseService<OdfCableFaultImages>, IOdfCableFaultImagesService
{
/// <summary>
/// 按故障 ID 查询图片列表
/// </summary>
/// <param name="faultId"></param>
/// <returns></returns>
public List<OdfCableFaultImages> GetByFaultId(int faultId)
{
return Queryable()
.Where(x => x.FaultId == faultId)
.OrderBy(x => x.Id)
.ToList();
}
/// <summary>
/// 批量插入图片记录
/// </summary>
/// <param name="faultId"></param>
/// <param name="imageUrls"></param>
/// <returns></returns>
public int BatchInsert(int faultId, List<string> imageUrls)
{
if (imageUrls == null || imageUrls.Count == 0)
{
return 0;
}
var list = imageUrls.Select(url => new OdfCableFaultImages
{
FaultId = faultId,
ImageUrl = url,
CreatedAt = DateTime.Now
}).ToList();
return Insert(list);
}
/// <summary>
/// 按故障 ID 删除所有图片记录
/// </summary>
/// <param name="faultId"></param>
/// <returns></returns>
public int DeleteByFaultId(int faultId)
{
return Deleteable()
.Where(x => x.FaultId == faultId)
.ExecuteCommand();
}
}
}

View File

@ -0,0 +1,223 @@
using Infrastructure;
using Infrastructure.Attribute;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
using ZR.Model.Dto;
using ZR.Repository;
using ZR.Service.Business.IBusinessService;
namespace ZR.Service.Business
{
/// <summary>
/// 干线故障Service业务层处理
/// </summary>
[AppService(ServiceType = typeof(IOdfCableFaultsService), ServiceLifetime = LifeTime.Transient)]
public class OdfCableFaultsService : BaseService<OdfCableFaults>, IOdfCableFaultsService
{
/// <summary>
/// 按 CableId 分页查询故障列表,联查光缆名称,按 FaultTime DESC 排序
/// </summary>
public PagedInfo<dynamic> GetList(OdfCableFaultsQueryDto parm)
{
var predicate = QueryExp(parm);
var response = Queryable()
.Where(predicate.ToExpression())
.LeftJoin<OdfCables>((f, c) => f.CableId == c.Id)
.OrderByDescending((f, c) => f.FaultTime)
.Select((f, c) => new
{
f.Id,
f.CableId,
f.FaultTime,
f.Personnel,
f.FaultReason,
f.Mileage,
f.Location,
f.Latitude,
f.Longitude,
f.Remark,
f.CreatedAt,
CableName = c.CableName
})
.ToPage<dynamic>(parm);
return response;
}
/// <summary>
/// 查询故障详情,联查光缆名称和图片列表
/// </summary>
public object GetDetail(int id)
{
var fault = Queryable()
.LeftJoin<OdfCables>((f, c) => f.CableId == c.Id)
.Where((f, c) => f.Id == id)
.Select((f, c) => new
{
f.Id,
f.CableId,
f.FaultTime,
f.Personnel,
f.FaultReason,
f.Mileage,
f.Location,
f.Latitude,
f.Longitude,
f.Remark,
f.UserId,
f.CreatedAt,
CableName = c.CableName
})
.First();
if (fault == null)
{
throw new CustomException("故障记录不存在");
}
// 查询关联图片列表
var images = Context.Queryable<OdfCableFaultImages>()
.Where(img => img.FaultId == id)
.OrderBy(img => img.Id)
.Select(img => new { img.Id, img.ImageUrl })
.ToList();
return new
{
fault.Id,
fault.CableId,
fault.FaultTime,
fault.Personnel,
fault.FaultReason,
fault.Mileage,
fault.Location,
fault.Latitude,
fault.Longitude,
fault.Remark,
fault.CableName,
fault.CreatedAt,
Images = images
};
}
/// <summary>
/// 新增故障(含图片上传)
/// </summary>
public async Task<int> AddFault(OdfCableFaultAddDto dto)
{
// 校验 CableId 存在
var cable = Context.Queryable<OdfCables>()
.Where(c => c.Id == dto.CableId)
.First();
if (cable == null)
{
throw new CustomException("光缆不存在");
}
// 校验至少 1 张图片
if (dto.Images == null || dto.Images.Length == 0)
{
throw new CustomException("请至少上传一张图片");
}
// 插入故障记录
var model = new OdfCableFaults
{
CableId = dto.CableId,
FaultTime = DateTime.Parse(dto.FaultTime),
Personnel = dto.Personnel,
FaultReason = dto.FaultReason,
Mileage = dto.Mileage,
Location = dto.Location,
Latitude = dto.Latitude,
Longitude = dto.Longitude,
Remark = dto.Remark,
UserId = dto.UserId,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
var faultEntity = Insertable(model).ExecuteReturnEntity();
int faultId = faultEntity.Id;
// 保存图片文件并插入图片记录
IWebHostEnvironment webHostEnvironment = App.WebHostEnvironment;
string webRootPath = webHostEnvironment.WebRootPath;
string uploadDir = Path.Combine("uploads", "fault");
string fullDir = Path.Combine(webRootPath, uploadDir);
if (!Directory.Exists(fullDir))
{
Directory.CreateDirectory(fullDir);
}
foreach (var image in dto.Images)
{
string fileExt = Path.GetExtension(image.FileName);
string fileName = $"{DateTime.Now:yyyyMMdd}_{Guid.NewGuid():N}{fileExt}";
string filePath = Path.Combine(fullDir, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await image.CopyToAsync(stream);
}
string imageUrl = $"/{uploadDir}/{fileName}".Replace("\\", "/");
var imageRecord = new OdfCableFaultImages
{
FaultId = faultId,
ImageUrl = imageUrl,
CreatedAt = DateTime.Now
};
Context.Insertable(imageRecord).ExecuteCommand();
}
return faultId;
}
/// <summary>
/// 删除故障记录并级联删除关联图片
/// </summary>
public int Delete(int id)
{
// 先删除关联图片记录
Context.Deleteable<OdfCableFaultImages>()
.Where(img => img.FaultId == id)
.ExecuteCommand();
// 再删除故障记录
return base.Delete(id);
}
/// <summary>
/// 导出故障列表
/// </summary>
public PagedInfo<dynamic> ExportList(OdfCableFaultsQueryDto parm)
{
parm.PageNum = 1;
parm.PageSize = 100000;
return GetList(parm);
}
/// <summary>
/// 查询表达式
/// </summary>
private static Expressionable<OdfCableFaults> QueryExp(OdfCableFaultsQueryDto parm)
{
var predicate = Expressionable.Create<OdfCableFaults>();
predicate = predicate.AndIF(parm.CableId != null, it => it.CableId == parm.CableId);
predicate = predicate.AndIF(parm.BeginFaultTime != null, it => it.FaultTime >= parm.BeginFaultTime);
predicate = predicate.AndIF(parm.EndFaultTime != null, it => it.FaultTime <= parm.EndFaultTime);
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.FaultReason), it => it.FaultReason.Contains(parm.FaultReason));
return predicate;
}
}
}

View File

@ -0,0 +1,128 @@
using Infrastructure.Attribute;
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
using ZR.Repository;
using ZR.Service.Business.IBusinessService;
namespace ZR.Service.Business
{
/// <summary>
/// 光缆管理Service业务层处理
/// </summary>
[AppService(ServiceType = typeof(IOdfCablesService), ServiceLifetime = LifeTime.Transient)]
public class OdfCablesService : BaseService<OdfCables>, IOdfCablesService
{
/// <summary>
/// 按 DeptId 过滤光缆列表(支持分页和 CableName 模糊查询)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public PagedInfo<OdfCables> GetList(OdfCablesQueryDto parm)
{
var predicate = Expressionable.Create<OdfCables>();
predicate = predicate.AndIF(parm.DeptId != null, it => it.DeptId == parm.DeptId);
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.CableName), it => it.CableName.Contains(parm.CableName));
var response = Queryable()
.Where(predicate.ToExpression())
.OrderByDescending(it => it.CreatedAt)
.ToPage(parm);
return response;
}
/// <summary>
/// 在指定公司范围内搜索光缆和故障
/// </summary>
/// <param name="deptId"></param>
/// <param name="keyword"></param>
/// <returns></returns>
public object Search(long deptId, string keyword)
{
// 搜索光缆:按 DeptId 过滤CableName LIKE keyword
var cables = Queryable()
.Where(c => c.DeptId == deptId)
.WhereIF(!string.IsNullOrEmpty(keyword), c => c.CableName.Contains(keyword))
.Select(c => new { c.Id, c.CableName })
.ToList();
// 搜索故障:联查 odf_cables 按 DeptId 过滤,对 FaultReason/Mileage/Location LIKE keyword
var faults = Context.Queryable<OdfCableFaults>()
.LeftJoin<OdfCables>((f, c) => f.CableId == c.Id)
.Where((f, c) => c.DeptId == deptId)
.WhereIF(!string.IsNullOrEmpty(keyword), (f, c) =>
f.FaultReason.Contains(keyword) ||
f.Mileage.Contains(keyword) ||
f.Location.Contains(keyword))
.OrderByDescending((f, c) => f.FaultTime)
.Select((f, c) => new
{
f.Id,
f.FaultTime,
f.FaultReason,
f.Mileage,
CableName = c.CableName
})
.ToList();
return new { cables, faults };
}
/// <summary>
/// 新增光缆
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public OdfCables Add(OdfCables model)
{
model.CreatedAt = DateTime.Now;
model.UpdatedAt = DateTime.Now;
return Insertable(model).ExecuteReturnEntity();
}
/// <summary>
/// 修改光缆
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public int Update(OdfCables model)
{
model.UpdatedAt = DateTime.Now;
return base.Update(model, true);
}
/// <summary>
/// 删除光缆
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public int Delete(int id)
{
return base.Delete(id);
}
/// <summary>
/// 获取光缆详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public OdfCables GetDetail(int id)
{
return GetFirst(x => x.Id == id);
}
/// <summary>
/// 导出光缆列表
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public PagedInfo<OdfCables> ExportList(OdfCablesQueryDto parm)
{
parm.PageNum = 1;
parm.PageSize = 100000;
return GetList(parm);
}
}
}

View File

@ -0,0 +1,113 @@
using Infrastructure.Attribute;
using ZR.Model;
using ZR.Model.Business;
using ZR.Model.Business.Dto;
using ZR.Model.System;
using ZR.Repository;
using ZR.Service.Business.IBusinessService;
namespace ZR.Service.Business
{
/// <summary>
/// 签到记录Service业务层处理
/// </summary>
[AppService(ServiceType = typeof(IOdfCheckinService), ServiceLifetime = LifeTime.Transient)]
public class OdfCheckinService : BaseService<OdfCheckin>, IOdfCheckinService
{
/// <summary>
/// 新增签到记录
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public OdfCheckin AddCheckin(OdfCheckinDto dto)
{
// 校验 RoomId 存在
var room = Context.Queryable<OdfRooms>()
.Where(r => r.Id == dto.RoomId)
.First();
if (room == null)
{
throw new CustomException("机房不存在");
}
var model = new OdfCheckin
{
RoomId = dto.RoomId,
Personnel = dto.Personnel,
CheckinTime = DateTime.Parse(dto.CheckinTime),
WorkContent = dto.WorkContent,
UserId = dto.UserId,
CreatedAt = DateTime.Now
};
return Insertable(model).ExecuteReturnEntity();
}
/// <summary>
/// 分页查询签到记录(联查机房名称和提交人用户名)
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public PagedInfo<OdfCheckinListDto> GetList(OdfCheckinQueryDto parm)
{
var predicate = QueryExp(parm);
var response = Queryable()
.Where(predicate.ToExpression())
.LeftJoin<OdfRooms>((it, r) => it.RoomId == r.Id)
.LeftJoin<SysUser>((it, r, u) => it.UserId == u.UserId)
.OrderByDescending((it, r, u) => it.CheckinTime)
.Select((it, r, u) => new OdfCheckinListDto()
{
RoomName = r.RoomName,
UserName = u.NickName
}, true)
.ToPage(parm);
return response;
}
/// <summary>
/// 导出签到记录
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
public PagedInfo<OdfCheckinListDto> ExportList(OdfCheckinQueryDto parm)
{
parm.PageNum = 1;
parm.PageSize = 100000;
var predicate = QueryExp(parm);
var response = Queryable()
.Where(predicate.ToExpression())
.LeftJoin<OdfRooms>((it, r) => it.RoomId == r.Id)
.LeftJoin<SysUser>((it, r, u) => it.UserId == u.UserId)
.OrderByDescending((it, r, u) => it.CheckinTime)
.Select((it, r, u) => new OdfCheckinListDto()
{
RoomName = r.RoomName,
UserName = u.NickName
}, true)
.ToPage(parm);
return response;
}
/// <summary>
/// 查询表达式
/// </summary>
/// <param name="parm"></param>
/// <returns></returns>
private static Expressionable<OdfCheckin> QueryExp(OdfCheckinQueryDto parm)
{
var predicate = Expressionable.Create<OdfCheckin>();
predicate = predicate.AndIF(parm.RoomId != null, it => it.RoomId == parm.RoomId);
predicate = predicate.AndIF(!string.IsNullOrEmpty(parm.Personnel), it => it.Personnel.Contains(parm.Personnel));
predicate = predicate.AndIF(parm.BeginCheckinTime != null, it => it.CheckinTime >= parm.BeginCheckinTime);
predicate = predicate.AndIF(parm.EndCheckinTime != null, it => it.CheckinTime <= parm.EndCheckinTime);
return predicate;
}
}
}

View File

@ -0,0 +1,74 @@
using Infrastructure.Attribute;
using ZR.Model.Business;
using ZR.Model.System;
using ZR.Repository;
using ZR.Service.Business.IBusinessService;
namespace ZR.Service.Business
{
/// <summary>
/// 用户模块权限Service业务层处理
/// </summary>
[AppService(ServiceType = typeof(IOdfUserModulesService), ServiceLifetime = LifeTime.Transient)]
public class OdfUserModulesService : BaseService<OdfUserModules>, IOdfUserModulesService
{
/// <summary>
/// 获取用户的功能版块列表
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public List<string> GetUserModules(long userId)
{
return Queryable()
.Where(x => x.UserId == userId)
.Select(x => x.ModuleCode)
.ToList();
}
/// <summary>
/// 获取用户列表userId, userName
/// </summary>
/// <returns></returns>
public List<dynamic> GetUserList()
{
return Context.Queryable<SysUser>()
.Where(u => u.DelFlag == 0 && u.Status == 0)
.Select(u => new { u.UserId, u.NickName })
.ToList()
.Select(u => (dynamic)new { u.UserId, userName = u.NickName })
.ToList();
}
/// <summary>
/// 保存用户模块权限(先删后插,事务保证原子性)
/// </summary>
/// <param name="userId"></param>
/// <param name="modules"></param>
/// <returns></returns>
public int SaveUserModules(long userId, List<string> modules)
{
var result = UseTran(() =>
{
// 先删除该用户的所有模块权限
Deleteable()
.Where(x => x.UserId == userId)
.ExecuteCommand();
// 再插入新的模块权限
if (modules != null && modules.Count > 0)
{
var list = modules.Select(m => new OdfUserModules
{
UserId = userId,
ModuleCode = m,
CreatedAt = DateTime.Now
}).ToList();
Insert(list);
}
});
return result.IsSuccess ? 1 : 0;
}
}
}

View File

@ -0,0 +1,41 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
/**
* 干线故障列表分页查询
* @param {查询条件} data
*/
export function listOdfCableFaults(query) {
return request({
url: 'business/OdfCableFaults/list',
method: 'get',
params: query,
})
}
/**
* 获取干线故障详情
* @param {Id}
*/
export function getOdfCableFaults(id) {
return request({
url: 'business/OdfCableFaults/' + id,
method: 'get'
})
}
/**
* 删除干线故障
* @param {主键} pid
*/
export function delOdfCableFaults(pid) {
return request({
url: 'business/OdfCableFaults/delete/' + pid,
method: 'POST'
})
}
// 导出干线故障列表
export async function exportOdfCableFaults(query) {
await downFile('business/OdfCableFaults/export', { ...query })
}

View File

@ -0,0 +1,65 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
/**
* 光缆列表分页查询
* @param {查询条件} data
*/
export function listOdfCables(query) {
return request({
url: 'business/OdfCables/list',
method: 'get',
params: query,
})
}
/**
* 新增光缆
* @param data
*/
export function addOdfCables(data) {
return request({
url: 'business/OdfCables',
method: 'post',
data: data,
})
}
/**
* 修改光缆
* @param data
*/
export function updateOdfCables(data) {
return request({
url: 'business/OdfCables',
method: 'PUT',
data: data,
})
}
/**
* 获取光缆详情
* @param {Id}
*/
export function getOdfCables(id) {
return request({
url: 'business/OdfCables/' + id,
method: 'get'
})
}
/**
* 删除光缆
* @param {主键} pid
*/
export function delOdfCables(pid) {
return request({
url: 'business/OdfCables/delete/' + pid,
method: 'POST'
})
}
// 导出光缆
export async function exportOdfCables(query) {
await downFile('business/OdfCables/export', { ...query })
}

View File

@ -0,0 +1,19 @@
import request from '@/utils/request'
import { downFile } from '@/utils/request'
/**
* 签到记录列表分页查询
* @param {查询条件} data
*/
export function listOdfCheckin(query) {
return request({
url: 'business/OdfCheckin/list',
method: 'get',
params: query,
})
}
// 导出签到记录
export async function exportOdfCheckin(query) {
await downFile('business/OdfCheckin/export', { ...query })
}

View File

@ -0,0 +1,37 @@
import request from '@/utils/request'
/**
* 获取用户列表
* @param {查询条件} query
*/
export function listUsers(query) {
return request({
url: 'business/OdfUserModules/users',
method: 'get',
params: query,
})
}
/**
* 获取指定用户的模块权限
* @param {用户ID} userId
*/
export function getUserModules(userId) {
return request({
url: 'business/OdfUserModules/list',
method: 'get',
params: { userId },
})
}
/**
* 批量保存用户模块权限
* @param data { userId, modules: [...] }
*/
export function saveUserModules(data) {
return request({
url: 'business/OdfUserModules/save',
method: 'post',
data: data,
})
}

View File

@ -0,0 +1,199 @@
<!--
* @Descripttion: 光缆表单组件
* @Author: (admin)
* @Date: (2025-01-16)
-->
<template>
<el-dialog :title="title" :lock-scroll="false" v-model="dialogVisible" @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :lg="12">
<el-form-item label="光缆名称" prop="cableName">
<el-input v-model="form.cableName" :disabled="isView" placeholder="请输入光缆名称" />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="所属部门" prop="deptId">
<el-tree-select
v-model="form.deptId"
:data="deptOptions"
:disabled="isView"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择所属部门"
check-strictly
:render-after-expand="false" />
</el-form-item>
</el-col>
<!-- 查看模式下显示时间信息 -->
<template v-if="isView && isEdit">
<el-col :lg="12">
<el-form-item label="创建时间">
<el-input v-model="form.createdAt" disabled />
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="修改时间">
<el-input v-model="form.updatedAt" disabled />
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<template #footer v-if="!isView">
<el-button text @click="handleClose">{{ $t('btn.cancel') }}</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">{{ $t('btn.submit') }}</el-button>
</template>
<template #footer v-else>
<el-button text @click="handleClose">{{ $t('btn.close') }}</el-button>
</template>
</el-dialog>
</template>
<script setup name="odfCableForm">
import { addOdfCables, updateOdfCables, getOdfCables } from '@/api/business/odfcables.js'
import { treeselect } from '@/api/system/dept'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
id: {
type: [String, Number],
default: null
},
mode: {
type: String,
default: 'edit', // 'add', 'edit', 'view'
validator: (value) => ['add', 'edit', 'view'].includes(value)
}
})
const emit = defineEmits(['update:visible', 'success'])
const { proxy } = getCurrentInstance()
const formRef = ref()
const submitLoading = ref(false)
const deptOptions = ref([])
//
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
const isEdit = computed(() => !!props.id)
const isView = computed(() => props.mode === 'view')
const title = computed(() => {
if (props.mode === 'view') return '查看光缆'
return isEdit.value ? '修改光缆' : '添加光缆'
})
//
const form = ref({
id: null,
cableName: null,
deptId: 0,
deptName: null,
createdAt: null,
updatedAt: null
})
//
const rules = {
cableName: [{ required: true, message: '光缆名称不能为空', trigger: 'blur' }]
}
//
function getDeptTreeData() {
treeselect().then((response) => {
deptOptions.value = [{ id: 0, label: '未知部门', children: [] }, ...response.data]
})
}
//
function resetForm() {
form.value = {
id: null,
cableName: null,
deptId: 0,
deptName: null,
createdAt: null,
updatedAt: null
}
nextTick(() => {
proxy.resetForm('formRef')
})
}
//
function loadData() {
if (props.id) {
getOdfCables(props.id).then((res) => {
const { code, data } = res
if (code == 200) {
form.value = { ...data }
}
})
} else {
resetForm()
}
}
//
function submitForm() {
proxy.$refs['formRef'].validate((valid) => {
if (valid) {
submitLoading.value = true
if (isEdit.value) {
updateOdfCables(form.value)
.then((res) => {
proxy.$modal.msgSuccess('修改成功')
handleSuccess()
})
.finally(() => {
submitLoading.value = false
})
} else {
addOdfCables(form.value)
.then((res) => {
proxy.$modal.msgSuccess('新增成功')
handleSuccess()
})
.finally(() => {
submitLoading.value = false
})
}
}
})
}
//
function handleSuccess() {
dialogVisible.value = false
emit('success')
}
//
function handleClose() {
dialogVisible.value = false
resetForm()
}
//
watch(
() => props.visible,
(newVal) => {
if (newVal) {
getDeptTreeData()
loadData()
}
}
)
</script>

View File

@ -0,0 +1,301 @@
<!--
* @Descripttion: (干线故障管理/odf_cable_faults)
* @Author: (admin)
* @Date: (2025-08-05)
-->
<template>
<div>
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
<el-form-item label="所属光缆" prop="cableId">
<el-select
v-model="queryParams.cableId"
filterable
remote
reserve-keyword
clearable
placeholder="请输入光缆名称搜索"
:remote-method="remoteCableSearch"
:loading="cableSearchLoading">
<el-option v-for="item in cableOptions" :key="item.id" :label="item.cableName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="故障时间" prop="faultTimeRange">
<el-date-picker
v-model="faultTimeRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="handleFaultTimeChange" />
</el-form-item>
<el-form-item label="故障原因" prop="faultReason">
<el-input v-model="queryParams.faultReason" placeholder="请输入故障原因" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
</el-form-item>
</el-form>
<!-- 工具区域 -->
<el-row :gutter="15" class="mb10">
<el-col :span="1.5">
<el-button type="danger" :disabled="multiple" v-hasPermi="['odfcablefaults:delete']" plain icon="delete" @click="handleDelete">
{{ $t('btn.delete') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" v-hasPermi="['odfcablefaults:export']" plain icon="download" @click="handleExport">
{{ $t('btn.export') }}
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
:data="dataList"
v-loading="loading"
ref="table"
border
header-cell-class-name="el-table-header-cell"
highlight-current-row
@sort-change="sortChange"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="id" label="Id" align="center" v-if="columns.showColumn('id')" />
<el-table-column prop="faultTime" label="故障时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('faultTime')" />
<el-table-column prop="personnel" label="人员" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('personnel')" />
<el-table-column prop="faultReason" label="故障原因" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('faultReason')" />
<el-table-column prop="mileage" label="表显故障里程" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('mileage')" />
<el-table-column prop="cableName" label="所属光缆" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('cableName')" />
<el-table-column prop="location" label="地点" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('location')" />
<el-table-column prop="createdAt" label="创建时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('createdAt')" />
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button type="primary" size="small" icon="view" title="详情" @click="handlePreview(scope.row)"></el-button>
<el-button
type="danger"
size="small"
icon="delete"
title="删除"
v-hasPermi="['odfcablefaults:delete']"
@click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="故障详情" width="700px" destroy-on-close>
<el-descriptions :column="2" border v-if="detailData">
<el-descriptions-item label="Id">{{ detailData.id }}</el-descriptions-item>
<el-descriptions-item label="故障时间">{{ detailData.faultTime }}</el-descriptions-item>
<el-descriptions-item label="人员">{{ detailData.personnel }}</el-descriptions-item>
<el-descriptions-item label="表显故障里程">{{ detailData.mileage }}</el-descriptions-item>
<el-descriptions-item label="所属光缆">{{ detailData.cableName }}</el-descriptions-item>
<el-descriptions-item label="地点">{{ detailData.location }}</el-descriptions-item>
<el-descriptions-item label="纬度">{{ detailData.latitude }}</el-descriptions-item>
<el-descriptions-item label="经度">{{ detailData.longitude }}</el-descriptions-item>
<el-descriptions-item label="故障原因" :span="2">{{ detailData.faultReason }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remark }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createdAt }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ detailData.updatedAt }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData && detailData.images && detailData.images.length > 0" style="margin-top: 16px;">
<div style="font-weight: bold; margin-bottom: 8px;">故障图片</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<el-image
v-for="(img, index) in detailData.images"
:key="index"
:src="img.imageUrl"
:preview-src-list="detailData.images.map(i => i.imageUrl)"
:initial-index="index"
fit="cover"
style="width: 120px; height: 120px; border-radius: 4px;" />
</div>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="odfcablefaults">
import { listOdfCableFaults, getOdfCableFaults, delOdfCableFaults } from '@/api/business/odfcablefaults.js'
import { listOdfCables } from '@/api/business/odfcables.js'
const { proxy } = getCurrentInstance()
const ids = ref([])
const loading = ref(false)
const showSearch = ref(true)
const detailVisible = ref(false)
const detailData = ref(null)
const detailLoading = ref(false)
const cableOptions = ref([])
const cableSearchLoading = ref(false)
const faultTimeRange = ref(null)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
sort: undefined,
sortType: undefined,
cableId: undefined,
beginFaultTime: undefined,
endFaultTime: undefined,
faultReason: undefined
})
const columns = ref([
{ visible: true, align: 'center', type: '', prop: 'id', label: 'Id' },
{ visible: true, align: 'center', type: '', prop: 'faultTime', label: '故障时间', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'personnel', label: '人员', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'faultReason', label: '故障原因', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'mileage', label: '表显故障里程', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'cableName', label: '所属光缆', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'location', label: '地点', showOverflowTooltip: true },
{ visible: false, align: 'center', type: '', prop: 'latitude', label: '纬度' },
{ visible: false, align: 'center', type: '', prop: 'longitude', label: '经度' },
{ visible: true, align: 'center', type: '', prop: 'createdAt', label: '创建时间', showOverflowTooltip: true }
])
const total = ref(0)
const dataList = ref([])
const queryRef = ref()
const state = reactive({
single: true,
multiple: true
})
const { single, multiple } = toRefs(state)
//
function remoteCableSearch(query) {
if (query) {
cableSearchLoading.value = true
listOdfCables({ cableName: query, pageNum: 1, pageSize: 50 }).then((res) => {
if (res.code == 200) {
cableOptions.value = res.data.result || []
}
cableSearchLoading.value = false
}).catch(() => {
cableSearchLoading.value = false
})
} else {
cableOptions.value = []
}
}
//
function handleFaultTimeChange(val) {
if (val) {
queryParams.beginFaultTime = val[0]
queryParams.endFaultTime = val[1]
} else {
queryParams.beginFaultTime = undefined
queryParams.endFaultTime = undefined
}
}
function getList() {
loading.value = true
listOdfCableFaults(queryParams).then((res) => {
const { code, data } = res
if (code == 200) {
dataList.value = data.result
total.value = data.totalNum
loading.value = false
}
})
}
//
function handleQuery() {
queryParams.pageNum = 1
getList()
}
//
function resetQuery() {
faultTimeRange.value = null
queryParams.beginFaultTime = undefined
queryParams.endFaultTime = undefined
proxy.resetForm('queryRef')
handleQuery()
}
//
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id)
state.single = selection.length != 1
state.multiple = !selection.length
}
//
function sortChange(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
/**
* 查看详情
* @param {*} row
*/
function handlePreview(row) {
detailLoading.value = true
detailVisible.value = true
getOdfCableFaults(row.id).then((res) => {
if (res.code == 200) {
detailData.value = res.data
}
detailLoading.value = false
}).catch(() => {
detailLoading.value = false
})
}
//
function handleDelete(row) {
const Ids = row.id || ids.value
proxy
.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: proxy.$t('common.ok'),
cancelButtonText: proxy.$t('common.cancel'),
type: 'warning'
})
.then(function () {
return delOdfCableFaults(Ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
}
//
function handleExport() {
proxy
.$confirm('是否确认导出干线故障列表数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await proxy.downFile('/business/OdfCableFaults/export', { ...queryParams })
})
}
handleQuery()
</script>

View File

@ -0,0 +1,254 @@
<!--
* @Descripttion: (光缆管理/odf_cables)
* @Author: (admin)
* @Date: (2025-08-05)
-->
<template>
<div>
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
<el-form-item label="光缆名称" prop="cableName">
<el-input v-model="queryParams.cableName" placeholder="请输入光缆名称" />
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
:data="deptOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择所属部门"
check-strictly
clearable
:render-after-expand="false" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
</el-form-item>
</el-form>
<!-- 工具区域 -->
<el-row :gutter="15" class="mb10">
<el-col :span="1.5">
<el-button type="primary" v-hasPermi="['odfcables:add']" plain icon="plus" @click="handleAdd">
{{ $t('btn.add') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" :disabled="single" v-hasPermi="['odfcables:edit']" plain icon="edit" @click="handleUpdate">
{{ $t('btn.edit') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" :disabled="multiple" v-hasPermi="['odfcables:delete']" plain icon="delete" @click="handleDelete">
{{ $t('btn.delete') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" v-hasPermi="['odfcables:export']" plain icon="download" @click="handleExport">
{{ $t('btn.export') }}
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
:data="dataList"
v-loading="loading"
ref="table"
border
header-cell-class-name="el-table-header-cell"
highlight-current-row
@sort-change="sortChange"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="id" label="Id" align="center" v-if="columns.showColumn('id')" />
<el-table-column prop="cableName" label="光缆名称" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('cableName')" />
<el-table-column prop="deptName" label="所属部门" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('deptName')" />
<el-table-column prop="createdAt" label="创建时间" :show-overflow-tooltip="true" v-if="columns.showColumn('createdAt')" />
<el-table-column prop="updatedAt" label="修改时间" :show-overflow-tooltip="true" v-if="columns.showColumn('updatedAt')" />
<el-table-column label="操作" width="160">
<template #default="scope">
<el-button type="primary" size="small" icon="view" title="详情" @click="handlePreview(scope.row)"></el-button>
<el-button
type="success"
size="small"
icon="edit"
title="编辑"
v-hasPermi="['odfcables:edit']"
@click="handleUpdate(scope.row)"></el-button>
<el-button
type="danger"
size="small"
icon="delete"
title="删除"
v-hasPermi="['odfcables:delete']"
@click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 光缆表单组件 -->
<OdfCableForm v-model:visible="formVisible" :id="currentId" :mode="formMode" @success="handleFormSuccess" />
</div>
</template>
<script setup name="odfcables">
import { listOdfCables, delOdfCables } from '@/api/business/odfcables.js'
import OdfCableForm from '@/components/business/OdfCableForm.vue'
import { treeselect } from '@/api/system/dept'
const { proxy } = getCurrentInstance()
const ids = ref([])
const loading = ref(false)
const showSearch = ref(true)
const formVisible = ref(false)
const currentId = ref(null)
const formMode = ref('edit') // 'add', 'edit', 'view'
const deptOptions = ref([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
sort: undefined,
sortType: undefined,
cableName: undefined,
deptId: undefined
})
const columns = ref([
{ visible: true, align: 'center', type: '', prop: 'id', label: 'Id' },
{ visible: true, align: 'center', type: '', prop: 'cableName', label: '光缆名称', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'deptName', label: '所属部门', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'createdAt', label: '创建时间', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'updatedAt', label: '修改时间', showOverflowTooltip: true }
])
const total = ref(0)
const dataList = ref([])
const queryRef = ref()
const state = reactive({
single: true,
multiple: true
})
const { single, multiple } = toRefs(state)
//
function getDeptTreeData() {
treeselect().then((response) => {
deptOptions.value = response.data
})
}
function getList() {
loading.value = true
listOdfCables(queryParams).then((res) => {
const { code, data } = res
if (code == 200) {
dataList.value = data.result
total.value = data.totalNum
loading.value = false
}
})
}
//
function handleQuery() {
queryParams.pageNum = 1
getList()
}
//
function resetQuery() {
proxy.resetForm('queryRef')
handleQuery()
}
//
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.id)
state.single = selection.length != 1
state.multiple = !selection.length
}
//
function sortChange(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
/**
* 查看
* @param {*} row
*/
function handlePreview(row) {
currentId.value = row.id
formMode.value = 'view'
formVisible.value = true
}
//
function handleAdd() {
currentId.value = null
formMode.value = 'add'
formVisible.value = true
}
//
function handleUpdate(row) {
const id = row?.id || ids.value[0]
currentId.value = id
formMode.value = 'edit'
formVisible.value = true
}
//
function handleFormSuccess() {
getList()
}
//
function handleDelete(row) {
const Ids = row.id || ids.value
proxy
.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: proxy.$t('common.ok'),
cancelButtonText: proxy.$t('common.cancel'),
type: 'warning'
})
.then(function () {
return delOdfCables(Ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
}
//
function handleExport() {
proxy
.$confirm('是否确认导出光缆列表数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await proxy.downFile('/business/OdfCables/export', { ...queryParams })
})
}
getDeptTreeData()
handleQuery()
</script>

View File

@ -0,0 +1,181 @@
<!--
* @Descripttion: (签到记录管理/odf_checkin)
* @Author: (admin)
* @Date: (2025-08-05)
-->
<template>
<div>
<el-form :model="queryParams" label-position="right" inline ref="queryRef" v-show="showSearch" @submit.prevent>
<el-form-item label="机房" prop="roomId">
<el-select clearable v-model="queryParams.roomId" placeholder="请选择机房">
<el-option v-for="item in options.sql_odf_room" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue">
<span class="fl">{{ item.dictLabel }}</span>
<span class="fr" style="color: var(--el-text-color-secondary)">{{ item.dictValue }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="人员" prop="personnel">
<el-input v-model="queryParams.personnel" placeholder="请输入人员" />
</el-form-item>
<el-form-item label="签到时间" prop="checkinTimeRange">
<el-date-picker
v-model="checkinTimeRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="handleCheckinTimeChange" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button>
<el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button>
</el-form-item>
</el-form>
<!-- 工具区域 -->
<el-row :gutter="15" class="mb10">
<el-col :span="1.5">
<el-button type="warning" v-hasPermi="['odfcheckin:export']" plain icon="download" @click="handleExport">
{{ $t('btn.export') }}
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table
:data="dataList"
v-loading="loading"
ref="table"
border
header-cell-class-name="el-table-header-cell"
highlight-current-row
@sort-change="sortChange">
<el-table-column prop="id" label="Id" align="center" v-if="columns.showColumn('id')" />
<el-table-column prop="roomName" label="机房名称" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('roomName')" />
<el-table-column prop="personnel" label="人员" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('personnel')" />
<el-table-column prop="checkinTime" label="签到时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('checkinTime')" />
<el-table-column prop="workContent" label="工作内容" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('workContent')" />
<el-table-column prop="userName" label="提交人" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('userName')" />
<el-table-column prop="createdAt" label="创建时间" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('createdAt')" />
</el-table>
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup name="odfcheckin">
import { listOdfCheckin, exportOdfCheckin } from '@/api/business/odfcheckin.js'
const { proxy } = getCurrentInstance()
const loading = ref(false)
const showSearch = ref(true)
const checkinTimeRange = ref(null)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
sort: undefined,
sortType: undefined,
roomId: undefined,
personnel: undefined,
beginCheckinTime: undefined,
endCheckinTime: undefined
})
const columns = ref([
{ visible: true, align: 'center', type: '', prop: 'id', label: 'Id' },
{ visible: true, align: 'center', type: '', prop: 'roomName', label: '机房名称', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'personnel', label: '人员', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'checkinTime', label: '签到时间', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'workContent', label: '工作内容', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'userName', label: '提交人', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'createdAt', label: '创建时间', showOverflowTooltip: true }
])
const total = ref(0)
const dataList = ref([])
const queryRef = ref()
const state = reactive({
options: {
sql_odf_room: []
}
})
const { options } = toRefs(state)
var dictParams = ['sql_odf_room']
proxy.getDicts(dictParams).then((response) => {
response.data.forEach((element) => {
state.options[element.dictType] = element.list
})
})
//
function handleCheckinTimeChange(val) {
if (val) {
queryParams.beginCheckinTime = val[0]
queryParams.endCheckinTime = val[1]
} else {
queryParams.beginCheckinTime = undefined
queryParams.endCheckinTime = undefined
}
}
function getList() {
loading.value = true
listOdfCheckin(queryParams).then((res) => {
const { code, data } = res
if (code == 200) {
dataList.value = data.result
total.value = data.totalNum
loading.value = false
}
})
}
//
function handleQuery() {
queryParams.pageNum = 1
getList()
}
//
function resetQuery() {
checkinTimeRange.value = null
queryParams.beginCheckinTime = undefined
queryParams.endCheckinTime = undefined
proxy.resetForm('queryRef')
handleQuery()
}
//
function sortChange(column) {
var sort = undefined
var sortType = undefined
if (column.prop != null && column.order != null) {
sort = column.prop
sortType = column.order
}
queryParams.sort = sort
queryParams.sortType = sortType
handleQuery()
}
//
function handleExport() {
proxy
.$confirm('是否确认导出签到记录列表数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await proxy.downFile('/business/OdfCheckin/export', { ...queryParams })
})
}
handleQuery()
</script>

View File

@ -0,0 +1,130 @@
<!--
* @Descripttion: (用户模块权限管理/odf_user_modules)
* @Author: (admin)
* @Date: (2025-08-05)
-->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧用户列表 -->
<el-col :span="8" style="min-width: 360px; max-width: 360px;">
<el-card shadow="never">
<template #header>
<el-input v-model="searchKeyword" placeholder="搜索用户" clearable prefix-icon="search" />
</template>
<el-table
:data="filteredUserList"
v-loading="userLoading"
border
highlight-current-row
@current-change="handleUserChange"
style="width: 100%"
max-height="600">
<el-table-column prop="userId" label="用户ID" align="center" width="80" />
<el-table-column prop="userName" label="用户名" align="center" />
</el-table>
</el-card>
</el-col>
<!-- 右侧模块权限配置 -->
<el-col :span="16" style="flex: 1;">
<el-card shadow="never">
<template #header>
<span>{{ currentUser ? currentUser.userName + ' - 模块权限配置' : '请选择用户' }}</span>
</template>
<div v-if="currentUser">
<el-checkbox-group v-model="selectedModules" :disabled="moduleLoading">
<el-checkbox label="odf">机房版块</el-checkbox>
<el-checkbox label="trunk">干线版块</el-checkbox>
<el-checkbox label="route">路线规划</el-checkbox>
</el-checkbox-group>
<div style="margin-top: 20px;">
<el-button
type="primary"
:loading="saveLoading"
v-hasPermi="['odfusermodules:edit']"
@click="handleSave">
保存
</el-button>
</div>
</div>
<el-empty v-else description="请在左侧选择用户" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="odfusermodules">
import { listUsers, getUserModules, saveUserModules } from '@/api/business/odfusermodules.js'
const { proxy } = getCurrentInstance()
const userLoading = ref(false)
const moduleLoading = ref(false)
const saveLoading = ref(false)
const searchKeyword = ref('')
const userList = ref([])
const currentUser = ref(null)
const selectedModules = ref([])
//
const filteredUserList = computed(() => {
if (!searchKeyword.value) return userList.value
const keyword = searchKeyword.value.toLowerCase()
return userList.value.filter((u) => u.userName && u.userName.toLowerCase().includes(keyword))
})
//
function getUserList() {
userLoading.value = true
listUsers()
.then((res) => {
if (res.code == 200) {
userList.value = res.data || []
}
})
.finally(() => {
userLoading.value = false
})
}
//
function handleUserChange(row) {
if (!row) return
currentUser.value = row
loadUserModules(row.userId)
}
//
function loadUserModules(userId) {
moduleLoading.value = true
selectedModules.value = []
getUserModules(userId)
.then((res) => {
if (res.code == 200) {
selectedModules.value = res.data || []
}
})
.finally(() => {
moduleLoading.value = false
})
}
//
function handleSave() {
if (!currentUser.value) return
saveLoading.value = true
saveUserModules({ userId: currentUser.value.userId, modules: selectedModules.value })
.then((res) => {
if (res.code == 200) {
proxy.$modal.msgSuccess('保存成功')
}
})
.finally(() => {
saveLoading.value = false
})
}
getUserList()
</script>

View File

@ -0,0 +1,20 @@
-- =============================================
-- ODF v1.0.2 - 创建签到记录表 odf_checkin
-- 需求: 2.6 (ODF 机架列表页签到功能)
-- =============================================
CREATE TABLE odf_checkin (
Id INT IDENTITY(1,1) PRIMARY KEY,
RoomId INT NOT NULL, -- 关联机房 odf_rooms.Id
Personnel NVARCHAR(200) NOT NULL, -- 签到人员
CheckinTime DATETIME NOT NULL, -- 签到时间(用户选择)
WorkContent NVARCHAR(MAX) NOT NULL, -- 工作内容
UserId BIGINT NULL, -- 提交人 sys_user.UserId
CreatedAt DATETIME DEFAULT GETDATE() -- 记录创建时间
);
-- 索引:按机房查询签到记录
CREATE INDEX IX_odf_checkin_RoomId ON odf_checkin(RoomId);
-- 索引:按时间倒序查询
CREATE INDEX IX_odf_checkin_CheckinTime ON odf_checkin(CheckinTime DESC);

View File

@ -0,0 +1,19 @@
-- =============================================
-- ODF v1.0.2 - 创建光缆表 odf_cables
-- 需求: 4.1 (干线版块 — 光缆列表页)
-- =============================================
CREATE TABLE odf_cables (
Id INT IDENTITY(1,1) PRIMARY KEY,
CableName NVARCHAR(200) NOT NULL, -- 光缆名称
DeptId BIGINT NOT NULL, -- 所属公司/部门 sys_dept.DeptId
DeptName NVARCHAR(100) NULL, -- 冗余:部门名称
CreatedAt DATETIME DEFAULT GETDATE(),
UpdatedAt DATETIME DEFAULT GETDATE()
);
-- 索引:按公司查询光缆列表
CREATE INDEX IX_odf_cables_DeptId ON odf_cables(DeptId);
-- 索引:按名称模糊搜索
CREATE INDEX IX_odf_cables_CableName ON odf_cables(CableName);

View File

@ -0,0 +1,26 @@
-- =============================================
-- ODF v1.0.2 - 创建干线故障表 odf_cable_faults
-- 需求: 5.1, 7.9
-- =============================================
CREATE TABLE odf_cable_faults (
Id INT IDENTITY(1,1) PRIMARY KEY,
CableId INT NOT NULL, -- 关联光缆 odf_cables.Id
FaultTime DATETIME NOT NULL, -- 故障时间
Personnel NVARCHAR(200) NULL, -- 人员
FaultReason NVARCHAR(MAX) NULL, -- 故障原因
Mileage NVARCHAR(200) NULL, -- 表显故障里程
Location NVARCHAR(500) NULL, -- 地点描述
Latitude DECIMAL(10,7) DEFAULT 0, -- 纬度
Longitude DECIMAL(10,7) DEFAULT 0, -- 经度
Remark NVARCHAR(MAX) NULL, -- 备注
UserId BIGINT NULL, -- 提交人 sys_user.UserId
CreatedAt DATETIME DEFAULT GETDATE(),
UpdatedAt DATETIME DEFAULT GETDATE()
);
-- 索引:按光缆查询故障列表
CREATE INDEX IX_odf_cable_faults_CableId ON odf_cable_faults(CableId);
-- 索引:按故障时间倒序
CREATE INDEX IX_odf_cable_faults_FaultTime ON odf_cable_faults(FaultTime DESC);

View File

@ -0,0 +1,14 @@
-- =============================================
-- ODF v1.0.2 - 创建故障图片表 odf_cable_fault_images
-- 需求: 7.9
-- =============================================
CREATE TABLE odf_cable_fault_images (
Id INT IDENTITY(1,1) PRIMARY KEY,
FaultId INT NOT NULL, -- 关联故障 odf_cable_faults.Id
ImageUrl NVARCHAR(500) NOT NULL, -- 图片访问 URL
CreatedAt DATETIME DEFAULT GETDATE()
);
-- 索引:按故障 ID 查询图片
CREATE INDEX IX_odf_cable_fault_images_FaultId ON odf_cable_fault_images(FaultId);

View File

@ -0,0 +1,17 @@
-- =============================================
-- ODF v1.0.2 - 创建用户功能版块权限表 odf_user_modules
-- 需求: 8.1
-- =============================================
CREATE TABLE odf_user_modules (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId BIGINT NOT NULL, -- 用户 sys_user.UserId
ModuleCode NVARCHAR(50) NOT NULL, -- 模块标识:'odf', 'trunk', 'route'
CreatedAt DATETIME DEFAULT GETDATE()
);
-- 唯一索引:同一用户同一模块不重复
CREATE UNIQUE INDEX UX_odf_user_modules_User_Module ON odf_user_modules(UserId, ModuleCode);
-- 索引:按用户查询
CREATE INDEX IX_odf_user_modules_UserId ON odf_user_modules(UserId);

View File

@ -0,0 +1,116 @@
-- =============================================
-- ODF v1.0.2 菜单权限初始化脚本
-- 新增 4 个一级菜单及按钮权限
-- =============================================
-- 1. 光缆管理(一级菜单)
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
VALUES (11190, N'光缆管理', 0, 5, 'OdfCables', 'business/OdfCables', 0, 0, 'C', '0', '0', 'odfcables:list', 'icon1', 'admin', GETDATE());
-- 光缆管理 - 按钮权限
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11191, N'查询', 11190, 1, '#', NULL, 'F', 'odfcables:query', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11192, N'新增', 11190, 2, '#', NULL, 'F', 'odfcables:add', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11193, N'删除', 11190, 3, '#', NULL, 'F', 'odfcables:delete', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11194, N'修改', 11190, 4, '#', NULL, 'F', 'odfcables:edit', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11195, N'导出', 11190, 5, '#', NULL, 'F', 'odfcables:export', 'admin', GETDATE());
-- 2. 干线故障管理(一级菜单)
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
VALUES (11200, N'干线故障管理', 0, 6, 'OdfCableFaults', 'business/OdfCableFaults', 0, 0, 'C', '0', '0', 'odfcablefaults:list', 'icon1', 'admin', GETDATE());
-- 干线故障管理 - 按钮权限
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11201, N'查询', 11200, 1, '#', NULL, 'F', 'odfcablefaults:query', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11202, N'删除', 11200, 2, '#', NULL, 'F', 'odfcablefaults:delete', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11203, N'导出', 11200, 3, '#', NULL, 'F', 'odfcablefaults:export', 'admin', GETDATE());
-- 3. 签到记录管理(一级菜单)
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
VALUES (11210, N'签到记录管理', 0, 7, 'OdfCheckin', 'business/OdfCheckin', 0, 0, 'C', '0', '0', 'odfcheckin:list', 'icon1', 'admin', GETDATE());
-- 签到记录管理 - 按钮权限
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11211, N'查询', 11210, 1, '#', NULL, 'F', 'odfcheckin:query', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11212, N'导出', 11210, 2, '#', NULL, 'F', 'odfcheckin:export', 'admin', GETDATE());
-- 4. 用户模块权限(一级菜单)
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, IsCache, IsFrame, MenuType, Visible, Status, Perms, Icon, Create_by, Create_time)
VALUES (11220, N'用户模块权限', 0, 8, 'OdfUserModules', 'business/OdfUserModules', 0, 0, 'C', '0', '0', 'odfusermodules:list', 'icon1', 'admin', GETDATE());
-- 用户模块权限 - 按钮权限
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11221, N'查询', 11220, 1, '#', NULL, 'F', 'odfusermodules:query', 'admin', GETDATE());
INSERT INTO sys_menu (MenuId, MenuName, ParentId, OrderNum, Path, Component, MenuType, Perms, Create_by, Create_time)
VALUES (11222, N'修改', 11220, 2, '#', NULL, 'F', 'odfusermodules:edit', 'admin', GETDATE());
-- =============================================
-- 角色菜单权限分配
-- Role 2 (common): 所有菜单 + 全部按钮权限
-- Role 3 (editor): 所有菜单 + 仅查询权限
-- Role 4 (lock): 所有菜单 + 仅查询权限
-- Role 1 (admin): 超级管理员自动拥有所有权限,无需插入
-- =============================================
-- Role 2 (common) — 光缆管理:全部权限
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11190, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11191, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11192, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11193, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11194, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11195, 'admin', GETDATE());
-- Role 2 (common) — 干线故障管理:全部权限
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11200, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11201, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11202, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11203, 'admin', GETDATE());
-- Role 2 (common) — 签到记录管理:全部权限
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11210, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11211, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11212, 'admin', GETDATE());
-- Role 2 (common) — 用户模块权限:全部权限
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11220, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11221, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (2, 11222, 'admin', GETDATE());
-- Role 3 (editor) — 光缆管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11190, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11191, 'admin', GETDATE());
-- Role 3 (editor) — 干线故障管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11200, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11201, 'admin', GETDATE());
-- Role 3 (editor) — 签到记录管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11210, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11211, 'admin', GETDATE());
-- Role 3 (editor) — 用户模块权限:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11220, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (3, 11221, 'admin', GETDATE());
-- Role 4 (lock) — 光缆管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11190, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11191, 'admin', GETDATE());
-- Role 4 (lock) — 干线故障管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11200, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11201, 'admin', GETDATE());
-- Role 4 (lock) — 签到记录管理:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11210, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11211, 'admin', GETDATE());
-- Role 4 (lock) — 用户模块权限:菜单 + 查询
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11220, 'admin', GETDATE());
INSERT INTO sys_role_menu (Role_id, Menu_id, Create_by, Create_time) VALUES (4, 11221, 'admin', GETDATE());