丢弃数据页批量管理:多选删除+一键清空(ConfirmDeleteModal 二次确认),后端 v1.2.0 / 前端 v0.3.0

This commit is contained in:
2026-08-07 21:36:16 +08:00
parent 62b6ff44d1
commit b6fabf59b7
5 changed files with 164 additions and 5 deletions
+13
View File
@@ -339,6 +339,19 @@ func applyDiscardDetailsFilters(q *gorm.DB, opts ListOptions) *gorm.DB {
return q return q
} }
func (s *Store) DeleteDiscardDetailsByIDs(ids []uint64) (int64, error) {
if len(ids) == 0 {
return 0, nil
}
result := s.db.Where("id IN ?", ids).Delete(&DiscardDetailsRecord{})
return result.RowsAffected, result.Error
}
func (s *Store) DeleteAllDiscardDetails() (int64, error) {
result := s.db.Where("1 = 1").Delete(&DiscardDetailsRecord{})
return result.RowsAffected, result.Error
}
func (s *Store) DeleteTextMessage(id uint64) error { func (s *Store) DeleteTextMessage(id uint64) error {
result := s.db.Where("id = ?", id).Delete(&TextMessageRecord{}) result := s.db.Where("id = ?", id).Delete(&TextMessageRecord{})
if result.Error != nil { if result.Error != nil {
+28 -1
View File
@@ -84,7 +84,7 @@ func NewRouter(cfg configpkg.WebConfig, consoleLog bool, store *storepkg.Store,
return r return r
} }
const BackendVersion = "1.1.0" const BackendVersion = "1.2.0"
var CommitVersion = "dev" var CommitVersion = "dev"
@@ -399,6 +399,33 @@ func registerAdminRoutes(r gin.IRouter, store *storepkg.Store, sessions *auth.Ma
rows, err := store.ListLoginLogs(opts) rows, err := store.ListLoginLogs(opts)
writeListResponse(c, rows, opts, err, loginLogDTO) writeListResponse(c, rows, opts, err, loginLogDTO)
}) })
protected.POST("/discard-details/batch-delete", func(c *gin.Context) {
var req struct {
IDs []uint64 `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ids is empty"})
return
}
count, err := store.DeleteDiscardDetailsByIDs(req.IDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "deleted_count": count})
})
protected.DELETE("/discard-details", func(c *gin.Context) {
count, err := store.DeleteAllDiscardDetails()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "deleted_count": count})
})
protected.DELETE("/text-messages/:id", func(c *gin.Context) { protected.DELETE("/text-messages/:id", func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
+8
View File
@@ -235,6 +235,14 @@ export function getDiscardDetails(limit = 100, offset = 0): Promise<ListResponse
return getJSON<ListResponse<DiscardDetails>>(listPath('/api/discard-details', limit, offset)) return getJSON<ListResponse<DiscardDetails>>(listPath('/api/discard-details', limit, offset))
} }
export function deleteDiscardDetailsByIDs(ids: number[]): Promise<{ status: string; deleted_count: number }> {
return postJSON<{ status: string; deleted_count: number }>('/api/admin/discard-details/batch-delete', { ids })
}
export function clearDiscardDetails(): Promise<{ status: string; deleted_count: number }> {
return deleteJSON<{ status: string; deleted_count: number }>('/api/admin/discard-details')
}
export function getTelemetry(limit = 500, offset = 0, nodeId = ''): Promise<ListResponse<TelemetryRecord>> { export function getTelemetry(limit = 500, offset = 0, nodeId = ''): Promise<ListResponse<TelemetryRecord>> {
return getJSON<ListResponse<TelemetryRecord>>(listPath('/api/telemetry', limit, offset, nodeId)) return getJSON<ListResponse<TelemetryRecord>>(listPath('/api/telemetry', limit, offset, nodeId))
} }
@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { getDiscardDetails } from '../api' import { clearDiscardDetails, deleteDiscardDetailsByIDs, getDiscardDetails } from '../api'
import type { DiscardDetails } from '../types' import type { DiscardDetails } from '../types'
import ConfirmDeleteModal from './ConfirmDeleteModal.vue'
const items = ref<DiscardDetails[]>([]) const items = ref<DiscardDetails[]>([])
const loading = ref(false) const loading = ref(false)
@@ -9,9 +10,48 @@ const error = ref('')
const page = ref(1) const page = ref(1)
const pageSize = 25 const pageSize = 25
const selectedIds = ref<Set<number>>(new Set())
const pendingAction = ref<'batch' | 'clear' | null>(null)
const canPrev = () => page.value > 1 const canPrev = () => page.value > 1
const canNext = () => items.value.length === pageSize const canNext = () => items.value.length === pageSize
const allSelected = computed(
() => items.value.length > 0 && items.value.every((item) => selectedIds.value.has(item.id)),
)
const someSelected = computed(() => selectedIds.value.size > 0 && !allSelected.value)
const selectedCount = computed(() => selectedIds.value.size)
const modalTitle = computed(() =>
pendingAction.value === 'clear' ? '清空丢弃数据' : '批量删除丢弃数据',
)
const modalMessage = computed(() =>
pendingAction.value === 'clear'
? '确定要清空全部丢弃数据吗?此操作不可撤销。'
: `确定要删除选中的 ${selectedCount.value} 条丢弃数据吗?此操作不可撤销。`,
)
const modalConfirmText = computed(() =>
pendingAction.value === 'clear' ? '清空全部' : '删除',
)
function toggleAll(checked: boolean) {
if (checked) {
items.value.forEach((item) => selectedIds.value.add(item.id))
} else {
selectedIds.value.clear()
}
selectedIds.value = new Set(selectedIds.value)
}
function toggleRow(id: number, checked: boolean) {
if (checked) {
selectedIds.value.add(id)
} else {
selectedIds.value.delete(id)
}
selectedIds.value = new Set(selectedIds.value)
}
function formatTime(value: string): string { function formatTime(value: string): string {
return new Date(value).toLocaleString() return new Date(value).toLocaleString()
} }
@@ -31,9 +71,43 @@ async function refreshItems() {
function changePage(nextPage: number) { function changePage(nextPage: number) {
page.value = Math.max(1, nextPage) page.value = Math.max(1, nextPage)
selectedIds.value.clear()
selectedIds.value = new Set(selectedIds.value)
refreshItems() refreshItems()
} }
function requestBatchDelete() {
if (selectedCount.value === 0) return
pendingAction.value = 'batch'
}
function requestClearAll() {
pendingAction.value = 'clear'
}
function cancelDeleteModal() {
pendingAction.value = null
}
async function confirmDeleteModal() {
const action = pendingAction.value
pendingAction.value = null
if (!action) return
try {
if (action === 'clear') {
await clearDiscardDetails()
} else {
await deleteDiscardDetailsByIDs([...selectedIds.value])
}
selectedIds.value.clear()
selectedIds.value = new Set(selectedIds.value)
page.value = 1
await refreshItems()
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
}
onMounted(refreshItems) onMounted(refreshItems)
</script> </script>
@@ -45,7 +119,19 @@ onMounted(refreshItems)
<p class="eyebrow">Discard details</p> <p class="eyebrow">Discard details</p>
<h2>丢弃数据</h2> <h2>丢弃数据</h2>
</div> </div>
<button class="admin-button" @click="refreshItems" :disabled="loading">{{ loading ? '刷新中...' : '刷新数据' }}</button> <div style="display: flex; gap: 8px;">
<button
class="admin-button admin-button-danger"
:disabled="loading || selectedCount === 0"
@click="requestBatchDelete"
>批量删除{{ selectedCount > 0 ? `${selectedCount}` : '' }}</button>
<button
class="admin-button admin-button-danger"
:disabled="loading"
@click="requestClearAll"
>清空全部</button>
<button class="admin-button" @click="refreshItems" :disabled="loading">{{ loading ? '刷新中...' : '刷新数据' }}</button>
</div>
</div> </div>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
@@ -53,6 +139,15 @@ onMounted(refreshItems)
<table class="node-table"> <table class="node-table">
<thead> <thead>
<tr> <tr>
<th style="width: 40px;">
<input
type="checkbox"
:checked="allSelected"
:indeterminate.prop="someSelected"
:disabled="items.length === 0"
@change="toggleAll(($event.target as HTMLInputElement).checked)"
/>
</th>
<th>时间</th> <th>时间</th>
<th>Topic</th> <th>Topic</th>
<th>Error</th> <th>Error</th>
@@ -67,6 +162,13 @@ onMounted(refreshItems)
</thead> </thead>
<tbody> <tbody>
<tr v-for="item in items" :key="item.id"> <tr v-for="item in items" :key="item.id">
<td>
<input
type="checkbox"
:checked="selectedIds.has(item.id)"
@change="toggleRow(item.id, ($event.target as HTMLInputElement).checked)"
/>
</td>
<td>{{ formatTime(item.created_at) }}</td> <td>{{ formatTime(item.created_at) }}</td>
<td>{{ item.topic || '-' }}</td> <td>{{ item.topic || '-' }}</td>
<td>{{ item.error || '-' }}</td> <td>{{ item.error || '-' }}</td>
@@ -90,5 +192,14 @@ onMounted(refreshItems)
<button :disabled="loading || !canNext()" @click="changePage(page + 1)">下一页</button> <button :disabled="loading || !canNext()" @click="changePage(page + 1)">下一页</button>
</div> </div>
</div> </div>
<ConfirmDeleteModal
:open="pendingAction !== null"
:title="modalTitle"
:message="modalMessage"
:confirm-text="modalConfirmText"
@cancel="cancelDeleteModal"
@confirm="confirmDeleteModal"
/>
</section> </section>
</template> </template>
+1 -1
View File
@@ -1 +1 @@
export const FRONTEND_VERSION = '0.2.0' export const FRONTEND_VERSION = '0.3.0'