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

@@ -124,7 +124,7 @@
gap: 6px;
height: var(--pd-topbar);
min-width: 0;
overflow: hidden;
/* 窄屏下横向滚动(overflow-x-auto),避免右侧保存/回写按钮被裁掉。 */
padding: 0 12px;
border-bottom: 1px solid var(--pd-b0);
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;
}
.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) {
.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 { useAssetStore, type ModelAsset } from "@/stores/use-asset-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 type { CanvasNodeData } from "@/types/canvas";
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);
}, []);
const [onboardingRestartSignal, setOnboardingRestartSignal] = useState(0);
const mode = usePrevisWorkbenchStore((state) => state.mode);
const setMode = usePrevisWorkbenchStore((state) => state.setMode);
const viewMode = usePrevisWorkbenchStore((state) => state.viewMode);
const setViewMode = usePrevisWorkbenchStore((state) => state.setViewMode);
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 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 renderModeOptions = PREVIS_RENDER_MODE_LABELS;
const capabilities = previsModeCapabilities(mode);
const renderModeOptions = PREVIS_RENDER_MODE_LABELS.filter((option) => capabilities.renderModes.includes(option.value));
const draftRef = useRef<PrevisScene | null>(null);
const stagedRef = useRef<PrevisTransaction | null>(null);
@@ -1096,8 +1099,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
setSelectedBone(null);
}
setPlaying(false);
setMode("animate");
setSequencerVisible(true);
}, [selectedActor, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]);
}, [selectedActor, setMode, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]);
useEffect(() => {
const onPreviewRequested = (event: Event) => {
@@ -1150,7 +1154,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
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}>
<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>
<div className="previs-desk-brand">
<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>{selectedActor ? `角色 · ${selectedActor.name}` : selectedLight ? `灯光 · ${selectedLight.name}` : selectedCameraId ? `机位 · ${selectedCamera?.name || activeCamera?.name || "摄影机"}` : `镜头 · ${activeShot.name}`}</span>
</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" />
<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}
@@ -1378,7 +1400,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
renderMode={renderMode}
playhead={playhead}
playing={playing}
showMotionPaths={draft.objects.some((object) => object.keyframes.length > 1) || draft.cameras.some((camera) => camera.keyframes.length > 1)}
showMotionPaths={capabilities.timeline}
trajectoryDrawing={trajectoryDrawing}
onTrajectoryComplete={handleTrajectoryComplete}
viewMode={viewMode}
@@ -1394,6 +1416,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
showModelLoadNotice
showNavigation={false}
/>
<CanvasPrevisOnboarding scope={onboardingScope} open={open} restartSignal={onboardingRestartSignal} className="pv-onboarding" />
{viewMode === "camera" && (selectedCamera || activeCamera) ? (
<div className="pv-cam-hud" role="status">
<Camera className="size-3" />
@@ -1417,7 +1440,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
onFocusSelected={() => viewportRef.current?.focusSelected()}
onFrameScene={() => viewportRef.current?.frameScene()}
onZoom={(factor) => viewportRef.current?.zoom(factor)}
timelineOpen={sequencerVisible}
timelineOpen={capabilities.timeline && sequencerVisible}
onOpenTimeline={enterAnimationMode}
scenePanelOpen={compactLayout ? scenePanelOpen : sceneDocked}
onToggleScenePanel={() => compactLayout ? setScenePanelOpen((value) => !value) : setSceneDocked((value) => !value)}
@@ -1540,11 +1563,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
</button>
</aside>
</div>
{sequencerVisible ? (
<section className="pv-inline-timeline">
<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>
) : null}
<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}
</section>
</div>
);

View File

@@ -39,7 +39,7 @@ export function PrevisViewToolbar({ viewMode, onViewModeChange, inline = false }
aria-label={`${item.label} ${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"
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) => {
onViewModeChange(item.mode);
// 焦点留在按钮上会让交互控件守卫吃掉 W/E/R 变换快捷键。

View File

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