功能基本完成

This commit is contained in:
2026-06-04 15:54:45 +08:00
parent 2e6eab3e01
commit 69e06a19e1
5 changed files with 164 additions and 15 deletions
+63 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { adminLogout, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages } from './api' import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages } from './api'
import AdminBlockingManagement from './components/AdminBlockingManagement.vue' import AdminBlockingManagement from './components/AdminBlockingManagement.vue'
import AdminDashboard from './components/AdminDashboard.vue' import AdminDashboard from './components/AdminDashboard.vue'
import AdminDiscardDetails from './components/AdminDiscardDetails.vue' import AdminDiscardDetails from './components/AdminDiscardDetails.vue'
@@ -46,6 +46,7 @@ const currentMapBounds = ref<MapBoundsQuery | null>(null)
const currentMapZoom = ref(2) const currentMapZoom = ref(2)
const mapReportsLoading = ref(false) const mapReportsLoading = ref(false)
const mapReportTotal = ref(0) const mapReportTotal = ref(0)
type NodeActionPayload = { nodeId: string; nodeNum: number | null; message?: TextMessage }
let refreshTimer: number | undefined let refreshTimer: number | undefined
let mapBoundsTimer: number | undefined let mapBoundsTimer: number | undefined
let mapReportRequestSeq = 0 let mapReportRequestSeq = 0
@@ -251,9 +252,7 @@ async function deleteMessage(message: TextMessage) {
} }
} }
async function deleteNodeById(nodeId: string) { async function removeNodeFromLocalState(nodeId: string) {
try {
await deleteNode(nodeId)
nodeInfoSource.value = nodeInfoSource.value.filter((node) => node.node_id !== nodeId) nodeInfoSource.value = nodeInfoSource.value.filter((node) => node.node_id !== nodeId)
pagedNodeInfo.value = pagedNodeInfo.value.filter((node) => node.node_id !== nodeId) pagedNodeInfo.value = pagedNodeInfo.value.filter((node) => node.node_id !== nodeId)
mapViewportItems.value = mapViewportItems.value.filter((item) => item.type === 'cluster' || item.node_id !== nodeId) mapViewportItems.value = mapViewportItems.value.filter((item) => item.type === 'cluster' || item.node_id !== nodeId)
@@ -261,6 +260,63 @@ async function deleteNodeById(nodeId: string) {
selectedNodeId.value = null selectedNodeId.value = null
} }
await loadNodePage(nodePage.value, false) await loadNodePage(nodePage.value, false)
}
function isAlreadyBlockedError(err: unknown): boolean {
return err instanceof Error && err.message === 'blocking rule already exists'
}
function isNodeNotFoundError(err: unknown): boolean {
return err instanceof Error && err.message === 'node not found'
}
function isMessageNotFoundError(err: unknown): boolean {
return err instanceof Error && err.message === 'message not found'
}
async function deleteNodeById(nodeId: string) {
try {
await deleteNode(nodeId)
await removeNodeFromLocalState(nodeId)
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
async function deleteAndBlockNode(payload: NodeActionPayload) {
try {
if (payload.message) {
try {
await deleteTextMessage(payload.message.id)
} catch (err) {
if (!isMessageNotFoundError(err)) {
throw err
}
}
messages.value = messages.value.filter((item) => item.id !== payload.message?.id)
}
try {
await createNodeBlockingRule({
node_id: payload.nodeId,
node_num: payload.nodeNum,
reason: '管理员右键删除并屏蔽节点',
enabled: true,
})
} catch (err) {
if (!isAlreadyBlockedError(err)) {
throw err
}
}
try {
await deleteNode(payload.nodeId)
} catch (err) {
if (!isNodeNotFoundError(err)) {
throw err
}
}
await removeNodeFromLocalState(payload.nodeId)
} catch (err) { } catch (err) {
error.value = err instanceof Error ? err.message : String(err) error.value = err instanceof Error ? err.message : String(err)
} }
@@ -368,6 +424,7 @@ onBeforeUnmount(() => {
@select-node="selectedNodeId = $event" @select-node="selectedNodeId = $event"
@load-older="loadOlderMessages" @load-older="loadOlderMessages"
@delete-message="deleteMessage" @delete-message="deleteMessage"
@delete-and-block-node="deleteAndBlockNode"
/> />
<MeshMap <MeshMap
:items="mapItems" :items="mapItems"
@@ -379,6 +436,7 @@ onBeforeUnmount(() => {
@select-node="selectedNodeId = $event" @select-node="selectedNodeId = $event"
@clear-node="selectedNodeId = null" @clear-node="selectedNodeId = null"
@delete-node="deleteNodeById" @delete-node="deleteNodeById"
@delete-and-block-node="deleteAndBlockNode"
/> />
</section> </section>
@@ -393,6 +451,7 @@ onBeforeUnmount(() => {
@select-node="selectedNodeId = $event" @select-node="selectedNodeId = $event"
@page-change="loadNodePage" @page-change="loadNodePage"
@delete-node="deleteNodeById" @delete-node="deleteNodeById"
@delete-and-block-node="deleteAndBlockNode"
/> />
</template> </template>
</main> </main>
@@ -15,6 +15,7 @@ const emit = defineEmits<{
'select-node': [nodeId: string] 'select-node': [nodeId: string]
'load-older': [] 'load-older': []
'delete-message': [message: TextMessage] 'delete-message': [message: TextMessage]
'delete-and-block-node': [payload: { nodeId: string; nodeNum: number | null; message: TextMessage }]
}>() }>()
const panelRef = ref<HTMLElement | null>(null) const panelRef = ref<HTMLElement | null>(null)
@@ -71,6 +72,13 @@ function deleteSelectedMessage() {
closeMessageMenu() closeMessageMenu()
} }
function deleteAndBlockSelectedNode() {
if (menuMessage.value) {
emit('delete-and-block-node', { nodeId: menuMessage.value.from_id, nodeNum: menuMessage.value.from_num ?? null, message: menuMessage.value })
}
closeMessageMenu()
}
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeMessageMenu() closeMessageMenu()
@@ -183,6 +191,7 @@ onUpdated(() => {
> >
<a :href="nodeDetailHref(menuMessage.from_id)">节点详细</a> <a :href="nodeDetailHref(menuMessage.from_id)">节点详细</a>
<button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedMessage">删除</button> <button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedMessage">删除</button>
<button v-if="isAdmin" class="danger" type="button" @click="deleteAndBlockSelectedNode">删除并屏蔽节点</button>
</div> </div>
</aside> </aside>
</template> </template>
+19 -7
View File
@@ -19,11 +19,12 @@ const emit = defineEmits<{
'select-node': [nodeId: string] 'select-node': [nodeId: string]
'clear-node': [] 'clear-node': []
'delete-node': [nodeId: string] 'delete-node': [nodeId: string]
'delete-and-block-node': [payload: { nodeId: string; nodeNum: number | null }]
'bounds-change': [payload: MapBoundsChangePayload] 'bounds-change': [payload: MapBoundsChangePayload]
}>() }>()
const mapEl = ref<HTMLElement | null>(null) const mapEl = ref<HTMLElement | null>(null)
const menuNodeId = ref<string | null>(null) const menuNode = ref<MapNode | null>(null)
const menuX = ref(0) const menuX = ref(0)
const menuY = ref(0) const menuY = ref(0)
let map: L.Map | null = null let map: L.Map | null = null
@@ -82,7 +83,7 @@ watch(
) )
function closeNodeMenu() { function closeNodeMenu() {
menuNodeId.value = null menuNode.value = null
} }
function nodeDetailHref(nodeId: string): string { function nodeDetailHref(nodeId: string): string {
@@ -92,14 +93,24 @@ function nodeDetailHref(nodeId: string): string {
function openNodeMenu(node: MapNode, event: L.LeafletMouseEvent) { function openNodeMenu(node: MapNode, event: L.LeafletMouseEvent) {
L.DomEvent.stopPropagation(event) L.DomEvent.stopPropagation(event)
emit('select-node', node.node_id) emit('select-node', node.node_id)
menuNodeId.value = node.node_id menuNode.value = node
menuX.value = event.originalEvent.clientX menuX.value = event.originalEvent.clientX
menuY.value = event.originalEvent.clientY menuY.value = event.originalEvent.clientY
} }
function deleteSelectedNode() { function deleteSelectedNode() {
if (menuNodeId.value) { if (menuNode.value) {
emit('delete-node', menuNodeId.value) emit('delete-node', menuNode.value.node_id)
}
closeNodeMenu()
}
function deleteAndBlockSelectedNode() {
if (menuNode.value) {
emit('delete-and-block-node', {
nodeId: menuNode.value.node_id,
nodeNum: menuNode.value.map_report?.node_num ?? menuNode.value.nodeinfo?.node_num ?? null,
})
} }
closeNodeMenu() closeNodeMenu()
} }
@@ -310,13 +321,14 @@ function escapeHTML(value: string): string {
<div v-if="loading" class="map-empty">正在加载当前区域坐标...</div> <div v-if="loading" class="map-empty">正在加载当前区域坐标...</div>
<div v-else-if="items.length === 0" class="map-empty">暂无可显示坐标的节点</div> <div v-else-if="items.length === 0" class="map-empty">暂无可显示坐标的节点</div>
<div <div
v-if="menuNodeId" v-if="menuNode"
class="context-menu" class="context-menu"
:style="{ left: `${menuX}px`, top: `${menuY}px` }" :style="{ left: `${menuX}px`, top: `${menuY}px` }"
@click.stop @click.stop
> >
<a :href="nodeDetailHref(menuNodeId)">节点详细</a> <a :href="nodeDetailHref(menuNode.node_id)">节点详细</a>
<button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedNode">删除</button> <button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedNode">删除</button>
<button v-if="isAdmin" class="danger" type="button" @click="deleteAndBlockSelectedNode">删除并屏蔽节点</button>
</div> </div>
</section> </section>
</template> </template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { deleteTextMessage, getMapReportById, getNodeInfoById, getPositions, getTelemetry, getTextMessages } from '../api' import { createNodeBlockingRule, deleteNode, deleteTextMessage, getMapReportById, getNodeInfoById, getPositions, getTelemetry, getTextMessages } from '../api'
import type { MapReport, NodeInfo, PositionRecord, TelemetryRecord, TextMessage } from '../types' import type { MapReport, NodeInfo, PositionRecord, TelemetryRecord, TextMessage } from '../types'
import NodeTrajectoryMap from './NodeTrajectoryMap.vue' import NodeTrajectoryMap from './NodeTrajectoryMap.vue'
@@ -178,6 +178,65 @@ async function deleteSelectedMessage() {
} }
} }
function isAlreadyBlockedError(err: unknown): boolean {
return err instanceof Error && err.message === 'blocking rule already exists'
}
function isNodeNotFoundError(err: unknown): boolean {
return err instanceof Error && err.message === 'node not found'
}
function isMessageNotFoundError(err: unknown): boolean {
return err instanceof Error && err.message === 'message not found'
}
async function deleteAndBlockSelectedMessageNode() {
if (!menuMessage.value) {
return
}
const message = menuMessage.value
const nodeId = message.from_id || props.nodeId
const nodeNum = message.from_num ?? mergedNode.value.node_num ?? null
closeMessageMenu()
try {
try {
await deleteTextMessage(message.id)
} catch (err) {
if (!isMessageNotFoundError(err)) {
throw err
}
}
messages.value = messages.value.filter((item) => item.id !== message.id)
try {
await createNodeBlockingRule({
node_id: nodeId,
node_num: nodeNum,
reason: '管理员右键删除并屏蔽节点',
enabled: true,
})
} catch (err) {
if (!isAlreadyBlockedError(err)) {
throw err
}
}
try {
await deleteNode(nodeId)
} catch (err) {
if (!isNodeNotFoundError(err)) {
throw err
}
}
if (nodeId === props.nodeId) {
nodeInfo.value = null
mapReport.value = null
}
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeMessageMenu() closeMessageMenu()
@@ -303,6 +362,7 @@ onBeforeUnmount(() => {
@click.stop @click.stop
> >
<button class="danger" type="button" @click="deleteSelectedMessage">删除</button> <button class="danger" type="button" @click="deleteSelectedMessage">删除</button>
<button class="danger" type="button" @click="deleteAndBlockSelectedMessageNode">删除并屏蔽节点</button>
</div> </div>
</div> </div>
@@ -16,6 +16,7 @@ const emit = defineEmits<{
'select-node': [nodeId: string] 'select-node': [nodeId: string]
'page-change': [page: number] 'page-change': [page: number]
'delete-node': [nodeId: string] 'delete-node': [nodeId: string]
'delete-and-block-node': [payload: { nodeId: string; nodeNum: number | null }]
}>() }>()
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize))) const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
@@ -51,6 +52,13 @@ function deleteSelectedNode() {
closeNodeMenu() closeNodeMenu()
} }
function deleteAndBlockSelectedNode() {
if (menuNode.value) {
emit('delete-and-block-node', { nodeId: menuNode.value.node_id, nodeNum: menuNode.value.node_num ?? null })
}
closeNodeMenu()
}
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
closeNodeMenu() closeNodeMenu()
@@ -121,6 +129,7 @@ onBeforeUnmount(() => {
> >
<a :href="nodeDetailHref(menuNode.node_id)">节点详细</a> <a :href="nodeDetailHref(menuNode.node_id)">节点详细</a>
<button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedNode">删除</button> <button v-if="isAdmin" class="danger" type="button" @click="deleteSelectedNode">删除</button>
<button v-if="isAdmin" class="danger" type="button" @click="deleteAndBlockSelectedNode">删除并屏蔽节点</button>
</div> </div>
<div class="pagination"> <div class="pagination">