更新地图

This commit is contained in:
2026-06-03 19:59:49 +08:00
parent 9748a1e681
commit 191651fce9
13 changed files with 946 additions and 173 deletions
+94 -66
View File
@@ -1,29 +1,81 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { getHealth, getNodes, getPositions, getTextMessages } from './api'
import type { HealthStatus, NodeInfoMap, PositionRecord, TextMessage } from './types'
import ChatPanel from './components/ChatPanel.vue'
import MeshMap from './components/MeshMap.vue'
import NodeListPanel from './components/NodeListPanel.vue'
import type { HealthStatus, MapNode, NodeInfoById, NodeInfoMap, PositionRecord, TextMessage } from './types'
const loading = ref(true)
const nodePageLoading = ref(false)
const error = ref('')
const selectedNodeId = ref<string | null>(null)
const health = ref<HealthStatus | null>(null)
const nodes = ref<NodeInfoMap[]>([])
const mapNodeSource = ref<NodeInfoMap[]>([])
const pagedNodes = ref<NodeInfoMap[]>([])
const nodePage = ref(1)
const nodePageSize = 25
const nodeTotal = ref(0)
const messages = ref<TextMessage[]>([])
const positions = ref<PositionRecord[]>([])
const nodesById = computed<NodeInfoById>(() => {
const map = new Map<string, NodeInfoMap>()
for (const node of mapNodeSource.value) {
map.set(node.node_id, node)
}
for (const node of pagedNodes.value) {
map.set(node.node_id, node)
}
return Object.fromEntries(map)
})
const mapNodes = computed<MapNode[]>(() => {
return mapNodeSource.value
.filter((node) => node.latitude != null && node.longitude != null)
.map((node) => ({
node_id: node.node_id,
label: node.short_name || node.node_id,
latitude: node.latitude as number,
longitude: node.longitude as number,
altitude: node.altitude,
source: 'node',
updated_at: node.updated_at,
node,
latest_position: null,
}))
})
async function loadNodePage(page: number) {
nodePageLoading.value = true
try {
const safePage = Math.max(1, page)
const response = await getNodes(nodePageSize, (safePage - 1) * nodePageSize)
pagedNodes.value = response.items
nodeTotal.value = response.total ?? response.offset + response.items.length
nodePage.value = safePage
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
nodePageLoading.value = false
}
}
async function refresh() {
loading.value = true
error.value = ''
try {
const [healthData, nodeData, messageData, positionData] = await Promise.all([
const [healthData, mapNodeData, messageData, positionData] = await Promise.all([
getHealth(),
getNodes(),
getTextMessages(),
getPositions(),
getNodes(500, 0),
getTextMessages(100),
getPositions(500),
])
health.value = healthData
nodes.value = nodeData.items
mapNodeSource.value = mapNodeData.items
messages.value = messageData.items
positions.value = positionData.items
await loadNodePage(nodePage.value)
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
} finally {
@@ -35,71 +87,47 @@ onMounted(refresh)
</script>
<template>
<main class="page">
<header class="header">
<main class="app-shell">
<header class="topbar">
<div>
<p class="eyebrow">Meshtastic MQTT Server</p>
<h1>MeshMap Dashboard</h1>
<h1>MeshMap</h1>
</div>
<div class="topbar-actions">
<span class="status-pill" :class="{ ok: health?.status === 'ok' }">
{{ health?.status ?? 'unknown' }} / db {{ health?.database ?? 'unknown' }}
</span>
<span class="counter">节点 {{ nodeTotal }} · 消息 {{ messages.length }} · 坐标 {{ mapNodes.length }}</span>
<button @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
</div>
<button @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
</header>
<section class="status" :class="{ ok: health?.status === 'ok' }">
<strong>服务状态</strong>
<span>{{ health?.status ?? 'unknown' }}</span>
<span>database: {{ health?.database ?? 'unknown' }}</span>
</section>
<p v-if="error" class="error">{{ error }}</p>
<section class="grid">
<article class="card wide">
<h2>节点</h2>
<table>
<thead>
<tr>
<th>Node</th>
<th>Name</th>
<th>Role</th>
<th>HW</th>
<th>Lat</th>
<th>Lon</th>
</tr>
</thead>
<tbody>
<tr v-for="node in nodes" :key="node.node_id">
<td>{{ node.node_id }}</td>
<td>{{ node.long_name || node.short_name || '-' }}</td>
<td>{{ node.role || '-' }}</td>
<td>{{ node.hw_model || '-' }}</td>
<td>{{ node.latitude ?? '-' }}</td>
<td>{{ node.longitude ?? '-' }}</td>
</tr>
</tbody>
</table>
</article>
<article class="card">
<h2>最近聊天</h2>
<ul class="list">
<li v-for="msg in messages" :key="msg.id">
<strong>{{ msg.from_id }}</strong>
<span>{{ msg.text || '[binary]' }}</span>
<small>{{ msg.mqtt_remote_host || '-' }}</small>
</li>
</ul>
</article>
<article class="card">
<h2>最近位置</h2>
<ul class="list">
<li v-for="pos in positions" :key="pos.id">
<strong>{{ pos.from_id }}</strong>
<span>{{ pos.latitude ?? '-' }}, {{ pos.longitude ?? '-' }}</span>
<small>alt {{ pos.altitude ?? '-' }}</small>
</li>
</ul>
</article>
<section class="workspace">
<ChatPanel
:messages="messages"
:nodes-by-id="nodesById"
:selected-node-id="selectedNodeId"
@select-node="selectedNodeId = $event"
/>
<MeshMap
:nodes="mapNodes"
:selected-node-id="selectedNodeId"
@select-node="selectedNodeId = $event"
@clear-node="selectedNodeId = null"
/>
</section>
<NodeListPanel
:nodes="pagedNodes"
:selected-node-id="selectedNodeId"
:page="nodePage"
:page-size="nodePageSize"
:total="nodeTotal"
:loading="nodePageLoading || loading"
@select-node="selectedNodeId = $event"
@page-change="loadNodePage"
/>
</main>
</template>