You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/frontend/src/views/dashboard/DashboardPage.vue

680 lines
30 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="dashboard-page">
<!-- 统计卡片 第1行 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
在线机床数
<el-tooltip content="当前网络连通的机床数量占所有已启用机床总数的比例。系统通过Ping检测机床网络连通即为在线。" placement="top">
<span class="info-icon"></span>
</el-tooltip>
</div>
<div class="stat-value">{{ summary.onlineCount }}<span class="stat-unit"> / {{ summary.totalMachines }}</span></div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
今日总产量
<el-tooltip content="今日所有机床加工完成的零件总数包含手工修正后的数值。系统以NC程序名区分零件切换程序时自动结算上一段产量。" placement="top">
<span class="info-icon"></span>
</el-tooltip>
</div>
<div class="stat-value">{{ summary.todayProduction?.toLocaleString() }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
采集服务
<el-tooltip content="数据采集服务的运行状态。服务每30秒向系统上报一次心跳超过90秒未上报则判定为停止。" placement="top">
<span class="info-icon"></span>
</el-tooltip>
</div>
<div class="stat-value">
<el-tag :type="collectorTagType" size="small">
{{ collectorStatusText }}
</el-tag>
</div>
<div class="stat-sub" v-if="collectorStatus.serviceStatus === 'Running' && collectorStatus.status === 'running'">运行 {{ formatUptime(collectorStatus.uptimeSeconds) }}</div>
</div>
<div class="collector-actions">
<el-button v-if="collectorStatus.serviceStatus !== 'Running'" size="small" type="success" :loading="startLoading" @click="startCollector">启动采集</el-button>
<el-button v-if="collectorStatus.status === 'running'" size="small" type="danger" :loading="stopLoading" @click="stopCollector">停止采集</el-button>
<el-button size="small" type="warning" :loading="refreshLoading" @click="refreshCollectorConfig">刷新配置</el-button>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
活跃告警
<el-tooltip content="当前未处理的告警总数。包括:采集失败、数据缺失、设备离线、发现未注册设备。" placement="top">
<span class="info-icon">ⓘ</span>
</el-tooltip>
</div>
<div class="stat-value alert-value" @click="$router.push(isMock ? '/mock/alert' : '/alert')">
{{ summary.activeAlerts }}
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 统计卡片 第2行 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
采集成功率
<el-tooltip content="今日采集成功的次数占总采集次数的百分比。每次采集一个地址算一次,一个地址可能包含多台机床的数据。" placement="top">
<span class="info-icon">ⓘ</span>
</el-tooltip>
</div>
<div class="stat-value">{{ formatNumber(summary.collectSuccessRate) }}<span class="stat-unit">%</span></div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
运行机床
<el-tooltip content="当前正在执行加工程序的机床数量。与「在线」的区别:在线表示网络连通,运行表示正在加工零件。" placement="top">
<span class="info-icon">ⓘ</span>
</el-tooltip>
</div>
<div class="stat-value">{{ summary.runningMachines }}<span class="stat-unit"> 台</span></div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-label">
数据缺失
<el-tooltip content="网络连通但未能成功采集到数据的机床数量。这些机床的产量在报表中显示为「-」而不是0不计入日产量汇总。" placement="top">
<span class="info-icon">ⓘ</span>
</el-tooltip>
</div>
<div class="stat-value" :class="{ 'alert-value': summary.dataMissingMachines > 0 }">{{ summary.dataMissingMachines }}<span class="stat-unit"> 台</span></div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区:产量趋势 + 车间对比 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="12">
<el-card shadow="hover">
<template #header><span class="card-title">产量趋势近7天<el-tooltip content="最近7天每天的零件总产量变化趋势。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span></template>
<div ref="trendChartRef" style="height: 260px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">车间平均单机产量({{ workshopDateLabel }}<el-tooltip placement="top">
<template #content>
<div style="max-width: 360px; line-height: 1.6">
<b>统计规则:</b>车间总产量 ÷ 车间机床数 ÷ 天数<br/><br/>
<b>数据来源:</b>系统每日凌晨1:00自动执行日终汇总将每台机床当天所有产量分段按程序名合并计算生成日产量记录。多天范围取日均单机产量。<br/><br/>
<b>产量计算:</b>以NC程序名标识零件程序切换时自动结算上一段产量。同程序多次出现时分段记录、日汇总合并。含手工修正值。
</div>
</template>
<span class="info-icon">ⓘ</span>
</el-tooltip></span>
<div class="date-filter">
<el-radio-group v-model="workshopDateType" size="small" @change="onWorkshopDateChange">
<el-radio-button value="today">今日</el-radio-button>
<el-radio-button value="yesterday">昨日</el-radio-button>
<el-radio-button value="last3">近3天</el-radio-button>
<el-radio-button value="last7">近7天</el-radio-button>
<el-radio-button value="custom">自定义</el-radio-button>
</el-radio-group>
<el-date-picker v-if="workshopDateType === 'custom'" v-model="workshopDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" size="small" value-format="YYYY-MM-DD" :disabled-date="(d: Date) => d > new Date()" style="margin-left: 8px" @change="loadWorkshopData" />
</div>
</div>
</template>
<div ref="workshopChartRef" style="height: 260px"></div>
</el-card>
</el-col>
</el-row>
<!-- 信息区:机床状态分布 + 最新告警 -->
<el-row :gutter="16" class="info-row">
<el-col :span="8">
<el-card shadow="hover">
<template #header><span class="card-title">机床状态分布<el-tooltip content="在线网络Ping检测通过离线网络Ping检测不通停用已手动关闭的机床。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span></template>
<div ref="statusPieRef" style="height: 260px"></div>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">最新告警<el-tooltip content="最近5条未处理的告警记录按时间倒序排列。类型包括采集失败、数据缺失、设备离线、发现未注册设备。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip></span>
<el-button link type="primary" @click="$router.push(isMock ? '/mock/alert' : '/alert')">查看全部</el-button>
</div>
</template>
<el-table :data="recentAlerts" stripe size="small" style="width: 100%">
<el-table-column prop="createdAt" label="时间" width="160" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="alertTypeTag(row.alertType)" size="small">{{ alertTypeLabel(row.alertType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="machineName" label="机床" width="100" />
<el-table-column prop="title" label="告警内容" show-overflow-tooltip />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.isResolved ? 'success' : 'danger'" size="small">{{ row.isResolved ? '已处理' : '未处理' }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 排行表格 -->
<el-row :gutter="16" class="rank-row">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">机床产量排行 TOP
<el-select v-model="machineTopN" size="small" style="width: 72px" @change="loadMachineRankData">
<el-option :value="5" label="5" />
<el-option :value="10" label="10" />
<el-option :value="20" label="20" />
<el-option :value="50" label="50" />
<el-option :value="100" label="100" />
</el-select>
<el-tooltip content="单台机床加工零件数排列,同时显示当前正在执行的程序名。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip>
</span>
<div class="date-filter">
<el-radio-group v-model="machineSortOrder" size="small" @change="loadMachineRankData" style="margin-right: 8px">
<el-radio-button value="asc">正序</el-radio-button>
<el-radio-button value="desc">倒序</el-radio-button>
</el-radio-group>
<el-radio-group v-model="machineDateType" size="small" @change="onMachineDateChange">
<el-radio-button value="today">今日</el-radio-button>
<el-radio-button value="yesterday">昨日</el-radio-button>
<el-radio-button value="last3">近3天</el-radio-button>
<el-radio-button value="last7">近7天</el-radio-button>
<el-radio-button value="custom">自定义</el-radio-button>
</el-radio-group>
<el-date-picker v-if="machineDateType === 'custom'" v-model="machineDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" size="small" value-format="YYYY-MM-DD" :disabled-date="(d: Date) => d > new Date()" style="margin-left: 8px" @change="loadMachineRankData" />
</div>
</div>
</template>
<el-table :data="machineRank" stripe size="small" style="width: 100%">
<el-table-column prop="rank" label="排名" width="60" align="center" />
<el-table-column label="机床名称" width="120">
<template #default="{ row }">
<router-link :to="isMock ? '/mock/machine/' + row.machineId : '/machine/' + row.machineId" class="machine-link">{{ row.machineName }}</router-link>
</template>
</el-table-column>
<el-table-column prop="program" label="当前程序" show-overflow-tooltip />
<el-table-column label="产量" width="80" align="center">
<template #default="{ row }">{{ formatNumber(row.quantity) }}</template>
</el-table-column>
<el-table-column label="状态" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">{{ row.status === 1 ? '在线' : '离线' }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">工人产量排行 TOP
<el-select v-model="workerTopN" size="small" style="width: 72px" @change="loadWorkerRankData">
<el-option :value="5" label="5" />
<el-option :value="10" label="10" />
<el-option :value="20" label="20" />
<el-option :value="50" label="50" />
<el-option :value="100" label="100" />
</el-select>
<el-tooltip content="工人所绑定的所有机床产量合计。" placement="top"><span class="info-icon">ⓘ</span></el-tooltip>
</span>
<div class="date-filter">
<el-radio-group v-model="workerSortOrder" size="small" @change="loadWorkerRankData" style="margin-right: 8px">
<el-radio-button value="asc">正序</el-radio-button>
<el-radio-button value="desc">倒序</el-radio-button>
</el-radio-group>
<el-radio-group v-model="workerDateType" size="small" @change="onWorkerDateChange">
<el-radio-button value="today">今日</el-radio-button>
<el-radio-button value="yesterday">昨日</el-radio-button>
<el-radio-button value="last3">近3天</el-radio-button>
<el-radio-button value="last7">近7天</el-radio-button>
<el-radio-button value="custom">自定义</el-radio-button>
</el-radio-group>
<el-date-picker v-if="workerDateType === 'custom'" v-model="workerDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" size="small" value-format="YYYY-MM-DD" :disabled-date="(d: Date) => d > new Date()" style="margin-left: 8px" @change="loadWorkerRankData" />
</div>
</div>
</template>
<el-table :data="workerRank" stripe size="small" style="width: 100%">
<el-table-column prop="rank" label="排名" width="60" align="center" />
<el-table-column prop="workerName" label="工人姓名" />
<el-table-column prop="machineCount" label="绑定机床" width="100" align="center" />
<el-table-column label="总产量" width="100" align="center">
<template #default="{ row }">{{ formatNumber(row.totalQuantity) }}</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import { useMockMode } from '@/composables/useMockMode'
import echarts from '@/utils/echarts'
import type { ECharts } from 'echarts/core'
import type { ApiResponse, DashboardSummary, CollectorStatus, MachineRankRow, WorkerRankRow, DashboardTrendItem, WorkshopProduction, MachineStatusDistribution, RecentAlert } from '@/types'
const { isMock } = useMockMode()
const summary = ref<DashboardSummary>({ onlineCount: 0, totalMachines: 0, todayProduction: 0, activeAlerts: 0, collectSuccessRate: 0, runningMachines: 0, dataMissingMachines: 0 })
const collectorStatus = ref<CollectorStatus>({ status: 'stopped', uptimeSeconds: 0, serviceStatus: 'NotInstalled', serviceName: '' })
const machineRank = ref<MachineRankRow[]>([])
const workerRank = ref<WorkerRankRow[]>([])
const trendData = ref<DashboardTrendItem[]>([])
const workshopData = ref<WorkshopProduction[]>([])
const statusDist = ref<MachineStatusDistribution>({ online: 0, offline: 0, disabled: 0 })
const recentAlerts = ref<RecentAlert[]>([])
const startLoading = ref(false)
const stopLoading = ref(false)
const refreshLoading = ref(false)
// 日期筛选状态
type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom'
const workshopDateType = ref<DateType>('today')
const workshopDateRange = ref<[string, string]>()
const machineDateType = ref<DateType>('today')
const machineDateRange = ref<[string, string]>()
const workerDateType = ref<DateType>('today')
const workerDateRange = ref<[string, string]>()
// TOP N 和排序
const machineTopN = ref(10)
const machineSortOrder = ref<'asc' | 'desc'>('asc')
const workerTopN = ref(10)
const workerSortOrder = ref<'asc' | 'desc'>('asc')
const dateLabels: Record<DateType, string> = { today: '今日', yesterday: '昨日', last3: '近3天', last7: '近7天', custom: '自定义' }
const workshopDateLabel = computed(() => dateLabels[workshopDateType.value])
const machineDateLabel = computed(() => dateLabels[machineDateType.value])
const workerDateLabel = computed(() => dateLabels[workerDateType.value])
// 车间平均单机产量的日均单位
const workshopDays = computed(() => {
const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value)
return Math.max(1, Math.round((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000) + 1)
})
const workshopUnit = computed(() => workshopDays.value > 1 ? '件/台/天' : '件/台')
const workshopUnitLabel = computed(() => workshopDays.value > 1 ? '日均' : '平均')
function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } {
const today = new Date()
const fmt = (d: Date) => d.toISOString().slice(0, 10)
const endDate = fmt(today)
switch (type) {
case 'today': return { startDate: endDate, endDate }
case 'yesterday': { const y = new Date(today); y.setDate(y.getDate() - 1); const ys = fmt(y); return { startDate: ys, endDate: ys } }
case 'last3': { const d = new Date(today); d.setDate(d.getDate() - 2); return { startDate: fmt(d), endDate } }
case 'last7': { const d = new Date(today); d.setDate(d.getDate() - 6); return { startDate: fmt(d), endDate } }
case 'custom': return { startDate: customRange?.[0] || endDate, endDate: customRange?.[1] || endDate }
}
}
function onWorkshopDateChange() { workshopDateType.value !== 'custom' ? loadWorkshopData() : undefined }
function onMachineDateChange() { machineDateType.value !== 'custom' ? loadMachineRankData() : undefined }
function onWorkerDateChange() { workerDateType.value !== 'custom' ? loadWorkerRankData() : undefined }
// ECharts refs
const trendChartRef = ref<HTMLElement>()
const workshopChartRef = ref<HTMLElement>()
const statusPieRef = ref<HTMLElement>()
let trendChart: ECharts | null = null
let workshopChart: ECharts | null = null
let statusPie: ECharts | null = null
async function startCollector() {
if (startLoading.value) return
startLoading.value = true
try {
// 在发起启动前,若已安装且正在运行,前端应给出友好提示
if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'Running') {
ElMessage.info('采集服务已在运行中');
return;
}
if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'NotInstalled') {
ElMessage.warning('采集服务未安装,请运行 install.ps1 安装脚本');
return;
}
await request.post('/admin/collector/start');
ElMessage.success('采集服务已启动');
await loadData();
} catch { /* request拦截器已显示错误 */ } finally { startLoading.value = false }
}
async function stopCollector() {
if (stopLoading.value) return
stopLoading.value = true
try { await request.post('/admin/collector/stop'); ElMessage.success('采集服务已停止'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { stopLoading.value = false }
}
async function refreshCollectorConfig() {
if (refreshLoading.value) return
refreshLoading.value = true
try { await request.post('/admin/collector/refresh'); ElMessage.success('配置已刷新'); await loadData() } catch { /* request拦截器已显示错误 */ } finally { refreshLoading.value = false }
}
function formatUptime(seconds: number | undefined): string {
if (!seconds) return '-'
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
if (days > 0) return `${days}天${hours}时`
return `${hours}时`
}
function formatNumber(val: number | undefined | null): string {
if (val == null) return '-'
return Number(val).toFixed(2)
}
// 采集服务状态:综合心跳 + Windows服务状态
const collectorTagType = computed(() => {
const { serviceStatus, status } = collectorStatus.value
if (serviceStatus === 'Running' && status === 'running') return 'success'
if (serviceStatus === 'Running' && status !== 'running') return 'warning' // 进程在但心跳超时
if (serviceStatus === 'NotInstalled') return 'danger'
if (serviceStatus === 'StartFailed') return 'danger'
return 'warning'
})
const collectorStatusText = computed(() => {
const { serviceStatus, status } = collectorStatus.value
if (serviceStatus === 'Running' && status === 'running') return '运行中'
if (serviceStatus === 'Running' && status !== 'running') return '心跳超时'
if (serviceStatus === 'NotInstalled') return '未安装'
if (serviceStatus === 'Stopped') return '已停止'
if (serviceStatus === 'Starting') return '启动中'
if (serviceStatus === 'StartFailed') return '启动失败'
return serviceStatus || '-'
})
function alertTypeTag(type: string): string {
const map: Record<string, string> = { collect_fail: 'danger', data_missing: 'warning', device_offline: 'danger', new_device: 'info' }
return map[type] || 'warning'
}
function alertTypeLabel(type: string): string {
const map: Record<string, string> = { collect_fail: '采集失败', data_missing: '数据缺失', device_offline: '设备离线', new_device: '新设备' }
return map[type] || type
}
function initWorkshopChart() {
if (workshopChartRef.value && workshopData.value.length) {
const unit = workshopUnit.value
const unitLabel = workshopUnitLabel.value
workshopChart = echarts.init(workshopChartRef.value)
workshopChart.setOption({
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const d = workshopData.value[params[0].dataIndex]
return `${d.workshopName}<br/>${unitLabel}产量: ${params[0].value} ${unit}<br/>总产量: ${d.quantity} 件<br/>机床数: ${d.machineCount} 台`
},
},
grid: { left: 60, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: workshopData.value.map(i => i.workshopName), axisLabel: { fontSize: 12 } },
yAxis: { type: 'value', name: unit, axisLabel: { fontSize: 12 } },
series: [{
type: 'bar', data: workshopData.value.map(i => i.avgQuantity),
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] }, barWidth: '40%',
label: { show: true, position: 'top', formatter: `{c} ${unit}`, fontSize: 12 },
}],
})
}
}
function initCharts() {
// 产量趋势折线图
if (trendChartRef.value && trendData.value.length) {
trendChart = echarts.init(trendChartRef.value)
trendChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: trendData.value.map(i => i.date.slice(5)), axisLabel: { fontSize: 12 } },
yAxis: { type: 'value', axisLabel: { fontSize: 12 } },
series: [{
type: 'line', data: trendData.value.map(i => i.quantity), smooth: true,
areaStyle: { opacity: 0.15 }, itemStyle: { color: '#409EFF' },
}],
})
}
// 车间平均单机产量柱状图
initWorkshopChart()
// 机床状态饼图
if (statusPieRef.value) {
const d = statusDist.value
statusPie = echarts.init(statusPieRef.value)
statusPie.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c}台 ({d}%)' },
legend: { bottom: 0, itemWidth: 12, itemHeight: 12, textStyle: { fontSize: 12 } },
series: [{
type: 'pie', radius: ['40%', '65%'], center: ['50%', '45%'],
label: { show: true, formatter: '{b}\n{c}台', fontSize: 12 },
data: [
{ value: d.online, name: '在线', itemStyle: { color: '#67C23A' } },
{ value: d.offline, name: '离线', itemStyle: { color: '#909399' } },
{ value: d.disabled, name: '停用', itemStyle: { color: '#F56C6C' } },
],
}],
})
}
}
function disposeCharts() {
trendChart?.dispose()
workshopChart?.dispose()
statusPie?.dispose()
trendChart = null
workshopChart = null
statusPie = null
}
async function loadWorkshopData() {
try {
const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value)
const res = await request.get<{ items: WorkshopProduction[] }>('/admin/dashboard/workshop-production', { params: { startDate, endDate } })
workshopData.value = res.data?.items || []
workshopChart?.dispose(); workshopChart = null
await nextTick(); initWorkshopChart()
} catch { /* */ }
}
async function loadMachineRankData() {
try {
const { startDate, endDate } = getDateRange(machineDateType.value, machineDateRange.value)
const res: ApiResponse<{ items: MachineRankRow[] }> = await request.get('/admin/dashboard/machine-rank', { params: { startDate, endDate, top: machineTopN.value, sortOrder: machineSortOrder.value } })
machineRank.value = res.data?.items || []
} catch { /* */ }
}
async function loadWorkerRankData() {
try {
const { startDate, endDate } = getDateRange(workerDateType.value, workerDateRange.value)
const res: ApiResponse<{ items: WorkerRankRow[] }> = await request.get('/admin/dashboard/worker-rank', { params: { startDate, endDate, top: workerTopN.value, sortOrder: workerSortOrder.value } })
workerRank.value = res.data?.items || []
} catch { /* */ }
}
async function loadData() {
try {
const [summaryRes, collectorRes, trendRes, statusRes, alertsRes] = await Promise.all([
request.get<DashboardSummary>('/admin/dashboard/summary'),
request.get<CollectorStatus>('/admin/collector/status'),
request.get<{ items: DashboardTrendItem[] }>('/admin/dashboard/trend'),
request.get<MachineStatusDistribution>('/admin/dashboard/machine-status-distribution'),
request.get<{ items: RecentAlert[] }>('/admin/dashboard/recent-alerts'),
])
summary.value = summaryRes.data || summary.value
collectorStatus.value = collectorRes.data || collectorStatus.value
trendData.value = trendRes.data?.items || []
statusDist.value = statusRes.data || statusDist.value
recentAlerts.value = alertsRes.data?.items || []
disposeCharts()
await nextTick()
initCharts()
// 日期筛选的三个区域独立加载
await Promise.all([loadWorkshopData(), loadMachineRankData(), loadWorkerRankData()])
} catch {
// 保持容错
}
}
onMounted(() => {
loadData()
})
onUnmounted(() => {
disposeCharts()
})
</script>
<style scoped lang="scss">
.dashboard-page {
.stat-row {
margin-bottom: 16px;
.el-card { height: 100%; }
.stat-card {
text-align: center;
padding: 10px 0;
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
.info-icon {
margin-left: 2px;
font-size: 13px;
color: #c0c4cc;
cursor: help;
vertical-align: middle;
&:hover {
color: #409EFF;
}
}
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
.stat-unit {
font-size: 14px;
font-weight: normal;
color: #909399;
}
}
.stat-sub {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.alert-value {
color: #e6a23c;
cursor: pointer;
}
}
.collector-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-start;
flex-wrap: wrap;
}
}
.chart-row,
.info-row,
.rank-row {
margin-bottom: 16px;
}
.card-title {
font-size: 15px;
font-weight: 500;
.info-icon {
margin-left: 4px;
font-size: 14px;
color: #b0b5bd;
cursor: help;
&:hover {
color: #409EFF;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.card-sub {
font-size: 12px;
color: #909399;
}
.date-filter {
display: flex;
align-items: center;
flex-wrap: wrap;
}
}
.machine-link {
color: var(--el-color-primary);
cursor: pointer;
text-decoration: none;
}
}
</style>