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

925 lines
32 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行 3个统计卡片 -->
<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">{{ summary.onlineCount }}<span class="stat-unit"> / {{ summary.totalMachines }}</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.todayProduction?.toLocaleString() }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 第2行 双图表 -->
<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">
NC程序产量分布
<el-tooltip content="按NC程序名统计的产量占比分布。" placement="top">
<span class="info-icon"></span>
</el-tooltip>
</span>
<div class="date-filter">
<el-radio-group v-model="programDistDateType" size="small" @change="onProgramDistDateChange">
<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="programDistDateType === 'custom'"
v-model="programDistDateRange"
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="loadProgramDistributionData"
/>
</div>
</div>
</template>
<div ref="programDistChartRef" style="height: 260px"></div>
</el-card>
</el-col>
</el-row>
<!-- 第3行 — 双排行 -->
<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-select v-model="machineSortOrder" size="small" style="width: 90px; margin-right: 8px" @change="loadMachineRankData">
<el-option value="desc" label="降序" />
<el-option value="asc" label="升序" />
</el-select>
<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.id : '/machine/' + row.id" 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">
NC程序产量排行 TOP
<el-select v-model="programRankTopN" size="small" style="width: 72px" @change="loadProgramRankData">
<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="按NC程序名统计的产量排行。" placement="top">
<span class="info-icon">ⓘ</span>
</el-tooltip>
</span>
<div class="date-filter">
<el-radio-group v-model="programRankDateType" size="small" @change="onProgramRankDateChange">
<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="programRankDateType === 'custom'"
v-model="programRankDateRange"
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="loadProgramRankData"
/>
</div>
</div>
</template>
<el-table :data="programRank" stripe size="small" style="width: 100%">
<el-table-column prop="rank" label="排名" width="60" align="center" />
<el-table-column prop="programName" label="程序名" show-overflow-tooltip />
<el-table-column label="总产量" width="100" align="center">
<template #default="{ row }">{{ formatNumber(row.totalQuantity) }}</template>
</el-table-column>
<el-table-column prop="machineCount" label="涉及机床数" width="120" align="center" />
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 第4行 — 双排行/图表 -->
<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="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-select v-model="workerSortOrder" size="small" style="width: 90px; margin-right: 8px" @change="loadWorkerRankData">
<el-option value="desc" label="降序" />
<el-option value="asc" label="升序" />
</el-select>
<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-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" style="margin-top:16px">
<el-col :span="24">
<el-card shadow="hover">
<template #header><span class="card-title">车间产量明细</span></template>
<el-table :data="workshopData" stripe size="small">
<el-table-column prop="workshopName" label="车间" />
<el-table-column prop="quantity" label="总产量" align="center" />
<el-table-column prop="machineCount" label="机床数" align="center" />
<el-table-column label="平均单产({{ workshopUnitLabel }})" align="center">
<template #default="{ row }">{{ row.machineCount ? (row.quantity / row.machineCount).toFixed(2) : '-' }} {{ workshopUnit }}</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 } from '@/types'
// ==================== 本地类型定义(不修改 types/index.ts ====================
/** NC程序产量排行项 */
interface ProgramRankItem {
rank: number
programName: string
totalQuantity: number
machineCount: number
}
/** NC程序产量分布项 */
interface ProgramDistributionItem {
programName: string
totalQuantity: number
percentage: number
}
// ==================== Mock 模式 ====================
const { isMock } = useMockMode()
// ==================== 日期类型 ====================
type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom'
const dateLabels: Record<DateType, string> = {
today: '今日',
yesterday: '昨日',
last3: '近3天',
last7: '近7天',
custom: '自定义',
}
// 日期范围格式化工具函数
function getDateRange(type: DateType, customRange?: [string, string]): { startDate: string; endDate: string } {
const today = new Date()
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
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 }
}
}
// ==================== 响应式数据 ====================
// 第1行 — 统计卡片
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 startLoading = ref(false)
const stopLoading = ref(false)
const refreshLoading = ref(false)
// 图表数据
const trendData = ref<DashboardTrendItem[]>([])
const programDistribution = ref<ProgramDistributionItem[]>([])
// 排行数据
const machineRank = ref<MachineRankRow[]>([])
const programRank = ref<ProgramRankItem[]>([])
const workerRank = ref<WorkerRankRow[]>([])
const workshopData = ref<WorkshopProduction[]>([])
// ==================== 日期筛选状态 ====================
// 产量趋势近7天— 无日期筛选
// NC程序产量分布日期
const programDistDateType = ref<DateType>('today')
const programDistDateRange = ref<[string, string]>()
// 机床排行日期
const machineDateType = ref<DateType>('today')
const machineDateRange = ref<[string, string]>()
// NC程序排行日期
const programRankDateType = ref<DateType>('today')
const programRankDateRange = ref<[string, string]>()
// 工人排行日期
const workerDateType = ref<DateType>('today')
const workerDateRange = ref<[string, string]>()
// 车间平均单机产量日期
const workshopDateType = ref<DateType>('today')
const workshopDateRange = ref<[string, string]>()
// ==================== TOP N 和排序 ====================
const machineTopN = ref(10)
const machineSortOrder = ref<'asc' | 'desc'>('desc')
const programRankTopN = ref(10)
const workerTopN = ref(10)
const workerSortOrder = ref<'asc' | 'desc'>('desc')
// ==================== 计算属性 ====================
const workshopDateLabel = computed(() => dateLabels[workshopDateType.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 ? '日均' : '平均')
// 采集服务状态标签
const collectorTagType = computed(() => {
const { status } = collectorStatus.value
if (status === 'running') return 'success'
if (status === 'paused') return 'info'
if (status === 'timeout') return 'warning'
if (status === 'stopped') return 'info'
if (status === 'not_installed') return 'danger'
return 'warning'
})
const collectorStatusText = computed(() => {
const { status } = collectorStatus.value
if (status === 'running') return '运行中'
if (status === 'paused') return '已暂停'
if (status === 'timeout') return '心跳超时'
if (status === 'stopped') return '已停止'
if (status === 'not_installed') return '未安装'
return status || '-'
})
// ==================== 工具函数 ====================
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)
}
// ==================== 日期变更处理 ====================
function onWorkshopDateChange() {
workshopDateType.value !== 'custom' ? loadWorkshopData() : undefined
}
function onMachineDateChange() {
machineDateType.value !== 'custom' ? loadMachineRankData() : undefined
}
function onWorkerDateChange() {
workerDateType.value !== 'custom' ? loadWorkerRankData() : undefined
}
function onProgramDistDateChange() {
programDistDateType.value !== 'custom' ? loadProgramDistributionData() : undefined
}
function onProgramRankDateChange() {
programRankDateType.value !== 'custom' ? loadProgramRankData() : undefined
}
// ==================== 采集服务控制 ====================
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
}
}
// ==================== ECharts 实例 ====================
const trendChartRef = ref<HTMLElement>()
const programDistChartRef = ref<HTMLElement>()
const workshopChartRef = ref<HTMLElement>()
let trendChart: ECharts | null = null
let programDistChart: ECharts | null = null
let workshopChart: ECharts | null = null
// ==================== 图表初始化 ====================
function initTrendChart() {
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' },
},
],
})
}
}
function initProgramDistChart() {
if (programDistChartRef.value && programDistribution.value.length) {
programDistChart = echarts.init(programDistChartRef.value)
programDistChart.setOption({
tooltip: {
trigger: 'item',
formatter: (params: { name: string; value: number; percent: number }) =>
`${params.name}: ${params.value}件 (${params.percent}%)`,
},
legend: {
type: 'scroll',
bottom: 0,
itemWidth: 12,
itemHeight: 12,
textStyle: { fontSize: 12 },
},
series: [
{
type: 'pie',
radius: ['40%', '65%'],
center: ['50%', '45%'],
label: {
show: true,
formatter: '{b}: {c}件 ({d}%)',
fontSize: 12,
},
data: programDistribution.value.map(item => ({
value: item.totalQuantity,
name: item.programName,
})),
},
],
})
}
}
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: { name: string; value: number; dataIndex: number }[]) => {
const d = workshopData.value[params[0].dataIndex]
return `${d.workshopName}<br/>${unitLabel}产量: ${Number(params[0].value).toFixed(2)} ${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: (p: { value: number }) => `${Number(p.value).toFixed(2)} ${unit}`,
fontSize: 12,
},
},
],
})
}
}
function initCharts() {
initTrendChart()
initProgramDistChart()
// workshopChart 在 loadWorkshopData 中初始化
}
function disposeCharts() {
trendChart?.dispose()
programDistChart?.dispose()
workshopChart?.dispose()
trendChart = null
programDistChart = null
workshopChart = null
}
// ==================== 数据加载 ====================
async function loadProgramDistributionData() {
try {
const { startDate, endDate } = getDateRange(programDistDateType.value, programDistDateRange.value)
const res = await request.get<{ items: ProgramDistributionItem[] }>(
'/admin/dashboard/program-distribution',
{ params: { startDate, endDate } }
)
programDistribution.value = res.data?.items || []
programDistChart?.dispose()
programDistChart = null
await nextTick()
initProgramDistChart()
} catch {
/* 容错 */
}
}
async function loadProgramRankData() {
try {
const { startDate, endDate } = getDateRange(programRankDateType.value, programRankDateRange.value)
const res: ApiResponse<{ items: ProgramRankItem[] }> = await request.get(
'/admin/dashboard/program-rank',
{ params: { startDate, endDate, top: programRankTopN.value } }
)
programRank.value = res.data?.items || []
} 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 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 loadData() {
try {
// 并行加载:统计摘要 + 采集状态 + 产量趋势
const [summaryRes, trendRes] = await Promise.all([
request.get<DashboardSummary>('/admin/dashboard/summary'),
request.get<{ items: DashboardTrendItem[] }>('/admin/dashboard/trend'),
])
summary.value = summaryRes.data || summary.value
trendData.value = trendRes.data?.items || []
// 重置图表
disposeCharts()
await nextTick()
initCharts()
// 并行加载各区域独立数据
await Promise.all([
loadProgramDistributionData(),
loadWorkshopData(),
loadMachineRankData(),
loadWorkerRankData(),
loadProgramRankData(),
])
} 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;
}
}
.collector-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-start;
flex-wrap: wrap;
}
}
.chart-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>