Kaynağa Gözat

优化应急事件管理

zengy 1 gün önce
ebeveyn
işleme
c5350636f8

+ 1 - 0
memory/index.md

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

+ 7 - 1
memory/long-term.md

@@ -40,11 +40,17 @@
 
 ## 用户已确认决策
 
+- `DisPlanEditing.vue` 模板的“责任单位”统一展示为“编制单位”,预案编制单位使用纯文本手动输入,不再提供系统部门下拉;接口字段仍分别使用 `responsibleUnit`、`compileUnit`(2026-10-19)。
+- 预案正文和模板章节正文统一使用 sys_notice 的 wangEditor;独立预案附件上传、展示和数据库附件字段已移除,图片附件直接随正文 HTML 富文本上传(2026-10-19)。
+- 预案编辑、审批列表的操作按钮使用不换行布局和足够列宽,优先保持同一行显示(2026-10-19)。
+- 数字预案库 `dis_plan_library` 移除 `create_by` 登录名快照,创建人只保存 `create_user_id`;实体、模板保存、种子数据和升级脚本同步调整(2026-10-19)。
+- PC 应急值守的 `jbgl.vue`、`cphs.vue`、`gzjc.vue`、`zjbs.vue` 共用 `EventDetailDrawer.vue`,以接报管理的事件详情字段和布局为准;统一调用事件详情接口获取最新档案与日志(2026-10-19)。
+- 应急事件 `dis_event` 不再存储 `reporter` 名称快照,接报人统一由 `create_user_id` 关联用户昵称;PC 接报管理 `jbgl.vue` 移除管网类型的展示、输入和提交(2026-10-19)。
 - PC Portal 通知公告滚动栏的时间使用“今天/昨天/前天 HH:mm”,其他日期使用 `yyyy-MM-dd HH:mm`;详情发布时间固定 `yyyy-MM-dd HH:mm:ss`(2026-10-15)。
 - PC 门户的设备、今日预警、今日报警指标按全网统一口径统计,所有登录用户看到相同结果,不应用部门或角色数据范围;公告取启用状态的 `sys_notice`,点击公告展示详情。门户接口统一放在管理后端 `com.zksy.web.controller.system.PortalController`,不要求具体菜单权限但仍要求登录。登录页移动端下载二维码固定放在前端 `public/images/mobile-app-qr.png`(2026-10-15)。
 - 移动端专用接口统一放到后端 `zksy-admin` 的 `com.zksy.web.controller.mobile`,与 PC 共用现有业务 Service,数据权限也留在 Service 校验;不复制移动端业务流程。七类 Controller 的 URL 统一为 `/api/mobile/...`:工单 `/ops/workOrder`、巡检 `/ops/inspectTask`、设备 `/equipment-base`、故障通知 `/alarm`、公告 `/notice`、本人登录日志 `/monitor/logininfor/list`、通讯录 `/contacts`(均接在统一前缀后)。移动端调用同步更新,不保留旧移动端专用双路由;此决定取代同日初版保留旧 URL 的约定。登录、资料、通用上传及工单处置等两端共用接口仍保留原入口;报警不再单设 `MobileAlarmService`(2026-10-14)。
 - 移动端“我的-基本资料”性别仅提供男(0)、女(1)两个 radio 单选项,不再提供保密或下拉选择。历史保密(2)或未设置的资料显示为未选中,保存前需明确选择男或女,避免静默修改性别(2026-10-14)。
-- 移动端“日常-故障通知”按当前用户部门自身或祖先 101/102/103 对应设备类型的直接 parent_id=1/2/3 过滤,不包含共享类型 4,也不扩展类型树;管理员同样按公司范围读取,客户端 rootTypeId 只能收窄。已转工单通过 work_order.device_id 关联设备,未转工单按报警设备编码关联;关联损坏不可回退放宽。列表与 PC 审核页一致只查未处理报警,右上角展示待核实/确认报警/误报警;顶部按全部/待核实/确认报警/误报警分 tab,报警等级下拉放在搜索输入框前,使用工单同款公共搜索栏。详情用“故障信息/处置全流程”左右页签,待核实且未处理并有 alarm:review:audit 权限时提供底部“核实”抽屉。移动端状态0统一显示“待核实”,操作与表单使用“核实”文案,PC 同步使用“待核实/核实”,接口枚举和权限保持原约定。核实结果必选、意见选填最多500字、图片最多5张(JPG/JPEG/PNG、每张10MB),复用 PC 附件票据和审核事务。历史沿用 alarm_log,不复制工单日志(2026-10-14)。
+- 移动端“日常-故障通知”通过报警自身 `alarm_data.device_code` 关联设备,不依赖工单字段;设备类型按当前用户部门自身/祖先 101/102/103 对应的直接父类型 1/2/3 加共享父类型 4 过滤,不扩展类型树;管理员同样按公司范围读取,客户端 `rootTypeId` 只能收窄所属公司范围。故障通知列表展示全部报警状态,首页“待处理”仅统计未审核(待核实)报警;报警等级筛选及待核实/确认报警/误报警 tab 保留。详情用“故障信息/处置全流程”左右页签,有 `alarm:review:audit` 权限时提供核实抽屉。状态文案、核实表单、附件审核和历史日志语义保持原约定(2026-10-19 更新)。
 - PC 与移动端报警业务统一展示“待核实/核实”:PC 共享列表、详情、提醒、研判、供水首页及台账导出同步;菜单旧标题“报警审核”由前端按报警组件映射为“报警核实”,兼容后端旧状态名,不修改数据库菜单、接口枚举、路由或权限。工单验收与延期仍使用审核文案(2026-10-14)。
 - 移动端照片预览按同一业务组连续切换,维修前、维修后、每条工单日志附件和巡检现场照片分别成组;工单详情无可执行操作时不展示底部提示栏(2026-10-14)。
 - 移动端“我的-登录日志”仅查询当前登录用户本人,管理员也不例外;服务端从登录态获取账号,按 `sys_logininfor.user_name` 精确匹配,不复用 PC 日志列表的账号模糊查询(2026-10-14)。

+ 171 - 0
memory/sessions/2026-10-19.md

@@ -0,0 +1,171 @@
+# 2026-10-19:移动端故障数据范围对齐预警管理
+
+## 目标与结论
+
+- 移动端故障通知列表按预警管理范围对齐设备数据权限。
+- 列表和详情/日志/审核复用服务端 SQL 范围,客户端不能传入用户名作为授权依据。
+
+### 用户补充要求(后续调整)
+
+- 故障权限不再依赖 `work_order_id`,统一通过报警自身 `device_code` 匹配设备;用户所属公司设备类型及父类为 4 的共享窨井设备均可见。
+- 移动端故障通知展示全部报警状态;移动端首页“待处理”仅统计审核状态为“未审核/待核实”的报警,今日新增统计可见范围内当天全部报警。
+
+## 变更与验证
+
+- 后端 `AlarmOperationsMapper` 改为始终按 `alarm_data.device_code` 匹配设备;移动端范围包含公司设备类型直接子级及父类 4 子级,列表不再强制未处理状态;首页统计显式传入未审核状态。
+- 后续排查发现 Mapper 列表 SQL 对 `query.alarmStatus` 无条件执行等值筛选,而“全部”查询的该参数为空,导致 SQL `alarm_status = NULL` 返回 0 条。已改为仅显式传入状态时追加条件,并增加空状态/指定状态 SQL 断言。
+- `git diff --check` 通过,Mapper 调用签名已检索确认。
+- `mvn -pl zksy-system '-Dtest=AlarmMobileServiceTest,MobileAlarmMapperTest' test` 未能执行到测试:工程编译被既有 `SmsProperties.getSupervisionTemplateCode()` 缺失阻断(`EarlyWarningServiceImpl`、`WarningSupervisionNotifier`)。
+
+## 风险与后续
+
+- 需要修复或同步 SMS 配置模型后重跑目标测试;真实账号与共享窨井工单数据尚未端到端验证。
+
+# 2026-10-19:PC 应急指挥值守与预案管理调整
+
+## 目标与结论
+
+- 按 `prd/emergency/index.html` 调整 PC 应急指挥值守、应急预案分级/编辑/审批页面,不触及其他业务模块。
+- 接报人由当前登录用户确定,接报时间由服务端记录当前时间;移除来源渠道表单项,将联系电话文案改为上报人电话。
+- 核实结论前置;真实事件显示类别、等级、必选处置单位、处置人和核验人,处置人候选限定在处置单位及所有下级部门;不真实时隐藏其他业务字段但保留可选核实意见。
+- 已核实和处置中均可转接、报送,两个操作共用转接部门多选和备注表单。开始处置、跟踪共用备注和附件表单。
+- 分级页及预案列表/审批页字段筛选按原型收敛,保留预案模板、版本和审批查看流程。
+
+## 变更与验证
+
+- 前端改动:`jbgl.vue`、`cphs.vue`、`zjbs.vue`、`gzjc.vue`、`DisEventGrade.vue`、`DisPlanEditing.vue`、`DisPlanApproval.vue`。
+- 后端修改 `EventVerifyDTO` 和 `DisEventServiceImpl`:接报时间强制服务端生成;编辑缺省时保留旧时间/渠道;核验人可指定、未指定默认当前用户;真实事件校验处置单位有效,处置人可选且后端校验部门祖先关系。不真实核实意见可空并写入默认日志说明。
+- `npm run build:prod` 通过;`mvn -pl pipe-network-service/zksy-system -am -DskipTests compile` 通过;目标测试 `DispatchTimeFormatGuardTest` 1 项通过;两仓 `git diff --check` 通过。
+
+## 风险与后续
+
+- 仍需用实际登录用户及部门树做浏览器联调,尤其验证下级部门处置人候选、默认核验人回显及附件上传结果。
+- Maven 目标模块本次编译通过;没有覆盖数据库集成和完整应急事件服务状态流测试。
+
+## 本轮继续修改
+
+- 不真实核实时保留核实意见输入,意见改为可选;真实事件仍校验类别、等级和处置单位。
+- 新增接报表单不展示接报人/接报时间,服务端强制使用当前用户和当前时间落库;上报人电话必填。
+- `zjbs.vue` 报送层级不再展示附件;`gzcb.vue` 默认查询、筛选和催办按钮仅覆盖已核实/处置中,电话文案统一为处置人电话。
+- `cphs.vue` 列表列顺序调整为发生时间、初判时间、接报时间;接报与核实详情抽屉压缩间距,事件编号置左上、状态置右上。
+- 新增 `pipe-network-service/sql/dispatch_cleanup_unused_fields.sql`,清理迁移后的旧流程表和旧字段;脚本明确保留仍被代码引用的 `channel`、`handler_phone`、`attach_url`。
+
+## 本轮验证
+
+- `npm run build:prod` 通过(仅有既有 Sass 弃用提示)。
+- `mvn -pl pipe-network-service/zksy-system -am -DskipTests compile` 通过。
+- `DispatchTimeFormatGuardTest` 通过;两仓 `git diff --check` 通过。
+
+## 主键序列统一
+
+- 四张表统一使用显式序列:`dis_event_grade_id_seq`、`dis_plan_id_seq`、`dis_plan_log_id_seq`、`dis_plan_version_id_seq`。
+- 新建表定义已改为 `DEFAULT nextval(...)`;`dis_event_grade` 服务移除 `MAX(id)+1`,改由数据库序列发号。
+- 新增升级脚本 `pipe-network-service/sql/dispatch_named_id_sequences.sql`,负责移除旧 Identity、绑定固定序列并按现有最大 ID 校准。
+- 后端目标测试重新通过。
+
+## 预案库字段收敛
+
+- `dis_plan_library.event_type`、`event_level` 统一存储 `dis_plan_type`、`dis_plan_level` 字典编码,并在模板管理/预案选择页面使用对应字典显示标签。
+- `dis_plan_library.event_region`、`topic`、`plan_summary` 已从实体、匹配结果和前端交互移除;专题匹配接口同步删除,匹配改为类型/等级/责任单位三项。
+- `dispatch_refactor.sql` 增加历史中文标签到字典编码的转换、三列幂等删除、字段注释和类型等级索引;模板种子脚本同步改为新字段。
+- 前端 `npm run build:prod` 通过,后端增量编译和 `DispatchTimeFormatGuardTest` 曾通过;本轮 clean compile 暴露工程既有 `SmsProperties#getSupervisionTemplateCode()` 编译阻断(与本次预案库改动无关)。
+
+## 事件详情样式调整
+
+- 接报管理、初判核实、工作处置三个事件详情抽屉参考预警详情布局:顶部展示事件编号和状态,基础信息与处置相关人员使用分组描述表格,事件记录单独分组。
+- “事件记录(操作日志)”统一改为“事件记录”。
+- 前端生产构建与 `git diff --check` 通过(仅有既有 Sass 弃用提示)。
+
+## Kingbase 脚本修正
+
+- `dispatch_refactor.sql` 在存量 Identity 主键上提前执行 `ALTER SEQUENCE ... OWNED BY` 会触发 Kingbase“无法更改标识列的所有权”。已移除主脚本中的四处 OWNED BY,统一延后到先 DROP IDENTITY 的 `dispatch_named_id_sequences.sql`。
+- 发生该错误后需先在当前连接执行 `ROLLBACK;`,再整体重跑修正后的主脚本;不要继续在 aborted transaction 中执行后续语句。
+
+## 预案与事件分级字典统一
+
+- `DisPlanEditing.vue` 的预案类型、预案级别,以及模板新增/编辑表单和模板列表,统一使用 `useDict('dis_plan_type', 'dis_plan_level')` 对应 `sys_dict_data.dict_type`,模板列表和表单文案改为“预警类型”“预警级别”。
+- `DisEventGrade.vue` 的事件类别、事件等级下拉、列表展示和导出同步使用 `dis_plan_type`、`dis_plan_level`。
+- `npm run build:prod` 通过,仅保留既有 Sass 弃用提示;前端 `git diff --check` 通过。
+
+## dis_event_grade.event_type 评估
+
+- 前端 `DisEventGrade.vue` 已无 `eventType` 字段使用。
+- 后端 `DisEventGrade` 实体、`DisEventGradePageDTO`、`DisEventGradeServiceImpl` 的查询和更新仍引用 `event_type`。
+- 该列可删除,但必须同步清理 Java 字段/查询赋值、`dispatch_refactor.sql` 建表与加列/注释,并提供 Kingbase `DROP COLUMN IF EXISTS` 脚本;不能只删除数据库列。
+- `dis_event.event_type`、`dis_plan_library.event_type` 和预案匹配对象的 `eventType` 是其他业务字段,不能一并删除。
+
+## 清理 dis_event_grade.event_type
+
+- 已从后端 `DisEventGrade` 实体、分页 DTO、查询条件和更新赋值中移除 `event_type`。
+- 已从 `dispatch_refactor.sql` 的建表/补列/注释中移除,并加入 `DROP COLUMN IF EXISTS event_type`;合并 Kingbase 脚本同步加入幂等删除。
+- `mvn -pl pipe-network-service/zksy-system -am -DskipTests compile` 通过;数据库脚本仍需在目标 Kingbase 执行后才会实际删除存量列。
+
+## 预案类型取数复核
+
+- 用户要求 `DisPlanEditing.vue` 所有预案类型读取 `sys_dict_data` 中 `dict_type = 'dis_plan_type'` 的数据。
+- 已确认现有筛选、预案列表/表单、模板列表/表单、新建模板选择与导出均复用 `useDict('dis_plan_type', 'dis_plan_level')`,本轮无需修改业务代码。
+- 已核对调用链:`getDicts` 请求 `/system/dict/data/type/dis_plan_type`,后端查询 `app_user.sys_dict_data`,按字典类型和启用状态过滤,并按 `dict_sort` 排序;显示 `dict_label`、提交 `dict_value`。
+- Vue SFC 脚本与模板编译、`git diff --check` 均通过。未连接目标数据库验证实际字典内容。
+
+## 删除事件接报人冗余列与接报管理管网类型
+
+- 按用户要求移除 `dis_event.reporter`:删除实体与接报入参字段、接报保存写入逻辑;展示 VO 的接报人继续按 `create_user_id` 查询昵称,不再依赖名称快照。
+- `jbgl.vue` 移除管网类型的列表列、详情项、表单项、字典请求及新增/编辑提交字段。
+- 后端主建表脚本及已有清理/合并脚本同步删除 reporter,修正人员迁移脚本的过期说明与自检示例;新增仅删该列的 `pipe-network-service/sql/dispatch_drop_event_reporter.sql`,便于存量库单独升级。
+- 后端 `mvn -pl pipe-network-service/zksy-system -am -DskipTests compile -q` 通过,前端 `jbgl.vue` 脚本与模板编译通过。
+- 尚未执行目标 Kingbase 的删列脚本;需部署新版后端后执行。创建人缺失或用户已删除的事件不再有接报人名称快照可回退。
+
+## 接报表单字段顺序
+
+- `jbgl.vue` 新增/编辑共用表单将“事件描述”移至第一行,其他字段顺序、校验和提交逻辑保持原样。
+- Vue SFC 脚本/模板编译及 `git diff --check` 通过;无新增风险或后续迁移。
+
+## 工作处置列表字段调整
+
+- 按用户要求移除 `gzjc.vue` 列表“管网类型”列;详情展示及其字典引用保留。
+- Vue SFC 脚本/模板编译及 `git diff --check` 通过;无新增风险或后续迁移。
+- 后续按用户要求将列表 `handler` 列标题从“负责人”改为“处置人”,与详情文案一致;`git diff --check` 通过。
+
+## 四个应急值守页面共用事件详情
+
+- 按用户要求从 `jbgl.vue` 提取 `disOnDuty/EventDetailDrawer.vue`;`jbgl.vue`、`gzjc.vue`、`zjbs.vue`、补充指定的 `cphs.vue` 均接入该组件,字段与布局统一按接报管理详情展示。
+- 公共组件统一通过 `getEventDetail(id)` 加载最新事件档案及 `logs`,事件记录继续复用 `EventLogTimeline.vue`;删除各页面重复的抽屉、日志转换、附件详情辅助函数、详情样式和不再使用的字典引用。
+- 保留核实、开始处置/跟踪、解除、转接/报送表单与数据状态;初判核实 GIS 点位点击也进入公共详情。
+- 公共组件提供加载状态、失败重试,并忽略切换事件、关闭及卸载后的旧响应,避免不同事件数据串用。
+- 五个 Vue 文件脚本/模板/样式编译通过;模拟接口异步验证覆盖最新档案与日志、乱序响应、关闭/卸载、失败重试和空详情,全部通过;`npm run build:prod` 通过(既有 Sass 弃用提示)。
+- 未进行真实登录态浏览器联调,无数据库结构或后端接口改动。
+
+## 数字预案库移除 create_by
+
+- 按用户要求移除后端 `DisPlanLibrary.createBy` 及模板新增时的登录名赋值,保留 `create_user_id`。
+- 种子脚本改用按登录名查询用户 ID 写入 `create_user_id`;主结构脚本和序列/字典合并脚本同步幂等删列;人员 ID 迁移脚本移除对预案库旧 `create_by` 的回填引用并修正说明。
+- 新增后端 `pipe-network-service/sql/dispatch_drop_plan_library_create_by.sql`,供存量 Kingbase 单独删列并核验。
+- `mvn -pl pipe-network-service/zksy-admin -am -DskipTests compile -q` 通过;后端 `git diff --check` 通过;前端未使用该字段,无业务代码改动。
+- 尚未执行目标数据库脚本,需部署新版后端后执行;历史记录已存在的 `create_user_id` 保留,未关联 ID 的旧登录名在删列后不再保留。
+
+## 预案与模板编制单位表单调整
+
+- 按用户要求将 `DisPlanEditing.vue` 模板新增/编辑表单及列表的“责任单位”改为“编制单位”,输入提示同步调整。
+- 预案新增/编辑共用表单的编制单位改为普通文本输入,移除部门下拉、候选加载和该页不再使用的 API 导入;保留原字段与提交逻辑。
+- Vue SFC 脚本与模板编译通过,`git diff --check` 通过。
+- 无数据库或接口变更;未进行浏览器登录态联调。
+
+## 预案正文改用公告富文本编辑器
+
+- `DisPlanEditing.vue` 的预案正文和模板章节正文改用 sys_notice 使用的 `NoticeEditor.vue`(wangEditor),统一工具栏、HTML 内容格式和图片上传能力。
+- `NoticeEditor.vue` 增加可配置 `placeholder` 属性,公告原有默认提示保持不变;预案场景传入对应提示。
+- 相关 Vue SFC 编译通过,`npm run build:prod` 通过(仅既有 Sass 弃用警告),`git diff --check` 通过。
+- 未进行真实登录态浏览器联调;正文仍通过原 `content`、`planContent` 字段提交。
+
+## 预案附件并入富文本
+
+- `DisPlanEditing.vue` 移除独立 PDF/Word 附件上传、粘贴上传、版本附件下载和导出附件列;预案正文与模板正文继续通过 sys_notice 的 wangEditor 上传图片。
+- 预案审批页同步移除附件展示入口;正文仍使用 `content` / `planContent`。
+- 后端移除 `DisPlan`、`DisPlanVersion`、`PlanSaveDTO`、`PlanVersionVO` 的附件字段及服务层映射;主结构/迁移 SQL 不再创建或搬迁这些字段。
+- 新增 `pipe-network-service/sql/dispatch_drop_plan_attachments.sql`,并将历史 `dispatch_plan_version_attach.sql` 改为幂等删列脚本,清理 `dis_plan`、`dis_plan_version` 的存量附件列。
+- 前端两个预案页面 SFC 编译、后端 Maven 编译、两端 `git diff --check` 均通过;数据库脚本尚未在目标 KingbaseES 执行。
+
+## 预案列表操作按钮布局
+
+- `DisPlanEditing.vue`、`DisPlanApproval.vue` 的表格操作按钮改为 flex 不换行布局,取消按钮默认间距并调整操作列宽度;模板操作和审批操作同样处理。
+- 两个 Vue SFC 模板/样式编译通过,`git diff --check` 通过。

