1. 修复海报图片
This commit is contained in:
@@ -22,7 +22,9 @@ var (
|
||||
|
||||
var configSectionKeys = map[string][]string{
|
||||
"basic": {
|
||||
"groom", "bride", "date", "lunar", "calendarDate", "hotel", "address", "heroImg", "endImg",
|
||||
"groom", "bride", "date", "lunar", "calendarDate", "hotel", "address",
|
||||
"heroImg", "heroImgResized", "heroImgResizedMeta",
|
||||
"endImg", "endImgResized", "endImgResizedMeta",
|
||||
},
|
||||
"copy": {
|
||||
"formalText1", "formalText2", "formalPageHeight",
|
||||
|
||||
@@ -23,7 +23,7 @@ func Register(api *gin.RouterGroup, database *gorm.DB, adminGuard func(*gin.Cont
|
||||
api.POST("/poster/config", handleSavePosterConfig)
|
||||
}
|
||||
|
||||
// resolveSide 返回 "1"(男方)、"2"(女方)或 "3"(回门宴);无法识别返回空
|
||||
// resolveSide 返回 "1"(男方)、"2"(女方)或 "3"(新人);无法识别返回空
|
||||
func resolveSide(raw string) string {
|
||||
v := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch v {
|
||||
@@ -31,7 +31,7 @@ func resolveSide(raw string) string {
|
||||
return "1"
|
||||
case "2", "female", "nv", "f", "bride":
|
||||
return "2"
|
||||
case "3", "hm", "hmy", "return", "huimen":
|
||||
case "3", "xr", "hm", "hmy", "return", "huimen", "couple", "xinren", "新人":
|
||||
return "3"
|
||||
default:
|
||||
return ""
|
||||
@@ -156,7 +156,7 @@ func normalizeBundle(raw map[string]interface{}) map[string]interface{} {
|
||||
if f := takeSide(raw, "2", "female"); len(f) > 0 {
|
||||
out["2"] = normalizeSideLayouts(f)
|
||||
}
|
||||
if h := takeSide(raw, "3", "hm", "hmy"); len(h) > 0 {
|
||||
if h := takeSide(raw, "3", "xr", "hm", "hmy"); len(h) > 0 {
|
||||
out["3"] = normalizeSideLayouts(h)
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv),3=回门宴(/hb-hm) */
|
||||
|
||||
/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv),3=新人模板(/hb-xr) */
|
||||
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' },
|
||||
{ key: 3, keyStr: '3', route: '/hb-xr', label: '新人模板', short: 'xr' },
|
||||
]
|
||||
|
||||
/** 布局:1=竖版,2=横版(URL 后缀 /1 /2) */
|
||||
@@ -118,7 +117,7 @@ export function resolvePosterSide(input) {
|
||||
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 === 'xr' || v === 'hm' || v === 'hmy' || v === 'return' || v === 'huimen' || v === 'xinren' || v === 'couple') 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
|
||||
@@ -264,23 +263,23 @@ export function defaultSide2PosterConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 回门宴(3) 默认:9月7日 */
|
||||
/** 新人模板(3) 默认 */
|
||||
export function defaultSide3PosterConfig() {
|
||||
return {
|
||||
...baseFields(3),
|
||||
side: 3,
|
||||
template: 'classic',
|
||||
title: '回门宴邀请函',
|
||||
pageTitle: '💕李琦❤️李逸烁 · 回门宴邀请',
|
||||
pageDescription: '💍公历2026年9月7日(农历七月廿六)· 李琦 & 李逸烁 回门宴,诚邀您光临。',
|
||||
footer: '—回门宴请 ♥ 恭请光临—',
|
||||
title: '婚礼邀请函',
|
||||
pageTitle: '💕李琦❤️李逸烁 · 新人婚礼邀请',
|
||||
pageDescription: '💍公历2026年9月7日(农历七月廿六)· 李琦 & 李逸烁 结婚典礼,诚邀您光临。',
|
||||
footer: '—诚挚邀请 ♥ 恭请光临—',
|
||||
sonLabel: '新郎:',
|
||||
sonName: '李琦',
|
||||
daughterInLawLabel: '新娘:',
|
||||
daughterInLawName: '李逸烁',
|
||||
groomName: '李琦',
|
||||
brideName: '李逸烁',
|
||||
verticalSlogan: '回门之喜',
|
||||
verticalSlogan: '喜结连理',
|
||||
subSlogan1: '琴瑟和鸣',
|
||||
subSlogan2: '永结同心',
|
||||
dateText: '2026年9月7日 星期一',
|
||||
@@ -290,11 +289,11 @@ export function defaultSide3PosterConfig() {
|
||||
loadingEffect: 'redSeal',
|
||||
lines: [
|
||||
'各位亲朋好友',
|
||||
'新婚燕尔 回门宴请',
|
||||
'良辰吉日 新人成婚',
|
||||
'良辰已定 吉日待访',
|
||||
'兹定于2026年9月7日 星期一',
|
||||
'农历七月廿六 中午11:08分',
|
||||
'为新婚夫妇举办回门宴',
|
||||
'为新婚夫妇举办结婚典礼',
|
||||
'敬备薄酒 诚邀您携家人光临',
|
||||
'地址:山西省 阳泉市 芙蓉酒楼(阳泉店) 太行国际新城B区10号楼 二楼',
|
||||
'李琦 & 李逸烁 敬邀',
|
||||
|
||||
@@ -33,15 +33,32 @@ const routes = [
|
||||
component: () => import('@/views/hb/index.vue'),
|
||||
meta: { posterSide: 2 },
|
||||
},
|
||||
{
|
||||
path: '/hb-xr',
|
||||
redirect: '/hb-xr/1',
|
||||
},
|
||||
{
|
||||
path: '/hb-xr/:layout(1|2)',
|
||||
name: 'PosterCouple',
|
||||
component: () => import('@/views/hb/index.vue'),
|
||||
meta: { posterSide: 3 },
|
||||
},
|
||||
// 旧路径兼容
|
||||
{
|
||||
path: '/hb-hm',
|
||||
redirect: '/hb-hm/1',
|
||||
redirect: '/hb-xr/1',
|
||||
},
|
||||
{
|
||||
path: '/hb-hm/:layout(1|2)',
|
||||
name: 'PosterReturnBanquet',
|
||||
component: () => import('@/views/hb/index.vue'),
|
||||
meta: { posterSide: 3 },
|
||||
redirect: (to) => `/hb-xr/${to.params.layout || 1}`,
|
||||
},
|
||||
{
|
||||
path: '/xr',
|
||||
redirect: '/hb-xr/1',
|
||||
},
|
||||
{
|
||||
path: '/xr/:layout(1|2)',
|
||||
redirect: (to) => `/hb-xr/${to.params.layout || 1}`,
|
||||
},
|
||||
{
|
||||
path: '/lijin',
|
||||
|
||||
@@ -653,14 +653,14 @@
|
||||
</a-card>
|
||||
</section>
|
||||
|
||||
<!-- 海报(独立 poster_configs:1 男方 /hb-n,2 女方 /hb-nv,3 回门宴 /hb-hm) -->
|
||||
<!-- 海报(独立 poster_configs:1 男方 /hb-n,2 女方 /hb-nv,3 新人模板 /hb-xr) -->
|
||||
<section v-if="activeTab === 'poster'">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="每一方同时维护竖版(/1)与横版(/2)。例:男方竖版 /hb-n/1,横版 /hb-n/2;女方 /hb-nv/1|/2;回门 /hb-hm/1|/2。"
|
||||
message="每一方同时维护竖版(/1)与横版(/2)。例:男方竖版 /hb-n/1,横版 /hb-n/2;女方 /hb-nv/1|/2;新人模板 /hb-xr/1|/2。"
|
||||
/>
|
||||
<a-tabs v-model:activeKey="posterSideTab" size="small" type="card" class="mb-2">
|
||||
<a-tab-pane v-for="s in POSTER_SIDES" :key="s.keyStr" :tab="`${s.label}(${s.key})`" />
|
||||
@@ -862,11 +862,17 @@
|
||||
<a-textarea v-model:value="posterLinesText" :rows="5" placeholder="每行一条正文" />
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="新郎">
|
||||
<a-input v-model:value="posterCfg.groomName" />
|
||||
<a-form-item :label="posterNameLabels.left">
|
||||
<div class="flex gap-2">
|
||||
<a-input v-model:value="posterCfg.sonLabel" class="!w-[100px]" :placeholder="posterNameLabels.leftPh" />
|
||||
<a-input v-model:value="posterCfg.sonName" placeholder="姓名" @update:value="v => posterCfg.groomName = v" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="新娘">
|
||||
<a-input v-model:value="posterCfg.brideName" />
|
||||
<a-form-item :label="posterNameLabels.right">
|
||||
<div class="flex gap-2">
|
||||
<a-input v-model:value="posterCfg.daughterInLawLabel" class="!w-[100px]" :placeholder="posterNameLabels.rightPh" />
|
||||
<a-input v-model:value="posterCfg.daughterInLawName" placeholder="姓名" @update:value="v => posterCfg.brideName = v" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="喜宴时间">
|
||||
<a-input v-model:value="posterCfg.dateText" />
|
||||
@@ -882,20 +888,47 @@
|
||||
<a-input v-model:value="posterCfg.loveFooter" placeholder="Love you" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<a-button type="primary" :loading="posterSaving" @click="onSavePosterConfig"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存海报配置
|
||||
</a-button>
|
||||
<a-button @click="openPosterPreview(posterSideTab, posterLayoutTab)">
|
||||
<i class="fa-solid fa-eye mr-1"></i>打开 {{ currentPosterPreviewPath }} 预览
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
<div class="poster-action-bar-spacer" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<div v-if="activeTab === 'poster'" class="poster-action-bar">
|
||||
<div class="poster-action-bar-inner">
|
||||
<div class="poster-action-bar-meta">
|
||||
<span>{{ currentPosterSide.label }} · {{ currentPosterLayout.label }}</span>
|
||||
<span class="poster-action-bar-path">{{ currentPosterPreviewPath }}</span>
|
||||
</div>
|
||||
<div class="poster-action-bar-btns">
|
||||
<a-button
|
||||
v-if="posterUndoSnapshot"
|
||||
size="small"
|
||||
@click="undoPosterImport"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left mr-1"></i>撤回导入
|
||||
</a-button>
|
||||
<a-button size="small" :loading="posterCopying" @click="copyPosterTemplate">
|
||||
<i class="fa-solid fa-copy mr-1"></i>复制
|
||||
</a-button>
|
||||
<a-button size="small" :loading="posterImporting" @click="importPosterTemplateDirect">
|
||||
<i class="fa-solid fa-file-import mr-1"></i>导入
|
||||
</a-button>
|
||||
<a-button size="small" @click="openPosterPreview(posterSideTab, posterLayoutTab)">
|
||||
<i class="fa-solid fa-eye mr-1"></i>预览
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="posterSaving"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]"
|
||||
@click="onSavePosterConfig"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="activeTab === 'photos'">
|
||||
<div class="admin-panel !p-4 mb-3">
|
||||
<div class="flex justify-between items-center gap-3 flex-wrap">
|
||||
@@ -1419,7 +1452,7 @@ import {
|
||||
metaHasFileSize,
|
||||
} from '@/composables/resizeImage'
|
||||
import {
|
||||
defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout,
|
||||
defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout, normalizePosterConfig,
|
||||
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_LAYOUTS, POSTER_TEMPLATES, MOBILE_LAYOUTS,
|
||||
HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, layoutKey,
|
||||
posterSideMeta, posterLayoutMeta, posterRoute,
|
||||
@@ -1616,6 +1649,12 @@ const uploadCfg = reactive(defaultUploadConfig())
|
||||
const posterSideTab = ref('1')
|
||||
const posterLayoutTab = ref('1')
|
||||
const posterBundle = reactive(defaultPosterBundle())
|
||||
const posterImporting = ref(false)
|
||||
const posterCopying = ref(false)
|
||||
/** 本会话最近一次复制的模板,导入时优先/兜底使用 */
|
||||
let posterTemplateCache = null
|
||||
/** 导入前快照,用于撤回 { s, l, config } */
|
||||
const posterUndoSnapshot = ref(null)
|
||||
const posterCfg = computed(() => {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
@@ -1737,7 +1776,7 @@ const posterNameLabels = computed(() => {
|
||||
return { left: '女儿称谓 / 姓名', right: '女婿称谓 / 姓名', leftPh: '女儿:', rightPh: '女婿:' }
|
||||
}
|
||||
if (s === '3') {
|
||||
return { left: '新郎称谓 / 姓名', right: '新娘称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' }
|
||||
return { left: '新人左侧称谓 / 姓名', right: '新人右侧称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' }
|
||||
}
|
||||
return { left: '儿子称谓 / 姓名', right: '儿媳称谓 / 姓名', leftPh: '儿子:', rightPh: '儿媳:' }
|
||||
})
|
||||
@@ -2026,9 +2065,17 @@ async function batchResizeBasicImages() {
|
||||
console.warn('[resize-basic]', slot.key, err)
|
||||
}
|
||||
}
|
||||
if (fail === 0) message.success(`已生成 ${ok} 张展示图`)
|
||||
else if (ok > 0) message.warning(`成功 ${ok} 张,失败 ${fail} 张(常见原因:跨域无法读取原图)`)
|
||||
else message.error('缩放上传全部失败,请确认原图可跨域读取')
|
||||
if (ok > 0) {
|
||||
try {
|
||||
await saveConfigSection('basic', pickSectionData('basic'))
|
||||
if (fail === 0) message.success(`已生成并保存 ${ok} 张展示图`)
|
||||
else message.warning(`成功 ${ok} 张并已保存,失败 ${fail} 张`)
|
||||
} catch (e) {
|
||||
message.warning(`已生成 ${ok} 张展示图,但自动保存失败:${e?.message || '请手动保存'}`)
|
||||
}
|
||||
} else {
|
||||
message.error('缩放上传全部失败,请确认原图可跨域读取')
|
||||
}
|
||||
} finally {
|
||||
resizingBasic.value = false
|
||||
resizeProgress.value = ''
|
||||
@@ -2285,6 +2332,7 @@ async function onSavePosterConfig() {
|
||||
posterBundle['1'] = bundle['1']
|
||||
posterBundle['2'] = bundle['2']
|
||||
posterBundle['3'] = bundle['3']
|
||||
posterUndoSnapshot.value = null
|
||||
message.success('海报配置已保存')
|
||||
} catch (e) {
|
||||
message.error(e?.message || '海报配置保存失败')
|
||||
@@ -2293,6 +2341,141 @@ async function onSavePosterConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPosterTemplatePayload() {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
const slot = ensurePosterSlot(s, l)
|
||||
return {
|
||||
version: 1,
|
||||
kind: 'poster-slot',
|
||||
side: Number(s),
|
||||
layout: Number(l),
|
||||
sideLabel: posterSideMeta(s).label,
|
||||
layoutLabel: posterLayoutMeta(l).label,
|
||||
route: posterRoute(s, l),
|
||||
config: JSON.parse(JSON.stringify(slot)),
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPosterTemplate() {
|
||||
posterCopying.value = true
|
||||
try {
|
||||
const payload = buildPosterTemplatePayload()
|
||||
posterTemplateCache = payload
|
||||
const text = JSON.stringify(payload, null, 2)
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
message.success(`已复制「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」`)
|
||||
} catch (e) {
|
||||
message.error(e?.message || '复制失败,请检查浏览器剪贴板权限')
|
||||
} finally {
|
||||
posterCopying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function extractPosterImportConfig(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
if (parsed.kind === 'poster-slot' && parsed.config && typeof parsed.config === 'object') {
|
||||
return parsed.config
|
||||
}
|
||||
if (parsed.config && typeof parsed.config === 'object' && !Array.isArray(parsed.config)) {
|
||||
return parsed.config
|
||||
}
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
const sideObj = parsed[s] || parsed[Number(s)]
|
||||
if (sideObj && typeof sideObj === 'object') {
|
||||
const slot = sideObj[l] || sideObj[Number(l)]
|
||||
if (slot && typeof slot === 'object') return slot
|
||||
}
|
||||
if ('template' in parsed || 'heroImg' in parsed || 'lines' in parsed || 'sonName' in parsed || 'groomName' in parsed) {
|
||||
return parsed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function readPosterImportSource() {
|
||||
// 1) 剪贴板 2) 本会话刚复制的缓存
|
||||
try {
|
||||
if (navigator?.clipboard?.readText) {
|
||||
const clip = String(await navigator.clipboard.readText() || '').trim()
|
||||
if (clip) {
|
||||
try {
|
||||
const parsed = JSON.parse(clip)
|
||||
if (extractPosterImportConfig(parsed)) return parsed
|
||||
} catch { /* not json */ }
|
||||
}
|
||||
}
|
||||
} catch { /* no permission */ }
|
||||
if (posterTemplateCache && extractPosterImportConfig(posterTemplateCache)) {
|
||||
return posterTemplateCache
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function applyImportedConfig(rawCfg) {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
posterUndoSnapshot.value = {
|
||||
s,
|
||||
l,
|
||||
config: JSON.parse(JSON.stringify(ensurePosterSlot(s, l))),
|
||||
}
|
||||
const next = normalizePosterConfig(rawCfg, s, l)
|
||||
next.side = Number(s)
|
||||
next.layout = Number(l)
|
||||
if (next.sonName && !next.groomName) next.groomName = next.sonName
|
||||
if (next.daughterInLawName && !next.brideName) next.brideName = next.daughterInLawName
|
||||
if (next.groomName && !next.sonName) next.sonName = next.groomName
|
||||
if (next.brideName && !next.daughterInLawName) next.daughterInLawName = next.brideName
|
||||
posterBundle[s][l] = next
|
||||
}
|
||||
|
||||
async function importPosterTemplateDirect() {
|
||||
posterImporting.value = true
|
||||
try {
|
||||
const parsed = await readPosterImportSource()
|
||||
if (!parsed) {
|
||||
message.warning('请先复制模板,再点导入')
|
||||
return
|
||||
}
|
||||
const rawCfg = extractPosterImportConfig(parsed)
|
||||
if (!rawCfg) {
|
||||
message.error('无法识别的模板数据')
|
||||
return
|
||||
}
|
||||
applyImportedConfig(rawCfg)
|
||||
message.success(`已导入到「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」,可撤回或保存`)
|
||||
} catch (e) {
|
||||
message.error(e?.message || '导入失败')
|
||||
} finally {
|
||||
posterImporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function undoPosterImport() {
|
||||
const snap = posterUndoSnapshot.value
|
||||
if (!snap?.config) {
|
||||
message.info('没有可撤回的导入')
|
||||
return
|
||||
}
|
||||
posterSideTab.value = snap.s
|
||||
posterLayoutTab.value = snap.l
|
||||
posterBundle[snap.s][snap.l] = snap.config
|
||||
posterUndoSnapshot.value = null
|
||||
message.success('已撤回导入')
|
||||
}
|
||||
|
||||
function pickSectionData(section) {
|
||||
const keys = SECTION_KEYS[section] || []
|
||||
const data = {}
|
||||
@@ -3514,4 +3697,57 @@ onUnmounted(() => {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.poster-action-bar-spacer {
|
||||
height: 72px;
|
||||
}
|
||||
.poster-action-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(168, 140, 107, 0.2);
|
||||
box-shadow: 0 -8px 24px rgba(88, 68, 42, 0.08);
|
||||
}
|
||||
.poster-action-bar-inner {
|
||||
max-width: 1220px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.admin-shell--top ~ .poster-action-bar .poster-action-bar-inner,
|
||||
.admin-shell--top .poster-action-bar-inner {
|
||||
max-width: 80vw;
|
||||
}
|
||||
.poster-action-bar-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #5c5348;
|
||||
}
|
||||
.poster-action-bar-path {
|
||||
font-size: 11px;
|
||||
color: #8A8680;
|
||||
}
|
||||
.poster-action-bar-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.poster-action-bar-meta { width: 100%; }
|
||||
.poster-action-bar-btns { width: 100%; justify-content: stretch; }
|
||||
.poster-action-bar-btns :deep(.ant-btn) { flex: 1 1 auto; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PC:右下角固定,不占版心空间;保存时会把二维码打进海报图 -->
|
||||
<!-- PC:右下角固定;仅 ?qr=1 时展示二维码,保存海报时也会打进图里 -->
|
||||
<aside
|
||||
v-if="isPc && phase === 'content' && contentVisible && cfg.enabled"
|
||||
class="hb-pc-tools"
|
||||
@@ -136,7 +136,13 @@
|
||||
<i class="fa-solid fa-download"></i>
|
||||
{{ savingImg ? '生成中…' : '保存海报' }}
|
||||
</button>
|
||||
<FestiveQrCard :url="pageUrl" :size="112" :center-img="cfg.qrCenterImg" compact />
|
||||
<FestiveQrCard
|
||||
v-if="showCaptureQr"
|
||||
:url="qrTargetUrl"
|
||||
:size="112"
|
||||
:center-img="cfg.qrCenterImg"
|
||||
compact
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<button
|
||||
@@ -180,7 +186,19 @@ const enterLeaving = ref(false)
|
||||
const contentVisible = ref(false)
|
||||
const savingImg = ref(false)
|
||||
const isPc = ref(false)
|
||||
const pageUrl = computed(() => (typeof window !== 'undefined' ? window.location.href : ''))
|
||||
/** 仅 ?qr=1 时:侧栏展示二维码,且保存海报打进图里 */
|
||||
const showCaptureQr = computed(() => String(route.query.qr ?? '') === '1')
|
||||
/** 二维码指向干净海报地址(去掉 qr 参数,避免扫开又带导出模式) */
|
||||
const qrTargetUrl = computed(() => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
try {
|
||||
const u = new URL(window.location.href)
|
||||
u.searchParams.delete('qr')
|
||||
return u.toString()
|
||||
} catch {
|
||||
return window.location.href
|
||||
}
|
||||
})
|
||||
const audio = useAudio()
|
||||
const hasMusic = computed(() => !!String(cfg.musicUrl || '').trim())
|
||||
/** 与请柬一致:有音乐且自动播放成功后延迟自动揭幕 */
|
||||
@@ -391,7 +409,7 @@ async function mountCaptureQr(host) {
|
||||
|
||||
target.appendChild(badge)
|
||||
const qrCanvas = badge.querySelector('canvas')
|
||||
await renderFestiveQr(qrCanvas, pageUrl.value, {
|
||||
await renderFestiveQr(qrCanvas, qrTargetUrl.value, {
|
||||
size: renderSize,
|
||||
centerImg: cfg.qrCenterImg,
|
||||
})
|
||||
@@ -432,7 +450,9 @@ async function savePosterImage() {
|
||||
let cleanupBake = null
|
||||
try {
|
||||
cleanupBake = await bakeObjectFitImages(el)
|
||||
cleanupQr = await mountCaptureQr(el)
|
||||
if (showCaptureQr.value) {
|
||||
cleanupQr = await mountCaptureQr(el)
|
||||
}
|
||||
// 导出前重绘囍印章位图,避免 html2canvas 对 CJK 文本错位
|
||||
try { await document.fonts?.ready } catch { /* ignore */ }
|
||||
el.querySelectorAll('canvas.sp-xi-mid, canvas.hb-xi-circle').forEach((c) => {
|
||||
|
||||
@@ -116,12 +116,12 @@
|
||||
|
||||
<div class="sp-couple" :class="{ 'is-show': stage >= 5 }">
|
||||
<div class="sp-person sp-person-l">
|
||||
<span class="sp-role calligraphy">新郎</span>
|
||||
<span class="sp-role calligraphy">{{ roleLeft }}</span>
|
||||
<BlurText v-if="stage >= 5" :text="groom" :active="true" animate-by="letters" :delay="40" class-name="sp-name calligraphy" />
|
||||
</div>
|
||||
<canvas ref="xiSealRef" class="sp-xi-mid" width="56" height="56" aria-hidden="true"></canvas>
|
||||
<div class="sp-person sp-person-r">
|
||||
<span class="sp-role calligraphy">新娘</span>
|
||||
<span class="sp-role calligraphy">{{ roleRight }}</span>
|
||||
<BlurText v-if="stage >= 5" :text="bride" :active="true" animate-by="letters" :delay="40" class-name="sp-name calligraphy" @animation-complete="bumpStage(6)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,8 +190,16 @@ const bodyLines = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const groom = computed(() => props.cfg.groomName || props.cfg.sonName || '')
|
||||
const bride = computed(() => props.cfg.brideName || props.cfg.daughterInLawName || '')
|
||||
const groom = computed(() => props.cfg.sonName || props.cfg.groomName || '')
|
||||
const bride = computed(() => props.cfg.daughterInLawName || props.cfg.brideName || '')
|
||||
const roleLeft = computed(() => {
|
||||
const s = String(props.cfg.sonLabel || '').replace(/[::]\s*$/, '').trim()
|
||||
return s || '新郎'
|
||||
})
|
||||
const roleRight = computed(() => {
|
||||
const s = String(props.cfg.daughterInLawLabel || '').replace(/[::]\s*$/, '').trim()
|
||||
return s || '新娘'
|
||||
})
|
||||
|
||||
async function paintXiSeal() {
|
||||
try { await document.fonts?.ready } catch { /* ignore */ }
|
||||
|
||||
Reference in New Issue
Block a user