fix(ci): 质量检查 - 修复预演台重构后 Web 与后端检查失败

补回预演台模式切换、模式门控时间轴与非模态引导挂载,取景切换器使用激活态 token,模板场景补齐 environment 字段;E2E 改为验证新版 FloatingDock 与模式切换下的时间轴,工作流改用重命名后的 test:previs:e2e;修正 canvas_share.go 的 gofmt 格式,并按新布局与欢迎页外观行为校准合同测试。
This commit is contained in:
ddcat666
2026-10-06 14:21:42 +08:00
parent 68491b4c20
commit b8d0b25e3c
10 changed files with 73 additions and 32 deletions

View File

@@ -152,7 +152,7 @@ jobs:
run: bun run test run: bun run test
# ubuntu-latest 预装 google-chrome;脚本的解析顺序已覆盖该路径, # ubuntu-latest 预装 google-chrome;脚本的解析顺序已覆盖该路径,
# 因此不引入第三方 setup action,也不需要任何 secret。 # 因此不引入第三方 setup action,也不需要任何 secret。
- name: Director P0 Chrome E2E - name: Previs P0 Chrome E2E
run: | run: |
google-chrome --version || chromium --version google-chrome --version || chromium --version
bun run test:director:e2e bun run test:previs:e2e

View File

@@ -234,7 +234,7 @@ func publicCanvasProject(project *model.CanvasProject, token string) (map[string
"connections": publicCanvasConnections(source["connections"]), "connections": publicCanvasConnections(source["connections"]),
"chatSessions": []any{}, "chatSessions": []any{},
"activeChatId": nil, "activeChatId": nil,
"previsScenes": []any{}, "previsScenes": []any{},
} }
rawNodes, _ := source["nodes"].([]any) rawNodes, _ := source["nodes"].([]any)
nodes := make([]any, 0, len(rawNodes)) nodes := make([]any, 0, len(rawNodes))

View File

