Procházet zdrojové kódy

发布预警可选设备与预警类型关联;
预警详情页面优化;

zengy před 5 dny
rodič
revize
037451c490
23 změnil soubory, kde provedl 1255 přidání a 789 odebrání
  1. 1 0
      memory/index.md
  2. 1 0
      memory/long-term.md
  3. 79 0
      memory/sessions/2026-10-16.md
  4. 4 4
      src/api/smxyjldczxt/yjxxfb/yjfb.js
  5. 138 21
      src/views/subSystem/lifeCompany/components/WarningAttachments.vue
  6. 68 0
      src/views/subSystem/lifeCompany/components/WarningContentCell.vue
  7. 373 0
      src/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue
  8. 235 0
      src/views/subSystem/lifeCompany/components/WarningDisposalTimeline.vue
  9. 5 28
      src/views/subSystem/lifeCompany/lAlertInformation/LCorrelation.vue
  10. 154 184
      src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue
  11. 3 3
      src/views/subSystem/lifeCompany/lAlertInformation/LReportUpload.vue
  12. 16 109
      src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue
  13. 2 2
      src/views/subSystem/lifeCompany/lOverviewWarning/LTodayNot.vue
  14. 2 2
      src/views/subSystem/lifeCompany/lOverviewWarning/Lhistory.vue
  15. 2 2
      src/views/subSystem/lifeCompany/lOverviewWarning/Ltoday.vue
  16. 0 69
      src/views/subSystem/lifeCompany/lOverviewWarning/WarningDetailDrawer.vue
  17. 1 1
      src/views/subSystem/lifeCompany/lhome/lHome.vue
  18. 19 124
      src/views/subSystem/lifeCompany/lwarningInfoCont/LAlertQuery.vue
  19. 10 79
      src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue
  20. 2 0
      src/views/subSystem/lifeCompany/warningAttachments.js
  21. 73 0
      src/views/subSystem/lifeCompany/warningDetail.js
  22. 7 161
      src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue
  23. 60 0
      tests/warningDetail.test.mjs

+ 1 - 0
memory/index.md

@@ -69,6 +69,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
+- [2026-10-16:PC 预警发布、列表、详情与统一详情组件优化](sessions/2026-10-16.md)
 - [2026-10-15:权限管理子系统、移动端工单权限、PC 登录页与门户高保真重构](sessions/2026-10-15.md)
 - [2026-10-14:故障通知筛选与两端核实命名、工单取消限制、本人登录日志、照片按组切换、性别单选、移动端接口分层及统一前缀](sessions/2026-10-14.md)
 - [2026-10-13:移动端工单管理、待办、详情与操作接入](sessions/2026-10-13.md)

+ 1 - 0
memory/long-term.md

@@ -101,6 +101,7 @@
 
 ## 产品级决策
 
+- PC 预警详情统一使用 `lifeCompany/components/WarningDetailDrawer.vue` 和 `GET /warning/detail/full/{warningId}`;发布、待办、处置、查询、关联管理、首页/今日/历史概览及管网预警入口不得再各自维护详情抽屉。公共组件负责基础信息、关联事项/设备/报警、处置时间轴、督办、附件和电子存档,页面只保留通知已读、编辑、处置操作等入口特有逻辑。旧简版 `GET /warning/detail/{warningId}` 已移除(2026-10-16)。
 - 预警列表与详情查看权限统一为 `warning:warning:query`,不再使用 `warning:warning:list`;列表、待办、搜索、跟踪、归档和通知已读同步,现有详情、附件、流程与统计沿用 query。菜单迁移仅更新旧权限值,保留菜单 ID 和角色关联,处置 handle、核验 audit 独立不变(2026-10-15)。
 - 预警维修接收、开始和提交处置使用 `warning:warning:handle`;管理核验解除/退回使用独立的 `warning:warning:audit`,包括 `/clear`、`/resolve`、`/return`、`/misreport`。仍须 HANDLED 且为指定 reviewer(历史为空回退 publisher),权限不代替人员和状态限制。PC 待办、预警处置和管网预警入口统一为“核验”表单,必选“核查解除/退回办理”、必填核验意见;退回仍由原处置人办理。迁移脚本只新增菜单,不从处置权限自动继承核验权限(2026-10-15)。
 - 预警联动系统事件六状态统一颜色:DRAFT 草稿灰、PENDING 待接收黄、RECEIVED 已接收蓝、PROCESSING 处置中橙、HANDLED 已处置紫、CLOSED 已解除绿。所有事件状态标签和统计图表复用 `lifeCompany/warningStatus.js`,禁止各页面按数据位置自行分配状态色(2026-10-15)。

+ 79 - 0
memory/sessions/2026-10-16.md

@@ -0,0 +1,79 @@
+# 2026-10-16:PC 预警发布、列表与详情优化
+
+## 目标
+
+- 优化预警发布、待办、处置和综合查询四个 PC 页面。
+- 发布草稿时选择处置人和核验人,发布人默认草稿创建人。
+- 关联设备按 `equipment_warning_type` 中当前预警类型对应的设备类型过滤。
+- 统一预警详情的处置时间、时间轴和附件展示。
+
+## 结论与变更
+
+- `LRelease.vue` 的新增/编辑表单移除发布人、处置人、核验人,将关联设备和内容描述移动到类型/级别后;发布操作新增处置人、核验人选择弹窗,并同时提交两个账号。
+- 新增 `/warning/config/equipment-type-ids`,通过 `warning_type.warning_code` 和 `equipment_warning_type.warning_type_id` 返回允许的设备类型;设备分页接口支持设备类型集合过滤,保存草稿时后端再次校验设备与预警类型的关系。
+- 草稿创建时将发布人初始化为创建人,发布时仍以 `create_by` 为准,兼容历史数据时回退当前发布人。
+- 四个主列表按要求移除序号/权属单位列并增加预警内容;待办查询移除预警专项、权属单位、发布人、处置人;综合查询表格使用内容列填充可用宽度。
+- 新增共享 `WarningDisposalTimeline.vue`,参照工单详情按日期倒序分组,展示时分节点、人员、动作、内容、原因和附件;共享附件改为文件类型卡片样式,保留受控预览/下载接口。
+- 无数据库结构变更,前后端需要同步部署。
+
+## 验证
+
+- `node --test tests/warningAttachments.test.mjs tests/warningReviewAction.test.mjs`:8 项通过。
+- 6 个相关 Vue SFC 使用 `@vue/compiler-sfc` 完成脚本和模板编译检查。
+- `mvn -pl pipe-network-service/zksy-admin -am -DskipTests compile`:成功。
+- 前后端 `git diff --check`:通过。
+- `npm run build:prod`:完成 1816 个模块转换后失败;既有 `DisPlanEditing.vue` 引用 `xlsx`,但当前依赖未安装(`npm ls xlsx --depth=0` 为空),与本次预警变更无关。
+
+## 风险与后续
+
+- 联调环境需确认各预警类型已在 `equipment_warning_type` 配置设备类型;未配置时设备选择器会明确提示且不能选择设备。
+- 完整生产构建需先补齐项目既有 `xlsx` 依赖问题。
+
+## 详情标题栏补充优化
+
+- 四个预警详情抽屉将预警单号移入“预警详情”标题右侧,并将当前状态放到同一行右侧;详情字段区不再重复显示单号和状态。
+- 清除 Element Plus 抽屉头部默认的大间距,头部与正文分别使用 10px、12px 的紧凑间距;有关联事项时同步去掉事项卡片顶部重复留白。
+- 四个详情字段区将预警内容调整为第一行并跨两列展示。
+- 相关列表的预警编号列统一加宽到 210px,避免常见长编号换行。
+- 新增共享 `WarningContentCell.vue`,列表预警内容单行省略,点击后通过弹层查看可换行、可滚动的完整内容。
+
+## 预警详情统一
+
+- 新增公共 `components/WarningDetailDrawer.vue` 和 `warningDetail.js`,统一详情加载、字段兼容和布局;预警内容置顶,按基础信息、关联事项、关联报警、处置记录、督办记录、其他附件、电子存档分区展示。
+- 发布、待办、处置、综合查询、关联管理、首页、今日预警、今日未处置、历史预警及管网预警共 10 个查看入口改用公共详情抽屉;待办通知已读等页面特有逻辑仍留在父页面。
+- 报告上传和发布编辑继续使用详情数据,但统一改调 `api/pipeNetwork/earlyWarning.js` 的 `getWarningFullDetailData`;删除 `yjfb.js` 中重复的详情 API 封装和概览模块旧版详情抽屉。
+- 后端删除未被仓内调用的简版 `/warning/detail/{warningId}` 及对应 Service 方法,以 `/warning/detail/full/{warningId}` 作为唯一详情接口;无数据库变更。
+- 新增 `tests/warningDetail.test.mjs`,覆盖下划线/驼峰字段兼容、数值零和完整详情集合标准化。
+
+## 统一详情验证
+
+- 11 项预警详情、附件和核验相关 Node 测试通过。
+- 12 个受影响 Vue SFC 的模板与脚本编译通过,前后端 `git diff --check` 通过。
+- `mvn -pl pipe-network-service/zksy-admin -am -DskipTests compile`:成功。
+- `npm run build:prod`:相关代码完成 1823 个模块转换,最终仍因既有 `DisPlanEditing.vue` 无法解析本地 `xlsx` 依赖而失败,与本次变更无关。
+- Vite 开发服务 `http://localhost:5173/` 返回 200,无热更新错误。
+
+## 处置记录图片预览补充
+
+- 统一预警详情的处置时间轴改为与工单详情一致的图片交互:图片附件显示 50×50 缩略图,点击后查看大图,同一条处置记录的多张图片可在预览器中切换。
+- 缩略图继续通过受控附件预览接口读取,不暴露对象存储地址;非图片附件仍保留原文件卡片、预览和下载操作,“其他附件”区域维持原布局。
+- 图片 Blob URL 在附件变化和组件销毁时主动释放;加载失败时在缩略图位置展示明确状态。
+- 11 项预警附件、详情和核验测试通过,`WarningAttachments.vue`、`WarningDisposalTimeline.vue` 模板与脚本编译通过,前端 `git diff --check` 通过;Vite 热更新无组件编译错误,本地仅有后端 8301 未启动造成的既有 SSE 代理失败。
+
+## 处置记录人员昵称补充
+
+- 处置记录查询关联 `app_user.sys_user`,新增 `disposal_user_name` 返回昵称;昵称为空或历史账号已无法匹配时回退 `warning_disposal.disposal_user`。
+- 公共预警详情时间轴优先展示昵称字段,数据库仍保存登录账号,流程人员校验、统计和历史审计语义不变。
+- 管理后端 Maven 编译、公共时间轴 Vue 编译及前后端 `git diff --check` 均通过,Vite 热更新无新增错误。
+
+## 详情入口文案统一
+
+- 预警发布、待办、今日预警、今日未处置和历史预警列表中原“查看”或“查看详情”按钮统一改为“详情”,与预警处置、综合查询、首页和管网预警入口一致。
+- 关联管理保留“查看预警”和“报警详情”,避免同一操作列中两个不同详情对象产生歧义。
+- 5 个受影响 Vue SFC 编译通过,预警相关页面已无“查看详情”或单独“查看”的详情按钮,`git diff --check` 和 Vite 热更新检查通过。
+
+## 新增预警设备类型来源调整
+
+- 新增/编辑预警的关联设备选择器左栏直接使用 `/equipmentType`(`equipment_type`)接口返回的 `id`、`typeName`,不再由前端推导并展示设备根大类。
+- 左栏接口类型与当前预警类型在 `equipment_warning_type` 中的关联 ID 取交集,只展示实际允许的具体设备类型;点击类型后通过 `equipmentTypeId` 精确筛选,选择“全部类型”时仍通过 `equipmentTypeIds` 限制在允许范围内。
+- `LRelease.vue` 模板与脚本编译、11 项预警测试及 `git diff --check` 通过,Vite 热更新无错误。

+ 4 - 4
src/api/smxyjldczxt/yjxxfb/yjfb.js

@@ -80,11 +80,11 @@ export async function getAlertTypeDictionaryData(query) {
   return { ...response, rows: response.data || [] }
 }
 
