Compare commits

...

1 Commits

Author SHA1 Message Date
xiaoxia c2e2a16ed8 feat: 封面模板对接后端 CRUD API,移除 mock 数据
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 45s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 50s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 52s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m9s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 51s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 19s
AI Code Review / AI Code Review (pull_request) Failing after 1m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m52s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m23s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m45s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 44s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m15s
- 新增 api/cover-templates.ts:封装 GET/POST/PUT/DELETE /cover-templates
- useStep6Cover.ts:弹窗打开时 fetch 模板列表,CRUD 操作走 API
- CoverSettingsModal.tsx:增加 loading/error 状态展示 + 删除按钮(非系统模板)
- Step6CoverSettings.tsx:透传 loading/error/delete 属性
- constants.ts:删除 MOCK_COVER_TEMPLATES 硬编码数据
2026-08-09 17:06:45 +08:00
5 changed files with 196 additions and 72 deletions
+53
View File
@@ -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>
)
}
+1 -38
View File
@@ -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,
}
}