Files
lc_frontend/src/views/screen/mainScreenV1.vue
2025-10-17 10:31:13 +08:00

1536 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="dashboard-container">
<!-- 顶部标题栏 -->
<div class="header-container">
<div class="header-left">
<div class="back-button" @click="openRegionSelector"> 总部
<span>···</span>
</div>
</div>
<h1 class="header-title">总部综合监控大屏</h1>
<div class="header-right">
{{ currentTime }}
</div>
</div>
<!-- 天气预报 -->
<WeatherWarning />
<!-- 主内容区 -->
<div class="content-container">
<div class="left-wrapper">
<OverviewPanel
:totalCount="totalCount" :formalEmployeeCount="formalEmployeeCount"
:externalStaffCount="externalStaffCount" :visitorCount="visitorCount" :parkStatistics="parkStatistics" />
<RiskStatisticsPanel :riskStatistics="riskStatistics" @tab-change="handleRiskTabChange" />
</div>
<div class="right-wrapper">
<HighRiskAlertPanel
:alertData="dashboardData?.alertData" :alertDetails="sourceAcitve"
:sourceIndex="sourceIndex" />
<TimeoutWorkOrderPanel
:timeoutWorkOrders="dashboardData?.timeoutWorkOrders" :alertDetails="sourceAcitve"
:sourceIndex="sourceIndex" />
</div>
<div class="center-container">
<div class="center-content">
<span class="title">隐患排查治理</span>
<img class="bottom-border-line" src="@/assets/images/title_border_line_1.png" />
<span class="sub-title">分类风险</span>
<img width="50%" src="@/assets/images/line_1.png" />
<div class="type-wrapper">
<div class="type-item">
<span class="type-btn">重大</span>
<span class="type-num">{{ dashboardData?.hiddenDangerData.major || 0 }}</span>
</div>
<div class="type-item">
<span class="type-btn active">一般</span>
<span class="type-num">{{ dashboardData?.hiddenDangerData.general || 0 }}</span>
</div>
</div>
<div class="clasic-wrapper">
<div class="clasic-item">
<span>处理进度</span>
<img width="100%" src="@/assets/images/line_1.png" />
</div>
<div class="clasic-item">
<span>Top3隐患类</span>
<img width="100%" src="@/assets/images/line_1.png" />
</div>
</div>
<div class="echart-wrapper">
<div class="lf-rt">
<Echart v-if="progressChart" :options="progressChart" class="progress-chart" height="80%" />
<div class="progress-legend">
<div class="legend-item"><span class="dot red"></span>已逾期</div>
<div class="legend-item"><span class="dot green"></span>已处理</div>
</div>
<div class="progress-legend">
<div class="legend-item"><span class="dot yellow"></span>待排查</div>
<div class="legend-item"><span class="dot blue"></span>处理中</div>
</div>
</div>
<div class="lf-rt">
<Echart v-if="classicChart" :options="classicChart" class="progress-chart" height="80%" />
<div class="progress-legend">
<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 yellow"></span>巡检</div>
</div>
</div>
</div>
<div class="safe-wrapper">
<span class="safe-title">
<img width="22" style="margin: 3px 5px 0 0" src="@/assets/images/ybp_icon.png" />
安全指数:
</span>
<span class="pending-count">{{ dashboardData?.hiddenDangerData.safetyIndex || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 区域选择弹窗 -->
<RegionSelector
v-model="regionSelectorVisible" :modelSelected="selectedRegion" :regions="regionOption"
@change="onRegionChange" />
</template>
<script setup lang="ts">
import { batchGetTableList } from '@/api/design/report'
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import type { EChartsOption } from 'echarts'
import RegionSelector from './components/RegionSelector.vue'
import WeatherWarning from './components/WeatherWarning.vue'
import { Echart } from '@/components/Echart'
import { getDashboardData, updateDashboardData, getAlertDetails, type DashboardData } from '@/api/dashboard'
import OverviewPanel from './components/OverviewPanel.vue'
import RiskStatisticsPanel from './components/RiskStatisticsPanel.vue'
import HighRiskAlertPanel from './components/HighRiskAlertPanel.vue'
import TimeoutWorkOrderPanel from './components/TimeoutWorkOrderPanel.vue'
// 类型定义
interface AlertItem {
text: string
error?: boolean
warn?: boolean
}
interface RegionItem {
name: string
code: string
}
type TabType = '高危作业' | '安全培训考试' | '安全培训考试'
// 响应式数据
const currentTime = ref<string>('')
const regionSelectorVisible = ref<boolean>(false)
const selectedRegion = ref<string>('')
const activeTab = ref<TabType>('高危作业')
const sourceIndex = ref<number>(1)
// 大屏数据
const dashboardData = ref<DashboardData | null>(null)
// 总体概览-总计数量数据
const totalCount = ref<number>(0)
const formalEmployeeCount = ref<number>(0)
const externalStaffCount = ref<number>(0)
const visitorCount = ref<number>(0)
// 总体概览-各园区统计
const parkStatistics = ref<any>()
// 高危作业/安全培训考试/安全培训考试
const riskStatistics = ref<any>()
// 动画相关的状态
const isAnimating = ref<boolean>(false)
const animationDuration = 2000 // 动画持续时间(毫秒)
// 数字变化时的闪烁效果
const flashNumbers = () => {
const numberElements = document.querySelectorAll('.total-number, .type-number')
numberElements.forEach((el) => {
el.classList.add('flash')
setTimeout(() => {
el.classList.remove('flash')
}, 300)
})
}
// 数字滚动动画方法
const animateNumber = (startValue: number, endValue: number, duration: number, callback: (value: number) => void) => {
const startTime = Date.now()
const difference = endValue - startValue
const animate = () => {
const currentTime = Date.now()
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用缓动函数让动画更自然
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
const currentValue = Math.round(startValue + difference * easeOutQuart)
callback(currentValue)
if (progress < 1) {
requestAnimationFrame(animate)
}
}
animate()
}
// 批量更新所有数字的方法
const updateAllCounts = (counts: {
total?: number
formal?: number
external?: number
visitor?: number
}) => {
if (isAnimating.value) return
isAnimating.value = true
const promises: Promise<void>[] = []
if (counts.total !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = totalCount.value
animateNumber(startValue, counts.total || 0, animationDuration, (value) => {
totalCount.value = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.formal !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = formalEmployeeCount.value
animateNumber(startValue, counts.formal || 0, animationDuration, (value) => {
formalEmployeeCount.value = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.external !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = externalStaffCount.value
animateNumber(startValue, counts.external || 0, animationDuration, (value) => {
externalStaffCount.value = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.visitor !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = visitorCount.value
animateNumber(startValue, counts.visitor || 0, animationDuration, (value) => {
visitorCount.value = value
})
setTimeout(resolve, animationDuration)
}))
}
Promise.all(promises).then(() => {
isAnimating.value = false
flashNumbers() // 更新完成后闪烁效果
})
}
// 图表引用
const barChart = ref<EChartsOption | null>(null)
const donutChart = ref<EChartsOption | null>(null)
const progressChart = ref<EChartsOption | null>(null)
const classicChart = ref<EChartsOption | null>(null)
// 告警数据
const sourceAcitve = ref<AlertItem[]>([])
// 路由
const router = useRouter()
// 监听器
watch(sourceIndex, async () => {
console.log('sourceIndex', sourceIndex.value)
try {
const type = sourceIndex.value === 2 ? 'timeout' : 'risk'
const details = await getAlertDetails(type)
sourceAcitve.value = details
} catch (error) {
console.error('获取告警详情失败:', error)
}
})
const regionOption = ref<RegionItem[]>([])
// 生命周期
onMounted(async () => {
updateTime()
setInterval(updateTime, 1000)
try {
let { park_info_list } = await batchGetTableList(
'park_info_list'
)
console.log("park_info_list>>>>", park_info_list.records.map(el => el.region));
// 更新为新的数据格式
regionOption.value = park_info_list.records.map(el => ({
name: el.region,
code: el.region_code || el.code || String(Math.random()).slice(2, 6) // 如果没有code字段生成一个随机code
}))
// 初始化数据
await loadDashboardData()
// 初始化图表
initCharts()
// 启动定时器
timeOut1()
// 添加周期性动画演示
setInterval(async () => {
await loadDashboardData()
}, 15000) // 每15秒更新一次
} catch (error) {
console.error('初始化大屏数据失败:', error)
}
})
// 数据初始化方法
const loadDashboardData = async (): Promise<void> => {
try {
const data = await getDashboardData()
dashboardData.value = data
let { generalTotal, parkscreen_user_info, hidden_danger_investigation } = await batchGetTableList(
'parkscreen_user_info,generalTotal,hidden_danger_investigation'
)
// -----------------------设置总体概览数据-----------------------
// 使用动画更新数字
updateAllCounts({
total: Number(generalTotal.records[0].totalCount),
formal: Number(generalTotal.records[0].formalEmployeeCount),
external: Number(generalTotal.records[0].externalStaffCount),
visitor: Number(generalTotal.records[0].visitorCount)
})
// 更新echart图表
parkStatistics.value = parkscreen_user_info.records
// -----------------------设置高危作业数据-----------------------
handleRiskTabChange('高危作业')
// -----------------------隐患排查治理-----------------------
dashboardData.value.hiddenDangerData.general = hidden_danger_investigation.records[0].general
dashboardData.value.hiddenDangerData.major = hidden_danger_investigation.records[0].major
dashboardData.value.hiddenDangerData.safetyIndex = hidden_danger_investigation.records[0].safetyIndex
let { hidden_danger_process_progress } = await batchGetTableList(
'hidden_danger_process_progress'
)
dashboardData.value.hiddenDangerData.progress = hidden_danger_process_progress.records[0]
} catch (error) {
console.error('初始化大屏数据失败:', error)
}
}
// 处理风险统计tab切换
const handleRiskTabChange = async (tab: TabType) => {
console.log('Tab changed to:', tab)
try {
let code = ''
switch (tab) {
case '高危作业':
code = 'hazardous_operations'
break
case '安全培训考试':
code = 'safety_assessment'
break
case '安全培训考试':
code = 'security_training'
break
default:
code = 'hazardous_operations'
}
// 根据不同的tab请求不同的接口
let { [code]: { records } } = await batchGetTableList(
code,
{ activeTab: tab }
)
// 更新风险统计数据
if (records && records.length > 0) {
riskStatistics.value = records[0]
} else {
// 如果没有数据,设置默认值
riskStatistics.value = {
overdue: 0,
processed: 0,
pending: 0,
processing: 0
}
}
} catch (error) {
console.error('获取风险统计数据失败:', error)
}
}
// 方法定义
const onRegionChange = (item: RegionItem): void => {
selectedRegion.value = item.name
router.push({
path: '/screen/region',
query: { region: item.name, regionCode: item.code }
})
}
const openRegionSelector = (): void => {
regionSelectorVisible.value = true
}
const updateTime = (): void => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekday = weekdays[now.getDay()]
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
currentTime.value = `${year}${month}${day}${weekday} ${hours}:${minutes}:${seconds}`
}
const timeOut1 = (): void => {
setInterval(() => {
if (sourceIndex.value === 2) {
sourceIndex.value--
} else {
sourceIndex.value++
}
}, 3000)
}
// 图表初始化方法
const initCharts = (): void => {
if (dashboardData.value) {
initDonutChart()
initProgressChart()
initClassicChart()
}
}
// 总体概览-风险统计-饼图
const initDonutChart = (): void => {
if (dashboardData.value) {
const option: EChartsOption = {
legend: {
bottom: '10%',
right: '0%',
orient: 'vertical' as const,
textStyle: {
color: '#ffffff',
fontSize: '11px'
}
},
series: [
{
type: 'pie' as const,
radius: ['25%', '55%'],
center: ['50%', '50%'],
data: [
{ value: dashboardData.value.riskStatistics.overdue, name: '已逾期', itemStyle: { color: '#ef4444' } },
{ value: dashboardData.value.riskStatistics.processed, name: '已处理', itemStyle: { color: '#10b981' } },
{ value: dashboardData.value.riskStatistics.pending, name: '待排查', itemStyle: { color: '#eab308' } },
{ value: dashboardData.value.riskStatistics.processing, name: '处理中', itemStyle: { color: '#3b82f6' } },
{ value: dashboardData.value.riskStatistics.other, name: '其他', itemStyle: { color: '#f59e0b' } }
],
label: {
show: true,
position: 'outside' as const,
formatter: function (params: any) {
return params.value + ' ' + params.percent + '%'
},
fontSize: 12,
color: '#ffffff',
fontWeight: 'bold'
},
labelLine: {
show: true,
length: 15,
length2: 10,
lineStyle: {
color: '#64748b',
width: 1
}
}
}
]
}
donutChart.value = option
}
}
// 隐患处理进度
const initProgressChart = (): void => {
if (dashboardData.value) {
const progressOption: EChartsOption = {
series: [
{
type: 'pie' as const,
radius: '60%',
center: ['50%', '50%'],
left: 0,
top: 0,
bottom: 0,
data: [
{ value: dashboardData.value.hiddenDangerData.progress.overdue, name: '已逾期' },
{ value: dashboardData.value.hiddenDangerData.progress.processed, name: '已处理' },
{ value: dashboardData.value.hiddenDangerData.progress.pending, name: '待排查' },
{ value: dashboardData.value.hiddenDangerData.progress.processing, name: '处理中' },
],
label: {
alignTo: 'edge' as const,
formatter: '{time|{c} %}\n',
minMargin: 5,
edgeDistance: 10,
lineHeight: 15,
rich: {
time: {
fontSize: 10,
color: '#fff'
}
}
},
labelLine: {
length: 5,
length2: 0,
maxSurfaceAngle: 10
},
labelLayout: function (params: any) {
const isLeft = params.labelRect.x < 200; // 使用固定宽度值
const points = params.labelLinePoints;
points[2][0] = isLeft
? params.labelRect.x
: params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
}
]
}
progressChart.value = progressOption
}
}
// Top3隐患类
const initClassicChart = (): void => {
if (dashboardData.value) {
const classicOption: EChartsOption = {
series: [
{
type: 'pie' as const,
roseType: 'radius' as const,
radius: [30, 50],
center: ['50%', '50%'],
left: 0,
top: 0,
bottom: 0,
data: [
{ value: dashboardData.value.hiddenDangerData.top3Types.access, name: '门禁' },
{ value: dashboardData.value.hiddenDangerData.top3Types.consumption, name: '消费' },
{ value: dashboardData.value.hiddenDangerData.top3Types.inspection, name: '巡检' },
],
label: {
alignTo: 'edge' as const,
formatter: '{time|{c}}\n',
minMargin: 5,
edgeDistance: 10,
lineHeight: 15,
rich: {
time: {
fontSize: 10,
color: '#fff'
}
}
},
labelLine: {
length: 5,
length2: 0,
maxSurfaceAngle: 10
},
labelLayout: function (params: any) {
const isLeft = params.labelRect.x < 200; // 使用固定宽度值
const points = params.labelLinePoints;
points[2][0] = isLeft
? params.labelRect.x
: params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
}
]
}
classicChart.value = classicOption
}
}
</script>
<style scoped lang="scss">
/* 响应式设计 - 自动适应不同屏幕尺寸 */
@media (width <=1200px) {
.dashboard-container {
.content-container {
column-gap: 4vw;
.center-container {
width: 60vh;
height: 60vh;
}
}
}
}
@media (width <=1024px) {
.dashboard-container {
.header-container {
.header-title {
font-size: 1.1rem;
}
.header-left .back-button {
min-width: 8vw;
font-size: 0.8rem;
}
.header-right {
font-size: 0.8rem;
}
}
.content-container {
column-gap: 3vw;
padding: 0 8px 15px;
.left-wrapper {
.left-top .top-card {
.top-card-left {
min-width: 18vw;
}
.top-card-right {
min-width: 25vw;
}
}
.left-bottom .donut-chart-with-labels {
width: 35vw;
height: 35vh;
}
}
.center-container {
width: 55vh;
height: 55vh;
.center-content {
.title {
font-size: 0.8rem;
}
.sub-title {
font-size: 0.7rem;
}
.type-wrapper {
width: 85%;
.type-item .type-btn {
padding: 2px 25px;
font-size: 0.65rem;
}
}
}
}
}
}
}
@media (width <=768px) {
.dashboard-container {
font-size: 0.7rem;
.header-container {
height: 70px;
.header-left {
line-height: 70px;
.back-button {
min-width: 12vw;
padding: 3px 12px;
font-size: 0.7rem;
}
}
.header-title {
font-size: 1rem;
line-height: 35px;
}
.header-right {
margin-right: 15px;
font-size: 0.7rem;
line-height: 70px;
}
}
.weather-warning {
margin-left: 5%;
font-size: 0.7rem;
line-height: 35px;
}
.content-container {
column-gap: 2vw;
padding: 0 5px 10px;
.panel-title {
margin: 3px 15px 0;
font-size: 0.7rem;
}
.left-wrapper {
.left-top .top-card {
padding: 0 15px;
column-gap: 10px;
.top-card-left {
height: 10vh;
min-width: 20vw;
.total-number {
width: 22px;
height: 40px;
font-size: 0.7rem;
line-height: 40px;
}
}
.top-card-right {
height: 10vh;
min-width: 30vw;
.top-card-right-item {
font-size: 0.65rem;
.type-number-wrapper .type-number {
width: 12px;
height: 20px;
font-size: 0.8rem;
line-height: 20px;
}
}
}
}
.left-bottom {
.tabs .tab {
padding: 1px 8px;
font-size: 0.7rem;
}
.donut-chart-with-labels {
width: 40vw;
height: 40vh;
margin-left: 1vw;
}
}
}
.right-wrapper {
.right-top,
.right-bottom {
.tip-container {
width: 80%;
height: 70px;
.tip-image img {
width: 60px;
height: 60px;
}
.tip-content .col-item {
font-size: 0.65rem;
}
}
.list-content {
width: 75%;
.list .list-item {
padding: 0.4vh 0.3vw;
font-size: 0.65rem;
}
}
}
}
.center-container {
top: 60%;
width: 50vh;
height: 50vh;
.center-content {
.title {
margin-top: 1.5vh;
font-size: 0.7rem;
}
.sub-title {
font-size: 0.65rem;
}
.type-wrapper {
width: 90%;
margin-top: 0.3vh;
.type-item {
.type-btn {
padding: 2px 20px;
font-size: 0.6rem;
}
.type-num {
font-size: 0.9rem;
}
}
}
.clasic-wrapper {
width: 90%;
margin-top: 1vh;
.clasic-item {
font-size: 0.7rem;
}
}
.echart-wrapper {
width: 90%;
.lf-rt .progress-legend .legend-item {
font-size: 0.6rem;
}
}
.safe-wrapper {
width: 50%;
.safe-title {
font-size: 0.7rem;
}
.pending-count {
font-size: 1.4rem;
}
}
}
}
}
}
}
@media (width <=480px) {
.dashboard-container {
.header-container {
height: 60px;
.header-left {
line-height: 60px;
.back-button {
min-width: 15vw;
padding: 2px 10px;
font-size: 0.65rem;
}
}
.header-title {
font-size: 0.9rem;
line-height: 30px;
}
.header-right {
margin-right: 10px;
font-size: 0.65rem;
line-height: 60px;
}
}
.weather-warning {
margin-left: 3%;
font-size: 0.65rem;
line-height: 30px;
}
.content-container {
column-gap: 1vw;
padding: 0 3px 8px;
.left-wrapper {
.left-top .top-card {
padding: 0 10px;
column-gap: 8px;
.top-card-left {
height: 8vh;
min-width: 25vw;
.total-number {
width: 18px;
height: 35px;
font-size: 0.65rem;
line-height: 35px;
}
}
.top-card-right {
height: 8vh;
min-width: 35vw;
.top-card-right-item {
font-size: 0.6rem;
.type-number-wrapper .type-number {
width: 10px;
height: 18px;
font-size: 0.7rem;
line-height: 18px;
}
}
}
}
.left-bottom .donut-chart-with-labels {
width: 45vw;
height: 45vh;
margin-left: 0.5vw;
}
}
.center-container {
top: 65%;
width: 45vh;
height: 45vh;
.center-content {
.title {
margin-top: 1vh;
font-size: 0.65rem;
}
.sub-title {
font-size: 0.6rem;
}
.type-wrapper {
width: 95%;
.type-item .type-btn {
padding: 1px 15px;
font-size: 0.55rem;
}
}
.echart-wrapper {
width: 95%;
}
.safe-wrapper {
width: 60%;
.safe-title {
font-size: 0.65rem;
}
.pending-count {
font-size: 1.2rem;
}
}
}
}
}
}
}
.dashboard-container {
position: relative;
display: flex;
width: 100vw;
height: 100vh;
font-size: 0.8rem;
color: #fff;
background: url('@/assets/images/v2_rel0n6.png');
background-color: #030e22;
background-size: cover;
flex-direction: column;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.dot.red {
background-color: #ef4444;
}
.dot.green {
background-color: #10b981;
}
.dot.yellow {
background-color: #eab308;
}
.dot.blue {
background-color: #3b82f6;
}
.dot.cyan {
background-color: #06b6d4;
}
.dot.purple {
background-color: #8b5cf6;
}
.dot.orange {
background-color: #f59e0b;
}
.header-container {
display: flex;
height: 80px;
background: url('@/assets/images/top_bg.png') no-repeat;
background-size: 100%;
.header-left {
display: flex;
padding-left: 1vw;
line-height: 80px;
flex: 1;
align-items: center;
.back-img {
height: 3vh;
cursor: pointer;
transition: all 0.3s ease;
}
.back-button {
display: inline-flex;
height: 2vh;
min-width: 6vw;
padding: 4px 16px;
margin-left: 0.5vw;
font-size: 0.9rem;
cursor: pointer;
background: rgb(13 24 84 / 80%);
border: 1px solid rgb(59 130 246 / 40%);
transition: all 0.3s ease;
align-items: center;
justify-content: space-between;
}
.back-button:hover {
background: rgb(59 130 246 / 30%);
border-color: rgb(59 130 246 / 60%);
}
}
.header-right {
margin-right: 20px;
font-size: 0.9rem;
line-height: 80px;
text-align: right;
flex: 1;
}
.header-title {
font-size: 1.3rem;
font-weight: bold;
line-height: 40px;
color: #fff;
text-align: center;
flex: 1;
}
}
.weather-warning {
margin-left: 10%;
line-height: 45px;
}
.content-container {
position: relative;
display: flex;
width: calc(100vw - 20px);
height: calc(100vh - 105px);
padding: 0 10px 20px;
column-gap: 7vw;
.panel-title {
margin: 4px 20px 0;
font-size: 0.8rem;
font-weight: bold;
color: #fff;
}
.bottom-card {
display: flex;
flex-direction: column;
.bottom-card-title {
display: flex;
margin-top: 5px;
margin-left: -15%;
flex-direction: column;
align-items: center;
}
.bar-chart {
flex: 1;
width: 80%;
min-height: 17.5vh;
}
}
.left-wrapper {
flex: 1;
display: flex;
flex-direction: column;
row-gap: 1rem;
height: 100%;
.left-top {
padding: 0 5px;
background-image:
url('@/assets/images/screen/left_top_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;
flex: 1;
/* 设置重复方式 */
.top-card {
display: flex;
padding: 0 20px;
column-gap: 15px;
font-size: 0.8rem;
.top-card-left {
@keyframes numberGlow {
0% {
box-shadow: 0 0 5px rgb(177 74 201 / 50%);
}
100% {
box-shadow: 0 0 20px rgb(177 74 201 / 90%);
}
}
@keyframes flashEffect {
0% {
background-color: rgb(177 74 201 / 100%);
}
50% {
background-color: rgb(255 255 255 / 100%);
}
100% {
background-color: rgb(177 74 201 / 100%);
}
}
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;
.number-wrapper {
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
color: #fff;
}
.total-number {
display: inline-block;
width: 26px;
height: 50px;
font-size: 0.8rem;
line-height: 50px;
color: #fff;
text-align: center;
background-color: rgb(177 74 201 / 100%);
border-radius: 4px;
animation: numberGlow 2s ease-in-out infinite alternate;
transition: all 0.3s ease;
}
.total-number:hover {
transform: scale(1.1);
box-shadow: 0 0 15px rgb(177 74 201 / 80%);
}
.flash {
animation: flashEffect 0.3s ease-in-out;
}
}
.top-card-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;
.top-card-right-item {
display: flex;
align-items: center;
column-gap: 5px;
padding: 0 10px;
font-size: 0.7rem;
color: #fff;
.type-number-wrapper {
@keyframes typeNumberPulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
@keyframes typeFlashEffect {
0% {
background-color: #1afb8f;
}
50% {
background-color: rgb(255 255 255 / 100%);
}
100% {
background-color: #1afb8f;
}
}
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
color: #fff;
.type-number {
display: inline-block;
width: 14px;
height: 25px;
font-size: 0.9rem;
line-height: 25px;
color: #fff;
text-align: center;
background-color: #1afb8f;
border-radius: 2px;
animation: typeNumberPulse 1.5s ease-in-out infinite;
transition: all 0.3s ease;
}
.type-number:hover {
background-color: #16d47a;
transform: scale(1.2);
}
.flash {
animation: typeFlashEffect 0.3s ease-in-out;
}
}
}
}
}
}
.left-bottom {
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;
flex: 1;
/* 设置重复方式 */
.tabs {
display: inline-flex;
align-items: center;
column-gap: 8px;
}
.tab {
padding: 2px 10px;
color: #fff;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease-in-out;
user-select: none;
}
.tab:hover {
color: #1afb8f;
}
.tab.active {
color: #1afb8f;
background: rgb(26 251 143 / 12%);
border: 1px solid rgb(26 251 143 / 35%);
}
.divider {
margin: 0 2px;
color: #94a3b8;
}
.donut-chart-with-labels {
width: 30vw;
height: 30vh;
margin-left: 2vw;
}
}
}
.right-wrapper {
display: flex;
height: 100%;
flex: 1;
flex-direction: column;
row-gap: 1rem;
}
.center-container {
position: fixed;
top: 55%;
left: 50%;
z-index: 1;
display: flex;
width: 65vh;
height: 65vh;
color: #fff;
background-image: url('@/assets/images/circle_bg.png');
background-size: cover;
transform: translate(-50%, -50%);
align-items: center;
justify-content: center;
.center-content {
display: flex;
// background: rgba(255, 22, 1, 0.3);
width: 77%;
height: 77%;
overflow: hidden;
border-radius: 50%;
align-items: center;
flex-direction: column;
.title {
margin-top: 2vh;
font-size: 0.9rem;
font-weight: bold;
}
.bottom-border-line {
width: 20%;
margin: 1vh 0 1.2vh;
}
.sub-title {
font-size: 0.8rem;
}
.type-wrapper {
display: flex;
justify-content: space-around;
width: 80%;
font-size: 0.8rem;
color: #fff;
.type-item {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 0.5vh;
.type-btn {
padding: 2px 30px;
margin-bottom: 5px;
font-size: 0.7rem;
background-color: #d97706;
border-radius: 15px;
&.active {
background-color: #059669;
}
}
.type-num {
font-size: 1rem;
}
}
}
.clasic-wrapper {
display: flex;
width: 80%;
margin-top: 1.2vh;
font-size: 0.8rem;
color: #fff;
justify-content: space-around;
.clasic-item {
display: flex;
width: 45%;
margin-top: 0.6vh;
margin-bottom: -1vh;
flex-direction: column;
align-items: center;
img {
width: 100%;
height: 2px;
}
}
}
.echart-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 85%;
height: 75%;
.lf-rt {
display: flex;
width: 50%;
height: 100%;
flex-direction: column;
.progress-chart {
display: flex;
width: 100%;
height: 80%;
align-items: center;
justify-content: center;
}
.progress-legend {
display: flex;
justify-content: center;
gap: 8px;
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
}
}
}
}
.safe-wrapper {
display: flex;
width: 40%;
height: 20%;
margin-bottom: 5%;
align-items: center;
column-gap: 1vw;
.safe-title {
display: flex;
align-items: center;
margin-left: 1vw;
}
.pending-count {
margin-bottom: 0.7vh;
font-size: 1.6rem;
font-weight: 500;
color: yellow;
}
}
}
}
}
/* 全局样式 */
</style>