| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- <template>
- <div class="app-container gzjc-page">
- <!-- 搜索区(若依风格) -->
- <div class="filter-panel">
- <el-form :inline="true" :model="searchForm" class="filter-form" label-width="80px">
- <el-form-item label="事件状态">
- <el-select v-model="searchForm.status" placeholder="请选择事件状态" clearable style="width:140px">
- <el-option v-for="d in trackStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="事件类别">
- <el-select v-model="searchForm.eventCategory" placeholder="请选择事件类别" clearable filterable style="width:140px">
- <el-option v-for="d in dis_event_category" :key="d.value" :label="d.label" :value="d.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="接报时间">
- <el-date-picker
- v-model="searchForm.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>
- </el-form-item>
- </el-form>
- </div>
- <!-- 数据表格 -->
- <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="eventCategory" label="事件类别" width="110">
- <template #default="{ row }">{{ dictLabel(dis_event_category, row.eventCategory) }}</template>
- </el-table-column>
- <el-table-column prop="eventLevel" label="事件等级" width="100">
- <template #default="{ row }">
- <el-tag v-if="row.eventLevel" :type="levelTagType(row.eventLevel)" size="small">{{ dictLabel(dis_event_level, row.eventLevel) }}</el-tag>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="location" label="发生地点" min-width="150" show-overflow-tooltip>
- <template #default="{ row }">{{ row.location || '-' }}</template>
- </el-table-column>
- <el-table-column prop="statusName" label="当前状态" width="100">
- <template #default="{ row }">
- <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
- </template>
- </el-table-column>
- <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">
- <template #default="{ row }">{{ row.lastVisitTime || '-' }}</template>
- </el-table-column>
- <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="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>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-wrap">
- <el-pagination
- v-model:current-page="pagination.page"
- v-model:page-size="pagination.pageSize"
- :page-sizes="[10, 20, 50]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
- <!-- 开始处置和跟踪共用备注+附件表单 -->
- <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"
- >
- <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="operationVisible = false">取消</el-button>
- <el-button type="primary" :loading="operationSubmitting" @click="confirmOperation">{{ operationType === 'start' ? '开始处置' : '保存跟踪' }}</el-button>
- </template>
- </el-dialog>
- <!-- 解除弹窗 -->
- <el-dialog v-model="releaseVisible" title="事件解除" width="600px" :close-on-click-modal="false">
- <el-form :model="releaseForm" :rules="releaseRules" ref="releaseFormRef" label-width="100px">
- <el-form-item label="事件编号">{{ releaseForm.eventNo }}</el-form-item>
- <el-form-item label="事件摘要">
- <div class="desc-text">{{ releaseForm.summary || '-' }}</div>
- </el-form-item>
- <el-form-item label="解除原因" prop="releaseReason">
- <el-select v-model="releaseForm.releaseReason" placeholder="请选择" style="width:100%">
- <el-option v-for="d in dis_event_release_reason" :key="d.value" :label="d.label" :value="d.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="解除说明" prop="releaseNote">
- <el-input v-model="releaseForm.releaseNote" type="textarea" :rows="4" placeholder="请输入解除说明" />
- </el-form-item>
- <el-form-item label="损失(万元)">
- <el-input-number v-model="releaseForm.lossAmount" :min="0" :precision="2" style="width:100%" />
- </el-form-item>
- <template v-if="isHandledRelease">
- <el-form-item label="处置前佐证">
- <el-upload
- ref="beforeUploadRef"
- v-model:file-list="releaseForm.beforeFiles"
- :action="uploadUrl"
- :headers="uploadHeaders"
- :limit="5"
- multiple
- accept=".jpg,.jpeg,.png,.gif,.bmp,.webp,.mp4,.mov,.m4v,.webm"
- :on-success="(res) => handleAttachSuccess(res, 'before')"
- :on-error="handleAttachError"
- :on-exceed="() => ElMessage.warning('处置前佐证最多 5 个')"
- :before-upload="handleAttachBefore"
- style="width:100%"
- >
- <el-button type="primary" size="small">上传处置前照片/视频</el-button>
- <template #tip><div class="el-upload__tip">图片或视频,单个不超过 50MB,最多 5 个</div></template>
- </el-upload>
- </el-form-item>
- <el-form-item label="处置后佐证">
- <el-upload
- ref="afterUploadRef"
- v-model:file-list="releaseForm.afterFiles"
- :action="uploadUrl"
- :headers="uploadHeaders"
- :limit="5"
- multiple
- accept=".jpg,.jpeg,.png,.gif,.bmp,.webp,.mp4,.mov,.m4v,.webm"
- :on-success="(res) => handleAttachSuccess(res, 'after')"
- :on-error="handleAttachError"
- :on-exceed="() => ElMessage.warning('处置后佐证最多 5 个')"
- :before-upload="handleAttachBefore"
- style="width:100%"
- >
- <el-button type="primary" size="small">上传处置后照片/视频</el-button>
- <template #tip><div class="el-upload__tip">图片或视频,单个不超过 50MB,最多 5 个</div></template>
- </el-upload>
- </el-form-item>
- </template>
- </el-form>
- <template #footer>
- <el-button @click="releaseVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmRelease">确认</el-button>
- </template>
- </el-dialog>
- <EventDetailDrawer ref="eventDetailRef" />
- </div>
- </template>
- <script setup name="Gzjc">
- import { ref, nextTick, computed, onMounted } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { useDict } from '@/utils/dict'
- import { getToken } from '@/utils/auth'
- import {
- getEventPage, deleteEventInfo, startDisposeEvent, trackEvent,
- releaseEventInfo, getEventStatusOptions
- } from '@/api/dispatch'
- import { Search, Refresh } from '@element-plus/icons-vue'
- import EventDetailDrawer from './EventDetailDrawer.vue'
- // ==================== 字典 ====================
- 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'
- )
- 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
- }
- // 本页覆盖「已核实(1) 待开始处置 / 处置中(2) 待跟踪解除 / 已解除(3) 终态查看」
- const trackStatusOptions = ref([])
- async function loadStatusOptions() {
- try {
- const res = await getEventStatusOptions()
- trackStatusOptions.value = (res.data || []).filter(item => [1, 2, 3].includes(item.value))
- } catch {
- trackStatusOptions.value = []
- }
- }
- // 状态码 -> 标签样式(0 待核实 1 已核实 2 处置中 3 已解除 4 不实事件)
- const STATUS_TAG = { 0: 'warning', 1: 'success', 2: 'primary', 3: 'warning', 4: 'info' }
- function statusTagType(status) {
- return STATUS_TAG[status] || 'info'
- }
- // 等级字典值 -> 标签样式(1 Ⅰ级紧急 2 Ⅱ级较大 3 Ⅲ级一般)
- const LEVEL_TAG = { 1: 'danger', 2: 'warning', 3: 'info' }
- function levelTagType(level) {
- return LEVEL_TAG[level] || 'info'
- }
- // ==================== 数据状态 ====================
- const tableData = ref([])
- const total = ref(0)
- const loading = ref(false)
- const searchForm = ref({ status: '', eventCategory: '', dateRange: null })
- const pagination = ref({ page: 1, pageSize: 10 })
- // 下拉清空后可能是 '' / null / undefined,统一判定,避免拼出空查询条件
- const hasValue = (val) => val !== '' && val !== null && val !== undefined
- // ==================== 加载数据 ====================
- async function loadData() {
- loading.value = true
- try {
- 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) {
- params.startDate = searchForm.value.dateRange[0]
- params.endDate = searchForm.value.dateRange[1]
- }
- const res = await getEventPage(pagination.value.page, pagination.value.pageSize, params)
- tableData.value = res.data?.rows || res.data?.records || res.data || []
- total.value = res.data?.total || 0
- } catch (error) {
- ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
- } finally {
- loading.value = false
- }
- }
- onMounted(() => {
- loadStatusOptions()
- loadData()
- })
- // ==================== 搜索筛选 ====================
- function handleSearch() {
- pagination.value.page = 1
- loadData()
- }
- function handleReset() {
- searchForm.value = { status: '', eventCategory: '', dateRange: null }
- pagination.value.page = 1
- loadData()
- }
- // ==================== 分页 ====================
- function handleSizeChange(val) {
- pagination.value.pageSize = val
- pagination.value.page = 1
- loadData()
- }
- function handleCurrentChange(val) {
- pagination.value.page = val
- loadData()
- }
- // ==================== 开始处置 / 跟踪 ====================
- const operationVisible = ref(false)
- const operationFormRef = ref(null)
- const operationUploadRef = ref(null)
- const currentRow = ref(null)
- const operationType = ref('start')
- const operationSubmitting = ref(false)
- const operationForm = ref({ operDesc: '', fileList: [], attachUrls: [] })
- const operationRules = {
- operDesc: [{ required: true, message: '请输入备注', trigger: 'blur' }]
- }
- function openOperation(row, type) {
- currentRow.value = { ...row }
- operationType.value = type
- operationForm.value = { operDesc: '', fileList: [], attachUrls: [] }
- operationVisible.value = true
- nextTick(() => operationFormRef.value?.clearValidate())
- }
- function handleOperationAttachSuccess(res) {
- if (res.code === 200 && res.url) {
- operationForm.value.attachUrls.push(res.relativePath || res.url)
- } else {
- ElMessage.error(res.msg || '上传失败')
- }
- }
- async function confirmOperation() {
- try {
- await operationFormRef.value.validate()
- } catch {
- return
- }
- operationSubmitting.value = true
- try {
- 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 || '未知错误'))
- } finally {
- operationSubmitting.value = false
- }
- }
- // ==================== 解除 ====================
- const releaseVisible = ref(false)
- const releaseFormRef = ref(null)
- const currentReleaseRow = ref(null)
- const releaseForm = ref({ eventNo: '', summary: '', releaseReason: '', releaseNote: '', lossAmount: undefined, beforeFiles: [], afterFiles: [], beforeUrls: [], afterUrls: [] })
- const releaseRules = {
- releaseReason: [{ required: true, message: '请选择解除原因', trigger: 'change' }],
- releaseNote: [{ required: true, message: '请输入解除说明', trigger: 'blur' }]
- }
- // ==================== 解除佐证附件(处置前 / 处置后) ====================
- // 解除原因为「事件已处置完成」(字典值 '1') 时展示上传控件;图片/视频各最多 5 个、单个 ≤50MB
- // 佐证附件统一存 MinIO(图片/视频直链可预览)
- const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload?dir=dispatch_release'
- const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
- const beforeUploadRef = ref(null)
- const afterUploadRef = ref(null)
- const ALLOWED_ATTACH_EXT = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp4', 'mov', 'm4v', 'webm']
- const isHandledRelease = computed(() => dictLabel(dis_event_release_reason.value, releaseForm.value.releaseReason) === '事件已处置完成')
- function handleAttachBefore(file) {
- const ext = String(file.name).split('.').pop().toLowerCase()
- if (!ALLOWED_ATTACH_EXT.includes(ext)) {
- ElMessage.error('仅支持图片(jpg/png/gif 等)或视频(mp4/mov 等)格式')
- return false
- }
- if (file.size / 1024 / 1024 >= 50) {
- ElMessage.error('文件大小不能超过 50MB')
- return false
- }
- return true
- }
- function handleAttachSuccess(res, slot) {
- if (res.code === 200 && res.url) {
- // 库内只存相对路径(迁移环境不用改数据),展示/下载由后端拼 MinIO 地址
- releaseForm.value[slot + 'Urls'].push(res.relativePath || res.url)
- } else {
- ElMessage.error(res.msg || '上传失败')
- }
- }
- function handleAttachError() {
- ElMessage.error('文件上传失败')
- }
- function handleRelease(row) {
- currentReleaseRow.value = row
- releaseForm.value = {
- eventNo: row.eventNo,
- summary: row.description || '',
- releaseReason: '',
- releaseNote: '',
- lossAmount: row.lossAmount !== null && row.lossAmount !== undefined ? Number(row.lossAmount) : undefined,
- beforeFiles: [],
- afterFiles: [],
- beforeUrls: [],
- afterUrls: []
- }
- releaseVisible.value = true
- // 清掉上次打开残留的上传文件列表
- nextTick(() => {
- beforeUploadRef.value?.clearFiles()
- afterUploadRef.value?.clearFiles()
- })
- }
- async function confirmRelease() {
- try {
- await releaseFormRef.value.validate()
- } catch {
- return
- }
- try {
- await releaseEventInfo(currentReleaseRow.value.id, {
- eventId: currentReleaseRow.value.id,
- releaseReason: releaseForm.value.releaseReason,
- releaseNote: releaseForm.value.releaseNote,
- lossAmount: releaseForm.value.lossAmount,
- // 处置前/后佐证(多文件英文逗号分隔),仅「事件已处置完成」时采集
- beforeUrl: releaseForm.value.beforeUrls.join(','),
- afterUrl: releaseForm.value.afterUrls.join(',')
- })
- ElMessage.success('事件已解除')
- releaseVisible.value = false
- nextTick(() => {
- beforeUploadRef.value?.clearFiles()
- afterUploadRef.value?.clearFiles()
- })
- loadData()
- } catch (error) {
- ElMessage.error('操作失败:' + (error.message || '未知错误'))
- }
- }
- // ==================== 删除 ====================
- async function handleDelete(row) {
- try {
- await ElMessageBox.confirm('确定要删除该事件吗?', '删除确认', { type: 'warning' })
- await deleteEventInfo(row.id)
- ElMessage.success('删除成功')
- loadData()
- } catch (error) {
- if (error !== 'cancel' && error !== 'close') {
- ElMessage.error('删除失败:' + (error.message || '未知错误'))
- }
- }
- }
- // ==================== 详情 ====================
- const eventDetailRef = ref(null)
- function handleDetail(row) {
- eventDetailRef.value?.open(row)
- }
- </script>
- <style scoped>
- .gzjc-page {
- padding: 16px;
- background: #f5f5f5;
- min-height: calc(100vh - 84px);
- }
- .filter-panel {
- background: #fff;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- padding: 12px 16px;
- margin-bottom: 12px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- }
- .filter-form {
- display: flex;
- align-items: center;
- gap: 0 12px;
- flex-wrap: wrap;
- }
- /* 若依风格查询栏:内联表单项去默认外边距,间距统一交给 gap */
- .filter-form :deep(.el-form-item) {
- margin-right: 0;
- margin-bottom: 0;
- }
- .table-panel {
- background: #fff;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- padding: 16px;
- }
- .pagination-wrap {
- margin-top: 16px;
- display: flex;
- justify-content: flex-end;
- }
- </style>
|