|
|
@@ -7,39 +7,45 @@
|
|
|
<!-- ========= 统计卡片区域 ========== -->
|
|
|
<el-row :gutter="16" class="stat-row">
|
|
|
<el-col :span="4">
|
|
|
- <div class="stat-item">
|
|
|
- <div class="stat-title">总预警</div>
|
|
|
- <div class="stat-value" :class="stats.total > 0 ? 'primary' : ''">{{ stats.total }}</div>
|
|
|
+ <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">
|
|
|
- <div class="stat-title">待确认</div>
|
|
|
- <div class="stat-value warning" :class="stats.unconfirmed > 0 ? 'highlight' : ''">{{ stats.unconfirmed }}</div>
|
|
|
+ <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">
|
|
|
- <div class="stat-title">处理中</div>
|
|
|
- <div class="stat-value info" :class="stats.processing > 0 ? 'highlight' : ''">{{ stats.processing }}</div>
|
|
|
+ <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">
|
|
|
- <div class="stat-title">已处置</div>
|
|
|
- <div class="stat-value success" :class="stats.handled > 0 ? 'highlight' : ''">{{ stats.handled }}</div>
|
|
|
+ <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">
|
|
|
- <div class="stat-title">已解除</div>
|
|
|
- <div class="stat-value done" :class="stats.closed > 0 ? 'highlight' : ''">{{ stats.closed }}</div>
|
|
|
+ <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">
|
|
|
- <div class="stat-title">今日新增</div>
|
|
|
- <div class="stat-value today" :class="stats.today > 0 ? 'highlight' : ''">{{ stats.today }}</div>
|
|
|
+ <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>
|
|
|
@@ -48,7 +54,7 @@
|
|
|
<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-col :span="6"><el-card><template #header><span>级别占比</span></template><div id="typeChart" style="height:240px"/></el-card></el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<!-- ========= 查询表单 ========== -->
|
|
|
@@ -57,6 +63,12 @@
|
|
|
<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="RELEASED"/>
|
|
|
@@ -98,10 +110,11 @@
|
|
|
<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="160">
|
|
|
+ <el-table-column label="关联设备" width="220">
|
|
|
<template #default="{ row }">
|
|
|
- <div v-if="row.deviceCode" class="device-cell">
|
|
|
- <el-tag :type="row.warningLevel === '4' ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
|
|
|
+ <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="row.warningLevel === '4' ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
|
|
|
</div>
|
|
|
<span v-else class="no-device">未关联</span>
|
|
|
</template>
|
|
|
@@ -124,11 +137,13 @@
|
|
|
<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 />
|
|
|
+ <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="isUnconfirmed(row.status)" type="primary" size="small" @click="openConfirm(row)">确认异常</el-button>
|
|
|
<el-button v-if="isUnconfirmed(row.status)" type="info" size="small" @click="confirmMisreport(row)">误报</el-button>
|
|
|
+ <el-button v-if="row.status === 'PROCESSING'" type="warning" size="small" @click="openProcess(row)">提交处理</el-button>
|
|
|
+ <el-button v-if="row.status === 'HANDLED'" type="success" size="small" @click="clearHandled(row)">清除预警</el-button>
|
|
|
<el-button type="info" size="small" @click="openDetail(row.warningId)">详情</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -143,16 +158,15 @@
|
|
|
<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-select v-model="deviceSelected" filterable remote clearable placeholder="请输入设备编码搜索" :remote-method="searchDevice" :loading="deviceLoading" style="width:100%">
|
|
|
- <el-option v-for="dev in deviceOptions" :key="dev.equipmentId" :label="`${dev.equipmentName}(${dev.equipmentCode})`" :value="dev.equipmentId"/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
<el-form-item label="备注信息">
|
|
|
- <el-input v-model="confirmRemark" type="textarea" :rows="3" placeholder="填写异常情况描述(将写入工单描述)" maxlength="300" show-word-limit/>
|
|
|
+ <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>
|
|
|
@@ -161,6 +175,29 @@
|
|
|
</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>
|
|
|
+
|
|
|
<!-- ========== 详情页抽屉 ========== -->
|
|
|
<el-drawer v-model="detailVisible" title="📋 预警处理详情" size="560px">
|
|
|
<template v-if="detail">
|
|
|
@@ -170,14 +207,15 @@
|
|
|
<el-descriptions-item label="预警类型">{{ detail.basicInfo.warning_type || '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="预警级别">{{ getLevelText(detail.basicInfo.warning_level) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="发布人">{{ detail.basicInfo.publisher || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="发布时间">{{ detail.basicInfo.publish_time || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="发布时间">{{ formatWarningTime(detail.basicInfo.publish_time) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="状态">{{ getStatusText(detail.basicInfo.status) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="预警内容">{{ detail.basicInfo.warning_content }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
|
|
|
<div class="detail-section-title">🔗 关联设备</div>
|
|
|
<el-descriptions :column="1" border size="small">
|
|
|
- <el-descriptions-item label="设备编码">{{ detail.basicInfo.remark?.startsWith('AUTO|') ? detail.basicInfo.remark.split('|')[1] : '(手工录入,无设备关联)' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备名称">{{ detail.basicInfo.device_name || detail.basicInfo.deviceName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备编码">{{ detail.basicInfo.device_code || detail.basicInfo.deviceCode || extractDeviceCodeStr(detail.basicInfo) || '(手工录入,无设备关联)' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="设备位置">{{ detail.basicInfo.location || '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="权属单位">{{ detail.basicInfo.ownership_unit || '-' }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
@@ -193,12 +231,38 @@
|
|
|
<div v-else class="detail-empty">暂无处置记录</div>
|
|
|
|
|
|
<div class="detail-section-title">📎 附件</div>
|
|
|
- <el-upload v-if="false" action="#" list-type="text"/>
|
|
|
<div v-if="detail.attachmentList?.length" class="attachment-list">
|
|
|
- <a v-for="(att,idx) in detail.attachmentList" :key="idx" class="attachment-link" :href="att.attachmentUrl" target="_blank">{{ att.attachmentName }}</a>
|
|
|
+ <div v-for="stage in ['REPORT','PROCESS']" :key="stage" class="attachment-group">
|
|
|
+ <div class="attachment-stage">{{ stage === 'PROCESS' ? '处理反馈附件' : '预警报告附件' }}</div>
|
|
|
+ <div v-if="attachmentsByStage(stage).length" class="attachment-grid">
|
|
|
+ <div v-for="(att,idx) in attachmentsByStage(stage)" :key="att.attachmentId || idx" class="attachment-item">
|
|
|
+ <el-image v-if="isImageAttachment(att) && attachmentPreviewUrl(att)" class="attachment-image" :src="attachmentPreviewUrl(att)" :preview-src-list="[attachmentPreviewUrl(att)]" fit="cover" preview-teleported>
|
|
|
+ <template #error><div class="attachment-image-error">图片加载失败</div></template>
|
|
|
+ </el-image>
|
|
|
+ <div v-else-if="isImageAttachment(att)" class="attachment-image attachment-image-loading">加载中...</div>
|
|
|
+ <div v-else class="attachment-file-icon"><el-icon><Document /></el-icon></div>
|
|
|
+ <div class="attachment-name" :title="displayAttachmentName(att)">{{ displayAttachmentName(att) }}</div>
|
|
|
+ <div class="attachment-actions">
|
|
|
+ <el-button v-if="canPreviewAttachment(att)" link type="primary" size="small" @click="previewAttachment(att)"><el-icon><View /></el-icon>预览</el-button>
|
|
|
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)"><el-icon><Download /></el-icon>下载</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="detail-empty">暂无附件</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-else class="detail-empty">暂无附件</div>
|
|
|
|
|
|
+ <div class="detail-section-title">预警督办</div>
|
|
|
+ <div v-if="detail.supervisionList?.length" class="supervision-list">
|
|
|
+ <div v-for="(item,idx) in detail.supervisionList" :key="idx" class="supervision-item">
|
|
|
+ <b>{{ item.supervisionUser || '-' }}</b>
|
|
|
+ <span>{{ formatWarningTime(item.createTime) }}</span>
|
|
|
+ <p>{{ item.supervisionContent || '-' }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="detail-empty">暂无督办记录</div>
|
|
|
+
|
|
|
<div class="detail-section-title">📦 电子归档</div>
|
|
|
<el-descriptions v-if="detail.electronicArchive" :column="2" border size="small">
|
|
|
<el-descriptions-item label="归档编号">{{ detail.electronicArchive.archiveNo }}</el-descriptions-item>
|
|
|
@@ -211,12 +275,12 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
+import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
|
import * as echarts from 'echarts'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import { Search, Refresh } from '@element-plus/icons-vue'
|
|
|
-import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, publishWarningData, resolveWarningData } from '@/api/pipeNetwork/earlyWarning'
|
|
|
-import { addManholeWorkOrder, getEquipmentFullPage } from '@/api/pipeNetwork/basic'
|
|
|
+import { Search, Refresh, Plus, Document, Download, View, Warning, Bell, Tools, CircleCheck, CircleClose, TrendCharts } from '@element-plus/icons-vue'
|
|
|
+import { getWarningDashboardStatisticsData, getWarningSearchData, getWarningFullDetailData, confirmWarningData, misreportWarningData, submitWarningProcessData, clearWarningData, getWarningAttachmentPreviewData, getWarningAttachmentDownloadData } from '@/api/pipeNetwork/earlyWarning'
|
|
|
+import { addManholeWorkOrder } from '@/api/pipeNetwork/basic'
|
|
|
|
|
|
// ========== 统计数据 ==========
|
|
|
const stats = ref({ total:0, unconfirmed:0, processing:0, handled:0, closed:0, today:0 })
|
|
|
@@ -225,7 +289,7 @@ const stats = ref({ total:0, unconfirmed:0, processing:0, handled:0, closed:0, t
|
|
|
const chartData = ref({ trend:[], levels:[], types:[] })
|
|
|
|
|
|
// ========== 查询参数 & 列表 ==========
|
|
|
-const queryParams = ref({ warningName:'', status:'', warningLevel:'', category:'', startDate:'', endDate:'' })
|
|
|
+const queryParams = ref({ warningName:'', deviceCode:'', deviceName:'', status:'', warningLevel:'', category:'', startDate:'', endDate:'' })
|
|
|
const tableData = ref([])
|
|
|
const loading = ref(false)
|
|
|
const pageNum = ref(1)
|
|
|
@@ -238,6 +302,19 @@ async function loadStats() {
|
|
|
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),
|
|
|
@@ -255,6 +332,8 @@ async function loadList() {
|
|
|
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 = '系统自动'
|
|
|
@@ -270,17 +349,15 @@ async function loadList() {
|
|
|
finally { loading.value = false }
|
|
|
}
|
|
|
|
|
|
-// 图表数据填充(模拟)
|
|
|
+// 后端统计接口异常或没有配置数据时,仍显示稳定的默认图表状态
|
|
|
const fillChartData = () => {
|
|
|
- if (chartData.value.trend?.length) return
|
|
|
- const allStats = stats.value
|
|
|
+ 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(Math.max(0, Math.floor(allStats.today * (0.5 + Math.random()*0.5)))) }
|
|
|
- chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
|
|
|
- chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count: Math.max(1, Math.floor(Math.random()*allStats.total/5)+1)}))
|
|
|
- chartData.value.types = [{typeCode:'drainage',typeName:'排水'}, {typeCode:'gas',typeName:'燃气管网'}, {typeCode:'fire',typeName:'消防水压'}]
|
|
|
- .map(t=>({...t, count: Math.max(1, Math.floor(Math.random()*allStats.total/3)+2)}))
|
|
|
+ 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}]
|
|
|
}
|
|
|
|
|
|
// 图表渲染时的空数据状态
|
|
|
@@ -306,10 +383,30 @@ function normalizeWarning(row) {
|
|
|
})
|
|
|
n.category = n.publisher === '系统自动' ? 'normal' : 'special'
|
|
|
// 从 remark 字段提取设备编码 (AUTO|deviceCode|warningCode)
|
|
|
- n.deviceCode = extractDeviceCodeStr(row)
|
|
|
+ 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|')) {
|
|
|
@@ -337,20 +434,22 @@ const levelChartOpt = {
|
|
|
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:30,
|
|
|
+ type:'bar', barWidth:34,
|
|
|
+ label:{ show:true, position:'top', color:'#606266', fontSize:12 },
|
|
|
data:[],
|
|
|
itemStyle:{
|
|
|
color:function(params){
|
|
|
- const colors={1:'#e3f2fd',2:'#fff8e1',3:'#fff3e0',4:'#ffebee'};
|
|
|
+ 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:'10%', orient:'vertical', right:0 },
|
|
|
+ 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 },
|
|
|
@@ -364,8 +463,8 @@ const resizeCharts = () => { trendChart?.resize(); levelChart?.resize(); typeCha
|
|
|
// ========== 图表渲染函数 ==========
|
|
|
function renderTrendChart() {
|
|
|
const dom = document.getElementById('trendChart')
|
|
|
- if (!dom || !trendChart) return
|
|
|
- trendChart.dispose()
|
|
|
+ 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) }]
|
|
|
@@ -374,8 +473,8 @@ function renderTrendChart() {
|
|
|
|
|
|
function renderLevelChart() {
|
|
|
const dom = document.getElementById('levelChart')
|
|
|
- if (!dom || !levelChart) return
|
|
|
- levelChart.dispose()
|
|
|
+ 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) }]
|
|
|
@@ -384,103 +483,107 @@ function renderLevelChart() {
|
|
|
|
|
|
function renderTypeChart() {
|
|
|
const dom = document.getElementById('typeChart')
|
|
|
- if (!dom || !typeChart) return
|
|
|
- typeChart.dispose()
|
|
|
+ if (!dom) return
|
|
|
+ if (typeChart) typeChart.dispose()
|
|
|
typeChart = echarts.init(dom)
|
|
|
- const data = chartData.value.types.length ? chartData.value.types : emptyChartData.types
|
|
|
- typeChart.setOption({ ...typeChartOpt, series:[{ ...typeChartOpt.series[0], data: data.map(t=>({ value:t.count || 0, name:t.typeName || t.typeCode })) }]
|
|
|
+ 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 }]
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-onMounted(async () => {
|
|
|
- await loadStats()
|
|
|
- await loadList()
|
|
|
- await nextTick()
|
|
|
- // 填充图表数据
|
|
|
- const allStats = stats.value
|
|
|
- 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(Math.max(0, allStats.today - Math.floor(Math.random()*allStats.total*0.3))) }
|
|
|
- chartData.value.trend = dates.map((date,idx)=>({date,count:counts[idx]}))
|
|
|
-
|
|
|
- // 级别图
|
|
|
- chartData.value.levels = [1,2,3,4].map(lv=>({levelCode:String(lv), count: Math.floor(Math.random()*allStats.total/5)+1}))
|
|
|
- // 类型图(无 API 时随机)
|
|
|
- chartData.value.types = [{typeCode:'drainage',typeName:'排水'}, {typeCode:'gas',typeName:'燃气管网'}, {typeCode:'fire',typeName:'消防水压'}]
|
|
|
- .map(t=>({...t, count: Math.floor(Math.random()*allStats.total/3)+2}))
|
|
|
-
|
|
|
- initCharts()
|
|
|
- window.addEventListener('resize', resizeCharts)
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- window.removeEventListener('resize', resizeCharts)
|
|
|
- trendChart?.dispose()
|
|
|
- levelChart?.dispose()
|
|
|
- typeChart?.dispose()
|
|
|
-})
|
|
|
-
|
|
|
// ========== 辅助函数 ==========
|
|
|
const isUnconfirmed = (status) => ['RELEASED','PENDING'].includes(status)
|
|
|
const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待办', PROCESSING:'处理中', RELEASED:'待确认', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
|
|
|
const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', PROCESSING:'warning', RELEASED:'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]||'')
|
|
|
-const disposalTypeText = (t) => ({ RELEASE:'发布预警', UPGRADE:'升级预警', RESOLVE:'解除预警', RETURN:'退回重办', SUPERVISION:'督办', INSTRUCTION:'批示' }[t]||t)
|
|
|
+const disposalTypeText = (t) => ({ RELEASE:'发布预警', CONFIRM:'确认异常', MISREPORT:'误报清除', HANDLE:'提交处理反馈', CLEAR:'清除预警', UPGRADE:'升级预警', RESOLVE:'解除预警', RETURN:'退回重办', SUPERVISION:'督办', INSTRUCTION:'批示' }[t]||t)
|
|
|
const disposalTimelineType = (t) => ({ RESOLVE:'success', UPGRADE:'warning', RETURN:'danger' }[t]||'primary')
|
|
|
+const attachmentsByStage = (stage) => (detail.value?.attachmentList || []).filter(item => (item.attachmentStage || item.attachment_stage || 'REPORT') === stage)
|
|
|
+const isImageAttachment = (attachment) => String(attachment.attachmentType || '').startsWith('image/') || /\.(png|jpe?g|gif|webp)$/i.test(attachment.attachmentName || '')
|
|
|
+const attachmentPreviewUrls = ref({})
|
|
|
+const attachmentPreviewUrl = (attachment) => attachmentPreviewUrls.value[attachment.attachmentId] || ''
|
|
|
+const displayAttachmentName = (attachment) => String(attachment.attachmentName || '预警附件').replace(/^\[(预警报告|处理反馈)\]\s*/, '')
|
|
|
+const canPreviewAttachment = (attachment) => isImageAttachment(attachment) || /\.pdf$/i.test(displayAttachmentName(attachment)) || String(attachment.attachmentType || '').toLowerCase() === 'application/pdf'
|
|
|
+
|
|
|
+function revokeAttachmentPreviews() {
|
|
|
+ Object.values(attachmentPreviewUrls.value).forEach(url => url && URL.revokeObjectURL(url))
|
|
|
+ attachmentPreviewUrls.value = {}
|
|
|
+}
|
|
|
+
|
|
|
+async function loadAttachmentPreviews(attachments) {
|
|
|
+ revokeAttachmentPreviews()
|
|
|
+ const imageAttachments = (attachments || []).filter(isImageAttachment)
|
|
|
+ await Promise.all(imageAttachments.map(async (attachment) => {
|
|
|
+ try {
|
|
|
+ const blob = await getWarningAttachmentPreviewData(attachment.attachmentId)
|
|
|
+ attachmentPreviewUrls.value[attachment.attachmentId] = URL.createObjectURL(blob)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('预警图片加载失败', attachment.attachmentId, e)
|
|
|
+ }
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+async function previewAttachment(attachment) {
|
|
|
+ try {
|
|
|
+ const blob = isImageAttachment(attachment) && attachmentPreviewUrl(attachment)
|
|
|
+ ? null
|
|
|
+ : await getWarningAttachmentPreviewData(attachment.attachmentId)
|
|
|
+ const url = attachmentPreviewUrl(attachment) || URL.createObjectURL(blob)
|
|
|
+ window.open(url, '_blank', 'noopener,noreferrer')
|
|
|
+ if (blob) setTimeout(() => URL.revokeObjectURL(url), 60000)
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('附件预览失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function downloadAttachment(attachment) {
|
|
|
+ try {
|
|
|
+ const blob = await getWarningAttachmentDownloadData(attachment.attachmentId)
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = url
|
|
|
+ link.download = displayAttachmentName(attachment)
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('附件下载失败')
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
// ========== 操作处理 ==========
|
|
|
function handleSearch() { pageNum.value=1; loadList() }
|
|
|
-function handleReset() { queryParams.value={warningName:'',status:'',warningLevel:'',category:'',startDate:'',endDate:''}; 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 deviceSelected = ref(null)
|
|
|
-const deviceOptions = ref([])
|
|
|
-const deviceLoading = ref(false)
|
|
|
const confirmingRemark = ref('')
|
|
|
+const assignedUser = ref('')
|
|
|
const submitting = ref(false)
|
|
|
|
|
|
function openConfirm(row) {
|
|
|
current.value = row
|
|
|
- deviceSelected.value = null
|
|
|
- deviceOptions.value = []
|
|
|
confirmingRemark.value = ''
|
|
|
- extractDeviceCode(row)
|
|
|
+ assignedUser.value = row.assignedUser || row.handler || ''
|
|
|
confirmVisible.value = true
|
|
|
}
|
|
|
|
|
|
-function extractDeviceCode(row) {
|
|
|
- // remark 格式:"AUTO|equipmentCode|warningCode"
|
|
|
- if (row.remark && row.remark.startsWith('AUTO|')) {
|
|
|
- const parts = row.remark.split('|')
|
|
|
- if (parts.length >= 2) searchDevice(parts[1])
|
|
|
- } else {
|
|
|
- const m = (row.warningContent || '').match(/设备 \[([^\]]+)\]/)
|
|
|
- if (m) searchDevice(m[1])
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function searchDevice(query) {
|
|
|
- if (!query) return
|
|
|
- deviceLoading.value = true
|
|
|
- try {
|
|
|
- const res = await getEquipmentFullPage(1, 20, { equipmentCode: query })
|
|
|
- if (res.data) deviceOptions.value = (res.data.rows || []) || []
|
|
|
- } catch(e) { console.warn('设备搜索失败', e) }
|
|
|
- finally { deviceLoading.value = false }
|
|
|
-}
|
|
|
-
|
|
|
async function submitConfirm() {
|
|
|
- if (!deviceSelected.value) { ElMessage.warning('请选择关联设备'); return }
|
|
|
if (!confirmingRemark.value.trim()) { ElMessage.warning('请填写异常描述'); return }
|
|
|
|
|
|
submitting.value = true
|
|
|
try {
|
|
|
- await publishWarningData(current.value.warningId)
|
|
|
+ await confirmWarningData({ warningId: current.value.warningId, assignedUser: assignedUser.value, confirmRemark: confirmingRemark.value })
|
|
|
const orderDesc = `预警「${current.value.warningName}」异常确认;${confirmingRemark.value}`
|
|
|
- const res = await addManholeWorkOrder({ alarmId: current.value.warningId, deviceId: deviceSelected.value, orderType: 1, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
|
|
|
+ await addManholeWorkOrder({ alarmId: current.value.warningId, orderType: 1, orderLevel: getWorkOrderLevel(current.value.warningLevel), orderDesc: orderDesc.slice(0,500) })
|
|
|
ElMessage.success(`✅ 预警已确认 | 工单已生成 (WO-...) | 请前往工单系统继续处理`)
|
|
|
confirmVisible.value = false
|
|
|
loadList()
|
|
|
@@ -500,12 +603,86 @@ function confirmMisreport(row) {
|
|
|
confirmButtonText:'确认误报',
|
|
|
cancelButtonText:'取消'
|
|
|
}).then(async ({value}) => {
|
|
|
- await resolveWarningData({ warningId: row.warningId, disposalContent: '误报:'+value })
|
|
|
+ await misreportWarningData({ warningId: row.warningId, reason: value || '系统确认误报' })
|
|
|
ElMessage.success('已标记为误报并解除')
|
|
|
loadList()
|
|
|
}).catch(()=>{})
|
|
|
}
|
|
|
|
|
|
+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
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function clearHandled(row) {
|
|
|
+ ElMessageBox.prompt('请输入清除说明', '清除预警', {
|
|
|
+ inputPlaceholder: '例如:维修完成,现场复核正常',
|
|
|
+ confirmButtonText: '确认清除',
|
|
|
+ cancelButtonText: '取消'
|
|
|
+ }).then(async ({ value }) => {
|
|
|
+ if (!value || !value.trim()) {
|
|
|
+ ElMessage.warning('请填写清除说明')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await clearWarningData({ warningId: row.warningId, clearRemark: value })
|
|
|
+ ElMessage.success('预警已清除')
|
|
|
+ loadList()
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
// ========== 详情抽屉 ==========
|
|
|
const detailVisible = ref(false)
|
|
|
const detail = ref(null)
|
|
|
@@ -513,13 +690,18 @@ const detail = ref(null)
|
|
|
async function openDetail(warningId) {
|
|
|
detailVisible.value = true
|
|
|
detail.value = null
|
|
|
+ revokeAttachmentPreviews()
|
|
|
try {
|
|
|
const res = await getWarningFullDetailData(warningId)
|
|
|
- if (res.data) detail.value = res.data
|
|
|
+ if (res.data) {
|
|
|
+ detail.value = res.data
|
|
|
+ await loadAttachmentPreviews(res.data.attachmentList)
|
|
|
+ }
|
|
|
} catch(e) { ElMessage.error('详情加载中'); console.error(e) }
|
|
|
}
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
+ revokeAttachmentPreviews()
|
|
|
window.removeEventListener('resize', resizeCharts)
|
|
|
trendChart?.dispose()
|
|
|
levelChart?.dispose()
|
|
|
@@ -539,19 +721,23 @@ onMounted(async () => {
|
|
|
.warning-container { padding: 20px; min-height: calc(100vh - 84px); }
|
|
|
.stat-row { margin-bottom: 16px; }
|
|
|
.stat-item {
|
|
|
- background: #fff; border-radius: 12px; padding: 12px 10px; text-align: center;
|
|
|
- box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid #eef2f6;
|
|
|
- transition: transform 0.2s, box-shadow 0.2s;
|
|
|
-}
|
|
|
-.stat-item:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.1); }
|
|
|
-.stat-title { font-size: 13px; color: #7a8a9a; margin-bottom: 4px; }
|
|
|
-.stat-value { font-size: 28px; font-weight: 700; color: #3a4a5a; transition: all 0.3s; }
|
|
|
-.stat-value.primary { color: #409eff; }
|
|
|
-.stat-value.warning { color: #e6a23c; }
|
|
|
-.stat-value.info { color: #909399; }
|
|
|
-.stat-value.success { color: #67c23a; }
|
|
|
-.stat-value.done { color: #27ae60; }
|
|
|
-.stat-value.today { color: #9b59b6; }
|
|
|
+ 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)} }
|
|
|
|
|
|
@@ -567,17 +753,35 @@ onMounted(async () => {
|
|
|
.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; }
|
|
|
|
|
|
.detail-section-title { font-weight:600;font-size:14px;color:#2c3e50;margin:16px 0 10px;padding-bottom:8px;border-bottom:1px solid #eef2f6 }
|
|
|
.detail-sub { font-size:12px;color:#9098a6;margin-top:4px }
|
|
|
.detail-empty { font-size:13px;color:#9098a6;padding:12px 0 }
|
|
|
.attachment-list { display:flex;flex-direction:column;gap:6px }
|
|
|
-.attachment-link { font-size:13px;color:#3498db;text-decoration:none }
|
|
|
-.attachment-link:hover { text-decoration:underline }
|
|
|
+.attachment-group { margin-bottom:12px }
|
|
|
+.attachment-stage { color:#606266;font-size:13px;font-weight:600;margin-bottom:6px }
|
|
|
+.attachment-grid { display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px }
|
|
|
+.attachment-item { min-width:0;padding:8px;border:1px solid #ebeef5;border-radius:6px;background:#fff }
|
|
|
+.attachment-image,.attachment-image-loading,.attachment-file-icon { width:100%;height:86px;border-radius:4px;overflow:hidden }
|
|
|
+.attachment-image-loading,.attachment-image-error,.attachment-file-icon { display:flex;align-items:center;justify-content:center;color:#a8abb2;background:#f5f7fa;font-size:12px }
|
|
|
+.attachment-file-icon { color:#409eff;font-size:32px }
|
|
|
+.attachment-name { margin-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#606266;font-size:12px }
|
|
|
+.attachment-actions { display:flex;justify-content:center;gap:4px;margin-top:3px }
|
|
|
+.attachment-actions .el-button { margin-left:0;padding:2px 3px }
|
|
|
+.supervision-list { display:flex;flex-direction:column;gap:8px }
|
|
|
+.supervision-item { padding:8px 10px;background:#f7f8fa;border-radius:4px;font-size:12px }
|
|
|
+.supervision-item span { color:#909399;margin-left:8px }
|
|
|
+.supervision-item p { margin:6px 0 0;color:#606266 }
|
|
|
+.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>
|