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

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,
title: APP_DISPLAY_NAME,
icon: getIconPath(),
...(process.platform === "darwin"
? {
titleBarStyle: "hiddenInset" as const,
trafficLightPosition: { x: 18, y: 16 },
}
: {}),
webPreferences: {
preload: path.join(__dirname, "..", "preload", "index.js"),
contextIsolation: true,

View File

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

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Popover } from "antd";
import { Tooltip } from "antd";
import {
AppstoreOutlined,
ApiOutlined,
@@ -8,15 +8,12 @@ import {
CommentOutlined,
DatabaseOutlined,
FileTextOutlined,
FolderOutlined,
InfoCircleOutlined,
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ProjectOutlined,
RobotOutlined,
SafetyOutlined,
SettingOutlined,
TeamOutlined,
ThunderboltOutlined,
ToolOutlined,
} from "@ant-design/icons";
@@ -37,17 +34,6 @@ export type NavKey =
| "toolIntegration"
| "systemSettings";
export type LegacyTabKey =
| "client"
| "sessions"
| "automation"
| "mcp"
| "settings"
| "dependencies"
| "permissions"
| "logs"
| "about";
interface NavItem {
key: NavKey;
label: string;
@@ -55,12 +41,6 @@ interface NavItem {
badge?: string;
}
interface LegacyItem {
key: LegacyTabKey;
label: string;
icon: React.ReactNode;
}
interface UpdateNotice {
tone: "available" | "downloading" | "install";
label: string;
@@ -69,13 +49,14 @@ interface UpdateNotice {
interface AppSidebarProps {
activeNavKey: NavKey;
collapsed: boolean;
username: string;
email?: string;
onlineStatus: boolean | null;
isMacOS: boolean;
updateNotice?: UpdateNotice;
showToggle?: boolean;
onNavSelect: (key: NavKey) => void;
onLegacySelect: (key: LegacyTabKey) => void;
onToggle: () => void;
onLogout: () => void;
}
@@ -100,27 +81,6 @@ const settingItems: NavItem[] = [
{ 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 {
const trimmed = name.trim();
if (!trimmed) return "陇";
@@ -131,16 +91,17 @@ function renderNavItem(
item: NavItem,
activeNavKey: NavKey,
onNavSelect: (key: NavKey) => void,
collapsed: boolean,
) {
const active = activeNavKey === item.key;
const isStatusBadge = item.badge === "运行中";
return (
const button = (
<button
key={item.key}
type="button"
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
onClick={() => onNavSelect(item.key)}
aria-label={collapsed ? item.label : undefined}
>
<span className={styles.navIcon}>{item.icon}</span>
<span className={styles.navLabel}>{item.label}</span>
@@ -153,22 +114,32 @@ function renderNavItem(
)}
</button>
);
return (
<Tooltip
key={item.key}
title={collapsed ? item.label : undefined}
placement="right"
>
{button}
</Tooltip>
);
}
export default function AppSidebar({
activeNavKey,
collapsed,
username,
email,
onlineStatus,
isMacOS,
updateNotice,
showToggle = true,
onNavSelect,
onLegacySelect,
onToggle,
onLogout,
}: AppSidebarProps) {
const displayName = username || "陈星宇";
const displayEmail = email || "xingyu@feitian.com";
const legacyItems = getLegacyItems(isMacOS);
const online = onlineStatus !== false;
const updateNoticeToneClass = updateNotice
? 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 (
<aside className={styles.sidebar}>
<div className={styles.brand}>
<img className={styles.brandLogo} src="./icon.png" alt="" />
<aside
className={`${styles.sidebar} ${collapsed ? styles.sidebarCollapsed : ""}`}
>
<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.brandName}>{APP_DISPLAY_NAME}</div>
<div className={styles.brandSubTitle}>DIGITAL EMPLOYEE</div>
@@ -211,65 +175,71 @@ export default function AppSidebar({
</button>
)}
</div>
<Popover
content={historyMenu}
trigger="click"
placement="rightTop"
overlayClassName={styles.historyPopover}
>
{showToggle && (
<button
type="button"
className={styles.historyTrigger}
title="导航目录"
className={styles.sidebarToggle}
title={collapsed ? "展开主菜单" : "收起主菜单"}
aria-label={collapsed ? "展开主菜单" : "收起主菜单"}
aria-expanded={!collapsed}
onClick={onToggle}
>
<MenuFoldOutlined />
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
</Popover>
)}
</div>
<nav className={styles.nav}>
<section className={styles.navSection}>
<div className={styles.sectionTitle}></div>
{workspaceItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect),
renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)}
</section>
<section className={styles.navSection}>
<div className={styles.sectionTitle}></div>
{marketplaceItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect),
renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)}
</section>
<section className={styles.navSection}>
<div className={styles.sectionTitle}></div>
{settingItems.map((item) =>
renderNavItem(item, activeNavKey, onNavSelect),
renderNavItem(item, activeNavKey, onNavSelect, collapsed),
)}
</section>
</nav>
<div className={styles.footer}>
<div className={styles.userCard}>
<div className={styles.avatar}>{getInitial(displayName)}</div>
<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>
<button
type="button"
className={styles.logoutButton}
onClick={onLogout}
<Tooltip
title={collapsed ? `${displayName} · ${displayEmail}` : undefined}
placement="right"
>
<LogoutOutlined />
<span>退</span>
</button>
<div className={styles.userCard}>
<div className={styles.avatar}>{getInitial(displayName)}</div>
<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>
</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 {
isOpen?: boolean;
onClose?: () => void;
embedded?: boolean;
}
function MCPSettings({ isOpen = true }: MCPSettingsProps) {
function MCPSettings({ isOpen = true, embedded = false }: MCPSettingsProps) {
const [isDarkMode, setIsDarkMode] = useState(
document.body.getAttribute("data-theme") === "dark",
);
@@ -454,7 +455,7 @@ function MCPSettings({ isOpen = true }: MCPSettingsProps) {
? currentServers[editingServerId]
: undefined;
return (
<div style={{ padding: 24 }}>
<div style={embedded ? undefined : { padding: 24 }}>
<MCPServerEditor
key={editorMode === "edit" ? editingServerId : "__create__"}
mode={editorMode}
@@ -471,7 +472,7 @@ function MCPSettings({ isOpen = true }: MCPSettingsProps) {
}
return (
<div style={{ padding: 24 }}>
<div style={embedded ? undefined : { padding: 24 }}>
<Card
title={
<Space>

View File

@@ -11,31 +11,31 @@
--color-primary-hover: #27272a;
--color-primary-active: #3f3f46;
--color-success: #16a34a;
--color-error: #EF4444;
--color-warning: #F59E0B;
--color-info: #3B82F6;
--color-error: #ef4444;
--color-warning: #f59e0b;
--color-info: #3b82f6;
--color-success-bg: #f0fdf4;
--color-error-bg: #fef2f2;
--color-warning-bg: #fffbeb;
--color-bg-layout: #F8F9FA;
--color-bg-layout: #f8f9fa;
--color-bg-container: #ffffff;
--color-bg-elevated: #ffffff;
--color-bg-header: #ffffff;
--color-bg-sider: #ffffff;
--color-bg-section: #ffffff;
--color-bg-section-header: #F9FAFB;
--color-bg-section-header: #f9fafb;
--color-border: #E5E7EB;
--color-border-secondary: #F3F4F6;
--color-border: #e5e7eb;
--color-border-secondary: #f3f4f6;
--color-text: #18181b;
--color-text-secondary: #6B7280;
--color-text-tertiary: #9CA3AF;
--color-text-quaternary: #D1D5DB;
--color-text-secondary: #6b7280;
--color-text-tertiary: #9ca3af;
--color-text-quaternary: #d1d5db;
--color-icon: #6B7280;
--color-divider: #F3F4F6;
--color-icon: #6b7280;
--color-divider: #f3f4f6;
--border-radius: 8px;
--border-radius-lg: 10px;
@@ -48,9 +48,9 @@
--color-primary-hover: #e4e4e7;
--color-primary-active: #d4d4d8;
--color-success: #22c55e;
--color-error: #EF4444;
--color-warning: #F59E0B;
--color-info: #3B82F6;
--color-error: #ef4444;
--color-warning: #f59e0b;
--color-info: #3b82f6;
--color-success-bg: #052e16;
--color-error-bg: #450a0a;
--color-warning-bg: #422006;
@@ -104,7 +104,9 @@ body {
font-size: 13px;
line-height: 1.5;
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);
}
.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 {
height: 48px;
background: var(--color-bg-header);
@@ -151,7 +202,9 @@ body {
border-radius: 10px;
line-height: 18px;
white-space: nowrap;
transition: opacity 0.2s, transform 0.15s;
transition:
opacity 0.2s,
transform 0.15s;
}
.app-header-update-tag[role="button"] {
@@ -266,7 +319,7 @@ body {
}
.app-sider .ant-menu-light .ant-menu-item:hover {
background: #F3F4F6;
background: #f3f4f6;
color: #18181b;
}
@@ -307,14 +360,14 @@ body {
/* ========== 卡片 ========== */
.ant-card {
border: 1px solid #E5E7EB;
border: 1px solid #e5e7eb;
border-radius: 10px;
}
.ant-card-head {
min-height: auto;
padding: 10px 16px;
border-bottom: 1px solid #F3F4F6;
border-bottom: 1px solid #f3f4f6;
font-size: 13px;
}
@@ -356,46 +409,46 @@ body {
.ant-tag-error,
.ant-tag-red {
background: #FEF2F2;
color: #EF4444;
border-color: #FEE2E2;
background: #fef2f2;
color: #ef4444;
border-color: #fee2e2;
}
.ant-tag-warning,
.ant-tag-orange {
background: #FFFBEB;
color: #D97706;
border-color: #FEF3C7;
background: #fffbeb;
color: #d97706;
border-color: #fef3c7;
}
.ant-tag-processing {
background: #F3F4F6;
color: #6B7280;
border-color: #E5E7EB;
background: #f3f4f6;
color: #6b7280;
border-color: #e5e7eb;
}
.ant-tag-default {
background: #F3F4F6;
color: #6B7280;
border-color: #E5E7EB;
background: #f3f4f6;
color: #6b7280;
border-color: #e5e7eb;
}
.ant-tag-blue {
background: #EFF6FF;
color: #2563EB;
border-color: #DBEAFE;
background: #eff6ff;
color: #2563eb;
border-color: #dbeafe;
}
.ant-tag-purple {
background: #F5F3FF;
color: #7C3AED;
border-color: #EDE9FE;
background: #f5f3ff;
color: #7c3aed;
border-color: #ede9fe;
}
.ant-tag-cyan {
background: #ECFEFF;
color: #0891B2;
border-color: #CFFAFE;
background: #ecfeff;
color: #0891b2;
border-color: #cffafe;
}
/* ========== 输入框 ========== */
@@ -403,7 +456,7 @@ body {
.ant-input-affix-wrapper,
.ant-input-number {
border-radius: 8px;
border-color: #E5E7EB;
border-color: #e5e7eb;
}
.ant-input:focus,
@@ -418,18 +471,18 @@ body {
.ant-input:hover,
.ant-input-affix-wrapper:hover,
.ant-input-number:hover {
border-color: #D1D5DB;
border-color: #d1d5db;
}
.ant-input::placeholder {
color: #9CA3AF;
color: #9ca3af;
}
/* ========== 表单 ========== */
.ant-form-item-label > label {
font-weight: 500;
font-size: 12px;
color: #6B7280;
color: #6b7280;
}
.ant-form-item {
@@ -451,7 +504,7 @@ body {
.ant-list-item-meta-description {
font-size: 12px;
color: #6B7280;
color: #6b7280;
}
/* ========== 警告框 - 更柔和 ========== */
@@ -468,18 +521,18 @@ body {
}
.ant-alert-warning {
background: #FFFBEB;
border-color: #FEF3C7;
background: #fffbeb;
border-color: #fef3c7;
}
.ant-alert-error {
background: #FEF2F2;
border-color: #FEE2E2;
background: #fef2f2;
border-color: #fee2e2;
}
.ant-alert-info {
background: #F9FAFB;
border-color: #E5E7EB;
background: #f9fafb;
border-color: #e5e7eb;
}
.ant-alert-message {
@@ -519,7 +572,7 @@ body {
/* ========== 进度条 ========== */
.ant-progress-text {
font-size: 11px;
color: #6B7280;
color: #6b7280;
}
/* ========== Tabs ========== */
@@ -553,18 +606,18 @@ body {
.ant-modal-footer {
padding: 12px 20px 16px;
border-top: 1px solid #F3F4F6;
border-top: 1px solid #f3f4f6;
}
/* ========== 徽章 ========== */
.ant-badge-status-text {
font-size: 12px;
color: #6B7280;
color: #6b7280;
}
/* ========== Divider ========== */
.ant-divider {
border-color: #F3F4F6;
border-color: #f3f4f6;
margin: 12px 0;
}
@@ -592,12 +645,12 @@ body {
align-items: center;
justify-content: center;
height: 100vh;
background: #F8F9FA;
background: #f8f9fa;
}
.app-loading-text {
margin-top: 16px;
color: #6B7280;
color: #6b7280;
font-size: 13px;
}
@@ -622,7 +675,7 @@ body {
.page-subtitle {
font-size: 12px;
color: #6B7280;
color: #6b7280;
margin-top: 2px;
}
@@ -632,7 +685,7 @@ body {
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #F3F4F6;
border-bottom: 1px solid #f3f4f6;
}
.status-row:last-child {
@@ -668,7 +721,7 @@ body {
.info-label {
font-size: 12px;
color: #6B7280;
color: #6b7280;
}
.info-value {
@@ -688,7 +741,7 @@ body {
.section-title {
font-size: 12px;
font-weight: 500;
color: #6B7280;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
@@ -705,7 +758,7 @@ body {
}
.message-assistant {
background: #F3F4F6;
background: #f3f4f6;
color: #18181b;
padding: 12px 16px;
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;
border-right: 1px solid #deded9;
color: #1f211f;
overflow: hidden;
transition:
width 0.18s ease,
min-width 0.18s ease;
}
.sidebarCollapsed {
width: 64px;
min-width: 64px;
}
.brand {
height: 104px;
height: 84px;
display: grid;
grid-template-columns: 38px 1fr 34px;
align-items: center;
gap: 10px;
padding: 12px 12px 14px 20px;
padding: 6px 12px 10px 20px;
}
.brandWithoutToggle {
grid-template-columns: 38px 1fr;
padding-right: 20px;
}
.brandLogo {
@@ -100,9 +114,10 @@
color: #d48806;
}
.historyTrigger {
.sidebarToggle {
width: 30px;
height: 30px;
flex: none;
border: 0;
border-radius: 8px;
display: inline-flex;
@@ -116,7 +131,7 @@
color 0.16s ease;
}
.historyTrigger:hover {
.sidebarToggle:hover {
background: #dadad4;
color: #252622;
}
@@ -320,52 +335,94 @@
color: #c8342e;
}
.historyPopover :global(.ant-popover-inner) {
padding: 8px;
border-radius: 10px;
box-shadow: 0 12px 28px rgba(22, 24, 26, 0.12);
.sidebarCollapsed .brand {
height: 84px;
display: flex;
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;
}
.historyMenu {
width: 176px;
.sidebarCollapsed .nav {
padding: 0 10px 16px;
overflow-x: hidden;
}
.historyTitle {
padding: 4px 8px 8px;
color: #8e8b84;
font-size: 12px;
font-weight: 600;
.sidebarCollapsed .navSection {
padding-bottom: 8px;
margin-bottom: 8px;
}
.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;
.sidebarCollapsed .navItem {
width: 42px;
padding: 0;
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;
}