Files
lc_frontend/src/views/Home/Index10.vue
2025-12-30 17:07:46 +08:00

2315 lines
62 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard-container">
<!-- 顶部标题栏 -->
<div class="header-container">
<div class="header-left">
<div class="back-button" @click="openRegionSelector">
{{ currentView }}
<span>···</span>
</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" @click="openOutsourcingManagement">管理</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="grid-wrapper">
<!-- 第一列区域名称 -->
<div class="grid-column">
<div class="grid-header empty-header"></div>
<div class="grid-park-name" v-for="item in outsourcingDistribution" :key="'area-' + item.region">
{{ item.region }}
</div>
</div>
<!-- 人数列 -->
<div class="grid-column">
<div class="grid-header">人数</div>
<div class="grid-number" v-for="item in outsourcingDistribution"
:key="'count-' + item.region">
{{ item.count }}
</div>
</div>
<!-- 百分比列 -->
<div class="grid-column">
<div class="grid-header">占比</div>
<div class="grid-number" v-for="item in outsourcingDistribution"
:key="'percent-' + item.region">
{{ item.percent }}
</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" @click="openRiskManagement">管理</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="grid-wrapper">
<!-- 第一列区域名称 -->
<div class="grid-column">
<div class="grid-header empty-header"></div>
<div class="grid-park-name" v-for="item in areaRiskDistribution" :key="'area-' + item.area">
{{ item.area }}
</div>
</div>
<!-- 低列 -->
<div class="grid-column">
<div class="grid-header status-low"></div>
<div class="grid-number status-low" v-for="item in areaRiskDistribution"
:key="'low-' + item.area">
{{ item.low || '0' }}
</div>
</div>
<!-- 一般列 -->
<div class="grid-column">
<div class="grid-header status-general">一般</div>
<div class="grid-number status-general" v-for="item in areaRiskDistribution"
:key="'general-' + item.area">
{{ item.general || '0' }}
</div>
</div>
<!-- 较大列 -->
<div class="grid-column">
<div class="grid-header status-moderate">较大</div>
<div class="grid-number status-moderate" v-for="item in areaRiskDistribution"
:key="'moderate-' + item.area">
{{ item.moderate || '0' }}
</div>
</div>
<!-- 重大列 -->
<div class="grid-column">
<div class="grid-header status-major">重大</div>
<div class="grid-number status-major" v-for="item in areaRiskDistribution"
:key="'major-' + item.area">
{{ item.major || '0' }}
</div>
</div>
</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" @click="openHiddenDangerManagement">管理</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 v-if="areaRectificationStatus.length === 0" class="empty-data-tip">
暂无数据
</div>
<div v-else class="grid-wrapper">
<!-- 第一列区域名称 -->
<div class="grid-column">
<div class="grid-header empty-header"></div>
<div class="grid-park-name" v-for="item in areaRectificationStatus" :key="'area-' + item.area">
{{ item.area }}
</div>
</div>
<!-- 已逾期列 -->
<div class="grid-column">
<div class="grid-header status-overdue">已逾期</div>
<div class="grid-number status-overdue" v-for="item in areaRectificationStatus"
:key="'overdue-' + item.area">
{{ item.overdue ?? 0 }}
</div>
</div>
<!-- 处理中列 -->
<div class="grid-column">
<div class="grid-header status-processing">处理中</div>
<div class="grid-number status-processing" v-for="item in areaRectificationStatus"
:key="'processing-' + item.area">
{{ item.processing ?? 0 }}
</div>
</div>
<!-- 已处理列 -->
<div class="grid-column">
<div class="grid-header status-processed">已处理</div>
<div class="grid-number status-processed" v-for="item in areaRectificationStatus"
:key="'processed-' + item.area">
{{ item.processed ?? 0 }}
</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" @click="openHighRiskManagement">管理</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 operationDistribution" :key="item.region">
<span class="region-name">{{ item.region }}</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" @click="openEmergencyPlanManagement">管理</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="regional-progress">
<div class="distribution-title">区域演练完成率</div>
<div class="progress-list">
<div class="progress-item" v-for="item in regionalDrillProgress" :key="item.region">
<span class="region-name">{{ item.region }}</span>
<div class="progress-bar-wrapper">
<div class="progress-bar" :style="{ width: item.percent, backgroundColor: '#10b981' }"></div>
</div>
<span class="progress-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" @click="openTrainingManagement">管理</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 regionalTrainingProgress" :key="item.region">
<span class="region-name">{{ item.region }}</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>
<!-- 区域选择弹窗 -->
<RegionSelector v-model="regionSelectorVisible" :modelSelected="selectedRegion" :regions="regionOption"
@change="onRegionChange" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Refresh } from '@element-plus/icons-vue'
import Echart from '@/components/Echart/src/Echart.vue'
import RegionSelector from '@/views/screen/components/RegionSelector.vue'
import { getTableList } from '@/api/design/report'
import type { EChartsOption } from 'echarts'
import dayjs from 'dayjs'
import {
getHiddenDangerManagementData,
getHiddenDangerManagementDataWeek,
getHiddenDangerManagementDataMonth,
getOutsourcingManagementData,
getRiskManagementData,
getHighRiskManagementData,
getEmergencyPlanManagementData,
getTrainingManagementData
} from '@/api'
defineOptions({ name: 'Home10' })
// 类型定义
interface RegionItem {
name: string
code: string
}
interface DistributionItem {
region?: string
level?: string
status?: string
count: number
percent?: string
color: string
}
interface AreaRiskItem {
area: string
low: number | string
general: number
moderate: number
major: number
}
const router = useRouter()
const route = useRoute()
// 区域选择相关
const currentView = ref('集团')
const regionSelectorVisible = ref(false)
const selectedRegion = ref('')
const regionOption = ref<RegionItem[]>([])
// 时间选择相关 - 默认当前月起止,如果路由中有日期范围参数则使用
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())
// 外协管理:/person/table/view/1959187451673116674
// 风险管理:
// fx/table/view/1978723750599790594
// 隐患管理:/fx/table/view/1963446160885366786
// 高危作业:/low/table/view/1964253329070571521
// 应急预案:/yayl/table/view/1966394259751907330
// 安全培训:/pxks/table/view/1968225010550091777
const openOutsourcingManagement = () => {
// 不打开新标签页
router.push('/person/table/view/1959187451673116674')
}
const openRiskManagement = () => {
router.push('/fx/table/view/1978723750599790594')
}
const openHiddenDangerManagement = () => {
router.push('/fx/table/view/1963446160885366786')
}
const openHighRiskManagement = () => {
router.push('/low/table/view/1964253329070571521')
}
const openEmergencyPlanManagement = () => {
router.push('/yayl/table/view/1966394259751907330')
}
const openTrainingManagement = () => {
router.push('/pxks/table/view/1968225010550091777')
}
// 外协管理数据
const outsourcingTotal = ref<number>(0) // 外协人员总数
const outsourcingDistribution = ref<DistributionItem[]>([])
// 区域颜色配置
const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899']
// 初始化外协管理数据
const initOutsourcingData = async () => {
try {
const response = await getOutsourcingManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
pageNo: 1,
pageSize: 10000
})
console.log('外协管理接口返回:', response)
// axios封装后response就是 { code: 0, data: {...}, msg: "" }
// 所以records在 response.data.records
const records = response?.records || []
if (records && records.length > 0) {
// 计算总人数从records中累加total
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
}
})
console.log('处理后的外协管理数据:', {
total: outsourcingTotal.value,
distribution: outsourcingDistribution.value
})
} else {
// 如果没有数据,重置为空
outsourcingTotal.value = 0
outsourcingDistribution.value = []
console.log('外协管理无数据')
}
} catch (error) {
console.error('获取外协管理数据失败:', error)
// 如果接口失败,重置为空
outsourcingTotal.value = 0
outsourcingDistribution.value = []
}
}
// 风险管理数据
const riskTotal = ref<number>(0) // 风险总数
const riskDistribution = ref<DistributionItem[]>([])
const areaRiskDistribution = ref<AreaRiskItem[]>([])
// 隐患管理数据
const hiddenDangerTrend = ref<Array<{ date: string; general: number; major: number }>>([])
interface AreaRectificationItem {
area: string
overdue: number
processing: number
processed: number
}
const areaRectificationStatus = ref<AreaRectificationItem[]>([])
// 高危作业数据
const highRiskTotal = ref<number>(0) // 高危作业总数
const operationTypeDistribution = ref<Array<{ type: string; count: number; percent: string; color: string }>>([]) // 作业类型分布(用于环形图)
const operationDistribution = ref<Array<{ region: string; count: number }>>([]) // 区域作业分布
// 作业类型颜色配置
const operationTypeColors: Record<string, string> = {
'动火作业': '#f59e0b',
'高处作业': '#8b5cf6',
'临时用电': '#3b82f6',
'有限空间': '#ec4899',
'动土作业': '#10b981',
'吊装作业': '#ef4444'
}
// 应急预案数据
const emergencyPlanTotal = ref<number>(0) // 应完成演练总数
const emergencyPlanCompleted = ref<number>(0) // 已完成演练总数
const regionalDrillProgress = ref<Array<{ region: string; percent: string }>>([
{ region: '华北区域', percent: '85%' },
{ region: '华东区域', percent: '75%' },
{ region: '华南区域', percent: '70%' }
])
// 安全培训数据
const trainingBarData = ref<{ regions: string[]; trainingCount: number[]; participants: number[] }>({
regions: [],
trainingCount: [],
participants: []
})
const regionalTrainingProgress = ref<Array<{ region: string; percent: string }>>([])
// 外协管理环形图配置
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
},
label: {
show: true,
position: 'center',
formatter: () => {
return `${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: () => {
return `${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
.filter(item => item.count > 0) // 只显示有数据的风险等级
.map(item => ({
value: item.count,
name: item.level,
itemStyle: { color: item.color }
}))
// 如果没有数据,显示空状态
if (chartData.length === 0 || riskTotal.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: () => `${riskTotal.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: () => `${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 ?? 0)
const majorData = hiddenDangerTrend.value.map(item => item.major ?? 0)
// 判断是否为空数据
const isEmpty = dates.length === 0 || (generalData.every(v => v === 0) && majorData.every(v => v === 0))
// 计算Y轴最大值向上取整到最近的10的倍数
const maxValue = isEmpty ? 10 : Math.max(
...generalData,
...majorData,
10 // 最小值为10避免图表显示过小
)
const yAxisMax = Math.ceil(maxValue / 10) * 10 || 10
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['一般隐患', '重大隐患'],
top: 0,
textStyle: {
fontSize: 12
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '12%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates.length > 0 ? dates : [],
show: !isEmpty
},
yAxis: {
type: 'value',
max: yAxisMax,
show: !isEmpty
},
graphic: isEmpty ? [
{
type: 'group',
left: 'center',
top: 'center',
children: [
{
type: 'text',
z: 100,
left: 'center',
top: 'center',
style: {
text: '暂无数据',
fontSize: 16,
fontWeight: 'normal',
fill: '#9ca3af',
textAlign: 'center'
}
}
]
}
] : [],
series: isEmpty ? [] : [
{
name: '一般隐患',
type: 'line',
smooth: true,
data: generalData,
itemStyle: { color: '#f59e0b' },
lineStyle: { color: '#f59e0b', width: 2 },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(245, 158, 11, 0.3)' },
{ offset: 1, color: 'rgba(245, 158, 11, 0.05)' }
]
}
}
},
{
name: '重大隐患',
type: 'line',
smooth: true,
data: majorData,
itemStyle: { color: '#ef4444' },
lineStyle: { color: '#ef4444', width: 2 },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(239, 68, 68, 0.3)' },
{ offset: 1, color: 'rgba(239, 68, 68, 0.05)' }
]
}
}
}
]
}
})
// 高危作业环形图配置
const highRiskChartOption = computed<EChartsOption>(() => {
const chartData = operationTypeDistribution.value
.filter(item => item.count > 0) // 只显示有数据的作业类型
.map(item => ({
value: item.count,
name: item.type,
itemStyle: { color: item.color }
}))
// 如果没有数据,显示空状态
if (chartData.length === 0 || highRiskTotal.value === 0) {
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '高危作业',
type: 'pie',
radius: ['45%', '65%'],
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'
},
data: [{ value: 1, name: '暂无数据', itemStyle: { color: '#e5e7eb' } }]
}]
}
}
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: chartData
}]
}
})
// 应急预案环形进度图配置
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'
},
emphasis: { label: { show: true, fontSize: 20, fontWeight: 'bold' } },
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 openRegionSelector = (): void => {
regionSelectorVisible.value = true
}
const onRegionChange = (item: RegionItem): void => {
selectedRegion.value = item.name
router.push({
path: '/region',
query: {
region: item.name,
regionCode: item.code,
sDate: dateRange.value[0],
eDate: dateRange.value[1]
}
})
}
const handleDateChange = async (value: string[] | null) => {
if (value) {
console.log('日期范围变化:', value)
await refreshData()
}
}
const refreshData = async () => {
initData()
}
// 初始化区域数据
const initRegionData = async () => {
try {
const { records } = await getTableList('park_info_list')
if (records && records.length > 0) {
// 去重region字段使用Map来确保唯一性
const regionMap = new Map<string, RegionItem>()
records.forEach((el: any) => {
if (!regionMap.has(el.region)) {
regionMap.set(el.region, {
name: el.region,
code: el.region_id
})
}
})
// 转换为数组
regionOption.value = Array.from(regionMap.values())
}
} catch (error) {
console.error('初始化区域数据失败:', error)
}
}
// 初始化风险管理数据
const initRiskData = async () => {
try {
const response = await getRiskManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
pageNo: 1,
pageSize: 10000
})
console.log('风险管理接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按风险等级分组统计,用于环形图
const levelMap = new Map<string, number>()
// 按区域和风险等级分组统计,用于表格
const areaLevelMap = new Map<string, { low: number; general: number; moderate: number; major: number }>()
records.forEach((item: any) => {
const level = item.name || ''
const area = item.area || ''
const count = Number(item.total || 0)
// 统计风险等级分布(用于环形图)
if (level) {
levelMap.set(level, (levelMap.get(level) || 0) + count)
}
// 统计区域风险分布(用于表格)
if (area) {
if (!areaLevelMap.has(area)) {
areaLevelMap.set(area, { low: 0, general: 0, moderate: 0, major: 0 })
}
const areaData = areaLevelMap.get(area)!
if (level === '低' || level === '低风险') {
areaData.low += count
} else if (level === '一般' || level === '一般风险') {
areaData.general += count
} else if (level === '较大' || level === '较大风险') {
areaData.moderate += count
} else if (level === '重大' || level === '重大风险') {
areaData.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
}
})
// 处理区域风险分布表
areaRiskDistribution.value = Array.from(areaLevelMap.entries())
.map(([area, data]) => ({
area,
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,
areaDistribution: areaRiskDistribution.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' }
]
areaRiskDistribution.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' }
]
areaRiskDistribution.value = []
}
}
// 根据日期范围判断应该调用哪个接口
const getHiddenDangerApi = (startDate: string, endDate: string) => {
const start = dayjs(startDate)
const end = dayjs(endDate)
const daysDiff = end.diff(start, 'day') + 1 // 包含起始和结束日期
// 如果日期范围 <= 7天使用"天"接口
if (daysDiff <= 7) {
return getHiddenDangerManagementData
}
// 如果日期范围 <= 30天使用"周"接口
else if (daysDiff <= 30) {
return getHiddenDangerManagementDataWeek
}
// 如果日期范围 > 30天使用"月"接口
else {
return getHiddenDangerManagementDataMonth
}
}
const initDangerData = async () => {
try {
// 根据日期范围选择接口
const apiFunc = getHiddenDangerApi(dateRange.value[0], dateRange.value[1])
const response = await apiFunc({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
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 areaStatusMap = new Map<string, { overdue: number; processing: number; processed: number }>()
records.forEach((item: any) => {
const dayname = item.dayName || item.dayname || ''
const level = item.name || ''
const status = item.status || ''
const area = 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 (area) {
if (!areaStatusMap.has(area)) {
areaStatusMap.set(area, { overdue: 0, processing: 0, processed: 0 })
}
const areaStatus = areaStatusMap.get(area)!
if (status === '已逾期') {
areaStatus.overdue += count
} else if (status === '处理中') {
areaStatus.processing += count
} else if (status === '已处理') {
areaStatus.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)
})
// 转换为区域整改状态数组
areaRectificationStatus.value = Array.from(areaStatusMap.entries())
.map(([area, status]) => ({
area,
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,
areaStatus: areaRectificationStatus.value
})
} else {
// 如果没有数据,重置为空
hiddenDangerTrend.value = []
areaRectificationStatus.value = []
console.log('隐患管理无数据')
}
} catch (error) {
console.error('获取隐患管理数据失败:', error)
// 如果接口失败,重置为空
hiddenDangerTrend.value = []
areaRectificationStatus.value = []
}
}
const initHighRiskData = async () => {
try {
const response = await getHighRiskManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
pageNo: 1,
pageSize: 10000
})
console.log('高危作业接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按作业类型分组统计,用于环形图
const typeMap = new Map<string, number>()
// 按区域分组统计,用于区域分布列表
const areaMap = new Map<string, number>()
records.forEach((item: any) => {
const itemType = item.item || ''
const area = item.area || ''
const count = Number(item.total || 0)
// 统计作业类型
if (itemType) {
typeMap.set(itemType, (typeMap.get(itemType) || 0) + count)
}
// 统计区域分布
if (area) {
areaMap.set(area, (areaMap.get(area) || 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) // 按数量降序排序
// 处理区域分布数据
operationDistribution.value = Array.from(areaMap.entries())
.map(([region, count]) => ({
region,
count
}))
.sort((a, b) => b.count - a.count) // 按数量降序排序
console.log('处理后的高危作业数据:', {
total: highRiskTotal.value,
typeDistribution: operationTypeDistribution.value,
areaDistribution: operationDistribution.value
})
} else {
// 如果没有数据,重置为空
highRiskTotal.value = 0
operationTypeDistribution.value = []
operationDistribution.value = []
console.log('高危作业无数据')
}
} catch (error) {
console.error('获取高危作业数据失败:', error)
// 如果接口失败,重置为空
highRiskTotal.value = 0
operationTypeDistribution.value = []
operationDistribution.value = []
}
}
const initEmergencyPlanData = async () => {
try {
const response = await getEmergencyPlanManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
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 areaMap = new Map<string, { total: number; completed: number }>()
records.forEach((item: any) => {
const area = item.area || ''
const count = Number(item.total || 0)
const status = item.status || ''
const isCompleted = status.includes('完成') || status.includes('已执行')
if (area) {
if (!areaMap.has(area)) {
areaMap.set(area, { total: 0, completed: 0 })
}
const areaData = areaMap.get(area)!
areaData.total += count
if (isCompleted) {
areaData.completed += count
}
}
})
// 转换为数组并计算完成率
regionalDrillProgress.value = Array.from(areaMap.entries())
.map(([region, data]) => {
const percent = data.total > 0
? ((data.completed / data.total) * 100).toFixed(0) + '%'
: '0%'
return { region, percent }
})
.sort((a, b) => {
// 按完成率降序排序
const percentA = parseFloat(a.percent)
const percentB = parseFloat(b.percent)
return percentB - percentA
})
console.log('处理后的应急预案数据:', {
total: emergencyPlanTotal.value,
completed: emergencyPlanCompleted.value,
regionalProgress: regionalDrillProgress.value
})
} else {
// 如果没有数据,重置为空
emergencyPlanTotal.value = 0
emergencyPlanCompleted.value = 0
regionalDrillProgress.value = []
console.log('应急预案无数据')
}
} catch (error) {
console.error('获取应急预案数据失败:', error)
// 如果接口失败,重置为空
emergencyPlanTotal.value = 0
emergencyPlanCompleted.value = 0
regionalDrillProgress.value = []
}
}
const initTrainingData = async () => {
try {
const response = await getTrainingManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
pageNo: 1,
pageSize: 10000
})
console.log('安全培训接口返回:', response)
const records = response?.records || []
if (records && records.length > 0) {
// 按区域分组统计
const areaMap = new Map<string, { trainingCount: number; participants: number }>()
records.forEach((item: any) => {
// 只统计有区域字段的记录
if (!item.area) {
return
}
const area = item.area
const trainingCount = Number(item.plannum || 0) // 计划数量作为培训次数
const participants = Number(item.exenum || 0) // 执行数量作为参与人次
if (!areaMap.has(area)) {
areaMap.set(area, { trainingCount: 0, participants: 0 })
}
const areaData = areaMap.get(area)!
areaData.trainingCount += trainingCount
areaData.participants += participants
})
// 转换为数组并排序
const areaDataArray = Array.from(areaMap.entries())
.map(([region, data]) => ({
region,
...data,
// 计算完成率(参与人次 / 培训次数 * 100%
percent: data.trainingCount > 0
? ((data.participants / data.trainingCount) * 100).toFixed(0) + '%'
: '0%'
}))
.sort((a, b) => {
// 按培训次数降序排序
return b.trainingCount - a.trainingCount
})
// 更新柱状图数据
trainingBarData.value = {
regions: areaDataArray.map(item => item.region),
trainingCount: areaDataArray.map(item => item.trainingCount),
participants: areaDataArray.map(item => item.participants)
}
// 更新区域培训完成率数据
regionalTrainingProgress.value = areaDataArray.map(item => ({
region: item.region,
percent: item.percent
}))
console.log('处理后的安全培训数据:', {
barData: trainingBarData.value,
regionalProgress: regionalTrainingProgress.value
})
} else {
// 如果没有数据,重置为空
trainingBarData.value = {
regions: [],
trainingCount: [],
participants: []
}
regionalTrainingProgress.value = []
console.log('安全培训无数据')
}
} catch (error) {
console.error('获取安全培训数据失败:', error)
// 如果接口失败,重置为空
trainingBarData.value = {
regions: [],
trainingCount: [],
participants: []
}
regionalTrainingProgress.value = []
}
}
const initData = async () => {
await initRegionData()
initOutsourcingData()
initRiskData()
initDangerData()
initHighRiskData()
initEmergencyPlanData()
initTrainingData()
}
onMounted(async () => {
initData()
})
</script>
<style lang="scss" scoped>
.dashboard-container {
width: 100%;
min-height: 100vh;
background: #f5f7fa;
padding: 20px;
box-sizing: border-box;
// 确保内容不会贴边
max-width: 100%;
overflow-x: hidden;
}
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
padding: 16px 24px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.header-left {
// flex: 1;
display: flex;
align-items: center;
.back-button {
display: inline-flex;
align-items: center;
padding: 8px 16px;
background: #409eff;
border: 1px solid #e5e7eb;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-size: 16px;
color: white;
user-select: none;
font-weight: 600;
gap: 5px;
&:hover {
background: #2563eb;
}
span {
font-size: 18px;
line-height: 1;
}
&:active {
transform: scale(0.98);
}
}
}
.header-title {
flex: 1;
text-align: center;
font-size: 20px;
font-weight: bold;
color: #1f2937;
margin: 0;
}
.header-right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
min-width: 0; // 防止flex子元素溢出
.date-range-wrapper {
:deep(.el-date-editor) {
width: 280px;
max-width: 100%;
}
}
.refresh-btn {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0; // 防止按钮被压缩
}
}
}
.content-container {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
box-sizing: border-box;
}
.card-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
width: 100%;
box-sizing: border-box;
@media (max-width: 1400px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: 16px;
}
}
@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) {
.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%;
}
}
.dashboard-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
.card-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: bold;
color: #1f2937;
.card-icon {
font-size: 18px;
&.warning {
color: #ef4444;
}
}
}
.manage-btn {
color: #3b82f6;
padding: 4px 8px;
font-size: 14px;
transition: color 0.3s;
&:hover {
color: #2563eb;
}
}
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
}
}
.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 {
height: 215px;
}
.progress-chart-wrapper {
height: 200px;
min-height: 200px;
}
.high-risk-top {
display: flex;
align-items: center;
gap: 15px;
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;
}
.region-distribution,
.risk-distribution-table,
.rectification-status-grid,
.operation-distribution,
.park-operation-distribution,
.regional-progress {
margin-top: 8px;
.distribution-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
span {
font-size: 14px;
font-weight: 600;
color: #374151;
}
}
.distribution-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.distribution-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6b7280;
padding: 4px 0;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.region-name {
flex: 1;
color: #374151;
}
.region-count {
color: #1f2937;
font-weight: 500;
margin-left: auto;
}
.region-percent {
color: #9ca3af;
margin-left: 4px;
}
}
}
// 网格样式
.rectification-status-grid {
.empty-data-tip {
text-align: center;
padding: 40px 0;
color: #9ca3af;
font-size: 14px;
}
.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-park-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;
}
}
}
// 风险管理网格样式(与隐患管理一致)
.risk-distribution-table {
.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-low {
color: #117cee;
}
&.status-general {
color: #fbde28;
}
&.status-moderate {
color: #ed740c;
}
&.status-major {
color: #df2a3f;
}
}
.grid-park-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-low {
color: #117cee;
}
&.status-general {
color: #fbde28;
}
&.status-moderate {
color: #ed740c;
}
&.status-major {
color: #df2a3f;
}
}
}
// 外协管理网格样式(与隐患管理一致)
.region-distribution {
.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;
color: #374151;
&.empty-header {
visibility: hidden;
}
}
.grid-park-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;
color: #1f2937;
&:last-child {
margin-bottom: 0;
}
}
}
.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;
}
.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%);
}
}
</style>