فهرست منبع

Merge remote-tracking branch 'origin/master'

null 5 روز پیش
والد
کامیت
bee50bf8af

+ 2 - 0
memory/index.md

@@ -34,6 +34,8 @@ npm run dev       # 默认 http://localhost:80
 
 ## 当前已知风险
 
+- 预警查看权限已统一为 `warning:warning:query`;部署管理后端时执行 `pipe-network-service/sql/migration_warning_query_permission.sql`,将原 list 菜单权限改为 query,保留角色关联并重新登录。脚本尚未在目标数据库执行(2026-10-15)。
+- 预警核验已拆分为 `warning:warning:audit`;同步部署前后端时执行后端 `sql/migration_warning_audit_permission.sql`,在角色管理中单独授权管理角色并重新登录。脚本尚未在目标数据库执行,不自动向维修处置角色授予核验权限(2026-10-15)。
 - 权限管理系统菜单迁移脚本已生成但尚未在目标 KingbaseES 执行;部署 PC 前端后执行 `pipe-network-service/sql/migration_permission_management_subsystem.sql`,重新登录以刷新动态路由(2026-10-15)。
 - 预警清单已接入发布和处置:可选事项、发布配置副本、三环节时限、处置原因多选。开发库已执行 `pipe-ner-server/pipe-network-service/sql/migration_warning_catalog_binding_kingbase.sql`,清单菜单及授权已核对,无菜单变更;主服务重启后页面闭环通过,测试数据清理完毕。采集自动关联仍需使用新版共享模块重新打包、重启并真实联调,详见后端 `docs/warning-catalog-binding-2026-09-29.md`。
 - 供水报警管理 #66 恢复独立只读分析页(流水、四级趋势、分类占比、台账),供水菜单按 rootTypeId=1 路由到该页;后端统一报警接口需同步部署设备报警状态及四态展示状态筛选,不能仅部署前端。

+ 2 - 0
memory/long-term.md

@@ -101,6 +101,8 @@
 
 ## 产品级决策
 
+- 预警列表与详情查看权限统一为 `warning:warning:query`,不再使用 `warning:warning:list`;列表、待办、搜索、跟踪、归档和通知已读同步,现有详情、附件、流程与统计沿用 query。菜单迁移仅更新旧权限值,保留菜单 ID 和角色关联,处置 handle、核验 audit 独立不变(2026-10-15)。
+- 预警维修接收、开始和提交处置使用 `warning:warning:handle`;管理核验解除/退回使用独立的 `warning:warning:audit`,包括 `/clear`、`/resolve`、`/return`、`/misreport`。仍须 HANDLED 且为指定 reviewer(历史为空回退 publisher),权限不代替人员和状态限制。PC 待办、预警处置和管网预警入口统一为“核验”表单,必选“核查解除/退回办理”、必填核验意见;退回仍由原处置人办理。迁移脚本只新增菜单,不从处置权限自动继承核验权限(2026-10-15)。
 - 预警联动系统事件六状态统一颜色:DRAFT 草稿灰、PENDING 待接收黄、RECEIVED 已接收蓝、PROCESSING 处置中橙、HANDLED 已处置紫、CLOSED 已解除绿。所有事件状态标签和统计图表复用 `lifeCompany/warningStatus.js`,禁止各页面按数据位置自行分配状态色(2026-10-15)。
 - 预警事项关联为可选项,允许人工新增或采集生成的预警不关联事项直接发布;缺少匹配事项不能阻断草稿留存或发布(2026-09-29 用户明确确认)。
 - 预警清单和系统预警的等级下拉统一为一至四级,共用 `alert_level`;原因库和编码生成同步使用该字典。2026-09-29 开发库已补齐原缺少的四级,现有等级标签保留。

+ 26 - 0
memory/sessions/2026-10-15.md

@@ -51,3 +51,29 @@
 ## 忘记密码提示
 
 - 登录页“忘记密码?”弹窗提示统一为“请联系平台管理员”;保留现有弹窗与确认按钮,并补齐 `ElMessageBox` 显式导入。
