feat: 智能体控制台前端(console)
- React+antd+zustand,注册表驱动路由/菜单/权限 - Tauri 桌面壳、后端托管 SPA、跨标签页消息队列
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import ThemeToggleButton from "./index";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (k: string) => k }),
|
||||
}));
|
||||
|
||||
// Wrap with real ThemeProvider, control initial theme via localStorage
|
||||
function renderWithTheme(mode: "light" | "dark" | "system" = "light") {
|
||||
localStorage.setItem("qwenpaw-theme", mode);
|
||||
return render(
|
||||
<ThemeProvider>
|
||||
<ThemeToggleButton />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("ThemeToggleButton", () => {
|
||||
it("renders the theme toggle button", () => {
|
||||
renderWithTheme("light");
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows sun icon when light mode is active", () => {
|
||||
renderWithTheme("light");
|
||||
expect(
|
||||
document.querySelector('[data-icon="SparkSunLine"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows moon icon when dark mode is active", () => {
|
||||
renderWithTheme("dark");
|
||||
expect(
|
||||
document.querySelector('[data-icon="SparkMoonLine"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows sun-moon icon when system mode is active", () => {
|
||||
renderWithTheme("system");
|
||||
expect(document.querySelector(".lucide-sun-moon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
expect(() => renderWithTheme("light")).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/* ---- Theme toggle button ---- */
|
||||
.toggleBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
font-size: 24px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(97, 92, 237, 0.08);
|
||||
color: #ff7f16;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Theme Dropdown ---- */
|
||||
.themeDropdown {
|
||||
:global {
|
||||
.qwenpaw-dropdown-menu-item,
|
||||
.ant-dropdown-menu-item {
|
||||
color: rgba(26, 23, 22, 0.65);
|
||||
|
||||
&:hover {
|
||||
background: rgba(43, 18, 0, 0.06) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.qwenpaw-dropdown-menu-item-selected,
|
||||
.ant-dropdown-menu-item-selected {
|
||||
background: rgba(43, 18, 0, 0.03) !important;
|
||||
color: rgba(26, 23, 22, 0.88) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
:global(.dark-mode) {
|
||||
.toggleBtn {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.themeDropdown {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5) !important;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
:global {
|
||||
.qwenpaw-dropdown-menu,
|
||||
.ant-dropdown-menu {
|
||||
background: #2a2a2a !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.qwenpaw-dropdown-menu-item,
|
||||
.ant-dropdown-menu-item {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.qwenpaw-dropdown-menu-item-selected,
|
||||
.ant-dropdown-menu-item-selected {
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
color: rgba(255, 255, 255, 0.88) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Dropdown, Button, type MenuProps } from "antd";
|
||||
import { SparkMoonLine, SparkSunLine } from "@agentscope-ai/icons";
|
||||
import { SunMoon } from "lucide-react";
|
||||
import { useTheme, type ThemeMode } from "../../contexts/ThemeContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ReactNode } from "react";
|
||||
import styles from "./index.module.less";
|
||||
|
||||
const ICONS: Record<ThemeMode, ReactNode> = {
|
||||
light: <SparkSunLine />,
|
||||
dark: <SparkMoonLine />,
|
||||
system: <SunMoon size="1em" />,
|
||||
};
|
||||
|
||||
export default function ThemeToggleButton() {
|
||||
const { themeMode, isDark, setThemeMode } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: MenuProps["items"] = [
|
||||
{
|
||||
key: "light",
|
||||
label: t("theme.light"),
|
||||
onClick: () => setThemeMode("light"),
|
||||
},
|
||||
{
|
||||
key: "dark",
|
||||
label: t("theme.dark"),
|
||||
onClick: () => setThemeMode("dark"),
|
||||
},
|
||||
{
|
||||
key: "system",
|
||||
label: t("theme.system"),
|
||||
onClick: () => setThemeMode("system"),
|
||||
},
|
||||
];
|
||||
|
||||
const icon =
|
||||
themeMode === "system" ? ICONS.system : ICONS[isDark ? "dark" : "light"];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items, selectedKeys: [themeMode] }}
|
||||
placement="bottomRight"
|
||||
overlayClassName={styles.themeDropdown}
|
||||
>
|
||||
<Button className={styles.toggleBtn} type="text" icon={icon} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user