Skip to content

Commit 65a932f

Browse files
authored
feat: Optimize aggregate group UI (#310)
* feat: Subgroup status * feat: Weight progress bar gradient * feat: Subgroup tooltip * feat: Subgroup card stats * feat: Group table filter
1 parent aec197d commit 65a932f

File tree

11 files changed

+523
-52
lines changed

11 files changed

+523
-52
lines changed

internal/models/types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,11 @@ type GroupSubGroup struct {
6262

6363
// SubGroupInfo 用于API响应的子分组信息
6464
type SubGroupInfo struct {
65-
GroupID uint `json:"group_id"`
66-
Name string `json:"name"`
67-
DisplayName string `json:"display_name"`
68-
Weight int `json:"weight"`
65+
Group Group `json:"group"`
66+
Weight int `json:"weight"`
67+
TotalKeys int64 `json:"total_keys"`
68+
ActiveKeys int64 `json:"active_keys"`
69+
InvalidKeys int64 `json:"invalid_keys"`
6970
}
7071

7172
// ParentAggregateGroupInfo 用于API响应的父聚合分组信息

internal/services/aggregate_group_service.go

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package services
22

33
import (
44
"context"
5+
"sync"
56

67
app_errors "gpt-load/internal/errors"
78
"gpt-load/internal/models"
@@ -108,7 +109,7 @@ func (s *AggregateGroupService) ValidateSubGroups(ctx context.Context, channelTy
108109
}, nil
109110
}
110111

111-
// GetSubGroups returns sub groups for an aggregate group
112+
// GetSubGroups returns sub groups for an aggregate group with complete information
112113
func (s *AggregateGroupService) GetSubGroups(ctx context.Context, groupID uint) ([]models.SubGroupInfo, error) {
113114
var group models.Group
114115
if err := s.db.WithContext(ctx).First(&group, groupID).Error; err != nil {
@@ -144,13 +145,24 @@ func (s *AggregateGroupService) GetSubGroups(ctx context.Context, groupID uint)
144145
return nil, err
145146
}
146147

148+
keyStatsMap := s.fetchSubGroupsKeyStats(ctx, subGroupIDs)
149+
147150
subGroups := make([]models.SubGroupInfo, 0, len(subGroupModels))
148151
for _, subGroup := range subGroupModels {
152+
stats := keyStatsMap[subGroup.ID]
153+
154+
if stats.Err != nil {
155+
logrus.WithContext(ctx).WithError(stats.Err).
156+
WithField("group_id", subGroup.ID).
157+
Warn("failed to fetch key stats for sub-group, using zero values")
158+
}
159+
149160
subGroups = append(subGroups, models.SubGroupInfo{
150-
GroupID: subGroup.ID,
151-
Name: subGroup.Name,
152-
DisplayName: subGroup.DisplayName,
161+
Group: subGroup,
153162
Weight: weightMap[subGroup.ID],
163+
TotalKeys: stats.TotalKeys,
164+
ActiveKeys: stats.ActiveKeys,
165+
InvalidKeys: stats.InvalidKeys,
154166
})
155167
}
156168

@@ -365,3 +377,62 @@ func (s *AggregateGroupService) GetParentAggregateGroups(ctx context.Context, su
365377

366378
return parentGroups, nil
367379
}
380+
381+
// keyStatsResult stores key statistics for a single group
382+
type keyStatsResult struct {
383+
GroupID uint
384+
TotalKeys int64
385+
ActiveKeys int64
386+
InvalidKeys int64
387+
Err error
388+
}
389+
390+
// fetchSubGroupsKeyStats batch fetches key statistics for multiple sub-groups concurrently
391+
func (s *AggregateGroupService) fetchSubGroupsKeyStats(ctx context.Context, groupIDs []uint) map[uint]keyStatsResult {
392+
results := make(map[uint]keyStatsResult)
393+
var mu sync.Mutex
394+
var wg sync.WaitGroup
395+
396+
for _, groupID := range groupIDs {
397+
wg.Add(1)
398+
go func(gid uint) {
399+
defer wg.Done()
400+
401+
var totalKeys, activeKeys int64
402+
result := keyStatsResult{GroupID: gid}
403+
404+
// Query total keys
405+
if err := s.db.WithContext(ctx).Model(&models.APIKey{}).
406+
Where("group_id = ?", gid).
407+
Count(&totalKeys).Error; err != nil {
408+
result.Err = err
409+
mu.Lock()
410+
results[gid] = result
411+
mu.Unlock()
412+
return
413+
}
414+
415+
// Query active keys
416+
if err := s.db.WithContext(ctx).Model(&models.APIKey{}).
417+
Where("group_id = ? AND status = ?", gid, models.KeyStatusActive).
418+
Count(&activeKeys).Error; err != nil {
419+
result.Err = err
420+
mu.Lock()
421+
results[gid] = result
422+
mu.Unlock()
423+
return
424+
}
425+
426+
result.TotalKeys = totalKeys
427+
result.ActiveKeys = activeKeys
428+
result.InvalidKeys = totalKeys - activeKeys
429+
430+
mu.Lock()
431+
results[gid] = result
432+
mu.Unlock()
433+
}(groupID)
434+
}
435+
436+
wg.Wait()
437+
return results
438+
}

web/src/components/keys/AddSubGroupModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const getAvailableOptions = computed(() => {
5757
}
5858
5959
// 获取已存在的子分组ID
60-
const existingIds = props.existingSubGroups.map(sg => sg.group_id);
60+
const existingIds = props.existingSubGroups.map(sg => sg.group.id);
6161
6262
return props.groups
6363
.filter(group => {

web/src/components/keys/EditSubGroupWeightModal.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const previewPercentage = computed(() => {
5151
5252
// 计算总权重(用新权重替换当前子分组的权重)
5353
const totalWeight = props.subGroups.reduce((sum, sg) => {
54-
if (sg.group_id === props.subGroup?.group_id) {
54+
if (sg.group.id === props.subGroup?.group.id) {
5555
return sum + formData.weight;
5656
}
5757
return sum + sg.weight;
@@ -113,9 +113,15 @@ async function handleSubmit() {
113113
return;
114114
}
115115
116+
const subGroupId = props.subGroup.group.id;
117+
if (!subGroupId) {
118+
message.error("no subGroupId");
119+
return;
120+
}
121+
116122
await keysApi.updateSubGroupWeight(
117123
props.aggregateGroup.id,
118-
props.subGroup.group_id,
124+
subGroupId,
119125
formData.weight // 保持原始数值,不进行取整
120126
);
121127
@@ -163,12 +169,14 @@ function adjustWeight(delta: number) {
163169
<div class="sub-group-info">
164170
<h4 class="section-title">
165171
{{ t("keys.editingSubGroup") }}:
166-
<span class="group-name">{{ subGroup?.display_name || subGroup?.name }}</span>
172+
<span class="group-name">
173+
{{ subGroup?.group.display_name || subGroup?.group.name }}
174+
</span>
167175
</h4>
168176
<div class="group-details">
169177
<span class="detail-item">
170178
<strong>{{ t("keys.groupId") }}:</strong>
171-
{{ subGroup?.group_id }}
179+
{{ subGroup?.group.id }}
172180
</span>
173181
<span class="detail-item">
174182
<strong>{{ t("keys.currentWeight") }}:</strong>

web/src/components/keys/GroupInfoCard.vue

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ import type {
1010
import { appState } from "@/utils/app-state";
1111
import { copy } from "@/utils/clipboard";
1212
import { getGroupDisplayName, maskProxyKeys } from "@/utils/display";
13-
import {
14-
CopyOutline,
15-
EyeOffOutline,
16-
EyeOutline,
17-
InformationCircleOutline,
18-
Pencil,
19-
Trash,
20-
} from "@vicons/ionicons5";
13+
import { CopyOutline, EyeOffOutline, EyeOutline, Pencil, Trash } from "@vicons/ionicons5";
2114
import {
2215
NButton,
2316
NButtonGroup,
@@ -96,6 +89,21 @@ const isAggregateGroup = computed(() => {
9689
return props.group?.group_type === "aggregate";
9790
});
9891
92+
// 计算有效子分组数(weight > 0 且有可用密钥)
93+
const activeSubGroupsCount = computed(() => {
94+
return props.subGroups?.filter(sg => sg.weight > 0 && sg.active_keys > 0).length || 0;
95+
});
96+
97+
// 计算禁用子分组数(weight = 0)
98+
const disabledSubGroupsCount = computed(() => {
99+
return props.subGroups?.filter(sg => sg.weight === 0).length || 0;
100+
});
101+
102+
// 计算无效子分组数(weight > 0 但无可用密钥)
103+
const unavailableSubGroupsCount = computed(() => {
104+
return props.subGroups?.filter(sg => sg.weight > 0 && sg.active_keys === 0).length || 0;
105+
});
106+
99107
async function copyProxyKeys() {
100108
if (!props.group?.proxy_keys) {
101109
return;
@@ -400,7 +408,7 @@ function resetPage() {
400408
<n-tooltip trigger="hover">
401409
<template #trigger>
402410
<n-gradient-text type="success" size="20">
403-
{{ props.subGroups?.filter(sg => sg.weight > 0).length || 0 }}
411+
{{ activeSubGroupsCount }}
404412
</n-gradient-text>
405413
</template>
406414
{{ t("keys.activeSubGroups") }}
@@ -409,10 +417,19 @@ function resetPage() {
409417
<n-tooltip trigger="hover">
410418
<template #trigger>
411419
<n-gradient-text type="warning" size="20">
412-
{{ props.subGroups?.filter(sg => sg.weight === 0).length || 0 }}
420+
{{ disabledSubGroupsCount }}
421+
</n-gradient-text>
422+
</template>
423+
{{ t("keys.disabledSubGroups") }}
424+
</n-tooltip>
425+
<n-divider vertical />
426+
<n-tooltip trigger="hover">
427+
<template #trigger>
428+
<n-gradient-text type="error" size="20">
429+
{{ unavailableSubGroupsCount }}
413430
</n-gradient-text>
414431
</template>
415-
{{ t("keys.inactiveSubGroups") }}
432+
{{ t("keys.unavailableSubGroups") }}
416433
</n-tooltip>
417434
</n-statistic>
418435

@@ -630,9 +647,9 @@ function resetPage() {
630647
:title="t('keys.viewGroupInfo')"
631648
>
632649
<template #icon>
633-
<n-icon :component="InformationCircleOutline" />
650+
<n-icon :component="EyeOutline" />
634651
</template>
635-
{{ t("keys.groupInfo") }}
652+
{{ t("common.view") }}
636653
</n-button>
637654
</n-form-item>
638655
</n-form>

0 commit comments

Comments
 (0)