@@ -405,35 +405,42 @@ async function smokeWorkbench(cdp, baseUrl) {
assert(defaultLayoutMode === "true", "A5a 统一预演台默认进入布景上下文", `got ${JSON.stringify(defaultLayoutMode)}`); assert(defaultLayoutMode === "true", "A5a 统一预演台默认进入布景上下文", `got ${JSON.stringify(defaultLayoutMode)}`);
await openUnifiedWorkbench(cdp, "A"); await openUnifiedWorkbench(cdp, "A");
// 使用真实产品 dock 验证 Tooltip 的 hover 与键盘触发,不能只验证包装 span 存在。 // 使用真实产品 dock(FloatingDock)验证提示的 hover 与键盘触发,不能只验证按钮存在。
const tooltipButton = 'button[aria-label="移动对象"]'; const dockSelector = '[role="toolbar"][aria-label="预演台视口工具"]';
const hoverPoint = await cdp.evaluate(`(() => { const r = document.querySelector(${JSON.stringify(tooltipButton)}).getBoundingClientRect(); return {x:r.x+r.width/2, y:r.y+r.height/2}; })()`); assert(await cdp.poll(`!!document.querySelector(${JSON.stringify(dockSelector)})`, "previs dock", 10000), "A5b 预演台视口工具条已渲染");
assert(await cdp.evaluate(`!!document.querySelector('${dockSelector} [role="radiogroup"][aria-label="变换模式"] [role="radio"][aria-label="移动对象 (W)"]')`), "A5b 变换模式切换包含移动对象 (W)");
const tooltipButton = `${dockSelector} button[aria-label="适配全部对象"]`;
const hoverPoint = await cdp.evaluate(`(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); if (!b) return null; const r = b.getBoundingClientRect(); return {x:r.x+r.width/2, y:r.y+r.height/2}; })()`);
if (!hoverPoint) throw new Error("A: 适配全部对象 dock button not found");
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...hoverPoint }); await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...hoverPoint });
const describedTooltip = `(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); const ids = (b?.getAttribute('aria-describedby') || '').split(/\\s+/); return ids.some(id => document.getElementById(id)?.textContent.includes('移动对象')); })()`; const describedTooltip = `(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); return !!b && [...b.querySelectorAll('.aceternity-dock-tooltip')].some((t) => (t.textContent || '').includes('适配全部对象')); })()`;
assert(await cdp.poll(describedTooltip, "tooltip on hover", 5000), "A5b Tooltip hover describes the actual control"); assert(await cdp.poll(describedTooltip, "tooltip on hover", 5000), "A5b Tooltip hover describes the actual control");
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1, y: 1 }); await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1, y: 1 });
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 }); assert(await cdp.poll(`!(${describedTooltip})`, "tooltip dismiss", 5000), "A5b Tooltip dismisses when pointer leaves");
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
assert(await cdp.poll(`!document.querySelector('[role="tooltip"]')`, "tooltip dismiss", 5000), "A5b Escape dismisses Tooltip");
await cdp.evaluate(`document.querySelector(${JSON.stringify(tooltipButton)}).focus()`); await cdp.evaluate(`document.querySelector(${JSON.stringify(tooltipButton)}).focus()`);
for (const modifiers of [8, 0]) { for (const modifiers of [8, 0]) {
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers }); await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers }); await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
} }
assert(await cdp.poll(`document.activeElement === document.querySelector(${JSON.stringify(tooltipButton)}) && (${describedTooltip})`, "tooltip on keyboard focus", 5000), "A5c Tab focus opens Tooltip without an extra wrapper stop"); assert(await cdp.poll(`document.activeElement === document.querySelector(${JSON.stringify(tooltipButton)}) && (${describedTooltip})`, "tooltip on keyboard focus", 5000), "A5c Tab focus opens Tooltip without an extra wrapper stop");
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await cdp.evaluate("document.activeElement?.blur()"); await cdp.evaluate("document.activeElement?.blur()");
// 统一工作台默认同时提供镜头设计与可收起的动画时间轴;自动关键帧初始关闭。 // 摆场模式不显示时间轴;切到动画模式才出现时间轴,且自动关键帧初始关闭。
const layoutGating = await cdp.evaluate(`(() => ({ const readGating = `(() => ({
workbench: !!document.querySelector('[data-canvas-previs-workbench]'), workbench: !!document.querySelector('[data-canvas-previs-workbench]'),
layout: !!document.querySelector('.pv-layout'), layout: !!document.querySelector('.pv-layout'),
sequencer: document.querySelectorAll('.previs-sequencer').length, sequencer: document.querySelectorAll('.previs-sequencer').length,
autoKey: document.querySelector('button[title="自动关键帧"]')?.getAttribute('aria-pressed') ?? null, autoKey: document.querySelector('button[title="自动关键帧"]')?.getAttribute('aria-pressed') ?? null,
}))()`); }))()`;
const layoutGating = await cdp.evaluate(readGating);
assert(layoutGating.workbench && layoutGating.layout, "A6 统一预演台布局上下文已就绪", JSON.stringify(layoutGating)); assert(layoutGating.workbench && layoutGating.layout, "A6 统一预演台布局上下文已就绪", JSON.stringify(layoutGating));
assert(layoutGating.sequencer === 1 && layoutGating.autoKey === "false", "A7 时间轴可见且 AutoKey 默认关闭", JSON.stringify(layoutGating)); assert(layoutGating.sequencer === 0, "A6a 摆场模式不显示时间轴", JSON.stringify(layoutGating));
if (!(await cdp.click('button[data-mode="animate"]'))) throw new Error("A: 动画模式按钮不可点击");
assert(await cdp.poll(`document.querySelectorAll('.previs-sequencer').length === 1`, "animate sequencer", 10000), "A7 动画模式显示时间轴");
const animateGating = await cdp.evaluate(readGating);
assert(animateGating.autoKey === "false", "A7a AutoKey 默认关闭", JSON.stringify(animateGating));
if (!(await cdp.click('button[data-mode="layout"]'))) throw new Error("A: 摆场模式按钮不可点击");
assert(await cdp.poll(`document.querySelectorAll('.previs-sequencer').length === 0`, "layout hides sequencer", 10000), "A7b 回到摆场模式隐藏时间轴");
const propMenuOpened = await cdp.click('[aria-label="添加道具"]'); const propMenuOpened = await cdp.click('[aria-label="添加道具"]');
if (!propMenuOpened) throw new Error("A: 添加道具 button not clickable"); if (!propMenuOpened) throw new Error("A: 添加道具 button not clickable");

View File

@@ -124,7 +124,7 @@
gap: 6px; gap: 6px;
height: var(--pd-topbar); height: var(--pd-topbar);
min-width: 0; min-width: 0;
overflow: hidden; /* 窄屏下横向滚动(overflow-x-auto),避免右侧保存/回写按钮被裁掉。 */
padding: 0 12px; padding: 0 12px;
border-bottom: 1px solid var(--pd-b0); border-bottom: 1px solid var(--pd-b0);
background: var(--pd-s1); background: var(--pd-s1);
@@ -1406,6 +1406,8 @@
backdrop-filter: blur(16px); color: var(--pd-t0); font-size: var(--pd-ts-xs); font-weight: 600; pointer-events: none; backdrop-filter: blur(16px); color: var(--pd-t0); font-size: var(--pd-ts-xs); font-weight: 600; pointer-events: none;
} }
.pv-cam-hud__meta { color: var(--pd-t2); font-weight: 400; font-variant-numeric: tabular-nums; } .pv-cam-hud__meta { color: var(--pd-t2); font-weight: 400; font-variant-numeric: tabular-nums; }
/* 非模态引导:贴视口右上角,不挡中央构图。 */
.pv-onboarding { position: absolute; top: 12px; right: 12px; z-index: 7; width: min(340px, calc(100% - 24px)); }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.pv-canvas-dock__view-grid button, .pv-canvas-dock__camera-list button { transition: none; } .pv-canvas-dock__view-grid button, .pv-canvas-dock__camera-list button { transition: none; }
} }

