Files
lc_frontend/src/views/screen/companyScreen.vue
2026-01-06 12:08:08 +08:00

2050 lines
65 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">
<HeaderSelector
back-button-type="image"
:show-back-button="true"
:on-back="returnToHeadquarters"
:display-text="selectedPark"
:clickable="false"
:show-selector-indicator="false"
selector-type="none"
/>
<h1 class="header-title">{{ selectedPark }}综合监控大屏</h1>
<div class="date-wrapper">
<span style="margin-top: 6%;font-size: 0.9rem;">{{ currentDate }}</span>
<span style="margin-top: 6%;font-size: 0.9rem;">{{ currentWeek }}</span>
<span style="margin-top: 6%;font-size: 0.9rem;">{{ currentTime }}</span>
</div>
</div>
<!-- 天气预报 -->
<WeatherWarning />
<!-- 主内容区 -->
<div class="content-container">
<div class="left-wrapper">
<!-- 左上区域 -->
<div class="left-top">
<div class="panel-title">人员管理</div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
<div class="top-card">
<div class="top-card-left">
<div>
<img width="33px" src="@/assets/images/1_224520_821.png" />
</div>
<span>总计</span>
<div class="number-wrapper">
<span class="total-number" v-for="(digit, index) in totalCountDigits" :key="index">
{{ digit }}
</span>
</div>
<span></span>
</div>
<div class="top-card-right">
<div class="top-card-right-item">
<img width="18px" src="@/assets/images/v2_rel0n8.png" />
<span>正式员工</span>
<div class="type-number-wrapper" style="margin-left: 2vw">
<span class="type-number" v-for="(digit, index) in formalEmployeeDigits" :key="index">
{{ digit }}
</span>
</div>
<span></span>
</div>
<div class="top-card-right-item">
<img width="18px" src="@/assets/images/v2_rel0n23.png" />
<span>外协人员</span>
<div class="type-number-wrapper" style="margin-left: 1vw">
<span class="type-number" v-for="(digit, index) in externalStaffDigits" :key="index">
{{ digit }}
</span>
</div>
<span></span>
</div>
<div class="top-card-right-item">
<img width="18px" src="@/assets/images/24508_654.png" />
<span>访客</span>
<div class="type-number-wrapper">
<span class="type-number" v-for="(digit, index) in visitorDigits" :key="index">
{{ digit }}
</span>
</div>
<span></span>
</div>
</div>
</div>
<div class="bottom-card">
<div class="bottom-card-title">
<!-- 隐患排查治理 标题需要隐藏 2025-10-31 -->
<span></span>
<img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" />
</div>
<div class="type-wrapper">
<div class="type-item">
<span class="type-btn yellow">重大</span>
<span class="type-num cursor-pointer" @click="handleSeverityCountClick">{{
mockData.hiddenDangerData.severityCount }}</span>
</div>
<div class="type-item">
<span class="type-btn green">一般</span>
<span class="type-num cursor-pointer" @click="handleGeneralCountClick">{{
mockData.hiddenDangerData.generalCount }}</span>
</div>
<!-- <div class="type-item">
<span class="type-btn blue">较大</span>
<span class="type-num">{{ mockData.hiddenDangerData.majorCount }}</span>
</div> -->
</div>
<div class="hazard-wrapper">
<div ref="progressChartRef" class="progress-chart"></div>
<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>
</div>
<!-- 左下区域 -->
<div class="left-bottom">
<!-- <div class="panel-title">
<div class="tabs">
<span class="tab" :class="{ active: activeTab === '高危作业' }" @click="handleTabClick('高危作业')">高危作业</span>
<span class="divider">|</span>
<span class="tab" :class="{ active: activeTab === '安全培训考试' }"
@click="handleTabClick('安全培训考试')">安全培训考试</span>
<span class="divider">|</span>
<span class="tab" :class="{ active: activeTab === '应急预案及演练' }"
@click="handleTabClick('应急预案及演练')">应急预案及演练</span>
</div>
</div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line.png" />
<div style="padding-top: 10px;display: flex; flex-direction: column; justify-content: start; height: 91%;">
<template v-if="activeTab === '高危作业'">
<div class="bottom-card">
<div class="bottom-card-title" style="margin-top: 0;">
<span>各园区统计</span>
<img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" />
</div>
</div>
<div class="row-wrapper">
<div class="row-item">
<img src="@/assets/images/screen/unfinished.png" width="20" />
<span class="row-item-title">未开始数量</span>
<span class="row-item-number">{{ unfinishedCount }}</span>
</div>
<div class="row-item">
<img src="@/assets/images/screen/inProcess.png" width="20" />
<span class="row-item-title">进行中数量</span>
<span class="row-item-number">{{ inProgressCount }}</span>
</div>
<div class="row-item">
<img src="@/assets/images/screen/finished.png" width="20" />
<span class="row-item-title">已完成数量</span>
<span class="row-item-number">{{ finishedCount }}</span>
</div>
</div>
<div style="display: flex; width: 100%;">
<AlertList maxHeight="25vh" style="margin-left: 1vw;" :list-data="dangerList" />
</div>
</template>
<template v-if="activeTab === '安全培训考试'">
<div class="bottom-card" style="margin-top: 10px;">
<div class="bottom-card-title" style="margin-top: 0;">
<span>安全培训考试</span>
<img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" />
</div>
</div>
<AlertList maxHeight="40vh" style="margin-left: 1vw;" :table-title="tableTitle" :list-data="examList" />
</template>
<template v-if="activeTab === '应急预案及演练'">
<div class="bottom-card">
<div class="bottom-card-title" style="margin-top: 0;">
<span>应急预案及演练</span>
<img width="50%" style="margin: 8px 0" src="@/assets/images/line_1.png" />
</div>
</div>
<AlertList maxHeight="40vh" style="margin-left: 1vw;" :table-title="trainingTableTitle"
:list-data="drillList" />
</template>
</div> -->
<RiskStatisticsPanel :riskStatistics="riskStatistics" :dangerDetail="dangerDetail" :park="parkValue"
@tab-change="handleRiskTabChange" :campus_id="query.campus_id" />
</div>
</div>
<div class="right-wrapper">
<!-- 右上区域 -->
<div class="right-top" style="text-align: right">
<div class="panel-title">高风险告警</div>
<div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
</div>
<div class="tip-container">
<div class="tip-image">
<img src="@/assets/images/screen/circle_image.png" width="80" height="80" />
<span class="number">{{ mockData.alertData.total }}</span>
</div>
<img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" />
<div class="tip-content">
<div class="col-item">
<img src="@/assets/images/screen/warning_img.png" width="23" />
<span>告警总数</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ mockData.alertData.total }}</span>
</div>
<div class="col-item">
<span>已处理</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: greenyellow;">{{ mockData.alertData.processed
}}</span>
</div>
<div class="col-item" style=" display: flex;margin-left: 2vw; align-items: center;">
<span>待处理</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ mockData.alertData.pending
}}</span>
</div>
<div class="col-item" style=" display: flex;margin-left: 2vw; align-items: center;">
<span>处理中</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: yellow;">{{ mockData.alertData.processing
}}</span>
</div>
</div>
</div>
<div style=" display: flex; width: 100%;margin-top: 1vw; flex: 1; justify-content: flex-end;">
<AlertList linkUrl="http://10.0.64.20/security/console/command-center?p=tabl" style="margin-right: 1vw;" title="告警详情" :list-data="mockData.alertData.details" />
</div>
</div>
<!-- 右下区域 -->
<div class="right-bottom" style="text-align: right">
<div class="panel-title">超时工单</div>
<img style="margin: 8px 0" src="@/assets/images/title_border_line_1.png" />
<div class="tip-container">
<div class="tip-image">
<img src="@/assets/images/screen/circle_image.png" width="80" height="80" />
<span class="number">{{ mockData.timeoutWorkOrders.total }}</span>
</div>
<img src="@/assets/images/screen/tip_bg_image.png" width="100%" height="70" />
<div class="tip-content">
<div class="col-item">
<img src="@/assets/images/screen/warning_img.png" width="23" />
<span>超时工单数</span>
<span style="font-size: 1.2rem; marker-start: 2vw; color: red;">{{ mockData.timeoutWorkOrders.total
}}</span>
</div>
</div>
</div>
<div style=" display: flex; width: 100%;margin-top: 1vw; flex: 1; justify-content: flex-end;">
<AlertList linkUrl="http://10.0.64.20/pms/workorder-list" style="margin-right: 1vw;" title="工单详情" :list-data="mockData.timeoutWorkOrders.details" />
</div>
</div>
</div>
<div class="center-container">
<!-- 中部区域 -->
<ParkCenter :parkName="selectedPark" :backgroundImage="backgroundImage" @point-hover="handlePointHover"
@point-leave="handlePointLeave" />
</div>
<!-- 点位信息弹窗 -->
<PointInfoPopup v-if="showPointPopup" :visible="showPointPopup" :point-info="currentPoint"
:park-name="selectedPark || '雄安园区'" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import * as echarts from 'echarts'
import ParkCenter from './components/ParkCenter.vue'
import PointInfoPopup from './components/PointInfoPopup.vue'
import WeatherWarning from './components/WeatherWarning.vue'
import AlertList from './components/AlertList.vue'
import { getTableList, getTableData, getDangerDetail, getDangerCount, getExamDetail, getDrillDetail, getWorkOrderStatistics } from './report'
import RiskStatisticsPanel from './components/RiskStatisticsPanel.vue'
import HeaderSelector from './components/HeaderSelector.vue'
interface PointPosition {
label: string
x: number
y: number
relativeX: number
relativeY: number
}
const totalCountDigits = computed(() => String(mockData.totalCount).split('').map(Number))
const formalEmployeeDigits = computed(() => String(mockData.formalEmployeeCount).split('').map(Number))
const externalStaffDigits = computed(() => String(mockData.externalStaffCount).split('').map(Number))
const visitorDigits = computed(() => String(mockData.visitorCount).split('').map(Number))
const route = useRoute()
const router = useRouter()
const selectedPark = ref('雄安园区')
const backgroundImage = ref('')
const currentDateTime = ref<string>('')
const currentDate = ref<string>('')
const currentWeek = ref<string>('')
const currentTime = ref<string>('')
const activeTab = ref<string>('高危作业')
const showPointPopup = ref(false)
const currentPoint = ref<PointPosition | null>(null)
const progressChart = ref<echarts.ECharts | null>(null)
const progressChartRef = ref<HTMLElement | null>(null)
const timeUpdateTimerId = ref<ReturnType<typeof setInterval> | null>(null)
const dangerList = ref<any[]>([])
const unfinishedCount = ref<number>(0)
const inProgressCount = ref<number>(0)
const finishedCount = ref<number>(0)
const handleSeverityCountClick = () => {
window.open('http://10.0.64.20/pms/workorder-list', '_blank')
}
const handleGeneralCountClick = () => {
window.open('http://10.0.64.20/pms/workorder-list', '_blank')
}
const riskStatistics = ref<any>({
'安全类': [],
'工程类': []
})
const dangerDetail = ref<any>({
'安全类': [],
'工程类': []
})
const parkValue = ref<string>('')
type TabType = '安全类' | '工程类'
const handleRiskTabChange = async (tab: TabType) => {
console.log('Tab changed to:', tab)
try {
let workOrderType = ''
switch (tab) {
case '安全类':
workOrderType = '安全生产'
break
case '工程类':
workOrderType = '物业服务-工程'
break
default:
workOrderType = '安全生产'
}
// 同时获取维保任务和巡检任务的数据
const [maintenanceResponse, inspectionResponse] = await Promise.all([
getWorkOrderStatistics({ workOrderType, taskType: '维保任务', campus_id: query.campus_id }).catch(error => {
console.error('获取维保任务数据失败:', error)
return { records: [] }
}),
getWorkOrderStatistics({ workOrderType, taskType: '巡检任务', campus_id: query.campus_id }).catch(error => {
console.error('获取巡检任务数据失败:', error)
return { records: [] }
})
])
// 周期映射
const cycleMap: Record<string, string> = {
'day': '每日',
'month': '每月',
'year': '每年'
}
// 将API数据转换为图表数据格式
const convertToChartData = (records: any[], taskTypeName: string): any[] => {
const charts: any[] = []
// 按周期分组
const cycleGroups: Record<string, any> = {}
records.forEach((record: any) => {
const cycle = record.cycle || 'day'
if (!cycleGroups[cycle]) {
cycleGroups[cycle] = record
}
})
// 创建6个图表3个周期 x 2个任务类型但这里只处理一种任务类型
const cycles = ['day', 'month', 'year']
cycles.forEach((cycle) => {
const data = cycleGroups[cycle] || {}
const title = `${cycleMap[cycle]}检查(${taskTypeName})`
charts.push({
title,
total: Number(data.total) || 0,
status: {
notStarted: Number(data.pending) || 0,
inProgress: Number(data.processing) || 0,
done: Number(data.processed) || 0,
voided: Number(data.closed) || 0
}
})
})
return charts
}
// 转换维保和巡检数据
const maintenanceCharts = convertToChartData(maintenanceResponse.records || [], '维保类')
const inspectionCharts = convertToChartData(inspectionResponse.records || [], '巡检类')
// 合并为6个图表先维保3个后巡检3个
const allCharts = [...maintenanceCharts, ...inspectionCharts]
// 更新riskStatistics
riskStatistics.value[tab] = allCharts
console.log('更新后的riskStatistics:', riskStatistics.value)
} catch (error) {
console.error('获取风险统计数据失败:', error)
}
}
const tableTitle = [
{
name: '培训(考试)名称',
key: 'examname'
},
{
name: '参与人次',
key: 'exampeoplenum'
},
{
name: '培训时长(小时)',
key: 'examduration',
},
{
name: '考试通过率(%)',
key: 'exampassrate',
}
]
const trainingTableTitle = [
{
name: '演练名称',
key: 'drill_plan_name'
},
{
name: '完成时间',
key: 'drill_time'
},
{
name: '参与人数',
key: 'drill_count'
}
]
const examList = ref<any[]>([])
const drillList = ref<any[]>([])
const query = reactive({
pageNo: 1,
pageSize: 10000,
startDate: currentDateTime.value + " 00:00:00",
endDate: currentDateTime.value + " 23:59:59",
campus_id: "1825468527486140416" //园区id如果是多个用逗号拼接
})
// 模拟数据
const mockData = reactive({
totalCount: 0,
formalEmployeeCount: 0,
externalStaffCount: 0,
visitorCount: 0,
buildInfoList: [
{
highRiskCount: 2,
buildName: "A楼栋",
alarmsCount: 23
},
{
highRiskCount: 1,
buildName: "B楼栋",
alarmsCount: 3
},
{
highRiskCount: 11,
buildName: "C楼栋",
alarmsCount: 33
},
{
highRiskCount: 11,
buildName: "D楼栋",
alarmsCount: 33
}
],
alertData: {
total: 0,
processed: 0,
pending: 0,
processing: 0,
details: [
// {
// description: '2024-07-10 上午8:00 雄安园区隐患内容管理 状态 处理',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283935'
// },
// {
// description: '2025-07-09 上海XXX区域A1门禁告警 处理中 紧急',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283936'
// },
// {
// description: '2025-07-09 上海XXX区域A1门禁告警 处理中 紧急',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2024-07-10 上午8:00 雄安园区隐患内容管理 状态 处理',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283935'
// },
// {
// description: '2025-07-09 上海XXX区域A1门禁告警 处理中 紧急',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283936'
// },
// {
// description: '2025-07-09 上海XXX区域A1门禁告警 处理中 紧急',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }
]
},
timeoutWorkOrders: {
total: 0,
details: [
// {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// },
// {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区隐患内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// },
]
},
hiddenDangerData: {
generalCount: 0,
majorCount: 0,
severityCount: 0,
progress: {
participateCount: 0,
toParticipateCount: 0,
finishCount: 0
},
progressDetail: [
// {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'severity',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// },
// {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'major',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'severity',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// }, {
// description: '2025-08-10 上午8:00 武汉园区参与内容管理 状态 处理',
// alarm_level_code: 'general',
// alarm_status: '已关闭',
// alarm_biz_id: '20250827164305283937'
// },
]
}
})
// 返回上一级
const returnToHeadquarters = () => {
router.back()
}
const handlePointHover = (position: PointPosition) => {
currentPoint.value = position
showPointPopup.value = true
}
const handlePointLeave = () => {
showPointPopup.value = false
currentPoint.value = null
}
// 初始化进度图表
const initProgressChart = () => {
const chartElement = progressChartRef.value
if (chartElement) {
progressChart.value = echarts.init(chartElement)
const width = progressChart.value.getWidth()
const progressOption = {
series: [
{
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
left: 0,
top: 0,
bottom: 0,
data: [
{ value: 20, name: '已逾期', itemStyle: { color: '#ef4444' } },
{ value: 35, name: '已处理', itemStyle: { color: '#10b981' } },
// { value: 25, name: '待排查', itemStyle: { color: '#eab308' } },
{ value: 20, name: '处理中', itemStyle: { color: '#3b82f6' } },
],
label: {
alignTo: 'edge',
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 < width / 2;
const points = params.labelLinePoints;
if (points && points.length && points[2]) {
points[2][0] = isLeft
? params.labelRect.x
: params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
}
},
}
]
}
progressChart.value.setOption(progressOption)
}
}
const isFirstLoad = ref<boolean>(true)
// 动画相关的状态
const isAnimating = ref<boolean>(false)
const animationDuration = 2000 // 动画持续时间(毫秒)
const dashboardTimerId = ref<ReturnType<typeof setInterval> | null>(null)
interface RegionItem {
name: string
code: string
pic_url: string
}
// 缓存工具函数 - 三个页面共享的缓存
const CACHE_KEY = 'shared_regionOption_cache'
const CACHE_DURATION = 5 * 60 * 1000 // 5分钟
interface CacheData {
records: any[] // 原始接口返回的数据
timestamp: number
}
const getCachedRegionOption = (): any[] | null => {
try {
const cached = sessionStorage.getItem(CACHE_KEY)
if (cached) {
const cacheData: CacheData = JSON.parse(cached)
const now = Date.now()
// 检查缓存是否在有效期内
if (now - cacheData.timestamp < CACHE_DURATION) {
console.log('使用缓存的 regionOption 数据')
return cacheData.records
} else {
console.log('缓存已过期,清除缓存')
sessionStorage.removeItem(CACHE_KEY)
}
}
} catch (error) {
console.error('读取缓存失败:', error)
sessionStorage.removeItem(CACHE_KEY)
}
return null
}
const setCachedRegionOption = (records: any[]) => {
try {
const cacheData: CacheData = {
records,
timestamp: Date.now()
}
sessionStorage.setItem(CACHE_KEY, JSON.stringify(cacheData))
console.log('regionOption 数据已缓存')
} catch (error) {
console.error('保存缓存失败:', error)
}
}
const regionOption = ref<RegionItem[]>([])
onMounted(async () => {
updateTime()
timeUpdateTimerId.value = setInterval(updateTime, 1000)
if (typeof route.query.parkName === 'string') {
selectedPark.value = route.query.parkName
query.campus_id = route.query.parkCode as string
}
handleRiskTabChange('安全类')
// 先检查缓存
const cachedRecords = getCachedRegionOption()
let records = cachedRecords
if (!records || records.length === 0) {
// 缓存不存在或已过期,调用接口
try {
let result = await getTableList('park_info_list')
records = result.records || []
if (records && records.length > 0) {
// 保存到缓存
setCachedRegionOption(records)
}
} catch (error) {
console.error('初始化园区数据失败:', error)
records = []
}
}
if (records && records.length > 0) {
// 更新为新的数据格式
regionOption.value = records.map(el => ({
name: el.park_name,
code: el.park_code,
pic_url: el.pic_url
}))
for (let i = 0; i < regionOption.value.length; i++) {
const el = regionOption.value[i];
if (el.code == query.campus_id && el.pic_url) {
backgroundImage.value = el.pic_url
}
}
}
// 初始化数据
await loadDashboardData()
// 添加周期性动画演示
dashboardTimerId.value = setInterval(async () => {
await loadDashboardData()
}, 2 * 60 * 1000) // 每2分钟更新一次
})
const loadDashboardData = () => {
if (isFirstLoad.value) {
isFirstLoad.value = false
}
// 获取总体概览数据
getTableList('generalTotal', query).then(generalTotal => {
if (generalTotal.records && generalTotal.records.length > 0) {
mockData.totalCount = Number(generalTotal.records[0].totalCount)
mockData.formalEmployeeCount = Number(generalTotal.records[0].formalEmployeeCount)
mockData.externalStaffCount = Number(generalTotal.records[0].externalStaffCount)
mockData.visitorCount = Number(generalTotal.records[0].visitorCount)
}
}).catch(error => {
console.log("获取总体概览数据失败>>>>>>>", error);
})
// 获取总体概览-隐患排查治理数量
// getTableList('hidden_danger_investigation', query).then(res => {
// if (res.records && res.records.length > 0) {
// mockData.hiddenDangerData.generalCount = res.records[0].general
// mockData.hiddenDangerData.severityCount = res.records[0].major
// // 获取隐患排查治理数据
// getTableList('risk_level_count', { ...query }).then(res => {
// mockData.hiddenDangerData.generalCount = Number(mockData.hiddenDangerData.generalCount) + Number(res.records[0].general_count)
// mockData.hiddenDangerData.severityCount = Number(mockData.hiddenDangerData.severityCount) + Number(res.records[0].major_count)
// })
// }
// }).catch(error => {
// console.log("获取隐患排查治理数量失败>>>>>>>", error);
// })
// 获取总体概览-隐患排查治理统计
// getTableList('hidden_danger_process_progress', query).then(res => {
// if (res.records && res.records.length > 0) {
// const process = res.records[0]
// progressChart.value = echarts.init(progressChartRef.value)
// const width = progressChart.value.getWidth()
// const progressOption = {
// series: [
// {
// type: 'pie',
// radius: '60%',
// center: ['50%', '50%'],
// left: 0,
// top: 0,
// bottom: 0,
// data: [
// { value: process.overdue || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
// { value: process.processed || 0, name: '已处理', itemStyle: { color: '#10b981' } },
// { value: process.pending || 0, name: '待排查', itemStyle: { color: '#eab308' } },
// { value: process.processing || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
// ],
// label: {
// alignTo: 'edge',
// 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 < width / 2;
// const points = params.labelLinePoints;
// points[2][0] = isLeft
// ? params.labelRect.x
// : params.labelRect.x + params.labelRect.width;
// return {
// labelLinePoints: points
// };
// },
// }
// ]
// }
// progressChart.value.setOption(progressOption)
// getTableData('1963446160885366786', { ...query, park_name: query.campus_id }).then(res => {
// let overdueCnt = 0
// let processedCnt = 0
// let processingCnt = 0
// res.records.forEach(el => {
// // 整改状态
// if (el.corrective_status == 2) { //已逾期
// overdueCnt++
// }
// if (el.corrective_status == 1) { //已处理
// processedCnt++
// }
// if (el.corrective_status == 0) { //处理中
// processingCnt++
// }
// });
// progressOption.series[0].data[0].value = Number(progressOption.series[0].data[0].value) + overdueCnt
// progressOption.series[0].data[1].value = Number(progressOption.series[0].data[1].value) + processedCnt
// progressOption.series[0].data[3].value = Number(progressOption.series[0].data[3].value) + processingCnt
// if (progressChart.value) {
// progressChart.value.setOption(progressOption)
// }
// })
// }
// }).catch(error => {
// console.log("获取隐患排查治理统计数据失败>>>>>>>", error);
// })
handleTabClick("高危作业")
// 获取风险预警数据
getTableList('risk_alert_data', query).then(risk_alert_data => {
if (risk_alert_data.records && risk_alert_data.records.length > 0) {
mockData.alertData.total = risk_alert_data.records[0].total
mockData.alertData.processed = risk_alert_data.records[0].processed
mockData.alertData.pending = risk_alert_data.records[0].pending
mockData.alertData.processing = risk_alert_data.records[0].processing
}
}).catch(error => {
console.error('获取风险预警数据失败:', error)
})
// 获取风险预警详情数据
getTableList('risk_alert_detail', query).then(risk_alert_detail => {
if (risk_alert_detail.records && risk_alert_detail.records.length > 0) {
mockData.alertData.details = risk_alert_detail.records
}
}).catch(error => {
console.error('获取风险预警详情数据失败:', error)
})
// 获取超期工单数据
getTableList('timeout_work_order', query).then(timeout_work_order => {
if (timeout_work_order.records && timeout_work_order.records.length >= 0) {
mockData.timeoutWorkOrders.total = timeout_work_order.records.length
mockData.timeoutWorkOrders.details = timeout_work_order.records
}
}).catch(error => {
console.error('获取超期工单数据失败:', error)
})
// 获取超期工单数据
getTableList('park_build_info', query).then(res => {
if (res.records && res.records.length > 0) {
}
}).catch(error => {
console.error('获取超期工单数据失败:', error)
})
handleHiddenDangerPannelData(query)
}
const handleHiddenDangerPannelData = (query) => {
let _data = {
flag: false,
general: 0,
major: 0,
overdue: 0,
processed: 0,
processing: 0,
pending: 0
}
let _data2 = {
flag: false,
general: 0,
major: 0,
overdue: 0,
processed: 0,
processing: 0,
pending: 0
}
progressChart.value = echarts.init(progressChartRef.value)
const width = progressChart.value.getWidth()
const progressOption = {
series: [
{
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
left: 0,
top: 0,
bottom: 0,
data: [
// { value: overdueCnt || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
// { value: processedCnt || 0, name: '已处理', itemStyle: { color: '#10b981' } },
// { value: pendingCnt || 0, name: '待排查', itemStyle: { color: '#eab308' } },
// { value: processingCnt || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
] as any,
label: {
alignTo: 'edge',
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 < width / 2;
const points = params.labelLinePoints;
if (points && points.length && points[2]) {
points[2][0] = isLeft
? params.labelRect.x
: params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
}
},
}
]
}
try {
// 获取隐患排查治理数据
getTableList('risk_level_count', query).then(res => {
if (res.records && res.records.length > 0) {
_data.general = _data.general + Number(res.records[0].general_count)
_data.major = _data.major + Number(res.records[0].major_count)
// 获取隐患排查治理数据
getTableList('risk_status_count', query).then(res => {
if (res.records && res.records.length > 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)
if (_data2.flag) {
// 合并数据
console.log("请求系统和第三方成功,合并数据", _data, _data2);
let generalCnt = _data.general + _data2.general
let majorCnt = _data.major + _data2.major
mockData.hiddenDangerData.generalCount = generalCnt
mockData.hiddenDangerData.severityCount = 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'
}
progressOption.series[0].data = [
{ value: Number(overdueCnt) || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
{ value: Number(processedCnt) || 0, name: '已处理', itemStyle: { color: '#10b981' } },
{ value: Number(pendingCnt) || 0, name: '待排查', itemStyle: { color: '#eab308' } },
{ value: Number(processingCnt) || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
]
console.log('合并后的图表数据:', progressOption.series[0].data)
if (progressChart.value != null) {
progressChart.value.setOption(progressOption)
}
} else {
console.log("请求系统成功,展示数据", _data, _data2);
mockData.hiddenDangerData.generalCount = _data.general
mockData.hiddenDangerData.severityCount = _data.major
// 接口返回的已经是百分比,直接使用
progressOption.series[0].data = [
{ value: _data.overdue || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
{ value: _data.processed || 0, name: '已处理', itemStyle: { color: '#10b981' } },
{ value: _data.pending || 0, name: '待排查', itemStyle: { color: '#eab308' } },
{ value: _data.processing || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
]
console.log('系统数据图表数据:', progressOption.series[0].data)
if (progressChart.value != null) {
progressChart.value.setOption(progressOption)
}
}
}
})
}
})
// 获取隐患排查治理数据
getTableList('hidden_danger_investigation', query).then(res => {
if (res.records && res.records.length > 0) {
_data2.general = Number(res.records[0].general)
_data2.major = Number(res.records[0].major)
// 获取隐患排查治理处理进度数据
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
// mockData.hiddenDangerData.generalCount = generalCnt
// mockData.hiddenDangerData.severityCount = 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)
// progressOption.series[0].data = [
// { value: overdueCnt || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
// { value: processedCnt || 0, name: '已处理', itemStyle: { color: '#10b981' } },
// { value: pendingCnt || 0, name: '待排查', itemStyle: { color: '#eab308' } },
// { value: processingCnt || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
// ]
// if (progressChart.value != null) {
// progressChart.value.setOption(progressOption)
// }
// } else {
// //显示三方数据
// console.log("请求第三方成功,展示数据", _data, _data2);
// mockData.hiddenDangerData.generalCount = _data2.general
// mockData.hiddenDangerData.severityCount = _data2.major
// let overdueCnt = res.records[0].overdue
// let processedCnt = res.records[0].processed
// let processingCnt = res.records[0].processing
// let pendingCnt = res.records[0].pending
// progressOption.series[0].data = [
// { value: overdueCnt || 0, name: '已逾期', itemStyle: { color: '#ef4444' } },
// { value: processedCnt || 0, name: '已处理', itemStyle: { color: '#10b981' } },
// { value: pendingCnt || 0, name: '待排查', itemStyle: { color: '#eab308' } },
// { value: processingCnt || 0, name: '处理中', itemStyle: { color: '#3b82f6' } }
// ]
// if (progressChart.value != null) {
// progressChart.value.setOption(progressOption)
// }
// }
// }
}).catch(error => {
console.error('获取隐患排查治理处理进度数据失败:', error)
})
}
}).catch(error => {
console.error('获取隐患排查治理数据失败:', error)
})
} catch (error) {
console.error('获取隐患排查治理数据失败:', error)
}
}
const trainingStatistics = ref<any>({})
const trainingList = ref<any>({})
const handleTabClick = async (tab: string) => {
activeTab.value = tab
let code = ''
let code1 = ''
switch (tab) {
case '高危作业':
code = "fire_drill"
code1 = "fire_drill_detail"
getDangerDetail(query.campus_id).then(res => {
const list = res.records.map((item: any) => ({ description: item.contenttext }))
dangerList.value = list
})
getDangerCount(query.campus_id).then(res => {
unfinishedCount.value = res.records[0].wks
inProgressCount.value = res.records[0].jxz
finishedCount.value = res.records[0].ywc
})
break
case '安全培训考试':
code = 'security_training_count'
code1 = 'security_training_detail'
const res = await getExamDetail(query.campus_id)
examList.value = res.records
break
case '应急预案及演练':
code = 'security_training_count'
code1 = 'security_training_detail'
const res1 = await getDrillDetail(query.campus_id)
drillList.value = res1.records
break
}
// 根据不同的tab请求不同的接口
getTableList(
code,
{ ...query, activeTab: tab }
).then(response => {
// 更新风险统计数据 - 传递完整的数组数据用于饼图显示
if (response.records && response.records.length > 0) {
trainingStatistics.value = response.records[0]
mockData.hiddenDangerData.progress = response.records[0]
}
}).catch(error => {
console.error('获取风险统计数据失败:', error)
})
getTableList(
code1,
{ ...query, activeTab: tab }
).then(response => {
// 更新风险统计数据 - 传递完整的数组数据用于饼图显示
if (response.records && response.records.length > 0) {
mockData.hiddenDangerData.progressDetail = response.records
}
}).catch(error => {
console.error('获取风险统计数据失败:', error)
})
}
// 批量更新所有数字的方法
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 = mockData.totalCount
animateNumber(startValue, counts.total || 0, animationDuration, (value) => {
mockData.totalCount = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.formal !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = mockData.formalEmployeeCount
animateNumber(startValue, counts.formal || 0, animationDuration, (value) => {
mockData.formalEmployeeCount = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.external !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = mockData.externalStaffCount
animateNumber(startValue, counts.external || 0, animationDuration, (value) => {
mockData.externalStaffCount = value
})
setTimeout(resolve, animationDuration)
}))
}
if (counts.visitor !== undefined) {
promises.push(new Promise<void>((resolve) => {
const startValue = mockData.visitorCount
animateNumber(startValue, counts.visitor || 0, animationDuration, (value) => {
mockData.visitorCount = value
})
setTimeout(resolve, animationDuration)
}))
}
Promise.all(promises).then(() => {
isAnimating.value = false
flashNumbers() // 更新完成后闪烁效果
})
}
// 数字滚动动画方法
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 flashNumbers = () => {
const numberElements = document.querySelectorAll('.total-number, .type-number')
numberElements.forEach((el) => {
el.classList.add('flash')
setTimeout(() => {
el.classList.remove('flash')
}, 300)
})
}
// 更新时间
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')
currentDate.value = `${year}${month}${day}`
currentDateTime.value = `${year}-${month}-${day}`
currentWeek.value = `${weekday}`
currentTime.value = `${hours}:${minutes}:${seconds}`
query.startDate = currentDateTime.value + " 00:00:00"
query.endDate = currentDateTime.value + " 23:59:59"
}
onUnmounted(() => {
if (dashboardTimerId.value) {
clearInterval(dashboardTimerId.value)
dashboardTimerId.value = null
}
if (timeUpdateTimerId.value) {
clearInterval(timeUpdateTimerId.value)
timeUpdateTimerId.value = null
}
})
</script>
<style scoped lang="scss">
.cursor-pointer {
cursor: pointer;
}
/* 响应式设计 - 自动适应不同屏幕尺寸 */
@media (width <=1200px) {
.dashboard-container {
.content-container {
column-gap: 4vw;
.center-container {
width: 60vh;
height: 60vh;
}
}
}
}
@media (width <=1024px) {
.dashboard-container {
.content-container {
column-gap: 3vw;
padding: 0 8px 15px;
.left-wrapper {
.left-top .hazard-wrapper {
.progress-chart {
min-height: 180px;
}
}
.left-bottom .donut-chart-with-labels {
width: 35vw;
height: 35vh;
}
}
.center-container {
width: 55vh;
height: 55vh;
}
}
}
}
@media (width <=768px) {
.dashboard-container {
.content-container {
column-gap: 2vw;
padding: 0 5px 10px;
.left-wrapper {
.left-top .hazard-wrapper {
.progress-chart {
min-height: 160px;
}
}
.left-bottom .donut-chart-with-labels {
width: 40vw;
height: 40vh;
margin-left: 1vw;
}
}
.center-container {
top: 60%;
width: 50vh;
height: 50vh;
}
}
}
}
@media (width <=480px) {
.dashboard-container {
.content-container {
column-gap: 1vw;
padding: 0 3px 8px;
.left-wrapper {
.left-top .hazard-wrapper {
.progress-chart {
min-height: 140px;
}
}
.left-bottom .donut-chart-with-labels {
width: 45vw;
height: 45vh;
margin-left: 0.5vw;
}
}
.center-container {
top: 65%;
width: 45vh;
height: 45vh;
}
}
}
}
.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;
}
.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;
.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%);
}
}
.date-wrapper {
display: flex;
margin-right: 20px;
flex: 1;
justify-content: flex-end;
column-gap: 2vw;
}
.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;
}
}
.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;
width: 67%;
margin-bottom: 20px;
.bottom-card-title {
display: flex;
margin-top: 5px;
flex-direction: column;
align-items: center;
}
.type-wrapper {
display: flex;
justify-content: center;
column-gap: 2vw;
margin-top: 5px;
.type-item {
display: flex;
flex-direction: column;
align-items: center;
column-gap: 4px;
.type-btn {
display: inline-block;
padding: 2px 30px;
font-size: 0.7rem;
cursor: pointer;
background-color: rgb(59 130 246 / 20%);
border-radius: 12px;
user-select: none;
&.blue {
background-color: rgb(32 148 235);
}
&.green {
background-color: rgb(80 139 79);
}
&.yellow {
background-color: rgb(204 96 26);
}
}
.type-num {
margin-top: 0.3vh;
font-size: 0.9rem;
}
}
}
.hazard-wrapper {
flex: 1;
display: flex;
align-items: center;
column-gap: 1.8vw;
.progress-chart {
width: 55%;
min-height: 140px;
}
.progress-legend {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5vw;
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
}
}
}
}
.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 {
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;
}
}
.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 {
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;
}
}
}
}
}
}
.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;
min-height: 200px;
margin-left: 2vw;
}
.row-wrapper {
display: flex;
width: 67%;
column-gap: 1vw;
margin-left: 1vw;
.row-item {
display: flex;
padding: 0.5vh 0;
font-size: 0.7rem;
background-color: rgb(4 35 125 / 50%);
flex-direction: column;
align-items: center;
column-gap: 10px;
flex: 1;
.row-item-title {}
.row-item-number {
font-size: 1rem;
}
}
}
}
}
.right-wrapper {
display: flex;
height: 100%;
flex: 1;
flex-direction: column;
row-gap: 1rem;
.right-top {
/* 设置重复方式 */
display: flex;
background-image:
url('@/assets/images/screen/right_top_img.png'),
url('@/assets/images/screen/right_center_img.png'),
url('@/assets/images/screen/right_bottom_img.png');
background-position:
top center,
right center,
bottom center;
/* 设置大小,注意中间的背景图应该覆盖整个容器 */
background-repeat: no-repeat, no-repeat, no-repeat;
/* 设置位置 */
background-size:
100% 90px,
cover,
100% 68px;
flex: 1;
flex-direction: column;
align-items: flex-end;
.tip-container {
position: relative;
display: flex;
width: 70%;
height: 80px;
padding-right: 20px;
align-items: center;
justify-content: flex-end;
.tip-image {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%) translateX(-50%);
.number {
position: absolute;
top: 50%;
left: 50%;
font-size: 1rem;
color: #fff;
transform: translate(-80%, -50%);
}
}
.tip-content {
position: absolute;
inset: 0% 0 0 6%;
display: flex;
padding-left: 20px;
align-items: center;
.col-item {
display: flex;
margin-left: 1vw;
align-items: center;
span {
margin-right: 10px;
}
}
}
}
}
.right-bottom {
/* 设置重复方式 */
display: flex;
background-image:
url('@/assets/images/screen/right_top_img.png'),
url('@/assets/images/screen/right_center_img.png'),
url('@/assets/images/screen/right_bottom_img.png');
background-position:
top center,
right center,
bottom center;
/* 设置大小,注意中间的背景图应该覆盖整个容器 */
background-repeat: no-repeat, no-repeat, no-repeat;
/* 设置位置 */
background-size:
100% 90px,
cover,
100% 68px;
flex: 1;
flex-direction: column;
align-items: flex-end;
.tip-container {
position: relative;
display: flex;
width: 50%;
height: 80px;
padding-right: 20px;
align-items: center;
justify-content: flex-end;
.tip-image {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%) translateX(-50%);
.number {
position: absolute;
top: 50%;
left: 50%;
font-size: 1rem;
color: #fff;
transform: translate(-80%, -50%);
}
}
.tip-content {
position: absolute;
inset: 0% 0 0 6%;
display: flex;
padding-left: 20px;
align-items: center;
.col-item {
display: flex;
margin-left: 1vw;
align-items: center;
span {
margin-right: 10px;
}
}
}
}
}
}
.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;
.test-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
padding: 8px 16px;
font-size: 12px;
color: white;
cursor: pointer;
background: rgb(59 130 246 / 80%);
border: 1px solid rgb(59 130 246 / 40%);
border-radius: 4px;
&:hover {
background: rgb(59 130 246 / 60%);
}
}
}
}
}
</style>