fix(title): 标题模板v3全面审计修复——选模板预览不变/新建保存变白 #2020

Merged
xiaoxia merged 1 commits from fix/template-v3-audit into develop 2026-09-23 15:53:26 +08:00
5 changed files with 1722 additions and 3150 deletions
+2
View File
@@ -23,6 +23,8 @@
"@tanstack/react-query": "^5.45.0",
"antd": "^5.18.0",
"axios": "^1.7.2",
"classnames": "^2.5.1",
"dayjs": "^1.11.23",
"mp4box": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+1642 -3087
View File
File diff suppressed because it is too large Load Diff
@@ -16,21 +16,21 @@ import { FONT_OPTIONS } from "./constants"
import type { TitleSettings } from "../../pages/generate/types"
import { DEFAULT_TITLE_SETTINGS_FULL } from "../../pages/generate/types"
import { titleStyleConfigToCamel, camelToTitleStyleConfig } from "./utils"
import { useTitleTemplates } from "./useTitleTemplates"
import type { TitleTemplate } from "./template-types"
import type { TitleStyleConfig } from "./types"
interface Props {
open: boolean
template: TitleTemplate
onClose: () => void
onSaved: (template: TitleTemplate) => void
/** 用户点击保存:将编辑结果回调给父组件(父组件统一做 CRUD,避免双 hook 实例不同步) */
onSave: (data: { name: string; emoji: string; style: Partial<TitleStyleConfig> }) => void
}
/** 编辑器预览用的背景图(复用卡片池第一张) */
const EDITOR_BG = "/title-templates/portrait1.jpg"
const TitleTemplateEditor: React.FC<Props> = ({ open, template, onClose, onSaved }) => {
const { updateTemplate, createTemplate } = useTitleTemplates()
const TitleTemplateEditor: React.FC<Props> = ({ open, template, onClose, onSave }) => {
const [settings, setSettings] = useState<TitleSettings>(() => ({
...DEFAULT_TITLE_SETTINGS_FULL,
...titleStyleConfigToCamel(template.style || {}),
@@ -63,19 +63,7 @@ const TitleTemplateEditor: React.FC<Props> = ({ open, template, onClose, onSaved
setSaving(true)
try {
const snake = camelToTitleStyleConfig(settings)
if (template.isBuiltin) {
const t = createTemplate({ name, emoji: formEmoji, style: snake })
onSaved(t)
} else {
updateTemplate(template.id, { name, emoji: formEmoji, style: snake })
onSaved({
...template,
name,
emoji: formEmoji,
style: snake,
updatedAt: new Date().toISOString(),
})
}
onSave({ name, emoji: formEmoji, style: snake })
} finally {
setSaving(false)
}
@@ -89,7 +77,13 @@ const TitleTemplateEditor: React.FC<Props> = ({ open, template, onClose, onSaved
return (
<Modal
title={template.isBuiltin ? `基于「${template.name}」创建模板` : `编辑模板:${template.name}`}
title={
!template.id
? "新建模板"
: template.isBuiltin
? `基于「${template.name}」创建模板`
: `编辑模板:${template.name}`
}
open={open}
onCancel={onClose}
width={860}
@@ -127,6 +127,7 @@ export interface TitlePreset {
}
const BASE: Partial<TitleStyleConfig> = {
position: "bottom",
line_overrides: [],
cover_title_config: null,
}
@@ -34,9 +34,14 @@ import { buildPresetPreviewSettings } from "@/components/title/utils"
import TitleMiniPreview from "./TitleMiniPreview"
import TitleTemplateEditor from "@/components/title/TitleTemplateEditor"
import { useTitleTemplates } from "@/components/title/useTitleTemplates"
import { titleStyleConfigToCamel, templateToPreviewSettings } from "@/components/title/utils"
import {
titleStyleConfigToCamel,
templateToPreviewSettings,
camelToTitleStyleConfig,
} from "@/components/title/utils"
import { DEFAULT_TITLE_SETTINGS_FULL } from "../../types"
import type { TitleTemplate } from "@/components/title/template-types"
import type { TitleStyleConfig } from "@/components/title/types"
import "./TitleStylePanel.css"
import "@/components/title/TitleTemplate.css"
@@ -146,13 +151,20 @@ const ColorPicker: React.FC<{
type="color"
className="ts-color-native"
value={value.startsWith("rgba") ? "#000000" : value}
onChange={(e) => onChange(e.target.value)}
onChange={(e) => {
onChange(e.target.value)
setCustomOpen(false)
}}
style={{
opacity: customOpen ? 1 : 0,
position: customOpen ? "static" : "absolute",
pointerEvents: customOpen ? "auto" : "none",
width: 0,
height: 0,
width: customOpen ? 28 : 0,
height: customOpen ? 28 : 0,
border: "none",
padding: 0,
cursor: "pointer",
background: "transparent",
}}
/>
</div>
@@ -231,8 +243,14 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
const [coverOpen, setCoverOpen] = useState(!!settings.coverTitle)
/* ── 模板相关 hook + 编辑器 state ── */
const { templates, createTemplate, duplicateTemplate, deleteTemplate, exportTemplate } =
useTitleTemplates()
const {
templates,
createTemplate,
duplicateTemplate,
updateTemplate,
deleteTemplate,
exportTemplate,
} = useTitleTemplates()
const [editingTemplate, setEditingTemplate] = useState<TitleTemplate | null>(null)
const [editorOpen, setEditorOpen] = useState(false)
@@ -246,6 +264,7 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
const handleRequestCreate = useCallback(() => {
// 新建模板:若已有选中模板,以其样式为起点;否则以当前 settings 为起点
// 注意:这里只构造「草稿」对象(不写入 localStorage),保存时再由 handleEditorSave 真正 createTemplate
let base: TitleSettings = settings
if (selectedTemplateId) {
const sel = templates.find((t) => t.id === selectedTemplateId)
@@ -259,40 +278,21 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
}
}
}
const t = createTemplate({
const draft: TitleTemplate = {
id: "", // 空 id 表示新建草稿
name: "我的标题模板",
style: {
font: base.font,
size: base.size,
color: base.color,
bold: base.bold,
italic: base.italic,
stroke: base.stroke,
stroke_width: base.strokeWidth ?? 4,
stroke_color: base.strokeColor ?? "#000000",
shadow: base.shadow,
shadow_offset_x: base.shadowOffsetX ?? 2,
shadow_offset_y: base.shadowOffsetY ?? 2,
shadow_blur: base.shadowBlur ?? 4,
shadow_color: base.shadowColor ?? "rgba(0,0,0,0.8)",
bg_enabled: base.bgEnabled,
bg_color: base.bgColor,
bg_padding: base.bgPadding,
bg_radius: base.bgRadius,
line_height: base.lineHeight,
margin_top: base.marginTop,
max_chars_per_line: base.maxCharsPerLine,
position: (base.position === "custom" ? "bottom" : base.position) as
"top" | "center" | "bottom" | "custom",
pos_x: base.posX ?? undefined,
pos_y: base.posY ?? undefined,
line_overrides: [],
cover_title_config: null,
},
})
setEditingTemplate(t)
emoji: "✨",
isBuiltin: false,
style: camelToTitleStyleConfig({
...base,
position: base.position === "custom" ? "bottom" : base.position,
} as TitleSettings),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
setEditingTemplate(draft)
setEditorOpen(true)
}, [createTemplate, settings, selectedTemplateId, templates])
}, [settings, selectedTemplateId, templates])
const handleRequestEdit = useCallback((tpl: TitleTemplate) => {
setEditingTemplate(tpl)
@@ -342,14 +342,34 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
[onApplyTemplate, settings],
)
const handleEditorSaved = useCallback(
(tpl: TitleTemplate) => {
const handleEditorSave = useCallback(
(data: { name: string; emoji: string; style: Partial<TitleStyleConfig> }) => {
if (!editingTemplate) return
let saved: TitleTemplate
if (editingTemplate.isBuiltin || !editingTemplate.id) {
// 系统模板编辑 → 创建副本;新草稿(空 id)→ 新建
saved = createTemplate({ name: data.name, emoji: data.emoji, style: data.style })
} else {
// 已有自定义模板 → 更新
updateTemplate(editingTemplate.id, {
name: data.name,
emoji: data.emoji,
style: data.style,
})
saved = {
...editingTemplate,
name: data.name,
emoji: data.emoji,
style: data.style,
updatedAt: new Date().toISOString(),
}
}
setEditorOpen(false)
setEditingTemplate(null)
message.success(`已保存:${tpl.name}`)
handleSelectTemplate(tpl)
message.success(`已保存:${data.name}`)
handleSelectTemplate(saved)
},
[handleSelectTemplate],
[editingTemplate, createTemplate, updateTemplate, handleSelectTemplate],
)
/* ── 渲染模板大卡片(v3:图片背景 + 透明 Canvas 叠字 + 底栏始终可见操作按钮) ── */
@@ -811,7 +831,7 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
setEditorOpen(false)
setEditingTemplate(null)
}}
onSaved={handleEditorSaved}
onSave={handleEditorSave}
/>
)}
</div>
@@ -854,7 +874,7 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
setEditorOpen(false)
setEditingTemplate(null)
}}
onSaved={handleEditorSaved}
onSave={handleEditorSave}
/>
)}
</div>