右键删除节点列表,好像实现了,还需要实现右键删除地图节点
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUpdate, onMounted, onUpdated, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onBeforeUpdate, onMounted, onUpdated, ref } from 'vue'
|
||||
import type { NodeInfoById, TextMessage } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -8,14 +8,19 @@ const props = defineProps<{
|
||||
selectedNodeId: string | null
|
||||
loadingOlder: boolean
|
||||
hasMoreMessages: boolean
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-node': [nodeId: string]
|
||||
'load-older': []
|
||||
'delete-message': [message: TextMessage]
|
||||
}>()
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const menuMessage = ref<TextMessage | null>(null)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
const topThreshold = 8
|
||||
const bottomThreshold = 40
|
||||
|
||||
@@ -44,7 +49,35 @@ function clearRestoreState() {
|
||||
restoreMessageCount = 0
|
||||
}
|
||||
|
||||
function closeMessageMenu() {
|
||||
menuMessage.value = null
|
||||
}
|
||||
|
||||
function openMessageMenu(message: TextMessage, event: MouseEvent) {
|
||||
if (!props.isAdmin) {
|
||||
return
|
||||
}
|
||||
emit('select-node', message.from_id)
|
||||
menuMessage.value = message
|
||||
menuX.value = event.clientX
|
||||
menuY.value = event.clientY
|
||||
}
|
||||
|
||||
function deleteSelectedMessage() {
|
||||
if (menuMessage.value) {
|
||||
emit('delete-message', menuMessage.value)
|
||||
}
|
||||
closeMessageMenu()
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
closeMessageMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
closeMessageMenu()
|
||||
const el = panelRef.value
|
||||
if (
|
||||
!el ||
|
||||
@@ -72,6 +105,8 @@ onBeforeUpdate(() => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('click', closeMessageMenu)
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
await nextTick()
|
||||
const el = panelRef.value
|
||||
if (el) {
|
||||
@@ -80,6 +115,11 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', closeMessageMenu)
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
const el = panelRef.value
|
||||
if (!el) {
|
||||
@@ -124,6 +164,7 @@ onUpdated(() => {
|
||||
:class="{ selected: selectedNodeId === message.from_id }"
|
||||
type="button"
|
||||
@click="emit('select-node', message.from_id)"
|
||||
@contextmenu.prevent.stop="openMessageMenu(message, $event)"
|
||||
>
|
||||
<span class="chat-meta">
|
||||
<strong>{{ senderName(message) }}</strong>
|
||||
@@ -132,5 +173,14 @@ onUpdated(() => {
|
||||
<span class="chat-topic">{{ message.topic }}</span>
|
||||
<span class="chat-text">{{ message.text || '[binary]' }}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="menuMessage"
|
||||
class="context-menu"
|
||||
:style="{ left: `${menuX}px`, top: `${menuY}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<button class="danger" type="button" @click="deleteSelectedMessage">删除</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -7,14 +7,19 @@ import type { MapNode } from '../types'
|
||||
const props = defineProps<{
|
||||
nodes: MapNode[]
|
||||
selectedNodeId: string | null
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-node': [nodeId: string]
|
||||
'clear-node': []
|
||||
'delete-node': [nodeId: string]
|
||||
}>()
|
||||
|
||||
const mapEl = ref<HTMLElement | null>(null)
|
||||
const menuNodeId = ref<string | null>(null)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
let map: L.Map | null = null
|
||||
let markerLayer: L.LayerGroup | null = null
|
||||
let hasFitBounds = false
|
||||
@@ -37,7 +42,10 @@ onMounted(async () => {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
}).addTo(map)
|
||||
map.on('click', () => emit('clear-node'))
|
||||
map.on('click', () => {
|
||||
closeNodeMenu()
|
||||
emit('clear-node')
|
||||
})
|
||||
markerLayer = L.layerGroup().addTo(map)
|
||||
renderMarkers(true)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import type { NodeInfo } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -9,20 +9,62 @@ const props = defineProps<{
|
||||
pageSize: number
|
||||
total: number
|
||||
loading: boolean
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-node': [nodeId: string]
|
||||
'page-change': [page: number]
|
||||
'delete-node': [nodeId: string]
|
||||
}>()
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
||||
const canPrev = computed(() => props.page > 1)
|
||||
const canNext = computed(() => props.page < totalPages.value)
|
||||
const menuNode = ref<NodeInfo | null>(null)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
|
||||
function formatTime(value: string): string {
|
||||
return new Date(value).toLocaleString()
|
||||
}
|
||||
|
||||
function closeNodeMenu() {
|
||||
menuNode.value = null
|
||||
}
|
||||
|
||||
function openNodeMenu(node: NodeInfo, event: MouseEvent) {
|
||||
if (!props.isAdmin) {
|
||||
return
|
||||
}
|
||||
emit('select-node', node.node_id)
|
||||
menuNode.value = node
|
||||
menuX.value = event.clientX
|
||||
menuY.value = event.clientY
|
||||
}
|
||||
|
||||
function deleteSelectedNode() {
|
||||
if (menuNode.value) {
|
||||
emit('delete-node', menuNode.value.node_id)
|
||||
}
|
||||
closeNodeMenu()
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
closeNodeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeNodeMenu)
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', closeNodeMenu)
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,7 +77,7 @@ function formatTime(value: string): string {
|
||||
<span class="badge">共 {{ total }} 条</span>
|
||||
</div>
|
||||
|
||||
<div class="node-table-wrap">
|
||||
<div class="node-table-wrap" @scroll="closeNodeMenu">
|
||||
<table class="node-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -55,6 +97,7 @@ function formatTime(value: string): string {
|
||||
class="node-row"
|
||||
:class="{ selected: selectedNodeId === node.node_id }"
|
||||
@click="emit('select-node', node.node_id)"
|
||||
@contextmenu.prevent.stop="openNodeMenu(node, $event)"
|
||||
>
|
||||
<td>{{ node.node_id }}</td>
|
||||
<td>{{ node.long_name || '-' }}</td>
|
||||
@@ -69,6 +112,15 @@ function formatTime(value: string): string {
|
||||
<div v-if="nodes.length === 0" class="empty">暂无节点数据</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="menuNode"
|
||||
class="context-menu"
|
||||
:style="{ left: `${menuX}px`, top: `${menuY}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<button class="danger" type="button" @click="deleteSelectedNode">删除</button>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button :disabled="loading || !canPrev" @click="emit('page-change', page - 1)">上一页</button>
|
||||
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
||||
|
||||
Reference in New Issue
Block a user