Files
hunli/vite-tailwindcss/src/composables/posterDefaults.js

510 lines
17 KiB
JavaScript
Raw Normal View History

2026-08-03 10:22:04 +08:00
/** 海报 side1=男方(/hb-n)2=女方(/hb-nv)3=回门宴(/hb-hm) */
2026-08-03 09:31:32 +08:00
export const POSTER_SIDES = [
2026-08-03 09:54:43 +08:00
{ key: 1, keyStr: '1', route: '/hb-n', label: '男方', short: 'n' },
{ key: 2, keyStr: '2', route: '/hb-nv', label: '女方', short: 'nv' },
2026-08-03 10:22:04 +08:00
{ key: 3, keyStr: '3', route: '/hb-hm', label: '回门宴', short: 'hm' },
2026-08-03 09:31:32 +08:00
]
2026-08-03 16:00:13 +08:00
/** 布局1=竖版2=横版URL 后缀 /1 /2 */
export const POSTER_LAYOUTS = [
{ value: '1', label: '竖版', desc: '经典竖向 / portrait', mobileLayout: 'portrait', template: 'classic' },
{ value: '2', label: '横版', desc: '左图右文 / landscape', mobileLayout: 'landscape', template: 'split' },
]
2026-08-03 09:54:43 +08:00
export const POSTER_TEMPLATES = [
{ value: 'classic', label: '经典竖版', desc: '顶图 + 正文逐行出现' },
{ value: 'split', label: '左图右文', desc: '竖排书法 + 右侧宴席信息' },
]
2026-08-03 10:18:23 +08:00
/** 手机端布局仅影响窄屏PC 仍按模板本身版式 */
export const MOBILE_LAYOUTS = [
{ value: 'auto', label: '自动布局', desc: '按屏幕宽度自动切换(当前默认)' },
{ value: 'portrait', label: '竖向展示', desc: '手机端强制上下排布' },
{ value: 'landscape', label: '横屏展示', desc: '手机端强制左右分栏' },
]
2026-08-03 13:19:09 +08:00
/** 后台 9×9 快速选点边长 */
export const HERO_FOCUS_GRID = 9
/** 旧版命名焦点 → 百分比(兼容已保存配置) */
const LEGACY_HERO_FOCUS = {
'top-left': [0, 0],
top: [50, 0],
'top-right': [100, 0],
left: [0, 50],
center: [50, 50],
right: [100, 50],
'bottom-left': [0, 100],
bottom: [50, 100],
'bottom-right': [100, 100],
}
export function clampHeroFocus(n, fallback = 50) {
const v = Number(n)
if (!Number.isFinite(v)) return fallback
return Math.min(100, Math.max(0, Math.round(v * 10) / 10))
}
/** 生成 9×9 网格点 {x,y,key} */
export function heroFocusGridPoints(size = HERO_FOCUS_GRID) {
const n = Math.max(2, size | 0)
const pts = []
for (let row = 0; row < n; row += 1) {
for (let col = 0; col < n; col += 1) {
const x = (col / (n - 1)) * 100
const y = (row / (n - 1)) * 100
pts.push({
key: `${row}-${col}`,
x: clampHeroFocus(x),
y: clampHeroFocus(y),
})
}
}
return pts
}
/**
* object-position CSS
* - heroFocusCss(cfg) cfg.heroFocusX/Y
* - heroFocusCss(x, y) 直接传百分比
* - heroFocusCss('center') 兼容旧字符串
*/
export function heroFocusCss(a, b) {
if (a && typeof a === 'object') {
return `${clampHeroFocus(a.heroFocusX, 50)}% ${clampHeroFocus(a.heroFocusY, 50)}%`
}
if (typeof a === 'string' && LEGACY_HERO_FOCUS[a]) {
const [x, y] = LEGACY_HERO_FOCUS[a]
return `${x}% ${y}%`
}
return `${clampHeroFocus(a, 50)}% ${clampHeroFocus(b, 50)}%`
}
export function resolveHeroFocusXY(loaded = {}) {
const hasXY = Number.isFinite(Number(loaded.heroFocusX)) && Number.isFinite(Number(loaded.heroFocusY))
if (hasXY) {
return {
heroFocusX: clampHeroFocus(loaded.heroFocusX),
heroFocusY: clampHeroFocus(loaded.heroFocusY),
}
}
const legacy = LEGACY_HERO_FOCUS[loaded.heroFocus] || LEGACY_HERO_FOCUS.center
return { heroFocusX: legacy[0], heroFocusY: legacy[1] }
}
2026-08-03 10:22:04 +08:00
/** 解析为 1 | 2 | 3无法识别返回 0 */
2026-08-03 09:31:32 +08:00
export function resolvePosterSide(input) {
2026-08-03 09:54:43 +08:00
if (input === 1 || input === '1') return 1
if (input === 2 || input === '2') return 2
2026-08-03 10:22:04 +08:00
if (input === 3 || input === '3') return 3
2026-08-03 09:31:32 +08:00
const v = String(input || '').trim().toLowerCase()
2026-08-03 10:22:04 +08:00
if (v === 'hm' || v === 'hmy' || v === 'return' || v === 'huimen') return 3
2026-08-03 09:54:43 +08:00
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)
2026-08-03 09:31:32 +08:00
}
2026-08-03 10:22:04 +08:00
export function posterSideMeta(side) {
const s = resolvePosterSide(side) || 1
return POSTER_SIDES.find((x) => x.key === s) || POSTER_SIDES[0]
}
2026-08-03 16:00:13 +08:00
/** 解析布局 1|2无法识别默认 1竖版 */
export function resolvePosterLayout(input) {
if (input === 2 || input === '2') return 2
if (input === 1 || input === '1') return 1
const v = String(input || '').trim().toLowerCase()
if (v === 'landscape' || v === 'h' || v === 'heng' || v === '横版') return 2
if (v === 'portrait' || v === 'v' || v === 'shu' || v === '竖版') return 1
return 1
}
export function layoutKey(layout) {
return String(resolvePosterLayout(layout))
}
export function posterLayoutMeta(layout) {
const k = layoutKey(layout)
return POSTER_LAYOUTS.find((x) => x.value === k) || POSTER_LAYOUTS[0]
}
export function posterRoute(side, layout = 1) {
const meta = posterSideMeta(side)
return `${meta.route}/${layoutKey(layout)}`
}
2026-08-03 09:54:43 +08:00
function baseFields(side) {
2026-08-03 09:31:32 +08:00
return {
2026-08-03 09:54:43 +08:00
side: resolvePosterSide(side) || 1,
2026-08-03 16:00:13 +08:00
layout: 1,
2026-08-03 09:54:43 +08:00
template: 'classic',
2026-08-03 10:18:23 +08:00
mobileLayout: 'auto',
2026-08-03 09:31:32 +08:00
enabled: true,
heroImg: '',
2026-08-03 13:19:09 +08:00
/** 主图焦点百分比 0100object-position */
heroFocusX: 50,
heroFocusY: 50,
2026-08-03 10:18:23 +08:00
/** 二维码中心图;空则显示圆圈大囍 */
qrCenterImg: '',
2026-08-03 09:31:32 +08:00
title: '婚礼邀请函',
2026-08-03 10:22:04 +08:00
/** 浏览器标签 / 分享用页面标题与描述(可与海报主标题不同) */
pageTitle: '',
pageDescription: '',
2026-08-03 09:31:32 +08:00
footer: '—诚挚邀请 ♥ 恭请光临—',
loadingEffect: 'redSeal',
enterEffect: 'fadeUp',
showInviteButton: true,
inviteButtonText: '打开请柬',
invitePath: '/',
2026-08-03 13:01:08 +08:00
/** 背景音乐 URL从请柬 musicList 中选择;空=不播放) */
musicUrl: '',
2026-08-03 09:54:43 +08:00
// split 模板字段
verticalSlogan: '吾家有喜',
subSlogan1: '佳偶天成',
subSlogan2: '百年好合',
greeting: '尊敬的各位亲朋好友',
groomName: '',
brideName: '',
dateText: '2026年10月3日 星期日',
lunarText: '农历九月初八',
venueText: '幸福大酒店二楼宴会厅',
loveFooter: 'Love you',
2026-08-03 09:31:32 +08:00
}
}
2026-08-03 09:54:43 +08:00
/** 男方(1) 默认 */
export function defaultSide1PosterConfig() {
2026-08-03 09:31:32 +08:00
return {
2026-08-03 09:54:43 +08:00
...baseFields(1),
side: 1,
2026-08-03 10:25:11 +08:00
pageTitle: '💕李琦❤️李逸烁 · 男方婚礼邀请',
pageDescription: '💍公历2026年9月4日农历七月廿三· 李琦 & 李逸烁 结婚典礼,诚邀您光临。',
2026-08-03 09:31:32 +08:00
sonLabel: '儿子:',
2026-08-03 10:22:04 +08:00
sonName: '李琦',
2026-08-03 09:31:32 +08:00
daughterInLawLabel: '儿媳:',
2026-08-03 10:22:04 +08:00
daughterInLawName: '李逸烁',
groomName: '李琦',
brideName: '李逸烁',
2026-08-03 09:54:43 +08:00
verticalSlogan: '吾家有喜',
2026-08-03 10:22:04 +08:00
dateText: '2026年9月4日 星期五',
lunarText: '农历七月廿三',
venueText: '山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼',
2026-08-03 09:31:32 +08:00
lines: [
'各位亲朋好友',
'吾家有喜 儿子结婚',
'良辰已定 吉日待访',
2026-08-03 10:22:04 +08:00
'兹定于2026年9月4日 星期五',
'农历七月廿三 中午11:08分',
2026-08-03 09:31:32 +08:00
'为儿子儿媳举办结婚典礼',
'敬备喜宴 诚邀您携家人光临',
2026-08-03 10:22:04 +08:00
'地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 二楼',
'李国贞 & 赵红燕 夫 妇 敬邀',
2026-08-03 09:31:32 +08:00
],
}
}
2026-08-03 09:54:43 +08:00
/** 女方(2) 默认 */
export function defaultSide2PosterConfig() {
2026-08-03 09:31:32 +08:00
return {
2026-08-03 09:54:43 +08:00
...baseFields(2),
side: 2,
2026-08-03 10:22:04 +08:00
template: 'split',
mobileLayout: 'landscape',
2026-08-03 10:25:11 +08:00
pageTitle: '💕李琦❤️李逸烁 · 女方婚礼邀请',
pageDescription: '💍公历2026年9月4日农历七月廿三· 李逸烁 & 李琦 结婚典礼,诚邀您光临。',
2026-08-03 09:31:32 +08:00
sonLabel: '女儿:',
2026-08-03 10:22:04 +08:00
sonName: '李逸烁',
2026-08-03 09:31:32 +08:00
daughterInLawLabel: '女婿:',
2026-08-03 10:22:04 +08:00
daughterInLawName: '李琦',
groomName: '李琦',
brideName: '李逸烁',
2026-08-03 09:54:43 +08:00
verticalSlogan: '小女出阁',
2026-08-03 10:22:04 +08:00
dateText: '2026年9月4日 星期五',
lunarText: '农历七月廿三',
venueText: '山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼',
2026-08-03 09:31:32 +08:00
lines: [
'各位亲朋好友',
'吾家有喜 女儿出嫁',
'良辰已定 吉日待访',
2026-08-03 10:22:04 +08:00
'兹定于2026年9月4日 星期五',
'农历七月廿三 中午11:08分',
2026-08-03 09:31:32 +08:00
'为女儿女婿举办结婚典礼',
'敬备喜宴 诚邀您携家人光临',
2026-08-03 10:22:04 +08:00
'地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼',
2026-08-03 09:31:32 +08:00
'刘建国 & 王秀兰 夫 妇 敬邀',
],
}
}
2026-08-03 10:22:04 +08:00
/** 回门宴(3) 默认9月7日 */
export function defaultSide3PosterConfig() {
return {
...baseFields(3),
side: 3,
template: 'classic',
title: '回门宴邀请函',
2026-08-03 10:25:11 +08:00
pageTitle: '💕李琦❤️李逸烁 · 回门宴邀请',
pageDescription: '💍公历2026年9月7日农历七月廿六· 李琦 & 李逸烁 回门宴,诚邀您光临。',
2026-08-03 10:22:04 +08:00
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号楼 二楼',
'李琦 & 李逸烁 敬邀',
],
}
}
2026-08-03 09:54:43 +08:00
export function defaultMalePosterConfig() {
return defaultSide1PosterConfig()
}
export function defaultFemalePosterConfig() {
return defaultSide2PosterConfig()
}
2026-08-03 09:31:32 +08:00
export function defaultPosterConfig() {
2026-08-03 09:54:43 +08:00
return defaultSide1PosterConfig()
2026-08-03 09:31:32 +08:00
}
2026-08-03 10:22:04 +08:00
export function defaultPosterForSide(side) {
const s = resolvePosterSide(side) || 1
if (s === 3) return defaultSide3PosterConfig()
if (s === 2) return defaultSide2PosterConfig()
return defaultSide1PosterConfig()
}
2026-08-03 16:00:13 +08:00
/** 按侧 + 布局生成默认配置(竖=classic/portrait横=split/landscape */
export function defaultPosterForLayout(side, layout = 1) {
const s = resolvePosterSide(side) || 1
const l = resolvePosterLayout(layout)
const base = defaultPosterForSide(s)
const meta = posterLayoutMeta(l)
return {
...base,
side: s,
layout: l,
template: meta.template,
mobileLayout: meta.mobileLayout,
}
}
export function defaultSideLayouts(side) {
return {
'1': defaultPosterForLayout(side, 1),
'2': defaultPosterForLayout(side, 2),
}
}
export function defaultPosterBundle() {
return {
'1': defaultSideLayouts(1),
'2': defaultSideLayouts(2),
'3': defaultSideLayouts(3),
}
}
2026-08-03 09:31:32 +08:00
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
2026-08-03 10:22:04 +08:00
if (raw['1'] || raw['2'] || raw['3'] || raw[1] || raw[2] || raw[3] || raw.male || raw.female) return false
2026-08-03 09:54:43 +08:00
return 'title' in raw || 'lines' in raw || 'heroImg' in raw || 'sonName' in raw || 'template' in raw
2026-08-03 09:31:32 +08:00
}
2026-08-03 16:00:13 +08:00
/** 判断对象是否为「单份海报配置」(扁平字段),而非 {1,2} 布局包 */
function isFlatPosterConfig(obj) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false
if (obj['1'] || obj['2'] || obj[1] || obj[2]) {
const hasNestedLayout = [obj['1'], obj['2'], obj[1], obj[2]].some(
(v) => v && typeof v === 'object' && ('title' in v || 'template' in v || 'heroImg' in v || 'lines' in v),
)
if (hasNestedLayout) return false
}
return 'title' in obj || 'lines' in obj || 'heroImg' in obj || 'sonName' in obj || 'template' in obj
|| 'groomName' in obj || 'mobileLayout' in obj || 'pageTitle' in obj
}
2026-08-03 09:54:43 +08:00
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
2026-08-03 10:22:04 +08:00
if (side === 3 && loaded.hm && typeof loaded.hm === 'object') return loaded.hm
2026-08-03 09:54:43 +08:00
return null
}
2026-08-03 16:00:13 +08:00
const SHARED_COPY_KEYS = [
'sonLabel', 'sonName', 'daughterInLawLabel', 'daughterInLawName',
'groomName', 'brideName', 'dateText', 'lunarText', 'venueText',
'lines', 'heroImg', 'heroFocusX', 'heroFocusY', 'qrCenterImg',
'musicUrl', 'invitePath', 'inviteButtonText', 'showInviteButton',
'enabled', 'pageTitle', 'pageDescription', 'footer', 'title',
'greeting', 'verticalSlogan', 'subSlogan1', 'subSlogan2', 'loveFooter',
'loadingEffect', 'enterEffect',
]
function copySharedFields(from, into) {
if (!from || !into) return into
for (const k of SHARED_COPY_KEYS) {
if (from[k] !== undefined && from[k] !== null && from[k] !== '') {
into[k] = Array.isArray(from[k]) ? from[k].slice() : from[k]
}
}
return into
}
export function normalizePosterConfig(raw, side = 1, layout = 1) {
2026-08-03 09:54:43 +08:00
const s = resolvePosterSide(side) || 1
2026-08-03 16:00:13 +08:00
const l = resolvePosterLayout(layout)
const base = defaultPosterForLayout(s, l)
2026-08-03 09:31:32 +08:00
const loaded = raw && typeof raw === 'object' ? raw : {}
const lines = Array.isArray(loaded.lines)
2026-08-03 16:00:13 +08:00
? loaded.lines.map((x) => String(x ?? ''))
2026-08-03 09:31:32 +08:00
: base.lines
2026-08-03 09:54:43 +08:00
const template = POSTER_TEMPLATES.some((t) => t.value === loaded.template)
? loaded.template
: base.template
2026-08-03 10:18:23 +08:00
const mobileLayout = MOBILE_LAYOUTS.some((t) => t.value === loaded.mobileLayout)
? loaded.mobileLayout
: base.mobileLayout
2026-08-03 09:54:43 +08:00
const groomName = loaded.groomName || loaded.sonName || base.groomName
const brideName = loaded.brideName || loaded.daughterInLawName || base.brideName
2026-08-03 09:31:32 +08:00
return {
...base,
...loaded,
2026-08-03 09:54:43 +08:00
side: s,
2026-08-03 16:00:13 +08:00
layout: l,
2026-08-03 09:54:43 +08:00
template,
2026-08-03 10:18:23 +08:00
mobileLayout,
2026-08-03 09:31:32 +08:00
lines: lines.length ? lines : base.lines,
2026-08-03 09:54:43 +08:00
groomName,
brideName,
2026-08-03 09:31:32 +08:00
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,
2026-08-03 13:01:08 +08:00
musicUrl: typeof loaded.musicUrl === 'string' ? loaded.musicUrl.trim() : '',
2026-08-03 10:25:11 +08:00
pageTitle: loaded.pageTitle || base.pageTitle || '',
pageDescription: loaded.pageDescription || base.pageDescription || '',
2026-08-03 10:18:23 +08:00
qrCenterImg: loaded.qrCenterImg || '',
2026-08-03 13:19:09 +08:00
...resolveHeroFocusXY(loaded),
2026-08-03 09:54:43 +08:00
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,
2026-08-03 09:31:32 +08:00
}
}
2026-08-03 16:00:13 +08:00
/** 将单侧数据归一为 { "1": 竖版, "2": 横版 } */
export function normalizeSideLayouts(raw, side = 1) {
const s = resolvePosterSide(side) || 1
const src = raw && typeof raw === 'object' ? raw : {}
if (!isFlatPosterConfig(src) && (src['1'] || src['2'] || src[1] || src[2])) {
const l1raw = (src['1'] && typeof src['1'] === 'object' ? src['1'] : null)
|| (src[1] && typeof src[1] === 'object' ? src[1] : null)
|| {}
const l2raw = (src['2'] && typeof src['2'] === 'object' ? src['2'] : null)
|| (src[2] && typeof src[2] === 'object' ? src[2] : null)
|| {}
const l1 = normalizePosterConfig(l1raw, s, 1)
l1.mobileLayout = 'portrait'
l1.layout = 1
let l2
if (Object.keys(l2raw).length) {
l2 = normalizePosterConfig(l2raw, s, 2)
} else {
l2 = defaultPosterForLayout(s, 2)
copySharedFields(l1, l2)
}
l2.mobileLayout = 'landscape'
l2.layout = 2
return { '1': l1, '2': l2 }
}
const flat = isFlatPosterConfig(src) ? src : {}
const l1 = normalizePosterConfig(flat, s, 1)
l1.mobileLayout = 'portrait'
l1.layout = 1
const l2 = defaultPosterForLayout(s, 2)
copySharedFields(l1, l2)
l2.mobileLayout = 'landscape'
l2.layout = 2
return { '1': l1, '2': l2 }
}
/** 归一化为 { 1|2|3: { 1|2: cfg } };兼容旧 male/female、扁平与单侧扁平 */
2026-08-03 09:31:32 +08:00
export function normalizePosterBundle(raw) {
const loaded = raw && typeof raw === 'object' ? raw : {}
if (isLegacyFlatPoster(loaded)) {
return {
2026-08-03 16:00:13 +08:00
'1': normalizeSideLayouts(loaded, 1),
'2': defaultSideLayouts(2),
'3': defaultSideLayouts(3),
2026-08-03 09:31:32 +08:00
}
}
return {
2026-08-03 16:00:13 +08:00
'1': normalizeSideLayouts(pickSideSource(loaded, 1) || {}, 1),
'2': normalizeSideLayouts(pickSideSource(loaded, 2) || {}, 2),
'3': normalizeSideLayouts(pickSideSource(loaded, 3) || {}, 3),
2026-08-03 09:31:32 +08:00
}
}
2026-08-03 16:00:13 +08:00
export function pickPosterConfig(bundle, side, layout = 1) {
2026-08-03 09:31:32 +08:00
const normalized = normalizePosterBundle(bundle)
2026-08-03 09:54:43 +08:00
const s = sideKey(side)
2026-08-03 16:00:13 +08:00
const l = layoutKey(layout)
return normalized[s]?.[l] || defaultPosterForLayout(s, l)
}
/** @deprecated 兼容旧调用,默认取竖版 */
export function pickPosterSide(bundle, side) {
return pickPosterConfig(bundle, side, 1)
2026-08-03 09:31:32 +08:00
}