refactor: white-label 3d world workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-06 20:16:50 +08:00
parent 7eba6d2fb8
commit 6c7f02c142
4 changed files with 111 additions and 219 deletions

View File

@ -190,6 +190,15 @@ const WORLDLABS_MODEL_OPTIONS = [
"marble-1.0",
"marble-1.0-draft",
] as const;
const WORLD_MODEL_DISPLAY_LABELS: Record<
(typeof WORLDLABS_MODEL_OPTIONS)[number],
string
> = {
"marble-1.1": "标准质量",
"marble-1.1-plus": "高保真",
"marble-1.0": "兼容质量",
"marble-1.0-draft": "快速草稿",
};
const ENV_PRESET_OPTIONS: Model3DViewerSettings["envPreset"][] = [
"city",
@ -540,6 +549,11 @@ function worldModeToTaskMode(mode: WorldGenerationMode): TaskMode {
}
}
function getWorldModelDisplayLabel(value?: string | null) {
const normalized = String(value || "").trim() as (typeof WORLDLABS_MODEL_OPTIONS)[number];
return WORLD_MODEL_DISPLAY_LABELS[normalized] || "标准质量";
}
export default function CanvasModel3DPage() {
const router = useRouter();
const routeParams = useParams();
@ -1666,9 +1680,7 @@ export default function CanvasModel3DPage() {
status: "completed",
activeView: primarySplatUrl
? "splat"
: worldAssetPayload?.worldUrl
? "world"
: worldImageUrl
: worldImageUrl
? "image"
: "mesh",
firstPerson: false,
@ -1829,15 +1841,6 @@ export default function CanvasModel3DPage() {
}
}, [currentTask, updateWorldShareState]);
const handleOpenWorldUrl = () => {
const worldUrl = currentTask?.assetPayload?.worldUrl;
if (!worldUrl) {
toast.error("当前任务暂无可打开的 World 链接");
return;
}
window.open(worldUrl, "_blank", "noopener,noreferrer");
};
const handleOpenPanoUrl = () => {
const imageUrl =
currentTask?.assetPayload?.panoUrl ||
@ -2139,8 +2142,6 @@ export default function CanvasModel3DPage() {
const currentWorldAsset =
currentTask?.provider === "worldlabs-3d" ? currentTask.assetPayload ?? null : null;
const currentWorldId =
currentWorldAsset?.worldId || currentTask?.sourceTaskId || null;
const currentWorldShareUrl =
currentTask?.provider === "worldlabs-3d" && currentTask?.shareEnabled
? buildWorldShareUrl(currentTask.shareToken)
@ -2166,21 +2167,18 @@ export default function CanvasModel3DPage() {
const availableWorldViewportModes = useMemo(() => {
const modes: WorldViewportMode[] = [];
if (currentPrimarySpzUrl) modes.push("splat");
if (currentWorldAsset?.worldUrl) modes.push("world");
if (currentWorldImageUrl) modes.push("imagery");
if (currentModelUrl) modes.push("mesh");
return modes;
}, [currentModelUrl, currentPrimarySpzUrl, currentWorldAsset?.worldUrl, currentWorldImageUrl]);
}, [currentModelUrl, currentPrimarySpzUrl, currentWorldImageUrl]);
const preferredWorldViewportMode: WorldViewportMode =
currentPrimarySpzUrl
? "splat"
: currentWorldAsset?.worldUrl
? "world"
: currentWorldImageUrl
? "imagery"
: currentModelUrl
? "mesh"
: "world";
: "splat";
const activeWorkspaceTitle =
workspaceMode === "world" ? "3D 世界工作台" : "3D 工作台";
const viewerEmptyStateTitle =
@ -2245,15 +2243,6 @@ export default function CanvasModel3DPage() {
</div>
<div className="flex items-center gap-3">
{/* DCC Bridge (Mock) */}
<Button
className="hidden h-8 gap-2 text-zinc-400 hover:bg-white/5 hover:text-white sm:flex"
variant="ghost"
>
<Download className="h-4 w-4" />
<span className="text-xs">DCC Bridge</span>
</Button>
<div className="flex h-8 items-center rounded-full bg-blue-500/10 px-3 text-blue-400">
<span className="font-medium text-xs">积分</span>
<span className="ml-1 font-semibold text-sm">{pointsLabel}</span>
@ -2821,7 +2810,7 @@ export default function CanvasModel3DPage() {
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-4">
<div className="space-y-2">
<Label className="text-xs text-zinc-400">World 模型</Label>
<Label className="text-xs text-zinc-400">生成质量</Label>
<Select
onValueChange={(value) => setWorldModel(value as any)}
value={worldModel}
@ -2836,7 +2825,7 @@ export default function CanvasModel3DPage() {
key={item}
value={item}
>
{item}
{getWorldModelDisplayLabel(item)}
</SelectItem>
))}
</SelectContent>
@ -2873,14 +2862,6 @@ export default function CanvasModel3DPage() {
/>
</div>
)}
<div className="flex items-center justify-between text-xs text-zinc-400">
<span>生成公开 World</span>
<Switch
checked={worldMakePublic}
className="data-[state=checked]:bg-yellow-500"
onCheckedChange={setWorldMakePublic}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
@ -3029,24 +3010,14 @@ export default function CanvasModel3DPage() {
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<Button
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
disabled={!currentWorldAsset?.worldUrl}
onClick={handleOpenWorldUrl}
variant="outline"
>
<Globe className="mr-2 h-4 w-4" /> 打开 World
</Button>
<Button
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
disabled={!currentWorldImageUrl}
onClick={handleOpenPanoUrl}
variant="outline"
>
<ImageIcon className="mr-2 h-4 w-4" /> 打开图像
</Button>
</div>
<Button
className="w-full justify-start border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
disabled={!currentWorldImageUrl}
onClick={handleOpenPanoUrl}
variant="outline"
>
<ImageIcon className="mr-2 h-4 w-4" /> 打开图像
</Button>
<div className="grid grid-cols-2 gap-2">
<Button
@ -3070,14 +3041,14 @@ export default function CanvasModel3DPage() {
<Card className="border-white/10 bg-zinc-950/60 text-zinc-100">
<CardHeader className="space-y-1 pb-3">
<CardTitle className="text-sm">当前世界资产</CardTitle>
<CardTitle className="text-sm">当前内容</CardTitle>
<CardDescription className="text-xs text-zinc-500">
当前支持以原生 world3d 节点导入画布,并保留 World、Splat、图像与 Mesh 入口。
这里保留当前世界可直接使用的核心内容入口。
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-xs text-zinc-400">
<div className="space-y-1">
<div className="text-[11px] text-zinc-500">描述</div>
<div className="text-[11px] text-zinc-500">场景描述</div>
<div className="line-clamp-3 rounded-md bg-white/5 px-3 py-2 text-zinc-200">
{currentWorldAsset?.caption ||
currentTask?.prompt ||
@ -3086,35 +3057,21 @@ export default function CanvasModel3DPage() {
</div>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">World ID</div>
<div className="text-[11px] text-zinc-500">生成质量</div>
<div className="truncate text-zinc-200">
{currentWorldId || "暂无"}
{getWorldModelDisplayLabel(
currentWorldAsset?.model || currentTask?.modelVersion
)}
</div>
</div>
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">模型</div>
<div className="text-[11px] text-zinc-500">共享状态</div>
<div className="truncate text-zinc-200">
{currentWorldAsset?.model ||
currentTask?.modelVersion ||
"默认"}
{currentWorldShareUrl ? "已开启" : "未开启"}
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
<span className="flex items-center gap-2">
<Link2 className="h-3.5 w-3.5" />
World 页面
</span>
<Badge
className="text-[10px]"
variant={
currentWorldAsset?.worldUrl ? "default" : "secondary"
}
>
{currentWorldAsset?.worldUrl ? "可用" : "暂无"}
</Badge>
</div>
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
<span className="flex items-center gap-2">
<ImageIcon className="h-3.5 w-3.5" />
@ -3124,7 +3081,7 @@ export default function CanvasModel3DPage() {
className="text-[10px]"
variant={currentWorldImageUrl ? "default" : "secondary"}
>
{currentWorldImageUrl ? "可用" : "暂无"}
{currentWorldImageUrl ? "可查看" : "暂无"}
</Badge>
</div>
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
@ -3141,23 +3098,17 @@ export default function CanvasModel3DPage() {
</div>
<div className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2">
<span className="flex items-center gap-2">
<Link2 className="h-3.5 w-3.5" />
分享页
<Sparkles className="h-3.5 w-3.5" />
World 视图
</span>
<Badge
className="text-[10px]"
variant={currentWorldShareUrl ? "default" : "secondary"}
variant={currentPrimarySpzUrl ? "default" : "secondary"}
>
{currentWorldShareUrl ? "已开启" : "未开启"}
{currentPrimarySpzUrl ? "可查看" : "暂无"}
</Badge>
</div>
</div>
{currentWorldShareUrl ? (
<div className="rounded-md bg-white/5 px-3 py-2 text-zinc-200">
<div className="text-[11px] text-zinc-500">分享链接</div>
<div className="mt-1 truncate">{currentWorldShareUrl}</div>
</div>
) : null}
</CardContent>
</Card>
</div>
@ -3244,21 +3195,21 @@ export default function CanvasModel3DPage() {
<div className="font-bold text-lg text-white">
{currentSpzEntries.length}
</div>
<div className="text-xs text-zinc-500">SPZ 资产</div>
<div className="text-xs text-zinc-500">World 资产</div>
</CardContent>
</Card>
</div>
<Card className="border-white/10 bg-zinc-950/60 text-zinc-100">
<CardHeader className="pb-3">
<CardTitle className="text-sm">世界摘要</CardTitle>
<CardTitle className="text-sm">内容概览</CardTitle>
<CardDescription className="text-xs text-zinc-500">
这里展示 World Labs 返回的主要资产信息,便于在当前工作台里做导入和跳转。
这里展示当前世界的核心内容状态,方便在工作台里继续使用。
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-xs text-zinc-400">
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">Caption</div>
<div className="text-[11px] text-zinc-500">场景描述</div>
<div className="mt-1 whitespace-pre-wrap text-zinc-200">
{currentWorldAsset?.caption ||
currentTask?.prompt ||
@ -3267,15 +3218,11 @@ export default function CanvasModel3DPage() {
</div>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">World ID</div>
<div className="text-[11px] text-zinc-500">生成质量</div>
<div className="truncate text-zinc-200">
{currentWorldId || "暂无"}
</div>
</div>
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">公开链接</div>
<div className="truncate text-zinc-200">
{currentWorldAsset?.worldUrl ? "已生成" : "暂无"}
{getWorldModelDisplayLabel(
currentWorldAsset?.model || currentTask?.modelVersion
)}
</div>
</div>
<div className="rounded-md bg-white/5 px-3 py-2">
@ -3291,48 +3238,17 @@ export default function CanvasModel3DPage() {
</div>
</div>
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">分享页</div>
<div className="text-[11px] text-zinc-500">共享状态</div>
<div className="truncate text-zinc-200">
{currentWorldShareUrl ? "已开启" : "未开启"}
</div>
</div>
</div>
{currentWorldShareUrl ? (
<div className="rounded-md bg-white/5 px-3 py-2 text-zinc-200">
<div className="text-[11px] text-zinc-500">分享链接</div>
<div className="mt-1 truncate">{currentWorldShareUrl}</div>
</div>
) : null}
<div className="space-y-2">
<h4 className="font-medium text-xs text-zinc-500">
SPZ 列表
</h4>
{currentSpzEntries.length ? (
currentSpzEntries.map(([label, url]) => (
<div
className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2"
key={label}
>
<span className="truncate text-zinc-200">
{label}
</span>
<Button
className="h-7 border-white/10 bg-transparent text-zinc-300 hover:bg-white/10"
onClick={() =>
window.open(url, "_blank", "noopener,noreferrer")
}
size="sm"
variant="outline"
>
打开
</Button>
</div>
))
) : (
<div className="rounded-md bg-white/5 px-3 py-3 text-zinc-500">
当前结果没有返回 SPZ 资产。
<div className="rounded-md bg-white/5 px-3 py-2">
<div className="text-[11px] text-zinc-500">World 资产</div>
<div className="truncate text-zinc-200">
{currentSpzEntries.length ? `${currentSpzEntries.length} 项` : "暂无"}
</div>
)}
</div>
</div>
</CardContent>
</Card>
@ -3349,16 +3265,10 @@ export default function CanvasModel3DPage() {
{[
{
id: "splat" as WorldViewportMode,
label: "Splat",
label: "World",
icon: Sparkles,
disabled: !currentPrimarySpzUrl,
},
{
id: "world" as WorldViewportMode,
label: "World",
icon: Globe,
disabled: !currentWorldAsset?.worldUrl,
},
{
id: "imagery" as WorldViewportMode,
label: "图像",

View File

@ -7,8 +7,6 @@ import {
Copy,
Crosshair,
Download,
ExternalLink,
Globe,
ImageIcon,
Loader2,
MoreVertical,
@ -41,16 +39,46 @@ const VIEW_META: Array<{
label: string;
icon: React.ComponentType<{ className?: string }>;
}> = [
{ id: "splat", label: "S", icon: Sparkles },
{ id: "world", label: "W", icon: Globe },
{ id: "splat", label: "W", icon: Sparkles },
{ id: "image", label: "I", icon: ImageIcon },
{ id: "mesh", label: "M", icon: Box },
];
function getWorldNodeModelLabel(value?: string | null) {
const normalized = String(value || "").trim();
switch (normalized) {
case "marble-1.1":
return "标准质量";
case "marble-1.1-plus":
return "高保真";
case "marble-1.0":
return "兼容质量";
case "marble-1.0-draft":
return "快速草稿";
case "":
return "原生世界节点";
default:
return "自定义质量";
}
}
function getWorldNodeViewLabel(view: World3DViewMode) {
switch (view) {
case "splat":
return "WORLD";
case "image":
return "IMAGE";
case "mesh":
return "MESH";
case "world":
default:
return "ONLINE";
}
}
function resolveAvailableViews(data: World3DNodeData) {
const views: World3DViewMode[] = [];
if (data.splatUrl) views.push("splat");
if (data.worldUrl) views.push("world");
if (data.imageUrl || data.previewImage) views.push("image");
if (data.meshUrl) views.push("mesh");
return views;
@ -89,14 +117,14 @@ function inferDownloadTarget(data: World3DNodeData, activeView: World3DViewMode)
function resolvePrimaryUrl(data: World3DNodeData, activeView: World3DViewMode) {
switch (activeView) {
case "world":
return data.worldUrl || data.splatUrl || data.meshUrl || data.imageUrl || null;
return data.splatUrl || data.meshUrl || data.imageUrl || null;
case "image":
return data.imageUrl || data.previewImage || data.worldUrl || data.meshUrl || null;
return data.imageUrl || data.previewImage || data.meshUrl || data.splatUrl || null;
case "mesh":
return data.meshUrl || data.splatUrl || data.worldUrl || null;
return data.meshUrl || data.splatUrl || data.imageUrl || null;
case "splat":
default:
return data.splatUrl || data.worldUrl || data.meshUrl || data.imageUrl || null;
return data.splatUrl || data.imageUrl || data.meshUrl || null;
}
}
@ -171,14 +199,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
toast.success("已添加世界热点");
};
const handleOpen = () => {
if (!primaryUrl) {
toast.error("暂无可打开的世界资产");
return;
}
window.open(primaryUrl, "_blank", "noopener,noreferrer");
};
const handleCopy = async () => {
if (!primaryUrl) {
toast.error("暂无可复制的世界地址");
@ -244,11 +264,11 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
<div className="flex items-end justify-between gap-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium text-white">
{data.prompt || "World Labs 世界"}
{data.prompt || "3D 世界"}
</p>
<div className="mt-1 flex flex-wrap items-center gap-2">
<Badge className="border-white/10 bg-black/55 text-zinc-200" variant="outline">
World Labs
3D 世界
</Badge>
{hotspotCount ? (
<Badge className="border-amber-500/30 bg-amber-500/10 text-amber-200" variant="outline">
@ -256,12 +276,12 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
</Badge>
) : null}
<Badge className="border-white/10 bg-black/55 text-zinc-300" variant="outline">
{activeView.toUpperCase()}
{getWorldNodeViewLabel(activeView)}
</Badge>
</div>
</div>
<p className="text-[11px] text-zinc-400">
{data.modelVersion || "原生世界节点"}
{getWorldNodeModelLabel(data.modelVersion)}
</p>
</div>
</div>
@ -310,15 +330,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
{isHovered && data.status === "completed" && (
<div className="absolute right-3 top-3 z-20 flex items-center gap-1">
<Button
className="h-7 w-7 rounded-full"
onClick={handleOpen}
size="icon"
title="打开世界"
variant="secondary"
>
<ExternalLink className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="h-7 w-7 rounded-full" size="icon" variant="secondary">
@ -326,18 +337,6 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{data.worldUrl ? (
<DropdownMenuItem onClick={() => window.open(data.worldUrl, "_blank", "noopener,noreferrer")}>
<Globe className="mr-2 h-4 w-4" />
打开 World
</DropdownMenuItem>
) : null}
{data.splatUrl ? (
<DropdownMenuItem onClick={() => window.open(data.splatUrl, "_blank", "noopener,noreferrer")}>
<Sparkles className="mr-2 h-4 w-4" />
打开 Splat
</DropdownMenuItem>
) : null}
{data.meshUrl ? (
<DropdownMenuItem onClick={() => window.open(data.meshUrl, "_blank", "noopener,noreferrer")}>
<Box className="mr-2 h-4 w-4" />
@ -370,4 +369,3 @@ export function World3DNode({ node, isSelected }: World3DNodeProps) {
</div>
);
}

View File

@ -525,11 +525,11 @@ function WorldSplatLoadingOverlay({
<div className="rounded-2xl border border-white/10 bg-black/45 px-3 py-2 shadow-[0_20px_60px_rgba(0,0,0,0.38)] backdrop-blur-xl">
<div className="flex items-center gap-2 text-[11px] text-zinc-100">
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
<span>{isRevealing ? "真实世界颗粒正在聚合" : "正在载入真实 Gaussian Splats"}</span>
<span>{isRevealing ? "世界数据正在聚合" : "正在载入世界数据"}</span>
</div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
<Loader2 className="h-3 w-3 animate-spin" />
<span>{isRevealing ? "真实 splat 正在从颗粒态恢复成世界" : "SPZ 数据准备中"}</span>
<span>{isRevealing ? "当前世界正在从颗粒态恢复细节" : "空间数据准备中"}</span>
</div>
</div>
</div>
@ -990,10 +990,10 @@ function WorldSplatViewer({
if (!splatUrl) {
return (
<WorldEmptyState
description="当前结果没有返回可用的 SPZ 世界资产。"
description="当前结果没有返回可用的世界数据。"
icon={Sparkles}
previewImageUrl={previewImageUrl}
title="准备加载 Splat 世界"
title="准备加载 World 视图"
/>
);
}
@ -1001,10 +1001,10 @@ function WorldSplatViewer({
if (loadState === "error") {
return (
<WorldEmptyState
description="SPZ 资源暂时无法加载,可以切换到 World、图像或 Mesh 视图继续查看。"
description="当前世界数据暂时无法加载,可以切换到图像或 Mesh 视图继续查看。"
icon={AlertCircle}
previewImageUrl={previewImageUrl}
title="Splat 视图加载失败"
title="World 视图加载失败"
/>
);
}
@ -1085,11 +1085,11 @@ export function World3DViewer({
className="h-full w-full border-0"
referrerPolicy="no-referrer-when-downgrade"
src={worldUrl}
title="World Labs 世界预览"
title="3D 世界在线预览"
/>
) : (
<WorldEmptyState
description="当前结果还没有可嵌入的 World 页面链接,可以先切换到 Splat、图像或 Mesh 视图。"
description="当前结果还没有可嵌入的在线预览入口,可以先切换到 World、图像或 Mesh 视图。"
icon={Globe}
previewImageUrl={previewImageUrl}
title={emptyStateTitle}
@ -1115,7 +1115,7 @@ export function World3DViewer({
/>
) : (
<WorldEmptyState
description="当前结果还没有可用的图像资产,可以先切换到 Splat 或 Mesh 视图。"
description="当前结果还没有可用的图像资产,可以先切换到 World 或 Mesh 视图。"
icon={ImageIcon}
previewImageUrl={previewImageUrl}
title={emptyStateTitle}

View File

@ -3,7 +3,6 @@
import {
Box,
Download,
Globe,
ImageIcon,
Link2,
Sparkles,
@ -20,7 +19,6 @@ type SharedWorldPageProps = {
title: string;
prompt?: string | null;
caption?: string | null;
model?: string | null;
worldUrl?: string | null;
imageUrl?: string | null;
meshUrl?: string | null;
@ -35,8 +33,7 @@ const VIEW_OPTIONS: Array<{
label: string;
icon: typeof Sparkles;
}> = [
{ id: "splat", label: "Splat", icon: Sparkles },
{ id: "world", label: "World", icon: Globe },
{ id: "splat", label: "World", icon: Sparkles },
{ id: "image", label: "图像", icon: ImageIcon },
{ id: "mesh", label: "Mesh", icon: Box },
];
@ -49,7 +46,6 @@ function resolveAvailableViews(params: {
}) {
const views: World3DViewMode[] = [];
if (params.splatUrl) views.push("splat");
if (params.worldUrl) views.push("world");
if (params.imageUrl) views.push("image");
if (params.meshUrl) views.push("mesh");
return views;
@ -57,7 +53,6 @@ function resolveAvailableViews(params: {
function inferInitialView(views: World3DViewMode[]) {
if (views.includes("splat")) return "splat";
if (views.includes("world")) return "world";
if (views.includes("image")) return "image";
if (views.includes("mesh")) return "mesh";
return "image";
@ -67,7 +62,6 @@ export function WorldSharePage({
title,
prompt,
caption,
model,
worldUrl,
imageUrl,
meshUrl,
@ -116,14 +110,9 @@ export function WorldSharePage({
<Badge className="border-cyan-500/30 bg-cyan-500/10 text-cyan-200" variant="outline">
3D 世界分享
</Badge>
{model ? (
<Badge className="border-white/10 bg-white/5 text-zinc-200" variant="outline">
{model}
</Badge>
) : null}
{availableViews.map((view) => (
<Badge className="border-white/10 bg-white/5 text-zinc-300" key={view} variant="outline">
{view.toUpperCase()}
{VIEW_OPTIONS.find((item) => item.id === view)?.label || view.toUpperCase()}
</Badge>
))}
</div>
@ -225,22 +214,17 @@ export function WorldSharePage({
<div className="rounded-3xl border border-white/10 bg-black/30 p-4 shadow-2xl backdrop-blur">
<h2 className="text-sm font-medium text-white">查看说明</h2>
<div className="mt-3 space-y-2 text-sm leading-6 text-zinc-300">
<p>`Splat` 视图最接近原生 3D 世界体验,支持方向键或 `WASD` 六向移动。</p>
<p>`World` 视图会直接打开 World Labs 提供的公开页面。</p>
<p>`World` 视图最接近原生 3D 世界体验,支持方向键或 `WASD` 六向移动。</p>
<p>当前页面是只读分享页,不会修改原项目或原画布内容。</p>
</div>
</div>
<div className="rounded-3xl border border-white/10 bg-black/30 p-4 shadow-2xl backdrop-blur">
<h2 className="text-sm font-medium text-white">资源状态</h2>
<div className="mt-3 grid grid-cols-2 gap-2 text-xs text-zinc-300">
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
<div className="text-zinc-500">Splat</div>
<div className="mt-1">{splatUrl ? "可查看" : "暂无"}</div>
</div>
<div className="mt-3 grid grid-cols-1 gap-2 text-xs text-zinc-300">
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
<div className="text-zinc-500">World</div>
<div className="mt-1">{worldUrl ? "可打开" : "暂无"}</div>
<div className="mt-1">{splatUrl ? "可查看" : "暂无"}</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 px-3 py-2">
<div className="text-zinc-500">图像</div>