From a574b2c661fab20a0e76076188b14cd459c20318 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Tue, 30 Jun 2026 15:49:58 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=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 | 470 +++++------------- .../renderer/components/layout/AppSidebar.tsx | 275 ++++++++++ .../renderer/components/pages/ClientPage.tsx | 70 --- .../renderer/styles/components/App.module.css | 26 - .../styles/components/AppSidebar.module.css | 321 ++++++++++++ 5 files changed, 717 insertions(+), 445 deletions(-) create mode 100644 qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx create mode 100644 qimingclaw/crates/agent-electron-client/src/renderer/styles/components/AppSidebar.module.css diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx index b2fb83fa..334f61a6 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/App.tsx @@ -9,26 +9,13 @@ import React, { } from "react"; import { ConfigProvider, - Menu, - Badge, Spin, Button, + Modal, notification, message, } from "antd"; -import type { PresetStatusColorType } from "antd/es/_util/colors"; -import { - SettingOutlined, - DashboardOutlined, - FolderOutlined, - InfoCircleOutlined, - SafetyOutlined, - FileTextOutlined, - TeamOutlined, - ArrowLeftOutlined, - ReloadOutlined, - ApiOutlined, -} from "@ant-design/icons"; +import { ArrowLeftOutlined, ReloadOutlined } from "@ant-design/icons"; import { setupService, authService, @@ -39,15 +26,11 @@ import { syncConfigToServer, normalizeServerHost, loginAndRegister, + logout, isLoggedIn as checkIsLoggedIn, } from "./services/core/auth"; -import { - APP_DISPLAY_NAME, - AUTH_KEYS, - normalizeAgentEngine, -} from "@shared/constants"; +import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants"; import type { QuickInitConfig } from "@shared/types/quickInit"; -import type { UpdateState } from "@shared/types/updateTypes"; import { t, getCurrentLang } from "./services/core/i18n"; import SetupWizard from "./components/setup/SetupWizard"; import LoginPage from "./components/setup/LoginPage"; @@ -60,6 +43,10 @@ import LogViewer from "./components/pages/LogViewer"; import PermissionsPage from "./components/pages/PermissionsPage"; import SessionsPage from "./components/pages/SessionsPage"; import MCPSettings from "./components/settings/MCPSettings"; +import AppSidebar, { + NavKey, + LegacyTabKey, +} from "./components/layout/AppSidebar"; import type { WebviewHeaderActions } from "./components/pages/SessionsPage"; import { createLogger } from "./services/utils/rendererLog"; import styles from "./styles/components/App.module.css"; @@ -112,27 +99,7 @@ type TabKey = | "dependencies" | "permissions" | "logs" - | "about" - | "model"; - -// 状态配置(对齐 Tauri 客户端) -// 就绪、繁忙使用橙色(warning)、小点展示 -const STATUS_CONFIG: Record< - string, - { status: PresetStatusColorType; textKey: string } -> = { - idle: { status: "warning", textKey: "Claw.Agent.Status.idle" }, - starting: { status: "processing", textKey: "Claw.Agent.Status.starting" }, - running: { status: "success", textKey: "Claw.Agent.Status.running" }, - busy: { status: "warning", textKey: "Claw.Agent.Status.busy" }, - stopped: { status: "default", textKey: "Claw.Agent.Status.stopped" }, - error: { status: "error", textKey: "Claw.Agent.Status.error" }, -}; - -/** macOS/Linux 无 download-progress 时,header tag 用本地模拟进度避免长期显示 0%。 */ -const HEADER_SIMULATED_PROGRESS_CAP = 90; -const HEADER_SIMULATED_PROGRESS_INTERVAL_MS = 500; -const HEADER_SIMULATED_DURATION_MS = 45_000; + | "about"; // 服务状态接口(与 ClientPage 共享) export interface ServiceItem { @@ -345,36 +312,21 @@ function App() { // 核心状态 // ============================================ const [activeTab, setActiveTab] = useState("client"); + const [activeNavKey, setActiveNavKey] = useState("overview"); const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false); const [webviewActions, setWebviewActions] = useState(null); const [username, setUsername] = useState(""); + const [userEmail, setUserEmail] = useState(""); const [onlineStatus, setOnlineStatus] = useState(null); - const [agentStatus, setAgentStatus] = useState("idle"); const [services, setServices] = useState([]); const [servicesLoading, setServicesLoading] = useState(true); - const [guiMcpEnabled, setGuiMcpEnabled] = useState(false); - const [pollFailCount, setPollFailCount] = useState(0); const [startingServices, setStartingServices] = useState>( new Set(), ); const servicesPollTimer = useRef | null>(null); /** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */ const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0); - const [updateState, setUpdateState] = useState({ - status: "idle", - }); - const [headerSimulatedPercent, setHeaderSimulatedPercent] = useState(0); - const headerSimulatedIntervalRef = useRef | null>(null); - const statusExpectedKeys = useMemo(() => { - const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"]; - if (FEATURES.ENABLE_GUI_AGENT_SERVER && guiMcpEnabled) { - keys.splice(3, 0, "guiServer"); - } - return keys; - }, [guiMcpEnabled]); const getStartupServiceKeys = useCallback(async (): Promise => { const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"]; if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys; @@ -439,6 +391,7 @@ function App() { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); + setUserEmail(user.email || user.phone || user.username || ""); } // 加载在线状态 @@ -461,8 +414,10 @@ function App() { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); + setUserEmail(user.email || user.phone || user.username || ""); } else { setUsername(""); + setUserEmail(""); } }, []); @@ -596,7 +551,6 @@ function App() { ]); const isGuiEnabled = FEATURES.ENABLE_GUI_AGENT_SERVER && (guiEnabledRes?.enabled ?? false); - setGuiMcpEnabled(isGuiEnabled); items.push({ key: "mcpProxy", label: t("Claw.Service.mcp"), @@ -649,10 +603,8 @@ function App() { error: lpStatus?.error, }); setServices(items); - setPollFailCount(0); } catch (error) { console.error("[App] pollServicesStatus failed:", error); - setPollFailCount((count) => count + 1); } finally { setServicesLoading(false); } @@ -838,57 +790,6 @@ function App() { getStartupServiceKeys, ]); - // ============================================ - // 根据服务状态计算 Agent 状态 - // ============================================ - // 根据服务状态计算 Agent 状态(对齐 Tauri 客户端逻辑) - useEffect(() => { - // 如果正在加载,保持当前状态不变(避免初始加载时的闪烁) - if (servicesLoading) { - return; - } - - if (statusExpectedKeys.length === 0) { - setAgentStatus("idle"); - return; - } - - const serviceMap = new Map(services.map((s) => [s.key, s])); - const trackedServices = statusExpectedKeys.map((key) => - serviceMap.get(key), - ); - const runningCount = trackedServices.filter((s) => s?.running).length; - const totalCount = statusExpectedKeys.length; - const hasErrors = trackedServices.some((s) => !!s?.error); - const hasStartingServices = Array.from(startingServices).some((key) => - statusExpectedKeys.includes(key), - ); - const hasStaleServiceStatus = pollFailCount >= 2; - - if (hasStaleServiceStatus) { - // 连续轮询失败时,避免继续展示可能过期的 running 状态。 - setAgentStatus("busy"); - } else if (hasErrors) { - setAgentStatus("error"); - } else if (hasStartingServices) { - setAgentStatus("starting"); - } else if (runningCount === totalCount && runningCount > 0) { - setAgentStatus("running"); - } else if (runningCount > 0 && runningCount < totalCount) { - setAgentStatus("busy"); - } else if (runningCount === 0) { - setAgentStatus("stopped"); - } else { - setAgentStatus("idle"); - } - }, [ - services, - servicesLoading, - startingServices, - statusExpectedKeys, - pollFailCount, - ]); - // 启动服务状态轮询 useEffect(() => { if (isSetupComplete !== true) return; @@ -906,55 +807,6 @@ function App() { }; }, [isSetupComplete]); - // ============================================ - // 监听更新状态(header tag 展示) - // ============================================ - useEffect(() => { - const handler = (state: UpdateState) => { - if (state) setUpdateState(state); - }; - window.electronAPI?.on("update:status", handler as any); - window.electronAPI?.app?.getUpdateState?.()?.then((state) => { - if (state) setUpdateState(state); - }); - return () => { - window.electronAPI?.off("update:status", handler as any); - }; - }, []); - - useEffect(() => { - const isDownloading = updateState.status === "downloading"; - const hasRealProgress = updateState.progress != null; - - if (isDownloading && !hasRealProgress) { - setHeaderSimulatedPercent(0); - const increment = - (HEADER_SIMULATED_PROGRESS_CAP / HEADER_SIMULATED_DURATION_MS) * - HEADER_SIMULATED_PROGRESS_INTERVAL_MS; - const id = setInterval(() => { - setHeaderSimulatedPercent((prev) => { - const next = prev + increment; - return next >= HEADER_SIMULATED_PROGRESS_CAP - ? HEADER_SIMULATED_PROGRESS_CAP - : next; - }); - }, HEADER_SIMULATED_PROGRESS_INTERVAL_MS); - headerSimulatedIntervalRef.current = id; - return () => { - clearInterval(id); - headerSimulatedIntervalRef.current = null; - }; - } - - if (!isDownloading || hasRealProgress) { - if (headerSimulatedIntervalRef.current) { - clearInterval(headerSimulatedIntervalRef.current); - headerSimulatedIntervalRef.current = null; - } - setHeaderSimulatedPercent(0); - } - }, [updateState.status, updateState.progress]); - // ============================================ // 监听托盘/菜单事件 // ============================================ @@ -1069,65 +921,103 @@ function App() { setIsSetupComplete(true); }; - // ============================================ - // 状态 Badge - // ============================================ - const badge = STATUS_CONFIG[agentStatus] || STATUS_CONFIG.idle; + const stopServicesForLogout = useCallback(async () => { + 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 (e) { + console.error(`[App] Failed to stop ${svc.label}:`, e); + } + } + + await window.electronAPI?.computerServer.stop().catch((e: unknown) => { + console.error("[App] Failed to stop computerServer:", e); + }); + }, [services]); + + const handleSidebarLogout = 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 { + await stopServicesForLogout(); + await logout(); + await handleAuthChange(); + setIsLoggedIn(false); + setActiveTab("client"); + setActiveNavKey("overview"); + setServices([]); + setServicesLoading(false); + setStartingServices(new Set()); + setOnlineStatus(false); + message.success(t("Claw.Auth.loggedOut")); + } catch { + message.error(t("Claw.Client.logoutFailed")); + } + }, + }); + }, [handleAuthChange, stopServicesForLogout]); + + const handleNavSelect = useCallback((key: NavKey) => { + setActiveNavKey(key); + + if (key === "overview") { + setActiveTab("client"); + return; + } + + if (key === "newTask") { + setSessionsAutoOpen(true); + setActiveTab("sessions"); + return; + } + + if (key === "aiChat") { + setActiveTab("sessions"); + return; + } + + if (key === "mcpLibrary") { + setActiveTab("mcp"); + return; + } + + if (key === "toolIntegration" || key === "systemSettings") { + setActiveTab("settings"); + return; + } + + setActiveTab("client"); + }, []); + + const handleLegacySelect = useCallback((key: LegacyTabKey) => { + setActiveTab(key); + const navByLegacyTab: Partial> = { + client: "overview", + sessions: "aiChat", + mcp: "mcpLibrary", + settings: "systemSettings", + }; + setActiveNavKey(navByLegacyTab[key] || "systemSettings"); + }, []); // ============================================ // 平台检测 // ============================================ const isMacOS = navigator.platform.toUpperCase().includes("MAC"); - // ============================================ - // 菜单配置(对齐 Tauri 客户端) - // ============================================ - const menuItems = useMemo(() => { - const items = [ - { - key: "client", - icon: , - label: t("Claw.Menu.client"), - }, - { - key: "sessions", - icon: , - label: t("Claw.Menu.session"), - }, - { - key: "mcp", - icon: , - label: t("Claw.Menu.mcp"), - }, - { - key: "settings", - icon: , - label: t("Claw.Menu.settings"), - }, - { - key: "dependencies", - icon: , - label: t("Claw.Menu.dependencies"), - }, - ]; - 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"), - }, - ); - return items; - }, [isMacOS, i18nLang]); - // ============================================ // i18n Context value // ============================================ @@ -1212,9 +1102,8 @@ function App() { value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }} >
- {/* 顶部栏 */} -
- {webviewActions ? ( + {webviewActions && ( +
- ) : ( -
- - {APP_DISPLAY_NAME} - {updateState.status === "available" && ( - { - if (updateState.canAutoUpdate === false) { - await window.electronAPI?.app?.openReleasesPage?.(); - return; - } - try { - setUpdateState((prev) => ({ - ...prev, - status: "downloading", - progress: undefined, - })); - const res = - await window.electronAPI?.app?.downloadUpdate?.(); - if (!res || !res.success) { - message.error( - res?.error || t("Claw.About.downloadFailed"), - ); - setUpdateState((prev) => ({ - ...prev, - status: "available", - })); - } - } catch { - message.error(t("Claw.About.downloadFailed")); - setUpdateState((prev) => ({ - ...prev, - status: "available", - })); - } - }} - onKeyDown={(e) => - (e.key === "Enter" || e.key === " ") && - (e.target as HTMLElement).click() - } - > - {updateState.canAutoUpdate === false - ? t("Claw.App.UpdateTag.newVersion", { - version: updateState.version, - }) - : t("Claw.App.UpdateTag.download", { - version: updateState.version, - })} - - )} - {updateState.status === "downloading" && ( - - {t("Claw.App.UpdateTag.downloading", { - percent: Math.round( - updateState.progress?.percent ?? - headerSimulatedPercent, - ), - })} - - )} - {updateState.status === "downloaded" && ( - { - try { - const res = - await window.electronAPI?.app?.installUpdate?.(); - if (res && !res.success) { - message.error( - res.error || t("Claw.About.installFailed"), - ); - } - } catch { - message.error(t("Claw.About.installFailed")); - } - }} - onKeyDown={(e) => - (e.key === "Enter" || e.key === " ") && - (e.target as HTMLElement).click() - } - > - {t("Claw.About.installUpdate")} - - )} -
- )} -
- {username && ( - {username} - )} - {t(badge.textKey)} - } - />
-
+ )} {/* 主体部分 */}
{/* 左侧边栏 (hidden when webview is active) */} {!webviewActions && ( -
- ({ - key: item.key, - icon: item.icon, - label: item.label, - onClick: () => setActiveTab(item.key as TabKey), - }))} - /> -
+ )} {/* 主内容区:flex 子撑满,便于日志等页占满高度 */} @@ -1392,6 +1161,9 @@ function App() { onNavigate={(tab) => { if (tab === "sessions") setSessionsAutoOpen(true); setActiveTab(tab); + setActiveNavKey( + tab === "sessions" ? "aiChat" : "overview", + ); }} services={services} servicesLoading={servicesLoading} diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx new file mode 100644 index 00000000..69d2f0a3 --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/layout/AppSidebar.tsx @@ -0,0 +1,275 @@ +import React from "react"; +import { Popover } from "antd"; +import { + AppstoreOutlined, + ApiOutlined, + BookOutlined, + BuildOutlined, + CommentOutlined, + DatabaseOutlined, + DeploymentUnitOutlined, + FileTextOutlined, + FolderOutlined, + InfoCircleOutlined, + LogoutOutlined, + MenuFoldOutlined, + PlusCircleOutlined, + ProjectOutlined, + SafetyOutlined, + SettingOutlined, + TeamOutlined, + ThunderboltOutlined, + ToolOutlined, +} from "@ant-design/icons"; +import { APP_DISPLAY_NAME } from "@shared/constants"; +import styles from "../../styles/components/AppSidebar.module.css"; + +export type NavKey = + | "overview" + | "newTask" + | "automation" + | "aiChat" + | "knowledge" + | "document" + | "dataFetch" + | "skills" + | "mcpLibrary" + | "workflow" + | "plugins" + | "toolIntegration" + | "systemSettings"; + +export type LegacyTabKey = + | "client" + | "sessions" + | "mcp" + | "settings" + | "dependencies" + | "permissions" + | "logs" + | "about"; + +interface NavItem { + key: NavKey; + label: string; + icon: React.ReactNode; + badge?: string; +} + +interface LegacyItem { + key: LegacyTabKey; + label: string; + icon: React.ReactNode; +} + +interface AppSidebarProps { + activeNavKey: NavKey; + username: string; + email?: string; + onlineStatus: boolean | null; + isMacOS: boolean; + onNavSelect: (key: NavKey) => void; + onLegacySelect: (key: LegacyTabKey) => void; + onLogout: () => void; +} + +const workspaceItems: NavItem[] = [ + { key: "overview", label: "概览", icon: }, + { + key: "newTask", + label: "新建任务", + icon: , + badge: "3", + }, + { + key: "automation", + label: "自动化", + icon: , + badge: "运行中", + }, + { key: "aiChat", label: "AI 对话", icon: }, + { key: "knowledge", label: "知识库", icon: }, + { key: "document", label: "文档编辑", icon: }, + { key: "dataFetch", label: "智能取数", icon: }, +]; + +const marketItems: NavItem[] = [ + { key: "skills", label: "技能库", icon: , badge: "获取" }, + { key: "mcpLibrary", label: "MCP 库", icon: , badge: "获取" }, + { + key: "workflow", + label: "工作流", + icon: , + badge: "安装", + }, + { + key: "plugins", + label: "插件", + icon: , + badge: "安装", + }, +]; + +const settingItems: NavItem[] = [ + { key: "toolIntegration", label: "工具集成", icon: }, + { key: "systemSettings", label: "系统设置", icon: }, +]; + +function getLegacyItems(isMacOS: boolean): LegacyItem[] { + const items: LegacyItem[] = [ + { key: "client", label: "客户端", icon: }, + { key: "sessions", label: "会话", icon: }, + { key: "mcp", label: "MCP", icon: }, + { key: "settings", label: "设置", icon: }, + { key: "dependencies", label: "依赖", icon: }, + ]; + + if (isMacOS) { + items.push({ key: "permissions", label: "授权", icon: }); + } + + items.push( + { key: "logs", label: "日志", icon: }, + { key: "about", label: "关于", icon: }, + ); + + return items; +} + +function getInitial(name: string): string { + const trimmed = name.trim(); + if (!trimmed) return "陇"; + return trimmed.slice(0, 1).toUpperCase(); +} + +function renderNavItem( + item: NavItem, + activeNavKey: NavKey, + onNavSelect: (key: NavKey) => void, +) { + const active = activeNavKey === item.key; + const isStatusBadge = item.badge === "运行中"; + + return ( + + ); +} + +export default function AppSidebar({ + activeNavKey, + username, + email, + onlineStatus, + isMacOS, + onNavSelect, + onLegacySelect, + onLogout, +}: AppSidebarProps) { + const displayName = username || "陈星宇"; + const displayEmail = email || "xingyu@feitian.com"; + const legacyItems = getLegacyItems(isMacOS); + const online = onlineStatus !== false; + + const historyMenu = ( +
+
历史菜单
+ {legacyItems.map((item) => ( + + ))} +
+ ); + + return ( + + ); +} diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/ClientPage.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/ClientPage.tsx index 7319e862..ccc10266 100644 --- a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/ClientPage.tsx +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/ClientPage.tsx @@ -27,7 +27,6 @@ import { UserOutlined, LockOutlined, GlobalOutlined, - LogoutOutlined, PlayCircleOutlined, PoweroffOutlined, SettingOutlined, @@ -43,7 +42,6 @@ import { import { QRCodeSVG } from "qrcode.react"; import { loginAndRegister, - logout, getCurrentAuth, syncConfigToServer, } from "../../services/core/auth"; @@ -235,65 +233,6 @@ function ClientPage({ } }; - const handleLogout = async () => { - 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 { - // 停止所有运行中或处于 error 状态的服务(error 状态进程可能仍驻留) - const toStop = services.filter((s) => s.running || !!s.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(); - } catch (e) { - console.error(`[ClientPage] Failed to stop ${svc.label}:`, e); - } - } - // computerServer 不在 services 列表中,需单独停止,避免进程残留导致端口冲突 - await window.electronAPI?.computerServer - .stop() - .catch((e: unknown) => { - console.error("[ClientPage] Failed to stop computerServer:", e); - }); - - await logout(); - // 退出登录后,默认回填上一次“服务域名”到登录输入框,减少用户重复输入。 - // 回填优先级说明(从高到低): - // 1) displayDomainFallback:本页面内最近一次明确业务域名(用户输入/认证状态同步得到); - // 2) authState.domain:当前登录态里记录的业务域名; - // 3) step1_config.serverHost:持久化配置兜底(首次进入或刷新后也可恢复)。 - // 说明:这里回填的是“业务域名”,仅用于登录输入体验,不改变 reg/lanproxy 的代理地址逻辑。 - const step1 = (await window.electronAPI?.settings.get( - "step1_config", - )) as { serverHost?: string } | null; - const lastDomain = - displayDomainFallback || - authState.domain || - step1?.serverHost || - ""; - setLoginDomain(lastDomain); - setDisplayDomainFallback(lastDomain); - - setAuthState({ isLoggedIn: false, username: null, domain: null }); - onAuthChange?.(); - } catch { - message.error(t("Claw.Client.logoutFailed")); - } - }, - }); - }; - const getRedirectUrl = useCallback(() => { if (!authState.domain || !authState.userId) return ""; return buildRedirectUrl(authState.domain, authState.userId); @@ -676,15 +615,6 @@ function ClientPage({ > {t("Claw.Client.qrCode")} -
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..a76644a5 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 @@ -3,32 +3,6 @@ * 包含顶部栏、布局等样式 */ -/* 顶部栏右侧信息区 */ -.headerRight { - display: flex; - align-items: center; - gap: 12px; -} - -/* 用户名 */ -.username { - color: #6B7280; - font-size: 12px; -} - -/* 状态徽章文字 */ -.badgeText { - color: #6B7280; - font-size: 12px; -} - -/* 就绪、繁忙状态:橙色小点 */ -.badgeIdle :global(.ant-badge-status-dot) { - width: 6px; - height: 6px; - min-width: 6px; -} - /* Webview 模式下顶部栏左侧操作区(替代 logo) */ .headerWebviewActions { display: flex; diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/AppSidebar.module.css b/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/AppSidebar.module.css new file mode 100644 index 00000000..fc082f30 --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/styles/components/AppSidebar.module.css @@ -0,0 +1,321 @@ +.sidebar { + width: 240px; + min-width: 240px; + height: 100%; + display: flex; + flex-direction: column; + background: #f1f2ef; + border-right: 1px solid #deded9; + color: #1f211f; +} + +.brand { + height: 122px; + display: grid; + grid-template-columns: 38px 1fr 34px; + align-items: center; + gap: 10px; + padding: 26px 12px 18px 20px; +} + +.brandLogo { + width: 36px; + height: 36px; + border-radius: 8px; + object-fit: cover; + box-shadow: 0 8px 18px rgba(217, 116, 74, 0.18); +} + +.brandText { + min-width: 0; +} + +.brandName { + font-size: 15px; + line-height: 22px; + font-weight: 700; + color: #20211f; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.brandSubTitle { + margin-top: 1px; + font-size: 9px; + line-height: 12px; + font-weight: 600; + color: #a09d98; + letter-spacing: 0; + white-space: nowrap; +} + +.historyTrigger { + width: 30px; + height: 30px; + border: 0; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #e4e4df; + color: #656661; + cursor: pointer; + transition: + background-color 0.16s ease, + color 0.16s ease; +} + +.historyTrigger:hover { + background: #dadad4; + color: #252622; +} + +.nav { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 0 11px 16px; +} + +.navSection { + padding-bottom: 12px; + margin-bottom: 12px; + border-bottom: 1px solid #deded8; +} + +.navSection:last-child { + border-bottom: 0; + margin-bottom: 0; +} + +.sectionTitle { + height: 20px; + display: flex; + align-items: center; + padding: 0 2px; + font-size: 11px; + line-height: 16px; + color: #8e8b84; +} + +.navItem { + position: relative; + width: 100%; + height: 34px; + border: 0; + border-radius: 6px; + display: flex; + align-items: center; + gap: 12px; + padding: 0 10px; + background: transparent; + color: #5e5d58; + font: inherit; + text-align: left; + cursor: pointer; + transition: + background-color 0.16s ease, + color 0.16s ease; +} + +.navItem:hover { + background: #e8e8e2; + color: #2d2d29; +} + +.navItemActive { + background: #eaded7; + color: #d66c47; + font-weight: 600; +} + +.navItemActive::after { + content: ""; + position: absolute; + top: 8px; + right: 10px; + width: 3px; + height: 18px; + border-radius: 2px; + background: #df7653; +} + +.navIcon { + width: 18px; + min-width: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; +} + +.navLabel { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; +} + +.navBadge { + max-width: 46px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 7px; + border-radius: 9px; + background: #e6f0ff; + color: #4d8dea; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.navBadgeSuccess { + max-width: 52px; + background: #dff5e7; + color: #16b268; +} + +.footer { + padding: 12px 11px 14px; + border-top: 1px solid #deded8; +} + +.userCard { + height: 52px; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border: 1px solid #deded8; + border-radius: 8px; + background: #f6f7f4; +} + +.avatar { + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #ec9a67, #df7653); + color: #ffffff; + font-size: 13px; + font-weight: 700; +} + +.userMeta { + flex: 1; + min-width: 0; +} + +.userName { + color: #252622; + font-size: 13px; + line-height: 18px; + font-weight: 700; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.userEmail { + color: #8e8b84; + font-size: 11px; + line-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.onlineDot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #b8b8b0; +} + +.onlineDotActive { + background: #21bf63; +} + +.logoutButton { + width: 100%; + height: 34px; + border: 0; + border-radius: 7px; + display: flex; + align-items: center; + gap: 8px; + margin-top: 9px; + padding: 0 12px; + background: #f1e3df; + color: #e64f45; + font: inherit; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: + background-color 0.16s ease, + color 0.16s ease; +} + +.logoutButton:hover { + background: #ead4cf; + color: #c8342e; +} + +.historyPopover :global(.ant-popover-inner) { + padding: 8px; + border-radius: 10px; + box-shadow: 0 12px 28px rgba(22, 24, 26, 0.12); +} + +.historyPopover :global(.ant-popover-arrow) { + display: none; +} + +.historyMenu { + width: 176px; +} + +.historyTitle { + padding: 4px 8px 8px; + color: #8e8b84; + font-size: 12px; + font-weight: 600; +} + +.historyItem { + width: 100%; + height: 32px; + border: 0; + border-radius: 7px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + background: transparent; + color: #4e4e49; + font: inherit; + font-size: 13px; + text-align: left; + cursor: pointer; +} + +.historyItem:hover { + background: #f4f4f1; + color: #d66c47; +} + +.historyIcon { + width: 16px; + display: inline-flex; + align-items: center; + justify-content: center; +}