fix(#1197): Prettier格式对齐 + 预览生成请求ID竞态防护
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 10s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m6s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m10s
AI Code Review / AI Code Review (pull_request) Successful in 1m46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m47s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 26s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m27s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m49s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1151h38m48s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1151h38m50s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 1151h38m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1151h38m50s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1151h38m50s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1151h40m4s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1151h40m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1151h40m16s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1151h41m16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 1151h40m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1151h43m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1151h43m32s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1152h11m58s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1152h11m59s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1152h13m14s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 1152h13m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1152h16m40s

This commit is contained in:
2026-07-30 15:42:35 +08:00
parent e8b210504e
commit 72f4be0b3c
@@ -3,33 +3,33 @@
* 封装预览信息计算 + 预览生成逻辑
* TODO: 后端预览接口对齐后,替换 mock 逻辑为真实 API 调用
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
import type { EditingTemplate } from "@/api/editing-planner";
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import type { EditingTemplate } from "@/api/editing-planner"
interface UseStep3PreviewProps {
templates: EditingTemplate[];
selectedTemplate: string;
materialMode: "manual" | "auto";
selectedMaterials: string[];
smartSelectedIds: string[];
duration: number;
videoRatio: string;
templates: EditingTemplate[]
selectedTemplate: string
materialMode: "manual" | "auto"
selectedMaterials: string[]
smartSelectedIds: string[]
duration: number
videoRatio: string
}
export type PreviewStatus = "idle" | "generating" | "ready" | "error";
export type PreviewStatus = "idle" | "generating" | "ready" | "error"
/** 预览生成结果 */
export interface PreviewResult {
/** 预计片段数 */
clipCount: number;
clipCount: number
/** 预计转场数 */
transitionCount: number;
transitionCount: number
/** 素材使用率 */
materialUsage: number;
materialUsage: number
/** 预览视频URL(后端接口出来后填充) */
previewVideoUrl: string | null;
previewVideoUrl: string | null
/** 生成耗时(秒) */
generateDuration: number;
generateDuration: number
}
export function useStep3Preview({
@@ -44,26 +44,24 @@ export function useStep3Preview({
const templateName = useMemo(
() => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择",
[templates, selectedTemplate],
);
)
const materialCount = useMemo(() => {
if (materialMode === "auto") {
return `${smartSelectedIds.length} 个素材(智能匹配)`;
return `${smartSelectedIds.length} 个素材(智能匹配)`
}
return `${selectedMaterials.length} 个素材`;
}, [materialMode, selectedMaterials.length, smartSelectedIds.length]);
return `${selectedMaterials.length} 个素材`
}, [materialMode, selectedMaterials.length, smartSelectedIds.length])
const materialTotal =
materialMode === "auto"
? smartSelectedIds.length
: selectedMaterials.length;
const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length
/* ── 预览生成状态 ── */
const [previewStatus, setPreviewStatus] = useState<PreviewStatus>("idle");
const [previewResult, setPreviewResult] = useState<PreviewResult | null>(
null,
);
const [previewError, setPreviewError] = useState<string>("");
const [previewStatus, setPreviewStatus] = useState<PreviewStatus>("idle")
const [previewResult, setPreviewResult] = useState<PreviewResult | null>(null)
const [previewError, setPreviewError] = useState<string>("")
// 请求 ID 计数器,用于防止竞态条件(旧请求覆盖新状态)
const requestIdRef = useRef(0)
/* ── 关键参数变化时重置预览状态(避免旧数据预览被误用) ── */
const prevDepsRef = useRef({
@@ -73,7 +71,7 @@ export function useStep3Preview({
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
});
})
useEffect(() => {
const currentKey = [
@@ -83,7 +81,7 @@ export function useStep3Preview({
[...smartSelectedIds].sort().join(","),
duration,
videoRatio,
].join("|");
].join("|")
const prevKey = [
prevDepsRef.current.selectedTemplate,
@@ -92,12 +90,14 @@ export function useStep3Preview({
prevDepsRef.current.smartSelectedIds,
prevDepsRef.current.duration,
prevDepsRef.current.videoRatio,
].join("|");
].join("|")
if (prevKey !== currentKey && previewStatus !== "idle") {
setPreviewStatus("idle");
setPreviewResult(null);
setPreviewError("");
// 参数变化时递增请求 ID,作废之前进行中的预览生成
requestIdRef.current += 1
setPreviewStatus("idle")
setPreviewResult(null)
setPreviewError("")
}
prevDepsRef.current = {
@@ -107,7 +107,7 @@ export function useStep3Preview({
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
};
}
}, [
selectedTemplate,
materialMode,
@@ -116,7 +116,7 @@ export function useStep3Preview({
duration,
videoRatio,
previewStatus,
]);
])
/**
* 生成预览
@@ -127,26 +127,33 @@ export function useStep3Preview({
*/
const generatePreview = useCallback(async () => {
if (!selectedTemplate) {
setPreviewError("请先选择模板");
setPreviewStatus("error");
return;
setPreviewError("请先选择模板")
setPreviewStatus("error")
return
}
if (materialTotal === 0) {
setPreviewError("请先选择素材");
setPreviewStatus("error");
return;
setPreviewError("请先选择素材")
setPreviewStatus("error")
return
}
setPreviewStatus("generating");
setPreviewError("");
// 生成新的请求 ID,用于竞态检查
const requestId = requestIdRef.current + 1
requestIdRef.current = requestId
setPreviewStatus("generating")
setPreviewError("")
try {
// TODO: 替换为真实后端接口调用
// const res = await generatePreviewAPI({ templateId: selectedTemplate, ... })
await new Promise((resolve) => setTimeout(resolve, 1500));
await new Promise((resolve) => setTimeout(resolve, 1500))
// 竞态检查:如果请求已被新请求或参数变化作废,则丢弃结果
if (requestIdRef.current !== requestId) return
// Mock 结果
const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20));
const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20))
const result: PreviewResult = {
clipCount: estimatedClips,
transitionCount: estimatedClips - 1,
@@ -156,23 +163,24 @@ export function useStep3Preview({
),
previewVideoUrl: null, // 后端接口出来后填充
generateDuration: 1.5,
};
}
setPreviewResult(result);
setPreviewStatus("ready");
setPreviewResult(result)
setPreviewStatus("ready")
} catch (e) {
setPreviewError(e instanceof Error ? e.message : "预览生成失败");
setPreviewStatus("error");
if (requestIdRef.current !== requestId) return
setPreviewError(e instanceof Error ? e.message : "预览生成失败")
setPreviewStatus("error")
}
}, [selectedTemplate, materialTotal]);
}, [selectedTemplate, materialTotal])
/** 重新生成预览 */
const regeneratePreview = useCallback(() => {
generatePreview();
}, [generatePreview]);
generatePreview()
}, [generatePreview])
/** 是否可以下一步(预览已生成) */
const canProceed = previewStatus === "ready";
const canProceed = previewStatus === "ready"
return {
templateName,
@@ -186,7 +194,7 @@ export function useStep3Preview({
canProceed,
generatePreview,
regeneratePreview,
};
}
}
export default useStep3Preview;
export default useStep3Preview