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

1168 lines
30 KiB
Vue
Raw Normal View History

2025-11-26 14:13:02 +08:00
<template>
2025-11-27 16:07:54 +08:00
<div class="dashboard-container">
<!-- 顶部标题栏 -->
<div class="header-container">
<div class="header-left">
2025-11-27 18:12:53 +08:00
<el-icon class="back-arrow" @click="returnToHeadquarters">
<ArrowLeft />
</el-icon>
2025-11-27 16:07:54 +08:00
<div class="back-button" @click="openRegionSelector">
{{ selectedPark || selectedRegion }}
<span>···</span>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
</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" />
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<el-button type="primary" :icon="Refresh" @click="refreshData" class="refresh-btn">
刷新数据
</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
</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>
外协管理
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<el-button type="text" class="manage-btn">管理</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="donut-chart-wrapper">
<Echart :options="outsourcingChartOption" width="100%" height="200px" />
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="region-distribution">
<div class="distribution-title">园区分布</div>
<div class="distribution-list">
<div class="distribution-item" v-for="item in outsourcingDistribution" :key="item.region">
<span class="dot" :style="{ backgroundColor: item.color }"></span>
<span class="region-name">{{ item.region }}</span>
<span class="region-count">{{ item.count }}</span>
<span class="region-percent">({{ item.percent }})</span>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 风险管理卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">风险管理</div>
<el-button type="text" class="manage-btn">管理</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="donut-chart-wrapper">
<Echart :options="riskChartOption" width="100%" height="200px" />
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="risk-distribution-table">
<div class="distribution-title">园区风险分布</div>
<div class="table-wrapper">
<table class="risk-table">
<thead>
<tr>
<th>园区</th>
<th></th>
<th>一般</th>
<th>较大</th>
<th>重大</th>
</tr>
</thead>
<tbody>
<tr v-for="item in parkRiskDistribution" :key="item.park">
<td>{{ item.park }}</td>
<td>{{ item.low || '' }}</td>
<td>{{ item.general }}</td>
<td>{{ item.moderate }}</td>
<td>{{ item.major }}</td>
</tr>
</tbody>
</table>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 隐患管理卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon warning"></span>
隐患管理
</div>
<el-button type="text" class="manage-btn">管理</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
<div class="line-chart-wrapper">
<Echart :options="hiddenDangerChartOption" width="100%" height="180px" />
</div>
<div class="rectification-status-table">
<div class="distribution-title">园区整改状态</div>
<div class="table-wrapper">
<table class="status-table">
<thead>
<tr>
<th>园区</th>
<th>已逾期</th>
<th>处理中</th>
<th>已处理</th>
</tr>
</thead>
<tbody>
<tr v-for="item in parkRectificationStatus" :key="item.park">
<td>{{ item.park }}</td>
<td class="overdue">{{ item.overdue }}</td>
<td class="processing">{{ item.processing }}</td>
<td class="processed">{{ item.processed }}</td>
</tr>
</tbody>
</table>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 第二行高危作业应急预案安全培训 -->
<div class="card-row">
<!-- 高危作业卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">高危作业</div>
<el-button type="text" class="manage-btn">管理</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
2025-11-27 18:12:53 +08:00
<div class="high-risk-top">
2025-11-27 16:07:54 +08:00
<div class="donut-chart-wrapper-small">
2025-11-27 18:12:53 +08:00
<Echart :options="highRiskChartOption" width="100%" height="250px" />
2025-11-27 16:07:54 +08:00
</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>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<div class="park-operation-distribution">
<div class="distribution-title">园区作业分布</div>
<div class="distribution-list">
<div class="distribution-item" v-for="item in parkOperationDistribution" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<span class="region-count">{{ item.count }}</span>
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<!-- 应急预案卡片 -->
<div class="dashboard-card">
<div class="card-header">
<div class="card-title">
<span class="card-icon">📄</span>
应急预案
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<el-button type="text" class="manage-btn">管理</el-button>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<div class="card-content">
2025-11-27 18:12:53 +08:00
<div class="emergency-plan-top">
2025-11-27 16:07:54 +08:00
<div class="progress-chart-wrapper">
2025-11-27 18:12:53 +08:00
<Echart :options="emergencyPlanChartOption" width="100%" height="250px" />
2025-11-27 16:07:54 +08:00
</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>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
<div class="regional-progress">
<div class="distribution-title">园区演练完成率</div>
<div class="progress-list">
<div class="progress-item" v-for="item in parkDrillProgress" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<div class="progress-bar-wrapper">
<div class="progress-bar" :style="{ width: item.percent, backgroundColor: '#10b981' }"></div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<span class="progress-percent">{{ item.percent }}</span>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
</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 parkTrainingProgress" :key="item.park">
<span class="region-name">{{ item.park }}</span>
<div class="progress-bar-wrapper">
<div class="progress-bar" :style="{ width: item.percent, backgroundColor: '#8b5cf6' }"></div>
2025-11-26 14:13:02 +08:00
</div>
2025-11-27 16:07:54 +08:00
<span class="progress-percent">{{ item.percent }}</span>
2025-11-26 14:13:02 +08:00
</div>
</div>
</div>
2025-11-27 16:07:54 +08:00
</div>
2025-11-26 14:13:02 +08:00
</div>
</div>
2025-11-27 16:07:54 +08:00
</div>
<!-- 区域选择弹窗 -->
<RegionSelector v-model="regionSelectorVisible" :modelSelected="selectedPark" :regions="regionOption"
@change="onRegionChange" />
2025-11-26 14:13:02 +08:00
</div>
</template>
2025-11-27 16:07:54 +08:00
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
2025-11-27 18:12:53 +08:00
import { Refresh, ArrowLeft } from '@element-plus/icons-vue'
2025-11-27 16:07:54 +08:00
import Echart from '@/components/Echart/src/Echart.vue'
import RegionSelector from '@/views/screen/components/RegionSelector.vue'
import { getTableList } from '@/api/design/report'
import type { EChartsOption } from 'echarts'
import dayjs from 'dayjs'
import { getOutsourcingManagementData } from '@/api'
defineOptions({ name: 'Home12' })
// 类型定义
interface RegionItem {
name: string
code: string
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
interface DistributionItem {
region?: string
park?: string
2025-11-27 18:12:53 +08:00
level?: string
type?: string
2025-11-27 16:07:54 +08:00
count: number
percent?: string
color: string
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
interface ParkRiskItem {
park: string
low: number | string
general: number
moderate: number
major: number
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
interface ParkRectificationItem {
park: string
overdue: number
processing: number
processed: number
}
const router = useRouter()
const route = useRoute()
// 区域选择相关 - 照抄regionScreen.vue的逻辑
const selectedRegion = ref<string>('')
const selectedPark = ref<string>('')
const regionSelectorVisible = ref<boolean>(false)
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 dateRange = ref(getCurrentMonthRange())
// 外协管理数据
const outsourcingTotal = ref<number>(0)
const outsourcingDistribution = ref<DistributionItem[]>([])
// 风险管理数据
const riskTotal = ref<number>(0)
const riskDistribution = ref<DistributionItem[]>([])
const parkRiskDistribution = ref<ParkRiskItem[]>([])
// 隐患管理数据
const hiddenDangerTrend = ref<any[]>([])
const parkRectificationStatus = ref<ParkRectificationItem[]>([])
// 高危作业数据
const highRiskTotal = ref<number>(0)
const operationTypeDistribution = ref<DistributionItem[]>([])
const parkOperationDistribution = ref<DistributionItem[]>([])
// 应急预案数据
const emergencyPlanTotal = ref<number>(0)
const emergencyPlanCompleted = ref<number>(0)
const parkDrillProgress = ref<DistributionItem[]>([])
// 安全培训数据
2025-11-27 18:12:53 +08:00
const trainingBarData = ref<{ regions: string[]; trainingCount: number[]; participants: number[] }>({
regions: [],
trainingCount: [],
participants: []
})
2025-11-27 16:07:54 +08:00
const parkTrainingProgress = ref<DistributionItem[]>([])
// 区域颜色配置
const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899']
// 初始化区域数据 - 照抄regionScreen.vue
const initRegionData = async () => {
try {
if (typeof route.query.region === 'string') {
selectedRegion.value = route.query.region
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
const { records } = await getTableList('park_info_list')
if (records && records.length > 0) {
// 根据regionCode过滤园区
const regionCode = route.query.regionCode as string
const regionMap = new Map()
records
.filter((el: any) => el.region_id == regionCode)
.forEach((el: any) => {
if (!regionMap.has(el.park_name)) {
regionMap.set(el.park_name, {
name: el.park_name,
code: el.park_code
})
}
})
regionOption.value = Array.from(regionMap.values())
// 默认选择第一个园区
if (regionOption.value.length > 0 && !selectedPark.value) {
selectedPark.value = regionOption.value[0].name
2025-11-26 14:13:02 +08:00
}
}
2025-11-27 16:07:54 +08:00
} catch (error) {
console.error('初始化区域数据失败:', error)
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
// 返回总部页面
const returnToHeadquarters = () => {
router.push({
path: '/index'
})
}
2025-11-27 16:07:54 +08:00
// 打开区域选择器
const openRegionSelector = (): void => {
regionSelectorVisible.value = true
}
// 区域选择变化 - 跳转到园区页面
const onRegionChange = (item: RegionItem): void => {
selectedPark.value = item.name
router.push({
2025-11-27 18:12:53 +08:00
path: '/park',
2025-11-27 16:07:54 +08:00
query: { region: selectedRegion.value, regionCode: route.query.regionCode, park: item.name, parkCode: item.code }
})
}
// 日期变化
const handleDateChange = () => {
refreshData()
}
// 刷新数据
const refreshData = () => {
initOutsourcingData()
initRiskData()
initHiddenDangerData()
initHighRiskData()
initEmergencyPlanData()
initSafetyTrainingData()
}
// 初始化外协管理数据
const initOutsourcingData = async () => {
try {
const response = await getOutsourcingManagementData({
sDate: dateRange.value[0],
eDate: dateRange.value[1],
pageNo: 1,
pageSize: 10000
})
const records = response?.records || []
if (records && records.length > 0) {
const total = records.reduce((sum: number, item: any) => {
return sum + Number(item.total || 0)
}, 0)
outsourcingTotal.value = total
outsourcingDistribution.value = records.map((item: any, index: number) => {
const count = Number(item.total || 0)
const percent = total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%'
const color = regionColors[index % regionColors.length]
return {
region: item.name,
count,
percent,
color
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
} else {
outsourcingTotal.value = 0
outsourcingDistribution.value = []
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
} catch (error) {
console.error('获取外协管理数据失败:', error)
outsourcingTotal.value = 0
outsourcingDistribution.value = []
}
}
// 初始化风险管理数据
const initRiskData = async () => {
// TODO: 调用风险管理API
riskTotal.value = 215
riskDistribution.value = [
{ level: '低风险', count: 0, percent: '0%', color: '#10b981' },
{ level: '一般风险', count: 115, percent: '53.5%', color: '#f59e0b' },
{ level: '较大风险', count: 51, percent: '23.7%', color: '#ef4444' },
{ level: '重大风险', count: 49, percent: '22.8%', color: '#dc2626' }
2025-11-26 14:13:02 +08:00
]
2025-11-27 16:07:54 +08:00
parkRiskDistribution.value = [
{ park: '雄安园区', low: '', general: 45, moderate: 28, major: 8 },
{ park: '重庆园区', low: '', general: 38, moderate: 22, major: 6 },
{ park: '北京园区', low: '', general: 32, moderate: 18, major: 5 }
]
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 初始化隐患管理数据
const initHiddenDangerData = async () => {
// TODO: 调用隐患管理API
hiddenDangerTrend.value = [
{ date: '16日', general: 16, major: 8 },
{ date: '18日', general: 25, major: 13 },
{ date: '20日', general: 31, major: 23 },
{ date: '22日', general: 18, major: 12 },
{ date: '24日', general: 28, major: 19 }
]
parkRectificationStatus.value = [
{ park: '雄安园区', overdue: 5, processing: 28, processed: 42 },
{ park: '重庆园区', overdue: 3, processing: 22, processed: 35 },
{ park: '北京园区', overdue: 2, processing: 15, processed: 28 }
]
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 初始化高危作业数据
const initHighRiskData = async () => {
// TODO: 调用高危作业API
highRiskTotal.value = 94
operationTypeDistribution.value = [
{ type: '动火作业', count: 35, percent: '37.2%', color: '#f59e0b' },
{ type: '高处作业', count: 22, percent: '23.4%', color: '#8b5cf6' },
{ type: '临时用电', count: 18, percent: '19.1%', color: '#3b82f6' },
{ type: '有限空间', count: 10, percent: '10.6%', color: '#ec4899' },
{ type: '动土作业', count: 6, percent: '6.4%', color: '#10b981' },
{ type: '吊装作业', count: 3, percent: '3.2%', color: '#ef4444' }
]
parkOperationDistribution.value = [
2025-11-27 18:12:53 +08:00
{ park: '雄安园区', count: 42, color: '#3b82f6' },
{ park: '重庆园区', count: 31, color: '#8b5cf6' },
{ park: '北京园区', count: 21, color: '#06b6d4' }
2025-11-27 16:07:54 +08:00
]
}
// 初始化应急预案数据
const initEmergencyPlanData = async () => {
// TODO: 调用应急预案API
emergencyPlanTotal.value = 36
emergencyPlanCompleted.value = 28
parkDrillProgress.value = [
2025-11-27 18:12:53 +08:00
{ park: '雄安园区', percent: '85%', color: '#10b981', count: 0 },
{ park: '重庆园区', percent: '78%', color: '#10b981', count: 0 },
{ park: '北京园区', percent: '70%', color: '#10b981', count: 0 }
2025-11-27 16:07:54 +08:00
]
}
// 初始化安全培训数据
const initSafetyTrainingData = async () => {
// TODO: 调用安全培训API
2025-11-27 18:12:53 +08:00
trainingBarData.value = {
regions: ['雄安园区', '重庆园区', '北京园区'],
trainingCount: [25, 15, 12],
participants: [12, 10, 8]
2025-11-26 14:13:02 +08:00
}
2025-11-27 18:12:53 +08:00
2025-11-27 16:07:54 +08:00
parkTrainingProgress.value = [
2025-11-27 18:12:53 +08:00
{ park: '雄安园区', percent: '92%', color: '#8b5cf6', count: 0 },
{ park: '重庆园区', percent: '88%', color: '#8b5cf6', count: 0 },
{ park: '北京园区', percent: '85%', color: '#8b5cf6', count: 0 }
2025-11-27 16:07:54 +08:00
]
}
// 外协管理环形图配置
const outsourcingChartOption = computed<EChartsOption>(() => {
const chartData = outsourcingDistribution.value.map(item => ({
value: item.count,
name: item.region,
itemStyle: { color: item.color }
}))
if (chartData.length === 0 || outsourcingTotal.value === 0) {
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '外协人员',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${outsourcingTotal.value}\n外协人员总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
data: [{ value: 1, name: '暂无数据', itemStyle: { color: '#e5e7eb' } }]
}]
}
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '外协人员',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${outsourcingTotal.value}\n外协人员总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
emphasis: { label: { show: true, fontSize: 18, fontWeight: 'bold' } },
data: chartData
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 风险管理环形图配置
const riskChartOption = computed<EChartsOption>(() => {
const chartData = riskDistribution.value.map(item => ({
value: item.count,
name: item.level,
itemStyle: { color: item.color }
}))
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '风险',
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${riskTotal.value}\n风险总数`,
fontSize: 16,
fontWeight: 'bold',
color: '#333'
},
emphasis: { label: { show: true, fontSize: 18, fontWeight: 'bold' } },
data: chartData
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 隐患管理折线图配置
const hiddenDangerChartOption = computed<EChartsOption>(() => {
const dates = hiddenDangerTrend.value.map(item => item.date)
const generalData = hiddenDangerTrend.value.map(item => item.general)
const majorData = hiddenDangerTrend.value.map(item => item.major)
return {
tooltip: { trigger: 'axis' },
legend: {
data: ['一般隐患', '重大隐患'],
top: 10
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: dates
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
yAxis: { type: 'value' },
series: [
{
name: '一般隐患',
type: 'line',
data: generalData,
itemStyle: { color: '#f59e0b' },
smooth: true
2025-11-26 14:13:02 +08:00
},
2025-11-27 16:07:54 +08:00
{
name: '重大隐患',
type: 'line',
data: majorData,
itemStyle: { color: '#ef4444' },
smooth: true
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
]
2025-11-26 14:13:02 +08:00
}
})
2025-11-27 16:07:54 +08:00
// 高危作业环形图配置
const highRiskChartOption = computed<EChartsOption>(() => {
return {
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
series: [{
name: '高危作业',
type: 'pie',
2025-11-27 18:12:53 +08:00
radius: ['60%', '75%'],
center: ['50%', '45%'],
2025-11-27 16:07:54 +08:00
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 },
label: {
show: true,
position: 'center',
formatter: () => `${highRiskTotal.value}\n本月作业`,
2025-11-27 18:12:53 +08:00
fontSize: 18,
2025-11-27 16:07:54 +08:00
fontWeight: 'bold',
color: '#333'
},
2025-11-27 18:12:53 +08:00
emphasis: { label: { show: true, fontSize: 20, fontWeight: 'bold' } },
2025-11-27 16:07:54 +08:00
data: operationTypeDistribution.value.map(item => ({
value: item.count,
name: item.type,
itemStyle: { color: item.color }
}))
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
// 应急预案环形图配置
const emergencyPlanChartOption = computed<EChartsOption>(() => {
const percent = emergencyPlanTotal.value > 0
? ((emergencyPlanCompleted.value / emergencyPlanTotal.value) * 100).toFixed(0)
: 0
return {
tooltip: { trigger: 'item' },
series: [{
name: '演练完成率',
type: 'pie',
2025-11-27 18:12:53 +08:00
radius: ['60%', '75%'],
center: ['50%', '45%'],
2025-11-27 16:07:54 +08:00
avoidLabelOverlap: false,
itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0, color: '#10b981' },
label: {
show: true,
position: 'center',
formatter: () => `${percent}%\n演练完成率`,
2025-11-27 18:12:53 +08:00
fontSize: 18,
2025-11-27 16:07:54 +08:00
fontWeight: 'bold',
color: '#333'
},
data: [
{ value: emergencyPlanCompleted.value, name: '已完成', itemStyle: { color: '#10b981' } },
{ value: emergencyPlanTotal.value - emergencyPlanCompleted.value, name: '未完成', itemStyle: { color: '#e5e7eb' } }
]
}]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 安全培训柱状图配置
const safetyTrainingChartOption = computed<EChartsOption>(() => {
return {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: ['培训次数', '参与人次'],
top: 10
},
2025-11-27 18:12:53 +08:00
grid: { left: '6%', right: '4%', bottom: '8%', containLabel: true },
2025-11-27 16:07:54 +08:00
xAxis: {
type: 'category',
2025-11-27 18:12:53 +08:00
data: trainingBarData.value.regions,
axisLine: { lineStyle: { color: '#d1d5db' } }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#d1d5db' } },
splitLine: { lineStyle: { color: '#f3f4f6' } }
2025-11-27 16:07:54 +08:00
},
series: [
{
name: '培训次数',
type: 'bar',
2025-11-27 18:12:53 +08:00
data: trainingBarData.value.trainingCount,
barWidth: 24,
itemStyle: { color: '#8b5cf6' },
label: { show: true, position: 'insideBottom', color: '#fff' }
2025-11-27 16:07:54 +08:00
},
{
name: '参与人次',
type: 'bar',
2025-11-27 18:12:53 +08:00
data: trainingBarData.value.participants,
barWidth: 24,
itemStyle: { color: '#c4b5fd' },
label: { show: true, position: 'top', color: '#7c3aed' }
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
]
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
})
// 初始化数据
const initData = async () => {
await initRegionData()
await initOutsourcingData()
await initRiskData()
await initHiddenDangerData()
await initHighRiskData()
await initEmergencyPlanData()
await initSafetyTrainingData()
2025-11-26 14:13:02 +08:00
}
onMounted(() => {
2025-11-27 16:07:54 +08:00
initData()
2025-11-26 14:13:02 +08:00
})
</script>
<style lang="scss" scoped>
2025-11-27 16:07:54 +08:00
.dashboard-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
overflow-x: hidden;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-left {
display: flex;
align-items: center;
2025-11-27 18:12:53 +08:00
gap: 10px;
}
.back-arrow {
font-size: 20px;
color: #3b82f6;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
color: #2563eb;
transform: translateX(-2px);
}
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.back-button {
padding: 8px 16px;
background: #3b82f6;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
span {
font-size: 18px;
line-height: 1;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.header-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin: 0;
flex: 1;
text-align: center;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.header-right {
display: flex;
align-items: center;
gap: 15px;
min-width: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.date-range-wrapper {
:deep(.el-date-editor) {
max-width: 100%;
2025-11-26 14:13:02 +08:00
}
}
2025-11-27 16:07:54 +08:00
.refresh-btn {
flex-shrink: 0;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.content-container {
width: 100%;
box-sizing: border-box;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
width: 100%;
box-sizing: border-box;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.dashboard-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
display: flex;
align-items: center;
gap: 8px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-icon {
font-size: 18px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.manage-btn {
color: #3b82f6;
padding: 0;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.card-content {
display: flex;
flex-direction: column;
gap: 15px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.donut-chart-wrapper,
.donut-chart-wrapper-small {
height: 200px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.line-chart-wrapper,
.bar-chart-wrapper {
height: 180px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.progress-chart-wrapper {
height: 180px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.region-distribution,
.risk-distribution-table,
.rectification-status-table,
.park-operation-distribution,
.regional-progress {
margin-top: 10px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-title {
font-size: 14px;
2025-11-26 14:13:02 +08:00
font-weight: bold;
2025-11-27 16:07:54 +08:00
color: #333;
margin-bottom: 10px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-list {
display: flex;
flex-direction: column;
gap: 8px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.distribution-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.region-name {
flex: 1;
color: #666;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.region-count {
color: #333;
font-weight: 500;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.region-percent {
color: #999;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.table-wrapper {
overflow-x: auto;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.risk-table,
.status-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
thead {
background-color: #f9fafb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
th {
font-weight: bold;
color: #333;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
td {
color: #666;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.status-table {
.overdue {
color: #ef4444;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.processing {
color: #f59e0b;
}
.processed {
color: #10b981;
}
}
2025-11-27 18:12:53 +08:00
.high-risk-top {
2025-11-27 16:07:54 +08:00
display: flex;
2025-11-27 18:12:53 +08:00
align-items: center;
2025-11-27 16:07:54 +08:00
gap: 15px;
margin-bottom: 15px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
.donut-chart-wrapper-small {
flex: 1.5;
min-width: 0;
display: flex;
align-items: center;
}
2025-11-27 16:07:54 +08:00
.operation-type-list {
flex: 1;
display: flex;
flex-direction: column;
2025-11-27 18:12:53 +08:00
gap: 8px;
min-width: 0;
justify-content: center;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-type-item {
display: flex;
align-items: center;
gap: 8px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-name {
2025-11-27 18:12:53 +08:00
width: 70px;
font-size: 12px;
2025-11-27 16:07:54 +08:00
color: #666;
flex-shrink: 0;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.operation-bar-wrapper {
flex: 1;
height: 8px;
background-color: #e5e7eb;
border-radius: 4px;
overflow: hidden;
2025-11-26 14:13:02 +08:00
}
2025-11-27 16:07:54 +08:00
.operation-bar {
height: 100%;
border-radius: 4px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 18:12:53 +08:00
.emergency-plan-top {
2025-11-27 16:07:54 +08:00
display: flex;
2025-11-27 18:12:53 +08:00
align-items: center;
2025-11-27 16:07:54 +08:00
gap: 15px;
margin-bottom: 15px;
2025-11-26 14:13:02 +08:00
}
2025-11-27 18:12:53 +08:00
.progress-chart-wrapper {
flex: 1.5;
min-width: 0;
display: flex;
align-items: center;
}
2025-11-27 16:07:54 +08:00
.drill-info {
display: flex;
flex-direction: column;
2025-11-27 18:12:53 +08:00
gap: 8px;
2025-11-27 16:07:54 +08:00
flex: 1;
2025-11-27 18:12:53 +08:00
min-width: 0;
justify-content: center;
2025-11-27 16:07:54 +08:00
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.drill-item {
display: flex;
justify-content: space-between;
align-items: center;
2025-11-27 18:12:53 +08:00
padding: 8px 10px;
2025-11-27 16:07:54 +08:00
background-color: #f0fdf4;
border-radius: 4px;
2025-11-27 18:12:53 +08:00
font-size: 12px;
2025-11-27 16:07:54 +08:00
color: #666;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.drill-number {
font-size: 16px;
font-weight: bold;
color: #10b981;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-list {
display: flex;
flex-direction: column;
gap: 10px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-item {
display: flex;
align-items: center;
gap: 10px;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-bar-wrapper {
flex: 1;
height: 8px;
background-color: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
2025-11-26 14:13:02 +08:00
2025-11-27 16:07:54 +08:00
.progress-bar {
height: 100%;
border-radius: 4px;
}
.progress-percent {
font-size: 13px;
color: #666;
min-width: 40px;
text-align: right;
}
@media (max-width: 1400px) {
.card-row {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.card-row {
grid-template-columns: 1fr;
}
.header-container {
flex-direction: column;
gap: 15px;
}
.header-title {
text-align: center;
}
.high-risk-content,
.emergency-plan-content {
flex-direction: column;
2025-11-26 14:13:02 +08:00
}
}
</style>