feat(s2a): Improve group name caching and display with api_base isolation

- Refactor groupNameCache structure to isolate caches by api_base: { [apiBase]: { [groupId]: groupName } }
- Add normalizeBase() utility to remove trailing slashes and prevent cache key mismatches
- Update group fetching logic to deduplicate requests by api_base and admin_key combination
- Auto-fetch available groups when entering edit mode for a profile with valid credentials
- Enhance group label display to show "name #id" format instead of just name or #id
- Sort profiles in list view to display active profile first
- Update ProfileCard to use normalized api_base for groupNameCache lookup
- Simplify group label rendering by removing conditional groupsFetched check
This commit is contained in:
2026-02-07 19:12:48 +08:00
parent 6eb156c555
commit eb129a4f85

View File

@@ -33,8 +33,8 @@ export default function S2AConfig() {
const [fetchingGroups, setFetchingGroups] = useState(false)
const [groupsFetched, setGroupsFetched] = useState(false)
// 列表视图分组名称缓存: { [groupId]: groupName }
const [groupNameCache, setGroupNameCache] = useState<Record<number, string>>({})
// 列表视图分组名称缓存: { [apiBase]: { [groupId]: groupName } }
const [groupNameCache, setGroupNameCache] = useState<Record<string, Record<number, string>>>({})
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
@@ -68,25 +68,30 @@ export default function S2AConfig() {
Promise.all([fetchProfiles(), fetchActiveConfig()]).finally(() => setLoading(false))
}, [fetchProfiles, fetchActiveConfig])
// 列表视图: 批量获取各 profile 的分组名称
// 规范化 api_base: 去掉尾部斜杠,防止缓存 key 不匹配
const normalizeBase = (base: string) => base.replace(/\/+$/, '')
// 列表视图: 批量获取各 profile 的分组名称(按 api_base 隔离)
useEffect(() => {
if (viewMode !== 'list' || profiles.length === 0) return
const fetchGroupNames = async () => {
const cache: Record<number, string> = {}
const cache: Record<string, Record<number, string>> = {}
// 按 api_base+admin_key 去重,避免重复请求
const seen = new Map<string, S2AProfile>()
for (const p of profiles) {
if (p.api_base && p.admin_key) {
const key = `${p.api_base}|${p.admin_key}`
const key = `${normalizeBase(p.api_base)}|${p.admin_key}`
if (!seen.has(key)) seen.set(key, p)
}
}
for (const p of seen.values()) {
try {
const groups = await fetchGroupsWithCredentials(p.api_base, p.admin_key)
const map: Record<number, string> = {}
for (const g of groups) {
cache[g.id] = g.name
map[g.id] = g.name
}
cache[normalizeBase(p.api_base)] = map
} catch { /* ignore - will show #id fallback */ }
}
setGroupNameCache(cache)
@@ -141,9 +146,21 @@ export default function S2AConfig() {
setFormProxyAddress(profile.proxy_address || '')
setTestResult(null)
setAvailableGroups([])
setFetchingGroups(false)
setGroupsFetched(false)
setViewMode('edit')
// 自动获取分组列表
if (profile.api_base && profile.admin_key) {
setFetchingGroups(true)
fetchGroupsWithCredentials(profile.api_base, profile.admin_key)
.then(groups => {
setAvailableGroups(groups)
setGroupsFetched(true)
})
.catch(() => { /* ignore - will show #id fallback */ })
.finally(() => setFetchingGroups(false))
} else {
setFetchingGroups(false)
}
}
// 返回列表
@@ -279,10 +296,10 @@ export default function S2AConfig() {
}
}
// 获取分组名称
const getGroupName = (id: number): string => {
// 获取分组显示文字: "name #id" 或 "#id"
const getGroupLabel = (id: number): string => {
const group = availableGroups.find(g => g.id === id)
return group ? group.name : `#${id}`
return group ? `${group.name} #${id}` : `#${id}`
}
const handleAddGroupId = () => {
@@ -424,7 +441,7 @@ export default function S2AConfig() {
<div className="flex flex-wrap gap-2">
{formGroupIds.map(id => (
<span key={id} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
{groupsFetched ? getGroupName(id) : `#${id}`}
{getGroupLabel(id)}
<button onClick={() => handleRemoveGroupId(id)} className="hover:text-red-500 transition-colors"><X className="h-3 w-3" /></button>
</span>
))}
@@ -549,13 +566,17 @@ export default function S2AConfig() {
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{profiles.map(profile => (
{[...profiles].sort((a, b) => {
const aActive = a.api_base === activeApiBase ? 1 : 0
const bActive = b.api_base === activeApiBase ? 1 : 0
return bActive - aActive
}).map(profile => (
<ProfileCard
key={profile.id}
profile={profile}
isActive={activeApiBase === profile.api_base}
isActivating={activating === profile.id}
groupNameCache={groupNameCache}
groupNameCache={groupNameCache[normalizeBase(profile.api_base)] || {}}
onActivate={() => handleActivate(profile)}
onEdit={() => handleEdit(profile)}
onDelete={() => handleDelete(profile.id, profile.name)}
@@ -609,7 +630,7 @@ function ProfileCard({ profile, isActive, isActivating, groupNameCache, onActiva
</span>
{parsedGroups.length > 0 && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">
{parsedGroups.map(id => groupNameCache[id] || `#${id}`).join(', ')}
{parsedGroups.map(id => groupNameCache[id] ? `${groupNameCache[id]} #${id}` : `#${id}`).join(', ')}
</span>
)}
{profile.proxy_enabled && (