fix(generate): 标题模板恢复内联大卡片网格 #2066
@@ -137,8 +137,8 @@ const GeneratePage: React.FC = () => {
|
||||
}
|
||||
}, [selectedVoice, isBatch, voiceModePerVideo, setVoiceLibraryIds])
|
||||
|
||||
/* ── 标题面板模式:默认展示样式参数(false);需要大卡片模板网格时再切 true ── */
|
||||
const enableTemplates = false
|
||||
/* ── 标题面板模式:true = 内联大卡片模板网格(默认),false = 旧预设+参数 Tab ── */
|
||||
const enableTemplates = true
|
||||
|
||||
/* ── Step5 保存中状态 ── */
|
||||
const [finishing, setFinishing] = useState(false)
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
* - 批量:N 个独立标题输入框(AutoComplete 支持标题库选择)
|
||||
* - 标题样式(字体/颜色/位置/大小/粗斜描边/预设):全局统一
|
||||
*/
|
||||
import React, { useMemo, useState } from "react"
|
||||
import React, { useMemo } from "react"
|
||||
import type { TitleSettings } from "../types"
|
||||
import { POSITION_OPTIONS } from "../constants"
|
||||
import { FONT_OPTIONS } from "@/components/title/constants"
|
||||
import { useStep4Title } from "../hooks/useStep4Title"
|
||||
import TitleLibraryAutoComplete from "./title/TitleLibraryAutoComplete"
|
||||
import TitleStyleModal from "./title/TitleStyleModal"
|
||||
import Button from "@/components/ui/Button"
|
||||
import TitleStylePanel from "./title/TitleStylePanel"
|
||||
import type { TitleTemplate } from "@/components/title/template-types"
|
||||
|
||||
interface Step4TitleSettingsProps {
|
||||
@@ -57,6 +58,13 @@ interface Step4TitleSettingsProps {
|
||||
const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
|
||||
const t = useStep4Title(props)
|
||||
const {
|
||||
onUpdatePosition,
|
||||
onUpdateFont,
|
||||
onUpdateSize,
|
||||
onToggleBold,
|
||||
onToggleItalic,
|
||||
onToggleStroke,
|
||||
onToggleShadow,
|
||||
onApplyPreset,
|
||||
onUpdateStyle,
|
||||
activePreset,
|
||||
@@ -64,12 +72,14 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
|
||||
previewCount = 1,
|
||||
previewTitles,
|
||||
onPreviewTitlesChange,
|
||||
enableTemplates,
|
||||
selectedTemplateId,
|
||||
onApplyTemplate,
|
||||
onConfirmGenerate,
|
||||
generating,
|
||||
selectedCount = 1,
|
||||
} = props
|
||||
|
||||
const [showTitleStyleModal, setShowTitleStyleModal] = useState(false)
|
||||
const isBatch = previewCount > 1
|
||||
|
||||
/** 更新单个变体标题;变体0同步写回 titleSettings.title(全局样式面板/草稿/TTS 链路依赖) */
|
||||
@@ -174,30 +184,27 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标题样式:点击打开弹窗(参考字幕模板/封面模板 Modal 模式) */}
|
||||
<TitleStyleModal
|
||||
open={showTitleStyleModal}
|
||||
onClose={() => setShowTitleStyleModal(false)}
|
||||
titleText={previewTitles?.[0] ?? t.titleSettings.title}
|
||||
{/* 标题样式面板(全局共用) */}
|
||||
<TitleStylePanel
|
||||
settings={t.titleSettings}
|
||||
onUpdatePosition={onUpdatePosition}
|
||||
onUpdateFont={onUpdateFont}
|
||||
onUpdateSize={onUpdateSize}
|
||||
onToggleBold={onToggleBold}
|
||||
onToggleItalic={onToggleItalic}
|
||||
onToggleStroke={onToggleStroke}
|
||||
onToggleShadow={onToggleShadow}
|
||||
onApplyPreset={onApplyPreset}
|
||||
onUpdateStyle={onUpdateStyle}
|
||||
showCoverToggle
|
||||
activePreset={activePreset}
|
||||
onSave={(nextSettings, nextPreset) => {
|
||||
onUpdateStyle?.(nextSettings)
|
||||
if (nextPreset) onApplyPreset(nextPreset)
|
||||
}}
|
||||
titlePresets={titlePresets}
|
||||
POSITION_OPTIONS={POSITION_OPTIONS}
|
||||
FONT_OPTIONS={FONT_OPTIONS}
|
||||
enableTemplates={enableTemplates}
|
||||
selectedTemplateId={selectedTemplateId}
|
||||
onApplyTemplate={onApplyTemplate}
|
||||
/>
|
||||
<div className="xx-form-field">
|
||||
<label>标题样式</label>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() => setShowTitleStyleModal(true)}
|
||||
style={{ justifyContent: "flex-start", textAlign: "left" }}
|
||||
>
|
||||
{activePreset
|
||||
? titlePresets.find((p) => p.key === activePreset)?.label || "自定义样式"
|
||||
: "点击选择标题样式(预设 / 字体 / 颜色 / 描边 / 阴影)"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
.tsm-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tsm-preview {
|
||||
width: 100%;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tsm-presets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 440px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 2px 8px;
|
||||
}
|
||||
|
||||
.tsm-preset-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 8px 6px 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tsm-preset-card:hover {
|
||||
border-color: #c4b5fd;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 10px rgba(124, 58, 237, 0.12);
|
||||
}
|
||||
.tsm-preset-card.active {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.18);
|
||||
}
|
||||
|
||||
.tsm-preset-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 9 / 16;
|
||||
background: #1a1a1a;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tsm-preset-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
}
|
||||
.tsm-preset-emoji {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tsm-preset-label {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.tsm-custom-panel {
|
||||
max-height: 440px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 2px 8px;
|
||||
}
|
||||
|
||||
.tsm-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* 标题样式弹窗(Step3/智能剪辑用)
|
||||
*
|
||||
* 交互:
|
||||
* - 点击"标题样式"按钮/卡片打开
|
||||
* - 默认 Tab「预设」:10 个爆款预设卡片网格(Canvas 实时预览),点击选中
|
||||
* - Tab「自定义」:详细参数(位置/字体/字号/粗斜/描边/阴影/颜色/背景)
|
||||
* - 底部「取消」「保存」按钮:保存才会把临时编辑的 settings 应用到父组件
|
||||
*
|
||||
* 设计参考:CoverSettingsModal / 字幕模板选择弹窗
|
||||
*/
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Tabs } from "antd"
|
||||
import Modal from "@/components/ui/Modal"
|
||||
import Button from "@/components/ui/Button"
|
||||
import type { TitleSettings } from "../../types"
|
||||
import { POSITION_OPTIONS } from "../../constants"
|
||||
import { FONT_OPTIONS } from "@/components/title/constants"
|
||||
import TitleStylePanel from "./TitleStylePanel"
|
||||
import TitleMiniPreview from "./TitleMiniPreview"
|
||||
import { titleStyleConfigToCamel } from "@/components/title/utils"
|
||||
import { TITLE_PRESETS as TITLE_PRESETS_DATA } from "@/components/title/constants"
|
||||
import "./TitleStyleModal.css"
|
||||
|
||||
interface TitleStyleModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
/** 当前标题文字(用于预览) */
|
||||
titleText?: string
|
||||
/** 当前生效的标题样式 */
|
||||
settings: TitleSettings
|
||||
/** 保存回调:把编辑后的 settings 回传父组件 */
|
||||
onSave: (settings: TitleSettings, presetKey: string | null) => void
|
||||
/** 当前选中的爆款预设 key(用于高亮) */
|
||||
activePreset?: string | null
|
||||
}
|
||||
|
||||
/** 把预设应用到 base settings(参考 useTitleStyleUpdaters.applyPreset 逻辑) */
|
||||
function applyPresetToSettings(base: TitleSettings, presetKey: string): TitleSettings {
|
||||
const preset = TITLE_PRESETS_DATA.find((p) => p.key === presetKey)
|
||||
if (!preset) return base
|
||||
return {
|
||||
...base,
|
||||
...(titleStyleConfigToCamel(preset.style) as Partial<TitleSettings>),
|
||||
lineOverrides: [],
|
||||
}
|
||||
}
|
||||
|
||||
const TitleStyleModal: React.FC<TitleStyleModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
titleText = "标题",
|
||||
settings,
|
||||
onSave,
|
||||
activePreset: externalActivePreset = null,
|
||||
}) => {
|
||||
// 弹窗内维护一份草稿 settings,点保存才提交到父组件;取消/关闭丢弃
|
||||
const [draft, setDraft] = useState<TitleSettings>(settings)
|
||||
const [activePreset, setActivePreset] = useState<string | null>(externalActivePreset)
|
||||
|
||||
// 每次打开弹窗,用父组件最新 settings 重置草稿
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDraft(settings)
|
||||
setActivePreset(externalActivePreset)
|
||||
}
|
||||
}, [open, settings, externalActivePreset])
|
||||
|
||||
const handleApplyPreset = (presetKey: string) => {
|
||||
setActivePreset(presetKey)
|
||||
const next = applyPresetToSettings(draft, presetKey)
|
||||
setDraft(next)
|
||||
}
|
||||
|
||||
const handleUpdateStyle = (patch: Partial<TitleSettings>) => {
|
||||
setDraft((prev) => ({ ...prev, ...patch }))
|
||||
// 手动调了参数,清除预设高亮(因为不再严格匹配某个预设)
|
||||
setActivePreset(null)
|
||||
}
|
||||
|
||||
const handleUpdatePosition = (position: string) =>
|
||||
handleUpdateStyle({ position: position as TitleSettings["position"] })
|
||||
const handleUpdateFont = (font: string) => handleUpdateStyle({ font })
|
||||
const handleUpdateSize = (size: number) => handleUpdateStyle({ size })
|
||||
const handleToggleBold = () => handleUpdateStyle({ bold: !draft.bold })
|
||||
const handleToggleItalic = () => handleUpdateStyle({ italic: !draft.italic })
|
||||
const handleToggleStroke = () => handleUpdateStyle({ stroke: !draft.stroke })
|
||||
const handleToggleShadow = () => handleUpdateStyle({ shadow: !draft.shadow })
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(draft, activePreset)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onCancel={onClose} title="标题样式" width={720} footer={null} destroyOnClose>
|
||||
<div className="tsm-body">
|
||||
{/* 顶部预览 */}
|
||||
<div className="tsm-preview">
|
||||
<TitleMiniPreview
|
||||
settings={draft}
|
||||
width={640}
|
||||
sampleText={titleText || "标题"}
|
||||
transparent={false}
|
||||
portrait
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey="presets"
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: "presets",
|
||||
label: "爆款预设",
|
||||
children: (
|
||||
<div className="tsm-presets-grid">
|
||||
{TITLE_PRESETS_DATA.map((p) => {
|
||||
const previewSettings = applyPresetToSettings(draft, p.key)
|
||||
const isActive = activePreset === p.key
|
||||
return (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
className={`tsm-preset-card${isActive ? " active" : ""}`}
|
||||
onClick={() => handleApplyPreset(p.key)}
|
||||
title={p.label}
|
||||
>
|
||||
<div className="tsm-preset-preview">
|
||||
<TitleMiniPreview
|
||||
settings={previewSettings}
|
||||
width={140}
|
||||
sampleText="标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="tsm-preset-meta">
|
||||
<span className="tsm-preset-emoji">{p.emoji}</span>
|
||||
<span className="tsm-preset-label">{p.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
label: "自定义",
|
||||
children: (
|
||||
<div className="tsm-custom-panel">
|
||||
<TitleStylePanel
|
||||
settings={draft}
|
||||
onUpdatePosition={handleUpdatePosition}
|
||||
onUpdateFont={handleUpdateFont}
|
||||
onUpdateSize={handleUpdateSize}
|
||||
onToggleBold={handleToggleBold}
|
||||
onToggleItalic={handleToggleItalic}
|
||||
onToggleStroke={handleToggleStroke}
|
||||
onToggleShadow={handleToggleShadow}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
onUpdateStyle={handleUpdateStyle}
|
||||
activePreset={activePreset}
|
||||
titlePresets={[]}
|
||||
POSITION_OPTIONS={POSITION_OPTIONS}
|
||||
FONT_OPTIONS={FONT_OPTIONS}
|
||||
paramsOnly
|
||||
hideEditor
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮栏 */}
|
||||
<div className="tsm-footer">
|
||||
<Button buttonType="ghost" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button buttonType="primary" onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TitleStyleModal
|
||||
Reference in New Issue
Block a user