浏览代码

refactor(warning): 统一预警状态颜色及标签展示

- 移除 dashboardShared 中 warningStatusTag 函数,改用共享 WarningStatusTag 组件
- 所有预警状态标签统一通过 warningStatusColor 映射颜色
- 统计图表中状态颜色改为从 warningStatus 模块获取
- 首页指标卡及状态分布图表颜色统一为六状态标准色
- 预警查询、发布、关联等页面状态列改用 WarningStatusTag
- 关联页增加设备大类选择器及报警编码传参逻辑修复
- 关联页增加权限校验和加载失败错误提示
- 关联页报警列表查询增加请求版本控制防止竞态
林仔 5 天之前
父节点
当前提交
d38d35e4c6
共有 23 个文件被更改,包括 331 次插入 和 173 次删除
  1. 1 1
      memory/index.md
  2. 1 0
      memory/long-term.md
  3. 9 0
      src/api/smxyjldczxt/yjxxfb/yjfb.js
  4. 13 0
      src/views/subSystem/lifeCompany/components/WarningStatusTag.vue
  5. 78 23
      src/views/subSystem/lifeCompany/lAlertInformation/LCorrelation.vue
  6. 77 60
      src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue
  7. 4 14
      src/views/subSystem/lifeCompany/lAlertInformation/LReportUpload.vue
  8. 4 8
      src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue
  9. 3 2
      src/views/subSystem/lifeCompany/lOverviewWarning/LTodayNot.vue
  10. 3 2
      src/views/subSystem/lifeCompany/lOverviewWarning/Lhistory.vue
  11. 5 3
      src/views/subSystem/lifeCompany/lOverviewWarning/Ltoday.vue
  12. 3 3
      src/views/subSystem/lifeCompany/lOverviewWarning/WarningDetailDrawer.vue
  13. 0 8
      src/views/subSystem/lifeCompany/lOverviewWarning/dashboardShared.js
  14. 6 9
      src/views/subSystem/lifeCompany/lhome/lHome.vue
  15. 2 1
      src/views/subSystem/lifeCompany/lstatistics/disposalEfficiency/index.vue
  16. 2 1
      src/views/subSystem/lifeCompany/lstatistics/disposalStatus/index.vue
  17. 5 3
      src/views/subSystem/lifeCompany/lstatistics/industryAnalysis/index.vue
  18. 2 1
      src/views/subSystem/lifeCompany/lstatistics/receiveEfficiency/index.vue
  19. 4 2
      src/views/subSystem/lifeCompany/lstatistics/today/index.vue
  20. 8 18
      src/views/subSystem/lifeCompany/lwarningInfoCont/LAlertQuery.vue
  21. 4 14
      src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue
  22. 58 0
      src/views/subSystem/lifeCompany/warningAlarmScope.js
  23. 39 0
      src/views/subSystem/lifeCompany/warningStatus.js

+ 1 - 1
memory/index.md

@@ -66,7 +66,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
-- [2026-10-15:预警联动处置系统菜单按钮授权](sessions/2026-10-15.md)
+- [2026-10-15:预警按钮授权、发布人员选择与排序、状态颜色统一及关联页设备大类传参修复](sessions/2026-10-15.md)
 - [2026-10-14:故障通知筛选与两端核实命名、工单取消限制、本人登录日志、照片按组切换、性别单选、移动端接口分层及统一前缀](sessions/2026-10-14.md)
 - [2026-10-13:移动端工单管理、待办、详情与操作接入](sessions/2026-10-13.md)
 - [2026-10-12:工单派单增加核验人](sessions/2026-10-12.md)

+ 1 - 0
memory/long-term.md

@@ -99,6 +99,7 @@
 
 ## 产品级决策
 
+- 预警联动系统事件六状态统一颜色:DRAFT 草稿灰、PENDING 待接收黄、RECEIVED 已接收蓝、PROCESSING 处置中橙、HANDLED 已处置紫、CLOSED 已解除绿。所有事件状态标签和统计图表复用 `lifeCompany/warningStatus.js`,禁止各页面按数据位置自行分配状态色(2026-10-15)。
 - 预警事项关联为可选项,允许人工新增或采集生成的预警不关联事项直接发布;缺少匹配事项不能阻断草稿留存或发布(2026-09-29 用户明确确认)。
 - 预警清单和系统预警的等级下拉统一为一至四级,共用 `alert_level`;原因库和编码生成同步使用该字典。2026-09-29 开发库已补齐原缺少的四级,现有等级标签保留。
 - 正式产品定位为“城市地下管网及设施建设改造智能化系统项目”软件平台中的“地下管网管控平台”;供水交付单元为“供水管网安全运行监测系统”,由本前端与 `E:\pipe-ner-server\pipe-network-service` 后端共同构成。

+ 9 - 0
src/api/smxyjldczxt/yjxxfb/yjfb.js

@@ -1,5 +1,14 @@
 import request from '@/utils/request'
 
