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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user