diff --git a/apps/web/src/components/cover/useSharedCover.ts b/apps/web/src/components/cover/useSharedCover.ts index 5f88e8e2b..063fdfcdd 100644 --- a/apps/web/src/components/cover/useSharedCover.ts +++ b/apps/web/src/components/cover/useSharedCover.ts @@ -76,8 +76,16 @@ export function useSharedCover(opts: UseSharedCoverOptions): UseSharedCoverRetur setTemplatesLoading(true) setTemplatesError(null) try { - const res = await fetchCoverTemplates() - setTemplates(res.items || []) + const res: any = await fetchCoverTemplates() + // 兼容两种响应:{items:[...]} 或直接数组 + const list = Array.isArray(res) ? res : res?.items || [] + // 过滤掉后端可能返回的没有 config 的模板(避免编辑器打开时访问 .title.text 崩溃) + setTemplates( + list.map((t: any) => ({ + ...t, + config: t.config || undefined, + })), + ) } catch (err) { console.error("[SharedCover] 加载封面模板失败:", err) setTemplatesError("加载模板失败,请稍后重试") diff --git a/apps/web/src/pages/generate/components/cover-settings/CoverEditorModal.tsx b/apps/web/src/pages/generate/components/cover-settings/CoverEditorModal.tsx index 24062538b..6accdeaf8 100644 --- a/apps/web/src/pages/generate/components/cover-settings/CoverEditorModal.tsx +++ b/apps/web/src/pages/generate/components/cover-settings/CoverEditorModal.tsx @@ -12,6 +12,31 @@ import Modal from "@/components/ui/Modal" import Button from "@/components/ui/Button" import "@/components/cover/cover.css" +/** 安全合并模板 config 与默认配置(深拷贝 title/subtitle/shadows/background 等嵌套对象,避免共享引用 & 字段缺失) */ +const mergeEditorConfig = (partial?: Partial | null): CoverEditorConfig => { + const def = DEFAULT_EDITOR_CONFIG + const src = partial || {} + const mergeText = ( + base: TextStyleConfig, + patch?: Partial | null, + ): TextStyleConfig => ({ + ...base, + ...(patch || {}), + position: { ...base.position, ...(patch?.position || {}) }, + background: { ...base.background, ...(patch?.background || {}) }, + shadows: Array.isArray(patch?.shadows) ? [...patch!.shadows] : [...base.shadows], + }) + return { + ...def, + ...src, + title: mergeText(def.title, src.title), + subtitle: mergeText(def.subtitle, src.subtitle), + portraitPosition: { ...def.portraitPosition, ...(src.portraitPosition || {}) }, + backgroundPosition: { ...def.backgroundPosition, ...(src.backgroundPosition || {}) }, + maskPosition: { ...def.maskPosition, ...(src.maskPosition || {}) }, + } +} + /* ── Props ── */ interface CoverEditorModalProps { open: boolean @@ -98,10 +123,11 @@ const getFontFamily = (name: string): string => { /* ── Text style panel (shared between title & subtitle) ── */ const TextStylePanel: React.FC<{ - config: TextStyleConfig + config: TextStyleConfig | undefined | null onChange: (c: TextStyleConfig) => void placeholder: string }> = ({ config, onChange, placeholder }) => { + if (!config) return null const upd = (key: K, val: TextStyleConfig[K]) => onChange({ ...config, [key]: val }) return ( @@ -274,9 +300,9 @@ const TextStylePanel: React.FC<{ /* ══════════════ Main Component ══════════════ */ const CoverEditorModal: React.FC = ({ open, onClose, template, onSave }) => { - const initCfg = template?.config ?? DEFAULT_EDITOR_CONFIG + const initCfg = mergeEditorConfig(template?.config) const [name, setName] = useState(template?.name ?? "") - const [cfg, setCfg] = useState(() => ({ ...initCfg })) + const [cfg, setCfg] = useState(() => mergeEditorConfig(template?.config)) const [sections, setSections] = useState>({ basic: true, portrait: false, @@ -294,8 +320,8 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp // reset state when modal opens with a new template useEffect(() => { if (open) { - const c = template?.config ?? DEFAULT_EDITOR_CONFIG - setCfg({ ...c }) + const c = mergeEditorConfig(template?.config) + setCfg(c) setName(template?.name ?? "") setBgImageUrl(c.backgroundImage || "") setPortraitImageUrl(c.portraitImage || "") @@ -366,7 +392,8 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp const s = (px: number) => Math.round(px * SCALE * 100) / 100 - const renderTextStyle = (tc: TextStyleConfig): React.CSSProperties => { + const renderTextStyle = (tc: TextStyleConfig | undefined | null): React.CSSProperties => { + if (!tc) return {} // wrap text by charsPerLine const rawText = tc.text || "" const lines: string[] = [] @@ -407,7 +434,8 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp return style } - const renderTextLines = (tc: TextStyleConfig) => { + const renderTextLines = (tc: TextStyleConfig | undefined | null): string => { + if (!tc) return "" const raw = tc.text || "" if (tc.direction === "vertical") return raw const per = Math.max(1, tc.charsPerLine) @@ -608,7 +636,7 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp {sections.title && (
@@ -625,7 +653,7 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp {sections.subtitle && (
@@ -887,10 +915,14 @@ const CoverEditorModal: React.FC = ({ open, onClose, temp )} {/* Title */} -
{renderTextLines(cfg.title)}
+ {cfg.title && ( +
{renderTextLines(cfg.title)}
+ )} {/* Subtitle */} -
{renderTextLines(cfg.subtitle)}
+ {cfg.subtitle && ( +
{renderTextLines(cfg.subtitle)}
+ )} {/* Mask overlay */} {cfg.maskEnabled && (