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