修复工单、日志、卡密管理页面的分页和筛选功能

- 工单管理:后端添加分页和筛选支持(status, priority, type, 日期范围)
- 日志管理:前端使用服务端分页和筛选(后端已支持)
- 卡密管理:前端使用服务端分页和筛选(后端已支持)
- 所有页面移除前端筛选逻辑,改为服务端筛选
- 修复分页总数与筛选数据不匹配的问题
- 添加watch监听筛选条件变化,自动刷新数据
This commit is contained in:
2026-05-08 14:45:38 +08:00
parent d0ef52ab4f
commit dde179a896
4 changed files with 195 additions and 148 deletions
+51 -4
View File
@@ -1,8 +1,9 @@
package admin package admin
import ( import (
"fmt" "fmt"
"log" "log"
"strconv"
"verification-platform-backend/internal/database" "verification-platform-backend/internal/database"
"verification-platform-backend/internal/model" "verification-platform-backend/internal/model"
@@ -49,12 +50,55 @@ func handleGetTicketStats(c *gin.Context) {
func handleGetTickets(c *gin.Context) { func handleGetTickets(c *gin.Context) {
userID := c.GetUint("user_id") userID := c.GetUint("user_id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
statusFilter := c.Query("status")
priorityFilter := c.Query("priority")
typeFilter := c.Query("type")
createdStartDate := c.Query("created_start_date")
createdEndDate := c.Query("created_end_date")
updatedStartDate := c.Query("updated_start_date")
updatedEndDate := c.Query("updated_end_date")
query := database.DB.Model(&model.Ticket{}).Where("user_id = ? OR assigned_to = ?", userID, userID)
if statusFilter != "" {
query = query.Where("status = ?", statusFilter)
}
if priorityFilter != "" {
query = query.Where("priority = ?", priorityFilter)
}
if typeFilter != "" {
query = query.Where("type = ?", typeFilter)
}
if createdStartDate != "" {
query = query.Where("created_at >= ?", createdStartDate+" 00:00:00")
}
if createdEndDate != "" {
query = query.Where("created_at <= ?", createdEndDate+" 23:59:59")
}
if updatedStartDate != "" {
query = query.Where("updated_at >= ?", updatedStartDate+" 00:00:00")
}
if updatedEndDate != "" {
query = query.Where("updated_at <= ?", updatedEndDate+" 23:59:59")
}
var total int64
query.Count(&total)
offset := (page - 1) * pageSize
var tickets []model.Ticket var tickets []model.Ticket
if err := database.DB.Where("user_id = ? OR assigned_to = ?", userID, userID). if err := query.
Preload("Replies"). Preload("Replies").
Preload("Application"). Preload("Application").
Preload("User"). Preload("User").
Preload("AssignedUser"). Preload("AssignedUser").
Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&tickets).Error; err != nil { Find(&tickets).Error; err != nil {
response.Error(c, 500, "获取工单列表失败") response.Error(c, 500, "获取工单列表失败")
return return
@@ -84,8 +128,11 @@ func handleGetTickets(c *gin.Context) {
} }
response.Success(c, gin.H{ response.Success(c, gin.H{
"tickets": result, "tickets": result,
"total": len(result), "total": total,
"page": page,
"page_size": pageSize,
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
}) })
} }
+47 -55
View File
@@ -34,6 +34,9 @@ const cards = ref<Card[]>([])
const tableRef = ref() const tableRef = ref()
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const cardTypes = ref<CardType[]>([]) const cardTypes = ref<CardType[]>([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const appFilter = ref<string>('') const appFilter = ref<string>('')
const cardTypeFilter = ref<string>('') const cardTypeFilter = ref<string>('')
@@ -56,52 +59,9 @@ const filteredCardTypes = computed(() => {
return cardTypes.value return cardTypes.value
}) })
const filteredCards = computed(() => { const unusedCount = computed(() => cards.value.filter(card => card.status === 'unused').length)
let result = cards.value const usedCount = computed(() => cards.value.filter(card => card.status === 'used').length)
const bannedCount = computed(() => cards.value.filter(card => card.status === 'banned').length)
if (appFilter.value) {
result = result.filter(card => String(card.application_id) === appFilter.value)
}
if (cardTypeFilter.value) {
result = result.filter(card => String(card.card_type_id) === cardTypeFilter.value)
}
if (statusFilter.value) {
result = result.filter(card => card.status === statusFilter.value)
}
if (startDate.value) {
const fromDateTime = startDate.value.includes('T')
? startDate.value.replace('T', ' ')
: `${startDate.value} 00:00`
result = result.filter(card => card.created_at >= fromDateTime)
}
if (endDate.value) {
const toDateTime = endDate.value.includes('T')
? endDate.value.replace('T', ' ')
: `${endDate.value} 23:59`
result = result.filter(card => card.created_at <= toDateTime)
}
if (searchFilter.value) {
const search = searchFilter.value.toLowerCase()
result = result.filter(card =>
card.card_key?.toLowerCase().includes(search)
|| card.application?.name?.toLowerCase().includes(search)
|| card.card_type?.name?.toLowerCase().includes(search)
|| card.creator?.username?.toLowerCase().includes(search)
|| card.app_user?.username?.toLowerCase().includes(search),
)
}
return result
})
const unusedCount = computed(() => filteredCards.value.filter(card => card.status === 'unused').length)
const usedCount = computed(() => filteredCards.value.filter(card => card.status === 'used').length)
const bannedCount = computed(() => filteredCards.value.filter(card => card.status === 'banned').length)
const applicationOptions = computed(() => { const applicationOptions = computed(() => {
return applications.value.map(app => ({ return applications.value.map(app => ({
@@ -149,16 +109,32 @@ async function fetchCardTypes() {
async function fetchCards() { async function fetchCards() {
loading.value = true loading.value = true
try { try {
const data = await api.get<any>('/dev/cards') const params = new URLSearchParams()
if (data && typeof data === 'object' && data.cards) { params.append('page', String(currentPage.value))
cards.value = data.cards params.append('page_size', String(pageSize.value))
if (appFilter.value) {
params.append('application_id', appFilter.value)
} }
else if (Array.isArray(data)) { if (cardTypeFilter.value) {
cards.value = data params.append('card_type_id', cardTypeFilter.value)
} }
else { if (statusFilter.value) {
cards.value = [] params.append('status', statusFilter.value)
} }
if (startDate.value) {
params.append('start_date', startDate.value.split('T')[0])
}
if (endDate.value) {
params.append('end_date', endDate.value.split('T')[0])
}
if (searchFilter.value) {
params.append('search', searchFilter.value.trim())
}
const data = await api.get<{ cards: Card[], total: number }>(`/dev/cards?${params.toString()}`)
cards.value = data?.cards || []
total.value = data?.total || 0
} }
catch (error) { catch (error) {
console.error('获取卡密列表失败:', error) console.error('获取卡密列表失败:', error)
@@ -281,6 +257,21 @@ onMounted(() => {
fetchCardTypes() fetchCardTypes()
fetchCards() fetchCards()
}) })
watch([appFilter, cardTypeFilter, statusFilter, startDate, endDate, searchFilter], () => {
currentPage.value = 1
fetchCards()
})
const serverPagination = computed(() => ({
currentPage: currentPage.value,
pageSize: pageSize.value,
total: total.value,
onChange: (page: number) => {
currentPage.value = page
fetchCards()
},
}))
</script> </script>
<template> <template>
@@ -307,7 +298,7 @@ onMounted(() => {
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<div class="text-2xl font-bold"> <div class="text-2xl font-bold">
{{ filteredCards.length }} {{ total }}
</div> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -360,7 +351,8 @@ onMounted(() => {
<DataTable <DataTable
ref="tableRef" ref="tableRef"
:loading :loading
:data="filteredCards" :data="cards"
:server-pagination="serverPagination"
:on-toggle-status="toggleStatus" :on-toggle-status="toggleStatus"
:on-delete="confirmDeleteCard" :on-delete="confirmDeleteCard"
:search-filter="searchFilter" :search-filter="searchFilter"
+47 -39
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Activity, CheckCircle, FileText, XCircle } from 'lucide-vue-next' import { Activity, CheckCircle, FileText, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { Log } from '@/pages/admin/logs/data/schema' import type { Log } from '@/pages/admin/logs/data/schema'
@@ -21,6 +21,9 @@ interface Application {
const loading = ref(true) const loading = ref(true)
const logs = ref<Log[]>([]) const logs = ref<Log[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const appFilter = ref<string>('') const appFilter = ref<string>('')
const typeFilter = ref<string>('') const typeFilter = ref<string>('')
@@ -28,41 +31,9 @@ const statusFilter = ref<string>('')
const startDate = ref<string>('') const startDate = ref<string>('')
const endDate = ref<string>('') const endDate = ref<string>('')
const filteredLogs = computed(() => { const successCount = computed(() => logs.value.filter(log => log.status === 'success').length)
let result = logs.value const failedCount = computed(() => logs.value.filter(log => log.status === 'failed').length)
const operationCount = computed(() => logs.value.filter(log => log.log_type === 'operation').length)
if (appFilter.value) {
result = result.filter(log => String(log.application_id) === appFilter.value)
}
if (typeFilter.value) {
result = result.filter(log => log.log_type === typeFilter.value)
}
if (statusFilter.value) {
result = result.filter(log => log.status === statusFilter.value)
}
if (startDate.value) {
const fromDateTime = startDate.value.includes('T')
? startDate.value.replace('T', ' ')
: `${startDate.value} 00:00`
result = result.filter(log => log.created_at >= fromDateTime)
}
if (endDate.value) {
const toDateTime = endDate.value.includes('T')
? endDate.value.replace('T', ' ')
: `${endDate.value} 23:59`
result = result.filter(log => log.created_at <= toDateTime)
}
return result
})
const successCount = computed(() => filteredLogs.value.filter(log => log.status === 'success').length)
const failedCount = computed(() => filteredLogs.value.filter(log => log.status === 'failed').length)
const operationCount = computed(() => filteredLogs.value.filter(log => log.log_type === 'operation').length)
const applicationOptions = computed(() => { const applicationOptions = computed(() => {
return applications.value.map(app => ({ return applications.value.map(app => ({
@@ -95,8 +66,29 @@ async function fetchApplications() {
async function fetchLogs() { async function fetchLogs() {
loading.value = true loading.value = true
try { try {
const data = await api.get<{ logs: Log[] }>('/dev/logs') const params = new URLSearchParams()
params.append('page', String(currentPage.value))
params.append('page_size', String(pageSize.value))
if (appFilter.value) {
params.append('application_id', appFilter.value)
}
if (typeFilter.value) {
params.append('type', typeFilter.value)
}
if (statusFilter.value) {
params.append('status', statusFilter.value)
}
if (startDate.value) {
params.append('start_date', startDate.value.split('T')[0])
}
if (endDate.value) {
params.append('end_date', endDate.value.split('T')[0])
}
const data = await api.get<{ logs: Log[], total: number }>(`/dev/logs?${params.toString()}`)
logs.value = data?.logs || [] logs.value = data?.logs || []
total.value = data?.total || 0
} }
catch (error) { catch (error) {
console.error('获取日志记录失败:', error) console.error('获取日志记录失败:', error)
@@ -111,6 +103,21 @@ onMounted(() => {
fetchApplications() fetchApplications()
fetchLogs() fetchLogs()
}) })
watch([appFilter, typeFilter, statusFilter, startDate, endDate], () => {
currentPage.value = 1
fetchLogs()
})
const serverPagination = computed(() => ({
currentPage: currentPage.value,
pageSize: pageSize.value,
total: total.value,
onChange: (page: number) => {
currentPage.value = page
fetchLogs()
},
}))
</script> </script>
<template> <template>
@@ -130,7 +137,7 @@ onMounted(() => {
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<div class="text-2xl font-bold"> <div class="text-2xl font-bold">
{{ filteredLogs.length }} {{ total }}
</div> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -182,7 +189,8 @@ onMounted(() => {
<UiCardContent class="p-6"> <UiCardContent class="p-6">
<DataTable <DataTable
:loading :loading
:data="filteredLogs" :data="logs"
:server-pagination="serverPagination"
@refresh="fetchLogs" @refresh="fetchLogs"
> >
<template #filters> <template #filters>
+50 -50
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { AlertCircle, CheckCircle, Clock, MessageSquare, Plus } from 'lucide-vue-next' import { AlertCircle, CheckCircle, Clock, MessageSquare, Plus } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
@@ -21,6 +21,9 @@ const router = useRouter()
const loading = ref(true) const loading = ref(true)
const tickets = ref<Ticket[]>([]) const tickets = ref<Ticket[]>([])
const tableRef = ref() const tableRef = ref()
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const statusFilter = ref<string>('') const statusFilter = ref<string>('')
const priorityFilter = ref<string>('') const priorityFilter = ref<string>('')
@@ -35,52 +38,6 @@ const deleteTarget = ref<Ticket | null>(null)
const batchDeleteDialogOpen = ref(false) const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<(string | number)[]>([]) const batchDeleteIds = ref<(string | number)[]>([])
const filteredTickets = computed(() => {
let result = [...tickets.value]
if (statusFilter.value) {
result = result.filter(t => t.status === statusFilter.value)
}
if (priorityFilter.value) {
result = result.filter(t => t.priority === priorityFilter.value)
}
if (typeFilter.value) {
result = result.filter(t => t.type === typeFilter.value)
}
if (createdStartDate.value) {
const fromDateTime = createdStartDate.value.includes('T')
? createdStartDate.value.replace('T', ' ')
: `${createdStartDate.value} 00:00`
result = result.filter(t => t.created_at >= fromDateTime)
}
if (createdEndDate.value) {
const toDateTime = createdEndDate.value.includes('T')
? createdEndDate.value.replace('T', ' ')
: `${createdEndDate.value} 23:59`
result = result.filter(t => t.created_at <= toDateTime)
}
if (updatedStartDate.value) {
const fromDateTime = updatedStartDate.value.includes('T')
? updatedStartDate.value.replace('T', ' ')
: `${updatedStartDate.value} 00:00`
result = result.filter(t => t.updated_at && t.updated_at >= fromDateTime)
}
if (updatedEndDate.value) {
const toDateTime = updatedEndDate.value.includes('T')
? updatedEndDate.value.replace('T', ' ')
: `${updatedEndDate.value} 23:59`
result = result.filter(t => t.updated_at && t.updated_at <= toDateTime)
}
return result
})
const openCount = computed(() => tickets.value.filter(t => t.status === 'open').length) const openCount = computed(() => tickets.value.filter(t => t.status === 'open').length)
const pendingCount = computed(() => tickets.value.filter(t => t.status === 'processing').length) const pendingCount = computed(() => tickets.value.filter(t => t.status === 'processing').length)
const resolvedCount = computed(() => tickets.value.filter(t => t.status === 'resolved').length) const resolvedCount = computed(() => tickets.value.filter(t => t.status === 'resolved').length)
@@ -107,8 +64,35 @@ const typeOptions = computed(() => [
async function fetchTickets() { async function fetchTickets() {
loading.value = true loading.value = true
try { try {
const data = await api.get<{ tickets: Ticket[] }>('/dev/tickets') const params = new URLSearchParams()
params.append('page', String(currentPage.value))
params.append('page_size', String(pageSize.value))
if (statusFilter.value) {
params.append('status', statusFilter.value)
}
if (priorityFilter.value) {
params.append('priority', priorityFilter.value)
}
if (typeFilter.value) {
params.append('type', typeFilter.value)
}
if (createdStartDate.value) {
params.append('created_start_date', createdStartDate.value.split('T')[0])
}
if (createdEndDate.value) {
params.append('created_end_date', createdEndDate.value.split('T')[0])
}
if (updatedStartDate.value) {
params.append('updated_start_date', updatedStartDate.value.split('T')[0])
}
if (updatedEndDate.value) {
params.append('updated_end_date', updatedEndDate.value.split('T')[0])
}
const data = await api.get<{ tickets: Ticket[], total: number }>(`/dev/tickets?${params.toString()}`)
tickets.value = data?.tickets || [] tickets.value = data?.tickets || []
total.value = data?.total || 0
} }
catch (error) { catch (error) {
console.error('获取工单列表失败:', error) console.error('获取工单列表失败:', error)
@@ -183,6 +167,21 @@ async function handleBatchDelete() {
onMounted(() => { onMounted(() => {
fetchTickets() fetchTickets()
}) })
watch([statusFilter, priorityFilter, typeFilter, createdStartDate, createdEndDate, updatedStartDate, updatedEndDate], () => {
currentPage.value = 1
fetchTickets()
})
const serverPagination = computed(() => ({
currentPage: currentPage.value,
pageSize: pageSize.value,
total: total.value,
onChange: (page: number) => {
currentPage.value = page
fetchTickets()
},
}))
</script> </script>
<template> <template>
@@ -251,7 +250,7 @@ onMounted(() => {
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent>
<div class="text-2xl font-bold"> <div class="text-2xl font-bold">
{{ filteredTickets.length }} {{ total }}
</div> </div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -262,7 +261,8 @@ onMounted(() => {
<DataTable <DataTable
ref="tableRef" ref="tableRef"
:loading :loading
:data="filteredTickets" :data="tickets"
:server-pagination="serverPagination"
:on-update-status="updateTicketStatus" :on-update-status="updateTicketStatus"
:on-view-detail="openTicketDetail" :on-view-detail="openTicketDetail"
:on-delete="confirmDeleteTicket" :on-delete="confirmDeleteTicket"