功能基本完成
This commit is contained in:
@@ -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,16 +252,71 @@ async function deleteMessage(message: TextMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeNodeFromLocalState(nodeId: string) {
|
||||||
|
nodeInfoSource.value = nodeInfoSource.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)
|
||||||
|
if (selectedNodeId.value === nodeId) {
|
||||||
|
selectedNodeId.value = null
|
||||||
|
}
|
||||||
|
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) {
|
async function deleteNodeById(nodeId: string) {
|
||||||
try {
|
try {
|
||||||
await deleteNode(nodeId)
|
await deleteNode(nodeId)
|
||||||
nodeInfoSource.value = nodeInfoSource.value.filter((node) => node.node_id !== nodeId)
|
await removeNodeFromLocalState(nodeId)
|
||||||
pagedNodeInfo.value = pagedNodeInfo.value.filter((node) => node.node_id !== nodeId)
|
} catch (err) {
|
||||||
mapViewportItems.value = mapViewportItems.value.filter((item) => item.type === 'cluster' || item.node_id !== nodeId)
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
if (selectedNodeId.value === nodeId) {
|
}
|
||||||
selectedNodeId.value = null
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
await loadNodePage(nodePage.value, false)
|
|
||||||
|
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,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">
|
||||||
|
|||||||
Reference in New Issue
Block a user