diff --git a/apps/web/src/api/cover-templates.ts b/apps/web/src/api/cover-templates.ts new file mode 100644 index 000000000..87ec268fe --- /dev/null +++ b/apps/web/src/api/cover-templates.ts @@ -0,0 +1,53 @@ +/** + * 封面模板 CRUD API + * 后端路由: /api/v1/cover-templates + */ +import apiClient from "./client" +import type { CoverTemplate } from "@/pages/editing-planner/types" + +export interface CoverTemplateListResponse { + items: CoverTemplate[] + total: number +} + +export interface CoverTemplateCreateRequest { + name: string + config?: { + background_enabled?: boolean + background_color?: string + portrait_enabled?: boolean + title_text?: string + subtitle_text?: string + mask_enabled?: boolean + } +} + +export type CoverTemplateUpdateRequest = Partial + +/** 获取封面模板列表 */ +export async function fetchCoverTemplates(): Promise { + const response = await apiClient.get("/cover-templates") + return response.data +} + +/** 创建封面模板 */ +export async function createCoverTemplate( + data: CoverTemplateCreateRequest, +): Promise { + const response = await apiClient.post("/cover-templates", data) + return response.data +} + +/** 更新封面模板 */ +export async function updateCoverTemplate( + id: string, + data: CoverTemplateUpdateRequest, +): Promise { + const response = await apiClient.put(`/cover-templates/${id}`, data) + return response.data +} + +/** 删除封面模板(系统模板不可删) */ +export async function deleteCoverTemplate(id: string): Promise { + await apiClient.delete(`/cover-templates/${id}`) +} diff --git a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx index 9ce4dd3f0..b112cadff 100755 --- a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx +++ b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx @@ -27,9 +27,12 @@ const Step6CoverSettings: React.FC = (props) => { selectedTemplateName, editingTemplate, coverTemplates, + templatesLoading, + templatesError, handleSelectTemplate, handleEditTemplate, handleSaveTemplate, + handleDeleteTemplate, } = useStep6Cover({ coverSettings: props.coverSettings, onCoverSettingsChange: props.onCoverSettingsChange, @@ -77,9 +80,12 @@ const Step6CoverSettings: React.FC = (props) => { open={showCoverSettings} onClose={() => setShowCoverSettings(false)} templates={coverTemplates} + loading={templatesLoading} + error={templatesError} selectedTemplateId={selectedTemplateId} onSelectTemplate={handleSelectTemplate} onEditTemplate={handleEditTemplate} + onDeleteTemplate={handleDeleteTemplate} onCreateNew={() => { setShowCoverSettings(false) setShowCoverEditor(true) diff --git a/apps/web/src/pages/generate/components/cover-settings/CoverSettingsModal.tsx b/apps/web/src/pages/generate/components/cover-settings/CoverSettingsModal.tsx index d0c168b50..9ca2fa3c6 100644 --- a/apps/web/src/pages/generate/components/cover-settings/CoverSettingsModal.tsx +++ b/apps/web/src/pages/generate/components/cover-settings/CoverSettingsModal.tsx @@ -7,9 +7,12 @@ interface CoverSettingsModalProps { open: boolean onClose: () => void templates: CoverTemplate[] + loading?: boolean + error?: string | null selectedTemplateId: string onSelectTemplate: (id: string) => void onEditTemplate: (template: CoverTemplate) => void + onDeleteTemplate: (id: string) => void onCreateNew: () => void } @@ -28,9 +31,12 @@ const CoverSettingsModal: React.FC = ({ open, onClose, templates, + loading = false, + error = null, selectedTemplateId, onSelectTemplate, onEditTemplate, + onDeleteTemplate, onCreateNew, }) => { return ( @@ -43,37 +49,62 @@ const CoverSettingsModal: React.FC = ({ -
- {templates.map((tpl) => ( -
onSelectTemplate(tpl.id)} - > + {loading && ( +
+ 加载中... +
+ )} + + {error && !loading && ( +
{error}
+ )} + + {!loading && !error && ( +
+ {templates.map((tpl) => (
onSelectTemplate(tpl.id)} > - 🖼️ -
-
-
- {tpl.name} - {tpl.is_system && ✨ 系统模板} +
+ 🖼️
-
{tpl.created_at}
-
e.stopPropagation()}> - - +
+
+ {tpl.name} + {tpl.is_system && ✨ 系统模板} +
+
{tpl.created_at}
+
e.stopPropagation()}> + + {!tpl.is_system && ( + + )} + +
-
- ))} -
+ ))} +
+ )} ) } diff --git a/apps/web/src/pages/generate/constants.ts b/apps/web/src/pages/generate/constants.ts index 703c3244d..478523f38 100644 --- a/apps/web/src/pages/generate/constants.ts +++ b/apps/web/src/pages/generate/constants.ts @@ -2,7 +2,7 @@ * 智能剪辑页面 — 常量定义 */ -import type { CoverConfig, CoverTemplate } from "../editing-planner/types" +import type { CoverConfig } from "../editing-planner/types" /* ── 克隆声音状态配置 ── */ export const CLONE_STATUS_CONFIG: Record = { @@ -198,40 +198,3 @@ export const DEFAULT_COVER_SETTINGS: CoverConfig = { ai_suggested_time: null, thumbnail_url: "", } - -/* ── Mock 封面模板数据 ── */ - -export const MOCK_COVER_TEMPLATES: CoverTemplate[] = [ - { id: "default", name: "默认模板", thumbnail_url: "", is_system: true, created_at: "2026-08-01" }, - { id: "bold-red", name: "大胆红", thumbnail_url: "", is_system: true, created_at: "2026-08-01" }, - { - id: "elegant-black", - name: "优雅黑", - thumbnail_url: "", - is_system: true, - created_at: "2026-08-01", - }, - { - id: "gradient-blue", - name: "渐变蓝", - thumbnail_url: "", - is_system: true, - created_at: "2026-08-01", - }, - { - id: "gradient-purple", - name: "渐变紫", - thumbnail_url: "", - is_system: true, - created_at: "2026-08-01", - }, - { id: "warm-orange", name: "暖橙", thumbnail_url: "", is_system: true, created_at: "2026-08-01" }, - { - id: "fresh-green", - name: "清新绿", - thumbnail_url: "", - is_system: true, - created_at: "2026-08-01", - }, - { id: "tech-blue", name: "科技蓝", thumbnail_url: "", is_system: true, created_at: "2026-08-01" }, -] diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index c73ca0ab0..1b5f44ff6 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -1,11 +1,16 @@ /** * Step 6 封面设置 Hook - * 封装封面设置的交互逻辑 + * 封装封面设置的交互逻辑,对接后端封面模板 CRUD API */ -import { useCallback, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import type { CoverConfig, CoverTemplate } from "../../editing-planner/types" -import { MOCK_COVER_TEMPLATES } from "../constants" import { generateCover } from "@/api/template-editor" +import { + fetchCoverTemplates, + createCoverTemplate, + updateCoverTemplate, + deleteCoverTemplate, +} from "@/api/cover-templates" interface UseStep6CoverProps { coverSettings: CoverConfig @@ -31,7 +36,33 @@ export function useStep6Cover({ const [showCoverEditor, setShowCoverEditor] = useState(false) const [selectedTemplateId, setSelectedTemplateId] = useState("default") const [editingTemplate, setEditingTemplate] = useState(null) - const [coverTemplates, setCoverTemplates] = useState(MOCK_COVER_TEMPLATES) + const [coverTemplates, setCoverTemplates] = useState([]) + + // ── API 加载状态 ── + const [templatesLoading, setTemplatesLoading] = useState(false) + const [templatesError, setTemplatesError] = useState(null) + + /** 从后端加载封面模板列表 */ + const loadTemplates = useCallback(async () => { + setTemplatesLoading(true) + setTemplatesError(null) + try { + const res = await fetchCoverTemplates() + setCoverTemplates(res.items || []) + } catch (err) { + console.error("[Step6] 加载封面模板失败:", err) + setTemplatesError("加载模板失败,请稍后重试") + } finally { + setTemplatesLoading(false) + } + }, []) + + /** 弹窗打开时加载模板列表 */ + useEffect(() => { + if (showCoverSettings) { + loadTemplates() + } + }, [showCoverSettings, loadTemplates]) /** 调用后端智能封面 API,生成封面并更新预览 */ const generateAutoCover = useCallback(async () => { @@ -67,10 +98,46 @@ export function useStep6Cover({ setShowCoverEditor(true) }, []) - const handleSaveTemplate = useCallback((tpl: CoverTemplate) => { - setCoverTemplates((prev) => prev.map((t) => (t.id === tpl.id ? tpl : t))) - setShowCoverEditor(false) - }, []) + /** 保存模板(创建或更新) */ + const handleSaveTemplate = useCallback( + async (tpl: CoverTemplate) => { + try { + if (tpl.id && coverTemplates.some((t) => t.id === tpl.id)) { + const updated = await updateCoverTemplate(tpl.id, { + name: tpl.name, + config: tpl.config, + }) + setCoverTemplates((prev) => prev.map((t) => (t.id === tpl.id ? { ...t, ...updated } : t))) + } else { + const created = await createCoverTemplate({ + name: tpl.name, + config: tpl.config, + }) + setCoverTemplates((prev) => [...prev, created]) + } + setShowCoverEditor(false) + } catch (err) { + console.error("[Step6] 保存模板失败:", err) + } + }, + [coverTemplates], + ) + + /** 删除模板 */ + const handleDeleteTemplate = useCallback( + async (id: string) => { + try { + await deleteCoverTemplate(id) + setCoverTemplates((prev) => prev.filter((t) => t.id !== id)) + if (selectedTemplateId === id) { + setSelectedTemplateId("default") + } + } catch (err) { + console.error("[Step6] 删除模板失败:", err) + } + }, + [selectedTemplateId], + ) const selectedTemplateName = coverTemplates.find((t) => t.id === selectedTemplateId)?.name || "默认" @@ -89,10 +156,14 @@ export function useStep6Cover({ setSelectedTemplateId, editingTemplate, coverTemplates, + templatesLoading, + templatesError, selectedTemplateName, handleSelectTemplate, handleEditTemplate, handleSaveTemplate, + handleDeleteTemplate, + loadTemplates, } }