Преглед изворни кода

🐛 fix(设备监控): 修正监控数据导出时间偏移

- 将时间戳转换为 Excel 数值日期序列,规避 SheetJS 在上海时区产生的 43 秒偏移
- 保留 Excel 日期格式及排序能力,确保导出时间与实际采集时间一致
- 新增监控数据导出时间回归测试并纳入 Vitest
Zimo пре 2 дана
родитељ
комит
fa683b946d

+ 31 - 0
src/views/oli-connection/monitoring/__tests__/excel-date.spec.ts

@@ -0,0 +1,31 @@
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import { toExcelDateValue } from '../excelDate'
+
+const originalTimezone = process.env.TZ
+
+describe('monitoring Excel date export', () => {
+  beforeAll(() => {
+    process.env.TZ = 'Asia/Shanghai'
+  })
+
+  afterAll(() => {
+    if (originalTimezone === undefined) delete process.env.TZ
+    else process.env.TZ = originalTimezone
+  })
+
+  it('keeps the local wall-clock time when SheetJS writes the workbook', async () => {
+    const XLSX = await import('xlsx')
+    const timestamp = new Date(2026, 8, 28, 10, 0, 0).getTime()
+    const worksheet = XLSX.utils.aoa_to_sheet([[toExcelDateValue(timestamp)]], {
+      cellDates: true
+    })
+    worksheet.A1.z = 'yyyy-mm-dd hh:mm:ss'
+
+    const workbook = XLSX.utils.book_new()
+    XLSX.utils.book_append_sheet(workbook, worksheet, '监控数据')
+    const output = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
+    const exportedCell = XLSX.read(output, { type: 'buffer' }).Sheets['监控数据'].A1
+
+    expect(exportedCell.w).toBe('2026-09-28 10:00:00')
+  })
+})

+ 4 - 3
src/views/oli-connection/monitoring/detail.vue

@@ -4,6 +4,7 @@ import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { rangeShortcuts } from '@/utils/formatTime'
 import { rangeShortcuts } from '@/utils/formatTime'
 import { useMqtt } from '@/utils/useMqtt'
 import { useMqtt } from '@/utils/useMqtt'
+import { toExcelDateValue } from './excelDate'
 import {
 import {
   Dimensions,
   Dimensions,
   formatChartValue,
   formatChartValue,
@@ -1014,13 +1015,13 @@ function buildExportRows(selectedDimensions: Dimensions[]) {
       new Map((chartData.value[dimension.name] || []).map((point) => [point.ts, point.value]))
       new Map((chartData.value[dimension.name] || []).map((point) => [point.ts, point.value]))
     ])
     ])
   )
   )
-  const rows: Array<Array<string | number | Date | null>> = [
+  const rows: Array<Array<string | number | null>> = [
     ['时间', ...selectedDimensions.map((dimension) => dimension.name)]
     ['时间', ...selectedDimensions.map((dimension) => dimension.name)]
   ]
   ]
 
 
   timestamps.forEach((timestamp) => {
   timestamps.forEach((timestamp) => {
     rows.push([
     rows.push([
-      new Date(timestamp),
+      toExcelDateValue(timestamp),
       ...selectedDimensions.map(
       ...selectedDimensions.map(
         (dimension) => valuesByDimension.get(dimension.name)?.get(timestamp) ?? null
         (dimension) => valuesByDimension.get(dimension.name)?.get(timestamp) ?? null
       )
       )
@@ -1054,7 +1055,7 @@ async function exportChartData() {
 
 
   exportDataLoading.value = true
   exportDataLoading.value = true
   try {
   try {
-    const worksheet = XLSX.utils.aoa_to_sheet(rows, { cellDates: true })
+    const worksheet = XLSX.utils.aoa_to_sheet(rows)
     worksheet['!cols'] = [{ wch: 22 }, ...selectedDimensions.map(() => ({ wch: 20 }))]
     worksheet['!cols'] = [{ wch: 22 }, ...selectedDimensions.map(() => ({ wch: 20 }))]
 
 
     timestamps.forEach((_, index) => {
     timestamps.forEach((_, index) => {

+ 20 - 0
src/views/oli-connection/monitoring/excelDate.ts

@@ -0,0 +1,20 @@
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000
+const EXCEL_UNIX_EPOCH_OFFSET_DAYS = 25_569
+
+export function toExcelDateValue(timestamp: number) {
+  const date = new Date(timestamp)
+  const localWallClockTimestamp = Date.UTC(
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate(),
+    date.getHours(),
+    date.getMinutes(),
+    date.getSeconds(),
+    date.getMilliseconds()
+  )
+
+  // xlsx 0.18.5 converts Date cells using the historical timezone offset at
+  // Excel's epoch. Asia/Shanghai therefore gains 43 seconds. Supplying the
+  // numeric Excel serial preserves the local wall-clock time without that path.
+  return localWallClockTimestamp / MILLISECONDS_PER_DAY + EXCEL_UNIX_EPOCH_OFFSET_DAYS
+}

+ 4 - 1
vitest.config.mts

@@ -9,6 +9,9 @@ export default defineConfig({
   },
   },
   test: {
   test: {
     environment: 'node',
     environment: 'node',
-    include: ['src/components/mt-edit/**/*.spec.ts']
+    include: [
+      'src/components/mt-edit/**/*.spec.ts',
+      'src/views/oli-connection/monitoring/**/*.spec.ts'
+    ]
   }
   }
 })
 })