Bladeren bron

Merge remote-tracking branch 'origin/master'

zengy 3 weken geleden
bovenliggende
commit
c71e6a5fe1

+ 1 - 0
memory/index.md

@@ -65,6 +65,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
+- [2026-09-29:预警发布、处置、核查与退回页面联调](sessions/2026-09-29.md)
 - [2026-08-14:项目探索、启动方式与记忆库建立](sessions/2026-08-14.md)
 - [2026-08-17:供水管网安全运行监测系统前端实现](sessions/2026-08-17.md)
 - [2026-08-21:工程技能仓库配置](sessions/2026-08-21.md)

+ 26 - 0
memory/sessions/2026-09-29.md

@@ -0,0 +1,26 @@
+# 2026-09-29 预警流程联调
+
+- 目标:在已登录的本地页面实际验证草稿、发布、接收、处置、核查、退回及解除流程。
+- 实测两条 `TEST-` 预警:一条走发布到解除;另一条走核查退回、再次处置到解除。预警状态和处置轨迹均按预期显示。
+- 首次发布因开发库缺少 `early_warning.reviewer`、`warning_todo.action_type` 失败;执行后端仓库的幂等迁移 `migration_warning_workflow_fields_kingbase.sql` 后成功。
+- 修复待办查询:同一用户同一预警的多条已办任务只展示最新一条;修复详情对下划线字段的处置类型、说明、人员和时间展示。页面复测两条预警各显示一行,完整处置轨迹可见。
+- 验证:`npm run build:stage` 成功;`mvn -pl zksy-system -am '-Dtest=EarlyWarningWorkflowTest' '-Dsurefire.failIfNoSpecifiedTests=false' test` 成功(3 项);前后端 `git diff --check` 无错误。
+- 两条测试预警及关联待办、处置、归档记录已从开发库精确清理,回查计数为 0;页面重载后我的已办为 0。未测试不同登录用户分别担任处置人和核查人的权限分支。
+
+## 督办、批示、升级联调
+
+- “预警处置”作为升级、督办、批示的统一入口;“待办预警”仅保留接收、开始处置、提交处置、核查解除/退回等个人工作流动作,避免重复入口。
+- 调整 `LwarningDisp.vue`:使用 `alert_type`/`alert_level` 字典和统一状态文案;草稿、已解除不显示操作,一级预警不显示升级,核查解除/退回仅 HANDLED 且当前用户为核查人;日期和处置轨迹兼容下划线字段。
+- 调整 `LTobeHandled.vue`:移除升级、督办、批示重复弹窗和按钮,保留个人工作流操作;状态、日期和详情轨迹统一显示。
+- 使用测试预警 `YJ20260929102646138AB5C16` 实测:三级升级到二级成功;督办记录成功;领导批示记录成功;详情轨迹显示升级、督办、批示。一级预警和草稿按钮显隐符合规则。
+- 督办通知、批示短信当前后端只保存记录,未接入真实外发渠道;页面已改为“通知方式(记录)/通知内容(记录)”,测试未填写真实号码。
+- 测试预警及其 `warning_todo`、`warning_disposal`、`warning_supervision`、归档、附件、报警关联已从开发库精确清理,回查计数均为 0。
+- `npm run build:stage` 成功;`git diff --check` 无错误。不同登录用户的权限分支和真实短信外发仍未验证。
+
+## 督办通知方式与附件调整
+
+- 督办弹框通知方式收敛为“短信”和“系统通知”,移除通知字段中的“(记录)”文案;附件地址改为单文件上传控件,限制 20 MB,并随 multipart 请求提交。
+- 短信复用现有 `SmsTemplate.sendBatchSms`,系统通知复用 `warning_todo` 的 `NOTICE` 类型;短信在事务提交后发送,失败写入 `warning_supervision.notification_status/notification_error`,前端提示“已保存但短信发送失败”,避免用户重复提交。
+- 前端增加通知内容 180 字提交前校验,上传按钮改用 Element Plus `Upload` 图标。
+- 新增 `WarningSupervisionNotifierTest`,并扩展 `EarlyWarningWorkflowTest` 覆盖非法通知方式、指定用户系统通知和短信失败;`mvn -pl zksy-system -am '-Dtest=EarlyWarningWorkflowTest,WarningSupervisionNotifierTest' '-Dsurefire.failIfNoSpecifiedTests=false' test` 通过(6 项)。
+- `npm run build:stage` 通过,前后端 `git diff --check` 通过;未在本轮真实页面提交短信督办,需先部署 `migration_warning_supervision_notification_kingbase.sql` 并在 Nacos 配置已审核的 `aliyun.sms.supervision-template-code`(模板变量 `warningName`、`content`)。

+ 18 - 2
src/api/pipeNetwork/earlyWarning.js

@@ -32,14 +32,22 @@ export function getWarningFullDetailData(warningId) {
   })
 }
 
