Files
hunli/vite-tailwindcss/src/composables/posterDefaults.js
2026-08-03 16:00:13 +08:00

510 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 海报 side1=男方(/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' },
]
/** 布局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' },
]
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: '手机端强制左右分栏' },
]
/** 后台 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] }
}
/** 解析为 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]
}
/** 解析布局 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)}`
}
function baseFields(side) {
return {
side: resolvePosterSide(side) || 1,
layout: 1,
template: 'classic',
mobileLayout: 'auto',
enabled: true,
heroImg: '',
/** 主图焦点百分比 0100object-position */
heroFocusX: 50,
heroFocusY: 50,
/** 二维码中心图;空则显示圆圈大囍 */
qrCenterImg: '',
title: '婚礼邀请函',
/** 浏览器标签 / 分享用页面标题与描述(可与海报主标题不同) */
pageTitle: '',
pageDescription: '',
footer: '—诚挚邀请 ♥ 恭请光临—',
loadingEffect: 'redSeal',
enterEffect: 'fadeUp',
showInviteButton: true,
inviteButtonText: '打开请柬',
invitePath: '/',
/** 背景音乐 URL从请柬 musicList 中选择;空=不播放) */
musicUrl: '',
// 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 defaultPosterForSide(side) {
const s = resolvePosterSide(side) || 1
if (s === 3) return defaultSide3PosterConfig()
if (s === 2) return defaultSide2PosterConfig()
return defaultSide1PosterConfig()
}
/** 按侧 + 布局生成默认配置(竖=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),
}
}
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
}
/** 判断对象是否为「单份海报配置」(扁平字段),而非 {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
}
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
}
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) {
const s = resolvePosterSide(side) || 1
const l = resolvePosterLayout(layout)
const base = defaultPosterForLayout(s, l)
const loaded = raw && typeof raw === 'object' ? raw : {}
const lines = Array.isArray(loaded.lines)
? loaded.lines.map((x) => String(x ?? ''))
: 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,
layout: l,
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,
musicUrl: typeof loaded.musicUrl === 'string' ? loaded.musicUrl.trim() : '',
pageTitle: loaded.pageTitle || base.pageTitle || '',
pageDescription: loaded.pageDescription || base.pageDescription || '',
qrCenterImg: loaded.qrCenterImg || '',
...resolveHeroFocusXY(loaded),
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": 横版 } */
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、扁平与单侧扁平 */
export function normalizePosterBundle(raw) {
const loaded = raw && typeof raw === 'object' ? raw : {}
if (isLegacyFlatPoster(loaded)) {
return {
'1': normalizeSideLayouts(loaded, 1),
'2': defaultSideLayouts(2),
'3': defaultSideLayouts(3),
}
}
return {
'1': normalizeSideLayouts(pickSideSource(loaded, 1) || {}, 1),
'2': normalizeSideLayouts(pickSideSource(loaded, 2) || {}, 2),
'3': normalizeSideLayouts(pickSideSource(loaded, 3) || {}, 3),
}
}
export function pickPosterConfig(bundle, side, layout = 1) {
const normalized = normalizePosterBundle(bundle)
const s = sideKey(side)
const l = layoutKey(layout)
return normalized[s]?.[l] || defaultPosterForLayout(s, l)
}
/** @deprecated 兼容旧调用,默认取竖版 */
export function pickPosterSide(bundle, side) {
return pickPosterConfig(bundle, side, 1)
}