Przeglądaj źródła

feat(water): add device ledger edit and polished panels

Kazerin 12 godzin temu
rodzic
commit
da038ed85f

+ 2 - 0
src/api/pipeNetwork/waterSupply.js

@@ -32,6 +32,8 @@ export const saveThresholds = (data) => request({ url: '/waterSupply/alarm/thres
 export const updateThreshold = (data) => request({ url: '/waterSupply/alarm/threshold/update', method: 'put', data })
 export const deleteThresholds = (ids) => request({ url: '/waterSupply/alarm/threshold/delete', method: 'delete', params: { ids } })
 
+export const getWaterDeviceById = (id) => get(`/EquipmentBase/getById/${id}`)
+export const updateWaterDevice = (data) => request({ url: '/EquipmentBase/updateById', method: 'put', data })
 export const listDeviceLedger = (params) => get('/waterSupply/device/ledger/page', params)
 export const getDeviceTypeStatistics = () => get('/waterSupply/device/ledger/typeStatistics')
 export const listDeviceGisPoints = (params) => get('/waterSupply/device/ledger/gisPoints', params)

+ 115 - 7
src/views/subSystem/waterSupply/components/WaterDevicePage.vue

@@ -5,7 +5,7 @@
         <span class="page-title-icon"><el-icon><component :is="pageIcon" /></el-icon></span>
         <div><h2>{{ title }}</h2><span class="muted">{{ subtitle }}</span></div>
       </div>
-      <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
+      <el-button class="heading-refresh" :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
     </div>
 
     <div v-if="view === 'abnormal'" class="metric-grid">
@@ -43,7 +43,7 @@
         <el-table-column label="关联工单" min-width="190" show-overflow-tooltip><template #default="{ row }">{{ view === 'maintain' ? (row.orderNo || '-') : row.relatedOrderNo }}</template></el-table-column>
         <el-table-column label="工单状态" min-width="110"><template #default="{ row }"><el-tag v-if="view !== 'maintain' && row.workOrderStatusText !== '暂无工单'" :type="workOrderStatusType(row.workOrderStatusCode)">{{ row.workOrderStatusText }}</el-tag><span v-else>{{ view === 'maintain' ? (row.status || '暂无') : row.workOrderStatusText }}</span></template></el-table-column>
         <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="接单责任人" min-width="110"><template #default="{ row }">{{ view === 'maintain' ? (row.maintainPerson || '-') : (row.relatedWorkOrderPerson || '-') }}</template></el-table-column>
-        <el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="selectPoint(row)">详情</el-button><el-button v-if="canDispatch(row)" link type="warning" @click="openOrder(row)">维修派单</el-button><el-button v-if="view === 'maintain'" link type="danger" @click="removeRecord(row)">删除</el-button></template></el-table-column>
+        <el-table-column label="操作" width="240" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="selectPoint(row)">详情</el-button><el-button v-if="view === 'ledger'" link type="primary" @click="openEdit(row)">修改</el-button><el-button v-if="canDispatch(row)" link type="warning" @click="openOrder(row)">维修派单</el-button><el-button v-if="view === 'maintain'" link type="danger" @click="removeRecord(row)">删除</el-button></template></el-table-column>
       </el-table>
       <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total" layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" @change="loadData" />
     </el-card>
@@ -60,6 +60,27 @@
       </el-descriptions>
       <template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
     </el-drawer>
+    <el-drawer v-model="editVisible" class="detail-drawer device-edit-drawer" title="修改设备信息" direction="rtl" size="680px" append-to-body>
+      <el-form ref="editFormRef" :model="editForm" label-width="110px" :rules="editRules">
+        <el-row :gutter="16">
+          <el-col :span="12"><el-form-item label="设备编码"><el-input v-model="editForm.equipmentCode" disabled /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="设备类型"><el-input v-model="editForm.equipmentTypeName" disabled /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="设备名称" prop="equipmentName"><el-input v-model="editForm.equipmentName" clearable /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="设备型号"><el-input v-model="editForm.equipmentModel" clearable /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="所属片区"><el-select v-model="editForm.district" clearable class="full-input"><el-option label="城南" value="城南" /><el-option label="城北" value="城北" /><el-option label="太常" value="太常" /></el-select></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="制造商"><el-input v-model="editForm.manufacturer" clearable /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="经度"><el-input-number v-model="editForm.longitude" :precision="8" :controls="false" class="full-input" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="纬度"><el-input-number v-model="editForm.latitude" :precision="8" :controls="false" class="full-input" /></el-form-item></el-col>
+          <el-col :span="24"><el-form-item label="安装位置"><el-input v-model="editForm.equipmentLocation" clearable /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="设备负责人"><el-input v-model="editForm.maintainer" clearable /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="联系方式"><el-input v-model="editForm.maintainerPhone" clearable /></el-form-item></el-col>
+          <el-col :span="24"><el-form-item label="权属单位"><el-input v-model="editForm.ownershipUnit" clearable /></el-form-item></el-col>
+          <el-col :span="24"><el-form-item label="设备图片URL"><el-input v-model="editForm.equipmentImage" clearable /></el-form-item></el-col>
+          <el-col :span="24"><el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></el-col>
+        </el-row>
+      </el-form>
+      <template #footer><el-button @click="editVisible = false">取消</el-button><el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button></template>
+    </el-drawer>
     <el-dialog v-model="dispatchVisible" class="dispatch-dialog" title="设备维修派单" width="860px">
       <div class="dispatch-layout">
         <div class="assignee-picker">
@@ -115,11 +136,11 @@
 <script setup>
 import { computed, onMounted, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Monitor, Refresh, Warning, Bell, Tools, DataLine, Upload, Download } from '@element-plus/icons-vue'
+import { Monitor, Refresh, Warning, Bell, Tools, DataLine, Upload, Download, Search } from '@element-plus/icons-vue'
 import { listUser } from '@/api/system/user'
 import { listDept } from '@/api/system/dept'
 import { handleTree } from '@/utils/ruoyi'
-import { createAbnormalOrder, exportAbnormalDevices, exportDeviceLedger, importDeviceLedger, listAbnormalDevices, listDeviceLedger, listDeviceGisPoints, listMaintainRecords, deleteMaintainRecord } from '@/api/pipeNetwork/waterSupply'
+import { createAbnormalOrder, exportAbnormalDevices, exportDeviceLedger, getWaterDeviceById, importDeviceLedger, listAbnormalDevices, listDeviceLedger, listDeviceGisPoints, listMaintainRecords, updateWaterDevice, deleteMaintainRecord } from '@/api/pipeNetwork/waterSupply'
 import { normalizePage, readMonitoringExportError } from '@/utils/waterSupplyModel'
 import { buildMaintainOrderPayload, normalizeDeviceRow, canDispatchDevice, normalizeDeviceGisPoints, preserveDevicePageOnError, normalizeAssigneeUser, buildAssigneeQuery, selectSingleAssignee, normalizeDispatchOrderResponse } from '@/utils/waterSupplyUiModel'
 import { saveAs } from 'file-saver'
@@ -127,7 +148,8 @@ import { saveAs } from 'file-saver'
 const props = defineProps({ title: { type: String, required: true }, subtitle: { type: String, default: '设备信息、运行状态与维护过程管理' }, view: { type: String, default: 'ledger' } })
 const { title, subtitle, view } = props
 const query = ref({ pageNum: 1, pageSize: 10, equipmentName: '', equipmentCode: '', alarmStatus: undefined, alertLevel: undefined })
-const importVisible = ref(false); const importUploading = ref(false); const importUpdateSupport = ref(false); const importFileList = ref([]); const importUploadRef = ref(null)
+const importVisible = ref(false); const importUploading = ref(false); const importUpdateSupport = ref(false); const importFileList = ref([]); const importUploadRef = ref(null); const editVisible = ref(false); const editSaving = ref(false); const editFormRef = ref(null); const editForm = ref({})
+const editRules = { equipmentName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }] }
 const page = ref({ records: [], total: 0 }); const loading = ref(false); const loadError = ref(''); const detailVisible = ref(false); const detail = ref({}); const dispatchVisible = ref(false); const dispatching = ref(false); const dispatchDevice = ref({}); const dispatchForm = ref({ description: '', receiveUserId: undefined }); const users = ref([]); const userLoading = ref(false); const userLoadError = ref(''); const deptLoadError = ref(''); const selectedAssignee = ref(null); const assigneeDraft = ref(null); const deptTreeRef = ref(null); const deptTreeData = ref([]); const deptFilterText = ref(''); const assigneeKeyword = ref(''); const assigneeDeptId = ref(undefined); const assigneePageNum = ref(1); const assigneePageSize = ref(10); const assigneeTotal = ref(0)
 const levels = [{ label: '一级报警', value: 1 }, { label: '二级报警', value: 2 }, { label: '三级报警', value: 3 }, { label: '四级报警', value: 4 }]; const pageIcon = Monitor; const alarmLevelType = level => ({ 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' }[Number(level)] || 'info')
 function canDispatch(row) {
@@ -199,6 +221,32 @@ async function submitDispatch() {
   }
 }
 async function removeRecord(row) { try { await deleteMaintainRecord(row.maintainId || row.id); ElMessage.success('记录已删除'); await loadData() } catch (error) { ElMessage.error('删除失败') } }
+async function openEdit(row) {
+  editVisible.value = true
+  editSaving.value = false
+  editForm.value = { ...row }
+  try {
+    const response = await getWaterDeviceById(row.equipmentId)
+    const full = response?.data || response
+    if (full) editForm.value = { ...editForm.value, ...full }
+  } catch (error) {
+    ElMessage.warning('设备详情加载失败,已使用列表数据填充')
+  }
+}
+async function submitEdit() {
+  try { await editFormRef.value?.validate() } catch { return }
+  editSaving.value = true
+  try {
+    await updateWaterDevice(editForm.value)
+    ElMessage.success('修改成功')
+    editVisible.value = false
+    await loadData()
+  } catch (error) {
+    ElMessage.error(error?.response?.data?.msg || error?.message || '修改失败,请稍后重试')
+  } finally {
+    editSaving.value = false
+  }
+}
 function openImport() { importVisible.value = true }
 function handleImportChange(file, fileList) {
   if (!/\.(xlsx|xls)$/iu.test(file?.name || '')) {
@@ -247,8 +295,68 @@ onMounted(loadData)
 </script>
 
 <style scoped>
-.device-page{padding:20px;background:#f5f7fa;min-height:calc(100vh - 84px)}.page-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.page-title{display:flex;gap:12px;align-items:center}.page-title h2{margin:0 0 5px;font-size:20px}.page-title-icon{display:grid;place-items:center;width:38px;height:38px;color:#2563eb;background:#eaf2ff;border-radius:8px}.muted{font-size:13px;color:#909399}.filter-card{margin-bottom:16px}.filter-card :deep(.el-form-item){margin-bottom:0}.card-title{display:flex;justify-content:space-between;align-items:center;font-weight:600;margin-bottom:14px}.el-pagination{justify-content:flex-end;margin-top:16px}.detail-summary{display:flex;align-items:center;gap:12px;margin-bottom:16px}.detail-name{font-size:18px;font-weight:700;color:#17324d}.device-detail-drawer :deep(.el-drawer__body){overflow:auto}.device-detail-drawer :deep(.el-descriptions__label){white-space:nowrap}.device-detail-drawer :deep(.el-descriptions__content){word-break:break-word;overflow-wrap:anywhere}
-.dispatch-layout{display:flex;gap:20px;min-height:420px}.dispatch-form{width:300px;min-width:280px;padding-left:20px;border-left:1px solid #e4e7ed}.assignee-field{display:flex;align-items:center;gap:12px;width:100%;min-height:32px}.assignee-summary{color:#17324d}.assignee-placeholder{color:#909399}.assignee-picker{display:flex;gap:16px;flex:1;min-width:0;min-height:390px}.assignee-depts{width:190px;min-width:190px;padding-right:16px;border-right:1px solid #e4e7ed}.assignee-tree{margin-top:10px;max-height:340px;overflow:auto}.assignee-users{flex:1;min-width:0}.assignee-search{display:flex;gap:8px;margin-bottom:12px}.assignee-search .el-input{flex:1}.assignee-users .el-pagination{margin-top:12px}.assignee-picked{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:14px;padding:10px 12px;background:#f5f7fa;border:1px solid #ebeef5;color:#606266}.assignee-picked strong{color:#2563eb}.assignee-users .el-alert{margin-bottom:12px}
+.device-page{padding:20px;background:linear-gradient(135deg,#eff6ff 0%,#f8fafc 45%,#eef2f7 100%);min-height:calc(100vh - 84px)}
+.page-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}
+.page-title{display:flex;align-items:center;gap:12px;min-width:0}
+.page-title-icon{display:grid;place-items:center;width:42px;height:42px;color:#fff;background:linear-gradient(135deg,#2563eb 0%,#06b6d4 100%);border-radius:8px;box-shadow:0 8px 16px rgba(37,99,235,.2)}
+.page-title h2{margin:0 0 4px;font-size:20px;color:#1e293b}
+.muted{font-size:13px;color:#64748b}
+.heading-refresh{border-color:#bfdbfe;color:#2563eb;background:#fff}
+.filter-card,.water-panel{border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 6px 14px rgba(15,23,42,.04)}
+.filter-card{margin-bottom:16px;background:#fff}
+.filter-card :deep(.el-card__header){padding:14px 18px;border-bottom:1px solid #e2e8f0;background:#fbfdff}
+.filter-card :deep(.el-form-item){margin-bottom:0}
+.filter-card :deep(.el-form-item__label){color:#475569}
+.metric-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:16px}
+.metric{display:flex;align-items:center;gap:14px;min-width:0;padding:16px 18px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 6px 14px rgba(15,23,42,.04)}
+.metric-icon{position:static;flex:0 0 42px;display:grid;place-items:center;width:42px;height:42px;color:#2563eb;background:linear-gradient(135deg,#dbeafe 0%,#eff6ff 100%);border-radius:8px}
+.metric-icon .el-icon{font-size:22px}
+.metric-body{min-width:0;text-align:left}
+.metric-body span{display:block;overflow:hidden;margin-bottom:3px;color:#64748b;font-size:13px;text-overflow:ellipsis;white-space:nowrap}
+.metric-value{display:flex;align-items:baseline;gap:5px;min-width:0}
+.metric-value strong{font-size:24px;line-height:1.2;color:#1e293b}
+.metric-value small{color:#94a3b8}
+.water-panel{overflow:hidden}
+.water-panel :deep(.el-card__header){padding:14px 18px;border-bottom:1px solid #e2e8f0;background:#fbfdff}
+.card-title{display:flex;align-items:center;gap:8px;color:#1e293b;font-weight:600}
+.card-title .el-icon{color:#2563eb;font-size:18px}
+.table-toolbar{display:flex;justify-content:space-between;align-items:center}
+.panel-actions{display:flex;align-items:center;gap:8px}
+.load-error{margin-bottom:14px}
+.device-page :deep(.el-table){--el-table-border-color:#e2e8f0;--el-table-header-bg-color:#f8fafc;--el-table-header-text-color:#475569;--el-table-row-hover-bg-color:#eff6ff}
+.device-page :deep(.el-table__row){cursor:pointer}
+.device-image{width:66px;height:44px;border-radius:6px;overflow:hidden}
+.image-placeholder{display:inline-grid;place-items:center;width:66px;height:44px;color:#94a3b8;font-size:12px;background:#f1f5f9;border-radius:6px}
+.detail-summary{display:flex;align-items:center;gap:12px;margin-bottom:16px}
+.detail-name{font-size:18px;font-weight:700;color:#17324d}
+.device-detail-drawer :deep(.el-drawer__body),.device-edit-drawer :deep(.el-drawer__body){overflow:auto;padding:20px 24px}
+.device-detail-drawer :deep(.el-descriptions__label){white-space:nowrap}
+.device-detail-drawer :deep(.el-descriptions__content){overflow-wrap:anywhere;word-break:break-word}
+.detail-media{margin-bottom:14px;padding:12px;background:#f7fbff;border:1px solid rgba(37,99,235,.1);border-radius:10px}
+.detail-media-label{margin-bottom:8px;font-weight:600;color:#17324d}
+.detail-image{width:100%;height:180px;border-radius:8px;overflow:hidden}
+.detail-image-placeholder{display:grid;place-items:center;height:120px;background:#f1f5f9;border-radius:8px;color:#909399}
+.full-input{width:100%}
+.import-update{margin-top:10px}
+.import-tip{line-height:1.6}
+.el-pagination{justify-content:flex-end;margin-top:16px}
+.dispatch-layout{display:flex;gap:20px;min-height:420px}
+.dispatch-form{width:300px;min-width:280px;padding-left:20px;border-left:1px solid #e4e7ed}
+.assignee-field{display:flex;align-items:center;gap:12px;width:100%;min-height:32px}
+.assignee-summary{color:#17324d}
+.assignee-placeholder{color:#909399}
+.assignee-picker{display:flex;gap:16px;flex:1;min-width:0;min-height:390px}
+.assignee-depts{width:190px;min-width:190px;padding-right:16px;border-right:1px solid #e4e7ed}
+.assignee-tree{margin-top:10px;max-height:340px;overflow:auto}
+.assignee-users{flex:1;min-width:0}
+.assignee-search{display:flex;gap:8px;margin-bottom:12px}
+.assignee-search .el-input{flex:1}
+.assignee-users .el-pagination{margin-top:12px}
+.assignee-picked{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:14px;padding:10px 12px;background:#f5f7fa;border:1px solid #ebeef5;color:#606266}
+.assignee-picked strong{color:#2563eb}
+.assignee-users .el-alert{margin-bottom:12px}
+@media(max-width:900px){.metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.filter-card :deep(.el-form-item){margin-bottom:12px}}
+@media(max-width:768px){.device-detail-drawer,.device-edit-drawer{width:100%!important}.dispatch-dialog{width:calc(100% - 24px)!important;margin-left:auto;margin-right:auto}.dispatch-layout{flex-direction:column}.assignee-picker{flex-direction:column}.assignee-depts{width:auto;min-width:0;padding-right:0;padding-bottom:12px;border-right:0;border-bottom:1px solid #e4e7ed}.assignee-tree{max-height:180px}.dispatch-form{width:auto;min-width:0;padding-left:0;padding-top:16px;border-left:0;border-top:1px solid #e4e7ed}}
 </style>
 <style lang="scss">
 @use './waterSupplyPageTheme.scss';

+ 4 - 0
tests/waterDeviceExport.test.mjs

@@ -25,11 +25,15 @@ test('water device ledger import and image display use the public contract', asy
     'utf8'
   )
 
+  assert.match(apiSource, /export const getWaterDeviceById = \(id\) => get\(`\/EquipmentBase\/getById\/\$\{id\}`\)/u)
+  assert.match(apiSource, /export const updateWaterDevice = \(data\) => request\(\{ url: '\/EquipmentBase\/updateById', method: 'put', data \}/u)
   assert.match(apiSource, /export const importDeviceLedger = \(file, updateSupport = false\)/u)
   assert.match(apiSource, /url: '\/waterSupply\/device\/ledger\/import'/u)
   assert.match(pageSource, /importDeviceLedger/u)
   assert.match(pageSource, /供水设备批量导入/u)
   assert.match(pageSource, /importUpdateSupport/u)
+  assert.match(pageSource, /openEdit\(row\)/u)
+  assert.match(pageSource, /updateWaterDevice\(editForm\.value\)/u)
   assert.match(pageSource, /row\.equipmentImage/u)
   assert.match(pageSource, /detail\.equipmentImage/u)
 })