From 01b0ad19999346be5e314d6e2a9afe16ada56871 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 19 Jun 2026 18:51:28 +0800 Subject: [PATCH] up --- meshmap_frontend/src/App.vue | 97 +++++++++++++++++-- .../src/components/NodeListPanel.vue | 14 ++- meshmap_frontend/src/style.css | 55 ++++++++++- 3 files changed, 153 insertions(+), 13 deletions(-) diff --git a/meshmap_frontend/src/App.vue b/meshmap_frontend/src/App.vue index 7acc098..cf4e751 100644 --- a/meshmap_frontend/src/App.vue +++ b/meshmap_frontend/src/App.vue @@ -67,6 +67,7 @@ const mapReportsLoading = ref(false) const mapReportTotal = ref(0) const mapSources = ref([fallbackMapSource]) const mapSource = ref(fallbackMapSource) +const nodeFilter = ref('') const pendingDeleteAction = ref(null) type DeletableTextMessage = TextMessage & { mergedCount?: number; mergedMessages?: TextMessage[] } type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: DeletableTextMessage } @@ -90,10 +91,74 @@ const nodesById = computed(() => { return Object.fromEntries(map) }) +const normalizedNodeFilter = computed(() => nodeFilter.value.trim().toLowerCase()) + +function nodeMatchesFilterByInfo(node: NodeInfo | null | undefined, keyword: string): boolean { + if (!keyword) { + return true + } + if (!node) { + return false + } + const fields = [node.node_id, node.long_name, node.short_name, node.hw_model, node.role] + return fields.some((value) => (value ?? '').toString().toLowerCase().includes(keyword)) +} + +function nodeIdMatchesFilter(nodeId: string | null | undefined): boolean { + const keyword = normalizedNodeFilter.value + if (!keyword) { + return true + } + if (!nodeId) { + return false + } + if (nodeId.toLowerCase().includes(keyword)) { + return true + } + return nodeMatchesFilterByInfo(nodesById.value[nodeId] ?? null, keyword) +} + +function mapReportMatchesFilter(item: MapViewportPoint, keyword: string): boolean { + if (!keyword) { + return true + } + const fields = [item.node_id, item.long_name, item.short_name, item.hw_model, item.role] + if (fields.some((value) => (value ?? '').toString().toLowerCase().includes(keyword))) { + return true + } + return nodeMatchesFilterByInfo(nodesById.value[item.node_id] ?? null, keyword) +} + +const filteredMessages = computed(() => { + if (!normalizedNodeFilter.value) { + return messages.value + } + return messages.value.filter((message) => nodeIdMatchesFilter(message.from_id)) +}) + +const filteredPagedNodeInfo = computed(() => { + const keyword = normalizedNodeFilter.value + if (!keyword) { + return pagedNodeInfo.value + } + // 过滤启用时,跨整个 nodeInfoSource(最多 500 条)匹配,分页交给前端忽略 + return nodeInfoSource.value.filter((node) => nodeMatchesFilterByInfo(node, keyword)) +}) + +const filteredNodeTotal = computed(() => { + if (!normalizedNodeFilter.value) { + return nodeTotal.value + } + return filteredPagedNodeInfo.value.length +}) + const mapItems = computed(() => { - const items = mapViewportItems.value + const keyword = normalizedNodeFilter.value + const items = keyword + ? mapViewportItems.value.filter((item) => isMapViewportPoint(item) && mapReportMatchesFilter(item, keyword)) + : mapViewportItems.value const selectedItem = selectedMapPoint.value - const renderItems = selectedItem && selectedItem.type === 'point' && !items.some((item) => item.type === 'point' && item.node_id === selectedItem.node_id) + const renderItems = selectedItem && selectedItem.type === 'point' && (!keyword || mapReportMatchesFilter(selectedItem, keyword)) && !items.some((item) => item.type === 'point' && item.node_id === selectedItem.node_id) ? [...items, selectedItem] : items @@ -578,13 +643,28 @@ onBeforeUnmount(() => { 管理 @@ -633,7 +713,7 @@ onBeforeUnmount(() => {
{
() const emit = defineEmits<{ @@ -133,10 +134,15 @@ onBeforeUnmount(() => { diff --git a/meshmap_frontend/src/style.css b/meshmap_frontend/src/style.css index 84a9ae6..6280c0f 100644 --- a/meshmap_frontend/src/style.css +++ b/meshmap_frontend/src/style.css @@ -90,7 +90,7 @@ a { .topbar-actions { display: flex; align-items: center; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: flex-end; gap: 10px; } @@ -208,6 +208,16 @@ h3 { font-weight: 700; } +.topbar-actions > .counter { + flex-shrink: 0; + white-space: nowrap; +} + +.topbar-actions > .topbar-link, +.topbar-actions > button { + flex-shrink: 0; +} + .status-pill.ok { color: color-mix(in srgb, var(--color-success) 68%, var(--color-heading)); background: var(--color-success-soft); @@ -280,6 +290,49 @@ h3 { background: var(--color-surface-soft); } +.topbar-filter { + display: flex; + align-items: center; + gap: 6px; + flex: 1 1 0; + min-width: 0; +} + +.topbar-filter-input { + flex: 1; + min-width: 0; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-sm); + padding: 8px 10px; + color: var(--color-heading); + background: var(--color-surface); + outline: none; + font-size: 13px; + transition: border-color 0.16s ease, box-shadow 0.16s ease; +} + +.topbar-filter-input:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent); +} + +.topbar-filter-clear { + flex-shrink: 0; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-sm); + padding: 7px 10px; + color: var(--color-heading); + background: var(--color-surface); + font-size: 12px; + font-weight: 700; +} + +.topbar-filter-clear:hover { + border-color: var(--color-primary); + color: var(--color-primary-hover); + background: var(--color-primary-soft); +} + .chat-item { display: grid; gap: 6px;