Przeglądaj źródła

优化工单,报警查询

zengy 1 tydzień temu
rodzic
commit
e4ceaf70f9
48 zmienionych plików z 417 dodań i 200 usunięć
  1. 1 0
      memory/index.md
  2. 40 0
      memory/sessions/2026-10-10.md
  3. 24 0
      memory/sessions/2026-10-12.md
  4. 16 8
      src/components/ImageUpload/index.vue
  5. 2 0
      src/store/modules/user.js
  6. 1 1
      src/utils/dict.js
  7. 14 0
      src/utils/minioAttachment.js
  8. 1 2
      src/utils/waterAlarmManagementModel.js
  9. 8 15
      src/utils/waterHomeModel.js
  10. 1 8
      src/utils/waterSupplyUiModel.js
  11. 1 1
      src/views/subSystem/alarm/AlarmAttachments.vue
  12. 1 1
      src/views/subSystem/alarm/AlarmChart.vue
  13. 4 4
      src/views/subSystem/alarm/AlarmDetail.vue
  14. 1 1
      src/views/subSystem/alarm/AlarmHistory.vue
  15. 4 2
      src/views/subSystem/alarm/AlarmSummary.vue
  16. 5 3
      src/views/subSystem/alarm/common/audit.vue
  17. 0 0
      src/views/subSystem/alarm/common/js/alarmHistoryModel.js
  18. 0 0
      src/views/subSystem/alarm/common/js/alarmOperationsModel.js
  19. 0 0
      src/views/subSystem/alarm/common/js/alarmReviewModel.js
  20. 0 0
      src/views/subSystem/alarm/common/js/useAlarmScope.js
  21. 8 6
      src/views/subSystem/alarm/common/list.vue
  22. 8 6
      src/views/subSystem/alarm/common/remind.vue
  23. 3 3
      src/views/subSystem/drainage/alarm/alarmGIS.vue
  24. 3 3
      src/views/subSystem/drainage/alarm/analysis.vue
  25. 2 2
      src/views/subSystem/drainage/jcsj/sbyx.vue
  26. 10 2
      src/views/subSystem/gas/gasjcyj/gxylyctx.vue
  27. 13 13
      src/views/subSystem/gas/gasjcyj/krqindbj.vue
  28. 9 10
      src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue
  29. 1 1
      src/views/subSystem/manholeCover/home/index.vue
  30. 10 12
      src/views/subSystem/manholeCover/operation/operationTask.vue
  31. 81 22
      src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue
  32. 7 12
      src/views/subSystem/pipeNetwork/ops/workOrder/index.vue
  33. 15 0
      src/views/subSystem/pipeNetwork/ops/workOrder/js/useWorkOrderLevelDict.js
  34. 14 10
      src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js
  35. 4 6
      src/views/subSystem/pipeNetwork/ops/workOrder/list.vue
  36. 3 3
      src/views/subSystem/waterSupply/alarm/alarmGIS.vue
  37. 8 6
      src/views/subSystem/waterSupply/components/WaterAlarmPage.vue
  38. 1 1
      src/views/subSystem/waterSupply/components/WaterDeviceAbnormalPage.vue
  39. 5 1
      src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue
  40. 7 4
      src/views/subSystem/waterSupply/components/WaterSupplyHome.vue
  41. 5 21
      src/views/subSystem/waterSupply/monitorAlarm/workOrder.vue
  42. 1 1
      tests/alarmListStatistics.test.mjs
  43. 65 0
      tests/minioAttachment.test.mjs
  44. 1 1
      tests/waterDeviceAbnormalBoard.test.mjs
  45. 3 3
      tests/waterHomeModel.test.mjs
  46. 2 2
      tests/waterSupplyAlarmGIS.test.mjs
  47. 2 1
      tests/waterSupplyUiModel.test.mjs
  48. 2 2
      tests/waterSupplyWorkOrderStatusDict.test.mjs

+ 1 - 0
memory/index.md

@@ -66,6 +66,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
+- [2026-10-12:工单派单增加核验人](sessions/2026-10-12.md)
 - [2026-10-10:报警内容固化到 alarm_data](sessions/2026-10-10.md)
 - [2026-09-29:预警流程、督办、附件和清单关联联调](sessions/2026-09-29.md)
 - [2026-08-14:项目探索、启动方式与记忆库建立](sessions/2026-08-14.md)

+ 40 - 0
memory/sessions/2026-10-10.md

@@ -0,0 +1,40 @@
+# 2026-10-10:报警内容固化到 alarm_data
+
+## 目标
+
+- `alarm_data` 新增 `alarm_content`,报警创建时按原前端 `alarmDescription()` 规则生成。
+- 报警列表、审核、提醒、摘要及供排水 GIS 直接显示 `alarm_content`。
+- 删除数据库 `remark` 列。
+
+## 结论与变更
+
+- 管理端与采集端写入统一按 `warning_code` 查询 `warning_type.warning_name`,再按实际值触及上下限拼接内容;指标名称缺失时以报警编码及“监测报警”兜底,不再使用 `AlarmData.warningName`。
+- 新增 `pipe-network-service/sql/migration_alarm_content.sql`,先回填历史内容并设为非空,再删除 `remark`。
+- 两套 `AlarmData` 实体彻底删除 `remark` 和 `warningName` 属性,各采集微服务不再设置该字段;数据库和 API 均以 `alarmContent` 为准。
+- 相关前端页面直接读取 `row.alarmContent`;`alarmDescription()` 仅保留为旧调用兼容工具,不再用于列表展示。
+- 供水报警测试数据脚本同步改为写入并生成 `alarm_content`。
+
+## 2026-10-10 后续变更
+
+- 完全移除两套 `AlarmData.warningName`,写入服务只从指标字典查询名称;采集端和演练入口同步清理赋值调用。
+- `alarmDescription()` 不再读取 `row.warningName`,仅返回后端固化的 `alarmContent`。
+
+## 验证
+
+- 前端 `npm test`:6 项通过。
+- 前端 `npm run build:prod`:通过,仅有既有 Sass 弃用警告。
+- `zk-api-service` 目标测试:17 项通过。
+- `zksy-system` 目标测试:16 项通过。
+- 两个后端模块 Maven compile:通过。
+- 删除 `AlarmData.remark` 后,后端全部 19 个 Maven 模块 compile:通过。
+
+## 部署
+
+- 停止报警写入后执行 `migration_alarm_content.sql`,再同步部署管理端、采集端和前端;迁移脚本未在本地数据库执行。
+
+## 小屏表格适配
+
+- 报警清单与阈值配置表格参照排水视频关联页:表格直接放在普通块级面板中并使用内联 `width: 100%`,由 Element Plus 自己生成横向滚动区和右侧固定操作列。
+- 移除会将表格按内容撑宽后裁切的 `el-card` 表格外壳及额外 `overflow` 容器;面板视觉样式由普通 `section` 保持。
+- 报警审核页同步采用该表格结构;列表“报警描述”仅展示 `alarmContent`,不再追加实际值和阈值快照。
+- `npm run build:prod` 通过,仅有项目既有的 Sass 弃用提示。

+ 24 - 0
memory/sessions/2026-10-12.md

@@ -0,0 +1,24 @@
+# 2026-10-12:工单派单增加核验人
+
+## 目标
+
+- 在统一工单处置页的派单操作中,于责任人下方增加必选核验人,提交字段为 `verifyUser`,默认当前登录账号。
+
+## 结论与变更
+
+- 修改 `src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue`。
+- 派单表单新增核验人显示字段和人员选择入口,复用原有部门/用户选择弹窗。
+- 用户 Store 新增保存 `nickName`;打开派单弹窗时使用当前用户 `id` 初始化核验人,并优先展示当前用户昵称,昵称缺失时才回退登录账号。
+- 提交前校验核验人必选,并在派单请求体中携带 `verifyUser` 用户 ID。
+- 后端 `WorkOrderDispatchDTO` 同步接收并强制校验 `verifyUser`;派单服务校验核验人有效性后写入 `work_order.verify_user`,操作日志同时记录核验人。
+- 后端补充核验人缺失、停用以及更新 SQL 包含 `verify_user` 的单元测试。
+
+## 验证
+
+- `git diff --check` 通过。
+- `npm run build:prod` 通过;仅输出项目既有 Sass 弃用提示。
+- `WorkOrderServiceImplTest` 通过:29 项测试,0 失败、0 错误。
+
+## 风险与后续
+
+- 前后端需同步部署后生效,无数据库结构变更。

+ 16 - 8
src/components/ImageUpload/index.vue

@@ -38,6 +38,7 @@
 
 <script setup>
 import { getToken } from "@/utils/auth";
