feat(brand): workspace 组件接入品牌文案和 Logo 切换

- layout.tsx: 包裹 BrandProvider + BrandSessionInitializer,SidebarProvider 注入 rootClassName
- welcome.tsx: copy.productLabel 替代硬编码,appLogoSrc 条件渲染 Image/文字
- workspace-header.tsx: 侧边栏折叠时显示品牌缩写,展开时显示 Logo 或 appName
This commit is contained in:
mt 2026-06-10 17:51:46 +08:00
parent 62fd2e6f06
commit 0bd9b9bdcb
3 changed files with 69 additions and 10 deletions

View File

@ -14,12 +14,25 @@ import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { CommandPalette } from "@/components/workspace/command-palette"; import { CommandPalette } from "@/components/workspace/command-palette";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar"; import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { BrandProvider, useBrand } from "@/core/brand/provider";
import { BrandSessionInitializer } from "@/core/brand/provider-client";
import { getLocalSettings, useLocalSettings } from "@/core/settings"; import { getLocalSettings, useLocalSettings } from "@/core/settings";
import { cn } from "@/lib/utils";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export default function WorkspaceLayout({ export default function WorkspaceLayout({
children, children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<BrandProvider>
<WorkspaceBrandShell>{children}</WorkspaceBrandShell>
</BrandProvider>
);
}
function WorkspaceBrandShell({
children,
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
const [settings, setSettings] = useLocalSettings(); const [settings, setSettings] = useLocalSettings();
const [open, setOpen] = useState(false); // SSR default: open (matches server render) const [open, setOpen] = useState(false); // SSR default: open (matches server render)
@ -27,6 +40,7 @@ export default function WorkspaceLayout({
const pressedKeysRef = useRef<Set<string>>(new Set()); const pressedKeysRef = useRef<Set<string>>(new Set());
const comboTriggeredRef = useRef(false); const comboTriggeredRef = useRef(false);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { rootClassName } = useBrand();
// iframe 技能模式mode=skill时隐藏侧边栏 // iframe 技能模式mode=skill时隐藏侧边栏
const isSkillMode = searchParams.get("mode") === "skill"; const isSkillMode = searchParams.get("mode") === "skill";
@ -110,8 +124,9 @@ export default function WorkspaceLayout({
); );
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrandSessionInitializer />
<SidebarProvider <SidebarProvider
className="h-screen" className={cn("h-screen", rootClassName)}
open={open} open={open}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
> >

View File

@ -1,8 +1,10 @@
"use client"; "use client";
import Image from "next/image";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useMemo } from "react"; import { useMemo } from "react";
import { useBrand } from "@/core/brand/provider";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -16,6 +18,7 @@ export function Welcome({
mode?: "ultra" | "pro" | "thinking" | "flash"; mode?: "ultra" | "pro" | "thinking" | "flash";
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const { copy } = useBrand();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isUltra = useMemo(() => mode === "ultra", [mode]); const isUltra = useMemo(() => mode === "ultra", [mode]);
const colors = useMemo(() => { const colors = useMemo(() => {
@ -39,12 +42,37 @@ export function Welcome({
className="flex items-center gap-2" className="flex items-center gap-2"
style={{ fontFamily: '"Microsoft YaHei"' }} style={{ fontFamily: '"Microsoft YaHei"' }}
> >
{/* <AuroraText
className="text-center text-[18px] leading-normal font-normal"
colors={colors}
>
{copy.productLabel}
</AuroraText> */}
<span className="text-[18px] font-normal text-foreground/70">
{copy.productLabel}
</span>
<span className="text-[18px] font-normal text-foreground/70">
·
</span>
{copy.appLogoSrc ? (
<Image
src={copy.appLogoSrc}
alt={copy.appLogoAlt ?? copy.appName}
width={104}
height={16}
draggable={false}
// className="h-[16px] w-auto"
priority
/>
) : (
<AuroraText <AuroraText
className="text-center text-[18px] leading-normal font-normal" className="text-center text-[18px] leading-normal font-normal"
colors={colors} colors={colors}
> >
{t.welcome.greeting} {copy.appName}
</AuroraText> </AuroraText>
)}
</div> </div>
)} )}
</div> </div>
@ -59,7 +87,8 @@ export function Welcome({
)} )}
</div> </div>
) : ( ) : (
<div> </div> // <div> </div>
<></>
)} )}
</div> </div>
); );

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { MessageSquarePlus } from "lucide-react"; import { MessageSquarePlus } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
@ -13,6 +14,7 @@ import {
useSidebar, useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useThreadChat } from "@/components/workspace/chats"; import { useThreadChat } from "@/components/workspace/chats";
import { useBrand } from "@/core/brand/provider";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { env } from "@/env"; import { env } from "@/env";
@ -21,10 +23,14 @@ import { copyToClipboard } from "@/lib/utils";
export function WorkspaceHeader({ className }: { className?: string }) { export function WorkspaceHeader({ className }: { className?: string }) {
const { t } = useI18n(); const { t } = useI18n();
const { copy } = useBrand();
const { state } = useSidebar(); const { state } = useSidebar();
const pathname = usePathname(); const pathname = usePathname();
const { threadId } = useThreadChat(); const { threadId } = useThreadChat();
const threadUrl = threadId ? `/workspace/chats/${threadId}` : ""; const threadUrl = threadId ? `/workspace/chats/${threadId}` : "";
const compactAppName = copy.appName.startsWith("cox")
? `C${copy.appName.charAt(3).toUpperCase()}`
: copy.appName.slice(0, 2).toUpperCase();
const handleCopyThreadId = async () => { const handleCopyThreadId = async () => {
if (!threadId) return; if (!threadId) return;
@ -51,7 +57,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
{state === "collapsed" ? ( {state === "collapsed" ? (
<div className="group-has-data-[collapsible=icon]/sidebar-wrapper:-translate-y flex w-full cursor-pointer items-center justify-center"> <div className="group-has-data-[collapsible=icon]/sidebar-wrapper:-translate-y flex w-full cursor-pointer items-center justify-center">
<div className="text-primary block pt-1 font-serif group-hover/workspace-header:hidden"> <div className="text-primary block pt-1 font-serif group-hover/workspace-header:hidden">
XC {compactAppName}
</div> </div>
<SidebarTrigger className="hidden pl-2 group-hover/workspace-header:block" /> <SidebarTrigger className="hidden pl-2 group-hover/workspace-header:block" />
</div> </div>
@ -62,9 +68,19 @@ export function WorkspaceHeader({ className }: { className?: string }) {
{t.workspaceHeader.sidebarTitle} {t.workspaceHeader.sidebarTitle}
</Link> </Link>
) : ( ) : (
<div className="text-primary ml-2 cursor-default font-serif"> <div className="text-primary ml-2 flex cursor-default items-center gap-2 font-serif">
{/* TODO: 测试标识 */} {copy.appLogoSrc ? (
XClaw{" "} <Image
src={copy.appLogoSrc}
alt={copy.appLogoAlt ?? copy.appName}
width={104}
height={16}
className="h-4 w-auto"
priority
/>
) : (
copy.appName
)}
<span className="text-sm text-ws-text-subtle-strong">v3.3.0 </span>{" "} <span className="text-sm text-ws-text-subtle-strong">v3.3.0 </span>{" "}
<span <span
className={cn( className={cn(
@ -80,7 +96,6 @@ export function WorkspaceHeader({ className }: { className?: string }) {
> >
id:{threadId ? threadId.slice(0, 5) : "-"} id:{threadId ? threadId.slice(0, 5) : "-"}
</span> </span>
{" "}
{threadId && ( {threadId && (
<a <a
href={threadUrl} href={threadUrl}