丢弃数据页批量管理:多选删除+一键清空(ConfirmDeleteModal 二次确认),后端 v1.2.0 / 前端 v0.3.0
This commit is contained in:
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
export const FRONTEND_VERSION = '0.2.0'
|
export const FRONTEND_VERSION = '0.3.0'
|
||||||
Reference in New Issue
Block a user