style: reformat index.ts with repo prettier config (#1899 #1900)
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (pull_request) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (pull_request) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (pull_request) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Check push changed paths (pull_request) Waiting to run
CI/CD Pipeline / Build Staging API Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Build Staging Web Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Build Staging Worker Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (pull_request) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (pull_request) Blocked by required conditions
CI/CD Pipeline / Deploy Production (pull_request) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (pull_request) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (pull_request) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (pull_request) Blocked by required conditions
CI/CD Pipeline / CI Gate (pull_request) Blocked by required conditions
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled

This commit is contained in:
2026-09-14 21:37:45 +08:00
parent 712e381ec6
commit 0e2cefaa5f
@@ -3,21 +3,21 @@
* 集中管理 5 步向导的所有共享状态、API 加载、URL 参数解析
* 模板仍在后台自动加载/选择(后端 API 需要 edit_plan_id),但不再暴露给用户。
*/
import { useState, useMemo, useEffect, useRef, useCallback } from "react";
import { useSearchParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { message } from "antd";
import type { EditingTemplate } from "@/api/editing-planner";
import { getEditingTemplates } from "@/api/editing-planner";
import type { EditPlanClip } from "@/api/template-editor";
import type { CoverConfig } from "../../types/cover";
import type { PresetVoiceItem } from "@/api/voices";
import { DEFAULT_COVER_SETTINGS, DEFAULT_CLIP_COUNT } from "../../constants";
import type { TitleSettings } from "../../types";
import { usePlanConfigLoader } from "./usePlanConfigLoader";
import { useVoiceState } from "./useVoiceState";
import { usePersistedState } from "../usePersistedState";
import { isValidTemplate } from "./templateFallback";
import { useState, useMemo, useEffect, useRef, useCallback } from "react"
import { useSearchParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { message } from "antd"
import type { EditingTemplate } from "@/api/editing-planner"
import { getEditingTemplates } from "@/api/editing-planner"
import type { EditPlanClip } from "@/api/template-editor"
import type { CoverConfig } from "../../types/cover"
import type { PresetVoiceItem } from "@/api/voices"
import { DEFAULT_COVER_SETTINGS, DEFAULT_CLIP_COUNT } from "../../constants"
import type { TitleSettings } from "../../types"
import { usePlanConfigLoader } from "./usePlanConfigLoader"
import { useVoiceState } from "./useVoiceState"
import { usePersistedState } from "../usePersistedState"
import { isValidTemplate } from "./templateFallback"
const DEFAULT_TITLE_SETTINGS: TitleSettings = {
aiAutoSelect: false,
@@ -32,173 +32,160 @@ const DEFAULT_TITLE_SETTINGS: TitleSettings = {
color: "#ffffff",
posX: null,
posY: null,
};
}
export interface GenerateFormState {
/* 步骤 */
currentStep: number;
setCurrentStep: (step: number | ((prev: number) => number)) => void;
currentStep: number
setCurrentStep: (step: number | ((prev: number) => number)) => void
/* 片段数量(#1899) */
clipCount: number;
setClipCount: (n: number) => void;
clipCount: number
setClipCount: (n: number) => void
/* 模板(后台自动选择,不暴露 UI) */
selectedTemplate: string;
setSelectedTemplate: (id: string) => void;
userTemplates: EditingTemplate[];
handleInvalidTemplate: () => boolean;
selectedTemplate: string
setSelectedTemplate: (id: string) => void
userTemplates: EditingTemplate[]
handleInvalidTemplate: () => boolean
/* 素材 */
selectedMaterials: string[];
setSelectedMaterials: (ids: string[]) => void;
materialMode: "manual" | "auto";
setMaterialMode: (mode: "manual" | "auto") => void;
smartSelectedIds: string[];
setSmartSelectedIds: (ids: string[]) => void;
selectedMaterials: string[]
setSelectedMaterials: (ids: string[]) => void
materialMode: "manual" | "auto"
setMaterialMode: (mode: "manual" | "auto") => void
smartSelectedIds: string[]
setSmartSelectedIds: (ids: string[]) => void
/* 服务端片段 */
serverClips: EditPlanClip[];
setServerClips: (clips: EditPlanClip[]) => void;
serverClips: EditPlanClip[]
setServerClips: (clips: EditPlanClip[]) => void
/* 标题 */
titleSettings: TitleSettings;
setTitleSettings: (
settings: TitleSettings | ((prev: TitleSettings) => TitleSettings),
) => void;
titleSettings: TitleSettings
setTitleSettings: (settings: TitleSettings | ((prev: TitleSettings) => TitleSettings)) => void
/* 封面 */
coverSettings: CoverConfig;
setCoverSettings: (
settings: CoverConfig | ((prev: CoverConfig) => CoverConfig),
) => void;
coverSettings: CoverConfig
setCoverSettings: (settings: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void
/* 配音 */
selectedVoice: string;
setSelectedVoice: (id: string) => void;
voiceMode: "preset" | "custom" | "clone";
setVoiceMode: (mode: "preset" | "custom" | "clone") => void;
selectedClonedVoice: string;
setSelectedClonedVoice: (id: string) => void;
presetVoices: PresetVoiceItem[];
selectedVoice: string
setSelectedVoice: (id: string) => void
voiceMode: "preset" | "custom" | "clone"
setVoiceMode: (mode: "preset" | "custom" | "clone") => void
selectedClonedVoice: string
setSelectedClonedVoice: (id: string) => void
presetVoices: PresetVoiceItem[]
/* 克隆弹窗 */
cloneModalOpen: boolean;
setCloneModalOpen: (open: boolean) => void;
cloneModalOpen: boolean
setCloneModalOpen: (open: boolean) => void
/* 高级设置 */
videoRatio: string;
duration: number;
style: string;
autoSubtitles: boolean;
bgm: boolean;
videoRatio: string
duration: number
style: string
autoSubtitles: boolean
bgm: boolean
/* URL 参数 */
editPlanId: string | null;
planConfigStr: string | null;
sourceEditPlanId: string | null;
editPlanId: string | null
planConfigStr: string | null
sourceEditPlanId: string | null
/** 预览任务 ID */
previewTaskId: string | null;
setPreviewTaskId: (id: string | null) => void;
storedSourceEditPlanId: string | null;
setStoredSourceEditPlanId: (planId: string | null) => void;
previewTaskId: string | null
setPreviewTaskId: (id: string | null) => void
storedSourceEditPlanId: string | null
setStoredSourceEditPlanId: (planId: string | null) => void
/* ── 批量生成(Issue #1677)── */
previewCount: number;
setPreviewCount: (n: number) => void;
previewTitles: string[];
setPreviewTitles: (titles: string[] | ((prev: string[]) => string[])) => void;
voiceModePerVideo: boolean;
setVoiceModePerVideo: (v: boolean) => void;
voiceLibraryIds: string[];
setVoiceLibraryIds: (ids: string[] | ((prev: string[]) => string[])) => void;
previewCovers: string[];
setPreviewCovers: (urls: string[] | ((prev: string[]) => string[])) => void;
selectedVariantIds: number[];
setSelectedVariantIds: (
ids: number[] | ((prev: number[]) => number[]),
) => void;
previewCount: number
setPreviewCount: (n: number) => void
previewTitles: string[]
setPreviewTitles: (titles: string[] | ((prev: string[]) => string[])) => void
voiceModePerVideo: boolean
setVoiceModePerVideo: (v: boolean) => void
voiceLibraryIds: string[]
setVoiceLibraryIds: (ids: string[] | ((prev: string[]) => string[])) => void
previewCovers: string[]
setPreviewCovers: (urls: string[] | ((prev: string[]) => string[])) => void
selectedVariantIds: number[]
setSelectedVariantIds: (ids: number[] | ((prev: number[]) => number[])) => void
}
export const useGenerateFormState = (): GenerateFormState => {
const [searchParams] = useSearchParams();
const editPlanId = searchParams.get("edit_plan_id");
const planConfigStr = searchParams.get("plan_config");
const [searchParams] = useSearchParams()
const editPlanId = searchParams.get("edit_plan_id")
const planConfigStr = searchParams.get("plan_config")
/* ── 步骤状态 ── */
const [currentStep, setCurrentStep] = useState(1);
const [currentStep, setCurrentStep] = useState(1)
/* ── 片段数量(#1899) ── */
const [clipCount, setClipCount] = useState(DEFAULT_CLIP_COUNT);
const [clipCount, setClipCount] = useState(DEFAULT_CLIP_COUNT)
/* ── 模板自动加载(#1899:后台静默加载,不暴露 UI) ── */
const [selectedTemplate, setSelectedTemplateRaw] = useState("");
const [selectedTemplate, setSelectedTemplateRaw] = useState("")
const { data: allTemplates = [] } = useQuery<EditingTemplate[]>({
queryKey: ["generate-templates"],
queryFn: () => getEditingTemplates({ validOnly: true }),
staleTime: 60_000,
});
const validTemplates = useMemo(
() => allTemplates.filter(isValidTemplate),
[allTemplates],
);
const userTemplates = validTemplates;
})
const validTemplates = useMemo(() => allTemplates.filter(isValidTemplate), [allTemplates])
const userTemplates = validTemplates
const templatesRef = useRef(validTemplates);
templatesRef.current = validTemplates;
const selectedRef = useRef(selectedTemplate);
selectedRef.current = selectedTemplate;
const fallbackNotifiedRef = useRef<string>("");
const templatesRef = useRef(validTemplates)
templatesRef.current = validTemplates
const selectedRef = useRef(selectedTemplate)
selectedRef.current = selectedTemplate
const fallbackNotifiedRef = useRef<string>("")
const setSelectedTemplate = useCallback((id: string) => {
fallbackNotifiedRef.current = "";
setSelectedTemplateRaw(id);
}, []);
fallbackNotifiedRef.current = ""
setSelectedTemplateRaw(id)
}, [])
// 自动选择第一个有效模板(静默,不弹 toast)
useEffect(() => {
if (selectedTemplate) return;
const firstValid = validTemplates[0];
if (selectedTemplate) return
const firstValid = validTemplates[0]
if (firstValid) {
setSelectedTemplateRaw(firstValid.id);
setSelectedTemplateRaw(firstValid.id)
}
}, [validTemplates, selectedTemplate]);
}, [validTemplates, selectedTemplate])
const handleInvalidTemplate = useCallback((): boolean => {
const current = selectedRef.current;
if (current && fallbackNotifiedRef.current === current) return true;
const fallback = validTemplates[0];
fallbackNotifiedRef.current = current || "__empty__";
const current = selectedRef.current
if (current && fallbackNotifiedRef.current === current) return true
const fallback = validTemplates[0]
fallbackNotifiedRef.current = current || "__empty__"
if (fallback) {
setSelectedTemplateRaw(fallback.id);
message.warning("原模板已失效,已自动切换");
setSelectedTemplateRaw(fallback.id)
message.warning("原模板已失效,已自动切换")
} else {
setSelectedTemplateRaw("");
setSelectedTemplateRaw("")
}
return true;
}, [validTemplates]);
return true
}, [validTemplates])
/* ── source_edit_plan_id ── */
const sourceEditPlanId = editPlanId || null;
const sourceEditPlanId = editPlanId || null
/* ── 素材 ── */
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([]);
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual");
const [smartSelectedIds, setSmartSelectedIds] = useState<string[]>([]);
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual")
const [smartSelectedIds, setSmartSelectedIds] = useState<string[]>([])
/* ── 服务端片段 ── */
const [serverClips, setServerClips] = useState<EditPlanClip[]>([]);
const [serverClips, setServerClips] = useState<EditPlanClip[]>([])
/* ── 标题设置 ── */
const [titleSettings, setTitleSettings] = useState<TitleSettings>(
DEFAULT_TITLE_SETTINGS,
);
const [titleSettings, setTitleSettings] = useState<TitleSettings>(DEFAULT_TITLE_SETTINGS)
/* ── 封面设置 ── */
const [coverSettings, setCoverSettings] = useState<CoverConfig>(
DEFAULT_COVER_SETTINGS,
);
const [coverSettings, setCoverSettings] = useState<CoverConfig>(DEFAULT_COVER_SETTINGS)
/* ── 配音状态 ── */
const {
@@ -209,45 +196,46 @@ export const useGenerateFormState = (): GenerateFormState => {
selectedClonedVoice,
setSelectedClonedVoice,
presetVoices,
} = useVoiceState();
} = useVoiceState()
/* ── 克隆声音弹窗 ── */
const [cloneModalOpen, setCloneModalOpen] = useState(false);
const [cloneModalOpen, setCloneModalOpen] = useState(false)
/* ── 高级设置(隐藏但保留) ── */
const [videoRatio] = useState("9:16");
const [duration] = useState(30);
const [style] = useState("business");
const [autoSubtitles] = useState(true);
const [bgm] = useState(true);
const [videoRatio] = useState("9:16")
const [duration] = useState(30)
const [style] = useState("business")
const [autoSubtitles] = useState(true)
const [bgm] = useState(true)
/* ── 预览任务 ID ── */
const previewStorageKey = editPlanId
? `preview_task_id_${editPlanId}`
: selectedTemplate
? `preview_task_id_tpl_${selectedTemplate}`
: null;
: null
const [previewTaskId, setPreviewTaskId] = usePersistedState<string | null>(
previewStorageKey,
null,
);
)
const planIdStorageKey = editPlanId
? `source_edit_plan_id_${editPlanId}`
: selectedTemplate
? `source_edit_plan_id_tpl_${selectedTemplate}`
: null;
const [storedSourceEditPlanId, setStoredSourceEditPlanId] = usePersistedState<
string | null
>(planIdStorageKey, null);
: null
const [storedSourceEditPlanId, setStoredSourceEditPlanId] = usePersistedState<string | null>(
planIdStorageKey,
null,
)
/* ── 批量生成状态 ── */
const [previewCount, setPreviewCount] = useState(1);
const [previewTitles, setPreviewTitles] = useState<string[]>([""]);
const [voiceModePerVideo, setVoiceModePerVideo] = useState(false);
const [voiceLibraryIds, setVoiceLibraryIds] = useState<string[]>([""]);
const [previewCovers, setPreviewCovers] = useState<string[]>([""]);
const [selectedVariantIds, setSelectedVariantIds] = useState<number[]>([0]);
const [previewCount, setPreviewCount] = useState(1)
const [previewTitles, setPreviewTitles] = useState<string[]>([""])
const [voiceModePerVideo, setVoiceModePerVideo] = useState(false)
const [voiceLibraryIds, setVoiceLibraryIds] = useState<string[]>([""])
const [previewCovers, setPreviewCovers] = useState<string[]>([""])
const [selectedVariantIds, setSelectedVariantIds] = useState<number[]>([0])
/* ── 从 URL / 编辑计划加载配置 ── */
usePlanConfigLoader({
@@ -256,7 +244,7 @@ export const useGenerateFormState = (): GenerateFormState => {
setTitleSettings,
setCoverSettings,
setSelectedMaterials,
});
})
return {
currentStep,
@@ -312,5 +300,5 @@ export const useGenerateFormState = (): GenerateFormState => {
setPreviewCovers,
selectedVariantIds,
setSelectedVariantIds,
};
};
}
}