diff --git a/frontend/src/views/dashboard/DashboardPage.vue b/frontend/src/views/dashboard/DashboardPage.vue index 8d84594..03de2db 100644 --- a/frontend/src/views/dashboard/DashboardPage.vue +++ b/frontend/src/views/dashboard/DashboardPage.vue @@ -1,13 +1,13 @@ @@ -288,44 +352,127 @@ 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' +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() -const summary = ref({ onlineCount: 0, totalMachines: 0, todayProduction: 0, activeAlerts: 0, collectSuccessRate: 0, runningMachines: 0, dataMissingMachines: 0 }) -const collectorStatus = ref({ status: 'stopped', uptimeSeconds: 0, serviceStatus: 'NotInstalled', serviceName: '' }) -const machineRank = ref([]) -const workerRank = ref([]) -const trendData = ref([]) -const workshopData = ref([]) -const statusDist = ref({ online: 0, offline: 0, disabled: 0 }) -const recentAlerts = ref([]) +// ==================== 日期类型 ==================== + +type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom' + +const dateLabels: Record = { + 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({ + onlineCount: 0, + totalMachines: 0, + todayProduction: 0, + activeAlerts: 0, + collectSuccessRate: 0, + runningMachines: 0, + dataMissingMachines: 0, +}) +const collectorStatus = ref({ + status: 'stopped', + uptimeSeconds: 0, + serviceStatus: 'NotInstalled', + serviceName: '', +}) + +// 采集服务按钮状态 const startLoading = ref(false) const stopLoading = ref(false) const refreshLoading = ref(false) -// 日期筛选状态 -type DateType = 'today' | 'yesterday' | 'last3' | 'last7' | 'custom' -const workshopDateType = ref('today') -const workshopDateRange = ref<[string, string]>() +// 图表数据 +const trendData = ref([]) +const programDistribution = ref([]) + +// 排行数据 +const machineRank = ref([]) +const programRank = ref([]) +const workerRank = ref([]) +const workshopData = ref([]) + +// ==================== 日期筛选状态 ==================== + +// 产量趋势(近7天)— 无日期筛选 + +// NC程序产量分布日期 +const programDistDateType = ref('today') +const programDistDateRange = ref<[string, string]>() + +// 机床排行日期 const machineDateType = ref('today') const machineDateRange = ref<[string, string]>() + +// NC程序排行日期 +const programRankDateType = ref('today') +const programRankDateRange = ref<[string, string]>() + +// 工人排行日期 const workerDateType = ref('today') const workerDateRange = ref<[string, string]>() -// TOP N 和排序 +// 车间平均单机产量日期 +const workshopDateType = ref('today') +const workshopDateRange = ref<[string, string]>() + +// ==================== TOP N 和排序 ==================== + const machineTopN = ref(10) -const machineSortOrder = ref<'asc' | 'desc'>('asc') +const machineSortOrder = ref<'asc' | 'desc'>('desc') + +const programRankTopN = ref(10) + const workerTopN = ref(10) -const workerSortOrder = ref<'asc' | 'desc'>('asc') +const workerSortOrder = ref<'asc' | 'desc'>('desc') + +// ==================== 计算属性 ==================== -const dateLabels: Record = { 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) @@ -333,105 +480,190 @@ const workshopDays = computed(() => { 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.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 } - } +// 采集服务状态标签 +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 onWorkshopDateChange() { workshopDateType.value !== 'custom' ? loadWorkshopData() : undefined } -function onMachineDateChange() { machineDateType.value !== 'custom' ? loadMachineRankData() : undefined } -function onWorkerDateChange() { workerDateType.value !== 'custom' ? loadWorkerRankData() : undefined } +function formatNumber(val: number | undefined | null): string { + if (val == null) return '-' + return Number(val).toFixed(2) +} +// ==================== 日期变更处理 ==================== -// ECharts refs -const trendChartRef = ref() -const workshopChartRef = ref() -const statusPieRef = ref() -let trendChart: ECharts | null = null -let workshopChart: ECharts | null = null -let statusPie: ECharts | null = null +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; + ElMessage.info('采集服务已在运行中') + return } if (collectorStatus.value.serviceStatus && collectorStatus.value.serviceStatus === 'NotInstalled') { - ElMessage.warning('采集服务未安装,请运行 install.ps1 安装脚本'); - return; + ElMessage.warning('采集服务未安装,请运行 install.ps1 安装脚本') + return } - await request.post('/admin/collector/start'); - ElMessage.success('采集服务已启动'); - await loadData(); - } catch { /* request拦截器已显示错误 */ } finally { startLoading.value = false } + 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 } + 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 } + 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}时` -} +// ==================== ECharts 实例 ==================== -function formatNumber(val: number | undefined | null): string { - if (val == null) return '-' - return Number(val).toFixed(2) -} +const trendChartRef = ref() +const programDistChartRef = ref() +const workshopChartRef = ref() -// 采集服务状态:5种精确状态 -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' -}) +let trendChart: ECharts | null = null +let programDistChart: ECharts | null = null +let workshopChart: ECharts | null = null -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 alertTypeTag(type: string): string { - const map: Record = { collect_fail: 'danger', data_missing: 'warning', device_offline: 'danger', new_device: 'info' } - return map[type] || 'warning' +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 alertTypeLabel(type: string): string { - const map: Record = { collect_fail: '采集失败', data_missing: '数据缺失', device_offline: '设备离线', new_device: '新设备' } - return map[type] || type +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() { @@ -442,123 +674,162 @@ function initWorkshopChart() { workshopChart.setOption({ tooltip: { trigger: 'axis', - formatter: (params: any) => { + formatter: (params: { name: string; value: number; dataIndex: number }[]) => { const d = workshopData.value[params[0].dataIndex] return `${d.workshopName}
${unitLabel}产量: ${Number(params[0].value).toFixed(2)} ${unit}
总产量: ${d.quantity} 件
机床数: ${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: any) => `${Number(p.value).toFixed(2)} ${unit}`, fontSize: 12 }, - }], + 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() { - // 产量趋势折线图 - 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' } }, - ], - }], - }) - } + initTrendChart() + initProgramDistChart() + // workshopChart 在 loadWorkshopData 中初始化 } function disposeCharts() { trendChart?.dispose() + programDistChart?.dispose() workshopChart?.dispose() - statusPie?.dispose() trendChart = null + programDistChart = null workshopChart = null - statusPie = null } -async function loadWorkshopData() { +// ==================== 数据加载 ==================== + +async function loadProgramDistributionData() { 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 { /* */ } + 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 } }) + 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 { /* */ } + } 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 } }) + 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 { /* */ } + } 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, collectorRes, trendRes, statusRes, alertsRes] = await Promise.all([ + // 并行加载:统计摘要 + 采集状态 + 产量趋势 + const [summaryRes, collectorRes, trendRes] = await Promise.all([ request.get('/admin/dashboard/summary'), request.get('/admin/collector/status'), request.get<{ items: DashboardTrendItem[] }>('/admin/dashboard/trend'), - request.get('/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()]) + // 并行加载各区域独立数据 + await Promise.all([ + loadProgramDistributionData(), + loadWorkshopData(), + loadMachineRankData(), + loadWorkerRankData(), + loadProgramRankData(), + ]) } catch { // 保持容错 } } +// ==================== 生命周期 ==================== + onMounted(() => { loadData() }) @@ -573,7 +844,9 @@ onUnmounted(() => { .stat-row { margin-bottom: 16px; - .el-card { height: 100%; } + .el-card { + height: 100%; + } .stat-card { text-align: center; @@ -592,7 +865,7 @@ onUnmounted(() => { vertical-align: middle; &:hover { - color: #409EFF; + color: #409eff; } } } @@ -614,11 +887,6 @@ onUnmounted(() => { color: #909399; margin-top: 4px; } - - .alert-value { - color: #e6a23c; - cursor: pointer; - } } .collector-actions { @@ -631,7 +899,6 @@ onUnmounted(() => { } .chart-row, - .info-row, .rank-row { margin-bottom: 16px; } @@ -647,7 +914,7 @@ onUnmounted(() => { cursor: help; &:hover { - color: #409EFF; + color: #409eff; } } }