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; +}