1. 海报

This commit is contained in:
李琦
2026-08-03 10:18:23 +08:00
parent 3b9520eb0f
commit 807adbc6a4
10 changed files with 1155 additions and 21 deletions

View File

@@ -10,6 +10,13 @@ export const POSTER_TEMPLATES = [
{ value: 'split', label: '左图右文', desc: '竖排书法 + 右侧宴席信息' },
]
/** 手机端布局仅影响窄屏PC 仍按模板本身版式 */
export const MOBILE_LAYOUTS = [
{ value: 'auto', label: '自动布局', desc: '按屏幕宽度自动切换(当前默认)' },
{ value: 'portrait', label: '竖向展示', desc: '手机端强制上下排布' },
{ value: 'landscape', label: '横屏展示', desc: '手机端强制左右分栏' },
]
/** 解析为 1 | 2无法识别返回 0 */
export function resolvePosterSide(input) {
if (input === 1 || input === '1') return 1
@@ -29,8 +36,11 @@ function baseFields(side) {
return {
side: resolvePosterSide(side) || 1,
template: 'classic',
mobileLayout: 'auto',
enabled: true,
heroImg: '',
/** 二维码中心图;空则显示圆圈大囍 */
qrCenterImg: '',
title: '婚礼邀请函',
footer: '—诚挚邀请 ♥ 恭请光临—',
loadingEffect: 'redSeal',
@@ -157,6 +167,9 @@ export function normalizePosterConfig(raw, side = 1) {
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
@@ -166,6 +179,7 @@ export function normalizePosterConfig(raw, side = 1) {
...loaded,
side: s,
template,
mobileLayout,
lines: lines.length ? lines : base.lines,
groomName,
brideName,
@@ -179,6 +193,7 @@ export function normalizePosterConfig(raw, side = 1) {
: base.enterEffect,
invitePath: loaded.invitePath || '/',
inviteButtonText: loaded.inviteButtonText || base.inviteButtonText,
qrCenterImg: loaded.qrCenterImg || '',
verticalSlogan: loaded.verticalSlogan || base.verticalSlogan,
subSlogan1: loaded.subSlogan1 || base.subSlogan1,
subSlogan2: loaded.subSlogan2 || base.subSlogan2,

View File

@@ -0,0 +1,83 @@
import QRCode from 'qrcode'
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = reject
img.src = src
})
}
/** 在 canvas 上绘制喜庆二维码,中心为配置图或圆圈大囍 */
export async function renderFestiveQr(canvas, url, options = {}) {
const size = options.size || 148
const centerImg = String(options.centerImg || '').trim()
await QRCode.toCanvas(canvas, url, {
width: size,
margin: 1,
color: {
dark: '#8b0000',
light: '#fff8e7',
},
errorCorrectionLevel: 'H',
})
const ctx = canvas.getContext('2d')
if (!ctx) return
const w = canvas.width
const h = canvas.height
const cx = w / 2
const cy = h / 2
const diameter = Math.round(Math.min(w, h) * 0.3)
const r = diameter / 2
const ring = Math.max(2, Math.round(w * 0.012))
ctx.save()
// 外圈底
ctx.beginPath()
ctx.arc(cx, cy, r + ring, 0, Math.PI * 2)
ctx.fillStyle = '#fff8e7'
ctx.fill()
ctx.strokeStyle = '#c41e3a'
ctx.lineWidth = ring
ctx.stroke()
let drewImg = false
if (centerImg) {
try {
const img = await loadImage(centerImg)
ctx.beginPath()
ctx.arc(cx, cy, r - 0.5, 0, Math.PI * 2)
ctx.clip()
const side = (r - 0.5) * 2
const scale = Math.max(side / img.width, side / img.height)
const dw = img.width * scale
const dh = img.height * scale
ctx.drawImage(img, cx - dw / 2, cy - dh / 2, dw, dh)
drewImg = true
} catch {
drewImg = false
}
}
if (!drewImg) {
ctx.beginPath()
ctx.arc(cx, cy, r - 0.5, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 248, 231, 0.96)'
ctx.fill()
ctx.strokeStyle = '#c41e3a'
ctx.lineWidth = Math.max(1.5, ring * 0.7)
ctx.stroke()
ctx.fillStyle = '#c41e3a'
ctx.font = `700 ${Math.round(r * 1.45)}px "Noto Serif SC","Source Han Serif SC","Songti SC",serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('囍', cx, cy + r * 0.04)
}
ctx.restore()
}