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.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 || '****');
setRuntime({
checkinCron: runtimeInfo.checkinCron || '0 8 * * *',
@@ -508,6 +500,10 @@ export default function Settings() {
} finally {
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(() => {
@@ -834,10 +830,9 @@ export default function Settings() {
setSavingBrandFilter(true);
try {
const res = await api.updateRuntimeSettings({ globalBlockedBrands: blockedBrands });
setRuntime((prev) => ({
...prev,
globalBlockedBrands: Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands,
}));
const resolved = Array.isArray(res?.globalBlockedBrands) ? res.globalBlockedBrands : blockedBrands;
setRuntime((prev) => ({ ...prev, globalBlockedBrands: resolved }));
setBlockedBrands(resolved);
toast.success('品牌屏蔽设置已保存');
try {
await api.rebuildRoutes(false);
+2 -2
View File
@@ -953,7 +953,7 @@ export default function Sites() {
) : (
<>
{/* Search and brand group controls */}
{availableModels.length > 0 && (
{brandGroups.length > 0 && (
<div style={{ marginBottom: 10 }}>
<input
placeholder="搜索模型名称..."
@@ -1016,7 +1016,7 @@ export default function Sites() {
checked={isDisabled}
onChange={(e) => {
if (e.target.checked) {
setDisabledModels((prev) => [...prev, model]);
setDisabledModels((prev) => Array.from(new Set([...prev, model])));
} else {
setDisabledModels((prev) => prev.filter((m) => m !== model));
}
+10 -4
View File
@@ -164,6 +164,7 @@ export default function TokenRoutes() {
const candidatesLoadedRef = useRef(false);
const candidatesPromiseRef = useRef<Promise<void> | null>(null);
const candidatesVersionRef = useRef(0);
const candidatesSeqRef = useRef(0);
const loadRouteDecisions = async (
routeRows: RouteSummaryRow[],
@@ -235,10 +236,12 @@ export default function TokenRoutes() {
const loadCandidates = (force?: boolean) => {
if (candidatesLoadedRef.current && !force) return;
if (candidatesPromiseRef.current && !force) return;
const seq = ++candidatesSeqRef.current;
candidatesLoadedRef.current = true;
const promise = (async () => {
try {
const candidateRows = await api.getModelTokenCandidates();
if (candidatesSeqRef.current !== seq) return; // stale
startTransition(() => {
setModelCandidates((candidateRows?.models || {}) as RouteModelCandidatesByModelName);
setMissingTokenModelsByName(
@@ -251,7 +254,7 @@ export default function TokenRoutes() {
});
candidatesVersionRef.current = Date.now();
} catch {
candidatesLoadedRef.current = false;
if (candidatesSeqRef.current === seq) candidatesLoadedRef.current = false;
} finally {
if (candidatesPromiseRef.current === promise) {
candidatesPromiseRef.current = null;
@@ -740,6 +743,7 @@ export default function TokenRoutes() {
let enabled = 0;
let disabled = 0;
for (const route of baseFilteredRoutes) {
if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) continue;
if (route.enabled) enabled++;
else disabled++;
}
@@ -748,8 +752,10 @@ export default function TokenRoutes() {
const filteredRoutes = useMemo(() => {
if (enabledFilter === 'all') return baseFilteredRoutes;
if (enabledFilter === 'enabled') return baseFilteredRoutes.filter((route) => route.enabled);
return baseFilteredRoutes.filter((route) => !route.enabled);
return baseFilteredRoutes.filter((route) => {
if (route.kind === 'zero_channel' || route.readOnly === true || route.isVirtual === true) return false;
return enabledFilter === 'enabled' ? route.enabled : !route.enabled;
});
}, [baseFilteredRoutes, enabledFilter]);
useEffect(() => {
@@ -1451,7 +1457,7 @@ export default function TokenRoutes() {
<button
type="button"
className="btn btn-link"
onClick={() => setAddChannelModalRouteId(route.id)}
onClick={() => stableAddChannel(route.id)}
>
{tr('添加通道')}
</button>