From 8b9affd8e85d6fb83a9a326349ed357036b828fd Mon Sep 17 00:00:00 2001 From: chenlin Date: Sat, 29 Nov 2025 14:44:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9C=8B=E6=9D=BF=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 117 ++- src/views/Home/Index10.vue | 1392 ++++++++++++++++++++++++++++-------- src/views/Home/Index12.vue | 1199 +++++++++++++++++++++++++------ src/views/Home/Index13.vue | 1119 +++++++++++++++++++++++------ 4 files changed, 3065 insertions(+), 762 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index a5572c5..9f98693 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,121 @@ import request from '@/config/axios' -// 获取外协管理数据 +// 获取集团外协管理数据 export const getOutsourcingManagementData = (data) => { return request.post({ url: '/jeelowcode/report-data/list/kb_wx_jt', data }) } + +// 获取集团风险管理数据 +export const getRiskManagementData = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_fx_jt', data }) +} + +// 数据看板-隐患管理-集团(天) /jeelowcode/report-data/list/kb_yh_jt_d +export const getHiddenDangerManagementData = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_jt_d', data }) +} + +// 数据看板-隐患管理-集团(周) /jeelowcode/report-data/list/kb_yh_jt_w +export const getHiddenDangerManagementDataWeek = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_jt_w', data }) +} + +// 数据看板-隐患管理-集团(月) /jeelowcode/report-data/list/kb_yh_jt_m +export const getHiddenDangerManagementDataMonth = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_jt_m', data }) +} + +// 数据看板-高危作业-集团 /jeelowcode/report-data/list/kb_gw_jt +export const getHighRiskManagementData = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_gw_jt', data }) +} + +// 数据看板-应急预案-集团 /jeelowcode/report-data/list/kb_yj_jt +export const getEmergencyPlanManagementData = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yj_jt', data }) +} + +// 数据看板-培训-集团 /jeelowcode/report-data/list/kb_px_jt +export const getTrainingManagementData = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_px_jt', data }) +} + +// 数据看板-外协管理-区域 /jeelowcode/report-data/list/kb_wx_qy +export const getOutsourcingManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_wx_qy', data }) +} + +// 数据看板-外协管理-园区 /jeelowcode/report-data/list/kb_wx_yq +export const getOutsourcingManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_wx_yq', data }) +} + +// 数据看板-风险管理-区域 /jeelowcode/report-data/list/kb_fx_qy +export const getRiskManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_fx_qy', data }) +} + +// 数据看板-风险管理-园区 /jeelowcode/report-data/list/kb_fx_yq +export const getRiskManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_fx_yq', data }) +} + +// 数据看板-隐患管理-区域(天) /jeelowcode/report-data/list/kb_yh_qy_d +export const getHiddenDangerManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_qy_d', data }) +} + +// 数据看板-隐患管理-区域(周) /jeelowcode/report-data/list/kb_yh_qy_w +export const getHiddenDangerManagementDataRegionWeek = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_qy_w', data }) +} + +// 数据看板-隐患管理-区域(月) /jeelowcode/report-data/list/kb_yh_qy_m +export const getHiddenDangerManagementDataRegionMonth = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_qy_m', data }) +} + +// 数据看板-隐患管理-园区(天) +export const getHiddenDangerManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_gw_qy_d', data }) +} + +// 数据看板-隐患管理-园区(周) /jeelowcode/report-data/list/kb_yh_yq_w +export const getHiddenDangerManagementDataParkWeek = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_yq_w', data }) +} + +// 数据看板-隐患管理-园区(月) /jeelowcode/report-data/list/kb_yh_yq_m +export const getHiddenDangerManagementDataParkMonth = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yh_yq_m', data }) +} + +// 数据看板-高危作业-区域 /jeelowcode/report-data/list/kb_gw_dq +export const getHighRiskManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_gw_dq', data }) +} + +// 数据看板-高危作业-园区 /jeelowcode/report-data/list/kb_gw_yq +export const getHighRiskManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_gw_yq', data }) +} + +// 数据看板-应急预案-区域 /jeelowcode/report-data/list/kb_yj_dq +export const getEmergencyPlanManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yj_dq', data }) +} + +// 数据看板-应急预案-园区 /jeelowcode/report-data/list/kb_yj_yq +export const getEmergencyPlanManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_yj_yq', data }) +} + +// 数据看板-安全培训-区域 /jeelowcode/report-data/list/kb_px_qy +export const getTrainingManagementDataRegion = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_px_qy', data }) +} + +// 数据看板-安全培训-园区 /jeelowcode/report-data/list/kb_px_yq +export const getTrainingManagementDataPark = (data) => { + return request.post({ url: '/jeelowcode/report-data/list/kb_px_yq', data }) +} \ No newline at end of file diff --git a/src/views/Home/Index10.vue b/src/views/Home/Index10.vue index 0404ffc..99eeb16 100644 --- a/src/views/Home/Index10.vue +++ b/src/views/Home/Index10.vue @@ -111,10 +111,22 @@ 管理
-
- +
+
+ +
+
+
+ + {{ item.type }} +
+
+
+ {{ item.count }}项 +
+
-
+
区域作业分布
@@ -136,17 +148,19 @@ 管理
-
- -
-
-
- 应完成演练 - 76次 +
+
+
-
- 已完成演练 - 59次 +
+
+ 应完成演练 + {{ emergencyPlanTotal }}次 +
+
+ 已完成演练 + {{ emergencyPlanCompleted }}次 +
@@ -183,7 +197,7 @@
{{ item.region }}
-
+
{{ item.percent }}
@@ -202,14 +216,23 @@ @@ -953,19 +1597,102 @@ onMounted(async () => { } .donut-chart-wrapper, +.donut-chart-wrapper-small { + height: 250px; +} + .line-chart-wrapper, -.bar-chart-wrapper, +.bar-chart-wrapper { + height: 200px; + background: linear-gradient(180deg, #faf9ff 0%, #ffffff 100%); + border-radius: 8px; + padding: 8px; + box-sizing: border-box; +} + .progress-chart-wrapper { + height: 250px; +} + +.high-risk-top { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.donut-chart-wrapper-small { flex: 1; - min-height: 200px; + min-width: 0; + display: flex; + align-items: center; +} + +.operation-type-list { + flex: 1.2; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + justify-content: center; +} + +.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: 15px; + margin-bottom: 15px; +} + +.progress-chart-wrapper { + flex: 1.5; + min-width: 0; + display: flex; + align-items: center; } .region-distribution, .risk-distribution, .rectification-status, .operation-distribution, +.park-operation-distribution, .regional-progress { - margin-top: 16px; + margin-top: 10px; .distribution-title { font-size: 14px; @@ -1019,35 +1746,34 @@ onMounted(async () => { .drill-info { display: flex; - gap: 16px; - margin-top: 16px; - padding: 12px; - background: #f0fdf4; - border-radius: 6px; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 0; + justify-content: center; +} - .drill-item { - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; +.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; +} - span:first-child { - font-size: 12px; - color: #6b7280; - } - - .drill-number { - font-size: 18px; - font-weight: bold; - color: #059669; - } - } +.drill-number { + font-size: 16px; + font-weight: bold; + color: #10b981; } .progress-list { display: flex; flex-direction: column; - gap: 12px; + gap: 14px; margin-top: 12px; } @@ -1056,33 +1782,67 @@ onMounted(async () => { align-items: center; gap: 12px; font-size: 13px; + padding: 4px 0; .region-name { - width: 80px; + width: 90px; color: #374151; flex-shrink: 0; + font-weight: 500; } .progress-bar-wrapper { flex: 1; - height: 8px; - background: #e5e7eb; - border-radius: 4px; + 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: 4px; - transition: width 0.3s ease; + 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 { - width: 50px; + min-width: 45px; text-align: right; - color: #1f2937; - font-weight: 500; + color: #7c3aed; + font-weight: 600; flex-shrink: 0; + font-size: 13px; + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); } } diff --git a/src/views/Home/Index12.vue b/src/views/Home/Index12.vue index 3228141..b08d125 100644 --- a/src/views/Home/Index12.vue +++ b/src/views/Home/Index12.vue @@ -6,22 +6,22 @@ -
- {{ selectedPark || selectedRegion }} +
+ {{ selectedRegion }} ···
-
+

区域视角数据看板

-
+
刷新数据 -
-
+
+
@@ -39,7 +39,7 @@
-
+
园区分布
@@ -48,11 +48,11 @@ {{ item.region }} {{ item.count }}人 ({{ item.percent }}) -
-
+
+
+
-
@@ -63,7 +63,7 @@
-
+
园区风险分布
@@ -87,10 +87,10 @@ -
+
- - + +
@@ -98,39 +98,49 @@
⚠️ 隐患管理 -
+
管理 - +
-
-
+
+
园区整改状态
-
- - - - - - - - - - - - - - - - - -
园区已逾期处理中已处理
{{ item.park }}{{ item.overdue }}{{ item.processing }}{{ item.processed }}
+
+ +
+
+
+ {{ item.park }} +
+
+ +
+
已逾期
+
+ {{ item.overdue }} +
+
+ +
+
处理中
+
+ {{ item.processing }} +
+
+ +
+
已处理
+
+ {{ item.processed }} +
+
+
+
+
- - -
@@ -144,28 +154,29 @@
-
+
{{ item.type }}
-
-
-
+ {{ item.count }}项 + + +
园区作业分布
{{ item.park }} {{ item.count }}项 +
+
+
- - -
@@ -173,39 +184,39 @@
📄 应急预案 -
+
管理 - +
-
+
应完成演练 {{ emergencyPlanTotal }}次 -
+
已完成演练 {{ emergencyPlanCompleted }}次 -
- + +
园区演练完成率
{{ item.park }}
-
+
{{ item.percent }}
+
-
@@ -213,33 +224,33 @@
📚 安全培训 -
+
管理 - +
-
+
园区培训完成率
{{ item.park }}
-
-
+
+
{{ item.percent }} -
+
+ + - - - + - - + + @@ -248,16 +259,25 @@ 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 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' +import { + getOutsourcingManagementDataRegion, + getHighRiskManagementDataRegion, + getEmergencyPlanManagementDataRegion, + getTrainingManagementDataRegion, + getRiskManagementDataRegion, + getHiddenDangerManagementDataRegion, + getHiddenDangerManagementDataRegionWeek, + getHiddenDangerManagementDataRegionMonth +} from '@/api' +import RegionSelector from '@/views/screen/components/RegionSelector.vue' +import { getTableList } from '@/api/design/report' defineOptions({ name: 'Home12' }) // 类型定义 -interface RegionItem { +interface ParkItem { name: string code: string } @@ -290,19 +310,28 @@ interface ParkRectificationItem { const router = useRouter() const route = useRoute() -// 区域选择相关 - 照抄regionScreen.vue的逻辑 +// 区域和园区选择相关 const selectedRegion = ref('') const selectedPark = ref('') -const regionSelectorVisible = ref(false) -const regionOption = ref([]) +const parkSelectorVisible = ref(false) +const parkOption = ref([]) -// 时间选择相关 - 默认当前月起止 +// 时间选择相关 - 默认当前月起止,如果路由中有日期范围参数则使用 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 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(0) @@ -338,37 +367,43 @@ const parkTrainingProgress = ref([]) // 区域颜色配置 const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899'] -// 初始化区域数据 - 照抄regionScreen.vue +// 作业类型颜色配置 +const operationTypeColors: Record = { + '动火作业': '#f59e0b', + '高处作业': '#8b5cf6', + '临时用电': '#3b82f6', + '有限空间': '#ec4899', + '动土作业': '#10b981', + '吊装作业': '#ef4444' +} + +// 初始化区域数据 - 加载园区列表 const initRegionData = async () => { try { if (typeof route.query.region === 'string') { selectedRegion.value = route.query.region } + // 加载园区列表 const { records } = await getTableList('park_info_list') if (records && records.length > 0) { // 根据regionCode过滤园区 const regionCode = route.query.regionCode as string - const regionMap = new Map() + const parkMap = new Map() records .filter((el: any) => el.region_id == regionCode) .forEach((el: any) => { - if (!regionMap.has(el.park_name)) { - regionMap.set(el.park_name, { + if (!parkMap.has(el.park_name)) { + parkMap.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 - } + parkOption.value = Array.from(parkMap.values()) } } catch (error) { console.error('初始化区域数据失败:', error) @@ -378,24 +413,37 @@ const initRegionData = async () => { // 返回总部页面 const returnToHeadquarters = () => { router.push({ - path: '/index' + path: '/index', + query: { + sDate: dateRange.value[0], + eDate: dateRange.value[1] + } }) } -// 打开区域选择器 -const openRegionSelector = (): void => { - regionSelectorVisible.value = true +// 打开园区选择器 +const openParkSelector = (): void => { + parkSelectorVisible.value = true } -// 区域选择变化 - 跳转到园区页面 -const onRegionChange = (item: RegionItem): void => { +// 园区选择变化 - 跳转到园区页面 +const onParkChange = (item: ParkItem): void => { selectedPark.value = item.name + parkSelectorVisible.value = false router.push({ path: '/park', - query: { region: selectedRegion.value, regionCode: route.query.regionCode, park: item.name, parkCode: item.code } + query: { + region: selectedRegion.value, + regionCode: route.query.regionCode as string, + park: item.name, + parkCode: item.code, + sDate: dateRange.value[0], + eDate: dateRange.value[1] + } }) } + // 日期变化 const handleDateChange = () => { refreshData() @@ -403,24 +451,22 @@ const handleDateChange = () => { // 刷新数据 const refreshData = () => { - initOutsourcingData() - initRiskData() - initHiddenDangerData() - initHighRiskData() - initEmergencyPlanData() - initSafetyTrainingData() + initData() } // 初始化外协管理数据 const initOutsourcingData = async () => { try { - const response = await getOutsourcingManagementData({ + const response = await getOutsourcingManagementDataRegion({ sDate: dateRange.value[0], eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, pageNo: 1, pageSize: 10000 }) + console.log('区域外协管理接口返回:', response) + const records = response?.records || [] if (records && records.length > 0) { @@ -442,12 +488,18 @@ const initOutsourcingData = async () => { color } }) + + console.log('处理后的区域外协管理数据:', { + total: outsourcingTotal.value, + distribution: outsourcingDistribution.value + }) } else { outsourcingTotal.value = 0 outsourcingDistribution.value = [] + console.log('区域外协管理无数据') } } catch (error) { - console.error('获取外协管理数据失败:', error) + console.error('获取区域外协管理数据失败:', error) outsourcingTotal.value = 0 outsourcingDistribution.value = [] } @@ -455,86 +507,509 @@ const initOutsourcingData = async () => { // 初始化风险管理数据 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' } - ] + try { + const response = await getRiskManagementDataRegion({ + sDate: dateRange.value[0], + eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, + pageNo: 1, + pageSize: 10000 + }) + + console.log('区域风险管理接口返回:', response) + + const records = response?.records || [] + + if (records && records.length > 0) { + // 按风险等级分组统计,用于环形图 + const levelMap = new Map() + + // 按园区和风险等级分组统计,用于表格 + const parkLevelMap = new Map() + + records.forEach((item: any) => { + const level = item.name || '' + const park = item.area || '' + const count = Number(item.total || 0) + + // 统计风险等级分布 + if (level) { + levelMap.set(level, (levelMap.get(level) || 0) + count) + } + + // 统计园区风险分布 + if (park) { + if (!parkLevelMap.has(park)) { + parkLevelMap.set(park, { low: 0, general: 0, moderate: 0, major: 0 }) + } + const parkData = parkLevelMap.get(park)! + if (level === '低' || level === '低风险') { + parkData.low += count + } else if (level === '一般' || level === '一般风险') { + parkData.general += count + } else if (level === '较大' || level === '较大风险') { + parkData.moderate += count + } else if (level === '重大' || level === '重大风险') { + parkData.major += count + } + } + }) + + // 计算总数 + const total = Array.from(levelMap.values()).reduce((sum, count) => sum + count, 0) + riskTotal.value = total + + // 处理风险等级分布数据(用于环形图) + const allLevels = [ + { key: '低', level: '低风险', count: 0, percent: '0%', color: '#10b981' }, + { key: '一般', level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' }, + { key: '较大', level: '较大风险', count: 0, percent: '0%', color: '#ef4444' }, + { key: '重大', level: '重大风险', count: 0, percent: '0%', color: '#dc2626' } + ] + + riskDistribution.value = allLevels.map(defaultItem => { + const count = levelMap.get(defaultItem.key) || levelMap.get(defaultItem.level) || 0 + const percent = total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%' + return { + level: defaultItem.level, + count, + percent, + color: defaultItem.color + } + }) + + // 处理园区风险分布表 + parkRiskDistribution.value = Array.from(parkLevelMap.entries()) + .map(([park, data]) => ({ + park, + low: data.low > 0 ? data.low : '', + general: data.general, + moderate: data.moderate, + major: data.major + })) + .sort((a, b) => { + // 按总数降序排序 + const totalA = (typeof a.low === 'number' ? a.low : 0) + a.general + a.moderate + a.major + const totalB = (typeof b.low === 'number' ? b.low : 0) + b.general + b.moderate + b.major + return totalB - totalA + }) + + console.log('处理后的区域风险管理数据:', { + total: riskTotal.value, + levelDistribution: riskDistribution.value, + parkDistribution: parkRiskDistribution.value + }) + } else { + riskTotal.value = 0 + riskDistribution.value = [ + { level: '低风险', count: 0, percent: '0%', color: '#10b981' }, + { level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' }, + { level: '较大风险', count: 0, percent: '0%', color: '#ef4444' }, + { level: '重大风险', count: 0, percent: '0%', color: '#dc2626' } + ] + parkRiskDistribution.value = [] + console.log('区域风险管理无数据') + } + } catch (error) { + console.error('获取区域风险管理数据失败:', error) + riskTotal.value = 0 + riskDistribution.value = [ + { level: '低风险', count: 0, percent: '0%', color: '#10b981' }, + { level: '一般风险', count: 0, percent: '0%', color: '#f59e0b' }, + { level: '较大风险', count: 0, percent: '0%', color: '#ef4444' }, + { level: '重大风险', count: 0, percent: '0%', color: '#dc2626' } + ] + parkRiskDistribution.value = [] + } +} + +// 根据日期范围选择隐患管理接口 +const getHiddenDangerApiRegion = (startDate: string, endDate: string) => { + const start = dayjs(startDate) + const end = dayjs(endDate) + const daysDiff = end.diff(start, 'day') + 1 - 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 } - ] + if (daysDiff <= 7) { + return getHiddenDangerManagementDataRegion + } else if (daysDiff <= 30) { + return getHiddenDangerManagementDataRegionWeek + } else { + return getHiddenDangerManagementDataRegionMonth + } } // 初始化隐患管理数据 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 } - ] + try { + // 根据日期范围选择接口 + const apiFunc = getHiddenDangerApiRegion(dateRange.value[0], dateRange.value[1]) + + const response = await apiFunc({ + sDate: dateRange.value[0], + eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, + pageNo: 1, + pageSize: 10000 + }) + + console.log('区域隐患管理接口返回:', response) + + const records = response?.records || [] + + if (records && records.length > 0) { + // 按日期和等级分组统计,用于折线图 + const trendMap = new Map() + + // 按园区和状态分组统计,用于整改状态表格 + const parkStatusMap = new Map() + + records.forEach((item: any) => { + const dayname = item.dayname || '' + const level = item.name || '' + const status = item.status || '' + const park = item.area || '' + const count = Number(item.total || 0) + + // 统计趋势数据(按日期和等级) + if (dayname) { + if (!trendMap.has(dayname)) { + trendMap.set(dayname, { general: 0, major: 0 }) + } + const trend = trendMap.get(dayname)! + if (level === '一般' || level === '一般隐患') { + trend.general += count + } else if (level === '重大' || level === '重大隐患') { + trend.major += count + } + } + + // 统计园区整改状态数据 + if (park) { + if (!parkStatusMap.has(park)) { + parkStatusMap.set(park, { overdue: 0, processing: 0, processed: 0 }) + } + const parkStatus = parkStatusMap.get(park)! + if (status === '已逾期') { + parkStatus.overdue += count + } else if (status === '处理中') { + parkStatus.processing += count + } else if (status === '已处理') { + parkStatus.processed += count + } + } + }) + + // 转换为数组并按日期排序 + hiddenDangerTrend.value = Array.from(trendMap.entries()) + .map(([date, counts]) => ({ date, ...counts })) + .sort((a, b) => { + // 处理日期排序:如果是"10月"这种格式,提取月份数字 + const numA = parseInt(a.date.replace(/[^0-9]/g, '')) || 0 + const numB = parseInt(b.date.replace(/[^0-9]/g, '')) || 0 + // 如果是"日"格式(如"10日"),也处理 + const dayA = parseInt(a.date.replace('日', '')) || 0 + const dayB = parseInt(b.date.replace('日', '')) || 0 + return (numA || dayA) - (numB || dayB) + }) + + // 转换为园区整改状态数组 + parkRectificationStatus.value = Array.from(parkStatusMap.entries()) + .map(([park, status]) => ({ + park, + overdue: status.overdue, + processing: status.processing, + processed: status.processed + })) + .sort((a, b) => { + // 按总数降序排序 + const totalA = a.overdue + a.processing + a.processed + const totalB = b.overdue + b.processing + b.processed + return totalB - totalA + }) + + console.log('处理后的区域隐患管理数据:', { + trend: hiddenDangerTrend.value, + parkStatus: parkRectificationStatus.value + }) + } else { + hiddenDangerTrend.value = [] + parkRectificationStatus.value = [] + console.log('区域隐患管理无数据') + } + } catch (error) { + console.error('获取区域隐患管理数据失败:', error) + hiddenDangerTrend.value = [] + parkRectificationStatus.value = [] + } } // 初始化高危作业数据 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 = [ - { park: '雄安园区', count: 42, color: '#3b82f6' }, - { park: '重庆园区', count: 31, color: '#8b5cf6' }, - { park: '北京园区', count: 21, color: '#06b6d4' } - ] + try { + const response = await getHighRiskManagementDataRegion({ + sDate: dateRange.value[0], + eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, + pageNo: 1, + pageSize: 10000 + }) + + console.log('区域高危作业接口返回:', response) + + const records = response?.records || [] + + if (records && records.length > 0) { + // 按作业类型分组统计,用于环形图 + const typeMap = new Map() + + // 按园区分组统计,用于园区分布列表 + const parkMap = new Map() + + records.forEach((item: any) => { + const itemType = item.item || '' + const park = item.area || item.park || '' + const count = Number(item.total || 0) + + // 统计作业类型 + if (itemType) { + typeMap.set(itemType, (typeMap.get(itemType) || 0) + count) + } + + // 统计园区分布 + if (park) { + parkMap.set(park, (parkMap.get(park) || 0) + count) + } + }) + + // 计算总数 + const total = Array.from(typeMap.values()).reduce((sum, count) => sum + count, 0) + highRiskTotal.value = total + + // 处理作业类型分布数据 + operationTypeDistribution.value = Array.from(typeMap.entries()) + .map(([type, count]) => ({ + type, + count, + percent: total > 0 ? ((count / total) * 100).toFixed(1) + '%' : '0%', + color: operationTypeColors[type] || '#9ca3af' + })) + .sort((a, b) => b.count - a.count) + + // 处理园区分布数据 + parkOperationDistribution.value = Array.from(parkMap.entries()) + .map(([park, count], index) => ({ + region: park, + park: park, + count, + color: regionColors[index % regionColors.length] + })) + .sort((a, b) => b.count - a.count) + + console.log('处理后的区域高危作业数据:', { + total: highRiskTotal.value, + typeDistribution: operationTypeDistribution.value, + parkDistribution: parkOperationDistribution.value + }) + } else { + highRiskTotal.value = 0 + operationTypeDistribution.value = [] + parkOperationDistribution.value = [] + console.log('区域高危作业无数据') + } + } catch (error) { + console.error('获取区域高危作业数据失败:', error) + highRiskTotal.value = 0 + operationTypeDistribution.value = [] + parkOperationDistribution.value = [] + } } // 初始化应急预案数据 const initEmergencyPlanData = async () => { - // TODO: 调用应急预案API - emergencyPlanTotal.value = 36 - emergencyPlanCompleted.value = 28 - parkDrillProgress.value = [ - { park: '雄安园区', percent: '85%', color: '#10b981', count: 0 }, - { park: '重庆园区', percent: '78%', color: '#10b981', count: 0 }, - { park: '北京园区', percent: '70%', color: '#10b981', count: 0 } - ] + try { + const response = await getEmergencyPlanManagementDataRegion({ + sDate: dateRange.value[0], + eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, + pageNo: 1, + pageSize: 10000 + }) + + console.log('区域应急预案接口返回:', response) + + const records = response?.records || [] + + if (records && records.length > 0) { + // 统计应完成演练总数(所有记录的total总和) + const total = records.reduce((sum: number, item: any) => { + return sum + Number(item.total || 0) + }, 0) + + emergencyPlanTotal.value = total + + // 统计已完成演练数(根据状态判断) + const completedCount = records.reduce((sum: number, item: any) => { + const status = item.status || '' + const count = Number(item.total || 0) + if (status.includes('完成') || status.includes('已执行')) { + return sum + count + } + return sum + }, 0) + + emergencyPlanCompleted.value = completedCount + + // 按园区统计演练完成率 + const parkMap = new Map() + + records.forEach((item: any) => { + const park = item.area || item.park || '' + const count = Number(item.total || 0) + const status = item.status || '' + const isCompleted = status.includes('完成') || status.includes('已执行') + + if (park) { + if (!parkMap.has(park)) { + parkMap.set(park, { total: 0, completed: 0 }) + } + const parkData = parkMap.get(park)! + parkData.total += count + if (isCompleted) { + parkData.completed += count + } + } + }) + + // 转换为数组并计算完成率 + parkDrillProgress.value = Array.from(parkMap.entries()) + .map(([park, data], index) => { + const percent = data.total > 0 + ? ((data.completed / data.total) * 100).toFixed(0) + '%' + : '0%' + return { + region: park, + park: park, + percent, + count: 0, + color: regionColors[index % regionColors.length] + } + }) + .sort((a, b) => { + const percentA = parseFloat(a.percent) + const percentB = parseFloat(b.percent) + return percentB - percentA + }) + + console.log('处理后的区域应急预案数据:', { + total: emergencyPlanTotal.value, + completed: emergencyPlanCompleted.value, + parkProgress: parkDrillProgress.value + }) + } else { + emergencyPlanTotal.value = 0 + emergencyPlanCompleted.value = 0 + parkDrillProgress.value = [] + console.log('区域应急预案无数据') + } + } catch (error) { + console.error('获取区域应急预案数据失败:', error) + emergencyPlanTotal.value = 0 + emergencyPlanCompleted.value = 0 + parkDrillProgress.value = [] + } } // 初始化安全培训数据 const initSafetyTrainingData = async () => { - // TODO: 调用安全培训API - trainingBarData.value = { - regions: ['雄安园区', '重庆园区', '北京园区'], - trainingCount: [25, 15, 12], - participants: [12, 10, 8] + try { + const response = await getTrainingManagementDataRegion({ + sDate: dateRange.value[0], + eDate: dateRange.value[1], + regiodId: route.query.regionCode as string, + pageNo: 1, + pageSize: 10000 + }) + + console.log('区域安全培训接口返回:', response) + + const records = response?.records || [] + + if (records && records.length > 0) { + // 按园区分组统计 + const parkMap = new Map() + + records.forEach((item: any) => { + // 只统计有园区字段的记录 + const park = item.area || item.park || '' + if (!park) { + return + } + + const trainingCount = Number(item.plannum || 0) // 计划数量作为培训次数 + const participants = Number(item.exenum || 0) // 执行数量作为参与人次 + + if (!parkMap.has(park)) { + parkMap.set(park, { trainingCount: 0, participants: 0 }) + } + const parkData = parkMap.get(park)! + parkData.trainingCount += trainingCount + parkData.participants += participants + }) + + // 转换为数组并排序 + const parkDataArray = Array.from(parkMap.entries()) + .map(([park, data], index) => ({ + park, + ...data, + // 计算完成率(参与人次 / 培训次数 * 100%) + percent: data.trainingCount > 0 + ? ((data.participants / data.trainingCount) * 100).toFixed(0) + '%' + : '0%', + color: regionColors[index % regionColors.length] + })) + .sort((a, b) => { + // 按培训次数降序排序 + return b.trainingCount - a.trainingCount + }) + + // 更新柱状图数据 + trainingBarData.value = { + regions: parkDataArray.map(item => item.park), + trainingCount: parkDataArray.map(item => item.trainingCount), + participants: parkDataArray.map(item => item.participants) + } + + // 更新园区培训完成率数据 + parkTrainingProgress.value = parkDataArray.map(item => ({ + region: item.park, + park: item.park, + percent: item.percent, + count: 0, + color: item.color + })) + + console.log('处理后的区域安全培训数据:', { + barData: trainingBarData.value, + parkProgress: parkTrainingProgress.value + }) + } else { + trainingBarData.value = { + regions: [], + trainingCount: [], + participants: [] + } + parkTrainingProgress.value = [] + console.log('区域安全培训无数据') + } + } catch (error) { + console.error('获取区域安全培训数据失败:', error) + trainingBarData.value = { + regions: [], + trainingCount: [], + participants: [] + } + parkTrainingProgress.value = [] } - - parkTrainingProgress.value = [ - { park: '雄安园区', percent: '92%', color: '#8b5cf6', count: 0 }, - { park: '重庆园区', percent: '88%', color: '#8b5cf6', count: 0 }, - { park: '北京园区', percent: '85%', color: '#8b5cf6', count: 0 } - ] } // 外协管理环形图配置 @@ -555,7 +1030,7 @@ const outsourcingChartOption = computed(() => { center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 }, - label: { + label: { show: true, position: 'center', formatter: () => `${outsourcingTotal.value}\n外协人员总数`, @@ -667,14 +1142,14 @@ const highRiskChartOption = computed(() => { series: [{ name: '高危作业', type: 'pie', - radius: ['60%', '75%'], - center: ['50%', '45%'], + radius: ['49%', '61%'], + center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 }, label: { show: true, position: 'center', - formatter: () => `${highRiskTotal.value}\n本月作业`, + formatter: () => `${highRiskTotal.value}\n累计作业`, fontSize: 18, fontWeight: 'bold', color: '#333' @@ -700,8 +1175,8 @@ const emergencyPlanChartOption = computed(() => { series: [{ name: '演练完成率', type: 'pie', - radius: ['60%', '75%'], - center: ['50%', '45%'], + radius: ['49%', '61%'], + center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0, color: '#10b981' }, label: { @@ -722,39 +1197,153 @@ const emergencyPlanChartOption = computed(() => { // 安全培训柱状图配置 const safetyTrainingChartOption = computed(() => { + const regions = trainingBarData.value.regions || [] + const trainingCount = trainingBarData.value.trainingCount || [] + const participants = trainingBarData.value.participants || [] + + // 计算Y轴最大值(向上取整到最近的10的倍数) + const maxValue = Math.max( + ...trainingCount, + ...participants, + 10 // 最小值为10,避免图表显示过小 + ) + const yAxisMax = Math.ceil(maxValue / 10) * 10 || 10 + return { - tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: (params: any) => { + let result = `${params[0].axisValue}
` + params.forEach((item: any) => { + result += `${item.marker}${item.seriesName}: ${item.value}
` + }) + return result + } + }, legend: { data: ['培训次数', '参与人次'], - top: 10 + top: 10, + textStyle: { + fontSize: 12, + color: '#666' + }, + itemGap: 20 }, - grid: { left: '6%', right: '4%', bottom: '8%', containLabel: true }, - xAxis: { - type: 'category', - data: trainingBarData.value.regions, - axisLine: { lineStyle: { color: '#d1d5db' } } + 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', - axisLine: { lineStyle: { color: '#d1d5db' } }, - splitLine: { lineStyle: { color: '#f3f4f6' } } + max: yAxisMax, + axisLine: { + show: false + }, + axisTick: { + show: false + }, + axisLabel: { + color: '#666', + fontSize: 12 + }, + splitLine: { + lineStyle: { + color: '#f3f4f6', + type: 'dashed' + } + } }, series: [ { name: '培训次数', type: 'bar', - data: trainingBarData.value.trainingCount, - barWidth: 24, - itemStyle: { color: '#8b5cf6' }, - label: { show: true, position: 'insideBottom', color: '#fff' } + data: trainingCount.length > 0 ? trainingCount : [], + barWidth: 32, + itemStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: '#8b5cf6' }, + { offset: 1, color: '#7c3aed' } + ] + }, + borderRadius: [6, 6, 0, 0] + }, + label: { + show: true, + position: 'insideTop', + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + formatter: (params: any) => { + return params.value > 0 ? params.value : '' + } + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(139, 92, 246, 0.5)' + } + } }, { name: '参与人次', - type: 'bar', - data: trainingBarData.value.participants, - barWidth: 24, - itemStyle: { color: '#c4b5fd' }, - label: { show: true, position: 'top', color: '#7c3aed' } + type: 'bar', + data: participants.length > 0 ? participants : [], + barWidth: 32, + itemStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: '#c4b5fd' }, + { offset: 1, color: '#a78bfa' } + ] + }, + borderRadius: [6, 6, 0, 0] + }, + label: { + show: true, + position: 'top', + color: '#7c3aed', + fontSize: 12, + fontWeight: 'bold', + formatter: (params: any) => { + return params.value > 0 ? params.value : '' + } + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(196, 181, 253, 0.5)' + } + } } ] } @@ -763,12 +1352,12 @@ const safetyTrainingChartOption = computed(() => { // 初始化数据 const initData = async () => { await initRegionData() - await initOutsourcingData() - await initRiskData() - await initHiddenDangerData() - await initHighRiskData() - await initEmergencyPlanData() - await initSafetyTrainingData() + initOutsourcingData() + initRiskData() + initHiddenDangerData() + initHighRiskData() + initEmergencyPlanData() + initSafetyTrainingData() } onMounted(() => { @@ -814,16 +1403,23 @@ onMounted(() => { } } -.back-button { +.region-name-clickable { + font-size: 16px; + font-weight: 600; + color: #333; padding: 8px 16px; background: #3b82f6; color: white; border-radius: 4px; cursor: pointer; - font-size: 14px; display: flex; align-items: center; gap: 5px; + transition: all 0.3s ease; + + &:hover { + background: #2563eb; + } span { font-size: 18px; @@ -849,7 +1445,7 @@ onMounted(() => { .date-range-wrapper { :deep(.el-date-editor) { - max-width: 100%; + max-width: 100%; } } @@ -913,21 +1509,25 @@ onMounted(() => { .donut-chart-wrapper, .donut-chart-wrapper-small { - height: 200px; + height: 250px; } .line-chart-wrapper, .bar-chart-wrapper { - height: 180px; + height: 200px; + background: linear-gradient(180deg, #faf9ff 0%, #ffffff 100%); + border-radius: 8px; + padding: 8px; + box-sizing: border-box; } .progress-chart-wrapper { - height: 180px; + height: 250px; } .region-distribution, .risk-distribution-table, -.rectification-status-table, +.rectification-status-grid, .park-operation-distribution, .regional-progress { margin-top: 10px; @@ -995,7 +1595,7 @@ onMounted(() => { } th { - font-weight: bold; + font-weight: bold; color: #333; } @@ -1004,17 +1604,93 @@ onMounted(() => { } } -.status-table { - .overdue { - color: #ef4444; +// 九宫格样式 +.rectification-status-grid { + .grid-wrapper { + display: flex; + gap: 0; + justify-content: flex-start; } - .processing { - color: #f59e0b; + .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; + } } - .processed { - color: #10b981; + .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; + } } } @@ -1026,14 +1702,14 @@ onMounted(() => { } .donut-chart-wrapper-small { - flex: 1.5; + flex: 1; min-width: 0; display: flex; align-items: center; } .operation-type-list { - flex: 1; + flex: 1.2; display: flex; flex-direction: column; gap: 8px; @@ -1048,7 +1724,7 @@ onMounted(() => { } .operation-name { - width: 70px; + width: 55px; font-size: 12px; color: #666; flex-shrink: 0; @@ -1056,15 +1732,24 @@ onMounted(() => { .operation-bar-wrapper { flex: 1; - height: 8px; + height: 12px; background-color: #e5e7eb; - border-radius: 4px; + border-radius: 6px; overflow: hidden; } .operation-bar { height: 100%; - border-radius: 4px; + 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 { @@ -1110,33 +1795,77 @@ onMounted(() => { .progress-list { display: flex; flex-direction: column; - gap: 10px; + gap: 14px; + margin-top: 12px; } .progress-item { display: flex; align-items: center; - gap: 10px; -} - -.progress-bar-wrapper { - flex: 1; - height: 8px; - background-color: #e5e7eb; - border-radius: 4px; - overflow: hidden; -} - -.progress-bar { - height: 100%; - border-radius: 4px; -} - -.progress-percent { + gap: 12px; font-size: 13px; - color: #666; - min-width: 40px; - text-align: right; + 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) { diff --git a/src/views/Home/Index13.vue b/src/views/Home/Index13.vue index 0bc01ab..bdef826 100644 --- a/src/views/Home/Index13.vue +++ b/src/views/Home/Index13.vue @@ -7,18 +7,18 @@
{{ selectedPark }}
- +

园区视角数据看板

-
+
刷新数据 - - + +
@@ -32,11 +32,11 @@ 外协管理
管理 - +
-
+
供应商分布
@@ -45,11 +45,11 @@ {{ item.supplier }} {{ item.count }}人 ({{ item.percent }}) -
-
- + + +
@@ -60,7 +60,7 @@
-
+
地点风险分布
@@ -84,10 +84,10 @@ -
+
- - + +
@@ -95,39 +95,49 @@
⚠️ 隐患管理 -
+
管理 - +
-
-
+
+
所属公司整改状态
-
- - - - - - - - - - - - - - - - - -
所属公司已逾期处理中已处理
{{ item.company }}{{ item.overdue }}{{ item.processing }}{{ item.processed }}
+
+ +
+
+
+ {{ item.company }} +
+
+ +
+
已逾期
+
+ {{ item.overdue }} +
+
+ +
+
处理中
+
+ {{ item.processing }} +
+
+ +
+
已处理
+
+ {{ item.processed }} +
+
+
+
+
- - -
@@ -141,28 +151,29 @@
-
+
{{ item.type }}
-
-
-
+ {{ item.count }}项 + + +
申请公司作业分布
{{ item.company }} {{ item.count }}项 +
+
+
- - -
@@ -170,25 +181,25 @@
📄 应急预案 -
+
管理 - +
-
+
应完成演练 {{ emergencyPlanTotal }}次 -
+
已完成演练 {{ emergencyPlanCompleted }}次 -
- + +
近期演练详情
@@ -199,9 +210,9 @@
{{ item.status }} -
+ @@ -212,29 +223,29 @@
📚 安全培训 -
+ 管理 - +
-
+
所属公司培训完成率
{{ item.company }}
-
-
+
+
{{ item.percent }} -
+
+ - - - + + @@ -245,7 +256,16 @@ 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 { getOutsourcingManagementData } from '@/api' +import { + getOutsourcingManagementDataPark, + getHighRiskManagementDataPark, + getEmergencyPlanManagementDataPark, + getTrainingManagementDataPark, + getRiskManagementDataPark, + getHiddenDangerManagementDataPark, + getHiddenDangerManagementDataParkWeek, + getHiddenDangerManagementDataParkMonth +} from '@/api' defineOptions({ name: 'Home13' }) @@ -289,13 +309,22 @@ const route = useRoute() // 园区名称 - 从路由参数获取 const selectedPark = ref('') -// 时间选择相关 - 默认当前月起止 +// 时间选择相关 - 默认当前月起止,如果路由中有日期范围参数则使用 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 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(0) @@ -321,12 +350,26 @@ const emergencyPlanCompleted = ref(0) const recentDrillDetails = ref([]) // 安全培训数据 -const trainingKLineData = ref<{ company: string; values: number[] }[]>([]) +const trainingBarData = ref<{ regions: string[]; trainingCount: number[]; participants: number[] }>({ + regions: [], + trainingCount: [], + participants: [] +}) const companyTrainingProgress = ref([]) // 区域颜色配置 const regionColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899'] +// 作业类型颜色配置 +const operationTypeColors: Record = { + '动火作业': '#f59e0b', + '高处作业': '#8b5cf6', + '临时用电': '#3b82f6', + '有限空间': '#ec4899', + '动土作业': '#10b981', + '吊装作业': '#ef4444' +} + // 初始化园区名称 const initParkName = () => { if (typeof route.query.park === 'string') { @@ -340,7 +383,12 @@ const initParkName = () => { const returnToRegion = () => { router.push({ path: '/region', - query: { region: route.query.region, regionCode: route.query.regionCode } + query: { + region: route.query.region, + regionCode: route.query.regionCode, + sDate: dateRange.value[0], + eDate: dateRange.value[1] + } }) } @@ -351,24 +399,23 @@ const handleDateChange = () => { // 刷新数据 const refreshData = () => { - initOutsourcingData() - initRiskData() - initHiddenDangerData() - initHighRiskData() - initEmergencyPlanData() - initSafetyTrainingData() + initData() } // 初始化外协管理数据 const initOutsourcingData = async () => { try { - const response = await getOutsourcingManagementData({ + 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) { @@ -390,12 +437,18 @@ const initOutsourcingData = async () => { color } }) + + console.log('处理后的园区外协管理数据:', { + total: outsourcingTotal.value, + distribution: supplierDistribution.value + }) } else { outsourcingTotal.value = 0 supplierDistribution.value = [] + console.log('园区外协管理无数据') } } catch (error) { - console.error('获取外协管理数据失败:', error) + console.error('获取园区外协管理数据失败:', error) outsourcingTotal.value = 0 supplierDistribution.value = [] } @@ -403,86 +456,481 @@ const initOutsourcingData = async () => { // 初始化风险管理数据 const initRiskData = async () => { - // TODO: 调用风险管理API - riskTotal.value = 92 - riskDistribution.value = [ - { level: '低风险', count: 0, percent: '0%', color: '#10b981' }, - { level: '一般风险', count: 61, percent: '66.3%', color: '#f59e0b' }, - { level: '较大风险', count: 20, percent: '21.7%', color: '#ef4444' }, - { level: '重大风险', count: 11, percent: '12.0%', color: '#dc2626' } - ] + 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() + + // 按地点和风险等级分组统计,用于表格 + const locationLevelMap = new Map() + + 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 - locationRiskDistribution.value = [ - { location: '办公区', low: '', general: 28, moderate: 15, major: 5 }, - { location: '停车场', low: '', general: 18, moderate: 12, major: 3 }, - { location: '仓储区', low: '', general: 15, moderate: 8, major: 2 } - ] + if (daysDiff <= 7) { + return getHiddenDangerManagementDataPark + } else if (daysDiff <= 30) { + return getHiddenDangerManagementDataParkWeek + } else { + return getHiddenDangerManagementDataParkMonth + } } // 初始化隐患管理数据 const initHiddenDangerData = async () => { - // TODO: 调用隐患管理API - hiddenDangerTrend.value = [ - { date: '16日', general: 17, major: 10 }, - { date: '18日', general: 25, major: 13 }, - { date: '20日', general: 31, major: 25 }, - { date: '22日', general: 20, major: 13 }, - { date: '24日', general: 28, major: 20 } - ] - - companyRectificationStatus.value = [ - { company: 'A供应商', overdue: 3, processing: 15, processed: 28 }, - { company: 'B供应商', overdue: 2, processing: 10, processed: 18 }, - { company: 'C供应商', overdue: 1, processing: 8, processed: 12 } - ] + 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() + + // 按公司和状态分组统计,用于整改状态表格 + const companyStatusMap = new Map() + + 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 () => { - // TODO: 调用高危作业API - highRiskTotal.value = 42 - operationTypeDistribution.value = [ - { type: '动火作业', count: 16, percent: '38.1%', color: '#f59e0b' }, - { type: '高处作业', count: 12, percent: '28.6%', color: '#8b5cf6' }, - { type: '临时用电', count: 8, percent: '19.0%', color: '#3b82f6' }, - { type: '有限空间', count: 4, percent: '9.5%', color: '#ec4899' }, - { type: '动土作业', count: 1, percent: '2.4%', color: '#10b981' }, - { type: '吊装作业', count: 1, percent: '2.4%', color: '#ef4444' } - ] - - companyOperationDistribution.value = [ - { company: 'A供应商', count: 22, color: '#3b82f6' }, - { company: 'B供应商', count: 15, color: '#8b5cf6' }, - { company: 'C供应商', count: 5, color: '#06b6d4' } - ] + 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() + + // 按供应商/公司分组统计,用于供应商分布列表 + const companyMap = new Map() + + 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 () => { - // TODO: 调用应急预案API - emergencyPlanTotal.value = 16 - emergencyPlanCompleted.value = 12 - recentDrillDetails.value = [ - { id: '1', name: '消防应急演练', date: '2025-10-15', status: '已完成' }, - { id: '2', name: '化学品泄漏演练', date: '2025-10-20', status: '已完成' }, - { id: '3', name: '停电应急演练', date: '2025-10-28', status: '计划中' } - ] + 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 () => { - // TODO: 调用安全培训API - trainingKLineData.value = [ - { company: 'A供应商', values: [18, 28, 15, 32] }, - { company: 'B供应商', values: [16, 24, 12, 26] }, - { company: 'C供应商', values: [14, 22, 10, 24] } - ] - - companyTrainingProgress.value = [ - { company: 'A供应商', percent: '92%', color: '#8b5cf6', count: 0 }, - { company: 'B供应商', percent: '88%', color: '#8b5cf6', count: 0 }, - { company: 'C供应商', percent: '85%', color: '#8b5cf6', count: 0 } - ] + 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() + + 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 = [] + } } // 外协管理环形图配置 @@ -503,7 +951,7 @@ const outsourcingChartOption = computed(() => { center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 }, - label: { + label: { show: true, position: 'center', formatter: () => `${outsourcingTotal.value}\n外协人员总数`, @@ -615,14 +1063,14 @@ const highRiskChartOption = computed(() => { series: [{ name: '高危作业', type: 'pie', - radius: ['60%', '75%'], - center: ['50%', '45%'], + radius: ['49%', '61%'], + center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0 }, label: { show: true, position: 'center', - formatter: () => `${highRiskTotal.value}\n本月作业`, + formatter: () => `${highRiskTotal.value}\n累计作业`, fontSize: 18, fontWeight: 'bold', color: '#333' @@ -648,8 +1096,8 @@ const emergencyPlanChartOption = computed(() => { series: [{ name: '演练完成率', type: 'pie', - radius: ['60%', '75%'], - center: ['50%', '45%'], + radius: ['49%', '61%'], + center: ['50%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 0, borderColor: 'transparent', borderWidth: 0, color: '#10b981' }, label: { @@ -668,37 +1116,154 @@ const emergencyPlanChartOption = computed(() => { } }) -// 安全培训K线图配置 +// 安全培训柱状图配置 const safetyTrainingChartOption = computed(() => { - const categories = trainingKLineData.value.map((item) => item.company) - const kData = trainingKLineData.value.map((item) => item.values) - + const regions = trainingBarData.value.regions || [] + const trainingCount = trainingBarData.value.trainingCount || [] + const participants = trainingBarData.value.participants || [] + + // 计算Y轴最大值(向上取整到最近的10的倍数) + const maxValue = Math.max( + ...trainingCount, + ...participants, + 10 // 最小值为10,避免图表显示过小 + ) + const yAxisMax = Math.ceil(maxValue / 10) * 10 || 10 + return { - tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } }, - legend: { data: ['培训走势K线'], top: 10 }, - grid: { left: '8%', right: '4%', bottom: '8%', containLabel: true }, - xAxis: { - type: 'category', - data: categories, - boundaryGap: true, - axisLine: { lineStyle: { color: '#d1d5db' } } + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: (params: any) => { + let result = `${params[0].axisValue}
` + params.forEach((item: any) => { + result += `${item.marker}${item.seriesName}: ${item.value}
` + }) + 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', - scale: true, - axisLine: { lineStyle: { color: '#d1d5db' } }, - splitLine: { lineStyle: { color: '#f3f4f6' } } + max: yAxisMax, + axisLine: { + show: false + }, + axisTick: { + show: false + }, + axisLabel: { + color: '#666', + fontSize: 12 + }, + splitLine: { + lineStyle: { + color: '#f3f4f6', + type: 'dashed' + } + } }, series: [ { - name: '培训走势K线', - type: 'candlestick', - data: kData, + name: '培训次数', + type: 'bar', + data: trainingCount.length > 0 ? trainingCount : [], + barWidth: 32, itemStyle: { - color: '#8b5cf6', - color0: '#c4b5fd', - borderColor: '#6d28d9', - borderColor0: '#a78bfa' + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: '#8b5cf6' }, + { offset: 1, color: '#7c3aed' } + ] + }, + borderRadius: [6, 6, 0, 0] + }, + label: { + show: true, + position: 'insideTop', + color: '#fff', + fontSize: 12, + fontWeight: 'bold', + formatter: (params: any) => { + return params.value > 0 ? params.value : '' + } + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(139, 92, 246, 0.5)' + } + } + }, + { + name: '参与人次', + type: 'bar', + data: participants.length > 0 ? participants : [], + barWidth: 32, + itemStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: '#c4b5fd' }, + { offset: 1, color: '#a78bfa' } + ] + }, + borderRadius: [6, 6, 0, 0] + }, + label: { + show: true, + position: 'top', + color: '#7c3aed', + fontSize: 12, + fontWeight: 'bold', + formatter: (params: any) => { + return params.value > 0 ? params.value : '' + } + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(196, 181, 253, 0.5)' + } } } ] @@ -708,12 +1273,12 @@ const safetyTrainingChartOption = computed(() => { // 初始化数据 const initData = async () => { initParkName() - await initOutsourcingData() - await initRiskData() - await initHiddenDangerData() - await initHighRiskData() - await initEmergencyPlanData() - await initSafetyTrainingData() + initOutsourcingData() + initRiskData() + initHiddenDangerData() + initHighRiskData() + initEmergencyPlanData() + initSafetyTrainingData() } onMounted(() => { @@ -786,7 +1351,7 @@ onMounted(() => { .date-range-wrapper { :deep(.el-date-editor) { - max-width: 100%; + max-width: 100%; } } @@ -850,12 +1415,16 @@ onMounted(() => { .donut-chart-wrapper, .donut-chart-wrapper-small { - height: 200px; + height: 250px; } .line-chart-wrapper, .bar-chart-wrapper { - height: 180px; + height: 200px; + background: linear-gradient(180deg, #faf9ff 0%, #ffffff 100%); + border-radius: 8px; + padding: 8px; + box-sizing: border-box; } .progress-chart-wrapper { @@ -864,7 +1433,7 @@ onMounted(() => { .region-distribution, .risk-distribution-table, -.rectification-status-table, +.rectification-status-grid, .park-operation-distribution, .recent-drill-details, .regional-progress { @@ -942,17 +1511,94 @@ onMounted(() => { } } -.status-table { - .overdue { - color: #ef4444; +// 九宫格样式 +// 九宫格样式 +.rectification-status-grid { + .grid-wrapper { + display: flex; + gap: 0; + justify-content: flex-start; } - .processing { - color: #f59e0b; + .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; + } } - .processed { - color: #10b981; + .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; + } } } @@ -964,14 +1610,14 @@ onMounted(() => { } .donut-chart-wrapper-small { - flex: 1.5; + flex: 1; min-width: 0; display: flex; align-items: center; } .operation-type-list { - flex: 1; + flex: 1.2; display: flex; flex-direction: column; gap: 8px; @@ -986,7 +1632,7 @@ onMounted(() => { } .operation-name { - width: 70px; + width: 55px; font-size: 12px; color: #666; flex-shrink: 0; @@ -994,15 +1640,24 @@ onMounted(() => { .operation-bar-wrapper { flex: 1; - height: 8px; + height: 12px; background-color: #e5e7eb; - border-radius: 4px; + border-radius: 6px; overflow: hidden; } .operation-bar { height: 100%; - border-radius: 4px; + 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 { @@ -1096,33 +1751,77 @@ onMounted(() => { .progress-list { display: flex; flex-direction: column; - gap: 10px; + gap: 14px; + margin-top: 12px; } .progress-item { display: flex; align-items: center; - gap: 10px; -} - -.progress-bar-wrapper { - flex: 1; - height: 8px; - background-color: #e5e7eb; - border-radius: 4px; - overflow: hidden; -} - -.progress-bar { - height: 100%; - border-radius: 4px; -} - -.progress-percent { + gap: 12px; font-size: 13px; - color: #666; - min-width: 40px; - text-align: right; + 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) {