+
+## 阈值配置父目录路径
+
+- 根据用户截图,菜单 2191(燃气)和 2193(排水)的“阈值配置”父目录原 path 为数字 1/2,统一改为 `thresholdConfig`;两个目录属于不同子系统,子菜单 `warningThreshold`、组件及权限不变。
+- 后端 `sql/migration_equipment_warning_threshold_config.sql` 同步路径修正;前端未发现数字路径硬编码,无需修改路由代码。提供按 menu_id 更新的 SQL,由用户在目标库执行后重新登录;本次未操作数据库。
+
+## 预警处置与核验权限讨论(未实施)
+
+- 用户提出将维修处置 `warning:warning:handle` 与管理核验 `warning:warning:audit` 拆分,并将预警待办的解除/退回入口合为“核验”表单。
+- 核对发现:待办实际按钮在 `WarningWorkflowActions.vue`,通过 `/clear` 执行通过、`/return` 执行退回;`/resolve` 委托同一 clear Service,`/misreport` 也调用 clear。核验接口目前均为 handle 权限,需要一并对齐,避免旧入口保留处置权限。
+- Service 已要求 HANDLED 状态及指定 reviewer(为空回退 publisher)才能解除/退回。建议保留此限制,核验表单必选结果、必填意见,按结果调用既有接口,其他 PC 入口与菜单授权同步。仅做代码核对和方案讨论,业务代码未修改。
+
+## 预警处置与核验权限实施
+
+- 用户确认按上述方案实施;Controller 的 `/clear`、`/resolve`、`/return`、`/misreport` 改用 `warning:warning:audit`,接收/开始/提交仍使用 handle,升级权限保持原样。
+- Service 同步校验处置/核验权限并过滤可执行操作;保留指定人员、HANDLED 状态、必填意见限制和原有处置闭环。
+- 新增共用 `WarningReviewAction.vue`:待办页、预警处置页、管网预警页统一“核验”入口,必选核验结果(核查解除/退回办理)、必填意见,不预选结果;提交中禁止重复请求和关闭,成功刷新列表。清理旧解除/退回弹窗及管网页不符合现有状态机的待接收误报入口。
+- 新增后端 `sql/migration_warning_audit_permission.sql`,为相关菜单添加“预警核验”按钮权限,可重复执行,不自动继承 handle 角色授权;本次未执行数据库脚本,部署需同步前后端、执行脚本、单独为管理角色授权并重新登录。
+- 验证:前端核验及附件行为测试 8 项通过;后端权限/状态/指定核验人/解除/原处置人退回等 8 项测试通过,Maven 相关模块构建成功;前端生产构建通过(既有 Sass 弃用警告)。未执行真实数据库或登录环境联调。
+
+## 预警查看权限统一
+
+- 用户确认列表与详情权限统一命名为 `warning:warning:query`。`EarlyWarningController` 中列表、待办、搜索、处置跟踪、已完成、归档、通知已读共 7 处原 list 注解改为 query;已有详情、附件、流程和统计沿用 query,处置 handle 与核验 audit 保持独立。
+- PC 前端所有相关查看判断已使用 query,无需修改运行时代码;移动端源码未发现旧 list 权限判断。后端运行时代码已无 `warning:warning:list` 引用。
+- 新增后端 `sql/migration_warning_query_permission.sql`,仅更新旧 list 菜单权限值,保留菜单 ID、层级与角色授权,可重复执行。数据库未执行,部署管理后端并执行脚本后需重新登录。
+- 扩展 Controller 权限测试,验证 query 可访问列表和详情但不能处置/核验,以及旧 list 和单独 handle/audit 无法替代 query。修正新增测试的归档方法参数后,Controller 5 项与 Service 5 项全部通过,Maven 构建成功;两仓 `git diff --check` 通过。未做真实数据库或登录联调。

+ 4 - 10
src/views/subSystem/lifeCompany/WarningWorkflowActions.vue

@@ -3,8 +3,7 @@
     <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>
+    <WarningReviewAction :row="row" @changed="emit('changed')" />
     <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 />
       <WarningItemSummary :warning="row" />
@@ -26,11 +25,12 @@
 
 <script setup>
 import WarningItemSummary from './components/WarningItemSummary.vue'
+import WarningReviewAction from './components/WarningReviewAction.vue'
 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 { confirmWarningData, startWarningProcessData, submitWarningProcessData } from '@/api/pipeNetwork/earlyWarning'
+import { getWarningReasonListData } from '@/api/smxyjldczxt/yjxxfb/yjfb'
 import { workflowActions } from './warningWorkflow'
 
 const props = defineProps({ row: { type: Object, required: true } })
