gzjc.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <template>
  2. <div class="app-container gzjc-page">
  3. <!-- 搜索区(若依风格) -->
  4. <div class="filter-panel">
  5. <el-form :inline="true" :model="searchForm" class="filter-form" label-width="80px">
  6. <el-form-item label="事件状态">
  7. <el-select v-model="searchForm.status" placeholder="请选择事件状态" clearable style="width:140px">
  8. <el-option v-for="d in trackStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
  9. </el-select>
  10. </el-form-item>
  11. <el-form-item label="事件类别">
  12. <el-select v-model="searchForm.eventCategory" placeholder="请选择事件类别" clearable filterable style="width:140px">
  13. <el-option v-for="d in dis_event_category" :key="d.value" :label="d.label" :value="d.value" />
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="接报时间">
  17. <el-date-picker
  18. v-model="searchForm.dateRange"
  19. type="daterange"
  20. range-separator="至"
  21. start-placeholder="开始日期"
  22. end-placeholder="结束日期"
  23. style="width:240px"
  24. value-format="YYYY-MM-DD"
  25. />
  26. </el-form-item>
  27. <el-form-item>
  28. <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
  29. <el-button :icon="Refresh" @click="handleReset">重置</el-button>
  30. </el-form-item>
  31. </el-form>
  32. </div>
  33. <!-- 数据表格 -->
  34. <div class="table-panel">
  35. <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
  36. <el-table-column prop="eventNo" label="事件编号" width="140" />
  37. <el-table-column prop="eventCategory" label="事件类别" width="110">
  38. <template #default="{ row }">{{ dictLabel(dis_event_category, row.eventCategory) }}</template>
  39. </el-table-column>
  40. <el-table-column prop="eventLevel" label="事件等级" width="100">
  41. <template #default="{ row }">
  42. <el-tag v-if="row.eventLevel" :type="levelTagType(row.eventLevel)" size="small">{{ dictLabel(dis_event_level, row.eventLevel) }}</el-tag>
  43. <span v-else>-</span>
  44. </template>
  45. </el-table-column>
  46. <el-table-column prop="location" label="发生地点" min-width="150" show-overflow-tooltip>
  47. <template #default="{ row }">{{ row.location || '-' }}</template>
  48. </el-table-column>
  49. <el-table-column prop="statusName" label="当前状态" width="100">
  50. <template #default="{ row }">
  51. <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
  52. </template>
  53. </el-table-column>
  54. <el-table-column prop="handler" label="处置人" width="100">
  55. <template #default="{ row }">{{ row.handler || '-' }}</template>
  56. </el-table-column>
  57. <el-table-column prop="lastVisitTime" label="最近回访" width="150">
  58. <template #default="{ row }">{{ row.lastVisitTime || '-' }}</template>
  59. </el-table-column>
  60. <el-table-column prop="nextVisitTime" label="下次回访" min-width="150">
  61. <template #default="{ row }">{{ row.nextVisitTime || '-' }}</template>
  62. </el-table-column>
  63. <el-table-column prop="createTime" label="创建时间" width="150" />
  64. <el-table-column label="操作" width="260" fixed="right">
  65. <template #default="{ row }">
  66. <el-button v-if="row.status === 1 && isHandler(row)" type="primary" plain size="small" @click="openOperation(row, 'start')" v-hasPermi="['dispatch:event:handle']">开始处置</el-button>
  67. <el-button v-if="row.status === 2 && isHandler(row)" type="warning" plain size="small" @click="openOperation(row, 'track')" v-hasPermi="['dispatch:event:handle']">跟踪</el-button>
  68. <el-button v-if="row.status === 2 && isHandler(row)" type="primary" plain size="small" @click="openOperation(row, 'submit')" v-hasPermi="['dispatch:event:handle']">提交核验</el-button>
  69. <el-button v-if="row.status === 3 && isVerifier(row)" type="success" plain size="small" @click="handleAudit(row)" v-hasPermi="['dispatch:event:audit']">核验</el-button>
  70. <el-button type="info" plain size="small" @click="handleDetail(row)">详情</el-button>
  71. <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
  72. </template>
  73. </el-table-column>
  74. </el-table>
  75. <div class="pagination-wrap">
  76. <el-pagination
  77. v-model:current-page="pagination.page"
  78. v-model:page-size="pagination.pageSize"
  79. :page-sizes="[10, 20, 50]"
  80. layout="total, sizes, prev, pager, next, jumper"
  81. :total="total"
  82. @size-change="handleSizeChange"
  83. @current-change="handleCurrentChange"
  84. />
  85. </div>
  86. </div>
  87. <!-- 开始处置和跟踪共用备注+附件表单 -->
  88. <el-dialog v-model="operationVisible" :title="operationType === 'start' ? '开始处置' : operationType === 'submit' ? '提交核验' : '事件跟踪'" width="600px" :close-on-click-modal="false">
  89. <el-form :model="operationForm" :rules="operationRules" ref="operationFormRef" label-width="100px">
  90. <el-form-item label="备注" prop="operDesc">
  91. <el-input v-model="operationForm.operDesc" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入备注" />
  92. </el-form-item>
  93. <el-form-item label="附件">
  94. <el-upload
  95. ref="operationUploadRef"
  96. v-model:file-list="operationForm.fileList"
  97. :action="uploadUrl"
  98. :headers="uploadHeaders"
  99. :limit="5"
  100. multiple
  101. accept=".jpg,.jpeg,.png,.gif,.bmp,.webp,.mp4,.mov,.m4v,.webm"
  102. :on-success="handleOperationAttachSuccess"
  103. :on-error="handleAttachError"
  104. :on-exceed="() => ElMessage.warning('附件最多 5 个')"
  105. :before-upload="handleAttachBefore"
  106. >
  107. <el-button type="primary" plain>上传附件</el-button>
  108. <template #tip><div class="el-upload__tip">图片或视频,单个不超过 50MB,最多 5 个</div></template>
  109. </el-upload>
  110. </el-form-item>
  111. </el-form>
  112. <template #footer>
  113. <el-button @click="operationVisible = false">取消</el-button>
  114. <el-button type="primary" :loading="operationSubmitting" @click="confirmOperation">{{ operationType === 'start' ? '开始处置' : operationType === 'submit' ? '提交核验' : '保存跟踪' }}</el-button>
  115. </template>
  116. </el-dialog>
  117. <el-dialog v-model="auditVisible" title="事件核验" width="560px" :close-on-click-modal="false">
  118. <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
  119. <el-form-item label="事件编号">{{ auditForm.eventNo }}</el-form-item>
  120. <el-form-item label="核验结果" prop="resolved">
  121. <el-radio-group v-model="auditForm.resolved"><el-radio :value="true">已解除</el-radio><el-radio :value="false">未解除</el-radio></el-radio-group>
  122. </el-form-item>
  123. <el-form-item label="核验意见" prop="opinion"><el-input v-model="auditForm.opinion" type="textarea" :rows="4" placeholder="请输入核验意见" /></el-form-item>
  124. </el-form>
  125. <template #footer><el-button @click="auditVisible = false">取消</el-button><el-button type="primary" @click="confirmAudit">确认提交</el-button></template>
  126. </el-dialog>
  127. <EventDetailDrawer ref="eventDetailRef" />
  128. </div>
  129. </template>
  130. <script setup name="Gzjc">
  131. import { ref, nextTick, onMounted } from 'vue'
  132. import { ElMessage, ElMessageBox } from 'element-plus'
  133. import { useDict } from '@/utils/dict'
  134. import { getToken } from '@/utils/auth'
  135. import useUserStore from '@/store/modules/user'
  136. import {
  137. getEventPage, deleteEventInfo, startDisposeEvent, trackEvent, submitVerifyEvent, auditEvent,
  138. getEventStatusOptions
  139. } from '@/api/dispatch'
  140. import { Search, Refresh } from '@element-plus/icons-vue'
  141. import EventDetailDrawer from './EventDetailDrawer.vue'
  142. // ==================== 字典 ====================
  143. const { dis_event_category, dis_event_level, dis_event_trend, dis_yes_no } = useDict(
  144. 'dis_event_category', 'dis_event_level', 'dis_event_trend', 'dis_yes_no'
  145. )
  146. const userStore = useUserStore()
  147. const isHandler = row => Number(userStore.id) > 0 && Number(userStore.id) === Number(row.handlerId)
  148. const isVerifier = row => Number(userStore.id) > 0 && Number(userStore.id) === Number(row.verifierId)
  149. function dictLabel(list, value) {
  150. if (value === null || value === undefined || value === '') return '-'
  151. const hit = (list || []).find(item => String(item.value) === String(value))
  152. return hit ? hit.label : value
  153. }
  154. // 本页覆盖「已核实(1) 待开始处置 / 处置中(2) 待跟踪 / 待验收(3) 待核验 / 已解除(4) 终态查看」
  155. const trackStatusOptions = ref([])
  156. async function loadStatusOptions() {
  157. try {
  158. const res = await getEventStatusOptions()
  159. trackStatusOptions.value = (res.data || []).filter(item => [1, 2, 3, 4].includes(item.value))
  160. } catch {
  161. trackStatusOptions.value = []
  162. }
  163. }
  164. // 状态码 -> 标签样式(0 待核实 1 已核实 2 处置中 3 待验收 4 已解除 5 不实事件)
  165. const STATUS_TAG = { 0: 'warning', 1: 'success', 2: 'primary', 3: 'warning', 4: 'warning', 5: 'info' }
  166. function statusTagType(status) {
  167. return STATUS_TAG[status] || 'info'
  168. }
  169. // 等级字典值 -> 标签样式(1 Ⅰ级紧急 2 Ⅱ级较大 3 Ⅲ级一般)
  170. const LEVEL_TAG = { 1: 'danger', 2: 'warning', 3: 'info' }
  171. function levelTagType(level) {
  172. return LEVEL_TAG[level] || 'info'
  173. }
  174. // ==================== 数据状态 ====================
  175. const tableData = ref([])
  176. const total = ref(0)
  177. const loading = ref(false)
  178. const searchForm = ref({ status: '', eventCategory: '', dateRange: null })
  179. const pagination = ref({ page: 1, pageSize: 10 })
  180. // 下拉清空后可能是 '' / null / undefined,统一判定,避免拼出空查询条件
  181. const hasValue = (val) => val !== '' && val !== null && val !== undefined
  182. // ==================== 加载数据 ====================
  183. async function loadData() {
  184. loading.value = true
  185. try {
  186. const params = { statusList: [1, 2, 3, 4], orderAsc: true }
  187. if (hasValue(searchForm.value.status)) params.statusList = [searchForm.value.status]
  188. if (searchForm.value.eventCategory) params.eventCategory = searchForm.value.eventCategory
  189. if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
  190. params.startDate = searchForm.value.dateRange[0]
  191. params.endDate = searchForm.value.dateRange[1]
  192. }
  193. const res = await getEventPage(pagination.value.page, pagination.value.pageSize, params)
  194. tableData.value = res.data?.rows || res.data?.records || res.data || []
  195. total.value = res.data?.total || 0
  196. } catch (error) {
  197. ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
  198. } finally {
  199. loading.value = false
  200. }
  201. }
  202. onMounted(() => {
  203. loadStatusOptions()
  204. loadData()
  205. })
  206. // ==================== 搜索筛选 ====================
  207. function handleSearch() {
  208. pagination.value.page = 1
  209. loadData()
  210. }
  211. function handleReset() {
  212. searchForm.value = { status: '', eventCategory: '', dateRange: null }
  213. pagination.value.page = 1
  214. loadData()
  215. }
  216. // ==================== 分页 ====================
  217. function handleSizeChange(val) {
  218. pagination.value.pageSize = val
  219. pagination.value.page = 1
  220. loadData()
  221. }
  222. function handleCurrentChange(val) {
  223. pagination.value.page = val
  224. loadData()
  225. }
  226. // ==================== 开始处置 / 跟踪 ====================
  227. const operationVisible = ref(false)
  228. const operationFormRef = ref(null)
  229. const operationUploadRef = ref(null)
  230. const currentRow = ref(null)
  231. const operationType = ref('start')
  232. const operationSubmitting = ref(false)
  233. const operationForm = ref({ operDesc: '', fileList: [], attachUrls: [] })
  234. const operationRules = {
  235. operDesc: [{ required: true, message: '请输入备注', trigger: 'blur' }]
  236. }
  237. const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload?dir=dispatch'
  238. const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
  239. const ALLOWED_ATTACH_EXT = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp4', 'mov', 'm4v', 'webm']
  240. function handleAttachBefore(file) {
  241. const ext = String(file.name).split('.').pop().toLowerCase()
  242. if (!ALLOWED_ATTACH_EXT.includes(ext) || file.size / 1024 / 1024 >= 50) {
  243. ElMessage.error('仅支持图片/视频格式且单个不超过 50MB')
  244. return false
  245. }
  246. return true
  247. }
  248. function handleAttachError() { ElMessage.error('文件上传失败') }
  249. function openOperation(row, type) {
  250. currentRow.value = { ...row }
  251. operationType.value = type
  252. operationForm.value = { operDesc: '', fileList: [], attachUrls: [] }
  253. operationVisible.value = true
  254. nextTick(() => operationFormRef.value?.clearValidate())
  255. }
  256. function handleOperationAttachSuccess(res) {
  257. if (res.code === 200 && res.url) {
  258. operationForm.value.attachUrls.push(res.relativePath || res.url)
  259. } else {
  260. ElMessage.error(res.msg || '上传失败')
  261. }
  262. }
  263. async function confirmOperation() {
  264. try {
  265. await operationFormRef.value.validate()
  266. } catch {
  267. return
  268. }
  269. operationSubmitting.value = true
  270. try {
  271. const payload = {
  272. eventId: currentRow.value.id,
  273. operDesc: operationForm.value.operDesc,
  274. attachUrl: operationForm.value.attachUrls.join(',')
  275. }
  276. if (operationType.value === 'start') {
  277. await startDisposeEvent(payload)
  278. ElMessage.success('已开始处置')
  279. } else if (operationType.value === 'submit') {
  280. await submitVerifyEvent(payload)
  281. ElMessage.success('已提交核验')
  282. } else {
  283. await trackEvent(payload)
  284. ElMessage.success('跟踪记录保存成功')
  285. }
  286. operationVisible.value = false
  287. operationUploadRef.value?.clearFiles()
  288. loadData()
  289. } catch (error) {
  290. ElMessage.error('操作失败:' + (error.message || '未知错误'))
  291. } finally {
  292. operationSubmitting.value = false
  293. }
  294. }
  295. const currentReleaseRow = ref(null)
  296. // ==================== 最终核验 ====================
  297. const auditVisible = ref(false)
  298. const auditFormRef = ref(null)
  299. const auditForm = ref({ eventNo: '', resolved: true, opinion: '' })
  300. const auditRules = { opinion: [{ required: true, message: '请输入核验意见', trigger: 'blur' }] }
  301. function handleAudit(row) {
  302. currentReleaseRow.value = row
  303. auditForm.value = { eventNo: row.eventNo, resolved: true, opinion: '' }
  304. auditVisible.value = true
  305. }
  306. async function confirmAudit() {
  307. try { await auditFormRef.value.validate() } catch { return }
  308. try {
  309. await auditEvent(currentReleaseRow.value.id, { resolved: auditForm.value.resolved, auditOpinion: auditForm.value.opinion })
  310. ElMessage.success('核验完成')
  311. auditVisible.value = false
  312. loadData()
  313. } catch (error) { ElMessage.error('操作失败:' + (error.message || '未知错误')) }
  314. }
  315. // ==================== 删除 ====================
  316. async function handleDelete(row) {
  317. try {
  318. await ElMessageBox.confirm('确定要删除该事件吗?', '删除确认', { type: 'warning' })
  319. await deleteEventInfo(row.id)
  320. ElMessage.success('删除成功')
  321. loadData()
  322. } catch (error) {
  323. if (error !== 'cancel' && error !== 'close') {
  324. ElMessage.error('删除失败:' + (error.message || '未知错误'))
  325. }
  326. }
  327. }
  328. // ==================== 详情 ====================
  329. const eventDetailRef = ref(null)
  330. function handleDetail(row) {
  331. eventDetailRef.value?.open(row)
  332. }
  333. </script>
  334. <style scoped>
  335. .gzjc-page {
  336. padding: 16px;
  337. background: #f5f5f5;
  338. min-height: calc(100vh - 84px);
  339. }
  340. .filter-panel {
  341. background: #fff;
  342. border: 1px solid #dcdfe6;
  343. border-radius: 4px;
  344. padding: 12px 16px;
  345. margin-bottom: 12px;
  346. display: flex;
  347. justify-content: space-between;
  348. align-items: center;
  349. flex-wrap: wrap;
  350. gap: 12px;
  351. }
  352. .filter-form {
  353. display: flex;
  354. align-items: center;
  355. gap: 0 12px;
  356. flex-wrap: wrap;
  357. }
  358. /* 若依风格查询栏:内联表单项去默认外边距,间距统一交给 gap */
  359. .filter-form :deep(.el-form-item) {
  360. margin-right: 0;
  361. margin-bottom: 0;
  362. }
  363. .table-panel {
  364. background: #fff;
  365. border: 1px solid #dcdfe6;
  366. border-radius: 4px;
  367. padding: 16px;
  368. }
  369. .pagination-wrap {
  370. margin-top: 16px;
  371. display: flex;
  372. justify-content: flex-end;
  373. }
  374. </style>