diff --git a/src/web/pages/Settings.tsx b/src/web/pages/Settings.tsx
index e110590..719c145 100644
--- a/src/web/pages/Settings.tsx
+++ b/src/web/pages/Settings.tsx
@@ -6,8 +6,9 @@ import { useToast } from '../components/Toast.js';
import { useIsMobile } from '../components/useIsMobile.js';
import ChangeKeyModal from '../components/ChangeKeyModal.js';
import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js';
-import { BrandGlyph, InlineBrandIcon, getBrand } from '../components/BrandIcon.js';
import ModernSelect from '../components/ModernSelect.js';
+import FactoryResetModal from './settings/FactoryResetModal.js';
+import RouteSelectorModal from './settings/RouteSelectorModal.js';
import {
applyRoutingProfilePreset,
resolveRoutingProfilePreset,
@@ -19,8 +20,6 @@ import { clearAppInstallationState } from '../appLocalState.js';
import { tr } from '../i18n.js';
import {
isExactModelPattern,
- resolveRouteBrand,
- resolveRouteIcon,
resolveRouteTitle,
} from './token-routes/utils.js';
import { generateDownstreamSkKey } from './helpers/generateDownstreamSkKey.js';
@@ -1730,287 +1729,34 @@ export default function Settings() {
);
return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
})()}
- {factoryResetPresence.shouldRender && (() => {
- const confirmLabel = factoryResetting
- ? '重新初始化中...'
- : (factoryResetSecondsLeft > 0
- ? `确认重新初始化系统(${factoryResetSecondsLeft}s)`
- : '确认重新初始化系统');
- const modal = (
-
-
e.stopPropagation()}
- >
-
确认重新初始化系统
-
-
- 这是不可逆操作。系统会清空当前 metapi 使用中的全部数据库内容,并在成功后立即退出当前登录状态。
-
-
-
• 当前若使用外部 MySQL/Postgres,也会先清空该外部库中的 metapi 数据。
-
• 系统随后会强制切回默认 SQLite。
-
• 管理员 Token 将重置为 {FACTORY_RESET_ADMIN_TOKEN}。
-
• 完成后会立即退出登录并刷新页面,回到当前首装初始状态。
-
-
-
-
-
-
-
-
- );
- return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
- })()}
- {selectorModalPresence.shouldRender && (() => {
- const modal = (
-
-
e.stopPropagation()}
- >
-
勾选模型和群组
-
-
- 选择结果会保存到当前下游 API Key:精确模型用于模型白名单,群组用于路由范围限制。
-
- {selectorLoading ? (
-
-
- 加载路由中...
-
- ) : (
-
-
-
- 精确模型 ({selectorModelSearch.trim()
- ? `${filteredExactModelOptions.length}/${exactModelOptions.length}`
- : exactModelOptions.length})
-
-
setSelectorModelSearch(e.target.value)}
- placeholder="搜索精确模型(支持模糊匹配)"
- style={{ ...inputStyle, padding: '8px 10px', fontSize: 12, marginBottom: 8 }}
- />
-
- {exactModelOptions.length === 0 ? (
-
暂无可选精确模型
- ) : filteredExactModelOptions.length === 0 ? (
-
没有匹配的精确模型
- ) : filteredExactModelOptions.map((modelName) => {
- const checked = downstreamCreate.selectedModels.includes(modelName);
- const brand = getBrand(modelName);
- return (
-
- );
- })}
-
-
-
-
-
- 群组 ({selectorGroupSearch.trim()
- ? `${filteredGroupRouteOptions.length}/${groupRouteOptions.length}`
- : groupRouteOptions.length})
-
-
setSelectorGroupSearch(e.target.value)}
- placeholder="Search groups (name / pattern)"
- style={{ ...inputStyle, padding: '8px 10px', fontSize: 12, marginBottom: 8 }}
- />
-
- {groupRouteOptions.length === 0 ? (
-
暂无可选群组
- ) : filteredGroupRouteOptions.length === 0 ? (
-
没有匹配的群组
- ) : filteredGroupRouteOptions.map((route) => {
- const checked = downstreamCreate.selectedGroupRouteIds.includes(route.id);
- const routeTitle = resolveRouteTitle(route);
- const routeIcon = resolveRouteIcon(route);
- const routeBrand = resolveRouteBrand(route);
- return (
-
- );
- })}
-
-
-
- )}
-
-
-
-
-
-
- );
- return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
- })()}
+
+
);
}
diff --git a/src/web/pages/settings.factory-reset-modal.architecture.test.ts b/src/web/pages/settings.factory-reset-modal.architecture.test.ts
new file mode 100644
index 0000000..efe1189
--- /dev/null
+++ b/src/web/pages/settings.factory-reset-modal.architecture.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+describe('Settings factory reset modal extraction', () => {
+ it('delegates the destructive factory reset portal to a dedicated settings component', () => {
+ const source = readFileSync(resolve(process.cwd(), 'src/web/pages/Settings.tsx'), 'utf8').replace(/\r\n/g, '\n');
+
+ expect(source).toContain("import FactoryResetModal from './settings/FactoryResetModal.js'");
+ expect(source).not.toContain('{factoryResetPresence.shouldRender && (() => {');
+ });
+});
diff --git a/src/web/pages/settings.route-selector-modal.architecture.test.ts b/src/web/pages/settings.route-selector-modal.architecture.test.ts
new file mode 100644
index 0000000..c8eeb67
--- /dev/null
+++ b/src/web/pages/settings.route-selector-modal.architecture.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+describe('Settings route selector modal extraction', () => {
+ it('delegates the downstream route selector portal to a dedicated settings component', () => {
+ const source = readFileSync(resolve(process.cwd(), 'src/web/pages/Settings.tsx'), 'utf8').replace(/\r\n/g, '\n');
+
+ expect(source).toContain("import RouteSelectorModal from './settings/RouteSelectorModal.js'");
+ expect(source).not.toContain('{selectorModalPresence.shouldRender && (() => {');
+ });
+});
diff --git a/src/web/pages/settings/FactoryResetModal.tsx b/src/web/pages/settings/FactoryResetModal.tsx
new file mode 100644
index 0000000..04bca49
--- /dev/null
+++ b/src/web/pages/settings/FactoryResetModal.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+
+type ModalPresence = {
+ shouldRender: boolean;
+ isVisible: boolean;
+};
+
+type FactoryResetModalProps = {
+ presence: ModalPresence;
+ factoryResetting: boolean;
+ factoryResetSecondsLeft: number;
+ adminToken: string;
+ onClose: () => void;
+ onConfirm: () => void;
+};
+
+export default function FactoryResetModal({
+ presence,
+ factoryResetting,
+ factoryResetSecondsLeft,
+ adminToken,
+ onClose,
+ onConfirm,
+}: FactoryResetModalProps) {
+ if (!presence.shouldRender) {
+ return null;
+ }
+
+ const confirmLabel = factoryResetting
+ ? '重新初始化中...'
+ : (factoryResetSecondsLeft > 0
+ ? `确认重新初始化系统(${factoryResetSecondsLeft}s)`
+ : '确认重新初始化系统');
+
+ const modal = (
+
+
e.stopPropagation()}
+ >
+
确认重新初始化系统
+
+
+ 这是不可逆操作。系统会清空当前 metapi 使用中的全部数据库内容,并在成功后立即退出当前登录状态。
+
+
+
• 当前若使用外部 MySQL/Postgres,也会先清空该外部库中的 metapi 数据。
+
• 系统随后会强制切回默认 SQLite。
+
• 管理员 Token 将重置为 {adminToken}。
+
• 完成后会立即退出登录并刷新页面,回到当前首装初始状态。
+
+
+
+
+
+
+
+
+ );
+
+ return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
+}
diff --git a/src/web/pages/settings/RouteSelectorModal.tsx b/src/web/pages/settings/RouteSelectorModal.tsx
new file mode 100644
index 0000000..83b6afd
--- /dev/null
+++ b/src/web/pages/settings/RouteSelectorModal.tsx
@@ -0,0 +1,307 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import { BrandGlyph, InlineBrandIcon, getBrand } from '../../components/BrandIcon.js';
+import {
+ resolveRouteBrand,
+ resolveRouteIcon,
+ resolveRouteTitle,
+} from '../token-routes/utils.js';
+
+type RouteSelectorItem = {
+ id: number;
+ modelPattern: string;
+ displayName?: string | null;
+ displayIcon?: string | null;
+ enabled: boolean;
+};
+
+type ModalPresence = {
+ shouldRender: boolean;
+ isVisible: boolean;
+};
+
+type DownstreamRouteSelection = {
+ selectedModels: string[];
+ selectedGroupRouteIds: number[];
+};
+
+type RouteSelectorModalProps = {
+ presence: ModalPresence;
+ loading: boolean;
+ exactModelOptions: string[];
+ filteredExactModelOptions: string[];
+ groupRouteOptions: RouteSelectorItem[];
+ filteredGroupRouteOptions: RouteSelectorItem[];
+ selectorModelSearch: string;
+ selectorGroupSearch: string;
+ onSelectorModelSearchChange: (value: string) => void;
+ onSelectorGroupSearchChange: (value: string) => void;
+ selection: DownstreamRouteSelection;
+ onToggleModelSelection: (modelName: string) => void;
+ onToggleGroupRouteSelection: (routeId: number) => void;
+ onClose: () => void;
+ inputStyle: React.CSSProperties;
+};
+
+export default function RouteSelectorModal({
+ presence,
+ loading,
+ exactModelOptions,
+ filteredExactModelOptions,
+ groupRouteOptions,
+ filteredGroupRouteOptions,
+ selectorModelSearch,
+ selectorGroupSearch,
+ onSelectorModelSearchChange,
+ onSelectorGroupSearchChange,
+ selection,
+ onToggleModelSelection,
+ onToggleGroupRouteSelection,
+ onClose,
+ inputStyle,
+}: RouteSelectorModalProps) {
+ if (!presence.shouldRender) {
+ return null;
+ }
+
+ const modal = (
+
+
e.stopPropagation()}
+ >
+
勾选模型和群组
+
+
+ 选择结果会保存到当前下游 API Key:精确模型用于模型白名单,群组用于路由范围限制。
+
+ {loading ? (
+
+
+ 加载路由中...
+
+ ) : (
+
+
+
+ 精确模型 ({selectorModelSearch.trim()
+ ? `${filteredExactModelOptions.length}/${exactModelOptions.length}`
+ : exactModelOptions.length})
+
+
onSelectorModelSearchChange(e.target.value)}
+ placeholder="搜索精确模型(支持模糊匹配)"
+ style={{ ...inputStyle, padding: '8px 10px', fontSize: 12, marginBottom: 8 }}
+ />
+
+ {exactModelOptions.length === 0 ? (
+
暂无可选精确模型
+ ) : filteredExactModelOptions.length === 0 ? (
+
没有匹配的精确模型
+ ) : filteredExactModelOptions.map((modelName) => {
+ const checked = selection.selectedModels.includes(modelName);
+ const brand = getBrand(modelName);
+ return (
+
+ );
+ })}
+
+
+
+
+
+ 群组 ({selectorGroupSearch.trim()
+ ? `${filteredGroupRouteOptions.length}/${groupRouteOptions.length}`
+ : groupRouteOptions.length})
+
+
onSelectorGroupSearchChange(e.target.value)}
+ placeholder="Search groups (name / pattern)"
+ style={{ ...inputStyle, padding: '8px 10px', fontSize: 12, marginBottom: 8 }}
+ />
+
+ {groupRouteOptions.length === 0 ? (
+
暂无可选群组
+ ) : filteredGroupRouteOptions.length === 0 ? (
+
没有匹配的群组
+ ) : filteredGroupRouteOptions.map((route) => {
+ const checked = selection.selectedGroupRouteIds.includes(route.id);
+ const routeTitle = resolveRouteTitle(route);
+ const routeIcon = resolveRouteIcon(route);
+ const routeBrand = resolveRouteBrand(route);
+ return (
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+
+
+
+ );
+
+ return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
+}