-// 预警详情
-export function getWarningDetailData(warningId) {
+export function getWarningEquipmentTypeIdsData(warningType) {
   return request({
-    url: `/warning/detail/full/${warningId}`,
-    method: 'get'
+    url: '/warning/config/equipment-type-ids',
+    method: 'get',
+    params: { warningType }
   })
 }
 

+ 138 - 21
src/views/subSystem/lifeCompany/components/WarningAttachments.vue

@@ -1,20 +1,49 @@
 <template>
-  <div v-if="files.length" class="warning-attachments">
-    <div v-for="(file, index) in files" :key="file.id || index" class="attachment-item">
-      <el-icon class="attachment-icon"><Paperclip /></el-icon>
-      <div class="attachment-info">
-        <div class="attachment-name">{{ file.name }}</div>
-        <div class="attachment-meta">
-          <span v-if="file.sizeText">{{ file.sizeText }}</span>
-          <span v-if="file.createTime">上传于 {{ formatTime(file.createTime) }}</span>
-          <span v-if="!file.id">附件信息不完整,暂无法读取</span>
-        </div>
+  <div v-if="files.length" class="warning-attachments" :class="{ 'thumbnail-mode': imageThumbnails }">
+    <template v-for="(file, index) in files" :key="file.id || index">
+      <el-image
+        v-if="imageThumbnails && file.previewKind === 'image' && thumbnailUrl(file, index)"
+        class="attachment-thumbnail"
+        :src="thumbnailUrl(file, index)"
+        :alt="file.name"
+        :title="`点击查看大图:${file.name}`"
+        :preview-src-list="thumbnailPreviewUrls"
+        :initial-index="thumbnailIndex(file, index)"
+        preview-teleported
+        fit="cover"
+      >
+        <template #error><span class="thumbnail-placeholder">图片加载失败</span></template>
+      </el-image>
+      <div
+        v-else-if="imageThumbnails && file.previewKind === 'image'"
+        class="attachment-thumbnail thumbnail-placeholder"
+        :title="file.name"
+      >
+        {{ thumbnailState(file, index) === 'error' || !file.id ? '图片加载失败' : '加载中...' }}
       </div>
-      <div v-hasPermi="['warning:warning:query']" class="attachment-actions">
-        <el-button v-if="file.previewKind" link type="primary" :disabled="!file.id || !!busy" :loading="busy === `preview:${file.id}`" @click="preview(file)">预览</el-button>
-        <el-button link type="primary" :disabled="!file.id || !!busy" :loading="busy === `download:${file.id}`" @click="download(file)">下载</el-button>
+      <div v-else class="attachment-item">
+        <div
+          class="attachment-tile"
+          :class="file.previewKind || 'file'"
+          :title="file.name"
+        >
+          <el-icon v-if="file.previewKind === 'image'"><Picture /></el-icon>
+          <span v-else>{{ file.extension }}</span>
+        </div>
+        <div class="attachment-info">
+          <div class="attachment-name">{{ file.name }}</div>
+          <div class="attachment-meta">
+            <span v-if="file.sizeText">{{ file.sizeText }}</span>
+            <span v-if="file.createTime">上传于 {{ formatTime(file.createTime) }}</span>
+            <span v-if="!file.id">附件信息不完整,暂无法读取</span>
+          </div>
+        </div>
+        <div v-hasPermi="['warning:warning:query']" class="attachment-actions">
+          <el-button v-if="file.previewKind" link type="primary" :disabled="!file.id || !!busy" :loading="busy === `preview:${file.id}`" @click="preview(file)">预览</el-button>
+          <el-button link type="primary" :disabled="!file.id || !!busy" :loading="busy === `download:${file.id}`" @click="download(file)">下载</el-button>
+        </div>
       </div>
-    </div>
+    </template>
   </div>
   <span v-else-if="emptyText" class="attachment-meta">{{ emptyText }}</span>
 
@@ -33,16 +62,17 @@
 </template>
 
 <script setup>
-import { computed, onBeforeUnmount, ref } from 'vue'
+import { computed, onBeforeUnmount, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Paperclip } from '@element-plus/icons-vue'
+import { Picture } from '@element-plus/icons-vue'
 import { parseTime } from '@/utils/ruoyi'
 import { getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
 import { normalizeWarningAttachment } from '../warningAttachments'
 
 const props = defineProps({
   attachments: { type: Array, default: () => [] },
-  emptyText: { type: String, default: '' }
+  emptyText: { type: String, default: '' },
+  imageThumbnails: { type: Boolean, default: false }
 })
 const files = computed(() => props.attachments.map(normalizeWarningAttachment))
 const busy = ref('')
@@ -51,8 +81,15 @@ const previewUrl = ref('')
 const previewName = ref('')
 const previewKind = ref('')
 const previewFile = ref(null)
+const thumbnailUrls = ref({})
+const thumbnailStates = ref({})
+const thumbnailImages = computed(() => files.value
+  .map((file, index) => ({ file, index, key: fileKey(file, index) }))
+  .filter(item => item.file.previewKind === 'image' && thumbnailUrls.value[item.key]))
+const thumbnailPreviewUrls = computed(() => thumbnailImages.value.map(item => thumbnailUrls.value[item.key]))
 const pdfPreviewSupported = typeof navigator !== 'undefined' && navigator.pdfViewerEnabled === true
 let disposed = false
+let thumbnailLoadVersion = 0
 const formatTime = value => {
   const date = new Date(value)
   return Number.isNaN(date.getTime()) ? value : parseTime(date)
@@ -68,6 +105,72 @@ async function readFile(file, forPreview) {
   return blob
 }
 
+function fileKey(file, index) {
+  return file.id == null || file.id === '' ? `missing-${index}` : String(file.id)
+}
+
+function thumbnailUrl(file, index) {
+  return thumbnailUrls.value[fileKey(file, index)] || ''
+}
+
+function thumbnailState(file, index) {
+  return thumbnailStates.value[fileKey(file, index)] || ''
+}
+
+function thumbnailIndex(file, index) {
+  const key = fileKey(file, index)
+  return Math.max(0, thumbnailImages.value.findIndex(item => item.key === key))
+}
+
+function imageMimeType(file, blob) {
+  const blobType = String(blob?.type || '').toLowerCase().split(';')[0]
+  if (/^image\/(png|jpeg|gif|webp|bmp)$/.test(blobType)) return blobType
+  const extension = String(file?.name || '').split('.').pop().toLowerCase()
+  return {
+    png: 'image/png',
+    jpg: 'image/jpeg',
+    jpeg: 'image/jpeg',
+    gif: 'image/gif',
+    webp: 'image/webp',
+    bmp: 'image/bmp'
+  }[extension] || 'image/jpeg'
+}
+
+function revokeThumbnailUrls() {
+  Object.values(thumbnailUrls.value).forEach(url => URL.revokeObjectURL(url))
+  thumbnailUrls.value = {}
+}
+
+async function loadThumbnails() {
+  const version = ++thumbnailLoadVersion
+  revokeThumbnailUrls()
+  thumbnailStates.value = {}
+  if (!props.imageThumbnails) return
+
+  const imageFiles = files.value
+    .map((file, index) => ({ file, index, key: fileKey(file, index) }))
+    .filter(item => item.file.previewKind === 'image')
+  thumbnailStates.value = Object.fromEntries(imageFiles.map(item => [item.key, item.file.id ? 'loading' : 'error']))
+
+  await Promise.all(imageFiles.filter(item => item.file.id).map(async ({ file, key }) => {
+    try {
+      const blob = await readFile(file, true)
+      if (disposed || version !== thumbnailLoadVersion) return
+      const url = URL.createObjectURL(new Blob([blob], { type: imageMimeType(file, blob) }))
+      if (disposed || version !== thumbnailLoadVersion) {
+        URL.revokeObjectURL(url)
+        return
+      }
+      thumbnailUrls.value = { ...thumbnailUrls.value, [key]: url }
+      thumbnailStates.value = { ...thumbnailStates.value, [key]: 'loaded' }
+    } catch (error) {
+      if (!disposed && version === thumbnailLoadVersion) {
+        thumbnailStates.value = { ...thumbnailStates.value, [key]: 'error' }
+      }
+    }
+  }))
+}
+
 async function preview(file) {
   busy.value = `preview:${file.id}`
   try {
@@ -75,7 +178,7 @@ async function preview(file) {
     if (disposed) return
     clearPreview()
     // 预览只渲染图片或 PDF,不将未知内容作为 HTML 打开。
-    const type = file.previewKind === 'pdf' ? 'application/pdf' : 'application/octet-stream'
+    const type = file.previewKind === 'pdf' ? 'application/pdf' : imageMimeType(file, blob)
     previewUrl.value = URL.createObjectURL(new Blob([blob], { type }))
     previewName.value = file.name
     previewKind.value = file.previewKind
@@ -113,16 +216,30 @@ function clearPreview() {
   previewUrl.value = ''
   previewFile.value = null
 }
+watch(
+  [() => props.imageThumbnails, () => files.value.map((file, index) => `${fileKey(file, index)}:${file.previewKind}`).join('|')],
+  loadThumbnails,
+  { immediate: true }
+)
 onBeforeUnmount(() => {
   disposed = true
+  thumbnailLoadVersion += 1
+  revokeThumbnailUrls()
   clearPreview()
 })
 </script>
 
 <style scoped>
-.warning-attachments { margin-top: 10px; display: grid; gap: 8px; }
-.attachment-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: #f8fafc; border: 1px solid #e4e7ed; border-radius: 6px; }
-.attachment-icon { color: #409eff; flex-shrink: 0; }
+.warning-attachments { margin-top: 10px; display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 8px; }
+.warning-attachments.thumbnail-mode { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
+.thumbnail-mode .attachment-item { min-width: 260px; max-width: 420px; flex: 1 1 260px; }
+.attachment-thumbnail { box-sizing: border-box; width: 50px; height: 50px; flex: 0 0 50px; border: 1px solid #dcdfe6; border-radius: 6px; background: #f5f7fa; cursor: zoom-in; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; }
+.attachment-thumbnail:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 10px rgb(0 0 0 / 18%); transform: scale(1.06); }
+.thumbnail-placeholder { display: inline-flex; align-items: center; justify-content: center; padding: 3px; color: #909399; cursor: default; font-size: 10px; line-height: 1.2; text-align: center; }
+.attachment-item { display: flex; align-items: center; gap: 10px; min-width: 0; padding: 10px 12px; background: #fafbfc; border: 1px solid #e4e7ed; border-radius: 7px; }
+.attachment-tile { display: flex; width: 42px; height: 42px; flex: 0 0 42px; align-items: center; justify-content: center; border: 0; border-radius: 6px; background: #f1efe8; color: #5f5e5a; font: 600 10px/1 ui-monospace, Consolas, "Courier New", monospace; }
+.attachment-tile.image { background: #e6f1fb; color: #185fa5; }
+.attachment-tile.pdf { background: #fcebeb; color: #a32d2d; }
 .attachment-info { flex: 1; min-width: 0; }
 .attachment-name { color: #303133; overflow-wrap: anywhere; line-height: 1.5; }
 .attachment-meta { display: flex; flex-wrap: wrap; gap: 10px; color: #909399; font-size: 12px; line-height: 1.5; }

+ 68 - 0
src/views/subSystem/lifeCompany/components/WarningContentCell.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-popover
+    v-if="contentText"
+    placement="top-start"
+    trigger="click"
+    :width="420"
+  >
+    <template #reference>
+      <button
+        type="button"
+        class="warning-content-trigger"
+        aria-label="查看完整预警内容"
+        @click.stop
+      >
+        {{ contentText }}
+      </button>
+    </template>
+    <div class="warning-content-full">{{ contentText }}</div>
+  </el-popover>
+  <span v-else>-</span>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  content: { type: [String, Number], default: '' }
+})
+
+const contentText = computed(() => String(props.content ?? '').trim())
+</script>
+
+<style scoped>
+.warning-content-trigger {
+  display: block;
+  width: 100%;
+  overflow: hidden;
+  padding: 0;
+  border: 0;
+  background: transparent;
+  color: #606266;
+  cursor: pointer;
+  font: inherit;
+  line-height: inherit;
+  text-align: left;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.warning-content-trigger:hover,
+.warning-content-trigger:focus-visible {
+  color: var(--el-color-primary);
+}
+
+.warning-content-trigger:focus-visible {
+  outline: 2px solid var(--el-color-primary-light-5);
+  outline-offset: 2px;
+}
+
+.warning-content-full {
+  max-height: 320px;
+  overflow: auto;
+  color: #303133;
+  line-height: 1.7;
+  overflow-wrap: anywhere;
+  white-space: pre-wrap;
+}
+</style>

+ 373 - 0
src/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue

@@ -0,0 +1,373 @@
+<template>
+  <el-drawer
+    v-model="visible"
+    size="min(900px, 96vw)"
+    append-to-body
+    direction="rtl"
+    class="warning-detail-drawer"
+  >
+    <template #header>
+      <div class="warning-detail-header">
+        <div class="warning-detail-heading">
+          <span class="warning-detail-title">预警详情</span>
+          <span class="warning-detail-no">{{ basic.warningNo || '-' }}</span>
+        </div>
+        <WarningStatusTag :status="basic.status">{{ statusText }}</WarningStatusTag>
+      </div>
+    </template>
+
+    <div v-loading="loading" class="warning-detail-body">
+      <el-alert
+        v-if="notice"
+        :title="notice"
+        type="info"
+        :closable="false"
+        show-icon
+        class="detail-notice"
+      />
+      <el-alert
+        v-if="errorText"
+        :title="errorText"
+        type="error"
+        :closable="false"
+        show-icon
+        class="detail-error"
+      >
+        <el-button link type="primary" @click="loadDetail">重新加载</el-button>
+      </el-alert>
+
+      <template v-if="hasDetail">
+        <section class="detail-section detail-section-first">
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="预警内容" :span="2">
+              <div class="warning-content">{{ basic.warningContent || '-' }}</div>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="basic.warningName" label="预警名称" :span="2">{{ basic.warningName }}</el-descriptions-item>
+            <el-descriptions-item label="预警类型">{{ basic.warningTypeText || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="预警级别">
+              <el-tag :type="levelTagType">{{ basic.warningLevelText || '-' }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="预警专项">{{ basic.warningSpecial || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="权属单位">{{ basic.ownershipUnit || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="关联设备" :span="2">
+              <span>{{ basic.deviceName || '-' }}</span>
+              <span v-if="basic.deviceCode" class="secondary-text">({{ basic.deviceCode }})</span>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="basic.location" label="设备位置" :span="2">{{ basic.location }}</el-descriptions-item>
+            <el-descriptions-item label="发布人">{{ basic.publisher || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="处置人">{{ basic.handler || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="核验人">{{ basic.reviewer || basic.publisher || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="发布时间">{{ formatDateTime(basic.publishTime) }}</el-descriptions-item>
+            <el-descriptions-item v-if="basic.remark" label="备注" :span="2">{{ basic.remark }}</el-descriptions-item>
+          </el-descriptions>
+        </section>
+
+        <WarningItemSummary :warning="basic" />
+
+        <section v-if="detail.alarmList.length" class="detail-section">
+          <div class="section-heading">
+            <span>关联报警</span>
+            <span class="section-count">{{ detail.alarmList.length }} 条</span>
+          </div>
+          <el-table :data="detail.alarmList" border size="small">
+            <el-table-column label="报警编号" min-width="170" show-overflow-tooltip>
+              <template #default="scope">{{ scope.row.alarmNo || scope.row.alarmId || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="报警类型" prop="alarmType" min-width="120" show-overflow-tooltip />
+            <el-table-column label="设备名称" prop="deviceName" min-width="140" show-overflow-tooltip />
+            <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip />
+            <el-table-column label="阈值" min-width="90">
+              <template #default="scope">{{ displayValue(scope.row.thresholdValue) }}</template>
+            </el-table-column>
+            <el-table-column label="实际值" min-width="90">
+              <template #default="scope">{{ displayValue(scope.row.actualValue) }}</template>
+            </el-table-column>
+            <el-table-column label="报警时间" min-width="170">
+              <template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
+            </el-table-column>
+          </el-table>
+        </section>
+
+        <section class="detail-section">
+          <div class="section-heading">
+            <span>处置记录</span>
+            <span v-if="detail.disposalHistory.length" class="section-count">{{ detail.disposalHistory.length }} 条</span>
+          </div>
+          <WarningDisposalTimeline
+            :records="detail.disposalHistory"
+            :attachments="detail.attachmentList"
+            :level-labels="levelLabels"
+          />
+        </section>
+
+        <section v-if="detail.supervisionList.length" class="detail-section">
+          <div class="section-heading">
+            <span>督办记录</span>
+            <span class="section-count">{{ detail.supervisionList.length }} 条</span>
+          </div>
+          <div class="supervision-list">
+            <div v-for="(item, index) in detail.supervisionList" :key="item.supervisionId || item.supervision_id || index" class="supervision-row">
+              <div class="supervision-meta">
+                <strong>{{ item.supervisionUser || item.supervision_user || '-' }}</strong>
+                <span>{{ formatDateTime(item.createTime || item.create_time) }}</span>
+              </div>
+              <div class="supervision-content">{{ item.supervisionContent || item.supervision_content || '-' }}</div>
+            </div>
+          </div>
+        </section>
+
+        <section v-if="otherAttachments.length" class="detail-section">
+          <div class="section-heading">
+            <span>其他附件</span>
+            <span class="section-count">{{ otherAttachments.length }} 个</span>
+          </div>
+          <WarningAttachments :attachments="otherAttachments" />
+        </section>
+
+        <section v-if="hasArchive" class="detail-section">
+          <div class="section-heading"><span>电子存档</span></div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="存档状态">{{ archive.archiveStatus || archive.status || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="存档编号">{{ archive.archiveNo || archive.archive_no || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="归档时间">{{ formatDateTime(archive.createTime || archive.create_time) }}</el-descriptions-item>
+            <el-descriptions-item label="存档说明">{{ archive.remark || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </section>
+      </template>
+
+      <el-empty v-else-if="!loading" description="暂无预警详情" />
+    </div>
+  </el-drawer>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { parseTime } from '@/utils/ruoyi'
+import { getWarningFullDetailData } from '@/api/pipeNetwork/earlyWarning'
+import { useWarningReferences } from '../warningWorkflow'
+import { normalizeWarningDetail, warningIdOf } from '../warningDetail'
+import { unlinkedWarningAttachments } from '../warningAttachments'
+import WarningAttachments from './WarningAttachments.vue'
+import WarningDisposalTimeline from './WarningDisposalTimeline.vue'
+import WarningItemSummary from './WarningItemSummary.vue'
+import WarningStatusTag from './WarningStatusTag.vue'
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  warningId: { type: [String, Number], default: '' },
+  row: { type: Object, default: null },
+  notice: { type: String, default: '' }
+})
+
+const emit = defineEmits(['update:modelValue', 'loaded'])
+const { databaseStatusLabels, typeLabels, levelLabels } = useWarningReferences()
+const loading = ref(false)
+const errorText = ref('')
+const rawDetail = ref({})
+let requestVersion = 0
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: value => emit('update:modelValue', value)
+})
+const resolvedWarningId = computed(() => props.warningId || warningIdOf(props.row))
+const references = computed(() => ({ typeLabels: typeLabels.value, levelLabels: levelLabels.value }))
+const detail = computed(() => normalizeWarningDetail(rawDetail.value, references.value))
+const basic = computed(() => detail.value.basicInfo || {})
+const archive = computed(() => detail.value.electronicArchive || {})
+const hasArchive = computed(() => Boolean(archive.value.archiveId || archive.value.archive_id || archive.value.archiveNo || archive.value.archive_no || archive.value.status || archive.value.archiveStatus))
+const hasDetail = computed(() => Boolean(basic.value.warningId || basic.value.warningNo))
+const otherAttachments = computed(() => unlinkedWarningAttachments(detail.value.disposalHistory, detail.value.attachmentList))
+const statusText = computed(() => databaseStatusLabels.value[basic.value.status] || basic.value.status || '-')
+const levelTagType = computed(() => ({ '1': 'danger', '2': 'warning', '3': 'success', '4': 'info' }[String(basic.value.warningLevel)] || 'info'))
+
+watch([visible, resolvedWarningId], ([isOpen]) => {
+  if (isOpen) loadDetail()
+})
+
+async function loadDetail() {
+  const warningId = resolvedWarningId.value
+  const version = ++requestVersion
+  errorText.value = ''
+  rawDetail.value = props.row ? { basicInfo: props.row } : {}
+  if (!warningId) {
+    errorText.value = props.row ? '' : '缺少预警标识,无法加载详情'
+    return
+  }
+  loading.value = true
+  try {
+    const response = await getWarningFullDetailData(warningId)
+    if (version !== requestVersion) return
+    rawDetail.value = response.data || {}
+    emit('loaded', rawDetail.value)
+  } catch (error) {
+    if (version !== requestVersion) return
+    rawDetail.value = props.row ? { basicInfo: props.row } : {}
+    errorText.value = error?.msg || error?.message || '预警详情加载失败'
+  } finally {
+    if (version === requestVersion) loading.value = false
+  }
+}
+
+function formatDateTime(value) {
+  return value ? parseTime(value) : '-'
+}
+
+function displayValue(value) {
+  return value === 0 || value ? value : '-'
+}
+</script>
+
+<style scoped>
+.warning-detail-body {
+  min-height: 220px;
+}
+
+.warning-detail-header {
+  display: flex;
+  width: 100%;
+  min-width: 0;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.warning-detail-heading {
+  display: flex;
+  min-width: 0;
+  align-items: baseline;
+  gap: 12px;
+}
+
+.warning-detail-title {
+  flex: none;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.warning-detail-no {
+  overflow: hidden;
+  color: #909399;
+  font-family: ui-monospace, Consolas, "Courier New", monospace;
+  font-size: 12px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.detail-error,
+.detail-notice {
+  margin-bottom: 16px;
+}
+
+.detail-section {
+  margin-top: 24px;
+}
+
+.detail-section-first {
+  margin-top: 0;
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+  color: #303133;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.section-heading::before {
+  width: 3px;
+  height: 14px;
+  border-radius: 2px;
+  background: #3b5bdb;
+  content: '';
+}
+
+.section-count,
+.secondary-text {
+  color: #909399;
+  font-size: 12px;
+  font-weight: 400;
+}
+
+.warning-content {
+  min-height: 24px;
+  color: #303133;
+  font-size: 14px;
+  line-height: 1.75;
+  overflow-wrap: anywhere;
+  white-space: pre-wrap;
+}
+
+.supervision-row {
+  padding: 10px 0;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.supervision-row:first-child {
+  padding-top: 0;
+}
+
+.supervision-row:last-child {
+  padding-bottom: 0;
+  border-bottom: 0;
+}
+
+.supervision-meta {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  color: #303133;
+  font-size: 13px;
+}
+
+.supervision-meta span {
+  color: #909399;
+  font-size: 12px;
+}
+
+.supervision-content {
+  margin-top: 4px;
+  color: #606266;
+  font-size: 13px;
+  line-height: 1.6;
+  overflow-wrap: anywhere;
+  white-space: pre-wrap;
+}
+
+:global(.warning-detail-drawer .el-drawer__header) {
+  margin-bottom: 0;
+  padding: 16px 20px 10px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+:global(.warning-detail-drawer .el-drawer__body) {
+  padding: 12px 20px 24px;
+}
+
+:global(.warning-detail-drawer .item-summary) {
+  margin: 16px 0 0;
+}
+
+:global(.warning-detail-drawer .el-descriptions__label) {
+  width: 96px;
+}
+
+@media (max-width: 768px) {
+  .warning-detail-heading {
+    align-items: flex-start;
+    flex-direction: column;
+    gap: 2px;
+  }
+
+  :global(.warning-detail-drawer .el-drawer__header),
+  :global(.warning-detail-drawer .el-drawer__body) {
+    padding-right: 14px;
+    padding-left: 14px;
+  }
+}
+</style>

+ 235 - 0
src/views/subSystem/lifeCompany/components/WarningDisposalTimeline.vue

@@ -0,0 +1,235 @@
+<template>
+  <div v-if="groups.length" class="warning-history">
+    <div class="history-hint">按时间倒序分组,滚动即可回溯</div>
+    <section v-for="group in groups" :key="group.day" class="history-group">
+      <div class="history-day">{{ dayLabel(group.day) }}</div>
+      <div
+        v-for="(item, index) in group.items"
+        :key="recordKey(item, index)"
+        class="history-row"
+        :class="{ 'last-record': index === group.items.length - 1 }"
+      >
+        <span class="history-time">{{ timeLabel(recordTime(item)) }}</span>
+        <div class="history-body">
+          <div class="history-title">
+            <b>{{ recordUser(item) }}</b>
+            <span class="history-separator">·</span>
+            {{ typeLabel(item) }}
+          </div>
+          <div v-if="levelChange(item)" class="history-content">预警级别:{{ levelChange(item) }}</div>
+          <div v-if="recordContent(item)" class="history-content">{{ recordContent(item) }}</div>
+          <WarningDisposalReasons :record="item" />
+          <WarningAttachments :attachments="attachmentsForDisposal(item, attachments)" image-thumbnails />
+        </div>
+      </div>
+    </section>
+  </div>
+  <el-empty v-else :description="emptyText" />
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import WarningAttachments from './WarningAttachments.vue'
+import WarningDisposalReasons from './WarningDisposalReasons.vue'
+import { attachmentsForDisposal } from '../warningAttachments'
+
+const props = defineProps({
+  records: { type: Array, default: () => [] },
+  attachments: { type: Array, default: () => [] },
+  levelLabels: { type: Object, default: () => ({}) },
+  emptyText: { type: String, default: '暂无处置记录' }
+})
+
+const typeLabels = {
+  RELEASE: '发布预警',
+  UPGRADE: '升级预警',
+  RESOLVE: '解除预警',
+  AUTO_CLEAR: '系统自动解除',
+  CLEAR: '核查解除',
+  RECEIVE: '接收预警',
+  START: '开始处置',
+  HANDLE: '提交处置结果',
+  RETURN: '退回重办',
+  SUPERVISION: '预警督办',
+  INSTRUCTION: '领导批示'
+}
+
+const groups = computed(() => {
+  const byDay = new Map()
+  ;(props.records || []).forEach((item) => {
+    const time = normalizeDate(recordTime(item))
+    const day = time ? localDateKey(time) : '-'
+    if (!byDay.has(day)) byDay.set(day, [])
+    byDay.get(day).push(item)
+  })
+  return Array.from(byDay.entries())
+    .sort(([left], [right]) => right.localeCompare(left))
+    .map(([day, items]) => ({
+      day,
+      items: items.slice().sort((left, right) => timestamp(recordTime(right)) - timestamp(recordTime(left)))
+    }))
+})
+
+function valueOf(item, camel, snake) {
+  return item?.[camel] ?? item?.[snake]
+}
+
+function recordKey(item, index) {
+  return valueOf(item, 'disposalId', 'disposal_id') || `${recordTime(item)}-${index}`
+}
+
+function recordTime(item) {
+  return valueOf(item, 'createTime', 'create_time') || valueOf(item, 'disposalTime', 'disposal_time') || ''
+}
+
+function recordUser(item) {
+  return valueOf(item, 'disposalUserName', 'disposal_user_name')
+    || valueOf(item, 'disposalUser', 'disposal_user')
+    || '-'
+}
+
+function recordContent(item) {
+  return valueOf(item, 'disposalContent', 'disposal_content') || item?.content || ''
+}
+
+function typeLabel(item) {
+  const type = valueOf(item, 'disposalType', 'disposal_type')
+  return valueOf(item, 'disposalTypeName', 'disposal_type_name') || typeLabels[type] || type || '处置记录'
+}
+
+function levelChange(item) {
+  const oldLevel = valueOf(item, 'oldLevel', 'old_level')
+  const newLevel = valueOf(item, 'newLevel', 'new_level')
+  if (oldLevel == null && newLevel == null) return ''
+  const label = value => props.levelLabels[value] || value || '-'
+  return `${label(oldLevel)} -> ${label(newLevel)}`
+}
+
+function normalizeDate(value) {
+  if (!value) return null
+  if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value
+  const text = String(value).trim().replace(' ', 'T')
+  const date = new Date(text)
+  return Number.isNaN(date.getTime()) ? null : date
+}
+
+function timestamp(value) {
+  return normalizeDate(value)?.getTime() || 0
+}
+
+function localDateKey(date) {
+  const pad = value => String(value).padStart(2, '0')
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
+}
+
+function timeLabel(value) {
+  const date = normalizeDate(value)
+  if (!date) return '--:--'
+  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+}
+
+function dayLabel(day) {
+  if (day === '-') return '时间未知'
+  const date = normalizeDate(`${day}T00:00:00`)
+  if (!date) return day
+  const today = new Date()
+  const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
+  const prefix = day === localDateKey(today) ? '今天 · ' : day === localDateKey(yesterday) ? '昨天 · ' : ''
+  const year = date.getFullYear() === today.getFullYear() ? '' : `${date.getFullYear()}年`
+  return `${prefix}${year}${date.getMonth() + 1}月${date.getDate()}日`
+}
+</script>
+
+<style scoped>
+.history-hint {
+  padding-bottom: 10px;
+  margin-bottom: 14px;
+  border-bottom: 1px solid #f0f2f4;
+  color: #909399;
+  font-size: 12px;
+}
+
+.history-day {
+  margin: 16px 0 4px;
+  color: #303133;
+  font-size: 13px;
+  font-weight: 500;
+}
+
+.history-row {
+  display: flex;
+  gap: 12px;
+  padding: 8px 0 8px 4px;
+}
+
+.history-time {
+  position: relative;
+  box-sizing: border-box;
+  width: 62px;
+  flex: none;
+  align-self: stretch;
+  padding-top: 1px;
+  padding-right: 22px;
+  color: #909399;
+  font-family: ui-monospace, Consolas, "Courier New", monospace;
+  font-size: 12px;
+}
+
+.history-time::before {
+  position: absolute;
+  top: 5px;
+  right: 8px;
+  z-index: 1;
+  width: 7px;
+  height: 7px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  background: #3b5bdb;
+  box-shadow: 0 0 0 1px #9bb3d9;
+  content: '';
+  transform: translateX(50%);
+}
+
+.history-time::after {
+  position: absolute;
+  top: 10px;
+  right: 8px;
+  bottom: -27px;
+  width: 1px;
+  background: repeating-linear-gradient(to bottom, #3b5bdb 0 4px, transparent 4px 8px);
+  content: '';
+  transform: translateX(50%);
+}
+
+.last-record .history-time::after {
+  display: none;
+}
+
+.history-body {
+  min-width: 0;
+  flex: 1;
+}
+
+.history-title {
+  color: #303133;
+  font-size: 13px;
+}
+
+.history-title b {
+  font-weight: 700;
+}
+
+.history-separator {
+  margin: 0 4px;
+  color: #909399;
+}
+
+.history-content {
+  margin-top: 3px;
+  color: #606266;
+  font-size: 12.5px;
+  line-height: 1.6;
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
+}
+</style>

+ 5 - 28
src/views/subSystem/lifeCompany/lAlertInformation/LCorrelation.vue

@@ -207,28 +207,7 @@
       </template>
     </el-dialog>
 
-    <el-drawer v-model="warningDetailVisible" title="预警详情" size="900px" append-to-body class="detail-drawer" direction="rtl">
-      <template v-if="warningDetailData.basicInfo">
-        <el-descriptions :column="2" border>
-          <el-descriptions-item label="预警编号">{{ warningDetailData.basicInfo.warningNo || "-" }}</el-descriptions-item>
-          <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="状态"><WarningStatusTag :status="warningDetailData.basicInfo.status" /></el-descriptions-item>
-          <el-descriptions-item label="内容描述" :span="2">{{ warningDetailData.basicInfo.warningContent || "-" }}</el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">已关联报警</el-divider>
-        <el-table :data="warningDetailData.alarmList || []" border size="small">
-          <el-table-column label="设备编码" prop="device_code" min-width="160" />
-          <el-table-column label="告警类型" prop="warning_type" width="120" />
-          <el-table-column label="告警编码" prop="warning_code" width="160" />
-          <el-table-column label="阈值" prop="warning_value" width="120" />
-          <el-table-column label="实际值" prop="actual_value" width="120" />
-          <el-table-column label="报警时间" prop="createTime" width="180" />
-        </el-table>
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer v-model="warningDetailVisible" :warning-id="selectedWarningId" />
 
     <el-drawer v-model="alarmDetailVisible" title="报警详情" size="680px" append-to-body class="detail-drawer" direction="rtl">
       <el-descriptions :column="2" border v-if="currentAlarmDetail">
@@ -247,7 +226,7 @@
 </template>
 
 <script setup>
-import WarningStatusTag from "../components/WarningStatusTag.vue"
+import WarningDetailDrawer from "../components/WarningDetailDrawer.vue"
 import { computed, onMounted, reactive, ref } from "vue";
 import auth from '@/plugins/auth';
 import { getRouters } from '@/api/menu';
@@ -257,7 +236,6 @@ import {
   getAlarmPageData,
   getAlertTypeDictionaryData,
   getLinkedAlarmListData,
-  getWarningDetailData,
   getdbyjlistData,
   updateWarningAlarm
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
@@ -279,8 +257,8 @@ const canViewRelations = computed(() => auth.hasPermi('warning:warning:query'));
 
 const total = ref(0);
 const warningDetailVisible = ref(false);
+const selectedWarningId = ref("");
 const alarmDetailVisible = ref(false);
-const warningDetailData = ref({});
 const currentAlarmDetail = ref(null);
 
 const associationDialog = reactive({
@@ -608,9 +586,8 @@ async function confirmBatchAssociation() {
   }
 }
 
-async function openWarningDetail(warningId) {
-  const res = await getWarningDetailData(warningId);
-  warningDetailData.value = res.data || {};
+function openWarningDetail(warningId) {
+  selectedWarningId.value = warningId;
   warningDetailVisible.value = true;
 }
 

+ 154 - 184
src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue

@@ -65,14 +65,12 @@
 
     <el-card shadow="never" class="page-card">
       <el-table v-loading="loading" :data="warningList">
-        <el-table-column label="序号" width="70" align="center">
-          <template #default="scope">
-            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
-          </template>
-        </el-table-column>
-        <el-table-column label="预警编号" prop="warningNo" width="180" />
+        <el-table-column label="预警编号" prop="warningNo" width="210" show-overflow-tooltip />
 
         <el-table-column label="预警类型" prop="warningTypeText" width="120" />
+        <el-table-column label="预警内容" min-width="240">
+          <template #default="scope"><WarningContentCell :content="scope.row.warningContent" /></template>
+        </el-table-column>
         <el-table-column label="预警级别" width="110" align="center">
           <template #default="scope">
             <el-tag :type="getLevelTagType(scope.row.warningLevel)">
@@ -96,7 +94,7 @@
         </el-table-column>
         <el-table-column label="操作" fixed="right" width="280" align="center">
           <template #default="scope">
-            <el-button v-hasPermi="['warning:warning:query']" link type="primary" @click="handleView(scope.row)">查看</el-button>
+            <el-button v-hasPermi="['warning:warning:query']" link type="primary" @click="handleView(scope.row)">详情</el-button>
             <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:edit']" link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
             <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:publish']" link type="success" @click="handlePublish(scope.row)">发布</el-button>
             <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:remove']" link type="danger" @click="handleDelete(scope.row)">删除</el-button>
@@ -118,7 +116,7 @@
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="预警类型" prop="warningType">
-              <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="handleTypeLevelChange">
+              <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="handleWarningTypeChange">
                 <el-option
                   v-for="item in warningTypeOptions"
                   :key="item.dictValue"
@@ -140,49 +138,9 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-form-item label="关联预警事项">
-              <el-select v-model="formData.itemId" clearable filterable :loading="itemLoading" placeholder="可选;不关联事项也可直接发布" style="width:100%" @change="selectItem">
-                <el-option v-for="item in itemOptions" :key="item.itemId" :label="`${item.itemName}(${item.itemCode})`" :value="item.itemId" />
-              </el-select>
-              <div class="el-form-item__extra">仅列出当前类型、级别下启用的事项。关联后带出处置时限与要求。</div>
-            </el-form-item>
-            <WarningItemSummary :warning="formData" />
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="预警专项">
-              <el-input v-model="formData.warningSpecial" placeholder="请输入预警专项" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="权属单位">
-              <el-input v-model="formData.ownershipUnit" placeholder="请输入权属单位" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="发布人">
-              <el-input :model-value="formData.publisher || '发布时确定'" disabled />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="处置人">
-              <el-input v-model="formData.handler" placeholder="请选择处置人" readonly @click="openUserPicker">
-                <template #suffix>
-                  <el-icon style="cursor: pointer;" @click="openUserPicker"><Search /></el-icon>
-                </template>
-              </el-input>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="核查人">
-              <el-input v-model="formData.reviewer" placeholder="默认发布人,可选择其他核查人" readonly clearable @click="openReviewerPicker">
-                <template #suffix><el-icon style="cursor:pointer" @click="openReviewerPicker"><Search /></el-icon></template>
-              </el-input>
-            </el-form-item>
-          </el-col>
           <el-col :span="24">
             <el-form-item label="关联设备" prop="deviceCode">
-              <el-input :model-value="formData.deviceName || formData.deviceCode" placeholder="点击选择关联设备" readonly @click="openDevicePicker">
+              <el-input :model-value="formData.deviceName || formData.deviceCode" placeholder="请先选择预警类型,再选择对应类型的设备" readonly @click="openDevicePicker">
                 <template #append>
                   <el-button :icon="Search" @click="openDevicePicker">选择</el-button>
                 </template>
@@ -201,6 +159,25 @@
               />
             </el-form-item>
           </el-col>
+          <el-col :span="24">
+            <el-form-item label="关联预警事项">
+              <el-select v-model="formData.itemId" clearable filterable :loading="itemLoading" placeholder="可选;不关联事项也可直接发布" style="width:100%" @change="selectItem">
+                <el-option v-for="item in itemOptions" :key="item.itemId" :label="`${item.itemName}(${item.itemCode})`" :value="item.itemId" />
+              </el-select>
+              <div class="el-form-item__extra">仅列出当前类型、级别下启用的事项。关联后带出处置时限与要求。</div>
+            </el-form-item>
+            <WarningItemSummary :warning="formData" />
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="预警专项">
+              <el-input v-model="formData.warningSpecial" placeholder="请输入预警专项" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="权属单位">
+              <el-input v-model="formData.ownershipUnit" placeholder="请输入权属单位" />
+            </el-form-item>
+          </el-col>
           <el-col :span="24" v-if="reasonOptions.length">
             <el-form-item label="推荐原因">
               <div class="reason-wrap">
@@ -267,90 +244,40 @@
       </template>
     </el-dialog>
 
-    <el-drawer v-model="detailVisible" title="预警详情" size="900px" append-to-body class="detail-drawer" direction="rtl">
-      <template v-if="detailData.basicInfo">
-        <WarningItemSummary :warning="detailData.basicInfo" />
-        <el-descriptions :column="2" border>
-          <el-descriptions-item label="预警编号">{{ detailData.basicInfo.warningNo || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="预警类型">{{ formatTypeText(detailData.basicInfo.warningType) }}</el-descriptions-item>
-          <el-descriptions-item label="预警级别">
-            <el-tag :type="getLevelTagType(detailData.basicInfo.warningLevel)">
-              {{ formatLevelText(detailData.basicInfo.warningLevel) }}
-            </el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="预警专项">{{ detailData.basicInfo.warningSpecial || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ detailData.basicInfo.ownershipUnit || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布人">{{ detailData.basicInfo.publisher || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="处置人">{{ detailData.basicInfo.handler || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="关联设备" :span="2">
-            <template v-if="detailData.basicInfo.deviceCode || detailData.basicInfo.deviceName">
-              {{ detailData.basicInfo.deviceName || "-" }}
-              <span v-if="detailData.basicInfo.deviceCode" style="color: #909399;">({{ detailData.basicInfo.deviceCode }})</span>
-            </template>
-            <span v-else>-</span>
-          </el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ formatDateTime(detailData.basicInfo.publishTime) }}</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>
-
-        <el-divider content-position="left">关联报警</el-divider>
-        <el-table :data="detailData.alarmList || []" border size="small">
-          <el-table-column label="报警ID" prop="alarmId" min-width="160" />
-          <el-table-column label="报警名称" prop="alarmName" min-width="180" show-overflow-tooltip />
-          <el-table-column label="报警类型" min-width="120">
-            <template #default="scope">
-              {{ formatTypeText(scope.row.alarmType || scope.row.warningType) }}
-            </template>
-          </el-table-column>
-        </el-table>
-
-        <el-divider content-position="left">处置记录</el-divider>
-        <el-table :data="detailData.disposalHistory || detailData.disposalList || []" border size="small">
-          <el-table-column label="处置类型" prop="disposalTypeName" min-width="120" />
-          <el-table-column label="处置人" prop="disposalUser" min-width="120" />
-          <el-table-column label="处置内容" prop="disposalContent" min-width="220" show-overflow-tooltip />
-          <el-table-column label="处置时间" prop="createTime" min-width="180" />
-          <el-table-column label="附件" min-width="300">
-            <template #default="{ row }">
-              <WarningAttachments :attachments="attachmentsForDisposal(row, detailData.attachmentList)" empty-text="无附件" />
-            </template>
-          </el-table-column>
-        </el-table>
-
-        <el-divider content-position="left">其他预警附件</el-divider>
-        <WarningAttachments :attachments="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList)" empty-text="暂无其他附件" />
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedWarningId" />
 
     <!-- 关联设备选择对话框 -->
     <el-dialog v-model="devicePickerVisible" title="选择关联设备" width="860px" append-to-body @open="loadDevicePickerData">
       <div class="device-picker-container">
-        <div class="device-picker-left">
+        <div v-loading="deviceConfigLoading" class="device-picker-left">
           <div class="picker-title">设备类型</div>
           <button
-            :class="{ active: deviceRootTypeId === '' }"
+            :class="{ active: selectedDeviceTypeId === '' }"
             class="device-type-item"
-            @click="selectDeviceRootType('')"
+            @click="selectDeviceType('')"
           >
             全部类型
           </button>
           <button
-            v-for="item in deviceRootTypes"
+            v-for="item in availableDeviceTypes"
             :key="item.id"
-            :class="{ active: String(deviceRootTypeId) === String(item.id) }"
+            :class="{ active: String(selectedDeviceTypeId) === String(item.id) }"
             class="device-type-item"
-            @click="selectDeviceRootType(item.id)"
+            @click="selectDeviceType(item.id)"
           >
             {{ item.typeName }}
           </button>
+          <div v-if="!deviceConfigLoading && !availableDeviceTypes.length" class="device-type-empty">暂无可选设备类型</div>
         </div>
         <div class="device-picker-right">
+          <el-alert
+            v-if="!deviceConfigLoading && !deviceTypeIds.length"
+            title="当前预警类型未配置可关联的设备类型"
+            type="warning"
+            :closable="false"
+            show-icon
+            style="margin-bottom: 12px"
+          />
           <el-form :inline="true" @submit.prevent>
             <el-form-item>
               <el-input v-model="deviceQuery.equipmentCode" placeholder="设备编码" clearable style="width: 180px" @keyup.enter="loadDeviceList" />
@@ -388,8 +315,30 @@
       </div>
     </el-dialog>
 
+    <el-dialog v-model="publishVisible" title="发布预警" width="560px" append-to-body destroy-on-close>
+      <el-form label-width="90px">
+        <el-form-item label="预警编号">
+          <el-input :model-value="publishRow?.warningNo || '-'" disabled />
+        </el-form-item>
+        <el-form-item label="处置人" required>
+          <el-input v-model="publishForm.handler" placeholder="请选择处置人" readonly @click="openUserPicker('handler')">
+            <template #append><el-button :icon="Search" @click="openUserPicker('handler')">选择</el-button></template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="核验人" required>
+          <el-input v-model="publishForm.reviewer" placeholder="请选择核验人" readonly @click="openUserPicker('reviewer')">
+            <template #append><el-button :icon="Search" @click="openUserPicker('reviewer')">选择</el-button></template>
+          </el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="publishVisible = false">取消</el-button>
+        <el-button type="primary" :loading="publishLoading" @click="submitPublish">确认发布</el-button>
+      </template>
+    </el-dialog>
+
     <!-- 处置人选择对话框 -->
-    <el-dialog v-model="userPickerVisible" :title="pickerTarget === 'reviewer' ? '选择核查人' : '选择处置人'" width="900px" append-to-body @open="loadUserPickerData" @closed="closeUserPicker">
+    <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>
@@ -445,6 +394,8 @@
 
 <script setup>
 import WarningStatusTag from "../components/WarningStatusTag.vue"
+import WarningContentCell from "../components/WarningContentCell.vue"
+import WarningDetailDrawer from "../components/WarningDetailDrawer.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import WarningItemSummary from '../components/WarningItemSummary.vue';
@@ -452,19 +403,17 @@ import {computed, onMounted, reactive, ref, watch} from "vue";
 import {ElMessage, ElMessageBox} from "element-plus";
 import { Search } from "@element-plus/icons-vue";
 import { parseTime } from "@/utils/ruoyi";
-import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
-import WarningAttachments from "../components/WarningAttachments.vue";
-import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
+import { getWarningFullDetailData } from "@/api/pipeNetwork/earlyWarning";
 import { listEquipment, listEquipmentTypeAll } from "@/api/pipeNetwork/pdeviceInform";
 import {
   deleteYjxxData,
   getAlertTypeDictionaryData,
   getdbyjlistData,
-  getWarningDetailData,
   getWarningReasonListData,
   getWarningItemListData,
   getWarningParticipantDepartments,
   getWarningParticipants,
+  getWarningEquipmentTypeIdsData,
   releaseYjxxData,
   submitYjxxData,
   updateYjxxData
@@ -488,19 +437,25 @@ const editVisible = ref(false);
 const detailVisible = ref(false);
 const userPickerVisible = ref(false);
 const pickerTarget = ref('handler');
+const publishVisible = ref(false);
+const publishLoading = ref(false);
+const publishRow = ref(null);
+const publishForm = reactive({ handler: '', reviewer: '' });
 const dialogTitle = ref("新增预警");
 const isEdit = ref(false);
 const formRef = ref();
-const detailData = ref({});
+const selectedWarningId = ref("");
 
 // 关联设备选择器
 const devicePickerVisible = ref(false);
-const deviceRootTypeId = ref("");
-const deviceRootTypes = ref([]);
+const selectedDeviceTypeId = ref("");
+const availableDeviceTypes = ref([]);
 const deviceList = ref([]);
 const deviceTotal = ref(0);
 const deviceLoading = ref(false);
+const deviceConfigLoading = ref(false);
 const deviceTypeNameMap = ref({});
+const deviceTypeIds = ref([]);
 const deviceQuery = reactive({
   pageNum: 1,
   pageSize: 10,
@@ -574,10 +529,7 @@ function createEmptyForm() {
 
 
     ownershipUnit: "",
-    publisher: "",
     publishTime: "",
-    handler: "",
-    reviewer: "",
     status: "DRAFT",
     warningContent: "",
     remark: ""
@@ -590,6 +542,8 @@ function resetForm() {
   itemOptions.value = [];
   itemRequestVersion++;
   formAttachments.value = [];
+  deviceTypeIds.value = [];
+  selectedDeviceTypeId.value = '';
   uploadRef.value?.clearFiles?.();
   formRef.value?.clearValidate();
 }
@@ -695,25 +649,6 @@ function normalizeWarningRow(item) {
   return row;
 }
 
-function normalizeAlarmRow(item) {
-  return {
-    ...item,
-    alarmId: item.alarmId || item.alarm_id || "",
-    alarmName: item.alarmName || item.alarm_name || item.deviceName || item.device_name || "",
-    alarmType: item.alarmType || item.alarm_type || item.warningType || item.warning_type || ""
-  };
-}
-
-function normalizeDisposalRow(item) {
-  return {
-    ...item,
-    disposalTypeName: item.disposalTypeName || item.disposal_type_name || item.disposalType || item.disposal_type || "",
-    disposalUser: item.disposalUser || item.disposal_user || "",
-    disposalContent: item.disposalContent || item.disposal_content || "",
-    createTime: item.createTime || item.create_time || ""
-  };
-}
-
 async function loadDicts() {
   const [typeResult, levelResult] = await Promise.allSettled([
     getAlertTypeDictionaryData({ dictType: "alert_type" }),
@@ -744,6 +679,14 @@ function handleTypeLevelChange() {
   loadReasonOptions().catch(() => { reasonOptions.value = []; });
   loadItemOptions();
 }
+
+function handleWarningTypeChange() {
+  formData.deviceCode = '';
+  formData.deviceName = '';
+  deviceTypeIds.value = [];
+  formRef.value?.clearValidate('deviceCode');
+  handleTypeLevelChange();
+}
 async function loadItemOptions() {
   const version = ++itemRequestVersion;
   itemOptions.value = [];
@@ -885,16 +828,8 @@ function handleUserCurrentChange(row) {
   selectedUser.value = row;
 }
 
-function openUserPicker() {
-  pickerTarget.value = 'handler';
-  selectedUser.value = null;
-  userFilterText.value = "";
-  deptFilterText.value = "";
-  userPickerVisible.value = true;
-}
-
-function openReviewerPicker() {
-  pickerTarget.value = 'reviewer';
+function openUserPicker(target) {
+  pickerTarget.value = target;
   selectedUser.value = null;
   userFilterText.value = '';
   deptFilterText.value = '';
@@ -906,39 +841,60 @@ function confirmSelectUser() {
     ElMessage.warning("请选择用户");
     return;
   }
-  formData[pickerTarget.value] = selectedUser.value.userName;
+  publishForm[pickerTarget.value] = selectedUser.value.userName;
   userPickerVisible.value = false;
 }
 
 function openDevicePicker() {
+  if (!formData.warningType) {
+    ElMessage.warning('请先选择预警类型');
+    return;
+  }
   devicePickerVisible.value = true;
 }
 
 async function loadDevicePickerData() {
+  deviceConfigLoading.value = true;
+  selectedDeviceTypeId.value = '';
   deviceQuery.pageNum = 1;
   deviceQuery.equipmentCode = "";
   deviceQuery.equipmentName = "";
   try {
-    const typeRes = await listEquipmentTypeAll();
+    const [typeRes, relationRes] = await Promise.all([
+      listEquipmentTypeAll(),
+      getWarningEquipmentTypeIdsData(formData.warningType)
+    ]);
     const allTypes = typeRes.data || [];
-    deviceRootTypes.value = allTypes.filter((t) => String(t.parentId) === "0" || t.parentId === 0 || t.parentId === null || t.parentId === undefined || t.parentId === "");
+    deviceTypeIds.value = (relationRes.data || []).map(String);
+    const allowed = new Set(deviceTypeIds.value);
+    // 左栏名称和主键直接取 equipment_type 接口,关联表只负责约束可选范围。
+    availableDeviceTypes.value = allTypes.filter(type => allowed.has(String(type.id)));
     deviceTypeNameMap.value = allTypes.reduce((acc, t) => {
       acc[t.id] = t.typeName;
       return acc;
     }, {});
   } catch (e) {
-    deviceRootTypes.value = [];
+    availableDeviceTypes.value = [];
+    deviceTypeIds.value = [];
+    ElMessage.error(e?.msg || e?.message || '加载预警类型对应设备失败');
+  } finally {
+    deviceConfigLoading.value = false;
   }
   await loadDeviceList();
 }
 
-function selectDeviceRootType(rootTypeId) {
-  deviceRootTypeId.value = rootTypeId === "" ? "" : String(rootTypeId);
+function selectDeviceType(typeId) {
+  selectedDeviceTypeId.value = typeId === "" ? "" : String(typeId);
   deviceQuery.pageNum = 1;
   loadDeviceList();
 }
 
 async function loadDeviceList() {
+  if (!deviceTypeIds.value.length) {
+    deviceList.value = [];
+    deviceTotal.value = 0;
+    return;
+  }
   deviceLoading.value = true;
   try {
     const res = await listEquipment({
@@ -946,7 +902,8 @@ async function loadDeviceList() {
       pageSize: deviceQuery.pageSize,
       equipmentCode: deviceQuery.equipmentCode || undefined,
       equipmentName: deviceQuery.equipmentName || undefined,
-      rootTypeId: deviceRootTypeId.value || undefined
+      equipmentTypeId: selectedDeviceTypeId.value || undefined,
+      equipmentTypeIds: deviceTypeIds.value.join(',')
     });
     deviceList.value = res.records || [];
     deviceTotal.value = Number(res.total || 0);
@@ -980,9 +937,8 @@ async function handleEdit(row) {
   dialogTitle.value = "编辑预警";
   resetForm();
   Object.assign(formData, { ...row });
-  const detailRes = await getWarningDetailData(row.warningId);
+  const detailRes = await getWarningFullDetailData(row.warningId);
   const detail = detailRes.data || {};
-  formData.reviewer = detail.basicInfo?.reviewer || "";
   formAttachments.value = (detail.attachmentList || []).map(normalizeExistingAttachment);
   formData.itemId = detail.basicInfo?.item_id || detail.basicInfo?.itemId || "";
   formData.itemSnapshot = detail.basicInfo?.item_snapshot || detail.basicInfo?.itemSnapshot || "";
@@ -1007,8 +963,6 @@ async function submitForm() {
 
 
         ownershipUnit: formData.ownershipUnit,
-        handler: formData.handler,
-        reviewer: formData.reviewer,
         warningContent: formData.warningContent,
         deviceCode: formData.deviceCode || "",
         deviceName: formData.deviceName || "",
@@ -1030,15 +984,7 @@ async function submitForm() {
 }
 
 async function handleView(row) {
-  const res = await getWarningDetailData(row.warningId);
-  const detail = res.data || {};
-  const basicSource = detail.basicInfo || detail;
-  detailData.value = {
-    ...detail,
-    basicInfo: normalizeWarningRow({ ...row, ...basicSource }),
-    alarmList: (detail.alarmList || []).map(normalizeAlarmRow),
-    disposalHistory: (detail.disposalHistory || detail.disposalList || []).map(normalizeDisposalRow)
-  };
+  selectedWarningId.value = row.warningId;
   detailVisible.value = true;
 }
 
@@ -1054,18 +1000,33 @@ async function handleDelete(row) {
 }
 
 async function handlePublish(row) {
-  if (!row.handler) {
-    ElMessage.warning("发布前请先编辑预警并选择处置人");
+  publishRow.value = row;
+  publishForm.handler = '';
+  publishForm.reviewer = '';
+  publishVisible.value = true;
+}
+
+async function submitPublish() {
+  if (!publishForm.handler || !publishForm.reviewer) {
+    ElMessage.warning('请选择处置人和核验人');
     return;
   }
-  await ElMessageBox.confirm(`确认发布预警“${row.warningNo}”吗?`, "提示", { type: "warning" });
-  const res = await releaseYjxxData(row.warningId, { reviewer: row.reviewer || undefined });
-  if (res.code !== 200) {
-    ElMessage.error(res.msg || "发布失败");
-    return;
+  publishLoading.value = true;
+  try {
+    const res = await releaseYjxxData(publishRow.value.warningId, {
+      handler: publishForm.handler,
+      reviewer: publishForm.reviewer
+    });
+    if (res.code !== 200) {
+      ElMessage.error(res.msg || '发布失败');
+      return;
+    }
+    ElMessage.success('发布成功');
+    publishVisible.value = false;
+    await loadList();
+  } finally {
+    publishLoading.value = false;
   }
-  ElMessage.success("发布成功");
-  loadList();
 }
 
 onMounted(async () => {
@@ -1259,4 +1220,13 @@ onMounted(async () => {
   background: #eff6ff;
   font-weight: 600;
 }
+
+.device-type-empty {
+  padding: 16px 4px;
+  color: #909399;
+  font-size: 12px;
+  line-height: 1.5;
+  text-align: center;
+}
+
 </style>

+ 3 - 3
src/views/subSystem/lifeCompany/lAlertInformation/LReportUpload.vue

@@ -256,11 +256,11 @@ import * as echarts from "echarts";
 import {
   getAlertTypeDictionaryData,
   getLinkedAlarmListData,
-    getWarningDetailData,
-    uploadWarningReportData,
+  uploadWarningReportData,
   getdbyjlistData,
   updateYjxxData
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
+import { getWarningFullDetailData } from "@/api/pipeNetwork/earlyWarning";
 
 const pageLoading = ref(false);
 const submitLoading = ref(false);
@@ -475,7 +475,7 @@ async function loadWarningDetail(warningId) {
   }
   pageLoading.value = true;
   try {
-    const res = await getWarningDetailData(warningId);
+    const res = await getWarningFullDetailData(warningId);
     warningDetail.value = res.data || {};
     await nextTick();
     renderCurveChart();

+ 16 - 109
src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue

@@ -40,9 +40,6 @@
             @keyup.enter="handleQuery"
           />
         </el-form-item>
-        <el-form-item label="预警专项">
-          <el-input v-model="queryParams.warningSpecial" placeholder="请输入预警专项" clearable style="width: 200px" />
-        </el-form-item>
         <el-form-item label="预警类型">
           <el-select v-model="queryParams.warningType" clearable placeholder="请选择预警类型" style="width: 180px">
             <el-option
@@ -63,18 +60,9 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="处置人">
-          <el-input v-model="queryParams.handler" placeholder="请输入处置人" clearable style="width: 180px" />
-        </el-form-item>
-        <el-form-item label="发布人">
-          <el-input v-model="queryParams.publisher" placeholder="请输入发布人" clearable style="width: 180px" />
-        </el-form-item>
         <el-form-item label="报警编号">
           <el-input v-model="queryParams.alarmCode" placeholder="请输入报警编号" clearable style="width: 180px" />
         </el-form-item>
-        <el-form-item label="权属单位">
-          <el-input v-model="queryParams.ownershipUnit" placeholder="请输入权属单位" clearable style="width: 180px" />
-        </el-form-item>
         <el-form-item label="发布时间">
           <el-date-picker
             v-model="queryParams.dateRange"
@@ -101,12 +89,6 @@
       </el-tabs>
 
       <el-table :data="pagedList" @row-click="handleViewDetail">
-        <el-table-column label="序号" width="70" align="center">
-          <template #default="scope">
-            {{ (pagination.pageNum - 1) * pagination.pageSize + scope.$index + 1 }}
-          </template>
-        </el-table-column>
-
         <el-table-column v-if="activeTab === 'NOTICE'" label="督办通知" prop="taskName" min-width="250" show-overflow-tooltip />
         <el-table-column v-if="activeTab === 'NOTICE'" label="阅读状态" width="90">
           <template #default="scope">{{ scope.row.readStatus === 'READ' ? '已读' : '未读' }}</template>
@@ -114,9 +96,12 @@
         <el-table-column label="当前环节截止" min-width="165">
           <template #default="{ row }">{{ formatDateTime(row.stage_deadline || row.stageDeadline) }}</template>
         </el-table-column>
-        <el-table-column label="预警编号" prop="warningNo" width="170" />
+        <el-table-column label="预警编号" prop="warningNo" width="210" show-overflow-tooltip />
         <el-table-column label="预警专项" prop="warningSpecial" width="140" show-overflow-tooltip />
         <el-table-column label="预警类型" prop="warningTypeText" width="120" />
+        <el-table-column label="预警内容" min-width="240">
+          <template #default="scope"><WarningContentCell :content="scope.row.warningContent" /></template>
+        </el-table-column>
         <el-table-column label="处置人" prop="handler" width="120" />
         <el-table-column label="发布人" prop="publisher" width="120" />
         <el-table-column label="报警编号" width="160">
@@ -144,7 +129,7 @@
         </el-table-column>
         <el-table-column label="操作" fixed="right" width="300" align="center">
           <template #default="scope">
-            <el-button v-hasPermi="['warning:warning:query']" link type="primary" @click.stop="handleViewDetail(scope.row)">查看详情</el-button>
+            <el-button v-hasPermi="['warning:warning:query']" link type="primary" @click.stop="handleViewDetail(scope.row)">详情</el-button>
             <template v-if="activeTab === 'TODO'">
               <WarningWorkflowActions :row="scope.row" @changed="refreshLists" />
             </template>
@@ -161,71 +146,26 @@
       />
     </el-card>
 
-    <el-drawer v-model="detailVisible" title="预警详情" size="760px" append-to-body direction="rtl" class="detail-drawer">
-      <el-alert v-if="activeTab === 'NOTICE' && selectedNotice" :title="selectedNotice" type="info" :closable="false" style="margin-bottom: 16px" />
-      <template v-if="detailData.basicInfo">
-        <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>
-          <el-descriptions-item label="处置人">{{ detailBasic.handler || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布人">{{ detailBasic.publisher || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ detailBasic.ownershipUnit || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ formatDateTime(detailBasic.publishTime) }}</el-descriptions-item>
-          <el-descriptions-item label="预警内容" :span="2"><span class="warning-content">{{ detailBasic.warningContent || "-" }}</span></el-descriptions-item>
-          <el-descriptions-item label="备注" :span="2">{{ detailBasic.remark || "-" }}</el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">关联报警</el-divider>
-        <el-table :data="detailData.alarmList || []" border size="small">
-          <el-table-column label="报警编号" prop="warning_code" min-width="160" />
-          <el-table-column label="报警类型" prop="warning_type" min-width="120" />
-          <el-table-column label="设备编码" prop="device_code" min-width="160" />
-          <el-table-column label="实际值" prop="actual_value" min-width="100" />
-          <el-table-column label="报警时间" prop="createTime" min-width="180" />
-        </el-table>
-
-        <el-divider content-position="left">处置记录</el-divider>
-        <el-timeline v-if="(detailData.disposalHistory || []).length">
-          <el-timeline-item
-            v-for="(item, index) in detailData.disposalHistory"
-            :key="index"
-            :timestamp="formatDateTime(item.createTime || item.create_time || item.disposalTime || item.disposal_time)"
-          >
-            {{ item.disposalTypeName || item.disposal_type_name || item.disposalType || item.disposal_type || "处置记录" }}
-            <span v-if="item.disposalContent || item.disposal_content || item.content">:{{ item.disposalContent || item.disposal_content || item.content }}</span>
-            <span v-if="item.disposalUser || item.disposal_user">({{ item.disposalUser || item.disposal_user }})</span>
-            <WarningDisposalReasons :record="item" />
-            <WarningAttachments :attachments="attachmentsForDisposal(item, detailData.attachmentList)" />
-          </el-timeline-item>
-        </el-timeline>
-        <el-empty v-else description="暂无处置记录" />
-        <template v-if="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList).length">
-          <el-divider content-position="left">其他预警附件</el-divider>
-          <WarningAttachments :attachments="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList)" />
-        </template>
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer
+      v-model="detailVisible"
+      :warning-id="selectedWarningId"
+      :notice="activeTab === 'NOTICE' ? selectedNotice : ''"
+    />
 
   </div>
 </template>
 
 <script setup>
 import WarningStatusTag from "../components/WarningStatusTag.vue"
+import WarningContentCell from "../components/WarningContentCell.vue"
+import WarningDetailDrawer from "../components/WarningDetailDrawer.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
-import WarningItemSummary from '../components/WarningItemSummary.vue';
-import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
-import WarningAttachments from "../components/WarningAttachments.vue";
-import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
 import { computed, onMounted, reactive, ref } from "vue";
 import auth from '@/plugins/auth';
 import { parseTime } from "@/utils/ruoyi";
 import WarningWorkflowActions from "../WarningWorkflowActions.vue";
-import { getAlertTypeDictionaryData, getTodoListData, getWarningDetailData, readWarningNotification } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
+import { getAlertTypeDictionaryData, getTodoListData, readWarningNotification } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
 const loading = ref(false);
 const activeTab = ref("TODO");
@@ -238,19 +178,15 @@ const noticeTotal = ref(0);
 const warningTypeOptions = ref([]);
 const warningLevelOptions = ref([]);
 const detailVisible = ref(false);
-const detailData = ref({});
+const selectedWarningId = ref("");
 const selectedNotice = ref("");
 
 const queryParams = reactive({
 
   warningNo: "",
-  warningSpecial: "",
   warningType: "",
   warningLevel: "",
-  handler: "",
-  publisher: "",
   alarmCode: "",
-  ownershipUnit: "",
   dateRange: []
 });
 
@@ -285,27 +221,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-const detailBasic = computed(() => {
-  const basic = detailData.value.basicInfo || {};
-  const warningType = basic.warningType || basic.warning_type || "";
-  const warningLevel = basic.warningLevel || basic.warning_level || "";
-  return {
-
-    warningNo: basic.warningNo || basic.warning_no || "",
-    warningSpecial: basic.warningSpecial || basic.warning_special || "",
-    warningTypeText: warningTypeLabelMap.value[warningType] || warningType || "-",
-    warningLevelText: warningLevelLabelMap.value[warningLevel] || "-",
-    handler: basic.handler || "",
-    publisher: basic.publisher || "",
-    ownershipUnit: basic.ownershipUnit || basic.ownership_unit || "",
-    publishTime: basic.publishTime || basic.publish_time || "",
-
-
-    warningContent: basic.warningContent || basic.warning_content || "",
-    remark: basic.remark || ""
-  };
-});
-
 function formatDateTime(value) {
   return value ? parseTime(value) : "-";
 }
@@ -343,6 +258,7 @@ function enrichTodoRows(records) {
 
       warningNo: item.warning_no || item.warningNo || "",
       warningSpecial: item.warning_special || item.warningSpecial || "",
+      warningContent: item.warning_content || item.warningContent || "",
       warningType,
       warningTypeText: warningTypeLabelMap.value[warningType] || warningType || "-",
       warningLevel,
@@ -368,13 +284,9 @@ async function loadTodoList(type) {
     todoType: type,
 
     warningNo: queryParams.warningNo || undefined,
-    warningSpecial: queryParams.warningSpecial || undefined,
     warningType: queryParams.warningType || undefined,
     warningLevel: queryParams.warningLevel || undefined,
-    handler: queryParams.handler || undefined,
-    publisher: queryParams.publisher || undefined,
     alarmCode: queryParams.alarmCode || undefined,
-    ownershipUnit: queryParams.ownershipUnit || undefined,
     startTime: startTime || undefined,
     endTime: endTime || undefined
   });
@@ -433,13 +345,9 @@ function handleQuery() {
 function resetQuery() {
 
   queryParams.warningNo = "";
-  queryParams.warningSpecial = "";
   queryParams.warningType = "";
   queryParams.warningLevel = "";
-  queryParams.handler = "";
-  queryParams.publisher = "";
   queryParams.alarmCode = "";
-  queryParams.ownershipUnit = "";
   queryParams.dateRange = [];
   pagination.pageNum = 1;
   refreshLists();
@@ -464,8 +372,7 @@ async function refreshLists() {
 
 async function handleViewDetail(row) {
   if (!auth.hasPermi('warning:warning:query')) return;
-  const res = await getWarningDetailData(row.warningId);
-  detailData.value = res.data || {};
+  selectedWarningId.value = row.warningId;
   selectedNotice.value = activeTab.value === "NOTICE" ? row.taskName : "";
   detailVisible.value = true;
   if (activeTab.value === "NOTICE" && row.readStatus !== "READ") {

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

@@ -19,7 +19,7 @@
         <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 }"><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-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" />
     </el-card>
@@ -35,7 +35,7 @@ import { ElMessage } from 'element-plus'
 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 WarningDetailDrawer from '../components/WarningDetailDrawer.vue'
 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)

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

@@ -16,7 +16,7 @@
         <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 }"><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-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" />
     </el-card>
@@ -32,7 +32,7 @@ import { ElMessage } from 'element-plus'
 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 WarningDetailDrawer from '../components/WarningDetailDrawer.vue'
 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

+ 2 - 2
src/views/subSystem/lifeCompany/lOverviewWarning/Ltoday.vue

@@ -18,7 +18,7 @@
         <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 }"><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-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" />
     </el-card>
@@ -35,7 +35,7 @@ import { ElMessage } from 'element-plus'
 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 WarningDetailDrawer from '../components/WarningDetailDrawer.vue'
 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)

+ 0 - 69
src/views/subSystem/lifeCompany/lOverviewWarning/WarningDetailDrawer.vue

@@ -1,69 +0,0 @@
-<template>
-  <el-drawer v-model="visible" title="预警详情" size="720px" append-to-body direction="rtl" class="detail-drawer">
-    <template v-if="row">
-      <div class="detail-hero">
-        <div class="detail-kicker">WARNING EVENT</div>
-        <div class="detail-title">{{ detail.warningNo }}</div>
-        <div class="detail-meta">
-          <el-tag :type="levelTag">{{ detail.warningLevel }}</el-tag>
-          <WarningStatusTag :status="row">{{ detail.status }}</WarningStatusTag>
-          <span>{{ detail.warningNo || detail.warningId || '暂无编号' }}</span>
-        </div>
-      </div>
-
-      <el-descriptions :column="2" border class="detail-descriptions">
-        <el-descriptions-item label="预警类型">{{ detail.warningType }}</el-descriptions-item>
-        <el-descriptions-item label="预警专项">{{ detail.warningSpecial }}</el-descriptions-item>
-        <el-descriptions-item label="权属单位">{{ detail.ownershipUnit }}</el-descriptions-item>
-        <el-descriptions-item label="发布人">{{ detail.publisher }}</el-descriptions-item>
-        <el-descriptions-item label="发布时间">{{ detail.publishTime }}</el-descriptions-item>
-        <el-descriptions-item label="创建时间">{{ detail.createTime }}</el-descriptions-item>
-      </el-descriptions>
-
-      <section class="content-block">
-        <div class="block-title">预警描述</div>
-        <p>{{ detail.warningContent }}</p>
-      </section>
-
-      <section v-if="detail.todoInfo" class="content-block todo-block">
-        <div class="block-title">待办信息</div>
-        <el-descriptions :column="2" size="small">
-          <el-descriptions-item label="处置人">{{ detail.todoInfo.userName || detail.todoInfo.user_name || '未分配' }}</el-descriptions-item>
-          <el-descriptions-item label="任务类型">{{ detail.todoInfo.taskName || detail.todoInfo.task_name || detail.todoInfo.todoType || '预警处置' }}</el-descriptions-item>
-          <el-descriptions-item label="待办时间">{{ formatDashboardTime(detail.todoInfo.createTime || detail.todoInfo.create_time, '未设置') }}</el-descriptions-item>
-        </el-descriptions>
-      </section>
-    </template>
-    <el-empty v-else description="暂无预警详情" />
-  </el-drawer>
-</template>
-
-<script setup>
-import WarningStatusTag from "../components/WarningStatusTag.vue"
-import { computed } from 'vue'
-import { detailView, formatDashboardTime, warningLevelTag } from './dashboardShared'
-
-const props = defineProps({
-  modelValue: { type: Boolean, default: false },
-  row: { type: Object, default: null }
-})
-const emit = defineEmits(['update:modelValue'])
-const visible = computed({
-  get: () => props.modelValue,
-  set: value => emit('update:modelValue', value)
-})
-const detail = computed(() => detailView(props.row || {}))
-const levelTag = computed(() => warningLevelTag(props.row || {}))
-</script>
-
-<style scoped lang="scss">
-.detail-hero { padding: 4px 0 20px; border-bottom: 1px solid #e8eef5; margin-bottom: 20px; }
-.detail-kicker { color: #2563eb; font-size: 11px; letter-spacing: 2px; font-weight: 700; }
-.detail-title { margin: 8px 0 12px; font-size: 23px; font-weight: 700; color: #172b4d; line-height: 1.35; }
-.detail-meta { display: flex; align-items: center; gap: 8px; color: #7b8ba5; font-size: 12px; }
-.detail-descriptions { margin-bottom: 22px; }
-.content-block { padding: 16px; margin-top: 16px; border: 1px solid #e8eef5; border-radius: 8px; background: #f8fafc; }
-.block-title { margin-bottom: 8px; font-weight: 650; color: #334155; }
-.content-block p { margin: 0; color: #52637a; line-height: 1.7; white-space: pre-wrap; }
-.todo-block { background: #fff8ed; border-color: #fde4b3; }
-</style>

+ 1 - 1
src/views/subSystem/lifeCompany/lhome/lHome.vue

@@ -223,7 +223,7 @@ import {
   pageWarningTodayUnprocessed
 } from '@/api/warning/dashboard'
 import { getDisposeStatusStat, getWarningTrendByDay } from '@/api/warning/statistics'
-import WarningDetailDrawer from '../lOverviewWarning/WarningDetailDrawer.vue'
+import WarningDetailDrawer from '../components/WarningDetailDrawer.vue'
 import {
   dashboardData,
   listValue,

+ 19 - 124
src/views/subSystem/lifeCompany/lwarningInfoCont/LAlertQuery.vue

@@ -86,15 +86,12 @@
       <template #header>
         <div class="section-title">预警查询</div>
       </template>
-      <el-table :data="warningList" @row-click="handleSelectWarning">
-        <el-table-column label="序号" width="70" align="center">
-          <template #default="scope">
-            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
-          </template>
-        </el-table-column>
-
-        <el-table-column label="预警编号" prop="warningNo" width="170" />
+      <el-table :data="warningList" style="width: 100%" @row-click="handleSelectWarning">
+        <el-table-column label="预警编号" prop="warningNo" width="210" show-overflow-tooltip />
         <el-table-column label="预警类型" prop="warningTypeText" width="120" />
+        <el-table-column label="预警内容" min-width="260">
+          <template #default="scope"><WarningContentCell :content="scope.row.warningContent" /></template>
+        </el-table-column>
         <el-table-column label="预警级别" width="110" align="center">
           <template #default="scope">
             <el-tag :type="getLevelTagType(scope.row.warningLevel)">
@@ -110,7 +107,6 @@
           </template>
         </el-table-column>
         <el-table-column label="发布人" prop="publisher" width="120" />
-        <el-table-column label="权属单位" prop="ownershipUnit" width="160" show-overflow-tooltip />
         <el-table-column label="发布时间" prop="publishTime" width="180" />
         <el-table-column label="操作" width="150" fixed="right" align="center">
           <template #default="scope">
@@ -140,7 +136,7 @@
             <el-tab-pane :label="`已完成 (${completedTotal})`" name="completed" />
           </el-tabs>
           <el-table :data="trackList" height="360" @row-click="handleSelectWarningFromTrack">
-            <el-table-column label="预警编号" prop="warningNo" min-width="180" show-overflow-tooltip />
+            <el-table-column label="预警编号" prop="warningNo" min-width="210" show-overflow-tooltip />
             <el-table-column label="预警类型" prop="warningTypeText" width="120" />
             <el-table-column label="预警级别" width="100" align="center">
               <template #default="scope">
@@ -206,87 +202,35 @@
             </div>
 
             <el-divider content-position="left">流程记录</el-divider>
-            <el-timeline>
-              <el-timeline-item
-                v-for="item in processList"
-                :key="item.disposalId"
-                :timestamp="formatDateTime(item.createTime)"
-                :type="getTimelineType(item.disposalType)"
-              >
-                <div class="timeline-title">{{ getProcessTypeText(item.disposalType) }}</div>
-                <div>{{ item.disposalContent || "-" }}</div>
-                <div class="timeline-sub">处置人:{{ item.disposalUser || "-" }}</div>
-                <WarningDisposalReasons :record="item" />
-                <WarningAttachments :attachments="attachmentsForDisposal(item, processAttachments)" />
-              </el-timeline-item>
-            </el-timeline>
+            <WarningDisposalTimeline
+              :records="processList"
+              :attachments="processAttachments"
+              :level-labels="warningLevelLabelMap"
+              empty-text="暂无流程记录"
+            />
           </template>
         </el-card>
       </el-col>
     </el-row>
 
-    <el-drawer v-model="detailVisible" title="预警详情" size="820px" append-to-body direction="rtl" class="detail-drawer">
-      <template v-if="detailData.basicInfo">
-        <WarningItemSummary :warning="detailData.basicInfo" />
-        <el-descriptions :column="2" border>
-          <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="状态"><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>
-          <el-descriptions-item label="发布时间">{{ detailBasic.publishTime || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="预警内容" :span="2"><span class="warning-content">{{ detailBasic.warningContent || "-" }}</span></el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">流程处置信息</el-divider>
-        <el-timeline v-if="(detailData.disposalHistory || []).length">
-          <el-timeline-item
-            v-for="(item, index) in detailData.disposalHistory"
-            :key="index"
-            :timestamp="formatDateTime(item.createTime || item.create_time || item.disposalTime || item.disposal_time)"
-          >
-            <div class="timeline-title">{{ getProcessTypeText(item.disposalType || item.disposal_type) }}</div>
-            <div>{{ item.disposalContent || item.disposal_content || item.content || "-" }}</div>
-            <div class="timeline-sub">处置人:{{ item.disposalUser || item.disposal_user || "-" }}</div>
-            <WarningDisposalReasons :record="item" />
-            <WarningAttachments :attachments="attachmentsForDisposal(item, detailData.attachmentList)" />
-          </el-timeline-item>
-        </el-timeline>
-        <el-empty v-else description="暂无流程处置信息" />
-        <template v-if="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList).length">
-          <el-divider content-position="left">其他预警附件</el-divider>
-          <WarningAttachments :attachments="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList)" />
-        </template>
-
-        <el-divider content-position="left">电子存档信息</el-divider>
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="存档状态">{{ detailArchive.archiveStatus || detailArchive.status || "暂无电子存档" }}</el-descriptions-item>
-          <el-descriptions-item label="存档编号">{{ detailArchive.archiveNo || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="存档说明">{{ detailArchive.remark || "-" }}</el-descriptions-item>
-        </el-descriptions>
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedDetailWarningId" />
   </div>
 </template>
 
 <script setup>
 import WarningStatusTag from "../components/WarningStatusTag.vue"
+import WarningContentCell from "../components/WarningContentCell.vue"
+import WarningDetailDrawer from "../components/WarningDetailDrawer.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import { computed, onMounted, reactive, ref } from "vue";
 import auth from '@/plugins/auth';
 import { parseTime } from "@/utils/ruoyi";
-import WarningItemSummary from '../components/WarningItemSummary.vue';
-import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
-import WarningAttachments from "../components/WarningAttachments.vue";
-import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
+import WarningDisposalTimeline from '../components/WarningDisposalTimeline.vue';
 import { getWarningAttachmentsData } from "@/api/pipeNetwork/earlyWarning";
 import {
   getAlertTypeDictionaryData,
   getWarningCompletedListData,
-  getWarningDetailData,
   getWarningDiagramData,
   getWarningProcessData,
   getWarningProcessStatisticsData,
@@ -314,7 +258,7 @@ const processStatistics = ref({});
 const processDiagram = ref({});
 
 const detailVisible = ref(false);
-const detailData = ref({});
+const selectedDetailWarningId = ref("");
 
 const queryParams = reactive({
   pageNum: 1,
@@ -351,26 +295,6 @@ const processMeta = computed(() => ({
   status: processDiagram.value.status || ""
 }));
 
-const detailBasic = computed(() => {
-  const basic = detailData.value.basicInfo || {};
-  const warningType = basic.warningType || basic.warning_type || "";
-  const warningLevel = basic.warningLevel || basic.warning_level || "";
-  return {
-
-    warningNo: basic.warningNo || basic.warning_no || "",
-    warningTypeText: basic.warning_type_name || basic.warningTypeName || warningTypeLabelMap.value[warningType] || "-",
-    warningLevelText: warningLevelLabelMap.value[warningLevel] || "-",
-    status: basic.status || "",
-    publisher: basic.publisher || "",
-    handler: basic.handler || "",
-    ownershipUnit: basic.ownershipUnit || basic.ownership_unit || "",
-    publishTime: formatDateTime(basic.publishTime || basic.publish_time),
-    warningContent: basic.warningContent || basic.warning_content || ""
-  };
-});
-
-const detailArchive = computed(() => detailData.value.electronicArchive || {});
-
 function formatDateTime(value) {
   if (!value) return "-";
   const date = new Date(value);
@@ -390,35 +314,6 @@ function getStatusText(status) {
   return databaseStatusLabels.value[status] || status || "-"
 }
 
-function getProcessTypeText(type) {
-  return {
-    RELEASE: "发布预警",
-    UPGRADE: "升级预警",
-    RESOLVE: "解除预警",
-    AUTO_CLEAR: "系统自动解除",
-    CLEAR: "核查解除",
-    RECEIVE: "接收预警",
-    START: "开始处置",
-    HANDLE: "提交处置结果",
-    RETURN: "退回重办",
-    SUPERVISION: "预警督办",
-    INSTRUCTION: "领导批示"
-  }[type] || type || "-";
-}
-
-function getTimelineType(type) {
-  return {
-    RELEASE: "primary",
-    UPGRADE: "warning",
-    RESOLVE: "success",
-    AUTO_CLEAR: "success",
-    CLEAR: "success",
-    RETURN: "danger",
-    SUPERVISION: "primary",
-    INSTRUCTION: "info"
-  }[type] || "primary";
-}
-
 function normalizeWarningRow(item) {
   const warningType = item.warningType || item.warning_type || "";
   const warningLevel = item.warningLevel || item.warning_level || "";
@@ -428,6 +323,7 @@ function normalizeWarningRow(item) {
     warningNo: item.warningNo || item.warning_no || "",
     warningType,
     warningTypeText: item.warning_type_name || item.warningTypeName || warningTypeLabelMap.value[warningType] || "-",
+    warningContent: item.warningContent || item.warning_content || "",
     warningLevel,
     warningLevelText: warningLevelLabelMap.value[warningLevel] || "-",
     status: item.status || "",
@@ -511,8 +407,7 @@ async function loadProcessData(warningId) {
 
 async function openDetail(row) {
   if (!auth.hasPermi('warning:warning:query')) return;
-  const res = await getWarningDetailData(row.warningId);
-  detailData.value = res.data || {};
+  selectedDetailWarningId.value = row.warningId;
   detailVisible.value = true;
 }
 

+ 10 - 79
src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue

@@ -65,14 +65,11 @@
 
     <el-card shadow="never" class="page-card">
       <el-table :data="warningList">
-        <el-table-column label="序号" width="70" align="center">
-          <template #default="scope">
-            {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
-          </template>
-        </el-table-column>
-
-        <el-table-column label="预警编号" prop="warningNo" width="170" />
+        <el-table-column label="预警编号" prop="warningNo" width="210" show-overflow-tooltip />
         <el-table-column label="预警类型" prop="warningTypeText" width="120" />
+        <el-table-column label="预警内容" min-width="240">
+          <template #default="scope"><WarningContentCell :content="scope.row.warningContent" /></template>
+        </el-table-column>
         <el-table-column label="预警级别" width="110" align="center">
           <template #default="scope">
             <el-tag :type="getLevelTagType(scope.row.warningLevel)">
@@ -84,7 +81,6 @@
         <el-table-column label="设备名称" prop="deviceName" min-width="140" show-overflow-tooltip />
         <el-table-column label="设备编码" prop="deviceCode" width="160" show-overflow-tooltip />
         <el-table-column label="发布人" prop="publisher" width="120" />
-        <el-table-column label="权属单位" prop="ownershipUnit" width="160" show-overflow-tooltip />
         <el-table-column label="状态" width="120" align="center">
           <template #default="scope">
             <WarningStatusTag :status="scope.row.status">
@@ -115,47 +111,7 @@
       />
     </el-card>
 
-    <el-drawer v-model="detailVisible" title="预警详情" size="820px" append-to-body direction="rtl" class="detail-drawer">
-      <template v-if="detailData.basicInfo">
-        <WarningItemSummary :warning="detailData.basicInfo" />
-        <el-descriptions :column="2" border>
-          <el-descriptions-item label="预警编号">{{ detailBasic.warningNo || "-" }}</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>
-          <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>
-          <el-descriptions-item label="发布人">{{ detailBasic.publisher || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="核查人">{{ detailBasic.reviewer || detailBasic.publisher || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ detailBasic.ownershipUnit || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ formatDateTime(detailBasic.publishTime) }}</el-descriptions-item>
-          <el-descriptions-item label="预警内容" :span="2"><span class="warning-content">{{ detailBasic.warningContent || "-" }}</span></el-descriptions-item>
-        </el-descriptions>
-
-        <el-divider content-position="left">处置记录</el-divider>
-        <el-timeline v-if="(detailData.disposalHistory || []).length">
-          <el-timeline-item
-            v-for="(item, index) in detailData.disposalHistory"
-            :key="index"
-            :timestamp="formatDateTime(item.createTime || item.create_time || item.disposalTime || item.disposal_time)"
-          >
-            {{ item.disposalTypeName || item.disposal_type_name || item.disposalType || item.disposal_type || "处置记录" }}
-            <span v-if="item.oldLevel || item.old_level">:{{ warningLevelLabelMap[item.oldLevel || item.old_level] || item.oldLevel || item.old_level }} → {{ warningLevelLabelMap[item.newLevel || item.new_level] || item.newLevel || item.new_level }}</span>
-            <span v-if="item.disposalContent || item.disposal_content || item.content">:{{ item.disposalContent || item.disposal_content || item.content }}</span>
-            <span v-if="item.disposalUser || item.disposal_user">({{ item.disposalUser || item.disposal_user }})</span>
-            <WarningDisposalReasons :record="item" />
-            <WarningAttachments :attachments="attachmentsForDisposal(item, detailData.attachmentList)" />
-          </el-timeline-item>
-        </el-timeline>
-        <el-empty v-else description="暂无处置记录" />
-        <template v-if="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList).length">
-          <el-divider content-position="left">其他预警附件</el-divider>
-          <WarningAttachments :attachments="unlinkedWarningAttachments(detailData.disposalHistory, detailData.attachmentList)" />
-        </template>
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedWarningId" />
 
     <el-dialog v-model="upgradeVisible" title="预警升级" width="560px" append-to-body>
       <el-form label-width="100px">
@@ -238,6 +194,8 @@
 
 <script setup>
 import WarningStatusTag from "../components/WarningStatusTag.vue"
+import WarningContentCell from "../components/WarningContentCell.vue"
+import WarningDetailDrawer from "../components/WarningDetailDrawer.vue"
 import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
 const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import { computed, onMounted, reactive, ref } from "vue";
@@ -245,14 +203,9 @@ import { ElMessage } from "element-plus";
 import { Upload } from "@element-plus/icons-vue";
 import { parseTime } from "@/utils/ruoyi";
 import { enabledAll } from "@/api/system/user";
-import WarningItemSummary from '../components/WarningItemSummary.vue';
 import WarningReviewAction from '../components/WarningReviewAction.vue';
-import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
-import WarningAttachments from "../components/WarningAttachments.vue";
-import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
 import {
   getAlertTypeDictionaryData,
-  getWarningDetailData,
   getdbyjlistData,
   instructionWarningData,
   supervisionWarningData,
@@ -271,7 +224,7 @@ const warningTypeOptions = ref([]);
 const warningLevelOptions = ref([]);
 const currentRow = ref(null);
 const detailVisible = ref(false);
-const detailData = ref({});
+const selectedWarningId = ref("");
 
 const upgradeVisible = ref(false);
 const supervisionVisible = ref(false);
@@ -334,28 +287,6 @@ function formatDateTime(value) {
   return parseTime(Number.isNaN(date.getTime()) ? value : date) || "-";
 }
 
-const detailBasic = computed(() => {
-  const basic = detailData.value.basicInfo || {};
-  const warningType = basic.warningType || basic.warning_type || "";
-  const warningLevel = basic.warningLevel || basic.warning_level || "";
-  return {
-
-    warningNo: basic.warningNo || basic.warning_no || "",
-    warningSpecial: basic.warningSpecial || basic.warning_special || "",
-    warningTypeText: basic.warning_type_name || basic.warningTypeName || warningTypeLabelMap.value[warningType] || "-",
-    warningLevelText: warningLevelLabelMap.value[warningLevel] || "-",
-    status: basic.status || "",
-    handler: basic.handler || "",
-    deviceCode: basic.deviceCode || basic.device_code || "",
-    deviceName: basic.deviceName || basic.device_name || "",
-    publisher: basic.publisher || "",
-    reviewer: basic.reviewer || "",
-    ownershipUnit: basic.ownershipUnit || basic.ownership_unit || "",
-    publishTime: basic.publishTime || basic.publish_time || "",
-    warningContent: basic.warningContent || basic.warning_content || ""
-  };
-});
-
 function getLevelTagType(level) {
   return {
     "1": "danger",
@@ -378,6 +309,7 @@ function normalizeWarningRow(item) {
     warningNo: item.warningNo || item.warning_no || "",
     warningType,
     warningTypeText: item.warning_type_name || item.warningTypeName || warningTypeLabelMap.value[warningType] || "-",
+    warningContent: item.warningContent || item.warning_content || "",
     warningLevel,
     warningLevelText: warningLevelLabelMap.value[warningLevel] || "-",
     handler: item.handler || "",
@@ -469,8 +401,7 @@ function resetQuery() {
 }
 
 async function openDetail(row) {
-  const res = await getWarningDetailData(row.warningId);
-  detailData.value = res.data || {};
+  selectedWarningId.value = row.warningId;
   detailVisible.value = true;
 }
 

+ 2 - 0
src/views/subSystem/lifeCompany/warningAttachments.js

@@ -18,10 +18,12 @@ export function normalizeWarningAttachment(file) {
   const mime = String(valueOf(file, 'attachmentType', 'attachment_type') || '').toLowerCase().split(';')[0]
   const rawSize = valueOf(file, 'attachmentSize', 'attachment_size')
   const size = rawSize == null || rawSize === '' ? null : Number(rawSize)
+  const extension = name.includes('.') ? name.split('.').pop().toUpperCase().slice(0, 4) : 'FILE'
   return {
     id: valueOf(file, 'attachmentId', 'attachment_id'),
     name,
     sizeText: size != null && Number.isFinite(size) && size >= 0 ? formatAttachmentSize(size) : '',
+    extension,
     createTime: valueOf(file, 'createTime', 'create_time'),
     previewKind: /^image\/(png|jpeg|gif|webp|bmp)$/.test(mime) || /\.(png|jpe?g|gif|webp|bmp)$/i.test(name)
       ? 'image' : mime === 'application/pdf' || /\.pdf$/i.test(name) ? 'pdf' : ''

+ 73 - 0
src/views/subSystem/lifeCompany/warningDetail.js

@@ -0,0 +1,73 @@
+export function warningValue(source, ...keys) {
+  for (const key of keys) {
+    const value = source?.[key]
+    if (value !== undefined && value !== null && value !== '') return value
+  }
+  return ''
+}
+
+export function warningIdOf(source) {
+  return warningValue(source, 'warningId', 'warning_id', 'id')
+}
+
+export function normalizeWarningBasic(source = {}, references = {}) {
+  const warningType = warningValue(source, 'warningType', 'warning_type')
+  const warningLevel = warningValue(source, 'warningLevel', 'warning_level')
+  return {
+    ...source,
+    warningId: warningIdOf(source),
+    warningNo: warningValue(source, 'warningNo', 'warning_no'),
+    warningName: warningValue(source, 'warningName', 'warning_name'),
+    warningType,
+    warningTypeText: warningValue(source, 'warningTypeText', 'warningTypeName', 'warning_type_name')
+      || references.typeLabels?.[warningType]
+      || warningType,
+    warningLevel,
+    warningLevelText: warningValue(source, 'warningLevelText', 'warningLevelName', 'warning_level_name')
+      || references.levelLabels?.[warningLevel]
+      || warningLevel,
+    warningSpecial: warningValue(source, 'warningSpecial', 'warning_special'),
+    warningContent: warningValue(source, 'warningContent', 'warning_content'),
+    ownershipUnit: warningValue(source, 'ownershipUnit', 'ownership_unit'),
+    deviceName: warningValue(source, 'deviceName', 'device_name'),
+    deviceCode: warningValue(source, 'deviceCode', 'device_code'),
+    location: warningValue(source, 'location', 'deviceLocation', 'device_location'),
+    publisher: warningValue(source, 'publisher'),
+    handler: warningValue(source, 'handler'),
+    reviewer: warningValue(source, 'reviewer'),
+    publishTime: warningValue(source, 'publishTime', 'publish_time'),
+    remark: warningValue(source, 'remark'),
+    status: warningValue(source, 'status'),
+    itemId: warningValue(source, 'itemId', 'item_id'),
+    itemSnapshot: warningValue(source, 'itemSnapshot', 'item_snapshot'),
+    disposalDeadline: warningValue(source, 'disposalDeadline', 'disposal_deadline'),
+    stageDeadline: warningValue(source, 'stageDeadline', 'stage_deadline')
+  }
+}
+
+export function normalizeWarningAlarm(source = {}) {
+  return {
+    ...source,
+    alarmId: warningValue(source, 'alarmId', 'alarm_id', 'id'),
+    alarmNo: warningValue(source, 'alarmNo', 'alarm_no', 'warningCode', 'warning_code'),
+    alarmType: warningValue(source, 'alarmTypeText', 'alarmType', 'alarm_type', 'warningTypeName', 'warning_type_name', 'warningType', 'warning_type'),
+    deviceName: warningValue(source, 'deviceName', 'device_name', 'equipmentName', 'equipment_name'),
+    deviceCode: warningValue(source, 'deviceCode', 'device_code', 'equipmentCode', 'equipment_code'),
+    thresholdValue: warningValue(source, 'thresholdValue', 'threshold_value', 'warningValue', 'warning_value'),
+    actualValue: warningValue(source, 'actualValue', 'actual_value'),
+    createTime: warningValue(source, 'createTime', 'create_time', 'alarmTime', 'alarm_time')
+  }
+}
+
+export function normalizeWarningDetail(source = {}, references = {}) {
+  const basicSource = source.basicInfo || source.basic_info || source
+  return {
+    ...source,
+    basicInfo: normalizeWarningBasic(basicSource, references),
+    alarmList: (source.alarmList || source.alarm_list || []).map(normalizeWarningAlarm),
+    disposalHistory: source.disposalHistory || source.disposal_history || source.disposalList || source.disposal_list || [],
+    attachmentList: source.attachmentList || source.attachment_list || [],
+    supervisionList: source.supervisionList || source.supervision_list || [],
+    electronicArchive: source.electronicArchive || source.electronic_archive || null
+  }
+}

+ 7 - 161
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue

@@ -215,79 +215,7 @@
       </template>
     </el-dialog>
 
-    <!-- ========== 详情页抽屉 ========== -->
-    <el-drawer v-model="detailVisible" title="📋 预警处理详情" size="560px" direction="rtl" class="detail-drawer">
-      <template v-if="detail">
-        <el-descriptions :column="1" border size="small">
-          <el-descriptions-item label="预警编号">{{ detail.basicInfo.warning_no }}</el-descriptions-item>
-          <el-descriptions-item label="预警名称">{{ detail.basicInfo.warning_name }}</el-descriptions-item>
-          <el-descriptions-item label="预警类型">{{ detail.basicInfo.warning_type || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="预警级别">{{ getLevelText(detail.basicInfo.warning_level) }}</el-descriptions-item>
-          <el-descriptions-item label="发布人">{{ detail.basicInfo.publisher || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ formatWarningTime(detail.basicInfo.publish_time) }}</el-descriptions-item>
-          <el-descriptions-item label="状态">{{ getStatusText(detail.basicInfo.status) }}</el-descriptions-item>
-          <el-descriptions-item label="预警内容">{{ detail.basicInfo.warning_content }}</el-descriptions-item>
-        </el-descriptions>
-
-        <div class="detail-section-title">🔗 关联设备</div>
-        <el-descriptions :column="1" border size="small">
-          <el-descriptions-item label="设备名称">{{ detail.basicInfo.device_name || detail.basicInfo.deviceName || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="设备编码">{{ detail.basicInfo.device_code || detail.basicInfo.deviceCode || extractDeviceCodeStr(detail.basicInfo) || '(手工录入,无设备关联)' }}</el-descriptions-item>
-          <el-descriptions-item label="设备位置">{{ detail.basicInfo.location || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="权属单位">{{ detail.basicInfo.ownership_unit || '-' }}</el-descriptions-item>
-        </el-descriptions>
-
-        <div class="detail-section-title">🕐 处置历史</div>
-        <el-timeline v-if="detail.disposalHistory?.length" style="padding:0 0 20px">
-          <el-timeline-item v-for="(item,idx) in detail.disposalHistory" :key="idx" :timestamp="item.create_time" :type="disposalTimelineType(item.disposal_type)">
-            <b>{{ disposalTypeText(item.disposal_type) }}</b>
-            <div>{{ item.disposal_content || '-' }}</div>
-            <div class="detail-sub">处置人:{{ item.disposal_user || '-' }}</div>
-          </el-timeline-item>
-        </el-timeline>
-        <div v-else class="detail-empty">暂无处置记录</div>
-
-        <div class="detail-section-title">📎 附件</div>
-        <div v-if="detail.attachmentList?.length" class="attachment-list">
-          <div v-for="stage in ['REPORT','PROCESS']" :key="stage" class="attachment-group">
-            <div class="attachment-stage">{{ stage === 'PROCESS' ? '处理反馈附件' : '预警报告附件' }}</div>
-            <div v-if="attachmentsByStage(stage).length" class="attachment-grid">
-              <div v-for="(att,idx) in attachmentsByStage(stage)" :key="att.attachmentId || idx" class="attachment-item">
-                <el-image v-if="isImageAttachment(att) && attachmentPreviewUrl(att)" class="attachment-image" :src="attachmentPreviewUrl(att)" :preview-src-list="[attachmentPreviewUrl(att)]" fit="cover" preview-teleported>
-                  <template #error><div class="attachment-image-error">图片加载失败</div></template>
-                </el-image>
-                <div v-else-if="isImageAttachment(att)" class="attachment-image attachment-image-loading">加载中...</div>
-                <div v-else class="attachment-file-icon"><el-icon><Document /></el-icon></div>
-                <div class="attachment-name" :title="displayAttachmentName(att)">{{ displayAttachmentName(att) }}</div>
-                <div class="attachment-actions">
-                  <el-button v-if="canPreviewAttachment(att)" link type="primary" size="small" @click="previewAttachment(att)"><el-icon><View /></el-icon>预览</el-button>
-                  <el-button link type="primary" size="small" @click="downloadAttachment(att)"><el-icon><Download /></el-icon>下载</el-button>
-                </div>
-              </div>
-            </div>
-            <div v-else class="detail-empty">暂无附件</div>
-          </div>
-        </div>
-        <div v-else class="detail-empty">暂无附件</div>
-
-        <div class="detail-section-title">预警督办</div>
-        <div v-if="detail.supervisionList?.length" class="supervision-list">
-          <div v-for="(item,idx) in detail.supervisionList" :key="idx" class="supervision-item">
-            <b>{{ item.supervisionUser || '-' }}</b>
-            <span>{{ formatWarningTime(item.createTime) }}</span>
-            <p>{{ item.supervisionContent || '-' }}</p>
-          </div>
-        </div>
-        <div v-else class="detail-empty">暂无督办记录</div>
-
-        <div class="detail-section-title">📦 电子归档</div>
-        <el-descriptions v-if="detail.electronicArchive" :column="2" border size="small">
-          <el-descriptions-item label="归档编号">{{ detail.electronicArchive.archiveNo }}</el-descriptions-item>
-          <el-descriptions-item label="归档时间">{{ detail.electronicArchive.createTime }}</el-descriptions-item>
-        </el-descriptions>
-        <div v-else class="detail-empty">暂无归档(解除预警后自动生成)</div>
-      </template>
-    </el-drawer>
+    <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedDetailWarningId" />
   </div>
 </template>
 
@@ -295,8 +223,9 @@
 import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import * as echarts from 'echarts'
 import { ElMessage } from 'element-plus'
-import { Search, Refresh, Plus, Document, Download, View, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
-import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, confirmWarningData, startWarningProcessData, submitWarningProcessData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
+import { Search, Refresh, Plus, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
+import { getWarningDashboardStatisticsData, getWarningSearchData, confirmWarningData, startWarningProcessData, submitWarningProcessData } from '@/api/pipeNetwork/earlyWarning'
+import WarningDetailDrawer from '@/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue'
 import WarningReviewAction from '@/views/subSystem/lifeCompany/components/WarningReviewAction.vue'
 import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
 
@@ -521,62 +450,6 @@ const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待接收', RECEIVED:'
 const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', RECEIVED:'warning', PROCESSING:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
 const getLevelText = (lv) => ({ '1':'1 级 (蓝)','2':'2 级 (黄)','3':'3 级 (橙)','4':'4 级 (红)' }[lv]||'-' )
 const getLevelType = (lv) => ({ '1':'','2':'warning','3':'warning','4':'danger' }[lv]||'')
-const disposalTypeText = (t) => ({ RELEASE:'发布预警', RECEIVE:'接收预警', START:'开始处置', MISREPORT:'误报清除', HANDLE:'提交处理反馈', CLEAR:'清除预警', UPGRADE:'升级预警', RESOLVE:'解除预警', RETURN:'退回重办', SUPERVISION:'督办', INSTRUCTION:'批示' }[t]||t)
-const disposalTimelineType = (t) => ({ RESOLVE:'success', UPGRADE:'warning', RETURN:'danger' }[t]||'primary')
-const attachmentsByStage = (stage) => (detail.value?.attachmentList || []).filter(item => (item.attachmentStage || item.attachment_stage || 'REPORT') === stage)
-const isImageAttachment = (attachment) => String(attachment.attachmentType || '').startsWith('image/') || /\.(png|jpe?g|gif|webp)$/i.test(attachment.attachmentName || '')
-const attachmentPreviewUrls = ref({})
-const attachmentPreviewUrl = (attachment) => attachmentPreviewUrls.value[attachment.attachmentId] || ''
-const displayAttachmentName = (attachment) => String(attachment.attachmentName || '预警附件').replace(/^\[(预警报告|处理反馈)\]\s*/, '')
-const canPreviewAttachment = (attachment) => isImageAttachment(attachment) || /\.pdf$/i.test(displayAttachmentName(attachment)) || String(attachment.attachmentType || '').toLowerCase() === 'application/pdf'
-
-function revokeAttachmentPreviews() {
-  Object.values(attachmentPreviewUrls.value).forEach(url => url && URL.revokeObjectURL(url))
-  attachmentPreviewUrls.value = {}
-}
-
-async function loadAttachmentPreviews(attachments) {
-  revokeAttachmentPreviews()
-  const imageAttachments = (attachments || []).filter(isImageAttachment)
-  await Promise.all(imageAttachments.map(async (attachment) => {
-    try {
-      const blob = await getWarningAttachmentPreviewData(attachment.attachmentId)
-      attachmentPreviewUrls.value[attachment.attachmentId] = URL.createObjectURL(blob)
-    } catch (e) {
-      console.error('预警图片加载失败', attachment.attachmentId, e)
-    }
-  }))
-}
-
-async function previewAttachment(attachment) {
-  try {
-    const blob = isImageAttachment(attachment) && attachmentPreviewUrl(attachment)
-      ? null
-      : await getWarningAttachmentPreviewData(attachment.attachmentId)
-    const url = attachmentPreviewUrl(attachment) || URL.createObjectURL(blob)
-    window.open(url, '_blank', 'noopener,noreferrer')
-    if (blob) setTimeout(() => URL.revokeObjectURL(url), 60000)
-  } catch (e) {
-    ElMessage.error('附件预览失败')
-  }
-}
-
-async function downloadAttachment(attachment) {
-  try {
-    const blob = await getWarningAttachmentDownloadData(attachment.attachmentId)
-    const url = URL.createObjectURL(blob)
-    const link = document.createElement('a')
-    link.href = url
-    link.download = displayAttachmentName(attachment)
-    document.body.appendChild(link)
-    link.click()
-    document.body.removeChild(link)
-    setTimeout(() => URL.revokeObjectURL(url), 1000)
-  } catch (e) {
-    ElMessage.error('附件下载失败')
-  }
-}
-
 // ========== 操作处理 ==========
 function handleSearch() { pageNum.value=1; loadList() }
 function handleReset() { queryParams.value={warningName:'',deviceCode:'',deviceName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; pageNum.value=1; loadList() }
@@ -694,23 +567,14 @@ async function submitProcess() {
 
 // ========== 详情抽屉 ==========
 const detailVisible = ref(false)
-const detail = ref(null)
+const selectedDetailWarningId = ref('')
 
-async function openDetail(warningId) {
+function openDetail(warningId) {
+  selectedDetailWarningId.value = warningId
   detailVisible.value = true
-  detail.value = null
-  revokeAttachmentPreviews()
-  try {
-    const res = await getWarningFullDetailData(warningId)
-    if (res.data) {
-      detail.value = res.data
-      await loadAttachmentPreviews(res.data.attachmentList)
-    }
-  } catch(e) { ElMessage.error('详情加载中'); console.error(e) }
 }
 
 onBeforeUnmount(() => {
-  revokeAttachmentPreviews()
   window.removeEventListener('resize', resizeCharts)
   trendChart?.dispose()
   levelChart?.dispose()
@@ -766,24 +630,6 @@ onMounted(async () => {
 .no-device { color:#c0c4cc; font-size:12px; }
 :deep(.publish-time-column .cell) { white-space: nowrap; }
 
-.detail-section-title { font-weight:600;font-size:14px;color:#2c3e50;margin:16px 0 10px;padding-bottom:8px;border-bottom:1px solid #eef2f6 }
-.detail-sub { font-size:12px;color:#9098a6;margin-top:4px }
-.detail-empty { font-size:13px;color:#9098a6;padding:12px 0 }
-.attachment-list { display:flex;flex-direction:column;gap:6px }
-.attachment-group { margin-bottom:12px }
-.attachment-stage { color:#606266;font-size:13px;font-weight:600;margin-bottom:6px }
-.attachment-grid { display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px }
-.attachment-item { min-width:0;padding:8px;border:1px solid #ebeef5;border-radius:6px;background:#fff }
-.attachment-image,.attachment-image-loading,.attachment-file-icon { width:100%;height:86px;border-radius:4px;overflow:hidden }
-.attachment-image-loading,.attachment-image-error,.attachment-file-icon { display:flex;align-items:center;justify-content:center;color:#a8abb2;background:#f5f7fa;font-size:12px }
-.attachment-file-icon { color:#409eff;font-size:32px }
-.attachment-name { margin-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#606266;font-size:12px }
-.attachment-actions { display:flex;justify-content:center;gap:4px;margin-top:3px }
-.attachment-actions .el-button { margin-left:0;padding:2px 3px }
-.supervision-list { display:flex;flex-direction:column;gap:8px }
-.supervision-item { padding:8px 10px;background:#f7f8fa;border-radius:4px;font-size:12px }
-.supervision-item span { color:#909399;margin-left:8px }
-.supervision-item p { margin:6px 0 0;color:#606266 }
 .upload-tip { color:#909399;font-size:12px;line-height:20px }
 
 @media (max-width: 1300px) {

+ 60 - 0
tests/warningDetail.test.mjs

@@ -0,0 +1,60 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import {
+  normalizeWarningAlarm,
+  normalizeWarningBasic,
+  normalizeWarningDetail,
+  warningIdOf
+} from '../src/views/subSystem/lifeCompany/warningDetail.js'
+
+test('预警基础信息统一兼容下划线和驼峰字段', () => {
+  const basic = normalizeWarningBasic({
+    warning_id: 'TEST-WARNING',
+    warning_no: 'YJ202610151644137245',
+    warning_type: 'PRESSURE',
+    warning_level: '2',
+    warning_content: '压力持续异常',
+    device_code: 'TEST-DEVICE',
+    publish_time: '2026-10-16 09:00:00'
+  }, {
+    typeLabels: { PRESSURE: '压力预警' },
+    levelLabels: { 2: '二级' }
+  })
+
+  assert.equal(warningIdOf(basic), 'TEST-WARNING')
+  assert.equal(basic.warningNo, 'YJ202610151644137245')
+  assert.equal(basic.warningTypeText, '压力预警')
+  assert.equal(basic.warningLevelText, '二级')
+  assert.equal(basic.warningContent, '压力持续异常')
+  assert.equal(basic.deviceCode, 'TEST-DEVICE')
+})
+
+test('报警数值为零时不会被当成空值', () => {
+  const alarm = normalizeWarningAlarm({
+    warning_code: 'TEST-ALARM',
+    warning_value: 0,
+    actual_value: 0
+  })
+
+  assert.equal(alarm.alarmNo, 'TEST-ALARM')
+  assert.equal(alarm.thresholdValue, 0)
+  assert.equal(alarm.actualValue, 0)
+})
+
+test('完整详情统一处置、附件和存档字段', () => {
+  const detail = normalizeWarningDetail({
+    basic_info: { warning_id: 'TEST-WARNING' },
+    alarm_list: [{ alarm_id: 'TEST-ALARM' }],
+    disposal_list: [{ disposal_id: 'TEST-DISPOSAL' }],
+    attachment_list: [{ attachment_id: 'TEST-FILE' }],
+    supervision_list: [{ supervision_id: 'TEST-SUPERVISION' }],
+    electronic_archive: { archive_id: 'TEST-ARCHIVE' }
+  })
+
+  assert.equal(detail.basicInfo.warningId, 'TEST-WARNING')
+  assert.equal(detail.alarmList[0].alarmId, 'TEST-ALARM')
+  assert.equal(detail.disposalHistory[0].disposal_id, 'TEST-DISPOSAL')
+  assert.equal(detail.attachmentList[0].attachment_id, 'TEST-FILE')
+  assert.equal(detail.supervisionList[0].supervision_id, 'TEST-SUPERVISION')
+  assert.equal(detail.electronicArchive.archive_id, 'TEST-ARCHIVE')
+})