Parcourir la source

feat(dispatch): 事件记录时间轴重做、富文本弹窗重排、按钮权限与列表优化

- 事件详情「事件记录」参考工单样式重做:新增 EventLogTimeline 组件,
  按日分组倒序 + 时间列 + 圆点虚线轴 + 操作人加粗 + 图片缩略图/文件链接
- 富文本:共享 Editor 组件加 media 开关(false 时无图片/视频/链接控件);
  预案正文与模板章节正文接入;预案/模板弹窗加宽 840px 重排(基础字段两列、
  正文加高 320、小屏弹窗体内滚动)
- 按钮权限:8 页 27 个操作按钮加 v-hasPermi,与后端权限串一一对应
- 列表与表单:9 处列表补「创建时间」列;核实处置人候选不再强依赖责任单位
  (未选显示全部用户带部门后缀);接报时间默认当前时间可清空;附件上传
  改存 relativePath;删除「下载模版」按钮
null il y a 4 jours
Parent
commit
3e3cc162d8

+ 9 - 3
src/components/Editor/index.vue

@@ -68,6 +68,11 @@ const props = defineProps({
   type: {
     type: String,
     default: "url",
+  },
+  /* 是否启用图片/视频/链接等媒体控件(false=纯文本富文本,如预案章节正文) */
+  media: {
+    type: Boolean,
+    default: true,
   }
 });
 
@@ -87,7 +92,8 @@ const options = ref({
       [{ color: [] }, { background: [] }],            // 字体颜色、字体背景颜色
       [{ align: [] }],                                // 对齐方式
       ["clean"],                                      // 清除文本格式
-      ["link", "image", "video"]                      // 链接、图片、视频
+      // 媒体控件(图片/视频/链接):media=false 时不注入(纯文本场景)
+      ...(props.media ? ["link", "image", "video"] : [])
     ],
   },
   placeholder: "请输入内容",
@@ -112,9 +118,9 @@ watch(() => props.modelValue, (v) => {
   }
 }, { immediate: true });
 
