完善视频提炼和视频复刻完善提交测试

This commit is contained in:
Azmat@qq.com
2026-08-28 10:15:15 +08:00
parent 52c5cdae1d
commit 849b14a0f6
33 changed files with 1475 additions and 208 deletions
+49 -6
View File
@@ -21,6 +21,7 @@ import {
} from "lucide-react";
import { api, ApiError, type QuickCreateJob } from "../api";
import { ConfirmModal, OverlayPortal, useBodyScrollLock } from "../components/overlays";
import { useFileDrop } from "../components/use-file-drop";
import { forgetQuickCreateJob, readQuickCreateJobId, rememberQuickCreateJob, stripQuickCreateSuffix } from "../quick-create-lock";
import type { ModelConfig } from "../types";
import {
@@ -135,6 +136,9 @@ export function QuickCreatePage({
const [submitting, setSubmitting] = useState(false);
const [cancelling, setCancelling] = useState(false);
const [confirmCancel, setConfirmCancel] = useState(false);
// 进页面先向后端确认有没有在跑的任务。localStorage 可能被清、也可能换了浏览器,
// 只认它会让刷新后看到一张空表单,用户以为没任务又提交一次 —— 那会重复建商品、重复扣费。
const [restoring, setRestoring] = useState(true);
const [serviceUnavailable, setServiceUnavailable] = useState(false);
const [unavailableMessage, setUnavailableMessage] = useState("");
const [pollEpoch, setPollEpoch] = useState(0);
@@ -183,8 +187,19 @@ export function QuickCreatePage({
}))
.catch(() => undefined);
void api.quickCreateHistory()
.then((payload) => setHistory((payload.results || []).filter(jobIsComplete)))
.catch(() => undefined);
.then((payload) => {
setHistory((payload.results || []).filter(jobIsComplete));
const running = payload.inflight || null;
if (running) {
setJob(running);
setJobId(running.id);
rememberQuickCreateJob(running.id);
} else if (!savedJobId()) {
forgetQuickCreateJob();
}
})
.catch(() => undefined)
.finally(() => setRestoring(false));
}, []);
useEffect(() => {
@@ -325,7 +340,7 @@ export function QuickCreatePage({
setPlaying({ url, title: title || "预览视频" });
}
function selectImages(files: FileList | null) {
function selectImages(files: FileList | File[] | null) {
const selected = Array.from(files || []).filter((file) => file.type.startsWith("image/"));
setImages((current) => {
const room = Math.max(0, 9 - savedImages.length - current.length);
@@ -386,6 +401,10 @@ export function QuickCreatePage({
}
async function startGeneration() {
if (restoring) {
onNotify?.("info", "正在读取任务状态,请稍候");
return;
}
if (!name.trim() || (!images.length && !savedImages.length)) {
onNotify?.("info", "请先填写商品名称并上传商品图片");
return;
@@ -433,7 +452,16 @@ export function QuickCreatePage({
onNotify?.("success", "一键成片任务已启动");
onProjectCreated?.();
} catch (error) {
if (error instanceof ApiError && error.status === 503) {
if (error instanceof ApiError && error.status === 409) {
// 后端单飞闸:已有任务在跑。直接接上它,别让用户对着报错干瞪眼。
const running = (error.payload as { inflight?: QuickCreateJob } | undefined)?.inflight;
if (running) {
setJob(running);
setJobId(running.id);
rememberQuickCreateJob(running.id);
}
onNotify?.("info", error.message || "已有一个一键成片正在进行中");
} else if (error instanceof ApiError && error.status === 503) {
setServiceUnavailable(true);
setUnavailableMessage(error.message || "一键成片服务暂不可用,请稍后再试");
onNotify?.("info", error.message || "一键成片服务暂不可用,请稍后再试");
@@ -520,6 +548,7 @@ export function QuickCreatePage({
}
const isGenerating = submitting || job?.status === "queued" || job?.status === "running";
const imageDrop = useFileDrop((files) => selectImages(files), { disabled: isGenerating });
const isComplete = job?.status === "succeeded";
const isCancelled = job?.status === "cancelled";
const isFailed = !isGenerating && !isComplete && (job?.status === "failed" || isCancelled || serviceUnavailable);
@@ -547,6 +576,7 @@ export function QuickCreatePage({
const estimatedPoints = videoEstimate.points > 0 ? videoEstimate.points + sceneCount * 60 : sceneCount * 240;
const shellClass = [
"quick-create-shell",
restoring ? "is-restoring" : "",
isGenerating ? "is-generating" : "",
isComplete ? "is-complete" : "",
isFailed ? "is-failed" : "",
@@ -562,7 +592,11 @@ export function QuickCreatePage({
</header>
<div className={shellClass} id="quickCreateShell">
<section className="quick-create-panel quick-form-panel">
<section
className={`quick-create-panel quick-form-panel${restoring ? " is-locked" : ""}`}
aria-busy={restoring}
inert={restoring}
>
<div className="quick-form-copy">
<h2>告诉我们这是什么商品</h2>
</div>
@@ -614,7 +648,10 @@ export function QuickCreatePage({
<div className="quick-field">
<span className="quick-field-label"><span>商品图片</span><small>必填 · 最多9张</small></span>
<div className={`quick-upload${imageCount ? " has-images" : ""}`}>
<div
className={`quick-upload${imageCount ? " has-images" : ""}${imageDrop.dragging ? " is-dragover" : ""}`}
{...imageDrop.dropProps}
>
<input ref={imageInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={(event) => { selectImages(event.target.files); event.currentTarget.value = ""; }} />
{imageCount ? (
<div className="quick-upload-filled">
@@ -686,6 +723,12 @@ export function QuickCreatePage({
</section>
<section className="quick-create-panel quick-status-panel" aria-live="polite">
<div className="quick-state quick-state-restoring" role="status">
<div className="quick-restoring-spinner" aria-hidden="true" />
<h2>正在读取任务状态</h2>
<p>确认有没有正在进行的一键成片,稍候</p>
</div>
<div className="quick-state quick-state-ready">
<div className="quick-ready-orbit"><span className="quick-ready-icon"><Sparkles /></span></div>
<h2>核心参数可选,其余自动完成</h2>