diff --git a/frontend-web/src/components/workspace/appearance-controls.tsx b/frontend-web/src/components/workspace/appearance-controls.tsx new file mode 100644 index 00000000..8decbe91 --- /dev/null +++ b/frontend-web/src/components/workspace/appearance-controls.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { Check, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme, type ThemeType } from "@/contexts/ThemeContext"; +import { useLayoutMode } from "@/core/system-settings/hooks"; +import { cn } from "@/lib/utils"; + +/** + * 换肤 / 界面模式 顶栏控件。深色顶栏(Header)与简洁模式页面级顶栏(ChatPage 等)共用, + * 通过 `tone` 适配不同底色: + * - "banner-dark" / "banner-light":Header 落在主题 banner 上(白字 / 浅色 hover)。 + * - "content":简洁模式聊天页浅底顶栏,走 foreground / muted-foreground。 + */ +export type AppearanceControlTone = "banner-dark" | "banner-light" | "content"; + +// 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),深蓝色=深色主题(dark) +export const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [ + { id: "light", label: "红色", color: "#cf1322" }, + { id: "lightblue", label: "蓝色", color: "#0062d9" }, + { id: "dark", label: "深蓝色", color: "#002466" }, +]; + +const triggerToneClass = (tone: AppearanceControlTone) => + tone === "banner-dark" + ? "text-slate-400 hover:bg-slate-800/50 hover:text-slate-400" + : tone === "banner-light" + ? "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]" + : "text-muted-foreground hover:bg-muted hover:text-foreground"; + +export function ThemeSwitcher({ tone }: { tone: AppearanceControlTone }) { + const { theme, setTheme } = useTheme(); + const [open, setOpen] = useState(false); + const closeTimer = useRef | null>(null); + + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + const scheduleClose = () => { + cancelClose(); + closeTimer.current = setTimeout(() => setOpen(false), 120); + }; + + useEffect(() => () => cancelClose(), []); + + return ( +
{ + cancelClose(); + setOpen(true); + }} + onMouseLeave={scheduleClose} + > + + {open ? ( +
+
+
+ {THEME_SWATCHES.map((swatch) => { + const active = theme === swatch.id; + return ( +
+
+ ) : null} +
+ ); +} + +const LAYOUT_OPTIONS = [ + { + id: "full" as const, + label: "全量模式", + description: "展示侧边栏与顶栏导航", + icon: PanelLeftIcon, + }, + { + id: "compact" as const, + label: "简洁模式", + description: "隐藏侧边栏与顶栏,获得更大工作区", + icon: LayoutIcon, + }, +]; + +/** + * 界面模式(全量 / 简洁)下拉切换。简洁模式下全局顶栏被隐藏,故把外观设置里的 + * 「界面模式」切换也搬到页面级顶栏右上角,图标 + 下拉,与换肤并排。 + */ +export function LayoutModeSwitcher({ tone }: { tone: AppearanceControlTone }) { + const [mode, setLayoutMode] = useLayoutMode(); + // 未显式设置过时简洁模式才会渲染本控件(全量模式有全局顶栏),故当前态按 compact 兜底。 + const current = mode === "full" ? "full" : "compact"; + const TriggerIcon = current === "full" ? PanelLeftIcon : LayoutIcon; + + return ( + + + + + + 界面模式 + + {LAYOUT_OPTIONS.map((option) => { + const Icon = option.icon; + const active = current === option.id; + return ( + setLayoutMode(option.id)} + className="gap-2" + > + +
+ {option.label} + + {option.description} + +
+ {active && } +
+ ); + })} +
+
+ ); +} diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index b1c24df3..325a8739 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -26,6 +26,10 @@ import { ThreadContext } from "@/components/workspace/messages/context"; import { QuestionNavigator } from "@/components/workspace/question-navigator"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; +import { + LayoutModeSwitcher, + ThemeSwitcher, +} from "@/components/workspace/appearance-controls"; import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator"; import { UserIdentityBadge } from "@/components/workspace/user-identity-badge"; import { Welcome } from "@/components/workspace/welcome"; @@ -484,8 +488,12 @@ export default function ChatPage() { {/* 不带全局顶栏(紧凑模式)时,名称+岗位回落到聊天页顶栏右侧, 与全量模式 Header 右上角保持一致。全量模式已有顶栏则不重复展示。 */} + {/* 简洁模式无全局顶栏:把换肤 + 界面模式切换搬到页面级顶栏右上角, + 与全量模式 Header 右上角保持一致(content 配色贴浅底顶栏)。 */} {!compactChrome && ( <> + +
cn( @@ -32,93 +33,6 @@ const iconButtonClass = (isDark: boolean) => const LEFT_MENU_IDS = ["qa-center", "task-management", "light-app-center"]; const RIGHT_MENU_IDS = ["comprehensive-management", "resource-management"]; -// 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),深蓝色=深色主题(dark) -const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [ - { id: "light", label: "红色", color: "#cf1322" }, - { id: "lightblue", label: "蓝色", color: "#0062d9" }, - { id: "dark", label: "深蓝色", color: "#002466" }, -]; - -function ThemeSwitcher({ isDark }: { isDark: boolean }) { - const { theme, setTheme } = useTheme(); - const [open, setOpen] = useState(false); - const closeTimer = useRef | null>(null); - - const cancelClose = () => { - if (closeTimer.current) { - clearTimeout(closeTimer.current); - closeTimer.current = null; - } - }; - const scheduleClose = () => { - cancelClose(); - closeTimer.current = setTimeout(() => setOpen(false), 120); - }; - - useEffect(() => () => cancelClose(), []); - - // 切换按钮:悬浮只改背景,不改图标颜色(显式锁定 hover 文字色等于常态色) - const triggerClass = cn( - "size-8 shrink-0 rounded-sm", - isDark - ? "text-slate-400 hover:bg-slate-800/50 hover:text-slate-400" - : "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]", - ); - - return ( -
{ - cancelClose(); - setOpen(true); - }} - onMouseLeave={scheduleClose} - > - - {open ? ( -
-
-
- {THEME_SWATCHES.map((swatch) => { - const active = theme === swatch.id; - return ( -
-
- ) : null} -
- ); -} - type HeaderProps = { collapsed?: boolean; }; @@ -210,7 +124,7 @@ const Header = ({ collapsed }: HeaderProps) => { // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> - +