挂架平效果

This commit is contained in:
chenlin
2026-01-16 12:31:23 +08:00
parent c9b35826ad
commit 96d93c867e
7 changed files with 748 additions and 228 deletions

View File

@@ -5,6 +5,32 @@
<img width="50%" src="@/assets/images/line_1.png" /> <img width="50%" src="@/assets/images/line_1.png" />
</div> </div>
<div class="list" :style="{ maxHeight: maxHeight }"> <div class="list" :style="{ maxHeight: maxHeight }">
<!-- 骨架屏 -->
<template v-if="props.loading">
<!-- 表格模式骨架屏 -->
<template v-if="tableTitle && tableTitle.length > 0">
<div class="table-header">
<div class="header-item skeleton-pulse" v-for="(title, index) in tableTitle" :key="`header-skeleton-${index}`"></div>
</div>
<div class="list-wrapper">
<div class="table-row skeleton-row" v-for="i in 10" :key="`table-skeleton-${i}`">
<div class="table-cell skeleton-pulse" v-for="(title, cellIndex) in tableTitle" :key="`cell-skeleton-${i}-${cellIndex}`"></div>
</div>
</div>
</template>
<!-- 列表模式骨架屏 -->
<template v-else>
<div class="list-wrapper">
<div class="list-item skeleton-item" v-for="i in 10" :key="`list-skeleton-${i}`">
<div class="alert-text skeleton-pulse"></div>
</div>
</div>
</template>
</template>
<!-- 实际内容 -->
<template v-else>
<!-- 表格头部 --> <!-- 表格头部 -->
<div v-if="tableTitle && tableTitle.length > 0" class="table-header"> <div v-if="tableTitle && tableTitle.length > 0" class="table-header">
<div class="header-item" v-for="(title, index) in tableTitle" :key="index"> <div class="header-item" v-for="(title, index) in tableTitle" :key="index">
@@ -31,6 +57,7 @@
</div> </div>
</template> </template>
</div> </div>
</template>
</div> </div>
</div> </div>
</template> </template>
@@ -51,6 +78,7 @@ interface TableTitle {
} }
interface Props { interface Props {
loading?: boolean
title?: string title?: string
listData: AlertItem[] listData: AlertItem[]
maxHeight?: string maxHeight?: string
@@ -313,4 +341,31 @@ onUnmounted(() => {
} }
} }
} }
// 骨架屏样式
.skeleton-pulse {
animation: skeleton-loading-alertlist 1.5s ease-in-out infinite;
background-color: #444;
border-radius: 4px;
}
.skeleton-row {
margin-bottom: 4px;
}
.skeleton-item {
margin-bottom: 4px;
}
@keyframes skeleton-loading-alertlist {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
</style> </style>

View File

@@ -10,11 +10,13 @@
<div class="type-wrapper"> <div class="type-wrapper">
<div class="type-item"> <div class="type-item">
<span class="type-btn">重大</span> <span class="type-btn">重大</span>
<span class="type-num cursor-pointer" @click="handleMajorClick">{{ hiddenDangerData?.major || 0 }}</span> <div style="width: 20px; height: 20px" v-if="props.loading" class="type-num skeleton-pulse"></div>
<span v-else class="type-num cursor-pointer" @click="handleMajorClick">{{ hiddenDangerData?.major || 0 }}</span>
</div> </div>
<div class="type-item"> <div class="type-item">
<span class="type-btn active">一般</span> <span class="type-btn active">一般</span>
<span class="type-num cursor-pointer" @click="handleMajorClick">{{ hiddenDangerData?.general || 0 }}</span> <div style="width: 20px; height: 20px" v-if="props.loading" class="type-num skeleton-pulse"></div>
<span v-else class="type-num cursor-pointer" @click="handleMajorClick">{{ hiddenDangerData?.general || 0 }}</span>
</div> </div>
</div> </div>
@@ -30,7 +32,8 @@
</div> </div>
<div class="echart-wrapper"> <div class="echart-wrapper">
<div class="lf-rt"> <div class="lf-rt">
<Echart :options="progressChartOption" class="progress-chart" height="80%" /> <div v-if="props.loading" class="skeleton-chart-circle skeleton-pulse"></div>
<Echart v-else :options="progressChartOption" class="progress-chart" height="80%" />
<div class="progress-legend"> <div class="progress-legend">
<div class="legend-item"><span class="dot red"></span>已逾期</div> <div class="legend-item"><span class="dot red"></span>已逾期</div>
<div class="legend-item"><span class="dot green"></span>已处理</div> <div class="legend-item"><span class="dot green"></span>已处理</div>
@@ -41,7 +44,8 @@
</div> </div>
</div> </div>
<div class="lf-rt"> <div class="lf-rt">
<Echart :options="top3TypesChartOption" class="progress-chart" height="80%" /> <div v-if="props.loading" class="skeleton-chart-circle skeleton-pulse"></div>
<Echart v-else :options="top3TypesChartOption" class="progress-chart" height="80%" />
<div class="progress-legend-column"> <div class="progress-legend-column">
<div class="legend-item"> <div class="legend-item">
<span class="dot blue"></span> <span class="dot blue"></span>
@@ -65,7 +69,8 @@
<p class="safe-tooltip" title="安全指数 = (1-逾期隐患百分比) × 40% + 安全考核通过率 × 10% + 安全培训完成率 × 10% <p class="safe-tooltip" title="安全指数 = (1-逾期隐患百分比) × 40% + 安全考核通过率 × 10% + 安全培训完成率 × 10%
+ 安全类工单完成率 × 20% + 工程类工单完成率 × 20%"></p> + 安全类工单完成率 × 20% + 工程类工单完成率 × 20%"></p>
</span> </span>
<span class="pending-count">{{ hiddenDangerData?.safetyIndex || 0 }}</span> <span v-if="props.loading" class="pending-count skeleton-pulse"></span>
<span v-else class="pending-count">{{ hiddenDangerData?.safetyIndex || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -77,6 +82,7 @@ import echarts from '@/plugins/echarts'
interface Props { interface Props {
loading?: boolean
hiddenDangerData?: { hiddenDangerData?: {
general: number general: number
major: number major: number
@@ -936,4 +942,32 @@ watch(() => props.hiddenDangerData?.top3Types, (newVal) => {
.dot.blue { .dot.blue {
background-color: #3b82f6; background-color: #3b82f6;
} }
// 骨架屏动画
.skeleton-pulse {
animation: skeleton-loading-hiddendanger 1.5s ease-in-out infinite;
background-color: #444;
border-radius: 4px;
}
// 圆形饼图骨架屏
.skeleton-chart-circle {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 30px auto 50px auto;
background-color: #444;
}
@keyframes skeleton-loading-hiddendanger {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
</style> </style>

View File

@@ -4,29 +4,37 @@
<div> <div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" /> <img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
</div> </div>
<!-- 骨架屏 -->
<!-- 实际内容 -->
<div class="tip-container"> <div class="tip-container">
<div class="tip-image"> <div class="tip-image">
<img src="@/assets/images/screen/circle_image.png" width="80" height="80" /> <img src="@/assets/images/screen/circle_image.png" width="80" height="80" />
<span class="number">{{ alertData?.total || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="number skeleton-pulse"></div>
<span v-else class="number">{{ alertData?.total || 0 }}</span>
</div> </div>
<img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" /> <img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" />
<div class="tip-content"> <div class="tip-content">
<div class="col-item"> <div class="col-item">
<img src="@/assets/images/screen/warning_img.png" width="23" /> <img src="@/assets/images/screen/warning_img.png" width="23" />
<span>告警总数</span> <span>告警总数</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.total || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="skeleton-pulse"></div>
<span v-else style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.total || 0 }}</span>
</div> </div>
<div class="col-item"> <div class="col-item">
<span>已处理</span> <span>已处理</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: greenyellow;">{{ alertData?.processed || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="skeleton-pulse"></div>
<span v-else style="font-size: 1.2rem; marker-start: 2vw; color: greenyellow;">{{ alertData?.processed || 0 }}</span>
</div> </div>
<div class="col-item" style="display: flex; margin-left: 1vw; align-items: center;"> <div class="col-item" style="display: flex; margin-left: 1vw; align-items: center;">
<span>待处理</span> <span>待处理</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.pending || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="skeleton-pulse"></div>
<span v-else style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.pending || 0 }}</span>
</div> </div>
<div class="col-item" style="display: flex; margin-left: 1vw; align-items: center;"> <div class="col-item" style="display: flex; margin-left: 1vw; align-items: center;">
<span>处理中</span> <span>处理中</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.processing }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="skeleton-pulse"></div>
<span v-else style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ alertData?.processing }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -45,7 +53,7 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<AlertList style="margin-right: 1vw;" title="告警详情" :list-data="alertDetails" :linkUrl="linkUrl"></AlertList> <AlertList style="margin-right: 1vw;" title="告警详情" :list-data="alertDetails" :linkUrl="linkUrl" :loading="props.loading"></AlertList>
</div> </div>
</template> </template>
@@ -73,6 +81,7 @@ interface Props {
alertDetails?: AlertItem[] alertDetails?: AlertItem[]
sourceIndex?: number sourceIndex?: number
linkUrl?: string linkUrl?: string
loading?: boolean
} }
// 默认值 // 默认值
@@ -84,7 +93,8 @@ const props = withDefaults(defineProps<Props>(), {
processing: 0 processing: 0
}), }),
alertDetails: () => [], alertDetails: () => [],
sourceIndex: 1 sourceIndex: 1,
loading: false
}) })
</script> </script>
@@ -265,5 +275,104 @@ const props = withDefaults(defineProps<Props>(), {
} }
} }
} }
// 骨架屏样式
.skeleton-container {
.skeleton-tip-container {
position: relative;
width: 100%;
height: 70px;
.skeleton-tip-image {
position: absolute;
top: -5px;
right: 10px;
z-index: 2;
.skeleton-circle {
width: 80px;
height: 80px;
background-color: #3a3a3a;
border-radius: 50%;
}
.skeleton-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
.skeleton-bg {
width: 100%;
height: 70px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-tip-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 8px 15px;
.skeleton-col-item {
display: flex;
align-items: center;
gap: 8px;
.skeleton-icon {
width: 23px;
height: 23px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-text {
flex: 1;
height: 16px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-value {
width: 40px;
height: 18px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
}
}
}
// 骨架屏动画
.skeleton-pulse {
animation: skeleton-loading-highrisk 1.5s ease-in-out infinite;
background-color: #444;
border-radius: 4px;
}
@keyframes skeleton-loading-highrisk {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
} }
</style> </style>

View File

@@ -3,7 +3,34 @@
<div class="panel-title">人员管理</div> <div class="panel-title">人员管理</div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" /> <img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
<div class="top-card"> <!-- 骨架屏 -->
<div v-if="props.loading" class="skeleton-container">
<div class="skeleton-card">
<div class="skeleton-left">
<div class="skeleton-icon skeleton-pulse"></div>
<div class="skeleton-text skeleton-pulse"></div>
<div class="skeleton-numbers">
<div class="skeleton-number skeleton-pulse" v-for="i in 6" :key="i"></div>
</div>
<div class="skeleton-text skeleton-pulse"></div>
</div>
<div class="skeleton-right">
<div class="skeleton-item" v-for="i in 3" :key="i">
<div class="skeleton-row">
<div class="skeleton-icon-small skeleton-pulse"></div>
<div class="skeleton-text skeleton-pulse"></div>
</div>
<div class="skeleton-numbers">
<div class="skeleton-number-small skeleton-pulse" v-for="j in 4" :key="j"></div>
</div>
<div class="skeleton-text skeleton-pulse"></div>
</div>
</div>
</div>
</div>
<!-- 实际内容 -->
<div v-else class="top-card">
<div class="top-card-left"> <div class="top-card-left">
<div> <div>
<img width="33px" src="@/assets/images/1_224520_821.png" /> <img width="33px" src="@/assets/images/1_224520_821.png" />
@@ -64,6 +91,7 @@
<span>各园区统计</span> <span>各园区统计</span>
<img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" /> <img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" />
</div> </div>
<!-- <div v-if="props.loading" class="skeleton-chart-circle skeleton-pulse"></div> -->
<Echart :options="barChartOption" class="bar-chart" height="17.5vh" /> <Echart :options="barChartOption" class="bar-chart" height="17.5vh" />
</div> </div>
</div> </div>
@@ -74,6 +102,7 @@ import { ref, onMounted, watch, computed } from 'vue'
import { rgbToHex } from '@/utils/color' import { rgbToHex } from '@/utils/color'
interface Props { interface Props {
loading?: boolean
totalCount: number totalCount: number
formalEmployeeCount: number formalEmployeeCount: number
externalStaffCount: number externalStaffCount: number
@@ -319,5 +348,141 @@ onMounted(() => {
min-height: 17.5vh; min-height: 17.5vh;
} }
} }
// 骨架屏样式
.skeleton-container {
.skeleton-card {
display: flex;
padding: 0 20px;
column-gap: 15px;
font-size: 0.8rem;
.skeleton-left {
display: flex;
height: 12vh;
min-width: 15vw;
padding: 0 10px;
background-image: url('@/assets/imgs/total_count_card_bg.png');
background-size: cover;
column-gap: 6px;
align-items: center;
.skeleton-icon {
width: 33px;
height: 33px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-text {
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-numbers {
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
.skeleton-number {
width: 26px;
height: 50px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
}
.skeleton-right {
display: flex;
height: 12vh;
min-width: 20vw;
background-image: url('@/assets/imgs/staff_types_bg.png');
background-position: top center;
background-size: cover;
flex-direction: column;
justify-content: center;
row-gap: 4px;
.skeleton-item {
display: flex;
align-items: center;
column-gap: 5px;
padding: 0 10px;
.skeleton-row {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
.skeleton-icon-small {
width: 18px;
height: 18px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-text {
height: 16px;
width: 60px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
.skeleton-numbers {
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
.skeleton-number-small {
width: 14px;
height: 25px;
background-color: #3a3a3a;
border-radius: 2px;
}
}
}
}
}
}
// 骨架屏动画
.skeleton-pulse {
animation: skeleton-loading-overview 1.5s ease-in-out infinite;
}
// 圆形饼图骨架屏
.skeleton-chart-circle {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 30px auto 50px auto;
background-color: #444;
}
// 柱状图骨架屏
// .skeleton-chart-bar {
// width: 100%;
// height: 17.5vh;
// background-color: #444;
// border-radius: 8px;
// }
@keyframes skeleton-loading-overview {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
} }
</style> </style>

View File

@@ -9,7 +9,31 @@
</div> </div>
<img class="title-line" src="@/assets/images/title_border_line.png" /> <img class="title-line" src="@/assets/images/title_border_line.png" />
<div class="chart-grid"> <!-- 骨架屏 -->
<div v-if="props.loading" class="skeleton-container">
<div class="skeleton-grid">
<div class="skeleton-card" v-for="i in 6" :key="i">
<div class="skeleton-title skeleton-pulse"></div>
<div class="skeleton-chart">
<div class="skeleton-chart-circle skeleton-pulse"></div>
<div class="skeleton-chart-center">
<div class="skeleton-text-small skeleton-pulse"></div>
<div class="skeleton-text-large skeleton-pulse"></div>
</div>
</div>
<div class="skeleton-legend">
<div class="skeleton-legend-item" v-for="j in 4" :key="j">
<div class="skeleton-dot skeleton-pulse"></div>
<div class="skeleton-text skeleton-pulse"></div>
<div class="skeleton-value skeleton-pulse"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 实际内容 -->
<div v-else class="chart-grid">
<div class="chart-card" v-for="item in currentCharts" :key="`${activeTab}-${item.title}`"> <div class="chart-card" v-for="item in currentCharts" :key="`${activeTab}-${item.title}`">
<div class="chart-title" @click="handleChartTitleClick()">{{ item.title }}</div> <div class="chart-title" @click="handleChartTitleClick()">{{ item.title }}</div>
<div class="chart-content"> <div class="chart-content">
@@ -85,6 +109,7 @@ const tabCharts = ref<Record<TabType, ChartItem[]>>({
}) })
const props = defineProps<{ const props = defineProps<{
loading?: boolean
riskStatistics?: Record<TabType, ChartItem[]> riskStatistics?: Record<TabType, ChartItem[]>
}>() }>()
@@ -415,5 +440,122 @@ const handleTabClick = (tab: TabType) => {
// padding: 5px; // padding: 5px;
// } // }
// } // }
// 骨架屏样式
.skeleton-container {
.skeleton-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
.skeleton-card {
background-image: url('@/assets/images/screen/left_top_2_img.png'),
url('@/assets/images/screen/left_center_img.png'),
url('@/assets/images/screen/left_bottom_img.png');
background-position: top center, left center, bottom center;
background-repeat: no-repeat, no-repeat, no-repeat;
background-size: 100% 90px, cover, 100% 68px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
.skeleton-title {
width: 120px;
height: 16px;
background-color: #3a3a3a;
border-radius: 4px;
margin-bottom: 10px;
}
.skeleton-chart {
position: relative;
width: 100px;
height: 100px;
margin-bottom: 10px;
.skeleton-chart-circle {
width: 100%;
height: 100%;
background-color: #3a3a3a;
border-radius: 50%;
}
.skeleton-chart-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
.skeleton-text-small {
width: 40px;
height: 12px;
background-color: #3a3a3a;
border-radius: 4px;
margin-bottom: 4px;
}
.skeleton-text-large {
width: 30px;
height: 16px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
}
.skeleton-legend {
width: 100%;
.skeleton-legend-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
.skeleton-dot {
width: 8px;
height: 8px;
background-color: #3a3a3a;
border-radius: 50%;
}
.skeleton-text {
flex: 1;
height: 12px;
background-color: #3a3a3a;
border-radius: 4px;
margin: 0 8px;
}
.skeleton-value {
width: 20px;
height: 12px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
}
}
}
}
// 骨架屏动画
.skeleton-pulse {
animation: skeleton-loading-riskstats 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading-riskstats {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
</style> </style>

View File

@@ -3,17 +3,21 @@
<div class="panel-title">超时工单</div> <div class="panel-title">超时工单</div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" /> <img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
<!-- 骨架屏 -->
<!-- 实际内容 -->
<div class="tip-container"> <div class="tip-container">
<div class="tip-image"> <div class="tip-image">
<img src="@/assets/images/screen/circle_image.png" width="80" height="80" /> <img src="@/assets/images/screen/circle_image.png" width="80" height="80" />
<span class="number">{{ timeoutWorkOrders?.total || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="number skeleton-pulse"></div>
<span v-else class="number">{{ timeoutWorkOrders?.total || 0 }}</span>
</div> </div>
<img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" /> <img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" />
<div class="tip-content"> <div class="tip-content">
<div class="col-item"> <div class="col-item">
<img src="@/assets/images/screen/warning_img.png" width="23" /> <img src="@/assets/images/screen/warning_img.png" width="23" />
<span>超时工单数</span> <span>超时工单数</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: red;">{{ timeoutWorkOrders?.total || 0 }}</span> <div v-if="props.loading" style="width: 20px; height: 20px;" class="skeleton-pulse"></div>
<span v-else style="font-size: 1.2rem; marker-start: 2vw; color: red;">{{ timeoutWorkOrders?.total || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -32,7 +36,7 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<AlertList :linkUrl="linkUrl" style="margin-right: 1vw;" title="工单详情" :list-data="alertDetails" ></AlertList> <AlertList :linkUrl="linkUrl" style="margin-right: 1vw;" title="工单详情" :list-data="alertDetails" :loading="props.loading" ></AlertList>
</div> </div>
</template> </template>
@@ -52,6 +56,7 @@ interface TimeoutWorkOrders {
// Props定义 // Props定义
interface Props { interface Props {
loading?: boolean
timeoutWorkOrders?: TimeoutWorkOrders timeoutWorkOrders?: TimeoutWorkOrders
alertDetails?: AlertItem[] alertDetails?: AlertItem[]
sourceIndex?: number sourceIndex?: number
@@ -247,5 +252,104 @@ const props = withDefaults(defineProps<Props>(), {
} }
} }
} }
// 骨架屏样式
.skeleton-container {
.skeleton-tip-container {
position: relative;
width: 100%;
height: 70px;
.skeleton-tip-image {
position: absolute;
top: -5px;
right: 10px;
z-index: 2;
.skeleton-circle {
width: 80px;
height: 80px;
background-color: #3a3a3a;
border-radius: 50%;
}
.skeleton-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
.skeleton-bg {
width: 100%;
height: 70px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-tip-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 8px 15px;
.skeleton-col-item {
display: flex;
align-items: center;
gap: 8px;
.skeleton-icon {
width: 23px;
height: 23px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-text {
flex: 1;
height: 16px;
background-color: #3a3a3a;
border-radius: 4px;
}
.skeleton-value {
width: 40px;
height: 18px;
background-color: #3a3a3a;
border-radius: 4px;
}
}
}
}
}
// 骨架屏动画
.skeleton-pulse {
animation: skeleton-loading-timeout 1.5s ease-in-out infinite;
background-color: #444;
border-radius: 4px;
}
@keyframes skeleton-loading-timeout {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
} }
</style> </style>

View File

@@ -24,25 +24,28 @@
<!-- 主内容区 --> <!-- 主内容区 -->
<div class="content-container"> <div class="content-container">
<div class="left-wrapper"> <div class="left-wrapper">
<OverviewPanel :totalCount="dashboardData?.totalCount || 0" <OverviewPanel :loading="isFirstLoading"
:totalCount="dashboardData?.totalCount || 0"
:formalEmployeeCount="dashboardData?.formalEmployeeCount || 0" :formalEmployeeCount="dashboardData?.formalEmployeeCount || 0"
:externalStaffCount="dashboardData?.externalStaffCount || 0" :externalStaffCount="dashboardData?.externalStaffCount || 0"
:visitorCount="dashboardData?.visitorCount || 0" :visitorCount="dashboardData?.visitorCount || 0"
:parkStatistics="dashboardData?.parkStatistics"/> :parkStatistics="dashboardData?.parkStatistics"/>
<RiskStatisticsPanel :riskStatistics="riskStatistics" :dangerDetail="dangerDetail" <RiskStatisticsPanel :loading="isFirstLoading" :riskStatistics="riskStatistics" :dangerDetail="dangerDetail"
@tab-change="handleRiskTabChange" :campus_id="query.campus_id"/> @tab-change="handleRiskTabChange" :campus_id="query.campus_id"/>
</div> </div>
<div class="right-wrapper"> <div class="right-wrapper">
<HighRiskAlertPanel :alertData="dashboardData?.alertData" <HighRiskAlertPanel :loading="isFirstLoading"
:alertData="dashboardData?.alertData"
:alertDetails="dashboardData?.alertData.details" :alertDetails="dashboardData?.alertData.details"
linkUrl="http://10.0.64.20/security/console/command-center?p=tabl" linkUrl="http://10.0.64.20/security/console/command-center?p=tabl"
:sourceIndex="sourceIndex"/> :sourceIndex="sourceIndex"/>
<TimeoutWorkOrderPanel :timeoutWorkOrders="dashboardData?.timeoutWorkOrders" <TimeoutWorkOrderPanel :loading="isFirstLoading"
:timeoutWorkOrders="dashboardData?.timeoutWorkOrders"
:alertDetails="dashboardData?.timeoutWorkOrders.details" :alertDetails="dashboardData?.timeoutWorkOrders.details"
linkUrl="http://10.0.64.20/pms/workorder-list" linkUrl="http://10.0.64.20/pms/workorder-list"
:sourceIndex="sourceIndex"/> :sourceIndex="sourceIndex"/>
</div> </div>
<HiddenDangerPanel :hiddenDangerData="dashboardData?.hiddenDangerData"/> <HiddenDangerPanel :loading="isFirstLoading" :hiddenDangerData="dashboardData?.hiddenDangerData"/>
</div> </div>
</div> </div>
@@ -100,6 +103,9 @@ const riskStatistics = ref<any>({
}) })
const dangerDetail = ref<any>() const dangerDetail = ref<any>()
// 加载状态
const isFirstLoading = ref<boolean>(true)
// 动画相关的状态 // 动画相关的状态
const isAnimating = ref<boolean>(false) const isAnimating = ref<boolean>(false)
const animationDuration = 2000 // 动画持续时间(毫秒) const animationDuration = 2000 // 动画持续时间(毫秒)
@@ -332,6 +338,11 @@ onUnmounted(() => {
let isFirstLoad = ref<boolean>(true) let isFirstLoad = ref<boolean>(true)
// 数据初始化方法 // 数据初始化方法
const loadDashboardData = async (): Promise<void> => { const loadDashboardData = async (): Promise<void> => {
// 第一次加载时显示骨架屏
if (isFirstLoading.value) {
console.log('第一次加载,显示骨架屏');
}
const data = await getDashboardData() const data = await getDashboardData()
if (isFirstLoad.value) { if (isFirstLoad.value) {
console.log('第一次加载'); console.log('第一次加载');
@@ -340,8 +351,11 @@ const loadDashboardData = async (): Promise<void> => {
} }
console.log('dashboardData.value>>>>>>>>>>', dashboardData.value); console.log('dashboardData.value>>>>>>>>>>', dashboardData.value);
try { // 收集所有异步请求
const promises = []
// 获取总体概览数据 // 获取总体概览数据
promises.push(
getTableList('generalTotal', query).then(generalTotal => { getTableList('generalTotal', query).then(generalTotal => {
if (generalTotal.records && generalTotal.records.length > 0) { if (generalTotal.records && generalTotal.records.length > 0) {
dashboardData.value.totalCount = Number(generalTotal.records[0].totalCount) dashboardData.value.totalCount = Number(generalTotal.records[0].totalCount)
@@ -355,13 +369,13 @@ const loadDashboardData = async (): Promise<void> => {
external: Number(dashboardData.value.externalStaffCount), external: Number(dashboardData.value.externalStaffCount),
visitor: Number(dashboardData.value.visitorCount) visitor: Number(dashboardData.value.visitorCount)
}) })
}) }).catch(error => {
} catch (error) {
console.error('获取总体概览数据失败:', error) console.error('获取总体概览数据失败:', error)
} })
)
try {
// 获取各园区统计数据 // 获取各园区统计数据
promises.push(
getTableList('parkscreen_user_info', query).then(parkscreen_user_info => { getTableList('parkscreen_user_info', query).then(parkscreen_user_info => {
if (parkscreen_user_info.records && parkscreen_user_info.records.length > 0) { if (parkscreen_user_info.records && parkscreen_user_info.records.length > 0) {
dashboardData.value.parkStatistics = parkscreen_user_info.records.map(el => { dashboardData.value.parkStatistics = parkscreen_user_info.records.map(el => {
@@ -373,13 +387,13 @@ const loadDashboardData = async (): Promise<void> => {
} }
}) })
} }
}) }).catch(error => {
} catch (error) {
console.error('获取各园区统计数据失败:', error) console.error('获取各园区统计数据失败:', error)
} })
)
try {
// 获取风险预警数据 // 获取风险预警数据
promises.push(
getTableList('risk_alert_data', query).then(risk_alert_data => { getTableList('risk_alert_data', query).then(risk_alert_data => {
if (risk_alert_data.records && risk_alert_data.records.length > 0) { if (risk_alert_data.records && risk_alert_data.records.length > 0) {
dashboardData.value.alertData.total = risk_alert_data.records[0].total dashboardData.value.alertData.total = risk_alert_data.records[0].total
@@ -390,12 +404,10 @@ const loadDashboardData = async (): Promise<void> => {
}).catch(error => { }).catch(error => {
console.error('获取风险预警数据失败:', error) console.error('获取风险预警数据失败:', error)
}) })
} catch (error) { )
console.error('获取风险预警数据失败:', error)
}
try {
// 获取风险预警详情数据 // 获取风险预警详情数据
promises.push(
getTableList('risk_alert_detail', query).then(risk_alert_detail => { getTableList('risk_alert_detail', query).then(risk_alert_detail => {
if (risk_alert_detail.records && risk_alert_detail.records.length > 0) { if (risk_alert_detail.records && risk_alert_detail.records.length > 0) {
dashboardData.value.alertData.details = risk_alert_detail.records dashboardData.value.alertData.details = risk_alert_detail.records
@@ -403,12 +415,10 @@ const loadDashboardData = async (): Promise<void> => {
}).catch(error => { }).catch(error => {
console.error('获取风险预警详情数据失败:', error) console.error('获取风险预警详情数据失败:', error)
}) })
} catch (error) { )
console.error('获取风险预警详情数据失败:', error)
}
try {
// 获取超期工单数据 // 获取超期工单数据
promises.push(
getTableList('timeout_work_order', query).then(timeout_work_order => { getTableList('timeout_work_order', query).then(timeout_work_order => {
if (timeout_work_order.records && timeout_work_order.records.length >= 0) { if (timeout_work_order.records && timeout_work_order.records.length >= 0) {
dashboardData.value.timeoutWorkOrders.total = timeout_work_order.records.length dashboardData.value.timeoutWorkOrders.total = timeout_work_order.records.length
@@ -417,180 +427,81 @@ const loadDashboardData = async (): Promise<void> => {
}).catch(error => { }).catch(error => {
console.error('获取超期工单数据失败:', error) console.error('获取超期工单数据失败:', error)
}) })
} catch (error) { )
console.error('获取超期工单数据失败:', error)
} // 处理风险统计和隐患数据(这些是异步的)
handleRiskTabChange('安全类事项') promises.push(
Promise.all([
handleRiskTabChange('安全类事项'),
handleHiddenDangerPannelData(query) handleHiddenDangerPannelData(query)
console.log('dashboardData.value>>>>>>>>>>', dashboardData.value); ]).catch(error => {
console.error('处理风险统计和隐患数据失败:', error)
})
)
// 等待所有异步操作完成
try {
await Promise.all(promises)
console.log('所有数据加载完成')
// 第一次加载完成后,隐藏骨架屏
if (isFirstLoading.value) {
isFirstLoading.value = false
console.log('隐藏骨架屏')
}
} catch (error) {
console.error('数据加载过程中出现错误:', error)
// 即使出错也要隐藏骨架屏,避免界面一直处于加载状态
if (isFirstLoading.value) {
isFirstLoading.value = false
}
}
} }
const handleHiddenDangerPannelData = (query) => { const handleHiddenDangerPannelData = async (query) => {
let _data = { const promises = []
flag: false,
general: 0,
major: 0,
overdue: 0,
processed: 0,
processing: 0,
pending: 0
}
let _data2 = { // 获取隐患排查治理数据 - 系统数据
flag: false, promises.push(
general: 0,
major: 0,
overdue: 0,
processed: 0,
processing: 0,
pending: 0
}
try {
// 获取隐患排查治理数据
getTableList('risk_level_count', query).then(res => { getTableList('risk_level_count', query).then(res => {
if (res.records && res.records.length > 0) { if (res.records && res.records.length > 0) {
_data.general = _data.general + Number(res.records[0].general_count) dashboardData.value.hiddenDangerData.general = Number(res.records[0].general_count)
_data.major = _data.major + Number(res.records[0].major_count) dashboardData.value.hiddenDangerData.major = Number(res.records[0].major_count)
// 获取隐患排查治理数据 }
getTableList('risk_status_count', query).then(res => { return getTableList('risk_status_count', query)
}).then(res => {
if (res.records && res.records.length > 0) { if (res.records && res.records.length > 0) {
// 接口返回的已经是百分比,直接使用
const record = res.records[0] const record = res.records[0]
_data.overdue = Number(record.overdueCnt) || 0
_data.processed = Number(record.processedCnt) || 0
_data.processing = Number(record.processingCnt) || 0
_data.pending = 0 // 接口没有返回pending设为0
_data.flag = true
console.log('risk_status_count 接口返回数据:', record)
console.log('处理后的 _data:', _data)
_data2.flag = false
if (_data2.flag) {
// 合并数据
console.log("请求系统和第三方成功,合并数据", _data, _data2);
let generalCnt = _data.general + _data2.general
let majorCnt = _data.major + _data2.major
dashboardData.value.hiddenDangerData.general = generalCnt
dashboardData.value.hiddenDangerData.major = majorCnt
// 如果第三方数据也是百分比,需要合并;否则使用系统数据
// 这里假设系统数据是百分比,第三方数据可能是数量或百分比
let overdueCnt, processedCnt, processingCnt, pendingCnt
if (_data2.overdue > 1 || _data2.processed > 1 || _data2.processing > 1) {
// 第三方数据可能是百分比,直接使用系统数据(因为系统数据更准确)
overdueCnt = _data.overdue.toFixed(2)
processedCnt = _data.processed.toFixed(2)
processingCnt = _data.processing.toFixed(2)
pendingCnt = _data.pending.toFixed(2)
} else {
// 第三方数据可能是数量,需要计算百分比
let totalCnt = generalCnt + majorCnt
overdueCnt = totalCnt > 0 ? ((_data.overdue + _data2.overdue) / totalCnt * 100).toFixed(2) : '0.00'
processedCnt = totalCnt > 0 ? ((_data.processed + _data2.processed) / totalCnt * 100).toFixed(2) : '0.00'
processingCnt = totalCnt > 0 ? ((_data.processing + _data2.processing) / totalCnt * 100).toFixed(2) : '0.00'
pendingCnt = totalCnt > 0 ? ((_data.pending + _data2.pending) / totalCnt * 100).toFixed(2) : '0.00'
}
dashboardData.value.hiddenDangerData.progress = { dashboardData.value.hiddenDangerData.progress = {
overdue: overdueCnt, overdue: Number(record.overdueCnt).toFixed(2),
processed: processedCnt, processed: Number(record.processedCnt).toFixed(2),
processing: processingCnt, processing: Number(record.processingCnt).toFixed(2),
pending: pendingCnt, pending: '0.00'
}
console.log('合并后的 progress:', dashboardData.value.hiddenDangerData.progress)
} else {
console.log("请求系统成功,展示数据", _data, _data2);
dashboardData.value.hiddenDangerData.general = _data.general
dashboardData.value.hiddenDangerData.major = _data.major
// 接口返回的已经是百分比,直接使用
dashboardData.value.hiddenDangerData.progress = {
overdue: _data.overdue.toFixed(2),
processed: _data.processed.toFixed(2),
processing: _data.processing.toFixed(2),
pending: _data.pending.toFixed(2),
}
console.log('系统数据 progress:', dashboardData.value.hiddenDangerData.progress)
} }
} }
}).catch(error => {
console.error('获取系统隐患数据失败:', error)
}) })
} )
})
// 获取第三方隐患排查治理数据
// 获取隐患排查治理数据 promises.push(
getTableList('hidden_danger_investigation', query).then(res => { getTableList('hidden_danger_investigation', query).then(res => {
if (res.records && res.records.length > 0) { if (res.records && res.records.length > 0) {
_data2.general = Number(res.records[0].general) // 获取安全指数
_data2.major = Number(res.records[0].major) return getTableList('hidden_danger_safety_index', query)
}
// 安全指数另算,再起一个报表 }).then(res => {
// dashboardData.value.hiddenDangerData.safetyIndex = res.records[0].safetyIndex
// 在这里添加获取安全指数的逻辑
getTableList('hidden_danger_safety_index', query).then(res => {
if (res.records && res.records.length > 0) { if (res.records && res.records.length > 0) {
dashboardData.value.hiddenDangerData.safetyIndex = res.records[0].safetyIndex dashboardData.value.hiddenDangerData.safetyIndex = res.records[0].safetyIndex
} }
}).catch(error => { }).catch(error => {
console.error('获取隐患排查治理数据失败:', error) console.error('获取第三方隐患数据失败:', error)
}) })
)
// 获取隐患排查治理处理进度数据
getTableList('hidden_danger_process_progress', query).then(res => {
// if (res.records && res.records.length > 0) {
// _data2.flag = true
// _data2.overdue = Number(res.records[0].overdue) / 100 * (_data2.general + _data2.major)
// _data2.processed = Number(res.records[0].processed) / 100 * (_data2.general + _data2.major)
// _data2.processing = Number(res.records[0].processing) / 100 * (_data2.general + _data2.major)
// _data2.pending = Number(res.records[0].pending) / 100 * (_data2.general + _data2.major)
// if (_data.flag) {
// console.log("请求第三方和系统成功,合并数据", _data, _data2);
// // 合并数据
// let generalCnt = _data.general + _data2.general
// let majorCnt = _data.major + _data2.major
// dashboardData.value.hiddenDangerData.general = generalCnt
// dashboardData.value.hiddenDangerData.major = majorCnt
// let totalCnt = generalCnt + majorCnt
// let overdueCnt = ((_data.overdue + _data2.overdue) / totalCnt * 100).toFixed(2)
// let processedCnt = ((_data.processed + _data2.processed) / totalCnt * 100).toFixed(2)
// let processingCnt = ((_data.processing + _data2.processing) / totalCnt * 100).toFixed(2)
// let pendingCnt = ((_data.pending + _data2.pending) / totalCnt * 100).toFixed(2)
// dashboardData.value.hiddenDangerData.progress = {
// overdue: overdueCnt,
// processed: processedCnt,
// processing: processingCnt,
// pending: pendingCnt,
// }
// } else {
// //显示三方数据
// console.log("请求第三方成功,展示数据", _data, _data2);
// dashboardData.value.hiddenDangerData.general = _data2.general
// dashboardData.value.hiddenDangerData.major = _data2.major
// dashboardData.value.hiddenDangerData.progress = {
// overdue: res.records[0].overdue,
// processed: res.records[0].processed,
// processing: res.records[0].processing,
// pending: res.records[0].pending,
// }
// }
// }
}).catch(error => {
console.error('获取隐患排查治理处理进度数据失败:', error)
})
}
}).catch(error => {
console.error('获取隐患排查治理数据失败:', error)
})
} catch (error) {
console.error('获取隐患排查治理数据失败:', error)
}
try {
// 获取隐患排查治理TOP3类型数据 // 获取隐患排查治理TOP3类型数据
promises.push(
getTableList('hidden_danger_top', query).then(hidden_danger_top => { getTableList('hidden_danger_top', query).then(hidden_danger_top => {
if (hidden_danger_top.records && hidden_danger_top.records.length > 0) { if (hidden_danger_top.records && hidden_danger_top.records.length > 0) {
dashboardData.value.hiddenDangerData.top3Types = hidden_danger_top.records dashboardData.value.hiddenDangerData.top3Types = hidden_danger_top.records
@@ -598,9 +509,9 @@ const handleHiddenDangerPannelData = (query) => {
}).catch(error => { }).catch(error => {
console.error('获取隐患排查治理TOP3类型数据失败:', error) console.error('获取隐患排查治理TOP3类型数据失败:', error)
}) })
} catch (error) { )
console.error('获取隐患排查治理TOP3类型数据失败:', error)
} return Promise.all(promises)
} }
// 处理风险统计tab切换 // 处理风险统计tab切换