refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat committed 2026-09-09 22:02:10 +08:00
1 parent bacd502f5c
commit b5f3413ded
298 files changed
+12681 -26428

No files matched your search

+30 -108
View File
@@ -47,7 +47,6 @@ import { CanvasVideoFrameDialog } from "@/components/canvas/canvas-video-frame-d
import { CanvasVideoSegmentDialog } from "@/components/canvas/canvas-video-segment-dialog";
import { CanvasTimelineDialog } from "@/components/canvas/canvas-timeline-dialog";
import { syncNodeSubtitlesToTimeline } from "@/lib/timeline/timeline-build";
import type { TimelineDirectMedia } from "@/types/timeline";
import { CanvasNodeAnglePanel } from "@/components/canvas/canvas-node-angle-dialog";
import { CanvasTextEditorModal } from "@/components/canvas/canvas-text-editor-modal";
import { CanvasNodeSearchModal } from "@/components/canvas/canvas-node-search-modal";
@@ -60,7 +59,7 @@ import { Minimap } from "@/components/canvas/canvas-mini-map";
import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
import { CanvasToolbar } from "@/components/canvas/canvas-toolbar";
import { useCanvasCreateCommands } from "@/components/canvas/use-canvas-create-commands";
import { AssetPickerModal, type InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
import { AssetPickerModal } from "@/components/canvas/asset-picker-modal";
import { getProject } from "@/services/api/projects";
import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
import { CanvasShareModal } from "@/components/canvas/canvas-share-modal";
@@ -126,6 +125,7 @@ import { useCanvasSelectionController } from "./use-canvas-selection-controller"
import { useCanvasShortDrama } from "./use-canvas-short-drama";
import { useCanvasStoryboard } from "./use-canvas-storyboard";
import { useCanvasUpload } from "./use-canvas-upload";
import { useCanvasTimelineAssetInsert } from "./use-canvas-timeline-asset-insert";
import { useCanvasViewportController } from "./use-canvas-viewport-controller";
import { usePortraitClearanceCoordinator } from "./use-portrait-clearance-coordinator";
import {
@@ -205,7 +205,8 @@ export default function CanvasPage() {
}
function InfiniteCanvasPage() {
const { message } = App.useApp();
// 命令式确认必须走 App.useApp().modal;静态 Modal.confirm 拿不到主题和 App 上下文。
const { message, modal } = App.useApp();
const queryClient = useQueryClient();
const params = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
@@ -281,10 +282,6 @@ function InfiniteCanvasPage() {
const [stylePickerOpen, setStylePickerOpen] = useState(false);
// 新建导演台镜头必须先选模板:null 表示未在选择中,undefined position 表示用画布中心。
const [directorTemplateRequest, setDirectorTemplateRequest] = useState<{ position?: Position } | null>(null);
const [projectAssetOpen, setProjectAssetOpen] = useState(false);
const [projectAssetInitialCategory, setProjectAssetInitialCategory] = useState("all");
const [projectAssetInitialFolderId, setProjectAssetInitialFolderId] = useState("all");
const [projectAssetInsertPosition, setProjectAssetInsertPosition] = useState<Position | undefined>();
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
const [subtitleNodeId, setSubtitleNodeId] = useState<string | null>(null);
const [timelineNodeId, setTimelineNodeId] = useState<string | null>(null);
@@ -523,7 +520,7 @@ function InfiniteCanvasPage() {
message.warning("官方即梦 CLI 当前不支持可靠取消,请等待官方状态同步");
return;
}
Modal.confirm({
modal.confirm({
title: "取消生成任务?",
content: "任务会立即停止本地执行;如果已经提交到上游,系统会继续核对取消结果和积分状态。",
okText: "取消任务",
@@ -543,7 +540,7 @@ function InfiniteCanvasPage() {
},
});
},
[bindGenerationTask, message, nodesRef, projectId, queryClient, setTaskDetail],
[bindGenerationTask, message, modal, nodesRef, projectId, queryClient, setTaskDetail],
);
useEffect(() => {
@@ -703,6 +700,28 @@ function InfiniteCanvasPage() {
setDialogNodeId,
});
const replaceCanvasNodeMedia = useCallback((node: CanvasNodeData) => handleUploadRequest(node.id), [handleUploadRequest]);
const {
timelineAddNodeRef,
timelineMediaAddRef,
assetInsertScope,
projectAssetScope,
projectAssetOpen,
projectAssetInitialCategory,
projectAssetInitialFolderId,
projectAssetInsertPosition,
handleLibraryAssetsInsert,
handleTimelineProjectAssetsInsert,
openProjectAssets,
openCanvasAssetLibrary,
openTimelineAssetLibrary,
closeProjectAssets,
} = useCanvasTimelineAssetInsert({
linkedProjectId,
refetchLinkedProject,
handleAssetsInsert,
handleProjectAssetsInsert,
openAssetsAtPosition,
});
useEffect(() => {
if (!projectLoaded || searchParams.get("mode") !== "handoff") return;
@@ -745,103 +764,6 @@ function InfiniteCanvasPage() {
});
}, [assets, assetsHydrated, handleProjectAssetsInsert, message, nodesRef, projectId, projectLoaded, searchParams, setSearchParams, updateProject]);
// 时间线弹窗内新增素材的回填通道:素材库/上传创建节点后由弹窗通过 ref 加入草稿。
const timelineAddNodeRef = useRef<((node: CanvasNodeData) => void) | null>(null);
// 时间线作用域直连媒体入轨通道:素材库/项目资产/本地上传不落画布,仅加入时间线草稿。
const timelineMediaAddRef = useRef<((media: TimelineDirectMedia) => void) | null>(null);
// 素材库与项目资产弹窗的插入作用域:时间线弹窗内打开时为 timeline,其余为 canvas。
const [assetInsertScope, setAssetInsertScope] = useState<"canvas" | "timeline">("canvas");
const [projectAssetScope, setProjectAssetScope] = useState<"canvas" | "timeline">("canvas");
// InsertAssetPayload → 直连媒体:仅音视频支持直接入轨;图片/文本/角色返回 null(避免在画布重复出现)。
const payloadToTimelineMedia = (payload: InsertAssetPayload): TimelineDirectMedia | null => {
const randomSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
if (payload.kind === "video") {
return {
id: payload.assetId || `asset-${randomSuffix}`,
kind: "video",
title: payload.title,
storageKey: payload.storageKey,
url: payload.url,
width: payload.width,
height: payload.height,
durationMs: payload.durationMs,
bytes: payload.bytes,
mimeType: payload.mimeType,
};
}
if (payload.kind === "audio") {
return { id: payload.assetId || `asset-${randomSuffix}`, kind: "audio", title: payload.title, storageKey: payload.storageKey, url: payload.url, durationMs: payload.durationMs, bytes: payload.bytes, mimeType: payload.mimeType };
}
return null;
};
const handleLibraryAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (assetInsertScope === "timeline") {
const media = payloads.map(payloadToTimelineMedia).filter((item): item is TimelineDirectMedia => Boolean(item));
if (media.length !== payloads.length) throw new Error("图片和文本素材暂不支持直接入轨,请先插入画布");
media.forEach((item) => timelineMediaAddRef.current?.(item));
return;
}
const created = await handleAssetsInsert(payloads);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[assetInsertScope, handleAssetsInsert],
);
// 项目资产库引入到时间线:复用现有引入逻辑,把创建出的节点回填到弹窗草稿。
const handleTimelineProjectAssetsInsert = useCallback(
async (payloads: InsertAssetPayload[]) => {
if (projectAssetScope === "timeline") {
let inserted = 0;
for (const payload of payloads) {
const media = payloadToTimelineMedia(payload);
if (media) {
timelineMediaAddRef.current?.(media);
inserted += 1;
}
}
if (inserted < payloads.length) message.info("图片/文本/角色素材暂不支持直接入轨,仅音视频素材已加入时间线");
return;
}
const created = await handleProjectAssetsInsert(payloads, projectAssetInsertPosition);
created.forEach((node) => timelineAddNodeRef.current?.(node));
},
[handleProjectAssetsInsert, message, projectAssetInsertPosition, projectAssetScope],
);
const openProjectAssets = useCallback(
(initialCategory = "all", position?: Position, scope: "canvas" | "timeline" = "canvas", initialFolderId = "all") => {
setProjectAssetScope(scope);
setProjectAssetInitialCategory(initialCategory);
setProjectAssetInitialFolderId(initialFolderId);
setProjectAssetInsertPosition(position);
setProjectAssetOpen(true);
// 资产与项目实时同步:打开弹窗前刷新关联短剧项目资产,避免缓存导致资产列表空白/过期。
if (linkedProjectId) void refetchLinkedProject();
},
[linkedProjectId, refetchLinkedProject],
);
// 素材库打开入口:画布作用域(工具栏/空态/侧栏)与时间线作用域(时间线弹窗)分别标记插入目标。
const openCanvasAssetLibrary = useCallback(
(position?: Position) => {
setAssetInsertScope("canvas");
openAssetsAtPosition(position);
},
[openAssetsAtPosition],
);
const openTimelineAssetLibrary = useCallback(() => {
setAssetInsertScope("timeline");
openAssetsAtPosition();
}, [openAssetsAtPosition]);
const closeProjectAssets = useCallback(() => {
setProjectAssetOpen(false);
setProjectAssetInsertPosition(undefined);
setProjectAssetInitialFolderId("all");
}, []);
const {
angleNodeId,
emotionNodeId,
@@ -1739,14 +1661,14 @@ function InfiniteCanvasPage() {
}, [arkPrivateAssetUploadNodeId, currentProject?.projectId, handleConfigNodeChange, message, projectId]);
const confirmUploadNodeImageToArkPrivateAsset = useCallback((node: CanvasNodeData) => {
Modal.confirm({
modal.confirm({
title: "上传到方舟素材库",
content: "仅可上传你拥有肖像、版权或其他合法使用权的图片。方舟审核通过后,Seedance 会使用受控素材标识生成视频。",
okText: "确认拥有使用权并上传",
cancelText: "取消",
onOk: () => uploadNodeImageToArkPrivateAsset(node),
});
}, [uploadNodeImageToArkPrivateAsset]);
}, [modal, uploadNodeImageToArkPrivateAsset]);
const handleCanvasContextMenu = useCallback(
(event: ReactMouseEvent) => {