From ee9b9b397136dd27c11f289bfa55ac71e80ec3a4 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:38:45 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(fe):=20=E4=BF=AE=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=88=86=E9=A1=B5=E5=92=8C=E5=9F=BA=E7=A1=80=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复的主要问题: 1. **分页切换失效**(AccountsView.vue) - 修复 useTableLoader 未解构 handlePageSizeChange 函数 - 添加 @update:pageSize 事件绑定到 Pagination 组件 2. **内存泄漏修复**(多个文件) - UsersView.vue: 添加 searchTimeout 清理和 abortController.abort() - ProxiesView.vue: 添加 onUnmounted 钩子清理定时器 - RedeemView.vue: 添加 onUnmounted 钩子清理定时器 3. **分页重置问题**(UsersView.vue) - toggleBuiltInFilter: 切换筛选器时重置 pagination.page = 1 - toggleAttributeFilter: 切换属性筛选时重置 pagination.page = 1 影响范围: - frontend/src/views/admin/AccountsView.vue - frontend/src/views/admin/ProxiesView.vue - frontend/src/views/admin/RedeemView.vue - frontend/src/views/admin/UsersView.vue 测试:✓ 前端构建测试通过 --- frontend/src/views/admin/AccountsView.vue | 4 ++-- frontend/src/views/admin/ProxiesView.vue | 7 ++++++- frontend/src/views/admin/RedeemView.vue | 7 ++++++- frontend/src/views/admin/UsersView.vue | 4 ++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 0ca22a76..560ae12b 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -107,7 +107,7 @@ - + @@ -175,7 +175,7 @@ const statsAcc = ref(null) const togglingSchedulable = ref(null) const menu = reactive<{show:boolean, acc:Account|null, pos:{top:number, left:number}|null}>({ show: false, acc: null, pos: null }) -const { items: accounts, loading, params, pagination, load, reload, debouncedReload, handlePageChange } = useTableLoader({ +const { items: accounts, loading, params, pagination, load, reload, debouncedReload, handlePageChange, handlePageSizeChange } = useTableLoader({ fetchFn: adminAPI.accounts.list, initialParams: { platform: '', type: '', status: '', search: '' } }) diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue index 00b43ba2..22d778d9 100644 --- a/frontend/src/views/admin/ProxiesView.vue +++ b/frontend/src/views/admin/ProxiesView.vue @@ -519,7 +519,7 @@ diff --git a/frontend/src/views/admin/RedeemView.vue b/frontend/src/views/admin/RedeemView.vue index 3503cd16..50c55ba3 100644 --- a/frontend/src/views/admin/RedeemView.vue +++ b/frontend/src/views/admin/RedeemView.vue @@ -364,7 +364,7 @@ diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue index d4bf555c..f1fc9e1f 100644 --- a/frontend/src/views/admin/UsersView.vue +++ b/frontend/src/views/admin/UsersView.vue @@ -943,6 +943,7 @@ const toggleBuiltInFilter = (key: string) => { visibleFilters.add(key) } saveFiltersToStorage() + pagination.page = 1 loadUsers() } @@ -957,6 +958,7 @@ const toggleAttributeFilter = (attr: UserAttributeDefinition) => { activeAttributeFilters[attr.id] = '' } saveFiltersToStorage() + pagination.page = 1 loadUsers() } @@ -1059,5 +1061,7 @@ onMounted(async () => { onUnmounted(() => { document.removeEventListener('click', handleClickOutside) + clearTimeout(searchTimeout) + abortController?.abort() }) From 514f5802b54b18aa0211d5d3ba713bd5a3febb6e Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:58:21 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(fe):=20=E4=BF=AE=E5=A4=8D=E4=B8=AD?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E8=A1=A8=E6=A0=BC=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复的问题: 1. **搜索和筛选防抖不同步**(AccountsView.vue) - 问题:筛选器使用 reload(立即),搜索使用 debouncedReload(300ms延迟) - 修复:统一使用 debouncedReload,避免多余的API调用 2. **useTableLoader 竞态条件**(useTableLoader.ts) - 问题:finally 块检查 signal.aborted 而不是 controller 实例 - 修复:检查 abortController === currentController 3. **改进错误处理**(UsersView.vue) - 添加详细错误消息:error.response?.data?.detail || error.message - 用户可以看到具体的错误原因而不是通用消息 4. **分页边界检查**(useTableLoader.ts, UsersView.vue) - 添加页码有效性检查:Math.max(1, Math.min(page, pagination.pages || 1)) - 防止分页越界导致显示空表 影响范围: - frontend/src/composables/useTableLoader.ts - frontend/src/views/admin/AccountsView.vue - frontend/src/views/admin/UsersView.vue 测试:✓ 前端构建测试通过 --- frontend/src/composables/useTableLoader.ts | 13 ++++++++----- frontend/src/views/admin/AccountsView.vue | 2 +- frontend/src/views/admin/UsersView.vue | 9 ++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/composables/useTableLoader.ts b/frontend/src/composables/useTableLoader.ts index 01703ee1..5fb6c5e0 100644 --- a/frontend/src/composables/useTableLoader.ts +++ b/frontend/src/composables/useTableLoader.ts @@ -43,7 +43,8 @@ export function useTableLoader>(options: TableL if (abortController) { abortController.abort() } - abortController = new AbortController() + const currentController = new AbortController() + abortController = currentController loading.value = true try { @@ -51,9 +52,9 @@ export function useTableLoader>(options: TableL pagination.page, pagination.page_size, toRaw(params) as P, - { signal: abortController.signal } + { signal: currentController.signal } ) - + items.value = response.items || [] pagination.total = response.total || 0 pagination.pages = response.pages || 0 @@ -63,7 +64,7 @@ export function useTableLoader>(options: TableL throw error } } finally { - if (abortController && !abortController.signal.aborted) { + if (abortController === currentController) { loading.value = false } } @@ -77,7 +78,9 @@ export function useTableLoader>(options: TableL const debouncedReload = useDebounceFn(reload, debounceMs) const handlePageChange = (page: number) => { - pagination.page = page + // 确保页码在有效范围内 + const validPage = Math.max(1, Math.min(page, pagination.pages || 1)) + pagination.page = validPage load() } diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 560ae12b..27cd9c19 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -7,7 +7,7 @@ v-model:searchQuery="params.search" :filters="params" @update:filters="(newFilters) => Object.assign(params, newFilters)" - @change="reload" + @change="debouncedReload" @update:searchQuery="debouncedReload" /> { } } } - } catch (error) { + } catch (error: any) { const errorInfo = error as { name?: string; code?: string } if (errorInfo?.name === 'AbortError' || errorInfo?.name === 'CanceledError' || errorInfo?.code === 'ERR_CANCELED') { return } - appStore.showError(t('admin.users.failedToLoad')) + const message = error.response?.data?.detail || error.message || t('admin.users.failedToLoad') + appStore.showError(message) console.error('Error loading users:', error) } finally { if (abortController === currentAbortController) { @@ -917,7 +918,9 @@ const handleSearch = () => { } const handlePageChange = (page: number) => { - pagination.page = page + // 确保页码在有效范围内 + const validPage = Math.max(1, Math.min(page, pagination.pages || 1)) + pagination.page = validPage loadUsers() } From 3820232241695aeab36c21b6bc9938b1ba8deb45 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:58:06 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(admin):=20=E4=BF=AE=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E6=89=B9=E9=87=8F=E6=93=8D=E4=BD=9C=E5=92=8C=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 恢复账号管理批量操作栏缺失的功能按钮 - 添加"本页全选"按钮支持批量选择当前页所有账号 - 添加"清除已选"按钮快速清空已选账号列表 - 在重构拆分组件时遗漏,现已恢复 2. 修复分组管理搜索功能仅搜索当前页的问题 - 前端:移除本地过滤逻辑,改用后端搜索 - 后端:添加 search 参数支持,搜索名称和描述字段 - 支持不区分大小写的模糊匹配 - 统一所有管理页面的搜索体验 --- .../internal/handler/admin/group_handler.go | 3 ++- backend/internal/repository/group_repo.go | 10 ++++++-- backend/internal/service/admin_service.go | 6 ++--- .../service/admin_service_delete_test.go | 2 +- .../service/admin_service_group_test.go | 2 +- .../service/gemini_multiplatform_test.go | 2 +- backend/internal/service/group_service.go | 2 +- frontend/src/api/admin/groups.ts | 3 ++- .../admin/account/AccountBulkActionsBar.vue | 23 ++++++++++++++--- frontend/src/views/admin/GroupsView.vue | 25 ++++++++++--------- 10 files changed, 52 insertions(+), 26 deletions(-) diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index acb9462c..1d9f7e24 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -67,6 +67,7 @@ func (h *GroupHandler) List(c *gin.Context) { page, pageSize := response.ParsePagination(c) platform := c.Query("platform") status := c.Query("status") + search := c.Query("search") isExclusiveStr := c.Query("is_exclusive") var isExclusive *bool @@ -75,7 +76,7 @@ func (h *GroupHandler) List(c *gin.Context) { isExclusive = &val } - groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, isExclusive) + groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 1fb4ae90..a54f3116 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -112,10 +112,10 @@ func (r *groupRepository) Delete(ctx context.Context, id int64) error { } func (r *groupRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.Group, *pagination.PaginationResult, error) { - return r.ListWithFilters(ctx, params, "", "", nil) + return r.ListWithFilters(ctx, params, "", "", "", nil) } -func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) { +func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) { q := r.client.Group.Query() if platform != "" { @@ -124,6 +124,12 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination if status != "" { q = q.Where(group.StatusEQ(status)) } + if search != "" { + q = q.Where(group.Or( + group.NameContainsFold(search), + group.DescriptionContainsFold(search), + )) + } if isExclusive != nil { q = q.Where(group.IsExclusiveEQ(*isExclusive)) } diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index e29bbdb4..cd427368 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -24,7 +24,7 @@ type AdminService interface { GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error) // Group management - ListGroups(ctx context.Context, page, pageSize int, platform, status string, isExclusive *bool) ([]Group, int64, error) + ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) GetAllGroups(ctx context.Context) ([]Group, error) GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error) GetGroup(ctx context.Context, id int64) (*Group, error) @@ -478,9 +478,9 @@ func (s *adminServiceImpl) GetUserUsageStats(ctx context.Context, userID int64, } // Group management implementations -func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status string, isExclusive *bool) ([]Group, int64, error) { +func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) { params := pagination.PaginationParams{Page: page, PageSize: pageSize} - groups, result, err := s.groupRepo.ListWithFilters(ctx, params, platform, status, isExclusive) + groups, result, err := s.groupRepo.ListWithFilters(ctx, params, platform, status, search, isExclusive) if err != nil { return nil, 0, err } diff --git a/backend/internal/service/admin_service_delete_test.go b/backend/internal/service/admin_service_delete_test.go index c1d2e4c9..351f64e8 100644 --- a/backend/internal/service/admin_service_delete_test.go +++ b/backend/internal/service/admin_service_delete_test.go @@ -124,7 +124,7 @@ func (s *groupRepoStub) List(ctx context.Context, params pagination.PaginationPa panic("unexpected List call") } -func (s *groupRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) { +func (s *groupRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) { panic("unexpected ListWithFilters call") } diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index 3171de11..45bab327 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -47,7 +47,7 @@ func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationP panic("unexpected List call") } -func (s *groupRepoStubForAdmin) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) { +func (s *groupRepoStubForAdmin) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) { panic("unexpected ListWithFilters call") } diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index 6007bce8..1a8b005f 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -166,7 +166,7 @@ func (m *mockGroupRepoForGemini) DeleteCascade(ctx context.Context, id int64) ([ func (m *mockGroupRepoForGemini) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) { return nil, nil, nil } -func (m *mockGroupRepoForGemini) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) { +func (m *mockGroupRepoForGemini) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) { return nil, nil, nil } func (m *mockGroupRepoForGemini) ListActive(ctx context.Context) ([]Group, error) { return nil, nil } diff --git a/backend/internal/service/group_service.go b/backend/internal/service/group_service.go index 403636e8..a444556f 100644 --- a/backend/internal/service/group_service.go +++ b/backend/internal/service/group_service.go @@ -21,7 +21,7 @@ type GroupRepository interface { DeleteCascade(ctx context.Context, id int64) ([]int64, error) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) - ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) + ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) ListActive(ctx context.Context) ([]Group, error) ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error) diff --git a/frontend/src/api/admin/groups.ts b/frontend/src/api/admin/groups.ts index 23db9104..44eebc99 100644 --- a/frontend/src/api/admin/groups.ts +++ b/frontend/src/api/admin/groups.ts @@ -16,7 +16,7 @@ import type { * List all groups with pagination * @param page - Page number (default: 1) * @param pageSize - Items per page (default: 20) - * @param filters - Optional filters (platform, status, is_exclusive) + * @param filters - Optional filters (platform, status, is_exclusive, search) * @returns Paginated list of groups */ export async function list( @@ -26,6 +26,7 @@ export async function list( platform?: GroupPlatform status?: 'active' | 'inactive' is_exclusive?: boolean + search?: string }, options?: { signal?: AbortSignal diff --git a/frontend/src/components/admin/account/AccountBulkActionsBar.vue b/frontend/src/components/admin/account/AccountBulkActionsBar.vue index 17bd634d..eaabc611 100644 --- a/frontend/src/components/admin/account/AccountBulkActionsBar.vue +++ b/frontend/src/components/admin/account/AccountBulkActionsBar.vue @@ -1,6 +1,23 @@