fix: 修复多个运行时崩溃和功能bug,优化画布连接线和剧本评分

- 修复 EcommercePage generateEcommerceImage 调用不存在变量导致运行时崩溃
- 修复 DigitalHumanPage/ImageWorkbenchPage 变量名错误导致页面不可用
- 修复 ecommerceVideoService token 读取用错 key 导致请求 401
- 修复画布连接线在弹窗出现后仍跟随鼠标的问题
- 剧本评分 .docx 文件改为服务端 mammoth 解析(新增 /api/files/extract-text)
- ErrorBoundary 加 key 支持切换页面时自动重置
- Vite proxy 改为指向公网域名 omniai.net.cn
- 新增视频生成历史记录面板和删除确认弹窗

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 01:12:51 +08:00
parent 6bb71fcc19
commit 7c6129555b
12 changed files with 637 additions and 105 deletions
+26 -13
View File
@@ -21,6 +21,7 @@ const ecommerceSlide4 = `${OSS_MUBAN}/slide-4.png`;
const ecommerceSlide5 = `${OSS_MUBAN}/slide-5.png`;
import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu";
import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace";
import EcommerceVideoHistoryPanel from "./panels/EcommerceVideoHistoryPanel";
import EcommerceDetailPanel from "./panels/EcommerceDetailPanel";
import EcommerceSetPanel from "./panels/EcommerceSetPanel";
import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel";
@@ -787,6 +788,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>("detail");
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
const [videoPlanTrigger, setVideoPlanTrigger] = useState(0);
const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState<CloneBasicSelectKey | null>(null);
const [openCloneModelSelect, setOpenCloneModelSelect] = useState<CloneModelSelectKey | null>(null);
const [cloneModelSelectDropUp, setCloneModelSelectDropUp] = useState(false);
@@ -1413,7 +1416,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
pRatio: string,
pLanguage: string,
pMarket: string,
setStatusFn: (status: "generating" | "done" | "idle") => void,
setStatusFn: (status: "generating" | "done" | "idle" | "failed") => void,
setResultFn: (urls: string[]) => void,
): Promise<void> => {
setStatusFn("generating");
@@ -1486,11 +1489,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
statusFn?: (status: "generating" | "done" | "idle" | "failed") => void,
resultFn?: (results: CloneImageItem[]) => void,
): Promise<void> => {
setStatusFn("generating");
statusFn?.("generating");
try {
const referenceUrls = await uploadCloneImages(images);
if (!referenceUrls.length) {
setStatusFn("idle");
statusFn?.("idle");
return;
}
@@ -1514,22 +1517,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
});
if (resultUrl) {
setResultFn([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]);
setStatusFn("done");
resultFn?.([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]);
statusFn?.("done");
imageGen.updateTask(storeId, { status: "completed", progress: 100, resultUrl });
} else {
setStatusFn("idle");
statusFn?.("idle");
imageGen.updateTask(storeId, { status: "failed", error: "生成未返回结果" });
}
} catch (err) {
if (err instanceof ServerRequestError && err.status === 402) {
setResultFn([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]);
resultFn?.([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]);
toast.error("余额不足,请充值后继续");
} else {
const msg = err instanceof Error ? err.message : "生成失败";
toast.error(msg);
}
setStatusFn("failed");
statusFn?.("failed");
}
};
@@ -1563,10 +1566,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
});
const { waitForTask } = await import("../../api/taskSubscription");
abortRef.current = { current: false };
const resultUrl = await waitForTask(taskId, { abortRef: abortRef.current });
imageAbortRef.current = { current: false };
const resultUrl = await waitForTask(taskId, { abortRef: imageAbortRef.current });
if (resultUrl) {
setResults([{ id: crypto.randomUUID(), name: "换装视频", src: resultUrl, type: "video", size: 0 }]);
setResults([{ id: crypto.randomUUID(), src: resultUrl, label: "换装视频" }]);
}
setStatus("done");
} catch (err) {
@@ -1602,7 +1605,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
void generateEcommerceImage(
cloneOutput, productImages, requirement,
platform, ratio, language, market,
(s) => setStatus(s as ProductCloneStatus), setResults,
undefined,
(s: string) => setStatus(s as ProductCloneStatus), setResults,
);
lastFailedActionRef.current = () => handleGenerate();
}
@@ -1681,7 +1685,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
void generateEcommerceImage(
"detail", detailProductImages, detailRequirement,
detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket,
(s) => setDetailStatus(s as DetailStatus),
undefined,
(s: string) => setDetailStatus(s as DetailStatus),
(res) => setDetailResultUrl(res[0]?.src ?? null),
);
};
@@ -1905,6 +1910,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
handleGenerate={handleGenerate}
formatRatioDisplayValue={formatRatioDisplayValue}
setVideoOutfitFiles={(video, ref) => { setVideoOutfitVideoFile(video); setVideoOutfitRefFile(ref); }}
onStartVideoPlan={() => setVideoPlanTrigger((n) => n + 1)}
/>
);
@@ -2404,6 +2410,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
durationSeconds={cloneVideoDuration}
resolution={cloneVideoQuality === "standard" ? "720P" : "1080P"}
onRequestLogin={() => ((_props as Record<string, unknown>).isAuthenticated ? undefined : (window.location.hash = "#/login"))}
onOpenHistory={() => setVideoHistoryVisible(true)}
triggerPlan={videoPlanTrigger}
/>
</main>
) : cloneOutput === "video-outfit" && results.length > 0 && results[0].type === "video" ? (
@@ -2472,6 +2480,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</section>
</div>
) : null}
<EcommerceVideoHistoryPanel
visible={videoHistoryVisible}
onClose={() => setVideoHistoryVisible(false)}
/>
</section>
);
}