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