+ 0 - 3
src/api/dispatch.js

@@ -155,9 +155,6 @@ export function deletePlanLibrary(id) {
 export function matchPlan(data) {
   return request({ url: '/api/dispatch/plan-library/match', method: 'post', data })
 }
-export function matchPlanByTopic(data) {
-  return request({ url: '/api/dispatch/plan-library/matchByTopic', method: 'post', data })
-}
 export function getPlanMatchHistory(limit = 10) {
   return request({ url: '/api/dispatch/plan-library/history', method: 'get', params: { limit } })
 }

+ 24 - 97
src/views/subSystem/dispatch/disDecisionDep/DisPlanSelection.vue

@@ -13,19 +13,14 @@
             </div>
           </template>
           <el-form label-position="top" size="default">
-            <el-form-item label="事件类型">
+            <el-form-item label="预案类型">
               <el-select v-model="form.eventType" placeholder="请选择事件类型" style="width:100%">
-                <el-option v-for="item in eventTypeOptions" :key="item" :label="item" :value="item" />
+                <el-option v-for="item in dis_plan_type" :key="item.value" :label="item.label" :value="item.value" />
               </el-select>
             </el-form-item>
-            <el-form-item label="事件级别">
+            <el-form-item label="预案等级">
               <el-select v-model="form.eventLevel" placeholder="请选择事件级别" style="width:100%">
-                <el-option v-for="item in eventLevelOptions" :key="item" :label="item" :value="item" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="发生区域">
-              <el-select v-model="form.eventRegion" placeholder="请选择发生区域" style="width:100%">
-                <el-option v-for="item in eventRegionOptions" :key="item" :label="item" :value="item" />
+                <el-option v-for="item in dis_plan_level" :key="item.value" :label="item.label" :value="item.value" />
               </el-select>
             </el-form-item>
             <el-form-item label="事故责任单位">
@@ -45,23 +40,19 @@
             </div>
           </template>
           <div class="rule-desc">
-            <p>根据事件类型、级别、区域、责任单位进行数字预案库权重匹配:</p>
+            <p>根据预案类型、预案等级、责任单位进行数字预案库匹配:</p>
             <div class="rule-weights">
               <div class="weight-item">
-                <span class="weight-label">事件类型</span>
+                <span class="weight-label">预案类型</span>
                 <el-progress :percentage="40" :stroke-width="14" :format="() => '40%'" color="#409EFF" />
               </div>
               <div class="weight-item">
-                <span class="weight-label">事件级别</span>
+                <span class="weight-label">预案等级</span>
                 <el-progress :percentage="25" :stroke-width="14" :format="() => '25%'" color="#67C23A" />
               </div>
-              <div class="weight-item">
-                <span class="weight-label">发生区域</span>
-                <el-progress :percentage="20" :stroke-width="14" :format="() => '20%'" color="#E6A23C" />
-              </div>
               <div class="weight-item">
                 <span class="weight-label">责任单位</span>
-                <el-progress :percentage="15" :stroke-width="14" :format="() => '15%'" color="#F56C6C" />
+                <el-progress :percentage="35" :stroke-width="14" :format="() => '35%'" color="#F56C6C" />
               </div>
             </div>
             <p class="rule-tip">系统根据加权相似度搜寻最佳匹配预案</p>
@@ -90,28 +81,6 @@
 
       <!-- 右侧结果面板 -->
       <div class="right-panel">
-        <!-- 一键快速查找 -->
-        <el-card shadow="never" class="panel-card">
-          <template #header>
-            <div class="card-title">
-              <el-icon><Promotion /></el-icon>
-              <span>常见专题预案 · 一键快速查找</span>
-            </div>
-          </template>
-          <div class="topic-row">
-            <el-button
-              v-for="topic in topics"
-              :key="topic.value"
-              :type="activeTopic === topic.value ? 'primary' : 'default'"
-              round
-              @click="matchByTopic(topic.value)"
-            >
-              {{ topic.label }}
-            </el-button>
-          </div>
-          <div class="topic-tip">支持一键选择,系统自动定位匹配数字预案库中的专题预案</div>
-        </el-card>
-
         <!-- 匹配结果 -->
         <el-card shadow="never" class="panel-card">
           <template #header>
@@ -144,10 +113,8 @@
               <div class="plan-tags">
                 <el-tag size="small" type="primary">{{ plan.type }}</el-tag>
                 <el-tag size="small" type="warning">{{ plan.level }}</el-tag>
-                <el-tag size="small">{{ plan.region }}</el-tag>
                 <el-tag size="small" type="success">{{ plan.unit }}</el-tag>
               </div>
-              <div class="plan-summary">{{ plan.summary }}</div>
             </div>
           </div>
         </el-card>
@@ -177,9 +144,8 @@
                   {{ selectedPlan.matchScore }}%
                 </el-tag>
               </el-descriptions-item>
-              <el-descriptions-item label="事件类型">{{ selectedPlan.type }}</el-descriptions-item>
-              <el-descriptions-item label="事件级别">{{ selectedPlan.level }}</el-descriptions-item>
-              <el-descriptions-item label="发生区域">{{ selectedPlan.region }}</el-descriptions-item>
+              <el-descriptions-item label="预案类型">{{ selectedPlan.type }}</el-descriptions-item>
+              <el-descriptions-item label="预案等级">{{ selectedPlan.level }}</el-descriptions-item>
               <el-descriptions-item label="责任单位">{{ selectedPlan.unit }}</el-descriptions-item>
             </el-descriptions>
             <div class="detail-summary">
@@ -199,8 +165,8 @@
       <div v-if="selectedPlan" style="line-height:2">
         <p><strong>预案名称:</strong>{{ selectedPlan.name }}</p>
         <p><strong>匹配度:</strong>{{ selectedPlan.matchScore }}%</p>
-        <p><strong>事件类型:</strong>{{ selectedPlan.type }} | <strong>级别:</strong>{{ selectedPlan.level }}</p>
-        <p><strong>区域:</strong>{{ selectedPlan.region }} | <strong>责任单位:</strong>{{ selectedPlan.unit }}</p>
+        <p><strong>预案类型:</strong>{{ selectedPlan.type }} | <strong>等级:</strong>{{ selectedPlan.level }}</p>
+        <p><strong>责任单位:</strong>{{ selectedPlan.unit }}</p>
         <el-divider />
         <p>预案内容将分发至应急指挥中心及相应处置单位,确定应用此预案?</p>
       </div>
@@ -215,51 +181,41 @@
 <script setup>
 import { ref, reactive } from 'vue'
 import { ElMessage } from 'element-plus'
-import { Setting, InfoFilled, Clock, Promotion, Document, Checked, Search } from '@element-plus/icons-vue'
-import { matchPlan, matchPlanByTopic, getPlanMatchHistory, applyPlan as applyPlanApi } from '@/api/dispatch'
+import { Setting, InfoFilled, Clock, Document, Checked, Search } from '@element-plus/icons-vue'
+import { useDict } from '@/utils/dict'
+import { matchPlan, getPlanMatchHistory, applyPlan as applyPlanApi } from '@/api/dispatch'
 
-// 下拉选项(排水/供水/燃气 三类规划)
-const eventTypeOptions = ['排水', '供水', '燃气']
-const eventLevelOptions = ['一般(IV级)', '较大(III级)', '重大(II级)', '特别重大(I级)']
-const eventRegionOptions = ['城南', '城北', '太常']
+const { dis_plan_type, dis_plan_level } = useDict('dis_plan_type', 'dis_plan_level')
+const dictLabel = (list, value) => {
+  const hit = (list || []).find(item => String(item.value) === String(value))
+  return hit ? hit.label : (value || '-')
+}
 const unitOptions = ['供水局', '燃气公司', '排水公司']
 
 // 表单
 const form = reactive({
-  eventType: '排水',
-  eventLevel: '较大(III级)',
-  eventRegion: '城南',
+  eventType: '2',
+  eventLevel: '2',
   responsibleUnit: '排水公司'
 })
 
 // 数字预案库由后端接口维护(dis_plan_library 表)
 
-// 专题列表(对应三大类)
-const topics = ref([
-  { label: '排水专题', value: '排水专题' },
-  { label: '供水专题', value: '供水专题' },
-  { label: '燃气专题', value: '燃气专题' }
-])
-
 // 最近匹配记录(接口加载)
 const matchHistory = ref([])
 
 // 状态
 const matchResults = ref([])
 const selectedPlan = ref(null)
-const activeTopic = ref('')
 const applyDialogVisible = ref(false)
 
 // 后端匹配结果 → 页面展示字段映射
 const toDisplayPlan = (vo) => ({
   id: vo.id,
   name: vo.planName,
-  type: vo.eventType,
-  level: vo.eventLevel,
-  region: vo.eventRegion,
+  type: dictLabel(dis_plan_type.value, vo.eventType),
+  level: dictLabel(dis_plan_level.value, vo.eventLevel),
   unit: vo.responsibleUnit,
-  topic: vo.topic,
-  summary: vo.planSummary,
   content: vo.planContent,
   matchScore: vo.matchScore
 })
@@ -279,23 +235,8 @@ const loadHistory = async () => {
   }
 }
 
