|
@@ -1,621 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div class="app-container">
|
|
|
|
|
- <!-- 查询条件 -->
|
|
|
|
|
- <el-form ref="queryRef" :inline="true" :model="queryParams" v-show="showSearch" label-width="80px">
|
|
|
|
|
- <el-form-item label="关键字" prop="searchKey">
|
|
|
|
|
- <el-input v-model="queryParams.searchKey" placeholder="设备名称/编号/地址" clearable style="width: 220px" @keyup.enter="handleQuery" />
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="异常等级" prop="level">
|
|
|
|
|
- <el-select v-model="queryParams.level" placeholder="全部" clearable style="width: 160px">
|
|
|
|
|
- <el-option label="严重" value="严重" />
|
|
|
|
|
- <el-option label="一般" value="一般" />
|
|
|
|
|
- <el-option label="轻微" value="轻微" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="设备类型" prop="type">
|
|
|
|
|
- <el-select v-model="queryParams.type" placeholder="全部" clearable style="width: 160px">
|
|
|
|
|
- <el-option label="阀门" value="阀门" />
|
|
|
|
|
- <el-option label="调压器" value="调压器" />
|
|
|
|
|
- <el-option label="流量计" value="流量计" />
|
|
|
|
|
- <el-option label="窨井盖" value="窨井盖" />
|
|
|
|
|
- <el-option label="终端节点" value="终端节点" />
|
|
|
|
|
- <el-option label="检查井" value="检查井" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="处理状态" prop="status">
|
|
|
|
|
- <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 160px">
|
|
|
|
|
- <el-option label="待处理" value="待处理" />
|
|
|
|
|
- <el-option label="处理中" value="处理中" />
|
|
|
|
|
- <el-option label="已修复" value="已修复" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item>
|
|
|
|
|
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
|
|
- <el-button icon="Refresh" @click="handleReset">重置</el-button>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-form>
|
|
|
|
|
-
|
|
|
|
|
- <el-row :gutter="10" class="mb8">
|
|
|
|
|
- <right-toolbar v-model:showSearch="showSearch" @queryTable="loadList" />
|
|
|
|
|
- </el-row>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 统计卡片 -->
|
|
|
|
|
- <div class="stats-row">
|
|
|
|
|
- <div class="stat-card stat-total">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><WarningFilled /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">异常设备总数</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.total }} <small>台</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-card stat-today">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><Clock /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">今日新增</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.today }} <small>台</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-card stat-rate">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><TrendCharts /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">异常占比</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.rate }} <small>%</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-card stat-processed">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><CircleCheckFilled /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">已修复</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.processed }} <small>台</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-card stat-pending">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><Warning /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">待处理</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.pending }} <small>台</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat-card stat-process-rate">
|
|
|
|
|
- <div class="stat-icon"><el-icon size="22"><DataAnalysis /></el-icon></div>
|
|
|
|
|
- <div class="stat-info">
|
|
|
|
|
- <div class="stat-label">修复率</div>
|
|
|
|
|
- <div class="stat-value">{{ stats.processRate }} <small>%</small></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 趋势图 -->
|
|
|
|
|
- <el-card shadow="never" class="chart-card">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="chart-header">
|
|
|
|
|
- <span class="chart-title">设备异常趋势图</span>
|
|
|
|
|
- <span class="chart-subtitle">近15天运行数据</span>
|
|
|
|
|
- <div class="chart-legend-custom">
|
|
|
|
|
- <span class="legend-tag tag-total">异常总数</span>
|
|
|
|
|
- <span class="legend-tag tag-new">新增异常</span>
|
|
|
|
|
- <span class="legend-tag tag-fixed">已修复</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div id="trendChart" style="height: 340px"></div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 双图并排 -->
|
|
|
|
|
- <div class="charts-row">
|
|
|
|
|
- <el-card shadow="never" class="chart-card">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="chart-header"><span class="chart-title">异常类型分布</span></div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div id="typeChart" style="height: 290px"></div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- <el-card shadow="never" class="chart-card">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="chart-header"><span class="chart-title">设备异常等级与处理状态</span></div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div id="levelChart" style="height: 290px"></div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 异常设备列表 -->
|
|
|
|
|
- <el-card shadow="never" class="chart-card">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="chart-header">
|
|
|
|
|
- <span class="chart-title">异常设备列表</span>
|
|
|
|
|
- <span class="chart-subtitle">共 {{ tableTotal }} 条记录</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <el-table
|
|
|
|
|
- :data="paginatedData"
|
|
|
|
|
- v-loading="loading"
|
|
|
|
|
- @sort-change="handleSortChange"
|
|
|
|
|
- :default-sort="{ prop: 'alarmTime', order: 'descending' }"
|
|
|
|
|
- >
|
|
|
|
|
- <el-table-column prop="id" label="序号" width="70" align="center" sortable="custom" />
|
|
|
|
|
- <el-table-column prop="deviceCode" label="设备编号" min-width="140" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="deviceName" label="设备名称" min-width="150" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="deviceType" label="设备类型" width="110" />
|
|
|
|
|
- <el-table-column prop="level" label="异常等级" width="100" align="center" sortable="custom">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="alarmDesc" label="异常描述" min-width="220" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="address" label="安装地址" min-width="200" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="alarmTime" label="异常时间" width="168" sortable="custom" />
|
|
|
|
|
- <el-table-column prop="processStatus" label="处理状态" width="100" align="center" sortable="custom">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-tag :type="row.processStatus === '已修复' ? 'success' : row.processStatus === '处理中' ? 'warning' : 'danger'" size="small">
|
|
|
|
|
- {{ row.processStatus }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column label="操作" width="160" fixed="right" align="center" class-name="small-padding fixed-width">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
|
|
|
|
|
- <el-button v-if="row.processStatus === '待处理'" link type="primary" icon="Edit" @click="handleProcess(row)">处理</el-button>
|
|
|
|
|
- <el-button v-if="row.processStatus === '处理中'" link type="success" icon="Select" @click="handleComplete(row)">完成</el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- </el-table>
|
|
|
|
|
-
|
|
|
|
|
- <pagination
|
|
|
|
|
- v-show="tableTotal > 0"
|
|
|
|
|
- :total="tableTotal"
|
|
|
|
|
- v-model:page="currentPage"
|
|
|
|
|
- v-model:limit="pageSize"
|
|
|
|
|
- :page-sizes="[10, 20]"
|
|
|
|
|
- @pagination="loadList"
|
|
|
|
|
- />
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup>
|
|
|
|
|
-import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
-import * as echarts from 'echarts'
|
|
|
|
|
-import { Clock, CircleCheckFilled, DataAnalysis, TrendCharts, Warning, WarningFilled } from '@element-plus/icons-vue'
|
|
|
|
|
-import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
-import { getEquipmentAbnormalList, getEquipmentAbnormalStats } from '@/api/pipeNetwork/gasMonitor'
|
|
|
|
|
-
|
|
|
|
|
-defineOptions({ name: 'EquipmentAbnormal' })
|
|
|
|
|
-
|
|
|
|
|
-const loading = ref(false)
|
|
|
|
|
-const showSearch = ref(true)
|
|
|
|
|
-const queryRef = ref(null)
|
|
|
|
|
-const queryParams = reactive({ searchKey: '', level: '', type: '', status: '' })
|
|
|
|
|
-const currentPage = ref(1)
|
|
|
|
|
-const pageSize = ref(10)
|
|
|
|
|
-const sortProp = ref('alarmTime')
|
|
|
|
|
-const sortOrder = ref('descending')
|
|
|
|
|
-
|
|
|
|
|
-let trendChart = null
|
|
|
|
|
-let typeChart = null
|
|
|
|
|
-let levelChart = null
|
|
|
|
|
-
|
|
|
|
|
-// ==================== 统计数据(从API加载) ====================
|
|
|
|
|
-const stats = ref({
|
|
|
|
|
- total: 0, today: 0, rate: 0, processed: 0, pending: 0,
|
|
|
|
|
- processRate: 0, critical: 0, normal: 0, minor: 0
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-async function loadStats() {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await getEquipmentAbnormalStats()
|
|
|
|
|
- if (res.data) {
|
|
|
|
|
- const d = res.data
|
|
|
|
|
- stats.value = {
|
|
|
|
|
- total: d.total || 0, today: d.today || 0, rate: d.rate || 0,
|
|
|
|
|
- processed: d.processed || 0, pending: d.pending || 0,
|
|
|
|
|
- processRate: d.processRate || 0, critical: d.critical || 0,
|
|
|
|
|
- normal: d.normal || 0, minor: d.minor || 0
|
|
|
|
|
- }
|
|
|
|
|
- // 图表数据
|
|
|
|
|
- chartData.value = {
|
|
|
|
|
- trend: d.trend || { dates: [], totalData: [], newData: [], fixedData: [] },
|
|
|
|
|
- typeDistribution: d.typeDistribution || { categories: [], values: [] },
|
|
|
|
|
- levelStatus: d.levelStatus || { levels: [], pending: [], processing: [], fixed: [] }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) { console.error(e) }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ==================== 图表数据(从API动态加载) ====================
|
|
|
|
|
-const chartData = ref({
|
|
|
|
|
- trend: { dates: [], totalData: [], newData: [], fixedData: [] },
|
|
|
|
|
- typeDistribution: { categories: [], values: [] },
|
|
|
|
|
- levelStatus: { levels: [], pending: [], processing: [], fixed: [] }
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-// ==================== 表格数据(从API加载) ====================
|
|
|
|
|
-const tableData = ref([])
|
|
|
|
|
-const tableTotal = ref(0)
|
|
|
|
|
-
|
|
|
|
|
-async function loadList() {
|
|
|
|
|
- loading.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await getEquipmentAbnormalList({
|
|
|
|
|
- pageNum: currentPage.value,
|
|
|
|
|
- pageSize: pageSize.value,
|
|
|
|
|
- searchKey: queryParams.searchKey || undefined,
|
|
|
|
|
- level: queryParams.level || undefined,
|
|
|
|
|
- type: queryParams.type || undefined,
|
|
|
|
|
- status: queryParams.status || undefined
|
|
|
|
|
- })
|
|
|
|
|
- if (res.data) {
|
|
|
|
|
- tableData.value = (res.data.rows || []).map((r, i) => ({
|
|
|
|
|
- ...r,
|
|
|
|
|
- id: String((currentPage.value - 1) * pageSize.value + i + 1),
|
|
|
|
|
- level: r.levelText || r.faultLevel,
|
|
|
|
|
- processStatus: r.processStatus || '待处理',
|
|
|
|
|
- alarmTime: r.alarmTime || '',
|
|
|
|
|
- alarmDesc: r.alarmDesc || '',
|
|
|
|
|
- deviceCode: r.deviceCode || '',
|
|
|
|
|
- deviceName: r.deviceName || '',
|
|
|
|
|
- deviceType: r.deviceType || '',
|
|
|
|
|
- address: r.address || ''
|
|
|
|
|
- }))
|
|
|
|
|
- tableTotal.value = res.data.total || 0
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) { console.error(e) } finally { loading.value = false }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const paginatedData = computed(() => {
|
|
|
|
|
- const list = [...tableData.value]
|
|
|
|
|
- if (sortProp.value) {
|
|
|
|
|
- list.sort((a, b) => {
|
|
|
|
|
- const va = a[sortProp.value] || ''
|
|
|
|
|
- const vb = b[sortProp.value] || ''
|
|
|
|
|
- const cmp = typeof va === 'string' ? va.localeCompare(vb) : va - vb
|
|
|
|
|
- return sortOrder.value === 'descending' ? -cmp : cmp
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- return list
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-// 排序切换
|
|
|
|
|
-function handleSortChange({ prop, order }) {
|
|
|
|
|
- sortProp.value = prop
|
|
|
|
|
- sortOrder.value = order || 'descending'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ==================== 工具函数 ====================
|
|
|
|
|
-function levelTagType(level) {
|
|
|
|
|
- if (level === '严重') return 'danger'
|
|
|
|
|
- if (level === '一般') return 'warning'
|
|
|
|
|
- return 'info'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleQuery() {
|
|
|
|
|
- currentPage.value = 1
|
|
|
|
|
- loadList()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleReset() {
|
|
|
|
|
- queryRef.value?.resetFields()
|
|
|
|
|
- Object.assign(queryParams, { searchKey: '', level: '', type: '', status: '' })
|
|
|
|
|
- currentPage.value = 1
|
|
|
|
|
- loadList()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleDetail(row) {
|
|
|
|
|
- ElMessageBox.alert(
|
|
|
|
|
- `<div style="line-height:2.2;font-size:14px;">
|
|
|
|
|
- <p><b>设备编号:</b>${row.deviceCode}</p>
|
|
|
|
|
- <p><b>设备名称:</b>${row.deviceName}</p>
|
|
|
|
|
- <p><b>设备类型:</b>${row.deviceType}</p>
|
|
|
|
|
- <p><b>异常等级:</b><span style="color:${row.level === '严重' ? '#e63946' : row.level === '一般' ? '#f77f00' : '#909399'}">${row.level}</span></p>
|
|
|
|
|
- <p><b>异常描述:</b>${row.alarmDesc}</p>
|
|
|
|
|
- <p><b>安装地址:</b>${row.address}</p>
|
|
|
|
|
- <p><b>异常时间:</b>${row.alarmTime}</p>
|
|
|
|
|
- <p><b>处理状态:</b>${row.processStatus}</p>
|
|
|
|
|
- </div>`,
|
|
|
|
|
- '设备异常详情',
|
|
|
|
|
- { dangerouslyUseHTMLString: true, confirmButtonText: '关闭', customClass: 'detail-dialog' }
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleProcess(row) {
|
|
|
|
|
- ElMessageBox.confirm(`确认开始处理设备「${row.deviceName}」的异常?系统将自动派发工单至运维班组。`, '处理确认', {
|
|
|
|
|
- confirmButtonText: '确认处理',
|
|
|
|
|
- cancelButtonText: '取消',
|
|
|
|
|
- type: 'warning'
|
|
|
|
|
- }).then(() => {
|
|
|
|
|
- row.processStatus = '处理中'
|
|
|
|
|
- ElMessage.success(`已派发工单,设备「${row.deviceName}」处理中`)
|
|
|
|
|
- }).catch(() => {})
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleComplete(row) {
|
|
|
|
|
- ElMessageBox.confirm(`确认设备「${row.deviceName}」的异常已修复完成?`, '修复确认', {
|
|
|
|
|
- confirmButtonText: '确认修复',
|
|
|
|
|
- cancelButtonText: '取消',
|
|
|
|
|
- type: 'success'
|
|
|
|
|
- }).then(() => {
|
|
|
|
|
- row.processStatus = '已修复'
|
|
|
|
|
- ElMessage.success(`设备「${row.deviceName}」异常已修复`)
|
|
|
|
|
- }).catch(() => {})
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ==================== ECharts ====================
|
|
|
|
|
-function initTrendChart() {
|
|
|
|
|
- const dom = document.getElementById('trendChart')
|
|
|
|
|
- if (!dom) return
|
|
|
|
|
- if (trendChart) trendChart.dispose()
|
|
|
|
|
- trendChart = echarts.init(dom)
|
|
|
|
|
- const t = chartData.value.trend
|
|
|
|
|
- trendChart.setOption({
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- trigger: 'axis',
|
|
|
|
|
- backgroundColor: 'rgba(255,255,255,0.96)',
|
|
|
|
|
- borderColor: '#e0e4ea',
|
|
|
|
|
- textStyle: { color: '#2c3e50', fontSize: 13 }
|
|
|
|
|
- },
|
|
|
|
|
- legend: {
|
|
|
|
|
- data: ['异常总数', '新增异常', '已修复'],
|
|
|
|
|
- top: 0,
|
|
|
|
|
- textStyle: { color: '#606a78', fontSize: 12 }
|
|
|
|
|
- },
|
|
|
|
|
- grid: { left: 52, right: 44, top: 44, bottom: 32 },
|
|
|
|
|
- xAxis: {
|
|
|
|
|
- type: 'category',
|
|
|
|
|
- data: t.dates || [],
|
|
|
|
|
- boundaryGap: false,
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 12 },
|
|
|
|
|
- axisLine: { lineStyle: { color: '#d0d5dd' } }
|
|
|
|
|
- },
|
|
|
|
|
- yAxis: {
|
|
|
|
|
- type: 'value',
|
|
|
|
|
- name: '数量(台)',
|
|
|
|
|
- nameTextStyle: { color: '#9098a6', fontSize: 12 },
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 12 },
|
|
|
|
|
- splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } }
|
|
|
|
|
- },
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- name: '异常总数', type: 'line',
|
|
|
|
|
- data: t.totalData || [], smooth: true, symbol: 'circle', symbolSize: 6,
|
|
|
|
|
- lineStyle: { width: 3, color: '#e63946' },
|
|
|
|
|
- itemStyle: { color: '#e63946', borderColor: '#fff', borderWidth: 2 },
|
|
|
|
|
- areaStyle: {
|
|
|
|
|
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
- { offset: 0, color: 'rgba(230,57,70,0.15)' },
|
|
|
|
|
- { offset: 1, color: 'rgba(230,57,70,0.0)' }
|
|
|
|
|
- ])
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: '新增异常', type: 'line',
|
|
|
|
|
- data: t.newData || [], smooth: true, symbol: 'emptyCircle', symbolSize: 6,
|
|
|
|
|
- lineStyle: { width: 2.5, color: '#f77f00' },
|
|
|
|
|
- itemStyle: { color: '#f77f00', borderColor: '#f77f00', borderWidth: 2 }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: '已修复', type: 'line',
|
|
|
|
|
- data: t.fixedData || [], smooth: true, symbol: 'diamond', symbolSize: 7,
|
|
|
|
|
- lineStyle: { width: 2.5, color: '#2a9d8f', type: 'dashed' },
|
|
|
|
|
- itemStyle: { color: '#2a9d8f', borderColor: '#fff', borderWidth: 2 }
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function initTypeChart() {
|
|
|
|
|
- const dom = document.getElementById('typeChart')
|
|
|
|
|
- if (!dom) return
|
|
|
|
|
- if (typeChart) typeChart.dispose()
|
|
|
|
|
- typeChart = echarts.init(dom)
|
|
|
|
|
- const d = chartData.value.typeDistribution
|
|
|
|
|
- const colors = ['#457b9d', '#e76f51', '#e63946']
|
|
|
|
|
- typeChart.setOption({
|
|
|
|
|
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
|
|
|
- grid: { left: 46, right: 20, top: 16, bottom: 32 },
|
|
|
|
|
- xAxis: {
|
|
|
|
|
- type: 'category',
|
|
|
|
|
- data: d.categories || [],
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 11, rotate: 15 },
|
|
|
|
|
- axisLine: { lineStyle: { color: '#d0d5dd' } }
|
|
|
|
|
- },
|
|
|
|
|
- yAxis: {
|
|
|
|
|
- type: 'value', name: '数量(台)',
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 11 },
|
|
|
|
|
- splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } }
|
|
|
|
|
- },
|
|
|
|
|
- series: [{
|
|
|
|
|
- type: 'bar',
|
|
|
|
|
- data: (d.values || []).map((v, i) => ({ value: v, itemStyle: { color: colors[i] || '#6c757d', borderRadius: [4, 4, 0, 0] } })),
|
|
|
|
|
- barWidth: 28,
|
|
|
|
|
- label: { show: true, position: 'top', color: '#4e5969', fontSize: 12, fontWeight: 600 }
|
|
|
|
|
- }]
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function initLevelChart() {
|
|
|
|
|
- const dom = document.getElementById('levelChart')
|
|
|
|
|
- if (!dom) return
|
|
|
|
|
- if (levelChart) levelChart.dispose()
|
|
|
|
|
- levelChart = echarts.init(dom)
|
|
|
|
|
- const d = chartData.value.levelStatus
|
|
|
|
|
- levelChart.setOption({
|
|
|
|
|
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
|
|
|
- legend: {
|
|
|
|
|
- data: ['待处理', '处理中', '已修复'],
|
|
|
|
|
- top: 0,
|
|
|
|
|
- textStyle: { color: '#606a78', fontSize: 12 }
|
|
|
|
|
- },
|
|
|
|
|
- grid: { left: 46, right: 20, top: 44, bottom: 28 },
|
|
|
|
|
- xAxis: {
|
|
|
|
|
- type: 'category',
|
|
|
|
|
- data: d.levels || [],
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 12 },
|
|
|
|
|
- axisLine: { lineStyle: { color: '#d0d5dd' } }
|
|
|
|
|
- },
|
|
|
|
|
- yAxis: {
|
|
|
|
|
- type: 'value', name: '数量(台)',
|
|
|
|
|
- axisLabel: { color: '#9098a6', fontSize: 11 },
|
|
|
|
|
- splitLine: { lineStyle: { color: '#f0f2f6', type: 'dashed' } },
|
|
|
|
|
- nameTextStyle: { color: '#9098a6', fontSize: 11 }
|
|
|
|
|
- },
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- name: '待处理', type: 'bar',
|
|
|
|
|
- data: d.pending || [], barWidth: 30, barGap: '20%',
|
|
|
|
|
- itemStyle: { color: '#e63946', borderRadius: [4, 4, 0, 0] }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: '处理中', type: 'bar',
|
|
|
|
|
- data: d.processing || [], barWidth: 30,
|
|
|
|
|
- itemStyle: { color: '#f77f00', borderRadius: [4, 4, 0, 0] }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: '已修复', type: 'bar',
|
|
|
|
|
- data: d.fixed || [], barWidth: 30,
|
|
|
|
|
- itemStyle: { color: '#2a9d8f', borderRadius: [4, 4, 0, 0] }
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function initCharts() {
|
|
|
|
|
- try { initTrendChart() } catch (e) { console.warn('趋势图初始化失败:', e) }
|
|
|
|
|
- try { initTypeChart() } catch (e) { console.warn('类型分布图初始化失败:', e) }
|
|
|
|
|
- try { initLevelChart() } catch (e) { console.warn('等级统计图初始化失败:', e) }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function resizeCharts() {
|
|
|
|
|
- trendChart?.resize()
|
|
|
|
|
- typeChart?.resize()
|
|
|
|
|
- levelChart?.resize()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-onMounted(async () => {
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- // 先加载数据,再初始化图表
|
|
|
|
|
- await Promise.all([loadStats(), loadList()])
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- initCharts()
|
|
|
|
|
- window.addEventListener('resize', resizeCharts)
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-onBeforeUnmount(() => {
|
|
|
|
|
- window.removeEventListener('resize', resizeCharts)
|
|
|
|
|
- trendChart?.dispose()
|
|
|
|
|
- typeChart?.dispose()
|
|
|
|
|
- levelChart?.dispose()
|
|
|
|
|
-})
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-/* 统计卡片 */
|
|
|
|
|
-.stats-row {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(6, 1fr);
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-card {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
- padding: 14px 16px;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border: 1px solid #ebeef5;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-icon {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- width: 44px;
|
|
|
|
|
- height: 44px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-total .stat-icon { background: #fef0f0; color: #e63946; }
|
|
|
|
|
-.stat-today .stat-icon { background: #fff3e6; color: #f77f00; }
|
|
|
|
|
-.stat-rate .stat-icon { background: #fef5e7; color: #e76f51; }
|
|
|
|
|
-.stat-processed .stat-icon { background: #e8f8f5; color: #2a9d8f; }
|
|
|
|
|
-.stat-pending .stat-icon { background: #eaf2f8; color: #457b9d; }
|
|
|
|
|
-.stat-process-rate .stat-icon { background: #e8f5e9; color: #2d6a4f; }
|
|
|
|
|
-
|
|
|
|
|
-.stat-label {
|
|
|
|
|
- margin-bottom: 4px;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #9098a6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-value {
|
|
|
|
|
- font-size: 24px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- line-height: 1.1;
|
|
|
|
|
- color: #1f2d3d;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-value small {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- font-weight: 400;
|
|
|
|
|
- color: #9098a6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 图表 / 列表卡片 */
|
|
|
|
|
-.chart-card {
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.charts-row {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
|
|
- gap: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.chart-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.chart-title {
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: #1f2d3d;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.chart-subtitle {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #a0a9b6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.chart-legend-custom {
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.legend-tag {
|
|
|
|
|
- padding: 2px 10px;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.tag-total { background: #e63946; }
|
|
|
|
|
-.tag-new { background: #f77f00; }
|
|
|
|
|
-.tag-fixed { background: #2a9d8f; }
|
|
|
|
|
-
|
|
|
|
|
-/* 响应式 */
|
|
|
|
|
-@media (max-width: 1600px) {
|
|
|
|
|
- .stats-row { grid-template-columns: repeat(3, 1fr); }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-@media (max-width: 1400px) {
|
|
|
|
|
- .charts-row { grid-template-columns: 1fr; }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-@media (max-width: 768px) {
|
|
|
|
|
- .stats-row { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|