Explorar o código

feat(water): refine device ledger and abnormal UI

Kazerin hai 8 horas
pai
achega
7c3b7e69a5

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

@@ -36,6 +36,12 @@ export const listDeviceLedger = (params) => get('/waterSupply/device/ledger/page
 export const getDeviceTypeStatistics = () => get('/waterSupply/device/ledger/typeStatistics')
 export const listDeviceGisPoints = (params) => get('/waterSupply/device/ledger/gisPoints', params)
 export const exportDeviceLedger = (params) => request({ url: '/waterSupply/device/ledger/export', method: 'post', params, responseType: 'blob' })
+export const importDeviceLedger = (file, updateSupport = false) => {
+  const formData = new FormData()
+  formData.append('file', file)
+  formData.append('updateSupport', String(updateSupport))
+  return request({ url: '/waterSupply/device/ledger/import', method: 'post', data: formData })
+}
 export const listAbnormalDevices = (params) => get('/waterSupply/device/abnormal/page', params)
 export const listAbnormalGisPoints = (params) => get('/waterSupply/device/abnormal/gisPoints', params)
 export const exportAbnormalDevices = (params) => request({ url: '/waterSupply/device/abnormal/export', method: 'post', params, responseType: 'blob' })

+ 57 - 6
src/views/subSystem/waterSupply/components/WaterDevicePage.vue

@@ -22,18 +22,19 @@
       <el-form :inline="true" :model="query">
         <el-form-item v-if="view !== 'gis'" label="设备名称"><el-input v-model="query.equipmentName" clearable /></el-form-item>
         <el-form-item v-if="view !== 'gis'" label="设备编码"><el-input v-model="query.equipmentCode" clearable /></el-form-item>
-        <el-form-item v-if="view === 'ledger'" label="报警状态"><el-select v-model="query.alarmStatus" clearable><el-option label="正常" :value="0" /><el-option label="报警" :value="1" /></el-select></el-form-item>
-        <el-form-item v-if="view === 'ledger' || view === 'dispatch' || view === 'abnormal'" label="报警等级"><el-select v-model="query.alertLevel" clearable><el-option v-for="item in levels" :key="item.value" v-bind="item" /></el-select></el-form-item>
+        <el-form-item v-if="view === 'ledger'" label="报警状态"><el-select v-model="query.alarmStatus" clearable class="filter-select" style="width:180px"><el-option label="正常" :value="0" /><el-option label="报警" :value="1" /></el-select></el-form-item>
+        <el-form-item v-if="view === 'ledger' || view === 'dispatch' || view === 'abnormal'" label="报警等级"><el-select v-model="query.alertLevel" clearable class="filter-select" style="width:180px"><el-option v-for="item in levels" :key="item.value" v-bind="item" /></el-select></el-form-item>
         <el-form-item><el-button type="primary" @click="loadData">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
       </el-form>
     </el-card>
 
     <el-card shadow="never" class="water-panel">
-      <div class="card-title"><span><el-icon><Monitor /></el-icon> {{ title }}清单</span><el-button v-if="view !== 'gis'" link type="primary" @click="exportData">导出</el-button></div>
+      <div class="card-title"><span><el-icon><Monitor /></el-icon> {{ title }}清单</span><div class="panel-actions"><el-button v-if="view === 'ledger'" link type="primary" :icon="Upload" @click="openImport">导入</el-button><el-button v-if="view !== 'gis'" link type="primary" :icon="Download" @click="exportData">导出</el-button></div></div>
       <el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" class="load-error"><el-button size="small" type="danger" plain @click="loadData">重试</el-button></el-alert>
       <el-empty v-if="view === 'gis' && !loading && !loadError && !page.records.length" description="暂无设备 GIS 点位数据" />
       <el-table :data="page.records" border stripe v-loading="loading" row-key="equipmentId">
         <el-table-column type="index" label="#" width="55" /><el-table-column prop="equipmentCode" label="设备编码" min-width="130" /><el-table-column prop="equipmentName" label="设备名称" min-width="150" /><el-table-column prop="equipmentTypeName" label="设备类型" min-width="120" /><el-table-column prop="equipmentLocation" label="安装位置" min-width="180" show-overflow-tooltip />
+        <el-table-column v-if="view === 'ledger'" label="设备图片" width="110" align="center"><template #default="{ row }"><el-image v-if="row.equipmentImage" :src="row.equipmentImage" :preview-src-list="[row.equipmentImage]" preview-teleported hide-on-click-modal fit="cover" class="device-image" /><span v-else class="image-placeholder">暂无图片</span></template></el-table-column>
         <el-table-column v-if="view !== 'maintain'" label="设备状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.currentStatus) === 3 ? 'danger' : 'success'">{{ row.currentStatusText }}</el-tag></template></el-table-column>
         <el-table-column v-if="view !== 'maintain'" label="在线状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.onlineStatus) === 1 ? 'success' : 'info'">{{ row.onlineStatusText }}</el-tag></template></el-table-column>
         <el-table-column v-if="view !== 'maintain'" label="报警状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.alarmStatus) === 1 ? 'danger' : 'success'">{{ row.alarmStatusText }}</el-tag></template></el-table-column>
@@ -49,6 +50,11 @@
 
     <el-drawer v-model="detailVisible" class="detail-drawer device-detail-drawer" title="设备详情" direction="rtl" size="680px" append-to-body>
       <div class="detail-summary"><span class="detail-name">{{ detail.equipmentName || '-' }}</span><el-tag :type="Number(detail.currentStatus) === 3 ? 'danger' : 'success'">{{ detail.currentStatusText || '-' }}</el-tag><el-tag :type="Number(detail.alarmStatus) === 1 ? 'danger' : 'success'">{{ detail.alarmStatusText || '-' }}</el-tag><el-tag v-if="detail.workOrderStatusText && detail.workOrderStatusText !== '暂无工单'" :type="workOrderStatusType(detail.workOrderStatusCode)">{{ detail.workOrderStatusText }}</el-tag></div>
+      <div v-if="view === 'ledger'" class="detail-media">
+        <div class="detail-media-label">设备图片</div>
+        <el-image v-if="detail.equipmentImage" :src="detail.equipmentImage" :preview-src-list="[detail.equipmentImage]" preview-teleported hide-on-click-modal fit="cover" class="detail-image" />
+        <div v-else class="detail-image-placeholder">暂无图片</div>
+      </div>
       <el-descriptions :column="2" border>
         <el-descriptions-item v-for="item in detailFields" :key="item.label" :label="item.label">{{ item.value }}</el-descriptions-item>
       </el-descriptions>
@@ -90,17 +96,30 @@
       </div>
       <template #footer><el-button @click="dispatchVisible = false">取消</el-button><el-button type="primary" :loading="dispatching" @click="submitDispatch">确认派单</el-button></template>
     </el-dialog>
+
+    <el-dialog v-model="importVisible" title="供水设备批量导入" width="520px" append-to-body @closed="resetImport">
+      <el-upload ref="importUploadRef" drag :limit="1" accept=".xlsx,.xls" :auto-upload="false" :file-list="importFileList" :on-change="handleImportChange" :on-remove="handleImportRemove" :disabled="importUploading">
+        <el-icon class="el-icon--upload"><Upload /></el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip><div class="el-upload__tip import-tip">仅支持 xls、xlsx;设备类型必须属于供水类型树。</div></template>
+      </el-upload>
+      <el-checkbox v-model="importUpdateSupport" class="import-update">更新已存在的设备编码</el-checkbox>
+      <template #footer>
+        <el-button type="primary" :loading="importUploading" @click="submitImport">开始导入</el-button>
+        <el-button @click="importVisible = false">取消</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
 import { computed, onMounted, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Monitor, Refresh, Warning, Bell, Tools, DataLine } from '@element-plus/icons-vue'
+import { Monitor, Refresh, Warning, Bell, Tools, DataLine, Upload, Download } 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, listAbnormalDevices, listDeviceLedger, listDeviceGisPoints, listMaintainRecords, deleteMaintainRecord } from '@/api/pipeNetwork/waterSupply'
+import { createAbnormalOrder, exportAbnormalDevices, exportDeviceLedger, importDeviceLedger, listAbnormalDevices, listDeviceLedger, listDeviceGisPoints, listMaintainRecords, 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'
@@ -108,8 +127,9 @@ 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 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')
+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) {
   if (view !== 'dispatch' && view !== 'abnormal') return false
   return canDispatchDevice(row)
@@ -179,6 +199,37 @@ async function submitDispatch() {
   }
 }
 async function removeRecord(row) { try { await deleteMaintainRecord(row.maintainId || row.id); ElMessage.success('记录已删除'); await loadData() } catch (error) { ElMessage.error('删除失败') } }
