重构客户端菜单与设置入口

This commit is contained in:
baiyanyun
2026-07-14 16:38:29 +08:00
parent c3d6c6dc8e
commit 5b14b3f48d
11 changed files with 616 additions and 336 deletions

View File

@@ -187,6 +187,12 @@ function createWindow() {
minHeight: DEFAULT_WINDOW_MIN_HEIGHT, minHeight: DEFAULT_WINDOW_MIN_HEIGHT,
title: APP_DISPLAY_NAME, title: APP_DISPLAY_NAME,
icon: getIconPath(), icon: getIconPath(),
...(process.platform === "darwin"
? {
titleBarStyle: "hiddenInset" as const,
trafficLightPosition: { x: 18, y: 16 },
}
: {}),
webPreferences: { webPreferences: {
preload: path.join(__dirname, "..", "preload", "index.js"), preload: path.join(__dirname, "..", "preload", "index.js"),
contextIsolation: true, contextIsolation: true,

View File

@@ -15,7 +15,7 @@ import {
notification, notification,
message, message,
} from "antd"; } from "antd";
import { ArrowLeftOutlined, ReloadOutlined } from "@ant-design/icons"; import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { import {
setupService, setupService,
authService, authService,
@@ -29,7 +29,11 @@ import {
logout, logout,
isLoggedIn as checkIsLoggedIn, isLoggedIn as checkIsLoggedIn,
} from "./services/core/auth"; } from "./services/core/auth";
import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants"; import {
APP_DISPLAY_NAME,
AUTH_KEYS,
normalizeAgentEngine,
} from "@shared/constants";
import type { QuickInitConfig } from "@shared/types/quickInit"; import type { QuickInitConfig } from "@shared/types/quickInit";
import type { UpdateState } from "@shared/types/updateTypes"; import type { UpdateState } from "@shared/types/updateTypes";
import { t, getCurrentLang } from "./services/core/i18n"; import { t, getCurrentLang } from "./services/core/i18n";
@@ -38,12 +42,6 @@ import LoginPage from "./components/setup/LoginPage";
import SetupDependencies from "./components/setup/SetupDependencies"; import SetupDependencies from "./components/setup/SetupDependencies";
import ClientPage from "./components/pages/ClientPage"; import ClientPage from "./components/pages/ClientPage";
import OverviewPage from "./components/pages/OverviewPage"; import OverviewPage from "./components/pages/OverviewPage";
import SettingsPage from "./components/pages/SettingsPage";
import DependenciesPage from "./components/pages/DependenciesPage";
import AboutPage from "./components/pages/AboutPage";
import LogViewer from "./components/pages/LogViewer";
import PermissionsPage from "./components/pages/PermissionsPage";
import SessionsPage from "./components/pages/SessionsPage";
import AiChatPage from "./components/pages/AiChatPage"; import AiChatPage from "./components/pages/AiChatPage";
import AutomationPage from "./components/pages/AutomationPage"; import AutomationPage from "./components/pages/AutomationPage";
import KnowledgePage from "./components/pages/KnowledgePage"; import KnowledgePage from "./components/pages/KnowledgePage";
@@ -53,15 +51,15 @@ import McpMarketplacePage from "./components/pages/McpMarketplacePage";
import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage"; import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage";
import DataFetchPage from "./components/pages/DataFetchPage"; import DataFetchPage from "./components/pages/DataFetchPage";
import DocumentEditorPage from "./components/pages/DocumentEditorPage"; import DocumentEditorPage from "./components/pages/DocumentEditorPage";
import MCPSettings from "./components/settings/MCPSettings"; import ToolIntegrationPage, {
import AppSidebar, { ToolIntegrationTab,
NavKey, } from "./components/pages/ToolIntegrationPage";
LegacyTabKey, import SystemSettingsPage, {
} from "./components/layout/AppSidebar"; SystemSettingsTab,
import type { WebviewHeaderActions } from "./components/pages/SessionsPage"; } from "./components/pages/SystemSettingsPage";
import AppSidebar, { NavKey } from "./components/layout/AppSidebar";
import type { AiAgentLaunchContext } from "./services/core/aiChat"; import type { AiAgentLaunchContext } from "./services/core/aiChat";
import { createLogger } from "./services/utils/rendererLog"; import { createLogger } from "./services/utils/rendererLog";
import styles from "./styles/components/App.module.css";
import { lightTheme, darkTheme } from "./styles/theme"; import { lightTheme, darkTheme } from "./styles/theme";
import { FEATURES } from "@shared/featureFlags"; import { FEATURES } from "@shared/featureFlags";
@@ -105,9 +103,7 @@ export function useI18nLang(): I18nContextValue {
// Tab 类型定义(对齐 Tauri 客户端) // Tab 类型定义(对齐 Tauri 客户端)
type TabKey = type TabKey =
| "overview" | "overview"
| "client"
| "sessions" | "sessions"
| "legacySessions"
| "automation" | "automation"
| "knowledge" | "knowledge"
| "document" | "document"
@@ -116,12 +112,8 @@ type TabKey =
| "agents" | "agents"
| "mcpLibrary" | "mcpLibrary"
| "workflow" | "workflow"
| "mcp" | "toolIntegration"
| "settings" | "systemSettings";
| "dependencies"
| "permissions"
| "logs"
| "about";
/** macOS/Linux 无 download-progress 时,用本地模拟进度避免长期显示 0%。 */ /** macOS/Linux 无 download-progress 时,用本地模拟进度避免长期显示 0%。 */
const UPDATE_SIMULATED_PROGRESS_CAP = 90; const UPDATE_SIMULATED_PROGRESS_CAP = 90;
@@ -340,11 +332,14 @@ function App() {
// ============================================ // ============================================
const [activeTab, setActiveTab] = useState<TabKey>("overview"); const [activeTab, setActiveTab] = useState<TabKey>("overview");
const [activeNavKey, setActiveNavKey] = useState<NavKey>("overview"); const [activeNavKey, setActiveNavKey] = useState<NavKey>("overview");
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [toolIntegrationTab, setToolIntegrationTab] =
useState<ToolIntegrationTab>("mcp");
const [systemSettingsTab, setSystemSettingsTab] =
useState<SystemSettingsTab>("settings");
const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false); const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false);
const [aiAgentLaunchContext, setAiAgentLaunchContext] = const [aiAgentLaunchContext, setAiAgentLaunchContext] =
useState<AiAgentLaunchContext | null>(null); useState<AiAgentLaunchContext | null>(null);
const [webviewActions, setWebviewActions] =
useState<WebviewHeaderActions | null>(null);
const [username, setUsername] = useState<string>(""); const [username, setUsername] = useState<string>("");
const [userEmail, setUserEmail] = useState<string>(""); const [userEmail, setUserEmail] = useState<string>("");
const [onlineStatus, setOnlineStatus] = useState<boolean | null>(null); const [onlineStatus, setOnlineStatus] = useState<boolean | null>(null);
@@ -903,7 +898,9 @@ function App() {
// 监听设置菜单 // 监听设置菜单
const handleSettings = () => { const handleSettings = () => {
console.log("[App] Received menu:settings event"); console.log("[App] Received menu:settings event");
setActiveTab("settings"); setSystemSettingsTab("settings");
setActiveTab("systemSettings");
setActiveNavKey("systemSettings");
}; };
window.electronAPI.on("menu:settings", handleSettings); window.electronAPI.on("menu:settings", handleSettings);
cleanupHandlers.push(() => cleanupHandlers.push(() =>
@@ -913,7 +910,9 @@ function App() {
// 监听依赖管理菜单 // 监听依赖管理菜单
const handleDependencies = () => { const handleDependencies = () => {
console.log("[App] Received menu:dependencies event"); console.log("[App] Received menu:dependencies event");
setActiveTab("dependencies"); setToolIntegrationTab("dependencies");
setActiveTab("toolIntegration");
setActiveNavKey("toolIntegration");
}; };
window.electronAPI.on("menu:dependencies", handleDependencies); window.electronAPI.on("menu:dependencies", handleDependencies);
cleanupHandlers.push(() => cleanupHandlers.push(() =>
@@ -923,7 +922,9 @@ function App() {
// 监听 MCP 设置菜单 // 监听 MCP 设置菜单
const handleMcpSettings = () => { const handleMcpSettings = () => {
console.log("[App] Received menu:mcp-settings event"); console.log("[App] Received menu:mcp-settings event");
setActiveTab("settings"); setToolIntegrationTab("mcp");
setActiveTab("toolIntegration");
setActiveNavKey("toolIntegration");
}; };
window.electronAPI.on("menu:mcp-settings", handleMcpSettings); window.electronAPI.on("menu:mcp-settings", handleMcpSettings);
cleanupHandlers.push(() => cleanupHandlers.push(() =>
@@ -1099,36 +1100,19 @@ function App() {
return; return;
} }
if (key === "toolIntegration" || key === "systemSettings") { if (key === "toolIntegration") {
setActiveTab("settings"); setActiveTab("toolIntegration");
return;
}
if (key === "systemSettings") {
setActiveTab("systemSettings");
return; return;
} }
setActiveTab("overview"); setActiveTab("overview");
}, []); }, []);
const handleLegacySelect = useCallback((key: LegacyTabKey) => {
if (key === "sessions") {
setActiveTab("legacySessions");
setActiveNavKey("aiChat");
return;
}
setActiveTab(key);
const navByLegacyTab: Partial<Record<LegacyTabKey, NavKey>> = {
client: "overview",
sessions: "aiChat",
automation: "automation",
mcp: "systemSettings",
settings: "systemSettings",
dependencies: "systemSettings",
permissions: "systemSettings",
logs: "systemSettings",
about: "systemSettings",
};
setActiveNavKey(navByLegacyTab[key] || "systemSettings");
}, []);
const handleDownloadUpdate = useCallback(async () => { const handleDownloadUpdate = useCallback(async () => {
if (updateState.canAutoUpdate === false) { if (updateState.canAutoUpdate === false) {
await window.electronAPI?.app?.openReleasesPage?.(); await window.electronAPI?.app?.openReleasesPage?.();
@@ -1311,48 +1295,44 @@ function App() {
value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }} value={{ themeMode, isDarkMode, setThemeMode: handleSetThemeMode }}
> >
<div className="app-container"> <div className="app-container">
{webviewActions && ( {isMacOS && (
<div className="app-header"> <div className="app-window-titlebar">
<div className={styles.headerWebviewActions}> <div className="app-window-title">{APP_DISPLAY_NAME}</div>
<Button <button
size="small" type="button"
icon={<ArrowLeftOutlined />} className="app-window-sidebar-toggle"
onClick={webviewActions.onBack} title={sidebarCollapsed ? "展开主菜单" : "收起主菜单"}
> aria-label={sidebarCollapsed ? "展开主菜单" : "收起主菜单"}
{t("Claw.App.back")} aria-expanded={!sidebarCollapsed}
</Button> onClick={() => setSidebarCollapsed((current) => !current)}
<Button >
size="small" {sidebarCollapsed ? (
icon={<ReloadOutlined />} <MenuUnfoldOutlined />
onClick={webviewActions.onReload} ) : (
> <MenuFoldOutlined />
{t("Claw.App.refresh")} )}
</Button> </button>
</div>
</div> </div>
)} )}
{/* 主体部分 */} {/* 主体部分 */}
<div className="app-body"> <div className="app-body">
{/* 左侧边栏 (hidden when webview is active) */} <AppSidebar
{!webviewActions && ( activeNavKey={activeNavKey}
<AppSidebar collapsed={sidebarCollapsed}
activeNavKey={activeNavKey} username={username}
username={username} email={userEmail}
email={userEmail} onlineStatus={onlineStatus}
onlineStatus={onlineStatus} updateNotice={sidebarUpdateNotice}
isMacOS={isMacOS} showToggle={!isMacOS}
updateNotice={sidebarUpdateNotice} onNavSelect={handleNavSelect}
onNavSelect={handleNavSelect} onToggle={() => setSidebarCollapsed((current) => !current)}
onLegacySelect={handleLegacySelect} onLogout={handleSidebarLogout}
onLogout={handleSidebarLogout} />
/>
)}
{/* 主内容区flex 子撑满,便于日志等页占满高度 */} {/* 主内容区flex 子撑满,便于日志等页占满高度 */}
<div <div
className={ className={
webviewActions ||
activeTab === "overview" || activeTab === "overview" ||
activeTab === "document" || activeTab === "document" ||
activeTab === "dataFetch" activeTab === "dataFetch"
@@ -1370,25 +1350,6 @@ function App() {
}} }}
> >
{activeTab === "overview" && <OverviewPage />} {activeTab === "overview" && <OverviewPage />}
{activeTab === "client" && (
<ClientPage
onNavigate={(tab) => {
if (tab === "sessions") setSessionsAutoOpen(true);
setActiveTab(tab);
setActiveNavKey(
tab === "sessions" ? "aiChat" : "overview",
);
}}
services={services}
servicesLoading={servicesLoading}
startingServices={startingServices}
setStartingServices={setStartingServices}
onRefreshServices={pollServicesStatus}
authRefreshTrigger={authRefreshTrigger}
onAuthChange={handleAuthChange}
onLoginStarted={handleLoginStarted}
/>
)}
{activeTab === "sessions" && ( {activeTab === "sessions" && (
<AiChatPage <AiChatPage
autoNew={sessionsAutoOpen} autoNew={sessionsAutoOpen}
@@ -1396,12 +1357,6 @@ function App() {
onAutoNewConsumed={() => setSessionsAutoOpen(false)} onAutoNewConsumed={() => setSessionsAutoOpen(false)}
/> />
)} )}
{activeTab === "legacySessions" && (
<SessionsPage
autoOpen={false}
onWebviewChange={setWebviewActions}
/>
)}
{activeTab === "automation" && <AutomationPage />} {activeTab === "automation" && <AutomationPage />}
{activeTab === "knowledge" && <KnowledgePage />} {activeTab === "knowledge" && <KnowledgePage />}
{activeTab === "document" && <DocumentEditorPage />} {activeTab === "document" && <DocumentEditorPage />}
@@ -1412,12 +1367,52 @@ function App() {
)} )}
{activeTab === "mcpLibrary" && <McpMarketplacePage />} {activeTab === "mcpLibrary" && <McpMarketplacePage />}
{activeTab === "workflow" && <WorkflowMarketplacePage />} {activeTab === "workflow" && <WorkflowMarketplacePage />}
{activeTab === "mcp" && <MCPSettings />} {activeTab === "toolIntegration" && (
{activeTab === "settings" && <SettingsPage />} <ToolIntegrationPage
{activeTab === "dependencies" && <DependenciesPage />} activeTab={toolIntegrationTab}
{activeTab === "permissions" && <PermissionsPage />} clientContent={
{activeTab === "logs" && <LogViewer />} <ClientPage
{activeTab === "about" && <AboutPage />} onNavigate={(tab) => {
if (tab === "sessions") {
setSessionsAutoOpen(true);
setActiveTab("sessions");
setActiveNavKey("aiChat");
return;
}
if (
tab === "client" ||
tab === "dependencies" ||
tab === "permissions"
) {
setToolIntegrationTab(tab);
setActiveTab("toolIntegration");
setActiveNavKey("toolIntegration");
return;
}
setSystemSettingsTab(tab);
setActiveTab("systemSettings");
setActiveNavKey("systemSettings");
}}
services={services}
servicesLoading={servicesLoading}
startingServices={startingServices}
setStartingServices={setStartingServices}
onRefreshServices={pollServicesStatus}
authRefreshTrigger={authRefreshTrigger}
onAuthChange={handleAuthChange}
onLoginStarted={handleLoginStarted}
/>
}
isMacOS={isMacOS}
onTabChange={setToolIntegrationTab}
/>
)}
{activeTab === "systemSettings" && (
<SystemSettingsPage
activeTab={systemSettingsTab}
onTabChange={setSystemSettingsTab}
/>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Popover } from "antd"; import { Tooltip } from "antd";
import { import {
AppstoreOutlined, AppstoreOutlined,
ApiOutlined, ApiOutlined,
@@ -8,15 +8,12 @@ import {
CommentOutlined, CommentOutlined,
DatabaseOutlined, DatabaseOutlined,
FileTextOutlined, FileTextOutlined,
FolderOutlined,
InfoCircleOutlined,
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined,
ProjectOutlined, ProjectOutlined,
RobotOutlined, RobotOutlined,
SafetyOutlined,
SettingOutlined, SettingOutlined,
TeamOutlined,
ThunderboltOutlined, ThunderboltOutlined,
ToolOutlined, ToolOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
@@ -37,17 +34,6 @@ export type NavKey =
| "toolIntegration" | "toolIntegration"
| "systemSettings"; | "systemSettings";
export type LegacyTabKey =
| "client"
| "sessions"
| "automation"
| "mcp"
| "settings"
| "dependencies"
| "permissions"
| "logs"
| "about";
interface NavItem { interface NavItem {
key: NavKey; key: NavKey;
label: string; label: string;
@@ -55,12 +41,6 @@ interface NavItem {
badge?: string; badge?: string;
} }
interface LegacyItem {
key: LegacyTabKey;
label: string;
icon: React.ReactNode;
}
interface UpdateNotice { interface UpdateNotice {
tone: "available" | "downloading" | "install"; tone: "available" | "downloading" | "install";
label: string; label: string;
@@ -69,13 +49,14 @@ interface UpdateNotice {
interface AppSidebarProps { interface AppSidebarProps {
activeNavKey: NavKey; activeNavKey: NavKey;
collapsed: boolean;
username: string; username: string;
email?: string; email?: string;
onlineStatus: boolean | null; onlineStatus: boolean | null;
isMacOS: boolean;
updateNotice?: UpdateNotice; updateNotice?: UpdateNotice;
showToggle?: boolean;
onNavSelect: (key: NavKey) => void; onNavSelect: (key: NavKey) => void;
onLegacySelect: (key: LegacyTabKey) => void; onToggle: () => void;
onLogout: () => void; onLogout: () => void;
} }
@@ -100,27 +81,6 @@ const settingItems: NavItem[] = [
{ key: "systemSettings", label: "系统设置", icon: <SettingOutlined /> }, { key: "systemSettings", label: "系统设置", icon: <SettingOutlined /> },
]; ];
function getLegacyItems(isMacOS: boolean): LegacyItem[] {
const items: LegacyItem[] = [
{ key: "client", label: "客户端", icon: <AppstoreOutlined /> },
{ key: "sessions", label: "会话", icon: <TeamOutlined /> },
{ key: "mcp", label: "MCP", icon: <ApiOutlined /> },
{ key: "settings", label: "设置", icon: <SettingOutlined /> },
{ key: "dependencies", label: "依赖", icon: <FolderOutlined /> },
];
if (isMacOS) {
items.push({ key: "permissions", label: "授权", icon: <SafetyOutlined /> });
}
items.push(
{ key: "logs", label: "日志", icon: <FileTextOutlined /> },
{ key: "about", label: "关于", icon: <InfoCircleOutlined /> },
);
return items;
}
function getInitial(name: string): string { function getInitial(name: string): string {
const trimmed = name.trim(); const trimmed = name.trim();
if (!trimmed) return "陇"; if (!trimmed) return "陇";
@@ -131,16 +91,17 @@ function renderNavItem(
item: NavItem, item: NavItem,
activeNavKey: NavKey, activeNavKey: NavKey,
onNavSelect: (key: NavKey) => void, onNavSelect: (key: NavKey) => void,
collapsed: boolean,
) { ) {
const active = activeNavKey === item.key; const active = activeNavKey === item.key;
const isStatusBadge = item.badge === "运行中"; const isStatusBadge = item.badge === "运行中";
return ( const button = (
<button <button
key={item.key}
type="button" type="button"
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`} className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
onClick={() => onNavSelect(item.key)} onClick={() => onNavSelect(item.key)}
aria-label={collapsed ? item.label : undefined}
> >
<span className={styles.navIcon}>{item.icon}</span> <span className={styles.navIcon}>{item.icon}</span>
<span className={styles.navLabel}>{item.label}</span> <span className={styles.navLabel}>{item.label}</span>
@@ -153,22 +114,32 @@ function renderNavItem(
)} )}
</button> </button>
); );
return (
<Tooltip
key={item.key}
title={collapsed ? item.label : undefined}
placement="right"
>
{button}
</Tooltip>
);
} }
export default function AppSidebar({ export default function AppSidebar({
activeNavKey, activeNavKey,
collapsed,
username, username,
email, email,
onlineStatus, onlineStatus,
isMacOS,
updateNotice, updateNotice,
showToggle = true,
onNavSelect, onNavSelect,
onLegacySelect, onToggle,
onLogout, onLogout,
}: AppSidebarProps) { }: AppSidebarProps) {
const displayName = username || "陈星宇"; const displayName = username || "陈星宇";
const displayEmail = email || "xingyu@feitian.com"; const displayEmail = email || "xingyu@feitian.com";
const legacyItems = getLegacyItems(isMacOS);
const online = onlineStatus !== false; const online = onlineStatus !== false;
const updateNoticeToneClass = updateNotice const updateNoticeToneClass = updateNotice
? styles[ ? styles[
@@ -176,27 +147,20 @@ export default function AppSidebar({
] ]
: ""; : "";
const historyMenu = (
<div className={styles.historyMenu}>
<div className={styles.historyTitle}></div>
{legacyItems.map((item) => (
<button
key={item.key}
type="button"
className={styles.historyItem}
onClick={() => onLegacySelect(item.key)}
>
<span className={styles.historyIcon}>{item.icon}</span>
<span>{item.label}</span>
</button>
))}
</div>
);
return ( return (
<aside className={styles.sidebar}> <aside
<div className={styles.brand}> className={`${styles.sidebar} ${collapsed ? styles.sidebarCollapsed : ""}`}
<img className={styles.brandLogo} src="./icon.png" alt="" /> >
<div
className={`${styles.brand} ${showToggle ? "" : styles.brandWithoutToggle}`}
>
<Tooltip title={collapsed ? APP_DISPLAY_NAME : undefined}>
<img
className={styles.brandLogo}
src="./icon.png"
alt={APP_DISPLAY_NAME}
/>
</Tooltip>
<div className={styles.brandText}> <div className={styles.brandText}>
<div className={styles.brandName}>{APP_DISPLAY_NAME}</div> <div className={styles.brandName}>{APP_DISPLAY_NAME}</div>
<div className={styles.brandSubTitle}>DIGITAL EMPLOYEE</div> <div className={styles.brandSubTitle}>DIGITAL EMPLOYEE</div>
@@ -211,65 +175,71 @@ export default function AppSidebar({
</button> </button>
)} )}
</div> </div>
<Popover {showToggle && (
content={historyMenu}
trigger="click"
placement="rightTop"
overlayClassName={styles.historyPopover}
>
<button <button
type="button" type="button"
className={styles.historyTrigger} className={styles.sidebarToggle}
title="导航目录" title={collapsed ? "展开主菜单" : "收起主菜单"}
aria-label={collapsed ? "展开主菜单" : "收起主菜单"}
aria-expanded={!collapsed}
onClick={onToggle}
> >
<MenuFoldOutlined /> {collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button> </button>
</Popover> )}
</div> </div>
<nav className={styles.nav}> <nav className={styles.nav}>
<section className={styles.navSection}> <section className={styles.navSection}>
<div className={styles.sectionTitle}></div> <div className={styles.sectionTitle}></div>
{workspaceItems.map((item) => {workspaceItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect), renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)} )}
</section> </section>
<section className={styles.navSection}> <section className={styles.navSection}>
<div className={styles.sectionTitle}></div> <div className={styles.sectionTitle}></div>
{marketplaceItems.map((item) => {marketplaceItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect), renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)} )}
</section> </section>
<section className={styles.navSection}> <section className={styles.navSection}>
<div className={styles.sectionTitle}></div> <div className={styles.sectionTitle}></div>
{settingItems.map((item) => {settingItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect), renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)} )}
</section> </section>
</nav> </nav>
<div className={styles.footer}> <div className={styles.footer}>
<div className={styles.userCard}> <Tooltip
<div className={styles.avatar}>{getInitial(displayName)}</div> title={collapsed ? `${displayName} · ${displayEmail}` : undefined}
<div className={styles.userMeta}> placement="right"
<div className={styles.userName}>{displayName}</div>
<div className={styles.userEmail}>{displayEmail}</div>
</div>
<span
className={`${styles.onlineDot} ${online ? styles.onlineDotActive : ""}`}
aria-label={online ? "在线" : "离线"}
/>
</div>
<button
type="button"
className={styles.logoutButton}
onClick={onLogout}
> >
<LogoutOutlined /> <div className={styles.userCard}>
<span>退</span> <div className={styles.avatar}>{getInitial(displayName)}</div>
</button> <div className={styles.userMeta}>
<div className={styles.userName}>{displayName}</div>
<div className={styles.userEmail}>{displayEmail}</div>
</div>
<span
className={`${styles.onlineDot} ${online ? styles.onlineDotActive : ""}`}
aria-label={online ? "在线" : "离线"}
/>
</div>
</Tooltip>
<Tooltip title={collapsed ? "退出登录" : undefined} placement="right">
<button
type="button"
className={styles.logoutButton}
onClick={onLogout}
aria-label={collapsed ? "退出登录" : undefined}
>
<LogoutOutlined />
<span className={styles.logoutLabel}>退</span>
</button>
</Tooltip>
</div> </div>
</aside> </aside>
); );

View File

@@ -0,0 +1,59 @@
import { Tabs } from "antd";
import AboutPage from "./AboutPage";
import LogViewer from "./LogViewer";
import SettingsPage from "./SettingsPage";
import styles from "../../styles/components/SystemSettingsPage.module.css";
export type SystemSettingsTab = "settings" | "logs" | "about";
interface SystemSettingsPageProps {
activeTab: SystemSettingsTab;
onTabChange: (tab: SystemSettingsTab) => void;
}
export default function SystemSettingsPage({
activeTab,
onTabChange,
}: SystemSettingsPageProps) {
const items = [
{
key: "settings",
label: "设置",
children: (
<div className={styles.scrollPanel}>
<SettingsPage />
</div>
),
},
{
key: "logs",
label: "日志",
children: (
<div className={styles.fullPanel}>
<LogViewer />
</div>
),
},
{
key: "about",
label: "关于",
children: (
<div className={styles.scrollPanel}>
<AboutPage />
</div>
),
},
];
return (
<section className={styles.page}>
<h1 className={styles.title}></h1>
<Tabs
className={styles.tabs}
activeKey={activeTab}
items={items}
onChange={(key) => onTabChange(key as SystemSettingsTab)}
/>
</section>
);
}

View File

@@ -0,0 +1,65 @@
import type { ReactNode } from "react";
import { Tabs } from "antd";
import DependenciesPage from "./DependenciesPage";
import PermissionsPage from "./PermissionsPage";
import MCPSettings from "../settings/MCPSettings";
import styles from "../../styles/components/ToolIntegrationPage.module.css";
export type ToolIntegrationTab =
| "client"
| "mcp"
| "dependencies"
| "permissions";
interface ToolIntegrationPageProps {
activeTab: ToolIntegrationTab;
clientContent: ReactNode;
isMacOS: boolean;
onTabChange: (tab: ToolIntegrationTab) => void;
}
export default function ToolIntegrationPage({
activeTab,
clientContent,
isMacOS,
onTabChange,
}: ToolIntegrationPageProps) {
const items = [
{
key: "client",
label: "客户端",
children: clientContent,
},
{
key: "mcp",
label: "MCP",
children: <MCPSettings embedded />,
},
{
key: "dependencies",
label: "依赖",
children: <DependenciesPage />,
},
...(isMacOS
? [
{
key: "permissions",
label: "授权",
children: <PermissionsPage />,
},
]
: []),
];
return (
<section className={styles.page}>
<h1 className={styles.title}></h1>
<Tabs
className={styles.tabs}
activeKey={activeTab}
items={items}
onChange={(key) => onTabChange(key as ToolIntegrationTab)}
/>
</section>
);
}

View File

@@ -47,9 +47,10 @@ const { Text } = Typography;
interface MCPSettingsProps { interface MCPSettingsProps {
isOpen?: boolean; isOpen?: boolean;
onClose?: () => void; onClose?: () => void;
embedded?: boolean;
} }
function MCPSettings({ isOpen = true }: MCPSettingsProps) { function MCPSettings({ isOpen = true, embedded = false }: MCPSettingsProps) {
const [isDarkMode, setIsDarkMode] = useState( const [isDarkMode, setIsDarkMode] = useState(
document.body.getAttribute("data-theme") === "dark", document.body.getAttribute("data-theme") === "dark",
); );
@@ -454,7 +455,7 @@ function MCPSettings({ isOpen = true }: MCPSettingsProps) {
? currentServers[editingServerId] ? currentServers[editingServerId]
: undefined; : undefined;
return ( return (
<div style={{ padding: 24 }}> <div style={embedded ? undefined : { padding: 24 }}>
<MCPServerEditor <MCPServerEditor
key={editorMode === "edit" ? editingServerId : "__create__"} key={editorMode === "edit" ? editingServerId : "__create__"}
mode={editorMode} mode={editorMode}
@@ -471,7 +472,7 @@ function MCPSettings({ isOpen = true }: MCPSettingsProps) {
} }
return ( return (
<div style={{ padding: 24 }}> <div style={embedded ? undefined : { padding: 24 }}>
<Card <Card
title={ title={
<Space> <Space>

View File

@@ -11,31 +11,31 @@
--color-primary-hover: #27272a; --color-primary-hover: #27272a;
--color-primary-active: #3f3f46; --color-primary-active: #3f3f46;
--color-success: #16a34a; --color-success: #16a34a;
--color-error: #EF4444; --color-error: #ef4444;
--color-warning: #F59E0B; --color-warning: #f59e0b;
--color-info: #3B82F6; --color-info: #3b82f6;
--color-success-bg: #f0fdf4; --color-success-bg: #f0fdf4;
--color-error-bg: #fef2f2; --color-error-bg: #fef2f2;
--color-warning-bg: #fffbeb; --color-warning-bg: #fffbeb;
--color-bg-layout: #F8F9FA; --color-bg-layout: #f8f9fa;
--color-bg-container: #ffffff; --color-bg-container: #ffffff;
--color-bg-elevated: #ffffff; --color-bg-elevated: #ffffff;
--color-bg-header: #ffffff; --color-bg-header: #ffffff;
--color-bg-sider: #ffffff; --color-bg-sider: #ffffff;
--color-bg-section: #ffffff; --color-bg-section: #ffffff;
--color-bg-section-header: #F9FAFB; --color-bg-section-header: #f9fafb;
--color-border: #E5E7EB; --color-border: #e5e7eb;
--color-border-secondary: #F3F4F6; --color-border-secondary: #f3f4f6;
--color-text: #18181b; --color-text: #18181b;
--color-text-secondary: #6B7280; --color-text-secondary: #6b7280;
--color-text-tertiary: #9CA3AF; --color-text-tertiary: #9ca3af;
--color-text-quaternary: #D1D5DB; --color-text-quaternary: #d1d5db;
--color-icon: #6B7280; --color-icon: #6b7280;
--color-divider: #F3F4F6; --color-divider: #f3f4f6;
--border-radius: 8px; --border-radius: 8px;
--border-radius-lg: 10px; --border-radius-lg: 10px;
@@ -48,9 +48,9 @@
--color-primary-hover: #e4e4e7; --color-primary-hover: #e4e4e7;
--color-primary-active: #d4d4d8; --color-primary-active: #d4d4d8;
--color-success: #22c55e; --color-success: #22c55e;
--color-error: #EF4444; --color-error: #ef4444;
--color-warning: #F59E0B; --color-warning: #f59e0b;
--color-info: #3B82F6; --color-info: #3b82f6;
--color-success-bg: #052e16; --color-success-bg: #052e16;
--color-error-bg: #450a0a; --color-error-bg: #450a0a;
--color-warning-bg: #422006; --color-warning-bg: #422006;
@@ -104,7 +104,9 @@ body {
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
background: var(--color-bg-layout); background: var(--color-bg-layout);
transition: background-color 0.2s ease, color 0.2s ease; transition:
background-color 0.2s ease,
color 0.2s ease;
} }
/* ========== 布局 ========== */ /* ========== 布局 ========== */
@@ -116,6 +118,55 @@ body {
background: var(--color-bg-layout); background: var(--color-bg-layout);
} }
.app-window-titlebar {
height: 44px;
flex: none;
display: flex;
align-items: center;
padding: 0 18px 0 84px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-header);
-webkit-app-region: drag;
user-select: none;
}
.app-window-sidebar-toggle {
width: 32px;
height: 32px;
flex: none;
border: 0;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #ededeb;
color: #555752;
font: inherit;
font-size: 16px;
margin-left: 10px;
cursor: pointer;
-webkit-app-region: no-drag;
transition:
background-color 0.16s ease,
color 0.16s ease;
}
.app-window-sidebar-toggle:hover {
background: #dfdfdb;
color: #20211f;
}
.app-window-title {
min-width: 0;
overflow: hidden;
color: var(--color-text);
font-size: 14px;
font-weight: 700;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-header { .app-header {
height: 48px; height: 48px;
background: var(--color-bg-header); background: var(--color-bg-header);
@@ -151,7 +202,9 @@ body {
border-radius: 10px; border-radius: 10px;
line-height: 18px; line-height: 18px;
white-space: nowrap; white-space: nowrap;
transition: opacity 0.2s, transform 0.15s; transition:
opacity 0.2s,
transform 0.15s;
} }
.app-header-update-tag[role="button"] { .app-header-update-tag[role="button"] {
@@ -266,7 +319,7 @@ body {
} }
.app-sider .ant-menu-light .ant-menu-item:hover { .app-sider .ant-menu-light .ant-menu-item:hover {
background: #F3F4F6; background: #f3f4f6;
color: #18181b; color: #18181b;
} }
@@ -307,14 +360,14 @@ body {
/* ========== 卡片 ========== */ /* ========== 卡片 ========== */
.ant-card { .ant-card {
border: 1px solid #E5E7EB; border: 1px solid #e5e7eb;
border-radius: 10px; border-radius: 10px;
} }
.ant-card-head { .ant-card-head {
min-height: auto; min-height: auto;
padding: 10px 16px; padding: 10px 16px;
border-bottom: 1px solid #F3F4F6; border-bottom: 1px solid #f3f4f6;
font-size: 13px; font-size: 13px;
} }
@@ -356,46 +409,46 @@ body {
.ant-tag-error, .ant-tag-error,
.ant-tag-red { .ant-tag-red {
background: #FEF2F2; background: #fef2f2;
color: #EF4444; color: #ef4444;
border-color: #FEE2E2; border-color: #fee2e2;
} }
.ant-tag-warning, .ant-tag-warning,
.ant-tag-orange { .ant-tag-orange {
background: #FFFBEB; background: #fffbeb;
color: #D97706; color: #d97706;
border-color: #FEF3C7; border-color: #fef3c7;
} }
.ant-tag-processing { .ant-tag-processing {
background: #F3F4F6; background: #f3f4f6;
color: #6B7280; color: #6b7280;
border-color: #E5E7EB; border-color: #e5e7eb;
} }
.ant-tag-default { .ant-tag-default {
background: #F3F4F6; background: #f3f4f6;
color: #6B7280; color: #6b7280;
border-color: #E5E7EB; border-color: #e5e7eb;
} }
.ant-tag-blue { .ant-tag-blue {
background: #EFF6FF; background: #eff6ff;
color: #2563EB; color: #2563eb;
border-color: #DBEAFE; border-color: #dbeafe;
} }
.ant-tag-purple { .ant-tag-purple {
background: #F5F3FF; background: #f5f3ff;
color: #7C3AED; color: #7c3aed;
border-color: #EDE9FE; border-color: #ede9fe;
} }
.ant-tag-cyan { .ant-tag-cyan {
background: #ECFEFF; background: #ecfeff;
color: #0891B2; color: #0891b2;
border-color: #CFFAFE; border-color: #cffafe;
} }
/* ========== 输入框 ========== */ /* ========== 输入框 ========== */
@@ -403,7 +456,7 @@ body {
.ant-input-affix-wrapper, .ant-input-affix-wrapper,
.ant-input-number { .ant-input-number {
border-radius: 8px; border-radius: 8px;
border-color: #E5E7EB; border-color: #e5e7eb;
} }
.ant-input:focus, .ant-input:focus,
@@ -418,18 +471,18 @@ body {
.ant-input:hover, .ant-input:hover,
.ant-input-affix-wrapper:hover, .ant-input-affix-wrapper:hover,
.ant-input-number:hover { .ant-input-number:hover {
border-color: #D1D5DB; border-color: #d1d5db;
} }
.ant-input::placeholder { .ant-input::placeholder {
color: #9CA3AF; color: #9ca3af;
} }
/* ========== 表单 ========== */ /* ========== 表单 ========== */
.ant-form-item-label > label { .ant-form-item-label > label {
font-weight: 500; font-weight: 500;
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6b7280;
} }
.ant-form-item { .ant-form-item {
@@ -451,7 +504,7 @@ body {
.ant-list-item-meta-description { .ant-list-item-meta-description {
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6b7280;
} }
/* ========== 警告框 - 更柔和 ========== */ /* ========== 警告框 - 更柔和 ========== */
@@ -468,18 +521,18 @@ body {
} }
.ant-alert-warning { .ant-alert-warning {
background: #FFFBEB; background: #fffbeb;
border-color: #FEF3C7; border-color: #fef3c7;
} }
.ant-alert-error { .ant-alert-error {
background: #FEF2F2; background: #fef2f2;
border-color: #FEE2E2; border-color: #fee2e2;
} }
.ant-alert-info { .ant-alert-info {
background: #F9FAFB; background: #f9fafb;
border-color: #E5E7EB; border-color: #e5e7eb;
} }
.ant-alert-message { .ant-alert-message {
@@ -519,7 +572,7 @@ body {
/* ========== 进度条 ========== */ /* ========== 进度条 ========== */
.ant-progress-text { .ant-progress-text {
font-size: 11px; font-size: 11px;
color: #6B7280; color: #6b7280;
} }
/* ========== Tabs ========== */ /* ========== Tabs ========== */
@@ -553,18 +606,18 @@ body {
.ant-modal-footer { .ant-modal-footer {
padding: 12px 20px 16px; padding: 12px 20px 16px;
border-top: 1px solid #F3F4F6; border-top: 1px solid #f3f4f6;
} }
/* ========== 徽章 ========== */ /* ========== 徽章 ========== */
.ant-badge-status-text { .ant-badge-status-text {
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6b7280;
} }
/* ========== Divider ========== */ /* ========== Divider ========== */
.ant-divider { .ant-divider {
border-color: #F3F4F6; border-color: #f3f4f6;
margin: 12px 0; margin: 12px 0;
} }
@@ -592,12 +645,12 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: #F8F9FA; background: #f8f9fa;
} }
.app-loading-text { .app-loading-text {
margin-top: 16px; margin-top: 16px;
color: #6B7280; color: #6b7280;
font-size: 13px; font-size: 13px;
} }
@@ -622,7 +675,7 @@ body {
.page-subtitle { .page-subtitle {
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6b7280;
margin-top: 2px; margin-top: 2px;
} }
@@ -632,7 +685,7 @@ body {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 0; padding: 8px 0;
border-bottom: 1px solid #F3F4F6; border-bottom: 1px solid #f3f4f6;
} }
.status-row:last-child { .status-row:last-child {
@@ -668,7 +721,7 @@ body {
.info-label { .info-label {
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6b7280;
} }
.info-value { .info-value {
@@ -688,7 +741,7 @@ body {
.section-title { .section-title {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: #6B7280; color: #6b7280;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: 8px; margin-bottom: 8px;
@@ -705,7 +758,7 @@ body {
} }
.message-assistant { .message-assistant {
background: #F3F4F6; background: #f3f4f6;
color: #18181b; color: #18181b;
padding: 12px 16px; padding: 12px 16px;
border-radius: 12px; border-radius: 12px;

View File

@@ -1,11 +0,0 @@
/**
* App 组件样式
* 包含顶部栏、布局等样式
*/
/* Webview 模式下顶部栏左侧操作区(替代 logo */
.headerWebviewActions {
display: flex;
align-items: center;
gap: 6px;
}

View File

@@ -7,15 +7,29 @@
background: #f1f2ef; background: #f1f2ef;
border-right: 1px solid #deded9; border-right: 1px solid #deded9;
color: #1f211f; color: #1f211f;
overflow: hidden;
transition:
width 0.18s ease,
min-width 0.18s ease;
}
.sidebarCollapsed {
width: 64px;
min-width: 64px;
} }
.brand { .brand {
height: 104px; height: 84px;
display: grid; display: grid;
grid-template-columns: 38px 1fr 34px; grid-template-columns: 38px 1fr 34px;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 12px 14px 20px; padding: 6px 12px 10px 20px;
}
.brandWithoutToggle {
grid-template-columns: 38px 1fr;
padding-right: 20px;
} }
.brandLogo { .brandLogo {
@@ -100,9 +114,10 @@
color: #d48806; color: #d48806;
} }
.historyTrigger { .sidebarToggle {
width: 30px; width: 30px;
height: 30px; height: 30px;
flex: none;
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
display: inline-flex; display: inline-flex;
@@ -116,7 +131,7 @@
color 0.16s ease; color 0.16s ease;
} }
.historyTrigger:hover { .sidebarToggle:hover {
background: #dadad4; background: #dadad4;
color: #252622; color: #252622;
} }
@@ -320,52 +335,94 @@
color: #c8342e; color: #c8342e;
} }
.historyPopover :global(.ant-popover-inner) { .sidebarCollapsed .brand {
padding: 8px; height: 84px;
border-radius: 10px; display: flex;
box-shadow: 0 12px 28px rgba(22, 24, 26, 0.12); flex-direction: column;
justify-content: center;
gap: 8px;
padding: 6px 0;
} }
.historyPopover :global(.ant-popover-arrow) { .sidebarCollapsed .brandLogo {
width: 32px;
height: 32px;
border-radius: 7px;
}
.sidebarCollapsed .brandText,
.sidebarCollapsed .sectionTitle,
.sidebarCollapsed .navLabel,
.sidebarCollapsed .navBadge,
.sidebarCollapsed .userMeta,
.sidebarCollapsed .logoutLabel {
display: none; display: none;
} }
.historyMenu { .sidebarCollapsed .nav {
width: 176px; padding: 0 10px 16px;
overflow-x: hidden;
} }
.historyTitle { .sidebarCollapsed .navSection {
padding: 4px 8px 8px; padding-bottom: 8px;
color: #8e8b84; margin-bottom: 8px;
font-size: 12px;
font-weight: 600;
} }
.historyItem { .sidebarCollapsed .navItem {
width: 100%; width: 42px;
height: 32px; padding: 0;
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; justify-content: center;
gap: 0;
}
.sidebarCollapsed .navItemActive::after {
right: 3px;
}
.sidebarCollapsed .footer {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0 12px;
}
.sidebarCollapsed .userCard {
position: relative;
width: 36px;
height: 36px;
justify-content: center;
gap: 0;
padding: 0;
border: 0;
background: transparent;
}
.sidebarCollapsed .avatar {
width: 32px;
height: 32px;
}
.sidebarCollapsed .onlineDot {
position: absolute;
right: 0;
bottom: 0;
width: 8px;
height: 8px;
border: 2px solid #f1f2ef;
}
.sidebarCollapsed .logoutButton {
width: 36px;
height: 36px;
margin-top: 8px;
padding: 0;
justify-content: center;
gap: 0;
background: transparent;
font-size: 15px;
}
.sidebarCollapsed .logoutButton:hover {
background: #f1e3df;
} }

View File

@@ -0,0 +1,57 @@
.page {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.title {
margin: 0 0 8px;
color: var(--color-text);
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.tabs {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.tabs :global(.ant-tabs-nav) {
flex: none;
margin-bottom: 20px;
}
.tabs :global(.ant-tabs-content-holder),
.tabs :global(.ant-tabs-content),
.tabs :global(.ant-tabs-tabpane) {
min-width: 0;
min-height: 0;
}
.tabs :global(.ant-tabs-content-holder),
.tabs :global(.ant-tabs-content) {
flex: 1;
}
.tabs :global(.ant-tabs-tabpane-active) {
height: 100%;
}
.scrollPanel {
height: 100%;
overflow-y: auto;
}
.fullPanel {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
overflow: hidden;
}

View File

@@ -0,0 +1,28 @@
.page {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.title {
margin: 0 0 8px;
color: var(--color-text);
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.tabs {
min-width: 0;
}
.tabs :global(.ant-tabs-nav) {
margin-bottom: 20px;
}
.tabs :global(.ant-tabs-content),
.tabs :global(.ant-tabs-tabpane) {
min-width: 0;
}