| screen/src/view/hiddenDanger/README.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| screen/src/view/hiddenDanger/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| screen/src/view/hiddenDanger/service.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
screen/src/view/hiddenDanger/README.md
New file @@ -0,0 +1,326 @@ # 隐患管理模块 - 项目端与委托方端兼容方案 ## 📋 概述 本模块实现了项目端和委托方端的统一管理,通过数据适配器模式,在不改变原有项目端逻辑的前提下,完美兼容委托方端的接口调用。 **判断逻辑:通过路由参数 `projectId` 区分** - ✅ **有 `projectId`** = 项目端 - ✅ **无 `projectId`** = 委托方端 ## 🔄 接口差异 ### 1. 列表查询接口 | 端 | 重大隐患 | 安全隐患 | 设备隐患 | |---|---------|---------|---------| | **项目端** | `/project/getHiddenDangerList` | 同左(通过type参数区分) | 同左(通过type参数区分) | | **委托方** | `/projectEntrust/errorList` | `/projectEntrust/safetyErrorList` | `/projectEntrust/deviceErrorList` | ### 2. 字段差异 | 字段含义 | 项目端 | 委托方 | 处理方式 | |---------|--------|--------|---------| | 问题描述 | `description` | `problemName` | 适配器映射 | | 检查时间 | `createTime` | `checkTime` | 适配器映射 | | 图片列表 | `userFiles` | `pictures` | 适配器映射 | | 图片数量 | `fileNum` | `pictures.length` | 适配器计算 | | 隐患等级 | 0-3 | 1-4 | 适配器转换(-1) | | 整改状态 | 0/1 (number) | true/false (boolean) | 适配器转换 | ### 3. 详情和编辑接口 | 功能 | 项目端 | 委托方 | |-----|--------|--------| | 获取详情 | `/project/getHiddenDangerDetail/{id}` | `/projectEntrust/getHiddenDangerDetail/{id}` | | 编辑责任人 | `/project/editHiddenDangerResponsiblePerson` | `/projectEntrust/editHiddenDangerResponsiblePerson` | ### 4. 项目列表接口 | 功能 | 接口 | 参数 | |-----|------|------| | 获取项目列表 | `/project/getLoginNameVoList` | `userId`(从 localStorage 获取) | ## 🛠️ 实现方案 ### 1. service.js 架构 ``` service.js ├── 项目端接口(原有) │ ├── getLoginNameVoList (需要 userId 参数) │ ├── getHiddenDangerList │ ├── getHiddenDangerDetail │ └── editHiddenDangerResponsiblePerson │ ├── 委托方接口(新增) │ ├── getClientMajorErrorList │ ├── getClientSafetyErrorList │ ├── getClientDeviceErrorList │ ├── getClientHiddenDangerDetail │ └── editClientHiddenDangerResponsiblePerson │ ├── 数据适配器(核心) │ ├── adaptClientDataToProject - 委托方数据 → 项目端格式 │ └── adaptProjectDataToClient - 项目端数据 → 委托方格式 │ └── 统一调用方法(对外暴露) ├── getHiddenDangerListByType ├── getHiddenDangerDetailByType └── editResponsiblePersonByType ``` ### 2. 数据适配器逻辑 #### adaptClientDataToProject(委托方 → 项目端) ```javascript { description: item.problemName, // 字段映射 createTime: item.checkTime, // 字段映射 userFiles: item.pictures, // 字段映射 fileNum: item.pictures.length, // 计算字段 level: item.level - 1, // 等级转换:1-4 → 0-3 reformed: item.reformed ? 1 : 0, // 布尔转数字 } ``` #### adaptProjectDataToClient(项目端 → 委托方) ```javascript { reformed: data.reformed === 1 || data.reformed === true // 数字转布尔 } ``` ### 3. index.vue 修改点 #### 新增数据字段和计算属性 ```javascript data() { return { isClient: false, // 是否委托方端(新增) // ... 其他字段保持不变 } }, computed: { isProjectMode() { return !!this.projectId; // 有projectId = 项目端 }, isClientMode() { return !this.projectId; // 无projectId = 委托方端 } } ``` #### 判断用户类型(created钩子) ```javascript created() { // 从路由获取参数 if (this.$route.query.projectId) { this.projectId = this.$route.query.projectId; } // 通过 projectId 判断:有 = 项目端,无 = 委托方端 this.isClient = !this.projectId; } ``` #### 加载项目列表(从 localStorage 获取 userId) ```javascript async loadProjectList() { try { // 从 localStorage 获取 userInfo const userInfoStr = localStorage.getItem('userInfo'); if (!userInfoStr) { console.error("未找到用户信息"); return; } const userInfo = JSON.parse(userInfoStr); const userId = userInfo.userId; if (!userId) { console.error("用户信息中未找到 userId"); return; } // 调用接口,传入 userId const res = await getLoginNameVoList({ userId }); if (res && res) { this.projectList = res.map((item) => ({ label: item.name, value: item.id, })); } } catch (error) { console.error("加载项目列表失败:", error); } } ``` #### 修改的方法(仅3处) 1. **loadTableData()** - 加载列表数据 ```javascript const res = await getHiddenDangerListByType( params, this.activeTab, this.isClient // 传入用户类型 ); ``` 2. **handleViewDetail()** - 查看详情 ```javascript const res = await getHiddenDangerDetailByType( row.problemId, this.isClient // 传入用户类型 ); ``` 3. **saveUpdatePeople()** - 保存责任人 ```javascript await editResponsiblePersonByType( { id: this.currentEditRow.id, responsiblePerson: this.peopleName }, this.isClient // 传入用户类型 ); ``` ## 🎯 使用方式 ### **项目端访问(原有方式)** ```javascript // 带 projectId 参数 = 项目端 this.$router.push({ path: '/hiddenDanger', query: { projectId: '123', // 👈 有这个参数就是项目端 projectName: '某某项目', activeTab: 1 } }) ``` ### **委托方端访问(新增方式)** ```javascript // 不带 projectId 参数 = 委托方端 this.$router.push({ path: '/hiddenDanger', query: { // 👈 没有 projectId 参数就是委托方端 activeTab: 1 } }) // 委托方也可以选择项目后传入 projectId(用于筛选) // 但页面会显示项目选择下拉框,用户可以切换项目 ``` ## 🔍 工作原理 ``` 用户访问页面 ↓ 读取路由参数 projectId ↓ ┌─────────────┬─────────────┐ │ 有projectId │ 无projectId │ │ 项目端 │ 委托方端 │ └─────────────┴─────────────┘ ↓ ↓ 调用项目端接口 调用委托方接口 ↓ ↓ │ 数据适配器转换 │ ↓ └──────┬───────┘ ↓ 统一的数据格式 ↓ 正常渲染页面 ``` ## ✨ 方案优势 | 优势 | 说明 | |-----|------| | 🎯 **零侵入** | 项目端代码逻辑完全不受影响 | | 🔄 **高复用** | UI和业务逻辑100%复用 | | 🛠️ **易维护** | 只需维护一个页面 | | 🚀 **易扩展** | 未来增加新角色只需添加适配器 | | ✅ **低风险** | 通过适配器隔离差异,不会数据混乱 | | 🎨 **简单判断** | 只需通过 projectId 即可区分端 | ## 🔍 测试要点 ### 1. 项目端测试(回归测试) - [ ] 带 projectId 参数访问 - [ ] 列表查询正常 - [ ] 三个Tab切换正常 - [ ] 筛选条件正常 - [ ] 详情查看正常 - [ ] 编辑责任人正常 - [ ] 图片预览正常 ### 2. 委托方端测试(新功能测试) - [ ] 不带 projectId 参数访问 - [ ] 显示项目选择下拉框 - [ ] 项目列表加载正常(需要 localStorage 中有 userInfo.userId) - [ ] 重大隐患列表正常(调用 `/projectEntrust/errorList`) - [ ] 安全隐患列表正常(调用 `/projectEntrust/safetyErrorList`) - [ ] 设备隐患列表正常(调用 `/projectEntrust/deviceErrorList`) - [ ] 字段映射正确(problemName → description) - [ ] 时间字段正确(checkTime → createTime) - [ ] 图片显示正常(pictures → userFiles) - [ ] 隐患等级正确(1-4 → 一级~四级) - [ ] 整改状态正确(true/false → 已整改/未整改) - [ ] 详情查看正常 - [ ] 编辑责任人正常 ### 3. 边界测试 - [ ] 空数据处理 - [ ] 图片为空时的处理 - [ ] 等级为null时的处理 - [ ] 整改状态为null时的处理 - [ ] localStorage 中没有 userInfo 的处理 - [ ] userInfo 中没有 userId 的处理 ## 📝 注意事项 1. **判断逻辑**: - 当前通过 `projectId` 是否存在来判断 - 项目端:从上游页面跳转时带 `projectId`,页面不显示项目选择框 - 委托方端:不带 `projectId`,页面显示项目选择下拉框 2. **localStorage 依赖**: - 委托方端需要从 `localStorage.getItem('userInfo')` 获取用户信息 - 确保登录时已将 `userInfo` 存储到 localStorage - `userInfo` 对象中必须包含 `userId` 字段 3. **接口路径确认**:如果委托方的详情和编辑接口路径不同,需要在 service.js 中修改 4. **权限控制**:如果需要权限控制,建议在路由守卫中处理 5. **错误处理**:适配器中已做了兼容处理,但建议在实际使用中增加更多的边界判断 ## 🚀 后续优化建议 1. 可以将 `isClient` 存储到 Vuex 中,避免每次从路由参数读取 2. 可以将适配器逻辑抽离为独立的工具类,便于其他模块复用 3. 可以增加日志记录,方便排查问题 4. 可以增加单元测试,确保适配器逻辑正确 5. 建议在应用初始化时检查 localStorage 中是否有 userInfo,避免运行时错误 ## 📞 联系方式 如有问题,请联系开发团队。 screen/src/view/hiddenDanger/index.vue
@@ -39,7 +39,7 @@ v-model="searchForm.projectId" placeholder="请选择项目" class="flex1" clearable @change="handleSelectChange" > <el-option v-for="item in projectList" @@ -49,10 +49,17 @@ > </el-option> </el-select> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearSelect('projectId')" class="el-icon-circle-close c-fff cursor-pointer" v-show="searchForm.projectId" /> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> </div> </div> <!-- 整改状态 --> @@ -63,7 +70,7 @@ v-model="searchForm.reformed" placeholder="请选择整改状态" class="flex1" clearable @change="handleSelectChange" > <el-option v-for="item in reformedOptions" @@ -73,10 +80,17 @@ > </el-option> </el-select> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearSelect('reformed')" class="el-icon-circle-close c-fff cursor-pointer" v-show="searchForm.reformed !== null" /> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> </div> </div> <!-- 隐患类型(仅重大隐患显示) --> @@ -88,15 +102,22 @@ v-model="searchForm.type" placeholder="请选择隐患类型" class="flex1" clearable @change="handleSelectChange" > <el-option label="安全隐患" :value="1"></el-option> <el-option label="设备隐患" :value="2"></el-option> </el-select> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearSelect('type')" class="el-icon-circle-close c-fff cursor-pointer" v-show="searchForm.type !== null" /> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> </div> </div> <!-- 隐患等级(安全和设备隐患显示) --> @@ -108,7 +129,7 @@ v-model="searchForm.level" placeholder="请选择隐患等级" class="flex1" clearable @change="handleSelectChange" > <el-option v-for="item in levelOptions" @@ -118,10 +139,17 @@ > </el-option> </el-select> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearSelect('level')" class="el-icon-circle-close c-fff cursor-pointer" v-show="searchForm.level !== null" /> <img src="../../assets/img/down-circle@2x.png" class="w--15 h--16 shrink0" /> </div> </div> <!-- 开始时间 --> @@ -149,10 +177,17 @@ > 开始时间 </div> <img src="../../assets/img/home/Shape@2x (6).png" class="w--13 h--13 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearDateTime('start')" class="el-icon-circle-close c-fff cursor-pointer" v-show="startDateTxt" /> <img src="../../assets/img/home/Shape@2x (6).png" class="w--13 h--13 shrink0" /> </div> </div> <!-- 结束时间 --> @@ -180,10 +215,17 @@ > 结束时间 </div> <img src="../../assets/img/home/Shape@2x (6).png" class="w--13 h--13 shrink0" /> <div class="flex a-center gap--5"> <i @click.stop="clearDateTime('end')" class="el-icon-circle-close c-fff cursor-pointer" v-show="endDateTxt" /> <img src="../../assets/img/home/Shape@2x (6).png" class="w--13 h--13 shrink0" /> </div> </div> <!-- 搜索内容 --> @@ -203,6 +245,7 @@ </div> <!-- 表格 --> <CustomTable :key="activeTab" :data="tableData" height="800" style="border: 2px solid #3d7fff" @@ -539,9 +582,9 @@ import moment from "moment"; import { getLoginNameVoList, getHiddenDangerList, getHiddenDangerDetail, editHiddenDangerResponsiblePerson, getHiddenDangerListByType, getHiddenDangerDetailByType, editResponsiblePersonByType, } from "./service"; import { errorLevelMap, @@ -559,6 +602,7 @@ // 路由参数 projectId: null, // 项目ID,如果有则是项目端使用 projectName: "", // 项目名称 isClient: false, // 是否委托方端 // 搜索条件 searchForm: { @@ -607,6 +651,11 @@ return !!this.projectId; }, // 是否是委托方端(没有projectId参数) isClientMode() { return !this.projectId; }, // 当前隐患类型(根据activeTab判断) currentType() { // 1=重大隐患(暂无对应type) 2=安全隐患(type=1) 3=设备隐患(type=2) @@ -642,6 +691,8 @@ if (newVal) { this.startDateTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss"); this.searchForm.startTime = moment(newVal).format("YYYY-MM-DD HH:mm:ss"); // 选择时间后自动请求 this.handleSelectChange(); } else { this.startDateTxt = ""; this.searchForm.startTime = null; @@ -655,6 +706,8 @@ if (newVal) { this.endDateTxt = moment(newVal).format("YYYY年MM月DD日 HH:mm:ss"); this.searchForm.endTime = moment(newVal).format("YYYY-MM-DD HH:mm:ss"); // 选择时间后自动请求 this.handleSelectChange(); } else { this.endDateTxt = ""; this.searchForm.endTime = null; @@ -674,6 +727,10 @@ if (this.$route.query.projectName) { this.projectName = this.$route.query.projectName; } // 判断是否委托方端:通过 projectId 判断 // 有 projectId = 项目端,无 projectId = 委托方端 this.isClient = !this.projectId; // 初始化下拉选项 this.initOptions(); @@ -723,7 +780,22 @@ // 加载项目列表 async loadProjectList() { try { const res = await getLoginNameVoList({}); // 从 localStorage 获取 userInfo const userInfoStr = localStorage.getItem('userInfo'); if (!userInfoStr) { console.error("未找到用户信息"); return; } const userInfo = JSON.parse(userInfoStr); const userId = userInfo.userId; if (!userId) { console.error("用户信息中未找到 userId"); return; } const res = await getLoginNameVoList({ userId }); if (res && res) { this.projectList = res.map((item) => ({ label: item.name, @@ -743,8 +815,21 @@ pageNum: this.pageNum, pageSize: this.pageSize, ...this.searchForm, type: this.currentType, }; // 项目端:使用 currentType(根据 Tab 自动判断) // 委托方: // - 重大隐患 Tab:使用 searchForm.type(用户选择的隐患类型筛选) // - 安全/设备 Tab:不需要传 type(已经通过不同接口区分) if (!this.isClient) { // 项目端:传递 type params.type = this.currentType; } else { // 委托方:只在重大隐患 Tab 且用户选择了类型时传递 type if (this.activeTab === 1 && this.searchForm.type) { params.type = this.searchForm.type; } } // 过滤掉空值 Object.keys(params).forEach((key) => { @@ -753,8 +838,14 @@ } }); const res = await getHiddenDangerList(params); if (res ) { // 使用统一方法,自动根据用户类型和Tab调用对应接口 const res = await getHiddenDangerListByType( params, this.activeTab, this.isClient ); if (res) { this.tableData = res.records || []; this.total = res.total || 0; } @@ -770,6 +861,32 @@ handleSearch() { this.pageNum = 1; this.loadTableData(); }, // 选择器改变时自动请求 handleSelectChange() { this.pageNum = 1; this.loadTableData(); }, // 清除选择器 clearSelect(field) { this.searchForm[field] = null; this.handleSelectChange(); }, // 清除日期时间 clearDateTime(type) { if (type === 'start') { this.startDateTime = null; this.startDateTxt = ""; this.searchForm.startTime = null; } else { this.endDateTime = null; this.endDateTxt = ""; this.searchForm.endTime = null; } this.handleSelectChange(); }, // 点击日期选择器 @@ -827,10 +944,14 @@ } try { await editHiddenDangerResponsiblePerson({ id: this.currentEditRow.id, responsiblePerson: this.peopleName, }); // 使用统一方法 await editResponsiblePersonByType( { id: this.currentEditRow.id, responsiblePerson: this.peopleName, }, this.isClient ); this.$message({ message: "保存成功", @@ -852,7 +973,12 @@ // 保存当前行数据 this.currentRow = row; const res = await getHiddenDangerDetail(row.problemId); // 使用统一方法 const res = await getHiddenDangerDetailByType( row.problemId, this.isClient ); if (res && res) { this.detailData = res; this.detailVisible = true; screen/src/view/hiddenDanger/service.js
@@ -1,25 +1,165 @@ import request from '@/utils/request' // ==================== 项目端接口 ==================== // 获取项目-委托方列表 export const getLoginNameVoList = async (params) => { return request(`/project/getLoginNameVoList`, { method: 'get', params }) } // 获取隐患列表 // 获取隐患列表(项目端) export const getHiddenDangerList = async (data) => { return request(`/project/getHiddenDangerList`, { method: 'post', data }) } // 获取隐患详情 // 获取隐患详情(项目端) export const getHiddenDangerDetail = async (problemId) => { return request(`/project/getHiddenDangerDetail/${problemId}`, { method: 'get' }) } // 编辑隐患责任人 // 编辑隐患责任人(项目端) export const editHiddenDangerResponsiblePerson = async (data) => { return request(`/project/editHiddenDangerResponsiblePerson`, { method: 'post', data }) } // ==================== 委托方接口 ==================== // 获取重大隐患列表(委托方) export const getClientMajorErrorList = async (data) => { return request(`/projectEntrust/errorList`, { method: 'post', data }) } // 获取安全隐患列表(委托方) export const getClientSafetyErrorList = async (data) => { return request(`/projectEntrust/safetyErrorList`, { method: 'post', data }) } // 获取设备隐患列表(委托方) export const getClientDeviceErrorList = async (data) => { return request(`/projectEntrust/deviceErrorList`, { method: 'post', data }) } // 获取隐患详情(委托方) export const getClientHiddenDangerDetail = async (problemId) => { return request(`/projectEntrust/getHiddenDangerDetail/${problemId}`, { method: 'get' }) } // 编辑隐患责任人(委托方) export const editClientHiddenDangerResponsiblePerson = async (data) => { return request(`/projectEntrust/editHiddenDangerResponsiblePerson`, { method: 'post', data }) } // ==================== 数据适配器 ==================== /** * 将委托方接口数据适配为项目端格式 */ const adaptClientDataToProject = (item) => { return { ...item, // 字段映射 description: item.problemName || item.description, // 问题描述 createTime: item.checkTime || item.createTime, // 检查时间 userFiles: item.pictures || item.userFiles || [], // 图片列表 fileNum: item.pictures ? item.pictures.length : (item.fileNum || 0), // 图片数量 // 隐患等级转换:委托方 1-4 -> 项目端 0-3 level: item.level !== undefined && item.level !== null ? item.level - 1 : item.level, // 整改状态兼容:委托方 boolean -> 项目端 number reformed: typeof item.reformed === 'boolean' ? (item.reformed ? 1 : 0) : item.reformed, } } /** * 将项目端数据适配为委托方格式(用于编辑等操作) */ const adaptProjectDataToClient = (data) => { const adapted = { ...data } // 整改状态转换:项目端 number -> 委托方 boolean if (adapted.reformed !== undefined && adapted.reformed !== null) { adapted.reformed = adapted.reformed === 1 || adapted.reformed === true } return adapted } // ==================== 统一调用方法 ==================== /** * 根据用户类型获取隐患列表 * @param {Object} data - 查询参数 * @param {Number} activeTab - 当前Tab (1=重大 2=安全 3=设备) * @param {Boolean} isClient - 是否委托方 */ export const getHiddenDangerListByType = async (data, activeTab, isClient) => { let result if (isClient) { // 委托方:根据不同Tab调用不同接口 const clientData = adaptProjectDataToClient(data) switch (activeTab) { case 1: // 重大隐患 result = await getClientMajorErrorList(clientData) break case 2: // 安全隐患 result = await getClientSafetyErrorList(clientData) break case 3: // 设备隐患 result = await getClientDeviceErrorList(clientData) break default: result = await getClientMajorErrorList(clientData) } // 数据适配 if (result && result.records) { result.records = result.records.map(item => adaptClientDataToProject(item)) } } else { // 项目端:统一接口 result = await getHiddenDangerList(data) } return result } /** * 根据用户类型获取隐患详情 * @param {String} problemId - 问题ID * @param {Boolean} isClient - 是否委托方 */ export const getHiddenDangerDetailByType = async (problemId, isClient) => { let result if (isClient) { result = await getClientHiddenDangerDetail(problemId) // 数据适配 if (result) { result = adaptClientDataToProject(result) } } else { result = await getHiddenDangerDetail(problemId) } return result } /** * 根据用户类型编辑责任人 * @param {Object} data - 编辑数据 * @param {Boolean} isClient - 是否委托方 */ export const editResponsiblePersonByType = async (data, isClient) => { if (isClient) { const clientData = adaptProjectDataToClient(data) return editClientHiddenDangerResponsiblePerson(clientData) } else { return editHiddenDangerResponsiblePerson(data) } }