Commit 5f8e60a1 authored by IanShaw027's avatar IanShaw027
Browse files

feat(table): 表格排序与搜索改为后端处理

parent 66e15a54
...@@ -21,13 +21,13 @@ import ( ...@@ -21,13 +21,13 @@ import (
// AdminService interface defines admin management operations // AdminService interface defines admin management operations
type AdminService interface { type AdminService interface {
// User management // User management
ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters) ([]User, int64, error) ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters, sortBy, sortOrder string) ([]User, int64, error)
GetUser(ctx context.Context, id int64) (*User, error) GetUser(ctx context.Context, id int64) (*User, error)
CreateUser(ctx context.Context, input *CreateUserInput) (*User, error) CreateUser(ctx context.Context, input *CreateUserInput) (*User, error)
UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*User, error) UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*User, error)
DeleteUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error
UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*User, error) UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*User, error)
GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]APIKey, int64, error) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error)
GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error) GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error)
// GetUserBalanceHistory returns paginated balance/concurrency change records for a user. // GetUserBalanceHistory returns paginated balance/concurrency change records for a user.
// codeType is optional - pass empty string to return all types. // codeType is optional - pass empty string to return all types.
...@@ -35,7 +35,7 @@ type AdminService interface { ...@@ -35,7 +35,7 @@ type AdminService interface {
GetUserBalanceHistory(ctx context.Context, userID int64, page, pageSize int, codeType string) ([]RedeemCode, int64, float64, error) GetUserBalanceHistory(ctx context.Context, userID int64, page, pageSize int, codeType string) ([]RedeemCode, int64, float64, error)
// Group management // Group management
ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]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)
...@@ -55,7 +55,7 @@ type AdminService interface { ...@@ -55,7 +55,7 @@ type AdminService interface {
ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error)
// Account management // Account management
ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, int64, error) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]Account, int64, error)
GetAccount(ctx context.Context, id int64) (*Account, error) GetAccount(ctx context.Context, id int64) (*Account, error)
GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error) GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error)
CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error) CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error)
...@@ -77,8 +77,8 @@ type AdminService interface { ...@@ -77,8 +77,8 @@ type AdminService interface {
CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error
// Proxy management // Proxy management
ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error)
ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string) ([]ProxyWithAccountCount, int64, error) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error)
GetAllProxies(ctx context.Context) ([]Proxy, error) GetAllProxies(ctx context.Context) ([]Proxy, error)
GetAllProxiesWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error) GetAllProxiesWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error)
GetProxy(ctx context.Context, id int64) (*Proxy, error) GetProxy(ctx context.Context, id int64) (*Proxy, error)
...@@ -93,7 +93,7 @@ type AdminService interface { ...@@ -93,7 +93,7 @@ type AdminService interface {
CheckProxyQuality(ctx context.Context, id int64) (*ProxyQualityCheckResult, error) CheckProxyQuality(ctx context.Context, id int64) (*ProxyQualityCheckResult, error)
// Redeem code management // Redeem code management
ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]RedeemCode, int64, error) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]RedeemCode, int64, error)
GetRedeemCode(ctx context.Context, id int64) (*RedeemCode, error) GetRedeemCode(ctx context.Context, id int64) (*RedeemCode, error)
GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]RedeemCode, error) GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]RedeemCode, error)
DeleteRedeemCode(ctx context.Context, id int64) error DeleteRedeemCode(ctx context.Context, id int64) error
...@@ -483,8 +483,8 @@ func NewAdminService( ...@@ -483,8 +483,8 @@ func NewAdminService(
} }
// User management implementations // User management implementations
func (s *adminServiceImpl) ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters) ([]User, int64, error) { func (s *adminServiceImpl) ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters, sortBy, sortOrder string) ([]User, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
users, result, err := s.userRepo.ListWithFilters(ctx, params, filters) users, result, err := s.userRepo.ListWithFilters(ctx, params, filters)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
...@@ -751,8 +751,8 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64, ...@@ -751,8 +751,8 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
return user, nil return user, nil
} }
func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]APIKey, int64, error) { func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
keys, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, params, APIKeyListFilters{}) keys, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, params, APIKeyListFilters{})
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
...@@ -787,8 +787,8 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int ...@@ -787,8 +787,8 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int
} }
// Group management implementations // Group management implementations
func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) { func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
groups, result, err := s.groupRepo.ListWithFilters(ctx, params, platform, status, search, 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
...@@ -1456,8 +1456,8 @@ func (s *adminServiceImpl) ReplaceUserGroup(ctx context.Context, userID, oldGrou ...@@ -1456,8 +1456,8 @@ func (s *adminServiceImpl) ReplaceUserGroup(ctx context.Context, userID, oldGrou
} }
// Account management implementations // Account management implementations
func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, int64, error) { func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]Account, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
accounts, result, err := s.accountRepo.ListWithFilters(ctx, params, platform, accountType, status, search, groupID, privacyMode) accounts, result, err := s.accountRepo.ListWithFilters(ctx, params, platform, accountType, status, search, groupID, privacyMode)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
...@@ -1885,8 +1885,8 @@ func (s *adminServiceImpl) SetAccountSchedulable(ctx context.Context, id int64, ...@@ -1885,8 +1885,8 @@ func (s *adminServiceImpl) SetAccountSchedulable(ctx context.Context, id int64,
} }
// Proxy management implementations // Proxy management implementations
func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error) { func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
proxies, result, err := s.proxyRepo.ListWithFilters(ctx, params, protocol, status, search) proxies, result, err := s.proxyRepo.ListWithFilters(ctx, params, protocol, status, search)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
...@@ -1894,8 +1894,8 @@ func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, ...@@ -1894,8 +1894,8 @@ func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int,
return proxies, result.Total, nil return proxies, result.Total, nil
} }
func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string) ([]ProxyWithAccountCount, int64, error) { func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
proxies, result, err := s.proxyRepo.ListWithFiltersAndAccountCount(ctx, params, protocol, status, search) proxies, result, err := s.proxyRepo.ListWithFiltersAndAccountCount(ctx, params, protocol, status, search)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
...@@ -2032,8 +2032,8 @@ func (s *adminServiceImpl) CheckProxyExists(ctx context.Context, host string, po ...@@ -2032,8 +2032,8 @@ func (s *adminServiceImpl) CheckProxyExists(ctx context.Context, host string, po
} }
// Redeem code management implementations // Redeem code management implementations
func (s *adminServiceImpl) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]RedeemCode, int64, error) { func (s *adminServiceImpl) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]RedeemCode, int64, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
codes, result, err := s.redeemCodeRepo.ListWithFilters(ctx, params, codeType, status, search) codes, result, err := s.redeemCodeRepo.ListWithFilters(ctx, params, codeType, status, search)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
......
...@@ -120,6 +120,22 @@ func (s *groupRepoStubForAdmin) UpdateSortOrders(_ context.Context, _ []GroupSor ...@@ -120,6 +120,22 @@ func (s *groupRepoStubForAdmin) UpdateSortOrders(_ context.Context, _ []GroupSor
return nil return nil
} }
func TestAdminService_ListGroups_PassesSortParams(t *testing.T) {
repo := &groupRepoStubForAdmin{
listWithFiltersGroups: []Group{{ID: 1, Name: "g1"}},
}
svc := &adminServiceImpl{groupRepo: repo}
_, _, err := svc.ListGroups(context.Background(), 3, 25, PlatformOpenAI, StatusActive, "needle", nil, "account_count", "ASC")
require.NoError(t, err)
require.Equal(t, pagination.PaginationParams{
Page: 3,
PageSize: 25,
SortBy: "account_count",
SortOrder: "ASC",
}, repo.listWithFiltersParams)
}
// TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递 // TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递
func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) {
repo := &groupRepoStubForAdmin{} repo := &groupRepoStubForAdmin{}
...@@ -258,7 +274,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { ...@@ -258,7 +274,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{groupRepo: repo} svc := &adminServiceImpl{groupRepo: repo}
groups, total, err := svc.ListGroups(context.Background(), 1, 20, "", "", "alpha", nil) groups, total, err := svc.ListGroups(context.Background(), 1, 20, "", "", "alpha", nil, "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), total) require.Equal(t, int64(1), total)
require.Equal(t, []Group{{ID: 1, Name: "alpha"}}, groups) require.Equal(t, []Group{{ID: 1, Name: "alpha"}}, groups)
...@@ -276,7 +292,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { ...@@ -276,7 +292,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{groupRepo: repo} svc := &adminServiceImpl{groupRepo: repo}
groups, total, err := svc.ListGroups(context.Background(), 2, 10, "", "", "", nil) groups, total, err := svc.ListGroups(context.Background(), 2, 10, "", "", "", nil, "", "")
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, groups) require.Empty(t, groups)
require.Equal(t, int64(0), total) require.Equal(t, int64(0), total)
...@@ -295,7 +311,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { ...@@ -295,7 +311,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{groupRepo: repo} svc := &adminServiceImpl{groupRepo: repo}
groups, total, err := svc.ListGroups(context.Background(), 3, 50, PlatformAntigravity, StatusActive, "beta", &isExclusive) groups, total, err := svc.ListGroups(context.Background(), 3, 50, PlatformAntigravity, StatusActive, "beta", &isExclusive, "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(42), total) require.Equal(t, int64(42), total)
require.Equal(t, []Group{{ID: 2, Name: "beta"}}, groups) require.Equal(t, []Group{{ID: 2, Name: "beta"}}, groups)
......
...@@ -13,11 +13,13 @@ import ( ...@@ -13,11 +13,13 @@ import (
type userRepoStubForListUsers struct { type userRepoStubForListUsers struct {
userRepoStub userRepoStub
users []User users []User
err error err error
listWithFiltersParams pagination.PaginationParams
} }
func (s *userRepoStubForListUsers) ListWithFilters(_ context.Context, params pagination.PaginationParams, _ UserListFilters) ([]User, *pagination.PaginationResult, error) { func (s *userRepoStubForListUsers) ListWithFilters(_ context.Context, params pagination.PaginationParams, _ UserListFilters) ([]User, *pagination.PaginationResult, error) {
s.listWithFiltersParams = params
if s.err != nil { if s.err != nil {
return nil, nil, s.err return nil, nil, s.err
} }
...@@ -103,7 +105,7 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) { ...@@ -103,7 +105,7 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) {
userGroupRateRepo: rateRepo, userGroupRateRepo: rateRepo,
} }
users, total, err := svc.ListUsers(context.Background(), 1, 20, UserListFilters{}) users, total, err := svc.ListUsers(context.Background(), 1, 20, UserListFilters{}, "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(2), total) require.Equal(t, int64(2), total)
require.Len(t, users, 2) require.Len(t, users, 2)
...@@ -112,3 +114,19 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) { ...@@ -112,3 +114,19 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) {
require.Equal(t, 1.1, users[0].GroupRates[11]) require.Equal(t, 1.1, users[0].GroupRates[11])
require.Equal(t, 2.2, users[1].GroupRates[22]) require.Equal(t, 2.2, users[1].GroupRates[22])
} }
func TestAdminService_ListUsers_PassesSortParams(t *testing.T) {
userRepo := &userRepoStubForListUsers{
users: []User{{ID: 1, Email: "a@example.com"}},
}
svc := &adminServiceImpl{userRepo: userRepo}
_, _, err := svc.ListUsers(context.Background(), 2, 50, UserListFilters{}, "email", "ASC")
require.NoError(t, err)
require.Equal(t, pagination.PaginationParams{
Page: 2,
PageSize: 50,
SortBy: "email",
SortOrder: "ASC",
}, userRepo.listWithFiltersParams)
}
...@@ -170,13 +170,13 @@ func TestAdminService_ListAccounts_WithSearch(t *testing.T) { ...@@ -170,13 +170,13 @@ func TestAdminService_ListAccounts_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{accountRepo: repo} svc := &adminServiceImpl{accountRepo: repo}
accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformGemini, AccountTypeOAuth, StatusActive, "acc", 0, "") accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformGemini, AccountTypeOAuth, StatusActive, "acc", 0, "", "name", "ASC")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(10), total) require.Equal(t, int64(10), total)
require.Equal(t, []Account{{ID: 1, Name: "acc"}}, accounts) require.Equal(t, []Account{{ID: 1, Name: "acc"}}, accounts)
require.Equal(t, 1, repo.listWithFiltersCalls) require.Equal(t, 1, repo.listWithFiltersCalls)
require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20}, repo.listWithFiltersParams) require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20, SortBy: "name", SortOrder: "ASC"}, repo.listWithFiltersParams)
require.Equal(t, PlatformGemini, repo.listWithFiltersPlatform) require.Equal(t, PlatformGemini, repo.listWithFiltersPlatform)
require.Equal(t, AccountTypeOAuth, repo.listWithFiltersType) require.Equal(t, AccountTypeOAuth, repo.listWithFiltersType)
require.Equal(t, StatusActive, repo.listWithFiltersStatus) require.Equal(t, StatusActive, repo.listWithFiltersStatus)
...@@ -192,7 +192,7 @@ func TestAdminService_ListAccounts_WithPrivacyMode(t *testing.T) { ...@@ -192,7 +192,7 @@ func TestAdminService_ListAccounts_WithPrivacyMode(t *testing.T) {
} }
svc := &adminServiceImpl{accountRepo: repo} svc := &adminServiceImpl{accountRepo: repo}
accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, StatusActive, "acc2", 0, PrivacyModeCFBlocked) accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, StatusActive, "acc2", 0, PrivacyModeCFBlocked, "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), total) require.Equal(t, int64(1), total)
require.Equal(t, []Account{{ID: 2, Name: "acc2"}}, accounts) require.Equal(t, []Account{{ID: 2, Name: "acc2"}}, accounts)
...@@ -208,13 +208,13 @@ func TestAdminService_ListProxies_WithSearch(t *testing.T) { ...@@ -208,13 +208,13 @@ func TestAdminService_ListProxies_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{proxyRepo: repo} svc := &adminServiceImpl{proxyRepo: repo}
proxies, total, err := svc.ListProxies(context.Background(), 3, 50, "http", StatusActive, "p1") proxies, total, err := svc.ListProxies(context.Background(), 3, 50, "http", StatusActive, "p1", "name", "ASC")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(7), total) require.Equal(t, int64(7), total)
require.Equal(t, []Proxy{{ID: 2, Name: "p1"}}, proxies) require.Equal(t, []Proxy{{ID: 2, Name: "p1"}}, proxies)
require.Equal(t, 1, repo.listWithFiltersCalls) require.Equal(t, 1, repo.listWithFiltersCalls)
require.Equal(t, pagination.PaginationParams{Page: 3, PageSize: 50}, repo.listWithFiltersParams) require.Equal(t, pagination.PaginationParams{Page: 3, PageSize: 50, SortBy: "name", SortOrder: "ASC"}, repo.listWithFiltersParams)
require.Equal(t, "http", repo.listWithFiltersProtocol) require.Equal(t, "http", repo.listWithFiltersProtocol)
require.Equal(t, StatusActive, repo.listWithFiltersStatus) require.Equal(t, StatusActive, repo.listWithFiltersStatus)
require.Equal(t, "p1", repo.listWithFiltersSearch) require.Equal(t, "p1", repo.listWithFiltersSearch)
...@@ -229,13 +229,13 @@ func TestAdminService_ListProxiesWithAccountCount_WithSearch(t *testing.T) { ...@@ -229,13 +229,13 @@ func TestAdminService_ListProxiesWithAccountCount_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{proxyRepo: repo} svc := &adminServiceImpl{proxyRepo: repo}
proxies, total, err := svc.ListProxiesWithAccountCount(context.Background(), 2, 10, "socks5", StatusDisabled, "p2") proxies, total, err := svc.ListProxiesWithAccountCount(context.Background(), 2, 10, "socks5", StatusDisabled, "p2", "account_count", "DESC")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(9), total) require.Equal(t, int64(9), total)
require.Equal(t, []ProxyWithAccountCount{{Proxy: Proxy{ID: 3, Name: "p2"}, AccountCount: 5}}, proxies) require.Equal(t, []ProxyWithAccountCount{{Proxy: Proxy{ID: 3, Name: "p2"}, AccountCount: 5}}, proxies)
require.Equal(t, 1, repo.listWithFiltersAndAccountCountCalls) require.Equal(t, 1, repo.listWithFiltersAndAccountCountCalls)
require.Equal(t, pagination.PaginationParams{Page: 2, PageSize: 10}, repo.listWithFiltersAndAccountCountParams) require.Equal(t, pagination.PaginationParams{Page: 2, PageSize: 10, SortBy: "account_count", SortOrder: "DESC"}, repo.listWithFiltersAndAccountCountParams)
require.Equal(t, "socks5", repo.listWithFiltersAndAccountCountProtocol) require.Equal(t, "socks5", repo.listWithFiltersAndAccountCountProtocol)
require.Equal(t, StatusDisabled, repo.listWithFiltersAndAccountCountStatus) require.Equal(t, StatusDisabled, repo.listWithFiltersAndAccountCountStatus)
require.Equal(t, "p2", repo.listWithFiltersAndAccountCountSearch) require.Equal(t, "p2", repo.listWithFiltersAndAccountCountSearch)
...@@ -250,13 +250,13 @@ func TestAdminService_ListRedeemCodes_WithSearch(t *testing.T) { ...@@ -250,13 +250,13 @@ func TestAdminService_ListRedeemCodes_WithSearch(t *testing.T) {
} }
svc := &adminServiceImpl{redeemCodeRepo: repo} svc := &adminServiceImpl{redeemCodeRepo: repo}
codes, total, err := svc.ListRedeemCodes(context.Background(), 1, 20, RedeemTypeBalance, StatusUnused, "ABC") codes, total, err := svc.ListRedeemCodes(context.Background(), 1, 20, RedeemTypeBalance, StatusUnused, "ABC", "value", "ASC")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(3), total) require.Equal(t, int64(3), total)
require.Equal(t, []RedeemCode{{ID: 4, Code: "ABC"}}, codes) require.Equal(t, []RedeemCode{{ID: 4, Code: "ABC"}}, codes)
require.Equal(t, 1, repo.listWithFiltersCalls) require.Equal(t, 1, repo.listWithFiltersCalls)
require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20}, repo.listWithFiltersParams) require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20, SortBy: "value", SortOrder: "ASC"}, repo.listWithFiltersParams)
require.Equal(t, RedeemTypeBalance, repo.listWithFiltersType) require.Equal(t, RedeemTypeBalance, repo.listWithFiltersType)
require.Equal(t, StatusUnused, repo.listWithFiltersStatus) require.Equal(t, StatusUnused, repo.listWithFiltersStatus)
require.Equal(t, "ABC", repo.listWithFiltersSearch) require.Equal(t, "ABC", repo.listWithFiltersSearch)
......
...@@ -116,6 +116,8 @@ const ( ...@@ -116,6 +116,8 @@ const (
SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮 SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口 SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src) SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src)
SettingKeyTableDefaultPageSize = "table_default_page_size" // 表格默认每页条数
SettingKeyTablePageSizeOptions = "table_page_size_options" // 表格可选每页条数(JSON 数组)
SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组) SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组)
SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组) SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组)
......
...@@ -492,7 +492,7 @@ func TestAdminService_ListAccounts_ExhaustedCodexExtraReturnsRateLimitedAccount( ...@@ -492,7 +492,7 @@ func TestAdminService_ListAccounts_ExhaustedCodexExtraReturnsRateLimitedAccount(
} }
svc := &adminServiceImpl{accountRepo: repo} svc := &adminServiceImpl{accountRepo: repo}
accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, "", "", 0, "") accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, "", "", 0, "", "", "")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), total) require.Equal(t, int64(1), total)
require.Len(t, accounts, 1) require.Len(t, accounts, 1)
......
...@@ -38,6 +38,8 @@ export async function list( ...@@ -38,6 +38,8 @@ export async function list(
search?: string search?: string
privacy_mode?: string privacy_mode?: string
lite?: string lite?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
...@@ -71,6 +73,8 @@ export async function listWithEtag( ...@@ -71,6 +73,8 @@ export async function listWithEtag(
search?: string search?: string
privacy_mode?: string privacy_mode?: string
lite?: string lite?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
...@@ -500,7 +504,11 @@ export async function exportData(options?: { ...@@ -500,7 +504,11 @@ export async function exportData(options?: {
platform?: string platform?: string
type?: string type?: string
status?: string status?: string
group?: string
privacy_mode?: string
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
} }
includeProxies?: boolean includeProxies?: boolean
}): Promise<AdminDataPayload> { }): Promise<AdminDataPayload> {
...@@ -508,11 +516,15 @@ export async function exportData(options?: { ...@@ -508,11 +516,15 @@ export async function exportData(options?: {
if (options?.ids && options.ids.length > 0) { if (options?.ids && options.ids.length > 0) {
params.ids = options.ids.join(',') params.ids = options.ids.join(',')
} else if (options?.filters) { } else if (options?.filters) {
const { platform, type, status, search } = options.filters const { platform, type, status, group, privacy_mode, search, sort_by, sort_order } = options.filters
if (platform) params.platform = platform if (platform) params.platform = platform
if (type) params.type = type if (type) params.type = type
if (status) params.status = status if (status) params.status = status
if (group) params.group = group
if (privacy_mode) params.privacy_mode = privacy_mode
if (search) params.search = search if (search) params.search = search
if (sort_by) params.sort_by = sort_by
if (sort_order) params.sort_order = sort_order
} }
if (options?.includeProxies === false) { if (options?.includeProxies === false) {
params.include_proxies = 'false' params.include_proxies = 'false'
......
...@@ -17,10 +17,16 @@ export async function list( ...@@ -17,10 +17,16 @@ export async function list(
filters?: { filters?: {
status?: string status?: string
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
},
options?: {
signal?: AbortSignal
} }
): Promise<BasePaginationResponse<Announcement>> { ): Promise<BasePaginationResponse<Announcement>> {
const { data } = await apiClient.get<BasePaginationResponse<Announcement>>('/admin/announcements', { const { data } = await apiClient.get<BasePaginationResponse<Announcement>>('/admin/announcements', {
params: { page, page_size: pageSize, ...filters } params: { page, page_size: pageSize, ...filters },
signal: options?.signal
}) })
return data return data
} }
...@@ -49,11 +55,21 @@ export async function getReadStatus( ...@@ -49,11 +55,21 @@ export async function getReadStatus(
id: number, id: number,
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
search: string = '' filters?: {
search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
},
options?: {
signal?: AbortSignal
}
): Promise<BasePaginationResponse<AnnouncementUserReadStatus>> { ): Promise<BasePaginationResponse<AnnouncementUserReadStatus>> {
const { data } = await apiClient.get<BasePaginationResponse<AnnouncementUserReadStatus>>( const { data } = await apiClient.get<BasePaginationResponse<AnnouncementUserReadStatus>>(
`/admin/announcements/${id}/read-status`, `/admin/announcements/${id}/read-status`,
{ params: { page, page_size: pageSize, search } } {
params: { page, page_size: pageSize, ...filters },
signal: options?.signal
}
) )
return data return data
} }
...@@ -68,4 +84,3 @@ const announcementsAPI = { ...@@ -68,4 +84,3 @@ const announcementsAPI = {
} }
export default announcementsAPI export default announcementsAPI
...@@ -83,6 +83,8 @@ export async function list( ...@@ -83,6 +83,8 @@ export async function list(
filters?: { filters?: {
status?: string status?: string
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { signal?: AbortSignal } options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<Channel>> { ): Promise<PaginatedResponse<Channel>> {
......
...@@ -27,6 +27,8 @@ export async function list( ...@@ -27,6 +27,8 @@ export async function list(
status?: 'active' | 'inactive' status?: 'active' | 'inactive'
is_exclusive?: boolean is_exclusive?: boolean
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
......
...@@ -17,10 +17,16 @@ export async function list( ...@@ -17,10 +17,16 @@ export async function list(
filters?: { filters?: {
status?: string status?: string
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
},
options?: {
signal?: AbortSignal
} }
): Promise<BasePaginationResponse<PromoCode>> { ): Promise<BasePaginationResponse<PromoCode>> {
const { data } = await apiClient.get<BasePaginationResponse<PromoCode>>('/admin/promo-codes', { const { data } = await apiClient.get<BasePaginationResponse<PromoCode>>('/admin/promo-codes', {
params: { page, page_size: pageSize, ...filters } params: { page, page_size: pageSize, ...filters },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -29,6 +29,8 @@ export async function list( ...@@ -29,6 +29,8 @@ export async function list(
protocol?: string protocol?: string
status?: 'active' | 'inactive' status?: 'active' | 'inactive'
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
...@@ -227,16 +229,20 @@ export async function exportData(options?: { ...@@ -227,16 +229,20 @@ export async function exportData(options?: {
protocol?: string protocol?: string
status?: 'active' | 'inactive' status?: 'active' | 'inactive'
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
} }
}): Promise<AdminDataPayload> { }): Promise<AdminDataPayload> {
const params: Record<string, string> = {} const params: Record<string, string> = {}
if (options?.ids && options.ids.length > 0) { if (options?.ids && options.ids.length > 0) {
params.ids = options.ids.join(',') params.ids = options.ids.join(',')
} else if (options?.filters) { } else if (options?.filters) {
const { protocol, status, search } = options.filters const { protocol, status, search, sort_by, sort_order } = options.filters
if (protocol) params.protocol = protocol if (protocol) params.protocol = protocol
if (status) params.status = status if (status) params.status = status
if (search) params.search = search if (search) params.search = search
if (sort_by) params.sort_by = sort_by
if (sort_order) params.sort_order = sort_order
} }
const { data } = await apiClient.get<AdminDataPayload>('/admin/proxies/data', { params }) const { data } = await apiClient.get<AdminDataPayload>('/admin/proxies/data', { params })
return data return data
......
...@@ -81,6 +81,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams { ...@@ -81,6 +81,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
user_id?: number user_id?: number
exact_total?: boolean exact_total?: boolean
billing_mode?: string billing_mode?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
} }
// ==================== API Functions ==================== // ==================== API Functions ====================
......
...@@ -24,6 +24,8 @@ export async function list( ...@@ -24,6 +24,8 @@ export async function list(
group_name?: string // fuzzy filter by allowed group name group_name?: string // fuzzy filter by allowed group name
attributes?: Record<number, string> // attributeId -> value attributes?: Record<number, string> // attributeId -> value
include_subscriptions?: boolean include_subscriptions?: boolean
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
...@@ -37,7 +39,9 @@ export async function list( ...@@ -37,7 +39,9 @@ export async function list(
role: filters?.role, role: filters?.role,
search: filters?.search, search: filters?.search,
group_name: filters?.group_name, group_name: filters?.group_name,
include_subscriptions: filters?.include_subscriptions include_subscriptions: filters?.include_subscriptions,
sort_by: filters?.sort_by,
sort_order: filters?.sort_order
} }
// Add attribute filters as attr[id]=value // Add attribute filters as attr[id]=value
......
...@@ -17,7 +17,13 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons ...@@ -17,7 +17,13 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons
export async function list( export async function list(
page: number = 1, page: number = 1,
pageSize: number = 10, pageSize: number = 10,
filters?: { search?: string; status?: string; group_id?: number | string }, filters?: {
search?: string
status?: string
group_id?: number | string
sort_by?: string
sort_order?: 'asc' | 'desc'
},
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
} }
......
...@@ -91,7 +91,7 @@ export async function list( ...@@ -91,7 +91,7 @@ export async function list(
* @returns Paginated list of usage logs * @returns Paginated list of usage logs
*/ */
export async function query( export async function query(
params: UsageQueryParams, params: UsageQueryParams & { sort_by?: string; sort_order?: 'asc' | 'desc' },
config: { signal?: AbortSignal } = {} config: { signal?: AbortSignal } = {}
): Promise<PaginatedResponse<UsageLog>> { ): Promise<PaginatedResponse<UsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', {
......
...@@ -21,7 +21,15 @@ ...@@ -21,7 +21,15 @@
</button> </button>
</div> </div>
<DataTable :columns="columns" :data="items" :loading="loading"> <DataTable
:columns="columns"
:data="items"
:loading="loading"
:server-side-sort="true"
default-sort-key="email"
default-sort-order="asc"
@sort="handleSort"
>
<template #cell-email="{ value }"> <template #cell-email="{ 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>
...@@ -62,7 +70,7 @@ ...@@ -62,7 +70,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onUnmounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
...@@ -98,23 +106,54 @@ const pagination = reactive({ ...@@ -98,23 +106,54 @@ const pagination = reactive({
pages: 0 pages: 0
}) })
const sortState = reactive({
sort_by: 'email',
sort_order: 'asc' as 'asc' | 'desc'
})
const items = ref<AnnouncementUserReadStatus[]>([]) const items = ref<AnnouncementUserReadStatus[]>([])
const columns = computed<Column[]>(() => [ const columns = computed<Column[]>(() => [
{ key: 'email', label: t('common.email') }, { key: 'email', label: t('common.email'), sortable: true },
{ key: 'username', label: t('admin.users.columns.username') }, { key: 'username', label: t('admin.users.columns.username'), sortable: true },
{ key: 'balance', label: t('common.balance') }, { key: 'balance', label: t('common.balance'), sortable: true },
{ key: 'eligible', label: t('admin.announcements.eligible') }, { key: 'eligible', label: t('admin.announcements.eligible') },
{ key: 'read_at', label: t('admin.announcements.readAt') } { key: 'read_at', label: t('admin.announcements.readAt') }
]) ])
let currentController: AbortController | null = null let currentController: AbortController | null = null
let searchDebounceTimer: number | null = null
function resetDialogState() {
loading.value = false
search.value = ''
items.value = []
pagination.page = 1
pagination.total = 0
pagination.pages = 0
sortState.sort_by = 'email'
sortState.sort_order = 'asc'
}
function cancelPendingLoad(resetState = false) {
if (searchDebounceTimer) {
window.clearTimeout(searchDebounceTimer)
searchDebounceTimer = null
}
currentController?.abort()
currentController = null
if (resetState) {
resetDialogState()
}
}
async function load() { async function load() {
if (!props.show || !props.announcementId) return if (!props.show || !props.announcementId) return
if (currentController) currentController.abort() currentController?.abort()
currentController = new AbortController() const requestController = new AbortController()
currentController = requestController
const { signal } = requestController
try { try {
loading.value = true loading.value = true
...@@ -122,20 +161,37 @@ async function load() { ...@@ -122,20 +161,37 @@ async function load() {
props.announcementId, props.announcementId,
pagination.page, pagination.page,
pagination.page_size, pagination.page_size,
search.value {
search: search.value,
sort_by: sortState.sort_by,
sort_order: sortState.sort_order
},
{ signal }
) )
if (signal.aborted || currentController !== requestController) return
items.value = res.items items.value = res.items
pagination.total = res.total pagination.total = res.total
pagination.pages = res.pages pagination.pages = res.pages
pagination.page = res.page pagination.page = res.page
pagination.page_size = res.page_size pagination.page_size = res.page_size
} catch (error: any) { } catch (error: any) {
if (currentController.signal.aborted || error?.name === 'AbortError') return if (
signal.aborted ||
currentController !== requestController ||
error?.name === 'AbortError' ||
error?.code === 'ERR_CANCELED'
) {
return
}
console.error('Failed to load read status:', error) console.error('Failed to load read status:', error)
appStore.showError(error.response?.data?.detail || t('admin.announcements.failedToLoadReadStatus')) appStore.showError(error.response?.data?.detail || t('admin.announcements.failedToLoadReadStatus'))
} finally { } finally {
loading.value = false if (currentController === requestController) {
loading.value = false
currentController = null
}
} }
} }
...@@ -150,7 +206,13 @@ function handlePageSizeChange(pageSize: number) { ...@@ -150,7 +206,13 @@ function handlePageSizeChange(pageSize: number) {
load() load()
} }
let searchDebounceTimer: number | null = null function handleSort(key: string, order: 'asc' | 'desc') {
sortState.sort_by = key
sortState.sort_order = order
pagination.page = 1
load()
}
function handleSearch() { function handleSearch() {
if (searchDebounceTimer) window.clearTimeout(searchDebounceTimer) if (searchDebounceTimer) window.clearTimeout(searchDebounceTimer)
searchDebounceTimer = window.setTimeout(() => { searchDebounceTimer = window.setTimeout(() => {
...@@ -160,13 +222,17 @@ function handleSearch() { ...@@ -160,13 +222,17 @@ function handleSearch() {
} }
function handleClose() { function handleClose() {
cancelPendingLoad(true)
emit('close') emit('close')
} }
watch( watch(
() => props.show, () => props.show,
(v) => { (v) => {
if (!v) return if (!v) {
cancelPendingLoad(true)
return
}
pagination.page = 1 pagination.page = 1
load() load()
} }
...@@ -181,7 +247,7 @@ watch( ...@@ -181,7 +247,7 @@ watch(
} }
) )
onMounted(() => { onUnmounted(() => {
// noop cancelPendingLoad()
}) })
</script> </script>
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import AnnouncementReadStatusDialog from '../AnnouncementReadStatusDialog.vue'
const { getReadStatus, showError } = vi.hoisted(() => ({
getReadStatus: vi.fn(),
showError: vi.fn(),
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
announcements: {
getReadStatus,
},
},
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError,
}),
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key,
}),
}
})
vi.mock('@/composables/usePersistedPageSize', () => ({
getPersistedPageSize: () => 20,
}))
const BaseDialogStub = {
props: ['show', 'title', 'width'],
emits: ['close'],
template: '<div><slot /><slot name="footer" /></div>',
}
describe('AnnouncementReadStatusDialog', () => {
beforeEach(() => {
getReadStatus.mockReset()
showError.mockReset()
vi.useFakeTimers()
})
it('closes by aborting active requests and clearing debounced reloads', async () => {
let activeSignal: AbortSignal | undefined
getReadStatus.mockImplementation(async (...args: any[]) => {
activeSignal = args[4]?.signal
return new Promise(() => {})
})
const wrapper = mount(AnnouncementReadStatusDialog, {
props: {
show: false,
announcementId: 1,
},
global: {
stubs: {
BaseDialog: BaseDialogStub,
DataTable: true,
Pagination: true,
Icon: true,
},
},
})
await wrapper.setProps({ show: true })
await flushPromises()
expect(getReadStatus).toHaveBeenCalledTimes(1)
expect(activeSignal?.aborted).toBe(false)
const setupState = (wrapper.vm as any).$?.setupState
setupState.search = 'alice'
setupState.handleSearch()
setupState.handleClose()
await flushPromises()
expect(activeSignal?.aborted).toBe(true)
expect(wrapper.emitted('close')).toHaveLength(1)
vi.advanceTimersByTime(350)
await flushPromises()
expect(getReadStatus).toHaveBeenCalledTimes(1)
})
})
...@@ -196,7 +196,6 @@ ...@@ -196,7 +196,6 @@
:total="localEntries.length" :total="localEntries.length"
:page="currentPage" :page="currentPage"
:page-size="pageSize" :page-size="pageSize"
:page-size-options="[10, 20, 50]"
@update:page="currentPage = $event" @update:page="currentPage = $event"
@update:pageSize="handlePageSizeChange" @update:pageSize="handlePageSizeChange"
/> />
......
<template> <template>
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div class="overflow-auto"> <div class="overflow-auto">
<DataTable :columns="columns" :data="data" :loading="loading"> <DataTable
:columns="columns"
:data="data"
:loading="loading"
:server-side-sort="serverSideSort"
:default-sort-key="defaultSortKey"
:default-sort-order="defaultSortOrder"
@sort="(key, order) => $emit('sort', key, order)"
>
<template #cell-user="{ row }"> <template #cell-user="{ row }">
<div class="text-sm"> <div class="text-sm">
<button <button
...@@ -334,9 +342,27 @@ import DataTable from '@/components/common/DataTable.vue' ...@@ -334,9 +342,27 @@ import DataTable from '@/components/common/DataTable.vue'
import EmptyState from '@/components/common/EmptyState.vue' import EmptyState from '@/components/common/EmptyState.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
import type { AdminUsageLog } from '@/types' import type { AdminUsageLog } from '@/types'
import type { Column } from '@/components/common/types'
interface Props {
data: AdminUsageLog[]
loading?: boolean
columns: Column[]
serverSideSort?: boolean
defaultSortKey?: string
defaultSortOrder?: 'asc' | 'desc'
}
defineProps(['data', 'loading', 'columns']) withDefaults(defineProps<Props>(), {
defineEmits(['userClick']) loading: false,
serverSideSort: false,
defaultSortKey: '',
defaultSortOrder: 'asc'
})
defineEmits<{
userClick: [userID: number, email?: string]
sort: [key: string, order: 'asc' | 'desc']
}>()
const { t } = useI18n() const { t } = useI18n()
// Tooltip state - cost // Tooltip state - cost
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment