From c0ceba93b7b7261b185c2efa8066ab354ec901ae Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 7 Jun 2026 00:22:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6=E8=BD=A8=E8=BF=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- meshmap_frontend/src/api.ts | 23 ++- .../src/components/NodeDetailedPage.vue | 138 +++++++++++++++++- meshmap_frontend/src/style.css | 55 +++++++ 3 files changed, 204 insertions(+), 12 deletions(-) diff --git a/meshmap_frontend/src/api.ts b/meshmap_frontend/src/api.ts index c03cb21..25ccb5d 100644 --- a/meshmap_frontend/src/api.ts +++ b/meshmap_frontend/src/api.ts @@ -55,10 +55,23 @@ async function requestJSON(path: string, init?: RequestInit): Promise { return response.json() as Promise } -function listPath(path: string, limit: number, offset: number, nodeId = ''): string { +type ListQueryOptions = { + nodeId?: string + since?: string + until?: string +} + +function listPath(path: string, limit: number, offset: number, nodeIdOrOptions: string | ListQueryOptions = ''): string { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }) - if (nodeId) { - params.set('node_id', nodeId) + const options = typeof nodeIdOrOptions === 'string' ? { nodeId: nodeIdOrOptions } : nodeIdOrOptions + if (options.nodeId) { + params.set('node_id', options.nodeId) + } + if (options.since) { + params.set('since', options.since) + } + if (options.until) { + params.set('until', options.until) } return `${path}?${params.toString()}` } @@ -150,8 +163,8 @@ export function deleteNode(nodeId: string): Promise<{ status: string }> { return deleteJSON<{ status: string }>(`/api/admin/nodes/${encodeURIComponent(nodeId)}`) } -export function getPositions(limit = 500, offset = 0, nodeId = ''): Promise> { - return getJSON>(listPath('/api/positions', limit, offset, nodeId)) +export function getPositions(limit = 500, offset = 0, nodeIdOrOptions: string | ListQueryOptions = ''): Promise> { + return getJSON>(listPath('/api/positions', limit, offset, nodeIdOrOptions)) } export function getDiscardDetails(limit = 100, offset = 0): Promise> { diff --git a/meshmap_frontend/src/components/NodeDetailedPage.vue b/meshmap_frontend/src/components/NodeDetailedPage.vue index 579afe7..e1a674d 100644 --- a/meshmap_frontend/src/components/NodeDetailedPage.vue +++ b/meshmap_frontend/src/components/NodeDetailedPage.vue @@ -21,8 +21,18 @@ const mapSource = ref(fallbackMapSource) const loading = ref(true) const chatLoadingOlder = ref(false) const chatHasMore = ref(true) +const telemetryLoading = ref(false) +const trajectoryLoading = ref(false) +const trajectoryError = ref('') +const trajectoryTruncated = ref(false) const error = ref('') const chatPageSize = 20 +const telemetryPageSize = 25 +const trajectoryPageSize = 500 +const maxTrajectoryPoints = 5000 +const telemetryPage = ref(1) +const trajectoryStartDate = ref(toDateInputValue()) +const trajectoryEndDate = ref(toDateInputValue()) const chatHistoryRef = ref(null) const scrollOverflowAllowance = 1 type GroupedTextMessage = TextMessage & { mergedCount: number; mergedMessages: TextMessage[] } @@ -98,6 +108,27 @@ function formatTime(value: string): string { return new Date(value).toLocaleString() } +function toDateInputValue(date = new Date()): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +function localDateRange(startDate: string, endDate: string): { since: string; until: string } | null { + if (!startDate || !endDate) { + trajectoryError.value = '请选择开始日期和结束日期' + return null + } + const safeStartDate = startDate <= endDate ? startDate : endDate + const safeEndDate = startDate <= endDate ? endDate : startDate + trajectoryStartDate.value = safeStartDate + trajectoryEndDate.value = safeEndDate + const since = new Date(`${safeStartDate}T00:00:00.000`) + const until = new Date(`${safeEndDate}T23:59:59.999`) + return { since: since.toISOString(), until: until.toISOString() } +} + function metricEntries(value: string | null): Array<[string, unknown]> { if (!value) { return [] @@ -179,6 +210,73 @@ async function optional(request: Promise): Promise { } } +function canTelemetryPrev(): boolean { + return telemetryPage.value > 1 +} + +function canTelemetryNext(): boolean { + return telemetry.value.length === telemetryPageSize +} + +async function loadTelemetryPage() { + telemetryLoading.value = true + try { + const response = await getTelemetry(telemetryPageSize, (telemetryPage.value - 1) * telemetryPageSize, props.nodeId) + telemetry.value = response.items + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } finally { + telemetryLoading.value = false + } +} + +function changeTelemetryPage(nextPage: number) { + telemetryPage.value = Math.max(1, nextPage) + loadTelemetryPage() +} + +async function loadTrajectoryRange() { + const range = localDateRange(trajectoryStartDate.value, trajectoryEndDate.value) + if (!range) { + return + } + + trajectoryLoading.value = true + trajectoryError.value = '' + trajectoryTruncated.value = false + positions.value = [] + try { + const items: PositionRecord[] = [] + for (let offset = 0; offset < maxTrajectoryPoints; offset += trajectoryPageSize) { + const response = await getPositions(trajectoryPageSize, offset, { + nodeId: props.nodeId, + since: range.since, + until: range.until, + }) + items.push(...response.items) + if (response.items.length < trajectoryPageSize) { + break + } + if (items.length >= maxTrajectoryPoints) { + trajectoryTruncated.value = true + break + } + } + positions.value = items.slice(0, maxTrajectoryPoints) + } catch (err) { + trajectoryError.value = err instanceof Error ? err.message : String(err) + } finally { + trajectoryLoading.value = false + } +} + +function applyTodayTrajectory() { + const today = toDateInputValue() + trajectoryStartDate.value = today + trajectoryEndDate.value = today + loadTrajectoryRange() +} + async function loadInitialMessages() { const response = await getTextMessages(chatPageSize, 0, props.nodeId) messages.value = toChronological(response.items) @@ -403,18 +501,20 @@ function selectMapSource(sourceId: number) { async function loadDetails() { loading.value = true error.value = '' + trajectoryError.value = '' + telemetryPage.value = 1 try { - const [nodeData, reportData, positionData, telemetryData] = await Promise.all([ + const [nodeData, reportData] = await Promise.all([ optional(getNodeInfoById(props.nodeId)), optional(getMapReportById(props.nodeId)), - getPositions(500, 0, props.nodeId), - getTelemetry(200, 0, props.nodeId), ]) nodeInfo.value = nodeData mapReport.value = reportData - positions.value = positionData.items - telemetry.value = telemetryData.items - await loadInitialMessages() + await Promise.all([ + loadTrajectoryRange(), + loadTelemetryPage(), + loadInitialMessages(), + ]) } catch (err) { error.value = err instanceof Error ? err.message : String(err) } finally { @@ -526,6 +626,23 @@ onBeforeUnmount(() => { {{ positions.length }} +
+ + + + +
+

{{ trajectoryError }}

+

轨迹点较多,仅显示前 {{ maxTrajectoryPoints }} 条,请缩小日期范围。

+

正在加载轨迹...

{

Telemetry

遥测数据:{{ nodeTitle }}

- {{ telemetry.length }} + 本页 {{ telemetry.length }} +
正在加载遥测数据...
@@ -569,6 +687,12 @@ onBeforeUnmount(() => {
暂无遥测数据
+