| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846 |
- <template>
- <div class="app-container lsyjgl-page">
- <!-- ==================== 分类统计 ==================== -->
- <div class="stats-panel">
- <div class="stats-left">
- <div class="stat-item" v-for="item in statItems" :key="item.label">
- <div class="stat-value" :class="item.cls">{{ item.value }}</div>
- <div class="stat-label">{{ item.label }}</div>
- </div>
- </div>
- <div class="stats-right">
- <div class="ring-chart-box" v-for="chart in ringCharts" :key="chart.id">
- <div :id="chart.id" class="ring-chart"></div>
- <div class="ring-legend">
- <div class="legend-row" v-for="(leg, i) in chart.legends.value" :key="i">
- <span class="legend-dot" :style="{ background: leg.color }"></span>
- <span class="legend-text">{{ leg.name }}</span>
- <span class="legend-num">{{ leg.value }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- ==================== 搜索区域 ==================== -->
- <el-form ref="queryRef" :model="filters" :inline="true" v-show="showSearch" label-width="80px">
- <el-form-item label="预警名称">
- <el-input v-model="filters.equipmentName" placeholder="请输入预警名称" clearable style="width: 200px"
- @keyup.enter="handleSearch" />
- </el-form-item>
- <el-form-item label="预警类型">
- <el-select v-model="filters.warningType" placeholder="请选择预警类型" clearable filterable style="width: 180px">
- <el-option v-for="item in warningTypeOptions" :key="item.dictValue" :label="item.dictLabel"
- :value="item.dictValue" />
- </el-select>
- </el-form-item>
- <el-form-item label="预警级别">
- <el-select v-model="filters.warningLevel" placeholder="请选择预警级别" clearable style="width: 180px">
- <el-option label="严重" :value="1" />
- <el-option label="重要" :value="2" />
- <el-option label="一般" :value="3" />
- </el-select>
- </el-form-item>
- <el-form-item label="处理状态">
- <el-select v-model="filters.status" placeholder="请选择处理状态" clearable style="width: 180px">
- <el-option label="已处置" value="HANDLED" />
- <el-option label="已解除" value="CLOSED" />
- </el-select>
- </el-form-item>
- <el-form-item label="预警时间" style="width: 308px">
- <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="-" start-placeholder="开始日期"
- end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
- <el-button icon="Refresh" @click="handleClear">重置</el-button>
- </el-form-item>
- </el-form>
- <!-- ==================== 操作栏 ==================== -->
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="warning" plain icon="Download" :loading="exporting" @click="handleExport">导出</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button type="primary" plain icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
- </el-col>
- <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData"></right-toolbar>
- </el-row>
- <!-- ==================== 数据表格 ==================== -->
- <div>
- <el-table :data="tableData" style="width:100%" v-loading="loading"
- empty-text="暂无已处置 / 已解除的预警记录">
- <el-table-column label="归档编号" width="150" show-overflow-tooltip>
- <template #default="{ row }">{{ archiveNoOf(row) }}</template>
- </el-table-column>
- <el-table-column prop="warningNo" label="预警编号" width="175" show-overflow-tooltip />
- <el-table-column prop="equipmentName" label="预警名称" min-width="130" show-overflow-tooltip />
- <el-table-column label="预警类型" width="130" align="center" show-overflow-tooltip>
- <template #default="{ row }">{{ warningTypeText(row) }}</template>
- </el-table-column>
- <el-table-column prop="warningLevel" label="预警级别" width="90" align="center">
- <template #default="{ row }">
- <el-tag :type="levelTagType(row.warningLevel)" size="small">{{ levelText(row.warningLevel) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="预警内容" min-width="190" show-overflow-tooltip>
- <template #default="{ row }">{{ row.content || '-' }}</template>
- </el-table-column>
- <el-table-column prop="ownershipUnit" label="权属单位" width="145" show-overflow-tooltip>
- <template #default="{ row }">{{ row.ownershipUnit || '-' }}</template>
- </el-table-column>
- <el-table-column prop="location" label="位置" min-width="140" show-overflow-tooltip>
- <template #default="{ row }">{{ row.location || '-' }}</template>
- </el-table-column>
- <el-table-column label="预警时间" width="168">
- <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
- </el-table-column>
- <el-table-column prop="publisher" label="发布人" width="110" show-overflow-tooltip>
- <template #default="{ row }">{{ row.publisher || '-' }}</template>
- </el-table-column>
- <el-table-column prop="processor" label="处置人" width="110" show-overflow-tooltip>
- <template #default="{ row }">{{ row.processor || '-' }}</template>
- </el-table-column>
- <el-table-column label="归档时间" width="168">
- <template #default="{ row }">{{ formatDateTime(archiveTimeOf(row)) }}</template>
- </el-table-column>
- <el-table-column prop="status" label="处理状态" width="100" align="center">
- <template #default="{ row }">
- <el-tag :type="statusTagType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="130" fixed="right">
- <template #default="{ row }">
- <el-button link type="primary" size="small" @click="handleView(row)">查看</el-button>
- <el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize"
- @pagination="loadData" />
- </div>
- <!-- ==================== 查看详情弹窗 ==================== -->
- <el-drawer v-model="dialogVisible" title="历史预警详情" size="720px" :close-on-click-modal="false"
- class="detail-drawer" direction="rtl">
- <div class="detail-content" v-if="currentDetail" v-loading="detailLoading">
- <div class="section-box">
- <div class="section-title">预警信息</div>
- <el-descriptions :column="2" border size="small">
- <el-descriptions-item label="预警编号">{{ currentDetail.warningNo || '-' }}</el-descriptions-item>
- <el-descriptions-item label="预警名称">{{ currentDetail.equipmentName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="预警类型">{{ warningTypeText(currentDetail) }}</el-descriptions-item>
- <el-descriptions-item label="预警级别">
- <el-tag :type="levelTagType(currentDetail.warningLevel)" size="small">{{
- levelText(currentDetail.warningLevel) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="预警专项">{{ currentDetail.warningSpecial || '-' }}</el-descriptions-item>
- <el-descriptions-item label="权属单位">{{ currentDetail.ownershipUnit || '-' }}</el-descriptions-item>
- <el-descriptions-item label="预警时间">{{ formatDateTime(currentDetail.createTime) }}</el-descriptions-item>
- <el-descriptions-item label="处理状态">
- <el-tag :type="statusTagType(currentDetail.status)" size="small">{{ statusText(currentDetail.status)
- }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="发布人">{{ currentDetail.publisher || '-' }}</el-descriptions-item>
- <el-descriptions-item label="创建人">{{ currentDetail.createBy || '-' }}</el-descriptions-item>
- <el-descriptions-item label="位置" :span="2">{{ currentDetail.location || '-' }}</el-descriptions-item>
- <el-descriptions-item label="预警内容" :span="2">
- <div class="desc-text">{{ currentDetail.content || '-' }}</div>
- </el-descriptions-item>
- <el-descriptions-item v-if="currentDetail.remark" label="备注" :span="2">
- {{ currentDetail.remark }}
- </el-descriptions-item>
- </el-descriptions>
- </div>
- <div class="section-box">
- <div class="section-title">归档信息</div>
- <el-descriptions :column="2" border size="small">
- <el-descriptions-item label="归档编号">{{ archiveNoOf(currentDetail) }}</el-descriptions-item>
- <el-descriptions-item label="归档时间">{{ formatDateTime(archiveTimeOf(currentDetail)) }}</el-descriptions-item>
- <el-descriptions-item label="处置人">{{ currentDetail.processor || '-' }}</el-descriptions-item>
- <el-descriptions-item label="归档状态">{{ statusText(currentDetail.status) }}</el-descriptions-item>
- <el-descriptions-item label="发布时间" :span="2">
- {{ formatDateTime(currentDetail.publishTime) }}
- </el-descriptions-item>
- </el-descriptions>
- </div>
- <div class="section-box">
- <div class="section-title">关联设备与预警指标</div>
- <el-descriptions v-if="detailDevice" :column="2" border size="small">
- <el-descriptions-item label="设备名称">{{ detailDevice.equipmentName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
- <el-descriptions-item label="设备类型">{{ equipmentTypeMap.get(String(detailDevice.equipmentTypeId)) || '-'
- }}</el-descriptions-item>
- <el-descriptions-item label="在线状态">
- <el-tag :type="detailDevice.onlineStatus === 1 ? 'success' : 'info'" size="small">
- {{ detailDevice.onlineStatus === 1 ? '在线' : '离线' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="安装位置" :span="2">{{ detailDevice.equipmentLocation ||
- currentDetail.location || '-' }}</el-descriptions-item>
- </el-descriptions>
- <el-alert v-else type="info" :closable="false" show-icon class="device-missing"
- title="未在设备台账中找到该预警关联的设备,指标数据无法展示" />
- <div class="metric-grid">
- <div class="metric-card">
- <div class="metric-label">实时液位</div>
- <div class="metric-value">
- <template v-if="detailMetric.level !== null">{{ detailMetric.level
- }}<span class="metric-unit">m</span></template>
- <span v-else class="metric-empty">—</span>
- </div>
- </div>
- <div class="metric-card">
- <div class="metric-label">报警阈值</div>
- <div class="metric-value metric-value-text">{{ detailMetric.thresholdText || '未配置' }}</div>
- </div>
- <div class="metric-card">
- <div class="metric-label">采集时间</div>
- <div class="metric-value metric-value-text">{{ detailMetric.collectTime || '—' }}</div>
- </div>
- </div>
- <el-table v-if="detailThresholds.length" :data="detailThresholds" border size="small"
- empty-text="该设备尚未配置报警阈值">
- <el-table-column label="监测指标" prop="warningName" min-width="120" />
- <el-table-column label="单位" width="90" align="center">
- <template #default="{ row }">{{ row.warningUnit || '-' }}</template>
- </el-table-column>
- <el-table-column label="阈值范围" min-width="150" align="center">
- <template #default="{ row }">{{ formatThreshold(row) }}</template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- <template #footer>
- <el-button @click="dialogVisible = false">关 闭</el-button>
- </template>
- </el-drawer>
- </div>
- </template>
- <script setup name="Lsyjgl">
- import { ref, reactive, computed, onMounted, nextTick } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import * as XLSX from 'xlsx'
- import { saveAs } from 'file-saver'
- import * as echarts from 'echarts'
- import { getWarningHistoryPage, deleteWarning, getEquipmentTypeList, getLatestWaterLevelByDevice } from '@/api/drainage'
- import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
- import { pageWarningThresholds } from '@/api/warningThreshold'
- import { listWarningTypes } from '@/api/warning/list'
- // ==================== 设备类型 / 预警类型映射 ====================
- const equipmentTypeMap = ref(new Map())
- /** warningType(类型编码) -> 类型名称,来自 warning_type 表 */
- const warningTypeMap = ref(new Map())
- const warningTypeOptions = ref([])
- async function loadEquipmentTypes() {
- try {
- const res = await getEquipmentTypeList('2')
- const list = res.code !== undefined ? res.data : res
- const map = new Map()
- if (Array.isArray(list)) {
- list.forEach(item => {
- if (item.id !== undefined && item.id !== null) map.set(String(item.id), item.typeName)
- })
- }
- equipmentTypeMap.value = map
- } catch (error) {
- console.error('加载设备类型列表失败', error)
- }
- }
- async function loadWarningTypes() {
- try {
- const res = await listWarningTypes()
- const list = res?.data || res?.rows || []
- const map = new Map()
- if (Array.isArray(list)) {
- list.forEach(item => {
- if (item.dictValue) map.set(String(item.dictValue), item.dictLabel || item.dictValue)
- })
- }
- warningTypeOptions.value = Array.isArray(list) ? list : []
- warningTypeMap.value = map
- } catch (error) {
- console.warn('加载预警类型配置失败', error)
- }
- }
- /** 预警类型展示:新数据看 warningType,历史数据回退到 deviceCode 的设备类型名 */
- function warningTypeText(row) {
- if (!row) return '-'
- const byType = row.warningType ? warningTypeMap.value.get(String(row.warningType)) : null
- if (byType) return byType
- const byDeviceType = row.deviceCode ? equipmentTypeMap.value.get(String(row.deviceCode)) : null
- if (byDeviceType) return byDeviceType
- return row.warningType || row.deviceCode || '-'
- }
- // ==================== 等级/状态工具方法 ====================
- function levelText(level) {
- const map = { 1: '严重', 2: '重要', 3: '一般' }
- return map[level] || '-'
- }
- function levelTagType(level) {
- const map = { 1: 'danger', 2: 'warning', 3: '' }
- return map[level] || 'info'
- }
- function statusText(status) {
- const map = { DRAFT: '草稿', PENDING: '待办', RECEIVED: '已接收', PROCESSING: '处置中', HANDLED: '已处置', CLOSED: '已解除' }
- return map[status] ?? '-'
- }
- function statusTagType(status) {
- const map = { DRAFT: 'info', PENDING: 'warning', RECEIVED: 'warning', PROCESSING: 'warning', HANDLED: 'success', CLOSED: '' }
- return map[status] || 'info'
- }
- function formatDateTime(value) {
- if (!value) return '-'
- return String(value).replace('T', ' ').slice(0, 19)
- }
- /** 归档编号:预警解除即归档,编号由预警编号派生,稳定可追溯 */
- function archiveNoOf(row) {
- if (!row?.warningNo) return '-'
- return `ARC${String(row.warningNo).replace(/^WRN/, '').slice(-12)}`
- }
- /** 归档时间:解除/处置动作发生在 update_time,缺失时退回发布时间 */
- function archiveTimeOf(row) {
- return row?.updateTime || row?.publishTime || null
- }
- // ==================== 分类统计 ====================
- const statItems = computed(() => {
- const list = tableData.value
- const devices = new Set(list.map(o => o.equipmentName).filter(Boolean))
- return [
- { label: '归档预警', value: total.value, cls: '' },
- { label: '已处置', value: list.filter(o => o.status === 'HANDLED').length, cls: 'green' },
- { label: '已解除', value: list.filter(o => o.status === 'CLOSED').length, cls: 'blue' },
- { label: '涉及设备', value: devices.size, cls: '' }
- ]
- })
- const statusLegends = computed(() => [
- { name: '已处置', value: tableData.value.filter(o => o.status === 'HANDLED').length, color: '#67C23A' },
- { name: '已解除', value: tableData.value.filter(o => o.status === 'CLOSED').length, color: '#409EFF' }
- ])
- const levelLegends = computed(() => [
- { name: '严重', value: tableData.value.filter(o => o.warningLevel === 1).length, color: '#F56C6C' },
- { name: '重要', value: tableData.value.filter(o => o.warningLevel === 2).length, color: '#E6A23C' },
- { name: '一般', value: tableData.value.filter(o => o.warningLevel === 3).length, color: '#909399' }
- ])
- const ringCharts = [
- { id: 'historyStatusChart', legends: statusLegends },
- { id: 'historyLevelChart', legends: levelLegends }
- ]
- function initCharts() {
- ringCharts.forEach(chart => {
- const dom = document.getElementById(chart.id)
- if (!dom) return
- const legends = chart.legends.value
- const chartInstance = echarts.getInstanceByDom(dom) || echarts.init(dom)
- chartInstance.setOption({
- tooltip: { trigger: 'item', formatter: '{b}:{c}({d}%)' },
- series: [{
- type: 'pie',
- radius: ['58%', '82%'],
- center: ['50%', '50%'],
- avoidLabelOverlap: false,
- label: { show: false },
- labelLine: { show: false },
- data: legends.map(item => ({
- name: item.name,
- value: item.value,
- itemStyle: { color: item.color }
- }))
- }]
- }, true)
- })
- }
- // ==================== 表格数据 ====================
- const tableData = ref([])
- const total = ref(0)
- const loading = ref(false)
- const pageNum = ref(1)
- const pageSize = ref(10)
- // ==================== 搜索筛选 ====================
- const showSearch = ref(true)
- const queryRef = ref()
- const filters = reactive({
- equipmentName: '',
- warningType: '',
- warningLevel: '',
- status: '',
- dateRange: null
- })
- function buildParams() {
- const params = {
- historyOnly: true,
- equipmentType: 'drainage'
- }
- if (filters.equipmentName) params.equipmentName = filters.equipmentName
- if (filters.warningType) params.warningType = filters.warningType
- if (filters.warningLevel) params.warningLevel = filters.warningLevel
- if (filters.status !== '' && filters.status !== null) params.status = filters.status
- if (filters.dateRange && filters.dateRange.length === 2) {
- params.startTime = `${filters.dateRange[0]} 00:00:00`
- params.endTime = `${filters.dateRange[1]} 23:59:59`
- }
- return params
- }
- // ==================== 数据加载 ====================
- async function loadData() {
- loading.value = true
- try {
- // 历史页由后端按历史状态(HANDLED/CLOSED)限定
- const res = await getWarningHistoryPage(pageNum.value, pageSize.value, buildParams())
- const pageData = res.code !== undefined ? res.data : res
- tableData.value = pageData.records || []
- total.value = pageData.total || 0
- } catch (error) {
- console.error('加载历史预警列表失败', error)
- ElMessage.error('加载历史预警列表失败')
- } finally {
- loading.value = false
- nextTick(() => initCharts())
- }
- }
- function handleSearch() {
- pageNum.value = 1
- loadData()
- }
- function handleClear() {
- filters.equipmentName = ''
- filters.warningType = ''
- filters.warningLevel = ''
- filters.status = ''
- filters.dateRange = null
- pageNum.value = 1
- loadData()
- }
- // ==================== 归档导出 ====================
- const exporting = ref(false)
- const EXPORT_PAGE_SIZE = 100
- const EXPORT_HEADER = [
- '归档编号', '预警编号', '预警名称', '预警类型', '预警级别', '预警内容',
- '权属单位', '位置', '预警时间', '发布人', '处置人', '归档时间', '处理状态'
- ]
- function exportValues(row) {
- return [
- archiveNoOf(row),
- row.warningNo || '',
- row.equipmentName || '',
- warningTypeText(row),
- levelText(row.warningLevel),
- row.content || '',
- row.ownershipUnit || '',
- row.location || '',
- formatDateTime(row.createTime),
- row.publisher || '',
- row.processor || '',
- formatDateTime(archiveTimeOf(row)),
- statusText(row.status)
- ]
- }
- /** 按当前筛选条件翻页拉全量,单页给大值容易被后端分页上限截断 */
- async function fetchAllRows() {
- const collected = []
- let page = 1
- while (page <= 50) {
- const res = await getWarningHistoryPage(page, EXPORT_PAGE_SIZE, buildParams())
- const pageData = res.code !== undefined ? res.data : res
- const rows = pageData.records || pageData.rows || []
- if (rows.length === 0) break
- collected.push(...rows)
- const totalCount = Number(pageData.total) || 0
- if (rows.length < EXPORT_PAGE_SIZE) break
- if (totalCount > 0 && collected.length >= totalCount) break
- page += 1
- }
- return collected
- }
- async function handleExport() {
- exporting.value = true
- try {
- const rows = await fetchAllRows()
- if (rows.length === 0) {
- ElMessage.warning('当前条件下没有可导出的归档记录')
- return
- }
- const sheet = XLSX.utils.aoa_to_sheet([EXPORT_HEADER, ...rows.map(exportValues)])
- const book = XLSX.utils.book_new()
- XLSX.utils.book_append_sheet(book, sheet, '历史预警归档')
- const d = new Date()
- const pad = (n) => String(n).padStart(2, '0')
- const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`
- saveAs(new Blob([XLSX.write(book, { bookType: 'xlsx', type: 'array' })]),
- `历史预警归档清单_${stamp}.xlsx`)
- ElMessage.success(`已导出 ${rows.length} 条归档记录`)
- } catch (error) {
- console.error('导出归档清单失败', error)
- ElMessage.error('导出失败,请稍后重试')
- } finally {
- exporting.value = false
- }
- }
- // ==================== 操作:删除 ====================
- async function handleDelete(row) {
- try {
- await ElMessageBox.confirm('确认删除该预警?删除后不可恢复。', '提示', { type: 'warning' })
- await deleteWarning(row.id)
- ElMessage.success('删除成功')
- loadData()
- } catch (error) {
- if (error !== 'cancel') console.error('删除预警失败', error)
- }
- }
- // ==================== 查看详情弹窗 ====================
- const dialogVisible = ref(false)
- const currentDetail = ref(null)
- const detailLoading = ref(false)
- const detailDevice = ref(null)
- const detailThresholds = ref([])
- const waterLevelMap = ref({})
- const thresholdMap = ref({})
- /** equipmentName -> 设备档案:预警 device_code 存的是类型 id,只能按名称反查设备 */
- const deviceByName = ref(null)
- const detailMetric = computed(() => {
- const code = detailDevice.value?.equipmentCode
- const levelItem = code ? waterLevelMap.value[code] : null
- const thresholds = code ? (thresholdMap.value[code] || []) : []
- return {
- level: levelItem && levelItem.value !== undefined && levelItem.value !== null ? levelItem.value : null,
- collectTime: levelItem?.time ? formatDateTime(levelItem.time) : '',
- thresholdText: thresholds.length > 0 ? thresholds.map(formatThreshold).join(' / ') : ''
- }
- })
- async function handleView(row) {
- currentDetail.value = { ...row }
- detailDevice.value = null
- detailThresholds.value = []
- dialogVisible.value = true
- detailLoading.value = true
- try {
- const [device] = await Promise.all([resolveDevice(row.equipmentName), loadLiveMetrics()])
- detailDevice.value = device || null
- const code = device?.equipmentCode
- detailThresholds.value = code ? (thresholdMap.value[code] || []) : []
- } catch (error) {
- console.warn('预警详情数据加载失败', error)
- } finally {
- detailLoading.value = false
- }
- }
- async function resolveDevice(equipmentName) {
- if (!equipmentName) return null
- if (!deviceByName.value) {
- const map = new Map()
- let page = 1
- while (page <= 10) {
- const res = await listEquipment({ rootTypeId: 2, pageNum: page, pageSize: 100 })
- const pageData = res?.data && Array.isArray(res.data.records) ? res.data : res
- const rows = pageData?.records || pageData?.rows || []
- if (rows.length === 0) break
- rows.forEach((item) => {
- if (item?.equipmentName && !map.has(item.equipmentName)) map.set(item.equipmentName, item)
- })
- if (rows.length < 100) break
- page += 1
- }
- deviceByName.value = map
- }
- return deviceByName.value.get(equipmentName) || null
- }
- async function loadLiveMetrics() {
- const [levelResult, thresholdResult] = await Promise.allSettled([
- getLatestWaterLevelByDevice(),
- pageWarningThresholds({ rootTypeId: 2, pageNum: 1, pageSize: 500 })
- ])
- if (levelResult.status === 'fulfilled') {
- const list = Array.isArray(levelResult.value) ? levelResult.value : (levelResult.value?.data || [])
- const map = {}
- list.forEach((item) => {
- if (!item?.deviceCode) return
- map[item.deviceCode] = { value: item.waterLevel, time: item.createTime }
- })
- waterLevelMap.value = map
- }
- if (thresholdResult.status === 'fulfilled') {
- const res = thresholdResult.value
- const pageData = res?.data && Array.isArray(res.data.records) ? res.data : res
- const rows = pageData?.records || pageData?.rows || []
- const map = {}
- rows.forEach((row) => {
- if (!row?.deviceCode) return
- if (!map[row.deviceCode]) map[row.deviceCode] = []
- map[row.deviceCode].push(row)
- })
- thresholdMap.value = map
- }
- }
- /** 阈值展示:dataType 0 区间 / 1 下限 / 2 上限 */
- function formatThreshold(item) {
- if (!item) return '-'
- const unit = item.warningUnit || ''
- const min = item.minValue
- const max = item.maxValue
- if (item.dataType === 1) return min === null || min === undefined ? '-' : `≥ ${min}${unit}`
- if (item.dataType === 2) return max === null || max === undefined ? '-' : `≤ ${max}${unit}`
- if ((min === null || min === undefined) && (max === null || max === undefined)) return '未设置'
- return `${min ?? '-'} ~ ${max ?? '-'}${unit}`
- }
- onMounted(async () => {
- await Promise.all([loadEquipmentTypes(), loadWarningTypes()])
- loadData()
- })
- </script>
- <style scoped>
- .lsyjgl-page {
- background: #fff;
- min-height: 100%;
- padding: 16px;
- }
- /* 分类统计 */
- .stats-panel {
- display: flex;
- gap: 16px;
- align-items: stretch;
- margin-bottom: 16px;
- padding: 16px 20px;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- }
- .stats-left {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- width: 300px;
- flex-shrink: 0;
- }
- .stat-item {
- border: 1px solid #ebeef5;
- border-radius: 4px;
- padding: 10px 14px;
- background: #fafbfc;
- }
- .stat-value {
- font-size: 24px;
- font-weight: 600;
- color: #303133;
- line-height: 1.2;
- }
- .stat-value.green {
- color: #67c23a;
- }
- .stat-value.blue {
- color: #409eff;
- }
- .stat-label {
- margin-top: 2px;
- font-size: 12px;
- color: #909399;
- }
- .stats-right {
- flex: 1;
- display: flex;
- gap: 32px;
- justify-content: center;
- align-items: center;
- }
- .ring-chart-box {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .ring-chart {
- width: 110px;
- height: 110px;
- }
- .ring-legend {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .legend-row {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #606266;
- white-space: nowrap;
- }
- .legend-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- }
- .legend-num {
- font-weight: 600;
- color: #303133;
- }
- /* 搜索区 */
- .search-panel {
- background: #fff;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- padding: 16px 20px;
- margin-bottom: 16px;
- }
- .search-form {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- }
- .form-item {
- display: flex;
- align-items: center;
- }
- .form-label {
- font-size: 14px;
- color: #606266;
- white-space: nowrap;
- margin-right: 4px;
- }
- /* 表格区 */
- .table-panel {
- background: #fff;
- border-radius: 4px;
- }
- /* 详情弹窗 */
- .detail-content {
- padding: 0 8px;
- }
- .section-box {
- border: 1px solid #ebeef5;
- border-radius: 4px;
- padding: 16px;
- margin-bottom: 16px;
- }
- .section-title {
- font-size: 14px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 12px;
- }
- .desc-text {
- background: #f5f7fa;
- border-radius: 4px;
- padding: 10px;
- font-size: 13px;
- color: #606266;
- line-height: 1.6;
- max-height: 120px;
- overflow-y: auto;
- }
- /* 预警指标卡片 */
- .metric-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 12px;
- margin: 12px 0;
- }
- .metric-card {
- border: 1px solid #ebeef5;
- border-radius: 4px;
- padding: 12px 14px;
- background: #fafbfc;
- }
- .metric-label {
- font-size: 12px;
- color: #909399;
- margin-bottom: 6px;
- }
- .metric-value {
- font-size: 22px;
- font-weight: 600;
- color: #303133;
- line-height: 1.2;
- }
- .metric-value-text {
- font-size: 14px;
- font-weight: 500;
- word-break: break-all;
- }
- .metric-unit {
- font-size: 12px;
- font-weight: 400;
- color: #909399;
- margin-left: 2px;
- }
- .metric-empty {
- color: #c0c4cc;
- }
- .device-missing {
- margin-bottom: 12px;
- }
- </style>
|