fix(admin): 修复表格批量操作和搜索功能问题
1. 恢复账号管理批量操作栏缺失的功能按钮 - 添加"本页全选"按钮支持批量选择当前页所有账号 - 添加"清除已选"按钮快速清空已选账号列表 - 在重构拆分组件时遗漏,现已恢复 2. 修复分组管理搜索功能仅搜索当前页的问题 - 前端:移除本地过滤逻辑,改用后端搜索 - 后端:添加 search 参数支持,搜索名称和描述字段 - 支持不区分大小写的模糊匹配 - 统一所有管理页面的搜索体验
This commit is contained in:
@@ -67,6 +67,7 @@ func (h *GroupHandler) List(c *gin.Context) {
|
|||||||
page, pageSize := response.ParsePagination(c)
|
page, pageSize := response.ParsePagination(c)
|
||||||
platform := c.Query("platform")
|
platform := c.Query("platform")
|
||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
|
search := c.Query("search")
|
||||||
isExclusiveStr := c.Query("is_exclusive")
|
isExclusiveStr := c.Query("is_exclusive")
|
||||||
|
|
||||||
var isExclusive *bool
|
var isExclusive *bool
|
||||||
@@ -75,7 +76,7 @@ func (h *GroupHandler) List(c *gin.Context) {
|
|||||||
isExclusive = &val
|
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 {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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) {
|
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()
|
q := r.client.Group.Query()
|
||||||
|
|
||||||
if platform != "" {
|
if platform != "" {
|
||||||
@@ -124,6 +124,12 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination
|
|||||||
if status != "" {
|
if status != "" {
|
||||||
q = q.Where(group.StatusEQ(status))
|
q = q.Where(group.StatusEQ(status))
|
||||||
}
|
}
|
||||||
|
if search != "" {
|
||||||
|
q = q.Where(group.Or(
|
||||||
|
group.NameContainsFold(search),
|
||||||
|
group.DescriptionContainsFold(search),
|
||||||
|
))
|
||||||
|
}
|
||||||
if isExclusive != nil {
|
if isExclusive != nil {
|
||||||
q = q.Where(group.IsExclusiveEQ(*isExclusive))
|
q = q.Where(group.IsExclusiveEQ(*isExclusive))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type AdminService interface {
|
|||||||
GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error)
|
GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error)
|
||||||
|
|
||||||
// Group management
|
// 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)
|
GetAllGroups(ctx context.Context) ([]Group, error)
|
||||||
GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error)
|
GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error)
|
||||||
GetGroup(ctx context.Context, id int64) (*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
|
// 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}
|
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 {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (s *groupRepoStub) List(ctx context.Context, params pagination.PaginationPa
|
|||||||
panic("unexpected List call")
|
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")
|
panic("unexpected ListWithFilters call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationP
|
|||||||
panic("unexpected List call")
|
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")
|
panic("unexpected ListWithFilters call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
func (m *mockGroupRepoForGemini) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
|
||||||
return nil, nil, nil
|
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
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
func (m *mockGroupRepoForGemini) ListActive(ctx context.Context) ([]Group, error) { return nil, nil }
|
func (m *mockGroupRepoForGemini) ListActive(ctx context.Context) ([]Group, error) { return nil, nil }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type GroupRepository interface {
|
|||||||
DeleteCascade(ctx context.Context, id int64) ([]int64, error)
|
DeleteCascade(ctx context.Context, id int64) ([]int64, error)
|
||||||
|
|
||||||
List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, 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)
|
ListActive(ctx context.Context) ([]Group, error)
|
||||||
ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error)
|
ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
* List all groups with pagination
|
* List all groups with pagination
|
||||||
* @param page - Page number (default: 1)
|
* @param page - Page number (default: 1)
|
||||||
* @param pageSize - Items per page (default: 20)
|
* @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
|
* @returns Paginated list of groups
|
||||||
*/
|
*/
|
||||||
export async function list(
|
export async function list(
|
||||||
@@ -26,6 +26,7 @@ export async function list(
|
|||||||
platform?: GroupPlatform
|
platform?: GroupPlatform
|
||||||
status?: 'active' | 'inactive'
|
status?: 'active' | 'inactive'
|
||||||
is_exclusive?: boolean
|
is_exclusive?: boolean
|
||||||
|
search?: string
|
||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="selectedIds.length > 0" class="mb-4 flex items-center justify-between p-3 bg-primary-50 rounded-lg">
|
<div v-if="selectedIds.length > 0" class="mb-4 flex items-center justify-between p-3 bg-primary-50 rounded-lg dark:bg-primary-900/20">
|
||||||
<span class="text-sm font-medium">{{ t('admin.accounts.bulkActions.selected', { count: selectedIds.length }) }}</span>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-primary-900 dark:text-primary-100">
|
||||||
|
{{ t('admin.accounts.bulkActions.selected', { count: selectedIds.length }) }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
@click="$emit('select-page')"
|
||||||
|
class="text-xs font-medium text-primary-700 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.bulkActions.selectCurrentPage') }}
|
||||||
|
</button>
|
||||||
|
<span class="text-gray-300 dark:text-primary-800">•</span>
|
||||||
|
<button
|
||||||
|
@click="$emit('clear')"
|
||||||
|
class="text-xs font-medium text-primary-700 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.bulkActions.clear') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="$emit('delete')" class="btn btn-danger btn-sm">{{ t('admin.accounts.bulkActions.delete') }}</button>
|
<button @click="$emit('delete')" class="btn btn-danger btn-sm">{{ t('admin.accounts.bulkActions.delete') }}</button>
|
||||||
<button @click="$emit('edit')" class="btn btn-primary btn-sm">{{ t('admin.accounts.bulkActions.edit') }}</button>
|
<button @click="$emit('edit')" class="btn btn-primary btn-sm">{{ t('admin.accounts.bulkActions.edit') }}</button>
|
||||||
@@ -10,5 +27,5 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
defineProps(['selectedIds']); defineEmits(['delete', 'edit']); const { t } = useI18n()
|
defineProps(['selectedIds']); defineEmits(['delete', 'edit', 'clear', 'select-page']); const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('admin.groups.searchGroups')"
|
:placeholder="t('admin.groups.searchGroups')"
|
||||||
class="input pl-10"
|
class="input pl-10"
|
||||||
|
@input="handleSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #table>
|
<template #table>
|
||||||
<DataTable :columns="columns" :data="displayedGroups" :loading="loading">
|
<DataTable :columns="columns" :data="groups" :loading="loading">
|
||||||
<template #cell-name="{ value }">
|
<template #cell-name="{ value }">
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -932,16 +933,6 @@ const pagination = reactive({
|
|||||||
|
|
||||||
let abortController: AbortController | null = null
|
let abortController: AbortController | null = null
|
||||||
|
|
||||||
const displayedGroups = computed(() => {
|
|
||||||
const q = searchQuery.value.trim().toLowerCase()
|
|
||||||
if (!q) return groups.value
|
|
||||||
return groups.value.filter((group) => {
|
|
||||||
const name = group.name?.toLowerCase?.() ?? ''
|
|
||||||
const description = group.description?.toLowerCase?.() ?? ''
|
|
||||||
return name.includes(q) || description.includes(q)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
@@ -1011,7 +1002,8 @@ const loadGroups = async () => {
|
|||||||
const response = await adminAPI.groups.list(pagination.page, pagination.page_size, {
|
const response = await adminAPI.groups.list(pagination.page, pagination.page_size, {
|
||||||
platform: (filters.platform as GroupPlatform) || undefined,
|
platform: (filters.platform as GroupPlatform) || undefined,
|
||||||
status: filters.status as any,
|
status: filters.status as any,
|
||||||
is_exclusive: filters.is_exclusive ? filters.is_exclusive === 'true' : undefined
|
is_exclusive: filters.is_exclusive ? filters.is_exclusive === 'true' : undefined,
|
||||||
|
search: searchQuery.value.trim() || undefined
|
||||||
}, { signal })
|
}, { signal })
|
||||||
if (signal.aborted) return
|
if (signal.aborted) return
|
||||||
groups.value = response.items
|
groups.value = response.items
|
||||||
@@ -1030,6 +1022,15 @@ const loadGroups = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout>
|
||||||
|
const handleSearch = () => {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadGroups()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
pagination.page = page
|
pagination.page = page
|
||||||
loadGroups()
|
loadGroups()
|
||||||
|
|||||||
Reference in New Issue
Block a user