From 6323987a0d8cd32ac384a3945f1c20ee5c81592c Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Fri, 12 Jun 2026 16:33:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E8=B4=A6=E5=8F=B7=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E4=BE=A7=E6=A0=8F=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/renderer/App.tsx | 296 ++++++++++++++---- .../components/pages/SettingsPage.tsx | 45 ++- .../src/renderer/services/core/setup.ts | 1 + .../renderer/styles/components/App.module.css | 78 +++++ .../renderer/utils/sidebarNavigation.test.ts | 32 ++ .../src/renderer/utils/sidebarNavigation.ts | 49 +++ .../src/shared/locales/en-US.json | 7 + .../src/shared/locales/zh-CN.json | 7 + .../src/shared/locales/zh-HK.json | 7 + .../src/shared/locales/zh-TW.json | 7 + 10 files changed, 458 insertions(+), 71 deletions(-) create mode 100644 qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.test.ts create mode 100644 qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.ts diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx index e0fcdea8..24275dc2 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx @@ -13,22 +13,25 @@ import { Badge, Spin, Button, + Dropdown, + Modal, notification, message, } from "antd"; +import type { MenuProps } from "antd"; import type { PresetStatusColorType } from "antd/es/_util/colors"; import { SettingOutlined, DashboardOutlined, - FolderOutlined, - InfoCircleOutlined, - SafetyOutlined, - FileTextOutlined, TeamOutlined, ArrowLeftOutlined, ReloadOutlined, - ApiOutlined, AppstoreOutlined, + SkinOutlined, + QuestionCircleOutlined, + SyncOutlined, + LogoutOutlined, + UserOutlined, } from "@ant-design/icons"; import { setupService, @@ -40,6 +43,7 @@ import { syncConfigToServer, normalizeServerHost, loginAndRegister, + logout as authLogout, } from "./services/core/auth"; import { APP_DISPLAY_NAME, @@ -65,6 +69,14 @@ import { createLogger } from "./services/utils/rendererLog"; import styles from "./styles/components/App.module.css"; import { lightTheme, darkTheme } from "./styles/theme"; import { FEATURES } from "@shared/featureFlags"; +import { + ACCOUNT_MENU_KEYS, + PRIMARY_NAV_KEYS, + getAccountMenuAction, + type AccountMenuKey, + type AppTabKey, + type SettingsFocusTarget, +} from "./utils/sidebarNavigation"; // 主题类型 export type ThemeMode = "light" | "dark" | "system"; @@ -104,17 +116,7 @@ export function useI18nLang(): I18nContextValue { } // Tab 类型定义(对齐 Tauri 客户端) -type TabKey = - | "client" - | "overview" - | "sessions" - | "mcp" - | "settings" - | "dependencies" - | "permissions" - | "logs" - | "about" - | "model"; +type TabKey = AppTabKey; // 状态配置(对齐 Tauri 客户端) // 就绪、繁忙使用橙色(warning)、小点展示 @@ -343,10 +345,14 @@ function App() { // 核心状态 // ============================================ const [activeTab, setActiveTab] = useState("client"); + const [settingsFocusTarget, setSettingsFocusTarget] = + useState(null); + const [settingsFocusNonce, setSettingsFocusNonce] = useState(0); const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false); const [webviewActions, setWebviewActions] = useState(null); const [username, setUsername] = useState(""); + const [userAvatar, setUserAvatar] = useState(""); const [onlineStatus, setOnlineStatus] = useState(null); const [agentStatus, setAgentStatus] = useState("idle"); const [services, setServices] = useState([]); @@ -432,6 +438,7 @@ function App() { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); + setUserAvatar(user.avatar || ""); } // 加载在线状态 @@ -452,8 +459,56 @@ function App() { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); + setUserAvatar(user.avatar || ""); } else { setUsername(""); + setUserAvatar(""); + } + }, []); + + const navigateToSettings = useCallback((focus?: SettingsFocusTarget) => { + if (focus) { + setSettingsFocusTarget(focus); + setSettingsFocusNonce((value) => value + 1); + } else { + setSettingsFocusTarget(null); + } + setActiveTab("settings"); + }, []); + + const handleCheckUpdate = useCallback(async () => { + setUpdateState((prev) => ({ ...prev, status: "checking" })); + try { + const result = await window.electronAPI?.app?.checkUpdate(); + if (!result) { + setUpdateState({ status: "idle" }); + return; + } + + if (result.alreadyChecking) { + const state = await window.electronAPI?.app?.getUpdateState?.(); + if (state) setUpdateState(state); + return; + } + + if (result.error) { + message.error( + t("Claw.About.checkFailedWithDetail", { error: result.error }), + ); + } else if (!result.hasUpdate) { + message.info(t("Claw.About.alreadyLatest")); + } + + const authoritative = await window.electronAPI?.app?.getUpdateState?.(); + if (authoritative) { + setUpdateState(authoritative); + } else if (result.error || !result.hasUpdate) { + setUpdateState({ status: "idle" }); + } + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + message.error(t("Claw.About.checkFailedWithDetail", { error: detail })); + setUpdateState({ status: "idle" }); } }, []); @@ -626,6 +681,71 @@ function App() { } }, []); + const handleLogout = useCallback(() => { + Modal.confirm({ + title: t("Claw.Client.logoutConfirm"), + content: t("Claw.Client.logoutConfirmDetail"), + okText: t("Claw.Client.logout"), + cancelText: t("Claw.Client.cancel"), + okButtonProps: { danger: true }, + onOk: async () => { + try { + const toStop = services.filter((svc) => svc.running || !!svc.error); + for (const svc of toStop) { + try { + if (svc.key === "agent") { + await window.electronAPI?.agent.destroy(); + } else if (svc.key === "fileServer") { + await window.electronAPI?.fileServer.stop(); + } else if (svc.key === "lanproxy") { + await window.electronAPI?.lanproxy.stop(); + } else if (svc.key === "mcpProxy") { + await window.electronAPI?.mcp.stop(); + } else if (svc.key === "guiServer") { + await window.electronAPI?.guiServer?.stop(); + } + } catch (error) { + console.error(`[App] Failed to stop ${svc.label}:`, error); + } + } + + await window.electronAPI?.computerServer.stop().catch((error) => { + console.error("[App] Failed to stop computerServer:", error); + }); + await authLogout(); + await handleAuthChange(); + setAuthRefreshTrigger((value) => value + 1); + await pollServicesStatus(); + setActiveTab("client"); + } catch { + message.error(t("Claw.Client.logoutFailed")); + } + }, + }); + }, [handleAuthChange, pollServicesStatus, services]); + + const handleAccountMenuClick = useCallback( + ({ key }: { key: string }) => { + const action = getAccountMenuAction(key as AccountMenuKey); + if ("tab" in action) { + if (action.tab === "settings") { + navigateToSettings(action.settingsFocus); + } else { + setSettingsFocusTarget(null); + setActiveTab(action.tab); + } + return; + } + + if (action.command === "checkUpdate") { + handleCheckUpdate(); + } else if (action.command === "logout") { + handleLogout(); + } + }, + [handleCheckUpdate, handleLogout, navigateToSettings], + ); + // ============================================ // 逐个启动服务(实时更新状态) // ============================================ @@ -776,6 +896,7 @@ function App() { user.username || t("Claw.App.defaultUsername"), ); + setUserAvatar(user.avatar || ""); } setAuthRefreshTrigger((v) => v + 1); await startServicesSequentially(await getStartupServiceKeys()); @@ -1042,64 +1163,66 @@ function App() { // ============================================ const badge = STATUS_CONFIG[agentStatus] || STATUS_CONFIG.idle; - // ============================================ - // 平台检测 - // ============================================ - const isMacOS = navigator.platform.toUpperCase().includes("MAC"); - // ============================================ // 菜单配置(对齐 Tauri 客户端) // ============================================ const menuItems = useMemo(() => { - const items = [ - { + const itemMap = { + client: { key: "client", icon: , label: t("Claw.Menu.client"), }, - { + overview: { key: "overview", icon: , label: t("Claw.Menu.overview"), }, - { + sessions: { key: "sessions", icon: , label: t("Claw.Menu.session"), }, - { - key: "mcp", - icon: , - label: t("Claw.Menu.mcp"), - }, - { + } satisfies Record< + (typeof PRIMARY_NAV_KEYS)[number], + NonNullable[number] + >; + + return PRIMARY_NAV_KEYS.map((key) => itemMap[key]); + }, [i18nLang]); + + const accountMenuItems = useMemo(() => { + const itemMap = { + settings: { key: "settings", icon: , - label: t("Claw.Menu.settings"), + label: t("Claw.AccountMenu.settings"), }, - { - key: "dependencies", - icon: , - label: t("Claw.Menu.dependencies"), + appearance: { + key: "appearance", + icon: , + label: t("Claw.AccountMenu.appearance"), }, - ]; - if (isMacOS) { - items.push({ - key: "permissions", - icon: , - label: t("Claw.Menu.authorization"), - }); - } - items.push( - { key: "logs", icon: , label: t("Claw.Menu.logs") }, - { - key: "about", - icon: , - label: t("Claw.Menu.about"), + helpFeedback: { + key: "helpFeedback", + icon: , + label: t("Claw.AccountMenu.helpFeedback"), }, - ); - return items; - }, [isMacOS, i18nLang]); + checkUpdate: { + key: "checkUpdate", + icon: , + label: t("Claw.AccountMenu.checkUpdate"), + }, + logout: { + key: "logout", + danger: true, + icon: , + label: t("Claw.AccountMenu.logout"), + }, + } satisfies Record[number]>; + + return ACCOUNT_MENU_KEYS.map((key) => itemMap[key]); + }, [i18nLang]); // ============================================ // i18n Context value @@ -1310,21 +1433,55 @@ function App() { className={ // 英文菜单文案通常更长,侧边栏适当加宽以减少截断;其他语言保持默认宽度 i18nLang.toLowerCase().startsWith("en") - ? "app-sider app-sider-en" - : "app-sider" + ? `app-sider app-sider-en ${styles.siderWithAccount}` + : `app-sider ${styles.siderWithAccount}` } > - ({ - key: item.key, - icon: item.icon, - label: item.label, - onClick: () => setActiveTab(item.key as TabKey), - }))} - /> +
+ ({ + ...item, + onClick: () => setActiveTab(item?.key as TabKey), + }))} + /> +
+ + + )} @@ -1370,7 +1527,12 @@ function App() { /> )} {activeTab === "mcp" && } - {activeTab === "settings" && } + {activeTab === "settings" && ( + + )} {activeTab === "dependencies" && } {activeTab === "permissions" && } {activeTab === "logs" && } diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/SettingsPage.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/SettingsPage.tsx index 1ca1b6a5..b430b94c 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/SettingsPage.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/SettingsPage.tsx @@ -7,7 +7,13 @@ * - 系统设置(主题、开机自启动、日志目录) */ -import React, { useState, useEffect, useCallback, Suspense } from "react"; +import React, { + useState, + useEffect, + useCallback, + Suspense, + useRef, +} from "react"; import { Button, Form, @@ -59,6 +65,7 @@ import i18next from "../../services/i18n"; import styles from "../../styles/components/ClientPage.module.css"; import { useTheme, useI18nLang, type ThemeMode } from "../../App"; +import type { SettingsFocusTarget } from "../../utils/sidebarNavigation"; import type { SandboxCapabilities, SandboxPolicy, @@ -95,9 +102,18 @@ const LOCAL_LANG_OPTIONS = [ { value: "zh-hk", label: t("Claw.Settings.system.langChineseHK") }, ]; -export default function SettingsPage() { +interface SettingsPageProps { + focusTarget?: SettingsFocusTarget | null; + focusNonce?: number; +} + +export default function SettingsPage({ + focusTarget, + focusNonce = 0, +}: SettingsPageProps) { // 主题 const { themeMode, setThemeMode } = useTheme(); + const appearanceRowRef = useRef(null); // 服务配置 const [form] = Form.useForm(); @@ -264,7 +280,24 @@ export default function SettingsPage() { handleAutolaunchChanged as any, ); }; - }, [loadConfig, loadAiConfig, loadSystemSettings, loadSandboxState]); + }, [ + loadConfig, + loadAiConfig, + loadSystemSettings, + loadSandboxState, + loadGuiMcpState, + ]); + + useEffect(() => { + if (focusTarget !== "appearance") return; + window.requestAnimationFrame(() => { + appearanceRowRef.current?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + appearanceRowRef.current?.focus({ preventScroll: true }); + }); + }, [focusTarget, focusNonce]); // ========== 加载语言列表 ========== useEffect(() => { @@ -786,7 +819,11 @@ export default function SettingsPage() { {/* 主题设置 */} -
+
diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/setup.ts b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/setup.ts index 7965934e..5923702e 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/services/core/setup.ts +++ b/qimingclaw/crates/agent-electron-client/src/renderer/services/core/setup.ts @@ -36,6 +36,7 @@ export interface AuthUserInfo { id?: number; username: string; displayName?: string; + avatar?: string; token?: string; userId?: string; email?: string; diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/App.module.css b/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/App.module.css index 23d31977..6240e4cb 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/App.module.css +++ b/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/App.module.css @@ -35,3 +35,81 @@ align-items: center; gap: 6px; } + +.siderWithAccount { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.siderNavArea { + flex: 1; + min-height: 0; + overflow-y: auto; + padding-top: 4px; +} + +.accountButton { + width: calc(100% - 16px); + min-height: 54px; + margin: 8px; + padding: 8px; + border: 0; + border-radius: var(--border-radius); + background: transparent; + color: var(--color-text); + display: flex; + align-items: center; + gap: 8px; + text-align: left; + cursor: pointer; + transition: background-color 0.16s ease; +} + +.accountButton:hover, +.accountButton:focus-visible { + background: var(--color-bg-section-header); + outline: none; +} + +.accountAvatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--color-bg-elevated); + color: var(--color-icon); + border: 1px solid var(--color-border); + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; +} + +.accountAvatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.accountText { + min-width: 0; + display: flex; + flex-direction: column; + line-height: 1.25; +} + +.accountName { + color: var(--color-text); + font-size: 13px; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accountHint { + color: var(--color-text-tertiary); + font-size: 11px; + margin-top: 2px; +} diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.test.ts b/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.test.ts new file mode 100644 index 00000000..43b66739 --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { + ACCOUNT_MENU_KEYS, + PRIMARY_NAV_KEYS, + getAccountMenuAction, +} from "./sidebarNavigation"; + +describe("sidebar navigation", () => { + it("keeps only client, overview, and sessions in the primary sidebar", () => { + expect(PRIMARY_NAV_KEYS).toEqual(["client", "overview", "sessions"]); + }); + + it("maps account menu entries to internal navigation or commands", () => { + expect(ACCOUNT_MENU_KEYS).toEqual([ + "settings", + "appearance", + "helpFeedback", + "checkUpdate", + "logout", + ]); + expect(getAccountMenuAction("settings")).toEqual({ tab: "settings" }); + expect(getAccountMenuAction("appearance")).toEqual({ + tab: "settings", + settingsFocus: "appearance", + }); + expect(getAccountMenuAction("helpFeedback")).toEqual({ tab: "about" }); + expect(getAccountMenuAction("checkUpdate")).toEqual({ + command: "checkUpdate", + }); + expect(getAccountMenuAction("logout")).toEqual({ command: "logout" }); + }); +}); diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.ts b/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.ts new file mode 100644 index 00000000..08f01f15 --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/utils/sidebarNavigation.ts @@ -0,0 +1,49 @@ +export type AppTabKey = + | "client" + | "overview" + | "sessions" + | "mcp" + | "settings" + | "dependencies" + | "permissions" + | "logs" + | "about" + | "model"; + +export type SettingsFocusTarget = "appearance"; + +export type AccountMenuKey = + | "settings" + | "appearance" + | "helpFeedback" + | "checkUpdate" + | "logout"; + +export type AccountMenuAction = + | { tab: AppTabKey; settingsFocus?: SettingsFocusTarget } + | { command: "checkUpdate" | "logout" }; + +export const PRIMARY_NAV_KEYS = ["client", "overview", "sessions"] as const; + +export const ACCOUNT_MENU_KEYS = [ + "settings", + "appearance", + "helpFeedback", + "checkUpdate", + "logout", +] as const; + +export function getAccountMenuAction(key: AccountMenuKey): AccountMenuAction { + switch (key) { + case "settings": + return { tab: "settings" }; + case "appearance": + return { tab: "settings", settingsFocus: "appearance" }; + case "helpFeedback": + return { tab: "about" }; + case "checkUpdate": + return { command: "checkUpdate" }; + case "logout": + return { command: "logout" }; + } +} diff --git a/qimingclaw/crates/agent-electron-client/src/shared/locales/en-US.json b/qimingclaw/crates/agent-electron-client/src/shared/locales/en-US.json index ed09d7cc..33299a19 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/locales/en-US.json +++ b/qimingclaw/crates/agent-electron-client/src/shared/locales/en-US.json @@ -172,6 +172,13 @@ "Claw.Menu.authorization": "Authorization", "Claw.Menu.logs": "Logs", "Claw.Menu.about": "About", + "Claw.AccountMenu.open": "Open account menu", + "Claw.AccountMenu.hint": "Account & settings", + "Claw.AccountMenu.settings": "Settings", + "Claw.AccountMenu.appearance": "Appearance", + "Claw.AccountMenu.helpFeedback": "Help & Feedback", + "Claw.AccountMenu.checkUpdate": "Check for Updates", + "Claw.AccountMenu.logout": "Log Out", "Claw.App.ConfigSyncFailed": "Config Sync Failed", "Claw.App.ConfigSyncFailedDetail": "Unable to connect to server for latest config. Services not restarted. Please check your network and try again.", "Claw.App.Retry": "Retry", diff --git a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-CN.json b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-CN.json index 35b8d5f3..92378aeb 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-CN.json +++ b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-CN.json @@ -172,6 +172,13 @@ "Claw.Menu.authorization": "授权", "Claw.Menu.logs": "日志", "Claw.Menu.about": "关于", + "Claw.AccountMenu.open": "打开账户菜单", + "Claw.AccountMenu.hint": "账户与设置", + "Claw.AccountMenu.settings": "设置", + "Claw.AccountMenu.appearance": "外观", + "Claw.AccountMenu.helpFeedback": "帮助与反馈", + "Claw.AccountMenu.checkUpdate": "检查更新", + "Claw.AccountMenu.logout": "退出登录", "Claw.App.ConfigSyncFailed": "配置同步失败", "Claw.App.ConfigSyncFailedDetail": "无法连接到服务器获取最新配置,服务未重启。请检查网络后重试。", "Claw.App.Retry": "重试", diff --git a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-HK.json b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-HK.json index 95084660..b6780671 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-HK.json +++ b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-HK.json @@ -172,6 +172,13 @@ "Claw.Menu.authorization": "授權", "Claw.Menu.logs": "日誌", "Claw.Menu.about": "關於", + "Claw.AccountMenu.open": "開啟帳戶選單", + "Claw.AccountMenu.hint": "帳戶與設定", + "Claw.AccountMenu.settings": "設定", + "Claw.AccountMenu.appearance": "外觀", + "Claw.AccountMenu.helpFeedback": "幫助與反饋", + "Claw.AccountMenu.checkUpdate": "檢查更新", + "Claw.AccountMenu.logout": "退出登入", "Claw.App.ConfigSyncFailed": "配置同步失敗", "Claw.App.ConfigSyncFailedDetail": "無法連接到服務器獲取最新配置,服務未重啟。請檢查網絡後重試。", "Claw.App.Retry": "重試", diff --git a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-TW.json b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-TW.json index 4b28bd68..003a6ab6 100644 --- a/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-TW.json +++ b/qimingclaw/crates/agent-electron-client/src/shared/locales/zh-TW.json @@ -172,6 +172,13 @@ "Claw.Menu.authorization": "授權", "Claw.Menu.logs": "日誌", "Claw.Menu.about": "關於", + "Claw.AccountMenu.open": "開啟帳戶選單", + "Claw.AccountMenu.hint": "帳戶與設定", + "Claw.AccountMenu.settings": "設定", + "Claw.AccountMenu.appearance": "外觀", + "Claw.AccountMenu.helpFeedback": "幫助與回饋", + "Claw.AccountMenu.checkUpdate": "檢查更新", + "Claw.AccountMenu.logout": "退出登入", "Claw.App.ConfigSyncFailed": "配置同步失敗", "Claw.App.ConfigSyncFailedDetail": "無法連接到服務器獲取最新配置,服務未重啟。請檢查網絡後重試。", "Claw.App.Retry": "重試",