fix: address review issues in brand filter sync, model guard, route counts, and candidate loading

- Sync blockedBrands local state after save to prevent stale chips
- Defer getBrandList from blocking Settings bootstrap
- Guard model management UI on brandGroups instead of availableModels
- Deduplicate single-model add in disabled models
- Skip read-only/zero-channel routes in enabledCounts and filter
- Add sequence counter to loadCandidates to prevent force race condition
- Use stableAddChannel for mobile add-channel button
This commit is contained in:
Hureru
2026-03-22 20:44:55 +08:00
parent b8ad89d1ab
commit d278b41be2
3 changed files with 19 additions and 18 deletions
+7 -12
View File
@@ -424,14 +424,6 @@ export default function Settings() {
api.getRoutesLite(), api.getRoutesLite(),
api.getRuntimeDatabaseConfig(), api.getRuntimeDatabaseConfig(),
]); ]);
let brandListBrands: string[] = [];
try {
const brandListRes = await api.getBrandList();
brandListBrands = Array.isArray(brandListRes?.brands) ? brandListRes.brands : [];
} catch {
// best-effort: brand list is non-critical
}
setAllBrandNames(brandListBrands);
setMaskedToken(authInfo.masked || '****'); setMaskedToken(authInfo.masked || '****');
setRuntime({ setRuntime({
checkinCron: runtimeInfo.checkinCron || '0 8 * * *', checkinCron: runtimeInfo.checkinCron || '0 8 * * *',
@@ -508,6 +500,10 @@ export default function Settings() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
// Load brand list in background (non-blocking, best-effort)
api.getBrandList()
.then((res: any) => setAllBrandNames(Array.isArray(res?.brands) ? res.brands : []))
.catch(() => {/* best-effort */});
}; };
useEffect(() => { useEffect(() => {
@@ -834,10 +830,9 @@ export default function Settings() {
setSavingBrandFilter(true); setSavingBrandFilter(true);
try { try {
const res = await api.updateRuntimeSettings({ globalBlockedBrands: blockedBrands }); const res = await api.updateRuntimeSettings({ globalBlockedBrands: blockedBrands });
setRuntime((prev) => ({ const resolved = Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands;
...prev, setRuntime((prev) => ({ ...prev, globalBlockedBrands: resolved }));
globalBlockedBrands: Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands, setBlockedBrands(resolved);
}));
toast.success('品牌屏蔽设置已保存'); toast.success('品牌屏蔽设置已保存');
try { try {
await api.rebuildRoutes(false); await api.rebuildRoutes(false);
+2 -2
View File
@@ -953,7 +953,7 @@ export default function Sites() {
) : ( ) : (
<> <>
{/* Search and brand group controls */} {/* Search and brand group controls */}
{availableModels.length > 0 && ( {brandGroups.length > 0 && (
<div style={{ marginBottom: 10 }}> <div style={{ marginBottom: 10 }}>
<input <input
placeholder="搜索模型名称..." placeholder="搜索模型名称..."
@@ -1016,7 +1016,7 @@ export default function Sites() {
checked={isDisabled} checked={isDisabled}
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
setDisabledModels((prev) => [...prev, model]); setDisabledModels((prev) => Array.from(new Set([...prev, model])));
} else { } else {
setDisabledModels((prev) => prev.filter((m) => m !== model)); setDisabledModels((prev) => prev.filter((m) => m !== model));
} }
+10 -4
View File
@@ -164,6 +164,7 @@ export default function TokenRoutes() {
const candidatesLoadedRef = useRef(false); const candidatesLoadedRef = useRef(false);
const candidatesPromiseRef = useRef<Promise<void> | null>(null); const candidatesPromiseRef = useRef<Promise<void> | null>(null);
const candidatesVersionRef = useRef(0); const candidatesVersionRef = useRef(0);
const candidatesSeqRef = useRef(0);
const loadRouteDecisions = async ( const loadRouteDecisions = async (
routeRows: RouteSummaryRow[], routeRows: RouteSummaryRow[],
@@ -235,10 +236,12 @@ export default function TokenRoutes() {
const loadCandidates = (force?: boolean) => { const loadCandidates = (force?: boolean) => {
if (candidatesLoadedRef.current && !force) return; if (candidatesLoadedRef.current && !force) return;
if (candidatesPromiseRef.current && !force) return; if (candidatesPromiseRef.current && !force) return;
const seq = ++candidatesSeqRef.current;
candidatesLoadedRef.current = true; candidatesLoadedRef.current = true;
const promise = (async () => { const promise = (async () => {
try { try {
const candidateRows = await api.getModelTokenCandidates(); const candidateRows = await api.getModelTokenCandidates();
if (candidatesSeqRef.current !== seq) return; // stale
startTransition(() => { startTransition(() => {
setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName); setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName);
setMissingTokenModelsByName( setMissingTokenModelsByName(
@@ -251,7 +254,7 @@ export default function TokenRoutes() {
}); });
candidatesVersionRef.current = Date.now(); candidatesVersionRef.current = Date.now();
} catch { } catch {
candidatesLoadedRef.current = false; if (candidatesSeqRef.current === seq) candidatesLoadedRef.current = false;
} finally { } finally {
if (candidatesPromiseRef.current === promise) { if (candidatesPromiseRef.current === promise) {
candidatesPromiseRef.current = null; candidatesPromiseRef.current = null;
@@ -740,6 +743,7 @@ export default function TokenRoutes() {
let enabled = 0; let enabled = 0;
let disabled = 0; let disabled = 0;
for (const route of baseFilteredRoutes) { for (const route of baseFilteredRoutes) {
if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) continue;
if (route.enabled) enabled++; if (route.enabled) enabled++;
else disabled++; else disabled++;
} }
@@ -748,8 +752,10 @@ export default function TokenRoutes() {
const filteredRoutes = useMemo(() => { const filteredRoutes = useMemo(() => {
if (enabledFilter === 'all') return baseFilteredRoutes; if (enabledFilter === 'all') return baseFilteredRoutes;
if (enabledFilter === 'enabled') return baseFilteredRoutes.filter((route) => route.enabled); return baseFilteredRoutes.filter((route) => {
return baseFilteredRoutes.filter((route) => !route.enabled); if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) return false;
return enabledFilter === 'enabled' ? route.enabled : !route.enabled;
});
}, [baseFilteredRoutes, enabledFilter]); }, [baseFilteredRoutes, enabledFilter]);
useEffect(() => { useEffect(() => {
@@ -1451,7 +1457,7 @@ export default function TokenRoutes() {
<button <button
type="button" type="button"
className="btn btn-link" className="btn btn-link"
onClick={() => setAddChannelModalRouteId(route.id)} onClick={() => stableAddChannel(route.id)}
> >
{tr('添加通道')} {tr('添加通道')}
</button> </button>