View File

@@ -31,6 +31,7 @@ import { usePrevisSaveCoordinator } from "@/components/canvas/previs/use-previs-
import "./canvas-previs-workbench.css"; import "./canvas-previs-workbench.css";
import { useAssetStore, type ModelAsset } from "@/stores/use-asset-store"; import { useAssetStore, type ModelAsset } from "@/stores/use-asset-store";
import { usePrevisWorkbenchStore } from "@/stores/canvas/use-previs-workbench-store"; import { usePrevisWorkbenchStore } from "@/stores/canvas/use-previs-workbench-store";
import { PREVIS_MODES, previsModeCapabilities } from "@/lib/canvas/previs/previs-modes";
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
import type { CanvasNodeData } from "@/types/canvas"; import type { CanvasNodeData } from "@/types/canvas";
import type { PrevisCameraMove, PrevisRenderMode, PrevisScene, PrevisSceneOutput, PrevisTransform, PrevisVec3 } from "@/types/previs"; import type { PrevisCameraMove, PrevisRenderMode, PrevisScene, PrevisSceneOutput, PrevisTransform, PrevisVec3 } from "@/types/previs";
@@ -70,6 +71,8 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
return () => media.removeEventListener("change", update); return () => media.removeEventListener("change", update);
}, []); }, []);
const [onboardingRestartSignal, setOnboardingRestartSignal] = useState(0); const [onboardingRestartSignal, setOnboardingRestartSignal] = useState(0);
const mode = usePrevisWorkbenchStore((state) => state.mode);
const setMode = usePrevisWorkbenchStore((state) => state.setMode);
const viewMode = usePrevisWorkbenchStore((state) => state.viewMode); const viewMode = usePrevisWorkbenchStore((state) => state.viewMode);
const setViewMode = usePrevisWorkbenchStore((state) => state.setViewMode); const setViewMode = usePrevisWorkbenchStore((state) => state.setViewMode);
const selectedObjectId = usePrevisWorkbenchStore((state) => state.selectedObjectId); const selectedObjectId = usePrevisWorkbenchStore((state) => state.selectedObjectId);
@@ -117,8 +120,8 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
const addAsset = useAssetStore((state) => state.addAsset); const addAsset = useAssetStore((state) => state.addAsset);
const modelAssets = useMemo(() => assets.filter((asset): asset is ModelAsset => asset.kind === "model"), [assets]); const modelAssets = useMemo(() => assets.filter((asset): asset is ModelAsset => asset.kind === "model"), [assets]);
const capabilities = { timeline: true, keyframes: sequencerVisible, bones: true, cameraTools: Boolean(selectedCameraId && !selectedObjectId && !selectedLightId), renderModes: PREVIS_RENDER_MODE_LABELS.map((option) => option.value) }; const capabilities = previsModeCapabilities(mode);
const renderModeOptions = PREVIS_RENDER_MODE_LABELS; const renderModeOptions = PREVIS_RENDER_MODE_LABELS.filter((option) => capabilities.renderModes.includes(option.value));
const draftRef = useRef<PrevisScene | null>(null); const draftRef = useRef<PrevisScene | null>(null);
const stagedRef = useRef<PrevisTransaction | null>(null); const stagedRef = useRef<PrevisTransaction | null>(null);
@@ -1096,8 +1099,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
setSelectedBone(null); setSelectedBone(null);
} }
setPlaying(false); setPlaying(false);
setMode("animate");
setSequencerVisible(true); setSequencerVisible(true);
}, [selectedActor, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]); }, [selectedActor, setMode, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]);
useEffect(() => { useEffect(() => {
const onPreviewRequested = (event: Event) => { const onPreviewRequested = (event: Event) => {
@@ -1150,7 +1154,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
return ( return (
<div data-canvas-previs-workbench data-canvas-no-zoom className="previs-desk fixed inset-0 z-[var(--z-toast)] flex min-h-0 min-w-0 flex-col overflow-hidden" style={workbenchThemeStyle}> <div data-canvas-previs-workbench data-canvas-no-zoom className="previs-desk fixed inset-0 z-[var(--z-toast)] flex min-h-0 min-w-0 flex-col overflow-hidden" style={workbenchThemeStyle}>
<header className="previs-desk-header"> <header className="previs-desk-header thin-scrollbar overflow-x-auto overflow-y-hidden">
<button type="button" className="previs-desk-icon-button" aria-label="关闭预演台" title="关闭预演台" onClick={closeWorkbench}><X className="size-4" /></button> <button type="button" className="previs-desk-icon-button" aria-label="关闭预演台" title="关闭预演台" onClick={closeWorkbench}><X className="size-4" /></button>
<div className="previs-desk-brand"> <div className="previs-desk-brand">
<span className="previs-desk-logo">影策</span> <span className="previs-desk-logo">影策</span>
@@ -1167,6 +1171,24 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
<span className="previs-desk-context-dot" /> <span className="previs-desk-context-dot" />
<span>{selectedActor ? `角色 · ${selectedActor.name}` : selectedLight ? `灯光 · ${selectedLight.name}` : selectedCameraId ? `机位 · ${selectedCamera?.name || activeCamera?.name || "摄影机"}` : `镜头 · ${activeShot.name}`}</span> <span>{selectedActor ? `角色 · ${selectedActor.name}` : selectedLight ? `灯光 · ${selectedLight.name}` : selectedCameraId ? `机位 · ${selectedCamera?.name || activeCamera?.name || "摄影机"}` : `镜头 · ${activeShot.name}`}</span>
</div> </div>
<nav className="previs-mode-switch" aria-label="预演台模式">
{PREVIS_MODES.map((item) => (
<button
key={item.mode}
type="button"
className={`previs-mode-switch-button ${mode === item.mode ? "is-active" : ""}`}
aria-pressed={mode === item.mode}
data-mode={item.mode}
title={item.hint}
onClick={(event) => {
setMode(item.mode);
releasePrevisFocusAfterPointer(event);
}}
>
{item.label}
</button>
))}
</nav>
<div className="previs-desk-header-spacer" /> <div className="previs-desk-header-spacer" />
<span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">{saveIndicator.label}</span> <span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">{saveIndicator.label}</span>
{saveIndicator.retryable ? <button type="button" className="previs-desk-header-action" aria-label="重试保存" disabled={retrying || saveIndicator.busy} onClick={() => void retrySave()}><RotateCcw className="size-3.5" />重试保存</button> : null} {saveIndicator.retryable ? <button type="button" className="previs-desk-header-action" aria-label="重试保存" disabled={retrying || saveIndicator.busy} onClick={() => void retrySave()}><RotateCcw className="size-3.5" />重试保存</button> : null}
@@ -1378,7 +1400,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
renderMode={renderMode} renderMode={renderMode}
playhead={playhead} playhead={playhead}
playing={playing} playing={playing}
showMotionPaths={draft.objects.some((object) => object.keyframes.length > 1) || draft.cameras.some((camera) => camera.keyframes.length > 1)} showMotionPaths={capabilities.timeline}
trajectoryDrawing={trajectoryDrawing} trajectoryDrawing={trajectoryDrawing}
onTrajectoryComplete={handleTrajectoryComplete} onTrajectoryComplete={handleTrajectoryComplete}
viewMode={viewMode} viewMode={viewMode}
@@ -1394,6 +1416,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
showModelLoadNotice showModelLoadNotice
showNavigation={false} showNavigation={false}
/> />
<CanvasPrevisOnboarding scope={onboardingScope} open={open} restartSignal={onboardingRestartSignal} className="pv-onboarding" />
{viewMode === "camera" && (selectedCamera || activeCamera) ? ( {viewMode === "camera" && (selectedCamera || activeCamera) ? (
<div className="pv-cam-hud" role="status"> <div className="pv-cam-hud" role="status">
<Camera className="size-3" /> <Camera className="size-3" />
@@ -1417,7 +1440,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
onFocusSelected={() => viewportRef.current?.focusSelected()} onFocusSelected={() => viewportRef.current?.focusSelected()}
onFrameScene={() => viewportRef.current?.frameScene()} onFrameScene={() => viewportRef.current?.frameScene()}
onZoom={(factor) => viewportRef.current?.zoom(factor)} onZoom={(factor) => viewportRef.current?.zoom(factor)}
timelineOpen={sequencerVisible} timelineOpen={capabilities.timeline && sequencerVisible}
onOpenTimeline={enterAnimationMode} onOpenTimeline={enterAnimationMode}
scenePanelOpen={compactLayout ? scenePanelOpen : sceneDocked} scenePanelOpen={compactLayout ? scenePanelOpen : sceneDocked}
onToggleScenePanel={() => compactLayout ? setScenePanelOpen((value) => !value) : setSceneDocked((value) => !value)} onToggleScenePanel={() => compactLayout ? setScenePanelOpen((value) => !value) : setSceneDocked((value) => !value)}
@@ -1540,11 +1563,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
</button> </button>
</aside> </aside>
</div> </div>
{sequencerVisible ? ( <section className="pv-inline-timeline">
<section className="pv-inline-timeline"> {capabilities.timeline ? <PrevisSequencer scene={draft} shot={activeShot} camera={activeCamera} objects={draft.objects} selectedObjectId={selectedObjectId} selectedBone={selectedBone} playhead={playhead} playing={playing} autoKey={autoKey} height={sequencerHeight} visible={sequencerVisible} onPlayToggle={() => setPlaying(!playing)} onPlayheadChange={setPlayhead} onAutoKeyChange={setAutoKey} onHeightChange={setSequencerHeight} onVisibilityChange={setSequencerVisible} onSelectObject={setSelectedObjectId} onSelectBone={setSelectedBone} onRecordKeyframe={recordSelectedKeyframe} onAddShot={addShot} onDeleteKeyframe={deleteKeyframe} onSetKeyframeEasing={setKeyframeEasing} onSelectShot={(id) => { commit((current) => ({ ...current, activeShotId: id })); setPlayhead(0); }} /> : null}
<PrevisSequencer scene={draft} shot={activeShot} camera={activeCamera} objects={draft.objects} selectedObjectId={selectedObjectId} selectedBone={selectedBone} playhead={playhead} playing={playing} autoKey={autoKey} height={sequencerHeight} visible onPlayToggle={() => setPlaying(!playing)} onPlayheadChange={setPlayhead} onAutoKeyChange={setAutoKey} onHeightChange={setSequencerHeight} onVisibilityChange={setSequencerVisible} onSelectObject={setSelectedObjectId} onSelectBone={setSelectedBone} onRecordKeyframe={recordSelectedKeyframe} onAddShot={addShot} onDeleteKeyframe={deleteKeyframe} onSetKeyframeEasing={setKeyframeEasing} onSelectShot={(id) => { commit((current) => ({ ...current, activeShotId: id })); setPlayhead(0); }} /> </section>
</section>
) : null}
</div> </div>
); );

View File

@@ -39,7 +39,7 @@ export function PrevisViewToolbar({ viewMode, onViewModeChange, inline = false }
aria-label={`${item.label} ${item.hint}`} aria-label={`${item.label} ${item.hint}`}
title={item.hint} title={item.hint}
className="previs-view-toolbar-button inline-flex h-8 min-w-11 items-center justify-center rounded-[var(--r-md)] px-2 text-[var(--fs-tiny)] font-semibold tracking-wide transition-colors hover:bg-[var(--previs-control-hover)] hover:text-[var(--previs-dock-fg-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--control-focus-ring)] motion-reduce:transition-none" className="previs-view-toolbar-button inline-flex h-8 min-w-11 items-center justify-center rounded-[var(--r-md)] px-2 text-[var(--fs-tiny)] font-semibold tracking-wide transition-colors hover:bg-[var(--previs-control-hover)] hover:text-[var(--previs-dock-fg-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--control-focus-ring)] motion-reduce:transition-none"
style={active ? { color: "var(--previs-dock-fg-strong)" } : undefined} style={active ? { background: "var(--previs-dock-active-surface)", color: "var(--previs-dock-fg-strong)" } : undefined}
onClick={(event) => { onClick={(event) => {
onViewModeChange(item.mode); onViewModeChange(item.mode);
// 焦点留在按钮上会让交互控件守卫吃掉 W/E/R 变换快捷键。 // 焦点留在按钮上会让交互控件守卫吃掉 W/E/R 变换快捷键。

View File

@@ -132,6 +132,7 @@ export function createPrevisSceneFromTemplate(templateId: PrevisTemplateId, titl
title, title,
background: "#d8dde3", background: "#d8dde3",
environmentIntensity: 0.7, environmentIntensity: 0.7,
environment: { mode: "color" },
gridVisible: true, gridVisible: true,
objects: blueprint.objects, objects: blueprint.objects,
cameras: [blueprint.camera], cameras: [blueprint.camera],

View File

@@ -64,7 +64,8 @@ for (const pathname of ["/welcome", "/welcome/"]) {
test(`public film entry remains independent: ${pathname}`, async () => { test(`public film entry remains independent: ${pathname}`, async () => {
const entry = await prepareEntry(false, pathname); const entry = await prepareEntry(false, pathname);
await entry.loaded; await entry.loaded;
expect(entry.events).toEqual(["./welcome-application"]); // 欢迎页沿用站点外观(#683),但外观请求不阻塞欢迎页入口加载。
expect(entry.events).toEqual(["appearance", "./welcome-application"]);
}); });
} }

View File

@@ -129,7 +129,11 @@ describe("模式接线", () => {
expect(dock).toContain("RENDER_VIEW_BUTTONS.filter((item) => renderModes.includes(item.mode))"); expect(dock).toContain("RENDER_VIEW_BUTTONS.filter((item) => renderModes.includes(item.mode))");
// 写死的按钮会绕过门控。 // 写死的按钮会绕过门控。
expect(dock).not.toContain('onClick={() => onRenderModeChange("pose")}'); expect(dock).not.toContain('onClick={() => onRenderModeChange("pose")}');
expect(workbench).toContain("renderModes={capabilities.renderModes}"); // 工作台当前挂的是 PrevisCanvasDock:它不提供渲染视图入口,唯一入口是已按模式过滤的顶栏下拉。
const canvasDock = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/previs-canvas-dock.tsx"), "utf8");
expect(workbench).toContain("<PrevisCanvasDock");
expect(canvasDock).not.toContain("onRenderModeChange");
expect(workbench).not.toContain("<PrevisViewportDock");
}); });
test("store 层夹住 renderMode:任何路径都无法设置当前模式不允许的视图", () => { test("store 层夹住 renderMode:任何路径都无法设置当前模式不允许的视图", () => {
@@ -148,7 +152,11 @@ describe("模式接线", () => {
}); });
test("小屏把属性检查器放到下方而不是隐藏,姿态与骨骼入口仍可达", () => { test("小屏把属性检查器放到下方而不是隐藏,姿态与骨骼入口仍可达", () => {
expect(workbench).toContain("max-lg:col-span-2 max-lg:max-h-[40vh] max-lg:border-l-0 max-lg:border-t"); // 窄屏下检查器改为可开合的侧滑面板,dock 提供开关入口,而不是直接隐藏。
const workbenchCss = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/canvas-previs-workbench.css"), "utf8");
expect(workbench).toContain('className={`pv-panel pv-panel--right thin-scrollbar ${inspectorOpen ? "is-open" : ""}`}');
expect(workbench).toContain("onToggleInspector={() => compactLayout ? setInspectorOpen((value) => !value) : setInspectorDocked((value) => !value)}");
expect(workbenchCss).toContain(".pv-panel--right.is-open { transform: translateX(0); }");
expect(workbench).not.toContain("border-l max-lg:hidden"); expect(workbench).not.toContain("border-l max-lg:hidden");
}); });

View File

@@ -22,6 +22,7 @@ export const SPLIT_MODULE_GROUPS: Record<string, readonly string[]> = {
"pages/admin/settings/appearance-settings-page.tsx": ["pages/admin/settings/appearance-settings-parts.tsx"], "pages/admin/settings/appearance-settings-page.tsx": ["pages/admin/settings/appearance-settings-parts.tsx"],
"pages/admin/settings/storage-settings-page.tsx": ["pages/admin/settings/storage-settings-model.ts"], "pages/admin/settings/storage-settings-page.tsx": ["pages/admin/settings/storage-settings-model.ts"],
"pages/assets/index.tsx": ["pages/assets/asset-library-cards.tsx", "pages/assets/asset-library-format.ts", "pages/assets/asset-library-panels.tsx"], "pages/assets/index.tsx": ["pages/assets/asset-library-cards.tsx", "pages/assets/asset-library-format.ts", "pages/assets/asset-library-panels.tsx"],
"components/canvas/previs/canvas-previs-workbench.tsx": ["components/canvas/previs/previs-inspectors.tsx"],
"pages/canvas/project.tsx": ["pages/canvas/canvas-clipboard.ts"], "pages/canvas/project.tsx": ["pages/canvas/canvas-clipboard.ts"],
"pages/create/creation-workspace.tsx": ["pages/create/creation-workspace-empty.tsx", "pages/create/creation-workspace-history.tsx", "pages/create/creation-workspace-messages.tsx"], "pages/create/creation-workspace.tsx": ["pages/create/creation-workspace-empty.tsx", "pages/create/creation-workspace-history.tsx", "pages/create/creation-workspace-messages.tsx"],
"services/user-data-sync.ts": ["services/user-data-sync-media.ts"], "services/user-data-sync.ts": ["services/user-data-sync-media.ts"],