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 { CommandPalette } from "@/components/workspace/command-palette";
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 { cn } from "@/lib/utils";
const queryClient = new QueryClient();
export default function WorkspaceLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<BrandProvider>
<WorkspaceBrandShell>{children}</WorkspaceBrandShell>
</BrandProvider>
);
}
function WorkspaceBrandShell({
children,
}: Readonly<{ children: React.ReactNode }>) {
const [settings, setSettings] = useLocalSettings();
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 comboTriggeredRef = useRef(false);
const searchParams = useSearchParams();
const { rootClassName } = useBrand();
// iframe 技能模式mode=skill时隐藏侧边栏
const isSkillMode = searchParams.get("mode") === "skill";
@ -110,8 +124,9 @@ export default function WorkspaceLayout({
);
return (
<QueryClientProvider client={queryClient}>
<BrandSessionInitializer />
<SidebarProvider
className="h-screen"
className={cn("h-screen", rootClassName)}
open={open}
onOpenChange={handleOpenChange}
>

View File

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

View File

@ -1,6 +1,7 @@
"use client";
import { MessageSquarePlus } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { toast } from "sonner";
@ -13,6 +14,7 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { useThreadChat } from "@/components/workspace/chats";
import { useBrand } from "@/core/brand/provider";
import { useI18n } from "@/core/i18n/hooks";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { env } from "@/env";
@ -21,10 +23,14 @@ import { copyToClipboard } from "@/lib/utils";
export function WorkspaceHeader({ className }: { className?: string }) {
const { t } = useI18n();
const { copy } = useBrand();
const { state } = useSidebar();
const pathname = usePathname();
const { threadId } = useThreadChat();
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 () => {
if (!threadId) return;
@ -51,7 +57,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
{state === "collapsed" ? (
<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">
XC
{compactAppName}
</div>
<SidebarTrigger className="hidden pl-2 group-hover/workspace-header:block" />
</div>
@ -62,9 +68,19 @@ export function WorkspaceHeader({ className }: { className?: string }) {
{t.workspaceHeader.sidebarTitle}
</Link>
) : (
<div className="text-primary ml-2 cursor-default font-serif">
{/* TODO: 测试标识 */}
XClaw{" "}
<div className="text-primary ml-2 flex cursor-default items-center gap-2 font-serif">
{copy.appLogoSrc ? (
<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={cn(
@ -80,7 +96,6 @@ export function WorkspaceHeader({ className }: { className?: string }) {
>
id:{threadId ? threadId.slice(0, 5) : "-"}
</span>
{" "}
{threadId && (
<a
href={threadUrl}