2b4bcefc57
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m4s
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 / PR Build API Image (pull_request) Successful in 3m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m31s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m59s
CI/CD Pipeline / Validate - Style (push) Successful in 4m28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m41s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
AI Code Review / AI Code Review (pull_request) Successful in 7m25s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m27s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m9s
CI/CD Pipeline / Validate - Security (push) Successful in 8m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m29s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m4s
CI/CD Pipeline / Unit Tests (push) Successful in 12m34s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
181 lines
6.4 KiB
TypeScript
181 lines
6.4 KiB
TypeScript
/**
|
|
* 标题模板编辑器(v3 重构)
|
|
*
|
|
* - Modal 弹窗 860px 宽
|
|
* - 左侧:300px 竖屏预览区(图片背景+暗色渐变遮罩+透明 Canvas 叠字)+ 模板名称输入框
|
|
* - 右侧:参数 Tab 面板(基础/描边/阴影/背景/排版),复用 TitleStylePanel 的 paramsOnly 模式
|
|
* - 底部:取消 / 保存模板 按钮
|
|
* - 内置模板编辑时保存会创建副本(带"副本"逻辑由 handleSave 处理)
|
|
*/
|
|
import React, { useEffect, useMemo, useState } from "react"
|
|
import { Modal, Button, Input, message } from "antd"
|
|
import TitleStylePanel from "../../pages/generate/components/title/TitleStylePanel"
|
|
import TitleMiniPreview from "../../pages/generate/components/title/TitleMiniPreview"
|
|
import { POSITION_OPTIONS } from "../../pages/generate/constants"
|
|
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"
|
|
|
|
interface Props {
|
|
open: boolean
|
|
template: TitleTemplate
|
|
onClose: () => void
|
|
onSaved: (template: TitleTemplate) => void
|
|
}
|
|
|
|
/** 编辑器预览用的背景图(复用卡片池第一张) */
|
|
const EDITOR_BG = "/title-templates/portrait1.jpg"
|
|
|
|
const TitleTemplateEditor: React.FC<Props> = ({ open, template, onClose, onSaved }) => {
|
|
const { updateTemplate, createTemplate } = useTitleTemplates()
|
|
const [settings, setSettings] = useState<TitleSettings>(() => ({
|
|
...DEFAULT_TITLE_SETTINGS_FULL,
|
|
...titleStyleConfigToCamel(template.style || {}),
|
|
title: "预览标题文字",
|
|
}))
|
|
const [formName, setFormName] = useState(template.name || "")
|
|
const [formEmoji, setFormEmoji] = useState(template.emoji || "✨")
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setSettings({
|
|
...DEFAULT_TITLE_SETTINGS_FULL,
|
|
...titleStyleConfigToCamel(template.style || {}),
|
|
title: "预览标题文字",
|
|
})
|
|
setFormName(template.name || "")
|
|
setFormEmoji(template.emoji || "✨")
|
|
}
|
|
}, [open, template])
|
|
|
|
const upd = (patch: Partial<TitleSettings>) => setSettings((s) => ({ ...s, ...patch }))
|
|
|
|
const handleSave = () => {
|
|
const name = formName.trim()
|
|
if (!name) {
|
|
message.warning("请填写模板名称")
|
|
return
|
|
}
|
|
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(),
|
|
})
|
|
}
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
// 编辑器内的预览用 settings:字号适配竖屏
|
|
const previewSettings = useMemo<TitleSettings>(() => {
|
|
// 竖屏宽度 200px,按比例缩放字号,让预览看起来协调
|
|
return { ...settings, size: Math.round(settings.size * 0.55) }
|
|
}, [settings])
|
|
|
|
return (
|
|
<Modal
|
|
title={template.isBuiltin ? `基于「${template.name}」创建模板` : `编辑模板:${template.name}`}
|
|
open={open}
|
|
onCancel={onClose}
|
|
width={860}
|
|
footer={
|
|
<div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
|
|
<Button onClick={onClose}>取消</Button>
|
|
<Button type="primary" loading={saving} onClick={handleSave}>
|
|
保存模板
|
|
</Button>
|
|
</div>
|
|
}
|
|
destroyOnClose
|
|
className="ttv3-modal"
|
|
styles={{ body: { padding: 0 } }}
|
|
>
|
|
<div className="ttv3-editor">
|
|
{/* 左侧:竖屏预览 + 名称 */}
|
|
<div className="ttv3-editor-left">
|
|
<div className="ttv3-editor-canvas">
|
|
<img className="ttv3-editor-bg" src={EDITOR_BG} alt="" />
|
|
<div className="ttv3-vignette" />
|
|
<div className="ttv3-editor-canvas-inner">
|
|
<TitleMiniPreview
|
|
settings={previewSettings}
|
|
width={200}
|
|
sampleText="预览标题文字"
|
|
transparent
|
|
portrait
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="ttv3-editor-form">
|
|
<div className="ttv3-form-row">
|
|
<label>图标</label>
|
|
<Input
|
|
value={formEmoji}
|
|
maxLength={2}
|
|
style={{ textAlign: "center", width: 64 }}
|
|
onChange={(e) => setFormEmoji(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="ttv3-form-row ttv3-form-row--grow">
|
|
<label>
|
|
模板名称<span style={{ color: "#ef4444" }}>*</span>
|
|
</label>
|
|
<Input
|
|
placeholder="给模板起个名字"
|
|
value={formName}
|
|
maxLength={20}
|
|
onChange={(e) => setFormName(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* 右侧:参数 Tab */}
|
|
<div className="ttv3-editor-right">
|
|
<TitleStylePanel
|
|
settings={settings}
|
|
onUpdatePosition={(p) => upd({ position: p, posX: null, posY: null })}
|
|
onUpdateFont={(f) => upd({ font: f })}
|
|
onUpdateSize={(v) => upd({ size: v })}
|
|
onToggleBold={() => upd({ bold: !settings.bold })}
|
|
onToggleItalic={() => upd({ italic: !settings.italic })}
|
|
onToggleStroke={() =>
|
|
upd({
|
|
stroke: !settings.stroke,
|
|
strokeWidth:
|
|
!settings.stroke && (settings.strokeWidth ?? 0) < 2 ? 4 : settings.strokeWidth,
|
|
})
|
|
}
|
|
onToggleShadow={() => upd({ shadow: !settings.shadow })}
|
|
onApplyPreset={() => {
|
|
/* 编辑器内不使用系统预设快捷键 */
|
|
}}
|
|
onUpdateStyle={(patch) => upd(patch)}
|
|
activePreset={null}
|
|
titlePresets={[]}
|
|
POSITION_OPTIONS={POSITION_OPTIONS}
|
|
FONT_OPTIONS={FONT_OPTIONS}
|
|
paramsOnly
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export default TitleTemplateEditor
|