+// 预警发布专用人员选择,沿用预警新增/修改/发布权限。
+export function getWarningParticipantDepartments() {
+  return request({ url: '/warning/participants/deptTree', method: 'get' })
+}
+
+export function getWarningParticipants(params) {
+  return request({ url: '/warning/participants/users', method: 'get', params })
+}
+
 function buildWarningFormData(payload = {}) {
   const formData = new FormData()
   const warning = payload.warning || {}

+ 13 - 0
src/views/subSystem/lifeCompany/components/WarningStatusTag.vue

@@ -0,0 +1,13 @@
+<template>
+  <el-tag type="info" effect="light" :style="warningStatusStyle(status)">
+    <slot>{{ warningStatusLabel(status) }}</slot>
+  </el-tag>
+</template>
+
+<script setup>
+import { warningStatusLabel, warningStatusStyle } from '../warningStatus'
+
+defineProps({
+  status: { type: [String, Object], default: '' }
+})
+</script>

+ 78 - 23
src/views/subSystem/lifeCompany/lAlertInformation/LCorrelation.vue

@@ -8,11 +8,13 @@
           <div class="page-subtitle">统一管理报警与预警的关联关系,将设备超指标异常报警与预警信息建立对应,作为发布和处置的重要依据</div>
         </div>
         <div class="header-actions">
-          <el-button type="primary" icon="Refresh" @click="loadAllData">刷新数据</el-button>
+          <el-button type="primary" icon="Refresh" :loading="loading" @click="loadAllData">刷新数据</el-button>
         </div>
       </div>
     </el-card>
 
+    <el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" style="margin-bottom: 16px" />
+
     <div class="summary-grid">
       <el-card shadow="hover" class="summary-card primary">
         <div class="summary-label">报警总数</div>
@@ -38,6 +40,11 @@
 
     <el-card shadow="never" class="page-card">
       <el-form :model="queryParams" :inline="true">
+        <el-form-item label="设备大类">
+          <el-select v-model="selectedAlarmScope" placeholder="请选择设备大类" :disabled="loading || !alarmScopes.length" style="width: 260px" @change="handleScopeChange">
+            <el-option v-for="scope in alarmScopes" :key="scope.value" :label="scope.label" :value="scope.value" />
+          </el-select>
+        </el-form-item>
         <el-form-item label="设备编码">
           <el-input
             v-model="queryParams.deviceCode"
@@ -47,10 +54,11 @@
             @keyup.enter="handleQuery"
           />
         </el-form-item>
-        <el-form-item label="告警类型">
+        <el-form-item label="报警编码">
           <el-input
-            v-model="queryParams.warningType"
-            placeholder="请输入告警类型"
+            v-model="queryParams.warningCode"
+            :placeholder="currentAlarmScope?.warningCode || '请输入报警编码'"
+            :disabled="!!currentAlarmScope?.warningCode"
             clearable
             style="width: 220px"
             @keyup.enter="handleQuery"
@@ -76,7 +84,7 @@
     </el-card>
 
     <el-card shadow="never" class="page-card">
-      <el-table v-loading="loading" :data="pagedAlarmList" @selection-change="handleSelectionChange">
+      <el-table v-loading="loading || alarmLoading" :data="pagedAlarmList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" />
         <el-table-column label="设备编码" prop="deviceCode" min-width="160" />
         <el-table-column label="告警类型" prop="warningTypeText" width="120" />
@@ -206,7 +214,7 @@
           <el-descriptions-item label="告警类型">{{ formatTypeText(warningDetailData.basicInfo.warningType) }}</el-descriptions-item>
           <el-descriptions-item label="预警级别">{{ formatLevelText(warningDetailData.basicInfo.warningLevel) }}</el-descriptions-item>
           <el-descriptions-item label="发布时间">{{ warningDetailData.basicInfo.publishTime || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="状态">{{ warningDetailData.basicInfo.status || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="状态"><WarningStatusTag :status="warningDetailData.basicInfo.status" /></el-descriptions-item>
           <el-descriptions-item label="内容描述" :span="2">{{ warningDetailData.basicInfo.warningContent || "-" }}</el-descriptions-item>
         </el-descriptions>
 
@@ -239,8 +247,11 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { computed, onMounted, reactive, ref } from "vue";
 import auth from '@/plugins/auth';
+import { getRouters } from '@/api/menu';
+import { warningAlarmQuery, warningAlarmScopes } from '../warningAlarmScope';
 import { ElMessage } from "element-plus";
 import {
   getAlarmPageData,
@@ -252,6 +263,12 @@ import {
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
 const loading = ref(false);
+const alarmLoading = ref(false);
+const loadError = ref('');
+const alarmScopes = ref([]);
+const selectedAlarmScope = ref('');
+const currentAlarmScope = computed(() => alarmScopes.value.find(scope => scope.value === selectedAlarmScope.value));
+let alarmRequestVersion = 0;
 const submitLoading = ref(false);
 const warningTypeOptions = ref([]);
 const availableWarnings = ref([]);
@@ -281,7 +298,7 @@ const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
   deviceCode: "",
-  warningType: "",
+  warningCode: "",
   alarmStatus: undefined,
   associationStatus: ""
 });
@@ -428,25 +445,66 @@ async function buildLinkedAlarmMap() {
 }
 
 async function loadAlarms() {
-  const res = await getAlarmPageData({
-    pageNum: queryParams.pageNum,
-    pageSize: queryParams.pageSize,
-    deviceCode: queryParams.deviceCode || undefined,
-    warningType: queryParams.warningType || undefined,
-    alarmStatus: queryParams.alarmStatus
-  });
-  const pageData = res.data || {};
-  const records = pageData.records || [];
-  total.value = pageData.total || 0;
-  rawAlarmList.value = records.map(normalizeAlarmRow);
+  const version = ++alarmRequestVersion;
+  rawAlarmList.value = [];
+  selectedRows.value = [];
+  total.value = 0;
+  if (!currentAlarmScope.value) {
+    alarmLoading.value = false;
+    return;
+  }
+  alarmLoading.value = true;
+  loadError.value = '';
+  try {
+    const res = await getAlarmPageData(warningAlarmQuery(currentAlarmScope.value, queryParams));
+    if (version !== alarmRequestVersion) return;
+    const pageData = res.data || {};
+    total.value = Number(pageData.total || 0);
+    rawAlarmList.value = (pageData.records || []).map(normalizeAlarmRow);
+  } catch (error) {
+    if (version === alarmRequestVersion) loadError.value = error?.msg || error?.message || '报警列表加载失败,请重试';
+  } finally {
+    if (version === alarmRequestVersion) alarmLoading.value = false;
+  }
+}
+
+async function loadAlarmScopes() {
+  if (!auth.hasPermi('alarm:operations:list')) {
+    throw new Error('当前账号未分配报警列表查询权限,请联系管理员分配');
+  }
+  const res = await getRouters();
+  alarmScopes.value = warningAlarmScopes(res.data || []);
+  if (!alarmScopes.value.some(scope => scope.value === selectedAlarmScope.value)) {
+    selectedAlarmScope.value = alarmScopes.value[0]?.value || '';
+  }
+  if (!currentAlarmScope.value) {
+    throw new Error('当前账号没有已授权的报警设备大类,请联系管理员分配对应行业的报警菜单');
+  }
+}
+
+function handleScopeChange() {
+  queryParams.pageNum = 1;
+  queryParams.warningCode = '';
+  loadAlarms();
 }
 
 async function loadAllData() {
+  if (loading.value) return;
   loading.value = true;
+  alarmLoading.value = false;
+  loadError.value = '';
+  rawAlarmList.value = [];
+  selectedRows.value = [];
+  total.value = 0;
+  ++alarmRequestVersion;
   try {
+    await loadAlarmScopes();
+    await loadWarningTypes();
     await loadWarnings();
     await buildLinkedAlarmMap();
     await loadAlarms();
+  } catch (error) {
+    loadError.value = error?.msg || error?.message || '关联数据加载失败,请重试';
   } finally {
     loading.value = false;
   }
@@ -465,7 +523,7 @@ function resetQuery() {
   queryParams.pageNum = 1;
   queryParams.pageSize = 10;
   queryParams.deviceCode = "";
-  queryParams.warningType = "";
+  queryParams.warningCode = "";
   queryParams.alarmStatus = undefined;
   queryParams.associationStatus = "";
   loadAllData();
@@ -561,10 +619,7 @@ function openAlarmDetail(row) {
   alarmDetailVisible.value = true;
 }
 
-onMounted(async () => {
-  await loadWarningTypes();
-  await loadAllData();
-});
+onMounted(loadAllData);
 </script>
 
 <style scoped>

+ 77 - 60
src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue

@@ -89,9 +89,9 @@
         <el-table-column label="发布人" prop="publisher" width="120" />
         <el-table-column label="状态" width="120" align="center">
           <template #default="scope">
-            <el-tag :type="getStatusTagType(scope.row.status)">
+            <WarningStatusTag :status="scope.row.status">
               {{ getStatusText(scope.row.status) }}
-            </el-tag>
+            </WarningStatusTag>
           </template>
         </el-table-column>
         <el-table-column label="操作" fixed="right" width="280" align="center">
@@ -290,7 +290,11 @@
             <span v-else>-</span>
           </el-descriptions-item>
           <el-descriptions-item label="发布时间">{{ formatDateTime(detailData.basicInfo.publishTime) }}</el-descriptions-item>
-          <el-descriptions-item label="状态">{{ getStatusText(detailData.basicInfo.status) }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <WarningStatusTag :status="detailData.basicInfo.status">
+              {{ getStatusText(detailData.basicInfo.status) }}
+            </WarningStatusTag>
+          </el-descriptions-item>
           <el-descriptions-item label="内容描述" :span="2"><span class="warning-content">{{ detailData.basicInfo.warningContent || "-" }}</span></el-descriptions-item>
           <el-descriptions-item label="备注" :span="2">{{ detailData.basicInfo.remark || "-" }}</el-descriptions-item>
         </el-descriptions>
@@ -385,7 +389,10 @@
     </el-dialog>
 
     <!-- 处置人选择对话框 -->
-    <el-dialog v-model="userPickerVisible" :title="pickerTarget === 'reviewer' ? '选择核查人' : '选择处置人'" width="900px" append-to-body @open="loadUserPickerData">
+    <el-dialog v-model="userPickerVisible" :title="pickerTarget === 'reviewer' ? '选择核查人' : '选择处置人'" width="900px" append-to-body @open="loadUserPickerData" @closed="closeUserPicker">
+      <el-alert v-if="userPickerError" :title="userPickerError" type="error" :closable="false" show-icon style="margin-bottom: 12px">
+        <el-button link type="primary" @click="loadUserPickerData">重新加载</el-button>
+      </el-alert>
       <div class="user-picker-container">
         <!-- 左侧部门树 -->
         <div class="user-picker-left">
@@ -394,6 +401,7 @@
           <el-tree
             ref="deptTreeRef"
             :data="deptTree"
+            node-key="id"
             :props="{ label: 'label', children: 'children' }"
             :filter-node-method="filterDeptNode"
             :highlight-current="true"
@@ -407,25 +415,28 @@
             用户列表
             <span v-if="currentDeptName" class="current-dept">({{ currentDeptName }})</span>
           </div>
-          <el-input v-model="userFilterText" placeholder="搜索用户" clearable style="margin-bottom: 12px;" @input="filterUserList" />
+          <el-input v-model="userFilterText" placeholder="搜索账号或姓名,回车查询" maxlength="100" clearable style="margin-bottom: 12px;" @keyup.enter="searchPickerUsers" @clear="searchPickerUsers">
+            <template #append><el-button :icon="Search" @click="searchPickerUsers" /></template>
+          </el-input>
           <el-table
             v-loading="userPickerLoading"
-            :data="filteredUserList"
+            :data="userList"
             highlight-current-row
             @current-change="handleUserCurrentChange"
-            height="400"
+            height="320"
+            empty-text="当前部门没有可选的启用人员,可切换其他部门或全部部门"
           >
             <el-table-column label="用户名称" prop="userName" min-width="120" show-overflow-tooltip />
             <el-table-column label="用户昵称" prop="nickName" min-width="120" show-overflow-tooltip />
             <el-table-column label="部门" prop="deptName" min-width="140" show-overflow-tooltip />
-            <el-table-column label="手机号码" prop="phonenumber" width="130" />
           </el-table>
+          <pagination v-show="userTotal > 0" :total="userTotal" v-model:page="userPage.pageNum" v-model:limit="userPage.pageSize" :auto-scroll="false" layout="total, prev, pager, next" @pagination="loadUserList" />
         </div>
       </div>
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="userPickerVisible = false">取 消</el-button>
-          <el-button type="primary" :disabled="!selectedUser" @click="confirmSelectUser">确 定</el-button>
+          <el-button type="primary" :disabled="!selectedUser || userPickerLoading || !!userPickerError" @click="confirmSelectUser">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -433,6 +444,7 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import WarningItemSummary from '../components/WarningItemSummary.vue';
@@ -443,7 +455,6 @@ import { parseTime } from "@/utils/ruoyi";
 import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
 import WarningAttachments from "../components/WarningAttachments.vue";
 import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
-import { deptTreeSelect, listUser } from "@/api/system/user";
 import { listEquipment, listEquipmentTypeAll } from "@/api/pipeNetwork/pdeviceInform";
 import {
   deleteYjxxData,
@@ -452,6 +463,8 @@ import {
   getWarningDetailData,
   getWarningReasonListData,
   getWarningItemListData,
+  getWarningParticipantDepartments,
+  getWarningParticipants,
   releaseYjxxData,
   submitYjxxData,
   updateYjxxData
@@ -498,7 +511,13 @@ const deviceQuery = reactive({
 // 用户选择器相关
 const deptTree = ref([]);
 const userList = ref([]);
-const filteredUserList = ref([]);
+const userTotal = ref(0);
+const userPage = reactive({ pageNum: 1, pageSize: 10 });
+const deptPickerError = ref("");
+const userListError = ref("");
+const userPickerError = computed(() => deptPickerError.value || userListError.value);
+let userRequestVersion = 0;
+let deptRequestVersion = 0;
 const selectedDeptId = ref("");
 const currentDeptName = ref("");
 const selectedUser = ref(null);
@@ -649,18 +668,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getStatusTagType(status) {
-  // 按照预警状态语义设置 tag 颜色
-  return {
-    DRAFT: "info",        // 草稿 - 灰色(未发布)
-    PENDING: "warning",   // 待办 - 黄色(等待处置)
-    RECEIVED: "warning",  // 已接收 - 黄色
-    PROCESSING: "warning", // 处置中 - 黄色(进行中)
-    HANDLED: "success",   // 已处置 - 绿色(已完成)
-    CLOSED: "info"        // 已解除 - 灰色
-  }[status] || "info";
-}
-
 function normalizeWarningRow(item) {
   const row = { ...item };
   row.itemId = item.itemId || item.item_id || "";
@@ -775,6 +782,7 @@ async function loadList() {
       warningNo: queryParams.warningNo,
       warningType: queryParams.warningType,
       warningLevel: queryParams.warningLevel,
+      releaseOrder: true,
       startTime: queryParams.dateRange?.[0] || "",
       endTime: queryParams.dateRange?.[1] || ""
     };
@@ -816,57 +824,61 @@ function filterDeptNode(value, data) {
 
 // 用户选择器相关函数
 async function loadUserPickerData() {
-  userPickerLoading.value = true;
+  const version = ++deptRequestVersion;
+  deptPickerError.value = "";
+  userList.value = [];
+  userTotal.value = 0;
+  selectedUser.value = null;
+  userPage.pageNum = 1;
+  selectedDeptId.value = "";
+  currentDeptName.value = "全部部门";
+  deptTree.value = [];
+  const users = loadUserList();
   try {
-    await loadDeptTree();
-    // 默认加载第一个部门的用户
-    if (deptTree.value.length > 0) {
-      const firstDept = deptTree.value[0];
-      selectedDeptId.value = firstDept.id;
-      currentDeptName.value = firstDept.label;
-      await loadUserList(firstDept.id);
-    }
-  } finally {
-    userPickerLoading.value = false;
+    const res = await getWarningParticipantDepartments();
+    if (version !== deptRequestVersion || !userPickerVisible.value) return;
+    deptTree.value = [{ id: '', label: '全部部门', children: res.data || [] }];
+  } catch {
+    if (version === deptRequestVersion && userPickerVisible.value) deptPickerError.value = "部门加载失败,请重新加载";
   }
+  await users;
 }
 
-async function loadDeptTree() {
-  const res = await deptTreeSelect();
-  deptTree.value = res.data || [];
-}
-
-async function loadUserList(deptId) {
+async function loadUserList() {
+  const version = ++userRequestVersion;
   userPickerLoading.value = true;
+  userListError.value = "";
+  selectedUser.value = null;
+  userList.value = [];
+  userTotal.value = 0;
   try {
-    const res = await listUser({ deptId, pageNum: 1, pageSize: 100 });
-    userList.value = res.rows || [];
-    filteredUserList.value = userList.value;
+    const res = await getWarningParticipants({ ...userPage, deptId: selectedDeptId.value || undefined, keyword: userFilterText.value.trim() || undefined });
+    if (version !== userRequestVersion || !userPickerVisible.value) return;
+    userList.value = res.data?.records || [];
+    userTotal.value = Number(res.data?.total || 0);
+  } catch {
+    if (version === userRequestVersion && userPickerVisible.value) userListError.value = "人员加载失败,请重新加载";
   } finally {
-    userPickerLoading.value = false;
+    if (version === userRequestVersion) userPickerLoading.value = false;
   }
 }
 
 function handleDeptClick(data) {
   selectedDeptId.value = data.id;
   currentDeptName.value = data.label;
-  loadUserList(data.id);
-  selectedUser.value = null;
-  userFilterText.value = "";
+  searchPickerUsers();
 }
 
-function filterUserList() {
-  const keyword = userFilterText.value.toLowerCase();
-  if (!keyword) {
-    filteredUserList.value = userList.value;
-    return;
-  }
-  filteredUserList.value = userList.value.filter((user) => {
-    const userName = (user.userName || "").toLowerCase();
-    const nickName = (user.nickName || "").toLowerCase();
-    const deptName = (user.deptName || "").toLowerCase();
-    return userName.includes(keyword) || nickName.includes(keyword) || deptName.includes(keyword);
-  });
+function searchPickerUsers() {
+  userPage.pageNum = 1;
+  loadUserList();
+}
+
+function closeUserPicker() {
+  ++userRequestVersion;
+  ++deptRequestVersion;
+  selectedUser.value = null;
+  userPickerLoading.value = false;
 }
 
 function handleUserCurrentChange(row) {
@@ -890,7 +902,7 @@ function openReviewerPicker() {
 }
 
 function confirmSelectUser() {
-  if (!selectedUser.value) {
+  if (!selectedUser.value || userPickerLoading.value || userPickerError.value) {
     ElMessage.warning("请选择用户");
     return;
   }
@@ -1183,6 +1195,11 @@ onMounted(async () => {
   overflow: hidden;
 }
 
+.user-picker-right :deep(.pagination-container) {
+  padding: 12px 0 0;
+  margin-top: 0;
+}
+
 .picker-title {
   font-size: 14px;
   font-weight: 600;

+ 4 - 14
src/views/subSystem/lifeCompany/lAlertInformation/LReportUpload.vue

@@ -149,9 +149,9 @@
             <el-descriptions-item label="发布时间">{{ currentBasicInfo.publishTime || "-" }}</el-descriptions-item>
             <el-descriptions-item label="权属单位">{{ currentBasicInfo.ownershipUnit || "-" }}</el-descriptions-item>
             <el-descriptions-item label="状态">
-              <el-tag :type="getStatusTagType(currentBasicInfo.status)">
+              <WarningStatusTag :status="currentBasicInfo.status">
                 {{ getStatusText(currentBasicInfo.status) }}
-              </el-tag>
+              </WarningStatusTag>
             </el-descriptions-item>
           </el-descriptions>
         </el-card>
@@ -193,7 +193,7 @@
                 <el-descriptions-item label="设备编码">{{ currentBasicInfo.deviceCode || "-" }}</el-descriptions-item>
                 <el-descriptions-item label="权属单位">{{ currentBasicInfo.ownershipUnit || "-" }}</el-descriptions-item>
                 <el-descriptions-item label="处置人">{{ currentBasicInfo.handler || "-" }}</el-descriptions-item>
-                <el-descriptions-item label="状态">{{ currentBasicInfo.statusText || currentBasicInfo.status || "-" }}</el-descriptions-item>
+                <el-descriptions-item label="状态"><WarningStatusTag :status="currentBasicInfo.status">{{ getStatusText(currentBasicInfo.status) }}</WarningStatusTag></el-descriptions-item>
               </el-descriptions>
             </el-tab-pane>
 
@@ -246,6 +246,7 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
@@ -368,17 +369,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getStatusTagType(status) {
-  return {
-    DRAFT: "info",
-    PENDING: "warning",
-    RECEIVED: "warning",
-    PROCESSING: "warning",
-    HANDLED: "success",
-    CLOSED: "info"
-  }[status] || "info";
-}
-
 function validateFile(file) {
   const allowExt = [".pdf", ".doc", ".docx", ".zip"];
   const lowerName = file.name.toLowerCase();

+ 4 - 8
src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue

@@ -137,9 +137,9 @@
         </el-table-column>
         <el-table-column label="状态" width="100" align="center">
           <template #default="scope">
-            <el-tag :type="getStatusTagType(scope.row.status)">
+            <WarningStatusTag :status="scope.row.status">
               {{ getStatusText(scope.row.status) }}
-            </el-tag>
+            </WarningStatusTag>
           </template>
         </el-table-column>
         <el-table-column label="操作" fixed="right" width="300" align="center">
@@ -167,6 +167,7 @@
         <WarningItemSummary :warning="detailData.basicInfo" />
         <el-descriptions :column="2" border>
           <el-descriptions-item label="预警编号">{{ detailBasic.warningNo || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="状态"><WarningStatusTag :status="detailData.basicInfo.status">{{ getStatusText(detailData.basicInfo.status) }}</WarningStatusTag></el-descriptions-item>
           <el-descriptions-item label="预警专项">{{ detailBasic.warningSpecial || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警类型">{{ detailBasic.warningTypeText || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警级别">{{ detailBasic.warningLevelText || "-" }}</el-descriptions-item>
@@ -263,6 +264,7 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import WarningItemSummary from '../components/WarningItemSummary.vue';
@@ -347,12 +349,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getStatusTagType(status) {
-  if (status === "CLOSED") return "success";
-  if (status === "DRAFT" || status === "PENDING") return "info";
-  return "warning";
-}
-
 const detailBasic = computed(() => {
   const basic = detailData.value.basicInfo || {};
   const warningType = basic.warningType || basic.warning_type || "";

+ 3 - 2
src/views/subSystem/lifeCompany/lOverviewWarning/LTodayNot.vue

@@ -18,7 +18,7 @@
         <el-table-column label="权属单位" min-width="155" show-overflow-tooltip><template #default="{ row }">{{ row.ownershipUnit || row.ownership_unit || '未设置' }}</template></el-table-column>
         <el-table-column label="处置人" width="115"><template #default="{ row }">{{ row.todoInfo?.userName || row.todo_info?.user_name || '待分配' }}</template></el-table-column>
         <el-table-column label="待办时间" min-width="165"><template #default="{ row }">{{ formatDashboardTime(row.todoInfo?.createTime || row.todo_info?.create_time || row.publishTime) }}</template></el-table-column>
-        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><el-tag :type="warningStatusTag(row)" effect="plain">{{ warningStatusText(row) }}</el-tag></template></el-table-column>
+        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><WarningStatusTag :status="row">{{ warningStatusText(row) }}</WarningStatusTag></template></el-table-column>
         <el-table-column label="操作" width="90" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click.stop="openDetail(row)">查看详情</el-button></template></el-table-column>
       </el-table>
       <el-pagination v-if="total" v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" :total="total" class="pagination" @current-change="loadPage" @size-change="handleSizeChange" />
@@ -28,6 +28,7 @@
 </template>
 
 <script setup name="WarningTodayUnprocessed">
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { loadWarningReferences } from '../lstatistics/statisticsShared'
 import { computed, onMounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
@@ -35,7 +36,7 @@ import { Refresh, Search } from '@element-plus/icons-vue'
 import { pageWarningTodayUnprocessed } from '@/api/warning/dashboard'
 import { warningTypeOptions, warningLevelLabels } from '../lstatistics/statisticsShared'
 import WarningDetailDrawer from './WarningDetailDrawer.vue'
-import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningLevelCode, warningLevelTag, warningLevelText, warningStatusTag, warningStatusText, warningTypeText } from './dashboardShared'
+import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningLevelCode, warningLevelTag, warningLevelText, warningStatusText, warningTypeText } from './dashboardShared'
 
 const loading=ref(false);const tableLoading=ref(false);const errorMessage=ref('');const keyword=ref('');const selectedType=ref('');const selectedLevel=ref('');const pageNum=ref(1);const pageSize=ref(10);const rows=ref([]);const total=ref(0);const ownershipCount=ref(0);const highestLevelValue=ref(0);const generatedAt=ref('');const detailVisible=ref(false);const selectedRow=ref(null)
 const typeOptions = warningTypeOptions;const levelOptions = computed(() => Object.entries(warningLevelLabels).map(([value, label]) => ({ value, label })));const highestLevel=computed(()=>warningLevelLabels[String(highestLevelValue.value)]||'-');

+ 3 - 2
src/views/subSystem/lifeCompany/lOverviewWarning/Lhistory.vue

@@ -15,7 +15,7 @@
         <el-table-column label="预警级别" width="105" align="center"><template #default="{ row }"><el-tag :type="warningLevelTag(row)">{{ warningLevelText(row) }}</el-tag></template></el-table-column>
         <el-table-column label="权属单位" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.ownershipUnit || row.ownership_unit || '未设置' }}</template></el-table-column>
         <el-table-column label="发布时间" min-width="165"><template #default="{ row }">{{ formatDashboardTime(row.publishTime || row.publish_time || row.createTime) }}</template></el-table-column>
-        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><el-tag :type="warningStatusTag(row)" effect="plain">{{ warningStatusText(row) }}</el-tag></template></el-table-column>
+        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><WarningStatusTag :status="row">{{ warningStatusText(row) }}</WarningStatusTag></template></el-table-column>
         <el-table-column label="操作" width="90" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click.stop="openDetail(row)">查看详情</el-button></template></el-table-column>
       </el-table>
       <el-pagination v-if="total" v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" :total="total" class="pagination" @current-change="loadPage" @size-change="handleSizeChange" />
@@ -25,6 +25,7 @@
 </template>
 
 <script setup name="WarningHistoryOverview">
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { loadWarningReferences } from '../lstatistics/statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
@@ -32,7 +33,7 @@ import { Refresh, Search } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { getWarningHistoryOverview, pageWarningHistory } from '@/api/warning/dashboard'
 import WarningDetailDrawer from './WarningDetailDrawer.vue'
-import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningLevelTag, warningLevelText, warningStatusTag, warningStatusText, warningTypeText } from './dashboardShared'
+import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningLevelTag, warningLevelText, warningStatusText, warningTypeText } from './dashboardShared'
 
 const loading=ref(false);const tableLoading=ref(false);const errorMessage=ref('');const result=ref({totalCount:0,specialStats:[],detailList:[]});const keyword=ref('');const selectedSpecial=ref('');const pageNum=ref(1);const pageSize=ref(10);const rows=ref([]);const total=ref(0);const chartRef=ref();const detailVisible=ref(false);const selectedRow=ref(null);let chart
 const totalCount=computed(()=>Number(result.value.totalCount||0));const specialStats=computed(()=>(result.value.specialStats||[]).map(item=>({specialName:item.specialName||item.special_name||'未分类',count:Number(item.count||0),percentage:Number(item.percentage||0)})))

+ 5 - 3
src/views/subSystem/lifeCompany/lOverviewWarning/Ltoday.vue

@@ -7,7 +7,7 @@
     <el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon closable class="error-alert" @close="errorMessage = ''" />
     <div class="metric-grid">
       <button class="metric-card danger" :class="{ active: activeTab === 'unresolved' }" type="button" @click="selectTab('unresolved')"><span class="metric-label">仍未解除</span><strong>{{ unresolvedCount }}</strong><small>截至当前仍需关注的预警</small></button>
-      <button class="metric-card success" :class="{ active: activeTab === 'resolved' }" type="button" @click="selectTab('resolved')"><span class="metric-label">今日已解除</span><strong>{{ resolvedCount }}</strong><small>今日完成闭环的预警数量</small></button>
+      <button class="metric-card success" :class="{ active: activeTab === 'resolved' }" type="button" @click="selectTab('resolved')"><span class="metric-label">今日已解除</span><strong :style="{ color: warningStatusColor('CLOSED') }">{{ resolvedCount }}</strong><small>今日完成闭环的预警数量</small></button>
       <button class="metric-card primary" :class="{ active: activeTab === 'all' }" type="button" @click="selectTab('all')"><span class="metric-label">今日预警总量</span><strong>{{ totalCount }}</strong><small>今天发布的全部非草稿预警</small></button>
     </div>
     <el-card shadow="never" class="table-card">
@@ -17,7 +17,7 @@
         <el-table-column label="预警类型" width="120"><template #default="{ row }">{{ warningTypeText(row) }}</template></el-table-column>
         <el-table-column label="预警专项" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.warningSpecial || row.warning_special || '未设置' }}</template></el-table-column>
         <el-table-column label="发布时间" min-width="165"><template #default="{ row }">{{ formatDashboardTime(row.publishTime || row.publish_time || row.createTime) }}</template></el-table-column>
-        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><el-tag :type="warningStatusTag(row)">{{ warningStatusText(row) }}</el-tag></template></el-table-column>
+        <el-table-column label="状态" width="105" align="center"><template #default="{ row }"><WarningStatusTag :status="row">{{ warningStatusText(row) }}</WarningStatusTag></template></el-table-column>
         <el-table-column label="操作" width="90" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click.stop="openDetail(row)">查看详情</el-button></template></el-table-column>
       </el-table>
       <el-pagination v-if="total" v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" :total="total" class="pagination" @current-change="loadPage" @size-change="handleSizeChange" />
@@ -27,6 +27,8 @@
 </template>
 
 <script setup name="WarningTodayOverview">
+import WarningStatusTag from '../components/WarningStatusTag.vue'
+import { warningStatusColor } from '../warningStatus'
 import { loadWarningReferences } from '../lstatistics/statisticsShared'
 import { computed, onMounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
@@ -34,7 +36,7 @@ import { Refresh, Search } from '@element-plus/icons-vue'
 import { getWarningTodayOverview, pageWarningTodayOverview } from '@/api/warning/dashboard'
 import { warningTypeOptions, warningLevelLabels } from '../lstatistics/statisticsShared'
 import WarningDetailDrawer from './WarningDetailDrawer.vue'
-import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningStatusTag, warningStatusText, warningTypeText } from './dashboardShared'
+import { dashboardData, formatDashboardTime, pageRecords, pageTotal, warningStatusText, warningTypeText } from './dashboardShared'
 
 const loading = ref(false); const tableLoading = ref(false); const errorMessage = ref(''); const activeTab = ref('unresolved'); const keyword = ref(''); const warningType = ref(''); const warningLevel = ref(''); const pageNum = ref(1); const pageSize = ref(10); const detailVisible = ref(false); const selectedRow = ref(null); const rows = ref([]); const total = ref(0)
 const stats = ref({ todayTotal: 0, unresolvedCount: 0, resolvedCount: 0 })

+ 3 - 3
src/views/subSystem/lifeCompany/lOverviewWarning/WarningDetailDrawer.vue

@@ -6,7 +6,7 @@
         <div class="detail-title">{{ detail.warningNo }}</div>
         <div class="detail-meta">
           <el-tag :type="levelTag">{{ detail.warningLevel }}</el-tag>
-          <el-tag :type="statusTag" effect="plain">{{ detail.status }}</el-tag>
+          <WarningStatusTag :status="row">{{ detail.status }}</WarningStatusTag>
           <span>{{ detail.warningNo || detail.warningId || '暂无编号' }}</span>
         </div>
       </div>
@@ -39,8 +39,9 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { computed } from 'vue'
-import { detailView, formatDashboardTime, warningLevelTag, warningStatusTag } from './dashboardShared'
+import { detailView, formatDashboardTime, warningLevelTag } from './dashboardShared'
 
 const props = defineProps({
   modelValue: { type: Boolean, default: false },
@@ -53,7 +54,6 @@ const visible = computed({
 })
 const detail = computed(() => detailView(props.row || {}))
 const levelTag = computed(() => warningLevelTag(props.row || {}))
-const statusTag = computed(() => warningStatusTag(props.row || {}))
 </script>
 
 <style scoped lang="scss">

+ 0 - 8
src/views/subSystem/lifeCompany/lOverviewWarning/dashboardShared.js

@@ -55,14 +55,6 @@ export function warningLevelTag(row) {
   return 'info'
 }
 
-export function warningStatusTag(row) {
-  const value = String(listValue(row, 'status', 'warningStatus', 'warning_status')).toUpperCase()
-  if (['CLOSED', 'HANDLED'].includes(value)) return 'success'
-  if (['PENDING', 'RECEIVED', 'PROCESSING'].includes(value)) return 'warning'
-  if (value === 'DRAFT') return 'info'
-  return 'primary'
-}
-
 export function formatDashboardTime(value, fallback = '-') {
   if (!value) return fallback
   const date = value instanceof Date ? value : new Date(value)

+ 6 - 9
src/views/subSystem/lifeCompany/lhome/lHome.vue

@@ -156,9 +156,9 @@
           </el-table-column>
           <el-table-column label="状态" width="94" align="center">
             <template #default="{ row }">
-              <el-tag :type="warningStatusTag(row)" effect="plain" size="small">
+              <WarningStatusTag :status="row" size="small">
                 {{ warningStatusText(row) }}
-              </el-tag>
+              </WarningStatusTag>
             </template>
           </el-table-column>
           <el-table-column label="操作" width="84" fixed="right" align="center">
@@ -199,6 +199,8 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
+import { warningStatusAppearance, warningStatusColor } from '../warningStatus'
 import { loadWarningReferences } from '../lstatistics/statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { useRouter } from 'vue-router'
@@ -230,7 +232,6 @@ import {
   warningLevelCode,
   warningLevelTag,
   warningLevelText,
-  warningStatusTag,
   warningStatusText,
   warningTypeText
 } from '../lOverviewWarning/dashboardShared'
@@ -292,7 +293,7 @@ const metricCards = computed(() => [
   {
     key: 'resolved', label: '今日已解除', value: numericValue(todayOverview.value.resolvedCount),
     note: '今日完成风险解除', routeName: 'WarningTodayOverview', icon: CircleCheck,
-    color: '#059669', softColor: '#ecfdf5'
+    color: warningStatusColor('CLOSED'), softColor: warningStatusAppearance('CLOSED').background
   },
   {
     key: 'unprocessed', label: '未处置预警', value: unprocessedTotal.value,
@@ -407,10 +408,6 @@ function renderStatusChart() {
     statusChart.clear()
     return
   }
-  const colorByStatus = {
-    PENDING: '#dc2626', RECEIVED: '#2563eb', PROCESSING: '#d97706',
-    HANDLED: '#0f766e', CLOSED: '#059669'
-  }
   statusChart.setOption({
     tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 条({d}%)' },
     legend: { type: 'scroll', bottom: 0, left: 'center', textStyle: { color: '#64748b' } },
@@ -420,7 +417,7 @@ function renderStatusChart() {
       emphasis: { label: { show: true, fontSize: 14, fontWeight: 600 } },
       data: data.map(item => ({
         name: item.name, value: item.value,
-        itemStyle: { color: colorByStatus[item.status] || '#64748b' }
+        itemStyle: { color: warningStatusColor(item.status) }
       }))
     }]
   }, true)

+ 2 - 1
src/views/subSystem/lifeCompany/lstatistics/disposalEfficiency/index.vue

@@ -4,10 +4,11 @@
     <el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon closable @close="errorMessage = ''" />
     <div class="metric-grid"><div v-for="metric in metrics" :key="metric.label" class="metric-card"><span>{{ metric.label }}</span><strong>{{ metric.value }}</strong><small>{{ metric.note }}</small></div></div>
     <div class="chart-grid"><el-card shadow="never"><template #header><div class="card-title"><span>处置时效区间</span><em>小时</em></div></template><div ref="rangeRef" class="chart-box" /></el-card><el-card shadow="never"><template #header><div class="card-title"><span>专项平均处置时效</span><em>高时效优先督导</em></div></template><div ref="specialRef" class="chart-box" /></el-card></div>
-    <el-card shadow="never" class="table-card"><template #header><div class="card-title"><span>已处置预警明细</span><em>{{ total }} 条</em></div></template><el-table v-loading="loading" :data="rows" stripe><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column label="预警专项" min-width="140"><template #default="{row}">{{ row.warning_special || row.warningSpecial || '-' }}</template></el-table-column><el-table-column label="发布时间" min-width="160"><template #default="{row}">{{ row.publish_time || row.publishTime || '-' }}</template></el-table-column><el-table-column label="处置时长" width="120"><template #default="{row}"><b>{{ row.dispose_hours ?? row.disposeHours ?? '-' }}</b> 小时</template></el-table-column><el-table-column label="状态" width="100"><template #default="{row}"><el-tag type="success">{{ row.status === 'CLOSED' ? '已解除' : '已处置' }}</el-tag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
+    <el-card shadow="never" class="table-card"><template #header><div class="card-title"><span>已处置预警明细</span><em>{{ total }} 条</em></div></template><el-table v-loading="loading" :data="rows" stripe><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column label="预警专项" min-width="140"><template #default="{row}">{{ row.warning_special || row.warningSpecial || '-' }}</template></el-table-column><el-table-column label="发布时间" min-width="160"><template #default="{row}">{{ row.publish_time || row.publishTime || '-' }}</template></el-table-column><el-table-column label="处置时长" width="120"><template #default="{row}"><b>{{ row.dispose_hours ?? row.disposeHours ?? '-' }}</b> 小时</template></el-table-column><el-table-column label="状态" width="100"><template #default="{row}"><WarningStatusTag :status="row.status" /></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
   </div>
 </template>
 <script setup name="WarningDisposeEfficiency">
+import WarningStatusTag from "../../components/WarningStatusTag.vue"
 import { loadWarningReferences } from '../statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'

+ 2 - 1
src/views/subSystem/lifeCompany/lstatistics/disposalStatus/index.vue

@@ -8,6 +8,7 @@
   </div>
 </template>
 <script setup name="WarningDisposeStatus">
+import { warningStatusColor } from '../../warningStatus'
 import { loadWarningReferences } from '../statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
@@ -18,7 +19,7 @@ import { defaultRange, disposalTypeLabels, labelOf, rangeParams, responseData, s
 const dateRange = ref(defaultRange()); const warningSpecial = ref(''); const loading = ref(false); const errorMessage = ref(''); const statusRows = ref([]); const typeRows = ref([]); const statusChartRef = ref(); const typeChartRef = ref(); let statusChart; let typeChart
 const count = rows => rows.reduce((total, row) => total + Number(row.status_count ?? row.statusCount ?? row.disposal_count ?? row.disposalCount ?? 0), 0)
 const metrics = computed(() => [{ label: '统计预警数', value: count(statusRows.value), note: '当前筛选周期' }, { label: '状态类型', value: statusRows.value.length, note: '非草稿状态' }, { label: '处置动作数', value: count(typeRows.value), note: '已记录动作' }, { label: '主要状态', value: statusRows.value[0] ? labelOf(statusLabels, statusRows.value[0].status) : '-', note: '数量最多' }])
-function renderCharts() { if (!statusChartRef.value || !typeChartRef.value) return; statusChart ||= echarts.init(statusChartRef.value); typeChart ||= echarts.init(typeChartRef.value); statusChart.setOption({ color: ['#2563eb', '#0f766e', '#f59e0b', '#ef4444'], tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['45%', '72%'], label: { formatter: '{b}\n{c} 条' }, data: statusRows.value.map(row => ({ name: labelOf(statusLabels, row.status), value: Number(row.status_count ?? row.statusCount ?? 0) })) }] }); typeChart.setOption({ color: ['#2563eb', '#0f766e', '#f59e0b', '#8b5cf6'], tooltip: { trigger: 'axis' }, grid: { left: 44, right: 24, top: 24, bottom: 36 }, xAxis: { type: 'category', data: typeRows.value.map(row => labelOf(disposalTypeLabels, row.disposal_type ?? row.disposalType)), axisLabel: { interval: 0 } }, yAxis: { type: 'value', minInterval: 1 }, series: [{ type: 'bar', barWidth: 32, data: typeRows.value.map(row => Number(row.disposal_count ?? row.disposalCount ?? 0)), itemStyle: { borderRadius: [6, 6, 0, 0] } }] }) }
+function renderCharts() { if (!statusChartRef.value || !typeChartRef.value) return; statusChart ||= echarts.init(statusChartRef.value); typeChart ||= echarts.init(typeChartRef.value); statusChart.setOption({ tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['45%', '72%'], label: { formatter: '{b}\n{c} 条' }, data: statusRows.value.map(row => ({ name: labelOf(statusLabels, row.status), value: Number(row.status_count ?? row.statusCount ?? 0), itemStyle: { color: warningStatusColor(row.status) } })) }] }); typeChart.setOption({ color: ['#2563eb', '#0f766e', '#f59e0b', '#8b5cf6'], tooltip: { trigger: 'axis' }, grid: { left: 44, right: 24, top: 24, bottom: 36 }, xAxis: { type: 'category', data: typeRows.value.map(row => labelOf(disposalTypeLabels, row.disposal_type ?? row.disposalType)), axisLabel: { interval: 0 } }, yAxis: { type: 'value', minInterval: 1 }, series: [{ type: 'bar', barWidth: 32, data: typeRows.value.map(row => Number(row.disposal_count ?? row.disposalCount ?? 0)), itemStyle: { borderRadius: [6, 6, 0, 0] } }] }) }
 async function loadData() { loading.value = true; errorMessage.value = ''; try { await loadWarningReferences(); const params = { ...rangeParams(dateRange.value), warningSpecial: warningSpecial.value || undefined }; const [statusResponse, typeResponse] = await Promise.all([getDisposeStatusStat(params), getDisposalTypeStat(params)]); statusRows.value = responseData(statusResponse, []); typeRows.value = responseData(typeResponse, []); await nextTick(); renderCharts() } catch (error) { errorMessage.value = error?.message || '统计数据加载失败'; ElMessage.error(errorMessage.value) } finally { loading.value = false } }
 function reset() { dateRange.value = defaultRange(); warningSpecial.value = ''; loadData() }
 function resize() { statusChart?.resize(); typeChart?.resize() }

+ 5 - 3
src/views/subSystem/lifeCompany/lstatistics/industryAnalysis/index.vue

@@ -70,7 +70,7 @@
         </el-table-column>
         <el-table-column label="发布时间" min-width="165"><template #default="{ row }">{{ row.publishTime ? formatDateTime(row.publishTime) : '-' }}</template></el-table-column>
         <el-table-column label="状态" min-width="155" align="center">
-          <template #default="{ row }"><el-tag>{{ statusLabel(row.status) }}</el-tag></template>
+          <template #default="{ row }"><WarningStatusTag :status="row.status">{{ statusLabel(row.status) }}</WarningStatusTag></template>
         </el-table-column>
         <el-table-column label="处置时长" width="110"><template #default="{ row }">{{ row.disposeHours == null ? '-' : `${row.disposeHours} 小时` }}</template></el-table-column>
         <el-table-column label="设备" min-width="150" show-overflow-tooltip>
@@ -83,6 +83,8 @@
 </template>
 
 <script setup name="WarningIndustryAnalysis">
+import WarningStatusTag from "../../components/WarningStatusTag.vue"
+import { warningStatusColor } from '../../warningStatus'
 import { loadWarningReferences } from '../statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
@@ -157,12 +159,12 @@ function renderCharts() {
     grid: { left: 46, right: 18, top: 65, bottom: 42 },
     xAxis: { type: 'category', data: industries },
     yAxis: { type: 'value', minInterval: 1, name: '预警数' },
-    series: statuses.map((currentStatus, index) => ({
+    series: statuses.map(currentStatus => ({
       name: statusLabel(currentStatus),
       type: 'bar',
       stack: 'status',
       barMaxWidth: 36,
-      itemStyle: { color: ['#f59e0b', '#2563eb', '#0f766e', '#7c3aed', '#16a34a'][index] },
+      itemStyle: { color: warningStatusColor(currentStatus) },
       data: industries.map(industry => {
         const row = statusRows.value.find(item => (item.industry_name || item.industryName || '未设置') === industry && (item.status || '') === currentStatus)
         return number(row?.warning_count ?? row?.warningCount)

+ 2 - 1
src/views/subSystem/lifeCompany/lstatistics/receiveEfficiency/index.vue

@@ -4,10 +4,11 @@
     <el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon closable @close="errorMessage = ''" />
     <div class="metric-grid"><div v-for="metric in metrics" :key="metric.label" class="metric-card"><span>{{ metric.label }}</span><strong>{{ metric.value }}</strong><small>{{ metric.note }}</small></div></div>
     <div class="chart-grid"><el-card shadow="never"><template #header><div class="card-title"><span>接警时效区间</span><em>从发布时间至首次接收</em></div></template><div ref="rangeRef" class="chart-box" /></el-card><el-card shadow="never"><template #header><div class="card-title"><span>按类型平均接警时效</span><em>小时</em></div></template><div ref="typeRef" class="chart-box" /></el-card></div>
-    <el-card shadow="never"><template #header><div class="card-title"><span>接警效率明细</span><em>{{ total }} 条</em></div></template><el-table v-loading="loading" :data="rows" stripe><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column label="类型" min-width="120"><template #default="{row}">{{ typeLabel(row.warning_type||row.warningType) }}</template></el-table-column><el-table-column label="首次接警" min-width="160"><template #default="{row}">{{ row.receive_time||row.receiveTime||'-' }}</template></el-table-column><el-table-column label="接警时长" width="120"><template #default="{row}"><b>{{ row.receive_hours??row.receiveHours??'-' }}</b> 小时</template></el-table-column><el-table-column label="状态" width="100"><template #default="{row}"><el-tag>{{ statusLabels[row.status]||row.status }}</el-tag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
+    <el-card shadow="never"><template #header><div class="card-title"><span>接警效率明细</span><em>{{ total }} 条</em></div></template><el-table v-loading="loading" :data="rows" stripe><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column label="类型" min-width="120"><template #default="{row}">{{ typeLabel(row.warning_type||row.warningType) }}</template></el-table-column><el-table-column label="首次接警" min-width="160"><template #default="{row}">{{ row.receive_time||row.receiveTime||'-' }}</template></el-table-column><el-table-column label="接警时长" width="120"><template #default="{row}"><b>{{ row.receive_hours??row.receiveHours??'-' }}</b> 小时</template></el-table-column><el-table-column label="状态" width="100"><template #default="{row}"><WarningStatusTag :status="row.status">{{ statusLabels[row.status] || row.status }}</WarningStatusTag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
   </div>
 </template>
 <script setup name="WarningReceiveEfficiency">
+import WarningStatusTag from "../../components/WarningStatusTag.vue"
 import { loadWarningReferences } from '../statisticsShared'
 import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'

+ 4 - 2
src/views/subSystem/lifeCompany/lstatistics/today/index.vue

@@ -1,10 +1,12 @@
 <template>
   <div class="statistics-page"><section class="page-head"><div><span class="eyebrow">LIVE WARNING DESK</span><h1>今日预警统计</h1><p>快速查看今日非草稿预警及处置进展。</p></div><el-button type="primary" :loading="loading" @click="loadData"><el-icon><Refresh /></el-icon>刷新数据</el-button></section>
-    <div class="metric-grid"><button v-for="metric in metricCards" :key="metric.key" class="metric-card" :class="{active: activeStatus === metric.status}" @click="selectStatus(metric.status)"><span>{{ metric.label }}</span><strong>{{ metric.value }}</strong><small>{{ metric.note }}</small></button></div>
-    <el-card shadow="never" class="table-card"><template #header><div class="card-title"><span>{{ activeStatus ? statusLabels[activeStatus] : '今日全部预警' }}</span><div><el-button text @click="selectStatus('')">全部</el-button><el-button type="primary" plain @click="loadData">刷新</el-button></div></div></template><el-table v-loading="loading" :data="rows" stripe empty-text="今日暂无预警"><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column prop="warningType" label="预警类型" min-width="120"/><el-table-column prop="warningSpecial" label="预警专项" min-width="140"/><el-table-column prop="publishTime" label="发布时间" min-width="165"/><el-table-column label="处置状态" width="110"><template #default="{row}"><el-tag :type="row.status === 'CLOSED' ? 'success' : row.status === 'HANDLED' ? 'primary' : 'warning'">{{ statusLabels[row.status] || row.status }}</el-tag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
+    <div class="metric-grid"><button v-for="metric in metricCards" :key="metric.key" class="metric-card" :class="{active: activeStatus === metric.status}" @click="selectStatus(metric.status)"><span><WarningStatusTag v-if="metric.status" :status="metric.status">{{ metric.label }}</WarningStatusTag><template v-else>{{ metric.label }}</template></span><strong :style="metric.status ? { color: warningStatusColor(metric.status) } : undefined">{{ metric.value }}</strong><small>{{ metric.note }}</small></button></div>
+    <el-card shadow="never" class="table-card"><template #header><div class="card-title"><span>{{ activeStatus ? statusLabels[activeStatus] : '今日全部预警' }}</span><div><el-button text @click="selectStatus('')">全部</el-button><el-button type="primary" plain @click="loadData">刷新</el-button></div></div></template><el-table v-loading="loading" :data="rows" stripe empty-text="今日暂无预警"><el-table-column prop="warningNo" label="预警编号" min-width="150"/><el-table-column prop="warningType" label="预警类型" min-width="120"/><el-table-column prop="warningSpecial" label="预警专项" min-width="140"/><el-table-column prop="publishTime" label="发布时间" min-width="165"/><el-table-column label="处置状态" width="110"><template #default="{row}"><WarningStatusTag :status="row.status">{{ statusLabels[row.status] || row.status }}</WarningStatusTag></template></el-table-column></el-table><pagination v-show="total > 0" v-model:page="query.pageNum" v-model:limit="query.pageSize" :total="total" @pagination="loadPage" /></el-card>
   </div>
 </template>
 <script setup name="WarningToday">
+import WarningStatusTag from "../../components/WarningStatusTag.vue"
+import { warningStatusColor } from '../../warningStatus'
 import { loadWarningReferences } from '../statisticsShared'
 import { computed, onMounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'

+ 8 - 18
src/views/subSystem/lifeCompany/lwarningInfoCont/LAlertQuery.vue

@@ -104,9 +104,9 @@
         </el-table-column>
         <el-table-column label="状态" width="110" align="center">
           <template #default="scope">
-            <el-tag :type="getStatusTagType(scope.row.status)">
+            <WarningStatusTag :status="scope.row.status">
               {{ getStatusText(scope.row.status) }}
-            </el-tag>
+            </WarningStatusTag>
           </template>
         </el-table-column>
         <el-table-column label="发布人" prop="publisher" width="120" />
@@ -152,9 +152,9 @@
             <el-table-column label="处置人" prop="handler" width="110" />
             <el-table-column label="状态" width="100" align="center">
               <template #default="scope">
-                <el-tag :type="getStatusTagType(scope.row.status)">
+                <WarningStatusTag :status="scope.row.status">
                   {{ getStatusText(scope.row.status) }}
-                </el-tag>
+                </WarningStatusTag>
               </template>
             </el-table-column>
             <el-table-column label="发布时间" prop="publishTime" width="170" />
@@ -175,7 +175,7 @@
               <div class="selected-name">{{ processMeta.warningNo || selectedWarningRow?.warningNo || "-" }}</div>
               <div class="selected-meta">
                 <span>编号:{{ processMeta.warningNo || selectedWarningRow?.warningNo || "-" }}</span>
-                <span>状态:{{ getStatusText(processMeta.status || selectedWarningRow?.status) }}</span>
+                <span>状态:<WarningStatusTag :status="processMeta.status || selectedWarningRow?.status">{{ getStatusText(processMeta.status || selectedWarningRow?.status) }}</WarningStatusTag></span>
               </div>
             </div>
 
@@ -186,7 +186,7 @@
               </div>
               <div class="stat-box">
                 <div class="stat-box-label">当前状态</div>
-                <div class="stat-box-value small">{{ getStatusText(processMeta.status || selectedWarningRow?.status) }}</div>
+                <div class="stat-box-value small"><WarningStatusTag :status="processMeta.status || selectedWarningRow?.status">{{ getStatusText(processMeta.status || selectedWarningRow?.status) }}</WarningStatusTag></div>
               </div>
             </div>
 
@@ -232,7 +232,7 @@
           <el-descriptions-item label="预警编号">{{ detailBasic.warningNo || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警类型">{{ detailBasic.warningTypeText || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警级别">{{ detailBasic.warningLevelText || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="状态">{{ getStatusText(detailBasic.status) }}</el-descriptions-item>
+          <el-descriptions-item label="状态"><WarningStatusTag :status="detailBasic.status">{{ getStatusText(detailBasic.status) }}</WarningStatusTag></el-descriptions-item>
           <el-descriptions-item label="发布人">{{ detailBasic.publisher || "-" }}</el-descriptions-item>
           <el-descriptions-item label="处置人">{{ detailBasic.handler || "-" }}</el-descriptions-item>
           <el-descriptions-item label="权属单位">{{ detailBasic.ownershipUnit || "-" }}</el-descriptions-item>
@@ -272,6 +272,7 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import { computed, onMounted, reactive, ref } from "vue";
@@ -389,17 +390,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getStatusTagType(status) {
-  return {
-    DRAFT: "info",
-    RECEIVED: "warning",
-    PENDING: "warning",
-    PROCESSING: "warning",
-    HANDLED: "success",
-    CLOSED: "info"
-  }[status] || "";
-}
-
 function getProcessTypeText(type) {
   return {
     RELEASE: "发布预警",

+ 4 - 14
src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue

@@ -87,9 +87,9 @@
         <el-table-column label="权属单位" prop="ownershipUnit" width="160" show-overflow-tooltip />
         <el-table-column label="状态" width="120" align="center">
           <template #default="scope">
-            <el-tag :type="getStatusTagType(scope.row.status)">
+            <WarningStatusTag :status="scope.row.status">
               {{ getStatusText(scope.row.status) }}
-            </el-tag>
+            </WarningStatusTag>
           </template>
         </el-table-column>
         <el-table-column label="发布时间" width="180">
@@ -124,7 +124,7 @@
           <el-descriptions-item label="预警专项">{{ detailBasic.warningSpecial || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警类型">{{ detailBasic.warningTypeText || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警级别">{{ detailBasic.warningLevelText || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="状态">{{ getStatusText(detailBasic.status) }}</el-descriptions-item>
+          <el-descriptions-item label="状态"><WarningStatusTag :status="detailBasic.status">{{ getStatusText(detailBasic.status) }}</WarningStatusTag></el-descriptions-item>
           <el-descriptions-item label="处置人">{{ detailBasic.handler || "-" }}</el-descriptions-item>
           <el-descriptions-item label="设备名称">{{ detailBasic.deviceName || "-" }}</el-descriptions-item>
           <el-descriptions-item label="设备编码">{{ detailBasic.deviceCode || "-" }}</el-descriptions-item>
@@ -267,6 +267,7 @@
 </template>
 
 <script setup>
+import WarningStatusTag from "../components/WarningStatusTag.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import { computed, onMounted, reactive, ref } from "vue";
@@ -412,17 +413,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getStatusTagType(status) {
-  return {
-    DRAFT: "info",
-    RECEIVED: "warning",
-    PENDING: "warning",
-    PROCESSING: "warning",
-    HANDLED: "success",
-    CLOSED: "info"
-  }[status] || "info";
-}
-
 function normalizeWarningRow(item) {
   const warningType = item.warningType || item.warning_type || "";
   const warningLevel = item.warningLevel || item.warning_level || "";

+ 58 - 0
src/views/subSystem/lifeCompany/warningAlarmScope.js

@@ -0,0 +1,58 @@
+// 与 AlarmReviewScopeService.resolveOperations 的报警读取菜单保持一致。
+const alarmMenuComponents = new Set([
+  'subSystem/alarm/common/remind',
+  'subSystem/alarm/common/list',
+  'subSystem/drainage/alarm/analysis',
+  'subSystem/drainage/alarm/alarmGIS',
+  'subSystem/waterSupply/alarm/alarmGIS'
+])
+// EquipmentTypeEnum 的四个设备大类根节点。
+const rootNames = { 1: '供水设备', 2: '排水设备', 3: '燃气设备', 4: '窨井设备' }
+
+export function warningAlarmScopes(routes = []) {
+  const scopes = new Map()
+  function visit(menus) {
+    for (const menu of menus) {
+      if (menu.status !== undefined && String(menu.status) !== '0') continue
+      if (alarmMenuComponents.has(menu.component)) {
+        let query = menu.query || {}
+        try {
+          if (typeof query === 'string') query = JSON.parse(query)
+        } catch {
+          query = {}
+        }
+        const rootTypeId = Number(query?.rootTypeId)
+        if (Object.hasOwn(rootNames, rootTypeId)) {
+          const warningCode = query.warning_code || query.warningCode || undefined
+          const value = JSON.stringify([rootTypeId, warningCode || ''])
+          scopes.set(value, {
+            value, rootTypeId, warningCode,
+            label: rootNames[rootTypeId] + (warningCode ? `(${warningCode})` : '')
+          })
+        }
+      }
+      if (menu.children) visit(menu.children)
+    }
+  }
+  visit(routes)
+  const result = [...scopes.values()]
+  // 同一大类已有不限定报警编码的菜单时,仅展示这一完整范围。
+  return result.filter(scope => !scope.warningCode || !result.some(other =>
+    other.rootTypeId === scope.rootTypeId && !other.warningCode
+  )).sort((a, b) => a.rootTypeId - b.rootTypeId)
+}
+
+export function warningAlarmQuery(scope, filters = {}) {
+  if (!scope || !Object.hasOwn(rootNames, scope.rootTypeId)) {
+    throw new Error('请选择已授权的设备大类')
+  }
+  return {
+    rootTypeId: scope.rootTypeId,
+    pageNum: filters.pageNum || 1,
+    pageSize: filters.pageSize || 10,
+    deviceCode: filters.deviceCode?.trim() || undefined,
+    // 菜单限定编码始终优先,不能被输入框覆盖。
+    warningCode: scope.warningCode || filters.warningCode?.trim() || undefined,
+    alarmStatus: filters.alarmStatus === '' ? undefined : filters.alarmStatus
+  }
+}

+ 39 - 0
src/views/subSystem/lifeCompany/warningStatus.js

@@ -0,0 +1,39 @@
+// 预警事件状态的统一配色,标签与图表均按状态编码取色。
+export const warningStatusPalette = Object.freeze({
+  DRAFT: Object.freeze({ label: '草稿', color: '#475569', background: '#f1f5f9', border: '#cbd5e1' }),
+  PENDING: Object.freeze({ label: '待接收', color: '#a16207', background: '#fffbeb', border: '#fde68a' }),
+  RECEIVED: Object.freeze({ label: '已接收', color: '#1d4ed8', background: '#eff6ff', border: '#bfdbfe' }),
+  PROCESSING: Object.freeze({ label: '处置中', color: '#c2410c', background: '#fff7ed', border: '#fed7aa' }),
+  HANDLED: Object.freeze({ label: '已处置', color: '#7e22ce', background: '#faf5ff', border: '#e9d5ff' }),
+  CLOSED: Object.freeze({ label: '已解除', color: '#15803d', background: '#f0fdf4', border: '#bbf7d0' })
+})
+
+export function warningStatusCode(value) {
+  const raw = value && typeof value === 'object'
+    ? value.status || value.warningStatus || value.warning_status
+    : value
+  const code = String(raw ?? '').trim().toUpperCase()
+  return Object.keys(warningStatusPalette).find(key => warningStatusPalette[key].label === code) || code
+}
+
+export function warningStatusAppearance(value) {
+  return warningStatusPalette[warningStatusCode(value)] || warningStatusPalette.DRAFT
+}
+
+export function warningStatusColor(value) {
+  return warningStatusAppearance(value).color
+}
+
+export function warningStatusStyle(value) {
+  const appearance = warningStatusAppearance(value)
+  return {
+    '--el-tag-bg-color': appearance.background,
+    '--el-tag-border-color': appearance.border,
+    '--el-tag-text-color': appearance.color
+  }
+}
+
+export function warningStatusLabel(value) {
+  const code = warningStatusCode(value)
+  return warningStatusPalette[code]?.label || code || '-'
+}