+import { buildMinioAttachmentUrl, minioAttachmentPath } from "@/utils/minioAttachment";
 
 const props = defineProps({
   modelValue: [String, Object, Array],
@@ -45,7 +46,8 @@ const props = defineProps({
   fileSize: { type: Number, default: 5 },
   fileType: { type: Array, default: () => ["png", "jpg", "jpeg"] },
   isShowTip: { type: Boolean, default: true },
-  uploadUrl: { type: String, default: "" }
+  uploadUrl: { type: String, default: "" },
+  relativePath: { type: Boolean, default: false }
 });
 
 const { proxy } = getCurrentInstance();
@@ -68,11 +70,12 @@ watch(() => props.modelValue, val => {
         ? val.split(",")
         : [val];
     fileList.value = list.map(item => {
-      const source = typeof item === "string" ? item : (item.url || item.fileName || item.name);
-      const url = normalizeImageUrl(source);
+      const source = typeof item === "string" ? item : (item.storagePath || item.url || item.fileName || item.name);
+      const storagePath = props.relativePath ? minioAttachmentPath(source) : source;
+      const url = normalizeImageUrl(storagePath);
       return typeof item === "object"
-        ? { ...item, name: item.name || source || url, url, status: item.status || "success" }
-        : { name: source || url, url, status: "success" };
+        ? { ...item, storagePath, name: item.name || source || url, url, status: item.status || "success" }
+        : { storagePath, name: source || url, url, status: "success" };
     }).filter(item => item.url);
   } else {
     fileList.value = [];
@@ -80,6 +83,7 @@ watch(() => props.modelValue, val => {
 }, { deep: true, immediate: true });
 
 function normalizeImageUrl(value) {
+  if (props.relativePath) return buildMinioAttachmentUrl(value);
   const url = String(value || "").trim();
   if (!url || /^(https?:|blob:|data:)/.test(url)) return url;
   if (baseUrl && (url === baseUrl || url.startsWith(baseUrl + "/"))) return url;
@@ -115,10 +119,13 @@ function handleExceed() {
 
 function handleUploadSuccess(res, file) {
   if (res.code === 200) {
-    const storedUrl = uploadImgUrl.value.includes("/minio/")
-      ? (res.url || res.fileName)
-      : (res.fileName || res.url);
+    const storedUrl = props.relativePath
+      ? minioAttachmentPath(res.relativePath || res.url)
+      : uploadImgUrl.value.includes("/minio/")
+        ? (res.url || res.fileName)
+        : (res.fileName || res.url);
     uploadList.value.push({
+      storagePath: storedUrl,
       name: res.originalFilename || storedUrl,
       url: normalizeImageUrl(storedUrl),
       status: "success",
@@ -169,6 +176,7 @@ function listToString(list, separator = ",") {
   return list
     .filter(item => item.url && !item.url.startsWith("blob:"))
     .map(item => {
+      if (props.relativePath) return minioAttachmentPath(item.storagePath || item.url);
       const url = item.url;
       return baseUrl && url.startsWith(baseUrl) ? url.slice(baseUrl.length) : url;
     })

+ 2 - 0
src/store/modules/user.js

@@ -9,6 +9,7 @@ const useUserStore = defineStore(
       token: getToken(),
       id: '',
       name: '',
+      nickName: '',
       avatar: '',
       roles: [],
       permissions: [],
@@ -46,6 +47,7 @@ const useUserStore = defineStore(
             }
             this.id = user.userId
             this.name = user.userName
+            this.nickName = user.nickName
             this.avatar = avatar
             this.deptId = user.deptId
             resolve(res)

+ 1 - 1
src/utils/dict.js

@@ -14,7 +14,7 @@ export function useDict(...args) {
         res.value[dictType] = dicts;
       } else {
         getDicts(dictType).then(resp => {
-          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
+          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass, listClass: p.listClass, cssClass: p.cssClass, isDefault: p.isDefault === true || p.isDefault === 1 || p.isDefault === '1' || p.isDefault === 'Y' }))
           useDictStore().setDict(dictType, res.value[dictType]);
         })
       }

+ 14 - 0
src/utils/minioAttachment.js

@@ -0,0 +1,14 @@
+export function buildMinioAttachmentUrl(value, baseUrl = import.meta.env.VITE_APP_MINIO_BASE_URL) {
+  const path = String(value || '').trim()
+  if (!path || /^(https?:|blob:|data:)/i.test(path)) return path
+  return `${String(baseUrl || '').replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`
+}
+
+export function minioAttachmentPath(value, baseUrl = import.meta.env.VITE_APP_MINIO_BASE_URL) {
+  const path = String(value || '').trim()
+  if (!path) return ''
+  const base = String(baseUrl || '').replace(/\/+$/, '')
+  if (base && path.startsWith(`${base}/`)) return `/${path.slice(base.length).replace(/^\/+/, '')}`
+  if (/^[a-z][a-z\d+.-]*:/i.test(path)) return path
+  return `/${path.replace(/^\/+/, '')}`
+}

+ 1 - 2
src/utils/waterAlarmManagementModel.js

@@ -3,7 +3,6 @@ import { normalizeAlarmTrend } from './waterHomeModel.js'
 export const WATER_ALARM_ROOT_TYPE_ID = 1
 
 const displayStatusLabels = { 1: '待审核', 2: '已转工单', 3: '已解除', 4: '误报警' }
-const alarmLevelLabels = { 1: '一级报警', 2: '二级报警', 3: '三级报警', 4: '四级报警' }
 const columns = [
   ['报警单号', 'alarmNo'], ['设备编码', 'deviceCode'], ['设备名称', 'deviceName'],
   ['报警类型', 'warningType'], ['报警等级', 'alarmLevelName'], ['报警时间', 'createTime'],
@@ -38,7 +37,7 @@ export function normalizeWaterAlarmRow(row = {}) {
   const orderStatus = row.orderStatusName ?? (row.orderStatus == null ? '未转工单' : `状态${row.orderStatus}`)
   return {
     ...row,
-    alarmLevelName: row.alarmLevelName || alarmLevelLabels[level] || '未分级',
+    alarmLevelName: row.alarmLevelName || (level ? String(level) : '未分级'),
     monitorStatusText: row.monitorStatusName || '未获取',
     deviceAlarmStatusText: row.deviceAlarmStatusText || (row.deviceAlarmStatus == null ? '未获取' : Number(row.deviceAlarmStatus) === 1 ? '报警中' : '正常'),
     displayStatusText: row.displayStatusText || displayStatusLabels[displayStatus] || '未知',

+ 8 - 15
src/utils/waterHomeModel.js

@@ -21,16 +21,9 @@ const WORK_ORDER_STATUS_META = {
 }
 
 const WORK_ORDER_LEVEL_META = {
-  0: { text: '紧急', type: 'danger' },
-  1: { text: '一般', type: 'warning' },
-  2: { text: '低', type: 'info' }
-}
-
-const ALARM_LEVEL_META = {
-  1: { text: '一级报警', type: 'danger' },
-  2: { text: '二级报警', type: 'warning' },
-  3: { text: '三级报警', type: 'primary' },
-  4: { text: '四级报警', type: 'success' }
+  0: { type: 'danger' },
+  1: { type: 'warning' },
+  2: { type: 'info' }
 }
 
 /**
@@ -144,7 +137,8 @@ export function normalizeAlarmTrend(trend = []) {
   const dates = items.map(item => item?.date || '-')
   const levels = [1, 2, 3, 4].map(level => ({
     name: `级${level}`,
-    text: (ALARM_LEVEL_META[level] || {}).text || `${level}级报警`,
+    level,
+    text: String(level),
     data: items.map(item => Number(item[`level${level}Count`] ?? 0))
   }))
   return { dates, levels }
@@ -164,8 +158,7 @@ export function normalizePieRatio(items = [], fallbackName = '未分类', top =
 
 export function alarmLevelText(level) {
   const numeric = Number(level)
-  const meta = ALARM_LEVEL_META[numeric]
-  return meta?.text || (Number.isFinite(numeric) && numeric > 0 ? `${numeric}级报警` : '未分级')
+  return Number.isFinite(numeric) && numeric > 0 ? String(numeric) : '未分级'
 }
 
 const ALARM_DISPLAY_STATUS_META = {
@@ -201,7 +194,7 @@ export function normalizeRealtimeAlarmRows(rows = [], limit = 5) {
         deviceName: row?.deviceName || row?.deviceCode || '监测设备',
         warningType: row?.warningType || '监测报警',
         alarmLevel: Number(row?.alarmLevel ?? 0),
-        alarmLevelText: row?.alarmLevelText || alarmLevelText(row?.alarmLevel),
+        alarmLevelText: row?.alarmLevelName || row?.alarmLevelText || alarmLevelText(row?.alarmLevel),
         alarmTime: row?.alarmTime || '-',
         location: row?.equipmentLocation || '-',
         displayStatus,
@@ -287,7 +280,7 @@ export function normalizeWorkOrderRows(rows = [], limit = 5) {
         orderStatusText: statusMeta.text,
         orderStatusType: statusMeta.type,
         orderLevel,
-        orderLevelText: levelMeta.text,
+        orderLevelText: row?.orderLevelName || (orderLevel >= 0 ? String(orderLevel) : '-'),
         orderLevelType: levelMeta.type,
         deviceCode: row?.deviceCode || '-',
         deviceName: row?.deviceName || '-',

+ 1 - 8
src/utils/waterSupplyUiModel.js

@@ -1,10 +1,3 @@
-const alarmLevelLabels = {
-  1: '一级报警',
-  2: '二级报警',
-  3: '三级报警',
-  4: '四级报警'
-}
-
 // 工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消
 const workOrderStatusLabels = {
   0: '待派单',
@@ -70,7 +63,7 @@ export function normalizeDeviceRow(row = {}) {
     alarmStatusText,
     abnormalReasonText: row.abnormalReason
       || (alarmStatus === 1 ? '设备报警' : currentStatus === 3 ? '设备维修中' : '-'),
-    alarmLevelText: alarmLevelLabels[alarmLevel] ?? (alarmLevel ? `${alarmLevel}级报警` : '未分级'),
+    alarmLevelText: row.alarmLevelName || row.alertLevelName || (alarmLevel ? String(alarmLevel) : '未分级'),
     relatedOrderNo,
     relatedWorkOrderPerson: row.relatedWorkOrderPerson ?? '-',
     workOrderStatusCode: workOrderStatus.code,

+ 1 - 1
src/views/subSystem/alarm/AlarmAttachments.vue

@@ -23,7 +23,7 @@
 
 <script setup>
 import { computed } from 'vue'
-import { imageAttachments } from './common/utils/alarmReviewModel'
+import { imageAttachments } from './common/js/alarmReviewModel'
 
 const props = defineProps({
   value: { type: String, default: '' },

+ 1 - 1
src/views/subSystem/alarm/AlarmChart.vue

@@ -12,7 +12,7 @@
 import { ref, computed, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
 import * as echarts from 'echarts'
 import { getAlarmAnalysis } from '@/api/alarm'
-import { seriesPoints, sampleTime } from './common/utils/alarmOperationsModel'
+import { seriesPoints, sampleTime } from './common/js/alarmOperationsModel'
 const props = defineProps({ alarmId: String, scope: { type: Object, required: true } })
 const period = ref('CURVE'), host = ref(), loading = ref(false), error = ref(''), result = ref({})
 const validCount = computed(() => (result.value.samples || []).reduce((sum, p) => sum + Number(p.validCount), 0))

+ 4 - 4
src/views/subSystem/alarm/AlarmDetail.vue

@@ -23,10 +23,10 @@ import { ElMessage } from 'element-plus'
 import { getAlarm, getAlarmTimeline } from '@/api/alarm'
 import AlarmSummary from './AlarmSummary.vue'
 import AlarmHistory from './AlarmHistory.vue'
-import { auditStatusTone, levelTone, monitorStatusTone } from './common/utils/alarmOperationsModel'
-import { orderStatusType } from '../pipeNetwork/ops/workOrder/workOrderDict'
-import { hasActualValue } from './common/utils/alarmReviewModel'
-import { useAlarmScope } from './common/utils/useAlarmScope'
+import { auditStatusTone, levelTone, monitorStatusTone } from './common/js/alarmOperationsModel'
+import { orderStatusType } from '../pipeNetwork/ops/workOrder/js/workOrderDict'
+import { hasActualValue } from './common/js/alarmReviewModel'
+import { useAlarmScope } from './common/js/useAlarmScope'
 const { scope, scopeKey, menu, navigate } = useAlarmScope()
 const { waterShortcuts = false } = defineProps({ waterShortcuts: Boolean })
 const router = useRouter()

+ 1 - 1
src/views/subSystem/alarm/AlarmHistory.vue

@@ -20,7 +20,7 @@
 <script setup>
 import { computed } from 'vue'
 import AlarmAttachments from './AlarmAttachments.vue'
-import { groupAlarmHistory, historyDayLabel, historyTime } from './common/utils/alarmHistoryModel'
+import { groupAlarmHistory, historyDayLabel, historyTime } from './common/js/alarmHistoryModel'
 
 const props = defineProps({
   records: { type: Array, default: () => [] },

+ 4 - 2
src/views/subSystem/alarm/AlarmSummary.vue

@@ -1,6 +1,6 @@
 <template>
   <section class="alarm-summary">
-    <div class="summary-title"><div><strong>{{ alarm.deviceName || '未命名设备' }}</strong><p>{{ alarm.deviceCode || '—' }}</p></div><el-tag :type="levelTone">{{ alarm.alarmLevelName || '未知等级' }}</el-tag></div>
+    <div class="summary-title"><div><strong>{{ alarm.deviceName || '未命名设备' }}</strong><p>{{ alarm.deviceCode || '—' }}</p></div><el-tag :type="levelTone">{{ alarmLevels.find(item => Number(item.value) === Number(alarm.alarmLevel))?.label || alarm.alarmLevelName || '未知等级' }}</el-tag></div>
     <div class="summary-body">
       <p>报警单号:{{ alarm.alarmNo || '—' }}</p>
       <div>{{ alarm.alarmContent || '—' }}</div>
@@ -11,7 +11,9 @@
   </section>
 </template>
 <script setup>
-import { thresholdText, valueText, hasActualValue } from './common/utils/alarmReviewModel'
+import { thresholdText, valueText, hasActualValue } from './common/js/alarmReviewModel'
+import { useDict } from '@/utils/dict'
+const { alarm_level: alarmLevels } = useDict('alarm_level')
 defineProps({ alarm: { type: Object, required: true }, levelTone: { type: String, default: 'info' } })
 </script>
 <style scoped>

+ 5 - 3
src/views/subSystem/alarm/common/audit.vue

@@ -3,7 +3,7 @@
     <el-card class="search-card" shadow="never">
       <el-form :inline="true" :model="filters" class="search-form" @submit.prevent="search">
         <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码 / 名称" @keyup.enter="search" /></el-form-item>
-        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in options.alarmLevels" :key="item.key" :label="item.label" :value="item.value" /></el-select></el-form-item>
+        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in alarmLevels" :key="item.value" :label="item.label" :value="Number(item.value)" /></el-select></el-form-item>
         <el-form-item label="审核结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.label" :value="item.value" /></el-select></el-form-item>
         <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
         <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
@@ -22,7 +22,7 @@
         <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
         <el-table-column prop="equipmentLocation" label="安装位置" min-width="210" show-overflow-tooltip><template #default="{ row }">{{ row.equipmentLocation || '—' }}</template></el-table-column>
         <el-table-column prop="alarmContent" label="报警描述" min-width="275"><template #default="{ row }">{{ row.alarmContent || '—' }}</template></el-table-column>
-        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)" effect="light">{{ row.alarmLevelName }}</el-tag></template></el-table-column>
+        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)" effect="light">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
         <el-table-column prop="createTime" label="报警时间" width="175" sortable="custom" />
         <el-table-column label="审核结果" width="110" align="center"><template #default="{ row }"><el-tag :type="auditTone(row.auditStatus)" effect="light">{{ row.auditStatusName }}</el-tag></template></el-table-column>
         <el-table-column label="操作" width="160" align="center" fixed="right"><template #default="{ row }"><el-button v-if="isUnreviewed(row)" v-hasPermi="['alarm:review:audit']" link type="primary" @click="openReview(row)">审核</el-button><el-button link type="primary" @click="openHistory(row)">处置记录</el-button></template></el-table-column>
@@ -63,6 +63,8 @@ import { Refresh, Search, Upload } from '@element-plus/icons-vue'
 import { getAlarmReviewOptions, getAlarmReviewPage, getAlarmReviewStatistics, getAlarmReviewDetail, getAlarmLogs, submitAlarmReview, uploadAlarmReviewAttachment } from '@/api/alarmReview'
 import AlarmSummary from '../AlarmSummary.vue'
 import AlarmHistory from '../AlarmHistory.vue'
+import { useDict } from '@/utils/dict'
+const { alarm_level: alarmLevels } = useDict('alarm_level')
 
 const route = useRoute()
 const baseUrl = import.meta.env.VITE_APP_BASE_API
@@ -87,7 +89,7 @@ const rules = {
 }
 const message = e => e?.response?.data?.msg || e?.message || '请求失败,请重试'
 const isUnreviewed = row => unreviewed.value !== undefined && row.auditStatus === unreviewed.value
-const levelTone = value => ({ LEVEL_ONE: 'danger', LEVEL_TWO: 'warning', LEVEL_THREE: 'primary', LEVEL_FOUR: 'success' })[options.alarmLevels.find(item => item.value === value)?.key] || 'info'
+const levelTone = value => ({ 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' })[Number(value)] || 'info'
 const auditTone = value => ({ UNREVIEWED: 'warning', CONFIRMED: 'primary', FALSE_ALARM: 'info' })[options.auditStatuses.find(item => item.value === value)?.key] || 'info'
 
 function params() {

+ 0 - 0
src/views/subSystem/alarm/common/utils/alarmHistoryModel.js → src/views/subSystem/alarm/common/js/alarmHistoryModel.js


+ 0 - 0
src/views/subSystem/alarm/common/utils/alarmOperationsModel.js → src/views/subSystem/alarm/common/js/alarmOperationsModel.js


+ 0 - 0
src/views/subSystem/alarm/common/utils/alarmReviewModel.js → src/views/subSystem/alarm/common/js/alarmReviewModel.js


+ 0 - 0
src/views/subSystem/alarm/common/utils/useAlarmScope.js → src/views/subSystem/alarm/common/js/useAlarmScope.js


+ 8 - 6
src/views/subSystem/alarm/common/list.vue

@@ -14,7 +14,7 @@
     <el-card shadow="never" class="alarm-filter-card"><el-form :model="filters" inline class="alarm-filters" @submit.prevent="search">
       <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" @keyup.enter="search" /></el-form-item>
       <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" @keyup.enter="search" /></el-form-item>
-      <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="v in options.alarmLevels" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
+      <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="Number(v.value)" /></el-select></el-form-item>
       <el-form-item label="监测状态"><el-select v-model="filters.alarmStatus" clearable placeholder="全部状态"><el-option v-for="v in options.monitorStatuses" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
       <el-form-item label="报警时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
       <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
@@ -27,7 +27,7 @@
         <el-table-column prop="deviceName" label="设备名称" min-width="155" show-overflow-tooltip />
         <el-table-column prop="equipmentLocation" label="安装位置" min-width="200" show-overflow-tooltip />
         <el-table-column prop="alarmContent" label="报警描述" min-width="270"><template #default="{ row }">{{ row.alarmContent }}</template></el-table-column>
-        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)">{{ row.alarmLevelName }}</el-tag></template></el-table-column>
+        <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
         <el-table-column prop="createTime" label="报警时间" width="180" sortable="custom" />
         <el-table-column label="监测状态" width="100" align="center"><template #default="{ row }"><el-tag :type="row.alarmStatus === 0 ? 'danger' : 'success'">{{ row.monitorStatusName }}</el-tag></template></el-table-column>
         <el-table-column prop="auditStatusName" label="审核结果" width="105" align="center">
@@ -48,12 +48,14 @@
 import { ref, reactive, watch, onBeforeUnmount } from 'vue'
 import { Search, Refresh, Document, Warning, CircleCheck } from '@element-plus/icons-vue'
 import { getAlarms, getAlarmOptions, getAlarmStatistics } from '@/api/alarm'
-import { hasActualValue, valueText, thresholdText } from './utils/alarmReviewModel'
-import { auditStatusTone, levelTone, rangeParams } from './utils/alarmOperationsModel'
-import { orderStatusType } from '../../pipeNetwork/ops/workOrder/workOrderDict'
-import { useAlarmScope } from './utils/useAlarmScope'
+import { hasActualValue, valueText, thresholdText } from './js/alarmReviewModel'
+import { auditStatusTone, levelTone, rangeParams } from './js/alarmOperationsModel'
+import { orderStatusType } from '../../pipeNetwork/ops/workOrder/js/workOrderDict'
+import { useAlarmScope } from './js/useAlarmScope'
 import AlarmDetail from '../AlarmDetail.vue'
 import './styles/alarmOperations.scss'
+import { useDict } from '@/utils/dict'
+const { alarm_level: alarmLevels } = useDict('alarm_level')
 const { scope, scopeKey, route } = useAlarmScope()
 const empty = () => ({ alarmNo: '', keyword: '', alarmLevel: undefined, alarmStatus: undefined })
 const filters = reactive(empty()), dates = ref([]), options = ref({}), rows = ref([]), total = ref(0)

+ 8 - 6
src/views/subSystem/alarm/common/remind.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="alarm-page">
-    <div class="alarm-stats"><div v-for="v in options.alarmLevels || []" :key="v.value" class="alarm-stat" :style="{ '--level-color': levelColor(v.value) }"><span>{{ v.label }}</span><strong>{{ count(v.value) }}</strong><span class="muted">当前未恢复报警事件</span></div></div>
+    <div class="alarm-stats"><div v-for="v in alarmLevels" :key="v.value" class="alarm-stat" :style="{ '--level-color': levelColor(v.value) }"><span>{{ v.label }}</span><strong>{{ count(v.value) }}</strong><span class="muted">当前未恢复报警事件</span></div></div>
     <el-card shadow="never">
       <div class="alarm-toolbar"><div><el-tag :type="refreshError ? 'danger' : 'success'">{{ refreshError ? '刷新失败' : '定时刷新已开启' }}</el-tag><span class="muted"> · 每 30 秒刷新 · {{ refreshedAt || '正在加载' }}</span></div><el-button :icon="manualPause ? VideoPlay : VideoPause" @click="manualPause = !manualPause">{{ manualPause ? '恢复滚动' : '暂停滚动' }}</el-button></div>
-      <div class="alarm-toolbar"><el-form inline class="alarm-filters"><el-form-item label="报警等级"><el-select v-model="level" clearable placeholder="全部等级" @change="restart"><el-option v-for="v in options.alarmLevels || []" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item></el-form><span class="muted">{{ pending ? `有 ${pending} 条更新,恢复后显示` : `最近更新的 ${visibleRows.length} 条事件` }}</span></div>
+      <div class="alarm-toolbar"><el-form inline class="alarm-filters"><el-form-item label="报警等级"><el-select v-model="level" clearable placeholder="全部等级" @change="restart"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="Number(v.value)" /></el-select></el-form-item></el-form><span class="muted">{{ pending ? `有 ${pending} 条更新,恢复后显示` : `最近更新的 ${visibleRows.length} 条事件` }}</span></div>
       <el-alert v-if="refreshError" :title="refreshError" show-icon type="error" :closable="false"><el-button link @click="retry">重新加载</el-button></el-alert>
       <div ref="feed" v-loading="loading && !hasLoaded" class="reminder-feed" tabindex="0" @mouseenter="hoverPause = true" @mouseleave="hoverPause = false" @focusin="focusPause = true" @focusout="focusPause = false">
         <article v-for="row in visibleRows" :key="row.id" class="reminder-message" :style="{ borderLeftColor: levelColor(row.alarmLevel) }">
-          <div class="alarm-toolbar"><strong>{{ row.deviceCode }} ({{ row.deviceName || row.deviceCode }}) <el-tag :type="levelTone(row.alarmLevel)">{{ row.alarmLevelName }}</el-tag></strong><span class="muted">{{ row.latestOperationTime || row.createTime }}</span></div>
+          <div class="alarm-toolbar"><strong>{{ row.deviceCode }} ({{ row.deviceName || row.deviceCode }}) <el-tag :type="levelTone(row.alarmLevel)">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></strong><span class="muted">{{ row.latestOperationTime || row.createTime }}</span></div>
           <div class="muted">报警单号:{{ row.alarmNo || '—' }} · {{ row.equipmentLocation || '暂无安装位置' }}</div>
           <div class="alarm-toolbar"><span class="muted">监测:{{ row.monitorStatusName }} · 审核:{{ row.auditStatusName }} · 处置:{{ row.orderStatusName }}</span><el-button link type="primary" @click="detailRef.open(row.id)">事件详情</el-button></div>
           <p>{{ row.latestOperation || row.alarmContent || '—' }}</p>
@@ -22,10 +22,12 @@
 import { computed, ref, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
 import { VideoPlay, VideoPause } from '@element-plus/icons-vue'
 import { getAlarmReminders, getAlarmOptions, getAlarmStatistics } from '@/api/alarm'
-import { levelTone, levelColor } from './utils/alarmOperationsModel'
-import { useAlarmScope } from './utils/useAlarmScope'
+import { levelTone, levelColor } from './js/alarmOperationsModel'
+import { useAlarmScope } from './js/useAlarmScope'
 import AlarmDetail from '../AlarmDetail.vue'
 import './styles/alarmOperations.scss'
+import { useDict } from '@/utils/dict'
+const { alarm_level: alarmLevels } = useDict('alarm_level')
 const { scope, scopeKey } = useAlarmScope()
 const options = ref({}), stats = ref([]), level = ref(), visibleRows = ref([]), received = ref([])
 const manualPause = ref(false), hoverPause = ref(false), focusPause = ref(false), feed = ref(), detailRef = ref()
@@ -35,7 +37,7 @@ const refreshError = computed(() => error.value || statisticsError.value || opti
 const detailPause = ref(false)
 const paused = computed(() => manualPause.value || hoverPause.value || focusPause.value || detailPause.value)
 const pending = computed(() => received.value.filter(row => !visibleRows.value.some(old => old.id === row.id && old.latestOperationTime === row.latestOperationTime)).length)
-const count = value => Number(stats.value.find(row => Number(row.level) === value)?.total || 0)
+const count = value => Number(stats.value.find(row => Number(row.level) === Number(value))?.total || 0)
 let generation = 0, poll, scroll
 async function load() {
   if (!active.value || loading.value) return

+ 3 - 3
src/views/subSystem/drainage/alarm/alarmGIS.vue

@@ -110,9 +110,9 @@ import { getAlarmMap, getAlarmOptions, getAlarms } from '@/api/alarm'
 import AlarmDetail from '../../alarm/AlarmDetail.vue'
 import AlarmChart from '../../alarm/AlarmChart.vue'
 import AlarmAttachments from '../../alarm/AlarmAttachments.vue'
-import { alarmCurveChoices, groupAlarmDevices, levelColor, levelTone, monitorStatusTone } from '../../alarm/common/utils/alarmOperationsModel'
-import { hasActualValue, thresholdText, valueText } from '../../alarm/common/utils/alarmReviewModel'
-import { useAlarmScope } from '../../alarm/common/utils/useAlarmScope'
+import { alarmCurveChoices, groupAlarmDevices, levelColor, levelTone, monitorStatusTone } from '../../alarm/common/js/alarmOperationsModel'
+import { hasActualValue, thresholdText, valueText } from '../../alarm/common/js/alarmReviewModel'
+import { useAlarmScope } from '../../alarm/common/js/useAlarmScope'
 import '../../alarm/common/styles/alarmOperations.scss'
 
 const { scope, scopeKey } = useAlarmScope()

+ 3 - 3
src/views/subSystem/drainage/alarm/analysis.vue

@@ -15,9 +15,9 @@ import { getAlarms, getAlarm } from '@/api/alarm'
 import AlarmSummary from '../../alarm/AlarmSummary.vue'
 import AlarmChart from '../../alarm/AlarmChart.vue'
 import AlarmDetail from '../../alarm/AlarmDetail.vue'
-import { alarmCurveChoices, levelTone } from '../../alarm/common/utils/alarmOperationsModel'
-import { hasActualValue } from '../../alarm/common/utils/alarmReviewModel'
-import { useAlarmScope } from '../../alarm/common/utils/useAlarmScope'
+import { alarmCurveChoices, levelTone } from '../../alarm/common/js/alarmOperationsModel'
+import { hasActualValue } from '../../alarm/common/js/alarmReviewModel'
+import { useAlarmScope } from '../../alarm/common/js/useAlarmScope'
 import '../../alarm/common/styles/alarmOperations.scss'
 const { scope, scopeKey, route } = useAlarmScope()
 const choices = ref([]), selectedId = ref(''), alarm = ref(null), loading = ref(false), searching = ref(false), error = ref(''), detailRef = ref()

+ 2 - 2
src/views/subSystem/drainage/jcsj/sbyx.vue

@@ -589,8 +589,8 @@ const repairForm = ref({
 const repairDeviceRows = ref([])
 // 前端紧急程度值 -> 后端工单优先级值
 // 前端 urgency: 1-一般 2-紧急 3-非常紧急
-// 后端 orderLevel: 1-紧急 2-一般 3-低
-const urgencyToOrderLevel = { '1': 2, '2': 1, '3': 1 }
+// work_order_level: 0-紧急 1-一般 2-低
+const urgencyToOrderLevel = { '1': 1, '2': 0, '3': 0 }
 
 const repairRules = {
   faultDesc: [{ required: true, message: '请输入故障描述', trigger: 'blur' }],

+ 10 - 2
src/views/subSystem/gas/gasjcyj/gxylyctx.vue

@@ -66,7 +66,7 @@
               @click="viewAlertDetail(alert)"
           >
             <div class="alert-level">
-              <el-tag type="danger" size="small">严重</el-tag>
+              <dict-tag :options="alarm_level" :value="alert.alarmLevel" />
             </div>
             <div class="alert-content">
               <div class="alert-title">{{ alert.pointName }}</div>
@@ -160,6 +160,9 @@
           <el-descriptions-item label="点位编号">{{ currentAlert.pointCode }}</el-descriptions-item>
           <el-descriptions-item label="详细地址">{{ currentAlert.address }}</el-descriptions-item>
           <el-descriptions-item label="所属管线">{{ currentAlert.pipelineName }}</el-descriptions-item>
+          <el-descriptions-item label="报警级别">
+            <dict-tag :options="alarm_level" :value="currentAlert.alarmLevel" />
+          </el-descriptions-item>
           <el-descriptions-item label="异常压力">{{ currentAlert.pressure }} kPa</el-descriptions-item>
           <el-descriptions-item label="实时流量">{{ currentAlert.flow }} m³/h</el-descriptions-item>
           <el-descriptions-item label="报警阈值">{{ currentAlert.threshold }} kPa</el-descriptions-item>
@@ -196,10 +199,13 @@
 <script setup>
 import { ref, computed, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search } from '@element-plus/icons-vue'
+import { useDict } from '@/utils/dict'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 import { getPressureAlarmEquipment, getPressureAlarmHistory, getPressureAlarmStats, resolvePressureAlarm, dispatchPressureAlarm } from '@/api/pipeNetwork/basic'
 
+const { alarm_level } = useDict('alarm_level')
+
 // 从 API 加载
 const equipments = ref([])
 const selectedEquipment = ref(null)
@@ -220,7 +226,8 @@ async function loadEquipment() {
       pointCode: e.equipmentCode, pointName: e.equipmentName, address: e.location,
       pressure: e.latestPressure, threshold: e.latestThreshold || 250,
       alertTime: e.latestCreateTime,
-      level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
+      alarmLevel: e.latestAlarmLevel,
+      level: e.hasAlarm ? (String(e.latestAlarmLevel) === '1' ? 'critical' : 'warning') : 'normal',
       status: e.hasAlarm ? 'pending' : 'normal',
       hasWorkOrder: Boolean(e.latestHasWorkOrder),
       workOrderId: e.latestWorkOrderId || null,
@@ -236,6 +243,7 @@ async function loadHistory() {
     const res = await getPressureAlarmHistory(historyPage.value, historyPageSize, d?.[0] || '', d?.[1] || '', currentDeviceCode.value || '')
     historyAlerts.value = (res.data?.records || []).map(a => ({
       id: a.id, pointCode: a.deviceCode, pointName: a.pointName, address: a.address,
+      alarmLevel: a.alarmLevel,
        pressure: a.pressure, threshold: a.threshold || 250, alertTime: a.createTime, status: a.alarmStatus === 0 ? 'pending' : 'resolved',
        remark: a.remark, flow: 0,
        hasWorkOrder: Boolean(a.hasWorkOrder), workOrderId: a.workOrderId || null,

+ 13 - 13
src/views/subSystem/gas/gasjcyj/krqindbj.vue

@@ -70,9 +70,8 @@
               @click="selectEquipment(eq)"
           >
             <div class="alert-level">
-              <el-tag :type="eq.level === 'critical' ? 'danger' : eq.level === 'warning' ? 'warning' : 'info'" size="small">
-                {{ eq.level === 'critical' ? '严重' : eq.level === 'warning' ? '预警' : '正常' }}
-              </el-tag>
+              <dict-tag v-if="eq.hasAlarm" :options="alarm_level" :value="eq.latestAlarmLevel" />
+              <el-tag v-else type="info" size="small">正常</el-tag>
             </div>
             <div class="alert-content">
               <div class="alert-title">{{ eq.equipmentName }}</div>
@@ -120,11 +119,9 @@
               </template>
             </el-table-column>
             <el-table-column prop="threshold" label="阈值(ppm)" width="90" />
-            <el-table-column prop="level" label="级别" width="80" align="center">
+            <el-table-column prop="alarmLevel" label="级别" width="80" align="center">
               <template #default="{ row }">
-                <el-tag :type="row.level === 'critical' ? 'danger' : 'warning'" size="small">
-                  {{ row.level === 'critical' ? '严重' : '预警' }}
-                </el-tag>
+                <dict-tag :options="alarm_level" :value="row.alarmLevel" />
               </template>
             </el-table-column>
             <el-table-column prop="alertTime" label="报警时间" width="150" sortable />
@@ -164,9 +161,7 @@
           <el-descriptions-item label="燃气公司">{{ currentAlert.companyName }}</el-descriptions-item>
           <el-descriptions-item label="详细地址">{{ currentAlert.address }}</el-descriptions-item>
           <el-descriptions-item label="报警级别">
-            <el-tag :type="currentAlert.level === 'critical' ? 'danger' : 'warning'">
-              {{ currentAlert.level === 'critical' ? '严重报警' : '预警' }}
-            </el-tag>
+            <dict-tag :options="alarm_level" :value="currentAlert.alarmLevel" />
           </el-descriptions-item>
           <el-descriptions-item label="甲烷浓度">
             <span :class="{ 'critical-value': currentAlert.concentration > 30 }">{{ currentAlert.concentration }} ppm</span>
@@ -205,10 +200,13 @@
 <script setup>
 import { ref, computed, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Document, Location, Search, OfficeBuilding } from '@element-plus/icons-vue'
+import { useDict } from '@/utils/dict'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
 import { getGasAlarmEquipment, getGasAlarmHistory, getGasAlarmStats, resolveGasAlarm, dispatchGasAlarm } from '@/api/pipeNetwork/basic'
 
+const { alarm_level } = useDict('alarm_level')
+
 // 左侧: 所有探测器设备(去重)
 const equipments = ref([])
 const selectedEquipment = ref(null)
@@ -231,7 +229,8 @@ async function loadEquipment() {
       concentration: e.latestConcentration,
       threshold: e.latestThreshold || 20,
       alertTime: e.latestCreateTime,
-      level: e.hasAlarm ? (e.latestAlarmLevel === 1 ? 'critical' : 'warning') : 'normal',
+      alarmLevel: e.latestAlarmLevel,
+      level: e.hasAlarm ? (String(e.latestAlarmLevel) === '1' ? 'critical' : 'warning') : 'normal',
       status: e.hasAlarm ? 'pending' : 'normal',
       hasWorkOrder: Boolean(e.latestHasWorkOrder),
       workOrderId: e.latestWorkOrderId || null,
@@ -256,8 +255,8 @@ function selectEquipment(eq) {
   loadHistory()
 }
 function mapAlarm(a) {
-  const lv = (a.alarmLevel === 1 || a.alarmLevelText === '严重') ? 'critical' : 'warning'
-  const status = (a.alarmStatus === 0 || a.statusText === '待处理') ? 'pending' : 'resolved'
+  const lv = String(a.alarmLevel) === '1' ? 'critical' : 'warning'
+  const status = String(a.alarmStatus) === '0' ? 'pending' : 'resolved'
   return {
     id: a.id, deviceCode: a.deviceCode,
     stationName: a.stationName || a.deviceCode,
@@ -265,6 +264,7 @@ function mapAlarm(a) {
     address: a.address || '',
     concentration: a.concentration || a.actualValue || 0,
     threshold: a.threshold || a.maxValue || 20,
+    alarmLevel: a.alarmLevel,
     level: lv, alertTime: a.createTime || '', status,
        remark: a.remark || null,
        hasWorkOrder: Boolean(a.hasWorkOrder), workOrderId: a.workOrderId || null,

+ 9 - 10
src/views/subSystem/gas/gasjcyj/xldxkjkrqindbj.vue

@@ -58,8 +58,7 @@
           <span><el-icon><BellFilled /></el-icon> 实时报警列表</span>
           <el-radio-group v-model="alertFilter" size="small">
             <el-radio-button label="all">全部</el-radio-button>
-            <el-radio-button label="critical">严重</el-radio-button>
-            <el-radio-button label="warning">预警</el-radio-button>
+            <el-radio-button v-for="dict in alarm_level" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
           </el-radio-group>
         </div>
         <div class="alert-list" v-loading="loading">
@@ -82,7 +81,7 @@
               <div class="alert-data">
                 <span class="data-item">浓度: <strong :class="{ 'critical-value': alert.concentration > 10 }">{{ alert.concentration }}%LEL</strong></span>
                 <span class="data-item">阈值: {{ alert.threshold }}%LEL</span>
-                <span class="data-item">级别: {{ alert.level === 'critical' ? '严重' : '预警' }}</span>
+                <span class="data-item">级别: <dict-tag :options="alarm_level" :value="alert.alarmLevel" /></span>
               </div>
               <div class="alert-time">{{ alert.alertTime }}</div>
             </div>
@@ -103,9 +102,7 @@
       <div class="analysis-panel" v-if="selectedAlert">
         <div class="panel-header">
           <span><el-icon><DataAnalysis /></el-icon> 综合研判</span>
-          <el-tag :type="selectedAlert.level === 'critical' ? 'danger' : 'warning'" size="large">
-            {{ selectedAlert.level === 'critical' ? '严重报警' : '预警' }}
-          </el-tag>
+          <dict-tag :options="alarm_level" :value="selectedAlert.alarmLevel" />
         </div>
 
         <!-- 报警基本信息 -->
@@ -115,9 +112,7 @@
             <el-descriptions-item label="点位编号">{{ selectedAlert.pointCode }}</el-descriptions-item>
             <el-descriptions-item label="报警位置">{{ selectedAlert.address }}</el-descriptions-item>
             <el-descriptions-item label="报警级别">
-              <el-tag :type="selectedAlert.level === 'critical' ? 'danger' : 'warning'">
-                {{ selectedAlert.level === 'critical' ? '严重报警' : '预警' }}
-              </el-tag>
+              <dict-tag :options="alarm_level" :value="selectedAlert.alarmLevel" />
             </el-descriptions-item>
             <el-descriptions-item label="实时浓度">
               <span :class="{ 'critical-value': selectedAlert.concentration > 10 }">{{ selectedAlert.concentration }} %LEL</span>
@@ -208,10 +203,13 @@
 <script setup>
 import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import { Warning, Refresh, Download, BellFilled, Search, Location, WarningFilled, InfoFilled, DataAnalysis, Clock, WindPower, Document } from '@element-plus/icons-vue'
+import { useDict } from '@/utils/dict'
 import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
 import { getGasAlarmHistory, resolveGasAlarm, dispatchGasAlarm } from '@/api/pipeNetwork/basic'
 
+const { alarm_level } = useDict('alarm_level')
+
 // 相邻地下空间报警和场站探测器统一来自 alarm_data;按位置描述筛出地下空间点位。
 const realtimeAlerts = ref([])
 const loading = ref(false)
@@ -234,6 +232,7 @@ function mapAlarm(alarm) {
     pointName: alarm.stationName || alarm.deviceName || alarm.deviceCode || '地下空间监测点',
     address: alarm.address || '',
     level,
+    alarmLevel: alarm.alarmLevel,
     concentration,
     threshold,
     alertTime: alarm.createTime || '',
@@ -301,7 +300,7 @@ const workOrderButtonText = computed(() => {
 const filteredAlerts = computed(() => {
   let list = realtimeAlerts.value
   if (alertFilter.value !== 'all') {
-    list = list.filter(a => a.level === alertFilter.value)
+    list = list.filter(a => String(a.alarmLevel) === String(alertFilter.value))
   }
   if (searchKeyword.value) {
     const kw = searchKeyword.value.toLowerCase()

+ 1 - 1
src/views/subSystem/manholeCover/home/index.vue

@@ -104,7 +104,7 @@ import { ref, reactive, onMounted, onBeforeUnmount, nextTick, computed } from 'v
 import * as echarts from 'echarts'
 import { getManholeLayerData } from '@/api/pipeNetwork/basic'
 import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
-import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
+import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
 import { getAlarmDataPage } from '@/api/drainage'
 import { ElMessage } from 'element-plus'
 import locationIcon from '@/assets/images/location.png'

+ 10 - 12
src/views/subSystem/manholeCover/operation/operationTask.vue

@@ -231,7 +231,7 @@
     </div>
 
     <!-- 提交工单对话框 -->
-    <el-dialog v-model="dispatchDialogVisible" title="提交工单" width="520px" @close="dispatchForm = { orderLevel: 1, orderDesc: '', owner: '' }">
+    <el-dialog v-model="dispatchDialogVisible" title="提交工单" width="520px" @close="dispatchForm = { orderLevel: defaultOrderLevel(), orderDesc: '', owner: '' }">
       <el-form :model="dispatchForm" label-width="100px">
         <el-form-item label="设备名称">
           <el-input :model-value="currentDispatchTerminal?.name" disabled />
@@ -242,9 +242,7 @@
 
         <el-form-item label="优先级" required>
           <el-select v-model="dispatchForm.orderLevel" placeholder="选择优先级" style="width: 100%">
-            <el-option label="紧急" :value="0" />
-            <el-option label="一般" :value="1" />
-            <el-option label="低" :value="2" />
+            <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
           </el-select>
         </el-form-item>
         <el-form-item label="问题描述" required>
@@ -272,9 +270,7 @@
       <el-form style="margin-top: 16px" label-width="80px">
         <el-form-item label="优先级">
           <el-select v-model="batchDispatchForm.orderLevel" style="width: 100%">
-            <el-option label="紧急" :value="0" />
-            <el-option label="一般" :value="1" />
-            <el-option label="低" :value="2" />
+            <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
           </el-select>
         </el-form-item>
         <el-form-item label="问题描述">
@@ -295,6 +291,9 @@ import { Warning, Search, Refresh, Location, Monitor, MapLocation, Document, Dat
 import { ElMessage } from 'element-plus'
 import { getManholeAlarmMonitorList, addManholeWorkOrder } from '@/api/pipeNetwork/basic'
 import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder'
+import { useWorkOrderLevelDict } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/useWorkOrderLevelDict.js'
+
+const { workOrderLevels, defaultOrderLevel, orderLevelText: priorityText } = useWorkOrderLevelDict()
 
 // ==================== 常量 ====================
 const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
@@ -320,11 +319,11 @@ const workOrders = ref([])
 
 const dispatchDialogVisible = ref(false)
 const currentDispatchTerminal = ref(null)
-const dispatchForm = ref({ orderLevel: 1, orderDesc: '', owner: '' })
+const dispatchForm = ref({ orderLevel: defaultOrderLevel(), orderDesc: '', owner: '' })
 
 const batchDispatchDialogVisible = ref(false)
 const batchDispatchIds = ref([])
-const batchDispatchForm = ref({ orderLevel: 1, orderDesc: '' })
+const batchDispatchForm = ref({ orderLevel: defaultOrderLevel(), orderDesc: '' })
 
 // ==================== 数据加载 ====================
 async function loadDevices() {
@@ -456,7 +455,6 @@ const pendingCount = computed(() => devices.value.filter(t => !t.workOrderSubmit
 const dispatchedCount = computed(() => devices.value.filter(t => t.workOrderSubmitted).length)
 
 // ==================== 辅助函数 ====================
-const priorityText = (l) => ({ 0: '紧急', 1: '一般', 2: '低' }[l] || '—')
 const orderStatusText = (s) => ({
   0: '待派单', 1: '待接单', 2: '已接单', 3: '处理中',
   4: '待验收', 5: '已结案', 6: '延期中', 7: '已取消'
@@ -516,7 +514,7 @@ const getTerminalCode = (id) => {
 const openDispatchDialog = (terminal) => {
   currentDispatchTerminal.value = terminal
   dispatchForm.value = {
-    orderLevel: terminal.abnormalLevel === 'critical' ? 0 : 1,
+    orderLevel: terminal.abnormalLevel === 'critical' && workOrderLevels.value.some(item => Number(item.value) === 0) ? 0 : defaultOrderLevel(),
     orderDesc: terminal.abnormalDesc || '',
     owner: ''
   }
@@ -566,7 +564,7 @@ const batchDispatch = () => {
     return
   }
   batchDispatchIds.value = [...selectedRows.value]
-  batchDispatchForm.value = { orderLevel: 1, orderDesc: '' }
+  batchDispatchForm.value = { orderLevel: defaultOrderLevel(), orderDesc: '' }
   batchDispatchDialogVisible.value = true
 }
 

+ 81 - 22
src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue

@@ -17,9 +17,7 @@
           <div class="qf">
             <label>优先级</label>
             <el-select v-model="queryParams.orderLevel" placeholder="全部" clearable style="width: 120px">
-              <el-option label="紧急" :value="WO_LEVEL.EMERGENCY" />
-              <el-option label="一般" :value="WO_LEVEL.NORMAL" />
-              <el-option label="低" :value="WO_LEVEL.LOW" />
+              <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
             </el-select>
           </div>
           <div class="qf">
@@ -158,7 +156,7 @@
                     <div class="zone-hd">维修前<span class="tip">{{ beforeAttach.length + ' 项' }}</span></div>
                     <div class="zbody">
                       <div v-for="(u, i) in beforeAttach" :key="'b' + i" class="att" :class="extCls(u) === 'img' ? 'att-img' : 'att-file'" @click="extCls(u) === 'img' && preview(u, beforeAttach)">
-                        <img v-if="extCls(u) === 'img'" :src="u" alt="" />
+                        <img v-if="extCls(u) === 'img'" :src="buildMinioAttachmentUrl(u)" alt="" />
                         <div v-else class="ext" :class="extCls(u)">{{ extText(u) }}</div>
                       </div>
                       <div v-if="!beforeAttach.length" class="hint">暂无维修前附件</div>
@@ -168,7 +166,7 @@
                     <div class="zone-hd">维修后<span class="tip">{{ afterAttach.length + ' 项' }}</span></div>
                     <div class="zbody">
                       <div v-for="(u, i) in afterAttach" :key="'a' + i" class="att" :class="extCls(u) === 'img' ? 'att-img' : 'att-file'" @click="extCls(u) === 'img' && preview(u, afterAttach)">
-                        <img v-if="extCls(u) === 'img'" :src="u" alt="" />
+                        <img v-if="extCls(u) === 'img'" :src="buildMinioAttachmentUrl(u)" alt="" />
                         <div v-else class="ext" :class="extCls(u)">{{ extText(u) }}</div>
                       </div>
                       <div v-if="!afterAttach.length" class="hint">维修完成后上传</div>
@@ -208,7 +206,7 @@
                         <template v-for="(u, k) in splitAttach(l.attachUrl)" :key="k">
                           <img
                             v-if="extCls(u) === 'img'"
-                            :src="u"
+                            :src="buildMinioAttachmentUrl(u)"
                             class="hthumb"
                             alt=""
                             @click="preview(u, splitAttach(l.attachUrl))"
@@ -296,11 +294,19 @@
         <el-form-item label="责任人" required>
           <div class="handler-box">
             <el-input :model-value="dispatchForm.handlerName" placeholder="请选择责任人" readonly style="width: 240px">
-              <template #append><el-button @click="userSelectVisible = true">选择</el-button></template>
+              <template #append><el-button @click="openUserSelect('handler')">选择</el-button></template>
             </el-input>
             <el-button v-if="dispatchForm.handlerId" link type="danger" @click="clearHandler">清除</el-button>
           </div>
         </el-form-item>
+        <el-form-item label="核验人" required>
+          <div class="handler-box">
+            <el-input :model-value="dispatchForm.verifyUserName" placeholder="请选择核验人" readonly style="width: 240px">
+              <template #append><el-button @click="openUserSelect('verifier')">选择</el-button></template>
+            </el-input>
+            <el-button v-if="dispatchForm.verifyUser" link type="danger" @click="clearVerifier">清除</el-button>
+          </div>
+        </el-form-item>
         <el-form-item label="派单备注">
           <el-input v-model="dispatchForm.remarks" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可填写故障要点、现场注意事项等派单说明" />
         </el-form-item>
@@ -320,8 +326,8 @@
       </template>
     </el-dialog>
 
-    <!-- 选择责任人 -->
-    <el-dialog v-model="userSelectVisible" title="选择责任人" width="760px" append-to-body>
+    <!-- 选择责任人 / 核验人 -->
+    <el-dialog v-model="userSelectVisible" :title="userSelectTitle" width="760px" append-to-body>
       <el-row :gutter="12">
         <el-col :span="8">
           <div class="dept-tree-box">
@@ -360,7 +366,7 @@
           <template #label>
             <span v-if="actionAttachRequired" class="req-mark">*</span>{{ actionAttachLabel }}
           </template>
-          <ImageUpload v-model="actionForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
+          <ImageUpload v-model="actionForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -382,7 +388,7 @@
           <el-input v-model="verifyForm.content" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入验收意见" />
         </el-form-item>
         <el-form-item label="验收附件(可选)">
-          <ImageUpload v-model="verifyForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
+          <ImageUpload v-model="verifyForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -401,7 +407,7 @@
           <el-input v-model="delayForm.delayedReason" type="textarea" :rows="4" maxlength="300" show-word-limit placeholder="请填写延期原因" />
         </el-form-item>
         <el-form-item label="附件">
-          <ImageUpload v-model="delayForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" />
+          <ImageUpload v-model="delayForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -451,7 +457,6 @@ import {
 } from '@/api/pipeNetwork/ops/workOrder.js'
 import {
   canOperate,
-  orderLevelText,
   orderStatusText,
   delayApplyStatusText,
   orderLogText,
@@ -461,15 +466,20 @@ import {
   WO_LOG_TYPE,
   DELAY_APPLY_STATUS,
   WO_LEVEL
-} from './workOrderDict.js'
+} from './js/workOrderDict.js'
+import { useWorkOrderLevelDict } from './js/useWorkOrderLevelDict.js'
 import ImageUpload from '@/components/ImageUpload/index.vue'
+import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
 import { useRoute } from 'vue-router'
+import useUserStore from '@/store/modules/user'
 
 defineOptions({ name: 'Pworkdisposition' })
 
 const { proxy } = getCurrentInstance()
-const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
+const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload?dir=workOrder'
+const { workOrderLevels, orderLevelText } = useWorkOrderLevelDict()
 const route = useRoute()
+const userStore = useUserStore()
 
 /**
  * 本组件被多个菜单复用,不同菜单按业务口径查询不同范围的工单:
@@ -690,7 +700,7 @@ const viewerList = ref([])
 const viewerIndex = ref(0)
 function preview(url, arr) {
   const a = arr && arr.length ? arr : [url]
-  viewerList.value = a
+  viewerList.value = a.map(path => buildMinioAttachmentUrl(path))
   viewerIndex.value = Math.max(0, a.indexOf(url))
   viewerVisible.value = true
 }
@@ -781,12 +791,22 @@ async function refresh(opOrderId) {
 
 /* ============ 派单 ============ */
 const dispatchVisible = ref(false)
-const dispatchForm = reactive({ orderId: null, handlerId: null, handlerName: '', remarks: '', planFinishTime: '' })
+const dispatchForm = reactive({
+  orderId: null,
+  handlerId: null,
+  handlerName: '',
+  verifyUser: null,
+  verifyUserName: '',
+  remarks: '',
+  planFinishTime: ''
+})
 
 function openDispatch(o) {
   dispatchForm.orderId = o.orderId
   dispatchForm.handlerId = o.receiveUser || null
   dispatchForm.handlerName = o.receiveUserName || ''
+  dispatchForm.verifyUser = userStore.id || null
+  dispatchForm.verifyUserName = userStore.nickName || userStore.name || ''
   dispatchForm.remarks = ''
   dispatchForm.planFinishTime = o.planFinishTime || ''
   dispatchVisible.value = true
@@ -797,9 +817,19 @@ async function submitDispatch() {
     proxy.$modal.msgWarning('请选择责任人')
     return
   }
+  if (!dispatchForm.verifyUser) {
+    proxy.$modal.msgWarning('请选择核验人')
+    return
+  }
   submitting.value = true
   try {
-    await dispatchWorkOrder({ orderId: dispatchForm.orderId, handlerId: dispatchForm.handlerId, remarks: dispatchForm.remarks, planFinishTime: dispatchForm.planFinishTime || null })
+    await dispatchWorkOrder({
+      orderId: dispatchForm.orderId,
+      handlerId: dispatchForm.handlerId,
+      verifyUser: dispatchForm.verifyUser,
+      remarks: dispatchForm.remarks,
+      planFinishTime: dispatchForm.planFinishTime || null
+    })
     proxy.$modal.msgSuccess('派单成功,工单状态变更为待接单')
     dispatchVisible.value = false
     await refresh(dispatchForm.orderId)
@@ -808,8 +838,10 @@ async function submitDispatch() {
   }
 }
 
-/* ============ 责任人选择 ============ */
+/* ============ 责任人 / 核验人选择 ============ */
 const userSelectVisible = ref(false)
+const userSelectTarget = ref('handler')
+const userSelectTitle = computed(() => userSelectTarget.value === 'verifier' ? '选择核验人' : '选择责任人')
 const deptTree = ref([])
 const deptUserList = ref([])
 const selectedUserId = ref(null)
@@ -821,15 +853,36 @@ function loadDeptTree() {
 function loadDeptUsers(deptId) {
   workOrderUsers(deptId).then((res) => { deptUserList.value = res.data || [] })
 }
+function openUserSelect(target) {
+  userSelectTarget.value = target
+  if (target === 'verifier') {
+    selectedUserId.value = dispatchForm.verifyUser
+    selectedUser.value = dispatchForm.verifyUser
+      ? { userId: dispatchForm.verifyUser, nickName: dispatchForm.verifyUserName }
+      : null
+  } else {
+    selectedUserId.value = dispatchForm.handlerId
+    selectedUser.value = dispatchForm.handlerId
+      ? { userId: dispatchForm.handlerId, nickName: dispatchForm.handlerName }
+      : null
+  }
+  userSelectVisible.value = true
+}
 function handleDeptNodeClick(node) { loadDeptUsers(node.id) }
 function handleUserCurrentChange(row) {
   if (row) { selectedUser.value = row; selectedUserId.value = row.userId }
 }
 function handleUserRadioChange(row) { selectedUser.value = row; selectedUserId.value = row.userId }
 function confirmUserSelect() {
-  if (!selectedUser.value) { proxy.$modal.msgWarning('请选择责任人'); return }
-  dispatchForm.handlerId = selectedUser.value.userId
-  dispatchForm.handlerName = selectedUser.value.nickName || selectedUser.value.userName
+  const label = userSelectTarget.value === 'verifier' ? '核验人' : '责任人'
+  if (!selectedUser.value) { proxy.$modal.msgWarning(`请选择${label}`); return }
+  if (userSelectTarget.value === 'verifier') {
+    dispatchForm.verifyUser = selectedUser.value.userId
+    dispatchForm.verifyUserName = selectedUser.value.nickName || selectedUser.value.userName
+  } else {
+    dispatchForm.handlerId = selectedUser.value.userId
+    dispatchForm.handlerName = selectedUser.value.nickName || selectedUser.value.userName
+  }
   userSelectVisible.value = false
 }
 function clearHandler() {
@@ -838,6 +891,12 @@ function clearHandler() {
   selectedUser.value = null
   selectedUserId.value = null
 }
+function clearVerifier() {
+  dispatchForm.verifyUser = null
+  dispatchForm.verifyUserName = ''
+  selectedUser.value = null
+  selectedUserId.value = null
+}
 
 /* ============ 通用操作 ============ */
 const actionVisible = ref(false)

+ 7 - 12
src/views/subSystem/pipeNetwork/ops/workOrder/index.vue

@@ -14,9 +14,7 @@
         </el-form-item>
         <el-form-item label="优先级">
           <el-select v-model="queryParams.orderLevel" placeholder="全部" clearable style="width: 120px">
-            <el-option label="紧急" :value="WO_LEVEL.EMERGENCY" />
-            <el-option label="一般" :value="WO_LEVEL.NORMAL" />
-            <el-option label="低" :value="WO_LEVEL.LOW" />
+            <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
           </el-select>
         </el-form-item>
         <el-form-item label="工单状态">
@@ -138,9 +136,7 @@
 
         <el-form-item label="优先级" prop="orderLevel">
           <el-radio-group v-model="addForm.orderLevel">
-            <el-radio-button :value="WO_LEVEL.EMERGENCY">紧急</el-radio-button>
-            <el-radio-button :value="WO_LEVEL.NORMAL">一般</el-radio-button>
-            <el-radio-button :value="WO_LEVEL.LOW">低</el-radio-button>
+            <el-radio-button v-for="item in workOrderLevels" :key="item.value" :value="Number(item.value)">{{ item.label }}</el-radio-button>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="故障时间" prop="faultTime">
@@ -187,17 +183,16 @@ import { addWorkOrder, listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
 import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
 import {
   ORDER_STATUS_MAP,
-  WO_LEVEL,
-  orderLevelText,
-  orderLevelType,
   orderStatusText,
   orderStatusType,
   timeText
-} from './workOrderDict.js'
+} from './js/workOrderDict.js'
+import { useWorkOrderLevelDict } from './js/useWorkOrderLevelDict.js'
 import { ElMessage } from 'element-plus'
 
 defineOptions({ name: 'PworkList' })
 
+const { workOrderLevels, defaultOrderLevel, orderLevelText, orderLevelType } = useWorkOrderLevelDict()
 const route = useRoute()
 
 /**
@@ -285,7 +280,7 @@ const deviceLoading = ref(false)
 const deviceInfo = ref(null)
 const addForm = reactive({
   deviceId: '',
-  orderLevel: WO_LEVEL.NORMAL,
+  orderLevel: defaultOrderLevel(),
   faultTime: '',
   planFinishTime: '',
   orderDesc: ''
@@ -339,7 +334,7 @@ const addRules = {
 
 function openAdd() {
   addForm.deviceId = ''
-  addForm.orderLevel = WO_LEVEL.NORMAL
+  addForm.orderLevel = defaultOrderLevel()
   addForm.faultTime = ''
   addForm.planFinishTime = ''
   addForm.orderDesc = ''

+ 15 - 0
src/views/subSystem/pipeNetwork/ops/workOrder/js/useWorkOrderLevelDict.js

@@ -0,0 +1,15 @@
+import { useDict } from '@/utils/dict'
+import { orderLevelText, orderLevelType } from './workOrderDict.js'
+
+export function useWorkOrderLevelDict() {
+  const { work_order_level: workOrderLevels } = useDict('work_order_level')
+  return {
+    workOrderLevels,
+    defaultOrderLevel: () => {
+      const item = workOrderLevels.value.find(item => item.isDefault === 'Y')
+      return item ? Number(item.value) : undefined
+    },
+    orderLevelText: (level) => orderLevelText(level, workOrderLevels.value),
+    orderLevelType: (level) => orderLevelType(level, workOrderLevels.value)
+  }
+}

+ 14 - 10
src/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js → src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js

@@ -4,7 +4,7 @@
 //   WorkOrderStatusEnum        工单状态  work_order.order_status
 //   WorkOrderLogOperTypeEnum   操作类型  work_order_log.oper_type
 //   WorkOrderDelayApplyStatusEnum 延期申请状态 work_order.delay_apply_status
-//   WorkOrderLevelEnum         优先级    work_order.order_level
+// 优先级名称来自 sys_dict:work_order_level
 
 /** 工单状态枚举(work_order.order_status) */
 export const WO_STATUS = {
@@ -50,9 +50,9 @@ export const WO_LEVEL = {
 
 // 工单优先级:0-紧急 1-一般 2-低
 export const ORDER_LEVEL_MAP = {
-  [WO_LEVEL.EMERGENCY]: { text: '紧急', type: 'danger' },
-  [WO_LEVEL.NORMAL]: { text: '一般', type: 'warning' },
-  [WO_LEVEL.LOW]: { text: '低', type: 'info' }
+  [WO_LEVEL.EMERGENCY]: { type: 'danger' },
+  [WO_LEVEL.NORMAL]: { type: 'warning' },
+  [WO_LEVEL.LOW]: { type: 'info' }
 }
 
 // 工单状态:0-待派单 1-待接单 2-已接单 3-处理中 4-待验收 5-已结案 6-延期中 7-已取消
@@ -127,14 +127,18 @@ export function delayApplyStatusType(status) {
   return DELAY_APPLY_STATUS_MAP[Number(status)]?.type || 'info'
 }
 
-/** 优先级文本 */
-export function orderLevelText(level) {
-  return ORDER_LEVEL_MAP[Number(level)]?.text || '-'
+/** 字典优先级文本;值仍保持后端整数编码。 */
+export function orderLevelText(level, levels = []) {
+  if (level === null || level === undefined || level === '') return '-'
+  const item = levels.find((entry) => Number(entry.value) === Number(level))
+  return item?.label || '-'
 }
 
-/** 优先级 Tag type */
-export function orderLevelType(level) {
-  return ORDER_LEVEL_MAP[Number(level)]?.type || 'info'
+/** 优先级 Tag type:优先使用字典样式,业务严重程度作为回退。 */
+export function orderLevelType(level, levels = []) {
+  if (level === null || level === undefined || level === '') return 'info'
+  const item = levels.find((entry) => Number(entry.value) === Number(level))
+  return item?.elTagType || item?.listClass || ORDER_LEVEL_MAP[Number(level)]?.type || 'info'
 }
 
 /** 是否属于某状态分组 */

+ 4 - 6
src/views/subSystem/pipeNetwork/ops/workOrder/list.vue

@@ -14,9 +14,7 @@
         </el-form-item>
         <el-form-item label="优先级">
           <el-select v-model="queryParams.orderLevel" placeholder="全部" clearable style="width: 120px">
-            <el-option label="紧急" :value="0" />
-            <el-option label="一般" :value="1" />
-            <el-option label="低" :value="2" />
+            <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
           </el-select>
         </el-form-item>
         <el-form-item label="故障时间">
@@ -92,17 +90,17 @@ import { ref, reactive, computed, onMounted, watch } from 'vue'
 import { useRoute } from 'vue-router'
 import { listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
 import {
-  orderLevelText,
-  orderLevelType,
   orderStatusText,
   orderStatusType,
   ORDER_STATUS_GROUP,
   WO_STATUS,
   timeText
-} from './workOrderDict.js'
+} from './js/workOrderDict.js'
+import { useWorkOrderLevelDict } from './js/useWorkOrderLevelDict.js'
 
 defineOptions({ name: 'PworkOrderList' })
 
+const { workOrderLevels, orderLevelText, orderLevelType } = useWorkOrderLevelDict()
 const route = useRoute()
 
 /**

+ 3 - 3
src/views/subSystem/waterSupply/alarm/alarmGIS.vue

@@ -111,9 +111,9 @@ import { getAlarmMap, getAlarmOptions, getAlarms } from '@/api/alarm'
 import AlarmDetail from '../../alarm/AlarmDetail.vue'
 import AlarmChart from '../../alarm/AlarmChart.vue'
 import AlarmAttachments from '../../alarm/AlarmAttachments.vue'
-import { alarmCurveChoices, groupAlarmDevices, levelColor, levelTone, monitorStatusTone } from '../../alarm/common/utils/alarmOperationsModel'
-import { hasActualValue, thresholdText, valueText } from '../../alarm/common/utils/alarmReviewModel'
-import { useAlarmScope } from '../../alarm/common/utils/useAlarmScope'
+import { alarmCurveChoices, groupAlarmDevices, levelColor, levelTone, monitorStatusTone } from '../../alarm/common/js/alarmOperationsModel'
+import { hasActualValue, thresholdText, valueText } from '../../alarm/common/js/alarmReviewModel'
+import { useAlarmScope } from '../../alarm/common/js/useAlarmScope'
 import '../../alarm/common/styles/alarmOperations.scss'
 
 const { scope, scopeKey } = useAlarmScope()

+ 8 - 6
src/views/subSystem/waterSupply/components/WaterAlarmPage.vue

@@ -29,7 +29,7 @@
       <el-form :inline="true" :model="filters" @submit.prevent="search">
         <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" /></el-form-item>
         <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" /></el-form-item>
-        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="level in levels" :key="level" :label="`第${level}级`" :value="level" /></el-select></el-form-item>
+        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="level in levels" :key="level.value" :label="level.label" :value="Number(level.value)" /></el-select></el-form-item>
         <el-form-item label="报警展示状态"><el-select v-model="filters.displayStatus" clearable placeholder="全部状态"><el-option v-for="status in displayStatuses" :key="status.value" v-bind="status" /></el-select></el-form-item>
         <el-form-item label="设备报警状态"><el-select v-model="filters.deviceAlarmStatus" clearable placeholder="全部状态"><el-option label="报警中" :value="1" /><el-option label="正常" :value="0" /></el-select></el-form-item>
         <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
@@ -45,7 +45,7 @@
         <el-table-column prop="deviceCode" label="设备编码" width="190" show-overflow-tooltip />
         <el-table-column prop="deviceName" label="设备名称" min-width="145" show-overflow-tooltip />
         <el-table-column prop="warningType" label="报警类型" min-width="145" show-overflow-tooltip />
-        <el-table-column label="报警等级" width="100" align="center"><template #default="{ row }"><el-tag :type="levelType(row.alarmLevel)">{{ row.alarmLevelName }}</el-tag></template></el-table-column>
+        <el-table-column label="报警等级" width="100" align="center"><template #default="{ row }"><el-tag :type="levelType(row.alarmLevel)">{{ levels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
         <el-table-column prop="createTime" label="报警时间" width="175" sortable="custom" />
         <el-table-column label="设备报警状态" width="120" align="center"><template #default="{ row }"><el-tag :type="row.deviceAlarmStatus == null ? 'info' : Number(row.deviceAlarmStatus) === 1 ? 'danger' : 'success'">{{ row.deviceAlarmStatusText }}</el-tag></template></el-table-column>
         <el-table-column label="报警展示状态" width="120" align="center"><template #default="{ row }"><el-tag :type="displayType(row.displayStatus)">{{ row.displayStatusText }}</el-tag></template></el-table-column>
@@ -60,7 +60,7 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { BellFilled, DataAnalysis, Download, List, Monitor, PieChart, Refresh, RefreshLeft, Search, TrendCharts, View } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import { saveAs } from 'file-saver'
@@ -69,6 +69,7 @@ import { getAlarmDashboard, getAlarms } from '@/api/alarm'
 import AlarmDetail from '@/views/subSystem/alarm/AlarmDetail.vue'
 import { WATER_ALARM_ROOT_TYPE_ID, buildWaterAlarmQuery, normalizeAlarmDashboard, normalizeWaterAlarmRow, toAlarmCsv } from '@/utils/waterAlarmManagementModel'
 import { normalizePage, unwrapResponse } from '@/utils/waterSupplyModel'
+import { useDict } from '@/utils/dict'
 
 const trendDays = 7
 const filters = ref({ alarmNo: '', keyword: '', alarmLevel: undefined, displayStatus: undefined, deviceAlarmStatus: undefined })
@@ -82,7 +83,7 @@ const exportLoading = ref(false)
 const detailRef = ref()
 const trendRef = ref(), warningRef = ref(), deviceRef = ref()
 let trendChart, warningChart, deviceChart
-const levels = [1, 2, 3, 4]
+const { alarm_level: levels } = useDict('alarm_level')
 const displayStatuses = [{ label: '待审核', value: 1 }, { label: '已转工单', value: 2 }, { label: '已解除', value: 3 }, { label: '误报警', value: 4 }]
 const metricCards = computed(() => [
   { key: 'totalCount', label: '报警总数', unit: '条', icon: BellFilled, tone: 'tone-blue' },
@@ -138,7 +139,7 @@ async function exportData() {
       if (!page.records.length) break
       currentPage++
     }
-    saveAs(new Blob([toAlarmCsv(records)], { type: 'text/csv;charset=utf-8' }), `供水报警流水_${new Date().toISOString().slice(0, 10)}.csv`)
+    saveAs(new Blob([toAlarmCsv(records.map(row => ({ ...row, alarmLevelName: levels.value.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName })))], { type: 'text/csv;charset=utf-8' }), `供水报警流水_${new Date().toISOString().slice(0, 10)}.csv`)
     ElMessage.success('导出完成')
   } catch (e) {
     ElMessage.error(e?.message || '导出失败,请稍后重试')
@@ -153,7 +154,7 @@ function renderCharts() {
 function renderTrend(container, trend) {
   if (!container) return null
   const chart = echarts.init(container)
-  chart.setOption({ tooltip: { trigger: 'axis' }, legend: { top: 0 }, grid: { left: 40, right: 18, top: 35, bottom: 30 }, xAxis: { type: 'category', data: trend.dates }, yAxis: { type: 'value', minInterval: 1 }, series: trend.levels.map((level, index) => ({ name: level.text, type: 'line', smooth: true, data: level.data, symbol: 'circle', symbolSize: 6, lineStyle: { width: 2, color: ['#ef4444', '#f97316', '#2563eb', '#22c55e'][index] }, itemStyle: { color: ['#ef4444', '#f97316', '#2563eb', '#22c55e'][index] } })) })
+  chart.setOption({ tooltip: { trigger: 'axis' }, legend: { top: 0 }, grid: { left: 40, right: 18, top: 35, bottom: 30 }, xAxis: { type: 'category', data: trend.dates }, yAxis: { type: 'value', minInterval: 1 }, series: trend.levels.map((level, index) => ({ name: levels.value.find(item => Number(item.value) === level.level)?.label || level.text, type: 'line', smooth: true, data: level.data, symbol: 'circle', symbolSize: 6, lineStyle: { width: 2, color: ['#ef4444', '#f97316', '#2563eb', '#22c55e'][index] }, itemStyle: { color: ['#ef4444', '#f97316', '#2563eb', '#22c55e'][index] } })) })
   return chart
 }
 function renderPie(container, data) {
@@ -162,6 +163,7 @@ function renderPie(container, data) {
   chart.setOption({ tooltip: { trigger: 'item' }, legend: { bottom: 0 }, series: [{ type: 'pie', radius: ['45%', '70%'], data: data.map(item => ({ name: item.name, value: item.count })), itemStyle: { borderColor: '#fff', borderWidth: 2 } }] })
   return chart
 }
+watch(levels, () => { if (trendChart) renderCharts() })
 function resizeCharts() { trendChart?.resize(); warningChart?.resize(); deviceChart?.resize() }
 onMounted(() => { load(); window.addEventListener('resize', resizeCharts) })
 onBeforeUnmount(() => { ++loadGeneration; window.removeEventListener('resize', resizeCharts); trendChart?.dispose(); warningChart?.dispose(); deviceChart?.dispose() })

+ 1 - 1
src/views/subSystem/waterSupply/components/WaterDeviceAbnormalPage.vue

@@ -275,7 +275,7 @@ import {
     filterAbnormalDevices,
     normalizeAbnormalRow
 } from '@/utils/waterDeviceAbnormalModel'
-import {orderStatusText} from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
+import {orderStatusText} from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
 
 const ABNORMAL_TYPE_FALLBACK = 'all'
 

+ 5 - 1
src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue

@@ -180,7 +180,7 @@
               <el-tag :type="deviceStatusTagType(selectedDetail.currentStatus)">设备状态:{{ selectedDetail.currentStatusText }}</el-tag>
               <el-tag :type="selectedDetail.onlineStatus === 1 ? 'success' : 'info'">在线状态:{{ selectedDetail.onlineStatusText }}</el-tag>
               <el-tag :type="selectedDetail.alarmStatus === 1 ? 'danger' : 'success'">报警状态:{{ selectedDetail.alarmStatusText }}</el-tag>
-              <el-tag :type="selectedDetail.alertLevel ? 'warning' : 'info'">报警等级:{{ selectedDetail.alarmLevelText }}</el-tag>
+              <el-tag :type="selectedDetail.alertLevel ? 'warning' : 'info'">报警等级:{{ alarmLevelText }}</el-tag>
             </div>
           </section>
 
@@ -241,6 +241,10 @@ import {
 } from '@/api/pipeNetwork/waterSupply'
 import { normalizeDeviceGisPoints, normalizeDeviceRow } from '@/utils/waterSupplyUiModel'
 import { unwrapResponse } from '@/utils/waterSupplyModel'
+import { useDict } from '@/utils/dict'
+
+const { alarm_level: alarmLevels } = useDict('alarm_level')
+const alarmLevelText = computed(() => alarmLevels.value.find(item => String(item.value) === String(selectedDetail.value?.alarmLevel))?.label || selectedDetail.value?.alarmLevelText || '—')
 
 const searchKeyword = ref('')
 const selectedTypeId = ref('')

+ 7 - 4
src/views/subSystem/waterSupply/components/WaterSupplyHome.vue

@@ -49,7 +49,7 @@
               </span>
               <span class="compact-meta">
                 <el-tag size="small" :type="item.displayStatusType">{{ item.displayStatusText }}</el-tag>
-                <el-tag size="small" :type="levelType(item.alarmLevel)">{{ item.alarmLevelText }}</el-tag>
+                <el-tag size="small" :type="levelType(item.alarmLevel)">{{ alarmLevels.find(level => Number(level.value) === Number(item.alarmLevel))?.label || item.alarmLevelText }}</el-tag>
                 <small>{{ item.alarmTime }}</small>
               </span>
             </button>
@@ -205,7 +205,7 @@
               <el-descriptions-item label="设备类型">{{ selectedDevice.typeName }}</el-descriptions-item>
               <el-descriptions-item label="所属区域">{{ selectedDevice.district || '-' }}</el-descriptions-item>
               <el-descriptions-item label="安装位置">{{ selectedDevice.location || '-' }}</el-descriptions-item>
-              <el-descriptions-item label="报警等级">{{ selectedDevice.alarmLevelText }}</el-descriptions-item>
+              <el-descriptions-item label="报警等级">{{ alarmLevels.find(level => Number(level.value) === Number(selectedDevice.alertLevel ?? selectedDevice.alarmLevel))?.label || selectedDevice.alarmLevelText }}</el-descriptions-item>
               <el-descriptions-item label="异常原因">{{ selectedDevice.abnormalReasonText || '-' }}</el-descriptions-item>
               <el-descriptions-item label="关联工单">
                 <el-button v-if="selectedDevice.relatedOrderNo && selectedDevice.relatedOrderNo !== '-'" link type="primary"
@@ -275,7 +275,7 @@
 </template>
 
 <script setup>
-import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import * as echarts from 'echarts'
 import {
@@ -295,6 +295,7 @@ import { gisPointStatusColors, gisPointStatusLabels, gisPointStatusTagTypes } fr
 import { buildFacilityMapModel } from '@/utils/waterFacilityGisModel'
 import { resolveWaterHomeRoute } from '@/router/waterHomeRouteResolver'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
+import { useDict } from '@/utils/dict'
 import {
   HOME_QUICK_ENTRIES, WATER_DEVICE_ROOT_TYPE_ID, buildHomeGisOverview, buildHomeWorkOrderQuery,
   normalizeAbnormalTodoRows, normalizeAlarmOverviewCards, normalizeAlarmTrend, normalizeDeviceSummary,
@@ -302,6 +303,7 @@ import {
   normalizeWorkOrderRows, normalizeWorkOrderStatusStat
 } from '@/utils/waterHomeModel'
 
+const { alarm_level: alarmLevels } = useDict('alarm_level')
 const router = useRouter()
 
 const loading = ref(true)
@@ -520,6 +522,7 @@ function overviewKindLabel(kind) {
   return { device: '监测设备', alarm: '报警', facility: '供水设施' }[kind] || '点位'
 }
 
+watch(alarmLevels, () => { if (trendChart) renderTrendChart() })
 function renderCharts() {
   renderTrendChart()
   warningPie?.dispose()
@@ -552,7 +555,7 @@ function renderTrendChart() {
     },
     color: palette,
     series: levels.map((level, index) => ({
-      name: level.text || `级${index + 1}`,
+      name: alarmLevels.value.find(item => Number(item.value) === level.level)?.label || level.text,
       type: 'bar',
       stack: 'alarm',
       barWidth: 26,

+ 5 - 21
src/views/subSystem/waterSupply/monitorAlarm/workOrder.vue

@@ -7,9 +7,7 @@
         <el-form :model="queryParams" ref="queryFormRef" :inline>
           <el-form-item label="优先级">
             <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
-              <el-option label="紧急" :value="0" />
-              <el-option label="一般" :value="1" />
-              <el-option label="低" :value="2" />
+              <el-option v-for="item in workOrderLevels" :key="item.value" :label="item.label" :value="Number(item.value)" />
             </el-select>
           </el-form-item>
           <el-form-item label="状态">
@@ -125,21 +123,7 @@
                   <el-descriptions-item label="关联隐患编号">{{ selectedOrder.hazardNo || '-' }}</el-descriptions-item>
                   <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
                   <el-descriptions-item label="优先级">
-                    <el-tag
-                      v-if="selectedOrder.orderLevel === 0"
-                      type="danger"
-                      size="small"
-                    >紧急</el-tag>
-                    <el-tag
-                      v-else-if="selectedOrder.orderLevel === 1"
-                      type="warning"
-                      size="small"
-                    >一般</el-tag>
-                    <el-tag
-                      v-else
-                      type="info"
-                      size="small"
-                    >低</el-tag>
+                    <el-tag :type="orderLevelType(selectedOrder.orderLevel)" size="small">{{ orderLevelText(selectedOrder.orderLevel) }}</el-tag>
                   </el-descriptions-item>
                   <el-descriptions-item label="故障发生时间">{{ formatDate(selectedOrder.faultTime) }}</el-descriptions-item>
                   <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc }}</el-descriptions-item>
@@ -365,14 +349,14 @@ import { listUser } from '@/api/system/user'
 import { handleTree } from '@/utils/ruoyi'
 import {
   canOperate,
-  orderLevelText,
-  orderLevelType,
   orderLogText,
   orderLogType,
   orderStatusText,
   orderStatusType
-} from '@/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
+} from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
+import { useWorkOrderLevelDict } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/useWorkOrderLevelDict.js'
 
+const { workOrderLevels, orderLevelText, orderLevelType } = useWorkOrderLevelDict()
 const listLoading = ref(false)
 const tableData = ref([])
 const pageNum = ref(1)

+ 1 - 1
tests/alarmListStatistics.test.mjs

@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
 import vm from 'node:vm'
 import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue'
 import { parse } from '@vue/compiler-sfc'
-import { rangeParams } from '../src/views/subSystem/alarm/common/utils/alarmOperationsModel.js'
+import { rangeParams } from '../src/views/subSystem/alarm/common/js/alarmOperationsModel.js'
 
 const { descriptor } = parse(await readFile(new URL('../src/views/subSystem/alarm/common/list.vue', import.meta.url), 'utf8'))
 const script = descriptor.scriptSetup.content.replace(/^import .+$/gm, '')

+ 65 - 0
tests/minioAttachment.test.mjs

@@ -0,0 +1,65 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import vm from 'node:vm'
+import { createRequire } from 'node:module'
+import { buildMinioAttachmentUrl, minioAttachmentPath } from '../src/utils/minioAttachment.js'
+
+const base = 'https://files.example.test/attachments/'
+const path = '/2026/10/09/workOrder/jpg/4649512327058833952.jpg'
+
+test('相对路径展示时补 MinIO 基址,存储时保持相对路径', () => {
+  assert.equal(buildMinioAttachmentUrl(path, base), base + path.slice(1))
+  assert.equal(buildMinioAttachmentUrl(path.slice(1), base), base + path.slice(1))
+  assert.equal(minioAttachmentPath(base + path.slice(1), base), path)
+  assert.equal(minioAttachmentPath(path, base), path)
+  assert.equal(buildMinioAttachmentUrl('', base), '')
+  assert.equal(buildMinioAttachmentUrl('https://legacy.test/a.jpg', base), 'https://legacy.test/a.jpg')
+})
+
+test('上传、回显、追加和删除不将展示 URL 回写到工单', () => {
+  const source = fs.readFileSync(new URL('../src/components/ImageUpload/index.vue', import.meta.url), 'utf8')
+  const script = source.match(/<script setup>([\s\S]*?)<\/script>/)[1]
+    .replace(/^import .*;\r?\n/gm, '')
+    .replaceAll('import.meta.env.VITE_APP_BASE_API', "'/api'")
+  const props = { modelValue: path, relativePath: true, uploadUrl: '/api/minio/upload?dir=workOrder' }
+  const emitted = []
+  let refresh
+  const context = vm.createContext({
+    defineProps: () => props,
+    defineEmits: () => (event, value) => emitted.push(value),
+    getCurrentInstance: () => ({ proxy: { $modal: { closeLoading() {} } } }),
+    getToken: () => 'test',
+    ref: value => ({ value }),
+    computed: getter => ({ get value() { return getter() } }),
+    watch: (getter, callback) => { refresh = () => callback(getter()); refresh() },
+    buildMinioAttachmentUrl: value => buildMinioAttachmentUrl(value, base),
+    minioAttachmentPath: value => minioAttachmentPath(value, base)
+  })
+  vm.runInContext(script, context)
+  assert.equal(vm.runInContext('fileList.value[0].url', context), base + path.slice(1))
+  vm.runInContext('number.value = 1; handleUploadSuccess({ code: 200, relativePath: "/2026/10/09/workOrder/jpg/next.jpg" }, { uid: 2 })', context)
+  assert.equal(emitted.at(-1), path + ',/2026/10/09/workOrder/jpg/next.jpg')
+  props.modelValue = emitted.at(-1)
+  refresh()
+  vm.runInContext('handleDelete(fileList.value[0])', context)
+  assert.equal(emitted.at(-1), '/2026/10/09/workOrder/jpg/next.jpg')
+  props.relativePath = false
+  props.modelValue = '/profile/normal.jpg'
+  refresh()
+  assert.equal(vm.runInContext('fileList.value[0].url', context), '/api/profile/normal.jpg')
+  assert.equal(vm.runInContext('listToString(fileList.value)', context), '/profile/normal.jpg')
+})
+
+test('工单页面与上传组件可通过 Vue SFC 编译', () => {
+  const require = createRequire(import.meta.url)
+  const { parse, compileScript, compileTemplate } = require('@vue/compiler-sfc')
+  for (const file of ['../src/components/ImageUpload/index.vue', '../src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue']) {
+    const filename = new URL(file, import.meta.url).pathname
+    const { descriptor, errors } = parse(fs.readFileSync(new URL(file, import.meta.url), 'utf8'), { filename })
+    assert.deepEqual(errors, [])
+    const compiled = compileScript(descriptor, { id: 'test' })
+    const template = compileTemplate({ source: descriptor.template.content, filename, id: 'test', compilerOptions: { bindingMetadata: compiled.bindings } })
+    assert.deepEqual(template.errors, [])
+  }
+})

+ 1 - 1
tests/waterDeviceAbnormalBoard.test.mjs

@@ -213,7 +213,7 @@ test('abnormal detail drawer shows the three independent state sources and relat
   assert.match(source, /orderStatusText/u)
   assert.match(
     source,
-    /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/workOrderDict\.js'/u,
+    /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/js\/workOrderDict\.js'/u,
     '异常看板应复用统一工单字典'
   )
   assert.doesNotMatch(source, /function\s+workOrderStatusType/u)

+ 3 - 3
tests/waterHomeModel.test.mjs

@@ -142,7 +142,7 @@ test('realtime alarm rows expose chinese friendly display fields', () => {
   ])
   assert.equal(rows.length, 1)
   assert.equal(rows[0].deviceName, '一号压力点')
-  assert.equal(rows[0].alarmLevelText, '一级报警')
+  assert.equal(rows[0].alarmLevelText, '1')
   assert.equal(rows[0].location, '城南')
   assert.equal(rows[0].displayStatusText, '待审核')
   assert.equal(rows[0].displayStatusType, 'warning')
@@ -243,7 +243,7 @@ test('abnormal todo rows reuse the shared device normalization contract', () =>
   assert.equal(rows.length, 1)
   assert.equal(rows[0].alarmStatusText, '报警')
   assert.equal(rows[0].onlineStatusText, '在线')
-  assert.equal(rows[0].alarmLevelText, '二级报警')
+  assert.equal(rows[0].alarmLevelText, '2')
   assert.equal(rows[0].relatedOrderNo, 'WO202609150001')
   assert.equal(rows[0].workOrderStatusText, '处理中')
 })
@@ -274,7 +274,7 @@ test('work order rows translate status and level codes to chinese', () => {
   ], 5)
   assert.equal(rows[0].orderStatusText, '待验收')
   assert.equal(rows[0].orderStatusType, 'primary')
-  assert.equal(rows[0].orderLevelText, '紧急')
+  assert.equal(rows[0].orderLevelText, '0')
   assert.equal(rows[0].orderLevelType, 'danger')
 })
 

+ 2 - 2
tests/waterSupplyAlarmGIS.test.mjs

@@ -4,8 +4,8 @@ import { readFile } from 'node:fs/promises'
 import vm from 'node:vm'
 import { parse } from '@vue/compiler-sfc'
 import { computed, effectScope, nextTick, ref, watch } from 'vue'
-import * as operations from '../src/views/subSystem/alarm/common/utils/alarmOperationsModel.js'
-import * as review from '../src/views/subSystem/alarm/common/utils/alarmReviewModel.js'
+import * as operations from '../src/views/subSystem/alarm/common/js/alarmOperationsModel.js'
+import * as review from '../src/views/subSystem/alarm/common/js/alarmReviewModel.js'
 import { HOME_QUICK_ENTRIES, WATER_DEVICE_ROOT_TYPE_ID, buildHomeGisOverview } from '../src/utils/waterHomeModel.js'
 
 const { descriptor } = parse(await readFile(new URL('../src/views/subSystem/waterSupply/alarm/alarmGIS.vue', import.meta.url), 'utf8'))

+ 2 - 1
tests/waterSupplyUiModel.test.mjs

@@ -21,12 +21,13 @@ test('normalizes alarm and related work-order fields for device pages', () => {
     equipmentName: '一号压力计',
     alarmStatus: 1,
     alertLevel: 2,
+    alarmLevelName: '字典配置等级',
     latestOrderNo: 'WO202606101703482640',
     latestMaintainStatus: '处理中'
   })
 
   assert.equal(row.alarmStatusText, '报警')
-  assert.equal(row.alarmLevelText, '二级报警')
+  assert.equal(row.alarmLevelText, '字典配置等级')
   assert.equal(row.relatedOrderNo, 'WO202606101703482640')
   assert.equal(row.workOrderStatusText, '处理中')
 })

+ 2 - 2
tests/waterSupplyWorkOrderStatusDict.test.mjs

@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
 import { readFile } from 'node:fs/promises'
 import test from 'node:test'
 
-import { orderStatusType, orderStatusText } from '../src/views/subSystem/pipeNetwork/ops/workOrder/workOrderDict.js'
+import { orderStatusType, orderStatusText } from '../src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
 
 test('shared work-order dictionary covers the authoritative 0-7 status codes', () => {
   assert.deepEqual(
@@ -20,7 +20,7 @@ test('water supply device pages reuse the shared dictionary instead of a private
   assert.doesNotMatch(source, /function\s+workOrderStatusType/, '页面不应自带工单状态色表')
   assert.match(
     source,
-    /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/workOrderDict\.js'/u,
+    /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/js\/workOrderDict\.js'/u,
     '页面应复用统一工单字典'
   )
 })