Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoxia 099134d88b fix: 修复 CoverTemplate 导入路径,修复 TS type check 失败
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 / 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 / Validate - Type Check (mypy) (pull_request) Successful in 47s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 48s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 55s
AI Code Review / AI Code Review (pull_request) Failing after 1m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m41s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m45s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m20s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 59s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m16s
2026-08-09 16:50:49 +08:00
xiaoxia b9144f7b97 fix: 清理封面 hook 未使用的变量和导入,修复 Frontend Lint
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 / 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 / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m1s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m5s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m51s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m0s
AI Code Review / AI Code Review (pull_request) Failing after 3m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m33s
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
2026-08-09 16:30:04 +08:00
xiaoxia 18c503e0af feat: 封面设置区域 UI 改造
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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 56s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 30s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m58s
AI Code Review / AI Code Review (pull_request) Failing after 2m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m32s
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
- 改造 Step6CoverSettings 顶层区域:删除三tab布局,改为双按钮+模板名+预览
- 新增 CoverSettingsModal 封面设置弹窗(4列网格模板卡片)
- 新增 CoverEditorModal 自定义封面编辑器弹窗(折叠面板+画布预览)
- 新增 CoverTemplate 类型定义及 MOCK_COVER_TEMPLATES 常量
- 更新 useStep6Cover hook 添加弹窗状态和模板操作方法
- 新增封面设置相关 CSS 样式
- TypeScript 编译通过,Prettier 格式化完成
2026-08-09 16:14:36 +08:00
8 changed files with 551 additions and 155 deletions
@@ -30,3 +30,20 @@ export const DEFAULT_COVER_CONFIG: CoverConfig = {
ai_suggested_time: null,
thumbnail_url: "",
}
/** 封面模板 */
export interface CoverTemplate {
id: string
name: string
thumbnail_url: string
is_system: boolean
created_at: string
config?: {
background_enabled?: boolean
background_color?: string
portrait_enabled?: boolean
title_text?: string
subtitle_text?: string
mask_enabled?: boolean
}
}
@@ -72,7 +72,7 @@ export {
} from "./sticker"
/* 封面 */
export { type CoverMode, type CoverConfig, DEFAULT_COVER_CONFIG } from "./cover"
export { type CoverMode, type CoverConfig, DEFAULT_COVER_CONFIG, type CoverTemplate } from "./cover"
/* 片段数据 */
export { type ClipType, type ClipData } from "./clip"
@@ -1,9 +1,9 @@
import React, { useEffect, useRef } from "react"
import React from "react"
import type { CoverConfig } from "../../editing-planner/types"
import { useStep6Cover } from "../hooks/useStep6Cover"
import { CoverModeSelector } from "./cover-settings/CoverModeSelector"
import { FrameCoverPicker } from "./cover-settings/FrameCoverPicker"
import { UploadCoverPicker } from "./cover-settings/UploadCoverPicker"
import Button from "@/components/ui/Button"
import CoverSettingsModal from "./cover-settings/CoverSettingsModal"
import CoverEditorModal from "./cover-settings/CoverEditorModal"
interface Step6CoverSettingsProps {
coverSettings: CoverConfig
@@ -18,15 +18,18 @@ interface Step6CoverSettingsProps {
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
const {
coverSettings,
formatTime,
toggleEnabled,
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
showCoverSettings,
setShowCoverSettings,
showCoverEditor,
setShowCoverEditor,
selectedTemplateId,
selectedTemplateName,
editingTemplate,
coverTemplates,
handleSelectTemplate,
handleEditTemplate,
handleSaveTemplate,
} = useStep6Cover({
coverSettings: props.coverSettings,
onCoverSettingsChange: props.onCoverSettingsChange,
@@ -35,32 +38,9 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
selectedTemplate: props.selectedTemplate,
})
// 进入 auto 模式时自动触发智能封面生成
const autoTriggeredRef = useRef(false)
useEffect(() => {
// 切换模式、禁用封面或素材变更时重置触发标记
if (coverSettings.mode !== "auto" || !coverSettings.enabled) {
autoTriggeredRef.current = false
return
}
// 有素材且未生成过封面时自动触发
if (
coverSettings.mode === "auto" &&
!coverSettings.thumbnail_url &&
!autoTriggeredRef.current &&
props.assetIds &&
props.assetIds.length > 0
) {
autoTriggeredRef.current = true
generateAutoCover()
}
}, [
coverSettings.enabled,
coverSettings.mode,
coverSettings.thumbnail_url,
generateAutoCover,
props.assetIds,
])
const handleAutoGenerate = () => {
generateAutoCover()
}
// 预览图:优先 thumbnail_url,其次 upload_url
const previewUrl = coverSettings.thumbnail_url || coverSettings.upload_url
@@ -69,70 +49,49 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<div className="xx-form-section">
<h3>🖼 </h3>
<div className="xx-cover-header">
<span className="xx-cover-header-label"></span>
<label className="xx-switch">
<input
type="checkbox"
checked={coverSettings.enabled}
onChange={(e) => toggleEnabled(e.target.checked)}
/>
<span className="xx-switch-slider" />
</label>
<div className="xx-cover-actions">
<Button buttonType="primary" onClick={handleAutoGenerate}>
</Button>
<Button buttonType="ghost" onClick={() => setShowCoverSettings(true)}>
</Button>
</div>
{coverSettings.enabled && (
<>
<div className="xx-section-title"></div>
<CoverModeSelector
mode={coverSettings.mode}
onModeChange={setMode}
modeLabels={COVER_MODE_LABELS}
modeIcons={COVER_MODE_ICONS}
/>
<div className="xx-cover-selected-template">: {selectedTemplateName}</div>
{coverSettings.mode === "auto" && (
<div className="xx-cover-auto">
<div className="xx-cover-auto-badge">
<span style={{ fontSize: 24 }}>🤖</span>
<span>AI </span>
</div>
</div>
)}
{coverSettings.mode === "frame" && (
<FrameCoverPicker
frameTime={coverSettings.frame_time}
totalDuration={totalDuration}
formatTime={formatTime}
onFrameTimeChange={setFrameTime}
/>
)}
{coverSettings.mode === "upload" && (
<UploadCoverPicker uploadUrl={coverSettings.upload_url} onUpload={handleUpload} />
)}
<div className="xx-section-title"></div>
<div className="xx-cover-preview-box">
{previewUrl ? (
<img src={previewUrl} alt="封面预览" className="xx-cover-preview-img" />
) : (
<div className="xx-cover-preview-placeholder">
<span style={{ fontSize: 28 }}>🖼</span>
<span>
{coverSettings.mode === "auto"
? "AI 正在选择..."
: coverSettings.mode === "frame"
? `${formatTime(coverSettings.frame_time)}`
: "未上传封面"}
</span>
</div>
)}
<div className="xx-cover-preview-ratio">9:16</div>
<div className="xx-section-title"></div>
<div className="xx-cover-preview-box">
{previewUrl ? (
<img src={previewUrl} alt="封面预览" className="xx-cover-preview-img" />
) : (
<div className="xx-cover-preview-placeholder">
<span style={{ fontSize: 28 }}>🖼</span>
<span>"自动生成封面"</span>
</div>
</>
)}
)}
<div className="xx-cover-preview-ratio">9:16</div>
</div>
<CoverSettingsModal
open={showCoverSettings}
onClose={() => setShowCoverSettings(false)}
templates={coverTemplates}
selectedTemplateId={selectedTemplateId}
onSelectTemplate={handleSelectTemplate}
onEditTemplate={handleEditTemplate}
onCreateNew={() => {
setShowCoverSettings(false)
setShowCoverEditor(true)
}}
/>
<CoverEditorModal
open={showCoverEditor}
onClose={() => setShowCoverEditor(false)}
template={editingTemplate}
onSave={handleSaveTemplate}
/>
</div>
)
}
@@ -0,0 +1,125 @@
import React, { useState } from "react"
import type { CoverTemplate } from "../../../editing-planner/types"
import Modal from "@/components/ui/Modal"
import Button from "@/components/ui/Button"
interface CoverEditorModalProps {
open: boolean
onClose: () => void
template: CoverTemplate | null
onSave: (template: CoverTemplate) => void
}
interface SectionState {
basic: boolean
portrait: boolean
background: boolean
title: boolean
subtitle: boolean
mask: boolean
}
const CoverEditorModal: React.FC<CoverEditorModalProps> = ({ open, onClose, template, onSave }) => {
const [name, setName] = useState(template?.name || "")
const [sections, setSections] = useState<SectionState>({
basic: true,
portrait: false,
background: false,
title: true,
subtitle: true,
mask: false,
})
const toggleSection = (key: keyof SectionState) => {
setSections((prev) => ({ ...prev, [key]: !prev[key] }))
}
const handleSave = () => {
if (!template) return
onSave({ ...template, name })
onClose()
}
return (
<Modal
open={open}
onCancel={onClose}
width={1000}
title="自定义封面编辑器"
centered
footer={null}
>
<div className="xx-cover-editor-header">
<input
className="xx-cover-editor-name-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入模板名称"
/>
<div className="xx-cover-editor-header-actions">
<Button buttonType="ghost" onClick={onClose}>
</Button>
<Button buttonType="primary" onClick={handleSave}>
</Button>
</div>
</div>
<div className="xx-cover-editor-layout">
{/* 左侧折叠面板 */}
<div className="xx-cover-editor-left">
{[
{ key: "basic" as const, label: "基础设置" },
{ key: "portrait" as const, label: "人像设置" },
{ key: "background" as const, label: "背景设置", toggle: true },
{ key: "title" as const, label: "主标题" },
{ key: "subtitle" as const, label: "副标题" },
{ key: "mask" as const, label: "蒙版", toggle: true },
].map((item) => (
<div key={item.key} className="xx-cover-editor-section">
<div
className="xx-cover-editor-section-header"
onClick={() => toggleSection(item.key)}
>
<span>{item.label}</span>
<span>{sections[item.key] ? "▾" : "▸"}</span>
</div>
{sections[item.key] && (
<div className="xx-cover-editor-section-body">
{item.toggle ? (
<label style={{ display: "flex", alignItems: "center", gap: 6 }}>
<input type="checkbox" defaultChecked={false} />
</label>
) : (
<span style={{ color: "var(--text-tertiary)" }}></span>
)}
</div>
)}
</div>
))}
</div>
{/* 右侧画布预览 */}
<div className="xx-cover-editor-right">
<div className="xx-cover-editor-canvas">
{/* 人像占位 */}
<div className="xx-cover-editor-portrait">
{/* 四角拖拽手柄 */}
<span className="xx-cover-editor-handle" style={{ top: -4, left: -4 }} />
<span className="xx-cover-editor-handle" style={{ top: -4, right: -4 }} />
<span className="xx-cover-editor-handle" style={{ bottom: -4, left: -4 }} />
<span className="xx-cover-editor-handle" style={{ bottom: -4, right: -4 }} />
</div>
{/* 文字占位 */}
<div className="xx-cover-editor-title-placeholder"></div>
<div className="xx-cover-editor-subtitle-placeholder"></div>
</div>
</div>
</div>
</Modal>
)
}
export default CoverEditorModal
@@ -0,0 +1,81 @@
import React from "react"
import type { CoverTemplate } from "../../../editing-planner/types"
import Modal from "@/components/ui/Modal"
import Button from "@/components/ui/Button"
interface CoverSettingsModalProps {
open: boolean
onClose: () => void
templates: CoverTemplate[]
selectedTemplateId: string
onSelectTemplate: (id: string) => void
onEditTemplate: (template: CoverTemplate) => void
onCreateNew: () => void
}
const GRADIENT_MAP: Record<string, string> = {
default: "linear-gradient(135deg, #e0e0e0, #c0c0c0)",
"bold-red": "linear-gradient(135deg, #ef4444, #b91c1c)",
"elegant-black": "linear-gradient(135deg, #374151, #111827)",
"gradient-blue": "linear-gradient(135deg, #3b82f6, #1d4ed8)",
"gradient-purple": "linear-gradient(135deg, #8b5cf6, #6d28d9)",
"warm-orange": "linear-gradient(135deg, #f97316, #ea580c)",
"fresh-green": "linear-gradient(135deg, #22c55e, #15803d)",
"tech-blue": "linear-gradient(135deg, #06b6d4, #0e7490)",
}
const CoverSettingsModal: React.FC<CoverSettingsModalProps> = ({
open,
onClose,
templates,
selectedTemplateId,
onSelectTemplate,
onEditTemplate,
onCreateNew,
}) => {
return (
<Modal open={open} onCancel={onClose} width={800} title="封面设置" centered footer={null}>
<div className="xx-cover-modal-toolbar">
<Button buttonType="primary"></Button>
<Button buttonType="ghost"></Button>
<Button buttonType="primary" onClick={onCreateNew}>
</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)}
>
<div
className="xx-cover-template-thumb"
style={{ background: GRADIENT_MAP[tpl.id] || GRADIENT_MAP.default }}
>
🖼
</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>
<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>
</div>
</div>
))}
</div>
</Modal>
)
}
export default CoverSettingsModal
+38 -1
View File
@@ -2,7 +2,7 @@
* 智能剪辑页面 — 常量定义
*/
import type { CoverConfig } from "../editing-planner/types"
import type { CoverConfig, CoverTemplate } from "../editing-planner/types"
/* ── 克隆声音状态配置 ── */
export const CLONE_STATUS_CONFIG: Record<string, { label: string; color: string }> = {
@@ -198,3 +198,40 @@ 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" },
]
+193
View File
@@ -2888,3 +2888,196 @@
.ant-modal-close {
color: #fff !important;
}
/* ── 封面设置区域改造样式 ── */
/* 封面操作按钮区 */
.xx-cover-actions {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
/* 已选模板文字 */
.xx-cover-selected-template {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 16px;
}
/* 封面设置弹窗 - 工具栏 */
.xx-cover-modal-toolbar {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
/* 封面模板网格 */
.xx-cover-template-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
/* 封面模板卡片 */
.xx-cover-template-card {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s;
}
.xx-cover-template-card:hover {
border-color: var(--primary-color);
}
.xx-cover-template-card.selected {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* 卡片缩略图 */
.xx-cover-template-thumb {
aspect-ratio: 9/16;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #ccc;
}
/* 卡片信息区 */
.xx-cover-template-info {
padding: 8px;
}
.xx-cover-template-name {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
margin-bottom: 2px;
}
.xx-cover-template-badge {
font-size: 11px;
color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
}
.xx-cover-template-date {
font-size: 11px;
color: var(--text-tertiary);
margin-bottom: 6px;
}
.xx-cover-template-actions {
display: flex;
gap: 4px;
}
/* 封面编辑器 */
.xx-cover-editor-layout {
display: flex;
gap: 20px;
min-height: 500px;
}
.xx-cover-editor-left {
width: 280px;
flex-shrink: 0;
}
.xx-cover-editor-right {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: var(--radius-md);
}
.xx-cover-editor-canvas {
width: 225px;
height: 400px;
background: #ddd;
position: relative;
border-radius: 8px;
overflow: hidden;
}
.xx-cover-editor-portrait {
position: absolute;
top: 20%;
left: 15%;
width: 70%;
height: 45%;
background: #a8d4f0;
border: 2px solid #333;
}
.xx-cover-editor-handle {
position: absolute;
width: 8px;
height: 8px;
background: #333;
border: 1px solid white;
}
/* 编辑器折叠面板 */
.xx-cover-editor-section {
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
margin-bottom: 8px;
}
.xx-cover-editor-section-header {
padding: 10px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-tertiary);
}
.xx-cover-editor-section-body {
padding: 12px;
font-size: 12px;
color: var(--text-secondary);
}
/* 编辑器顶部 */
.xx-cover-editor-header {
margin-bottom: 16px;
}
.xx-cover-editor-name-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 14px;
margin-bottom: 12px;
}
.xx-cover-editor-header-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 编辑器画布内文字占位 */
.xx-cover-editor-title-placeholder {
position: absolute;
top: 72%;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
font-weight: 700;
color: #fff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
white-space: nowrap;
}
.xx-cover-editor-subtitle-placeholder {
position: absolute;
top: 82%;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(255, 255, 255, 0.85);
white-space: nowrap;
}
@@ -2,9 +2,9 @@
* Step 6 封面设置 Hook
* 封装封面设置的交互逻辑
*/
import { useCallback, useRef } from "react"
import type { CoverConfig } from "../../editing-planner/types"
import { COVER_MODE_LABELS, COVER_MODE_ICONS, DEFAULT_COVER_SETTINGS } from "../constants"
import { useCallback, useRef, useState } from "react"
import type { CoverConfig, CoverTemplate } from "../../editing-planner/types"
import { MOCK_COVER_TEMPLATES } from "../constants"
import { generateCover } from "@/api/template-editor"
interface UseStep6CoverProps {
@@ -26,50 +26,12 @@ export function useStep6Cover({
}: UseStep6CoverProps) {
const generatingRef = useRef(false)
const formatTime = useCallback((seconds: number) => {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 10)
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${ms}`
}, [])
const toggleEnabled = useCallback(
(enabled: boolean) => {
onCoverSettingsChange({ ...coverSettings, enabled })
},
[coverSettings, onCoverSettingsChange],
)
const setMode = useCallback(
(mode: CoverConfig["mode"]) => {
onCoverSettingsChange({ ...coverSettings, mode })
},
[coverSettings, onCoverSettingsChange],
)
const setFrameTime = useCallback(
(frameTime: number) => {
onCoverSettingsChange({ ...coverSettings, frame_time: frameTime })
},
[coverSettings, onCoverSettingsChange],
)
const handleUpload = useCallback(
(file: File) => {
const reader = new FileReader()
reader.onload = (ev) => {
const url = ev.target?.result as string
onCoverSettingsChange({
...coverSettings,
upload_url: url,
thumbnail_url: url,
mode: "upload",
})
}
reader.readAsDataURL(file)
},
[coverSettings, onCoverSettingsChange],
)
// ── 封面设置弹窗状态 ──
const [showCoverSettings, setShowCoverSettings] = useState(false)
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)
/** 调用后端智能封面 API,生成封面并更新预览 */
const generateAutoCover = useCallback(async () => {
@@ -95,20 +57,42 @@ export function useStep6Cover({
}
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange])
// ── 模板操作方法 ──
const handleSelectTemplate = useCallback((id: string) => {
setSelectedTemplateId(id)
}, [])
const handleEditTemplate = useCallback((tpl: CoverTemplate) => {
setEditingTemplate(tpl)
setShowCoverEditor(true)
}, [])
const handleSaveTemplate = useCallback((tpl: CoverTemplate) => {
setCoverTemplates((prev) => prev.map((t) => (t.id === tpl.id ? tpl : t)))
setShowCoverEditor(false)
}, [])
const selectedTemplateName =
coverTemplates.find((t) => t.id === selectedTemplateId)?.name || "默认"
const totalDuration = duration || 30
return {
coverSettings,
formatTime,
toggleEnabled,
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
DEFAULT_COVER_SETTINGS,
showCoverSettings,
setShowCoverSettings,
showCoverEditor,
setShowCoverEditor,
selectedTemplateId,
setSelectedTemplateId,
editingTemplate,
coverTemplates,
selectedTemplateName,
handleSelectTemplate,
handleEditTemplate,
handleSaveTemplate,
}
}