diff --git a/backend/src/main/java/org/apache/tsfile/viewer/service/FileService.java b/backend/src/main/java/org/apache/tsfile/viewer/service/FileService.java index 42225be..9074e91 100644 --- a/backend/src/main/java/org/apache/tsfile/viewer/service/FileService.java +++ b/backend/src/main/java/org/apache/tsfile/viewer/service/FileService.java @@ -322,10 +322,15 @@ public String getFilePath(String fileId) throws TsFileNotFoundException, AccessD return path; } - // Try to decode fileId as base64-encoded path (server-side files) + // Try to decode fileId as base64-encoded path (server-side files). + // The frontend encodes paths as URL-safe base64 without padding so that + // non-Latin1 characters (e.g. Chinese) survive and the value is safe as a + // single-segment route parameter. getUrlDecoder() also accepts standard + // base64 alphabets that contain no '+' or '/', preserving backward + // compatibility with previously issued fileIds. String decodedPath; try { - byte[] decodedBytes = Base64.getDecoder().decode(fileId); + byte[] decodedBytes = Base64.getUrlDecoder().decode(fileId); decodedPath = new String(decodedBytes, StandardCharsets.UTF_8); logger.debug("Decoded fileId from base64: {} -> {}", fileId, decodedPath); } catch (IllegalArgumentException e) { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6d84f70..fcd85e5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -82,7 +82,7 @@ function handleDirectorySelect(path: string, _name: string) { :collapsed-width="0" :theme="isDark ? 'dark' : 'light'" > -
+
diff --git a/frontend/src/api/tsfile/data.ts b/frontend/src/api/tsfile/data.ts index c398ea3..383f319 100644 --- a/frontend/src/api/tsfile/data.ts +++ b/frontend/src/api/tsfile/data.ts @@ -28,7 +28,29 @@ import type { import { apiClient } from "../request"; export function previewData(request: DataPreviewRequest) { - return apiClient.post("/data/preview", request); + return apiClient.post("/data/preview", request, { + // 保留时间戳原始精度:JSON.parse 会把 >2^53 的纳秒时间戳转成有损 double, + // 因此在解析前把每个 timestamp 的精确数字串旁挂为 timestampRaw 字符串字段。 + // 仅作用于本接口,避免影响其它响应。 + transformResponse: [ + (raw: unknown) => { + if (typeof raw !== "string") return raw; + try { + const patched = raw.replace( + /"timestamp"\s*:\s*(-?\d+)/g, + '"timestampRaw":"$1","timestamp":$1', + ); + return JSON.parse(patched); + } catch { + try { + return JSON.parse(raw); + } catch { + return raw; + } + } + }, + ], + }); } export function queryTableData(request: TableDataRequest) { diff --git a/frontend/src/api/tsfile/types.ts b/frontend/src/api/tsfile/types.ts index 3f038ca..c660cd8 100644 --- a/frontend/src/api/tsfile/types.ts +++ b/frontend/src/api/tsfile/types.ts @@ -160,6 +160,11 @@ export interface FilterConditions { export interface DataRow { timestamp: number; + /** + * 时间戳的精确原始数字串。JSON.parse 会把 >2^53 的纳秒时间戳转成有损 double, + * 该字段在解析前从响应文本中原样保留,用于无损展示原始存储值。 + */ + timestampRaw?: string; device: string; measurements: Record; } diff --git a/frontend/src/components/tsfile/DataTable.vue b/frontend/src/components/tsfile/DataTable.vue index 33f650d..3c2c5f1 100644 --- a/frontend/src/components/tsfile/DataTable.vue +++ b/frontend/src/components/tsfile/DataTable.vue @@ -28,9 +28,11 @@ import type { TableColumnType } from "antdv-next"; import { computed, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; -import { Alert, Button, Card, Input, Pagination, Select, Spin, Table } from "antdv-next"; +import { Alert, Button, Card, Input, Pagination, Select, Spin, Table, Tooltip } from "antdv-next"; import { DownloadOutlined } from "@antdv-next/icons"; +import { formatTimestamp } from "@/utils/timestamp"; + interface Props { data: DataRow[]; total: number; @@ -148,15 +150,16 @@ const columns = computed(() => { }, { title: t("tsfile.data.device"), - dataIndex: "device", - key: "device", + dataIndex: "__device__", + key: "__device__", fixed: "left", width: 180, sorter: true, }, ]; - // 标签列(固定左侧) + // 标签列(固定左侧)—— tag 名可能与保留列同名(如 "device"), + // 固定"设备"列已改用保留 key "__device__" 避免冲突,此处 tag 列可安全使用原名。 for (const tagCol of props.tagColumns) { cols.push({ title: tagCol, @@ -195,7 +198,8 @@ const tableData = computed(() => { const flatRow: Record = { _key: `${row.timestamp}-${row.device}-${index}`, timestamp: row.timestamp, - device: row.device, + timestampRaw: row.timestampRaw, + __device__: row.device, }; for (const measurement of visibleMeasurements) { flatRow[measurement] = formatValue(row.measurements[measurement]); @@ -263,13 +267,7 @@ function handleTableChange( } } -// 格式化时间戳(含毫秒) -function formatTimestamp(timestamp: number): string { - const d = new Date(timestamp); - const pad = (n: number, len = 2) => String(n).padStart(len, '0'); - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`; -} - +// 将不同精度的时间戳归一到毫秒并格式化,见 utils/timestamp.ts // 格式化值 function formatValue(value: unknown): number | string { if (value === null || value === undefined) return "-"; @@ -348,6 +346,16 @@ function formatValue(value: unknown): number | string { />
+ + + -