From d648811233195e2b8bed25c7c9d63d96f99284a5 Mon Sep 17 00:00:00 2001 From: erio Date: Thu, 12 Mar 2026 23:37:36 +0800 Subject: [PATCH] feat(groups): add rate multipliers management modal Add a dedicated modal in group management for viewing, adding, editing, and deleting per-user rate multipliers within a group. Backend: - GET /admin/groups/:id/rate-multipliers - list entries with user details - PUT /admin/groups/:id/rate-multipliers - batch sync (full replace) - DELETE /admin/groups/:id/rate-multipliers - clear all entries - Repository: GetByGroupID, SyncGroupRateMultipliers methods on user_group_rate_multipliers table (same table as user-side rates) Frontend: - New GroupRateMultipliersModal component with: - User search and add with email autocomplete - Editable rate column with local edit mode (cancel/save) - Batch adjust: multiply all rates by a factor - Clear all (local operation, requires save to persist) - Pagination (10/20/50 per page) - Platform icon with brand colors in group info bar - Unsaved changes indicator with revert option - Unit tests for all three backend endpoints --- .../handler/admin/admin_service_stub_test.go | 12 + .../internal/handler/admin/group_handler.go | 66 +++ .../repository/user_group_rate_repo.go | 54 ++ backend/internal/server/routes/admin.go | 3 + backend/internal/service/admin_service.go | 24 + .../service/admin_service_group_rate_test.go | 176 +++++++ .../service/admin_service_list_users_test.go | 10 +- backend/internal/service/user_group_rate.go | 22 + frontend/src/api/admin/groups.ts | 54 ++ .../admin/group/GroupRateMultipliersModal.vue | 496 ++++++++++++++++++ frontend/src/i18n/locales/en.ts | 26 +- frontend/src/i18n/locales/zh.ts | 26 +- frontend/src/views/admin/GroupsView.vue | 23 + 13 files changed, 989 insertions(+), 3 deletions(-) create mode 100644 backend/internal/service/admin_service_group_rate_test.go create mode 100644 frontend/src/components/admin/group/GroupRateMultipliersModal.vue diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index 84a9f102..c1ef9e53 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -175,6 +175,18 @@ func (s *stubAdminService) GetGroupAPIKeys(ctx context.Context, groupID int64, p return s.apiKeys, int64(len(s.apiKeys)), nil } +func (s *stubAdminService) GetGroupRateMultipliers(_ context.Context, _ int64) ([]service.UserGroupRateEntry, error) { + return nil, nil +} + +func (s *stubAdminService) ClearGroupRateMultipliers(_ context.Context, _ int64) error { + return nil +} + +func (s *stubAdminService) BatchSetGroupRateMultipliers(_ context.Context, _ int64, _ []service.GroupRateMultiplierInput) error { + return nil +} + func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64) ([]service.Account, int64, error) { return s.accounts, int64(len(s.accounts)), nil } diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 734acaaa..5be66768 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -335,6 +335,72 @@ func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) { response.Paginated(c, outKeys, total, page, pageSize) } +// GetGroupRateMultipliers handles getting rate multipliers for users in a group +// GET /api/v1/admin/groups/:id/rate-multipliers +func (h *GroupHandler) GetGroupRateMultipliers(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + entries, err := h.adminService.GetGroupRateMultipliers(c.Request.Context(), groupID) + if err != nil { + response.ErrorFrom(c, err) + return + } + + if entries == nil { + entries = []service.UserGroupRateEntry{} + } + response.Success(c, entries) +} + +// ClearGroupRateMultipliers handles clearing all rate multipliers for a group +// DELETE /api/v1/admin/groups/:id/rate-multipliers +func (h *GroupHandler) ClearGroupRateMultipliers(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + if err := h.adminService.ClearGroupRateMultipliers(c.Request.Context(), groupID); err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, gin.H{"message": "Rate multipliers cleared successfully"}) +} + +// BatchSetGroupRateMultipliersRequest represents batch set rate multipliers request +type BatchSetGroupRateMultipliersRequest struct { + Entries []service.GroupRateMultiplierInput `json:"entries" binding:"required"` +} + +// BatchSetGroupRateMultipliers handles batch setting rate multipliers for a group +// PUT /api/v1/admin/groups/:id/rate-multipliers +func (h *GroupHandler) BatchSetGroupRateMultipliers(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + var req BatchSetGroupRateMultipliersRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if err := h.adminService.BatchSetGroupRateMultipliers(c.Request.Context(), groupID, req.Entries); err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, gin.H{"message": "Rate multipliers updated successfully"}) +} + // UpdateSortOrderRequest represents the request to update group sort orders type UpdateSortOrderRequest struct { Updates []struct { diff --git a/backend/internal/repository/user_group_rate_repo.go b/backend/internal/repository/user_group_rate_repo.go index e3b11096..e2471ae5 100644 --- a/backend/internal/repository/user_group_rate_repo.go +++ b/backend/internal/repository/user_group_rate_repo.go @@ -95,6 +95,35 @@ func (r *userGroupRateRepository) GetByUserIDs(ctx context.Context, userIDs []in return result, nil } +// GetByGroupID 获取指定分组下所有用户的专属倍率 +func (r *userGroupRateRepository) GetByGroupID(ctx context.Context, groupID int64) ([]service.UserGroupRateEntry, error) { + query := ` + SELECT ugr.user_id, u.username, u.email, COALESCE(u.notes, ''), u.status, ugr.rate_multiplier + FROM user_group_rate_multipliers ugr + JOIN users u ON u.id = ugr.user_id + WHERE ugr.group_id = $1 + ORDER BY ugr.user_id + ` + rows, err := r.sql.QueryContext(ctx, query, groupID) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + var result []service.UserGroupRateEntry + for rows.Next() { + var entry service.UserGroupRateEntry + if err := rows.Scan(&entry.UserID, &entry.UserName, &entry.UserEmail, &entry.UserNotes, &entry.UserStatus, &entry.RateMultiplier); err != nil { + return nil, err + } + result = append(result, entry) + } + if err := rows.Err(); err != nil { + return nil, err + } + return result, nil +} + // GetByUserAndGroup 获取用户在特定分组的专属倍率 func (r *userGroupRateRepository) GetByUserAndGroup(ctx context.Context, userID, groupID int64) (*float64, error) { query := `SELECT rate_multiplier FROM user_group_rate_multipliers WHERE user_id = $1 AND group_id = $2` @@ -164,6 +193,31 @@ func (r *userGroupRateRepository) SyncUserGroupRates(ctx context.Context, userID return nil } +// SyncGroupRateMultipliers 批量同步分组的用户专属倍率(先删后插) +func (r *userGroupRateRepository) SyncGroupRateMultipliers(ctx context.Context, groupID int64, entries []service.GroupRateMultiplierInput) error { + if _, err := r.sql.ExecContext(ctx, `DELETE FROM user_group_rate_multipliers WHERE group_id = $1`, groupID); err != nil { + return err + } + if len(entries) == 0 { + return nil + } + userIDs := make([]int64, len(entries)) + rates := make([]float64, len(entries)) + for i, e := range entries { + userIDs[i] = e.UserID + rates[i] = e.RateMultiplier + } + now := time.Now() + _, err := r.sql.ExecContext(ctx, ` + INSERT INTO user_group_rate_multipliers (user_id, group_id, rate_multiplier, created_at, updated_at) + SELECT data.user_id, $1::bigint, data.rate_multiplier, $2::timestamptz, $2::timestamptz + FROM unnest($3::bigint[], $4::double precision[]) AS data(user_id, rate_multiplier) + ON CONFLICT (user_id, group_id) + DO UPDATE SET rate_multiplier = EXCLUDED.rate_multiplier, updated_at = EXCLUDED.updated_at + `, groupID, now, pq.Array(userIDs), pq.Array(rates)) + return err +} + // DeleteByGroupID 删除指定分组的所有用户专属倍率 func (r *userGroupRateRepository) DeleteByGroupID(ctx context.Context, groupID int64) error { _, err := r.sql.ExecContext(ctx, `DELETE FROM user_group_rate_multipliers WHERE group_id = $1`, groupID) diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 9fdb233b..46c2ccde 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -228,6 +228,9 @@ func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) { groups.PUT("/:id", h.Admin.Group.Update) groups.DELETE("/:id", h.Admin.Group.Delete) groups.GET("/:id/stats", h.Admin.Group.GetStats) + groups.GET("/:id/rate-multipliers", h.Admin.Group.GetGroupRateMultipliers) + groups.PUT("/:id/rate-multipliers", h.Admin.Group.BatchSetGroupRateMultipliers) + groups.DELETE("/:id/rate-multipliers", h.Admin.Group.ClearGroupRateMultipliers) groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys) } } diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index dec4ed33..dbfa17cd 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -42,6 +42,9 @@ type AdminService interface { UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error) DeleteGroup(ctx context.Context, id int64) error GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]APIKey, int64, error) + GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error) + ClearGroupRateMultipliers(ctx context.Context, groupID int64) error + BatchSetGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error // API Key management (admin) @@ -1244,6 +1247,27 @@ func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, p return keys, result.Total, nil } +func (s *adminServiceImpl) GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error) { + if s.userGroupRateRepo == nil { + return nil, nil + } + return s.userGroupRateRepo.GetByGroupID(ctx, groupID) +} + +func (s *adminServiceImpl) ClearGroupRateMultipliers(ctx context.Context, groupID int64) error { + if s.userGroupRateRepo == nil { + return nil + } + return s.userGroupRateRepo.DeleteByGroupID(ctx, groupID) +} + +func (s *adminServiceImpl) BatchSetGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error { + if s.userGroupRateRepo == nil { + return nil + } + return s.userGroupRateRepo.SyncGroupRateMultipliers(ctx, groupID, entries) +} + func (s *adminServiceImpl) UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error { return s.groupRepo.UpdateSortOrders(ctx, updates) } diff --git a/backend/internal/service/admin_service_group_rate_test.go b/backend/internal/service/admin_service_group_rate_test.go new file mode 100644 index 00000000..77635247 --- /dev/null +++ b/backend/internal/service/admin_service_group_rate_test.go @@ -0,0 +1,176 @@ +//go:build unit + +package service + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// userGroupRateRepoStubForGroupRate implements UserGroupRateRepository for group rate tests. +type userGroupRateRepoStubForGroupRate struct { + getByGroupIDData map[int64][]UserGroupRateEntry + getByGroupIDErr error + + deletedGroupIDs []int64 + deleteByGroupErr error + + syncedGroupID int64 + syncedEntries []GroupRateMultiplierInput + syncGroupErr error +} + +func (s *userGroupRateRepoStubForGroupRate) GetByUserID(_ context.Context, _ int64) (map[int64]float64, error) { + panic("unexpected GetByUserID call") +} + +func (s *userGroupRateRepoStubForGroupRate) GetByUserAndGroup(_ context.Context, _, _ int64) (*float64, error) { + panic("unexpected GetByUserAndGroup call") +} + +func (s *userGroupRateRepoStubForGroupRate) GetByGroupID(_ context.Context, groupID int64) ([]UserGroupRateEntry, error) { + if s.getByGroupIDErr != nil { + return nil, s.getByGroupIDErr + } + return s.getByGroupIDData[groupID], nil +} + +func (s *userGroupRateRepoStubForGroupRate) SyncUserGroupRates(_ context.Context, _ int64, _ map[int64]*float64) error { + panic("unexpected SyncUserGroupRates call") +} + +func (s *userGroupRateRepoStubForGroupRate) SyncGroupRateMultipliers(_ context.Context, groupID int64, entries []GroupRateMultiplierInput) error { + s.syncedGroupID = groupID + s.syncedEntries = entries + return s.syncGroupErr +} + +func (s *userGroupRateRepoStubForGroupRate) DeleteByGroupID(_ context.Context, groupID int64) error { + s.deletedGroupIDs = append(s.deletedGroupIDs, groupID) + return s.deleteByGroupErr +} + +func (s *userGroupRateRepoStubForGroupRate) DeleteByUserID(_ context.Context, _ int64) error { + panic("unexpected DeleteByUserID call") +} + +func TestAdminService_GetGroupRateMultipliers(t *testing.T) { + t.Run("returns entries for group", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{ + getByGroupIDData: map[int64][]UserGroupRateEntry{ + 10: { + {UserID: 1, UserName: "alice", UserEmail: "alice@test.com", RateMultiplier: 1.5}, + {UserID: 2, UserName: "bob", UserEmail: "bob@test.com", RateMultiplier: 0.8}, + }, + }, + } + svc := &adminServiceImpl{userGroupRateRepo: repo} + + entries, err := svc.GetGroupRateMultipliers(context.Background(), 10) + require.NoError(t, err) + require.Len(t, entries, 2) + require.Equal(t, int64(1), entries[0].UserID) + require.Equal(t, "alice", entries[0].UserName) + require.Equal(t, 1.5, entries[0].RateMultiplier) + require.Equal(t, int64(2), entries[1].UserID) + require.Equal(t, 0.8, entries[1].RateMultiplier) + }) + + t.Run("returns nil when repo is nil", func(t *testing.T) { + svc := &adminServiceImpl{userGroupRateRepo: nil} + + entries, err := svc.GetGroupRateMultipliers(context.Background(), 10) + require.NoError(t, err) + require.Nil(t, entries) + }) + + t.Run("returns empty slice for group with no entries", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{ + getByGroupIDData: map[int64][]UserGroupRateEntry{}, + } + svc := &adminServiceImpl{userGroupRateRepo: repo} + + entries, err := svc.GetGroupRateMultipliers(context.Background(), 99) + require.NoError(t, err) + require.Nil(t, entries) + }) + + t.Run("propagates repo error", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{ + getByGroupIDErr: errors.New("db error"), + } + svc := &adminServiceImpl{userGroupRateRepo: repo} + + _, err := svc.GetGroupRateMultipliers(context.Background(), 10) + require.Error(t, err) + require.Contains(t, err.Error(), "db error") + }) +} + +func TestAdminService_ClearGroupRateMultipliers(t *testing.T) { + t.Run("deletes by group ID", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{} + svc := &adminServiceImpl{userGroupRateRepo: repo} + + err := svc.ClearGroupRateMultipliers(context.Background(), 42) + require.NoError(t, err) + require.Equal(t, []int64{42}, repo.deletedGroupIDs) + }) + + t.Run("returns nil when repo is nil", func(t *testing.T) { + svc := &adminServiceImpl{userGroupRateRepo: nil} + + err := svc.ClearGroupRateMultipliers(context.Background(), 42) + require.NoError(t, err) + }) + + t.Run("propagates repo error", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{ + deleteByGroupErr: errors.New("delete failed"), + } + svc := &adminServiceImpl{userGroupRateRepo: repo} + + err := svc.ClearGroupRateMultipliers(context.Background(), 42) + require.Error(t, err) + require.Contains(t, err.Error(), "delete failed") + }) +} + +func TestAdminService_BatchSetGroupRateMultipliers(t *testing.T) { + t.Run("syncs entries to repo", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{} + svc := &adminServiceImpl{userGroupRateRepo: repo} + + entries := []GroupRateMultiplierInput{ + {UserID: 1, RateMultiplier: 1.5}, + {UserID: 2, RateMultiplier: 0.8}, + } + err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, entries) + require.NoError(t, err) + require.Equal(t, int64(10), repo.syncedGroupID) + require.Equal(t, entries, repo.syncedEntries) + }) + + t.Run("returns nil when repo is nil", func(t *testing.T) { + svc := &adminServiceImpl{userGroupRateRepo: nil} + + err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, nil) + require.NoError(t, err) + }) + + t.Run("propagates repo error", func(t *testing.T) { + repo := &userGroupRateRepoStubForGroupRate{ + syncGroupErr: errors.New("sync failed"), + } + svc := &adminServiceImpl{userGroupRateRepo: repo} + + err := svc.BatchSetGroupRateMultipliers(context.Background(), 10, []GroupRateMultiplierInput{ + {UserID: 1, RateMultiplier: 1.0}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "sync failed") + }) +} diff --git a/backend/internal/service/admin_service_list_users_test.go b/backend/internal/service/admin_service_list_users_test.go index 8b50530a..37f348df 100644 --- a/backend/internal/service/admin_service_list_users_test.go +++ b/backend/internal/service/admin_service_list_users_test.go @@ -68,7 +68,15 @@ func (s *userGroupRateRepoStubForListUsers) SyncUserGroupRates(_ context.Context panic("unexpected SyncUserGroupRates call") } -func (s *userGroupRateRepoStubForListUsers) DeleteByGroupID(_ context.Context, groupID int64) error { +func (s *userGroupRateRepoStubForListUsers) GetByGroupID(_ context.Context, _ int64) ([]UserGroupRateEntry, error) { + panic("unexpected GetByGroupID call") +} + +func (s *userGroupRateRepoStubForListUsers) SyncGroupRateMultipliers(_ context.Context, _ int64, _ []GroupRateMultiplierInput) error { + panic("unexpected SyncGroupRateMultipliers call") +} + +func (s *userGroupRateRepoStubForListUsers) DeleteByGroupID(_ context.Context, _ int64) error { panic("unexpected DeleteByGroupID call") } diff --git a/backend/internal/service/user_group_rate.go b/backend/internal/service/user_group_rate.go index 9eb5f067..3d221a25 100644 --- a/backend/internal/service/user_group_rate.go +++ b/backend/internal/service/user_group_rate.go @@ -2,6 +2,22 @@ package service import "context" +// UserGroupRateEntry 分组下用户专属倍率条目 +type UserGroupRateEntry struct { + UserID int64 `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + UserNotes string `json:"user_notes"` + UserStatus string `json:"user_status"` + RateMultiplier float64 `json:"rate_multiplier"` +} + +// GroupRateMultiplierInput 批量设置分组倍率的输入条目 +type GroupRateMultiplierInput struct { + UserID int64 `json:"user_id"` + RateMultiplier float64 `json:"rate_multiplier"` +} + // UserGroupRateRepository 用户专属分组倍率仓储接口 // 允许管理员为特定用户设置分组的专属计费倍率,覆盖分组默认倍率 type UserGroupRateRepository interface { @@ -13,10 +29,16 @@ type UserGroupRateRepository interface { // 如果未设置专属倍率,返回 nil GetByUserAndGroup(ctx context.Context, userID, groupID int64) (*float64, error) + // GetByGroupID 获取指定分组下所有用户的专属倍率 + GetByGroupID(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error) + // SyncUserGroupRates 同步用户的分组专属倍率 // rates: map[groupID]*rateMultiplier,nil 表示删除该分组的专属倍率 SyncUserGroupRates(ctx context.Context, userID int64, rates map[int64]*float64) error + // SyncGroupRateMultipliers 批量同步分组的用户专属倍率(替换整组数据) + SyncGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error + // DeleteByGroupID 删除指定分组的所有用户专属倍率(分组删除时调用) DeleteByGroupID(ctx context.Context, groupID int64) error diff --git a/frontend/src/api/admin/groups.ts b/frontend/src/api/admin/groups.ts index 3d18ba87..7c2658fa 100644 --- a/frontend/src/api/admin/groups.ts +++ b/frontend/src/api/admin/groups.ts @@ -153,6 +153,30 @@ export async function getGroupApiKeys( return data } +/** + * Rate multiplier entry for a user in a group + */ +export interface GroupRateMultiplierEntry { + user_id: number + user_name: string + user_email: string + user_notes: string + user_status: string + rate_multiplier: number +} + +/** + * Get rate multipliers for users in a group + * @param id - Group ID + * @returns List of user rate multiplier entries + */ +export async function getGroupRateMultipliers(id: number): Promise { + const { data } = await apiClient.get( + `/admin/groups/${id}/rate-multipliers` + ) + return data +} + /** * Update group sort orders * @param updates - Array of { id, sort_order } objects @@ -167,6 +191,33 @@ export async function updateSortOrder( return data } +/** + * Clear all rate multipliers for a group + * @param id - Group ID + * @returns Success confirmation + */ +export async function clearGroupRateMultipliers(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/groups/${id}/rate-multipliers`) + return data +} + +/** + * Batch set rate multipliers for users in a group + * @param id - Group ID + * @param entries - Array of { user_id, rate_multiplier } + * @returns Success confirmation + */ +export async function batchSetGroupRateMultipliers( + id: number, + entries: Array<{ user_id: number; rate_multiplier: number }> +): Promise<{ message: string }> { + const { data } = await apiClient.put<{ message: string }>( + `/admin/groups/${id}/rate-multipliers`, + { entries } + ) + return data +} + export const groupsAPI = { list, getAll, @@ -178,6 +229,9 @@ export const groupsAPI = { toggleStatus, getStats, getGroupApiKeys, + getGroupRateMultipliers, + clearGroupRateMultipliers, + batchSetGroupRateMultipliers, updateSortOrder } diff --git a/frontend/src/components/admin/group/GroupRateMultipliersModal.vue b/frontend/src/components/admin/group/GroupRateMultipliersModal.vue new file mode 100644 index 00000000..cbd18af6 --- /dev/null +++ b/frontend/src/components/admin/group/GroupRateMultipliersModal.vue @@ -0,0 +1,496 @@ + + + + + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9fd0c006..c9eae3ab 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1372,7 +1372,11 @@ export default { accounts: 'Accounts', status: 'Status', actions: 'Actions', - billingType: 'Billing Type' + billingType: 'Billing Type', + userName: 'Username', + userEmail: 'Email', + userNotes: 'Notes', + userStatus: 'Status' }, rateAndAccounts: '{rate}x rate · {count} accounts', accountsCount: '{count} accounts', @@ -1411,6 +1415,26 @@ export default { failedToUpdate: 'Failed to update group', failedToDelete: 'Failed to delete group', nameRequired: 'Please enter group name', + rateMultipliers: 'Rate Multipliers', + rateMultipliersTitle: 'Group Rate Multipliers', + addUserRate: 'Add User Rate Multiplier', + searchUserPlaceholder: 'Search user email...', + noRateMultipliers: 'No user rate multipliers configured', + rateUpdated: 'Rate multiplier updated', + rateDeleted: 'Rate multiplier removed', + rateAdded: 'Rate multiplier added', + clearAll: 'Clear All', + confirmClearAll: 'Are you sure you want to clear all rate multiplier settings for this group? This cannot be undone.', + rateCleared: 'All rate multipliers cleared', + batchAdjust: 'Batch Adjust Rates', + multiplierFactor: 'Factor', + applyMultiplier: 'Apply', + rateAdjusted: 'Rates adjusted successfully', + rateSaved: 'Rate multipliers saved', + finalRate: 'Final Rate', + unsavedChanges: 'Unsaved changes', + revertChanges: 'Revert', + userInfo: 'User Info', platforms: { all: 'All Platforms', anthropic: 'Anthropic', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index d139cd34..4a663de1 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1428,7 +1428,11 @@ export default { accounts: '账号数', status: '状态', actions: '操作', - billingType: '计费类型' + billingType: '计费类型', + userName: '用户名', + userEmail: '邮箱', + userNotes: '备注', + userStatus: '状态' }, form: { name: '名称', @@ -1510,6 +1514,26 @@ export default { failedToCreate: '创建分组失败', failedToUpdate: '更新分组失败', nameRequired: '请输入分组名称', + rateMultipliers: '专属倍率', + rateMultipliersTitle: '分组专属倍率管理', + addUserRate: '添加用户专属倍率', + searchUserPlaceholder: '搜索用户邮箱...', + noRateMultipliers: '暂无用户设置了专属倍率', + rateUpdated: '专属倍率已更新', + rateDeleted: '专属倍率已删除', + rateAdded: '专属倍率已添加', + clearAll: '全部清空', + confirmClearAll: '确定要清空该分组所有用户的专属倍率设置吗?此操作不可撤销。', + rateCleared: '已清空所有专属倍率', + batchAdjust: '批量调整倍率', + multiplierFactor: '乘数', + applyMultiplier: '应用', + rateAdjusted: '倍率已批量调整', + rateSaved: '专属倍率已保存', + finalRate: '最终倍率', + unsavedChanges: '有未保存的修改', + revertChanges: '撤销修改', + userInfo: '用户信息', subscription: { title: '订阅设置', type: '计费类型', diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 01b98c0c..a78762d6 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -181,6 +181,13 @@ {{ t('common.edit') }} +