@@ -76,12 +76,6 @@ async function submit() {
   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(), reasonIds: selectedReasons.value, 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>

+ 84 - 0
src/views/subSystem/lifeCompany/components/WarningReviewAction.vue

@@ -0,0 +1,84 @@
+<template>
+  <span v-if="canReview" class="warning-review-action" @click.stop>
+    <el-button v-hasPermi="['warning:warning:audit']" link type="primary" :disabled="busy" @click="open">核验</el-button>
+    <el-dialog v-model="visible" title="预警核验" width="560px" append-to-body :close-on-click-modal="!busy" :close-on-press-escape="!busy" :before-close="close">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="busy">
+        <el-form-item label="预警编号">{{ row.warningNo || row.warning_no || '—' }}</el-form-item>
+        <el-form-item label="核验结果" prop="result">
+          <el-radio-group v-model="form.result">
+            <el-radio v-if="allowed('CLEAR')" label="CLEAR">核查解除</el-radio>
+            <el-radio v-if="allowed('RETURN')" label="RETURN">退回办理</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="核验意见" prop="opinion">
+          <el-input v-model="form.opinion" type="textarea" :rows="5" maxlength="2000" show-word-limit :placeholder="form.result === 'RETURN' ? '请填写退回原因及重新处置要求' : '请填写核验意见;核查解除时请说明解除依据'" />
+        </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, nextTick, reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import auth from '@/plugins/auth'
+import useUserStore from '@/store/modules/user'
+import { clearWarningData } from '@/api/pipeNetwork/earlyWarning'
+import { returnWarningData } 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 canReview = computed(() => auth.hasPermi('warning:warning:audit') && (allowed('CLEAR') || allowed('RETURN')))
+const visible = ref(false)
+const busy = ref(false)
+const formRef = ref()
+const form = reactive({ result: '', opinion: '' })
+const rules = {
+  result: [{ required: true, message: '请选择核验结果', trigger: 'change' }],
+  opinion: [{ required: true, whitespace: true, message: '请填写核验意见', trigger: 'blur' }]
+}
+
+async function open() {
+  if (busy.value || !canReview.value) return
+  form.result = ''
+  form.opinion = ''
+  visible.value = true
+  await nextTick()
+  formRef.value?.clearValidate()
+}
+
+function close(done) { if (!busy.value) done() }
+
+async function submit() {
+  if (busy.value || !canReview.value) return
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid || busy.value) return
+  if (!allowed(form.result)) return ElMessage.warning('当前预警不支持所选核验操作,请刷新后重试')
+  busy.value = true
+  try {
+    const warningId = props.row.warningId || props.row.warning_id
+    const opinion = form.opinion.trim()
+    const res = await (form.result === 'CLEAR'
+      ? clearWarningData({ warningId, clearRemark: opinion })
+      : returnWarningData({ warningId, disposalContent: opinion }))
+    if (res.code !== 200) return ElMessage.error(res.msg || '核验失败')
+    ElMessage.success(form.result === 'CLEAR' ? '核验通过,预警已解除' : '已退回原处置人重新办理')
+    visible.value = false
+    emit('changed')
+  } finally {
+    busy.value = false
+  }
+}
+</script>
+
+<style scoped>
+.warning-review-action { display: inline-flex; align-items: center; }
+</style>

+ 1 - 123
src/views/subSystem/lifeCompany/lAlertInformation/LTobeHandled.vue

@@ -210,56 +210,6 @@
       </template>
     </el-drawer>
 
-    <!-- 预警解除对话框 -->
-    <el-dialog v-model="resolveDialog.visible" title="解除预警" width="500px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="预警编号">
-          <el-input :model-value="resolveDialog.row?.warningNo || '-'" disabled />
-        </el-form-item>
-        <el-form-item label="处置说明" required>
-          <el-input
-            v-model="resolveDialog.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="resolveDialog.visible = false">取 消</el-button>
-          <el-button type="primary" :loading="actionLoading" @click="confirmResolve">确 定</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 退回重办对话框 -->
-    <el-dialog v-model="returnDialog.visible" title="退回重办" width="500px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="预警编号">
-          <el-input :model-value="returnDialog.row?.warningNo || '-'" disabled />
-        </el-form-item>
-        <el-form-item label="退回说明" required>
-          <el-input
-            v-model="returnDialog.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="returnDialog.visible = false">取 消</el-button>
-          <el-button type="primary" :loading="actionLoading" @click="confirmReturn">确 定</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
   </div>
 </template>
 
@@ -273,10 +223,9 @@ import WarningAttachments from "../components/WarningAttachments.vue";
 import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
 import { computed, onMounted, reactive, ref } from "vue";
 import auth from '@/plugins/auth';
-import { ElMessage, ElMessageBox } from "element-plus";
 import { parseTime } from "@/utils/ruoyi";
 import WarningWorkflowActions from "../WarningWorkflowActions.vue";
-import { getAlertTypeDictionaryData, getTodoListData, getWarningDetailData, readWarningNotification, resolveWarningData, returnWarningData } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
+import { getAlertTypeDictionaryData, getTodoListData, getWarningDetailData, readWarningNotification } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
 const loading = ref(false);
 const activeTab = ref("TODO");
@@ -291,19 +240,6 @@ const warningLevelOptions = ref([]);
 const detailVisible = ref(false);
 const detailData = ref({});
 const selectedNotice = ref("");
-const actionLoading = ref(false);
-
-const resolveDialog = reactive({
-  visible: false,
-  row: null,
-  disposalContent: ""
-});
-
-const returnDialog = reactive({
-  visible: false,
-  row: null,
-  disposalContent: ""
-});
 
 const queryParams = reactive({
 
@@ -538,64 +474,6 @@ async function handleViewDetail(row) {
   }
 }
 
-function openResolveDialog(row) {
-  resolveDialog.row = row;
-  resolveDialog.disposalContent = "";
-  resolveDialog.visible = true;
-}
-
-function openReturnDialog(row) {
-  returnDialog.row = row;
-  returnDialog.disposalContent = "";
-  returnDialog.visible = true;
-}
-
-async function confirmResolve() {
-  if (!resolveDialog.disposalContent) {
-    ElMessage.warning("请输入处置说明");
-    return;
-  }
-  actionLoading.value = true;
-  try {
-    const res = await resolveWarningData({
-      warningId: resolveDialog.row.warningId,
-      disposalContent: resolveDialog.disposalContent
-    });
-    if (res.code === 200) {
-      ElMessage.success("解除成功");
-      resolveDialog.visible = false;
-      await refreshLists();
-    } else {
-      ElMessage.error(res.msg || "解除失败");
-    }
-  } finally {
-    actionLoading.value = false;
-  }
-}
-
-async function confirmReturn() {
-  if (!returnDialog.disposalContent) {
-    ElMessage.warning("请输入退回说明");
-    return;
-  }
-  actionLoading.value = true;
-  try {
-    const res = await returnWarningData({
-      warningId: returnDialog.row.warningId,
-      disposalContent: returnDialog.disposalContent
-    });
-    if (res.code === 200) {
-      ElMessage.success("退回成功");
-      returnDialog.visible = false;
-      await refreshLists();
-    } else {
-      ElMessage.error(res.msg || "退回失败");
-    }
-  } finally {
-    actionLoading.value = false;
-  }
-}
-
 onMounted(async () => {
   await loadDicts();
   await preloadCounts();

+ 2 - 106
src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue

@@ -98,11 +98,10 @@
         <el-table-column label="操作" fixed="right" width="330" align="center">
           <template #default="scope">
             <el-button v-hasPermi="['warning:warning:query']" link type="primary" @click="openDetail(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>
+            <WarningReviewAction :row="scope.row" @changed="loadList" />
             <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>
@@ -158,21 +157,6 @@
       </template>
     </el-drawer>
 
-    <el-dialog v-model="resolveVisible" title="预警解除" width="520px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="预警编号">
-          <el-input :model-value="currentRow?.warningNo || '-'" disabled />
-        </el-form-item>
-        <el-form-item label="核查说明">
-          <el-input v-model="resolveForm.disposalContent" type="textarea" :rows="4" placeholder="请输入预警解除说明" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="resolveVisible = false">取 消</el-button>
-        <el-button type="primary" :loading="submitLoading" @click="submitResolve">确 定</el-button>
-      </template>
-    </el-dialog>
-
     <el-dialog v-model="upgradeVisible" title="预警升级" width="560px" append-to-body>
       <el-form label-width="100px">
         <el-form-item label="预警编号">
@@ -249,20 +233,6 @@
       </template>
     </el-dialog>
 
-    <el-dialog v-model="returnVisible" title="预警退回" width="520px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="预警编号">
-          <el-input :model-value="currentRow?.warningNo || '-'" disabled />
-        </el-form-item>
-        <el-form-item label="退回说明">
-          <el-input v-model="returnForm.disposalContent" type="textarea" :rows="4" placeholder="请输入退回重办说明" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="returnVisible = false">取 消</el-button>
-        <el-button type="primary" :loading="submitLoading" @click="submitReturn">确 定</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
@@ -274,9 +244,9 @@ 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 WarningItemSummary from '../components/WarningItemSummary.vue';
+import WarningReviewAction from '../components/WarningReviewAction.vue';
 import WarningDisposalReasons from '../components/WarningDisposalReasons.vue';
 import WarningAttachments from "../components/WarningAttachments.vue";
 import { attachmentsForDisposal, unlinkedWarningAttachments } from "../warningAttachments";
@@ -285,14 +255,11 @@ import {
   getWarningDetailData,
   getdbyjlistData,
   instructionWarningData,
-  resolveWarningData,
-  returnWarningData,
   supervisionWarningData,
   upgradeWarningData
 } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
 
 const loading = ref(false);
-const userStore = useUserStore();
 const submitLoading = ref(false);
 const usersLoading = ref(false);
 const enabledUsers = ref([]);
@@ -306,11 +273,9 @@ const currentRow = ref(null);
 const detailVisible = ref(false);
 const detailData = ref({});
 
-const resolveVisible = ref(false);
 const upgradeVisible = ref(false);
 const supervisionVisible = ref(false);
 const instructionVisible = ref(false);
-const returnVisible = ref(false);
 
 const queryParams = reactive({
   pageNum: 1,
@@ -325,10 +290,6 @@ const queryParams = reactive({
   dateRange: []
 });
 
-const resolveForm = reactive({
-  disposalContent: ""
-});
-
 const upgradeForm = reactive({
   newLevel: "",
   disposalContent: ""
@@ -346,10 +307,6 @@ const instructionForm = reactive({
   instructionContent: ""
 });
 
-const returnForm = reactive({
-  disposalContent: ""
-});
-
 const warningTypeLabelMap = computed(() => {
   return warningTypeOptions.value.reduce((acc, item) => {
     acc[item.dictValue] = item.dictLabel;
@@ -370,7 +327,6 @@ const upgradeLevelOptions = computed(() => warningLevelOptions.value.filter(
 
 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 "-";
@@ -436,7 +392,6 @@ function normalizeWarningRow(item) {
 }
 
 function resetActionForms() {
-  resolveForm.disposalContent = "";
   upgradeForm.newLevel = "";
   upgradeForm.disposalContent = "";
   supervisionForm.supervisionUser = "";
@@ -446,7 +401,6 @@ function resetActionForms() {
   supervisionFiles.value = [];
   instructionForm.leaderName = "";
   instructionForm.instructionContent = "";
-  returnForm.disposalContent = "";
 }
 
 function filterSupervisionUser(keyword) {
@@ -520,12 +474,6 @@ async function openDetail(row) {
   detailVisible.value = true;
 }
 
-function openResolve(row) {
-  currentRow.value = row;
-  resetActionForms();
-  resolveVisible.value = true;
-}
-
 function openUpgrade(row) {
   currentRow.value = row;
   resetActionForms();
@@ -555,35 +503,6 @@ function openInstruction(row) {
   instructionVisible.value = true;
 }
 
-function openReturn(row) {
-  currentRow.value = row;
-  resetActionForms();
-  returnVisible.value = true;
-}
-
-async function submitResolve() {
-  if (!resolveForm.disposalContent) {
-    ElMessage.warning("请输入解除说明");
-    return;
-  }
-  submitLoading.value = true;
-  try {
-    const res = await resolveWarningData({
-      warningId: currentRow.value.warningId,
-      disposalContent: resolveForm.disposalContent
-    });
-    if (res.code !== 200) {
-      ElMessage.error(res.msg || "解除失败");
-      return;
-    }
-    ElMessage.success("解除成功");
-    resolveVisible.value = false;
-    loadList();
-  } finally {
-    submitLoading.value = false;
-  }
-}
-
 async function submitUpgrade() {
   if (!upgradeForm.newLevel) {
     ElMessage.warning("请选择新预警级别");
@@ -680,29 +599,6 @@ async function submitInstruction() {
   }
 }
 
-async function submitReturn() {
-  if (!returnForm.disposalContent) {
-    ElMessage.warning("请输入退回说明");
-    return;
-  }
-  submitLoading.value = true;
-  try {
-    const res = await returnWarningData({
-      warningId: currentRow.value.warningId,
-      disposalContent: returnForm.disposalContent
-    });
-    if (res.code !== 200) {
-      ElMessage.error(res.msg || "退回失败");
-      return;
-    }
-    ElMessage.success("退回成功");
-    returnVisible.value = false;
-    loadList();
-  } finally {
-    submitLoading.value = false;
-  }
-}
-
 onMounted(async () => {
   await loadDicts();
   await loadList();

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

@@ -140,11 +140,10 @@
         <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="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 v-if="isPending(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openConfirm(row)">接收</el-button>
+            <el-button v-if="isReceived(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openStart(row)">开始处置</el-button>
+            <el-button v-if="row.status === 'PROCESSING'" v-hasPermi="['warning:warning:handle']" type="warning" size="small" @click="openProcess(row)">提交处理</el-button>
+            <WarningReviewAction :row="row" @changed="loadList" />
             <el-button type="info" size="small" @click="openDetail(row.warningId)">详情</el-button>
           </template>
         </el-table-column>
@@ -295,9 +294,10 @@
 <script setup>
 import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
 import * as echarts from 'echarts'
-import { ElMessage, ElMessageBox } from 'element-plus'
+import { ElMessage } from 'element-plus'
 import { Search, Refresh, Plus, Document, Download, View, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
-import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, confirmWarningData, startWarningProcessData, misreportWarningData, submitWarningProcessData, clearWarningData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
+import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, confirmWarningData, startWarningProcessData, submitWarningProcessData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
+import WarningReviewAction from '@/views/subSystem/lifeCompany/components/WarningReviewAction.vue'
 import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
 
 // ========== 统计数据 ==========
@@ -634,22 +634,6 @@ async function submitStart() {
   finally { submitting.value=false }
 }
 
-// ========== 误报处理 ==========
-function confirmMisreport(row) {
-  ElMessageBox.prompt('请输入误报原因', '误报确认', {
-    inputPlaceholder: '例如:传感器误判、测试数据等',
-    inputValue: '',
-    inputPattern: /.*/,
-    message: '误报后将直接解除该预警',
-    confirmButtonText:'确认误报',
-    cancelButtonText:'取消'
-  }).then(async ({value}) => {
-    await misreportWarningData({ warningId: row.warningId, reason: value || '系统确认误报' })
-    ElMessage.success('已标记为误报并解除')
-    loadList()
-  }).catch(()=>{})
-}
-
 const processVisible = ref(false)
 const processRow = ref({})
 const processContent = ref('')
@@ -708,22 +692,6 @@ async function submitProcess() {
   }
 }
 
-function clearHandled(row) {
-  ElMessageBox.prompt('请输入清除说明', '清除预警', {
-    inputPlaceholder: '例如:维修完成,现场复核正常',
-    confirmButtonText: '确认清除',
-    cancelButtonText: '取消'
-  }).then(async ({ value }) => {
-    if (!value || !value.trim()) {
-      ElMessage.warning('请填写清除说明')
-      return
-    }
-    await clearWarningData({ warningId: row.warningId, clearRemark: value })
-    ElMessage.success('预警已清除')
-    loadList()
-  }).catch(() => {})
-}
-
 // ========== 详情抽屉 ==========
 const detailVisible = ref(false)
 const detail = ref(null)

+ 95 - 0
tests/warningReviewAction.test.mjs

@@ -0,0 +1,95 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { computed, nextTick, reactive, ref } from 'vue'
+import { parse } from '@vue/compiler-sfc'
+import AsyncValidator from 'async-validator'
+
+const read = path => readFileSync(new URL(path, import.meta.url), 'utf8')
+const script = parse(read('../src/views/subSystem/lifeCompany/components/WarningReviewAction.vue')).descriptor.scriptSetup.content
+  .replace(/^import .*\r?\n/gm, '')
+const workflowSource = read('../src/views/subSystem/lifeCompany/warningWorkflow.js')
+const workflowActions = new Function(`${workflowSource.slice(workflowSource.indexOf('export function workflowActions')).replace('export ', '')}; return workflowActions`)()
+const Validator = AsyncValidator.default || AsyncValidator
+
+// Execute the component's setup with real Vue reactivity and the form validator;
+// replace only browser stores, messages and network requests.
+function setup({ permission = 'warning:warning:audit', row = {}, request } = {}) {
+  const user = reactive({ name: 'reviewer', permissions: [permission] })
+  const props = reactive({ row: { warningId: 'w1', status: 'HANDLED', handler: 'handler', reviewer: 'reviewer', ...row } })
+  const calls = [], messages = [], events = []
+  const api = kind => async data => {
+    calls.push({ kind, data })
+    return request ? request() : { code: 200 }
+  }
+  const auth = new Function('useUserStore', read('../src/plugins/auth.js')
+    .replace(/^import .*\r?\n/gm, '').replace('export default', 'return'))(() => user)
+  const factory = new Function('computed', 'nextTick', 'reactive', 'ref', 'ElMessage', 'auth', 'useUserStore',
+    'clearWarningData', 'returnWarningData', 'workflowActions', 'defineProps', 'defineEmits',
+    `${script}; return { canReview, visible, busy, formRef, form, rules, open, submit, close }`)
+  const state = factory(computed, nextTick, reactive, ref,
+    { success: text => messages.push(text), error: text => messages.push(text), warning: text => messages.push(text) },
+    auth, () => user, api('clear'), api('return'), workflowActions, () => props, () => event => events.push(event))
+  state.formRef.value = { clearValidate() {}, validate: () => new Validator(state.rules).validate(state.form).then(() => true) }
+  return { ...state, user, props, calls, messages, events }
+}
+
+test('核验仅对指定核验人、已处置状态及核验权限开放,处置权限不能替代', () => {
+  assert.equal(setup().canReview.value, true)
+  assert.equal(setup({ permission: 'warning:warning:handle' }).canReview.value, false)
+  assert.equal(setup({ row: { reviewer: 'someone-else' } }).canReview.value, false)
+  assert.equal(setup({ permission: '*:*:*', row: { reviewer: 'someone-else' } }).canReview.value, false)
+  assert.equal(setup({ row: { status: 'PROCESSING' } }).canReview.value, false)
+  assert.equal(setup({ row: { todo_type: 'DONE' } }).canReview.value, false)
+  assert.equal(setup({ row: { reviewer: '', publisher: 'reviewer' } }).canReview.value, true)
+})
+
+test('每次打开清空结果和意见,未选结果或空白意见不能提交', async () => {
+  const s = setup()
+  await s.open()
+  assert.equal(s.form.result, '')
+  await s.submit()
+  s.form.result = 'CLEAR'
+  s.form.opinion = '   '
+  await s.submit()
+  assert.deepEqual(s.calls, [])
+  s.form.opinion = '旧意见'
+  await s.open()
+  assert.deepEqual({ ...s.form }, { result: '', opinion: '' })
+})
+
+test('核查解除与退回办理分别调用既有接口,成功后关闭并刷新', async () => {
+  for (const [result, kind, field] of [['CLEAR', 'clear', 'clearRemark'], ['RETURN', 'return', 'disposalContent']]) {
+    const s = setup()
+    await s.open()
+    s.form.result = result
+    s.form.opinion = '  核验意见  '
+    await s.submit()
+    assert.deepEqual(s.calls, [{ kind, data: { warningId: 'w1', [field]: '核验意见' } }])
+    assert.equal(s.visible.value, false)
+    assert.deepEqual(s.events, ['changed'])
+  }
+})
+
+test('接口失败保留表单,提交期间禁止重复提交或关闭', async () => {
+  let resolveRequest
+  const s = setup({ request: () => new Promise(resolve => { resolveRequest = resolve }) })
+  await s.open()
+  s.form.result = 'RETURN'
+  s.form.opinion = '需要补充处置'
+  const first = s.submit()
+  await nextTick()
+  await new Promise(resolve => setImmediate(resolve))
+  assert.equal(s.busy.value, true)
+  await s.submit()
+  let closed = false
+  s.close(() => { closed = true })
+  assert.equal(closed, false)
+  assert.equal(s.calls.length, 1)
+  resolveRequest({ code: 500, msg: '状态已经改变' })
+  await first
+  assert.equal(s.busy.value, false)
+  assert.equal(s.visible.value, true)
+  assert.equal(s.form.opinion, '需要补充处置')
+  assert.deepEqual(s.events, [])
+})