-// 专题匹配
-const matchByTopic = async (topicName) => {
-  activeTopic.value = topicName
-  try {
-    const res = await matchPlanByTopic({ topic: topicName })
-    const matched = (res.data || []).map(toDisplayPlan)
-    matchResults.value = matched
-    selectedPlan.value = matched.length > 0 ? matched[0] : null
-    loadHistory()
-  } catch (e) {
-    ElMessage.error('专题匹配失败')
-  }
-}
-
 // 手动匹配
 const manualMatch = async () => {
-  activeTopic.value = ''
   try {
     const res = await matchPlan({ ...form })
     const matched = (res.data || []).map(toDisplayPlan)
@@ -453,20 +394,6 @@ init()
   color: #303133;
 }
 
-/* 专题按钮 */
-.topic-row {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
-}
-
-.topic-tip {
-  margin-top: 10px;
-  font-size: 12px;
-  color: #909399;
-  text-align: center;
-}
-
 /* 预案列表 */
 .plan-list {
   max-height: 360px;

+ 211 - 0
src/views/subSystem/dispatch/disOnDuty/EventDetailDrawer.vue

@@ -0,0 +1,211 @@
+<template>
+  <el-drawer v-model="detailVisible" size="min(900px, 96vw)" :close-on-click-modal="false" class="event-detail-drawer" direction="rtl">
+    <template #header>
+      <div class="event-detail-header">
+        <div class="event-detail-heading">
+          <span class="event-detail-title">事件详情</span>
+          <span class="event-detail-no">{{ currentRow?.eventNo || '-' }}</span>
+        </div>
+        <el-tag :type="statusTagType(currentRow?.status)" size="small">{{ currentRow?.statusName || '-' }}</el-tag>
+      </div>
+    </template>
+    <el-skeleton v-if="loading" :rows="8" animated />
+    <el-alert v-else-if="loadFailed" title="事件详情加载失败,请重试" type="error" :closable="false" show-icon>
+      <el-button type="primary" link @click="open(currentRow)">重新加载</el-button>
+    </el-alert>
+    <div v-else-if="currentRow" class="detail-content">
+      <section class="detail-section detail-section-first">
+        <div class="section-heading"><span>事件信息</span></div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="事件描述" :span="2"><div class="detail-text">{{ currentRow.description || '-' }}</div></el-descriptions-item>
+          <el-descriptions-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-descriptions-item>
+          <el-descriptions-item label="发生地点">{{ currentRow.location || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="发生时间">{{ currentRow.happenTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="接报时间">{{ currentRow.reportTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="上报人电话">{{ currentRow.reporterPhone || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="事件坐标" :span="2">{{ formatGeo(currentRow.longitude, currentRow.latitude) }}</el-descriptions-item>
+        </el-descriptions>
+      </section>
+
+      <section class="detail-section">
+        <div class="section-heading"><span>处置相关人员</span></div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="接报人">{{ currentRow.reporter || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="处置单位">{{ currentRow.department || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="处置人">{{ currentRow.handler || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="核验人">{{ currentRow.verifier || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </section>
+
+      <section class="detail-section">
+        <div class="section-heading"><span>事件记录</span></div>
+        <EventLogTimeline v-if="currentRow.logs && currentRow.logs.length" :logs="currentRow.logs" />
+        <div v-else class="empty-detail">暂无记录</div>
+      </section>
+    </div>
+    <template #footer>
+      <el-button @click="detailVisible = false">关闭</el-button>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup>
+import { ref, watch, onBeforeUnmount } from 'vue'
+import { useDict } from '@/utils/dict'
+import { getEventDetail } from '@/api/dispatch'
+import EventLogTimeline from './EventLogTimeline.vue'
+
+const { dis_event_source } = useDict('dis_event_source')
+const detailVisible = ref(false)
+const currentRow = ref(null)
+const loading = ref(false)
+const loadFailed = ref(false)
+let requestId = 0
+
+const STATUS_TAG = { 0: 'warning', 1: 'success', 2: 'primary', 3: 'warning', 4: 'info' }
+function statusTagType(status) {
+  return STATUS_TAG[status] || 'info'
+}
+
+function dictLabel(list, value) {
+  if (value === null || value === undefined || value === '') return '-'
+  const hit = (list || []).find(item => String(item.value) === String(value))
+  return hit ? hit.label : value
+}
+
+function formatGeo(lng, lat) {
+  if (lng === null || lng === undefined || lng === '' || lat === null || lat === undefined || lat === '') return '-'
+  return `${Number(lng).toFixed(6)}, ${Number(lat).toFixed(6)}`
+}
+
+// 四个业务页面共用事件档案接口,列表行仅用于加载期间的标题展示。
+async function open(row) {
+  const id = ++requestId
+  currentRow.value = { ...row, logs: [] }
+  detailVisible.value = true
+  loading.value = true
+  loadFailed.value = false
+  try {
+    const res = await getEventDetail(row.id)
+    if (id !== requestId || !detailVisible.value) return
+    if (!res.data) throw new Error('事件不存在')
+    currentRow.value = { ...res.data, logs: res.data.logs || [] }
+  } catch {
+    if (id === requestId && detailVisible.value) loadFailed.value = true
+  } finally {
+    if (id === requestId) loading.value = false
+  }
+}
+
+// 关闭或切换事件后,忽略先前尚未完成的请求。
+watch(detailVisible, visible => {
+  if (!visible) requestId++
+}, { flush: 'sync' })
+onBeforeUnmount(() => { requestId++ })
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.detail-content {
+  padding: 0 4px 8px;
+}
+
+.event-detail-header {
+  display: flex;
+  width: 100%;
+  min-width: 0;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.event-detail-heading {
+  display: flex;
+  min-width: 0;
+  align-items: baseline;
+  gap: 12px;
+}
+
+.event-detail-title {
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.event-detail-no {
+  overflow: hidden;
+  color: #909399;
+  font-family: ui-monospace, Consolas, "Courier New", monospace;
+  font-size: 12px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.detail-section {
+  margin-top: 24px;
+}
+
+.detail-section-first {
+  margin-top: 0;
+}
+
+.section-heading {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+  color: #303133;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.section-heading::before {
+  width: 3px;
+  height: 14px;
+  border-radius: 2px;
+  background: #3b5bdb;
+  content: '';
+}
+
+.detail-text {
+  line-height: 1.7;
+  overflow-wrap: anywhere;
+  white-space: pre-wrap;
+}
+
+.empty-detail {
+  padding: 16px 0;
+  color: #909399;
+  font-size: 13px;
+  text-align: center;
+}
+
+:global(.event-detail-drawer .el-drawer__header) {
+  margin-bottom: 0;
+  padding: 16px 20px 10px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+:global(.event-detail-drawer .el-drawer__body) {
+  padding: 12px 20px 24px;
+}
+
+:global(.event-detail-drawer .el-descriptions__label) {
+  width: 96px;
+}
+
+@media (max-width: 768px) {
+  .event-detail-heading {
+    align-items: flex-start;
+    flex-direction: column;
+    gap: 2px;
+  }
+
+  :global(.event-detail-drawer .el-drawer__header),
+  :global(.event-detail-drawer .el-drawer__body) {
+    padding-right: 14px;
+    padding-left: 14px;
+  }
+}
+</style>

+ 86 - 155
src/views/subSystem/dispatch/disOnDuty/cphs.vue

@@ -40,9 +40,6 @@
     <div class="table-panel">
       <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
-        <el-table-column prop="eventType" label="管网类型" width="100">
-          <template #default="{ row }">{{ dictLabel(dis_event_type, row.eventType) }}</template>
-        </el-table-column>
         <el-table-column prop="eventCategory" label="事件类别" width="110">
           <template #default="{ row }">{{ dictLabel(dis_event_category, row.eventCategory) }}</template>
         </el-table-column>
@@ -53,9 +50,10 @@
           </template>
         </el-table-column>
         <el-table-column prop="location" label="发生地点" show-overflow-tooltip />
+        <el-table-column prop="happenTime" label="发生时间" width="150" />
         <el-table-column prop="verifyTime" label="初判时间" width="150" />
-        <el-table-column prop="createTime" label="创建时间" width="150" />
-        <el-table-column prop="verifier" label="核实人" width="100" />
+        <el-table-column prop="reportTime" label="接报时间" width="150" />
+        <el-table-column prop="verifier" label="核验人" width="100" />
         <el-table-column prop="statusName" label="状态" width="100">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
@@ -118,6 +116,13 @@
           <div class="desc-text">{{ verifyForm.description }}</div>
         </el-form-item>
         <div class="section-title">核实信息</div>
+        <el-form-item label="核实结论" prop="verifyResult">
+          <el-radio-group v-model="verifyForm.verifyResult" @change="handleVerifyResultChange">
+            <el-radio :label="0">真实</el-radio>
+            <el-radio :label="1">不真实</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <template v-if="verifyForm.verifyResult === 0">
         <el-form-item label="事件类别" prop="eventCategory">
           <el-select v-model="verifyForm.eventCategory" placeholder="请选择" style="width:100%">
             <el-option v-for="d in dis_event_category" :key="d.value" :label="d.label" :value="d.value" />
@@ -128,7 +133,7 @@
             <el-option v-for="d in dis_event_level" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
         </el-form-item>
-        <el-form-item label="责任单位" prop="department">
+        <el-form-item label="处置单位" prop="deptId">
           <el-tree-select
             v-model="verifyForm.deptId"
             :data="deptTree"
@@ -136,33 +141,31 @@
             node-key="deptId"
             check-strictly
             filterable
-            placeholder="请选择责任单位(系统部门)"
+            placeholder="请选择处置单位(系统部门)"
             style="width:100%"
             @change="handleDeptChange"
           />
         </el-form-item>
-        <el-form-item label="责任处置人" prop="handler">
+        <el-form-item label="处置人" prop="handlerId">
           <el-select
-            v-model="verifyForm.handler"
-            :placeholder="verifyForm.deptId ? '请选择责任处置人(该部门人员)' : '请选择责任处置人(系统用户)'"
+            v-model="verifyForm.handlerId"
+            :placeholder="verifyForm.deptId ? '请选择处置人(该单位及下级部门人员)' : '请先选择处置单位'"
             filterable
             style="width:100%"
+            :disabled="!verifyForm.deptId"
             @change="handleHandlerChange"
           >
-            <el-option v-for="u in filteredHandlerOptions" :key="u.userId" :label="u.deptName ? `${u.nickName}(${u.deptName})` : u.nickName" :value="u.nickName" />
+            <el-option v-for="u in filteredHandlerOptions" :key="u.userId" :label="u.deptName ? `${u.nickName}(${u.deptName})` : u.nickName" :value="u.userId" />
           </el-select>
         </el-form-item>
-        <el-form-item label="联系电话" prop="handlerPhone">
-          <el-input v-model="verifyForm.handlerPhone" placeholder="选人自动带出;未维护时可手动填写" />
+        <el-form-item label="核验人" prop="verifierId">
+          <el-select v-model="verifyForm.verifierId" placeholder="请选择核验人" filterable clearable style="width:100%" @change="handleVerifierChange">
+            <el-option v-for="u in userOptions" :key="u.userId" :label="u.deptName ? `${u.nickName}(${u.deptName})` : u.nickName" :value="u.userId" />
+          </el-select>
         </el-form-item>
+        </template>
         <el-form-item label="核实意见" prop="verifyOpinion">
-          <el-input v-model="verifyForm.verifyOpinion" type="textarea" :rows="3" placeholder="请输入核实意见" />
-        </el-form-item>
-        <el-form-item label="是否真实" prop="verifyResult">
-          <el-radio-group v-model="verifyForm.verifyResult">
-            <el-radio :label="0">真实</el-radio>
-            <el-radio :label="1">不实</el-radio>
-          </el-radio-group>
+          <el-input v-model="verifyForm.verifyOpinion" type="textarea" :rows="3" placeholder="请输入核实意见(可选)" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -171,80 +174,7 @@
       </template>
     </el-dialog>
 
-    <!-- 详情弹窗 -->
-    <el-drawer v-model="detailVisible" title="事件详情" size="600px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
-      <div v-if="currentRow" class="detail-content">
-        <el-form label-width="100px" size="default">
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件类别">{{ dictLabel(dis_event_category, currentRow.eventCategory) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件等级">{{ dictLabel(dis_event_level, currentRow.eventLevel) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="发生地点">{{ currentRow.location || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="发生时间">{{ currentRow.happenTime || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件坐标">{{ formatGeo(currentRow.longitude, currentRow.latitude) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="事件描述">
-            <div class="desc-text">{{ currentRow.description || '-' }}</div>
-          </el-form-item>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="核实人">{{ currentRow.verifier || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="核实时间">{{ currentRow.verifyTime || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="核实意见">
-            <div class="desc-text">{{ currentRow.verifyOpinion || '-' }}</div>
-          </el-form-item>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="责任单位">{{ currentRow.department || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="责任处置人">
-                <template v-if="currentRow.handler">{{ currentRow.handler }}<span v-if="currentRow.handlerPhone">({{ currentRow.handlerPhone }})</span></template>
-                <template v-else>-</template>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="状态">
-            <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
-          </el-form-item>
-          <div class="section-title">事件记录(操作日志)</div>
-          <!-- 参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件缩略图 -->
-          <EventLogTimeline v-if="currentRow.rawLogs && currentRow.rawLogs.length" :logs="currentRow.rawLogs" />
-          <div v-else class="desc-text">暂无记录</div>
-        </el-form>
-      </div>
-      <template #footer>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </template>
-    </el-drawer>
+    <EventDetailDrawer ref="eventDetailRef" />
   </div>
 </template>
 
@@ -253,17 +183,20 @@ import { ref, computed, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useDict } from '@/utils/dict'
 import { handleTree } from '@/utils/ruoyi'
+import useUserStore from '@/store/modules/user'
 import { bd09ToWgs84 } from '@/utils/coordTransform'
 import { gisPointStatusIcons } from '@/utils/gisMapColors'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
-import EventLogTimeline from './EventLogTimeline.vue'
-import { getEventPage, deleteEventInfo, verifyEventInfo, getEventStatusOptions, getEventLogs, getUserOptions, getDeptOptions } from '@/api/dispatch'
+import EventDetailDrawer from './EventDetailDrawer.vue'
+import { getEventPage, deleteEventInfo, verifyEventInfo, getEventStatusOptions, getUserOptions, getDeptOptions } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
 
 // ==================== 字典 ====================
-const { dis_event_source, dis_event_type, dis_event_category, dis_event_level } = useDict(
-  'dis_event_source', 'dis_event_type', 'dis_event_category', 'dis_event_level'
+const { dis_event_category, dis_event_level } = useDict(
+  'dis_event_category', 'dis_event_level'
 )
+const userStore = useUserStore()
+const currentUserId = Number(userStore.id) || null
 
 // 核实环节可选状态:待核实(0) / 已核实(1) / 不实事件(4),其余状态不属于本页
 const verifyStatusOptions = ref([])
@@ -289,10 +222,8 @@ function formatGeo(lng, lat) {
   return `${Number(lng).toFixed(6)}, ${Number(lat).toFixed(6)}`
 }
 
-// ==================== 责任单位(系统部门)+ 责任处置人(该部门人员) ====================
-// 责任单位由手填改为选择系统部门(el-tree-select 树形);选中后责任处置人候选
-// 限定为该部门所属人员,选人自动带出联系电话(用户未维护电话时可手填)。
-// 提交时仍按 dis_event.department 的既有口径写「部门中文名」。
+// ==================== 处置单位 + 处置人 ====================
+// 处置人候选限定在所选单位及其全部下级部门。
 const userOptions = ref([])
 const deptList = ref([])
 const deptTree = computed(() => {
@@ -318,32 +249,48 @@ async function loadDeptOptions() {
   }
 }
 
-// 责任处置人候选:选了责任单位 → 只显示该单位人员;未选 → 显示全部启用用户,不强制先选单位
+// 处置人候选包含所选处置单位及其所有下级部门。
 const filteredHandlerOptions = computed(() => {
-  if (!verifyForm.value.deptId) return userOptions.value
-  return userOptions.value.filter(u => String(u.deptId) === String(verifyForm.value.deptId))
+  if (!verifyForm.value.deptId) return []
+  const allowed = new Set([String(verifyForm.value.deptId)])
+  let changed = true
+  while (changed) {
+    changed = false
+    deptList.value.forEach(dept => {
+      if (allowed.has(String(dept.parentId)) && !allowed.has(String(dept.deptId))) {
+        allowed.add(String(dept.deptId))
+        changed = true
+      }
+    })
+  }
+  return userOptions.value.filter(u => allowed.has(String(u.deptId)))
 })
 
 function handleDeptChange() {
-  // 切换责任单位后,原处置人可能不属于新部门,一并清空重选
+  // 切换处置单位后,原处置人需重新选择。
   verifyForm.value.handler = ''
   verifyForm.value.handlerId = null
-  verifyForm.value.handlerPhone = ''
-}
-
-function handleHandlerChange(nickName) {
-  const user = filteredHandlerOptions.value.find(item => item.nickName === nickName)
-  if (user) {
-    // 选中系统用户后:记下用户ID(随核实请求落库),并自动带出联系电话
-    verifyForm.value.handlerId = user.userId
-    if (user.phonenumber) {
-      verifyForm.value.handlerPhone = user.phonenumber
-    } else {
-      // 用户信息里没维护电话:清空旧值,提示手动填写
-      verifyForm.value.handlerPhone = ''
-      ElMessage.info('该用户未维护联系电话,请手动填写')
-    }
-  }
+}
+
+function handleHandlerChange(userId) {
+  verifyForm.value.handler = filteredHandlerOptions.value.find(item => String(item.userId) === String(userId))?.nickName || ''
+}
+
+function handleVerifierChange(userId) {
+  verifyForm.value.verifier = userOptions.value.find(item => String(item.userId) === String(userId))?.nickName || ''
+}
+
+function handleVerifyResultChange(value) {
+  if (value !== 1) return
+  verifyForm.value.eventCategory = ''
+  verifyForm.value.eventLevel = ''
+  verifyForm.value.deptId = null
+  verifyForm.value.department = ''
+  verifyForm.value.handler = ''
+  verifyForm.value.handlerId = null
+  verifyForm.value.verifier = ''
+  verifyForm.value.verifierId = null
+  nextTick(() => verifyFormRef.value?.clearValidate(['eventCategory', 'eventLevel', 'deptId', 'handlerId', 'verifierId']))
 }
 
 // 按部门名称反查部门ID(核实回显用:department 存的是部门中文名)
@@ -552,17 +499,17 @@ const currentVerifyId = ref(null)
 const verifyForm = ref({
   eventNo: '', location: '', longitude: null, latitude: null, reporter: '', description: '',
   eventCategory: '', eventLevel: '',
-  deptId: null, department: '', handler: '', handlerId: null, handlerPhone: '', verifyOpinion: '', verifyResult: 0
+  deptId: null, department: '', handler: '', handlerId: null, verifier: userStore.nickName || '', verifierId: currentUserId, verifyOpinion: '', verifyResult: 0
 })
-// 类别/等级/责任处置人只在「真实」时必填;「不实」只需核销原因
+// 类别、等级、处置单位只在真实事件时展示并校验;不真实事件同样允许填写核实意见。
 const verifyRules = computed(() => {
   const isReal = verifyForm.value.verifyResult === 0
   return {
     eventCategory: isReal ? [{ required: true, message: '请选择事件类别', trigger: 'change' }] : [],
     eventLevel: isReal ? [{ required: true, message: '请选择事件等级', trigger: 'change' }] : [],
-    handler: isReal ? [{ required: true, message: '请输入责任处置人', trigger: 'blur' }] : [],
-    verifyOpinion: [{ required: true, message: '请输入核实意见', trigger: 'blur' }],
-    verifyResult: [{ required: true, message: '请选择是否真实', trigger: 'change' }]
+    deptId: isReal ? [{ required: true, message: '请选择处置单位', trigger: 'change' }] : [],
+    verifyOpinion: [],
+    verifyResult: [{ required: true, message: '请选择核实结论', trigger: 'change' }]
   }
 })
 
@@ -582,7 +529,8 @@ function handleVerify(row) {
     department: row.department || '',
     handler: row.handler || '',
     handlerId: row.handlerId || null,
-    handlerPhone: row.handlerPhone || '',
+    verifier: userStore.nickName || '',
+    verifierId: currentUserId,
     verifyOpinion: row.verifyOpinion || '',
     verifyResult: 0
   }
@@ -593,7 +541,9 @@ function handleVerify(row) {
 
 async function confirmVerify() {
   try {
-    await verifyFormRef.value.validate()
+    await verifyFormRef.value.validateField(verifyForm.value.verifyResult === 0
+      ? ['verifyResult', 'eventCategory', 'eventLevel', 'deptId', 'verifyOpinion']
+      : ['verifyResult', 'verifyOpinion'])
   } catch {
     return
   }
@@ -605,10 +555,12 @@ async function confirmVerify() {
       eventCategory: verifyForm.value.eventCategory,
       eventLevel: verifyForm.value.eventLevel,
       department: selectedDept ? selectedDept.deptName : verifyForm.value.department,
+      deptId: verifyForm.value.deptId,
       handler: verifyForm.value.handler,
       handlerId: verifyForm.value.handlerId,
-      handlerPhone: verifyForm.value.handlerPhone,
-      verifyOpinion: verifyForm.value.verifyOpinion
+      verifyOpinion: verifyForm.value.verifyOpinion,
+      verifier: verifyForm.value.verifier,
+      verifierId: verifyForm.value.verifierId
     })
     ElMessage.success('核实成功')
     verifyVisible.value = false
@@ -633,18 +585,10 @@ async function handleDelete(row) {
 }
 
 // ==================== 详情 ====================
-const detailVisible = ref(false)
-const currentRow = ref(null)
+const eventDetailRef = ref(null)
 
-async function handleDetail(row) {
-  currentRow.value = { ...row, rawLogs: [] }
-  detailVisible.value = true
-  try {
-    const res = await getEventLogs(row.id)
-    currentRow.value = { ...currentRow.value, rawLogs: res.data || [] }
-  } catch {
-    // 加载失败时保留列表行数据
-  }
+function handleDetail(row) {
+  eventDetailRef.value?.open(row)
 }
 </script>
 
@@ -714,19 +658,6 @@ async function handleDetail(row) {
   font-size: 16px;
 }
 
-.detail-content {
-  padding: 0 8px;
-}
-
-.section-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  margin-bottom: 12px;
-  padding-left: 8px;
-  border-left: 3px solid #409EFF;
-}
-
 .desc-text {
   background: #f5f7fa;
   border-radius: 4px;

+ 7 - 7
src/views/subSystem/dispatch/disOnDuty/gzcb.vue

@@ -8,8 +8,8 @@
             <el-option v-for="d in urgeStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
         </el-form-item>
-        <el-form-item label="责任人">
-          <el-input v-model="searchForm.handler" placeholder="请输入责任人" clearable style="width:140px" @keyup.enter="handleSearch" />
+        <el-form-item label="处置人">
+          <el-input v-model="searchForm.handler" placeholder="请输入处置人" clearable style="width:140px" @keyup.enter="handleSearch" />
         </el-form-item>
         <el-form-item label="事件编号">
           <el-input v-model="searchForm.eventNo" placeholder="请输入事件编号" clearable style="width:160px" @keyup.enter="handleSearch" />
@@ -116,9 +116,9 @@
             <el-form-item label="部门">{{ urgeForm.receiverDept || '-' }}</el-form-item>
           </el-col>
         </el-row>
-        <el-form-item label="责任人电话">
+        <el-form-item label="处置人电话">
           <span v-if="urgeForm.receiverPhone" style="color:#2f6fe4;font-weight:600;font-size:14px">{{ urgeForm.receiverPhone }}</span>
-          <span v-else style="color:#c0c4cc;font-size:13px">责任人未维护联系电话,请改用电话/短信以外的催办方式</span>
+          <span v-else style="color:#c0c4cc;font-size:13px">处置人未维护联系电话,请改用电话/短信以外的催办方式</span>
         </el-form-item>
         <div class="section-title">催办内容</div>
         <el-form-item label="催办方式" prop="methods">
@@ -195,13 +195,13 @@ function methodLabel(value) {
   return String(value).split('、').map(item => dictLabel(dis_event_urge_method.value, item)).join('、')
 }
 
-// 未办结状态均可催办(0 待核实 1 已核实 2 处置中)
+// 工作催办只面向已核实、处置中事件。
 const urgeStatusOptions = ref([])
 
 async function loadStatusOptions() {
   try {
     const res = await getEventStatusOptions()
-    urgeStatusOptions.value = (res.data || []).filter(item => [0, 1, 2].includes(item.value))
+    urgeStatusOptions.value = (res.data || []).filter(item => [1, 2].includes(item.value))
   } catch {
     urgeStatusOptions.value = []
   }
@@ -214,7 +214,7 @@ function statusTagType(status) {
 }
 
 function canUrge(row) {
-  return [0, 1, 2].includes(row.status)
+  return [1, 2].includes(row.status)
 }
 
 // ==================== 数据状态 ====================

+ 78 - 328
src/views/subSystem/dispatch/disOnDuty/gzjc.vue

@@ -35,9 +35,6 @@
     <div class="table-panel">
       <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
-        <el-table-column prop="eventType" label="管网类型" width="100">
-          <template #default="{ row }">{{ dictLabel(dis_event_type, row.eventType) }}</template>
-        </el-table-column>
         <el-table-column prop="eventCategory" label="事件类别" width="110">
           <template #default="{ row }">{{ dictLabel(dis_event_category, row.eventCategory) }}</template>
         </el-table-column>
@@ -55,7 +52,7 @@
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="handler" label="负责人" width="100">
+        <el-table-column prop="handler" label="处置人" width="100">
           <template #default="{ row }">{{ row.handler || '-' }}</template>
         </el-table-column>
         <el-table-column prop="lastVisitTime" label="最近回访" width="150">
@@ -67,8 +64,8 @@
         <el-table-column prop="createTime" label="创建时间" width="150" />
         <el-table-column label="操作" width="260" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="row.status === 1" type="primary" plain size="small" @click="handleStart(row)" v-hasPermi="['dispatch:event:dispose']">开始处置</el-button>
-            <el-button v-if="row.status === 2" type="warning" plain size="small" @click="handleTrack(row)" v-hasPermi="['dispatch:event:track']">跟踪</el-button>
+            <el-button v-if="row.status === 1" type="primary" plain size="small" @click="openOperation(row, 'start')" v-hasPermi="['dispatch:event:dispose']">开始处置</el-button>
+            <el-button v-if="row.status === 2" type="warning" plain size="small" @click="openOperation(row, 'track')" v-hasPermi="['dispatch:event:track']">跟踪</el-button>
             <el-button v-if="row.status === 2" type="success" plain size="small" @click="handleRelease(row)" v-hasPermi="['dispatch:event:release']">解除</el-button>
             <el-button type="info" plain size="small" @click="handleDetail(row)">详情</el-button>
             <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
@@ -88,70 +85,34 @@
       </div>
     </div>
 
-    <!-- 跟踪弹窗 -->
-    <el-dialog v-model="trackVisible" title="事件跟踪" width="700px" :close-on-click-modal="false">
-      <div v-if="currentRow">
-        <div class="section-title">事件信息</div>
-        <el-form label-width="100px" size="default" style="margin-bottom:12px">
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="当前状态">
-                <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="负责人">{{ currentRow.handler || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="发生地点">
-            <div class="desc-text">{{ currentRow.location || '-' }}</div>
-          </el-form-item>
-        </el-form>
-
-        <div class="section-title">事件进展时间线</div>
-        <el-timeline>
-          <el-timeline-item
-            v-for="(item, index) in currentRow.timeline"
-            :key="index"
-            :type="item.type"
-            :timestamp="item.time"
+    <!-- 开始处置和跟踪共用备注+附件表单 -->
+    <el-dialog v-model="operationVisible" :title="operationType === 'start' ? '开始处置' : '事件跟踪'" width="600px" :close-on-click-modal="false">
+      <el-form :model="operationForm" :rules="operationRules" ref="operationFormRef" label-width="100px">
+        <el-form-item label="备注" prop="operDesc">
+          <el-input v-model="operationForm.operDesc" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="附件">
+          <el-upload
+            ref="operationUploadRef"
+            v-model:file-list="operationForm.fileList"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            :limit="5"
+            multiple
+            accept=".jpg,.jpeg,.png,.gif,.bmp,.webp,.mp4,.mov,.m4v,.webm"
+            :on-success="handleOperationAttachSuccess"
+            :on-error="handleAttachError"
+            :on-exceed="() => ElMessage.warning('附件最多 5 个')"
+            :before-upload="handleAttachBefore"
           >
-            <div style="font-weight:600">{{ item.title }}</div>
-            <div style="color:#606266;font-size:13px">{{ item.content }}</div>
-          </el-timeline-item>
-        </el-timeline>
-
-        <div class="section-title">新增跟踪记录</div>
-        <el-form :model="trackForm" :rules="trackRules" ref="trackFormRef" label-width="120px">
-          <el-form-item label="跟踪内容" prop="operDesc">
-            <el-input v-model="trackForm.operDesc" type="textarea" :rows="3" placeholder="请输入跟踪内容" />
-          </el-form-item>
-          <el-form-item label="当前态势评估" prop="trend">
-            <el-radio-group v-model="trackForm.trend">
-              <el-radio v-for="d in dis_event_trend" :key="d.value" :label="d.value">{{ d.label }}</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="是否需要升级" prop="needUpgrade">
-            <el-radio-group v-model="trackForm.needUpgrade">
-              <el-radio v-for="d in dis_yes_no" :key="d.value" :label="Number(d.value)">{{ d.label }}</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="下次回访时间" prop="nextVisitTime">
-            <el-date-picker v-model="trackForm.nextVisitTime" type="datetime" placeholder="选择日期时间" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
-          </el-form-item>
-        </el-form>
-      </div>
+            <el-button type="primary" plain>上传附件</el-button>
+            <template #tip><div class="el-upload__tip">图片或视频,单个不超过 50MB,最多 5 个</div></template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
       <template #footer>
-        <el-button @click="trackVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmTrack">保存跟踪记录</el-button>
+        <el-button @click="operationVisible = false">取消</el-button>
+        <el-button type="primary" :loading="operationSubmitting" @click="confirmOperation">{{ operationType === 'start' ? '开始处置' : '保存跟踪' }}</el-button>
       </template>
     </el-dialog>
 
@@ -220,122 +181,7 @@
       </template>
     </el-dialog>
 
-    <!-- 详情弹窗 -->
-    <el-drawer v-model="detailVisible" title="事件详情" size="600px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
-      <div v-if="currentRow" class="detail-content">
-        <el-form label-width="100px" size="default">
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件类别">{{ dictLabel(dis_event_category, currentRow.eventCategory) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件等级">
-                <el-tag v-if="currentRow.eventLevel" :type="levelTagType(currentRow.eventLevel)" size="small">{{ dictLabel(dis_event_level, currentRow.eventLevel) }}</el-tag>
-                <span v-else>-</span>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="当前状态">
-                <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="负责人">{{ currentRow.handler || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="发生地点">{{ currentRow.location || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="最近回访">{{ currentRow.lastVisitTime || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="下次回访">{{ currentRow.nextVisitTime || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="解除时间">{{ currentRow.releaseTime || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="损失(万元)">{{ formatLoss(currentRow.lossAmount) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item v-if="currentRow.beforeUrl" label="处置前佐证">
-            <template v-for="(item, i) in attachItems(currentRow.beforeUrl)" :key="'b' + i">
-              <el-image
-                v-if="isImage(item.name)"
-                :src="item.url"
-                :preview-src-list="[item.url]"
-                preview-teleported
-                fit="cover"
-                :initial-index="0"
-                style="width:64px;height:48px;border-radius:4px;border:1px solid #ebeef5;margin:0 8px 6px 0;cursor:zoom-in"
-              />
-              <el-link
-                v-else
-                type="primary"
-                style="margin-right:16px;font-size:13px"
-                @click="downloadAttach(item.url, item.name)"
-              >{{ item.name }}(点击下载)</el-link>
-            </template>
-          </el-form-item>
-          <el-form-item v-if="currentRow.afterUrl" label="处置后佐证">
-            <template v-for="(item, i) in attachItems(currentRow.afterUrl)" :key="'a' + i">
-              <el-image
-                v-if="isImage(item.name)"
-                :src="item.url"
-                :preview-src-list="[item.url]"
-                preview-teleported
-                fit="cover"
-                :initial-index="0"
-                style="width:64px;height:48px;border-radius:4px;border:1px solid #ebeef5;margin:0 8px 6px 0;cursor:zoom-in"
-              />
-              <el-link
-                v-else
-                type="primary"
-                style="margin-right:16px;font-size:13px"
-                @click="downloadAttach(item.url, item.name)"
-              >{{ item.name }}(点击下载)</el-link>
-            </template>
-          </el-form-item>
-          <el-form-item label="事件描述">
-            <div class="desc-text">{{ currentRow.description || '-' }}</div>
-          </el-form-item>
-          <div class="section-title">处置时间线</div>
-          <el-timeline>
-            <el-timeline-item
-              v-for="(item, index) in currentRow.timeline"
-              :key="index"
-              :type="item.type"
-              :timestamp="item.time"
-            >
-              <div style="font-weight:600">{{ item.title }}</div>
-              <div style="color:#606266;font-size:13px">{{ item.content }}</div>
-            </el-timeline-item>
-          </el-timeline>
-        </el-form>
-      </div>
-      <template #footer>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </template>
-    </el-drawer>
+    <EventDetailDrawer ref="eventDetailRef" />
   </div>
 </template>
 
@@ -346,53 +192,17 @@ import { useDict } from '@/utils/dict'
 import { getToken } from '@/utils/auth'
 import {
   getEventPage, deleteEventInfo, startDisposeEvent, trackEvent,
-  releaseEventInfo, getEventLogs, getEventStatusOptions
+  releaseEventInfo, getEventStatusOptions
 } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
 
+import EventDetailDrawer from './EventDetailDrawer.vue'
+
 // ==================== 字典 ====================
-const { dis_event_source, dis_event_type, dis_event_category, dis_event_level, dis_event_trend, dis_event_release_reason, dis_yes_no } = useDict(
-  'dis_event_source', 'dis_event_type', 'dis_event_category', 'dis_event_level', 'dis_event_trend', 'dis_event_release_reason', 'dis_yes_no'
+const { dis_event_category, dis_event_level, dis_event_trend, dis_event_release_reason, dis_yes_no } = useDict(
+  'dis_event_category', 'dis_event_level', 'dis_event_trend', 'dis_event_release_reason', 'dis_yes_no'
 )
 
-// 损失金额展示:空显示 -,有值保留 2 位小数
-const formatLoss = (val) => (val === null || val === undefined || val === '') ? '-' : Number(val).toFixed(2)
-
-// ---------- 处置佐证附件(URL 逗号分隔 → {url,name} 列表;图片预览 / 视频下载) ----------
-const IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
-
-function attachUrls(attachUrl) {
-  return String(attachUrl || '').split(',').map(s => s.trim()).filter(Boolean)
-}
-
-function attachItems(attachUrl) {
-  return attachUrls(attachUrl).map(url => {
-    let name = url.substring(url.lastIndexOf('/') + 1)
-    try { name = decodeURIComponent(name) } catch { /* 保留原值 */ }
-    const underscore = name.indexOf('_')
-    return { url, name: underscore > 0 ? name.substring(underscore + 1) : name }
-  })
-}
-
-const isImage = (name) => IMAGE_EXT.includes(String(name).split('.').pop().toLowerCase())
-
-// 视频强制下载:fetch 转 blob 后用 a[download] 触发;CORS 拦截时兜底新窗口打开
-async function downloadAttach(url, name) {
-  if (!url) return
-  try {
-    const res = await fetch(url)
-    if (!res.ok) throw new Error('HTTP ' + res.status)
-    const blob = await res.blob()
-    const link = document.createElement('a')
-    link.href = URL.createObjectURL(blob)
-    link.download = name || '视频'
-    link.click()
-    URL.revokeObjectURL(link.href)
-  } catch {
-    window.open(url, '_blank')
-  }
-}
-
 function dictLabel(list, value) {
   if (value === null || value === undefined || value === '') return '-'
   const hit = (list || []).find(item => String(item.value) === String(value))
@@ -423,17 +233,6 @@ function levelTagType(level) {
   return LEVEL_TAG[level] || 'info'
 }
 
-// 操作日志 -> 时间线节点
-const LOG_TYPE = { 0: 'primary', 1: 'success', 2: 'info', 3: 'primary', 4: 'primary', 5: 'warning', 6: '', 7: 'success', 8: 'warning', 9: 'info' }
-function toTimeline(logs) {
-  return (logs || []).map(log => ({
-    type: LOG_TYPE[log.operType] ?? '',
-    time: log.operTime,
-    title: log.operTypeName || '操作',
-    content: log.operDesc || log.targets || '-'
-  }))
-}
-
 // ==================== 数据状态 ====================
 const tableData = ref([])
 const total = ref(0)
@@ -494,72 +293,61 @@ function handleCurrentChange(val) {
   loadData()
 }
 
-// ==================== 开始处置 ====================
-async function handleStart(row) {
-  try {
-    await ElMessageBox.confirm('确认开始处置【' + (row.eventNo || '') + '】吗?', '开始处置', { type: 'warning' })
-  } catch {
-    return
-  }
-  try {
-    await startDisposeEvent({ eventId: row.id, operDesc: '开始处置' })
-    ElMessage.success('已开始处置')
-    loadData()
-  } catch (error) {
-    ElMessage.error('操作失败:' + (error.message || '未知错误'))
-  }
-}
-
-// ==================== 跟踪 ====================
-const trackVisible = ref(false)
-const trackFormRef = ref(null)
+// ==================== 开始处置 / 跟踪 ====================
+const operationVisible = ref(false)
+const operationFormRef = ref(null)
+const operationUploadRef = ref(null)
 const currentRow = ref(null)
-const currentTrackId = ref(null)
-const trackForm = ref({ operDesc: '', trend: '', needUpgrade: 0, nextVisitTime: '' })
-const trackRules = {
-  operDesc: [{ required: true, message: '请输入跟踪内容', trigger: 'blur' }],
-  trend: [{ required: true, message: '请选择态势评估', trigger: 'change' }],
-  needUpgrade: [{ required: true, message: '请选择是否升级', trigger: 'change' }],
-  nextVisitTime: [{ required: true, message: '请选择下次回访时间', trigger: 'change' }]
+const operationType = ref('start')
+const operationSubmitting = ref(false)
+const operationForm = ref({ operDesc: '', fileList: [], attachUrls: [] })
+const operationRules = {
+  operDesc: [{ required: true, message: '请输入备注', trigger: 'blur' }]
 }
 
-async function handleTrack(row) {
-  currentRow.value = { ...row, timeline: [] }
-  currentTrackId.value = row.id
-  trackForm.value = { operDesc: '', trend: '1', needUpgrade: 0, nextVisitTime: '' }
-  trackVisible.value = true
-  nextTick(() => trackFormRef.value?.clearValidate())
-  loadTimeline(row.id)
+function openOperation(row, type) {
+  currentRow.value = { ...row }
+  operationType.value = type
+  operationForm.value = { operDesc: '', fileList: [], attachUrls: [] }
+  operationVisible.value = true
+  nextTick(() => operationFormRef.value?.clearValidate())
 }
 
-async function loadTimeline(eventId) {
-  try {
-    const res = await getEventLogs(eventId)
-    currentRow.value = { ...currentRow.value, timeline: toTimeline(res.data) }
-  } catch {
-    // 时间线加载失败时不影响跟踪录入
+function handleOperationAttachSuccess(res) {
+  if (res.code === 200 && res.url) {
+    operationForm.value.attachUrls.push(res.relativePath || res.url)
+  } else {
+    ElMessage.error(res.msg || '上传失败')
   }
 }
 
-async function confirmTrack() {
+async function confirmOperation() {
   try {
-    await trackFormRef.value.validate()
+    await operationFormRef.value.validate()
   } catch {
     return
   }
+  operationSubmitting.value = true
   try {
-    await trackEvent({
-      eventId: currentTrackId.value,
-      operDesc: trackForm.value.operDesc,
-      trend: trackForm.value.trend,
-      needUpgrade: trackForm.value.needUpgrade,
-      nextVisitTime: trackForm.value.nextVisitTime
-    })
-    ElMessage.success('跟踪记录保存成功')
-    trackVisible.value = false
+    const payload = {
+      eventId: currentRow.value.id,
+      operDesc: operationForm.value.operDesc,
+      attachUrl: operationForm.value.attachUrls.join(',')
+    }
+    if (operationType.value === 'start') {
+      await startDisposeEvent(payload)
+      ElMessage.success('已开始处置')
+    } else {
+      await trackEvent(payload)
+      ElMessage.success('跟踪记录保存成功')
+    }
+    operationVisible.value = false
+    operationUploadRef.value?.clearFiles()
     loadData()
   } catch (error) {
-    ElMessage.error('保存失败:' + (error.message || '未知错误'))
+    ElMessage.error('操作失败:' + (error.message || '未知错误'))
+  } finally {
+    operationSubmitting.value = false
   }
 }
 
@@ -674,17 +462,10 @@ async function handleDelete(row) {
 }
 
 // ==================== 详情 ====================
-const detailVisible = ref(false)
+const eventDetailRef = ref(null)
 
-async function handleDetail(row) {
-  currentRow.value = { ...row, timeline: [] }
-  detailVisible.value = true
-  try {
-    const res = await getEventLogs(row.id)
-    currentRow.value = { ...currentRow.value, timeline: toTimeline(res.data) }
-  } catch {
-    // 加载失败时保留列表行数据
-  }
+function handleDetail(row) {
+  eventDetailRef.value?.open(row)
 }
 </script>
 
@@ -734,35 +515,4 @@ async function handleDetail(row) {
   justify-content: flex-end;
 }
 
-.detail-content {
-  padding: 0 8px;
-}
-
-.section-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  margin-bottom: 12px;
-  padding-left: 8px;
-  border-left: 3px solid #409EFF;
-}
-
-.desc-text {
-  background: #f5f7fa;
-  border-radius: 4px;
-  padding: 10px;
-  font-size: 13px;
-  color: #606266;
-  line-height: 1.6;
-  max-height: 120px;
-  overflow-y: auto;
-}
-
-:deep(.el-form-item__label) {
-  color: #909399;
-}
-
-:deep(.el-form-item__content) {
-  color: #303133;
-}
 </style>

+ 18 - 150
src/views/subSystem/dispatch/disOnDuty/jbgl.vue

@@ -41,12 +41,9 @@
         <el-table-column prop="eventSource" label="事件来源" width="110">
           <template #default="{ row }">{{ dictLabel(dis_event_source, row.eventSource) }}</template>
         </el-table-column>
-        <el-table-column prop="eventType" label="管网类型" width="110">
-          <template #default="{ row }">{{ dictLabel(dis_event_type, row.eventType) }}</template>
-        </el-table-column>
         <el-table-column prop="location" label="发生地点" show-overflow-tooltip />
         <el-table-column prop="happenTime" label="发生时间" width="150" />
-        <el-table-column prop="createTime" label="创建时间" width="150" />
+        <el-table-column prop="reportTime" label="接报时间" width="150" />
         <el-table-column prop="description" label="事件描述" show-overflow-tooltip />
         <el-table-column prop="reporter" label="接报人" width="100" />
         <el-table-column prop="statusName" label="状态" width="100">
@@ -75,80 +72,19 @@
       </div>
     </div>
 
-    <!-- 详情弹窗 -->
-    <el-drawer v-model="detailVisible" title="事件详情" size="600px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
-      <div v-if="currentRow" class="detail-content">
-        <el-form label-width="100px" size="default">
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="发生地点">{{ currentRow.location }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="发生时间">{{ currentRow.happenTime }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="事件描述">
-            <div class="desc-text">{{ currentRow.description }}</div>
-          </el-form-item>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="接报人">{{ currentRow.reporter }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="接报时间">{{ currentRow.reportTime }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="来源渠道">{{ dictLabel(dis_event_channel, currentRow.channel) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件坐标">{{ formatGeo(currentRow.longitude, currentRow.latitude) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="联系电话">{{ currentRow.reporterPhone || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="状态">
-            <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
-          </el-form-item>
-          <div class="section-title">事件记录(操作日志)</div>
-          <!-- 参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件缩略图 -->
-          <EventLogTimeline v-if="currentRow.rawLogs && currentRow.rawLogs.length" :logs="currentRow.rawLogs" />
-          <div v-else class="desc-text">暂无记录</div>
-        </el-form>
-      </div>
-      <template #footer>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </template>
-    </el-drawer>
+    <EventDetailDrawer ref="eventDetailRef" />
 
     <!-- 新增/编辑接报弹窗 -->
     <el-dialog v-model="addVisible" :title="editingId ? '编辑接报' : '新增接报'" width="600px" :close-on-click-modal="false">
       <el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px">
+        <el-form-item label="事件描述" prop="description">
+          <el-input v-model="addForm.description" type="textarea" :rows="3" placeholder="请输入事件描述" />
+        </el-form-item>
         <el-form-item label="事件来源" prop="eventSource">
           <el-select v-model="addForm.eventSource" placeholder="请选择" style="width:100%">
             <el-option v-for="d in dis_event_source" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
         </el-form-item>
-        <el-form-item label="管网类型" prop="eventType">
-          <el-select v-model="addForm.eventType" placeholder="请选择管网类型" style="width:100%">
-            <el-option v-for="d in dis_event_type" :key="d.value" :label="d.label" :value="d.value" />
-          </el-select>
-        </el-form-item>
         <el-form-item label="发生地点" prop="location">
           <el-input v-model="addForm.location" placeholder="请输入发生地点" />
         </el-form-item>
@@ -161,23 +97,8 @@
         <el-form-item label="发生时间" prop="happenTime">
           <el-date-picker v-model="addForm.happenTime" type="datetime" placeholder="选择日期时间" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
         </el-form-item>
-        <el-form-item label="事件描述" prop="description">
-          <el-input v-model="addForm.description" type="textarea" :rows="3" placeholder="请输入事件描述" />
-        </el-form-item>
-        <el-form-item label="接报人" prop="reporter">
-          <el-input v-model="addForm.reporter" readonly placeholder="自动取当前录入人员" />
-        </el-form-item>
-        <el-form-item label="接报时间" prop="reportTime">
-          <!-- 默认当前时间,可选填:清空后由后端兜底为保存时刻 -->
-          <el-date-picker v-model="addForm.reportTime" type="datetime" placeholder="默认当前时间,可修改" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
-        </el-form-item>
-        <el-form-item label="联系电话" prop="reporterPhone">
-          <el-input v-model="addForm.reporterPhone" placeholder="请输入上报人联系电话" />
-        </el-form-item>
-        <el-form-item label="来源渠道" prop="channel">
-          <el-select v-model="addForm.channel" placeholder="请选择来源渠道" style="width:100%">
-            <el-option v-for="d in dis_event_channel" :key="d.value" :label="d.label" :value="d.value" />
-          </el-select>
+        <el-form-item label="上报人电话" prop="reporterPhone">
+          <el-input v-model="addForm.reporterPhone" placeholder="请输入上报人联系电话" maxlength="20" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -192,23 +113,15 @@
 import { ref, computed, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useDict } from '@/utils/dict'
-import useUserStore from '@/store/modules/user'
 import { bd09ToWgs84 } from '@/utils/coordTransform'
-import { getEventPage, saveEventReport, updateEventInfo, deleteEventInfo, getEventStatusOptions, getEventLogs } from '@/api/dispatch'
+import { getEventPage, saveEventReport, updateEventInfo, deleteEventInfo, getEventStatusOptions } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
-import EventLogTimeline from './EventLogTimeline.vue'
+import EventDetailDrawer from './EventDetailDrawer.vue'
 
 // ==================== 字典 ====================
-const { dis_event_source, dis_event_channel, dis_event_type } = useDict('dis_event_source', 'dis_event_channel', 'dis_event_type')
+const { dis_event_source } = useDict('dis_event_source')
 const eventStatusOptions = ref([])
 
-// ==================== 当前录入人员 ====================
-// 接报人固定取当前录入人员,不手填(后端落库时同样以当前登录用户为准)
-const userStore = useUserStore()
-function currentOperatorName() {
-  return userStore.nickName || userStore.name || ''
-}
-
 function dictLabel(list, value) {
   if (value === null || value === undefined || value === '') return '-'
   const hit = (list || []).find(item => String(item.value) === String(value))
@@ -288,18 +201,10 @@ function handleCurrentChange(val) {
 }
 
 // ==================== 详情 ====================
-const detailVisible = ref(false)
-const currentRow = ref(null)
+const eventDetailRef = ref(null)
 
-async function handleDetail(row) {
-  currentRow.value = { ...row, rawLogs: [] }
-  detailVisible.value = true
-  try {
-    const res = await getEventLogs(row.id)
-    currentRow.value = { ...currentRow.value, rawLogs: res.data || [] }
-  } catch {
-    // 加载失败时保留列表行数据
-  }
+function handleDetail(row) {
+  eventDetailRef.value?.open(row)
 }
 
 // ==================== 新增/编辑接报 ====================
@@ -307,31 +212,21 @@ const addVisible = ref(false)
 const addFormRef = ref(null)
 const editingId = ref(null)
 
-// 接报表单初值(含原型要求的「事件坐标」与「联系电话」;接报人自动取当前录入人员)
+// 接报表单初值(含事件坐标与上报人电话;接报人自动取当前录入人员)
 function blankForm() {
   return {
-    eventSource: '', eventType: '', location: '', longitude: null, latitude: null,
-    happenTime: '', description: '', reporter: currentOperatorName(), reportTime: nowStr(),
-    reporterPhone: '', channel: ''
+    eventSource: '', location: '', longitude: null, latitude: null,
+    happenTime: '', description: '', reporterPhone: ''
   }
 }
 
-// 当前时间(yyyy-MM-dd HH:mm:ss),接报时间默认值
-function nowStr() {
-  const d = new Date()
-  const p = n => String(n).padStart(2, '0')
-  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
-}
-
 const addForm = ref(blankForm())
 const addRules = {
   eventSource: [{ required: true, message: '请选择事件来源', trigger: 'change' }],
   location: [{ required: true, message: '请输入发生地点', trigger: 'blur' }],
   happenTime: [{ required: true, message: '请选择发生时间', trigger: 'change' }],
   description: [{ required: true, message: '请输入事件描述', trigger: 'blur' }],
-  reporter: [{ required: true, message: '请输入接报人', trigger: 'blur' }],
-  reportTime: [],
-  channel: [{ required: true, message: '请选择来源渠道', trigger: 'change' }]
+  reporterPhone: [{ required: true, message: '请输入上报人电话', trigger: 'blur' }],
 }
 
 // ==================== 事件坐标 ====================
@@ -391,17 +286,12 @@ function handleEdit(row) {
   editingId.value = row.id
   addForm.value = {
     eventSource: row.eventSource,
-    eventType: row.eventType,
     location: row.location,
     longitude: row.longitude,
     latitude: row.latitude,
     happenTime: row.happenTime,
     description: row.description,
-    // 接报人在接报登记时已按「当前录入人员」确定,编辑环节只读回显、不可改
-    reporter: row.reporter,
-    reportTime: row.reportTime,
-    reporterPhone: row.reporterPhone,
-    channel: row.channel
+    reporterPhone: row.reporterPhone
   }
   addVisible.value = true
   nextTick(() => addFormRef.value?.clearValidate())
@@ -495,26 +385,4 @@ async function handleDelete(row) {
   justify-content: flex-end;
 }
 
-.detail-content {
-  padding: 0 8px;
-}
-
-.desc-text {
-  background: #f5f7fa;
-  border-radius: 4px;
-  padding: 10px;
-  font-size: 13px;
-  color: #606266;
-  line-height: 1.6;
-  max-height: 120px;
-  overflow-y: auto;
-}
-
-:deep(.el-form-item__label) {
-  color: #909399;
-}
-
-:deep(.el-form-item__content) {
-  color: #303133;
-}
 </style>

+ 46 - 360
src/views/subSystem/dispatch/disOnDuty/zjbs.vue

@@ -56,14 +56,6 @@
         <el-table-column prop="lastReportTargets" label="报送层级" min-width="130">
           <template #default="{ row }">
             <span>{{ row.lastReportTargets || '-' }}</span>
-            <el-popover v-if="row.lastReportAttachUrl" placement="top" trigger="hover" width="280">
-              <template #reference>
-                <el-button type="primary" link size="small" style="margin-left:4px">附件({{ attachUrls(row.lastReportAttachUrl).length }})</el-button>
-              </template>
-              <div v-for="(name, i) in attachNames(row.lastReportAttachUrl)" :key="i" style="margin:4px 0">
-                <el-link type="primary" @click="downloadAttach(attachUrls(row.lastReportAttachUrl)[i])">{{ name }}</el-link>
-              </div>
-            </el-popover>
           </template>
         </el-table-column>
         <el-table-column prop="lastTransferTime" label="转接时间" min-width="150">
@@ -77,8 +69,8 @@
         </el-table-column>
         <el-table-column label="操作" width="240" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="row.status === 1" type="primary" plain size="small" @click="handleTransfer(row)" v-hasPermi="['dispatch:event:transfer']">转接</el-button>
-            <el-button v-if="canReport(row)" type="warning" plain size="small" @click="handleReport(row)" v-hasPermi="['dispatch:event:report']">报送</el-button>
+            <el-button v-if="[1, 2].includes(row.status)" type="primary" plain size="small" @click="openOperation(row, 'transfer')" v-hasPermi="['dispatch:event:transfer']">转接</el-button>
+            <el-button v-if="[1, 2].includes(row.status)" type="warning" plain size="small" @click="openOperation(row, 'report')" v-hasPermi="['dispatch:event:report']">报送</el-button>
             <el-button type="info" plain size="small" @click="handleDetail(row)">详情</el-button>
             <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
           </template>
@@ -97,168 +89,40 @@
       </div>
     </div>
 
-    <!-- 转接弹窗 -->
-    <el-dialog v-model="transferVisible" title="事件转接" width="600px" :close-on-click-modal="false">
-      <el-form :model="transferForm" :rules="transferRules" ref="transferFormRef" label-width="120px">
-        <el-form-item label="事件摘要">
-          <div class="desc-text" style="width:100%;min-height:90px">
-            <div>事件编号:{{ transferForm.eventNo || '-' }}</div>
-            <div>发生时间:{{ transferForm.happenTime || '-' }}</div>
-            <div>发生地点:{{ transferForm.location || '-' }}</div>
-            <div style="margin-top:4px">事件描述:{{ transferForm.summary || '-' }}</div>
-          </div>
-        </el-form-item>
-        <el-form-item label="选择转接部门" prop="depts">
-          <el-select v-model="transferForm.depts" multiple placeholder="请选择" style="width:100%">
+    <!-- 转接和报送共用部门+备注表单,操作类型仅决定接口和标题 -->
+    <el-dialog v-model="operationVisible" :title="operationType === 'transfer' ? '事件转接' : '事件报送'" width="600px" :close-on-click-modal="false">
+      <el-form :model="operationForm" :rules="operationRules" ref="operationFormRef" label-width="120px">
+        <el-form-item label="选择部门" prop="depts">
+          <el-select v-model="operationForm.depts" multiple filterable placeholder="请选择部门" style="width:100%">
             <el-option v-for="d in dis_transfer_dept" :key="d.value" :label="d.label" :value="d.label" />
           </el-select>
         </el-form-item>
-        <el-form-item label="备注">
-          <el-input v-model="transferForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="operationForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" />
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="transferVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmTransfer">确认转接</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 报送弹窗 -->
-    <el-dialog v-model="reportVisible" title="事件报送" width="600px" :close-on-click-modal="false">
-      <div @paste="handleDialogPaste">
-        <el-form :model="reportForm" :rules="reportRules" ref="reportFormRef" label-width="120px">
-          <el-form-item label="报送层级" prop="reportTargets">
-            <el-checkbox-group v-model="reportForm.reportTargets">
-              <el-checkbox v-for="d in dis_report_target" :key="d.value" :label="d.label" />
-            </el-checkbox-group>
-          </el-form-item>
-          <el-form-item label="报送内容摘要">
-            <div class="desc-text" style="width:100%;min-height:90px">
-              <div>事件编号:{{ reportForm.eventNo || '-' }}</div>
-              <div>发生时间:{{ reportForm.happenTime || '-' }}</div>
-              <div>发生地点:{{ reportForm.location || '-' }}</div>
-              <div style="margin-top:4px">事件描述:{{ reportForm.summary || '-' }}</div>
-            </div>
-          </el-form-item>
-          <el-form-item label="附件上传" prop="fileList">
-            <el-upload
-              ref="uploadRef"
-              v-model:file-list="reportForm.fileList"
-              drag
-              multiple
-              :action="uploadUrl"
-              :headers="uploadHeaders"
-              :limit="5"
-              :on-success="handleUploadSuccess"
-              :on-error="handleUploadError"
-              :on-exceed="handleUploadExceed"
-              :before-upload="handleBeforeUpload"
-              style="width:100%"
-            >
-              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
-              <div class="el-upload__text">将文件拖到此处,或<em>点击选择</em>,也可直接 Ctrl+V 粘贴</div>
-              <template #tip>
-                <div class="el-upload__tip">支持 PDF / 图片 / Excel / Word 格式,单个文件不超过 50MB,最多上传 5 个文件</div>
-              </template>
-            </el-upload>
-          </el-form-item>
-        </el-form>
-      </div>
-      <template #footer>
-        <el-button @click="reportVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmReport" :loading="submitting">确认报送</el-button>
+        <el-button @click="operationVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="confirmOperation">确认{{ operationType === 'transfer' ? '转接' : '报送' }}</el-button>
       </template>
     </el-dialog>
 
-    <!-- 详情弹窗 -->
-    <el-drawer v-model="detailVisible" title="流转详情" size="700px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
-      <div v-if="currentRow" class="detail-content">
-        <el-form label-width="100px" size="default">
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件类别">{{ dictLabel(dis_event_category, currentRow.eventCategory) }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件等级">{{ dictLabel(dis_event_level, currentRow.eventLevel) }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="责任单位">{{ currentRow.department || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="转接部门">{{ currentRow.lastTransferTargets || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="报送层级">{{ currentRow.lastReportTargets || '-' }}</el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="转接时间">{{ currentRow.lastTransferTime || '-' }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="当前状态">
-                <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="事件描述">
-            <div class="desc-text">{{ currentRow.description || '-' }}</div>
-          </el-form-item>
-        </el-form>
-        <div class="section-title">流转历史</div>
-        <el-timeline>
-          <el-timeline-item
-            v-for="(item, index) in currentRow.history"
-            :key="index"
-            :type="item.type"
-            :timestamp="item.actionTime"
-          >
-            <div>{{ item.content }}</div>
-            <div v-if="item.attachUrls && item.attachUrls.length" style="margin-top:4px">
-              <el-link
-                v-for="(name, i) in item.attachNames"
-                :key="i"
-                type="primary"
-                style="margin-right:16px;font-size:13px"
-                @click="downloadAttach(item.attachUrls[i])"
-              >附件:{{ name }}</el-link>
-            </div>
-          </el-timeline-item>
-        </el-timeline>
-      </div>
-      <template #footer>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </template>
-    </el-drawer>
+    <EventDetailDrawer ref="eventDetailRef" />
   </div>
 </template>
 
 <script setup name="Zjbs">
 import { ref, nextTick, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { UploadFilled, Search, Refresh } from '@element-plus/icons-vue'
-import { getToken } from '@/utils/auth'
+import { Search, Refresh } from '@element-plus/icons-vue'
 import { useDict } from '@/utils/dict'
-import { getEventPage, getEventDetail, getEventStatusOptions, deleteEventInfo, transferEvent, reportEvent } from '@/api/dispatch'
+import { getEventPage, getEventStatusOptions, deleteEventInfo, transferEvent, reportEvent } from '@/api/dispatch'
+
+import EventDetailDrawer from './EventDetailDrawer.vue'
 
 // ==================== 字典 ====================
-const { dis_event_source, dis_event_type, dis_event_category, dis_event_level, dis_transfer_dept, dis_report_target } = useDict(
-  'dis_event_source', 'dis_event_type', 'dis_event_category', 'dis_event_level', 'dis_transfer_dept', 'dis_report_target'
+const { dis_event_type, dis_event_category, dis_event_level, dis_transfer_dept } = useDict(
+  'dis_event_type', 'dis_event_category', 'dis_event_level', 'dis_transfer_dept'
 )
 
 function dictLabel(list, value) {
@@ -356,181 +220,50 @@ function handleCurrentChange(val) {
   loadData()
 }
 
-// 已核实或处置中、且已完成过一次转接后可报送
-function canReport(row) {
-  return row.lastTransferTargets && (row.status === 1 || row.status === 2)
-}
-
-// ==================== 转接 ====================
-const transferVisible = ref(false)
-const transferFormRef = ref(null)
-const currentTransferId = ref(null)
-const transferForm = ref({ eventNo: '', summary: '', happenTime: '', location: '', depts: [], remark: '' })
-const transferRules = {
-  depts: [{ required: true, message: '请选择转接部门', trigger: 'change', type: 'array' }]
-}
-
-function handleTransfer(row) {
-  currentTransferId.value = row.id
-  transferForm.value = {
-    eventNo: row.eventNo,
-    summary: row.description || '',
-    happenTime: row.happenTime || '',
-    location: row.location || '',
-    depts: [],
-    remark: ''
-  }
-  transferVisible.value = true
-  nextTick(() => transferFormRef.value?.clearValidate())
-}
-
-async function confirmTransfer() {
-  try {
-    await transferFormRef.value.validate()
-  } catch {
-    return
-  }
-  try {
-    await transferEvent({
-      eventId: currentTransferId.value,
-      depts: transferForm.value.depts,
-      operDesc: transferForm.value.remark
-    })
-    ElMessage.success('转接成功')
-    transferVisible.value = false
-    loadData()
-  } catch (error) {
-    ElMessage.error('转接失败:' + (error.message || '未知错误'))
-  }
-}
-
-// ==================== 附件 ====================
-// 报送附件:pdf / 图片 / Excel / Word,单个 ≤50MB,最多 5 个;多个附件 URL 逗号拼接存日志 attach_url
-const ALLOWED_UPLOAD_EXT = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'xls', 'xlsx', 'doc', 'docx']
-
-function attachUrls(attachUrl) {
-  return String(attachUrl || '').split(',').map(s => s.trim()).filter(Boolean)
-}
-
-// 从 URL 末段取文件名展示;RuoYi 存储名带「uuid_」前缀,截掉更可读
-function attachNames(attachUrl) {
-  return attachUrls(attachUrl).map(url => {
-    let name = url.substring(url.lastIndexOf('/') + 1)
-    try { name = decodeURIComponent(name) } catch { /* 保留原值 */ }
-    const underscore = name.indexOf('_')
-    return underscore > 0 ? name.substring(underscore + 1) : name
-  })
-}
-
-function downloadAttach(url) {
-  if (url) window.open(url, '_blank')
-}
-
-// ==================== 报送 ====================
-const reportVisible = ref(false)
-const reportFormRef = ref(null)
-const uploadRef = ref(null)
-const currentReportId = ref(null)
+// ==================== 转接 / 报送 ====================
+const operationVisible = ref(false)
+const operationFormRef = ref(null)
+const currentOperationId = ref(null)
+const operationType = ref('transfer')
 const submitting = ref(false)
-// 附件统一存 MinIO(响应结构与 /common/upload 兼容:code/url/originalFilename)
-const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload?dir=dispatch_report'
-const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
-const reportForm = ref({ eventNo: '', summary: '', happenTime: '', location: '', reportTargets: [], fileList: [], attachments: [] })
-const reportRules = {
-  reportTargets: [{ required: true, message: '请选择报送层级', trigger: 'change', type: 'array' }]
-}
-
-function handleReport(row) {
-  currentReportId.value = row.id
-  reportForm.value = {
-    eventNo: row.eventNo,
-    summary: row.description || '',
-    happenTime: row.happenTime || '',
-    location: row.location || '',
-    reportTargets: [],
-    fileList: [],
-    attachments: []
-  }
-  reportVisible.value = true
-  nextTick(() => reportFormRef.value?.clearValidate())
-}
-
-function handleBeforeUpload(file) {
-  const ext = String(file.name).split('.').pop().toLowerCase()
-  if (!ALLOWED_UPLOAD_EXT.includes(ext)) {
-    ElMessage.error('仅支持 PDF、图片、Excel、Word 格式文件')
-    return false
-  }
-  if (file.size / 1024 / 1024 >= 50) {
-    ElMessage.error('文件大小不能超过 50MB')
-    return false
-  }
-  return true
-}
-
-function handleUploadSuccess(res) {
-  if (res.code === 200 && res.url) {
-    // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
-    reportForm.value.attachments.push({
-      fileName: res.originalFilename,
-      url: res.relativePath || res.url
-    })
-  } else {
-    ElMessage.error(res.msg || '上传失败')
-  }
-}
-
-function handleUploadError() {
-  ElMessage.error('文件上传失败')
-}
-
-function handleUploadExceed() {
-  ElMessage.warning('附件数量不能超过 5 个')
+const operationForm = ref({ depts: [], remark: '' })
+const operationRules = {
+  depts: [{ required: true, message: '请选择转接部门', trigger: 'change', type: 'array' }]
 }
 
-// 弹窗内 Ctrl+V 粘贴文件(截图 / 复制的文件):塞给 el-upload 走同一套校验与上传流程
-function handleDialogPaste(event) {
-  const files = Array.from(event.clipboardData?.files || [])
-  if (!files.length || !uploadRef.value) return
-  const remain = 5 - (reportForm.value.fileList?.length || 0)
-  if (remain <= 0) {
-    ElMessage.warning('附件数量不能超过 5 个')
-    return
-  }
-  if (files.length > remain) {
-    ElMessage.warning('附件数量不能超过 5 个,本次只取前 ' + remain + ' 个')
-  }
-  files.slice(0, remain).forEach(file => uploadRef.value.handleStart(file))
-  uploadRef.value.submit()
+function openOperation(row, type) {
+  currentOperationId.value = row.id
+  operationType.value = type
+  operationForm.value = { depts: [], remark: '' }
+  operationVisible.value = true
+  nextTick(() => operationFormRef.value?.clearValidate())
 }
 
-async function confirmReport() {
+async function confirmOperation() {
   try {
-    await reportFormRef.value.validate()
+    await operationFormRef.value.validate()
   } catch {
     return
   }
   submitting.value = true
   try {
-    await reportEvent({
-      eventId: currentReportId.value,
-      depts: reportForm.value.reportTargets,
-      attachUrl: reportForm.value.attachments.map(a => a.url).join(',')
+    const action = operationType.value === 'transfer' ? transferEvent : reportEvent
+    await action({
+      eventId: currentOperationId.value,
+      depts: operationForm.value.depts,
+      operDesc: operationForm.value.remark
     })
-    ElMessage.success('报送成功')
-    reportVisible.value = false
-    // 清掉 el-upload 的残留文件列表,避免下次打开还显示上一次的文件
-    uploadRef.value?.clearFiles()
+    ElMessage.success(operationType.value === 'transfer' ? '转接成功' : '报送成功')
+    operationVisible.value = false
     loadData()
   } catch (error) {
-    if (error !== 'cancel' && error !== 'close') {
-      ElMessage.error('报送失败:' + (error.message || '未知错误'))
-    }
+    ElMessage.error((operationType.value === 'transfer' ? '转接' : '报送') + '失败:' + (error.message || '未知错误'))
   } finally {
     submitting.value = false
   }
 }
 
+// ==================== 报送 ====================
 // ==================== 删除 ====================
 async function handleDelete(row) {
   try {
@@ -546,33 +279,10 @@ async function handleDelete(row) {
 }
 
 // ==================== 详情 ====================
-const detailVisible = ref(false)
-const currentRow = ref(null)
-
-const LOG_TYPE = { 3: 'primary', 4: 'success', 5: 'warning', 6: '', 7: 'danger', 8: 'warning' }
+const eventDetailRef = ref(null)
 
-async function handleDetail(row) {
-  currentRow.value = { ...row, history: [] }
-  detailVisible.value = true
-  try {
-    const res = await getEventDetail(row.id)
-    const detail = res.data || {}
-    currentRow.value = {
-      ...detail,
-      history: (detail.logs || []).map(log => {
-        const urls = attachUrls(log.attachUrl)
-        return {
-          type: LOG_TYPE[log.operType] ?? '',
-          actionTime: log.operTime,
-          content: (log.operTypeName || '操作') + ':' + (log.operDesc || log.targets || '-'),
-          attachUrls: urls,
-          attachNames: urls.length ? attachNames(log.attachUrl) : []
-        }
-      })
-    }
-  } catch {
-    // 加载失败时保留列表行数据
-  }
+function handleDetail(row) {
+  eventDetailRef.value?.open(row)
 }
 </script>
 
@@ -622,30 +332,6 @@ async function handleDetail(row) {
   justify-content: flex-end;
 }
 
-.detail-content {
-  padding: 0 8px;
-}
-
-.section-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  margin-bottom: 12px;
-  padding-left: 8px;
-  border-left: 3px solid #409EFF;
-}
-
-.desc-text {
-  background: #f5f7fa;
-  border-radius: 4px;
-  padding: 10px;
-  font-size: 13px;
-  color: #606266;
-  line-height: 1.6;
-  max-height: 120px;
-  overflow-y: auto;
-}
-
 :deep(.el-form-item__label) {
   color: #909399;
 }

+ 13 - 76
src/views/subSystem/dispatch/disPlanManage/DisEventGrade.vue

@@ -13,22 +13,6 @@
           <el-option v-for="d in dis_event_level" :key="d.value" :label="d.label" :value="d.value" />
         </el-select>
       </el-form-item>
-      <el-form-item label="事件状态">
-        <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px">
-          <el-option v-for="d in gradeStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="queryParams.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          style="width: 240px"
-          value-format="YYYY-MM-DD"
-        />
-      </el-form-item>
       <el-form-item>
         <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
         <el-button :icon="Refresh" @click="handleReset">重置</el-button>
@@ -43,8 +27,6 @@
     <!-- 数据表格 -->
     <el-table :data="pageList" border stripe @selection-change="handleSelectionChange" style="width: 100%">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="序号" type="index" width="60" align="center" />
-      <el-table-column prop="gradeNo" label="标准编号" width="140" />
       <el-table-column prop="eventCategory" label="事件类别" min-width="120">
         <template #default="{ row }">{{ dictLabel(dis_event_category, row.eventCategory) }}</template>
       </el-table-column>
@@ -53,15 +35,8 @@
           <el-tag :type="levelTagType(row.eventLevel)" size="small">{{ dictLabel(dis_event_level, row.eventLevel) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column prop="scenario" label="适用场景" min-width="140" />
-      <el-table-column prop="criteria" label="判断标准" min-width="200" show-overflow-tooltip />
+      <el-table-column prop="criteria" label="判定条件" min-width="280" show-overflow-tooltip />
       <el-table-column prop="response" label="响应要求" min-width="160" show-overflow-tooltip />
-      <el-table-column prop="createTime" label="创建时间" width="150" align="center" />
-      <el-table-column prop="status" label="状态" width="90" align="center">
-        <template #default="{ row }">
-          <el-tag :type="row.status === 0 ? 'success' : 'info'">{{ row.statusName }}</el-tag>
-        </template>
-      </el-table-column>
       <el-table-column label="操作" width="140" align="center">
         <template #default="{ row }">
           <el-button type="primary" plain size="small" @click="handleEdit(row)" v-hasPermi="['dispatch:grade:edit']">编辑</el-button>
@@ -95,30 +70,16 @@
             <el-option v-for="d in dis_event_level" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
         </el-form-item>
-        <el-form-item label="类别细分" prop="subCategory">
-          <el-input v-model="form.subCategory" placeholder="请输入类别细分" />
-        </el-form-item>
-        <el-form-item label="适用场景" prop="scenario">
-          <el-input v-model="form.scenario" placeholder="请输入适用场景" />
-        </el-form-item>
-        <el-form-item label="分级分类判断标准" prop="criteria">
+        <el-form-item label="判定条件" prop="criteria">
           <el-input
             v-model="form.criteria"
             type="textarea"
-            :rows="4"
-            placeholder="填写具体判断规则、适用条件、划分依据"
+            :rows="5"
+            placeholder="填写事件判定条件、影响范围和分级依据"
           />
         </el-form-item>
         <el-form-item label="响应要求" prop="response">
-          <el-input v-model="form.response" placeholder="如:30 分钟内启动预案" />
-        </el-form-item>
-        <el-form-item label="标准生效状态" prop="status">
-          <el-radio-group v-model="form.status">
-            <el-radio v-for="d in gradeStatusOptions" :key="d.value" :label="d.value">{{ d.label }}</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="备注说明">
-          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注说明" />
+          <el-input v-model="form.response" type="textarea" :rows="3" placeholder="填写响应时限、启动要求和协同措施" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -133,10 +94,11 @@
 import { ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useDict } from '@/utils/dict'
-import { getEventGradePage, addEventGrade, updateEventGrade, deleteEventGrade, getEventGradeStatusOptions } from '@/api/dispatch'
+import { getEventGradePage, addEventGrade, updateEventGrade, deleteEventGrade } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
 
 // ---------- 字典 ----------
+// 事件分级页面的类别/等级与预案类型、预案级别共用同一套字典编码。
 const { dis_event_category, dis_event_level } = useDict('dis_event_category', 'dis_event_level')
 
 function dictLabel(list, value) {
@@ -145,19 +107,6 @@ function dictLabel(list, value) {
   return hit ? hit.label : value
 }
 
-// ---------- 下拉选项 ----------
-// 标准生效状态:GradeStatusEnum 0启用 1停用
-const gradeStatusOptions = ref([])
-
-async function loadStatusOptions() {
-  try {
-    const res = await getEventGradeStatusOptions()
-    gradeStatusOptions.value = res.data || []
-  } catch {
-    gradeStatusOptions.value = []
-  }
-}
-
 // 等级字典值 -> 标签样式(1 Ⅰ级紧急 2 Ⅱ级较大 3 Ⅲ级一般)
 const LEVEL_TAG = { 1: 'danger', 2: 'warning', 3: 'info' }
 function levelTagType(level) {
@@ -165,7 +114,7 @@ function levelTagType(level) {
 }
 
 // ---------- 查询参数 ----------
-const queryParams = ref({ eventCategory: '', eventLevel: '', status: '', dateRange: null })
+const queryParams = ref({ eventCategory: '', eventLevel: '' })
 
 // ---------- 数据 ----------
 const tableData = ref([])
@@ -175,11 +124,6 @@ async function loadData() {
     const params = {}
     if (queryParams.value.eventCategory) params.eventCategory = queryParams.value.eventCategory
     if (queryParams.value.eventLevel) params.eventLevel = queryParams.value.eventLevel
-    if (queryParams.value.status !== '' && queryParams.value.status !== null) params.status = queryParams.value.status
-    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
-      params.startDate = queryParams.value.dateRange[0]
-      params.endDate = queryParams.value.dateRange[1]
-    }
     const res = await getEventGradePage(1, 999, params)
     tableData.value = res?.data?.records ?? res?.data ?? res ?? []
   } catch {
@@ -187,7 +131,7 @@ async function loadData() {
   }
 }
 
-onMounted(() => { loadStatusOptions(); loadData() })
+onMounted(() => { loadData() })
 
 // ---------- 分页 ----------
 const pagination = ref({ page: 1, size: 10 })
@@ -203,7 +147,7 @@ const onPageChange = () => {}
 // ---------- 搜索 / 重置 ----------
 const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
-  queryParams.value = { eventCategory: '', eventLevel: '', status: '', dateRange: null }
+  queryParams.value = { eventCategory: '', eventLevel: '' }
   pagination.value.page = 1
   loadData()
 }
@@ -217,16 +161,14 @@ const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
 const defaultForm = () => ({
-  id: null, eventCategory: '', eventLevel: '', subCategory: '', scenario: '', criteria: '', response: '', status: 0, remark: ''
+  id: null, eventCategory: '', eventLevel: '', criteria: '', response: '', remark: ''
 })
 const form = ref(defaultForm())
 
 const formRules = {
   eventCategory: [{ required: true, message: '请选择事件类别', trigger: 'change' }],
   eventLevel: [{ required: true, message: '请选择事件等级', trigger: 'change' }],
-  scenario: [{ required: true, message: '请输入适用场景', trigger: 'blur' }],
   criteria: [{ required: true, message: '请填写判断标准', trigger: 'blur' }],
-  status: [{ required: true, message: '请选择状态', trigger: 'change' }],
 }
 
 const handleAdd = () => {
@@ -288,17 +230,12 @@ const handleBatchDelete = () => {
 // ---------- 导出 ----------
 const handleExport = () => {
   if (!tableData.value.length) return ElMessage.warning('暂无数据可导出')
-  const headers = ['标准编号', '事件类别', '事件等级', '类别细分', '适用场景', '判断标准', '响应要求', '状态', '备注']
+  const headers = ['事件类别', '事件等级', '判定条件', '响应要求']
   const rows = tableData.value.map(row => [
-    row.gradeNo,
     dictLabel(dis_event_category.value, row.eventCategory),
     dictLabel(dis_event_level.value, row.eventLevel),
-    row.subCategory,
-    row.scenario,
     row.criteria,
-    row.response,
-    row.statusName,
-    row.remark
+    row.response
   ])
   const csvContent = [
     '' + headers.join(','),

+ 26 - 37
src/views/subSystem/dispatch/disPlanManage/DisPlanApproval.vue

@@ -11,17 +11,6 @@
           <el-option v-for="d in approveStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
         </el-select>
       </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="queryParams.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          style="width: 240px"
-          value-format="YYYY-MM-DD"
-        />
-      </el-form-item>
       <el-form-item>
         <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
         <el-button :icon="Refresh" @click="handleReset">重置</el-button>
@@ -30,7 +19,6 @@
 
     <!-- 数据表格 -->
     <el-table :data="pageList" border stripe style="width: 100%">
-      <el-table-column label="序号" type="index" width="60" align="center" />
       <el-table-column prop="planNo" label="预案编号" width="140" />
       <el-table-column prop="planName" label="预案名称" min-width="180" show-overflow-tooltip />
       <el-table-column prop="planType" label="预案类型" width="130">
@@ -42,19 +30,22 @@
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column prop="auditRecord" label="审批记录/意见" min-width="240" show-overflow-tooltip>
-        <template #default="{ row }">{{ auditText(row) }}</template>
-      </el-table-column>
-      <el-table-column prop="createTime" label="创建时间" width="150" align="center" />
+      <el-table-column prop="version" label="版本" width="90" align="center" />
+      <el-table-column prop="compileUnit" label="编制单位" min-width="140" show-overflow-tooltip />
       <el-table-column prop="status" label="状态" width="100" align="center">
         <template #default="{ row }">
           <el-tag :type="statusTagType(row.status)">{{ row.statusName }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="130" align="center">
+      <el-table-column label="审批结果" min-width="240" show-overflow-tooltip>
+        <template #default="{ row }">{{ auditText(row) }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="155" align="center">
         <template #default="{ row }">
+          <div class="plan-actions">
           <el-button type="info" plain size="small" @click="handleView(row)">查看</el-button>
           <el-button v-if="row.status === 1" type="primary" plain size="small" @click="handleApprove(row)" v-hasPermi="['dispatch:plan:approve']">审批</el-button>
+          </div>
         </template>
       </el-table-column>
     </el-table>
@@ -88,10 +79,6 @@
           <el-descriptions-item label="当前状态">
             <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="附件">
-            <el-link v-if="currentRow.attachPath" type="primary" :underline="false" @click="downloadAttach(currentRow)">{{ currentRow.attachName || '查看附件' }}</el-link>
-            <span v-else>—</span>
-          </el-descriptions-item>
         </el-descriptions>
       </div>
 
@@ -145,10 +132,6 @@
           <el-descriptions-item label="预案类型">{{ dictLabel(dis_plan_type, currentRow.planType) }}</el-descriptions-item>
           <el-descriptions-item label="预案等级">{{ dictLabel(dis_plan_level, currentRow.planLevel) }}</el-descriptions-item>
           <el-descriptions-item label="编制单位">{{ currentRow.compileUnit || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="附件">
-            <el-link v-if="currentRow.attachPath" type="primary" :underline="false" @click="downloadAttach(currentRow)">{{ currentRow.attachName || '查看附件' }}</el-link>
-            <span v-else>—</span>
-          </el-descriptions-item>
           <el-descriptions-item label="修订说明">{{ currentRow.changeNote || '—' }}</el-descriptions-item>
         </el-descriptions>
       </div>
@@ -226,7 +209,7 @@ async function loadStatusOptions() {
 const LOG_TL_TYPE = { 0: 'primary', 1: 'primary', 2: 'success', 3: 'warning', 4: 'primary' }
 
 // ---------- 查询 ----------
-const queryParams = ref({ planName: '', status: 1, dateRange: null })
+const queryParams = ref({ planName: '', status: '' })
 
 // ---------- 数据 ----------
 const tableData = ref([])
@@ -236,12 +219,9 @@ async function loadData() {
     const params = {}
     if (queryParams.value.planName) params.planName = queryParams.value.planName
     if (queryParams.value.status !== '' && queryParams.value.status !== null) params.status = queryParams.value.status
-    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
-      params.startDate = queryParams.value.dateRange[0]
-      params.endDate = queryParams.value.dateRange[1]
-    }
     const res = await getEventPlanPage(1, 999, params)
-    tableData.value = res?.data?.records ?? res?.data ?? res ?? []
+    const records = res?.data?.records ?? res?.data ?? res ?? []
+    tableData.value = (records || []).filter(row => [1, 2, 3].includes(row.status))
   } catch {
     ElMessage.error('加载数据失败')
   }
@@ -261,7 +241,7 @@ const onPageChange = () => {}
 // ---------- 搜索 / 重置 ----------
 const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
-  queryParams.value = { planName: '', status: 1, dateRange: null }
+  queryParams.value = { planName: '', status: '' }
   pagination.value.page = 1
   loadData()
 }
@@ -295,11 +275,6 @@ const handleView = async (row) => {
   }
 }
 
-// 附件下载(MinIO 直链)
-const downloadAttach = (row) => {
-  if (row?.attachPath) window.open(row.attachPath, '_blank')
-}
-
 // ---------- 审批弹窗 ----------
 const approveVisible = ref(false)
 const approveOpinion = ref('')
@@ -349,6 +324,20 @@ const handleComplete = async () => {
   }
 }
 
+.plan-actions {
+  display: flex;
+  flex-wrap: nowrap;
+  justify-content: center;
+  align-items: center;
+  gap: 4px;
+  white-space: nowrap;
+
+  :deep(.el-button) {
+    margin-left: 0;
+    flex: 0 0 auto;
+  }
+}
+
 /* ---- 查看详情样式 ---- */
 .detail-section {
   margin-bottom: 20px;

+ 75 - 251
src/views/subSystem/dispatch/disPlanManage/DisPlanEditing.vue

@@ -8,16 +8,15 @@
       <el-form-item label="预案名称">
         <el-input v-model="queryParams.planName" placeholder="请输入预案名称" clearable style="width: 200px" @keyup.enter="handleSearch" />
       </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="queryParams.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          style="width: 240px"
-          value-format="YYYY-MM-DD"
-        />
+      <el-form-item label="预案类型">
+        <el-select v-model="queryParams.planType" placeholder="全部类型" clearable style="width: 160px">
+          <el-option v-for="d in dis_plan_type" :key="d.value" :label="d.label" :value="d.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 140px">
+          <el-option v-for="d in planStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
+        </el-select>
       </el-form-item>
       <el-form-item>
         <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
@@ -31,27 +30,9 @@
       </el-form-item>
     </el-form>
 
-    <!-- 主体:左侧树 + 右侧表格 -->
-    <div class="main-layout">
-      <!-- 左侧预案分类树(紧凑窄栏,不再挤压表格) -->
-      <div class="left-tree">
-        <div class="tree-title">预案分类</div>
-        <el-tree
-          ref="treeRef"
-          :data="treeData"
-          :props="treeProps"
-          node-key="id"
-          default-expand-all
-          highlight-current
-          @node-click="handleNodeClick"
-        />
-      </div>
-
-      <!-- 右侧表格 -->
-      <div class="right-table">
+    <div class="plan-list">
         <el-table :data="pageList" border stripe @selection-change="handleSelectionChange" style="width: 100%">
           <el-table-column type="selection" width="55" align="center" />
-          <el-table-column label="序号" type="index" width="60" align="center" />
           <el-table-column prop="planNo" label="预案编号" width="140" />
           <el-table-column prop="planName" label="预案名称" min-width="180" show-overflow-tooltip />
           <el-table-column prop="planType" label="预案类型" width="130">
@@ -64,35 +45,20 @@
             </template>
           </el-table-column>
           <el-table-column prop="version" label="版本" width="90" />
-          <el-table-column prop="attachName" label="附件名称" min-width="200" show-overflow-tooltip>
-            <template #default="{ row }">
-              <!-- 附件名称即下载入口:有附件时可点击下载,无附件显示 - -->
-              <el-link
-                v-if="row.attachName"
-                type="primary"
-                :underline="false"
-                :disabled="!row.attachPath"
-                @click="handleDownload(row)"
-              >
-                <el-icon style="margin-right:2px"><Download /></el-icon>{{ row.attachName }}
-              </el-link>
-              <span v-else>-</span>
-            </template>
-          </el-table-column>
-          <el-table-column prop="publishDate" label="发布时间" width="120" align="center" />
-          <el-table-column prop="createTime" label="创建时间" width="150" align="center" />
-          <el-table-column prop="compileUnit" label="编制单位" width="100" />
+          <el-table-column prop="compileUnit" label="编制单位" min-width="130" show-overflow-tooltip />
           <el-table-column prop="statusName" label="状态" width="100" align="center">
             <template #default="{ row }">
               <el-tag :type="statusTagType(row.status)">{{ row.statusName }}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="操作" width="270" align="center">
+          <el-table-column label="操作" width="330" align="center">
             <template #default="{ row }">
+              <div class="plan-actions">
               <el-button type="primary" plain size="small" @click="handleEdit(row)" v-hasPermi="['dispatch:plan:edit']">{{ isReviseRow(row) ? '修订' : '编辑' }}</el-button>
               <el-button type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:plan:remove']">删除</el-button>
               <el-button v-if="row.submittable" type="success" plain size="small" @click="handleSubmitApproval(row)" v-hasPermi="['dispatch:plan:submit']">提交审核</el-button>
               <el-button type="warning" plain size="small" @click="handleVersions(row)">版本</el-button>
+              </div>
             </template>
           </el-table-column>
         </el-table>
@@ -108,7 +74,6 @@
           @size-change="onSizeChange"
           @current-change="onPageChange"
         />
-      </div>
     </div>
       </el-tab-pane>
 
@@ -119,21 +84,23 @@
           <el-button type="primary" size="small" @click="handleTemplateAdd" v-hasPermi="['dispatch:planLibrary:add']">新增模板</el-button>
         </div>
         <el-table :data="templatePageList" border stripe style="width:100%" v-loading="templateLoading">
-          <el-table-column type="index" :index="templateIndex" label="序号" width="60" align="center" />
           <el-table-column prop="planName" label="模板名称" min-width="200" show-overflow-tooltip />
-          <el-table-column prop="eventType" label="类型" width="100" align="center" />
-          <el-table-column prop="eventLevel" label="等级" width="90" align="center" />
-          <el-table-column prop="responsibleUnit" label="责任单位" width="120">
-            <template #default="{ row }">{{ row.responsibleUnit || '-' }}</template>
+          <el-table-column prop="eventType" label="预案类型" width="110" align="center">
+            <template #default="{ row }">{{ dictLabel(dis_plan_type, row.eventType) }}</template>
           </el-table-column>
-          <el-table-column prop="planSummary" label="概要" min-width="260" show-overflow-tooltip>
-            <template #default="{ row }">{{ row.planSummary || '-' }}</template>
+          <el-table-column prop="eventLevel" label="预案级别" width="100" align="center">
+            <template #default="{ row }">{{ dictLabel(dis_plan_level, row.eventLevel) }}</template>
+          </el-table-column>
+          <el-table-column prop="responsibleUnit" label="编制单位" width="120">
+            <template #default="{ row }">{{ row.responsibleUnit || '-' }}</template>
           </el-table-column>
           <el-table-column prop="createTime" label="创建时间" width="150" align="center" />
-          <el-table-column label="操作" width="140" align="center">
+          <el-table-column label="操作" width="150" align="center">
             <template #default="{ row }">
+              <div class="plan-actions">
               <el-button type="primary" plain size="small" @click="handleTemplateEdit(row)" v-hasPermi="['dispatch:planLibrary:add']">编辑</el-button>
               <el-button type="danger" plain size="small" @click="handleTemplateDelete(row)" v-hasPermi="['dispatch:planLibrary:remove']">删除</el-button>
+              </div>
             </template>
           </el-table-column>
         </el-table>
@@ -170,7 +137,9 @@
           @click="pickCreate(tpl)"
         >
           <div style="font-weight:600;font-size:14px">{{ tpl.planName }}</div>
-          <div style="font-size:12px;color:#909399;margin-top:4px">{{ tpl.planSummary || '暂无概要' }}</div>
+          <div style="font-size:12px;color:#909399;margin-top:4px">
+            {{ dictLabel(dis_plan_type, tpl.eventType) }} · {{ dictLabel(dis_plan_level, tpl.eventLevel) }}
+          </div>
         </div>
       </template>
       <template #footer>
@@ -180,7 +149,7 @@
 
     <!-- 新增/修改预案弹窗 -->
     <el-dialog v-model="dialogVisible" :title="dialogTitle" width="840px" top="5vh" destroy-on-close class="plan-dialog">
-      <div @paste="handleDialogPaste">
+      <div>
         <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
           <el-row :gutter="16">
             <el-col :span="12">
@@ -211,51 +180,13 @@
             </el-col>
           </el-row>
           <el-form-item label="编制单位" prop="compileUnit">
-            <!-- 可从系统部门选择,也可手动输入(allow-create 允许创建新条目) -->
-            <el-select
-              v-model="form.compileUnit"
-              filterable
-              allow-create
-              default-first-option
-              clearable
-              placeholder="选择系统部门或手动输入"
-              style="width: 100%"
-            >
-              <el-option
-                v-for="d in deptOptions"
-                :key="d.deptId"
-                :label="d.deptName"
-                :value="d.deptName"
-              />
-            </el-select>
+            <el-input v-model="form.compileUnit" clearable placeholder="请输入编制单位" />
           </el-form-item>
           <el-form-item v-if="isRevising" label="修订说明" prop="changeNote">
             <el-input v-model="form.changeNote" type="textarea" :rows="3" placeholder="请填写本次修订说明(修订已通过预案必填)" />
           </el-form-item>
           <el-form-item label="预案正文">
-            <!-- 与模板库章节正文同一套纯文本富文本(无图片/视频控件);内容随版本快照保存 -->
-            <editor v-model="form.content" :media="false" :min-height="320" placeholder="请输入预案正文(将随版本快照保存,可在版本管理中查看历史正文)" />
-          </el-form-item>
-          <el-form-item label="附件上传">
-            <el-upload
-              ref="uploadRef"
-              v-model:file-list="form.fileList"
-              :action="uploadUrl"
-              :headers="uploadHeaders"
-              :limit="1"
-              accept=".pdf,.doc,.docx"
-              :on-success="handleUploadSuccess"
-              :on-error="handleUploadError"
-              :on-remove="handleUploadRemove"
-              :on-exceed="() => ElMessage.warning('只能上传一个文件')"
-              :before-upload="handleBeforeUpload"
-              style="width:100%"
-            >
-              <el-button type="primary">添加文件</el-button>
-              <template #tip>
-                <div class="upload-tip">仅支持 PDF / Word 格式文件,单个文件不超过 50MB,支持 Ctrl+V 粘贴</div>
-              </template>
-            </el-upload>
+            <NoticeEditor v-if="dialogVisible" v-model="form.content" placeholder="请输入预案正文(将随版本快照保存,可在版本管理中查看历史正文)" @paste.stop />
           </el-form-item>
         </el-form>
       </div>
@@ -273,35 +204,29 @@
           </el-form-item>
           <el-row :gutter="16">
             <el-col :span="12">
-              <el-form-item label="类型">
+              <el-form-item label="预案类型">
                 <el-select v-model="templateForm.eventType" placeholder="请选择" style="width:100%">
-                  <el-option v-for="t in ['综合', '供水', '燃气', '排水']" :key="t" :label="t" :value="t" />
+                  <el-option v-for="t in dis_plan_type" :key="t.value" :label="t.label" :value="t.value" />
                 </el-select>
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item label="等级">
+              <el-form-item label="预案级别">
                 <el-select v-model="templateForm.eventLevel" placeholder="请选择" style="width:100%">
-                  <el-option v-for="l in ['Ⅰ级', 'Ⅱ级', 'Ⅲ级']" :key="l" :label="l" :value="l" />
+                  <el-option v-for="l in dis_plan_level" :key="l.value" :label="l.label" :value="l.value" />
                 </el-select>
               </el-form-item>
             </el-col>
           </el-row>
           <el-row :gutter="16">
             <el-col :span="12">
-              <el-form-item label="责任单位">
-                <el-input v-model="templateForm.responsibleUnit" maxlength="50" placeholder="请输入责任单位(选填)" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="模板概要">
-                <el-input v-model="templateForm.planSummary" maxlength="200" show-word-limit placeholder="一句话说明模板适用场景(新建预案选择时展示)" />
+              <el-form-item label="编制单位">
+                <el-input v-model="templateForm.responsibleUnit" maxlength="50" placeholder="请输入编制单位(选填)" />
               </el-form-item>
             </el-col>
           </el-row>
           <el-form-item label="章节正文">
-            <!-- 纯文本富文本:无图片/视频/链接控件 -->
-            <editor v-model="templateForm.planContent" :media="false" :min-height="320" placeholder="模板章节大纲/正文,新建预案套用后可在正文基础上编写" />
+            <NoticeEditor v-if="templateFormVisible" v-model="templateForm.planContent" placeholder="模板章节大纲/正文,新建预案套用后可在正文基础上编写" />
           </el-form-item>
         </el-form>
         <template #footer>
@@ -317,12 +242,6 @@
       </div>
       <el-table :data="versionList" border stripe style="width: 100%">
         <el-table-column prop="version" label="版本号" width="90" />
-        <el-table-column prop="attachName" label="附件" min-width="160" show-overflow-tooltip>
-          <template #default="{ row }">
-            <el-link v-if="row.attachPath" type="primary" :underline="false" @click="downloadVersionAttach(row)">{{ row.attachName || '下载附件' }}</el-link>
-            <span v-else>—</span>
-          </template>
-        </el-table-column>
         <el-table-column prop="editor" label="修订人" width="110" />
         <el-table-column prop="editTime" label="修订时间" width="160" />
         <el-table-column prop="changeNote" label="变更说明" min-width="160" show-overflow-tooltip />
@@ -358,14 +277,14 @@
 <script setup>
 import { ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Download, Search, Refresh } from '@element-plus/icons-vue'
+import { Search, Refresh } from '@element-plus/icons-vue'
 import * as XLSX from 'xlsx'
-import { getToken } from '@/utils/auth'
 import { useDict } from '@/utils/dict'
+import NoticeEditor from '@/views/system/notice/NoticeEditor.vue'
 import {
   getEventPlanPage, getEventPlanById, saveEventPlan, deleteEventPlan,
   submitEventPlan, getPlanVersions, getPlanVersion, getPlanLibraryList,
-  savePlanLibrary, deletePlanLibrary, getDeptOptions
+  savePlanLibrary, deletePlanLibrary, getPlanStatusOptions
 } from '@/api/dispatch'
 
 // ---------- 字典 ----------
@@ -389,35 +308,28 @@ const levelTagType = (level) => LEVEL_TAG[level] || 'info'
 const VER_TAG = { 0: 'info', 1: 'success', 2: 'warning', 3: 'danger' }
 const verTagType = (verStatus) => VER_TAG[verStatus] || 'info'
 
-// ---------- 分类树(复用预案类型字典,后端不再单独维护分类表) ----------
-const treeRef = ref(null)
-const treeProps = { children: 'children', label: 'label' }
-const treeData = computed(() => (dis_plan_type.value || []).map(d => ({ id: d.value, label: d.label })))
+// ---------- 查询 ----------
+const queryParams = ref({ planName: '', planType: '', status: '' })
+const planStatusOptions = ref([])
 
-const selectedTreeNode = ref(null)
-const handleNodeClick = (node) => {
-  selectedTreeNode.value = node
-  pagination.value.page = 1
-  loadData()
+async function loadPlanStatusOptions() {
+  try {
+    const res = await getPlanStatusOptions()
+    planStatusOptions.value = res?.data || []
+  } catch {
+    planStatusOptions.value = []
+  }
 }
 
-// ---------- 查询 ----------
-const queryParams = ref({ planName: '', dateRange: null })
-
 // ---------- 数据 ----------
 const tableData = ref([])
 
 async function loadData() {
   try {
-    // 手工拼参数:dateRange 是数组不能直接展开进查询参数
-    const params = { planName: queryParams.value.planName }
-    if (selectedTreeNode.value) {
-      params.planType = selectedTreeNode.value.id
-    }
-    if (queryParams.value.dateRange && queryParams.value.dateRange.length === 2) {
-      params.startDate = queryParams.value.dateRange[0]
-      params.endDate = queryParams.value.dateRange[1]
-    }
+    const params = {}
+    if (queryParams.value.planName) params.planName = queryParams.value.planName
+    if (queryParams.value.planType !== '' && queryParams.value.planType != null) params.planType = queryParams.value.planType
+    if (queryParams.value.status !== '' && queryParams.value.status != null) params.status = queryParams.value.status
     const res = await getEventPlanPage(1, 999, params)
     tableData.value = res?.data?.records ?? res?.data ?? res ?? []
   } catch {
@@ -425,18 +337,7 @@ async function loadData() {
   }
 }
 
-onMounted(() => { loadData(); loadDeptOptions() })
-
-// ---------- 编制单位候选(系统部门,可手输) ----------
-const deptOptions = ref([])
-async function loadDeptOptions() {
-  try {
-    const res = await getDeptOptions()
-    deptOptions.value = res.data || []
-  } catch {
-    deptOptions.value = []
-  }
-}
+onMounted(() => { loadData(); loadPlanStatusOptions() })
 
 // ---------- 分页 ----------
 const pagination = ref({ page: 1, size: 10 })
@@ -450,10 +351,7 @@ const onPageChange = () => {}
 // ---------- 搜索 / 重置 ----------
 const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
-  queryParams.value.planName = ''
-  queryParams.value.dateRange = null
-  selectedTreeNode.value = null
-  treeRef.value?.setCurrentKey(null)
+  queryParams.value = { planName: '', planType: '', status: '' }
   pagination.value.page = 1
   loadData()
 }
@@ -468,7 +366,7 @@ const dialogTitle = ref('新增预案')
 const formRef = ref(null)
 const defaultForm = () => ({
   id: null, planName: '', planType: '', planLevel: '', version: '', compileUnit: '',
-  status: null, changeNote: '', content: '', attachName: '', attachPath: '', fileList: []
+  status: null, changeNote: '', content: ''
 })
 const form = ref(defaultForm())
 
@@ -512,8 +410,6 @@ const templatePageList = computed(() => {
   const start = (templatePagination.value.page - 1) * templatePagination.value.pageSize
   return templateList.value.slice(start, start + templatePagination.value.pageSize)
 })
-// 序号跨页连续(第2页从11开始)
-const templateIndex = (index) => (templatePagination.value.page - 1) * templatePagination.value.pageSize + index + 1
 const templateLoading = ref(false)
 const templateFormVisible = ref(false)
 const templateFormRef = ref(null)
@@ -553,7 +449,7 @@ async function loadTemplateList() {
   }
 }
 
-const defaultTemplateForm = () => ({ id: null, planName: '', eventType: '', eventLevel: '', responsibleUnit: '', planSummary: '', planContent: '' })
+const defaultTemplateForm = () => ({ id: null, planName: '', eventType: '', eventLevel: '', responsibleUnit: '', planContent: '' })
 
 function handleTemplateAdd() {
   templateForm.value = defaultTemplateForm()
@@ -608,10 +504,6 @@ const formRules = {
   changeNote: [{ required: true, message: '请填写修订说明', trigger: 'blur' }],
 }
 
-// 预案附件统一存 MinIO
-const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload?dir=dispatch_plan'
-const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
-
 const handleEdit = async (row) => {
   let detail = { ...row }
   try {
@@ -620,63 +512,11 @@ const handleEdit = async (row) => {
   } catch {
     // 详情接口失败时退回列表行数据
   }
-  form.value = {
-    ...detail,
-    changeNote: '',
-    fileList: detail.attachName ? [{ name: detail.attachName, url: detail.attachPath }] : []
-  }
+  form.value = { ...detail, changeNote: '' }
   dialogTitle.value = detail.status === 2 ? '修订预案' : '修改预案'
   dialogVisible.value = true
 }
 
-// 附件仅支持 PDF / Word(预案正文文档),单个 ≤50MB;弹窗内支持 Ctrl+V 粘贴
-const ALLOWED_PLAN_EXT = ['pdf', 'doc', 'docx']
-
-function handleBeforeUpload(file) {
-  const ext = String(file.name).split('.').pop().toLowerCase()
-  if (!ALLOWED_PLAN_EXT.includes(ext)) {
-    ElMessage.error('仅支持 PDF / Word 格式文件')
-    return false
-  }
-  if (file.size / 1024 / 1024 >= 50) {
-    ElMessage.error('文件大小不能超过 50MB')
-    return false
-  }
-  return true
-}
-
-function handleUploadSuccess(res) {
-  if (res.code === 200) {
-    form.value.attachName = res.originalFilename
-    // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
-    form.value.attachPath = res.relativePath || res.url
-  } else {
-    ElMessage.error(res.msg || '上传失败')
-  }
-}
-
-// 移除已上传文件时同步清空附件字段,避免列表残留旧附件
-function handleUploadRemove() {
-  form.value.attachName = ''
-  form.value.attachPath = ''
-}
-
-function handleUploadError() {
-  ElMessage.error('文件上传失败')
-}
-
-// 弹窗内 Ctrl+V 粘贴文件:塞给 el-upload 走同一套校验与上传流程(预案只允许 1 个附件)
-function handleDialogPaste(event) {
-  const files = Array.from(event.clipboardData?.files || [])
-  if (!files.length || !uploadRef.value) return
-  if ((form.value.fileList?.length || 0) >= 1) {
-    ElMessage.warning('只能上传一个文件,请先移除已上传的文件')
-    return
-  }
-  files.slice(0, 1).forEach(file => uploadRef.value.handleStart(file))
-  uploadRef.value.submit()
-}
-
 const handleSubmit = () => {
   formRef.value?.validate(async (valid) => {
     if (!valid) return
@@ -688,8 +528,6 @@ const handleSubmit = () => {
         planLevel: form.value.planLevel,
         compileUnit: form.value.compileUnit,
         content: form.value.content,
-        attachName: form.value.attachName,
-        attachPath: form.value.attachPath,
         changeNote: form.value.changeNote
       })
       dialogVisible.value = false
@@ -727,25 +565,10 @@ const handleBatchDelete = () => {
   }).catch(() => {})
 }
 
-// ---------- 下载 / 导出 ----------
-const handleDownload = (row) => {
-  if (!row.attachPath) return
-  window.open(row.attachPath, '_blank')
-}
-
-// Excel 公式参数转义(URL/文件名一般不含引号,兜底处理)
-const excelQuote = (value) => String(value ?? '').replace(/"/g, '""')
-
-// 附件列单元格:有附件写 HYPERLINK 公式(Excel/WPS 中点击打开附件文件),无附件留空
-function attachCell(row) {
-  if (!row.attachPath) return ''
-  const name = row.attachName || '附件'
-  return `=HYPERLINK("${excelQuote(row.attachPath)}","${excelQuote(name)}")`
-}
-
+// ---------- 导出 ----------
 const handleExport = () => {
   if (!tableData.value.length) return ElMessage.warning('暂无数据可导出')
-  const headers = ['预案编号', '预案名称', '预案类型', '预案级别', '版本', '附件名称', '发布时间', '编制单位', '状态']
+  const headers = ['预案编号', '预案名称', '预案类型', '预案级别', '版本', '发布时间', '编制单位', '状态']
   const aoa = [headers]
   tableData.value.forEach(row => {
     aoa.push([
@@ -754,19 +577,17 @@ const handleExport = () => {
       dictLabel(dis_plan_type.value, row.planType),
       dictLabel(dis_plan_level.value, row.planLevel),
       row.version,
-      // 附件列:HYPERLINK 公式,Excel/WPS 中点击即打开 MinIO 附件文件
-      attachCell(row),
       row.publishDate,
       row.compileUnit,
       row.statusName
     ])
   })
   const ws = XLSX.utils.aoa_to_sheet(aoa)
-  ws['!cols'] = [{ wch: 18 }, { wch: 24 }, { wch: 12 }, { wch: 10 }, { wch: 8 }, { wch: 28 }, { wch: 14 }, { wch: 16 }, { wch: 10 }]
+  ws['!cols'] = [{ wch: 18 }, { wch: 24 }, { wch: 12 }, { wch: 10 }, { wch: 8 }, { wch: 14 }, { wch: 16 }, { wch: 10 }]
   const wb = XLSX.utils.book_new()
   XLSX.utils.book_append_sheet(wb, ws, '应急预案')
   XLSX.writeFile(wb, '应急预案_' + new Date().toISOString().slice(0, 10) + '.xlsx')
-  ElMessage.success('导出成功(附件列可点击打开附件文件)')
+  ElMessage.success('导出成功')
 }
 
 const handleSubmitApproval = (row) => {
@@ -801,11 +622,6 @@ async function handleVersions(row) {
 const contentVisible = ref(false)
 const currentVersion = ref({})
 
-// 版本快照附件下载(MinIO 直链)
-const downloadVersionAttach = (row) => {
-  if (row?.attachPath) window.open(row.attachPath, '_blank')
-}
-
 // 正文为空时给出占位提示,避免 v-html 里再嵌引号
 const versionContentHtml = computed(
   () => currentVersion.value.content || '<p style="color:#909399">暂无正文</p>'
@@ -873,10 +689,18 @@ async function handlePreviewVersion(row) {
   min-width: 0;
 }
 
-.upload-tip {
-  font-size: 12px;
-  color: #909399;
-  margin-top: 4px;
+.plan-actions {
+  display: flex;
+  flex-wrap: nowrap;
+  justify-content: center;
+  align-items: center;
+  gap: 4px;
+  white-space: nowrap;
+
+  :deep(.el-button) {
+    margin-left: 0;
+    flex: 0 0 auto;
+  }
 }
 
 /* 宽体弹窗:小屏下内容超出时在弹窗体内滚动,footer 始终可见 */

+ 5 - 2
src/views/system/notice/NoticeEditor.vue

@@ -11,7 +11,10 @@ import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import '@wangeditor/editor/dist/css/style.css'
 import request from '@/utils/request'
 
-const props = defineProps({ modelValue: { type: String, default: '' } })
+const props = defineProps({
+  modelValue: { type: String, default: '' },
+  placeholder: { type: String, default: '请输入公告内容…' }
+})
 const emit = defineEmits(['update:modelValue'])
 const editorRef = shallowRef()
 const html = computed({
@@ -20,7 +23,7 @@ const html = computed({
 })
 const toolbarConfig = { excludeKeys: ['uploadVideo', 'fullScreen'] }
 const editorConfig = {
-  placeholder: '请输入公告内容…',
+  placeholder: props.placeholder,
   MENU_CONF: {
     uploadImage: {
       maxFileSize: 10 * 1024 * 1024,