1889 lines
50 KiB
Vue
1889 lines
50 KiB
Vue
<template>
|
||
<div class="dashboard-container">
|
||
<!-- 顶部标题栏 -->
|
||
<div class="header-container">
|
||
<div class="header-left">
|
||
<el-icon class="back-arrow" @click="returnToRegion">
|
||
<ArrowLeft />
|
||
</el-icon>
|
||
<div class="park-name">{{ selectedPark }}</div>
|
||
</div>
|
||
<!-- <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" />
|
||
</div>
|
||
<el-button type="primary" :icon="Refresh" @click="refreshData" class="refresh-btn">
|
||
刷新数据
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区 - 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>
|
||
外协管理
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="donut-chart-wrapper">
|
||
<Echart :options="outsourcingChartOption" width="100%" height="200px" />
|
||
</div>
|
||
<div class="region-distribution">
|
||
<div class="distribution-title">供应商分布</div>
|
||
<div class="distribution-list">
|
||
<div class="distribution-item" v-for="item in supplierDistribution" :key="item.supplier">
|
||
<span class="dot" :style="{ backgroundColor: item.color }"></span>
|
||
<span class="region-name">{{ item.supplier }}</span>
|
||
<span class="region-count">{{ item.count }}人</span>
|
||
<span class="region-percent">({{ item.percent }})</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 风险管理卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span class="card-icon">🛡️</span>
|
||
风险管理
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="donut-chart-wrapper">
|
||
<Echart :options="riskChartOption" width="100%" height="200px" />
|
||
</div>
|
||
<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 locationRiskDistribution" :key="item.location">
|
||
<td>{{ item.location }}</td>
|
||
<td>{{ item.low || '' }}</td>
|
||
<td>{{ item.general }}</td>
|
||
<td>{{ item.moderate }}</td>
|
||
<td>{{ item.major }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 隐患管理卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span class="card-icon warning">⚠️</span>
|
||
隐患管理
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="line-chart-wrapper">
|
||
<Echart :options="hiddenDangerChartOption" width="100%" height="180px" />
|
||
</div>
|
||
<div class="rectification-status-grid">
|
||
<div class="distribution-title">所属公司整改状态</div>
|
||
<div class="grid-wrapper">
|
||
<!-- 第一列:公司名称 -->
|
||
<div class="grid-column">
|
||
<div class="grid-header empty-header"></div>
|
||
<div class="grid-company-name" v-for="item in companyRectificationStatus" :key="'company-' + item.company">
|
||
{{ item.company }}
|
||
</div>
|
||
</div>
|
||
<!-- 已逾期列 -->
|
||
<div class="grid-column">
|
||
<div class="grid-header status-overdue">已逾期</div>
|
||
<div class="grid-number status-overdue" v-for="item in companyRectificationStatus" :key="'overdue-' + item.company">
|
||
{{ 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 companyRectificationStatus" :key="'processing-' + item.company">
|
||
{{ 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 companyRectificationStatus" :key="'processed-' + item.company">
|
||
{{ item.processed }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第二行:高危作业、应急预案、安全培训 -->
|
||
<div class="card-row">
|
||
<!-- 高危作业卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span class="card-icon">🚧</span>
|
||
高危作业
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="high-risk-top">
|
||
<div class="donut-chart-wrapper-small">
|
||
<Echart :options="highRiskChartOption" width="100%" height="280px" />
|
||
</div>
|
||
<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>
|
||
</div>
|
||
<span class="operation-percent">{{ item.count }}项</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="park-operation-distribution">
|
||
<div class="distribution-title">申请公司作业分布</div>
|
||
<div class="distribution-list">
|
||
<div class="distribution-item" v-for="item in companyOperationDistribution" :key="item.company">
|
||
<span class="region-name">{{ item.company }}</span>
|
||
<span class="region-count">{{ item.count }}项</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 应急预案卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span class="card-icon">📄</span>
|
||
应急预案
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="emergency-plan-top">
|
||
<div class="progress-chart-wrapper">
|
||
<Echart :options="emergencyPlanChartOption" width="100%" height="280px" />
|
||
</div>
|
||
<div class="drill-info">
|
||
<div class="drill-item">
|
||
<span>应完成演练</span>
|
||
<span class="drill-number">{{ emergencyPlanTotal }}次</span>
|
||
</div>
|
||
<div class="drill-item">
|
||
<span>已完成演练</span>
|
||
<span class="drill-number">{{ emergencyPlanCompleted }}次</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="recent-drill-details">
|
||
<div class="distribution-title">近期演练详情</div>
|
||
<div class="drill-details-list">
|
||
<div class="drill-detail-item" v-for="item in recentDrillDetails" :key="item.id">
|
||
<div class="drill-detail-info">
|
||
<div class="drill-detail-name">{{ item.name }}</div>
|
||
<div class="drill-detail-date">{{ item.date }}</div>
|
||
</div>
|
||
<div class="drill-detail-status" :class="item.status === '已完成' ? 'completed' : 'planned'">
|
||
{{ item.status }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 安全培训卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<div class="card-title">
|
||
<span class="card-icon">📚</span>
|
||
安全培训
|
||
</div>
|
||
<!-- <el-button type="text" class="manage-btn">管理</el-button> -->
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="bar-chart-wrapper">
|
||
<Echart :options="safetyTrainingChartOption" width="100%" height="180px" />
|
||
</div>
|
||
<div class="regional-progress">
|
||
<div class="distribution-title">所属公司培训完成率</div>
|
||
<div class="progress-list">
|
||
<div class="progress-item" v-for="item in companyTrainingProgress" :key="item.company">
|
||
<span class="region-name">{{ item.company }}</span>
|
||
<div class="progress-bar-wrapper">
|
||
<div class="progress-bar" :style="{ width: item.percent }"></div>
|
||
</div>
|
||
<span class="progress-percent">{{ item.percent }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { Refresh, ArrowLeft } from '@element-plus/icons-vue'
|
||
import Echart from '@/components/Echart/src/Echart.vue'
|
||
import type { EChartsOption } from 'echarts'
|
||
import dayjs from 'dayjs'
|
||
import {
|
||
getOutsourcingManagementDataPark,
|
||
getHighRiskManagementDataPark,
|
||
getEmergencyPlanManagementDataPark,
|
||
getTrainingManagementDataPark,
|
||
getRiskManagementDataPark,
|
||
getHiddenDangerManagementDataPark,
|
||
getHiddenDangerManagementDataParkWeek,
|
||
getHiddenDangerManagementDataParkMonth
|
||
} from '@/api'
|
||
|
||
defineOptions({ name: 'Home13' })
|
||
|
||
// 类型定义
|
||
interface DistributionItem {
|
||
supplier?: string
|
||
company?: string
|
||
location?: string
|
||
level?: string
|
||
type?: string
|
||
count: number
|
||
percent?: string
|
||
color: string
|
||
}
|
||
|
||
interface LocationRiskItem {
|
||
location: string
|
||
low: number | string
|
||
general: number
|
||
moderate: number
|
||
major: number
|
||
}
|
||
|
||
interface CompanyRectificationItem {
|
||
company: string
|
||
overdue: number
|
||
processing: number
|
||
processed: number
|
||
}
|
||
|
||
interface RecentDrillItem {
|
||
id: string
|
||
name: string
|
||
date: string
|
||
status: string
|
||
}
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
// 园区名称 - 从路由参数获取
|
||
const selectedPark = ref<string>('')
|
||
|
||
// 时间选择相关 - 默认当前月起止,如果路由中有日期范围参数则使用
|
||
const getCurrentMonthRange = () => {
|
||
const start = dayjs().startOf('month').format('YYYY-MM-DD')
|
||
const end = dayjs().endOf('month').format('YYYY-MM-DD')
|
||
return [start, end]
|
||
}
|
||
// 从路由参数读取日期范围,如果没有则使用默认值
|
||
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())
|
||
|
||
// 外协管理数据
|
||
const outsourcingTotal = ref<number>(0)
|
||
const supplierDistribution = ref<DistributionItem[]>([])
|
||
|
||
// 风险管理数据
|
||
const riskTotal = ref<number>(0)
|
||
const riskDistribution = ref<DistributionItem[]>([])
|
||
const locationRiskDistribution = ref<LocationRiskItem[]>([])
|
||
|
||
// 隐患管理数据
|
||
const hiddenDangerTrend = ref<any[]>([])
|
||
const companyRectificationStatus = ref<CompanyRectificationItem[]>([])
|
||
|
||
// 高危作业数据
|
||
const highRiskTotal = ref<number>(0)
|
||
const operationTypeDistribution = ref<DistributionItem[]>([])
|
||
const companyOperationDistribution = ref<DistributionItem[]>([])
|
||
|
||
// 应急预案数据
|
||
const emergencyPlanTotal = ref<number>(0)
|
||
const emergencyPlanCompleted = ref<number>(0)
|
||
const recentDrillDetails = ref<RecentDrillItem[]>([])
|
||
|
||
// 安全培训数据
|
||
const trainingBarData = ref<{ regions: string[]; trainingCount: number[]; participants: number[] }>({
|
||
regions: [],
|
||
trainingCount: [],
|
||
participants: []
|
||
})
|
||
const companyTrainingProgress = ref<DistributionItem[]>([])
|
||
|
||
// 区域颜色配置
|
||
const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899']
|
||
|
||
// 作业类型颜色配置
|
||
const operationTypeColors: Record<string, string> = {
|
||
'动火作业': '#f59e0b',
|
||
'高处作业': '#8b5cf6',
|
||
'临时用电': '#3b82f6',
|
||
'有限空间': '#ec4899',
|
||
'动土作业': '#10b981',
|
||
'吊装作业': '#ef4444'
|
||
}
|
||
|
||
// 初始化园区名称
|
||
const initParkName = () => {
|
||
if (typeof route.query.park === 'string') {
|
||
selectedPark.value = route.query.park
|
||
} else {
|
||
selectedPark.value = '雄安园区'
|
||
}
|
||
}
|
||
|
||
// 返回区域页面
|
||
const returnToRegion = () => {
|
||
router.push({
|
||
path: '/region',
|
||
query: {
|
||
region: route.query.region,
|
||
regionCode: route.query.regionCode,
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1]
|
||
}
|
||
})
|
||
}
|
||
|
||
// 日期变化
|
||
const handleDateChange = () => {
|
||
refreshData()
|
||
}
|
||
|
||
// 刷新数据
|
||
const refreshData = () => {
|
||
initData()
|
||
}
|
||
|
||
// 初始化外协管理数据
|
||
const initOutsourcingData = async () => {
|
||
try {
|
||
const response = await getOutsourcingManagementDataPark({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park as string,
|
||
pageNo: 1,
|
||
pageSize: 10000
|
||
})
|
||
|
||
console.log('园区外协管理接口返回:', response)
|
||
|
||
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
|
||
|
||
supplierDistribution.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 {
|
||
supplier: item.name || `供应商${String.fromCharCode(65 + index)}`,
|
||
count,
|
||
percent,
|
||
color
|
||
}
|
||
})
|
||
|
||
console.log('处理后的园区外协管理数据:', {
|
||
total: outsourcingTotal.value,
|
||
distribution: supplierDistribution.value
|
||
})
|
||
} else {
|
||
outsourcingTotal.value = 0
|
||
supplierDistribution.value = []
|
||
console.log('园区外协管理无数据')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取园区外协管理数据失败:', error)
|
||
outsourcingTotal.value = 0
|
||
supplierDistribution.value = []
|
||
}
|
||
}
|
||
|
||
// 初始化风险管理数据
|
||
const initRiskData = async () => {
|
||
try {
|
||
const response = await getRiskManagementDataPark({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park 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 locationLevelMap = new Map<string, { low: number; general: number; moderate: number; major: number }>()
|
||
|
||
records.forEach((item: any) => {
|
||
const level = item.name || ''
|
||
const location = item.area || ''
|
||
const count = Number(item.total || 0)
|
||
|
||
// 统计风险等级分布
|
||
if (level) {
|
||
levelMap.set(level, (levelMap.get(level) || 0) + count)
|
||
}
|
||
|
||
// 统计地点风险分布
|
||
if (location) {
|
||
if (!locationLevelMap.has(location)) {
|
||
locationLevelMap.set(location, { low: 0, general: 0, moderate: 0, major: 0 })
|
||
}
|
||
const locationData = locationLevelMap.get(location)!
|
||
if (level === '低' || level === '低风险') {
|
||
locationData.low += count
|
||
} else if (level === '一般' || level === '一般风险') {
|
||
locationData.general += count
|
||
} else if (level === '较大' || level === '较大风险') {
|
||
locationData.moderate += count
|
||
} else if (level === '重大' || level === '重大风险') {
|
||
locationData.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
|
||
}
|
||
})
|
||
|
||
// 处理地点风险分布表
|
||
locationRiskDistribution.value = Array.from(locationLevelMap.entries())
|
||
.map(([location, data]) => ({
|
||
location,
|
||
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,
|
||
locationDistribution: locationRiskDistribution.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' }
|
||
]
|
||
locationRiskDistribution.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' }
|
||
]
|
||
locationRiskDistribution.value = []
|
||
}
|
||
}
|
||
|
||
// 根据日期范围选择隐患管理接口
|
||
const getHiddenDangerApiPark = (startDate: string, endDate: string) => {
|
||
const start = dayjs(startDate)
|
||
const end = dayjs(endDate)
|
||
const daysDiff = end.diff(start, 'day') + 1
|
||
|
||
if (daysDiff <= 7) {
|
||
return getHiddenDangerManagementDataPark
|
||
} else if (daysDiff <= 30) {
|
||
return getHiddenDangerManagementDataParkWeek
|
||
} else {
|
||
return getHiddenDangerManagementDataParkMonth
|
||
}
|
||
}
|
||
|
||
// 初始化隐患管理数据
|
||
const initHiddenDangerData = async () => {
|
||
try {
|
||
// 根据日期范围选择接口
|
||
const apiFunc = getHiddenDangerApiPark(dateRange.value[0], dateRange.value[1])
|
||
|
||
const response = await apiFunc({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park 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 companyStatusMap = 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 company = 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 (company) {
|
||
if (!companyStatusMap.has(company)) {
|
||
companyStatusMap.set(company, { overdue: 0, processing: 0, processed: 0 })
|
||
}
|
||
const companyStatus = companyStatusMap.get(company)!
|
||
if (status === '已逾期') {
|
||
companyStatus.overdue += count
|
||
} else if (status === '处理中') {
|
||
companyStatus.processing += count
|
||
} else if (status === '已处理') {
|
||
companyStatus.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)
|
||
})
|
||
|
||
// 转换为公司整改状态数组
|
||
companyRectificationStatus.value = Array.from(companyStatusMap.entries())
|
||
.map(([company, status]) => ({
|
||
company,
|
||
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,
|
||
companyStatus: companyRectificationStatus.value
|
||
})
|
||
} else {
|
||
hiddenDangerTrend.value = []
|
||
companyRectificationStatus.value = []
|
||
console.log('园区隐患管理无数据')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取园区隐患管理数据失败:', error)
|
||
hiddenDangerTrend.value = []
|
||
companyRectificationStatus.value = []
|
||
}
|
||
}
|
||
|
||
// 初始化高危作业数据
|
||
const initHighRiskData = async () => {
|
||
try {
|
||
const response = await getHighRiskManagementDataPark({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park 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 companyMap = new Map<string, number>()
|
||
|
||
records.forEach((item: any) => {
|
||
const itemType = item.item || ''
|
||
const company = item.area || ''
|
||
const count = Number(item.total || 0)
|
||
|
||
// 统计作业类型
|
||
if (itemType) {
|
||
typeMap.set(itemType, (typeMap.get(itemType) || 0) + count)
|
||
}
|
||
|
||
// 统计公司分布(使用area字段)
|
||
if (company) {
|
||
companyMap.set(company, (companyMap.get(company) || 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)
|
||
|
||
// 处理供应商分布数据
|
||
companyOperationDistribution.value = Array.from(companyMap.entries())
|
||
.map(([company, count], index) => ({
|
||
company,
|
||
count,
|
||
color: regionColors[index % regionColors.length]
|
||
}))
|
||
.sort((a, b) => b.count - a.count)
|
||
|
||
console.log('处理后的园区高危作业数据:', {
|
||
total: highRiskTotal.value,
|
||
typeDistribution: operationTypeDistribution.value,
|
||
companyDistribution: companyOperationDistribution.value
|
||
})
|
||
} else {
|
||
highRiskTotal.value = 0
|
||
operationTypeDistribution.value = []
|
||
companyOperationDistribution.value = []
|
||
console.log('园区高危作业无数据')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取园区高危作业数据失败:', error)
|
||
highRiskTotal.value = 0
|
||
operationTypeDistribution.value = []
|
||
companyOperationDistribution.value = []
|
||
}
|
||
}
|
||
|
||
// 初始化应急预案数据
|
||
const initEmergencyPlanData = async () => {
|
||
try {
|
||
const response = await getEmergencyPlanManagementDataPark({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park 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
|
||
|
||
// 提取最近的演练详情(取前几条记录)
|
||
recentDrillDetails.value = records
|
||
.slice(0, 10) // 最多显示10条
|
||
.map((item: any, index: number) => ({
|
||
id: String(item.row_id || index + 1),
|
||
name: item.name || item.plan_name || `演练${index + 1}`,
|
||
date: item.date || item.plan_date || '',
|
||
status: item.status || '计划中'
|
||
}))
|
||
|
||
console.log('处理后的园区应急预案数据:', {
|
||
total: emergencyPlanTotal.value,
|
||
completed: emergencyPlanCompleted.value,
|
||
recentDrills: recentDrillDetails.value
|
||
})
|
||
} else {
|
||
emergencyPlanTotal.value = 0
|
||
emergencyPlanCompleted.value = 0
|
||
recentDrillDetails.value = []
|
||
console.log('园区应急预案无数据')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取园区应急预案数据失败:', error)
|
||
emergencyPlanTotal.value = 0
|
||
emergencyPlanCompleted.value = 0
|
||
recentDrillDetails.value = []
|
||
}
|
||
}
|
||
|
||
// 初始化安全培训数据
|
||
const initSafetyTrainingData = async () => {
|
||
try {
|
||
const response = await getTrainingManagementDataPark({
|
||
sDate: dateRange.value[0],
|
||
eDate: dateRange.value[1],
|
||
campusId: route.query.parkCode as string,
|
||
park: route.query.park as string,
|
||
pageNo: 1,
|
||
pageSize: 10000
|
||
})
|
||
|
||
console.log('园区安全培训接口返回:', response)
|
||
|
||
const records = response?.records || []
|
||
|
||
if (records && records.length > 0) {
|
||
// 按供应商/公司分组统计
|
||
const companyMap = new Map<string, { trainingCount: number; participants: number }>()
|
||
|
||
records.forEach((item: any) => {
|
||
// 使用area字段作为公司名称
|
||
const company = item.area || ''
|
||
if (!company) {
|
||
return
|
||
}
|
||
|
||
const trainingCount = Number(item.plannum || 0) // 计划数量作为培训次数
|
||
const participants = Number(item.exenum || 0) // 执行数量作为参与人次
|
||
|
||
if (!companyMap.has(company)) {
|
||
companyMap.set(company, { trainingCount: 0, participants: 0 })
|
||
}
|
||
const companyData = companyMap.get(company)!
|
||
companyData.trainingCount += trainingCount
|
||
companyData.participants += participants
|
||
})
|
||
|
||
// 转换为数组并排序
|
||
const companyDataArray = Array.from(companyMap.entries())
|
||
.map(([company, data], index) => ({
|
||
company,
|
||
...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: companyDataArray.map(item => item.company),
|
||
trainingCount: companyDataArray.map(item => item.trainingCount),
|
||
participants: companyDataArray.map(item => item.participants)
|
||
}
|
||
|
||
// 更新供应商培训完成率数据
|
||
companyTrainingProgress.value = companyDataArray.map(item => ({
|
||
company: item.company,
|
||
percent: item.percent,
|
||
count: 0,
|
||
color: item.color
|
||
}))
|
||
|
||
console.log('处理后的园区安全培训数据:', {
|
||
barData: trainingBarData.value,
|
||
companyProgress: companyTrainingProgress.value
|
||
})
|
||
} else {
|
||
trainingBarData.value = {
|
||
regions: [],
|
||
trainingCount: [],
|
||
participants: []
|
||
}
|
||
companyTrainingProgress.value = []
|
||
console.log('园区安全培训无数据')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取园区安全培训数据失败:', error)
|
||
trainingBarData.value = {
|
||
regions: [],
|
||
trainingCount: [],
|
||
participants: []
|
||
}
|
||
companyTrainingProgress.value = []
|
||
}
|
||
}
|
||
|
||
// 外协管理环形图配置
|
||
const outsourcingChartOption = computed<EChartsOption>(() => {
|
||
const chartData = supplierDistribution.value.map(item => ({
|
||
value: item.count,
|
||
name: item.supplier,
|
||
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 },
|
||
label: {
|
||
show: true,
|
||
position: 'center',
|
||
formatter: () => `${outsourcingTotal.value}\n外协人员总数`,
|
||
fontSize: 16,
|
||
fontWeight: 'bold',
|
||
color: '#333'
|
||
},
|
||
data: [{ value: 1, name: '暂无数据', itemStyle: { color: '#e5e7eb' } }]
|
||
}]
|
||
}
|
||
}
|
||
|
||
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
|
||
}]
|
||
}
|
||
})
|
||
|
||
// 风险管理环形图配置
|
||
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
|
||
}]
|
||
}
|
||
})
|
||
|
||
// 隐患管理折线图配置
|
||
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
|
||
},
|
||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: dates
|
||
},
|
||
yAxis: { type: 'value', max: 45 },
|
||
series: [
|
||
{
|
||
name: '一般隐患',
|
||
type: 'line',
|
||
data: generalData,
|
||
itemStyle: { color: '#f59e0b' },
|
||
smooth: true
|
||
},
|
||
{
|
||
name: '重大隐患',
|
||
type: 'line',
|
||
data: majorData,
|
||
itemStyle: { color: '#ef4444' },
|
||
smooth: true
|
||
}
|
||
]
|
||
}
|
||
})
|
||
|
||
// 高危作业环形图配置
|
||
const highRiskChartOption = computed<EChartsOption>(() => {
|
||
return {
|
||
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||
series: [{
|
||
name: '高危作业',
|
||
type: 'pie',
|
||
radius: ['49%', '61%'],
|
||
center: ['50%', '50%'],
|
||
avoidLabelOverlap: false,
|
||
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
|
||
label: {
|
||
show: true,
|
||
position: 'center',
|
||
formatter: () => `${highRiskTotal.value}\n累计作业`,
|
||
fontSize: 15,
|
||
fontWeight: 'bold',
|
||
color: '#333'
|
||
},
|
||
emphasis: { label: { show: true, fontSize: 20, fontWeight: 'bold' } },
|
||
data: operationTypeDistribution.value.map(item => ({
|
||
value: item.count,
|
||
name: item.type,
|
||
itemStyle: { color: item.color }
|
||
}))
|
||
}]
|
||
}
|
||
})
|
||
|
||
// 应急预案环形图配置
|
||
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',
|
||
radius: ['49%', '61%'],
|
||
center: ['50%', '50%'],
|
||
avoidLabelOverlap: false,
|
||
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0, color: '#10b981' },
|
||
label: {
|
||
show: true,
|
||
position: 'center',
|
||
formatter: () => `${percent}%\n演练完成率`,
|
||
fontSize: 15,
|
||
fontWeight: 'bold',
|
||
color: '#333'
|
||
},
|
||
data: [
|
||
{ value: emergencyPlanCompleted.value, name: '已完成', itemStyle: { color: '#10b981' } },
|
||
{ value: emergencyPlanTotal.value - emergencyPlanCompleted.value, name: '未完成', itemStyle: { color: '#e5e7eb' } }
|
||
]
|
||
}]
|
||
}
|
||
})
|
||
|
||
// 安全培训柱状图配置
|
||
const safetyTrainingChartOption = computed<EChartsOption>(() => {
|
||
const regions = trainingBarData.value.regions || []
|
||
const trainingCount = trainingBarData.value.trainingCount || []
|
||
const participants = trainingBarData.value.participants || []
|
||
|
||
// 计算堆叠后的最大值(培训次数 + 参与人次)
|
||
const stackedValues = trainingCount.map((count, index) => count + (participants[index] || 0))
|
||
const maxValue = Math.max(...stackedValues, 10) // 最小值为10,避免图表显示过小
|
||
const yAxisMax = Math.ceil(maxValue / 10) * 10 || 10
|
||
|
||
return {
|
||
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
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['培训次数', '参与人次'],
|
||
top: 10,
|
||
textStyle: {
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
itemGap: 20
|
||
},
|
||
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
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
max: yAxisMax,
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
},
|
||
axisLabel: {
|
||
color: '#666',
|
||
fontSize: 12
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f3f4f6',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '培训次数',
|
||
type: 'bar',
|
||
stack: 'total',
|
||
data: trainingCount.length > 0 ? trainingCount : [],
|
||
barWidth: 32,
|
||
itemStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#3b82f6' },
|
||
{ offset: 1, color: '#2563eb' }
|
||
]
|
||
},
|
||
borderRadius: [0, 0, 0, 0]
|
||
},
|
||
label: {
|
||
show: false
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowColor: 'rgba(59, 130, 246, 0.5)'
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '参与人次',
|
||
type: 'bar',
|
||
stack: 'total',
|
||
data: participants.length > 0 ? participants : [],
|
||
barWidth: 32,
|
||
itemStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#84cc16' },
|
||
{ offset: 1, color: '#65a30d' }
|
||
]
|
||
},
|
||
borderRadius: [6, 6, 0, 0]
|
||
},
|
||
label: {
|
||
show: false
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowColor: 'rgba(132, 204, 22, 0.5)'
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
})
|
||
|
||
// 初始化数据
|
||
const initData = async () => {
|
||
initParkName()
|
||
initOutsourcingData()
|
||
initRiskData()
|
||
initHiddenDangerData()
|
||
initHighRiskData()
|
||
initEmergencyPlanData()
|
||
initSafetyTrainingData()
|
||
}
|
||
|
||
onMounted(() => {
|
||
initData()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.dashboard-container {
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
box-sizing: border-box;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.back-arrow {
|
||
font-size: 20px;
|
||
color: #3b82f6;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
color: #2563eb;
|
||
transform: translateX(-2px);
|
||
}
|
||
}
|
||
|
||
.park-name {
|
||
padding: 8px 16px;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin: 0;
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.date-range-wrapper {
|
||
:deep(.el-date-editor) {
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
|
||
.refresh-btn {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.content-container {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.card-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.dashboard-card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.card-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.manage-btn {
|
||
color: #3b82f6;
|
||
padding: 0;
|
||
}
|
||
|
||
.card-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.donut-chart-wrapper,
|
||
.donut-chart-wrapper-small {
|
||
height: 200px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.line-chart-wrapper,
|
||
.bar-chart-wrapper {
|
||
height: 200px;
|
||
background: linear-gradient(180deg, #faf9ff 0%, #ffffff 100%);
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.bar-chart-wrapper {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.progress-chart-wrapper {
|
||
height: 200px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.region-distribution,
|
||
.risk-distribution-table,
|
||
.rectification-status-grid,
|
||
.park-operation-distribution,
|
||
.recent-drill-details,
|
||
.regional-progress {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.distribution-title {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.distribution-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.distribution-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.region-name {
|
||
flex: 1;
|
||
color: #666;
|
||
}
|
||
|
||
.region-count {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.region-percent {
|
||
color: #999;
|
||
}
|
||
|
||
.table-wrapper {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.risk-table,
|
||
.status-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
|
||
thead {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
th, td {
|
||
padding: 8px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
th {
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
td {
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
// 九宫格样式
|
||
// 九宫格样式
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.grid-company-name {
|
||
font-size: 13px;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
text-align: left;
|
||
height: 24px;
|
||
line-height: 24px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.high-risk-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.donut-chart-wrapper-small {
|
||
flex: 0.8;
|
||
min-width: 150px;
|
||
max-width: 45%;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.operation-type-list {
|
||
flex: 0.7;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 140px;
|
||
max-width: 170px;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.operation-type-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.operation-name {
|
||
width: 55px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.operation-bar-wrapper {
|
||
flex: 1;
|
||
height: 12px;
|
||
background-color: #e5e7eb;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.operation-bar {
|
||
height: 100%;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.operation-percent {
|
||
font-size: 12px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
min-width: 40px;
|
||
text-align: right;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.emergency-plan-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.progress-chart-wrapper {
|
||
flex: 0.8;
|
||
min-width: 150px;
|
||
max-width: 45%;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.drill-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
flex: 0.7;
|
||
min-width: 140px;
|
||
max-width: 170px;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.drill-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 10px;
|
||
background-color: #f0fdf4;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.drill-number {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #10b981;
|
||
}
|
||
|
||
.recent-drill-details {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.drill-details-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.drill-detail-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px;
|
||
background-color: #f9fafb;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.drill-detail-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.drill-detail-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.drill-detail-date {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.drill-detail-status {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
|
||
&.completed {
|
||
color: #10b981;
|
||
}
|
||
|
||
&.planned {
|
||
color: #f59e0b;
|
||
}
|
||
}
|
||
|
||
.progress-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.progress-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 13px;
|
||
padding: 4px 0;
|
||
|
||
.region-name {
|
||
width: 90px;
|
||
color: #374151;
|
||
flex-shrink: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% {
|
||
transform: translateX(-100%);
|
||
}
|
||
100% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.card-row {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@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;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.card-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.header-container {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.header-title {
|
||
text-align: center;
|
||
}
|
||
|
||
.high-risk-top,
|
||
.emergency-plan-top {
|
||
flex-direction: column;
|
||
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%;
|
||
}
|
||
}
|
||
</style>
|