From 79f1a004a0616f834914429dbf48df625fa33d20 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Mon, 22 Jun 2026 11:02:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(layout):=20=E7=AE=80=E6=B4=81=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8F=B3=E4=B8=8A=E8=A7=92=E5=8A=A0=E6=8D=A2=E8=82=A4?= =?UTF-8?q?=20+=20=E7=95=8C=E9=9D=A2=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出共享 appearance-controls(ThemeSwitcher 按 tone 适配 banner/content 配色,新增图标+下拉的 LayoutModeSwitcher);Header 复用换肤组件,简洁模式 ChatPage 顶栏右上角补上换肤与界面模式切换,与全量模式保持一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace/appearance-controls.tsx | 178 ++++++++++++++++++ frontend-web/src/pages/ChatPage.tsx | 8 + .../src/strategy-components/Header.tsx | 96 +--------- 3 files changed, 191 insertions(+), 91 deletions(-) create mode 100644 frontend-web/src/components/workspace/appearance-controls.tsx 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 = ( <> - +