From 8481d207ff83c7fecbe47cdc530ba3f0a6bd0e20 Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Sat, 5 Sep 2026 02:02:29 +0800 Subject: [PATCH 1/7] Redesign Settings as floating window --- web/src/components/SettingsPage.jsx | 275 ++++++++++-------- web/src/i18n/sourceCatalog.generated.js | 10 - web/src/lib/hooksSettings.js | 2 +- web/src/lib/hooksSettings.test.js | 2 +- web/src/lib/modelSettingsArchitecture.test.js | 2 +- web/src/lib/runTests.js | 1 + .../lib/settingsWindowArchitecture.test.js | 89 ++++++ web/src/styles/globals.css | 38 +++ 8 files changed, 287 insertions(+), 132 deletions(-) create mode 100644 web/src/lib/settingsWindowArchitecture.test.js diff --git a/web/src/components/SettingsPage.jsx b/web/src/components/SettingsPage.jsx index 7b8c56546..57291ba78 100644 --- a/web/src/components/SettingsPage.jsx +++ b/web/src/components/SettingsPage.jsx @@ -1,4 +1,4 @@ -// 全屏设置页:左栏导航 + 右栏内容(Codex 风格)。 +// 浮动设置窗口:mask + 左栏导航 + 右栏内容(Codex 风格)。 // // 左侧导航按 Codex 风格分组,section key 与深链行为保持稳定。 // 后端真实接入的 section:常规 (权限模式) / 外观 (主题) / 配置 / 个性化 / 技能 / 模型 / 工具。 @@ -94,12 +94,6 @@ import { selectRemoteWebConnection, waitForRemoteWebMode, } from '../lib/remoteWeb.js'; -import { - WindowControls, - isInteractiveTarget, - nativePointerEvent, - useFramelessWindowState, -} from './WindowControls.jsx'; const DEFAULT_UPGRADE_SERVICE_URL = 'http://2017studio.imwork.net:82/aupdate/'; const FONT_SIZE_OPTIONS = [ @@ -156,52 +150,75 @@ export function SettingsPage({ () => settingsNavIndexForKey(initialNavKey), ); const [show, setShow] = useState(false); - const { framelessDesktop, isMaximized } = useFramelessWindowState(); + const [expanded, setExpanded] = useState(false); + const closeTimerRef = useRef(null); const activeNavKey = SETTINGS_NAV_ITEMS[activeNav]?.key || 'general'; useEffect(() => { requestAnimationFrame(() => setShow(true)); }, []); useEffect(() => { setActiveNav(settingsNavIndexForKey(initialNavKey)); }, [initialNavKey]); - const close = () => { setShow(false); setTimeout(onClose, 240); }; - - const onHeaderMouseDown = (event) => { - if (!framelessDesktop || event.button !== 0 || isInteractiveTarget(event.target)) return; - event.preventDefault(); - if (event.detail >= 2 && typeof window.aceDesktop_toggleMaximizeWindow === 'function') { - window.aceDesktop_toggleMaximizeWindow(); - return; - } - window.aceDesktop_startWindowDrag(nativePointerEvent(event)); + useEffect(() => () => { + if (closeTimerRef.current) clearTimeout(closeTimerRef.current); + }, []); + const close = useCallback(() => { + if (closeTimerRef.current) return; + setShow(false); + closeTimerRef.current = setTimeout(onClose, 220); + }, [onClose]); + + const onMaskClick = (event) => { + if (event.target === event.currentTarget) close(); }; return (
- - 设置 - {/* 占位:让标题居中,与 TopBar 视觉对齐;frameless 模式下右侧由 WindowControls 占据 */} -
- {framelessDesktop && } +
+ + 设置 + +
+ + +
-
-
+
+
); @@ -1281,6 +1299,7 @@ function SectionConfig() { const [upgradeSaving, setUpgradeSaving] = useState(false); const [upgradeSaved, setUpgradeSaved] = useState(false); const [upgradeError, setUpgradeError] = useState(''); + const lastSavedUpgradeUrlRef = useRef(DEFAULT_UPGRADE_SERVICE_URL); const [depPython, setDepPython] = useState(true); const [depNode, setDepNode] = useState(true); const [depCsharp, setDepCsharp] = useState(false); @@ -1293,7 +1312,11 @@ function SectionConfig() { setUpgradeError(''); api.getUpgradeConfig() .then((cfg) => { - if (!cancelled) setUpgradeUrl(cfg?.base_url || DEFAULT_UPGRADE_SERVICE_URL); + if (!cancelled) { + const loadedUrl = cfg?.base_url || DEFAULT_UPGRADE_SERVICE_URL; + lastSavedUpgradeUrlRef.current = loadedUrl; + setUpgradeUrl(loadedUrl); + } }) .catch((e) => { if (!cancelled) setUpgradeError(e?.message || String(e)); @@ -1304,25 +1327,33 @@ function SectionConfig() { return () => { cancelled = true; }; }, []); - const saveUpgradeUrl = async () => { - const baseUrl = upgradeUrl.trim(); + const saveUpgradeUrl = async (candidate = upgradeUrl) => { + if (upgradeSaving || upgradeLoading) return false; + const baseUrl = candidate.trim(); if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) { setUpgradeError('升级服务 URL 必须使用 http 或 https'); - return; + return false; + } + if (baseUrl === lastSavedUpgradeUrlRef.current) { + setUpgradeUrl(baseUrl); + return true; } setUpgradeSaving(true); setUpgradeSaved(false); setUpgradeError(''); try { const saved = await api.setUpgradeConfig({ base_url: baseUrl }); - setUpgradeUrl(saved?.base_url || baseUrl); + const savedUrl = saved?.base_url || baseUrl; + lastSavedUpgradeUrlRef.current = savedUrl; + setUpgradeUrl(savedUrl); setUpgradeSaved(true); - toast({ kind: 'ok', text: '升级服务 URL 已保存' }); setTimeout(() => setUpgradeSaved(false), 1500); + return true; } catch (e) { const message = e?.message || String(e); setUpgradeError(message); toast({ kind: 'err', text: message }); + return false; } finally { setUpgradeSaving(false); } @@ -1368,6 +1399,7 @@ function SectionConfig() { setUpgradeSaved(false); setUpgradeError(''); }} + onBlur={() => { void saveUpgradeUrl(); }} disabled={upgradeLoading || upgradeSaving} spellCheck={false} className={clsx( @@ -1378,44 +1410,25 @@ function SectionConfig() { /> - {upgradeError &&
{upgradeError}
} + {!upgradeError && (upgradeSaving || upgradeSaved) && ( +
+ {upgradeSaving ? '保存中...' : '已保存'} +
+ )}
工作空间依赖项
@@ -1506,13 +1519,18 @@ function SectionPersonalization() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); + const lastSavedTextRef = useRef(''); useEffect(() => { let cancelled = false; setLoading(true); api.getCustomInstructions() .then((cfg) => { - if (!cancelled) setText(typeof cfg?.text === 'string' ? cfg.text : ''); + if (!cancelled) { + const loadedText = typeof cfg?.text === 'string' ? cfg.text : ''; + lastSavedTextRef.current = loadedText; + setText(loadedText); + } }) .catch((e) => { if (!cancelled) { @@ -1525,18 +1543,22 @@ function SectionPersonalization() { return () => { cancelled = true; }; }, []); - const save = async () => { - if (saving || loading) return; + const save = async (candidate = text) => { + if (saving || loading) return false; + if (candidate === lastSavedTextRef.current) return true; setSaving(true); setSaved(false); try { - const result = await api.setCustomInstructions({ text }); - if (typeof result?.text === 'string') setText(result.text); + const result = await api.setCustomInstructions({ text: candidate }); + const savedText = typeof result?.text === 'string' ? result.text : candidate; + lastSavedTextRef.current = savedText; + setText(savedText); setSaved(true); - toast({ kind: 'ok', text: '自定义指令已保存' }); setTimeout(() => setSaved(false), 1500); + return true; } catch (e) { toast({ kind: 'err', text: '保存自定义指令失败:' + (e?.message || '') }); + return false; } finally { setSaving(false); } @@ -1555,6 +1577,7 @@ function SectionPersonalization() {