Files
lc_frontend/src/views/Home/Index12.vue

1944 lines
52 KiB
Vue
Raw Normal View History

2025-11-26 14:13:02 +08:00
<template>
2025-11-27 16:07:54 +08:00
<div class="dashboard-container">
<!-- 顶部标题栏 -->
<div class="header-container">
<div class="header-left">
2025-11-27 18:12:53 +08:00
<el-icon class="back-arrow" @click="returnToHeadquarters">
<ArrowLeft />
</el-icon>
2025-11-29 14:44:18 +08:00
<div class="region-name-clickable" @click="openParkSelector">
{{ selectedRegion }}
2025-11-27 16:07:54 +08:00
<span>···</span>
2025-11-26 14:13:02 +08:00
</div>
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<h1 class="header-title">区域视角数据看板</h1>
<div class="header-right">
<div class="date-range-wrapper">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" @change="handleDateChange" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<el-button type="primary" :icon="Refresh" @click="refreshData" class="refresh-btn">
刷新数据
</el-button>
2025-11-29 14:44:18 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 主内容区 - 6个卡片 -->
<div class="content-container">
<!-- 第一行外协管理风险管理隐患管理 -->
<div class="card-row">
<!-- 外协管理卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon">👥</span>
外协管理
2025-11-26 14:13:02 +08:00
</div>
2025-11-29 15:20:48 +08:00
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="donut-chart-wrapper">
<Echart :options="outsourcingChartOption" width="100%" height="200px" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="region-distribution">
<div class="distribution-title">园区分布</div>
<div class="distribution-list">
<div class="distribution-item" v-for="item in outsourcingDistribution" :key="item.region">
<span class="dot" :style="{ backgroundColor: item.color }"></span>
<span class="region-name">{{ item.region }}</span>
<span class="region-count">{{ item.count }}</span>
<span class="region-percent">({{ item.percent }})</span>
2025-11-29 14:44:18 +08:00
</div>
</div>
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 风险管理卡片 -->
<div class="dashboard-card">
<div class="card-header">
2025-11-29 15:20:48 +08:00
<div class="card-title">
<span class="card-icon">🛡</span>
风险管理
</div>
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="donut-chart-wrapper">
<Echart :options="riskChartOption" width="100%" height="200px" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="risk-distribution-table">
<div class="distribution-title">园区风险分布</div>
<div class="table-wrapper">
<table class="risk-table">
<thead>
<tr>
<th>园区</th>
<th></th>
<th>一般</th>
<th>较大</th>
<th>重大</th>
</tr>
</thead>
<tbody>
<tr v-for="item in parkRiskDistribution" :key="item.park">
<td>{{ item.park }}</td>
<td>{{ item.low || '' }}</td>
<td>{{ item.general }}</td>
<td>{{ item.moderate }}</td>
<td>{{ item.major }}</td>
</tr>
</tbody>
</table>
2025-11-29 14:44:18 +08:00
</div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-29 14:44:18 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 隐患管理卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon warning"></span>
隐患管理
2025-11-29 14:44:18 +08:00
</div>
2025-11-29 15:20:48 +08:00
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="line-chart-wrapper">
<Echart :options="hiddenDangerChartOption" width="100%" height="180px" />
2025-11-29 14:44:18 +08:00
</div>
<div class="rectification-status-grid">
2025-11-27 16:07:54 +08:00
<div class="distribution-title">园区整改状态</div>
2025-11-29 14:44:18 +08:00
<div class="grid-wrapper">
<!-- 第一列园区名称 -->
<div class="grid-column">
<div class="grid-header empty-header"></div>
<div class="grid-park-name" v-for="item in parkRectificationStatus" :key="'park-' + item.park">
{{ item.park }}
</div>
</div>
<!-- 已逾期列 -->
<div class="grid-column">
<div class="grid-header status-overdue">已逾期</div>
<div class="grid-number status-overdue" v-for="item in parkRectificationStatus" :key="'overdue-' + item.park">
{{ item.overdue }}
</div>
</div>
<!-- 处理中列 -->
<div class="grid-column">
<div class="grid-header status-processing">处理中</div>
<div class="grid-number status-processing" v-for="item in parkRectificationStatus" :key="'processing-' + item.park">
{{ item.processing }}
</div>
</div>
<!-- 已处理列 -->
<div class="grid-column">
<div class="grid-header status-processed">已处理</div>
<div class="grid-number status-processed" v-for="item in parkRectificationStatus" :key="'processed-' + item.park">
{{ item.processed }}
</div>
</div>
</div>
</div>
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 第二行高危作业应急预案安全培训 -->
<div class="card-row">
<!-- 高危作业卡片 -->
<div class="dashboard-card">
<div class="card-header">
2025-11-29 15:20:48 +08:00
<div class="card-title">
<span class="card-icon">🚧</span>
高危作业
</div>
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
2025-11-27 18:12:53 +08:00
<div class="high-risk-top">
2025-11-27 16:07:54 +08:00
<div class="donut-chart-wrapper-small">
2025-11-29 15:20:48 +08:00
<Echart :options="highRiskChartOption" width="100%" height="280px" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="operation-type-list">
<div class="operation-type-item" v-for="item in operationTypeDistribution" :key="item.type">
<span class="dot" :style="{ backgroundColor: item.color }"></span>
<span class="operation-name">{{ item.type }}</span>
<div class="operation-bar-wrapper">
<div class="operation-bar" :style="{ width: item.percent, backgroundColor: item.color }"></div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-29 14:44:18 +08:00
<span class="operation-percent">{{ item.count }}</span>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<div class="park-operation-distribution">
<div class="distribution-title">园区作业分布</div>
<div class="distribution-list">
<div class="distribution-item" v-for="item in parkOperationDistribution" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<span class="region-count">{{ item.count }}</span>
2025-11-29 14:44:18 +08:00
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
</div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<!-- 应急预案卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon">📄</span>
应急预案
2025-11-29 14:44:18 +08:00
</div>
2025-11-29 15:20:48 +08:00
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
2025-11-27 18:12:53 +08:00
<div class="emergency-plan-top">
2025-11-27 16:07:54 +08:00
<div class="progress-chart-wrapper">
2025-11-29 15:20:48 +08:00
<Echart :options="emergencyPlanChartOption" width="100%" height="280px" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="drill-info">
<div class="drill-item">
<span>应完成演练</span>
<span class="drill-number">{{ emergencyPlanTotal }}</span>
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="drill-item">
<span>已完成演练</span>
<span class="drill-number">{{ emergencyPlanCompleted }}</span>
2025-11-26 14:13:02 +08:00
</div>
2025-11-29 14:44:18 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<div class="regional-progress">
<div class="distribution-title">园区演练完成率</div>
<div class="progress-list">
<div class="progress-item" v-for="item in parkDrillProgress" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<div class="progress-bar-wrapper">
2025-11-29 14:44:18 +08:00
<div class="progress-bar" :style="{ width: item.percent, background: 'linear-gradient(90deg, #10b981 0%, #34d399 100%)' }"></div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<span class="progress-percent">{{ item.percent }}</span>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-29 14:44:18 +08:00
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 安全培训卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon">📚</span>
安全培训
2025-11-29 14:44:18 +08:00
</div>
2025-11-29 15:20:48 +08:00
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="bar-chart-wrapper">
<Echart :options="safetyTrainingChartOption" width="100%" height="180px" />
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="regional-progress">
<div class="distribution-title">园区培训完成率</div>
<div class="progress-list">
<div class="progress-item" v-for="item in parkTrainingProgress" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<div class="progress-bar-wrapper">
2025-11-29 14:44:18 +08:00
<div class="progress-bar" :style="{ width: item.percent }"></div>
</div>
2025-11-27 16:07:54 +08:00
<span class="progress-percent">{{ item.percent }}</span>
2025-11-29 14:44:18 +08:00
</div>
</div>
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
</div>
2025-11-29 14:44:18 +08:00
</div>
2025-11-27 16:07:54 +08:00
2025-11-29 14:44:18 +08:00
<!-- 园区选择弹窗 -->
<RegionSelector v-model="parkSelectorVisible" :modelSelected="selectedPark" :regions="parkOption"
@change="onParkChange" />
2025-11-26 14:13:02 +08:00
</div>
</template>
2025-11-27 16:07:54 +08:00
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
2025-11-27 18:12:53 +08:00
import { Refresh, ArrowLeft } from '@element-plus/icons-vue'
2025-11-27 16:07:54 +08:00
import Echart from '@/components/Echart/src/Echart.vue'
import type { EChartsOption } from 'echarts'
import dayjs from 'dayjs'
2025-11-29 14:44:18 +08:00
import {
getOutsourcingManagementDataRegion,
getHighRiskManagementDataRegion,
getEmergencyPlanManagementDataRegion,
getTrainingManagementDataRegion,
getRiskManagementDataRegion,
getHiddenDangerManagementDataRegion,
getHiddenDangerManagementDataRegionWeek,
getHiddenDangerManagementDataRegionMonth
} from '@/api'
import RegionSelector from '@/views/screen/components/RegionSelector.vue'
import { getTableList } from '@/api/design/report'
2025-11-27 16:07:54 +08:00
defineOptions({ name: 'Home12' })
// 类型定义
2025-11-29 14:44:18 +08:00
interface ParkItem {
2025-11-27 16:07:54 +08:00
name: string
code: string
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
interface DistributionItem {
region?: string
park?: string
2025-11-27 18:12:53 +08:00
level?: string
type?: string
2025-11-27 16:07:54 +08:00
count: number
percent?: string
color: string
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
interface ParkRiskItem {
park: string
low: number | string
general: number
moderate: number
major: number
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
interface ParkRectificationItem {
park: string
overdue: number
processing: number
processed: number
}
const router = useRouter()
const route = useRoute()
2025-11-29 14:44:18 +08:00
// 区域和园区选择相关
2025-11-27 16:07:54 +08:00
const selectedRegion = ref<string>('')
const selectedPark = ref<string>('')
2025-11-29 14:44:18 +08:00
const parkSelectorVisible = ref<boolean>(false)
const parkOption = ref<ParkItem[]>([])
2025-11-27 16:07:54 +08:00
2025-11-29 14:44:18 +08:00
// 时间选择相关 - 默认当前月起止,如果路由中有日期范围参数则使用
2025-11-27 16:07:54 +08:00
const getCurrentMonthRange = () => {
const start = dayjs().startOf('month').format('YYYY-MM-DD')
const end = dayjs().endOf('month').format('YYYY-MM-DD')
return [start, end]
}
2025-11-29 14:44:18 +08:00
// 从路由参数读取日期范围,如果没有则使用默认值
const getInitialDateRange = () => {
const sDate = route.query.sDate as string
const eDate = route.query.eDate as string
if (sDate && eDate) {
return [sDate, eDate]
}
return getCurrentMonthRange()
}
const dateRange = ref(getInitialDateRange())
2025-11-27 16:07:54 +08:00
// 外协管理数据
const outsourcingTotal = ref<number>(0)
const outsourcingDistribution = ref<DistributionItem[]>([])
// 风险管理数据
const riskTotal = ref<number>(0)
const riskDistribution = ref<DistributionItem[]>([])
const parkRiskDistribution = ref<ParkRiskItem[]>([])
// 隐患管理数据
const hiddenDangerTrend = ref<any[]>([])
const parkRectificationStatus = ref<ParkRectificationItem[]>([])
// 高危作业数据
const highRiskTotal = ref<number>(0)
const operationTypeDistribution = ref<DistributionItem[]>([])
const parkOperationDistribution = ref<DistributionItem[]>([])
// 应急预案数据
const emergencyPlanTotal = ref<number>(0)
const emergencyPlanCompleted = ref<number>(0)
const parkDrillProgress = ref<DistributionItem[]>([])
// 安全培训数据
2025-11-27 18:12:53 +08:00
const trainingBarData = ref<{ regions: string[]; trainingCount: number[]; participants: number[] }>({
regions: [],
trainingCount: [],
participants: []
})
2025-11-27 16:07:54 +08:00
const parkTrainingProgress = ref<DistributionItem[]>([])
// 区域颜色配置
const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899']
2025-11-29 14:44:18 +08:00
// 作业类型颜色配置
const operationTypeColors: Record<string, string> = {
'动火作业': '#f59e0b',
'高处作业': '#8b5cf6',
'临时用电': '#3b82f6',
'有限空间': '#ec4899',
'动土作业': '#10b981',
'吊装作业': '#ef4444'
}
// 初始化区域数据 - 加载园区列表
2025-11-27 16:07:54 +08:00
const initRegionData = async () => {
try {
if (typeof route.query.region === 'string') {
selectedRegion.value = route.query.region
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
2025-11-29 14:44:18 +08:00
// 加载园区列表
2025-11-27 16:07:54 +08:00
const { records } = await getTableList('park_info_list')
if (records && records.length > 0) {
// 根据regionCode过滤园区
const regionCode = route.query.regionCode as string
2025-11-29 14:44:18 +08:00
const parkMap = new Map<string, ParkItem>()
2025-11-27 16:07:54 +08:00
records
.filter((el: any) => el.region_id == regionCode)
.forEach((el: any) => {
2025-11-29 14:44:18 +08:00
if (!parkMap.has(el.park_name)) {
parkMap.set(el.park_name, {
2025-11-27 16:07:54 +08:00
name: el.park_name,
code: el.park_code
})
}
})
2025-11-29 14:44:18 +08:00
parkOption.value = Array.from(parkMap.values())
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
} catch (error) {
console.error('初始化区域数据失败:', error)
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
// 返回总部页面
const returnToHeadquarters = () => {
router.push({
2025-11-29 14:44:18 +08:00
path: '/index',
query: {
sDate: dateRange.value[0],
eDate: dateRange.value[1]
}
2025-11-27 18:12:53 +08:00
})
}
2025-11-29 14:44:18 +08:00
// 打开园区选择器
const openParkSelector = (): void => {
parkSelectorVisible.value = true
2025-11-27 16:07:54 +08:00
}
2025-11-29 14:44:18 +08:00
// 园区选择变化 - 跳转到园区页面
const onParkChange = (item: ParkItem): void => {
2025-11-27 16:07:54 +08:00
selectedPark.value = item.name
2025-11-29 14:44:18 +08:00
parkSelectorVisible.value = false
2025-11-27 16:07:54 +08:00
router.push({
2025-11-27 18:12:53 +08:00
path: '/park',
2025-11-29 14:44:18 +08:00
query: {
region: selectedRegion.value,
regionCode: route.query.regionCode as string,
park: item.name,
parkCode: item.code,
sDate: dateRange.value[0],
eDate: dateRange.value[1]
}
2025-11-27 16:07:54 +08:00
})
}
2025-11-29 14:44:18 +08:00
2025-11-27 16:07:54 +08:00
// 日期变化
const handleDateChange = () => {
refreshData()
}
// 刷新数据
const refreshData = () => {
2025-11-29 14:44:18 +08:00
initData()
2025-11-27 16:07:54 +08:00
}
// 初始化外协管理数据
const initOutsourcingData = async () => {
try {
2025-11-29 14:44:18 +08:00
const response = await getOutsourcingManagementDataRegion({
2025-11-27 16:07:54 +08:00
sDate: dateRange.value[0],
eDate: dateRange.value[1],
2025-11-29 14:44:18 +08:00
regiodId: route.query.regionCode as string,
2025-11-27 16:07:54 +08:00
pageNo: 1,
pageSize: 10000
})
2025-11-29 14:44:18 +08:00
console.log('区域外协管理接口返回:', response)
2025-11-27 16:07:54 +08:00
const records = response?.records || []
if (records && records.length > 0) {
const total = records.reduce((sum: number, item: any) => {
return sum + Number(item.total || 0)
}, 0)
outsourcingTotal.value = total
outsourcingDistribution.value = records.map((item: any, index: number) => {
const count = Number(item.total || 0)
const percent = total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%'
const color = regionColors[index % regionColors.length]
return {
region: item.name,
count,
percent,
color
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
2025-11-29 14:44:18 +08:00
console.log('处理后的区域外协管理数据:', {
total: outsourcingTotal.value,
distribution: outsourcingDistribution.value
})
2025-11-27 16:07:54 +08:00
} else {
outsourcingTotal.value = 0
outsourcingDistribution.value = []
2025-11-29 14:44:18 +08:00
console.log('区域外协管理无数据')
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
} catch (error) {
2025-11-29 14:44:18 +08:00
console.error('获取区域外协管理数据失败:', error)
2025-11-27 16:07:54 +08:00
outsourcingTotal.value = 0
outsourcingDistribution.value = []
}
}
// 初始化风险管理数据
const initRiskData = async () => {
2025-11-29 14:44:18 +08:00
try {
const response = await getRiskManagementDataRegion({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
regiodId: route.query.regionCode as string,
pageNo: 1,
pageSize: 10000
})
console.log('区域风险管理接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按风险等级分组统计,用于环形图
const levelMap = new Map<string, number>()
// 按园区和风险等级分组统计,用于表格
const parkLevelMap = new Map<string, { low: number; general: number; moderate: number; major: number }>()
records.forEach((item: any) => {
const level = item.name || ''
const park = item.area || ''
const count = Number(item.total || 0)
// 统计风险等级分布
if (level) {
levelMap.set(level, (levelMap.get(level) || 0) + count)
}
// 统计园区风险分布
if (park) {
if (!parkLevelMap.has(park)) {
parkLevelMap.set(park, { low: 0, general: 0, moderate: 0, major: 0 })
}
const parkData = parkLevelMap.get(park)!
if (level === '低' || level === '低风险') {
parkData.low += count
} else if (level === '一般' || level === '一般风险') {
parkData.general += count
} else if (level === '较大' || level === '较大风险') {
parkData.moderate += count
} else if (level === '重大' || level === '重大风险') {
parkData.major += count
}
}
})
// 计算总数
const total = Array.from(levelMap.values()).reduce((sum, count) => sum + count, 0)
riskTotal.value = total
// 处理风险等级分布数据(用于环形图)
const allLevels = [
{ key: '低', level: '低风险', count: 0, percent: '0%', color: '#10b981' },
{ key: '一般', level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' },
{ key: '较大', level: '较大风险', count: 0, percent: '0%', color: '#ef4444' },
{ key: '重大', level: '重大风险', count: 0, percent: '0%', color: '#dc2626' }
]
riskDistribution.value = allLevels.map(defaultItem => {
const count = levelMap.get(defaultItem.key) || levelMap.get(defaultItem.level) || 0
const percent = total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%'
return {
level: defaultItem.level,
count,
percent,
color: defaultItem.color
}
})
// 处理园区风险分布表
parkRiskDistribution.value = Array.from(parkLevelMap.entries())
.map(([park, data]) => ({
park,
low: data.low > 0 ? data.low : '',
general: data.general,
moderate: data.moderate,
major: data.major
}))
.sort((a, b) => {
// 按总数降序排序
const totalA = (typeof a.low === 'number' ? a.low : 0) + a.general + a.moderate + a.major
const totalB = (typeof b.low === 'number' ? b.low : 0) + b.general + b.moderate + b.major
return totalB - totalA
})
console.log('处理后的区域风险管理数据:', {
total: riskTotal.value,
levelDistribution: riskDistribution.value,
parkDistribution: parkRiskDistribution.value
})
} else {
riskTotal.value = 0
riskDistribution.value = [
{ level: '低风险', count: 0, percent: '0%', color: '#10b981' },
{ level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' },
{ level: '较大风险', count: 0, percent: '0%', color: '#ef4444' },
{ level: '重大风险', count: 0, percent: '0%', color: '#dc2626' }
]
parkRiskDistribution.value = []
console.log('区域风险管理无数据')
}
} catch (error) {
console.error('获取区域风险管理数据失败:', error)
riskTotal.value = 0
riskDistribution.value = [
{ level: '低风险', count: 0, percent: '0%', color: '#10b981' },
{ level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' },
{ level: '较大风险', count: 0, percent: '0%', color: '#ef4444' },
{ level: '重大风险', count: 0, percent: '0%', color: '#dc2626' }
]
parkRiskDistribution.value = []
}
}
// 根据日期范围选择隐患管理接口
const getHiddenDangerApiRegion = (startDate: string, endDate: string) => {
const start = dayjs(startDate)
const end = dayjs(endDate)
const daysDiff = end.diff(start, 'day') + 1
2025-11-27 16:07:54 +08:00
2025-11-29 14:44:18 +08:00
if (daysDiff <= 7) {
return getHiddenDangerManagementDataRegion
} else if (daysDiff <= 30) {
return getHiddenDangerManagementDataRegionWeek
} else {
return getHiddenDangerManagementDataRegionMonth
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 初始化隐患管理数据
const initHiddenDangerData = async () => {
2025-11-29 14:44:18 +08:00
try {
// 根据日期范围选择接口
const apiFunc = getHiddenDangerApiRegion(dateRange.value[0], dateRange.value[1])
const response = await apiFunc({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
regiodId: route.query.regionCode as string,
pageNo: 1,
pageSize: 10000
})
console.log('区域隐患管理接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按日期和等级分组统计,用于折线图
const trendMap = new Map<string, { general: number; major: number }>()
// 按园区和状态分组统计,用于整改状态表格
const parkStatusMap = new Map<string, { overdue: number; processing: number; processed: number }>()
records.forEach((item: any) => {
const dayname = item.dayname || ''
const level = item.name || ''
const status = item.status || ''
const park = item.area || ''
const count = Number(item.total || 0)
// 统计趋势数据(按日期和等级)
if (dayname) {
if (!trendMap.has(dayname)) {
trendMap.set(dayname, { general: 0, major: 0 })
}
const trend = trendMap.get(dayname)!
if (level === '一般' || level === '一般隐患') {
trend.general += count
} else if (level === '重大' || level === '重大隐患') {
trend.major += count
}
}
// 统计园区整改状态数据
if (park) {
if (!parkStatusMap.has(park)) {
parkStatusMap.set(park, { overdue: 0, processing: 0, processed: 0 })
}
const parkStatus = parkStatusMap.get(park)!
if (status === '已逾期') {
parkStatus.overdue += count
} else if (status === '处理中') {
parkStatus.processing += count
} else if (status === '已处理') {
parkStatus.processed += count
}
}
})
// 转换为数组并按日期排序
hiddenDangerTrend.value = Array.from(trendMap.entries())
.map(([date, counts]) => ({ date, ...counts }))
.sort((a, b) => {
// 处理日期排序:如果是"10月"这种格式,提取月份数字
const numA = parseInt(a.date.replace(/[^0-9]/g, '')) || 0
const numB = parseInt(b.date.replace(/[^0-9]/g, '')) || 0
// 如果是"日"格式(如"10日"),也处理
const dayA = parseInt(a.date.replace('日', '')) || 0
const dayB = parseInt(b.date.replace('日', '')) || 0
return (numA || dayA) - (numB || dayB)
})
// 转换为园区整改状态数组
parkRectificationStatus.value = Array.from(parkStatusMap.entries())
.map(([park, status]) => ({
park,
overdue: status.overdue,
processing: status.processing,
processed: status.processed
}))
.sort((a, b) => {
// 按总数降序排序
const totalA = a.overdue + a.processing + a.processed
const totalB = b.overdue + b.processing + b.processed
return totalB - totalA
})
console.log('处理后的区域隐患管理数据:', {
trend: hiddenDangerTrend.value,
parkStatus: parkRectificationStatus.value
})
} else {
hiddenDangerTrend.value = []
parkRectificationStatus.value = []
console.log('区域隐患管理无数据')
}
} catch (error) {
console.error('获取区域隐患管理数据失败:', error)
hiddenDangerTrend.value = []
parkRectificationStatus.value = []
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 初始化高危作业数据
const initHighRiskData = async () => {
2025-11-29 14:44:18 +08:00
try {
const response = await getHighRiskManagementDataRegion({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
regiodId: route.query.regionCode as string,
pageNo: 1,
pageSize: 10000
})
console.log('区域高危作业接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按作业类型分组统计,用于环形图
const typeMap = new Map<string, number>()
// 按园区分组统计,用于园区分布列表
const parkMap = new Map<string, number>()
records.forEach((item: any) => {
const itemType = item.item || ''
const park = item.area || item.park || ''
const count = Number(item.total || 0)
// 统计作业类型
if (itemType) {
typeMap.set(itemType, (typeMap.get(itemType) || 0) + count)
}
// 统计园区分布
if (park) {
parkMap.set(park, (parkMap.get(park) || 0) + count)
}
})
// 计算总数
const total = Array.from(typeMap.values()).reduce((sum, count) => sum + count, 0)
highRiskTotal.value = total
// 处理作业类型分布数据
operationTypeDistribution.value = Array.from(typeMap.entries())
.map(([type, count]) => ({
type,
count,
percent: total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%',
color: operationTypeColors[type] || '#9ca3af'
}))
.sort((a, b) => b.count - a.count)
// 处理园区分布数据
parkOperationDistribution.value = Array.from(parkMap.entries())
.map(([park, count], index) => ({
region: park,
park: park,
count,
color: regionColors[index % regionColors.length]
}))
.sort((a, b) => b.count - a.count)
console.log('处理后的区域高危作业数据:', {
total: highRiskTotal.value,
typeDistribution: operationTypeDistribution.value,
parkDistribution: parkOperationDistribution.value
})
} else {
highRiskTotal.value = 0
operationTypeDistribution.value = []
parkOperationDistribution.value = []
console.log('区域高危作业无数据')
}
} catch (error) {
console.error('获取区域高危作业数据失败:', error)
highRiskTotal.value = 0
operationTypeDistribution.value = []
parkOperationDistribution.value = []
}
2025-11-27 16:07:54 +08:00
}
// 初始化应急预案数据
const initEmergencyPlanData = async () => {
2025-11-29 14:44:18 +08:00
try {
const response = await getEmergencyPlanManagementDataRegion({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
regiodId: route.query.regionCode as string,
pageNo: 1,
pageSize: 10000
})
console.log('区域应急预案接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 统计应完成演练总数所有记录的total总和
const total = records.reduce((sum: number, item: any) => {
return sum + Number(item.total || 0)
}, 0)
emergencyPlanTotal.value = total
// 统计已完成演练数(根据状态判断)
const completedCount = records.reduce((sum: number, item: any) => {
const status = item.status || ''
const count = Number(item.total || 0)
if (status.includes('完成') || status.includes('已执行')) {
return sum + count
}
return sum
}, 0)
emergencyPlanCompleted.value = completedCount
// 按园区统计演练完成率
const parkMap = new Map<string, { total: number; completed: number }>()
records.forEach((item: any) => {
const park = item.area || item.park || ''
const count = Number(item.total || 0)
const status = item.status || ''
const isCompleted = status.includes('完成') || status.includes('已执行')
if (park) {
if (!parkMap.has(park)) {
parkMap.set(park, { total: 0, completed: 0 })
}
const parkData = parkMap.get(park)!
parkData.total += count
if (isCompleted) {
parkData.completed += count
}
}
})
// 转换为数组并计算完成率
parkDrillProgress.value = Array.from(parkMap.entries())
.map(([park, data], index) => {
const percent = data.total > 0
? ((data.completed / data.total) * 100).toFixed(0) + '%'
: '0%'
return {
region: park,
park: park,
percent,
count: 0,
color: regionColors[index % regionColors.length]
}
})
.sort((a, b) => {
const percentA = parseFloat(a.percent)
const percentB = parseFloat(b.percent)
return percentB - percentA
})
console.log('处理后的区域应急预案数据:', {
total: emergencyPlanTotal.value,
completed: emergencyPlanCompleted.value,
parkProgress: parkDrillProgress.value
})
} else {
emergencyPlanTotal.value = 0
emergencyPlanCompleted.value = 0
parkDrillProgress.value = []
console.log('区域应急预案无数据')
}
} catch (error) {
console.error('获取区域应急预案数据失败:', error)
emergencyPlanTotal.value = 0
emergencyPlanCompleted.value = 0
parkDrillProgress.value = []
}
2025-11-27 16:07:54 +08:00
}
// 初始化安全培训数据
const initSafetyTrainingData = async () => {
2025-11-29 14:44:18 +08:00
try {
const response = await getTrainingManagementDataRegion({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
regiodId: route.query.regionCode as string,
pageNo: 1,
pageSize: 10000
})
console.log('区域安全培训接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按园区分组统计
const parkMap = new Map<string, { trainingCount: number; participants: number }>()
records.forEach((item: any) => {
// 只统计有园区字段的记录
const park = item.area || item.park || ''
if (!park) {
return
}
const trainingCount = Number(item.plannum || 0) // 计划数量作为培训次数
const participants = Number(item.exenum || 0) // 执行数量作为参与人次
if (!parkMap.has(park)) {
parkMap.set(park, { trainingCount: 0, participants: 0 })
}
const parkData = parkMap.get(park)!
parkData.trainingCount += trainingCount
parkData.participants += participants
})
// 转换为数组并排序
const parkDataArray = Array.from(parkMap.entries())
.map(([park, data], index) => ({
park,
...data,
// 计算完成率(参与人次 / 培训次数 * 100%
percent: data.trainingCount > 0
? ((data.participants / data.trainingCount) * 100).toFixed(0) + '%'
: '0%',
color: regionColors[index % regionColors.length]
}))
.sort((a, b) => {
// 按培训次数降序排序
return b.trainingCount - a.trainingCount
})
// 更新柱状图数据
trainingBarData.value = {
regions: parkDataArray.map(item => item.park),
trainingCount: parkDataArray.map(item => item.trainingCount),
participants: parkDataArray.map(item => item.participants)
}
// 更新园区培训完成率数据
parkTrainingProgress.value = parkDataArray.map(item => ({
region: item.park,
park: item.park,
percent: item.percent,
count: 0,
color: item.color
}))
console.log('处理后的区域安全培训数据:', {
barData: trainingBarData.value,
parkProgress: parkTrainingProgress.value
})
} else {
trainingBarData.value = {
regions: [],
trainingCount: [],
participants: []
}
parkTrainingProgress.value = []
console.log('区域安全培训无数据')
}
} catch (error) {
console.error('获取区域安全培训数据失败:', error)
trainingBarData.value = {
regions: [],
trainingCount: [],
participants: []
}
parkTrainingProgress.value = []
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
// 外协管理环形图配置
const outsourcingChartOption = computed<EChartsOption>(() => {
const chartData = outsourcingDistribution.value.map(item => ({
value: item.count,
name: item.region,
itemStyle: { color: item.color }
}))
if (chartData.length === 0 || outsourcingTotal.value === 0) {
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '外协人员',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
2025-11-29 14:44:18 +08:00
label: {
2025-11-27 16:07:54 +08:00
show: true,
position: 'center',
formatter: () => `${outsourcingTotal.value}\n外协人员总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
data: [{ value: 1, name: '暂无数据', itemStyle: { color: '#e5e7eb' } }]
}]
}
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '外协人员',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${outsourcingTotal.value}\n外协人员总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
emphasis: { label: { show: true, fontSize: 18, fontWeight: 'bold' } },
data: chartData
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 风险管理环形图配置
const riskChartOption = computed<EChartsOption>(() => {
const chartData = riskDistribution.value.map(item => ({
value: item.count,
name: item.level,
itemStyle: { color: item.color }
}))
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '风险',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${riskTotal.value}\n风险总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
emphasis: { label: { show: true, fontSize: 18, fontWeight: 'bold' } },
data: chartData
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 隐患管理折线图配置
const hiddenDangerChartOption = computed<EChartsOption>(() => {
const dates = hiddenDangerTrend.value.map(item => item.date)
const generalData = hiddenDangerTrend.value.map(item => item.general)
const majorData = hiddenDangerTrend.value.map(item => item.major)
return {
tooltip: { trigger: 'axis' },
legend: {
data: ['一般隐患', '重大隐患'],
top: 10
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: dates
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
yAxis: { type: 'value' },
series: [
{
name: '一般隐患',
type: 'line',
data: generalData,
itemStyle: { color: '#f59e0b' },
smooth: true
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
{
name: '重大隐患',
type: 'line',
data: majorData,
itemStyle: { color: '#ef4444' },
smooth: true
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
]
2025-11-26 14:13:02 +08:00
}
})
2025-11-27 16:07:54 +08:00
// 高危作业环形图配置
const highRiskChartOption = computed<EChartsOption>(() => {
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '高危作业',
type: 'pie',
2025-11-29 15:20:48 +08:00
radius: ['45%', '65%'],
2025-11-29 14:44:18 +08:00
center: ['50%', '50%'],
2025-11-27 16:07:54 +08:00
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
2025-11-29 14:44:18 +08:00
formatter: () => `${highRiskTotal.value}\n累计作业`,
2025-11-29 15:20:48 +08:00
fontSize: 15,
2025-11-27 16:07:54 +08:00
fontWeight: 'bold',
color: '#333'
},
2025-11-27 18:12:53 +08:00
emphasis: { label: { show: true, fontSize: 20, fontWeight: 'bold' } },
2025-11-27 16:07:54 +08:00
data: operationTypeDistribution.value.map(item => ({
value: item.count,
name: item.type,
itemStyle: { color: item.color }
}))
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 应急预案环形图配置
const emergencyPlanChartOption = computed<EChartsOption>(() => {
const percent = emergencyPlanTotal.value > 0
? ((emergencyPlanCompleted.value / emergencyPlanTotal.value) * 100).toFixed(0)
: 0
return {
tooltip: { trigger: 'item' },
series: [{
name: '演练完成率',
type: 'pie',
2025-11-29 15:20:48 +08:00
radius: ['45%', '65%'],
2025-11-29 14:44:18 +08:00
center: ['50%', '50%'],
2025-11-27 16:07:54 +08:00
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0, color: '#10b981' },
label: {
show: true,
position: 'center',
formatter: () => `${percent}%\n演练完成率`,
2025-11-29 15:20:48 +08:00
fontSize: 15,
2025-11-27 16:07:54 +08:00
fontWeight: 'bold',
color: '#333'
},
data: [
{ value: emergencyPlanCompleted.value, name: '已完成', itemStyle: { color: '#10b981' } },
{ value: emergencyPlanTotal.value - emergencyPlanCompleted.value, name: '未完成', itemStyle: { color: '#e5e7eb' } }
]
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 安全培训柱状图配置
const safetyTrainingChartOption = computed<EChartsOption>(() => {
2025-11-29 14:44:18 +08:00
const regions = trainingBarData.value.regions || []
const trainingCount = trainingBarData.value.trainingCount || []
const participants = trainingBarData.value.participants || []
// 计算Y轴最大值向上取整到最近的10的倍数
const maxValue = Math.max(
...trainingCount,
...participants,
10 // 最小值为10避免图表显示过小
)
const yAxisMax = Math.ceil(maxValue / 10) * 10 || 10
2025-11-27 16:07:54 +08:00
return {
2025-11-29 14:44:18 +08:00
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
let result = `${params[0].axisValue}<br/>`
params.forEach((item: any) => {
result += `${item.marker}${item.seriesName}: ${item.value}<br/>`
})
return result
}
},
2025-11-27 16:07:54 +08:00
legend: {
data: ['培训次数', '参与人次'],
2025-11-29 14:44:18 +08:00
top: 10,
textStyle: {
fontSize: 12,
color: '#666'
},
itemGap: 20
2025-11-27 16:07:54 +08:00
},
2025-11-29 14:44:18 +08:00
grid: {
left: '6%',
right: '4%',
bottom: '0',
top: '25%',
containLabel: true
},
xAxis: {
type: 'category',
data: regions.length > 0 ? regions : [],
axisLine: {
lineStyle: {
color: '#d1d5db'
}
},
axisLabel: {
color: '#666',
fontSize: 12
}
2025-11-27 18:12:53 +08:00
},
yAxis: {
type: 'value',
2025-11-29 14:44:18 +08:00
max: yAxisMax,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#666',
fontSize: 12
},
splitLine: {
lineStyle: {
color: '#f3f4f6',
type: 'dashed'
}
}
2025-11-27 16:07:54 +08:00
},
series: [
{
name: '培训次数',
type: 'bar',
2025-11-29 14:44:18 +08:00
data: trainingCount.length > 0 ? trainingCount : [],
barWidth: 32,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#8b5cf6' },
{ offset: 1, color: '#7c3aed' }
]
},
borderRadius: [6, 6, 0, 0]
},
label: {
show: true,
position: 'insideTop',
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
formatter: (params: any) => {
return params.value > 0 ? params.value : ''
}
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(139, 92, 246, 0.5)'
}
}
2025-11-27 16:07:54 +08:00
},
{
name: '参与人次',
2025-11-29 14:44:18 +08:00
type: 'bar',
data: participants.length > 0 ? participants : [],
barWidth: 32,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#c4b5fd' },
{ offset: 1, color: '#a78bfa' }
]
},
borderRadius: [6, 6, 0, 0]
},
label: {
show: true,
position: 'top',
color: '#7c3aed',
fontSize: 12,
fontWeight: 'bold',
formatter: (params: any) => {
return params.value > 0 ? params.value : ''
}
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(196, 181, 253, 0.5)'
}
}
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 初始化数据
const initData = async () => {
await initRegionData()
2025-11-29 14:44:18 +08:00
initOutsourcingData()
initRiskData()
initHiddenDangerData()
initHighRiskData()
initEmergencyPlanData()
initSafetyTrainingData()
2025-11-26 14:13:02 +08:00
}
onMounted(() => {
2025-11-27 16:07:54 +08:00
initData()
2025-11-26 14:13:02 +08:00
})
</script>
<style lang="scss" scoped>
2025-11-27 16:07:54 +08:00
.dashboard-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
overflow-x: hidden;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-left {
display: flex;
align-items: center;
2025-11-27 18:12:53 +08:00
gap: 10px;
}
.back-arrow {
font-size: 20px;
color: #3b82f6;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
color: #2563eb;
transform: translateX(-2px);
}
2025-11-26 14:13:02 +08:00
}
2025-11-29 14:44:18 +08:00
.region-name-clickable {
font-size: 16px;
font-weight: 600;
color: #333;
2025-11-27 16:07:54 +08:00
padding: 8px 16px;
background: #3b82f6;
color: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
2025-11-29 14:44:18 +08:00
transition: all 0.3s ease;
&:hover {
background: #2563eb;
}
2025-11-27 16:07:54 +08:00
span {
font-size: 18px;
line-height: 1;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.header-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin: 0;
flex: 1;
text-align: center;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-right {
display: flex;
align-items: center;
gap: 15px;
min-width: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.date-range-wrapper {
:deep(.el-date-editor) {
2025-11-29 14:44:18 +08:00
max-width: 100%;
2025-11-26 14:13:02 +08:00
}
}
2025-11-27 16:07:54 +08:00
.refresh-btn {
flex-shrink: 0;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.content-container {
width: 100%;
box-sizing: border-box;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
width: 100%;
box-sizing: border-box;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.dashboard-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
display: flex;
align-items: center;
gap: 8px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-icon {
font-size: 18px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.manage-btn {
color: #3b82f6;
padding: 0;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-content {
display: flex;
flex-direction: column;
gap: 15px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.donut-chart-wrapper,
.donut-chart-wrapper-small {
2025-11-29 15:20:48 +08:00
height: 280px;
min-height: 280px;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.line-chart-wrapper,
.bar-chart-wrapper {
2025-11-29 14:44:18 +08:00
height: 200px;
background: linear-gradient(180deg, #faf9ff 0%, #ffffff 100%);
border-radius: 8px;
padding: 8px;
box-sizing: border-box;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.progress-chart-wrapper {
2025-11-29 15:20:48 +08:00
height: 280px;
min-height: 280px;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.region-distribution,
.risk-distribution-table,
2025-11-29 14:44:18 +08:00
.rectification-status-grid,
2025-11-27 16:07:54 +08:00
.park-operation-distribution,
.regional-progress {
margin-top: 10px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-title {
font-size: 14px;
2025-11-26 14:13:02 +08:00
font-weight: bold;
2025-11-27 16:07:54 +08:00
color: #333;
margin-bottom: 10px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-list {
display: flex;
flex-direction: column;
gap: 8px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.region-name {
flex: 1;
color: #666;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.region-count {
color: #333;
font-weight: 500;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.region-percent {
color: #999;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.table-wrapper {
overflow-x: auto;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.risk-table,
.status-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
thead {
background-color: #f9fafb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
th {
2025-11-29 14:44:18 +08:00
font-weight: bold;
2025-11-27 16:07:54 +08:00
color: #333;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
td {
color: #666;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-29 14:44:18 +08:00
// 九宫格样式
.rectification-status-grid {
.grid-wrapper {
display: flex;
gap: 0;
justify-content: flex-start;
}
.grid-column {
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
&:first-child {
align-items: flex-start;
margin-right: 20px;
flex: 0 0 auto;
min-width: 80px;
}
&:not(:first-child) {
flex: 1;
min-width: 60px;
}
}
.grid-header {
font-size: 14px;
font-weight: bold;
margin-bottom: 12px;
height: 20px;
line-height: 20px;
&.empty-header {
visibility: hidden;
}
&.status-overdue {
color: #ef4444;
}
&.status-processing {
color: #f59e0b;
}
&.status-processed {
color: #10b981;
}
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
2025-11-29 14:44:18 +08:00
.grid-park-name {
font-size: 13px;
color: #333;
margin-bottom: 8px;
text-align: left;
height: 24px;
line-height: 24px;
&:last-child {
margin-bottom: 0;
}
2025-11-27 16:07:54 +08:00
}
2025-11-29 14:44:18 +08:00
.grid-number {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
text-align: center;
height: 24px;
line-height: 24px;
&:last-child {
margin-bottom: 0;
}
&.status-overdue {
color: #ef4444;
}
&.status-processing {
color: #f59e0b;
}
&.status-processed {
color: #10b981;
}
2025-11-27 16:07:54 +08:00
}
}
2025-11-27 18:12:53 +08:00
.high-risk-top {
2025-11-27 16:07:54 +08:00
display: flex;
2025-11-27 18:12:53 +08:00
align-items: center;
2025-11-29 15:20:48 +08:00
gap: 12px;
2025-11-27 16:07:54 +08:00
margin-bottom: 15px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
.donut-chart-wrapper-small {
2025-11-29 15:20:48 +08:00
flex: 0.8;
min-width: 150px;
max-width: 45%;
2025-11-27 18:12:53 +08:00
display: flex;
align-items: center;
}
2025-11-27 16:07:54 +08:00
.operation-type-list {
2025-11-29 15:20:48 +08:00
flex: 0.7;
2025-11-27 16:07:54 +08:00
display: flex;
flex-direction: column;
2025-11-27 18:12:53 +08:00
gap: 8px;
2025-11-29 15:20:48 +08:00
min-width: 140px;
max-width: 170px;
2025-11-27 18:12:53 +08:00
justify-content: center;
2025-11-29 15:20:48 +08:00
flex-shrink: 0;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-type-item {
display: flex;
align-items: center;
gap: 8px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-name {
2025-11-29 14:44:18 +08:00
width: 55px;
2025-11-27 18:12:53 +08:00
font-size: 12px;
2025-11-27 16:07:54 +08:00
color: #666;
flex-shrink: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-bar-wrapper {
flex: 1;
2025-11-29 14:44:18 +08:00
height: 12px;
2025-11-27 16:07:54 +08:00
background-color: #e5e7eb;
2025-11-29 14:44:18 +08:00
border-radius: 6px;
2025-11-27 16:07:54 +08:00
overflow: hidden;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.operation-bar {
height: 100%;
2025-11-29 14:44:18 +08:00
border-radius: 6px;
}
.operation-percent {
font-size: 12px;
color: #666;
font-weight: 500;
min-width: 40px;
text-align: right;
flex-shrink: 0;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
.emergency-plan-top {
2025-11-27 16:07:54 +08:00
display: flex;
2025-11-27 18:12:53 +08:00
align-items: center;
2025-11-29 15:20:48 +08:00
gap: 12px;
2025-11-27 16:07:54 +08:00
margin-bottom: 15px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 18:12:53 +08:00
.progress-chart-wrapper {
2025-11-29 15:20:48 +08:00
flex: 0.8;
min-width: 150px;
max-width: 45%;
2025-11-27 18:12:53 +08:00
display: flex;
align-items: center;
}
2025-11-27 16:07:54 +08:00
.drill-info {
display: flex;
flex-direction: column;
2025-11-27 18:12:53 +08:00
gap: 8px;
2025-11-29 15:20:48 +08:00
flex: 0.7;
min-width: 140px;
max-width: 170px;
2025-11-27 18:12:53 +08:00
justify-content: center;
2025-11-29 15:20:48 +08:00
flex-shrink: 0;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.drill-item {
display: flex;
justify-content: space-between;
align-items: center;
2025-11-27 18:12:53 +08:00
padding: 8px 10px;
2025-11-27 16:07:54 +08:00
background-color: #f0fdf4;
border-radius: 4px;
2025-11-27 18:12:53 +08:00
font-size: 12px;
2025-11-27 16:07:54 +08:00
color: #666;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.drill-number {
font-size: 16px;
font-weight: bold;
color: #10b981;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-list {
display: flex;
flex-direction: column;
2025-11-29 14:44:18 +08:00
gap: 14px;
margin-top: 12px;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-item {
display: flex;
align-items: center;
2025-11-29 14:44:18 +08:00
gap: 12px;
font-size: 13px;
padding: 4px 0;
2025-11-26 14:13:02 +08:00
2025-11-29 14:44:18 +08:00
.region-name {
width: 90px;
color: #374151;
flex-shrink: 0;
font-weight: 500;
}
2025-11-26 14:13:02 +08:00
2025-11-29 14:44:18 +08:00
.progress-bar-wrapper {
flex: 1;
height: 10px;
background: #f3f4f6;
border-radius: 5px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.progress-bar {
height: 100%;
border-radius: 5px;
transition: width 0.5s ease;
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%);
box-shadow: 0 2px 4px rgba(139, 92, 246, 0.3);
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 50%,
transparent 100%
);
animation: shimmer 2s infinite;
}
}
.progress-percent {
min-width: 45px;
text-align: right;
color: #7c3aed;
font-weight: 600;
flex-shrink: 0;
font-size: 13px;
}
2025-11-27 16:07:54 +08:00
}
2025-11-29 14:44:18 +08:00
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
2025-11-27 16:07:54 +08:00
}
@media (max-width: 1400px) {
.card-row {
grid-template-columns: repeat(2, 1fr);
}
}
2025-11-29 15:20:48 +08:00
@media (max-width: 1200px) {
.high-risk-top,
.emergency-plan-top {
gap: 10px;
}
.donut-chart-wrapper-small,
.progress-chart-wrapper {
min-width: 160px;
}
.operation-type-list,
.drill-info {
min-width: 140px;
max-width: 180px;
}
}
2025-11-27 16:07:54 +08:00
@media (max-width: 768px) {
.card-row {
grid-template-columns: 1fr;
}
.header-container {
flex-direction: column;
gap: 15px;
}
.header-title {
text-align: center;
}
2025-11-29 15:20:48 +08:00
.high-risk-top,
.emergency-plan-top {
2025-11-27 16:07:54 +08:00
flex-direction: column;
2025-11-29 15:20:48 +08:00
align-items: stretch;
}
.donut-chart-wrapper-small,
.progress-chart-wrapper {
min-width: 100%;
max-width: 100%;
height: 220px;
min-height: 220px;
}
.operation-type-list,
.drill-info {
min-width: 100%;
max-width: 100%;
2025-11-26 14:13:02 +08:00
}
}
</style>