| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- <template>
- <div class="order-container alarm-review-page">
- <el-card class="search-card" shadow="never">
- <el-form :inline="true" :model="filters" class="search-form" @submit.prevent="search">
- <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码 / 名称" @keyup.enter="search" /></el-form-item>
- <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in alarmLevels" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
- <el-form-item label="核实结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.key === 'UNREVIEWED' ? '待核实' : item.label" :value="item.value" /></el-select></el-form-item>
- <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
- <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
- </el-form>
- </el-card>
- <section class="table-card">
- <el-tabs :model-value="activeTab" @tab-change="changeTab">
- <el-tab-pane name="all"><template #label>全部报警 <span class="tab-count">{{ statistics.total }}</span></template></el-tab-pane>
- <el-tab-pane name="pending"><template #label>待核实 <span class="tab-count">{{ statistics.pending }}</span></template></el-tab-pane>
- </el-tabs>
- <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" class="load-error"><template #default><el-button link type="primary" @click="load">重新加载</el-button></template></el-alert>
- <el-table v-loading="loading" :data="rows" row-key="id" border stripe style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :default-sort="{ prop: 'createTime', order: 'descending' }" @sort-change="sortChanged">
- <el-table-column prop="alarmNo" label="报警单号" width="210" show-overflow-tooltip><template #default="{ row }">{{ row.alarmNo || '—' }}</template></el-table-column>
- <el-table-column prop="deviceCode" label="设备编码" width="200" show-overflow-tooltip />
- <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
- <el-table-column prop="equipmentLocation" label="安装位置" min-width="210" show-overflow-tooltip><template #default="{ row }">{{ row.equipmentLocation || '—' }}</template></el-table-column>
- <el-table-column prop="alarmContent" label="报警描述" min-width="275"><template #default="{ row }">{{ row.alarmContent || '—' }}</template></el-table-column>
- <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)" effect="light">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
- <el-table-column prop="createTime" label="报警时间" width="175" sortable="custom" />
- <el-table-column label="核实结果" width="110" align="center"><template #default="{ row }"><el-tag :type="auditTone(row.auditStatus)" effect="light">{{ alarmAuditText(row) }}</el-tag></template></el-table-column>
- <el-table-column label="操作" width="160" align="center" fixed="right"><template #default="{ row }"><el-button v-if="isUnreviewed(row)" v-hasPermi="['alarm:review:audit']" link type="primary" @click="openReview(row)">核实</el-button><el-button link type="primary" @click="openHistory(row)">处置记录</el-button></template></el-table-column>
- <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配的未处理报警'" /></template>
- </el-table>
- <el-pagination class="pagination" v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" />
- </section>
- <el-dialog v-model="reviewVisible" title="报警核实" width="680px" append-to-body destroy-on-close :close-on-click-modal="false" :before-close="closeReview">
- <div v-loading="detailLoading">
- <AlarmSummary v-if="current" :alarm="current" :level-tone="levelTone(current.alarmLevel)" />
- <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="submitting || detailLoading">
- <el-form-item label="核实结果" prop="auditStatus"><el-radio-group v-model="form.auditStatus" class="review-choices"><el-radio v-for="item in reviewChoices" :key="item.key" :label="item.value" border>{{ item.label }}</el-radio></el-radio-group></el-form-item>
- <el-form-item label="核实意见" prop="opinion"><el-input v-model="form.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="选填,可填写核实情况、判定依据或误报原因" /></el-form-item>
- <el-form-item label="图片"><div class="attachments"><el-upload v-model:file-list="files" :http-request="uploadFile" :before-upload="beforeUpload" :limit="5" :on-exceed="exceedFiles" :disabled="submitting || detailLoading" accept=".jpg,.jpeg,.png"><el-button :icon="Upload">选择图片</el-button><template #tip><div class="el-upload__tip">支持 png / jpg / jpeg,单个文件不超过 10MB</div></template></el-upload></div></el-form-item>
- </el-form>
- <el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon />
- </div>
- <template #footer><el-button :disabled="submitting || uploading" @click="closeReview()">取消</el-button><el-button type="primary" :loading="submitting" :disabled="detailLoading || uploading || !current" @click="submit">提交核实</el-button></template>
- </el-dialog>
- <el-drawer v-model="historyVisible" title="处置记录" size="640px" append-to-body destroy-on-close>
- <div v-loading="historyLoading">
- <AlarmSummary v-if="historyAlarm" :alarm="historyAlarm" :level-tone="levelTone(historyAlarm.alarmLevel)" />
- <el-alert v-if="historyError" :title="historyError" type="error" :closable="false" />
- <AlarmHistory :records="logs" :loading="historyLoading" :error="historyError" :base-url="baseUrl" />
- </div>
- <template #footer><el-button @click="historyVisible = false">关闭</el-button></template>
- </el-drawer>
- </div>
- </template>
- <script setup>
- import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
- import { useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import { Refresh, Search, Upload } from '@element-plus/icons-vue'
- import { getAlarmReviewOptions, getAlarmReviewPage, getAlarmReviewStatistics, getAlarmReviewDetail, getAlarmLogs, submitAlarmReview, uploadAlarmReviewAttachment } from '@/api/alarmReview'
- import { alarmAuditText } from '@/utils/alarmLabels'
- import AlarmSummary from '../AlarmSummary.vue'
- import AlarmHistory from '../AlarmHistory.vue'
- import { useDict } from '@/utils/dict'
- const { alarm_level: alarmLevels } = useDict('alarm_level')
- const route = useRoute()
- const baseUrl = import.meta.env.VITE_APP_BASE_API
- const options = reactive({ auditStatuses: [], alarmLevels: [] })
- const filters = reactive({ keyword: '', alarmLevel: undefined, auditStatus: undefined })
- const dateRange = ref([]), rows = ref([]), logs = ref([]), total = ref(0), pageNum = ref(1), pageSize = ref(10)
- const statistics = reactive({ total: 0, pending: 0 })
- const loading = ref(false), error = ref(''), ascending = ref(false)
- const reviewVisible = ref(false), detailLoading = ref(false), current = ref(null), formRef = ref(), files = ref([])
- const form = reactive({ auditStatus: undefined, opinion: '' }), formError = ref(''), submitting = ref(false)
- const historyVisible = ref(false), historyLoading = ref(false), historyAlarm = ref(null), historyError = ref('')
- let listRequest = 0, detailRequest = 0, historyRequest = 0
- const scope = computed(() => ({ rootTypeId: Number(route.query.rootTypeId), warningCode: route.query.warning_code || route.query.warningCode || undefined }))
- const scopeKey = computed(() => JSON.stringify(scope.value))
- const unreviewed = computed(() => options.auditStatuses.find(item => item.key === 'UNREVIEWED')?.value)
- const reviewChoices = computed(() => options.auditStatuses.filter(item => item.key !== 'UNREVIEWED'))
- const activeTab = computed(() => filters.auditStatus !== undefined && filters.auditStatus === unreviewed.value ? 'pending' : 'all')
- const uploading = computed(() => files.value.some(file => ['ready', 'uploading'].includes(file.status)))
- const rules = {
- auditStatus: [{ required: true, message: '请选择核实结果', trigger: 'change' }],
- opinion: [{ max: 500, message: '核实意见最多500字', trigger: 'blur' }]
- }
- const message = e => e?.response?.data?.msg || e?.message || '请求失败,请重试'
- const isUnreviewed = row => unreviewed.value !== undefined && row.auditStatus === unreviewed.value
- const levelTone = value => ({ 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' })[Number(value)] || 'info'
- const auditTone = value => ({ UNREVIEWED: 'warning', CONFIRMED: 'primary', FALSE_ALARM: 'info' })[options.auditStatuses.find(item => item.value === value)?.key] || 'info'
- function params() {
- return { ...scope.value, keyword: filters.keyword.trim() || undefined, alarmLevel: filters.alarmLevel === '' ? undefined : filters.alarmLevel,
- auditStatus: filters.auditStatus === '' ? undefined : filters.auditStatus, pageNum: pageNum.value, pageSize: pageSize.value, ascending: ascending.value,
- startTime: dateRange.value?.[0] ? `${dateRange.value[0]} 00:00:00` : undefined, endTime: dateRange.value?.[1] ? `${dateRange.value[1]} 23:59:59.999999` : undefined }
- }
- async function load() {
- const request = ++listRequest
- loading.value = true; error.value = ''
- try {
- if (!Number.isInteger(scope.value.rootTypeId) || scope.value.rootTypeId <= 0) throw new Error('菜单缺少有效的 rootTypeId,请检查菜单 query 配置')
- if (!options.auditStatuses.length) Object.assign(options, (await getAlarmReviewOptions()).data)
- if (request !== listRequest) return
- const [page, count] = await Promise.all([getAlarmReviewPage(params()), getAlarmReviewStatistics(params())])
- if (request !== listRequest) return
- rows.value = page.data.records || []; total.value = Number(page.data.total || 0)
- Object.assign(statistics, { total: Number(count.data.total || 0), pending: Number(count.data.pending || 0) })
- } catch (e) {
- if (request === listRequest) { error.value = message(e); rows.value = []; total.value = 0; Object.assign(statistics, { total: 0, pending: 0 }) }
- } finally { if (request === listRequest) loading.value = false }
- }
- function search() { pageNum.value = 1; return load() }
- function reset() { Object.assign(filters, { keyword: '', alarmLevel: undefined, auditStatus: undefined }); dateRange.value = []; return search() }
- function changeTab(tab) { filters.auditStatus = tab === 'pending' ? unreviewed.value : undefined; search() }
- function sortChanged({ order }) { ascending.value = order === 'ascending'; search() }
- async function openReview(row) {
- const request = ++detailRequest
- reviewVisible.value = true; detailLoading.value = true; current.value = null; files.value = []; formError.value = ''
- Object.assign(form, { auditStatus: undefined, opinion: '' })
- try {
- const response = await getAlarmReviewDetail(row.id, { ...scope.value })
- if (request !== detailRequest) return
- if (!isUnreviewed(response.data)) { reviewVisible.value = false; ElMessage.warning('该报警已核实,请刷新'); return load() }
- current.value = response.data
- } catch (e) { if (request === detailRequest) formError.value = message(e) }
- finally { if (request === detailRequest) detailLoading.value = false }
- }
- function beforeUpload(file) {
- if (!/\.(jpe?g|png)$/i.test(file.name) || file.size > 10 * 1024 * 1024 || !file.size) { ElMessage.error('请选择不超过10MB的JPG、JPEG或PNG图片'); return false }
- return true
- }
- function exceedFiles() { ElMessage.warning('最多上传5张图片') }
- function uploadFile({ file }) { return uploadAlarmReviewAttachment(current.value.id, { ...scope.value }, file).then(response => response.data) }
- async function submit() {
- if (submitting.value || uploading.value || !current.value) return
- if (!await formRef.value.validate().catch(() => false)) return
- if (files.value.some(file => !file.response?.token)) { formError.value = '存在未上传成功的附件,请移除后重新上传'; return }
- submitting.value = true; formError.value = ''
- try {
- await submitAlarmReview(current.value.id, { ...scope.value }, { auditStatus: form.auditStatus, opinion: form.opinion.trim(), attachmentTokens: files.value.map(file => file.response.token) })
- reviewVisible.value = false; files.value = []; Object.assign(form, { auditStatus: undefined, opinion: '' })
- ElMessage.success('核实已提交'); await load()
- } catch (e) { formError.value = message(e) }
- finally { submitting.value = false }
- }
- async function canLeave() {
- if (submitting.value || uploading.value) { ElMessage.warning('请等待上传或提交完成'); return false }
- return true
- }
- async function closeReview(done) { if (await canLeave()) { ++detailRequest; reviewVisible.value = false; if (typeof done === 'function') done() } }
- async function openHistory(row) {
- const request = ++historyRequest
- historyVisible.value = true; historyLoading.value = true; historyAlarm.value = null; logs.value = []; historyError.value = ''
- try {
- const [detail, history] = await Promise.all([getAlarmReviewDetail(row.id, { ...scope.value }), getAlarmLogs(row.id, { ...scope.value })])
- if (request === historyRequest) { historyAlarm.value = detail.data; logs.value = history.data || [] }
- } catch (e) { if (request === historyRequest) historyError.value = message(e) }
- finally { if (request === historyRequest) historyLoading.value = false }
- }
- watch(scopeKey, async () => {
- ++detailRequest; ++historyRequest; reviewVisible.value = false; historyVisible.value = false
- await reset()
- if (route.query.alarmId && !error.value) openReview({ id: route.query.alarmId })
- }, { immediate: true })
- onBeforeRouteLeave(canLeave)
- onBeforeRouteUpdate(canLeave)
- onBeforeUnmount(() => { ++listRequest; ++detailRequest; ++historyRequest })
- </script>
- <style scoped lang="scss">
- .order-container{padding:20px;background:#f5f7fa;min-height:calc(100vh - 84px)}.search-card{margin-bottom:20px;border-radius:8px}.search-form{display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;margin:0}.search-form :deep(.el-form-item){margin:0}.search-form :deep(.el-input){width:220px}.search-form :deep(.el-select){width:160px}.search-form :deep(.el-date-editor){width:300px}.search-card :deep(.el-card__body){padding:16px 20px}.table-card{padding:15px 20px 20px;background:#fff;border-radius:8px}.pagination{margin-top:20px;justify-content:flex-end}.tab-count{margin-left:8px;background:#f0f2f5;padding:1px 7px;border-radius:4px;font-size:12px}.load-error{margin-bottom:16px}.review-choices{display:flex;flex-wrap:wrap;gap:12px}.review-choices .el-radio{margin-right:0;min-width:160px}.attachments{width:100%;padding:14px 16px;background:#fafbfc;border:1px dashed #dcdfe6;border-radius:6px}:deep(.el-tabs__header){margin-bottom:18px}:deep(.el-table .cell){line-height:1.7}@media(max-width:900px){.order-container{padding:12px}.pagination{overflow:auto;justify-content:flex-start}}
- </style>
|