|
|
@@ -1,642 +0,0 @@
|
|
|
-<!--预警管理页面(RuoYi Element Plus 标准)-->
|
|
|
-<!--数据源:early_warning 体系 /warning/*接口 -->
|
|
|
-<!--接收后只建工单不派单,由工单系统处理后续流程-->
|
|
|
-
|
|
|
-<template>
|
|
|
- <div class="warning-container">
|
|
|
- <!-- ========= 统计卡片区域 ========== -->
|
|
|
- <el-row :gutter="16" class="stat-row">
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-primary">
|
|
|
- <div class="stat-icon"><el-icon><Warning /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">总预警</div><div class="stat-value">{{ stats.total }}</div></div>
|
|
|
- <div class="stat-caption">累计预警总量</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-warning">
|
|
|
- <div class="stat-icon"><el-icon><Bell /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">待确认</div><div class="stat-value">{{ stats.unconfirmed }}</div></div>
|
|
|
- <div class="stat-caption">待核实异常</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-processing">
|
|
|
- <div class="stat-icon"><el-icon><Tools /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">处理中</div><div class="stat-value">{{ stats.processing }}</div></div>
|
|
|
- <div class="stat-caption">正在处置</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-handled">
|
|
|
- <div class="stat-icon"><el-icon><CircleCheck /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">已处置</div><div class="stat-value">{{ stats.handled }}</div></div>
|
|
|
- <div class="stat-caption">等待复核清除</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-closed">
|
|
|
- <div class="stat-icon"><el-icon><CircleClose /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">已解除</div><div class="stat-value">{{ stats.closed }}</div></div>
|
|
|
- <div class="stat-caption">已完成闭环</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
- <div class="stat-item stat-today">
|
|
|
- <div class="stat-icon"><el-icon><TrendCharts /></el-icon></div>
|
|
|
- <div class="stat-content"><div class="stat-title">今日新增</div><div class="stat-value">{{ stats.today }}</div></div>
|
|
|
- <div class="stat-caption">今日新增预警</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
-
|
|
|
- <!-- ========== 图表区域 ========== -->
|
|
|
- <el-row :gutter="16" class="chart-row">
|
|
|
- <el-col :span="12"><el-card><template #header><span>近 7 天趋势</span></template><div id="trendChart" style="height:240px"/></el-card></el-col>
|
|
|
- <el-col :span="6"><el-card><template #header><span>级别分布</span></template><div id="levelChart" style="height:240px"/></el-card></el-col>
|
|
|
- <el-col :span="6"><el-card><template #header><span>级别占比</span></template><div id="typeChart" style="height:240px"/></el-card></el-col>
|
|
|
- </el-row>
|
|
|
-
|
|
|
- <!-- ========= 查询表单 ========== -->
|
|
|
- <el-card class="search-card">
|
|
|
- <el-form :inline="true" :model="queryParams">
|
|
|
- <el-form-item label="预警名称">
|
|
|
- <el-input v-model="queryParams.warningName" placeholder="请输入预警名称" clearable style="width:200px"/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="设备编码">
|
|
|
- <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编码" clearable style="width:180px"/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="设备名称">
|
|
|
- <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable style="width:180px"/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="状态">
|
|
|
- <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width:150px">
|
|
|
- <el-option label="草稿" value="DRAFT"/>
|
|
|
- <el-option label="待接收" value="PENDING"/>
|
|
|
- <el-option label="已接收" value="RECEIVED"/>
|
|
|
- <el-option label="处理中" value="PROCESSING"/>
|
|
|
- <el-option label="已处置" value="HANDLED"/>
|
|
|
- <el-option label="已解除" value="CLOSED"/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="级别">
|
|
|
- <el-select v-model="queryParams.warningLevel" placeholder="全部级别" clearable style="width:120px">
|
|
|
- <el-option label="1 级 (蓝)" value="1"/>
|
|
|
- <el-option label="2 级 (黄)" value="2"/>
|
|
|
- <el-option label="3 级 (橙)" value="3"/>
|
|
|
- <el-option label="4 级 (红)" value="4"/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="类别">
|
|
|
- <el-select v-model="queryParams.category" placeholder="全部类别" clearable style="width:120px">
|
|
|
- <el-option label="普通预警" value="normal"/>
|
|
|
- <el-option label="特殊预警" value="special"/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="日期范围">
|
|
|
- <el-date-picker v-model="queryParams.startDate" type="date" placeholder="开始日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
|
|
|
- <span style="margin:0 8px;color:#909399;">至</span>
|
|
|
- <el-date-picker v-model="queryParams.endDate" type="date" placeholder="结束日期" clearable value-format="YYYY-MM-DD" style="width:160px"/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" @click="handleSearch"><el-icon><Search/></el-icon> 查询</el-button>
|
|
|
- <el-button @click="handleReset"><el-icon><Refresh/></el-icon> 重置</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- ========== 预警列表 ========== -->
|
|
|
- <el-card class="table-card">
|
|
|
- <el-table :data="tableData" border stripe v-loading="loading" max-height="600">
|
|
|
- <el-table-column prop="warningNo" label="预警编号" width="140" />
|
|
|
- <el-table-column prop="warningName" label="预警名称" min-width="160" show-overflow-tooltip />
|
|
|
- <el-table-column label="关联设备" width="220">
|
|
|
- <template #default="{ row }">
|
|
|
- <div v-if="row.deviceCode || row.deviceName" class="device-cell">
|
|
|
- <span v-if="row.deviceName" class="device-name">{{ row.deviceName }}</span>
|
|
|
- <el-tag v-if="row.deviceCode" :type="Number(row.warningLevel) === 4 ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
|
|
|
- </div>
|
|
|
- <span v-else class="no-device">未关联</span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="location" label="设备位置" min-width="140" show-overflow-tooltip />
|
|
|
- <el-table-column label="类别" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="row.category === 'normal' ? '' : 'warning'" size="small">
|
|
|
- {{ row.category === 'normal' ? '普通预警' : '特殊预警' }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="级别" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="getLevelType(row.warningLevel)" size="small">{{ getLevelText(row.warningLevel) }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="状态" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="getStatusType(row.status)" size="small">{{ getStatusText(row.status) }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="publishTime" label="发布时间" width="170" sortable class-name="publish-time-column" :formatter="formatPublishTime" />
|
|
|
- <el-table-column label="操作" width="180" fixed="right">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button v-if="isPending(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openConfirm(row)">接收</el-button>
|
|
|
- <el-button v-if="isReceived(row.status)" v-hasPermi="['warning:warning:handle']" type="primary" size="small" @click="openStart(row)">开始处置</el-button>
|
|
|
- <el-button v-if="row.status === 'PROCESSING'" v-hasPermi="['warning:warning:handle']" type="warning" size="small" @click="openProcess(row)">提交处理</el-button>
|
|
|
- <WarningReviewAction :row="row" @changed="loadList" />
|
|
|
- <el-button type="info" size="small" @click="openDetail(row.warningId)">详情</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total" layout="total,prev,pager,next,sizes" :page-sizes="[10,20,30]"
|
|
|
- @change="loadList" style="margin-top:16px;justify-content:flex-end" />
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- ========== 接收预警弹窗 ========== -->
|
|
|
- <el-dialog v-model="confirmVisible" title="接收预警" width="600px" append-to-body destroy-on-close>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="预警编号">{{ current.warningNo }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="预警名称">{{ current.warningName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="预警内容">{{ current.warningContent || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="关联设备">{{ formatWarningDevice(current) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备位置">{{ current.location || '-' }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- <el-form label-width="100px" style="margin-top:16px">
|
|
|
- <el-form-item label="备注信息">
|
|
|
- <el-input v-model="confirmingRemark" type="textarea" :rows="3" placeholder="填写异常情况描述(将写入工单描述)" maxlength="300" show-word-limit/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="处理人员">
|
|
|
- <el-input v-model="assignedUser" placeholder="可填写处理人账号,不填则使用预警原处理人或当前用户" clearable />
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="confirmVisible=false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="submitting" @click="submitConfirm">确认提交</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <el-dialog v-model="startVisible" title="开始处置" width="560px" append-to-body destroy-on-close>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="预警编号">{{ startRow.warningNo }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="预警名称">{{ startRow.warningName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="关联设备">{{ formatWarningDevice(startRow) }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- <el-form label-width="100px" style="margin-top:16px">
|
|
|
- <el-form-item label="处置说明">
|
|
|
- <el-input v-model="startRemark" type="textarea" :rows="3" placeholder="填写开始处置的说明(可选)" maxlength="300" show-word-limit/>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="startVisible=false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="submitting" @click="submitStart">确认开始</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <el-dialog v-model="processVisible" title="提交预警处理反馈" width="620px" append-to-body destroy-on-close>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="预警编号">{{ processRow.warningNo }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="预警名称">{{ processRow.warningName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="关联设备">{{ formatWarningDevice(processRow) }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- <el-form label-width="100px" style="margin-top:16px">
|
|
|
- <el-form-item label="处理反馈" required>
|
|
|
- <el-input v-model="processContent" type="textarea" :rows="5" maxlength="1000" show-word-limit placeholder="请填写处理过程和处理结果" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="处理附件">
|
|
|
- <el-upload v-model:file-list="processFileList" action="#" list-type="text" :auto-upload="false" :limit="9" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.xls,.xlsx,.doc,.docx" :on-change="handleProcessFileChange" :on-remove="handleProcessFileRemove" :on-exceed="handleProcessFileExceed">
|
|
|
- <el-icon><Plus /></el-icon>
|
|
|
- </el-upload>
|
|
|
- <div class="upload-tip">支持图片、PDF、Excel、Word,最多 9 个文件,单个不超过 20MB</div>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="processVisible=false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="submitting" @click="submitProcess">提交反馈</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <WarningDetailDrawer v-model="detailVisible" :warning-id="selectedDetailWarningId" />
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
|
-import * as echarts from 'echarts'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import { Search, Refresh, Plus, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
|
|
|
-import { getWarningDashboardStatisticsData, getWarningSearchData, confirmWarningData, startWarningProcessData, submitWarningProcessData } from '@/api/pipeNetwork/earlyWarning'
|
|
|
-import WarningDetailDrawer from '@/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue'
|
|
|
-import WarningReviewAction from '@/views/subSystem/lifeCompany/components/WarningReviewAction.vue'
|
|
|
-import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
|
|
|
-
|
|
|
-// ========== 统计数据 ==========
|
|
|
-const stats = ref({ total:0, unconfirmed:0, processing:0, handled:0, closed:0, today:0 })
|
|
|
-
|
|
|
-// ========== 图表数据 ==========
|
|
|
-const chartData = ref({ trend:[], levels:[], types:[] })
|
|
|
-
|
|
|
-// ========== 查询参数 & 列表 ==========
|
|
|
-const queryParams = ref({ warningName:'', deviceCode:'', deviceName:'', status:'', warningLevel:'', category:'', startDate:'', endDate:'' })
|
|
|
-const tableData = ref([])
|
|
|
-const loading = ref(false)
|
|
|
-const pageNum = ref(1)
|
|
|
-const pageSize = ref(10)
|
|
|
-const total = ref(0)
|
|
|
-
|
|
|
-async function loadStats() {
|
|
|
- try {
|
|
|
- const res = await getWarningDashboardStatisticsData()
|
|
|
- if (!res.data) return
|
|
|
- const b = res.data.basicStats
|
|
|
- const t = res.data.trendStats || []
|
|
|
- chartData.value.trend = t.map(item => ({
|
|
|
- date: String(item.date || '').slice(5) || '-',
|
|
|
- count: Number(item.count || 0)
|
|
|
- }))
|
|
|
- chartData.value.levels = (res.data.levelStats || []).map(item => ({
|
|
|
- levelCode: String(item.levelCode || ''),
|
|
|
- count: Number(item.count || 0)
|
|
|
- }))
|
|
|
- chartData.value.types = (res.data.typeStats || []).map(item => ({
|
|
|
- typeCode: item.typeCode,
|
|
|
- typeName: item.typeName || item.typeCode,
|
|
|
- count: Number(item.count || 0)
|
|
|
- }))
|
|
|
- // releasedCount + pendingCount = 待确认
|
|
|
- stats.value = {
|
|
|
- total: Number(b.total || 0),
|
|
|
- unconfirmed: Number(b.releasedCount || 0) + Number(b.pendingCount || 0),
|
|
|
- processing: Number(b.processingCount || 0),
|
|
|
- handled: Number(b.handledCount || 0),
|
|
|
- closed: Number(b.closedCount || 0),
|
|
|
- today: t.length ? Number(t[t.length-1].count || 0) : 0
|
|
|
- }
|
|
|
- } catch(e) { console.warn('加载统计数据失败', e) }
|
|
|
-}
|
|
|
-
|
|
|
-async function loadList() {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const params = { pageNum: pageNum.value, pageSize: pageSize.value }
|
|
|
- if (queryParams.value.warningName) params.warningName = queryParams.value.warningName
|
|
|
- if (queryParams.value.deviceCode) params.deviceCode = queryParams.value.deviceCode
|
|
|
- if (queryParams.value.deviceName) params.deviceName = queryParams.value.deviceName
|
|
|
- if (queryParams.value.status) params.status = queryParams.value.status
|
|
|
- if (queryParams.value.warningLevel) params.warningLevel = queryParams.value.warningLevel
|
|
|
- if (queryParams.value.category === 'normal') params.publisher = '系统自动'
|
|
|
- if (queryParams.value.category === 'special') params.excludePublisher = '系统自动'
|
|
|
- if (queryParams.value.startDate) params.startTime = queryParams.value.startDate + ' 00:00:00'
|
|
|
- if (queryParams.value.endDate) params.endTime = queryParams.value.endDate + ' 23:59:59'
|
|
|
-
|
|
|
- const res = await getWarningSearchData(params)
|
|
|
- if (!res.data) return
|
|
|
- tableData.value = (res.data.records || []).map(row => normalizeWarning(row))
|
|
|
- total.value = Number(res.data.total || 0)
|
|
|
- } catch(e) { ElMessage.error('加载预警列表失败'); console.error(e) }
|
|
|
- finally { loading.value = false }
|
|
|
-}
|
|
|
-
|
|
|
-// 后端统计接口异常或没有配置数据时,仍显示稳定的默认图表状态
|
|
|
-const fillChartData = () => {
|
|
|
- if (chartData.value.trend?.length && chartData.value.levels?.length && chartData.value.types?.length) return
|
|
|
- const dates = [], counts = []
|
|
|
- for(let i=6;i>=0;i--) { const d=new Date(); d.setDate(d.getDate()-i); dates.push((d.getMonth()+1)+'-'+d.getDate()) }
|
|
|
- for(let i=6;i>=0;i--) { counts.push(0) }
|
|
|
- if (!chartData.value.trend?.length) chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
|
|
|
- if (!chartData.value.levels?.length) chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count:0}))
|
|
|
- if (!chartData.value.types?.length) chartData.value.types = [{typeCode:'empty',typeName:'暂无数据',count:1}]
|
|
|
-}
|
|
|
-
|
|
|
-// 图表渲染时的空数据状态
|
|
|
-const emptyChartData = { trend:[], levels:[], types:[] }
|
|
|
-
|
|
|
-// ========== 图表管理 ==========
|
|
|
-let trendChart, levelChart, typeChart
|
|
|
-
|
|
|
-const initCharts = () => {
|
|
|
- // 如果数据为空,填充模拟数据
|
|
|
- if (chartData.value.trend?.length === 0) fillChartData()
|
|
|
- try { renderTrendChart() } catch(e){ console.warn('趋势图初始化失败:', e) }
|
|
|
- try { renderLevelChart() } catch(e){ console.warn('级别图初始化失败:', e) }
|
|
|
- try { renderTypeChart() } catch(e){ console.warn('类型图初始化失败:', e) }
|
|
|
-}
|
|
|
-
|
|
|
-// 下划线转驼峰 + 判断类别
|
|
|
-function normalizeWarning(row) {
|
|
|
- const n = {}
|
|
|
- Object.keys(row).forEach(key => {
|
|
|
- const camel = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
|
|
- n[camel] = row[key]
|
|
|
- })
|
|
|
- n.category = n.publisher === '系统自动' ? 'normal' : 'special'
|
|
|
- // 从 remark 字段提取设备编码 (AUTO|deviceCode|warningCode)
|
|
|
- n.deviceCode = n.deviceCode || extractDeviceCodeStr(row)
|
|
|
- n.deviceName = n.deviceName || row.device_name || row.deviceName || ''
|
|
|
- return n
|
|
|
-}
|
|
|
-
|
|
|
-function formatPublishTime(row, column, value) {
|
|
|
- return formatWarningTime(value)
|
|
|
-}
|
|
|
-
|
|
|
-function formatWarningTime(value) {
|
|
|
- if (!value) return '-'
|
|
|
- const text = String(value).trim()
|
|
|
- const match = text.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})/)
|
|
|
- if (match) return `${match[1]} ${match[2]}`
|
|
|
- return text.replace('T', ' ').replace(/\.\d+(Z|[+-]\d{2}:?\d{2})$/, '')
|
|
|
-}
|
|
|
-
|
|
|
-function formatWarningDevice(warning) {
|
|
|
- const name = warning?.deviceName || ''
|
|
|
- const code = warning?.deviceCode || ''
|
|
|
- if (name && code) return `${name}(${code})`
|
|
|
- return name || code || '未关联'
|
|
|
-}
|
|
|
-
|
|
|
-// 从预警行中提取设备编码
|
|
|
-function extractDeviceCodeStr(row) {
|
|
|
- if (row.remark && typeof row.remark === 'string' && row.remark.startsWith('AUTO|')) {
|
|
|
- const parts = row.remark.split('|')
|
|
|
- if (parts.length >= 2) return parts[1]
|
|
|
- }
|
|
|
- const m = (row.warning_content || row.warningContent || '').match(/设备\s*\[([^\]]+)\]/)
|
|
|
- if (m) return m[1]
|
|
|
- return ''
|
|
|
-}
|
|
|
-
|
|
|
-// ========== ECharts ==========
|
|
|
-const trendChartOpt = {
|
|
|
- tooltip:{ trigger:'axis' },
|
|
|
- legend:{ data:['新增预警'], top:0 },
|
|
|
- grid:{ left:40,right:20,top:44,bottom:32 },
|
|
|
- xAxis:{ type:'category', axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
|
|
|
- yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
|
|
|
- series:[{ type:'line', smooth:true, data:[], lineStyle:{ color:'#e74c3c',width:2 }, itemStyle:{ color:'#e74c3c' } }]
|
|
|
-}
|
|
|
-
|
|
|
-const levelChartOpt = {
|
|
|
- tooltip:{ trigger:'axis' },
|
|
|
- grid:{ left:40,right:20,top:44,bottom:32 },
|
|
|
- xAxis:{ type:'category', data: ['1 级','2 级','3 级','4 级'], axisLabel:{ color:'#9098a6' }, axisLine:{ lineStyle:{ color:'#d0d5dd' } } },
|
|
|
- yAxis:{ type:'value', name:'数量', axisLabel:{ color:'#9098a6' }, splitLine:{ lineStyle:{ color:'#f0f2f6',type:'dashed' } } },
|
|
|
- series:[{
|
|
|
- type:'bar', barWidth:34,
|
|
|
- label:{ show:true, position:'top', color:'#606266', fontSize:12 },
|
|
|
- data:[],
|
|
|
- itemStyle:{
|
|
|
- color:function(params){
|
|
|
- const colors={1:'#5b8ff9',2:'#f6bd16',3:'#f6903d',4:'#e8684a'};
|
|
|
- return colors[params.dataIndex+1]||'#e3f2fd';
|
|
|
- },
|
|
|
- borderRadius:[4,4,0,0]
|
|
|
- }
|
|
|
- }]
|
|
|
-}
|
|
|
-
|
|
|
-const typeChartOpt = {
|
|
|
- tooltip:{ trigger:'item' },
|
|
|
- legend:{ top:'center', orient:'vertical', right:4, itemWidth:12, itemHeight:12, textStyle:{ color:'#606266' } },
|
|
|
- series:[{
|
|
|
- type:'pie', radius:['40%','70%'], avoidLabelOverlap:false,
|
|
|
- itemStyle:{ borderRadius:4, borderColor:'#fff', borderWidth:1 },
|
|
|
- label:{ show:false },
|
|
|
- data:[]
|
|
|
- }]
|
|
|
-}
|
|
|
-
|
|
|
-const resizeCharts = () => { trendChart?.resize(); levelChart?.resize(); typeChart?.resize() }
|
|
|
-
|
|
|
-// ========== 图表渲染函数 ==========
|
|
|
-function renderTrendChart() {
|
|
|
- const dom = document.getElementById('trendChart')
|
|
|
- if (!dom) return
|
|
|
- if (trendChart) trendChart.dispose()
|
|
|
- trendChart = echarts.init(dom)
|
|
|
- const data = chartData.value.trend.length ? chartData.value.trend : emptyChartData.trend
|
|
|
- trendChart.setOption({ ...trendChartOpt, xAxis:{ ...trendChartOpt.xAxis, data: data.map(i=>i.date) }, series:[{ ...trendChartOpt.series[0], data: data.map(i=>i.count || 0) }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function renderLevelChart() {
|
|
|
- const dom = document.getElementById('levelChart')
|
|
|
- if (!dom) return
|
|
|
- if (levelChart) levelChart.dispose()
|
|
|
- levelChart = echarts.init(dom)
|
|
|
- const data = chartData.value.levels.length ? chartData.value.levels : emptyChartData.levels
|
|
|
- levelChart.setOption({ ...levelChartOpt, series:[{ ...levelChartOpt.series[0], data: data.map(l=>l.count || 0) }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function renderTypeChart() {
|
|
|
- const dom = document.getElementById('typeChart')
|
|
|
- if (!dom) return
|
|
|
- if (typeChart) typeChart.dispose()
|
|
|
- typeChart = echarts.init(dom)
|
|
|
- const levels = chartData.value.levels || []
|
|
|
- const levelNames = { '1':'1 级(蓝)', '2':'2 级(黄)', '3':'3 级(橙)', '4':'4 级(红)' }
|
|
|
- const total = levels.reduce((sum, item) => sum + Number(item.count || 0), 0)
|
|
|
- const data = total > 0
|
|
|
- ? levels.map(item => ({ value:Number(item.count || 0), name:levelNames[String(item.levelCode)] || `${item.levelCode} 级` }))
|
|
|
- : [{ value:1, name:'暂无数据', itemStyle:{ color:'#c0c4cc' } }]
|
|
|
- typeChart.setOption({ ...typeChartOpt, series:[{ ...typeChartOpt.series[0], data }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// ========== 辅助函数 ==========
|
|
|
-const isPending = (status) => status === 'PENDING'
|
|
|
-const isReceived = (status) => status === 'RECEIVED'
|
|
|
-const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待接收', RECEIVED:'已接收', PROCESSING:'处置中', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
|
|
|
-const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', RECEIVED:'warning', PROCESSING:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
|
|
|
-const getLevelText = (lv) => ({ '1':'1 级 (蓝)','2':'2 级 (黄)','3':'3 级 (橙)','4':'4 级 (红)' }[lv]||'-' )
|
|
|
-const getLevelType = (lv) => ({ '1':'','2':'warning','3':'warning','4':'danger' }[lv]||'')
|
|
|
-// ========== 操作处理 ==========
|
|
|
-function handleSearch() { pageNum.value=1; loadList() }
|
|
|
-function handleReset() { queryParams.value={warningName:'',deviceCode:'',deviceName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; pageNum.value=1; loadList() }
|
|
|
-
|
|
|
-// ========== 接收预警 ==========
|
|
|
-const confirmVisible = ref(false)
|
|
|
-const current = ref({})
|
|
|
-const confirmingRemark = ref('')
|
|
|
-const assignedUser = ref('')
|
|
|
-const submitting = ref(false)
|
|
|
-
|
|
|
-function openConfirm(row) {
|
|
|
- current.value = row
|
|
|
- confirmingRemark.value = ''
|
|
|
- assignedUser.value = row.assignedUser || row.handler || ''
|
|
|
- confirmVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-async function submitConfirm() {
|
|
|
- if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写接收说明'); return }
|
|
|
-
|
|
|
- submitting.value = true
|
|
|
- try {
|
|
|
- await confirmWarningData({ warningId: current.value.warningId, assignedUser: assignedUser.value, confirmRemark: confirmingRemark.value })
|
|
|
- const orderDesc = `预警「${current.value.warningName}」已接收;${confirmingRemark.value}`
|
|
|
- await addManholeWorkOrder({ alarmId: current.value.warningId, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
|
|
|
- ElMessage.success('预警已接收')
|
|
|
- confirmVisible.value = false
|
|
|
- loadList()
|
|
|
- } catch(e) { ElMessage.error('接收失败:'+(e.msg||e.message||'重试')); console.error(e) }
|
|
|
- finally { submitting.value=false }
|
|
|
-}
|
|
|
-
|
|
|
-function getWorkOrderLevel(warningLevel) { const lv = Number(warningLevel); return lv>=3?1:(lv===2?2:3) }
|
|
|
-
|
|
|
-// ========== 开始处置 ==========
|
|
|
-const startVisible = ref(false)
|
|
|
-const startRow = ref({})
|
|
|
-const startRemark = ref('')
|
|
|
-
|
|
|
-function openStart(row) {
|
|
|
- startRow.value = row
|
|
|
- startRemark.value = ''
|
|
|
- startVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-async function submitStart() {
|
|
|
- submitting.value = true
|
|
|
- try {
|
|
|
- await startWarningProcessData({ warningId: startRow.value.warningId, remark: startRemark.value })
|
|
|
- ElMessage.success('已开始处置')
|
|
|
- startVisible.value = false
|
|
|
- loadList()
|
|
|
- } catch(e) { ElMessage.error('开始处置失败:'+(e.msg||e.message||'重试')); console.error(e) }
|
|
|
- finally { submitting.value=false }
|
|
|
-}
|
|
|
-
|
|
|
-const processVisible = ref(false)
|
|
|
-const processRow = ref({})
|
|
|
-const processContent = ref('')
|
|
|
-const processFileList = ref([])
|
|
|
-const processFiles = ref([])
|
|
|
-
|
|
|
-function openProcess(row) {
|
|
|
- processRow.value = row
|
|
|
- processContent.value = ''
|
|
|
- processFiles.value = []
|
|
|
- processFileList.value = []
|
|
|
- processVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-function handleProcessFileChange(uploadFile, uploadFiles) {
|
|
|
- const raw = uploadFile.raw
|
|
|
- const extension = String(raw?.name || '').split('.').pop().toLowerCase()
|
|
|
- const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xls', 'xlsx', 'doc', 'docx']
|
|
|
- if (!raw || !allowedExtensions.includes(extension)) {
|
|
|
- processFileList.value = uploadFiles.filter(item => item.uid !== uploadFile.uid)
|
|
|
- ElMessage.warning('仅支持图片、PDF、Excel、Word文件')
|
|
|
- return
|
|
|
- }
|
|
|
- if (raw.size > 20 * 1024 * 1024) {
|
|
|
- processFileList.value = uploadFiles.filter(item => item.uid !== uploadFile.uid)
|
|
|
- ElMessage.warning('单个附件不能超过20MB')
|
|
|
- return
|
|
|
- }
|
|
|
- processFileList.value = uploadFiles
|
|
|
- processFiles.value = uploadFiles.map(item => item.raw).filter(Boolean)
|
|
|
-}
|
|
|
-
|
|
|
-function handleProcessFileRemove(uploadFile, uploadFiles) {
|
|
|
- handleProcessFileChange(uploadFile, uploadFiles)
|
|
|
-}
|
|
|
-
|
|
|
-function handleProcessFileExceed() {
|
|
|
- ElMessage.warning('最多上传9个附件')
|
|
|
-}
|
|
|
-
|
|
|
-async function submitProcess() {
|
|
|
- if (!processContent.value.trim()) {
|
|
|
- ElMessage.warning('请填写处理反馈')
|
|
|
- return
|
|
|
- }
|
|
|
- submitting.value = true
|
|
|
- try {
|
|
|
- await submitWarningProcessData({ warningId: processRow.value.warningId, processContent: processContent.value, files: processFiles.value })
|
|
|
- ElMessage.success('处理反馈已提交')
|
|
|
- processVisible.value = false
|
|
|
- await loadList()
|
|
|
- } catch (e) {
|
|
|
- ElMessage.error('提交处理反馈失败')
|
|
|
- } finally {
|
|
|
- submitting.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ========== 详情抽屉 ==========
|
|
|
-const detailVisible = ref(false)
|
|
|
-const selectedDetailWarningId = ref('')
|
|
|
-
|
|
|
-function openDetail(warningId) {
|
|
|
- selectedDetailWarningId.value = warningId
|
|
|
- detailVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- window.removeEventListener('resize', resizeCharts)
|
|
|
- trendChart?.dispose()
|
|
|
- levelChart?.dispose()
|
|
|
- typeChart?.dispose()
|
|
|
-})
|
|
|
-
|
|
|
-onMounted(async () => {
|
|
|
- await nextTick()
|
|
|
- await Promise.all([loadStats(), loadList()])
|
|
|
- await fillChartData()
|
|
|
- initCharts()
|
|
|
- window.addEventListener('resize', resizeCharts)
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.warning-container { padding: 20px; min-height: calc(100vh - 84px); }
|
|
|
-.stat-row { margin-bottom: 16px; }
|
|
|
-.stat-item {
|
|
|
- position:relative;min-height:104px;padding:16px 16px 12px 62px;background:#fff;
|
|
|
- border:1px solid #e9edf3;border-radius:10px;box-shadow:0 3px 12px rgba(31,45,61,.07);
|
|
|
- overflow:hidden;transition:transform .2s,box-shadow .2s;
|
|
|
-}
|
|
|
-.stat-item::before { content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--stat-color); }
|
|
|
-.stat-item:hover { transform:translateY(-2px);box-shadow:0 8px 20px rgba(31,45,61,.12); }
|
|
|
-.stat-icon { position:absolute;left:16px;top:22px;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--stat-color);background:var(--stat-bg);font-size:19px; }
|
|
|
-.stat-content { display:flex;align-items:baseline;justify-content:space-between;gap:8px; }
|
|
|
-.stat-title { font-size:13px;color:#667085;white-space:nowrap; }
|
|
|
-.stat-value { font-size:30px;line-height:34px;font-weight:700;color:var(--stat-color);transition:all .3s; }
|
|
|
-.stat-caption { margin-top:8px;color:#98a2b3;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
|
|
|
-.stat-primary { --stat-color:#409eff;--stat-bg:#ecf5ff; }
|
|
|
-.stat-warning { --stat-color:#e6a23c;--stat-bg:#fdf6ec; }
|
|
|
-.stat-processing { --stat-color:#8a96a3;--stat-bg:#f2f4f7; }
|
|
|
-.stat-handled { --stat-color:#67c23a;--stat-bg:#f0f9eb; }
|
|
|
-.stat-closed { --stat-color:#18a567;--stat-bg:#eafaf3; }
|
|
|
-.stat-today { --stat-color:#8e5cc7;--stat-bg:#f5effb; }
|
|
|
-.stat-value.highlight { animation: pulse 1.5s ease-in-out; }
|
|
|
-@keyframes pulse { 0%{transform:scale(1)}50%{transform:scale(1.12)}100%{transform:scale(1)} }
|
|
|
-
|
|
|
-.chart-row { margin-bottom: 16px; }
|
|
|
-.chart-card { min-height: 300px; }
|
|
|
-.chart-empty {
|
|
|
- display: flex; align-items: center; justify-content: center;
|
|
|
- height: 240px; color: #c0c4cc; font-size: 14px;
|
|
|
- flex-direction: column; gap: 8px;
|
|
|
-}
|
|
|
-.chart-empty::before { content:'\1F4CA'; font-size: 32px; opacity: 0.5; }
|
|
|
-
|
|
|
-.search-card { margin-bottom: 16px; }
|
|
|
-.table-card { min-height: 600px; }
|
|
|
-.device-cell { display:flex; align-items:center; gap:4px; }
|
|
|
-.device-name { max-width:96px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#606266; font-size:12px; }
|
|
|
-.no-device { color:#c0c4cc; font-size:12px; }
|
|
|
-:deep(.publish-time-column .cell) { white-space: nowrap; }
|
|
|
-
|
|
|
-.upload-tip { color:#909399;font-size:12px;line-height:20px }
|
|
|
-
|
|
|
-@media (max-width: 1300px) {
|
|
|
- .stat-row { display:flex; gap:10px; flex-wrap:wrap; }
|
|
|
- .stat-row > * { flex-shrink:0; width: calc(33.33% - 14px) !important; }
|
|
|
-}
|
|
|
-@media (max-width: 900px) {
|
|
|
- .stat-row > * { width: calc(50% - 10px) !important; }
|
|
|
-}
|
|
|
-</style>
|