+function openImport() { importVisible.value = true }
+function handleImportChange(file, fileList) {
+  if (!/\.(xlsx|xls)$/iu.test(file?.name || '')) {
+    ElMessage.error('仅支持 xls、xlsx 文件')
+    importFileList.value = []
+    return false
+  }
+  importFileList.value = fileList.slice(-1)
+}
+function handleImportRemove(file, fileList) { importFileList.value = fileList }
+function resetImport() {
+  importUploading.value = false
+  importUpdateSupport.value = false
+  importFileList.value = []
+  importUploadRef.value?.clearFiles()
+}
+async function submitImport() {
+  if (!importFileList.value.length) return ElMessage.warning('请选择 xls 或 xlsx 文件')
+  importUploading.value = true
+  try {
+    const response = await importDeviceLedger(importFileList.value[0].raw, importUpdateSupport.value)
+    if (response?.code && response.code !== 200) throw new Error(response.msg || response.message || '导入失败,请检查文件内容')
+    ElMessage.success(response?.msg || response?.data || '导入成功')
+    importVisible.value = false
+    await loadData()
+  } catch (error) {
+    ElMessage.error(error?.response?.data?.msg || error?.response?.data?.message || error?.message || '导入失败,请稍后重试')
+  } finally {
+    importUploading.value = false
+  }
+}
 async function exportData() {
   try {
     const response = await (view === 'dispatch' || view === 'abnormal' ? exportAbnormalDevices : exportDeviceLedger)({ ...query.value })

+ 16 - 0
tests/waterDeviceExport.test.mjs

@@ -18,6 +18,22 @@ test('water device exports download Excel blobs and surface backend JSON errors'
   assert.match(pageSource, /saveAs\(blob/u)
 })
 
+test('water device ledger import and image display use the public contract', async () => {
+  const apiSource = await readFile('src/api/pipeNetwork/waterSupply.js', 'utf8')
+  const pageSource = await readFile(
+    'src/views/subSystem/waterSupply/components/WaterDevicePage.vue',
+    'utf8'
+  )
+
+  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, /row\.equipmentImage/u)
+  assert.match(pageSource, /detail\.equipmentImage/u)
+})
+
 test('recognizes JSON export errors when the MIME includes a charset', async () => {
   const message = await readMonitoringExportError(
     new Blob(['{"msg":"导出失败"}'], { type: 'application/json;charset=UTF-8' })