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:
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user