perf: memoize derived render data
This commit is contained in:
@@ -578,7 +578,17 @@ function CanvasPage({
|
||||
// Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition)
|
||||
// — see useEffect below near runCanvasAutoSave
|
||||
|
||||
const canvasAssets = serverAssets.filter((asset) => asset.imageUrl);
|
||||
const canvasAssets = useMemo(
|
||||
() => serverAssets.filter((asset) => asset.imageUrl),
|
||||
[serverAssets],
|
||||
);
|
||||
const assetCountsByCategory = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const asset of serverAssets) {
|
||||
counts.set(asset.type, (counts.get(asset.type) ?? 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
}, [serverAssets]);
|
||||
const shouldShowEmptyProjectState =
|
||||
projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0;
|
||||
const isWaitingForProjects = isAuthenticated && !projectsLoaded;
|
||||
@@ -2640,10 +2650,13 @@ function CanvasPage({
|
||||
setConnectorDrag(null);
|
||||
};
|
||||
|
||||
const collapsedPackageNodeKeys = new Set(
|
||||
nodePackages.flatMap((nodePackage) =>
|
||||
nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : []
|
||||
)
|
||||
const collapsedPackageNodeKeys = useMemo(
|
||||
() => new Set(
|
||||
nodePackages.flatMap((nodePackage) =>
|
||||
nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : []
|
||||
)
|
||||
),
|
||||
[nodePackages],
|
||||
);
|
||||
const isNodeCollapsedInPackage = (kind: CanvasNodeKind, id: string) =>
|
||||
collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id }));
|
||||
@@ -2684,6 +2697,18 @@ function CanvasPage({
|
||||
return positionedLink ? [positionedLink] : [];
|
||||
}),
|
||||
].filter((link) => !isLinkCollapsedInPackage(link));
|
||||
const visibleTextNodes = useMemo(
|
||||
() => textNodes.filter((textNode) => !isNodeCollapsedInPackage("text", textNode.id)),
|
||||
[collapsedPackageNodeKeys, textNodes],
|
||||
);
|
||||
const visibleImageNodes = useMemo(
|
||||
() => imageNodes.filter((imageNode) => !isNodeCollapsedInPackage("image", imageNode.id)),
|
||||
[collapsedPackageNodeKeys, imageNodes],
|
||||
);
|
||||
const visibleVideoNodes = useMemo(
|
||||
() => videoNodes.filter((videoNode) => !isNodeCollapsedInPackage("video", videoNode.id)),
|
||||
[collapsedPackageNodeKeys, videoNodes],
|
||||
);
|
||||
const pendingLinkPreview =
|
||||
pendingLinkPort && pendingLinkPreviewPoint
|
||||
? (() => {
|
||||
@@ -4002,7 +4027,7 @@ function CanvasPage({
|
||||
) : null}
|
||||
</svg>
|
||||
) : null}
|
||||
{textNodes.filter((textNode) => !isNodeCollapsedInPackage("text", textNode.id)).map((textNode) => {
|
||||
{visibleTextNodes.map((textNode) => {
|
||||
const textNodeSelected = isSelectedNode("text", textNode.id);
|
||||
const textNodeActive = isActiveSelectedNode("text", textNode.id);
|
||||
const textNodeResizing = nodeResizeDrag?.kind === "text" && nodeResizeDrag.nodeId === textNode.id;
|
||||
@@ -4270,7 +4295,7 @@ function CanvasPage({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{imageNodes.filter((imageNode) => !isNodeCollapsedInPackage("image", imageNode.id)).map((imageNode) => {
|
||||
{visibleImageNodes.map((imageNode) => {
|
||||
const imageNodeSelected = isSelectedNode("image", imageNode.id);
|
||||
const imageNodeActive = isActiveSelectedNode("image", imageNode.id);
|
||||
const imageNodeResizing = nodeResizeDrag?.kind === "image" && nodeResizeDrag.nodeId === imageNode.id;
|
||||
@@ -4774,7 +4799,7 @@ function CanvasPage({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{videoNodes.filter((videoNode) => !isNodeCollapsedInPackage("video", videoNode.id)).map((videoNode) => {
|
||||
{visibleVideoNodes.map((videoNode) => {
|
||||
const videoNodeSelected = isSelectedNode("video", videoNode.id);
|
||||
const videoNodeActive = isActiveSelectedNode("video", videoNode.id);
|
||||
const videoNodeResizing = nodeResizeDrag?.kind === "video" && nodeResizeDrag.nodeId === videoNode.id;
|
||||
@@ -5485,7 +5510,7 @@ function CanvasPage({
|
||||
onClick={() => setSelectedExistingCategory(category.key)}
|
||||
>
|
||||
{category.label}
|
||||
<span>{serverAssets.filter((asset) => asset.type === category.key).length} 个素材</span>
|
||||
<span>{assetCountsByCategory.get(category.key) ?? 0} 个素材</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -901,25 +901,58 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds);
|
||||
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
|
||||
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
|
||||
const productSetRatioOptions = getPlatformRatioOptions(productSetPlatform, productSetOutput);
|
||||
const hotUploadedRatioOption = cloneOutput === "hot" ? formatUploadedImageRatio(cloneReferenceImages[0]) : null;
|
||||
const baseCloneRatioOptions = getPlatformRatioOptions(platform, cloneOutput);
|
||||
const cloneRatioOptions = hotUploadedRatioOption
|
||||
? getUniqueRatioOptions([...baseCloneRatioOptions, hotUploadedRatioOption])
|
||||
: baseCloneRatioOptions;
|
||||
const productSetLanguageOptions = getPlatformLanguageOptions(productSetPlatform, productSetMarket);
|
||||
const cloneLanguageOptions = getPlatformLanguageOptions(platform, market);
|
||||
const detailLanguageOptions = getPlatformLanguageOptions(detailPlatform, detailMarket);
|
||||
const productSetRatioOptions = useMemo(
|
||||
() => getPlatformRatioOptions(productSetPlatform, productSetOutput),
|
||||
[productSetOutput, productSetPlatform],
|
||||
);
|
||||
const hotUploadedRatioOption = useMemo(
|
||||
() => cloneOutput === "hot" ? formatUploadedImageRatio(cloneReferenceImages[0]) : null,
|
||||
[cloneOutput, cloneReferenceImages],
|
||||
);
|
||||
const baseCloneRatioOptions = useMemo(
|
||||
() => getPlatformRatioOptions(platform, cloneOutput),
|
||||
[cloneOutput, platform],
|
||||
);
|
||||
const cloneRatioOptions = useMemo(
|
||||
() => hotUploadedRatioOption
|
||||
? getUniqueRatioOptions([...baseCloneRatioOptions, hotUploadedRatioOption])
|
||||
: baseCloneRatioOptions,
|
||||
[baseCloneRatioOptions, hotUploadedRatioOption],
|
||||
);
|
||||
const productSetLanguageOptions = useMemo(
|
||||
() => getPlatformLanguageOptions(productSetPlatform, productSetMarket),
|
||||
[productSetMarket, productSetPlatform],
|
||||
);
|
||||
const cloneLanguageOptions = useMemo(
|
||||
() => getPlatformLanguageOptions(platform, market),
|
||||
[market, platform],
|
||||
);
|
||||
const detailLanguageOptions = useMemo(
|
||||
() => getPlatformLanguageOptions(detailPlatform, detailMarket),
|
||||
[detailMarket, detailPlatform],
|
||||
);
|
||||
const ecommerceMentionImages: MentionImageOption[] = [
|
||||
...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })),
|
||||
...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })),
|
||||
];
|
||||
|
||||
const ecommerceVideoImageDataUrls = useMemo(
|
||||
() => productImages.map((img) => img.src),
|
||||
[productImages],
|
||||
);
|
||||
const ecommerceVideoImageFiles = useMemo(
|
||||
() => productImages.map((img) => img.file),
|
||||
[productImages],
|
||||
);
|
||||
|
||||
const selectedProductSetOutput =
|
||||
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
|
||||
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
|
||||
const productSetPreviewReady = productSetStatus === "done";
|
||||
const cloneSetTotal = Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0);
|
||||
const cloneSetTotal = useMemo(
|
||||
() => Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0),
|
||||
[cloneSetCounts],
|
||||
);
|
||||
const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating";
|
||||
const canGenerate = (cloneOutput === "video-outfit"
|
||||
? Boolean(videoOutfitVideoFile && videoOutfitRefFile)
|
||||
@@ -928,9 +961,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const canGenerateDetail = detailProductImages.length > 0 && detailStatus !== "generating";
|
||||
const cloneVideoDurationProgress =
|
||||
((cloneVideoDuration - cloneVideoDurationMin) / (cloneVideoDurationMax - cloneVideoDurationMin)) * 100;
|
||||
const cloneVideoDurationStyle: CSSProperties = {
|
||||
"--clone-video-duration-progress": `${cloneVideoDurationProgress}%`,
|
||||
} as CSSProperties;
|
||||
const cloneVideoDurationStyle: CSSProperties = useMemo(
|
||||
() => ({
|
||||
"--clone-video-duration-progress": `${cloneVideoDurationProgress}%`,
|
||||
}) as CSSProperties,
|
||||
[cloneVideoDurationProgress],
|
||||
);
|
||||
|
||||
const trackEcommerceTask = (taskId: string) => {
|
||||
activeEcommerceTaskIdsRef.current.add(taskId);
|
||||
@@ -2647,8 +2683,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<main className="product-clone-preview product-clone-preview--video" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<EcommerceVideoWorkspace
|
||||
isAuthenticated={Boolean((_props as Record<string, unknown>).isAuthenticated)}
|
||||
productImageDataUrls={productImages.map((img) => img.src)}
|
||||
productImageFiles={productImages.map((img) => img.file)}
|
||||
productImageDataUrls={ecommerceVideoImageDataUrls}
|
||||
productImageFiles={ecommerceVideoImageFiles}
|
||||
requirement={requirement}
|
||||
platform={platform}
|
||||
aspectRatio={ratio.includes("9:16") || ratio.includes("9:16") ? "9:16" : ratio.includes("16:9") || ratio.includes("16:9") ? "16:9" : ratio.includes("3:4") || ratio.includes("3:4") ? "3:4" : "9:16"}
|
||||
|
||||
Reference in New Issue
Block a user