-// 发布预警(确认异常)
-export function publishWarningData(warningId) {
+// 发布预警
+export function publishWarningData(warningId, data = {}) {
   return request({
     url: `/warning/publish/${warningId}`,
     method: 'post'
+    , params: data
   })
 }
 
+export function uploadWarningReportData(warningId, files) {
+  const data = new FormData()
+  ;(files || []).forEach(file => data.append('files', file))
+  return request({ url: `/warning/report/${warningId}`, method: 'post', data })
+}
+
+// 接收预警(PENDING → RECEIVED)
 export function confirmWarningData(data) {
   return request({
     url: '/warning/confirm',
@@ -48,6 +56,14 @@ export function confirmWarningData(data) {
   })
 }
 
+export function startWarningProcessData(data) {
+  return request({
+    url: '/warning/start-process',
+    method: 'post',
+    params: data
+  })
+}
+
 export function misreportWarningData(data) {
   return request({
     url: '/warning/misreport',

+ 18 - 5
src/api/smxyjldczxt/yjxxfb/yjfb.js

@@ -11,8 +11,10 @@ function buildWarningFormData(payload = {}) {
     formData.append('warning', JSON.stringify(rest))
   }
 
-  // 后端 @RequestPart("alarmIds") 要求单个名为 "alarmIds" 的 part,内容为 JSON 数组字符串
-  formData.append('alarmIds', JSON.stringify(payload.alarmIds || []))
+  // 未提供关联列表表示保持已有报警关联;空数组才表示明确清空。
+  if (payload.alarmIds != null) {
+    formData.append('alarmIds', JSON.stringify(payload.alarmIds))
+  }
 
   ;(payload.files || []).forEach((file) => {
     formData.append('files', file)
@@ -75,13 +77,20 @@ export function getWarningDetailData(warningId) {
 }
 
 // 发布预警
-export function releaseYjxxData(warningId) {
+export function releaseYjxxData(warningId, data = {}) {
   return request({
     url: `/warning/publish/${warningId}`,
-    method: 'post'
+    method: 'post',
+    params: data
   })
 }
 
+export function uploadWarningReportData(warningId, files) {
+  const data = new FormData()
+  ;(files || []).forEach(file => data.append('files', file))
+  return request({ url: `/warning/report/${warningId}`, method: 'post', data })
+}
+
 // 删除预警
 export function deleteYjxxData(warningId) {
   return request({
@@ -182,10 +191,14 @@ export function supervisionWarningData(data) {
   return request({
     url: '/warning/supervision',
     method: 'post',
-    params: data
+    data
   })
 }
 
+export function readWarningNotification(todoId) {
+  return request({ url: `/warning/notifications/${todoId}/read`, method: 'post' })
+}
+
 // 添加领导批示
 export function instructionWarningData(data) {
   return request({

+ 1 - 1
src/views/subSystem/drainage/jcyj/dqyjGIS.vue

@@ -145,7 +145,7 @@ const warningList = ref([])
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
 
 function statusText(status) {
-  const map = { DRAFT: '草稿', PENDING: '待办', PROCESSING: '处置中', RELEASED: '已发布', HANDLED: '已处置', CLOSED: '已解除' }
+  const map = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
   return map[status] ?? '-'
 }
 

+ 5 - 5
src/views/subSystem/drainage/jcyj/dqyjgl.vue

@@ -219,11 +219,11 @@ function levelTagType(level) {
   return map[level] || 'info'
 }
 function statusText(status) {
-  const map = { DRAFT: '草稿', PENDING: '待办', PROCESSING: '处置中', RELEASED: '已发布', HANDLED: '已处置', CLOSED: '已解除' }
+  const map = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
   return map[status] ?? '-'
 }
 function statusTagType(status) {
-  const map = { DRAFT: 'info', PENDING: 'warning', PROCESSING: 'warning', RELEASED: 'success', HANDLED: 'success', CLOSED: '' }
+  const map = { DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning', HANDLED: 'success', CLOSED: '' }
   return map[status] || 'info'
 }
 
@@ -253,7 +253,7 @@ const statItems = computed(() => {
     { label: '今日预警', value: total.value, cls: '' },
     { label: '待办', value: list.filter(o => o.status === 'PENDING').length, cls: 'red' },
     { label: '处置中', value: list.filter(o => o.status === 'PROCESSING').length, cls: '' },
-    { label: '已发布', value: list.filter(o => o.status === 'RELEASED').length, cls: '' }
+    { label: '已接收', value: list.filter(o => o.status === 'RECEIVED').length, cls: '' }
   ]
 })
 
@@ -262,7 +262,7 @@ const statusLegends = computed(() => [
   { name: '草稿', value: tableData.value.filter(o => o.status === 'DRAFT').length, color: '#909399' },
   { name: '待办', value: tableData.value.filter(o => o.status === 'PENDING').length, color: '#E6A23C' },
   { name: '处置中', value: tableData.value.filter(o => o.status === 'PROCESSING').length, color: '#409EFF' },
-  { name: '已发布', value: tableData.value.filter(o => o.status === 'RELEASED').length, color: '#67C23A' }
+  { name: '已接收', value: tableData.value.filter(o => o.status === 'RECEIVED').length, color: '#67C23A' }
 ])
 const levelLegends = computed(() => [
   { name: '严重', value: tableData.value.filter(o => o.warningLevel === 1).length, color: '#F56C6C' },
@@ -309,7 +309,7 @@ async function loadData() {
     // 告诉后端只返回当前预警(未处置的)
     params.currentOnly = true
     // Tab 映射为状态筛选
-    const tabStatusMap = { '草稿': 'DRAFT', '待办': 'PENDING', '处置中': 'PROCESSING', '已发布': 'RELEASED' }
+    const tabStatusMap = { '草稿': 'DRAFT', '待办': 'PENDING', '已接收': 'RECEIVED', '处置中': 'PROCESSING' }
     if (activeTab.value !== '全部' && tabStatusMap[activeTab.value] !== undefined) {
       params.status = tabStatusMap[activeTab.value]
     }

+ 2 - 2
src/views/subSystem/drainage/jcyj/lsyjGIS.vue

@@ -140,7 +140,7 @@ const warningList = ref([])
 
 // 预警级别/状态数字→文字映射
 const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
-const statusMap = { HANDLED: '已处置', CLOSED: '已解除', DRAFT: '草稿', PENDING: '待办', PROCESSING: '处置中', RELEASED: '已发布' }
+const statusMap = { HANDLED: '已处置', CLOSED: '已解除', DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中' }
 
 // 加载历史预警数据
 async function loadData() {
@@ -206,7 +206,7 @@ function getLevelKey(level) {
 }
 
 function getStatusTagType(status) {
-  const map = { HANDLED: 'success', CLOSED: '', DRAFT: 'info', PENDING: 'warning', PROCESSING: 'warning', RELEASED: 'success' }
+  const map = { HANDLED: 'success', CLOSED: '', DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning' }
   return map[status] || 'info'
 }
 

+ 2 - 2
src/views/subSystem/drainage/jcyj/lsyjgl.vue

@@ -177,11 +177,11 @@ function levelTagType(level) {
   return map[level] || 'info'
 }
 function statusText(status) {
-  const map = { DRAFT: '草稿', PENDING: '待办', PROCESSING: '处置中', RELEASED: '已发布', HANDLED: '已处置', CLOSED: '已解除' }
+  const map = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
   return map[status] ?? '-'
 }
 function statusTagType(status) {
-  const map = { DRAFT: 'info', PENDING: 'warning', PROCESSING: 'warning', RELEASED: 'success', HANDLED: 'success', CLOSED: '' }
+  const map = { DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning', HANDLED: 'success', CLOSED: '' }
   return map[status] || 'info'
 }
 

+ 85 - 0
src/views/subSystem/lifeCompany/WarningWorkflowActions.vue

@@ -0,0 +1,85 @@
+<template>
+  <span class="workflow-actions" @click.stop>
+    <el-button v-if="allowed('RECEIVE')" v-hasPermi="['warning:warning:handle']" link type="primary" :loading="busy" @click="receive">接收</el-button>
+    <el-button v-if="allowed('START')" v-hasPermi="['warning:warning:handle']" link type="primary" :loading="busy" @click="start">开始处置</el-button>
+    <el-button v-if="allowed('HANDLE')" v-hasPermi="['warning:warning:handle']" link type="warning" @click="openProcess">提交处置</el-button>
+    <el-button v-if="allowed('CLEAR')" v-hasPermi="['warning:warning:handle']" link type="success" :loading="busy" @click="review(true)">核查解除</el-button>
+    <el-button v-if="allowed('RETURN')" v-hasPermi="['warning:warning:handle']" link type="danger" :loading="busy" @click="review(false)">退回重办</el-button>
+    <el-dialog v-model="visible" title="提交现场处置结果" width="620px" append-to-body :close-on-click-modal="!busy" :before-close="closeProcess">
+      <el-alert title="提交后进入待核查,由指定核查人解除或退回。" type="info" :closable="false" show-icon />
+      <el-form label-width="90px" style="margin-top: 16px">
+        <el-form-item v-if="reasons.length" label="参考原因">
+          <el-select placeholder="按预警类型和级别选择原因" @change="appendReason" style="width: 100%">
+            <el-option v-for="reason in reasons" :key="reason.reasonId" :label="reason.reasonName" :value="reason.reasonName" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="处置结果" required><el-input v-model="content" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="填写核查情况、原因、措施和现场恢复情况" /></el-form-item>
+        <el-form-item label="处置附件">
+          <el-upload v-model:file-list="files" :auto-upload="false" action="#" multiple :limit="9" accept=".pdf,.doc,.docx,.xls,.xlsx,.zip,.jpg,.jpeg,.png"><el-button>选择附件</el-button><template #tip><div class="el-upload__tip">单个文件不超过 10MB,总计不超过 20MB。</div></template></el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="visible = false">取消</el-button><el-button type="primary" :loading="busy" @click="submit">提交核查</el-button></template>
+    </el-dialog>
+  </span>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import useUserStore from '@/store/modules/user'
+import { confirmWarningData, startWarningProcessData, submitWarningProcessData, clearWarningData } from '@/api/pipeNetwork/earlyWarning'
+import { returnWarningData, getWarningReasonListData } from '@/api/smxyjldczxt/yjxxfb/yjfb'
+import { workflowActions } from './warningWorkflow'
+
+const props = defineProps({ row: { type: Object, required: true } })
+const emit = defineEmits(['changed'])
+const user = useUserStore()
+const actions = computed(() => workflowActions(props.row, user.name))
+const allowed = action => actions.value.includes(action)
+const warningId = () => props.row.warningId || props.row.warning_id
+const busy = ref(false)
+const visible = ref(false)
+const content = ref('')
+const files = ref([])
+const reasons = ref([])
+function closeProcess(done) { if (!busy.value) done() }
+function appendReason(reason) { content.value = [content.value, '原因:' + reason].filter(Boolean).join('\n') }
+async function run(action, message) {
+  if (busy.value) return
+  busy.value = true
+  try {
+    const res = await action()
+    if (res.code !== 200) { ElMessage.error(res.msg || '操作失败'); return }
+    ElMessage.success(message)
+    visible.value = false
+    emit('changed')
+  } finally { busy.value = false }
+}
+async function receive() {
+  try { await ElMessageBox.confirm('确认接收该预警?', '接收预警'); await run(() => confirmWarningData({ warningId: warningId() }), '已接收,请开始处置') } catch (e) { if (!['cancel', 'close'].includes(e)) console.error('接收预警失败', e) }
+}
+async function start() {
+  try { await ElMessageBox.confirm('确认开始现场处置?', '开始处置'); await run(() => startWarningProcessData({ warningId: warningId() }), '已开始处置') } catch (e) { if (!['cancel', 'close'].includes(e)) console.error('开始处置失败', e) }
+}
+async function openProcess() {
+  content.value = ''; files.value = []; reasons.value = []; visible.value = true
+  try {
+    const res = await getWarningReasonListData({ warningType: props.row.warningType || props.row.warning_type, warningLevel: props.row.warningLevel || props.row.warning_level })
+    reasons.value = res.data || []
+  } catch { ElMessage.warning('参考原因加载失败,可直接填写处置结果') }
+}
+async function submit() {
+  if (!content.value.trim()) return ElMessage.warning('请填写现场处置结果')
+  const rawFiles = files.value.map(file => file.raw).filter(Boolean)
+  if (rawFiles.some(file => file.size > 10 * 1024 * 1024) || rawFiles.reduce((sum, file) => sum + file.size, 0) > 20 * 1024 * 1024) return ElMessage.warning('附件超过允许大小')
+  await run(() => submitWarningProcessData({ warningId: warningId(), processContent: content.value.trim(), files: rawFiles }), '处置结果已提交,等待核查')
+}
+async function review(pass) {
+  try {
+    const { value } = await ElMessageBox.prompt(pass ? '请填写核查通过和解除依据' : '请填写退回原因及重新处置要求', pass ? '核查解除' : '退回重办', { inputType: 'textarea', inputValidator: value => !!value?.trim() || '请填写核查意见' })
+    await run(() => pass ? clearWarningData({ warningId: warningId(), clearRemark: value.trim() }) : returnWarningData({ warningId: warningId(), disposalContent: value.trim() }), pass ? '核查通过,预警已解除' : '已退回原处置人重新处置')
+  } catch (e) { if (!['cancel', 'close'].includes(e)) console.error('预警核查失败', e) }
+}
+</script>
+
+<style scoped>.workflow-actions { display: inline-flex; flex-wrap: wrap; gap: 4px; align-items: center; }</style>

+ 256 - 59
src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue

@@ -8,7 +8,7 @@
           <div class="page-subtitle">支持按预警类型、预警级别、发布时间查询,并对预警信息进行新增、编辑、查看、发布等操作</div>
         </div>
         <div class="header-actions">
-          <el-button type="primary" icon="Plus" @click="handleAdd">新增预警</el-button>
+          <el-button v-hasPermi="['warning:warning:add']" type="primary" icon="Plus" @click="handleAdd">新增预警</el-button>
           <el-button icon="Refresh" @click="loadList">刷新</el-button>
         </div>
       </div>
@@ -81,7 +81,9 @@
           </template>
         </el-table-column>
         <el-table-column label="预警位置" prop="location" min-width="180" show-overflow-tooltip />
-        <el-table-column label="发布时间" prop="publishTime" width="180" />
+        <el-table-column label="发布时间" width="180">
+          <template #default="scope">{{ formatDateTime(scope.row.publishTime) }}</template>
+        </el-table-column>
         <el-table-column label="发布人" prop="publisher" width="120" />
         <el-table-column label="状态" width="120" align="center">
           <template #default="scope">
@@ -93,9 +95,9 @@
         <el-table-column label="操作" fixed="right" width="280" align="center">
           <template #default="scope">
             <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
-            <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === 'DRAFT'">编辑</el-button>
-            <el-button link type="success" @click="handlePublish(scope.row)" v-if="scope.row.status === 'DRAFT'">发布</el-button>
-            <el-button link type="danger" @click="handleDelete(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>
           </template>
         </el-table-column>
       </el-table>
@@ -148,7 +150,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="发布人">
-              <el-input v-model="formData.publisher" placeholder="自动填充当前用户" disabled />
+              <el-input :model-value="formData.publisher || '发布时确定'" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -160,6 +162,13 @@
               </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="warningName">
               <el-input v-model="formData.warningName" placeholder="请输入预警名称" maxlength="100" show-word-limit />
@@ -170,6 +179,15 @@
               <el-input v-model="formData.location" placeholder="请输入预警位置" />
             </el-form-item>
           </el-col>
+          <el-col :span="24">
+            <el-form-item label="关联设备">
+              <el-input v-model="formData.deviceName" placeholder="点击选择关联设备" readonly @click="openDevicePicker">
+                <template #append>
+                  <el-button :icon="Search" @click="openDevicePicker">选择</el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="经度">
               <el-input v-model="formData.longitude" placeholder="请输入经度" />
@@ -180,29 +198,6 @@
               <el-input v-model="formData.latitude" placeholder="请输入纬度" />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="发布时间">
-              <el-date-picker
-                v-model="formData.publishTime"
-                type="datetime"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                placeholder="请选择发布时间"
-                style="width: 100%"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态">
-              <el-select v-model="formData.status" style="width: 100%">
-                <el-option
-                  v-for="item in warningStatusOptions"
-                  :key="item.dictValue"
-                  :label="item.dictLabel"
-                  :value="item.dictValue"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
           <el-col :span="24">
             <el-form-item label="内容描述" prop="warningContent">
               <el-input
@@ -297,7 +292,14 @@
           <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">{{ detailData.basicInfo.location || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ detailData.basicInfo.publishTime || "-" }}</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="状态">{{ getStatusText(detailData.basicInfo.status) }}</el-descriptions-item>
           <el-descriptions-item label="内容描述" :span="2">{{ detailData.basicInfo.warningContent || "-" }}</el-descriptions-item>
           <el-descriptions-item label="备注" :span="2">{{ detailData.basicInfo.remark || "-" }}</el-descriptions-item>
@@ -335,8 +337,68 @@
       </template>
     </el-drawer>
 
+    <!-- 关联设备选择对话框 -->
+    <el-dialog v-model="devicePickerVisible" title="选择关联设备" width="860px" append-to-body @open="loadDevicePickerData">
+      <div class="device-picker-container">
+        <div class="device-picker-left">
+          <div class="picker-title">设备类型</div>
+          <button
+            :class="{ active: deviceRootTypeId === '' }"
+            class="device-type-item"
+            @click="selectDeviceRootType('')"
+          >
+            全部类型
+          </button>
+          <button
+            v-for="item in deviceRootTypes"
+            :key="item.id"
+            :class="{ active: String(deviceRootTypeId) === String(item.id) }"
+            class="device-type-item"
+            @click="selectDeviceRootType(item.id)"
+          >
+            {{ item.typeName }}
+          </button>
+        </div>
+        <div class="device-picker-right">
+          <el-form :inline="true" @submit.prevent>
+            <el-form-item>
+              <el-input v-model="deviceQuery.equipmentCode" placeholder="设备编码" clearable style="width: 180px" @keyup.enter="loadDeviceList" />
+            </el-form-item>
+            <el-form-item>
+              <el-input v-model="deviceQuery.equipmentName" placeholder="设备名称" clearable style="width: 180px" @keyup.enter="loadDeviceList" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="loadDeviceList">查询</el-button>
+            </el-form-item>
+          </el-form>
+          <el-table v-loading="deviceLoading" :data="deviceList" border size="small" height="320">
+            <el-table-column prop="equipmentCode" label="设备编码" min-width="180" show-overflow-tooltip />
+            <el-table-column prop="equipmentName" label="设备名称" min-width="180" show-overflow-tooltip />
+            <el-table-column label="设备类型" min-width="140">
+              <template #default="{ row }">{{ deviceTypeNameMap[row.equipmentTypeId] || row.equipmentTypeName || "-" }}</template>
+            </el-table-column>
+            <el-table-column label="操作" width="90" align="center">
+              <template #default="{ row }">
+                <el-button link type="primary" @click="confirmDevice(row)">选择</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination
+            v-model:current-page="deviceQuery.pageNum"
+            v-model:page-size="deviceQuery.pageSize"
+            :total="deviceTotal"
+            :page-sizes="[10, 20, 50]"
+            layout="total, sizes, prev, pager, next"
+            style="margin-top: 12px; justify-content: flex-end"
+            @size-change="loadDeviceList"
+            @current-change="loadDeviceList"
+          />
+        </div>
+      </div>
+    </el-dialog>
+
     <!-- 处置人选择对话框 -->
-    <el-dialog v-model="userPickerVisible" title="选择处置人" width="900px" append-to-body @open="loadUserPickerData">
+    <el-dialog v-model="userPickerVisible" :title="pickerTarget === 'reviewer' ? '选择核查人' : '选择处置人'" width="900px" append-to-body @open="loadUserPickerData">
       <div class="user-picker-container">
         <!-- 左侧部门树 -->
         <div class="user-picker-left">
@@ -387,8 +449,10 @@
 import {computed, onMounted, reactive, ref, watch} from "vue";
 import {ElMessage, ElMessageBox} from "element-plus";
 import { Search } from "@element-plus/icons-vue";
-import useUserStore from "@/store/modules/user";
+import { parseTime } from "@/utils/ruoyi";
+import { warningStatusLabels } from "../warningWorkflow";
 import { deptTreeSelect, listUser } from "@/api/system/user";
+import { listEquipment, listEquipmentTypeAll } from "@/api/pipeNetwork/pdeviceInform";
 import {
   deleteYjxxData,
   getAlertTypeDictionaryData,
@@ -400,15 +464,12 @@ import {
   updateYjxxData
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
-const userStore = useUserStore();
-
 const loading = ref(false);
 const submitLoading = ref(false);
 const total = ref(0);
 const warningList = ref([]);
 const warningTypeOptions = ref([]);
 const warningLevelOptions = ref([]);
-const warningStatusOptions = ref([]);
 const reasonOptions = ref([]);
 const uploadRef = ref();
 const formAttachments = ref([]);
@@ -417,11 +478,27 @@ const minioBaseUrl = (import.meta.env.VITE_APP_MINIO_BASE_URL || "").trim();
 const editVisible = ref(false);
 const detailVisible = ref(false);
 const userPickerVisible = ref(false);
+const pickerTarget = ref('handler');
 const dialogTitle = ref("新增预警");
 const isEdit = ref(false);
 const formRef = ref();
 const detailData = ref({});
 
+// 关联设备选择器
+const devicePickerVisible = ref(false);
+const deviceRootTypeId = ref("");
+const deviceRootTypes = ref([]);
+const deviceList = ref([]);
+const deviceTotal = ref(0);
+const deviceLoading = ref(false);
+const deviceTypeNameMap = ref({});
+const deviceQuery = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  equipmentCode: "",
+  equipmentName: ""
+});
+
 // 用户选择器相关
 const deptTree = ref([]);
 const userList = ref([]);
@@ -467,13 +544,6 @@ const warningLevelLabelMap = computed(() => {
   }, {});
 });
 
-const warningStatusLabelMap = computed(() => {
-  return warningStatusOptions.value.reduce((acc, item) => {
-    acc[item.dictValue] = item.dictLabel;
-    return acc;
-  }, {});
-});
-
 function createEmptyForm() {
   return {
     warningId: "",
@@ -482,6 +552,8 @@ function createEmptyForm() {
     warningType: "",
     warningLevel: "",
     warningSpecial: "",
+    deviceCode: "",
+    deviceName: "",
     location: "",
     longitude: "",
     latitude: "",
@@ -489,6 +561,7 @@ function createEmptyForm() {
     publisher: "",
     publishTime: "",
     handler: "",
+    reviewer: "",
     status: "DRAFT",
     warningContent: "",
     remark: ""
@@ -559,6 +632,10 @@ function formatLevelText(value) {
   return warningLevelLabelMap.value[value] || value || "-";
 }
 
+function formatDateTime(value) {
+  return value ? parseTime(value) : "-";
+}
+
 function getLevelTagType(level) {
   return {
     "0": "danger",
@@ -570,7 +647,7 @@ function getLevelTagType(level) {
 }
 
 function getStatusText(status) {
-  return warningStatusLabelMap.value[status] || status || "-";
+  return warningStatusLabels[status] || status || "-";
 }
 
 function getStatusTagType(status) {
@@ -578,8 +655,8 @@ function getStatusTagType(status) {
   return {
     DRAFT: "info",        // 草稿 - 灰色(未发布)
     PENDING: "warning",   // 待办 - 黄色(等待处置)
+    RECEIVED: "warning",  // 已接收 - 黄色
     PROCESSING: "warning", // 处置中 - 黄色(进行中)
-    RELEASED: "success",  // 已发布 - 绿色
     HANDLED: "success",   // 已处置 - 绿色(已完成)
     CLOSED: "info"        // 已解除 - 灰色
   }[status] || "info";
@@ -600,8 +677,11 @@ function normalizeWarningRow(item) {
   row.publisher = item.publisher || "";
   row.publishTime = item.publishTime || item.publish_time || "";
   row.handler = item.handler || "";
+  row.reviewer = item.reviewer || "";
   row.status = item.status || "";
   row.warningContent = item.warningContent || item.warning_content || "";
+  row.deviceCode = item.deviceCode || item.device_code || "";
+  row.deviceName = item.deviceName || item.device_name || "";
   row.remark = item.remark || "";
   row.warningTypeText = formatTypeText(row.warningType);
   row.warningLevelText = formatLevelText(row.warningLevel);
@@ -628,14 +708,16 @@ function normalizeDisposalRow(item) {
 }
 
 async function loadDicts() {
-  const [typeRes, levelRes, statusRes] = await Promise.all([
+  const [typeResult, levelResult] = await Promise.allSettled([
     getAlertTypeDictionaryData({ dictType: "alert_type" }),
-    getAlertTypeDictionaryData({ dictType: "alert_level" }),
-    getAlertTypeDictionaryData({ dictType: "pre_alarm_status" })
+    getAlertTypeDictionaryData({ dictType: "alert_level" })
   ]);
-  warningTypeOptions.value = typeRes.rows || [];
-  warningLevelOptions.value = levelRes.rows || [];
-  warningStatusOptions.value = statusRes.rows || [];
+  if (typeResult.status === "fulfilled") {
+    warningTypeOptions.value = typeResult.value.rows || [];
+  }
+  if (levelResult.status === "fulfilled") {
+    warningLevelOptions.value = levelResult.value.rows || [];
+  }
 }
 
 async function loadReasonOptions() {
@@ -676,6 +758,9 @@ async function loadList() {
     const res = await getdbyjlistData(params);
     warningList.value = (res.data?.records || []).map(normalizeWarningRow);
     total.value = res.data?.total || 0;
+  } catch {
+    warningList.value = [];
+    total.value = 0;
   } finally {
     loading.value = false;
   }
@@ -766,27 +851,87 @@ function handleUserCurrentChange(row) {
 }
 
 function openUserPicker() {
+  pickerTarget.value = 'handler';
   selectedUser.value = null;
   userFilterText.value = "";
   deptFilterText.value = "";
   userPickerVisible.value = true;
 }
 
+function openReviewerPicker() {
+  pickerTarget.value = 'reviewer';
+  selectedUser.value = null;
+  userFilterText.value = '';
+  deptFilterText.value = '';
+  userPickerVisible.value = true;
+}
+
 function confirmSelectUser() {
   if (!selectedUser.value) {
     ElMessage.warning("请选择用户");
     return;
   }
-  formData.handler = selectedUser.value.userName;
+  formData[pickerTarget.value] = selectedUser.value.userName;
   userPickerVisible.value = false;
 }
 
+function openDevicePicker() {
+  devicePickerVisible.value = true;
+}
+
+async function loadDevicePickerData() {
+  deviceQuery.pageNum = 1;
+  deviceQuery.equipmentCode = "";
+  deviceQuery.equipmentName = "";
+  try {
+    const typeRes = await listEquipmentTypeAll();
+    const allTypes = typeRes.data || [];
+    deviceRootTypes.value = allTypes.filter((t) => String(t.parentId) === "0" || t.parentId === 0 || t.parentId === null || t.parentId === undefined || t.parentId === "");
+    deviceTypeNameMap.value = allTypes.reduce((acc, t) => {
+      acc[t.id] = t.typeName;
+      return acc;
+    }, {});
+  } catch (e) {
+    deviceRootTypes.value = [];
+  }
+  await loadDeviceList();
+}
+
+function selectDeviceRootType(rootTypeId) {
+  deviceRootTypeId.value = rootTypeId === "" ? "" : String(rootTypeId);
+  deviceQuery.pageNum = 1;
+  loadDeviceList();
+}
+
+async function loadDeviceList() {
+  deviceLoading.value = true;
+  try {
+    const res = await listEquipment({
+      pageNum: deviceQuery.pageNum,
+      pageSize: deviceQuery.pageSize,
+      equipmentCode: deviceQuery.equipmentCode || undefined,
+      equipmentName: deviceQuery.equipmentName || undefined,
+      rootTypeId: deviceRootTypeId.value || undefined
+    });
+    deviceList.value = res.records || [];
+    deviceTotal.value = Number(res.total || 0);
+  } catch (e) {
+    ElMessage.error(e?.msg || e?.message || "加载设备失败");
+  } finally {
+    deviceLoading.value = false;
+  }
+}
+
+function confirmDevice(row) {
+  formData.deviceCode = row.equipmentCode || "";
+  formData.deviceName = row.equipmentName || "";
+  devicePickerVisible.value = false;
+}
+
 function handleAdd() {
   isEdit.value = false;
   dialogTitle.value = "新增预警";
   resetForm();
-  // 自动填充发布人为当前用户
-  formData.publisher = userStore.name;
   editVisible.value = true;
 }
 
@@ -797,13 +942,15 @@ async function handleEdit(row) {
   Object.assign(formData, { ...row });
   const detailRes = await getWarningDetailData(row.warningId);
   const detail = detailRes.data || {};
+  formData.reviewer = detail.basicInfo?.reviewer || "";
   formAttachments.value = (detail.attachmentList || []).map(normalizeExistingAttachment);
   await loadReasonOptions();
   editVisible.value = true;
 }
 
 async function submitForm() {
-  await formRef.value?.validate();
+  const valid = await formRef.value?.validate().catch(() => false);
+  if (!valid) return;
   submitLoading.value = true;
   try {
     const payload = {
@@ -818,14 +965,13 @@ async function submitForm() {
         longitude: formData.longitude,
         latitude: formData.latitude,
         ownershipUnit: formData.ownershipUnit,
-        publisher: formData.publisher,
-        publishTime: formData.publishTime,
         handler: formData.handler,
-        status: formData.status || "DRAFT",
+        reviewer: formData.reviewer,
         warningContent: formData.warningContent,
+        deviceCode: formData.deviceCode || "",
+        deviceName: formData.deviceName || "",
         remark: formData.remark
       },
-      alarmIds: [],
       files: formAttachments.value.map((item) => item.raw).filter(Boolean)
     };
     const res = isEdit.value ? await updateYjxxData(payload) : await submitYjxxData(payload);
@@ -866,8 +1012,12 @@ async function handleDelete(row) {
 }
 
 async function handlePublish(row) {
+  if (!row.handler) {
+    ElMessage.warning("发布前请先编辑预警并选择处置人");
+    return;
+  }
   await ElMessageBox.confirm(`确认发布预警“${row.warningName}”吗?`, "提示", { type: "warning" });
-  const res = await releaseYjxxData(row.warningId);
+  const res = await releaseYjxxData(row.warningId, { reviewer: row.reviewer || undefined });
   if (res.code !== 200) {
     ElMessage.error(res.msg || "发布失败");
     return;
@@ -1015,4 +1165,51 @@ onMounted(async () => {
   color: #6b7280;
   font-size: 13px;
 }
+
+.device-picker-container {
+  display: flex;
+  gap: 16px;
+  height: 460px;
+}
+
+.device-picker-left {
+  width: 180px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  padding: 12px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.device-picker-right {
+  flex: 1;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  padding: 12px;
+  overflow: hidden;
+}
+
+.device-type-item {
+  padding: 9px 12px;
+  text-align: left;
+  color: #4b5563;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  background: #fff;
+  cursor: pointer;
+  font-size: 13px;
+}
+
+.device-type-item:hover {
+  background: #f3f4f6;
+}
+
+.device-type-item.active {
+  color: #2563eb;
+  border-color: #2563eb;
+  background: #eff6ff;
+  font-weight: 600;
+}
 </style>

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

@@ -265,7 +265,8 @@ import * as echarts from "echarts";
 import {
   getAlertTypeDictionaryData,
   getLinkedAlarmListData,
-  getWarningDetailData,
+    getWarningDetailData,
+    uploadWarningReportData,
   getdbyjlistData,
   updateYjxxData
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
@@ -386,8 +387,8 @@ function getStatusTagType(status) {
   return {
     DRAFT: "info",
     PENDING: "warning",
+    RECEIVED: "warning",
     PROCESSING: "warning",
-    RELEASED: "success",
     HANDLED: "success",
     CLOSED: "info"
   }[status] || "info";
@@ -581,33 +582,7 @@ async function submitReport() {
 
   submitLoading.value = true;
   try {
-    const linkedRes = await getLinkedAlarmListData(selectedWarningId.value);
-    const alarmIds = (linkedRes.data || []).map((item) => item.alarmId || item.alarm_id).filter(Boolean);
-    const payload = {
-      warning: {
-        warningId: currentBasicInfo.value.warningId,
-        warningNo: currentBasicInfo.value.warningNo,
-        warningName: currentBasicInfo.value.warningName,
-        warningType: currentBasicInfo.value.warningType,
-        warningLevel: currentBasicInfo.value.warningLevel,
-        warningSpecial: currentBasicInfo.value.warningSpecial,
-        location: currentBasicInfo.value.location,
-        longitude: currentBasicInfo.value.longitude,
-        latitude: currentBasicInfo.value.latitude,
-        ownershipUnit: currentBasicInfo.value.ownershipUnit,
-        publisher: currentBasicInfo.value.publisher,
-        publishTime: currentBasicInfo.value.publishTime,
-        handler: currentBasicInfo.value.handler,
-        status: currentBasicInfo.value.status,
-        warningContent: currentBasicInfo.value.warningContent,
-        remark: reportSummary.value
-          ? [currentBasicInfo.value.remark, `报告摘要:${reportSummary.value}`].filter(Boolean).join("\n")
-          : currentBasicInfo.value.remark
-      },
-      alarmIds,
-      files: [uploadedFile.value]
-    };
-    const res = await updateYjxxData(payload);
+    const res = await uploadWarningReportData(selectedWarningId.value, [uploadedFile.value]);
     if (res.code !== 200) {
       ElMessage.error(res.msg || "报告上传失败");
       return;

+ 149 - 198
src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue

@@ -5,7 +5,7 @@
       <div class="page-header">
         <div>
           <div class="page-title">待办预警</div>
-          <div class="page-subtitle">展示当前登录用户需处置的预警列表,支持“我的待办”和“我的已办”快速查阅与筛选</div>
+          <div class="page-subtitle">预警任务与督办通知</div>
         </div>
         <el-button type="primary" icon="Refresh" @click="loadCurrentTabData">刷新</el-button>
       </div>
@@ -24,7 +24,7 @@
       </el-card>
       <el-card shadow="hover" class="summary-card primary">
         <div class="summary-label">当前视图</div>
-        <div class="summary-value">{{ filteredList.length }}</div>
+        <div class="summary-value">{{ pagedList.length }}</div>
         <div class="summary-desc">本页筛选后可查看记录数</div>
       </el-card>
     </div>
@@ -103,6 +103,7 @@
       <el-tabs v-model="activeTab" @tab-change="handleTabChange">
         <el-tab-pane :label="`我的待办 (${todoCount})`" name="TODO" />
         <el-tab-pane :label="`我的已办 (${doneCount})`" name="DONE" />
+        <el-tab-pane :label="`系统通知 (${noticeCount})`" name="NOTICE" />
       </el-tabs>
 
       <el-table :data="pagedList" @row-click="handleViewDetail">
@@ -112,6 +113,10 @@
           </template>
         </el-table-column>
         <el-table-column label="预警名称" prop="warningName" min-width="180" show-overflow-tooltip />
+        <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>
+        </el-table-column>
         <el-table-column label="预警编号" prop="warningNo" width="170" />
         <el-table-column label="预警专项" prop="warningSpecial" width="140" show-overflow-tooltip />
         <el-table-column label="预警类型" prop="warningTypeText" width="120" />
@@ -131,29 +136,29 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="发布时间" prop="publishTime" width="180" />
+        <el-table-column label="发布时间" width="180">
+          <template #default="scope">{{ formatDateTime(scope.row.publishTime) }}</template>
+        </el-table-column>
         <el-table-column label="状态" width="100" align="center">
           <template #default="scope">
-            <el-tag :type="activeTab === 'TODO' ? 'danger' : 'success'">
-              {{ activeTab === "TODO" ? "待办" : "已办" }}
+            <el-tag :type="getStatusTagType(scope.row.status)">
+              {{ getStatusText(scope.row.status) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" fixed="right" width="220" align="center">
+        <el-table-column label="操作" fixed="right" width="300" align="center">
           <template #default="scope">
             <el-button link type="primary" @click.stop="handleViewDetail(scope.row)">查看详情</el-button>
             <template v-if="activeTab === 'TODO'">
-              <el-button link type="warning" @click.stop="openUpgradeDialog(scope.row)">升级</el-button>
-              <el-button link type="success" @click.stop="openResolveDialog(scope.row)">解除</el-button>
-              <el-button link type="danger" @click.stop="openReturnDialog(scope.row)">退回</el-button>
+              <WarningWorkflowActions :row="scope.row" @changed="refreshLists" />
             </template>
           </template>
         </el-table-column>
       </el-table>
 
       <pagination
-        v-show="filteredList.length > 0"
-        :total="filteredList.length"
+        v-show="currentTotal > 0"
+        :total="currentTotal"
         v-model:page="pagination.pageNum"
         v-model:limit="pagination.pageSize"
         @pagination="handlePageChange"
@@ -161,6 +166,7 @@
     </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">
         <el-descriptions :column="2" border>
           <el-descriptions-item label="预警名称">{{ detailBasic.warningName || "-" }}</el-descriptions-item>
@@ -172,7 +178,7 @@
           <el-descriptions-item label="发布人">{{ detailBasic.publisher || "-" }}</el-descriptions-item>
           <el-descriptions-item label="权属单位">{{ detailBasic.ownershipUnit || "-" }}</el-descriptions-item>
           <el-descriptions-item label="报警位置" :span="2">{{ detailBasic.location || "-" }}</el-descriptions-item>
-          <el-descriptions-item label="发布时间">{{ detailBasic.publishTime || "-" }}</el-descriptions-item>
+          <el-descriptions-item label="发布时间">{{ formatDateTime(detailBasic.publishTime) }}</el-descriptions-item>
           <el-descriptions-item label="经纬度">{{ detailBasic.longitude || "-" }} / {{ detailBasic.latitude || "-" }}</el-descriptions-item>
           <el-descriptions-item label="预警内容" :span="2">{{ detailBasic.warningContent || "-" }}</el-descriptions-item>
           <el-descriptions-item label="备注" :span="2">{{ detailBasic.remark || "-" }}</el-descriptions-item>
@@ -192,55 +198,17 @@
           <el-timeline-item
             v-for="(item, index) in detailData.disposalHistory"
             :key="index"
-            :timestamp="item.createTime || item.disposalTime || '-'"
+            :timestamp="formatDateTime(item.createTime || item.create_time || item.disposalTime || item.disposal_time)"
           >
-            {{ item.disposalContent || item.content || "-" }}
+            {{ 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>
           </el-timeline-item>
         </el-timeline>
         <el-empty v-else description="暂无处置记录" />
       </template>
     </el-drawer>
 
-    <!-- 预警升级对话框 -->
-    <el-dialog v-model="upgradeDialog.visible" title="预警升级" width="500px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="预警名称">
-          <el-input :model-value="upgradeDialog.row?.warningName || '-'" disabled />
-        </el-form-item>
-        <el-form-item label="当前级别">
-          <el-tag :type="getLevelTagType(upgradeDialog.row?.warningLevel)">
-            {{ upgradeDialog.row?.warningLevelText || "-" }}
-          </el-tag>
-        </el-form-item>
-        <el-form-item label="新预警级别" required>
-          <el-select v-model="upgradeDialog.newLevel" placeholder="请选择新级别" style="width: 100%">
-            <el-option
-              v-for="item in warningLevelOptions"
-              :key="item.dictValue"
-              :label="item.dictLabel"
-              :value="item.dictValue"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="处置说明" required>
-          <el-input
-            v-model="upgradeDialog.disposalContent"
-            type="textarea"
-            :rows="4"
-            maxlength="500"
-            show-word-limit
-            placeholder="请输入处置说明"
-          />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="upgradeDialog.visible = false">取 消</el-button>
-          <el-button type="primary" :loading="actionLoading" @click="confirmUpgrade">确 定</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
     <!-- 预警解除对话框 -->
     <el-dialog v-model="resolveDialog.visible" title="解除预警" width="500px" append-to-body>
       <el-form label-width="100px">
@@ -290,34 +258,33 @@
         </div>
       </template>
     </el-dialog>
+
   </div>
 </template>
 
 <script setup>
 import { computed, onMounted, reactive, ref } from "vue";
-import useUserStore from "@/store/modules/user";
 import { ElMessage, ElMessageBox } from "element-plus";
-import { getAlertTypeDictionaryData, getTodoListData, getWarningDetailData, upgradeWarningData, resolveWarningData, returnWarningData } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
+import { parseTime } from "@/utils/ruoyi";
+import WarningWorkflowActions from "../WarningWorkflowActions.vue";
+import { warningStatusLabels } from "../warningWorkflow";
+import { getAlertTypeDictionaryData, getTodoListData, getWarningDetailData, readWarningNotification, resolveWarningData, returnWarningData } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
-const userStore = useUserStore();
 const loading = ref(false);
 const activeTab = ref("TODO");
 const todoList = ref([]);
 const doneList = ref([]);
+const noticeList = ref([]);
+const todoTotal = ref(0);
+const doneTotal = ref(0);
+const noticeTotal = ref(0);
 const warningTypeOptions = ref([]);
 const warningLevelOptions = ref([]);
-const warningStatusOptions = ref([]);
 const detailVisible = ref(false);
 const detailData = ref({});
+const selectedNotice = ref("");
 const actionLoading = ref(false);
 
-const upgradeDialog = reactive({
-  visible: false,
-  row: null,
-  newLevel: "",
-  disposalContent: ""
-});
-
 const resolveDialog = reactive({
   visible: false,
   row: null,
@@ -363,41 +330,23 @@ const warningLevelLabelMap = computed(() => {
   }, {});
 });
 
-const warningStatusLabelMap = computed(() => {
-  return warningStatusOptions.value.reduce((acc, item) => {
-    acc[item.dictValue] = item.dictLabel;
-    return acc;
-  }, {});
-});
+const currentList = computed(() => ({ TODO: todoList.value, DONE: doneList.value, NOTICE: noticeList.value })[activeTab.value] || []);
+const currentTotal = computed(() => ({ TODO: todoTotal.value, DONE: doneTotal.value, NOTICE: noticeTotal.value })[activeTab.value] || 0);
+const todoCount = computed(() => todoTotal.value);
+const doneCount = computed(() => doneTotal.value);
+const noticeCount = computed(() => noticeTotal.value);
 
-const currentList = computed(() => (activeTab.value === "TODO" ? todoList.value : doneList.value));
-const todoCount = computed(() => todoList.value.length);
-const doneCount = computed(() => doneList.value.length);
-
-const filteredList = computed(() => {
-  return currentList.value.filter((item) => {
-    const publishTime = item.publishTime || "";
-    const [start, end] = queryParams.dateRange || [];
-    if (queryParams.warningName && !String(item.warningName || "").includes(queryParams.warningName)) return false;
-    if (queryParams.warningNo && !String(item.warningNo || "").includes(queryParams.warningNo)) return false;
-    if (queryParams.warningSpecial && !String(item.warningSpecial || "").includes(queryParams.warningSpecial)) return false;
-    if (queryParams.warningType && item.warningType !== queryParams.warningType) return false;
-    if (queryParams.warningLevel && item.warningLevel !== queryParams.warningLevel) return false;
-    if (queryParams.handler && !String(item.handler || "").includes(queryParams.handler)) return false;
-    if (queryParams.publisher && !String(item.publisher || "").includes(queryParams.publisher)) return false;
-    if (queryParams.location && !String(item.location || "").includes(queryParams.location)) return false;
-    if (queryParams.ownershipUnit && !String(item.ownershipUnit || "").includes(queryParams.ownershipUnit)) return false;
-    if (queryParams.alarmCode && !item.alarmCodeList.some((code) => String(code).includes(queryParams.alarmCode))) return false;
-    if (start && publishTime && publishTime < start) return false;
-    if (end && publishTime && publishTime > end) return false;
-    return true;
-  });
-});
+const pagedList = computed(() => currentList.value);
 
-const pagedList = computed(() => {
-  const start = (pagination.pageNum - 1) * pagination.pageSize;
-  return filteredList.value.slice(start, start + pagination.pageSize);
-});
+function getStatusText(status) {
+  return warningStatusLabels[status] || status || "-";
+}
+
+function getStatusTagType(status) {
+  if (status === "CLOSED") return "success";
+  if (status === "DRAFT" || status === "PENDING") return "info";
+  return "warning";
+}
 
 const detailBasic = computed(() => {
   const basic = detailData.value.basicInfo || {};
@@ -421,6 +370,10 @@ const detailBasic = computed(() => {
   };
 });
 
+function formatDateTime(value) {
+  return value ? parseTime(value) : "-";
+}
+
 function getLevelTagType(level) {
   return {
     "1": "danger",
@@ -430,81 +383,87 @@ function getLevelTagType(level) {
   }[level] || "info";
 }
 
-async function ensureUserName() {
-  if (userStore.name) {
-    return userStore.name;
-  }
-  await userStore.getInfo();
-  return userStore.name;
-}
-
 async function loadDicts() {
-  const [typeRes, levelRes, statusRes] = await Promise.all([
+  const [typeResult, levelResult] = await Promise.allSettled([
     getAlertTypeDictionaryData({ dictType: "alert_type" }),
-    getAlertTypeDictionaryData({ dictType: "alert_level" }),
-    getAlertTypeDictionaryData({ dictType: "pre_alarm_status" })
+    getAlertTypeDictionaryData({ dictType: "alert_level" })
   ]);
-  warningTypeOptions.value = typeRes.rows || [];
-  warningLevelOptions.value = levelRes.rows || [];
-  warningStatusOptions.value = statusRes.rows || [];
-}
-
-async function enrichTodoRows(records) {
-  const rows = await Promise.all(
-    (records || []).map(async (item) => {
-      const warningId = item.warningId || item.warning_id;
-      const detailRes = await getWarningDetailData(warningId);
-      const detail = detailRes.data || {};
-      const basic = detail.basicInfo || {};
-      const alarmList = detail.alarmList || [];
-      const warningType = basic.warningType || basic.warning_type || item.warning_type || "";
-      const warningLevel = basic.warningLevel || basic.warning_level || item.warning_level || "";
-      return {
-        todoId: item.todoId || item.todo_id,
-        warningId,
-        warningName: basic.warningName || basic.warning_name || item.warning_name || "",
-        warningNo: basic.warningNo || basic.warning_no || "",
-        warningSpecial: basic.warningSpecial || basic.warning_special || "",
-        warningType,
-        warningTypeText: warningTypeLabelMap.value[warningType] || warningType || "-",
-        warningLevel,
-        warningLevelText: warningLevelLabelMap.value[warningLevel] || warningLevel || "-",
-        handler: basic.handler || item.handler || "",
-        publisher: basic.publisher || item.publisher || "",
-        location: basic.location || item.location || "",
-        ownershipUnit: basic.ownershipUnit || basic.ownership_unit || item.ownership_unit || "",
-        publishTime: basic.publishTime || basic.publish_time || item.publish_time || "",
-        alarmCodeList: alarmList.map((alarm) => alarm.warning_code || alarm.warningCode || alarm.alarmId || alarm.alarm_id).filter(Boolean),
-        detail
-      };
-    })
-  );
-  return rows;
+  if (typeResult.status === "fulfilled") {
+    warningTypeOptions.value = typeResult.value.rows || [];
+  }
+  if (levelResult.status === "fulfilled") {
+    warningLevelOptions.value = levelResult.value.rows || [];
+  }
+}
+
+function enrichTodoRows(records) {
+  return (records || []).map(item => {
+    const warningType = item.warning_type || item.warningType || "";
+    const warningLevel = item.warning_level || item.warningLevel || "";
+    return {
+      ...item,
+      todoId: item.todo_id || item.todoId,
+      warningId: item.warning_id || item.warningId,
+      warningName: item.warning_name || item.warningName || "",
+      warningNo: item.warning_no || item.warningNo || "",
+      warningSpecial: item.warning_special || item.warningSpecial || "",
+      warningType,
+      warningTypeText: warningTypeLabelMap.value[warningType] || warningType || "-",
+      warningLevel,
+      warningLevelText: warningLevelLabelMap.value[warningLevel] || warningLevel || "-",
+      ownershipUnit: item.ownership_unit || item.ownershipUnit || "",
+      publishTime: item.publish_time || item.publishTime || "",
+      reviewer: item.reviewer || item.publisher || "",
+      alarmCodeList: String(item.alarm_numbers || item.alarmNumbers || "").split(",").filter(Boolean),
+      availableActions: item.availableActions || [],
+      todoType: item.todo_type || item.todoType,
+      actionType: item.action_type || item.actionType,
+      taskName: item.task_name || item.taskName || "",
+      readStatus: item.read_status || item.readStatus || ""
+    };
+  });
 }
 
 async function loadTodoList(type) {
-  const userName = await ensureUserName();
+  const [startTime, endTime] = queryParams.dateRange || [];
   const res = await getTodoListData({
-    pageNum: 1,
-    pageSize: 100,
-    userName,
+    pageNum: pagination.pageNum,
+    pageSize: pagination.pageSize,
     todoType: type,
-    warningName: queryParams.warningName || undefined
+    warningName: queryParams.warningName || undefined,
+    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,
+    location: queryParams.location || undefined,
+    ownershipUnit: queryParams.ownershipUnit || undefined,
+    startTime: startTime || undefined,
+    endTime: endTime || undefined
   });
-  const records = res.data?.records || res.data || [];
-  return enrichTodoRows(records);
+  const records = res.data?.records || [];
+  return { rows: enrichTodoRows(records), total: Number(res.data?.total || 0) };
 }
 
 async function loadCurrentTabData() {
   loading.value = true;
   try {
     if (activeTab.value === "TODO") {
-      todoList.value = await loadTodoList("TODO");
+      const result = await loadTodoList("TODO");
+      todoList.value = result.rows;
+      todoTotal.value = result.total;
     } else {
-      doneList.value = await loadTodoList("DONE");
+      const result = await loadTodoList(activeTab.value);
+      if (activeTab.value === "NOTICE") {
+        noticeList.value = result.rows;
+        noticeTotal.value = result.total;
+      } else {
+        doneList.value = result.rows;
+        doneTotal.value = result.total;
+      }
     }
-    pagination.pageNum = 1;
-    ElMessage.success("列表已刷新");
   } finally {
     loading.value = false;
   }
@@ -513,9 +472,19 @@ async function loadCurrentTabData() {
 async function preloadCounts() {
   loading.value = true;
   try {
-    const [todoRows, doneRows] = await Promise.all([loadTodoList("TODO"), loadTodoList("DONE")]);
-    todoList.value = todoRows;
-    doneList.value = doneRows;
+    const [todoResult, doneResult, noticeResult] = await Promise.allSettled([loadTodoList("TODO"), loadTodoList("DONE"), loadTodoList("NOTICE")]);
+    if (todoResult.status === "fulfilled") {
+      todoList.value = todoResult.value.rows;
+      todoTotal.value = todoResult.value.total;
+    }
+    if (doneResult.status === "fulfilled") {
+      doneList.value = doneResult.value.rows;
+      doneTotal.value = doneResult.value.total;
+    }
+    if (noticeResult.status === "fulfilled") {
+      noticeList.value = noticeResult.value.rows;
+      noticeTotal.value = noticeResult.value.total;
+    }
   } finally {
     loading.value = false;
   }
@@ -523,6 +492,7 @@ async function preloadCounts() {
 
 function handleQuery() {
   pagination.pageNum = 1;
+  refreshLists();
 }
 
 function resetQuery() {
@@ -538,26 +508,35 @@ function resetQuery() {
   queryParams.ownershipUnit = "";
   queryParams.dateRange = [];
   pagination.pageNum = 1;
+  refreshLists();
 }
 
 function handlePageChange() {
-  // pagination component sync only
+  loadCurrentTabData();
 }
 
 function handleTabChange() {
   pagination.pageNum = 1;
+  loadCurrentTabData();
 }
 
-function handleViewDetail(row) {
-  detailData.value = row.detail || {};
-  detailVisible.value = true;
+async function refreshLists() {
+  await preloadCounts();
+  if (pagination.pageNum > 1 && currentList.value.length === 0) {
+    pagination.pageNum = Math.max(1, Math.ceil(currentTotal.value / pagination.pageSize));
+    await preloadCounts();
+  }
 }
 
-function openUpgradeDialog(row) {
-  upgradeDialog.row = row;
-  upgradeDialog.newLevel = "";
-  upgradeDialog.disposalContent = "";
-  upgradeDialog.visible = true;
+async function handleViewDetail(row) {
+  const res = await getWarningDetailData(row.warningId);
+  detailData.value = res.data || {};
+  selectedNotice.value = activeTab.value === "NOTICE" ? row.taskName : "";
+  detailVisible.value = true;
+  if (activeTab.value === "NOTICE" && row.readStatus !== "READ") {
+    await readWarningNotification(row.todoId);
+    row.readStatus = "READ";
+  }
 }
 
 function openResolveDialog(row) {
@@ -572,34 +551,6 @@ function openReturnDialog(row) {
   returnDialog.visible = true;
 }
 
-async function confirmUpgrade() {
-  if (!upgradeDialog.newLevel) {
-    ElMessage.warning("请选择新预警级别");
-    return;
-  }
-  if (!upgradeDialog.disposalContent) {
-    ElMessage.warning("请输入处置说明");
-    return;
-  }
-  actionLoading.value = true;
-  try {
-    const res = await upgradeWarningData({
-      warningId: upgradeDialog.row.warningId,
-      newLevel: upgradeDialog.newLevel,
-      disposalContent: upgradeDialog.disposalContent
-    });
-    if (res.code === 200) {
-      ElMessage.success("升级成功");
-      upgradeDialog.visible = false;
-      await loadCurrentTabData();
-    } else {
-      ElMessage.error(res.msg || "升级失败");
-    }
-  } finally {
-    actionLoading.value = false;
-  }
-}
-
 async function confirmResolve() {
   if (!resolveDialog.disposalContent) {
     ElMessage.warning("请输入处置说明");
@@ -614,7 +565,7 @@ async function confirmResolve() {
     if (res.code === 200) {
       ElMessage.success("解除成功");
       resolveDialog.visible = false;
-      await loadCurrentTabData();
+      await refreshLists();
     } else {
       ElMessage.error(res.msg || "解除失败");
     }
@@ -637,7 +588,7 @@ async function confirmReturn() {
     if (res.code === 200) {
       ElMessage.success("退回成功");
       returnDialog.visible = false;
-      await loadCurrentTabData();
+      await refreshLists();
     } else {
       ElMessage.error(res.msg || "退回失败");
     }

+ 4 - 5
src/views/subSystem/lifeCompany/lOverviewWarning/dashboardShared.js

@@ -19,11 +19,10 @@ export const warningLevelLabels = {
 export const warningStatusLabels = {
   DRAFT: '草稿',
   PENDING: '待处置',
+  RECEIVED: '已接收',
   PROCESSING: '处置中',
-  RELEASED: '已发布',
   HANDLED: '已处置',
-  CLOSED: '已解除',
-  RESOLVED: '已解除'
+  CLOSED: '已解除'
 }
 
 export function dashboardData(response, fallback = {}) {
@@ -86,8 +85,8 @@ export function warningLevelTag(row) {
 
 export function warningStatusTag(row) {
   const value = String(listValue(row, 'status', 'warningStatus', 'warning_status')).toUpperCase()
-  if (['CLOSED', 'RESOLVED', 'HANDLED'].includes(value)) return 'success'
-  if (['PENDING', 'PROCESSING'].includes(value)) return 'warning'
+  if (['CLOSED', 'HANDLED'].includes(value)) return 'success'
+  if (['PENDING', 'RECEIVED', 'PROCESSING'].includes(value)) return 'warning'
   if (value === 'DRAFT') return 'info'
   return 'primary'
 }

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

@@ -407,8 +407,8 @@ function renderStatusChart() {
     return
   }
   const colorByStatus = {
-    PENDING: '#dc2626', PROCESSING: '#d97706', RELEASED: '#2563eb',
-    HANDLED: '#0f766e', CLOSED: '#059669', RESOLVED: '#059669'
+    PENDING: '#dc2626', RECEIVED: '#2563eb', PROCESSING: '#d97706',
+    HANDLED: '#0f766e', CLOSED: '#059669'
   }
   statusChart.setOption({
     tooltip: { trigger: 'item', formatter: '{b}<br/>{c} 条({d}%)' },

+ 2 - 3
src/views/subSystem/lifeCompany/lstatistics/statisticsShared.js

@@ -26,11 +26,10 @@ export const warningTypeOptions = [
 export const statusLabels = {
   DRAFT: '草稿',
   PENDING: '待处置',
+  RECEIVED: '已接收',
   PROCESSING: '处置中',
-  RELEASED: '已发布',
   HANDLED: '已处置',
-  CLOSED: '已解除',
-  RESOLVED: '已解除'
+  CLOSED: '已解除'
 }
 
 export const disposalTypeLabels = {

+ 3 - 3
src/views/subSystem/lifeCompany/lwarningInfoCont/LAlertQuery.vue

@@ -58,7 +58,7 @@
         </el-form-item>
         <el-form-item label="状态">
           <el-select v-model="queryParams.status" clearable placeholder="请选择状态" style="width: 180px">
-            <el-option label="已发布" value="RELEASED" />
+            <el-option label="已接收" value="RECEIVED" />
             <el-option label="待处理" value="PENDING" />
             <el-option label="处理中" value="PROCESSING" />
             <el-option label="已处理" value="HANDLED" />
@@ -379,7 +379,7 @@ function getLevelTagType(level) {
 function getStatusText(status) {
   return {
     DRAFT: "草稿",
-    RELEASED: "已发布",
+    RECEIVED: "已接收",
     PENDING: "待处理",
     PROCESSING: "处理中",
     HANDLED: "已处理",
@@ -391,7 +391,7 @@ function getStatusText(status) {
 function getStatusTagType(status) {
   return {
     DRAFT: "info",
-    RELEASED: "success",
+    RECEIVED: "warning",
     PENDING: "warning",
     PROCESSING: "warning",
     HANDLED: "success",

+ 97 - 143
src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue

@@ -3,36 +3,12 @@
     <el-card shadow="never" class="page-card header-card">
       <div class="page-header">
         <div>
-          <div class="page-title">预警信息处置</div>
-          <div class="page-subtitle">支持预警解除、预警升级、预警督办、添加领导批示和预警退回,形成发现问题到处置问题的闭环管理</div>
+          <div class="page-title">预警处置</div>
         </div>
         <el-button type="primary" icon="Refresh" @click="loadList">刷新</el-button>
       </div>
     </el-card>
 
-    <div class="action-grid">
-      <el-card shadow="hover" class="action-card resolve">
-        <div class="action-title">预警解除</div>
-        <div class="action-desc">现场处置核查无误后解除预警,形成处置闭环。</div>
-      </el-card>
-      <el-card shadow="hover" class="action-card upgrade">
-        <div class="action-title">预警升级</div>
-        <div class="action-desc">结合地理位置、人口密度、建筑密度等因素调整预警级别。</div>
-      </el-card>
-      <el-card shadow="hover" class="action-card supervise">
-        <div class="action-title">预警督办</div>
-        <div class="action-desc">通过督办人员、督办意见、通知方式和通知内容进行提醒。</div>
-      </el-card>
-      <el-card shadow="hover" class="action-card instruction">
-        <div class="action-title">添加领导批示</div>
-        <div class="action-desc">向相关领导发送预警信息并记录领导批示指导意见。</div>
-      </el-card>
-      <el-card shadow="hover" class="action-card return">
-        <div class="action-title">预警退回</div>
-        <div class="action-desc">现场仍有异常时退回处置单位重新复核和处置。</div>
-      </el-card>
-    </div>
-
     <el-card shadow="never" class="page-card">
       <el-form :model="queryParams" :inline="true">
         <el-form-item label="预警名称">
@@ -63,12 +39,11 @@
         </el-form-item>
         <el-form-item label="状态">
           <el-select v-model="queryParams.status" clearable placeholder="请选择状态" style="width: 180px">
-            <el-option label="已发布" value="RELEASED" />
-            <el-option label="待处理" value="PENDING" />
+            <el-option label="已接收" value="RECEIVED" />
+            <el-option label="已发布(待接收)" value="PENDING" />
             <el-option label="处理中" value="PROCESSING" />
-            <el-option label="已处理" value="HANDLED" />
+            <el-option label="已处置(待核查)" value="HANDLED" />
             <el-option label="已解除" value="CLOSED" />
-            <el-option label="已退回" value="RETURNED" />
           </el-select>
         </el-form-item>
         <el-form-item label="处置人">
@@ -125,15 +100,17 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="发布时间" prop="publishTime" width="180" />
-        <el-table-column label="操作" fixed="right" width="360" align="center">
+        <el-table-column label="发布时间" width="180">
+          <template #default="scope">{{ formatDateTime(scope.row.publishTime) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" width="330" align="center">
           <template #default="scope">
             <el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
-            <el-button link type="success" @click="openResolve(scope.row)">解除</el-button>
-            <el-button link type="warning" @click="openUpgrade(scope.row)">升级</el-button>
-            <el-button link type="primary" @click="openSupervision(scope.row)">督办</el-button>
-            <el-button link type="info" @click="openInstruction(scope.row)">批示</el-button>
-            <el-button link type="danger" @click="openReturn(scope.row)">退回</el-button>
+            <el-button v-if="canReview(scope.row)" v-hasPermi="['warning:warning:handle']" link type="success" @click="openResolve(scope.row)">核查解除</el-button>
+            <el-button v-if="canUpgrade(scope.row)" v-hasPermi="['warning:warning:handle']" link type="warning" @click="openUpgrade(scope.row)">升级</el-button>
+            <el-button v-if="canManage(scope.row)" v-hasPermi="['warning:warning:supervision']" link type="primary" @click="openSupervision(scope.row)">督办</el-button>
+            <el-button v-if="canManage(scope.row)" v-hasPermi="['warning:warning:instruction']" link type="info" @click="openInstruction(scope.row)">批示</el-button>
+            <el-button v-if="canReview(scope.row)" v-hasPermi="['warning:warning:handle']" link type="danger" @click="openReturn(scope.row)">退回</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -160,9 +137,10 @@
           <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="报警位置" :span="2">{{ detailBasic.location || "-" }}</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="发布时间">{{ formatDateTime(detailBasic.publishTime) }}</el-descriptions-item>
           <el-descriptions-item label="预警内容" :span="2">{{ detailBasic.warningContent || "-" }}</el-descriptions-item>
         </el-descriptions>
 
@@ -171,9 +149,12 @@
           <el-timeline-item
             v-for="(item, index) in detailData.disposalHistory"
             :key="index"
-            :timestamp="item.createTime || item.disposalTime || '-'"
+            :timestamp="formatDateTime(item.createTime || item.create_time || item.disposalTime || item.disposal_time)"
           >
-            {{ item.disposalContent || item.content || "-" }}
+            {{ 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>
           </el-timeline-item>
         </el-timeline>
         <el-empty v-else description="暂无处置记录" />
@@ -203,7 +184,7 @@
         <el-form-item label="新预警级别">
           <el-select v-model="upgradeForm.newLevel" placeholder="请选择新预警级别" style="width: 100%">
             <el-option
-              v-for="item in warningLevelOptions"
+              v-for="item in upgradeLevelOptions"
               :key="item.dictValue"
               :label="item.dictLabel"
               :value="item.dictValue"
@@ -223,7 +204,9 @@
     <el-dialog v-model="supervisionVisible" title="预警督办" width="620px" append-to-body>
       <el-form label-width="100px">
         <el-form-item label="督办人员">
-          <el-input v-model="supervisionForm.supervisionUser" placeholder="请输入督办人员" />
+          <el-select v-model="supervisionForm.supervisionUser" filterable placeholder="请选择督办人员" style="width: 100%" :loading="usersLoading">
+            <el-option v-for="user in enabledUsers" :key="user.userId" :label="`${user.nickName || user.userName} (${user.userName})`" :value="user.userName" />
+          </el-select>
         </el-form-item>
         <el-form-item label="督办意见">
           <el-input v-model="supervisionForm.supervisionContent" type="textarea" :rows="3" placeholder="请输入督办意见" />
@@ -231,16 +214,17 @@
         <el-form-item label="通知方式">
           <el-select v-model="supervisionForm.notificationType" placeholder="请选择通知方式" style="width: 100%">
             <el-option label="短信" value="短信" />
-            <el-option label="电话" value="电话" />
             <el-option label="系统通知" value="系统通知" />
-            <el-option label="邮件" value="邮件" />
           </el-select>
         </el-form-item>
         <el-form-item label="通知内容">
-          <el-input v-model="supervisionForm.notificationContent" type="textarea" :rows="3" placeholder="请输入通知内容" />
+          <el-input v-model="supervisionForm.notificationContent" type="textarea" :rows="3" maxlength="180" show-word-limit placeholder="不填时使用督办意见" />
         </el-form-item>
-        <el-form-item label="附件地址">
-          <el-input v-model="supervisionForm.attachmentUrl" placeholder="请输入附件地址" />
+        <el-form-item label="附件">
+          <el-upload v-model:file-list="supervisionFiles" :auto-upload="false" :limit="1" :on-exceed="() => ElMessage.warning('最多上传一个附件')" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.xls,.xlsx,.doc,.docx">
+            <el-button :icon="Upload">选取文件</el-button>
+            <template #tip><div class="el-upload__tip">图片、PDF、Excel 或 Word,最大 20 MB</div></template>
+          </el-upload>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -257,9 +241,6 @@
         <el-form-item label="批示内容">
           <el-input v-model="instructionForm.instructionContent" type="textarea" :rows="4" placeholder="请输入领导批示内容" />
         </el-form-item>
-        <el-form-item label="短信接收人">
-          <el-input v-model="instructionForm.smsReceivers" placeholder="请输入短信接收人,多个用逗号分隔" />
-        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="instructionVisible = false">取 消</el-button>
@@ -287,6 +268,11 @@
 <script setup>
 import { computed, onMounted, reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
+import { Upload } from "@element-plus/icons-vue";
+import { parseTime } from "@/utils/ruoyi";
+import useUserStore from "@/store/modules/user";
+import { enabledAll } from "@/api/system/user";
+import { warningStatusLabels } from "../warningWorkflow";
 import {
   getAlertTypeDictionaryData,
   getWarningDetailData,
@@ -299,7 +285,11 @@ import {
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
 const loading = ref(false);
+const userStore = useUserStore();
 const submitLoading = ref(false);
+const usersLoading = ref(false);
+const enabledUsers = ref([]);
+const supervisionFiles = ref([]);
 const warningList = ref([]);
 const total = ref(0);
 const warningTypeOptions = ref([]);
@@ -340,14 +330,12 @@ const supervisionForm = reactive({
   supervisionUser: "",
   supervisionContent: "",
   notificationType: "",
-  notificationContent: "",
-  attachmentUrl: ""
+  notificationContent: ""
 });
 
 const instructionForm = reactive({
   leaderName: "",
-  instructionContent: "",
-  smsReceivers: ""
+  instructionContent: ""
 });
 
 const returnForm = reactive({
@@ -368,6 +356,20 @@ const warningLevelLabelMap = computed(() => {
   }, {});
 });
 
+const upgradeLevelOptions = computed(() => warningLevelOptions.value.filter(
+  item => Number(item.dictValue) < Number(currentRow.value?.warningLevel)
+));
+
+const canManage = row => ["PENDING", "RECEIVED", "PROCESSING", "HANDLED"].includes(row.status);
+const canUpgrade = row => canManage(row) && Number(row.warningLevel) > 1 && Number(row.warningLevel) <= 4;
+const canReview = row => row.status === "HANDLED" && (row.reviewer || row.publisher) === userStore.name;
+
+function formatDateTime(value) {
+  if (!value) return "-";
+  const date = new Date(value);
+  return parseTime(Number.isNaN(date.getTime()) ? value : date) || "-";
+}
+
 const detailBasic = computed(() => {
   const basic = detailData.value.basicInfo || {};
   const warningType = basic.warningType || basic.warning_type || "";
@@ -383,6 +385,7 @@ const detailBasic = computed(() => {
     deviceCode: basic.deviceCode || basic.device_code || "",
     deviceName: basic.deviceName || basic.device_name || "",
     publisher: basic.publisher || "",
+    reviewer: basic.reviewer || "",
     location: basic.location || "",
     ownershipUnit: basic.ownershipUnit || basic.ownership_unit || "",
     publishTime: basic.publishTime || basic.publish_time || "",
@@ -400,21 +403,13 @@ function getLevelTagType(level) {
 }
 
 function getStatusText(status) {
-  return {
-    DRAFT: "草稿",
-    RELEASED: "已发布",
-    PENDING: "待处理",
-    PROCESSING: "处理中",
-    HANDLED: "已处理",
-    CLOSED: "已解除",
-    RETURNED: "已退回"
-  }[status] || status || "-";
+  return warningStatusLabels[status] || status || "-";
 }
 
 function getStatusTagType(status) {
   return {
     DRAFT: "info",
-    RELEASED: "success",
+    RECEIVED: "warning",
     PENDING: "warning",
     PROCESSING: "warning",
     HANDLED: "success",
@@ -438,6 +433,7 @@ function normalizeWarningRow(item) {
     deviceCode: item.deviceCode || item.device_code || "",
     deviceName: item.deviceName || item.device_name || "",
     publisher: item.publisher || "",
+    reviewer: item.reviewer || "",
     location: item.location || "",
     ownershipUnit: item.ownershipUnit || item.ownership_unit || "",
     publishTime: item.publishTime || item.publish_time || "",
@@ -453,17 +449,16 @@ function resetActionForms() {
   supervisionForm.supervisionContent = "";
   supervisionForm.notificationType = "";
   supervisionForm.notificationContent = "";
-  supervisionForm.attachmentUrl = "";
+  supervisionFiles.value = [];
   instructionForm.leaderName = "";
   instructionForm.instructionContent = "";
-  instructionForm.smsReceivers = "";
   returnForm.disposalContent = "";
 }
 
 async function loadDicts() {
   const [typeRes, levelRes] = await Promise.all([
-    getAlertTypeDictionaryData({ dictType: "warning_type" }),
-    getAlertTypeDictionaryData({ dictType: "warning_level" })
+    getAlertTypeDictionaryData({ dictType: "alert_type" }),
+    getAlertTypeDictionaryData({ dictType: "alert_level" })
   ]);
   warningTypeOptions.value = typeRes.rows || [];
   warningLevelOptions.value = levelRes.rows || [];
@@ -526,7 +521,6 @@ function openResolve(row) {
 function openUpgrade(row) {
   currentRow.value = row;
   resetActionForms();
-  upgradeForm.newLevel = row.warningLevel;
   upgradeVisible.value = true;
 }
 
@@ -534,6 +528,14 @@ function openSupervision(row) {
   currentRow.value = row;
   resetActionForms();
   supervisionVisible.value = true;
+  usersLoading.value = true;
+  enabledAll().then(res => {
+    enabledUsers.value = res.data || [];
+  }).catch(() => {
+    ElMessage.error("督办人员加载失败");
+  }).finally(() => {
+    usersLoading.value = false;
+  });
 }
 
 function openInstruction(row) {
@@ -580,6 +582,10 @@ async function submitUpgrade() {
     ElMessage.warning("请输入升级说明");
     return;
   }
+  if (!canUpgrade(currentRow.value) || Number(upgradeForm.newLevel) >= Number(currentRow.value.warningLevel)) {
+    ElMessage.warning("只能升级至更严重的级别");
+    return;
+  }
   submitLoading.value = true;
   try {
     const res = await upgradeWarningData({
@@ -604,21 +610,34 @@ async function submitSupervision() {
     ElMessage.warning("请完善督办信息");
     return;
   }
+  if ((supervisionForm.notificationContent.trim() || supervisionForm.supervisionContent.trim()).length > 180) {
+    ElMessage.warning("通知内容不能超过 180 字");
+    return;
+  }
+  const attachment = supervisionFiles.value[0]?.raw;
+  if (attachment && attachment.size > 20 * 1024 * 1024) {
+    ElMessage.warning("附件不能超过 20 MB");
+    return;
+  }
+  const data = new FormData();
+  data.append("warningId", currentRow.value.warningId);
+  data.append("supervisionUser", supervisionForm.supervisionUser);
+  data.append("supervisionContent", supervisionForm.supervisionContent);
+  data.append("notificationType", supervisionForm.notificationType);
+  data.append("notificationContent", supervisionForm.notificationContent);
+  if (attachment) data.append("attachment", attachment);
   submitLoading.value = true;
   try {
-    const res = await supervisionWarningData({
-      warningId: currentRow.value.warningId,
-      supervisionUser: supervisionForm.supervisionUser,
-      supervisionContent: supervisionForm.supervisionContent,
-      notificationType: supervisionForm.notificationType,
-      notificationContent: supervisionForm.notificationContent,
-      attachmentUrl: supervisionForm.attachmentUrl
-    });
+    const res = await supervisionWarningData(data);
     if (res.code !== 200) {
       ElMessage.error(res.msg || "督办失败");
       return;
     }
-    ElMessage.success("督办成功");
+    if (res.data?.notificationStatus === "FAILED") {
+      ElMessage.warning("督办已保存,但短信发送失败,请联系管理员核查,勿重复提交");
+    } else {
+      ElMessage.success(res.msg || "督办通知已发送");
+    }
     supervisionVisible.value = false;
     loadList();
   } finally {
@@ -636,8 +655,7 @@ async function submitInstruction() {
     const res = await instructionWarningData({
       warningId: currentRow.value.warningId,
       leaderName: instructionForm.leaderName,
-      instructionContent: instructionForm.instructionContent,
-      smsReceivers: instructionForm.smsReceivers
+      instructionContent: instructionForm.instructionContent
     });
     if (res.code !== 200) {
       ElMessage.error(res.msg || "批示失败");
@@ -708,71 +726,7 @@ onMounted(async () => {
   color: #1e3a8a;
 }
 
-.page-subtitle {
-  margin-top: 8px;
-  font-size: 13px;
-  color: #64748b;
-}
-
-.action-grid {
-  display: grid;
-  grid-template-columns: repeat(5, minmax(0, 1fr));
-  gap: 16px;
-  margin-bottom: 16px;
-}
-
-.action-card {
-  border: none;
-  color: #fff;
-}
-
-.action-card :deep(.el-card__body) {
-  padding: 18px;
-}
-
-.action-card.resolve {
-  background: linear-gradient(135deg, #059669, #34d399);
-}
-
-.action-card.upgrade {
-  background: linear-gradient(135deg, #d97706, #f7b955);
-}
-
-.action-card.supervise {
-  background: linear-gradient(135deg, #2563eb, #5b93ff);
-}
-
-.action-card.instruction {
-  background: linear-gradient(135deg, #7c3aed, #9d72ff);
-}
-
-.action-card.return {
-  background: linear-gradient(135deg, #dc2626, #f87171);
-}
-
-.action-title {
-  font-size: 16px;
-  font-weight: 700;
-}
-
-.action-desc {
-  margin-top: 10px;
-  font-size: 12px;
-  line-height: 1.7;
-  opacity: 0.95;
-}
-
-@media (max-width: 1400px) {
-  .action-grid {
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-  }
-}
-
 @media (max-width: 768px) {
-  .action-grid {
-    grid-template-columns: 1fr;
-  }
-
   .page-header {
     flex-direction: column;
     align-items: flex-start;

File diff suppressed because it is too large
+ 0 - 0
src/views/subSystem/lifeCompany/warningThreshold/index.vue


+ 19 - 0
src/views/subSystem/lifeCompany/warningWorkflow.js

@@ -0,0 +1,19 @@
+// 处置完成仍需核查;退回进入处置中,只有 CLOSED 是终态。
+export const warningStatusLabels = {
+  DRAFT: '草稿', PENDING: '已发布(待接收)', RECEIVED: '已接收',
+  PROCESSING: '处置中', HANDLED: '已处置(待核查)', CLOSED: '已解除'
+}
+
+export function workflowActions(row, username) {
+  if (row?.todo_type === 'DONE' || row?.todoType === 'DONE') return []
+  if (Array.isArray(row?.availableActions)) return row.availableActions
+  const actions = []
+  if (row?.handler === username) {
+    if (row.status === 'PENDING') actions.push('RECEIVE')
+    if (row.status === 'RECEIVED') actions.push('START')
+    if (row.status === 'PROCESSING') actions.push('HANDLE')
+  }
+  if (row?.status === 'HANDLED' && (row.reviewer || row.publisher) === username) actions.push('CLEAR', 'RETURN')
+  if (['PENDING', 'RECEIVED', 'PROCESSING', 'HANDLED'].includes(row?.status)) actions.push('UPGRADE', 'SUPERVISION', 'INSTRUCTION')
+  return actions
+}

+ 58 - 17
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue

@@ -1,6 +1,6 @@
 <!--预警管理页面(RuoYi Element Plus 标准)-->
 <!--数据源:early_warning 体系 /warning/*接口 -->
-<!--确认异常后只建工单不派单,由工单系统处理后续流程-->
+<!--接收后只建工单不派单,由工单系统处理后续流程-->
 
 <template>
   <div class="warning-container">
@@ -71,12 +71,12 @@
         </el-form-item>
         <el-form-item label="状态">
           <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width:150px">
-            <el-option label="待确认" value="RELEASED"/>
+            <el-option label="草稿" value="DRAFT"/>
             <el-option label="待办" value="PENDING"/>
+            <el-option label="已接收" value="RECEIVED"/>
             <el-option label="处理中" value="PROCESSING"/>
             <el-option label="已处置" value="HANDLED"/>
             <el-option label="已解除" value="CLOSED"/>
-            <el-option label="草稿" value="DRAFT"/>
           </el-select>
         </el-form-item>
         <el-form-item label="级别">
@@ -140,8 +140,9 @@
         <el-table-column prop="publishTime" label="发布时间" width="170" sortable class-name="publish-time-column" :formatter="formatPublishTime" />
         <el-table-column label="操作" width="180" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="isUnconfirmed(row.status)" type="primary" size="small" @click="openConfirm(row)">确认异常</el-button>
-            <el-button v-if="isUnconfirmed(row.status)" type="info" size="small" @click="confirmMisreport(row)">误报</el-button>
+            <el-button v-if="isPending(row.status)" type="primary" size="small" @click="openConfirm(row)">接收</el-button>
+            <el-button v-if="isPending(row.status)" type="info" size="small" @click="confirmMisreport(row)">误报</el-button>
+            <el-button v-if="isReceived(row.status)" type="primary" size="small" @click="openStart(row)">开始处置</el-button>
             <el-button v-if="row.status === 'PROCESSING'" type="warning" size="small" @click="openProcess(row)">提交处理</el-button>
             <el-button v-if="row.status === 'HANDLED'" type="success" size="small" @click="clearHandled(row)">清除预警</el-button>
             <el-button type="info" size="small" @click="openDetail(row.warningId)">详情</el-button>
@@ -152,8 +153,8 @@
                      @change="loadList" style="margin-top:16px;justify-content:flex-end" />
     </el-card>
 
-    <!-- ========== 确认异常弹窗 ========== -->
-    <el-dialog v-model="confirmVisible" title="⚠️ 确认预警异常" width="600px" append-to-body destroy-on-close>
+    <!-- ========== 接收预警弹窗 ========== -->
+    <el-dialog v-model="confirmVisible" title="接收预警" width="600px" append-to-body destroy-on-close>
       <el-descriptions :column="1" border>
         <el-descriptions-item label="预警编号">{{ current.warningNo }}</el-descriptions-item>
         <el-descriptions-item label="预警名称">{{ current.warningName }}</el-descriptions-item>
@@ -175,6 +176,23 @@
       </template>
     </el-dialog>
 
+    <el-dialog v-model="startVisible" title="开始处置" width="560px" append-to-body destroy-on-close>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="预警编号">{{ startRow.warningNo }}</el-descriptions-item>
+        <el-descriptions-item label="预警名称">{{ startRow.warningName }}</el-descriptions-item>
+        <el-descriptions-item label="关联设备">{{ formatWarningDevice(startRow) }}</el-descriptions-item>
+      </el-descriptions>
+      <el-form label-width="100px" style="margin-top:16px">
+        <el-form-item label="处置说明">
+          <el-input v-model="startRemark" type="textarea" :rows="3" placeholder="填写开始处置的说明(可选)" maxlength="300" show-word-limit/>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="startVisible=false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitStart">确认开始</el-button>
+      </template>
+    </el-dialog>
+
     <el-dialog v-model="processVisible" title="提交预警处理反馈" width="620px" append-to-body destroy-on-close>
       <el-descriptions :column="1" border>
         <el-descriptions-item label="预警编号">{{ processRow.warningNo }}</el-descriptions-item>
@@ -279,7 +297,7 @@ import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox } 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, misreportWarningData, submitWarningProcessData, clearWarningData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
+import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, confirmWarningData, startWarningProcessData, misreportWarningData, submitWarningProcessData, clearWarningData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
 import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
 
 // ========== 统计数据 ==========
@@ -497,12 +515,13 @@ function renderTypeChart() {
 }
 
 // ========== 辅助函数 ==========
-const isUnconfirmed = (status) => ['RELEASED','PENDING'].includes(status)
-const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待办', PROCESSING:'处理中', RELEASED:'待确认', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
-const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', PROCESSING:'warning', RELEASED:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
+const isPending = (status) => status === 'PENDING'
+const isReceived = (status) => status === 'RECEIVED'
+const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待办', RECEIVED:'已接收', PROCESSING:'处理中', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
+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:'发布预警', CONFIRM:'确认异常', MISREPORT:'误报清除', HANDLE:'提交处理反馈', CLEAR:'清除预警', UPGRADE:'升级预警', RESOLVE:'解除预警', RETURN:'退回重办', SUPERVISION:'督办', INSTRUCTION:'批示' }[t]||t)
+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 || '')
@@ -562,7 +581,7 @@ async function downloadAttachment(attachment) {
 function handleSearch() { pageNum.value=1; loadList() }
 function handleReset() { queryParams.value={warningName:'',deviceCode:'',deviceName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; pageNum.value=1; loadList() }
 
-// ========== 确认异常 ==========
+// ========== 接收预警 ==========
 const confirmVisible = ref(false)
 const current = ref({})
 const confirmingRemark = ref('')
@@ -577,22 +596,44 @@ function openConfirm(row) {
 }
 
 async function submitConfirm() {
-  if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写异常描述'); return }
+  if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写接收说明'); return }
   
   submitting.value = true
   try {
     await confirmWarningData({ warningId: current.value.warningId, assignedUser: assignedUser.value, confirmRemark: confirmingRemark.value })
-    const orderDesc = `预警「${current.value.warningName}」异常确认;${confirmingRemark.value}`
+    const orderDesc = `预警「${current.value.warningName}」已接收;${confirmingRemark.value}`
     await addManholeWorkOrder({ alarmId: current.value.warningId, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
-    ElMessage.success(`✅ 预警已确认 | 工单已生成 (WO-...) | 请前往工单系统继续处理`)
+    ElMessage.success('预警已接收')
     confirmVisible.value = false
     loadList()
-  } catch(e) { ElMessage.error('确认失败:'+(e.msg||e.message||'重试')); console.error(e) }
+  } catch(e) { ElMessage.error('接收失败:'+(e.msg||e.message||'重试')); console.error(e) }
   finally { submitting.value=false }
 }
 
 function getWorkOrderLevel(warningLevel) { const lv = Number(warningLevel); return lv>=3?1:(lv===2?2:3) }
 
+// ========== 开始处置 ==========
+const startVisible = ref(false)
+const startRow = ref({})
+const startRemark = ref('')
+
+function openStart(row) {
+  startRow.value = row
+  startRemark.value = ''
+  startVisible.value = true
+}
+
+async function submitStart() {
+  submitting.value = true
+  try {
+    await startWarningProcessData({ warningId: startRow.value.warningId, remark: startRemark.value })
+    ElMessage.success('已开始处置')
+    startVisible.value = false
+    loadList()
+  } catch(e) { ElMessage.error('开始处置失败:'+(e.msg||e.message||'重试')); console.error(e) }
+  finally { submitting.value=false }
+}
+
 // ========== 误报处理 ==========
 function confirmMisreport(row) {
   ElMessageBox.prompt('请输入误报原因', '误报确认', {

Some files were not shown because too many files changed in this diff