|
|
@@ -1,859 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="app-container bjyp-page">
|
|
|
- <!-- Tab栏 -->
|
|
|
- <div class="tab-bar">
|
|
|
- <span :class="['tab-item', { active: activeTab === 'home' }]" @click="activeTab = 'home'">首页</span>
|
|
|
- <!-- <span :class="['tab-item', { active: activeTab === 'monthly' }]" @click="activeTab = 'monthly'">报警月统计 ▼</span> -->
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 统计卡片 -->
|
|
|
- <div class="stats-row">
|
|
|
- <div class="stat-card" v-for="item in statCards" :key="item.label">
|
|
|
- <div class="stat-label">{{ item.label }}</div>
|
|
|
- <div class="stat-value">
|
|
|
- {{ item.value }}
|
|
|
- <span class="stat-sub">{{ item.sub }}</span>
|
|
|
- </div>
|
|
|
- <div class="stat-compare">
|
|
|
- 比昨天
|
|
|
- <span v-if="item.trend === 0" style="color: rgba(255,255,255,0.4)">— 持平</span>
|
|
|
- <span v-else :class="item.trend > 0 ? 'trend-up' : 'trend-down'">
|
|
|
- {{ item.trend > 0 ? '↑' : '↓' }} {{ Math.abs(item.trend) }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 统计时间范围:当前范围会同时作用于卡片、列表和全部图表 -->
|
|
|
- <div class="range-bar">
|
|
|
- <span class="range-label">统计时间:</span>
|
|
|
- <el-radio-group v-model="activeDays" size="small" @change="handleRangeChange">
|
|
|
- <el-radio-button v-for="option in rangeOptions" :key="option.value" :label="option.value">
|
|
|
- {{ option.label }}
|
|
|
- </el-radio-button>
|
|
|
- </el-radio-group>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 图表区域 -->
|
|
|
- <div class="chart-section">
|
|
|
- <div class="chart-left">
|
|
|
- <div class="section-title">最近{{ activeDays }}天报警趋势</div>
|
|
|
- <div ref="trendChartRef" class="trend-chart"></div>
|
|
|
- </div>
|
|
|
- <div class="chart-right">
|
|
|
- <div class="pie-box">
|
|
|
- <div class="section-title">报警比例分布图</div>
|
|
|
- <div ref="pieChartRef" class="pie-chart"></div>
|
|
|
- </div>
|
|
|
- <div class="pie-box">
|
|
|
- <div class="section-title">报警处理比例分布</div>
|
|
|
- <div ref="statusPieRef" class="pie-chart"></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 报警列表 -->
|
|
|
- <div class="table-section">
|
|
|
- <div class="section-title">报警数据列表</div>
|
|
|
- <el-table :data="tableData" v-loading="loading" border size="small" style="width: 100%">
|
|
|
- <el-table-column label="观测井编号" prop="wellCode" min-width="130" align="center" show-overflow-tooltip />
|
|
|
- <el-table-column label="预警类型" prop="warningType" min-width="110" align="center" />
|
|
|
- <el-table-column label="报警时间" prop="warningTime" min-width="160" align="center" />
|
|
|
- <el-table-column label="是否处理" min-width="90" align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="statusTagType(row.status)" size="small" effect="plain">{{ statusText(row.status) }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="设备编号" prop="equipmentId" min-width="130" align="center" show-overflow-tooltip />
|
|
|
- <el-table-column label="观测井类型" prop="wellType" min-width="100" align="center" />
|
|
|
- <el-table-column label="报警编号" prop="id" min-width="80" align="center" />
|
|
|
- <el-table-column label="操作" width="90" align="center" fixed="right">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button link type="primary" @click="openAnalysis(row)">详情</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 分页 -->
|
|
|
- <div class="pagination-wrap">
|
|
|
- <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
|
|
|
- :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
|
|
|
- @current-change="handlePageChange" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 研判分析弹窗 -->
|
|
|
- <el-drawer v-model="analysisVisible" title="研判分析" size="900px" :before-close="closeAnalysis" class="detail-drawer" direction="rtl">
|
|
|
- <!-- 报警基本信息 -->
|
|
|
- <div class="analysis-header">
|
|
|
- <el-descriptions :column="3" border size="small">
|
|
|
- <el-descriptions-item label="设备名称">{{ currentAlarm.equipmentName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备编号">{{ currentAlarm.id }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备位置">{{ currentAlarm.location }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警等级">
|
|
|
- <span :class="levelClass(currentAlarm.warningLevel)">{{ levelText(currentAlarm.warningLevel) }}</span>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警时间">{{ currentAlarm.warningTime }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警类型">{{ currentAlarm.warningType || '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="实际值">{{ currentAlarm.currentValue ?? '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="阈值">{{ currentAlarm.thresholdValue ?? '-' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="处理状态">
|
|
|
- <el-tag :type="statusTagType(currentAlarm.status)" size="small" effect="plain">{{ statusText(currentAlarm.status) }}</el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- </div>
|
|
|
-
|
|
|
- <template #footer>
|
|
|
- <el-button @click="closeAnalysis">关闭</el-button>
|
|
|
- </template>
|
|
|
- </el-drawer>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup name="Bjyp">
|
|
|
-import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import * as echarts from 'echarts'
|
|
|
-import { getAlarmDataPage, firstApproveWarning, finalApproveWarning, handleAlarm } from '@/api/drainage'
|
|
|
-import useUserStore from '@/store/modules/user'
|
|
|
-
|
|
|
-// ==================== 用户信息 ====================
|
|
|
-const userStore = useUserStore()
|
|
|
-const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
|
|
|
-
|
|
|
-// ==================== Tab栏 ====================
|
|
|
-const activeTab = ref('home')
|
|
|
-const activeDays = ref(7)
|
|
|
-const rangeOptions = [
|
|
|
- { label: '7天', value: 7 },
|
|
|
- { label: '15天', value: 15 },
|
|
|
- { label: '30天', value: 30 }
|
|
|
-]
|
|
|
-
|
|
|
-// ==================== 等级/状态映射 ====================
|
|
|
-const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
|
|
|
-const statusMap = { 0: '未处理', 1: '已处理' }
|
|
|
-
|
|
|
-function levelText(level) {
|
|
|
- return levelMap[level] || '-'
|
|
|
-}
|
|
|
-function levelClass(level) {
|
|
|
- const map = { 1: 'level-critical', 2: 'level-major', 3: 'level-minor' }
|
|
|
- return map[level] || ''
|
|
|
-}
|
|
|
-function statusText(status) {
|
|
|
- return statusMap[status] ?? '未处理'
|
|
|
-}
|
|
|
-function statusTagType(status) {
|
|
|
- if (status === 1) return 'success'
|
|
|
- return 'danger'
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 预警类型分类 ====================
|
|
|
-function getWarningCategory(wt) {
|
|
|
- if (!wt) return '其他'
|
|
|
- if (wt.includes('液位')) return '液位报警'
|
|
|
- if (wt.includes('液流')) return '液流报警'
|
|
|
- if (wt.includes('流量')) return '流量报警'
|
|
|
- if (wt.includes('水质')) return '水质报警'
|
|
|
- if (wt.includes('压力')) return '压力报警'
|
|
|
- return '其他'
|
|
|
-}
|
|
|
-function getWellType(wt) {
|
|
|
- if (!wt) return '排水井'
|
|
|
- if (wt.includes('液位')) return '液位井'
|
|
|
- if (wt.includes('液流')) return '液流井'
|
|
|
- if (wt.includes('流量')) return '流量井'
|
|
|
- if (wt.includes('水质')) return '水质井'
|
|
|
- if (wt.includes('压力')) return '压力井'
|
|
|
- return '排水井'
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 全量数据(统计与图表用) ====================
|
|
|
-const allRecords = ref([])
|
|
|
-const sevenDayRecords = ref([])
|
|
|
-const tableData = ref([])
|
|
|
-const total = ref(0)
|
|
|
-const loading = ref(false)
|
|
|
-const pageNum = ref(1)
|
|
|
-const pageSize = ref(10)
|
|
|
-
|
|
|
-// 真实"比昨天"趋势数据(从7天数据中计算)
|
|
|
-const statTrends = ref([0, 0, 0, 0, 0, 0])
|
|
|
-
|
|
|
-function computeRealTrends() {
|
|
|
- const now = new Date()
|
|
|
- const todayStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0')
|
|
|
- const yesterday = new Date(now.getTime() - 86400000)
|
|
|
- const yesterdayStr = yesterday.getFullYear() + '-' + String(yesterday.getMonth() + 1).padStart(2, '0') + '-' + String(yesterday.getDate()).padStart(2, '0')
|
|
|
-
|
|
|
- function getDateStr(record) {
|
|
|
- const t = record.warningTime
|
|
|
- if (!t) return ''
|
|
|
- return String(t).slice(0, 10)
|
|
|
- }
|
|
|
-
|
|
|
- const todayCount = { total: 0, 液位: 0, 液流: 0, 流量: 0, 水质: 0, 压力: 0 }
|
|
|
- const yesterdayCount = { total: 0, 液位: 0, 液流: 0, 流量: 0, 水质: 0, 压力: 0 }
|
|
|
-
|
|
|
- sevenDayRecords.value.forEach(r => {
|
|
|
- const ds = getDateStr(r)
|
|
|
- const wt = r.warningType || ''
|
|
|
- if (ds === todayStr) {
|
|
|
- todayCount.total++
|
|
|
- if (wt.includes('液位')) todayCount.液位++
|
|
|
- if (wt.includes('液流')) todayCount.液流++
|
|
|
- if (wt.includes('流量')) todayCount.流量++
|
|
|
- if (wt.includes('水质')) todayCount.水质++
|
|
|
- if (wt.includes('压力')) todayCount.压力++
|
|
|
- } else if (ds === yesterdayStr) {
|
|
|
- yesterdayCount.total++
|
|
|
- if (wt.includes('液位')) yesterdayCount.液位++
|
|
|
- if (wt.includes('液流')) yesterdayCount.液流++
|
|
|
- if (wt.includes('流量')) yesterdayCount.流量++
|
|
|
- if (wt.includes('水质')) yesterdayCount.水质++
|
|
|
- if (wt.includes('压力')) yesterdayCount.压力++
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- statTrends.value = [
|
|
|
- todayCount.total - yesterdayCount.total,
|
|
|
- todayCount.液位 - yesterdayCount.液位,
|
|
|
- todayCount.液流 - yesterdayCount.液流,
|
|
|
- todayCount.流量 - yesterdayCount.流量,
|
|
|
- todayCount.水质 - yesterdayCount.水质,
|
|
|
- todayCount.压力 - yesterdayCount.压力
|
|
|
- ]
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 统计卡片 ====================
|
|
|
-const statCards = computed(() => {
|
|
|
- const all = allRecords.value
|
|
|
- const cnt = (kw) => all.filter(r => r.warningType && r.warningType.includes(kw)).length
|
|
|
- return [
|
|
|
- { label: '报警总数', value: all.length, sub: '', trend: statTrends.value[0] },
|
|
|
- { label: '液位报警', value: cnt('液位'), sub: '', trend: statTrends.value[1] },
|
|
|
- { label: '液流报警', value: cnt('液流'), sub: '', trend: statTrends.value[2] },
|
|
|
- { label: '流量报警', value: cnt('流量'), sub: '', trend: statTrends.value[3] },
|
|
|
- { label: '水质报警', value: cnt('水质'), sub: '', trend: statTrends.value[4] },
|
|
|
- { label: '压力报警', value: cnt('压力'), sub: '', trend: statTrends.value[5] }
|
|
|
- ]
|
|
|
-})
|
|
|
-
|
|
|
-// ==================== 数据加载 ====================
|
|
|
-async function loadData() {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const now = new Date()
|
|
|
- const start = new Date(now.getTime() - (activeDays.value - 1) * 86400000)
|
|
|
- const formatDateTime = (date) => {
|
|
|
- const datePart = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
|
- return `${datePart} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
|
|
- }
|
|
|
- const res = await getAlarmDataPage(1, 9999, {
|
|
|
- equipmentType: 'drainage',
|
|
|
- startTime: formatDateTime(start),
|
|
|
- endTime: formatDateTime(now)
|
|
|
- })
|
|
|
- const pageData = res.code !== undefined ? res.data : res
|
|
|
- const records = pageData.records || pageData.rows || []
|
|
|
-
|
|
|
- const mapped = records.map(row => ({
|
|
|
- id: row.id,
|
|
|
- equipmentId: row.deviceCode,
|
|
|
- equipmentName: row.deviceName || '-',
|
|
|
- wellCode: row.deviceCode || row.deviceName || '-',
|
|
|
- wellType: getWellType(row.warningType),
|
|
|
- warningTime: row.alarmTime,
|
|
|
- warningLevel: row.alarmLevel,
|
|
|
- warningType: row.warningType || '-',
|
|
|
- content: row.warningType || '-',
|
|
|
- location: '-',
|
|
|
- currentValue: row.actualValue,
|
|
|
- thresholdValue: row.maxValue,
|
|
|
- status: row.alarmStatus,
|
|
|
- longitude: row.longitude,
|
|
|
- latitude: row.latitude
|
|
|
- }))
|
|
|
-
|
|
|
- sevenDayRecords.value = mapped
|
|
|
- allRecords.value = mapped
|
|
|
- total.value = mapped.length
|
|
|
- computeRealTrends()
|
|
|
- updateTablePage()
|
|
|
-
|
|
|
- nextTick(() => {
|
|
|
- initAllCharts()
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- console.error('加载报警列表失败', error)
|
|
|
- ElMessage.error('加载报警列表失败')
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function handleRangeChange() {
|
|
|
- pageNum.value = 1
|
|
|
- loadData()
|
|
|
-}
|
|
|
-
|
|
|
-function updateTablePage() {
|
|
|
- const start = (pageNum.value - 1) * pageSize.value
|
|
|
- tableData.value = allRecords.value.slice(start, start + pageSize.value)
|
|
|
-}
|
|
|
-
|
|
|
-function handlePageChange() {
|
|
|
- updateTablePage()
|
|
|
-}
|
|
|
-function handleSizeChange() {
|
|
|
- pageNum.value = 1
|
|
|
- updateTablePage()
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== ECharts 图表 ====================
|
|
|
-const trendChartRef = ref(null)
|
|
|
-const pieChartRef = ref(null)
|
|
|
-const statusPieRef = ref(null)
|
|
|
-
|
|
|
-let trendChart = null
|
|
|
-let pieChart = null
|
|
|
-let statusChart = null
|
|
|
-
|
|
|
-function initAllCharts() {
|
|
|
- initTrendChart()
|
|
|
- initPieChart()
|
|
|
- initStatusPie()
|
|
|
-}
|
|
|
-
|
|
|
-function initTrendChart() {
|
|
|
- if (!trendChartRef.value) return
|
|
|
- if (trendChart) trendChart.dispose()
|
|
|
- trendChart = echarts.init(trendChartRef.value)
|
|
|
-
|
|
|
- // 生成当前时间范围的日期键(M/D 格式与 YYYY-MM-DD 格式)
|
|
|
- const days = []
|
|
|
- const dayKeys = []
|
|
|
- const now = new Date()
|
|
|
- for (let i = activeDays.value - 1; i >= 0; i--) {
|
|
|
- const d = new Date(now.getTime() - i * 86400000)
|
|
|
- days.push((d.getMonth() + 1) + '/' + d.getDate())
|
|
|
- dayKeys.push(d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'))
|
|
|
- }
|
|
|
-
|
|
|
- // 按日期 + 类型分组统计
|
|
|
- const categories = ['液位', '液流', '流量', '水质', '压力']
|
|
|
- const countMap = {}
|
|
|
- dayKeys.forEach(dk => {
|
|
|
- countMap[dk] = {}
|
|
|
- categories.forEach(c => { countMap[dk][c] = 0 })
|
|
|
- })
|
|
|
-
|
|
|
- sevenDayRecords.value.forEach(r => {
|
|
|
- const t = r.warningTime
|
|
|
- if (!t) return
|
|
|
- const ds = String(t).slice(0, 10)
|
|
|
- if (!countMap[ds]) return
|
|
|
- const wt = r.warningType || ''
|
|
|
- categories.forEach(c => {
|
|
|
- if (wt.includes(c)) countMap[ds][c]++
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- const seriesNames = categories.map(c => c + '报警')
|
|
|
- const colors = ['#5470c6', '#ee6666', '#91cc75', '#fac858', '#73c0de']
|
|
|
-
|
|
|
- const series = categories.map((cat, idx) => ({
|
|
|
- name: cat + '报警',
|
|
|
- type: 'line',
|
|
|
- stack: '总量',
|
|
|
- data: dayKeys.map(dk => countMap[dk][cat]),
|
|
|
- smooth: true,
|
|
|
- symbol: 'circle',
|
|
|
- symbolSize: 5,
|
|
|
- itemStyle: { color: colors[idx] },
|
|
|
- areaStyle: { opacity: 0.35 }
|
|
|
- }))
|
|
|
-
|
|
|
- trendChart.setOption({
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- axisPointer: { type: 'cross', label: { backgroundColor: 'rgba(20,40,70,0.8)' } }
|
|
|
- },
|
|
|
- legend: {
|
|
|
- data: seriesNames,
|
|
|
- top: 0,
|
|
|
- textStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 11 },
|
|
|
- itemWidth: 12,
|
|
|
- itemHeight: 8
|
|
|
- },
|
|
|
- grid: { left: '2%', right: '3%', top: '18%', bottom: '5%', containLabel: true },
|
|
|
- xAxis: {
|
|
|
- type: 'category',
|
|
|
- data: days,
|
|
|
- boundaryGap: false,
|
|
|
- axisLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 11 },
|
|
|
- axisLine: { lineStyle: { color: 'rgba(255,255,255,0.2)' } }
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: 'value',
|
|
|
- minInterval: 1,
|
|
|
- axisLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 11 },
|
|
|
- splitLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } }
|
|
|
- },
|
|
|
- series
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function initPieChart() {
|
|
|
- if (!pieChartRef.value) return
|
|
|
- if (pieChart) pieChart.dispose()
|
|
|
- pieChart = echarts.init(pieChartRef.value)
|
|
|
-
|
|
|
- const catCount = {}
|
|
|
- allRecords.value.forEach(r => {
|
|
|
- const cat = getWarningCategory(r.warningType)
|
|
|
- catCount[cat] = (catCount[cat] || 0) + 1
|
|
|
- })
|
|
|
-
|
|
|
- // 确保所有类型都出现
|
|
|
- const allCats = ['液位报警', '液流报警', '流量报警', '水质报警', '压力报警', '其他']
|
|
|
- allCats.forEach(c => { if (!catCount[c]) catCount[c] = 0 })
|
|
|
-
|
|
|
- const data = Object.entries(catCount)
|
|
|
- .filter(([_, v]) => v > 0)
|
|
|
- .map(([name, value]) => ({ name, value }))
|
|
|
-
|
|
|
- if (data.length === 0) {
|
|
|
- data.push({ name: '暂无数据', value: 1 })
|
|
|
- }
|
|
|
-
|
|
|
- const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#fc8452']
|
|
|
-
|
|
|
- pieChart.setOption({
|
|
|
- tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
|
- legend: {
|
|
|
- bottom: 0,
|
|
|
- textStyle: { color: 'rgba(255,255,255,0.6)', fontSize: 10 },
|
|
|
- type: 'scroll',
|
|
|
- itemWidth: 10,
|
|
|
- itemHeight: 8
|
|
|
- },
|
|
|
- color: colors,
|
|
|
- series: [{
|
|
|
- type: 'pie',
|
|
|
- radius: ['38%', '62%'],
|
|
|
- center: ['50%', '42%'],
|
|
|
- data,
|
|
|
- label: { color: 'rgba(255,255,255,0.7)', fontSize: 10, formatter: '{b}\n{d}%' },
|
|
|
- labelLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' }, length: 8, length2: 8 },
|
|
|
- itemStyle: { borderColor: 'rgba(10,22,40,0.6)', borderWidth: 2 }
|
|
|
- }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function initStatusPie() {
|
|
|
- if (!statusPieRef.value) return
|
|
|
- if (statusChart) statusChart.dispose()
|
|
|
- statusChart = echarts.init(statusPieRef.value)
|
|
|
-
|
|
|
- const statusCount = { '未处理': 0, '已处理': 0 }
|
|
|
- allRecords.value.forEach(r => {
|
|
|
- const s = statusText(r.status)
|
|
|
- if (statusCount[s] !== undefined) statusCount[s]++
|
|
|
- else statusCount['未处理']++
|
|
|
- })
|
|
|
-
|
|
|
- const data = Object.entries(statusCount)
|
|
|
- .filter(([_, v]) => v > 0)
|
|
|
- .map(([name, value]) => ({ name, value }))
|
|
|
-
|
|
|
- if (data.length === 0) {
|
|
|
- data.push({ name: '暂无数据', value: 1 })
|
|
|
- }
|
|
|
-
|
|
|
- const colors = ['#ee6666', '#91cc75']
|
|
|
-
|
|
|
- statusChart.setOption({
|
|
|
- tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
|
- legend: {
|
|
|
- bottom: 0,
|
|
|
- textStyle: { color: 'rgba(255,255,255,0.6)', fontSize: 10 },
|
|
|
- type: 'scroll',
|
|
|
- itemWidth: 10,
|
|
|
- itemHeight: 8
|
|
|
- },
|
|
|
- color: colors,
|
|
|
- series: [{
|
|
|
- type: 'pie',
|
|
|
- radius: ['38%', '62%'],
|
|
|
- center: ['50%', '42%'],
|
|
|
- data,
|
|
|
- label: { color: 'rgba(255,255,255,0.7)', fontSize: 10, formatter: '{b}\n{d}%' },
|
|
|
- labelLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' }, length: 8, length2: 8 },
|
|
|
- itemStyle: { borderColor: 'rgba(10,22,40,0.6)', borderWidth: 2 }
|
|
|
- }]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function handleResize() {
|
|
|
- trendChart && trendChart.resize()
|
|
|
- pieChart && pieChart.resize()
|
|
|
- statusChart && statusChart.resize()
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 研判分析弹窗 ====================
|
|
|
-const analysisVisible = ref(false)
|
|
|
-const currentAlarm = ref({})
|
|
|
-
|
|
|
-const conclusionForm = ref({
|
|
|
- result: '',
|
|
|
- desc: ''
|
|
|
-})
|
|
|
-
|
|
|
-// 下派表单
|
|
|
-const dispatchForm = ref({
|
|
|
- handleUser: '',
|
|
|
- remark: ''
|
|
|
-})
|
|
|
-
|
|
|
-// 按钮loading状态
|
|
|
-const dispatching = ref(false)
|
|
|
-const handling = ref(false)
|
|
|
-const approving = ref(false)
|
|
|
-
|
|
|
-async function openAnalysis(row) {
|
|
|
- currentAlarm.value = { ...row }
|
|
|
- conclusionForm.value = { result: '', desc: '' }
|
|
|
- dispatchForm.value = { handleUser: '', remark: '' }
|
|
|
- analysisVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-function closeAnalysis() {
|
|
|
- analysisVisible.value = false
|
|
|
-}
|
|
|
-
|
|
|
-function submitConclusion() {
|
|
|
- if (!conclusionForm.value.result) {
|
|
|
- ElMessage.warning('请选择研判结果')
|
|
|
- return
|
|
|
- }
|
|
|
- if (!conclusionForm.value.desc) {
|
|
|
- ElMessage.warning('请输入研判说明')
|
|
|
- return
|
|
|
- }
|
|
|
- const idx = allRecords.value.findIndex(item => item.id === currentAlarm.value.id)
|
|
|
- if (idx !== -1) {
|
|
|
- allRecords.value[idx].status = 1
|
|
|
- }
|
|
|
- // 同步更新表格当前页
|
|
|
- updateTablePage()
|
|
|
- // 刷新图表
|
|
|
- nextTick(() => {
|
|
|
- initPieChart()
|
|
|
- initStatusPie()
|
|
|
- })
|
|
|
- ElMessage.success('研判提交成功')
|
|
|
- closeAnalysis()
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 生命周期 ====================
|
|
|
-onMounted(() => {
|
|
|
- loadData()
|
|
|
- window.addEventListener('resize', handleResize)
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeUnmount(() => {
|
|
|
- window.removeEventListener('resize', handleResize)
|
|
|
- if (trendChart) { trendChart.dispose(); trendChart = null }
|
|
|
- if (pieChart) { pieChart.dispose(); pieChart = null }
|
|
|
- if (statusChart) { statusChart.dispose(); statusChart = null }
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.bjyp-page {
|
|
|
- background: #0a1628;
|
|
|
- min-height: calc(100vh - 84px);
|
|
|
- padding: 16px;
|
|
|
- box-sizing: border-box;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== Tab栏 ==================== */
|
|
|
-.tab-bar {
|
|
|
- display: flex;
|
|
|
- gap: 4px;
|
|
|
- margin-bottom: 16px;
|
|
|
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
-}
|
|
|
-
|
|
|
-.tab-item {
|
|
|
- padding: 8px 24px;
|
|
|
- font-size: 14px;
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
- cursor: pointer;
|
|
|
- border-bottom: 2px solid transparent;
|
|
|
- transition: all 0.2s;
|
|
|
- user-select: none;
|
|
|
-}
|
|
|
-
|
|
|
-.tab-item:hover {
|
|
|
- color: rgba(255, 255, 255, 0.8);
|
|
|
-}
|
|
|
-
|
|
|
-.tab-item.active {
|
|
|
- color: #409eff;
|
|
|
- border-bottom-color: #409eff;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 统计卡片 ==================== */
|
|
|
-.stats-row {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-card {
|
|
|
- flex: 1;
|
|
|
- background: rgba(255, 255, 255, 0.05);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 14px 16px;
|
|
|
- transition: border-color 0.2s;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-card:hover {
|
|
|
- border-color: rgba(64, 158, 255, 0.3);
|
|
|
-}
|
|
|
-
|
|
|
-.stat-label {
|
|
|
- font-size: 13px;
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-value {
|
|
|
- font-size: 28px;
|
|
|
- font-weight: 700;
|
|
|
- color: #fff;
|
|
|
- line-height: 1.2;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-sub {
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 400;
|
|
|
- color: rgba(255, 255, 255, 0.4);
|
|
|
- margin-left: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.stat-compare {
|
|
|
- font-size: 12px;
|
|
|
- color: rgba(255, 255, 255, 0.4);
|
|
|
- margin-top: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.range-bar {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 12px;
|
|
|
- color: rgba(255, 255, 255, 0.65);
|
|
|
-}
|
|
|
-
|
|
|
-.range-label {
|
|
|
- font-size: 13px;
|
|
|
-}
|
|
|
-
|
|
|
-.trend-up {
|
|
|
- color: #f56c6c;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.trend-down {
|
|
|
- color: #67c23a;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 图表区域 ==================== */
|
|
|
-.chart-section {
|
|
|
- display: flex;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.chart-left {
|
|
|
- flex: 1;
|
|
|
- background: rgba(255, 255, 255, 0.05);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.chart-right {
|
|
|
- width: 340px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.pie-box {
|
|
|
- flex: 1;
|
|
|
- background: rgba(255, 255, 255, 0.05);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.section-title {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 600;
|
|
|
- color: rgba(255, 255, 255, 0.85);
|
|
|
- margin-bottom: 8px;
|
|
|
- padding-left: 10px;
|
|
|
- border-left: 3px solid #409eff;
|
|
|
- line-height: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.trend-chart {
|
|
|
- width: 100%;
|
|
|
- height: 260px;
|
|
|
-}
|
|
|
-
|
|
|
-.pie-chart {
|
|
|
- width: 100%;
|
|
|
- height: 170px;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 表格区域 ==================== */
|
|
|
-.table-section {
|
|
|
- background: rgba(255, 255, 255, 0.05);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 深色主题表格覆盖 */
|
|
|
-.bjyp-page :deep(.el-table) {
|
|
|
- background: transparent;
|
|
|
- color: rgba(255, 255, 255, 0.85);
|
|
|
- --el-table-border-color: rgba(255, 255, 255, 0.08);
|
|
|
- --el-table-header-bg-color: rgba(255, 255, 255, 0.06);
|
|
|
- --el-table-row-hover-bg-color: rgba(255, 255, 255, 0.04);
|
|
|
- --el-table-bg-color: transparent;
|
|
|
- --el-table-tr-bg-color: transparent;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table th.el-table__cell) {
|
|
|
- background: rgba(255, 255, 255, 0.06) !important;
|
|
|
- color: rgba(255, 255, 255, 0.7);
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table .el-table__cell) {
|
|
|
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table--border .el-table__cell) {
|
|
|
- border-right: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table tr) {
|
|
|
- background: transparent;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table__body tr:hover > td.el-table__cell) {
|
|
|
- background: rgba(255, 255, 255, 0.04) !important;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table__inner-wrapper::before) {
|
|
|
- background: rgba(255, 255, 255, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table--border::after) {
|
|
|
- background: rgba(255, 255, 255, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-table__empty-text) {
|
|
|
- color: rgba(255, 255, 255, 0.4);
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 分页 ==================== */
|
|
|
-.pagination-wrap {
|
|
|
- margin-top: 12px;
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination) {
|
|
|
- --el-pagination-bg-color: transparent;
|
|
|
- --el-pagination-text-color: rgba(255, 255, 255, 0.7);
|
|
|
- --el-pagination-button-color: rgba(255, 255, 255, 0.7);
|
|
|
- --el-pagination-button-bg-color: transparent;
|
|
|
- --el-pagination-hover-color: #409eff;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination .el-pager li) {
|
|
|
- background: transparent;
|
|
|
- color: rgba(255, 255, 255, 0.7);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination .el-pager li.is-active) {
|
|
|
- background: #409eff;
|
|
|
- color: #fff;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination .btn-prev),
|
|
|
-.bjyp-page :deep(.el-pagination .btn-next) {
|
|
|
- background: transparent;
|
|
|
- color: rgba(255, 255, 255, 0.7);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination .el-select .el-input__wrapper) {
|
|
|
- background: rgba(255, 255, 255, 0.08);
|
|
|
- box-shadow: none;
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination .el-input__inner) {
|
|
|
- color: rgba(255, 255, 255, 0.7);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination__total) {
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
-}
|
|
|
-
|
|
|
-.bjyp-page :deep(.el-pagination__jump) {
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 弹窗内部样式 ==================== */
|
|
|
-.analysis-header {
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.analysis-conclusion-section {
|
|
|
- margin-top: 16px;
|
|
|
- border-top: 1px solid #e4e7ed;
|
|
|
- padding-top: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.conclusion-title {
|
|
|
- font-size: 15px;
|
|
|
- font-weight: bold;
|
|
|
- margin-bottom: 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* 等级彩色文字 */
|
|
|
-.level-critical {
|
|
|
- color: #f56c6c;
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
-
|
|
|
-.level-major {
|
|
|
- color: #e6a23c;
|
|
|
- font-weight: 700;
|
|
|
-}
|
|
|
-
|
|
|
-.level-minor {
|
|
|
- color: #409eff;
|
|
|
- font-weight: 600;
|
|
|
-}</style>
|