Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2e2a16ed8 |
@@ -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<CoverTemplateCreateRequest>
|
||||
|
||||
/** 获取封面模板列表 */
|
||||
export async function fetchCoverTemplates(): Promise<CoverTemplateListResponse> {
|
||||
const response = await apiClient.get<CoverTemplateListResponse>("/cover-templates")
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 创建封面模板 */
|
||||
export async function createCoverTemplate(
|
||||
data: CoverTemplateCreateRequest,
|
||||
): Promise<CoverTemplate> {
|
||||
const response = await apiClient.post<CoverTemplate>("/cover-templates", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 更新封面模板 */
|
||||
export async function updateCoverTemplate(
|
||||
id: string,
|
||||
data: CoverTemplateUpdateRequest,
|
||||
): Promise<CoverTemplate> {
|
||||
const response = await apiClient.put<CoverTemplate>(`/cover-templates/${id}`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除封面模板(系统模板不可删) */
|
||||
export async function deleteCoverTemplate(id: string): Promise<void> {
|
||||
await apiClient.delete(`/cover-templates/${id}`)
|
||||
}
|
||||
@@ -27,9 +27,12 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (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<Step6CoverSettingsProps> = (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)
|
||||
|
||||
@@ -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<CoverSettingsModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
templates,
|
||||
loading = false,
|
||||
error = null,
|
||||
selectedTemplateId,
|
||||
onSelectTemplate,
|
||||
onEditTemplate,
|
||||
onDeleteTemplate,
|
||||
onCreateNew,
|
||||
}) => {
|
||||
return (
|
||||
@@ -43,37 +49,62 @@ const CoverSettingsModal: React.FC<CoverSettingsModalProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="xx-cover-template-grid">
|
||||
{templates.map((tpl) => (
|
||||
<div
|
||||
key={tpl.id}
|
||||
className={`xx-cover-template-card${selectedTemplateId === tpl.id ? " selected" : ""}`}
|
||||
onClick={() => onSelectTemplate(tpl.id)}
|
||||
>
|
||||
{loading && (
|
||||
<div style={{ textAlign: "center", padding: "40px 0", color: "var(--text-secondary)" }}>
|
||||
加载中...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div style={{ textAlign: "center", padding: "40px 0", color: "#ef4444" }}>{error}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="xx-cover-template-grid">
|
||||
{templates.map((tpl) => (
|
||||
<div
|
||||
className="xx-cover-template-thumb"
|
||||
style={{ background: GRADIENT_MAP[tpl.id] || GRADIENT_MAP.default }}
|
||||
key={tpl.id}
|
||||
className={`xx-cover-template-card${selectedTemplateId === tpl.id ? " selected" : ""}`}
|
||||
onClick={() => onSelectTemplate(tpl.id)}
|
||||
>
|
||||
🖼️
|
||||
</div>
|
||||
<div className="xx-cover-template-info">
|
||||
<div className="xx-cover-template-name">
|
||||
{tpl.name}
|
||||
{tpl.is_system && <span className="xx-cover-template-badge">✨ 系统模板</span>}
|
||||
<div
|
||||
className="xx-cover-template-thumb"
|
||||
style={{ background: GRADIENT_MAP[tpl.id] || GRADIENT_MAP.default }}
|
||||
>
|
||||
🖼️
|
||||
</div>
|
||||
<div className="xx-cover-template-date">{tpl.created_at}</div>
|
||||
<div className="xx-cover-template-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<Button buttonType="ghost" buttonSize="sm" onClick={() => onEditTemplate(tpl)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button buttonType="ghost" buttonSize="sm">
|
||||
导出
|
||||
</Button>
|
||||
<div className="xx-cover-template-info">
|
||||
<div className="xx-cover-template-name">
|
||||
{tpl.name}
|
||||
{tpl.is_system && <span className="xx-cover-template-badge">✨ 系统模板</span>}
|
||||
</div>
|
||||
<div className="xx-cover-template-date">{tpl.created_at}</div>
|
||||
<div className="xx-cover-template-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<Button buttonType="ghost" buttonSize="sm" onClick={() => onEditTemplate(tpl)}>
|
||||
编辑
|
||||
</Button>
|
||||
{!tpl.is_system && (
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
buttonSize="sm"
|
||||
onClick={() => {
|
||||
if (confirm("确定删除此模板?")) {
|
||||
onDeleteTemplate(tpl.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
<Button buttonType="ghost" buttonSize="sm">
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string, { label: string; color: string }> = {
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
@@ -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<CoverTemplate | null>(null)
|
||||
const [coverTemplates, setCoverTemplates] = useState<CoverTemplate[]>(MOCK_COVER_TEMPLATES)
|
||||
const [coverTemplates, setCoverTemplates] = useState<CoverTemplate[]>([])
|
||||
|
||||
// ── API 加载状态 ──
|
||||
const [templatesLoading, setTemplatesLoading] = useState(false)
|
||||
const [templatesError, setTemplatesError] = useState<string | null>(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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user