Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function handleDirectorySelect(path: string, _name: string) {
:collapsed-width="0"
:theme="isDark ? 'dark' : 'light'"
>
<div style="padding: 16px; overflow-y: auto; height: 100%;">
<div style="padding: 16px; overflow: hidden; height: 100%;">
<FileTree @select="handleFileSelect" @select-directory="handleDirectorySelect" />
</div>
</LayoutSider>
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/api/tsfile/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,29 @@ import type {
import { apiClient } from "../request";

export function previewData(request: DataPreviewRequest) {
return apiClient.post<unknown, DataPreviewResponse>("/data/preview", request);
return apiClient.post<unknown, DataPreviewResponse>("/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) {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/api/tsfile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ export interface FilterConditions {

export interface DataRow {
timestamp: number;
/**
* 时间戳的精确原始数字串。JSON.parse 会把 >2^53 的纳秒时间戳转成有损 double,
* 该字段在解析前从响应文本中原样保留,用于无损展示原始存储值。
*/
timestampRaw?: string;
device: string;
measurements: Record<string, unknown>;
}
Expand Down
52 changes: 35 additions & 17 deletions frontend/src/components/tsfile/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -148,15 +150,16 @@ const columns = computed<TableColumnType[]>(() => {
},
{
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,
Expand Down Expand Up @@ -195,7 +198,8 @@ const tableData = computed(() => {
const flatRow: Record<string, unknown> = {
_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]);
Expand Down Expand Up @@ -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 "-";
Expand Down Expand Up @@ -348,6 +346,16 @@ function formatValue(value: unknown): number | string {
/>
</div>

<!-- 时间戳精度说明 -->
<Alert
v-if="!error"
type="info"
show-icon
:message="t('tsfile.data.precisionNote')"
class="mb-3"
banner
/>

<!-- 数据表格 -->
<Table
v-if="!error"
Expand All @@ -362,13 +370,15 @@ function formatValue(value: unknown): number | string {
row-key="_key"
@change="handleTableChange"
>
<template #bodyCell="{ column, text }">
<template #bodyCell="{ column, text, record }">
<template v-if="column.key === 'timestamp'">
<span class="font-mono text-xs">
{{ formatTimestamp(text as number) }}
</span>
<Tooltip :title="`${t('tsfile.data.rawTimestamp')}: ${record.timestampRaw ?? text}`">
<span class="font-mono text-xs timestamp-cell">
{{ formatTimestamp(text as number) }}
</span>
</Tooltip>
</template>
<template v-else-if="column.key === 'device'">
<template v-else-if="column.key === '__device__'">
<span class="font-medium">{{ text }}</span>
</template>
<template v-else>
Expand Down Expand Up @@ -412,3 +422,11 @@ function formatValue(value: unknown): number | string {
</div>
</Card>
</template>

<style scoped>
/* 时间戳单元格:虚线下划线提示悬浮可查看原始存储值 */
.timestamp-cell {
border-bottom: 1px dotted var(--ant-color-border, #bbb);
cursor: help;
}
</style>
Loading