feat(#1197): Phase1 预览生成前端改造 - 真实接口对接+竖屏9:16+右侧预览常驻

- 新增 /api/generation 层:preview 创建/查询接口 + 类型定义
- useStep3Preview 重写:mock 替换为真实 /generation/preview 接口 + 轮询状态机(pending/generating/completed/failed/cancelled)
- Step3GeneratePreview 组件:加视频播放器 + 进度条 + 排队中/生成中/失败/成功 四态
- 新增 PreviewVideoPanel:右侧预览视频常驻面板(Step3+ 显示)
- videoRatio 默认值从 16:9 改为 9:16(竖屏)
- 生成页右侧布局:预览面板 + 正式结果面板上下排列
- 补 smoke test 覆盖新增模块
This commit is contained in:
前端Agent
2026-07-31 16:17:57 +08:00
parent 49a56afc51
commit bc84dedeb7
11 changed files with 541 additions and 77 deletions
+8
View File
@@ -0,0 +1,8 @@
export type {
PreviewStatus,
CreatePreviewRequest,
CreatePreviewResponse,
PreviewTaskResponse,
} from "./types"
export { createPreview, getPreviewStatus } from "./preview"
+16
View File
@@ -0,0 +1,16 @@
import apiClient from "../client"
import type { CreatePreviewRequest, CreatePreviewResponse, PreviewTaskResponse } from "./types"
/** 创建预览生成任务(单版本) */
export const createPreview = async (
params: CreatePreviewRequest,
): Promise<CreatePreviewResponse> => {
const response = await apiClient.post<CreatePreviewResponse>("/generation/preview", params)
return response.data
}
/** 查询预览任务状态及结果 */
export const getPreviewStatus = async (taskId: string): Promise<PreviewTaskResponse> => {
const response = await apiClient.get<PreviewTaskResponse>(`/generation/preview/${taskId}`)
return response.data
}
+47
View File
@@ -0,0 +1,47 @@
/** 预览任务状态 */
export type PreviewStatus = "pending" | "generating" | "completed" | "failed" | "cancelled"
/** 创建预览任务请求 */
export interface CreatePreviewRequest {
template_id: string
asset_ids: string[]
title_ids?: string[]
voice_ids?: string[]
video_title?: string
duration?: number
video_ratio?: string
bgm_config?: {
enabled: boolean
preset_id?: string
volume?: number
}
}
/** 创建预览任务响应 */
export interface CreatePreviewResponse {
task_id: string
status: PreviewStatus
is_preview: boolean
resolution: string
created_at: string
}
/** 预览任务详情响应 */
export interface PreviewTaskResponse {
task_id: string
status: PreviewStatus
progress: number
is_preview: boolean
resolution: string
video_url?: string
duration?: number
file_size?: number
clip_count?: number
transition_count?: number
material_usage?: number
error_message?: string
created_at: string
started_at?: string
finished_at?: string
generate_duration?: number
}
+32 -15
View File
@@ -18,6 +18,7 @@ import CloneModal from "@/components/voice/CloneModal"
import GenerateHeader from "./components/GenerateHeader"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateResultPanel from "./components/GenerateResultPanel"
import PreviewVideoPanel from "./components/PreviewVideoPanel"
import GenerateStepContent from "./components/GenerateStepContent"
import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
@@ -191,6 +192,7 @@ const GeneratePage: React.FC = () => {
previewError={step3Preview.previewError}
previewTemplateName={step3Preview.templateName}
previewMaterialCount={step3Preview.materialCount}
previewProgress={step3Preview.progress}
onGeneratePreview={step3Preview.generatePreview}
onRegeneratePreview={step3Preview.regeneratePreview}
/>
@@ -206,21 +208,36 @@ const GeneratePage: React.FC = () => {
/>
</div>
{/* ════ 右侧:生成结果 ════ */}
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
{/* ════ 右侧:预览 + 生成结果 ════ */}
<div className="xx-generate-right-col">
{/* 预览视频面板(Step3+ 常驻) */}
{currentStep >= 3 && (
<PreviewVideoPanel
previewStatus={step3Preview.previewStatus}
previewResult={step3Preview.previewResult}
previewError={step3Preview.previewError}
progress={step3Preview.progress}
videoRatio={videoRatio}
onRegenerate={step3Preview.regeneratePreview}
/>
)}
{/* 正式生成结果 */}
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
</div>
</div>
{/* ── 视频预览弹窗 ── */}
@@ -69,6 +69,7 @@ export interface GenerateStepContentProps {
previewError: string
previewTemplateName: string
previewMaterialCount: string
previewProgress: number
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
@@ -117,6 +118,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
previewError,
previewTemplateName,
previewMaterialCount,
previewProgress,
onGeneratePreview,
onRegeneratePreview,
} = props
@@ -151,6 +153,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
previewStatus={previewStatus}
previewResult={previewResult}
previewError={previewError}
progress={previewProgress}
onGeneratePreview={onGeneratePreview}
onRegeneratePreview={onRegeneratePreview}
/>
@@ -0,0 +1,106 @@
/**
* 右侧预览视频面板
* Step3 生成预览后常驻显示预览视频
*/
import React from "react"
import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
interface PreviewVideoPanelProps {
previewStatus: PreviewStatus
previewResult: PreviewResult | null
previewError: string
progress: number
videoRatio: string
onRegenerate: () => void
}
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
previewStatus,
previewResult,
previewError,
progress,
videoRatio,
onRegenerate,
}) => {
const hasPreview = previewStatus === "ready" && previewResult
const isLoading = previewStatus === "pending" || previewStatus === "generating"
const isError = previewStatus === "error"
return (
<div className="xx-generate-preview">
<div className="xx-preview-header">
<h3></h3>
{hasPreview && <span className="xx-preview-badge">480p </span>}
</div>
{/* 空状态:还没生成预览 */}
{previewStatus === "idle" && (
<div className="xx-preview-empty">
<PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
/>
<p className="xx-preview-empty-title"></p>
<p className="xx-preview-empty-desc"> 3 </p>
</div>
)}
{/* 生成中 */}
{isLoading && (
<div className="xx-preview-loading-panel">
<div className="xx-preview-video">
<div className="xx-preview-loading-center">
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<p style={{ marginTop: 12, color: "rgba(255,255,255,0.8)", fontSize: 14 }}>
{previewStatus === "pending" ? "排队中..." : `生成中 ${progress}%`}
</p>
</div>
</div>
<div className="xx-preview-progress-bar-wrap">
<div className="xx-preview-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
)}
{/* 生成失败 */}
{isError && (
<div className="xx-preview-error-panel">
<div className="xx-preview-video xx-preview-video--error">
<p style={{ color: "rgba(255,255,255,0.8)", fontSize: 14 }}></p>
</div>
<p className="xx-preview-error-msg">{previewError || "请重试"}</p>
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onRegenerate}>
</button>
</div>
)}
{/* 预览成功 */}
{hasPreview && (
<div className="xx-preview-video">
<video src={previewResult.videoUrl} controls preload="metadata" />
</div>
)}
{/* 预览信息 */}
{hasPreview && previewResult && (
<div className="xx-preview-info">
<div className="xx-preview-info-row">
<span></span>
<span>{previewResult.duration.toFixed(1)} </span>
</div>
<div className="xx-preview-info-row">
<span></span>
<span>{previewResult.clipCount} </span>
</div>
<div className="xx-preview-info-row">
<span></span>
<span>{videoRatio}</span>
</div>
</div>
)}
</div>
)
}
export default PreviewVideoPanel
@@ -1,7 +1,6 @@
/**
* Step 3 生成预览组件
* 展示剪辑方案预览,用户确认后进入下一步
* TODO: 后端预览接口对齐后,替换 mock 预览为真实视频/方案预览
* 调用后端预览生成接口,展示真实视频预览
*/
import React from "react"
import {
@@ -10,6 +9,7 @@ import {
ReloadOutlined,
PlayCircleOutlined,
ExclamationCircleFilled,
ClockCircleOutlined,
} from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
@@ -21,6 +21,7 @@ interface Step3GeneratePreviewProps {
previewStatus: PreviewStatus
previewResult: PreviewResult | null
previewError: string
progress: number
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
@@ -33,6 +34,7 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
previewStatus,
previewResult,
previewError,
progress,
onGeneratePreview,
onRegeneratePreview,
}) => {
@@ -47,7 +49,7 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
<PlayCircleOutlined style={{ fontSize: 32, color: "#3b82f6", marginBottom: 12 }} />
<p className="xx-preview-generate-title"></p>
<p className="xx-preview-generate-desc">
AI
AI 480p
</p>
</div>
<button
@@ -59,12 +61,24 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
</div>
)}
{/* 排队中 */}
{previewStatus === "pending" && (
<div className="xx-preview-loading">
<ClockCircleOutlined style={{ fontSize: 32, color: "#faad14" }} spin />
<p className="xx-preview-loading-text">...</p>
<p className="xx-preview-loading-desc"></p>
</div>
)}
{/* 生成中 */}
{previewStatus === "generating" && (
<div className="xx-preview-loading">
<LoadingOutlined style={{ fontSize: 32, color: "#3b82f6" }} spin />
<p className="xx-preview-loading-text">...</p>
<p className="xx-preview-loading-desc">AI </p>
<p className="xx-preview-loading-text">... {progress}%</p>
<p className="xx-preview-loading-desc">AI </p>
<div className="xx-preview-progress-bar">
<div className="xx-preview-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
)}
@@ -80,12 +94,12 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
</div>
)}
{/* 生成成功 - 预览卡片 */}
{/* 生成成功 - 视频预览 */}
{previewStatus === "ready" && previewResult && (
<>
<div className="xx-preview-tip">
<CheckCircleFilled style={{ color: "#52c41a", marginRight: 8 }} />
<span></span>
<span></span>
<button
className="xx-preview-regenerate-btn"
onClick={onRegeneratePreview}
@@ -95,8 +109,19 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
</button>
</div>
{/* 视频播放器 */}
<div className="xx-preview-video-wrapper">
<video
className="xx-preview-video"
src={previewResult.videoUrl}
controls
preload="metadata"
/>
</div>
{/* 剪辑方案信息 */}
<div className="xx-preview-plan-card">
<div className="xx-preview-plan-title"></div>
<div className="xx-preview-plan-title"></div>
<div className="xx-preview-plan-info">
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
@@ -107,7 +132,7 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
<span className="xx-preview-plan-value">{materialCount}</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{previewResult.clipCount} </span>
</div>
<div className="xx-preview-plan-row">
@@ -119,17 +144,17 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
<span className="xx-preview-plan-value">{previewResult.materialUsage}%</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{duration} </span>
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">
{previewResult.duration.toFixed(1)}
</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{videoRatio}</span>
</div>
</div>
<div className="xx-preview-plan-hint">
💡 AI
</div>
<div className="xx-preview-plan-hint">💡 480p </div>
</div>
</>
)}
+140
View File
@@ -2544,6 +2544,146 @@
生成结果(右侧)
================================================================ */
.xx-generate-right-col {
display: flex;
flex-direction: column;
gap: 16px;
}
.xx-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.xx-preview-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.xx-preview-badge {
font-size: 12px;
color: var(--text-secondary);
background: var(--bg-tertiary);
padding: 2px 8px;
border-radius: 12px;
}
.xx-preview-empty {
text-align: center;
padding: 40px 0;
}
.xx-preview-empty-title {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
margin: 0 0 4px 0;
}
.xx-preview-empty-desc {
font-size: 12px;
color: var(--text-tertiary);
margin: 0;
}
.xx-preview-loading-panel {
text-align: center;
}
.xx-preview-loading-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
z-index: 2;
}
.xx-preview-progress-bar-wrap {
width: 100%;
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
margin-top: 12px;
overflow: hidden;
}
.xx-preview-progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 2px;
transition: width 0.3s ease;
}
.xx-preview-error-panel {
text-align: center;
}
.xx-preview-video--error {
opacity: 0.7;
}
.xx-preview-error-msg {
font-size: 13px;
color: #ef4444;
margin: 12px 0;
}
.xx-preview-info {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 8px;
}
.xx-preview-info-row {
display: flex;
justify-content: space-between;
font-size: 13px;
color: var(--text-secondary);
}
.xx-preview-info-row span:last-child {
color: var(--text-primary);
font-weight: 500;
}
/* Step3 内部进度条 */
.xx-preview-loading .xx-preview-progress-bar {
width: 200px;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
margin: 12px auto 0;
overflow: hidden;
}
.xx-preview-loading .xx-preview-progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 3px;
transition: width 0.3s ease;
}
.xx-preview-video-wrapper {
margin: 16px 0;
display: flex;
justify-content: center;
}
.xx-preview-video-wrapper .xx-preview-video {
max-width: 300px;
width: 100%;
aspect-ratio: 9 / 16;
margin-bottom: 0;
}
.xx-generate-result {
background: var(--bg-secondary);
border-radius: var(--radius-xl);
@@ -137,7 +137,7 @@ export const useGenerateFormState = (): GenerateFormState => {
const [generateCount, setGenerateCount] = useState(1)
/* ── 高级设置(隐藏但保留) ── */
const [videoRatio] = useState("16:9")
const [videoRatio] = useState("9:16")
const [duration] = useState(30)
const [style] = useState("business")
const [autoSubtitles] = useState(true)
@@ -1,9 +1,10 @@
/**
* Step 3 生成预览 Hook
* 封装预览信息计算 + 预览生成逻辑
* TODO: 后端预览接口对齐后,替换 mock 逻辑为真实 API 调用
* 调用 /generation/preview 接口创建预览任务,轮询状态直到完成
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation"
import type { PreviewTaskResponse, PreviewStatus as ApiPreviewStatus } from "@/api/generation"
import type { EditingTemplate } from "@/api/editing-planner"
interface UseStep3PreviewProps {
@@ -14,22 +15,23 @@ interface UseStep3PreviewProps {
smartSelectedIds: string[]
duration: number
videoRatio: string
titleText?: string
voiceId?: string
}
export type PreviewStatus = "idle" | "generating" | "ready" | "error"
export type PreviewStatus = "idle" | "pending" | "generating" | "ready" | "error"
/** 预览生成结果 */
export interface PreviewResult {
/** 预计片段数 */
taskId: string
videoUrl: string
clipCount: number
/** 预计转场数 */
transitionCount: number
/** 素材使用率 */
materialUsage: number
/** 预览视频URL(后端接口出来后填充) */
previewVideoUrl: string | null
/** 生成耗时(秒) */
duration: number
fileSize: number
generateDuration: number
progress: number
}
export function useStep3Preview({
@@ -59,11 +61,24 @@ export function useStep3Preview({
const [previewStatus, setPreviewStatus] = useState<PreviewStatus>("idle")
const [previewResult, setPreviewResult] = useState<PreviewResult | null>(null)
const [previewError, setPreviewError] = useState<string>("")
const [progress, setProgress] = useState(0)
// 请求 ID 计数器,用于防止竞态条件(旧请求覆盖新状态)
const requestIdRef = useRef(0)
// 任务 ID + 轮询定时器,用于防止竞态条件
const currentTaskIdRef = useRef<string | null>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout>>()
const startTimeRef = useRef<number>(0)
/* ── 关键参数变化时重置预览状态(避免旧数据预览被误用) ── */
// 轮询超时时间(10 分钟)
const POLL_TIMEOUT_MS = 10 * 60 * 1000
const clearPollTimer = useCallback(() => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current)
pollTimerRef.current = undefined
}
}, [])
/* ── 参数变化时重置预览状态 ── */
const prevDepsRef = useRef({
selectedTemplate,
materialMode,
@@ -93,11 +108,13 @@ export function useStep3Preview({
].join("|")
if (prevKey !== currentKey && previewStatus !== "idle") {
// 参数变化时递增请求 ID,作废之前进行中的预览生成
requestIdRef.current += 1
// 参数变化,作废当前任务
currentTaskIdRef.current = null
clearPollTimer()
setPreviewStatus("idle")
setPreviewResult(null)
setPreviewError("")
setProgress(0)
}
prevDepsRef.current = {
@@ -116,15 +133,86 @@ export function useStep3Preview({
duration,
videoRatio,
previewStatus,
clearPollTimer,
])
/**
* 生成预览
* TODO: 后端 /generation-preview 接口对齐后替换此处 mock 逻辑
* 预计接口:POST /generate-preview
* 入参:template_id, material_ids, duration, video_ratio, style
* 出参:preview_id, status, clip_count, preview_video_url
*/
// 组件卸载时清理轮询
useEffect(() => {
return () => {
clearPollTimer()
}
}, [clearPollTimer])
/** 轮询预览任务状态 */
const pollPreviewStatus = useCallback((taskId: string) => {
const poll = async () => {
// 检查是否已被取消(参数变化或重新生成)
if (currentTaskIdRef.current !== taskId) return
// 超时检查
if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) {
setPreviewError("预览生成超时,请重试")
setPreviewStatus("error")
return
}
try {
const data: PreviewTaskResponse = await getPreviewStatus(taskId)
if (currentTaskIdRef.current !== taskId) return
const status = data.status as ApiPreviewStatus
if (status === "completed") {
setProgress(100)
setPreviewResult({
taskId: data.task_id,
videoUrl: data.video_url ?? "",
clipCount: data.clip_count ?? 0,
transitionCount: data.transition_count ?? 0,
materialUsage: data.material_usage ?? 0,
duration: data.duration ?? 0,
fileSize: data.file_size ?? 0,
generateDuration: data.generate_duration ?? 0,
progress: 100,
})
setPreviewStatus("ready")
return
}
if (status === "failed") {
setPreviewError(data.error_message || "预览生成失败,请重试")
setPreviewStatus("error")
return
}
if (status === "cancelled") {
setPreviewError("预览任务已取消")
setPreviewStatus("error")
return
}
// pending / generating 状态继续轮询
setProgress(data.progress || 0)
if (status === "pending") {
setPreviewStatus("pending")
pollTimerRef.current = setTimeout(poll, 5000)
} else {
setPreviewStatus("generating")
pollTimerRef.current = setTimeout(poll, 2000)
}
} catch (pollErr) {
if (currentTaskIdRef.current !== taskId) return
// 轮询出错,延迟后重试
pollTimerRef.current = setTimeout(poll, 3000)
}
}
// 首次延迟 1 秒开始轮询
pollTimerRef.current = setTimeout(poll, 1000)
}, [])
/** 生成预览 */
const generatePreview = useCallback(async () => {
if (!selectedTemplate) {
setPreviewError("请先选择模板")
@@ -137,42 +225,44 @@ export function useStep3Preview({
return
}
// 生成新的请求 ID,用于竞态检查
const requestId = requestIdRef.current + 1
requestIdRef.current = requestId
// 取消之前的轮询
clearPollTimer()
setPreviewStatus("generating")
setPreviewStatus("pending")
setPreviewError("")
setProgress(0)
setPreviewResult(null)
startTimeRef.current = Date.now()
try {
// TODO: 替换为真实后端接口调用
// const res = await generatePreviewAPI({ templateId: selectedTemplate, ... })
await new Promise((resolve) => setTimeout(resolve, 1500))
const assetIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials
const response = await createPreview({
template_id: selectedTemplate,
asset_ids: assetIds,
duration: duration || undefined,
video_ratio: videoRatio,
})
// 竞态检查:如果请求已被新请求或参数变化作废,则丢弃结果
if (requestIdRef.current !== requestId) return
// 竞态检查
if (startTimeRef.current === 0) return // 已被重置
// Mock 结果
const estimatedClips = Math.max(3, Math.min(materialTotal * 2, 20))
const result: PreviewResult = {
clipCount: estimatedClips,
transitionCount: estimatedClips - 1,
materialUsage: Math.min(
100,
Math.round((materialTotal / Math.max(estimatedClips, 1)) * 100),
),
previewVideoUrl: null, // 后端接口出来后填充
generateDuration: 1.5,
}
setPreviewResult(result)
setPreviewStatus("ready")
currentTaskIdRef.current = response.task_id
pollPreviewStatus(response.task_id)
} catch (e) {
if (requestIdRef.current !== requestId) return
setPreviewError(e instanceof Error ? e.message : "预览生成失败")
setPreviewStatus("error")
}
}, [selectedTemplate, materialTotal])
}, [
selectedTemplate,
materialTotal,
materialMode,
smartSelectedIds,
selectedMaterials,
duration,
videoRatio,
clearPollTimer,
pollPreviewStatus,
])
/** 重新生成预览 */
const regeneratePreview = useCallback(() => {
@@ -191,6 +281,7 @@ export function useStep3Preview({
previewStatus,
previewResult,
previewError,
progress,
canProceed,
generatePreview,
regeneratePreview,
@@ -5,14 +5,24 @@
*
* 重构记录:
* - useStep2Materials 拆分为 useMaterialLibrary + useSmartMatch 子 Hook
* - Phase1 预览生成:新增 generation API 层 + PreviewVideoPanel
*/
import { describe, it, expect } from "vitest"
// API 层 - 预览生成
import "@/api/generation"
import "@/api/generation/preview"
import "@/api/generation/types"
// 直接引入所有 Step 组件,建立完整依赖链
import "@/pages/generate/GeneratePage"
import "@/pages/generate/components/Step2MaterialSelect"
import "@/pages/generate/components/Step3GeneratePreview"
import "@/pages/generate/components/Step4TitleSettings"
import "@/pages/generate/components/Step5VoiceSelect"
import "@/pages/generate/components/PreviewVideoPanel"
import "@/pages/generate/components/GenerateResultPanel"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/components/voice/VoiceRecommendSection"
import "@/pages/generate/components/voice/VoiceChoiceCard"
import "@/pages/generate/components/voice/PresetVoiceDetail"
@@ -37,6 +47,7 @@ describe("GeneratePage module smoke test", () => {
})
})
import "@/pages/generate/hooks/useGenerateVideo"
import "@/pages/generate/hooks/useStep3Preview"
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
import "@/pages/generate/hooks/useGenerateFormState"
import "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection"