右键删除节点列表,好像实现了,还需要实现右键删除地图节点
This commit is contained in:
@@ -131,6 +131,7 @@ GET /api/admin/log/login
|
|||||||
GET /api/admin/users
|
GET /api/admin/users
|
||||||
POST /api/admin/users
|
POST /api/admin/users
|
||||||
PUT /api/admin/users/:id/password
|
PUT /api/admin/users/:id/password
|
||||||
|
DELETE /api/admin/text-messages/:id
|
||||||
GET /api/nodeinfo
|
GET /api/nodeinfo
|
||||||
GET /api/nodeinfo/:id
|
GET /api/nodeinfo/:id
|
||||||
GET /api/map-reports
|
GET /api/map-reports
|
||||||
|
|||||||
+26
@@ -361,6 +361,32 @@ func TestInsertTextMessageAppendsRows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteTextMessageDeletesRows(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
if err := st.InsertTextMessage(textMessageTestRecord("hello"), mqttClientInfo{}); err != nil {
|
||||||
|
t.Fatalf("InsertTextMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
var id uint64
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT id FROM text_message LIMIT 1").Scan(&id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := st.DeleteTextMessage(id); err != nil {
|
||||||
|
t.Fatalf("DeleteTextMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM text_message WHERE id = ?", id).Scan(&count); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatalf("text_message count = %d, want 0", count)
|
||||||
|
}
|
||||||
|
if err := st.DeleteTextMessage(id); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
t.Fatalf("DeleteTextMessage(missing) error = %v, want record not found", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInsertTextMessageStoresClientInfo(t *testing.T) {
|
func TestInsertTextMessageStoresClientInfo(t *testing.T) {
|
||||||
st := openTestStore(t)
|
st := openTestStore(t)
|
||||||
defer st.Close()
|
defer st.Close()
|
||||||
|
|||||||
@@ -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, getAdminMe, getHealth, getMapReports, getNodeInfo, getPositions, getTextMessages } from './api'
|
import { adminLogout, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReports, getNodeInfo, getPositions, getTextMessages } from './api'
|
||||||
import AdminDashboard from './components/AdminDashboard.vue'
|
import AdminDashboard from './components/AdminDashboard.vue'
|
||||||
import AdminLogin from './components/AdminLogin.vue'
|
import AdminLogin from './components/AdminLogin.vue'
|
||||||
import AdminLoginLogs from './components/AdminLoginLogs.vue'
|
import AdminLoginLogs from './components/AdminLoginLogs.vue'
|
||||||
@@ -182,11 +182,36 @@ async function logoutAdmin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(message: TextMessage) {
|
||||||
|
try {
|
||||||
|
await deleteTextMessage(message.id)
|
||||||
|
messages.value = messages.value.filter((item) => item.id !== message.id)
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteNodeById(nodeId: string) {
|
||||||
|
try {
|
||||||
|
await deleteNode(nodeId)
|
||||||
|
nodeInfoSource.value = nodeInfoSource.value.filter((node) => node.node_id !== nodeId)
|
||||||
|
pagedNodeInfo.value = pagedNodeInfo.value.filter((node) => node.node_id !== nodeId)
|
||||||
|
mapReportSource.value = mapReportSource.value.filter((report) => report.node_id !== nodeId)
|
||||||
|
if (selectedNodeId.value === nodeId) {
|
||||||
|
selectedNodeId.value = null
|
||||||
|
}
|
||||||
|
await loadNodePage(nodePage.value, false)
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isAdminPage) {
|
if (isAdminPage) {
|
||||||
checkAdminSession()
|
checkAdminSession()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
checkAdminSession()
|
||||||
refresh()
|
refresh()
|
||||||
refreshTimer = window.setInterval(() => refresh(false), 5000)
|
refreshTimer = window.setInterval(() => refresh(false), 5000)
|
||||||
})
|
})
|
||||||
@@ -252,14 +277,18 @@ onBeforeUnmount(() => {
|
|||||||
:selected-node-id="selectedNodeId"
|
:selected-node-id="selectedNodeId"
|
||||||
:loading-older="chatLoadingOlder"
|
:loading-older="chatLoadingOlder"
|
||||||
:has-more-messages="chatHasMore"
|
:has-more-messages="chatHasMore"
|
||||||
|
:is-admin="!!adminUser"
|
||||||
@select-node="selectedNodeId = $event"
|
@select-node="selectedNodeId = $event"
|
||||||
@load-older="loadOlderMessages"
|
@load-older="loadOlderMessages"
|
||||||
|
@delete-message="deleteMessage"
|
||||||
/>
|
/>
|
||||||
<MeshMap
|
<MeshMap
|
||||||
:nodes="mapNodes"
|
:nodes="mapNodes"
|
||||||
:selected-node-id="selectedNodeId"
|
:selected-node-id="selectedNodeId"
|
||||||
|
:is-admin="!!adminUser"
|
||||||
@select-node="selectedNodeId = $event"
|
@select-node="selectedNodeId = $event"
|
||||||
@clear-node="selectedNodeId = null"
|
@clear-node="selectedNodeId = null"
|
||||||
|
@delete-node="deleteNodeById"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -270,8 +299,10 @@ onBeforeUnmount(() => {
|
|||||||
:page-size="nodePageSize"
|
:page-size="nodePageSize"
|
||||||
:total="nodeTotal"
|
:total="nodeTotal"
|
||||||
:loading="nodePageLoading || loading"
|
:loading="nodePageLoading || loading"
|
||||||
|
:is-admin="!!adminUser"
|
||||||
@select-node="selectedNodeId = $event"
|
@select-node="selectedNodeId = $event"
|
||||||
@page-change="loadNodePage"
|
@page-change="loadNodePage"
|
||||||
|
@delete-node="deleteNodeById"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ function putJSON<T>(path: string, body?: unknown): Promise<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteJSON<T>(path: string): Promise<T> {
|
||||||
|
return requestJSON<T>(path, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
export function getHealth(): Promise<HealthStatus> {
|
export function getHealth(): Promise<HealthStatus> {
|
||||||
return getJSON<HealthStatus>('/api/health')
|
return getJSON<HealthStatus>('/api/health')
|
||||||
}
|
}
|
||||||
@@ -65,6 +69,14 @@ export function getTextMessages(limit = 100, offset = 0): Promise<ListResponse<T
|
|||||||
return getJSON<ListResponse<TextMessage>>(`/api/text-messages?limit=${limit}&offset=${offset}`)
|
return getJSON<ListResponse<TextMessage>>(`/api/text-messages?limit=${limit}&offset=${offset}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteTextMessage(id: number): Promise<{ status: string }> {
|
||||||
|
return deleteJSON<{ status: string }>(`/api/admin/text-messages/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteNode(nodeId: string): Promise<{ status: string }> {
|
||||||
|
return deleteJSON<{ status: string }>(`/api/admin/nodes/${encodeURIComponent(nodeId)}`)
|
||||||
|
}
|
||||||
|
|
||||||
export function getPositions(limit = 500): Promise<ListResponse<PositionRecord>> {
|
export function getPositions(limit = 500): Promise<ListResponse<PositionRecord>> {
|
||||||
return getJSON<ListResponse<PositionRecord>>(`/api/positions?limit=${limit}`)
|
return getJSON<ListResponse<PositionRecord>>(`/api/positions?limit=${limit}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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'
|
import type { NodeInfoById, TextMessage } from '../types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -8,14 +8,19 @@ const props = defineProps<{
|
|||||||
selectedNodeId: string | null
|
selectedNodeId: string | null
|
||||||
loadingOlder: boolean
|
loadingOlder: boolean
|
||||||
hasMoreMessages: boolean
|
hasMoreMessages: boolean
|
||||||
|
isAdmin: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'select-node': [nodeId: string]
|
'select-node': [nodeId: string]
|
||||||
'load-older': []
|
'load-older': []
|
||||||
|
'delete-message': [message: TextMessage]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const panelRef = ref<HTMLElement | null>(null)
|
const panelRef = ref<HTMLElement | null>(null)
|
||||||
|
const menuMessage = ref<TextMessage | null>(null)
|
||||||
|
const menuX = ref(0)
|
||||||
|
const menuY = ref(0)
|
||||||
const topThreshold = 8
|
const topThreshold = 8
|
||||||
const bottomThreshold = 40
|
const bottomThreshold = 40
|
||||||
|
|
||||||
@@ -44,7 +49,35 @@ function clearRestoreState() {
|
|||||||
restoreMessageCount = 0
|
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() {
|
function handleScroll() {
|
||||||
|
closeMessageMenu()
|
||||||
const el = panelRef.value
|
const el = panelRef.value
|
||||||
if (
|
if (
|
||||||
!el ||
|
!el ||
|
||||||
@@ -72,6 +105,8 @@ onBeforeUpdate(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
window.addEventListener('click', closeMessageMenu)
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const el = panelRef.value
|
const el = panelRef.value
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -80,6 +115,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('click', closeMessageMenu)
|
||||||
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
const el = panelRef.value
|
const el = panelRef.value
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -124,6 +164,7 @@ onUpdated(() => {
|
|||||||
:class="{ selected: selectedNodeId === message.from_id }"
|
:class="{ selected: selectedNodeId === message.from_id }"
|
||||||
type="button"
|
type="button"
|
||||||
@click="emit('select-node', message.from_id)"
|
@click="emit('select-node', message.from_id)"
|
||||||
|
@contextmenu.prevent.stop="openMessageMenu(message, $event)"
|
||||||
>
|
>
|
||||||
<span class="chat-meta">
|
<span class="chat-meta">
|
||||||
<strong>{{ senderName(message) }}</strong>
|
<strong>{{ senderName(message) }}</strong>
|
||||||
@@ -132,5 +173,14 @@ onUpdated(() => {
|
|||||||
<span class="chat-topic">{{ message.topic }}</span>
|
<span class="chat-topic">{{ message.topic }}</span>
|
||||||
<span class="chat-text">{{ message.text || '[binary]' }}</span>
|
<span class="chat-text">{{ message.text || '[binary]' }}</span>
|
||||||
</button>
|
</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>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ import type { MapNode } from '../types'
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nodes: MapNode[]
|
nodes: MapNode[]
|
||||||
selectedNodeId: string | null
|
selectedNodeId: string | null
|
||||||
|
isAdmin: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'select-node': [nodeId: string]
|
'select-node': [nodeId: string]
|
||||||
'clear-node': []
|
'clear-node': []
|
||||||
|
'delete-node': [nodeId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mapEl = ref<HTMLElement | null>(null)
|
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 map: L.Map | null = null
|
||||||
let markerLayer: L.LayerGroup | null = null
|
let markerLayer: L.LayerGroup | null = null
|
||||||
let hasFitBounds = false
|
let hasFitBounds = false
|
||||||
@@ -37,7 +42,10 @@ onMounted(async () => {
|
|||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: '© OpenStreetMap contributors',
|
attribution: '© OpenStreetMap contributors',
|
||||||
}).addTo(map)
|
}).addTo(map)
|
||||||
map.on('click', () => emit('clear-node'))
|
map.on('click', () => {
|
||||||
|
closeNodeMenu()
|
||||||
|
emit('clear-node')
|
||||||
|
})
|
||||||
markerLayer = L.layerGroup().addTo(map)
|
markerLayer = L.layerGroup().addTo(map)
|
||||||
renderMarkers(true)
|
renderMarkers(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import type { NodeInfo } from '../types'
|
import type { NodeInfo } from '../types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -9,20 +9,62 @@ const props = defineProps<{
|
|||||||
pageSize: number
|
pageSize: number
|
||||||
total: number
|
total: number
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
isAdmin: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'select-node': [nodeId: string]
|
'select-node': [nodeId: string]
|
||||||
'page-change': [page: number]
|
'page-change': [page: number]
|
||||||
|
'delete-node': [nodeId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)))
|
||||||
const canPrev = computed(() => props.page > 1)
|
const canPrev = computed(() => props.page > 1)
|
||||||
const canNext = computed(() => props.page < totalPages.value)
|
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 {
|
function formatTime(value: string): string {
|
||||||
return new Date(value).toLocaleString()
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,7 +77,7 @@ function formatTime(value: string): string {
|
|||||||
<span class="badge">共 {{ total }} 条</span>
|
<span class="badge">共 {{ total }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap" @scroll="closeNodeMenu">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -55,6 +97,7 @@ function formatTime(value: string): string {
|
|||||||
class="node-row"
|
class="node-row"
|
||||||
:class="{ selected: selectedNodeId === node.node_id }"
|
:class="{ selected: selectedNodeId === node.node_id }"
|
||||||
@click="emit('select-node', node.node_id)"
|
@click="emit('select-node', node.node_id)"
|
||||||
|
@contextmenu.prevent.stop="openNodeMenu(node, $event)"
|
||||||
>
|
>
|
||||||
<td>{{ node.node_id }}</td>
|
<td>{{ node.node_id }}</td>
|
||||||
<td>{{ node.long_name || '-' }}</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 v-if="nodes.length === 0" class="empty">暂无节点数据</div>
|
||||||
</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">
|
<div class="pagination">
|
||||||
<button :disabled="loading || !canPrev" @click="emit('page-change', page - 1)">上一页</button>
|
<button :disabled="loading || !canPrev" @click="emit('page-change', page - 1)">上一页</button>
|
||||||
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
||||||
|
|||||||
@@ -220,6 +220,35 @@ h3 {
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
min-width: 120px;
|
||||||
|
border: 1px solid #dbe4ef;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu button {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu button:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu button.danger {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.map-panel {
|
.map-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,23 @@ func (s *store) GetMapReport(nodeID string) (*mapReportRecord, error) {
|
|||||||
return &row, nil
|
return &row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) DeleteNode(nodeID string) error {
|
||||||
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
nodeResult := tx.Where("node_id = ?", nodeID).Delete(&nodeInfoRecord{})
|
||||||
|
if nodeResult.Error != nil {
|
||||||
|
return nodeResult.Error
|
||||||
|
}
|
||||||
|
reportResult := tx.Where("node_id = ?", nodeID).Delete(&mapReportRecord{})
|
||||||
|
if reportResult.Error != nil {
|
||||||
|
return reportResult.Error
|
||||||
|
}
|
||||||
|
if nodeResult.RowsAffected+reportResult.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func applyNodeFilters(q *gorm.DB, opts listOptions) *gorm.DB {
|
func applyNodeFilters(q *gorm.DB, opts listOptions) *gorm.DB {
|
||||||
if opts.NodeID != "" {
|
if opts.NodeID != "" {
|
||||||
q = q.Where("node_id = ?", opts.NodeID)
|
q = q.Where("node_id = ?", opts.NodeID)
|
||||||
@@ -101,6 +118,17 @@ func (s *store) ListTextMessages(opts listOptions) ([]textMessageRecord, error)
|
|||||||
return rows, s.listAppendRows(opts, &rows).Error
|
return rows, s.listAppendRows(opts, &rows).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) DeleteTextMessage(id uint64) error {
|
||||||
|
result := s.db.Where("id = ?", id).Delete(&textMessageRecord{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) ListPositions(opts listOptions) ([]positionRecord, error) {
|
func (s *store) ListPositions(opts listOptions) ([]positionRecord, error) {
|
||||||
var rows []positionRecord
|
var rows []positionRecord
|
||||||
return rows, s.listAppendRows(opts, &rows).Error
|
return rows, s.listAppendRows(opts, &rows).Error
|
||||||
|
|||||||
@@ -218,6 +218,36 @@ func registerAdminRoutes(r gin.IRouter, store *store, sessions *sessionManager,
|
|||||||
rows, err := store.ListLoginLogs(opts)
|
rows, err := store.ListLoginLogs(opts)
|
||||||
writeListResponse(c, rows, opts, err, loginLogDTO)
|
writeListResponse(c, rows, opts, err, loginLogDTO)
|
||||||
})
|
})
|
||||||
|
protected.DELETE("/text-messages/:id", func(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := store.DeleteTextMessage(id); errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
|
protected.DELETE("/nodes/:id", func(c *gin.Context) {
|
||||||
|
nodeID := c.Param("id")
|
||||||
|
if nodeID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := store.DeleteNode(nodeID); errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "node not found"})
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerNodeInfoRoutes(r gin.IRouter, store *store, path string) {
|
func registerNodeInfoRoutes(r gin.IRouter, store *store, path string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user