feat(screen): 更新主屏幕组件和风险统计面板

- 调整了组件标签格式,移除多余空格
- 修复了导入语句的空格格式问题
- 更新了JSON解析的空格格式
- 修改了工作订单类型从"安全生产"为"物业服务-安全"
- 增加了API响应数据的rate字段支持
- 将图表中心显示从总数改为完成率百分比
- 将状态标签从"已作废"更新为"已关闭"
- 优化了背景图片的CSS配置
- 添加了空行以提高代码可读性
This commit is contained in:
2025-12-24 10:18:33 +08:00
parent fcb0a38523
commit 301d47368d
2 changed files with 115 additions and 101 deletions

View File

@@ -16,8 +16,8 @@
<div class="chart-wrapper"> <div class="chart-wrapper">
<Echart class="donut-chart" :options="buildOption(item)" /> <Echart class="donut-chart" :options="buildOption(item)" />
<div class="chart-center"> <div class="chart-center">
<div class="center-title">总数</div> <div class="center-title">完成率</div>
<div class="center-value">{{ item.total }}</div> <div class="center-value">{{ item.rate }}%</div>
</div> </div>
</div> </div>
<div class="legend"> <div class="legend">
@@ -55,6 +55,7 @@ type StatusKey = 'notStarted' | 'inProgress' | 'done' | 'voided'
interface ChartItem { interface ChartItem {
title: string title: string
total: number total: number
rate: number
status: Record<StatusKey, number> status: Record<StatusKey, number>
} }
@@ -62,16 +63,16 @@ const statusList: { key: StatusKey; label: string; color: string }[] = [
{ key: 'notStarted', label: '未开始', color: '#2a59ff' }, { key: 'notStarted', label: '未开始', color: '#2a59ff' },
{ key: 'inProgress', label: '进行中', color: '#ff8a00' }, { key: 'inProgress', label: '进行中', color: '#ff8a00' },
{ key: 'done', label: '已完成', color: '#1bd9ff' }, { key: 'done', label: '已完成', color: '#1bd9ff' },
{ key: 'voided', label: '已作废', color: '#9fa0a6' } { key: 'voided', label: '已关闭', color: '#9fa0a6' }
] ]
const defaultChart: ChartItem[] = [ const defaultChart: ChartItem[] = [
{ title: '每日检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }, { title: '每日检查(维保类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每月检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }, { title: '每月检查(维保类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每年检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }, { title: '每年检查(维保类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每日检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }, { title: '每日检查(巡检类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每月检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }, { title: '每月检查(巡检类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每年检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } } { title: '每年检查(巡检类)', total: 6, rate: 0, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }
] ]
const tabCharts = ref<Record<TabType, ChartItem[]>>({ const tabCharts = ref<Record<TabType, ChartItem[]>>({
@@ -114,7 +115,7 @@ const buildOption = (item: ChartItem): EChartsOption => ({
{ value: item.status.notStarted, name: '未开始', itemStyle: { color: '#2a59ff' }, label: { show: false } }, { value: item.status.notStarted, name: '未开始', itemStyle: { color: '#2a59ff' }, label: { show: false } },
{ value: item.status.inProgress, name: '进行中', itemStyle: { color: '#ff8a00' }, label: { show: false } }, { value: item.status.inProgress, name: '进行中', itemStyle: { color: '#ff8a00' }, label: { show: false } },
{ value: item.status.done, name: '已完成', itemStyle: { color: '#1bd9ff' }, label: { show: false } }, { value: item.status.done, name: '已完成', itemStyle: { color: '#1bd9ff' }, label: { show: false } },
{ value: item.status.voided, name: '已作废', itemStyle: { color: '#9fa0a6' }, label: { show: false } } { value: item.status.voided, name: '已关闭', itemStyle: { color: '#9fa0a6' }, label: { show: false } }
], ],
emphasis: { scale: true, scaleSize: 4 } emphasis: { scale: true, scaleSize: 4 }
} }

View File

@@ -15,46 +15,50 @@
</div> </div>
</div> </div>
<!-- 天气预报 --> <!-- 天气预报 -->
<WeatherWarning /> <WeatherWarning/>
<!-- 主内容区 --> <!-- 主内容区 -->
<div class="content-container"> <div class="content-container">
<div class="left-wrapper"> <div class="left-wrapper">
<OverviewPanel :totalCount="dashboardData?.totalCount || 0" <OverviewPanel :totalCount="dashboardData?.totalCount || 0"
:formalEmployeeCount="dashboardData?.formalEmployeeCount || 0" :formalEmployeeCount="dashboardData?.formalEmployeeCount || 0"
:externalStaffCount="dashboardData?.externalStaffCount || 0" :visitorCount="dashboardData?.visitorCount || 0" :externalStaffCount="dashboardData?.externalStaffCount || 0"
:parkStatistics="dashboardData?.parkStatistics" /> :visitorCount="dashboardData?.visitorCount || 0"
:parkStatistics="dashboardData?.parkStatistics"/>
<RiskStatisticsPanel :riskStatistics="riskStatistics" :dangerDetail="dangerDetail" <RiskStatisticsPanel :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" :alertDetails="dashboardData?.alertData.details" <HighRiskAlertPanel :alertData="dashboardData?.alertData"
:sourceIndex="sourceIndex" /> :alertDetails="dashboardData?.alertData.details"
:sourceIndex="sourceIndex"/>
<TimeoutWorkOrderPanel :timeoutWorkOrders="dashboardData?.timeoutWorkOrders" <TimeoutWorkOrderPanel :timeoutWorkOrders="dashboardData?.timeoutWorkOrders"
:alertDetails="dashboardData?.timeoutWorkOrders.details" :sourceIndex="sourceIndex" /> :alertDetails="dashboardData?.timeoutWorkOrders.details"
:sourceIndex="sourceIndex"/>
</div> </div>
<HiddenDangerPanel :hiddenDangerData="dashboardData?.hiddenDangerData" /> <HiddenDangerPanel :hiddenDangerData="dashboardData?.hiddenDangerData"/>
</div> </div>
</div> </div>
<!-- 区域选择弹窗 --> <!-- 区域选择弹窗 -->
<RegionSelector v-model="regionSelectorVisible" :modelSelected="selectedRegion" :regions="regionOption" <RegionSelector v-model="regionSelectorVisible" :modelSelected="selectedRegion"
@change="onRegionChange" /> :regions="regionOption"
@change="onRegionChange"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getTableList, getTableData, getWorkOrderStatistics } from './report' import {getTableList, getTableData, getWorkOrderStatistics} from './report'
import { ref, onMounted, watch, onUnmounted } from 'vue' import {ref, onMounted, watch, onUnmounted} from 'vue'
import { useRouter } from 'vue-router' import {useRouter} from 'vue-router'
import RegionSelector from './components/RegionSelector.vue' import RegionSelector from './components/RegionSelector.vue'
import WeatherWarning from './components/WeatherWarning.vue' import WeatherWarning from './components/WeatherWarning.vue'
import { getDashboardData, getAlertDetails, type DashboardData } from '@/api/dashboard' import {getDashboardData, getAlertDetails, type DashboardData} from '@/api/dashboard'
import OverviewPanel from './components/OverviewPanel.vue' import OverviewPanel from './components/OverviewPanel.vue'
import RiskStatisticsPanel from './components/RiskStatisticsPanel.vue' import RiskStatisticsPanel from './components/RiskStatisticsPanel.vue'
import HighRiskAlertPanel from './components/HighRiskAlertPanel.vue' import HighRiskAlertPanel from './components/HighRiskAlertPanel.vue'
import TimeoutWorkOrderPanel from './components/TimeoutWorkOrderPanel.vue' import TimeoutWorkOrderPanel from './components/TimeoutWorkOrderPanel.vue'
import HiddenDangerPanel from './components/HiddenDangerPanel.vue' import HiddenDangerPanel from './components/HiddenDangerPanel.vue'
import { error } from "echarts/types/src/util/log"; import {error} from "echarts/types/src/util/log";
// 类型定义 // 类型定义
interface AlertItem { interface AlertItem {
@@ -224,7 +228,7 @@ const getCachedRegionOption = (): CacheData | null => {
try { try {
const cached = sessionStorage.getItem(CACHE_KEY) const cached = sessionStorage.getItem(CACHE_KEY)
if (cached) { if (cached) {
const { data, timestamp } = JSON.parse(cached) const {data, timestamp} = JSON.parse(cached)
const now = Date.now() const now = Date.now()
// 检查缓存是否在有效期内 // 检查缓存是否在有效期内
if (now - timestamp < CACHE_DURATION) { if (now - timestamp < CACHE_DURATION) {
@@ -263,7 +267,7 @@ const setCachedRegionOption = (regionOption: RegionItem[], campus_id: string) =>
onMounted(async () => { onMounted(async () => {
updateTime() updateTime()
timeUpdateTimerId.value = setInterval(updateTime, 1000) timeUpdateTimerId.value = setInterval(updateTime, 1000)
// 先检查缓存 // 先检查缓存
const cachedData = getCachedRegionOption() const cachedData = getCachedRegionOption()
if (cachedData && cachedData.regionOption && cachedData.regionOption.length > 0) { if (cachedData && cachedData.regionOption && cachedData.regionOption.length > 0) {
@@ -273,62 +277,62 @@ onMounted(async () => {
} else { } else {
// 缓存不存在或已过期,调用接口 // 缓存不存在或已过期,调用接口
try { try {
let { records } = await getTableList( let {records} = await getTableList(
'park_info_list' 'park_info_list'
) )
// records = [ // records = [
// { // {
// "region_id": "130601", // "region_id": "130601",
// "park_code": "1825468527486140416", // "park_code": "1825468527486140416",
// "region": "北京", // "region": "北京",
// "park_name": "雄安新区总部" // "park_name": "雄安新区总部"
// }, // },
// { // {
// "region_id": "130601", // "region_id": "130601",
// "park_code": "1825468527486140417", // "park_code": "1825468527486140417",
// "region": "北京", // "region": "北京",
// "park_name": "雄安地面站" // "park_name": "雄安地面站"
// }, // },
// { // {
// "region_id": "130603", // "region_id": "130603",
// "park_code": "1825468527486140426", // "park_code": "1825468527486140426",
// "region": "武汉", // "region": "武汉",
// "park_name": "花山新区总部" // "park_name": "花山新区总部"
// } // }
// ] // ]
if (records && records.length > 0) { if (records && records.length > 0) {
// 去重region字段使用Map来确保唯一性 // 去重region字段使用Map来确保唯一性
const regionMap = new Map() const regionMap = new Map()
records.forEach(el => { records.forEach(el => {
if (!regionMap.has(el.region)) { if (!regionMap.has(el.region)) {
regionMap.set(el.region, { regionMap.set(el.region, {
name: el.region, name: el.region,
code: el.region_id // 使用region_code作为code code: el.region_id // 使用region_code作为code
}) })
} }
}) })
// 转换为数组 // 转换为数组
regionOption.value = Array.from(regionMap.values()) regionOption.value = Array.from(regionMap.values())
console.log('regionOption.value>>>>', regionOption.value); console.log('regionOption.value>>>>', regionOption.value);
// 将园区信息去重 // 将园区信息去重
const parkMap = new Map(); const parkMap = new Map();
records.forEach(el => { records.forEach(el => {
if (!parkMap.has(el.park_code)) { if (!parkMap.has(el.park_code)) {
parkMap.set(el.park_code, { parkMap.set(el.park_code, {
name: el.park_name, name: el.park_name,
code: el.park_code code: el.park_code
}) })
} }
}) })
// 将parkMap转换为数组 // 将parkMap转换为数组
query.campus_id = Array.from(parkMap.values()).map(e1 => e1.code).join(); query.campus_id = Array.from(parkMap.values()).map(e1 => e1.code).join();
// 保存到缓存
setCachedRegionOption(regionOption.value, query.campus_id)
} // 保存到缓存
setCachedRegionOption(regionOption.value, query.campus_id)
}
} catch (error) { } catch (error) {
console.error('初始化园区数据失败:', error) console.error('初始化园区数据失败:', error)
} }
@@ -527,7 +531,7 @@ const handleHiddenDangerPannelData = (query) => {
processingCnt = totalCnt > 0 ? ((_data.processing + _data2.processing) / 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' pendingCnt = totalCnt > 0 ? ((_data.pending + _data2.pending) / totalCnt * 100).toFixed(2) : '0.00'
} }
dashboardData.value.hiddenDangerData.progress = { dashboardData.value.hiddenDangerData.progress = {
overdue: overdueCnt, overdue: overdueCnt,
processed: processedCnt, processed: processedCnt,
@@ -646,24 +650,32 @@ const handleRiskTabChange = async (tab: TabType) => {
let workOrderType = '' let workOrderType = ''
switch (tab) { switch (tab) {
case '安全类': case '安全类':
workOrderType = '安全生产' workOrderType = '物业服务-安全'
break break
case '工程类': case '工程类':
workOrderType = '物业服务-工程' workOrderType = '物业服务-工程'
break break
default: default:
workOrderType = '安全生产' workOrderType = '物业服务-安全'
} }
// 同时获取维保任务和巡检任务的数据 // 同时获取维保任务和巡检任务的数据
const [maintenanceResponse, inspectionResponse] = await Promise.all([ const [maintenanceResponse, inspectionResponse] = await Promise.all([
getWorkOrderStatistics({workOrderType, taskType: '维保任务',campus_id: query.campus_id}).catch(error => { getWorkOrderStatistics({
workOrderType,
taskType: '维保任务',
campus_id: query.campus_id
}).catch(error => {
console.error('获取维保任务数据失败:', error) console.error('获取维保任务数据失败:', error)
return { records: [] } return {records: []}
}), }),
getWorkOrderStatistics({workOrderType, taskType: '巡检任务',campus_id: query.campus_id}).catch(error => { getWorkOrderStatistics({
workOrderType,
taskType: '巡检任务',
campus_id: query.campus_id
}).catch(error => {
console.error('获取巡检任务数据失败:', error) console.error('获取巡检任务数据失败:', error)
return { records: [] } return {records: []}
}) })
]) ])
@@ -677,7 +689,7 @@ const handleRiskTabChange = async (tab: TabType) => {
// 将API数据转换为图表数据格式 // 将API数据转换为图表数据格式
const convertToChartData = (records: any[], taskTypeName: string): any[] => { const convertToChartData = (records: any[], taskTypeName: string): any[] => {
const charts: any[] = [] const charts: any[] = []
// 按周期分组 // 按周期分组
const cycleGroups: Record<string, any> = {} const cycleGroups: Record<string, any> = {}
records.forEach((record: any) => { records.forEach((record: any) => {
@@ -692,10 +704,11 @@ const handleRiskTabChange = async (tab: TabType) => {
cycles.forEach((cycle) => { cycles.forEach((cycle) => {
const data = cycleGroups[cycle] || {} const data = cycleGroups[cycle] || {}
const title = `${cycleMap[cycle]}检查(${taskTypeName})` const title = `${cycleMap[cycle]}检查(${taskTypeName})`
charts.push({ charts.push({
title, title,
total: Number(data.total) || 0, total: Number(data.total) || 0,
rate: Number(data.rate) || 0,
status: { status: {
notStarted: Number(data.pending) || 0, notStarted: Number(data.pending) || 0,
inProgress: Number(data.processing) || 0, inProgress: Number(data.processing) || 0,
@@ -729,7 +742,7 @@ const onRegionChange = (item: RegionItem): void => {
selectedRegion.value = item.name selectedRegion.value = item.name
router.push({ router.push({
path: '/screen/region', path: '/screen/region',
query: { region: item.name, regionCode: item.code } query: {region: item.name, regionCode: item.code}
}) })
} }
@@ -1296,19 +1309,19 @@ const timeOut1 = (): void => {
.left-top { .left-top {
padding: 0 5px; padding: 0 5px;
background-image: url('@/assets/images/screen/left_top_img.png'), background-image: url('@/assets/images/screen/left_top_img.png'),
url('@/assets/images/screen/left_center_img.png'), url('@/assets/images/screen/left_center_img.png'),
url('@/assets/images/screen/left_bottom_img.png'); url('@/assets/images/screen/left_bottom_img.png');
background-position: top center, background-position: top center,
left center, left center,
bottom center; bottom center;
/* 设置大小,注意中间的背景图应该覆盖整个容器 */ /* 设置大小,注意中间的背景图应该覆盖整个容器 */
background-repeat: no-repeat, no-repeat, no-repeat; background-repeat: no-repeat, no-repeat, no-repeat;
/* 设置位置 */ /* 设置位置 */
background-size: 100% 90px, background-size: 100% 90px,
cover, cover,
100% 68px; 100% 68px;
flex: 1; flex: 1;
/* 设置重复方式 */ /* 设置重复方式 */
@@ -1470,19 +1483,19 @@ const timeOut1 = (): void => {
.left-bottom { .left-bottom {
background-image: url('@/assets/images/screen/left_top_2_img.png'), background-image: url('@/assets/images/screen/left_top_2_img.png'),
url('@/assets/images/screen/left_center_img.png'), url('@/assets/images/screen/left_center_img.png'),
url('@/assets/images/screen/left_bottom_img.png'); url('@/assets/images/screen/left_bottom_img.png');
background-position: top center, background-position: top center,
left center, left center,
bottom center; bottom center;
/* 设置大小,注意中间的背景图应该覆盖整个容器 */ /* 设置大小,注意中间的背景图应该覆盖整个容器 */
background-repeat: no-repeat, no-repeat, no-repeat; background-repeat: no-repeat, no-repeat, no-repeat;
/* 设置位置 */ /* 设置位置 */
background-size: 100% 90px, background-size: 100% 90px,
cover, cover,
100% 68px; 100% 68px;
flex: 1; flex: 1;
/* 设置重复方式 */ /* 设置重复方式 */