/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv),3=回门宴(/hb-hm) */ export const POSTER_SIDES = [ { key: 1, keyStr: '1', route: '/hb-n', label: '男方', short: 'n' }, { key: 2, keyStr: '2', route: '/hb-nv', label: '女方', short: 'nv' }, { key: 3, keyStr: '3', route: '/hb-hm', label: '回门宴', short: 'hm' }, ] export const POSTER_TEMPLATES = [ { value: 'classic', label: '经典竖版', desc: '顶图 + 正文逐行出现' }, { value: 'split', label: '左图右文', desc: '竖排书法 + 右侧宴席信息' }, ] /** 手机端布局:仅影响窄屏;PC 仍按模板本身版式 */ export const MOBILE_LAYOUTS = [ { value: 'auto', label: '自动布局', desc: '按屏幕宽度自动切换(当前默认)' }, { value: 'portrait', label: '竖向展示', desc: '手机端强制上下排布' }, { value: 'landscape', label: '横屏展示', desc: '手机端强制左右分栏' }, ] /** 解析为 1 | 2 | 3,无法识别返回 0 */ export function resolvePosterSide(input) { if (input === 1 || input === '1') return 1 if (input === 2 || input === '2') return 2 if (input === 3 || input === '3') return 3 const v = String(input || '').trim().toLowerCase() if (v === 'hm' || v === 'hmy' || v === 'return' || v === 'huimen') return 3 if (v === 'female' || v === 'nv' || v === 'f' || v === 'bride') return 2 if (v === 'male' || v === 'n' || v === 'm' || v === 'groom') return 1 return 0 } export function sideKey(side) { const s = resolvePosterSide(side) || 1 return String(s) } export function posterSideMeta(side) { const s = resolvePosterSide(side) || 1 return POSTER_SIDES.find((x) => x.key === s) || POSTER_SIDES[0] } function baseFields(side) { return { side: resolvePosterSide(side) || 1, template: 'classic', mobileLayout: 'auto', enabled: true, heroImg: '', /** 二维码中心图;空则显示圆圈大囍 */ qrCenterImg: '', title: '婚礼邀请函', /** 浏览器标签 / 分享用页面标题与描述(可与海报主标题不同) */ pageTitle: '', pageDescription: '', footer: '—诚挚邀请 ♥ 恭请光临—', loadingEffect: 'redSeal', enterEffect: 'fadeUp', showInviteButton: true, inviteButtonText: '打开请柬', invitePath: '/', // split 模板字段 verticalSlogan: '吾家有喜', subSlogan1: '佳偶天成', subSlogan2: '百年好合', greeting: '尊敬的各位亲朋好友', groomName: '', brideName: '', dateText: '2026年10月3日 星期日', lunarText: '农历九月初八', venueText: '幸福大酒店二楼宴会厅', loveFooter: 'Love you', } } /** 男方(1) 默认 */ export function defaultSide1PosterConfig() { return { ...baseFields(1), side: 1, pageTitle: '💕李琦❤️李逸烁 · 男方婚礼邀请', pageDescription: '💍公历2026年9月4日(农历七月廿三)· 李琦 & 李逸烁 结婚典礼,诚邀您光临。', sonLabel: '儿子:', sonName: '李琦', daughterInLawLabel: '儿媳:', daughterInLawName: '李逸烁', groomName: '李琦', brideName: '李逸烁', verticalSlogan: '吾家有喜', dateText: '2026年9月4日 星期五', lunarText: '农历七月廿三', venueText: '山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', lines: [ '各位亲朋好友', '吾家有喜 儿子结婚', '良辰已定 吉日待访', '兹定于2026年9月4日 星期五', '农历七月廿三 中午11:08分', '为儿子儿媳举办结婚典礼', '敬备喜宴 诚邀您携家人光临', '地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 二楼', '李国贞 & 赵红燕 夫 妇 敬邀', ], } } /** 女方(2) 默认 */ export function defaultSide2PosterConfig() { return { ...baseFields(2), side: 2, template: 'split', mobileLayout: 'landscape', pageTitle: '💕李琦❤️李逸烁 · 女方婚礼邀请', pageDescription: '💍公历2026年9月4日(农历七月廿三)· 李逸烁 & 李琦 结婚典礼,诚邀您光临。', sonLabel: '女儿:', sonName: '李逸烁', daughterInLawLabel: '女婿:', daughterInLawName: '李琦', groomName: '李琦', brideName: '李逸烁', verticalSlogan: '小女出阁', dateText: '2026年9月4日 星期五', lunarText: '农历七月廿三', venueText: '山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', lines: [ '各位亲朋好友', '吾家有喜 女儿出嫁', '良辰已定 吉日待访', '兹定于2026年9月4日 星期五', '农历七月廿三 中午11:08分', '为女儿女婿举办结婚典礼', '敬备喜宴 诚邀您携家人光临', '地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', '刘建国 & 王秀兰 夫 妇 敬邀', ], } } /** 回门宴(3) 默认:9月7日 */ export function defaultSide3PosterConfig() { return { ...baseFields(3), side: 3, template: 'classic', title: '回门宴邀请函', pageTitle: '💕李琦❤️李逸烁 · 回门宴邀请', pageDescription: '💍公历2026年9月7日(农历七月廿六)· 李琦 & 李逸烁 回门宴,诚邀您光临。', footer: '—回门宴请 ♥ 恭请光临—', sonLabel: '新郎:', sonName: '李琦', daughterInLawLabel: '新娘:', daughterInLawName: '李逸烁', groomName: '李琦', brideName: '李逸烁', verticalSlogan: '回门之喜', subSlogan1: '琴瑟和鸣', subSlogan2: '永结同心', dateText: '2026年9月7日 星期一', lunarText: '农历七月廿六', venueText: '山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', enterEffect: 'curtain', loadingEffect: 'redSeal', lines: [ '各位亲朋好友', '新婚燕尔 回门宴请', '良辰已定 吉日待访', '兹定于2026年9月7日 星期一', '农历七月廿六 中午11:08分', '为新婚夫妇举办回门宴', '敬备薄酒 诚邀您携家人光临', '地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼', '李琦 & 李逸烁 敬邀', ], } } export function defaultMalePosterConfig() { return defaultSide1PosterConfig() } export function defaultFemalePosterConfig() { return defaultSide2PosterConfig() } export function defaultPosterConfig() { return defaultSide1PosterConfig() } export function defaultPosterBundle() { return { '1': defaultSide1PosterConfig(), '2': defaultSide2PosterConfig(), '3': defaultSide3PosterConfig(), } } export function defaultPosterForSide(side) { const s = resolvePosterSide(side) || 1 if (s === 3) return defaultSide3PosterConfig() if (s === 2) return defaultSide2PosterConfig() return defaultSide1PosterConfig() } export const LOADING_EFFECTS = [ { value: 'fade', label: '淡入加载' }, { value: 'redSeal', label: '红印囍字' }, { value: 'goldShimmer', label: '金字微光' }, ] export const ENTER_EFFECTS = [ { value: 'fadeUp', label: '上浮淡入' }, { value: 'scaleIn', label: '缩放进入' }, { value: 'curtain', label: '金幕拉开' }, ] function isLegacyFlatPoster(raw) { if (!raw || typeof raw !== 'object') return false if (raw['1'] || raw['2'] || raw['3'] || raw[1] || raw[2] || raw[3] || raw.male || raw.female) return false return 'title' in raw || 'lines' in raw || 'heroImg' in raw || 'sonName' in raw || 'template' in raw } function pickSideSource(loaded, side) { const k = sideKey(side) if (loaded[k] && typeof loaded[k] === 'object') return loaded[k] if (side === 1 && loaded.male && typeof loaded.male === 'object') return loaded.male if (side === 2 && loaded.female && typeof loaded.female === 'object') return loaded.female if (side === 3 && loaded.hm && typeof loaded.hm === 'object') return loaded.hm return null } export function normalizePosterConfig(raw, side = 1) { const s = resolvePosterSide(side) || 1 const base = defaultPosterForSide(s) const loaded = raw && typeof raw === 'object' ? raw : {} const lines = Array.isArray(loaded.lines) ? loaded.lines.map((l) => String(l ?? '')) : base.lines const template = POSTER_TEMPLATES.some((t) => t.value === loaded.template) ? loaded.template : base.template const mobileLayout = MOBILE_LAYOUTS.some((t) => t.value === loaded.mobileLayout) ? loaded.mobileLayout : base.mobileLayout const groomName = loaded.groomName || loaded.sonName || base.groomName const brideName = loaded.brideName || loaded.daughterInLawName || base.brideName return { ...base, ...loaded, side: s, template, mobileLayout, lines: lines.length ? lines : base.lines, groomName, brideName, enabled: loaded.enabled !== false, showInviteButton: loaded.showInviteButton !== false, loadingEffect: LOADING_EFFECTS.some((e) => e.value === loaded.loadingEffect) ? loaded.loadingEffect : base.loadingEffect, enterEffect: ENTER_EFFECTS.some((e) => e.value === loaded.enterEffect) ? loaded.enterEffect : base.enterEffect, invitePath: loaded.invitePath || '/', inviteButtonText: loaded.inviteButtonText || base.inviteButtonText, pageTitle: loaded.pageTitle || base.pageTitle || '', pageDescription: loaded.pageDescription || base.pageDescription || '', qrCenterImg: loaded.qrCenterImg || '', verticalSlogan: loaded.verticalSlogan || base.verticalSlogan, subSlogan1: loaded.subSlogan1 || base.subSlogan1, subSlogan2: loaded.subSlogan2 || base.subSlogan2, greeting: loaded.greeting || base.greeting, dateText: loaded.dateText || base.dateText, lunarText: loaded.lunarText || base.lunarText, venueText: loaded.venueText || base.venueText, loveFooter: loaded.loveFooter || base.loveFooter, } } /** 归一化为 { 1, 2, 3 };兼容旧 male/female 与扁平数据 */ export function normalizePosterBundle(raw) { const loaded = raw && typeof raw === 'object' ? raw : {} if (isLegacyFlatPoster(loaded)) { return { '1': normalizePosterConfig(loaded, 1), '2': defaultSide2PosterConfig(), '3': defaultSide3PosterConfig(), } } return { '1': normalizePosterConfig(pickSideSource(loaded, 1) || {}, 1), '2': normalizePosterConfig(pickSideSource(loaded, 2) || {}, 2), '3': normalizePosterConfig(pickSideSource(loaded, 3) || {}, 3), } } export function pickPosterSide(bundle, side) { const normalized = normalizePosterBundle(bundle) const s = sideKey(side) return normalized[s] }