Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 577f0c58c5 |
@@ -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",
|
||||
|
||||
Generated
+1642
-3087
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>
|
||||
|
||||
Reference in New Issue
Block a user