Skip to content

Commit 96edbc0

Browse files
committed
fix: 优化分组选择器和账号筛选名称
重排 API 密钥分组选择器为平台导航与分组双栏布局,固定展示全部支持平台并规范化平台值。修复我的账号分组筛选器的乱码显示。\n\n验证:pnpm exec vitest run src/views/user/__tests__/KeysView.spec.ts src/views/user/__tests__/AccountsView.proxyScope.spec.ts src/views/user/__tests__/AccountsView.squareDelete.spec.ts;pnpm run typecheck;pnpm run lint:check;pnpm run build。\n\n本次不修改数据库,文档站保持不变。
1 parent a2654c4 commit 96edbc0

3 files changed

Lines changed: 142 additions & 75 deletions

File tree

backend/cmd/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.72
1+
1.2.73

frontend/src/views/user/AccountsView.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ import type { Account, AccountLevel, AccountPlatform, AccountType, AdminGroup, G
590590
import type { Column } from '@/components/common/types'
591591
import { formatDateTime, formatRelativeTime } from '@/utils/format'
592592
import { extractApiErrorCode, extractApiErrorMessage, extractApiErrorMetadata, isAbortError } from '@/utils/apiError'
593+
import { displayText } from '@/utils/displayText'
593594
594595
type UserAccountStatus = 'active' | 'disabled'
595596
@@ -767,7 +768,7 @@ const groupFilterOptions = computed(() => [
767768
{ value: -1, label: t('userAccounts.privateDefaultGroupOnly') },
768769
...groups.value.map((group) => ({
769770
value: group.id,
770-
label: group.name
771+
label: displayText(group.name)
771772
}))
772773
])
773774

frontend/src/views/user/KeysView.vue

Lines changed: 139 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@
14221422
:id="getGroupSelectorPopoverId(groupSelectorKeyId)"
14231423
ref="dropdownRef"
14241424
:data-ui-skin="uiSkin"
1425-
class="data-teleport-panel animate-in fade-in slide-in-from-top-2 fixed z-[100000020] flex max-h-[calc(100dvh-2rem)] min-w-0 w-[min(34rem,calc(100vw-2rem))] flex-col duration-200"
1425+
class="data-teleport-panel animate-in fade-in slide-in-from-top-2 fixed z-[100000020] flex max-h-[calc(100dvh-2rem)] min-w-0 w-[min(58rem,calc(100vw-2rem))] flex-col overflow-hidden duration-200"
14261426
style="pointer-events: auto !important;"
14271427
role="dialog"
14281428
:aria-label="t('keys.groupSelectorLabel')"
@@ -1433,8 +1433,21 @@
14331433
@click.stop
14341434
@keydown="handleGroupSelectorKeydown"
14351435
>
1436-
<!-- Search box -->
1437-
<div class="flex-shrink-0 border-b border-line p-2">
1436+
<!-- Search and context header -->
1437+
<div class="flex-shrink-0 border-b border-line bg-surface/95 p-4 backdrop-blur-sm">
1438+
<div class="mb-3 flex items-start justify-between gap-4">
1439+
<div class="min-w-0">
1440+
<p class="text-sm font-semibold text-content">{{ t('keys.groupSelectorLabel') }}</p>
1441+
<p class="mt-0.5 text-xs text-content-subtle">
1442+
{{ activeGroupPlatform ? t(`admin.groups.platforms.${activeGroupPlatform}`) : t('admin.groups.platforms.all') }}
1443+
<span class="mx-1 text-content-faint">·</span>
1444+
{{ filteredGroupOptions.length }}
1445+
</p>
1446+
</div>
1447+
<span class="hidden rounded-full bg-brand-soft px-2.5 py-1 text-[11px] font-medium text-brand sm:inline-flex">
1448+
{{ t('keys.searchGroup') }}
1449+
</span>
1450+
</div>
14381451
<div class="relative">
14391452
<svg class="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-content-subtle" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
14401453
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -1452,73 +1465,104 @@
14521465
@click.stop
14531466
/>
14541467
</div>
1455-
<div class="mt-2 flex items-center gap-1 overflow-x-auto pb-0.5" role="tablist" :aria-label="t('keys.groupSelectorLabel')">
1456-
<button
1457-
v-for="platform in groupPlatforms"
1458-
:key="platform.value || 'all'"
1459-
type="button"
1460-
role="tab"
1461-
:aria-selected="activeGroupPlatform === platform.value"
1462-
:class="[
1463-
'inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-lg px-2.5 text-xs font-medium transition-colors',
1464-
activeGroupPlatform === platform.value
1465-
? 'bg-brand-soft text-brand'
1466-
: 'text-content-muted hover:bg-surface-muted hover:text-content'
1467-
]"
1468-
@click="activeGroupPlatform = platform.value"
1469-
>
1470-
<PlatformIcon v-if="platform.value" :platform="platform.value" size="xs" />
1471-
<span>{{ platform.label }}</span>
1472-
<span class="rounded-full bg-black/5 px-1.5 py-0.5 text-[10px] dark:bg-white/10">{{ platform.count }}</span>
1473-
</button>
1474-
</div>
14751468
</div>
1476-
<!-- Group list -->
1477-
<div
1478-
:id="getGroupSelectorListId(groupSelectorKeyId)"
1479-
class="min-h-0 flex-1 overflow-y-auto p-1.5"
1480-
role="group"
1481-
:aria-label="t('keys.groupLabel')"
1482-
>
1483-
<button
1484-
v-for="option in filteredGroupOptions"
1485-
:key="option.value ?? 'null'"
1486-
type="button"
1487-
:aria-pressed="
1488-
selectedKeyForGroup?.group_id === option.value ||
1489-
(!selectedKeyForGroup?.group_id && option.value === null)
1490-
"
1491-
@click="changeGroup(selectedKeyForGroup!, option.value)"
1492-
:class="[
1493-
'data-teleport-option',
1494-
selectedKeyForGroup?.group_id === option.value ||
1495-
(!selectedKeyForGroup?.group_id && option.value === null)
1496-
? 'bg-brand-soft'
1497-
: ''
1498-
]"
1499-
:title="option.description || undefined"
1469+
<div class="flex min-h-0 flex-1 flex-col sm:flex-row">
1470+
<!-- Platform navigation -->
1471+
<aside class="shrink-0 border-b border-line bg-surface-muted/45 p-2 sm:w-48 sm:border-b-0 sm:border-r">
1472+
<div class="mb-2 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-content-faint">
1473+
{{ t('admin.groups.platforms.all') }}
1474+
</div>
1475+
<div class="flex gap-1 overflow-x-auto sm:block sm:space-y-1 sm:overflow-visible" role="tablist" :aria-label="t('keys.groupSelectorLabel')">
1476+
<button
1477+
v-for="platform in groupPlatforms"
1478+
:key="platform.value || 'all'"
1479+
type="button"
1480+
role="tab"
1481+
:aria-selected="activeGroupPlatform === platform.value"
1482+
:class="[
1483+
'group inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-left text-xs font-medium transition-all sm:flex sm:w-full',
1484+
activeGroupPlatform === platform.value
1485+
? 'bg-brand text-white shadow-sm shadow-brand/20'
1486+
: 'text-content-muted hover:bg-surface hover:text-content'
1487+
]"
1488+
@click="activeGroupPlatform = platform.value"
1489+
>
1490+
<span
1491+
:class="[
1492+
'grid h-6 w-6 shrink-0 place-items-center rounded-lg',
1493+
activeGroupPlatform === platform.value ? 'bg-white/15' : 'bg-black/5 dark:bg-white/10'
1494+
]"
1495+
>
1496+
<PlatformIcon v-if="platform.value" :platform="platform.value" size="xs" />
1497+
<Icon v-else name="grid" size="xs" />
1498+
</span>
1499+
<span class="min-w-0 flex-1 truncate">{{ platform.label }}</span>
1500+
<span
1501+
:class="[
1502+
'rounded-full px-1.5 py-0.5 text-[10px] tabular-nums',
1503+
activeGroupPlatform === platform.value ? 'bg-white/15' : 'bg-black/5 dark:bg-white/10'
1504+
]"
1505+
>{{ platform.count }}</span>
1506+
</button>
1507+
</div>
1508+
</aside>
1509+
<!-- Group list -->
1510+
<div
1511+
:id="getGroupSelectorListId(groupSelectorKeyId)"
1512+
class="min-h-0 flex-1 overflow-y-auto bg-surface p-3"
1513+
role="group"
1514+
:aria-label="t('keys.groupLabel')"
15001515
>
1501-
<GroupOptionItem
1502-
:name="option.label"
1503-
:platform="option.platform"
1504-
:scope="option.scope"
1505-
:subscription-type="option.subscriptionType"
1506-
:rate-multiplier="option.rate"
1507-
:user-rate-multiplier="option.userRate"
1508-
:effective-rate-multiplier="option.effectiveRate"
1509-
:rate-multiplier-source="option.rateSource"
1510-
:description="option.description"
1511-
:api-key-badge-type="option.apiKeyBadgeType"
1512-
:api-key-badge-text="option.apiKeyBadgeText"
1513-
:selected="
1514-
selectedKeyForGroup?.group_id === option.value ||
1515-
(!selectedKeyForGroup?.group_id && option.value === null)
1516-
"
1517-
/>
1518-
</button>
1519-
<!-- Empty state when search has no results -->
1520-
<div v-if="filteredGroupOptions.length === 0" class="py-4 text-center text-sm text-content-subtle">
1521-
{{ t('keys.noGroupFound') }}
1516+
<div v-if="filteredGroupOptions.length > 0" class="mb-2 flex items-center justify-between px-1">
1517+
<span class="text-xs font-medium text-content-muted">{{ t('keys.groupLabel') }}</span>
1518+
<span class="text-[11px] tabular-nums text-content-faint">{{ filteredGroupOptions.length }}</span>
1519+
</div>
1520+
<div v-if="filteredGroupOptions.length > 0" class="grid gap-2 lg:grid-cols-2">
1521+
<button
1522+
v-for="option in filteredGroupOptions"
1523+
:key="option.value ?? 'null'"
1524+
type="button"
1525+
:aria-pressed="
1526+
selectedKeyForGroup?.group_id === option.value ||
1527+
(!selectedKeyForGroup?.group_id && option.value === null)
1528+
"
1529+
@click="changeGroup(selectedKeyForGroup!, option.value)"
1530+
:class="[
1531+
'data-teleport-option min-w-0 rounded-xl border border-line bg-surface-raised p-0 text-left transition-all hover:-translate-y-px hover:border-brand/45 hover:shadow-sm',
1532+
selectedKeyForGroup?.group_id === option.value ||
1533+
(!selectedKeyForGroup?.group_id && option.value === null)
1534+
? 'border-brand/60 bg-brand-soft ring-1 ring-brand/20'
1535+
: ''
1536+
]"
1537+
:title="option.description || undefined"
1538+
>
1539+
<GroupOptionItem
1540+
:name="option.label"
1541+
:platform="option.platform"
1542+
:scope="option.scope"
1543+
:subscription-type="option.subscriptionType"
1544+
:rate-multiplier="option.rate"
1545+
:user-rate-multiplier="option.userRate"
1546+
:effective-rate-multiplier="option.effectiveRate"
1547+
:rate-multiplier-source="option.rateSource"
1548+
:description="option.description"
1549+
:api-key-badge-type="option.apiKeyBadgeType"
1550+
:api-key-badge-text="option.apiKeyBadgeText"
1551+
:selected="
1552+
selectedKeyForGroup?.group_id === option.value ||
1553+
(!selectedKeyForGroup?.group_id && option.value === null)
1554+
"
1555+
/>
1556+
</button>
1557+
</div>
1558+
<!-- Empty state when search has no results -->
1559+
<div v-else class="flex min-h-56 flex-col items-center justify-center rounded-2xl border border-dashed border-line px-6 text-center">
1560+
<span class="mb-3 grid h-10 w-10 place-items-center rounded-full bg-surface-muted text-content-faint">
1561+
<Icon name="search" size="md" />
1562+
</span>
1563+
<p class="text-sm font-medium text-content">{{ t('keys.noGroupFound') }}</p>
1564+
<p class="mt-1 text-xs text-content-subtle">{{ t('keys.searchGroup') }}</p>
1565+
</div>
15221566
</div>
15231567
</div>
15241568
</div>
@@ -2041,7 +2085,7 @@ const groupOptions = computed<GroupOption[]>(() =>
20412085
effectiveRate: group.effective_rate_multiplier ?? fallbackUserRate,
20422086
rateSource: group.effective_rate_multiplier_source ?? (fallbackUserRate != null ? 'user_group' : 'group_default'),
20432087
subscriptionType: group.subscription_type,
2044-
platform: group.platform,
2088+
platform: group.platform.trim().toLowerCase() as GroupPlatform,
20452089
scope: group.scope,
20462090
apiKeyBadgeType: group.api_key_badge_type,
20472091
apiKeyBadgeText: group.api_key_badge_text
@@ -2199,18 +2243,39 @@ const groupRoutingChanged = (
21992243
// Group dropdown search
22002244
const groupSearchQuery = ref('')
22012245
const activeGroupPlatform = ref<GroupPlatform | null>(null)
2246+
2247+
// Keep the platform rail stable even when a platform currently has no available
2248+
// groups. This avoids the selector layout shifting as group visibility changes.
2249+
const canonicalGroupPlatforms: GroupPlatform[] = [
2250+
'anthropic',
2251+
'openai',
2252+
'gemini',
2253+
'antigravity',
2254+
'grok',
2255+
'opencode',
2256+
'kimi',
2257+
'zhipu',
2258+
'deepseek',
2259+
'minimax',
2260+
'qwen'
2261+
]
2262+
22022263
const groupPlatforms = computed(() => {
2203-
const platforms = Array.from(new Set(groupOptions.value.map((option) => option.platform)))
2264+
const counts = new Map<GroupPlatform, number>()
2265+
for (const option of groupOptions.value) {
2266+
const platform = option.platform.trim().toLowerCase() as GroupPlatform
2267+
counts.set(platform, (counts.get(platform) ?? 0) + 1)
2268+
}
22042269
return [
22052270
{
22062271
value: null as GroupPlatform | null,
22072272
label: t('admin.groups.platforms.all'),
22082273
count: groupOptions.value.length
22092274
},
2210-
...platforms.map((platform) => ({
2275+
...canonicalGroupPlatforms.map((platform) => ({
22112276
value: platform,
22122277
label: t(`admin.groups.platforms.${platform}`),
2213-
count: groupOptions.value.filter((option) => option.platform === platform).length
2278+
count: counts.get(platform) ?? 0
22142279
}))
22152280
]
22162281
})
@@ -2408,7 +2473,8 @@ const updateGroupSelectorPosition = (): boolean => {
24082473
const dropdownHeight = Math.min(dropdownRef.value?.offsetHeight || 400, maxDropdownHeight)
24092474
const spaceBelow = window.innerHeight - rect.bottom - viewportPadding
24102475
const spaceAbove = rect.top - viewportPadding
2411-
const dropdownWidth = Math.min(544, Math.max(0, window.innerWidth - viewportPadding * 2))
2476+
// Keep the clamp in sync with the 58rem (928px) two-column selector.
2477+
const dropdownWidth = Math.min(928, Math.max(0, window.innerWidth - viewportPadding * 2))
24122478
const maxLeft = Math.max(viewportPadding, window.innerWidth - dropdownWidth - viewportPadding)
24132479
const clampedLeft = Math.min(Math.max(rect.left, viewportPadding), maxLeft)
24142480
const preferredTop = spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove

0 commit comments

Comments
 (0)