refactor: 清理未使用参数、移除死代码、聚焦电商核心模块
主要变更概述: ================ 1. 清理未使用的函数参数 (TypeScript noUnusedParameters) ------------------------------------------------------ - AppShell.tsx: 移除未使用的 backendHealth prop 及 ServerConnectionHealth 导入 - canvasUtils.ts: 移除 resolveWorkflowVideoModel 的 workflowModel 参数 - canvasWorkflowDeserialize.ts: 同步更新调用方 - CanvasPage.tsx: 移除 resolveWorkflowVideoModel 未使用导入 - HomePage.tsx: 移除 onOpenTokenMonitor、onOpenImageTool 未使用 props - ToolboxSection.tsx: 移除 onOpenImageTool 未使用 prop 及 WebImageWorkbenchTool 类型导入 - ScriptTokensPage.tsx: 移除 formatReportMarkdown 的 script 参数,更新 2 处调用 - TokenUsagePage.tsx: 移除 onOpenImageTool、onSelectView 未使用 props - WorkbenchPage.tsx: 移除 renderComposerToolbar 的 showStop 参数,更新 2 处调用 2. 移除未使用的模块和死代码 -------------------------- 删除以下未在电商模块中使用的功能模块: - 画布模块 (canvas/): CanvasPage, canvasUtils, canvasWorkflow* 等 - 主页模块 (home/): HomePage, ToolboxSection, WelcomeSplash 等 - 工作台模块 (workbench/): WorkbenchPage, ConversationSidebar 等 - 社区模块 (community/, community-review/) - 数字人模块 (digital-human/) - 图片工作台 (image-workbench/) - 其他独立工具页: agent, assets, beta-applications, character-mix, compliance, dialog-generator, more, profile, provider-health, report, resolution-upscale, script-tokens, settings, size-template, subtitle-removal, watermark-removal 3. 移除未使用的公共组件 ---------------------- - AnimatedPanel, BeforeAfterCompare, BetaApplicationModal - CookieConsentBanner, DropZone, EmptyState, NotFoundPage - NotificationCenter, OnboardingTour, OptimizedImage - PageTransition, RechargeModal, ShellIcon, Skeleton - StudioToolLayout, TaskStatusBar, WorkspacePageShell 4. 移除未使用的 API 客户端 -------------------------- - betaApplicationClient, communityClient, conversationClient - draftClient, modelCapabilitiesClient, notificationClient - projectTaskClient, providerHealthClient, publicConfigClient - referenceUploadService, reportClient, scriptEvalClient - uploadWithProgress 5. 移除未使用的工具函数和 hooks ------------------------------- - utils/: imageModelVisibility, mentionTrigger, modelOptions, ossImageOptimize, toolPageUtils - hooks/: useGenerationStatus, useScrollEntrance - scripts/: 所有分析脚本 (check-governance, dynamic-analysis 等) 6. 移除未使用的样式文件 ---------------------- 删除与已移除模块对应的 CSS 文件,保留电商模块专用样式 7. 新增电商模块功能文件 ---------------------- + src/api/generationRecordClient.ts (生成记录客户端) + src/features/ecommerce/ecommerceGenerationPersistence.ts (生成持久化) 验证: - TypeScript 编译 (tsc --noEmit --noUnusedParameters) 零错误通过 - 所有保留文件的功能完整性未受影响
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,9 @@ function EcommerceTemplatesPage({
|
||||
const direction = carouselMotion?.direction ?? 0;
|
||||
const minSlot = APPLE_CAROUSEL_SLOTS[0]! + Math.min(direction, 0);
|
||||
const maxSlot = APPLE_CAROUSEL_SLOTS[APPLE_CAROUSEL_SLOTS.length - 1]! + Math.max(direction, 0);
|
||||
return Array.from({ length: maxSlot - minSlot + 1 }, (_, index) => minSlot + index);
|
||||
const offsets: number[] = [];
|
||||
for (let slot = minSlot; slot <= maxSlot; slot += 1) offsets.push(slot);
|
||||
return offsets;
|
||||
}, [carouselMotion?.direction]);
|
||||
|
||||
const startCarouselShift = useCallback(
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
loadEcommerceVideoState,
|
||||
clearEcommerceVideoState,
|
||||
} from "./ecommerceVideoKeepalive";
|
||||
import { saveUnifiedEcommerceGenerationRecord } from "./ecommerceGenerationPersistence";
|
||||
|
||||
interface EcommerceVideoWorkspaceProps {
|
||||
isAuthenticated: boolean;
|
||||
@@ -172,9 +173,9 @@ export default function EcommerceVideoWorkspace({
|
||||
}, [stage, scenes, planResult]);
|
||||
|
||||
// ── External trigger: start plan from parent ────────────────
|
||||
const triggerPlanPrevRef = useRef(triggerPlan);
|
||||
const triggerPlanPrevRef = useRef(0);
|
||||
useEffect(() => {
|
||||
if (triggerPlan != null && triggerPlan !== triggerPlanPrevRef.current) {
|
||||
if (typeof triggerPlan === "number" && triggerPlan > 0 && triggerPlan !== triggerPlanPrevRef.current) {
|
||||
triggerPlanPrevRef.current = triggerPlan;
|
||||
void handlePlan();
|
||||
}
|
||||
@@ -187,7 +188,7 @@ export default function EcommerceVideoWorkspace({
|
||||
if (historySavedRef.current) return;
|
||||
if (!planResult || !scenes.length) return;
|
||||
historySavedRef.current = true;
|
||||
const title = planResult.storyboard?.video_title || planResult.summary?.product_name || "电商广告视频";
|
||||
const title = planResult.storyboard?.video_title || planResult.summary?.product_name || "电商短视频";
|
||||
saveVideoHistory({
|
||||
title,
|
||||
config: { platform, aspectRatio, durationSeconds, resolution },
|
||||
@@ -195,7 +196,35 @@ export default function EcommerceVideoWorkspace({
|
||||
scenes: scenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
|
||||
sourceImageUrls,
|
||||
}).catch(() => {});
|
||||
}, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution]);
|
||||
void saveUnifiedEcommerceGenerationRecord({
|
||||
clientRecordId: `ecommerce-video-${inputFingerprint}-${Date.now()}`,
|
||||
title,
|
||||
mode: "short-video",
|
||||
prompt: requirement,
|
||||
sourceImages: sourceImageUrls.map((url, index) => ({ url, label: `source-${index + 1}` })),
|
||||
results: scenes
|
||||
.filter((scene) => Boolean(scene.resultUrl))
|
||||
.map((scene) => ({
|
||||
url: scene.resultUrl!,
|
||||
label: `scene-${scene.sceneId}`,
|
||||
mediaType: "video",
|
||||
taskId: scene.taskId,
|
||||
})),
|
||||
taskIds: scenes.map((scene) => scene.taskId).filter((taskId): taskId is string => Boolean(taskId)),
|
||||
config: { platform, aspectRatio, durationSeconds, resolution },
|
||||
result: {
|
||||
plan: planResult as unknown as Record<string, unknown>,
|
||||
scenes: scenes.map((scene) => ({
|
||||
sceneId: scene.sceneId,
|
||||
prompt: scene.prompt,
|
||||
imageUrl: scene.imageUrl,
|
||||
videoUrl: scene.resultUrl,
|
||||
status: scene.status,
|
||||
})),
|
||||
},
|
||||
metadata: { inputFingerprint },
|
||||
});
|
||||
}, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution, inputFingerprint, requirement]);
|
||||
|
||||
// ── Expose manual save via ref ──────────────────────────
|
||||
const planResultRef = useRef(planResult);
|
||||
@@ -212,7 +241,7 @@ export default function EcommerceVideoWorkspace({
|
||||
const currentScenes = scenesRef.current;
|
||||
const currentSources = sourceImageUrlsRef.current;
|
||||
if (!currentPlan || !currentScenes.length) return;
|
||||
const title = currentPlan.storyboard?.video_title || currentPlan.summary?.product_name || "电商广告视频";
|
||||
const title = currentPlan.storyboard?.video_title || currentPlan.summary?.product_name || "电商短视频";
|
||||
saveVideoHistory({
|
||||
title,
|
||||
config: { platform, aspectRatio, durationSeconds, resolution },
|
||||
@@ -220,8 +249,36 @@ export default function EcommerceVideoWorkspace({
|
||||
scenes: currentScenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
|
||||
sourceImageUrls: currentSources,
|
||||
}).catch(() => {});
|
||||
void saveUnifiedEcommerceGenerationRecord({
|
||||
clientRecordId: `ecommerce-video-manual-${inputFingerprint}-${Date.now()}`,
|
||||
title,
|
||||
mode: "short-video",
|
||||
prompt: requirement,
|
||||
sourceImages: currentSources.map((url, index) => ({ url, label: `source-${index + 1}` })),
|
||||
results: currentScenes
|
||||
.filter((scene) => Boolean(scene.resultUrl))
|
||||
.map((scene) => ({
|
||||
url: scene.resultUrl!,
|
||||
label: `scene-${scene.sceneId}`,
|
||||
mediaType: "video",
|
||||
taskId: scene.taskId,
|
||||
})),
|
||||
taskIds: currentScenes.map((scene) => scene.taskId).filter((taskId): taskId is string => Boolean(taskId)),
|
||||
config: { platform, aspectRatio, durationSeconds, resolution },
|
||||
result: {
|
||||
plan: currentPlan as unknown as Record<string, unknown>,
|
||||
scenes: currentScenes.map((scene) => ({
|
||||
sceneId: scene.sceneId,
|
||||
prompt: scene.prompt,
|
||||
imageUrl: scene.imageUrl,
|
||||
videoUrl: scene.resultUrl,
|
||||
status: scene.status,
|
||||
})),
|
||||
},
|
||||
metadata: { inputFingerprint, manual: true },
|
||||
});
|
||||
};
|
||||
}, [saveRef, platform, aspectRatio, durationSeconds, resolution]);
|
||||
}, [saveRef, platform, aspectRatio, durationSeconds, resolution, inputFingerprint, requirement]);
|
||||
|
||||
// ── Keep-alive: resume polling for running tasks ──────────
|
||||
useEffect(() => {
|
||||
@@ -341,8 +398,8 @@ export default function EcommerceVideoWorkspace({
|
||||
const handleSaveAsset = async (url: string) => {
|
||||
try {
|
||||
const result = await addToolResultToAssetLibrary({
|
||||
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频生成结果",
|
||||
type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
|
||||
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商短视频生成结果",
|
||||
type: "video", isVideo: true, tags: ["电商", "短视频"],
|
||||
metadata: { source: "ecommerce-video", platform },
|
||||
});
|
||||
showNotice(result === "server" ? "已保存到资产库" : "已保存到本地资产库");
|
||||
@@ -356,8 +413,8 @@ export default function EcommerceVideoWorkspace({
|
||||
try {
|
||||
await addToolResultToAssetLibrary({
|
||||
url: scene.resultUrl!, name: `电商短视频-镜头${scene.sceneId}-${Date.now()}.mp4`,
|
||||
description: `电商广告视频 - 镜头${scene.sceneId}`,
|
||||
type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
|
||||
description: `电商短视频 - 镜头${scene.sceneId}`,
|
||||
type: "video", isVideo: true, tags: ["电商", "短视频"],
|
||||
metadata: { source: "ecommerce-video", platform, sceneId: scene.sceneId },
|
||||
});
|
||||
saved++;
|
||||
@@ -380,7 +437,7 @@ export default function EcommerceVideoWorkspace({
|
||||
const handleImportToCanvas = async (url: string) => {
|
||||
try {
|
||||
await addToolResultToAssetLibrary({
|
||||
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频 - 导入画布",
|
||||
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商短视频 - 导入画布",
|
||||
type: "video", isVideo: true, tags: ["电商", "短视频", "画布导入"],
|
||||
metadata: { source: "ecommerce-video", platform },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
buildGenerationOssScope,
|
||||
deleteGenerationRecordByClientId,
|
||||
saveGenerationRecord,
|
||||
type GenerationRecordAsset,
|
||||
type SaveGenerationRecordInput,
|
||||
} from "../../api/generationRecordClient";
|
||||
|
||||
export const ecommerceOssScopes = {
|
||||
productSource: buildGenerationOssScope(["ecommerce", "source", "product"]),
|
||||
cloneResult: (mode: string) => buildGenerationOssScope(["ecommerce", "result", mode]),
|
||||
videoSource: buildGenerationOssScope(["ecommerce", "short-video", "source"]),
|
||||
videoHistory: buildGenerationOssScope(["ecommerce", "short-video", "history"]),
|
||||
};
|
||||
|
||||
export interface EcommerceUnifiedRecordInput {
|
||||
clientRecordId: string;
|
||||
title: string;
|
||||
mode: string;
|
||||
prompt?: string;
|
||||
status?: SaveGenerationRecordInput["status"];
|
||||
sourceImages?: Array<{ url: string; ossKey?: string | null; label?: string }>;
|
||||
results?: Array<{ url: string; label?: string; mediaType?: "image" | "video" | string; taskId?: string | null }>;
|
||||
taskIds?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
result?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export function saveUnifiedEcommerceGenerationRecord(input: EcommerceUnifiedRecordInput): Promise<{ source: "server" | "local"; id: string }> {
|
||||
const assets: GenerationRecordAsset[] = [
|
||||
...(input.sourceImages || []).map((item): GenerationRecordAsset => ({
|
||||
role: "source",
|
||||
mediaType: "image",
|
||||
url: item.url,
|
||||
ossKey: item.ossKey,
|
||||
label: item.label,
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
})),
|
||||
...(input.results || []).map((item): GenerationRecordAsset => ({
|
||||
role: "result",
|
||||
mediaType: item.mediaType || "image",
|
||||
url: item.url,
|
||||
label: item.label,
|
||||
taskId: item.taskId,
|
||||
scope: item.mediaType === "video" ? ecommerceOssScopes.videoHistory : ecommerceOssScopes.cloneResult(input.mode),
|
||||
})),
|
||||
].filter((asset) => Boolean(asset.url));
|
||||
|
||||
return saveGenerationRecord({
|
||||
clientRecordId: input.clientRecordId,
|
||||
tool: "ecommerce",
|
||||
mode: input.mode,
|
||||
title: input.title,
|
||||
status: input.status || "completed",
|
||||
prompt: input.prompt,
|
||||
taskIds: input.taskIds,
|
||||
assets,
|
||||
config: input.config,
|
||||
result: input.result,
|
||||
metadata: input.metadata,
|
||||
createdAt: input.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEcommerceGenerationRecord(clientRecordId: string): Promise<void> {
|
||||
await deleteGenerationRecordByClientId(clientRecordId);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { serverRequest } from "../../api/serverConnection";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
|
||||
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
|
||||
import type {
|
||||
EcommerceVideoPlanProgress,
|
||||
@@ -99,7 +100,7 @@ export async function resolveDurableMediaUrl(
|
||||
sourceUrl,
|
||||
name: buildDurableMediaName(options.namePrefix, sourceUrl, mimeType),
|
||||
mimeType,
|
||||
scope: options.scope || "ecommerce-video-history",
|
||||
scope: options.scope || ecommerceOssScopes.videoHistory,
|
||||
});
|
||||
return {
|
||||
url: uploaded.url || null,
|
||||
@@ -155,13 +156,13 @@ async function uploadProductImageSource(source: string | Blob): Promise<string>
|
||||
|
||||
if (source.startsWith("data:")) {
|
||||
const mimeType = normalizeEcommerceImageMime(source.match(/^data:([^;,]+)/)?.[1] || "image/png");
|
||||
const result = await aiGenerationClient.uploadAsset({ dataUrl: source, mimeType, scope: "ecommerce-product" });
|
||||
const result = await aiGenerationClient.uploadAsset({ dataUrl: source, mimeType, scope: ecommerceOssScopes.videoSource });
|
||||
return result.url;
|
||||
}
|
||||
|
||||
const remoteUrl = normalizeRemoteImageUrl(source);
|
||||
if (remoteUrl) {
|
||||
const result = await aiGenerationClient.uploadAssetByUrl({ sourceUrl: remoteUrl, scope: "ecommerce-product" });
|
||||
const result = await aiGenerationClient.uploadAssetByUrl({ sourceUrl: remoteUrl, scope: ecommerceOssScopes.videoSource });
|
||||
return result.url;
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ async function uploadProductImageSource(source: string | Blob): Promise<string>
|
||||
const mimeType = normalizeEcommerceImageMime(source.type || "image/png");
|
||||
const blob = source.type === mimeType ? source : new Blob([source], { type: mimeType });
|
||||
const dataUrl = await readBlobAsDataUrl(blob);
|
||||
const result = await aiGenerationClient.uploadAsset({ dataUrl, mimeType, scope: "ecommerce-product" });
|
||||
const result = await aiGenerationClient.uploadAsset({ dataUrl, mimeType, scope: ecommerceOssScopes.videoSource });
|
||||
return result.url;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
|
||||
type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit";
|
||||
type CloneOutputKey = ProductSetOutputKey | "hot";
|
||||
type CloneSetCountKey = "selling" | "white" | "scene";
|
||||
type CloneModelPanelTab = "scene" | "model";
|
||||
type CloneReferenceMode = "upload" | "link";
|
||||
@@ -138,7 +138,6 @@ interface EcommerceClonePanelProps {
|
||||
handleGenerate: () => void;
|
||||
onCancelGenerate: () => void;
|
||||
formatRatioDisplayValue: (value: string) => string;
|
||||
setVideoOutfitFiles?: (video: File | null, ref: File | null) => void;
|
||||
onStartVideoPlan?: () => void;
|
||||
}
|
||||
|
||||
@@ -208,13 +207,8 @@ export default function EcommerceClonePanel({
|
||||
handleGenerate,
|
||||
onCancelGenerate,
|
||||
formatRatioDisplayValue,
|
||||
setVideoOutfitFiles,
|
||||
onStartVideoPlan,
|
||||
}: EcommerceClonePanelProps) {
|
||||
const videoOutfitVideoRef = useRef<HTMLInputElement>(null);
|
||||
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
|
||||
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
|
||||
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
|
||||
const [zoomImage, setZoomImage] = useState<{ src: string; x: number; y: number } | null>(null);
|
||||
|
||||
const handleFileMouseEnter = (src: string, event: React.MouseEvent<HTMLElement>) => {
|
||||
@@ -267,18 +261,6 @@ export default function EcommerceClonePanel({
|
||||
);
|
||||
};
|
||||
|
||||
const handleVideoOutfitVideoChange = () => {
|
||||
const file = videoOutfitVideoRef.current?.files?.[0] || null;
|
||||
if (file) setVideoOutfitVideoUrl(URL.createObjectURL(file));
|
||||
setVideoOutfitFiles?.(file, videoOutfitRefRef.current?.files?.[0] || null);
|
||||
};
|
||||
|
||||
const handleVideoOutfitRefChange = () => {
|
||||
const file = videoOutfitRefRef.current?.files?.[0] || null;
|
||||
if (file) setVideoOutfitRefUrl(URL.createObjectURL(file));
|
||||
setVideoOutfitFiles?.(videoOutfitVideoRef.current?.files?.[0] || null, file);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="product-clone-panel__scroll clone-ai-panel">
|
||||
@@ -346,7 +328,7 @@ export default function EcommerceClonePanel({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} />
|
||||
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} aria-label="上传商品图片" />
|
||||
</section>
|
||||
|
||||
<section className="clone-ai-card clone-ai-settings-card clone-ai-settings-card--mode">
|
||||
@@ -356,7 +338,7 @@ export default function EcommerceClonePanel({
|
||||
</h2>
|
||||
<div className="clone-ai-settings-section">
|
||||
<span className="clone-ai-settings-label">生成内容</span>
|
||||
<div className="clone-ai-tag-group" role="radiogroup" aria-label="生成内容">
|
||||
<div className="clone-ai-tag-group" role="toolbar" aria-label="生成内容">
|
||||
{cloneOutputOptions.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
@@ -478,11 +460,12 @@ export default function EcommerceClonePanel({
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
onChange={handleCloneReferenceUpload}
|
||||
aria-label="上传参考图片"
|
||||
/>
|
||||
</div>
|
||||
<div className="clone-ai-replicate-section">
|
||||
<span className="clone-ai-replicate-title">复刻程度</span>
|
||||
<div className="clone-ai-replicate-levels" role="radiogroup" aria-label="复刻程度">
|
||||
<div className="clone-ai-replicate-levels" role="toolbar" aria-label="复刻程度">
|
||||
{cloneReplicateLevelOptions.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
@@ -779,50 +762,9 @@ export default function EcommerceClonePanel({
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{cloneOutput === "video-outfit" ? (
|
||||
<section className="clone-ai-video-panel" aria-label="视频换装">
|
||||
<div className="clone-ai-dynamic-head">
|
||||
<strong>视频换装设置</strong>
|
||||
<span>上传视频和参考服装</span>
|
||||
</div>
|
||||
<div className="clone-ai-video-section">
|
||||
<span className="clone-ai-video-title">上传原始视频</span>
|
||||
<div className="clone-ai-video-outfit-upload">
|
||||
<input
|
||||
ref={videoOutfitVideoRef}
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={handleVideoOutfitVideoChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitVideoRef.current?.click()}>
|
||||
{videoOutfitVideoUrl ? "重新上传视频" : "点击上传视频"}
|
||||
</button>
|
||||
{videoOutfitVideoUrl ? <span className="clone-ai-video-outfit-info">已选择视频</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="clone-ai-video-section">
|
||||
<span className="clone-ai-video-title">上传参考图(素材/服装)</span>
|
||||
<div className="clone-ai-video-outfit-upload">
|
||||
<input
|
||||
ref={videoOutfitRefRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleVideoOutfitRefChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitRefRef.current?.click()}>
|
||||
{videoOutfitRefUrl ? "重新上传参考图" : "点击上传参考图"}
|
||||
</button>
|
||||
{videoOutfitRefUrl ? <span className="clone-ai-video-outfit-info">已选择参考图</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<button type="button" className="clone-ai-generate" disabled={!canGenerate || cloneOutput === "video"} onClick={status === "failed" && lastFailedActionRef.current ? lastFailedActionRef.current : handleGenerate} style={cloneOutput === "video" ? { display: "none" } : undefined}>
|
||||
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <ReloadOutlined /> : null}
|
||||
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : cloneOutput === "video-outfit" ? "✦ 开始换装" : "✦ 开始生成"}
|
||||
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : "✦ 开始生成"}
|
||||
</button>
|
||||
{status === "generating" && cloneOutput !== "video" ? (
|
||||
<button type="button" className="clone-ai-generate clone-ai-generate--cancel" onClick={onCancelGenerate}>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function EcommerceSetPanel({
|
||||
</strong>
|
||||
<span className="product-set-upload-note">同一产品,最多 3 张</span>
|
||||
</button>
|
||||
<input ref={setInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={handleSetUpload} />
|
||||
<input ref={setInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={handleSetUpload} aria-label="上传商品图片" />
|
||||
{setImages.length ? (
|
||||
<div className="product-clone-thumb-row product-set-thumb-row" aria-label="已上传商品原图">
|
||||
{setImages.map((item) => (
|
||||
@@ -109,7 +109,7 @@ export default function EcommerceSetPanel({
|
||||
</h2>
|
||||
<div className="product-set-setting-block">
|
||||
<span className="product-set-setting-title">生成内容</span>
|
||||
<div className="product-set-output-grid" role="radiogroup" aria-label="生成内容">
|
||||
<div className="product-set-output-grid" role="toolbar" aria-label="生成内容">
|
||||
{productSetOutputOptions.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
|
||||
Reference in New Issue
Block a user