-// 如果设置了上传地址则自定义图片上传事件
+// 如果设置了上传地址则自定义图片上传事件(media=false 无图片控件,跳过)
 onMounted(() => {
-  if (props.type == 'url') {
+  if (props.type == 'url' && props.media) {
     let quill = quillEditorRef.value.getQuill();
     let toolbar = quill.getModule("toolbar");
     toolbar.addHandler("image", (value) => {

+ 234 - 0
src/views/subSystem/dispatch/disOnDuty/EventLogTimeline.vue

@@ -0,0 +1,234 @@
+<template>
+  <div class="log-timeline">
+    <div class="log-hint">按时间倒序排列 · 滚动即可回溯</div>
+    <div v-for="group in groups" :key="group.date" class="log-group">
+      <div class="log-date">{{ group.date }}</div>
+      <div class="log-list">
+        <div v-for="(item, i) in group.items" :key="i" class="log-row">
+          <div class="log-time">{{ item.hhmm }}</div>
+          <div class="log-rail">
+            <i class="log-dot" :style="{ background: item.color }"></i>
+            <span v-if="i < group.items.length - 1" class="log-line"></span>
+          </div>
+          <div class="log-body">
+            <div class="log-title">
+              <b>{{ item.operator }}</b>&nbsp;{{ item.action }}
+            </div>
+            <div v-if="item.desc" class="log-desc">{{ item.desc }}</div>
+            <div v-if="item.images.length" class="log-atts">
+              <el-image
+                v-for="(src, j) in item.images"
+                :key="j"
+                :src="src"
+                :preview-src-list="item.images"
+                :initial-index="j"
+                fit="cover"
+                class="log-img"
+                preview-teleported
+              />
+            </div>
+            <div v-if="item.files.length" class="log-files">
+              <el-link
+                v-for="(f, j) in item.files"
+                :key="j"
+                type="primary"
+                :underline="false"
+                @click="downloadFile(f)"
+              >
+                <el-icon><Document /></el-icon>&nbsp;{{ f.name }}
+              </el-link>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div v-if="!groups.length" class="log-empty">暂无记录</div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { Document } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+// 事件操作日志时间轴(参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件展示)
+// logs:后端 dis_event_log 原始对象数组(operTime/operTypeName/operUserName/operDesc/targets/attachUrl/operType)
+const props = defineProps({
+  logs: { type: Array, default: () => [] }
+})
+
+// operType -> 节点圆点颜色(与原 el-timeline 的 type 语义一致)
+const LOG_COLOR = {
+  0: '#409eff', 1: '#67c23a', 2: '#909399', 3: '#409eff', 4: '#409eff',
+  5: '#e6a23c', 6: '#909399', 7: '#67c23a', 8: '#e6a23c', 9: '#909399'
+}
+
+const IMG_EXT = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
+
+function splitAttach(attachUrl) {
+  return String(attachUrl || '').split(',').map(s => s.trim()).filter(Boolean)
+}
+
+function extOf(url) {
+  const clean = url.split('?')[0]
+  return String(clean.split('.').pop() || '').toLowerCase()
+}
+
+function nameOf(url) {
+  return decodeURIComponent(url.split('?')[0].split('/').pop() || '附件')
+}
+
+const groups = computed(() => {
+  // 整体按时间倒序(后端返回升序),再按日期分组保持组内倒序
+  const list = [...(props.logs || [])].reverse()
+  const map = new Map()
+  list.forEach(log => {
+    const t = String(log.operTime || '')
+    const datePart = t.slice(0, 10)
+    if (!datePart) return
+    const [y, m, d] = datePart.split('-')
+    const dateLabel = `${Number(m)}月${Number(d)}日`
+    const hhmm = t.slice(11, 16) || ''
+    const urls = splitAttach(log.attachUrl)
+    const images = urls.filter(u => IMG_EXT.includes(extOf(u)))
+    const files = urls.filter(u => !IMG_EXT.includes(extOf(u))).map(u => ({ url: u, name: nameOf(u) }))
+    const key = dateLabel
+    if (!map.has(key)) map.set(key, { date: dateLabel, year: y, items: [] })
+    map.get(key).items.push({
+      hhmm,
+      color: LOG_COLOR[log.operType] || '#409eff',
+      operator: log.operUserName || '系统',
+      action: log.operTypeName || '操作',
+      desc: log.operDesc || log.targets || '',
+      images,
+      files
+    })
+  })
+  return [...map.values()]
+})
+
+function downloadFile(f) {
+  // 简单可靠:新窗口打开,浏览器按 Content-Disposition 处理(图片直接预览,文档触发下载)
+  if (!f?.url) return
+  const w = window.open(f.url, '_blank')
+  if (!w) ElMessage.warning('浏览器拦截了新窗口,请允许弹窗后重试')
+}
+</script>
+
+<style scoped>
+.log-timeline {
+  max-height: 420px;
+  overflow-y: auto;
+  padding: 2px 2px 0 4px;
+}
+
+.log-hint {
+  font-size: 12px;
+  color: #c0c4cc;
+  margin-bottom: 10px;
+}
+
+.log-group {
+  margin-bottom: 6px;
+}
+
+.log-date {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  padding: 6px 0;
+}
+
+.log-row {
+  display: flex;
+  gap: 8px;
+}
+
+.log-time {
+  width: 42px;
+  flex-shrink: 0;
+  font-size: 12px;
+  color: #909399;
+  padding-top: 3px;
+  text-align: left;
+}
+
+.log-rail {
+  width: 14px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.log-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-top: 6px;
+  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.15);
+  flex-shrink: 0;
+}
+
+.log-line {
+  flex: 1;
+  width: 0;
+  border-left: 1px dashed #dcdfe6;
+  margin: 2px 0 4px;
+}
+
+.log-body {
+  flex: 1;
+  min-width: 0;
+  padding-bottom: 18px;
+}
+
+.log-title {
+  font-size: 13px;
+  color: #303133;
+  line-height: 1.5;
+}
+
+.log-title b {
+  font-weight: 600;
+  color: #303133;
+}
+
+.log-desc {
+  margin-top: 2px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+  word-break: break-all;
+  white-space: pre-wrap;
+}
+
+.log-atts {
+  margin-top: 8px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.log-img {
+  width: 72px;
+  height: 72px;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  cursor: zoom-in;
+}
+
+.log-files {
+  margin-top: 6px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  font-size: 13px;
+}
+
+.log-empty {
+  font-size: 13px;
+  color: #909399;
+  padding: 12px 0;
+}
+</style>

+ 14 - 31
src/views/subSystem/dispatch/disOnDuty/cphs.vue

@@ -54,6 +54,7 @@
         </el-table-column>
         <el-table-column prop="location" label="发生地点" show-overflow-tooltip />
         <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="statusName" label="状态" width="100">
           <template #default="{ row }">
@@ -62,9 +63,9 @@
         </el-table-column>
         <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="row.status === 0" type="primary" plain size="small" @click="handleVerify(row)">核实</el-button>
+            <el-button v-if="row.status === 0" type="primary" plain size="small" @click="handleVerify(row)" v-hasPermi="['dispatch:event:verify']">核实</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)">删除</el-button>
+            <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -143,12 +144,12 @@
         <el-form-item label="责任处置人" prop="handler">
           <el-select
             v-model="verifyForm.handler"
-            :placeholder="verifyForm.deptId ? '请选择责任处置人(该部门人员)' : '请先选择责任单位'"
+            :placeholder="verifyForm.deptId ? '请选择责任处置人(该部门人员)' : '请选择责任处置人(系统用户)'"
             filterable
             style="width:100%"
             @change="handleHandlerChange"
           >
-            <el-option v-for="u in filteredHandlerOptions" :key="u.userId" :label="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.nickName" />
           </el-select>
         </el-form-item>
         <el-form-item label="联系电话" prop="handlerPhone">
@@ -235,17 +236,8 @@
             <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
           </el-form-item>
           <div class="section-title">事件记录(操作日志)</div>
-          <el-timeline v-if="currentRow.timeline && currentRow.timeline.length">
-            <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>
+          <!-- 参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件缩略图 -->
+          <EventLogTimeline v-if="currentRow.rawLogs && currentRow.rawLogs.length" :logs="currentRow.rawLogs" />
           <div v-else class="desc-text">暂无记录</div>
         </el-form>
       </div>
@@ -264,6 +256,7 @@ import { handleTree } from '@/utils/ruoyi'
 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 { Search, Refresh } from '@element-plus/icons-vue'
 
@@ -325,9 +318,9 @@ async function loadDeptOptions() {
   }
 }
 
-// 责任处置人候选:所选责任单位(部门)所属人员
+// 责任处置人候选:选了责任单位 → 只显示该单位人员;未选 → 显示全部启用用户,不强制先选单位
 const filteredHandlerOptions = computed(() => {
-  if (!verifyForm.value.deptId) return []
+  if (!verifyForm.value.deptId) return userOptions.value
   return userOptions.value.filter(u => String(u.deptId) === String(verifyForm.value.deptId))
 })
 
@@ -373,16 +366,6 @@ function levelTagType(level) {
 }
 
 // 操作日志 -> 时间线节点(operType 见后端 EventLogOperTypeEnum:0 接报登记 ... 9 信息修改)
-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)
@@ -394,8 +377,8 @@ const pagination = ref({ page: 1, pageSize: 10 })
 async function loadData() {
   loading.value = true
   try {
-    // 初判核实页默认只看待核实(0)
-    const params = { status: 0 }
+    // 初判核实页默认只看待核实(0);按接报时间升序,先报先核
+    const params = { status: 0, orderAsc: true }
     if (searchForm.value.eventCategory) params.eventCategory = searchForm.value.eventCategory
     if (searchForm.value.eventLevel) params.eventLevel = searchForm.value.eventLevel
     if (searchForm.value.status !== '' && searchForm.value.status !== null) params.status = searchForm.value.status
@@ -654,11 +637,11 @@ const detailVisible = ref(false)
 const currentRow = ref(null)
 
 async function handleDetail(row) {
-  currentRow.value = { ...row, timeline: [] }
+  currentRow.value = { ...row, rawLogs: [] }
   detailVisible.value = true
   try {
     const res = await getEventLogs(row.id)
-    currentRow.value = { ...currentRow.value, timeline: toTimeline(res.data) }
+    currentRow.value = { ...currentRow.value, rawLogs: res.data || [] }
   } catch {
     // 加载失败时保留列表行数据
   }

+ 2 - 1
src/views/subSystem/dispatch/disOnDuty/gzcb.vue

@@ -56,6 +56,7 @@
         <el-table-column prop="lastUrgeTime" label="最近催办时间" width="150">
           <template #default="{ row }">{{ row.lastUrgeTime || '-' }}</template>
         </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="150" />
         <el-table-column prop="statusName" label="状态" width="90">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
@@ -63,7 +64,7 @@
         </el-table-column>
         <el-table-column label="操作" width="160" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="canUrge(row)" type="primary" plain size="small" @click="handleUrge(row)">催办</el-button>
+            <el-button v-if="canUrge(row)" type="primary" plain size="small" @click="handleUrge(row)" v-hasPermi="['dispatch:event:urge']">催办</el-button>
             <el-button type="info" plain size="small" @click="handleViewRecords(row)">查看记录</el-button>
           </template>
         </el-table-column>

+ 8 - 6
src/views/subSystem/dispatch/disOnDuty/gzjc.vue

@@ -64,13 +64,14 @@
         <el-table-column prop="nextVisitTime" label="下次回访" min-width="150">
           <template #default="{ row }">{{ row.nextVisitTime || '-' }}</template>
         </el-table-column>
+        <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)">开始处置</el-button>
-            <el-button v-if="row.status === 2" type="warning" plain size="small" @click="handleTrack(row)">跟踪</el-button>
-            <el-button v-if="row.status === 2" type="success" plain size="small" @click="handleRelease(row)">解除</el-button>
+            <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 === 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)">删除</el-button>
+            <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -447,7 +448,7 @@ const hasValue = (val) => val !== '' && val !== null && val !== undefined
 async function loadData() {
   loading.value = true
   try {
-    const params = { statusList: [1, 2, 3] }
+    const params = { statusList: [1, 2, 3], orderAsc: true }
     if (hasValue(searchForm.value.status)) params.statusList = [searchForm.value.status]
     if (searchForm.value.eventCategory) params.eventCategory = searchForm.value.eventCategory
     if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
@@ -598,7 +599,8 @@ function handleAttachBefore(file) {
 
 function handleAttachSuccess(res, slot) {
   if (res.code === 200 && res.url) {
-    releaseForm.value[slot + 'Urls'].push(res.url)
+    // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
+    releaseForm.value[slot + 'Urls'].push(res.relativePath || res.url)
   } else {
     ElMessage.error(res.msg || '上传失败')
   }

+ 20 - 30
src/views/subSystem/dispatch/disOnDuty/jbgl.vue

@@ -30,7 +30,7 @@
         </el-form-item>
       </el-form>
       <div class="filter-actions">
-        <el-button type="primary" @click="handleAdd">新增接报</el-button>
+        <el-button type="primary" @click="handleAdd" v-hasPermi="['dispatch:event:add']">新增接报</el-button>
       </div>
     </div>
 
@@ -46,6 +46,7 @@
         </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="description" label="事件描述" show-overflow-tooltip />
         <el-table-column prop="reporter" label="接报人" width="100" />
         <el-table-column prop="statusName" label="状态" width="100">
@@ -55,9 +56,9 @@
         </el-table-column>
         <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
-            <el-button v-if="row.editable" type="primary" plain size="small" @click="handleEdit(row)">编辑</el-button>
+            <el-button v-if="row.editable" type="primary" plain size="small" @click="handleEdit(row)" v-hasPermi="['dispatch:event:edit']">编辑</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)">删除</el-button>
+            <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -125,17 +126,8 @@
             <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
           </el-form-item>
           <div class="section-title">事件记录(操作日志)</div>
-          <el-timeline v-if="currentRow.timeline && currentRow.timeline.length">
-            <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>
+          <!-- 参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件缩略图 -->
+          <EventLogTimeline v-if="currentRow.rawLogs && currentRow.rawLogs.length" :logs="currentRow.rawLogs" />
           <div v-else class="desc-text">暂无记录</div>
         </el-form>
       </div>
@@ -176,7 +168,8 @@
           <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-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="请输入上报人联系电话" />
@@ -203,6 +196,7 @@ import useUserStore from '@/store/modules/user'
 import { bd09ToWgs84 } from '@/utils/coordTransform'
 import { getEventPage, saveEventReport, updateEventInfo, deleteEventInfo, getEventStatusOptions, getEventLogs } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
+import EventLogTimeline from './EventLogTimeline.vue'
 
 // ==================== 字典 ====================
 const { dis_event_source, dis_event_channel, dis_event_type } = useDict('dis_event_source', 'dis_event_channel', 'dis_event_type')
@@ -227,17 +221,6 @@ function statusTagType(status) {
   return STATUS_TAG[status] || 'info'
 }
 
-// 操作日志 -> 时间线节点(operType 见后端 EventLogOperTypeEnum:0 接报登记 ... 9 信息修改)
-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)
@@ -309,11 +292,11 @@ const detailVisible = ref(false)
 const currentRow = ref(null)
 
 async function handleDetail(row) {
-  currentRow.value = { ...row, timeline: [] }
+  currentRow.value = { ...row, rawLogs: [] }
   detailVisible.value = true
   try {
     const res = await getEventLogs(row.id)
-    currentRow.value = { ...currentRow.value, timeline: toTimeline(res.data) }
+    currentRow.value = { ...currentRow.value, rawLogs: res.data || [] }
   } catch {
     // 加载失败时保留列表行数据
   }
@@ -328,11 +311,18 @@ const editingId = ref(null)
 function blankForm() {
   return {
     eventSource: '', eventType: '', location: '', longitude: null, latitude: null,
-    happenTime: '', description: '', reporter: currentOperatorName(), reportTime: '',
+    happenTime: '', description: '', reporter: currentOperatorName(), reportTime: nowStr(),
     reporterPhone: '', channel: ''
   }
 }
 
+// 当前时间(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' }],
@@ -340,7 +330,7 @@ const addRules = {
   happenTime: [{ required: true, message: '请选择发生时间', trigger: 'change' }],
   description: [{ required: true, message: '请输入事件描述', trigger: 'blur' }],
   reporter: [{ required: true, message: '请输入接报人', trigger: 'blur' }],
-  reportTime: [{ required: true, message: '请选择接报时间', trigger: 'change' }],
+  reportTime: [],
   channel: [{ required: true, message: '请选择来源渠道', trigger: 'change' }]
 }
 

+ 7 - 5
src/views/subSystem/dispatch/disOnDuty/zjbs.vue

@@ -69,6 +69,7 @@
         <el-table-column prop="lastTransferTime" label="转接时间" min-width="150">
           <template #default="{ row }">{{ row.lastTransferTime || '-' }}</template>
         </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="150" />
         <el-table-column prop="statusName" label="状态" width="100">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
@@ -76,10 +77,10 @@
         </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)">转接</el-button>
-            <el-button v-if="canReport(row)" type="warning" plain size="small" @click="handleReport(row)">报送</el-button>
+            <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 type="info" plain size="small" @click="handleDetail(row)">详情</el-button>
-            <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(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>
         </el-table-column>
       </el-table>
@@ -306,7 +307,7 @@ const hasValue = (val) => val !== '' && val !== null && val !== undefined
 async function loadData() {
   loading.value = true
   try {
-    const params = { statusList: [1, 2] }
+    const params = { statusList: [1, 2], orderAsc: true }
     if (searchForm.value.eventNo) params.eventNo = searchForm.value.eventNo
     if (hasValue(searchForm.value.status)) {
       params.statusList = [searchForm.value.status]
@@ -469,9 +470,10 @@ function handleBeforeUpload(file) {
 
 function handleUploadSuccess(res) {
   if (res.code === 200 && res.url) {
+    // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
     reportForm.value.attachments.push({
       fileName: res.originalFilename,
-      url: res.url
+      url: res.relativePath || res.url
     })
   } else {
     ElMessage.error(res.msg || '上传失败')

+ 5 - 4
src/views/subSystem/dispatch/disPlanManage/DisEventGrade.vue

@@ -34,8 +34,8 @@
         <el-button :icon="Refresh" @click="handleReset">重置</el-button>
       </el-form-item>
       <el-form-item style="float: right">
-        <el-button type="success" @click="handleAdd">新增</el-button>
-        <el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
+        <el-button type="success" @click="handleAdd" v-hasPermi="['dispatch:grade:add']">新增</el-button>
+        <el-button type="danger" @click="handleBatchDelete" v-hasPermi="['dispatch:grade:remove']">批量删除</el-button>
         <el-button type="primary" @click="handleExport">导出</el-button>
       </el-form-item>
     </el-form>
@@ -56,6 +56,7 @@
       <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="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>
@@ -63,8 +64,8 @@
       </el-table-column>
       <el-table-column label="操作" width="140" align="center">
         <template #default="{ row }">
-          <el-button type="primary" plain size="small" @click="handleEdit(row)">编辑</el-button>
-          <el-button type="danger" plain size="small" @click="handleDelete(row)">删除</el-button>
+          <el-button type="primary" plain size="small" @click="handleEdit(row)" v-hasPermi="['dispatch:grade:edit']">编辑</el-button>
+          <el-button type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:grade:remove']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>

+ 3 - 2
src/views/subSystem/dispatch/disPlanManage/DisPlanApproval.vue

@@ -45,6 +45,7 @@
       <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="status" label="状态" width="100" align="center">
         <template #default="{ row }">
           <el-tag :type="statusTagType(row.status)">{{ row.statusName }}</el-tag>
@@ -53,7 +54,7 @@
       <el-table-column label="操作" width="130" align="center">
         <template #default="{ row }">
           <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)">审批</el-button>
+          <el-button v-if="row.status === 1" type="primary" plain size="small" @click="handleApprove(row)" v-hasPermi="['dispatch:plan:approve']">审批</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -129,7 +130,7 @@
           </template>
           <template v-else>
             <div class="unpublished-tip">未发布预案(应急处置不可用)</div>
-            <el-button v-if="currentRow.status === 1" type="success" @click="handleApprove(currentRow)">去审批</el-button>
+            <el-button v-if="currentRow.status === 1" type="success" @click="handleApprove(currentRow)" v-hasPermi="['dispatch:plan:approve']">去审批</el-button>
           </template>
         </div>
       </div>

+ 67 - 59
src/views/subSystem/dispatch/disPlanManage/DisPlanEditing.vue

@@ -24,11 +24,10 @@
         <el-button :icon="Refresh" @click="handleReset">重置</el-button>
       </el-form-item>
       <el-form-item style="float: right">
-        <el-button type="success" @click="handleAdd">新增</el-button>
+        <el-button type="success" @click="handleAdd" v-hasPermi="['dispatch:plan:add']">新增</el-button>
         <el-button @click="openTemplateLibrary">模板库</el-button>
-        <el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
+        <el-button type="danger" @click="handleBatchDelete" v-hasPermi="['dispatch:plan:remove']">批量删除</el-button>
         <el-button type="primary" @click="handleExport">导出</el-button>
-        <el-button type="primary" @click="handleDownloadTemplate">下载模版</el-button>
       </el-form-item>
     </el-form>
 
@@ -81,6 +80,7 @@
             </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="statusName" label="状态" width="100" align="center">
             <template #default="{ row }">
@@ -89,9 +89,9 @@
           </el-table-column>
           <el-table-column label="操作" width="270" align="center">
             <template #default="{ row }">
-              <el-button type="primary" plain size="small" @click="handleEdit(row)">{{ isReviseRow(row) ? '修订' : '编辑' }}</el-button>
-              <el-button type="danger" plain size="small" @click="handleDelete(row)">删除</el-button>
-              <el-button v-if="row.submittable" type="success" plain size="small" @click="handleSubmitApproval(row)">提交审核</el-button>
+              <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>
             </template>
           </el-table-column>
@@ -116,7 +116,7 @@
       <el-tab-pane label="预案模板库" name="templates">
         <div style="margin-bottom:12px;display:flex;justify-content:space-between;align-items:center">
           <span style="color:#909399;font-size:13px">模板用于「新建预案」时预填名称与章节正文</span>
-          <el-button type="primary" size="small" @click="handleTemplateAdd">新增模板</el-button>
+          <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" />
@@ -129,10 +129,11 @@
           <el-table-column prop="planSummary" label="概要" min-width="260" show-overflow-tooltip>
             <template #default="{ row }">{{ row.planSummary || '-' }}</template>
           </el-table-column>
+          <el-table-column prop="createTime" label="创建时间" width="150" align="center" />
           <el-table-column label="操作" width="140" align="center">
             <template #default="{ row }">
-              <el-button type="primary" plain size="small" @click="handleTemplateEdit(row)">编辑</el-button>
-              <el-button type="danger" plain size="small" @click="handleTemplateDelete(row)">删除</el-button>
+              <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>
             </template>
           </el-table-column>
         </el-table>
@@ -178,25 +179,37 @@
     </el-dialog>
 
     <!-- 新增/修改预案弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="580px" destroy-on-close>
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="840px" top="5vh" destroy-on-close class="plan-dialog">
       <div @paste="handleDialogPaste">
         <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
-          <el-form-item label="预案名称" prop="planName">
-            <el-input v-model="form.planName" placeholder="请输入预案名称" />
-          </el-form-item>
-          <el-form-item label="预案级别" prop="planLevel">
-            <el-select v-model="form.planLevel" placeholder="请选择" style="width: 100%">
-              <el-option v-for="d in dis_plan_level" :key="d.value" :label="d.label" :value="d.value" />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="预案类型" prop="planType">
-            <el-select v-model="form.planType" placeholder="请选择预案类型" style="width: 100%">
-              <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-input v-model="form.version" disabled placeholder="由系统在审批通过后生成" />
-          </el-form-item>
+          <el-row :gutter="16">
+            <el-col :span="12">
+              <el-form-item label="预案名称" prop="planName">
+                <el-input v-model="form.planName" placeholder="请输入预案名称" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="预案级别" prop="planLevel">
+                <el-select v-model="form.planLevel" placeholder="请选择" style="width: 100%">
+                  <el-option v-for="d in dis_plan_level" :key="d.value" :label="d.label" :value="d.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="16">
+            <el-col :span="12">
+              <el-form-item label="预案类型" prop="planType">
+                <el-select v-model="form.planType" placeholder="请选择预案类型" style="width: 100%">
+                  <el-option v-for="d in dis_plan_type" :key="d.value" :label="d.label" :value="d.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="预案版本">
+                <el-input v-model="form.version" disabled placeholder="由系统在审批通过后生成" />
+              </el-form-item>
+            </el-col>
+          </el-row>
           <el-form-item label="编制单位" prop="compileUnit">
             <!-- 可从系统部门选择,也可手动输入(allow-create 允许创建新条目) -->
             <el-select
@@ -220,14 +233,8 @@
             <el-input v-model="form.changeNote" type="textarea" :rows="3" placeholder="请填写本次修订说明(修订已通过预案必填)" />
           </el-form-item>
           <el-form-item label="预案正文">
-            <el-input
-              v-model="form.content"
-              type="textarea"
-              :rows="5"
-              maxlength="2000"
-              show-word-limit
-              placeholder="请输入预案正文要点(将随版本快照保存,可在版本管理中查看历史正文)"
-            />
+            <!-- 与模板库章节正文同一套纯文本富文本(无图片/视频控件);内容随版本快照保存 -->
+            <editor v-model="form.content" :media="false" :min-height="320" placeholder="请输入预案正文(将随版本快照保存,可在版本管理中查看历史正文)" />
           </el-form-item>
           <el-form-item label="附件上传">
             <el-upload
@@ -259,7 +266,7 @@
     </el-dialog>
 
     <!-- 模板新增/编辑弹窗 -->
-    <el-dialog v-model="templateFormVisible" :title="templateForm.id ? '编辑模板' : '新增模板'" width="620px" destroy-on-close>
+    <el-dialog v-model="templateFormVisible" :title="templateForm.id ? '编辑模板' : '新增模板'" width="840px" top="5vh" destroy-on-close class="plan-dialog">
         <el-form ref="templateFormRef" :model="templateForm" :rules="templateRules" label-width="90px">
           <el-form-item label="模板名称" prop="planName">
             <el-input v-model="templateForm.planName" maxlength="60" placeholder="请输入模板名称" />
@@ -280,14 +287,21 @@
               </el-form-item>
             </el-col>
           </el-row>
-          <el-form-item label="责任单位">
-            <el-input v-model="templateForm.responsibleUnit" maxlength="50" placeholder="请输入责任单位(选填)" />
-          </el-form-item>
-          <el-form-item label="模板概要">
-            <el-input v-model="templateForm.planSummary" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="一句话说明模板适用场景(新建预案选择时展示)" />
-          </el-form-item>
+          <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>
+            </el-col>
+          </el-row>
           <el-form-item label="章节正文">
-            <el-input v-model="templateForm.planContent" type="textarea" :rows="8" placeholder="模板章节大纲/正文,新建预案套用后可在正文基础上编写" />
+            <!-- 纯文本富文本:无图片/视频/链接控件 -->
+            <editor v-model="templateForm.planContent" :media="false" :min-height="320" placeholder="模板章节大纲/正文,新建预案套用后可在正文基础上编写" />
           </el-form-item>
         </el-form>
         <template #footer>
@@ -634,7 +648,8 @@ function handleBeforeUpload(file) {
 function handleUploadSuccess(res) {
   if (res.code === 200) {
     form.value.attachName = res.originalFilename
-    form.value.attachPath = res.url
+    // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
+    form.value.attachPath = res.relativePath || res.url
   } else {
     ElMessage.error(res.msg || '上传失败')
   }
@@ -728,22 +743,6 @@ function attachCell(row) {
   return `=HYPERLINK("${excelQuote(row.attachPath)}","${excelQuote(name)}")`
 }
 
-// 下载模板:真实 xlsx 模板(表头 + 示例行),供后续批量导入参照
-const handleDownloadTemplate = () => {
-  const headers = ['预案名称', '预案类型', '预案级别', '编制单位', '附件名称', '预案正文']
-  const sampleType = dis_plan_type.value?.[0]?.label || '综合'
-  const sampleLevel = dis_plan_level.value?.[0]?.label || 'Ⅲ级'
-  const aoa = [
-    headers,
-    ['示例:沅陵县供水管网爆管专项预案', sampleType, sampleLevel, '沅陵县供水公司', '示例_预案文档.docx', '预案正文要点:适用场景、响应分级、处置流程…(导入前请删除本示例行)']
-  ]
-  const ws = XLSX.utils.aoa_to_sheet(aoa)
-  ws['!cols'] = [{ wch: 32 }, { wch: 12 }, { wch: 10 }, { wch: 18 }, { wch: 24 }, { wch: 50 }]
-  const wb = XLSX.utils.book_new()
-  XLSX.utils.book_append_sheet(wb, ws, '预案导入模板')
-  XLSX.writeFile(wb, '应急预案导入模板.xlsx')
-}
-
 const handleExport = () => {
   if (!tableData.value.length) return ElMessage.warning('暂无数据可导出')
   const headers = ['预案编号', '预案名称', '预案类型', '预案级别', '版本', '附件名称', '发布时间', '编制单位', '状态']
@@ -879,4 +878,13 @@ async function handlePreviewVersion(row) {
   color: #909399;
   margin-top: 4px;
 }
+
+/* 宽体弹窗:小屏下内容超出时在弹窗体内滚动,footer 始终可见 */
+.plan-dialog {
+  :deep(.el-dialog__body) {
+    max-height: calc(86vh - 110px);
+    overflow-y: auto;
+    padding-top: 16px;
+  }
+}
 </style>