From afc7685e8998870d5530c28a446a4a5cf19f4ad7 Mon Sep 17 00:00:00 2001 From: liutao <790864623@qq.com> Date: Wed, 6 May 2026 16:04:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E8=BF=90=E8=A1=8C=E6=A6=82?= =?UTF-8?q?=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/en.ts | 55 ++++ src/locales/zh-CN.ts | 54 ++++ src/router/modules/remaining.ts | 22 ++ .../components/OperationTimelineChart.vue | 270 ++++++++++++++++++ .../components/OverviewFilterBar.vue | 114 ++++++++ .../components/OverviewMetricCards.vue | 120 ++++++++ .../components/StatusDistributionChart.vue | 240 ++++++++++++++++ src/views/iot/runoverview/components/types.ts | 58 ++++ src/views/iot/runoverview/index.vue | 176 ++++++++++++ src/views/iot/runoverview/mock.ts | 178 ++++++++++++ 10 files changed, 1287 insertions(+) create mode 100644 src/views/iot/runoverview/components/OperationTimelineChart.vue create mode 100644 src/views/iot/runoverview/components/OverviewFilterBar.vue create mode 100644 src/views/iot/runoverview/components/OverviewMetricCards.vue create mode 100644 src/views/iot/runoverview/components/StatusDistributionChart.vue create mode 100644 src/views/iot/runoverview/components/types.ts create mode 100644 src/views/iot/runoverview/index.vue create mode 100644 src/views/iot/runoverview/mock.ts diff --git a/src/locales/en.ts b/src/locales/en.ts index e856ed1c..bd588c96 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -4174,6 +4174,61 @@ export default { exportFilename: 'DeviceOperationReport.xls' }, + RunOverview: { + moduleName: 'Device Operation Overview', + pageIntro: + 'View device status distribution, KPI summary, and operation timelines in the menu page, with fullscreen support.', + + groupLabel: 'Device Group', + groupPlaceholder: 'Please select a group', + deviceLabel: 'Device Name', + devicePlaceholder: 'Please select a device', + timeRangeLabel: 'Time Range', + timeRangeStartPlaceholder: 'Start Time', + timeRangeEndPlaceholder: 'End Time', + + searchButtonText: 'Search', + resetButtonText: 'Reset', + exportButtonText: 'Export', + exportPlaceholderMessage: 'This is a static overview page for now. Export is reserved.', + + statusDistributionTitle: 'Time Distribution (Status Ratio)', + summaryTitle: 'Status Ratio (Summary)', + timelineTitle: 'Device Operation Timeline (Gantt)', + granularityHour: 'By Hour', + totalTimeLabel: 'Total Time', + compareLabel: 'vs Yesterday', + deviceFilterAll: 'All Devices', + expandAllText: 'Expand All', + enterFullscreen: 'Enter Fullscreen', + exitFullscreen: 'Exit Fullscreen', + statisticsTimeText: 'Statistics: {start} ~ {end}', + totalDevicesText: '{total} devices total', + pageUnit: '/page', + + tableDeviceNameColumn: 'Device Name', + tableUtilizationRateColumn: 'Utilization Rate', + + quickRange: { + today: 'Today', + yesterday: 'Yesterday', + last7Days: 'Last 7 Days', + last30Days: 'Last 30 Days', + custom: 'Custom' + }, + metrics: { + utilizationRate: 'Average Utilization', + powerOnRate: 'Average Power-on Rate', + faultRate: 'Average Fault Rate', + standbyRate: 'Average Standby Rate' + }, + legend: { + running: 'Running', + standby: 'Standby', + fault: 'Fault', + offline: 'Offline' + } + }, RealTimeMonitoring: { moduleName: 'Real-time Data Monitoring', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 1193d35b..16e4c65b 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -4011,6 +4011,60 @@ export default { exportFilename: '设备运行报表.xls' }, + RunOverview: { + moduleName: '设备运行总览', + pageIntro: '在菜单页中查看设备状态分布、指标概况与设备运行轨迹,并支持全屏查看。', + + groupLabel: '设备分组', + groupPlaceholder: '请选择分组', + deviceLabel: '设备名称', + devicePlaceholder: '请选择设备', + timeRangeLabel: '时间范围', + timeRangeStartPlaceholder: '开始时间', + timeRangeEndPlaceholder: '结束时间', + + searchButtonText: '查询', + resetButtonText: '重置', + exportButtonText: '导出', + exportPlaceholderMessage: '当前为静态总览页面,导出功能预留中', + + statusDistributionTitle: '时间分布图(状态占比)', + summaryTitle: '状态占比(汇总)', + timelineTitle: '设备运行轨迹(甘特图)', + granularityHour: '按小时', + totalTimeLabel: '总时间', + compareLabel: '较昨日', + deviceFilterAll: '全部设备', + expandAllText: '展开全部', + enterFullscreen: '全屏查看', + exitFullscreen: '退出全屏', + statisticsTimeText: '统计时间:{start} ~ {end}', + totalDevicesText: '共 {total} 台设备', + pageUnit: '条/页', + + tableDeviceNameColumn: '设备名称', + tableUtilizationRateColumn: '稼动率', + + quickRange: { + today: '今天', + yesterday: '昨天', + last7Days: '近7天', + last30Days: '近30天', + custom: '自定义' + }, + metrics: { + utilizationRate: '平均稼动率', + powerOnRate: '平均开机率', + faultRate: '平均故障率', + standbyRate: '平均待机率' + }, + legend: { + running: '运行', + standby: '待机', + fault: '故障', + offline: '离线' + } + }, RealTimeMonitoring: { moduleName: '数据实时监控', diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 659eafb2..2a28f0b1 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -642,6 +642,28 @@ const remainingRouter: AppRouteRecordRaw[] = [ canTo: true } }, + { + path: '/iot', + component: Layout, + name: 'IotHiddenPages', + meta: { + hidden: true + }, + children: [ + { + path: 'runoverview', + component: () => import('@/views/iot/runoverview/index.vue'), + name: 'IotRunOverview', + meta: { + title: t('DataCollection.RunOverview.moduleName'), + hidden: true, + noTagsView: false, + canTo: true, + activeMenu: '/iot/runoverview' + } + } + ] + }, { path: '/:pathMatch(.*)*', diff --git a/src/views/iot/runoverview/components/OperationTimelineChart.vue b/src/views/iot/runoverview/components/OperationTimelineChart.vue new file mode 100644 index 00000000..a5ec5dd2 --- /dev/null +++ b/src/views/iot/runoverview/components/OperationTimelineChart.vue @@ -0,0 +1,270 @@ + + + + {{ t('DataCollection.RunOverview.timelineTitle') }} + + + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + + + + {{ t('DataCollection.RunOverview.tableDeviceNameColumn') }} + {{ t('DataCollection.RunOverview.tableUtilizationRateColumn') }} + + + + {{ hour }} + + + + + + + {{ row.name }} + {{ row.utilizationRate.toFixed(2) }}% + + + + + + + + + + + + + + + diff --git a/src/views/iot/runoverview/components/OverviewFilterBar.vue b/src/views/iot/runoverview/components/OverviewFilterBar.vue new file mode 100644 index 00000000..74c7aec7 --- /dev/null +++ b/src/views/iot/runoverview/components/OverviewFilterBar.vue @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + + + {{ t('DataCollection.RunOverview.searchButtonText') }} + + + + {{ t('DataCollection.RunOverview.resetButtonText') }} + + + + {{ t('DataCollection.RunOverview.exportButtonText') }} + + + + + + + + + diff --git a/src/views/iot/runoverview/components/OverviewMetricCards.vue b/src/views/iot/runoverview/components/OverviewMetricCards.vue new file mode 100644 index 00000000..79496b2f --- /dev/null +++ b/src/views/iot/runoverview/components/OverviewMetricCards.vue @@ -0,0 +1,120 @@ + + + + + + + + {{ t(`DataCollection.RunOverview.metrics.${item.key}`) }} + + {{ item.value.toFixed(2) }}{{ item.unit }} + + + {{ t('DataCollection.RunOverview.compareLabel') }} + + {{ item.change >= 0 ? '↑' : '↓' }} {{ Math.abs(item.change).toFixed(2) }}% + + + + + + + + + + diff --git a/src/views/iot/runoverview/components/StatusDistributionChart.vue b/src/views/iot/runoverview/components/StatusDistributionChart.vue new file mode 100644 index 00000000..2dacea95 --- /dev/null +++ b/src/views/iot/runoverview/components/StatusDistributionChart.vue @@ -0,0 +1,240 @@ + + + + + {{ t('DataCollection.RunOverview.statusDistributionTitle') }} + + + + + + + + + + + + + + + + + + + + {{ t('DataCollection.RunOverview.summaryTitle') }} + + + + + {{ statusLabelMap[item.status] }} + {{ item.percent.toFixed(2) }}% ({{ item.hours.toFixed(2) }}h) + + + + + + + + + + diff --git a/src/views/iot/runoverview/components/types.ts b/src/views/iot/runoverview/components/types.ts new file mode 100644 index 00000000..c4556d4b --- /dev/null +++ b/src/views/iot/runoverview/components/types.ts @@ -0,0 +1,58 @@ +export type RunStatus = 'running' | 'standby' | 'fault' | 'offline' + +export type QuickRangeKey = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'custom' + +export interface OverviewOption { + label: string + value: string +} + +export interface RunOverviewQueryParams { + groupId: string + deviceId: string + quickRange: QuickRangeKey + timeRange: [string, string] +} + +export interface RunOverviewMetric { + key: string + icon: string + value: number + unit: string + change: number +} + +export interface HourlyStatusItem { + hour: string + running: number + standby: number + fault: number + offline: number +} + +export interface StatusSummaryItem { + status: RunStatus + percent: number + hours: number +} + +export interface TimelineSegment { + status: RunStatus + startHour: number + endHour: number +} + +export interface DeviceTimelineRow { + id: string + name: string + utilizationRate: number + segments: TimelineSegment[] +} + +export interface RunOverviewData { + metrics: RunOverviewMetric[] + hourlyStatus: HourlyStatusItem[] + summary: StatusSummaryItem[] + timelineRows: DeviceTimelineRow[] + totalDevices: number +} diff --git a/src/views/iot/runoverview/index.vue b/src/views/iot/runoverview/index.vue new file mode 100644 index 00000000..c767b370 --- /dev/null +++ b/src/views/iot/runoverview/index.vue @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/iot/runoverview/mock.ts b/src/views/iot/runoverview/mock.ts new file mode 100644 index 00000000..1c3ba0b8 --- /dev/null +++ b/src/views/iot/runoverview/mock.ts @@ -0,0 +1,178 @@ +import dayjs from 'dayjs' +import type { + DeviceTimelineRow, + HourlyStatusItem, + OverviewOption, + RunOverviewData, + RunOverviewQueryParams, + RunOverviewMetric, + RunStatus +} from './components/types' + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' + +export const GROUP_OPTIONS: OverviewOption[] = [ + { label: 'SMT一组', value: 'group-1' }, + { label: '成型二组', value: 'group-2' }, + { label: '总装三组', value: 'group-3' } +] + +export const DEVICE_OPTIONS: OverviewOption[] = [ + { label: '模拟干燥设备04', value: 'device-01' }, + { label: '模拟干燥设备03', value: 'device-02' }, + { label: '模拟干燥设备02', value: 'device-03' }, + { label: '成型模拟设备05', value: 'device-04' }, + { label: '成型模拟设备04', value: 'device-05' }, + { label: '模拟干燥设备', value: 'device-06' }, + { label: '成型模拟设备02', value: 'device-07' }, + { label: '成型模拟设备01', value: 'device-08' }, + { label: '物流输送设备01', value: 'device-09' }, + { label: '包装工作站02', value: 'device-10' }, + { label: '包装工作站03', value: 'device-11' }, + { label: '拧紧设备01', value: 'device-12' }, + { label: '视觉检测设备01', value: 'device-13' } +] + +const shiftTimeline = (segments: Array<{ status: RunStatus; startHour: number; endHour: number }>, offset: number) => + segments.map((segment, index) => { + let nextStart = segment.startHour + offset + let nextEnd = segment.endHour + offset + if (index === 0 && nextStart < 0) nextStart = 0 + if (index === segments.length - 1 && nextEnd > 24) nextEnd = 24 + return { + ...segment, + startHour: Math.max(0, Math.min(24, Number(nextStart.toFixed(2)))), + endHour: Math.max(0, Math.min(24, Number(nextEnd.toFixed(2)))) + } + }) + +const BASE_SEGMENTS = [ + { status: 'running' as const, startHour: 0, endHour: 2.7 }, + { status: 'standby' as const, startHour: 2.7, endHour: 3.1 }, + { status: 'running' as const, startHour: 3.1, endHour: 6.6 }, + { status: 'standby' as const, startHour: 6.6, endHour: 7.15 }, + { status: 'running' as const, startHour: 7.15, endHour: 13.9 }, + { status: 'standby' as const, startHour: 13.9, endHour: 14.25 }, + { status: 'offline' as const, startHour: 14.25, endHour: 14.95 }, + { status: 'running' as const, startHour: 14.95, endHour: 17.45 }, + { status: 'fault' as const, startHour: 17.45, endHour: 18.15 }, + { status: 'standby' as const, startHour: 18.15, endHour: 18.8 }, + { status: 'running' as const, startHour: 18.8, endHour: 20.55 }, + { status: 'standby' as const, startHour: 20.55, endHour: 20.95 }, + { status: 'running' as const, startHour: 20.95, endHour: 22.25 }, + { status: 'offline' as const, startHour: 22.25, endHour: 23.05 }, + { status: 'running' as const, startHour: 23.05, endHour: 23.7 }, + { status: 'offline' as const, startHour: 23.7, endHour: 24 } +] + +const TIMELINE_OFFSETS = [0, -0.3, 0.8, -0.5, 0.45, 1.05, -1.1, 0.25, -0.75, 0.6, -0.2, 1.2, -0.45] + +export const buildDefaultQueryParams = (): RunOverviewQueryParams => { + const start = dayjs().startOf('day') + const end = dayjs().endOf('day') + return { + groupId: GROUP_OPTIONS[0].value, + deviceId: '', + quickRange: 'today', + timeRange: [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)] + } +} + +const toTimelineRows = (): DeviceTimelineRow[] => + DEVICE_OPTIONS.map((device, index) => ({ + id: device.value, + name: device.label, + utilizationRate: [82.35, 76.12, 68.54, 75.63, 72.18, 91.24, 65.32, 78.44, 71.05, 83.67, 69.18, 87.42, 74.56][index], + segments: shiftTimeline(BASE_SEGMENTS, TIMELINE_OFFSETS[index] || 0) + })) + +const toHourlyStatus = (summaryFactor: number): HourlyStatusItem[] => { + return Array.from({ length: 24 }, (_, hour) => { + const runningBase = 74 + Math.sin((hour / 24) * Math.PI * 3) * 14 + summaryFactor * 2 + const standbyBase = 10 + Math.cos((hour / 24) * Math.PI * 4) * 7 + const faultBase = 2 + Math.max(0, Math.sin((hour - 5) / 2.2)) * 5 + const offlineBase = 100 - runningBase - standbyBase - faultBase + + const running = Math.max(48, Math.min(88, Number(runningBase.toFixed(2)))) + const standby = Math.max(4, Math.min(26, Number(standbyBase.toFixed(2)))) + const fault = Math.max(1, Math.min(8, Number(faultBase.toFixed(2)))) + const offline = Number(Math.max(4, 100 - running - standby - fault).toFixed(2)) + + return { + hour: `${String(hour).padStart(2, '0')}:00`, + running, + standby, + fault, + offline + } + }) +} + +const createMetrics = (summaryFactor: number): RunOverviewMetric[] => [ + { + key: 'utilizationRate', + icon: 'ep:pie-chart', + value: Number((75.42 + summaryFactor * 1.2).toFixed(2)), + unit: '%', + change: 4.32 + }, + { + key: 'powerOnRate', + icon: 'ep:video-play', + value: Number((90.12 + summaryFactor * 0.7).toFixed(2)), + unit: '%', + change: 2.15 + }, + { + key: 'faultRate', + icon: 'ep:warning', + value: Number(Math.max(1.5, 3.21 - summaryFactor * 0.35).toFixed(2)), + unit: '%', + change: -1.03 + }, + { + key: 'standbyRate', + icon: 'ep:timer', + value: Number(Math.max(4, 6.67 - summaryFactor * 0.25).toFixed(2)), + unit: '%', + change: -1.14 + } +] + +const createSummary = (summaryFactor: number) => { + const running = Number((75.42 + summaryFactor * 1.2).toFixed(2)) + const standby = Number(Math.max(4, 6.67 - summaryFactor * 0.25).toFixed(2)) + const fault = Number(Math.max(1.5, 3.21 - summaryFactor * 0.35).toFixed(2)) + const offline = Number((100 - running - standby - fault).toFixed(2)) + + return [ + { status: 'running' as const, percent: running, hours: Number(((24 * running) / 100).toFixed(2)) }, + { status: 'standby' as const, percent: standby, hours: Number(((24 * standby) / 100).toFixed(2)) }, + { status: 'fault' as const, percent: fault, hours: Number(((24 * fault) / 100).toFixed(2)) }, + { status: 'offline' as const, percent: offline, hours: Number(((24 * offline) / 100).toFixed(2)) } + ] +} + +export const buildRunOverviewData = (query: RunOverviewQueryParams): RunOverviewData => { + const summaryFactor = + query.quickRange === 'today' + ? 0 + : query.quickRange === 'yesterday' + ? -0.8 + : query.quickRange === 'last7Days' + ? 1.1 + : query.quickRange === 'last30Days' + ? 0.45 + : 0.15 + + const rows = toTimelineRows() + const filteredRows = query.deviceId ? rows.filter((row) => row.id === query.deviceId) : rows + + return { + metrics: createMetrics(summaryFactor), + hourlyStatus: toHourlyStatus(summaryFactor), + summary: createSummary(summaryFactor), + timelineRows: filteredRows, + totalDevices: filteredRows.length + } +}