1. 海报
This commit is contained in:
Binary file not shown.
@@ -23,13 +23,14 @@ func Register(api *gin.RouterGroup, database *gorm.DB, adminGuard func(*gin.Cont
|
|||||||
api.POST("/poster/config", handleSavePosterConfig)
|
api.POST("/poster/config", handleSavePosterConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveSide 返回 "1"(男方)或 "2"(女方);无法识别返回空
|
||||||
func resolveSide(raw string) string {
|
func resolveSide(raw string) string {
|
||||||
v := strings.ToLower(strings.TrimSpace(raw))
|
v := strings.ToLower(strings.TrimSpace(raw))
|
||||||
switch v {
|
switch v {
|
||||||
case "female", "nv", "f", "bride":
|
case "1", "male", "n", "m", "groom":
|
||||||
return "female"
|
return "1"
|
||||||
case "male", "n", "m", "groom":
|
case "2", "female", "nv", "f", "bride":
|
||||||
return "male"
|
return "2"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -39,13 +40,12 @@ func isLegacyFlat(m map[string]interface{}) bool {
|
|||||||
if m == nil {
|
if m == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if _, ok := m["male"]; ok {
|
for _, k := range []string{"1", "2", "male", "female"} {
|
||||||
return false
|
if _, ok := m[k]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if _, ok := m["female"]; ok {
|
for _, k := range []string{"title", "lines", "heroImg", "sonName", "template"} {
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, k := range []string{"title", "lines", "heroImg", "sonName"} {
|
|
||||||
if _, ok := m[k]; ok {
|
if _, ok := m[k]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -64,24 +64,33 @@ func asObject(v interface{}) map[string]interface{} {
|
|||||||
return emptyObj()
|
return emptyObj()
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeBundle 统一为 { male, female };旧扁平配置迁入 male
|
func takeSide(raw map[string]interface{}, keys ...string) map[string]interface{} {
|
||||||
|
for _, k := range keys {
|
||||||
|
if m := asObject(raw[k]); len(m) > 0 {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emptyObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeBundle 统一为 { "1", "2" };兼容旧 male/female 与扁平配置
|
||||||
func normalizeBundle(raw map[string]interface{}) map[string]interface{} {
|
func normalizeBundle(raw map[string]interface{}) map[string]interface{} {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
raw = emptyObj()
|
raw = emptyObj()
|
||||||
}
|
}
|
||||||
out := map[string]interface{}{
|
out := map[string]interface{}{
|
||||||
"male": emptyObj(),
|
"1": emptyObj(),
|
||||||
"female": emptyObj(),
|
"2": emptyObj(),
|
||||||
}
|
}
|
||||||
if isLegacyFlat(raw) {
|
if isLegacyFlat(raw) {
|
||||||
out["male"] = raw
|
out["1"] = raw
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
if m := asObject(raw["male"]); len(m) > 0 {
|
if m := takeSide(raw, "1", "male"); len(m) > 0 {
|
||||||
out["male"] = m
|
out["1"] = m
|
||||||
}
|
}
|
||||||
if f := asObject(raw["female"]); len(f) > 0 {
|
if f := takeSide(raw, "2", "female"); len(f) > 0 {
|
||||||
out["female"] = f
|
out["2"] = f
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ export function saveUploadConfig(payload) {
|
|||||||
return service.post('/upload/config', payload)
|
return service.post('/upload/config', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** side 可选:male|female|n|nv;不传则返回 { male, female } 整包 */
|
/** side 可选:1|2(亦兼容 male/female/n/nv);不传则返回 { "1", "2" } 整包 */
|
||||||
export function getPosterConfig(side) {
|
export function getPosterConfig(side) {
|
||||||
const params = side ? { side } : undefined
|
const params = side != null && side !== '' ? { side } : undefined
|
||||||
return service.get('/poster/config', { params })
|
return service.get('/poster/config', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
/** 海报 side:male=/hb-n 男方,female=/hb-nv 女方 */
|
/** 海报 side:1=男方(/hb-n),2=女方(/hb-nv) */
|
||||||
|
|
||||||
export const POSTER_SIDES = [
|
export const POSTER_SIDES = [
|
||||||
{ key: 'male', route: '/hb-n', label: '男方', short: 'n' },
|
{ key: 1, keyStr: '1', route: '/hb-n', label: '男方', short: 'n' },
|
||||||
{ key: 'female', route: '/hb-nv', label: '女方', short: 'nv' },
|
{ key: 2, keyStr: '2', route: '/hb-nv', label: '女方', short: 'nv' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const POSTER_TEMPLATES = [
|
||||||
|
{ value: 'classic', label: '经典竖版', desc: '顶图 + 正文逐行出现' },
|
||||||
|
{ value: 'split', label: '左图右文', desc: '竖排书法 + 右侧宴席信息' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 解析为 1 | 2,无法识别返回 0 */
|
||||||
export function resolvePosterSide(input) {
|
export function resolvePosterSide(input) {
|
||||||
|
if (input === 1 || input === '1') return 1
|
||||||
|
if (input === 2 || input === '2') return 2
|
||||||
const v = String(input || '').trim().toLowerCase()
|
const v = String(input || '').trim().toLowerCase()
|
||||||
if (v === 'female' || v === 'nv' || v === 'f' || v === 'bride') return 'female'
|
if (v === 'female' || v === 'nv' || v === 'f' || v === 'bride') return 2
|
||||||
if (v === 'male' || v === 'n' || v === 'm' || v === 'groom') return 'male'
|
if (v === 'male' || v === 'n' || v === 'm' || v === 'groom') return 1
|
||||||
return ''
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function baseFields() {
|
export function sideKey(side) {
|
||||||
|
const s = resolvePosterSide(side) || 1
|
||||||
|
return String(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseFields(side) {
|
||||||
return {
|
return {
|
||||||
|
side: resolvePosterSide(side) || 1,
|
||||||
|
template: 'classic',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
heroImg: '',
|
heroImg: '',
|
||||||
title: '婚礼邀请函',
|
title: '婚礼邀请函',
|
||||||
@@ -23,17 +38,32 @@ function baseFields() {
|
|||||||
showInviteButton: true,
|
showInviteButton: true,
|
||||||
inviteButtonText: '打开请柬',
|
inviteButtonText: '打开请柬',
|
||||||
invitePath: '/',
|
invitePath: '/',
|
||||||
|
// split 模板字段
|
||||||
|
verticalSlogan: '吾家有喜',
|
||||||
|
subSlogan1: '佳偶天成',
|
||||||
|
subSlogan2: '百年好合',
|
||||||
|
greeting: '尊敬的各位亲朋好友',
|
||||||
|
groomName: '',
|
||||||
|
brideName: '',
|
||||||
|
dateText: '2026年10月3日 星期日',
|
||||||
|
lunarText: '农历九月初八',
|
||||||
|
venueText: '幸福大酒店二楼宴会厅',
|
||||||
|
loveFooter: 'Love you',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 男方默认文案 */
|
/** 男方(1) 默认 */
|
||||||
export function defaultMalePosterConfig() {
|
export function defaultSide1PosterConfig() {
|
||||||
return {
|
return {
|
||||||
...baseFields(),
|
...baseFields(1),
|
||||||
|
side: 1,
|
||||||
sonLabel: '儿子:',
|
sonLabel: '儿子:',
|
||||||
sonName: '张浩强',
|
sonName: '张浩强',
|
||||||
daughterInLawLabel: '儿媳:',
|
daughterInLawLabel: '儿媳:',
|
||||||
daughterInLawName: '刘佳怡',
|
daughterInLawName: '刘佳怡',
|
||||||
|
groomName: '张浩强',
|
||||||
|
brideName: '刘佳怡',
|
||||||
|
verticalSlogan: '吾家有喜',
|
||||||
lines: [
|
lines: [
|
||||||
'各位亲朋好友',
|
'各位亲朋好友',
|
||||||
'吾家有喜 儿子结婚',
|
'吾家有喜 儿子结婚',
|
||||||
@@ -48,14 +78,18 @@ export function defaultMalePosterConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 女方默认文案 */
|
/** 女方(2) 默认 */
|
||||||
export function defaultFemalePosterConfig() {
|
export function defaultSide2PosterConfig() {
|
||||||
return {
|
return {
|
||||||
...baseFields(),
|
...baseFields(2),
|
||||||
|
side: 2,
|
||||||
sonLabel: '女儿:',
|
sonLabel: '女儿:',
|
||||||
sonName: '刘佳怡',
|
sonName: '刘佳怡',
|
||||||
daughterInLawLabel: '女婿:',
|
daughterInLawLabel: '女婿:',
|
||||||
daughterInLawName: '张浩强',
|
daughterInLawName: '张浩强',
|
||||||
|
groomName: '张浩强',
|
||||||
|
brideName: '刘佳怡',
|
||||||
|
verticalSlogan: '小女出阁',
|
||||||
lines: [
|
lines: [
|
||||||
'各位亲朋好友',
|
'各位亲朋好友',
|
||||||
'吾家有喜 女儿出嫁',
|
'吾家有喜 女儿出嫁',
|
||||||
@@ -70,15 +104,20 @@ export function defaultFemalePosterConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated 兼容旧调用,等同男方默认 */
|
export function defaultMalePosterConfig() {
|
||||||
|
return defaultSide1PosterConfig()
|
||||||
|
}
|
||||||
|
export function defaultFemalePosterConfig() {
|
||||||
|
return defaultSide2PosterConfig()
|
||||||
|
}
|
||||||
export function defaultPosterConfig() {
|
export function defaultPosterConfig() {
|
||||||
return defaultMalePosterConfig()
|
return defaultSide1PosterConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultPosterBundle() {
|
export function defaultPosterBundle() {
|
||||||
return {
|
return {
|
||||||
male: defaultMalePosterConfig(),
|
'1': defaultSide1PosterConfig(),
|
||||||
female: defaultFemalePosterConfig(),
|
'2': defaultSide2PosterConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,24 +133,42 @@ export const ENTER_EFFECTS = [
|
|||||||
{ value: 'curtain', label: '金幕拉开' },
|
{ value: 'curtain', label: '金幕拉开' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 是否为旧版扁平配置(未分男女方) */
|
|
||||||
function isLegacyFlatPoster(raw) {
|
function isLegacyFlatPoster(raw) {
|
||||||
if (!raw || typeof raw !== 'object') return false
|
if (!raw || typeof raw !== 'object') return false
|
||||||
if (raw.male || raw.female) return false
|
if (raw['1'] || raw['2'] || raw[1] || raw[2] || raw.male || raw.female) return false
|
||||||
return 'title' in raw || 'lines' in raw || 'heroImg' in raw || 'sonName' in raw
|
return 'title' in raw || 'lines' in raw || 'heroImg' in raw || 'sonName' in raw || 'template' in raw
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizePosterConfig(raw, side = 'male') {
|
function pickSideSource(loaded, side) {
|
||||||
const s = resolvePosterSide(side) || 'male'
|
const k = sideKey(side)
|
||||||
const base = s === 'female' ? defaultFemalePosterConfig() : defaultMalePosterConfig()
|
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
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePosterConfig(raw, side = 1) {
|
||||||
|
const s = resolvePosterSide(side) || 1
|
||||||
|
const base = s === 2 ? defaultSide2PosterConfig() : defaultSide1PosterConfig()
|
||||||
const loaded = raw && typeof raw === 'object' ? raw : {}
|
const loaded = raw && typeof raw === 'object' ? raw : {}
|
||||||
const lines = Array.isArray(loaded.lines)
|
const lines = Array.isArray(loaded.lines)
|
||||||
? loaded.lines.map((l) => String(l ?? ''))
|
? loaded.lines.map((l) => String(l ?? ''))
|
||||||
: base.lines
|
: base.lines
|
||||||
|
const template = POSTER_TEMPLATES.some((t) => t.value === loaded.template)
|
||||||
|
? loaded.template
|
||||||
|
: base.template
|
||||||
|
|
||||||
|
const groomName = loaded.groomName || loaded.sonName || base.groomName
|
||||||
|
const brideName = loaded.brideName || loaded.daughterInLawName || base.brideName
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
...loaded,
|
...loaded,
|
||||||
|
side: s,
|
||||||
|
template,
|
||||||
lines: lines.length ? lines : base.lines,
|
lines: lines.length ? lines : base.lines,
|
||||||
|
groomName,
|
||||||
|
brideName,
|
||||||
enabled: loaded.enabled !== false,
|
enabled: loaded.enabled !== false,
|
||||||
showInviteButton: loaded.showInviteButton !== false,
|
showInviteButton: loaded.showInviteButton !== false,
|
||||||
loadingEffect: LOADING_EFFECTS.some((e) => e.value === loaded.loadingEffect)
|
loadingEffect: LOADING_EFFECTS.some((e) => e.value === loaded.loadingEffect)
|
||||||
@@ -122,26 +179,34 @@ export function normalizePosterConfig(raw, side = 'male') {
|
|||||||
: base.enterEffect,
|
: base.enterEffect,
|
||||||
invitePath: loaded.invitePath || '/',
|
invitePath: loaded.invitePath || '/',
|
||||||
inviteButtonText: loaded.inviteButtonText || base.inviteButtonText,
|
inviteButtonText: loaded.inviteButtonText || base.inviteButtonText,
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 归一化整包 { male, female };兼容旧扁平数据迁入 male */
|
/** 归一化为 { 1, 2 };兼容旧 male/female 与扁平数据 */
|
||||||
export function normalizePosterBundle(raw) {
|
export function normalizePosterBundle(raw) {
|
||||||
const loaded = raw && typeof raw === 'object' ? raw : {}
|
const loaded = raw && typeof raw === 'object' ? raw : {}
|
||||||
if (isLegacyFlatPoster(loaded)) {
|
if (isLegacyFlatPoster(loaded)) {
|
||||||
return {
|
return {
|
||||||
male: normalizePosterConfig(loaded, 'male'),
|
'1': normalizePosterConfig(loaded, 1),
|
||||||
female: defaultFemalePosterConfig(),
|
'2': defaultSide2PosterConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
male: normalizePosterConfig(loaded.male, 'male'),
|
'1': normalizePosterConfig(pickSideSource(loaded, 1) || {}, 1),
|
||||||
female: normalizePosterConfig(loaded.female, 'female'),
|
'2': normalizePosterConfig(pickSideSource(loaded, 2) || {}, 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickPosterSide(bundle, side) {
|
export function pickPosterSide(bundle, side) {
|
||||||
const normalized = normalizePosterBundle(bundle)
|
const normalized = normalizePosterBundle(bundle)
|
||||||
const s = resolvePosterSide(side) || 'male'
|
const s = sideKey(side)
|
||||||
return normalized[s]
|
return normalized[s]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ const routes = [
|
|||||||
path: '/hb-n',
|
path: '/hb-n',
|
||||||
name: 'PosterMale',
|
name: 'PosterMale',
|
||||||
component: () => import('@/views/hb/index.vue'),
|
component: () => import('@/views/hb/index.vue'),
|
||||||
meta: { posterSide: 'male' },
|
meta: { posterSide: 1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/hb-nv',
|
path: '/hb-nv',
|
||||||
name: 'PosterFemale',
|
name: 'PosterFemale',
|
||||||
component: () => import('@/views/hb/index.vue'),
|
component: () => import('@/views/hb/index.vue'),
|
||||||
meta: { posterSide: 'female' },
|
meta: { posterSide: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
<a-menu-item key="male" @click="openPosterPreview('male')">男方 /hb-n</a-menu-item>
|
<a-menu-item key="1" @click="openPosterPreview(1)">男方(1) /hb-n</a-menu-item>
|
||||||
<a-menu-item key="female" @click="openPosterPreview('female')">女方 /hb-nv</a-menu-item>
|
<a-menu-item key="2" @click="openPosterPreview(2)">女方(2) /hb-nv</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
@@ -410,20 +410,34 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- 海报(独立 poster_configs:男方 /hb-n、女方 /hb-nv) -->
|
<!-- 海报(独立 poster_configs:side 1 男方 /hb-n,2 女方 /hb-nv) -->
|
||||||
<a-tab-pane key="poster" tab="海报">
|
<a-tab-pane key="poster" tab="海报">
|
||||||
<a-card v-if="activeTab === 'poster'" :bordered="false" class="!shadow-sm">
|
<a-card v-if="activeTab === 'poster'" :bordered="false" class="!shadow-sm">
|
||||||
<a-alert
|
<a-alert
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
message="海报配置独立存储。男方前台 /hb-n,女方前台 /hb-nv"
|
message="海报配置独立存储。side=1 男方 /hb-n,side=2 女方 /hb-nv;两侧可分别选择模板。"
|
||||||
/>
|
/>
|
||||||
<a-tabs v-model:activeKey="posterSideTab" size="small" type="card" class="mb-3">
|
<a-tabs v-model:activeKey="posterSideTab" size="small" type="card" class="mb-3">
|
||||||
<a-tab-pane key="male" tab="男方海报" />
|
<a-tab-pane key="1" tab="男方(1)" />
|
||||||
<a-tab-pane key="female" tab="女方海报" />
|
<a-tab-pane key="2" tab="女方(2)" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
<a-form layout="vertical" :class="{ 'opacity-60 pointer-events-none': posterLoading }">
|
<a-form layout="vertical" :class="{ 'opacity-60 pointer-events-none': posterLoading }">
|
||||||
|
<a-form-item label="所属方">
|
||||||
|
<a-tag color="gold">{{ posterSideTab === '2' ? '2 · 女方' : '1 · 男方' }}</a-tag>
|
||||||
|
<span class="text-[11px] text-[#8A8680] ml-2">路由 {{ posterSideTab === '2' ? '/hb-nv' : '/hb-n' }}</span>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="版式模板">
|
||||||
|
<a-radio-group v-model:value="posterCfg.template" class="poster-tpl-group">
|
||||||
|
<a-radio-button v-for="t in POSTER_TEMPLATES" :key="t.value" :value="t.value">
|
||||||
|
{{ t.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<div class="text-[11px] text-[#8A8680] mt-1">
|
||||||
|
{{ POSTER_TEMPLATES.find(t => t.value === posterCfg.template)?.desc }}
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<a-form-item label="启用海报页">
|
<a-form-item label="启用海报页">
|
||||||
<a-switch v-model:checked="posterCfg.enabled" />
|
<a-switch v-model:checked="posterCfg.enabled" />
|
||||||
@@ -450,39 +464,85 @@
|
|||||||
<a-input v-model:value="posterCfg.invitePath" placeholder="/" />
|
<a-input v-model:value="posterCfg.invitePath" placeholder="/" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
<a-form-item label="顶部照片">
|
<a-form-item :label="posterCfg.template === 'split' ? '左侧照片' : '顶部照片'">
|
||||||
<image-field v-model="posterCfg.heroImg" label="海报顶图" @pick="openUpload(u => posterCfg.heroImg = u)" :uploading="uploading" />
|
<image-field v-model="posterCfg.heroImg" label="海报图片" @pick="setPosterHero" :uploading="uploading" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
|
||||||
<a-form-item :label="posterSideTab === 'female' ? '女儿称谓 / 姓名' : '儿子称谓 / 姓名'">
|
<!-- classic 字段 -->
|
||||||
<div class="flex gap-2">
|
<template v-if="posterCfg.template === 'classic'">
|
||||||
<a-input v-model:value="posterCfg.sonLabel" class="!w-[100px]" :placeholder="posterSideTab === 'female' ? '女儿:' : '儿子:'" />
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||||
<a-input v-model:value="posterCfg.sonName" placeholder="姓名" />
|
<a-form-item :label="posterSideTab === '2' ? '女儿称谓 / 姓名' : '儿子称谓 / 姓名'">
|
||||||
</div>
|
<div class="flex gap-2">
|
||||||
|
<a-input v-model:value="posterCfg.sonLabel" class="!w-[100px]" :placeholder="posterSideTab === '2' ? '女儿:' : '儿子:'" />
|
||||||
|
<a-input v-model:value="posterCfg.sonName" placeholder="姓名" />
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="posterSideTab === '2' ? '女婿称谓 / 姓名' : '儿媳称谓 / 姓名'">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a-input v-model:value="posterCfg.daughterInLawLabel" class="!w-[100px]" :placeholder="posterSideTab === '2' ? '女婿:' : '儿媳:'" />
|
||||||
|
<a-input v-model:value="posterCfg.daughterInLawName" placeholder="姓名" />
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
<a-form-item label="主标题">
|
||||||
|
<a-input v-model:value="posterCfg.title" placeholder="婚礼邀请函" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="posterSideTab === 'female' ? '女婿称谓 / 姓名' : '儿媳称谓 / 姓名'">
|
<a-form-item label="正文(每行一条)">
|
||||||
<div class="flex gap-2">
|
<a-textarea v-model:value="posterLinesText" :rows="10" placeholder="每行一条正文" />
|
||||||
<a-input v-model:value="posterCfg.daughterInLawLabel" class="!w-[100px]" :placeholder="posterSideTab === 'female' ? '女婿:' : '儿媳:'" />
|
|
||||||
<a-input v-model:value="posterCfg.daughterInLawName" placeholder="姓名" />
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
<a-form-item label="底部文案">
|
||||||
<a-form-item label="主标题">
|
<a-input v-model:value="posterCfg.footer" />
|
||||||
<a-input v-model:value="posterCfg.title" placeholder="婚礼邀请函" />
|
</a-form-item>
|
||||||
</a-form-item>
|
</template>
|
||||||
<a-form-item label="正文(每行一条)">
|
|
||||||
<a-textarea v-model:value="posterLinesText" :rows="10" placeholder="每行一条正文" />
|
<!-- split 字段 -->
|
||||||
</a-form-item>
|
<template v-else>
|
||||||
<a-form-item label="底部文案">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-6">
|
||||||
<a-input v-model:value="posterCfg.footer" />
|
<a-form-item label="竖排主标">
|
||||||
</a-form-item>
|
<a-input v-model:value="posterCfg.verticalSlogan" placeholder="吾家有喜" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="副标一">
|
||||||
|
<a-input v-model:value="posterCfg.subSlogan1" placeholder="佳偶天成" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="副标二">
|
||||||
|
<a-input v-model:value="posterCfg.subSlogan2" placeholder="百年好合" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
<a-form-item label="问候语">
|
||||||
|
<a-input v-model:value="posterCfg.greeting" placeholder="尊敬的各位亲朋好友" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="正文(每行一条,建议 2~4 行)">
|
||||||
|
<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>
|
||||||
|
<a-form-item label="新娘">
|
||||||
|
<a-input v-model:value="posterCfg.brideName" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="喜宴时间">
|
||||||
|
<a-input v-model:value="posterCfg.dateText" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="农历">
|
||||||
|
<a-input v-model:value="posterCfg.lunarText" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
<a-form-item label="地点">
|
||||||
|
<a-input v-model:value="posterCfg.venueText" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="底部 Love 文案">
|
||||||
|
<a-input v-model:value="posterCfg.loveFooter" placeholder="Love you" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<a-button type="primary" :loading="posterSaving" @click="onSavePosterConfig"
|
<a-button type="primary" :loading="posterSaving" @click="onSavePosterConfig"
|
||||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
||||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存海报配置
|
<i class="fa-solid fa-floppy-disk mr-1"></i>保存海报配置
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="openPosterPreview(posterSideTab)">
|
<a-button @click="openPosterPreview(posterSideTab)">
|
||||||
<i class="fa-solid fa-eye mr-1"></i>打开 {{ posterSideTab === 'female' ? '/hb-nv' : '/hb-n' }} 预览
|
<i class="fa-solid fa-eye mr-1"></i>打开 {{ posterSideTab === '2' ? '/hb-nv' : '/hb-n' }} 预览
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
@@ -830,7 +890,7 @@ import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
|
|||||||
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
|
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
|
||||||
import {
|
import {
|
||||||
defaultPosterBundle, normalizePosterBundle, normalizePosterConfig,
|
defaultPosterBundle, normalizePosterBundle, normalizePosterConfig,
|
||||||
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES,
|
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_TEMPLATES, sideKey,
|
||||||
} from '@/composables/posterDefaults'
|
} from '@/composables/posterDefaults'
|
||||||
|
|
||||||
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||||
@@ -888,22 +948,27 @@ let regionChart = null
|
|||||||
|
|
||||||
const cfg = reactive(defaultConfig())
|
const cfg = reactive(defaultConfig())
|
||||||
const uploadCfg = reactive(defaultUploadConfig())
|
const uploadCfg = reactive(defaultUploadConfig())
|
||||||
const posterSideTab = ref('male')
|
const posterSideTab = ref('1')
|
||||||
const posterBundle = reactive(defaultPosterBundle())
|
const posterBundle = reactive(defaultPosterBundle())
|
||||||
const posterCfg = computed(() => posterBundle[posterSideTab.value] || posterBundle.male)
|
const posterCfg = computed(() => posterBundle[sideKey(posterSideTab.value)] || posterBundle['1'])
|
||||||
const posterLinesText = computed({
|
const posterLinesText = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
const lines = posterCfg.value?.lines
|
const lines = posterCfg.value?.lines
|
||||||
return Array.isArray(lines) ? lines.join('\n') : ''
|
return Array.isArray(lines) ? lines.join('\n') : ''
|
||||||
},
|
},
|
||||||
set: (v) => {
|
set: (v) => {
|
||||||
const side = posterSideTab.value
|
const side = sideKey(posterSideTab.value)
|
||||||
if (!posterBundle[side]) posterBundle[side] = normalizePosterConfig(null, side)
|
if (!posterBundle[side]) posterBundle[side] = normalizePosterConfig(null, side)
|
||||||
posterBundle[side].lines = String(v || '')
|
posterBundle[side].lines = String(v || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((s) => s.trimEnd())
|
.map((s) => s.trimEnd())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
function setPosterHero(url) {
|
||||||
|
const side = sideKey(posterSideTab.value)
|
||||||
|
if (!posterBundle[side]) posterBundle[side] = normalizePosterConfig(null, side)
|
||||||
|
posterBundle[side].heroImg = url
|
||||||
|
}
|
||||||
const currentPhotoIndex = computed(() => Math.min(Number(activePhotoTab.value) || 0, Math.max(cfg.photoPages.length - 1, 0)))
|
const currentPhotoIndex = computed(() => Math.min(Number(activePhotoTab.value) || 0, Math.max(cfg.photoPages.length - 1, 0)))
|
||||||
const isConfigTab = computed(() => CONFIG_SECTIONS.includes(activeTab.value))
|
const isConfigTab = computed(() => CONFIG_SECTIONS.includes(activeTab.value))
|
||||||
const visitPagination = computed(() => ({
|
const visitPagination = computed(() => ({
|
||||||
@@ -1324,8 +1389,8 @@ async function loadPosterConfig() {
|
|||||||
try {
|
try {
|
||||||
const res = await getPosterConfig()
|
const res = await getPosterConfig()
|
||||||
const bundle = normalizePosterBundle(res.data || {})
|
const bundle = normalizePosterBundle(res.data || {})
|
||||||
posterBundle.male = bundle.male
|
posterBundle['1'] = bundle['1']
|
||||||
posterBundle.female = bundle.female
|
posterBundle['2'] = bundle['2']
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e?.message || '海报配置加载失败')
|
message.error(e?.message || '海报配置加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1339,8 +1404,8 @@ async function onSavePosterConfig() {
|
|||||||
const payload = normalizePosterBundle(posterBundle)
|
const payload = normalizePosterBundle(posterBundle)
|
||||||
const res = await savePosterConfig(payload)
|
const res = await savePosterConfig(payload)
|
||||||
const bundle = normalizePosterBundle(res.data || payload)
|
const bundle = normalizePosterBundle(res.data || payload)
|
||||||
posterBundle.male = bundle.male
|
posterBundle['1'] = bundle['1']
|
||||||
posterBundle.female = bundle.female
|
posterBundle['2'] = bundle['2']
|
||||||
message.success('海报配置已保存')
|
message.success('海报配置已保存')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e?.message || '海报配置保存失败')
|
message.error(e?.message || '海报配置保存失败')
|
||||||
@@ -1547,8 +1612,9 @@ async function onTabActivate(tab) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPreview() { window.open('/', '_blank') }
|
function openPreview() { window.open('/', '_blank') }
|
||||||
function openPosterPreview(side = 'male') {
|
function openPosterPreview(side = 1) {
|
||||||
const meta = POSTER_SIDES.find((s) => s.key === side) || POSTER_SIDES[0]
|
const s = Number(sideKey(side))
|
||||||
|
const meta = POSTER_SIDES.find((x) => x.key === s) || POSTER_SIDES[0]
|
||||||
window.open(meta.route, '_blank')
|
window.open(meta.route, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="hb-root">
|
<div class="hb-root">
|
||||||
<!-- 全屏背景婚礼装饰 -->
|
|
||||||
<div class="hb-bg-decor" aria-hidden="true">
|
<div class="hb-bg-decor" aria-hidden="true">
|
||||||
<span class="hb-bg-xi hb-bg-xi-1">囍</span>
|
<span class="hb-bg-xi hb-bg-xi-1">囍</span>
|
||||||
<span class="hb-bg-xi hb-bg-xi-2">囍</span>
|
<span class="hb-bg-xi hb-bg-xi-2">囍</span>
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
<i class="hb-bg-spark hb-bg-spark-4"></i>
|
<i class="hb-bg-spark hb-bg-spark-4"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 共用 SVG 图标定义 -->
|
|
||||||
<svg class="hb-sprite" aria-hidden="true">
|
<svg class="hb-sprite" aria-hidden="true">
|
||||||
<symbol id="hb-icon-knot" viewBox="0 0 64 64">
|
<symbol id="hb-icon-knot" viewBox="0 0 64 64">
|
||||||
<path d="M32 6c-4 8-14 12-14 22 0 6 4 10 10 12-6 2-10 6-10 12 0 10 10 14 14 22 4-8 14-12 14-22 0-6-4-10-10-12 6-2 10-6 10-12 0-10-10-14-14-22z" stroke="currentColor" stroke-width="2.2" fill="none"/>
|
<path d="M32 6c-4 8-14 12-14 22 0 6 4 10 10 12-6 2-10 6-10 12 0 10 10 14 14 22 4-8 14-12 14-22 0-6-4-10-10-12 6-2 10-6 10-12 0-10-10-14-14-22z" stroke="currentColor" stroke-width="2.2" fill="none"/>
|
||||||
@@ -46,14 +44,12 @@
|
|||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Loading -->
|
|
||||||
<div v-if="phase === 'loading'" class="hb-loading" :class="`hb-loading--${cfg.loadingEffect}`">
|
<div v-if="phase === 'loading'" class="hb-loading" :class="`hb-loading--${cfg.loadingEffect}`">
|
||||||
<div v-if="cfg.loadingEffect === 'redSeal'" class="hb-seal">囍</div>
|
<div v-if="cfg.loadingEffect === 'redSeal'" class="hb-seal">囍</div>
|
||||||
<div v-else-if="cfg.loadingEffect === 'goldShimmer'" class="hb-shimmer">婚礼邀请函</div>
|
<div v-else-if="cfg.loadingEffect === 'goldShimmer'" class="hb-shimmer">婚礼邀请函</div>
|
||||||
<div v-else class="hb-fade-dot"></div>
|
<div v-else class="hb-fade-dot"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enter curtain -->
|
|
||||||
<div
|
<div
|
||||||
v-if="phase === 'enter'"
|
v-if="phase === 'enter'"
|
||||||
class="hb-enter"
|
class="hb-enter"
|
||||||
@@ -64,133 +60,20 @@
|
|||||||
<div class="hb-enter-veil"></div>
|
<div class="hb-enter-veil"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Poster -->
|
<template v-if="cfg.enabled && (phase === 'content' || phase === 'enter')">
|
||||||
<div v-show="phase === 'content' || phase === 'enter'" class="hb-stage" :class="{ 'is-visible': contentVisible }">
|
<ClassicPoster
|
||||||
<div class="hb-frame">
|
v-if="cfg.template === 'classic'"
|
||||||
<div class="hb-frame-ornament hb-frame-ornament-tl" aria-hidden="true">
|
:cfg="cfg"
|
||||||
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
:ready="contentVisible && phase === 'content'"
|
||||||
</div>
|
@invite="goInvite"
|
||||||
<div class="hb-frame-ornament hb-frame-ornament-tr" aria-hidden="true">
|
/>
|
||||||
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
<SplitPoster
|
||||||
</div>
|
v-else
|
||||||
<div class="hb-frame-ornament hb-frame-ornament-bl" aria-hidden="true">
|
:cfg="cfg"
|
||||||
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
:ready="contentVisible && phase === 'content'"
|
||||||
</div>
|
@invite="goInvite"
|
||||||
<div class="hb-frame-ornament hb-frame-ornament-br" aria-hidden="true">
|
/>
|
||||||
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
</template>
|
||||||
</div>
|
|
||||||
<div class="hb-inner">
|
|
||||||
<!-- 纸面装饰:角花、水印囍、同心结、祥云 -->
|
|
||||||
<div class="hb-paper-decor" aria-hidden="true">
|
|
||||||
<span class="hb-paper-watermark">囍</span>
|
|
||||||
<svg class="hb-paper-knot hb-paper-knot-tl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
|
||||||
<svg class="hb-paper-knot hb-paper-knot-tr" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
|
||||||
<svg class="hb-paper-knot hb-paper-knot-bl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
|
||||||
<svg class="hb-paper-knot hb-paper-knot-br" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
|
||||||
<svg class="hb-paper-cloud hb-paper-cloud-t" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
|
|
||||||
<svg class="hb-paper-cloud hb-paper-cloud-b" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
|
|
||||||
<span class="hb-paper-xi hb-paper-xi-l">囍</span>
|
|
||||||
<span class="hb-paper-xi hb-paper-xi-r">囍</span>
|
|
||||||
<span class="hb-paper-dot hb-paper-dot-1"></span>
|
|
||||||
<span class="hb-paper-dot hb-paper-dot-2"></span>
|
|
||||||
<span class="hb-paper-dot hb-paper-dot-3"></span>
|
|
||||||
<span class="hb-paper-dot hb-paper-dot-4"></span>
|
|
||||||
<div class="hb-paper-side hb-paper-side-l"></div>
|
|
||||||
<div class="hb-paper-side hb-paper-side-r"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hb-hero-wrap" :class="{ 'is-show': stage >= 1 }">
|
|
||||||
<div class="hb-hero">
|
|
||||||
<img v-if="cfg.heroImg" :src="cfg.heroImg" alt="" class="hb-hero-img" />
|
|
||||||
<div v-else class="hb-hero-placeholder">婚礼照片</div>
|
|
||||||
<span class="hb-xi hb-xi-l">囍</span>
|
|
||||||
<span class="hb-xi hb-xi-r">囍</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hb-names" :class="{ 'is-show': stage >= 2 }">
|
|
||||||
<BlurText
|
|
||||||
v-if="stage >= 2"
|
|
||||||
:text="nameLeft"
|
|
||||||
:active="stage >= 2"
|
|
||||||
animate-by="letters"
|
|
||||||
:delay="40"
|
|
||||||
class-name="hb-name"
|
|
||||||
@animation-complete="onNamesLeftDone"
|
|
||||||
/>
|
|
||||||
<span class="hb-xi-circle">囍</span>
|
|
||||||
<BlurText
|
|
||||||
v-if="namesRightReady"
|
|
||||||
:text="nameRight"
|
|
||||||
:active="namesRightReady"
|
|
||||||
animate-by="letters"
|
|
||||||
:delay="40"
|
|
||||||
class-name="hb-name"
|
|
||||||
@animation-complete="bumpStage(3)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hb-title-wrap" :class="{ 'is-show': stage >= 3 }">
|
|
||||||
<BlurText
|
|
||||||
v-if="stage >= 3"
|
|
||||||
:text="cfg.title"
|
|
||||||
:active="stage >= 3"
|
|
||||||
animate-by="letters"
|
|
||||||
direction="bottom"
|
|
||||||
:delay="70"
|
|
||||||
:step-duration="0.4"
|
|
||||||
class-name="hb-title calligraphy-strong"
|
|
||||||
@animation-complete="bumpStage(4)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hb-body">
|
|
||||||
<template v-for="(line, i) in cfg.lines" :key="`line-${i}-${line}`">
|
|
||||||
<TextType
|
|
||||||
v-if="lineStage > i && i % 2 === 0"
|
|
||||||
:text="line"
|
|
||||||
:active="lineStage > i"
|
|
||||||
:loop="false"
|
|
||||||
:show-cursor="lineStage === i + 1"
|
|
||||||
cursor-character="|"
|
|
||||||
cursor-class-name="hb-cursor"
|
|
||||||
:typing-speed="48"
|
|
||||||
:initial-delay="80"
|
|
||||||
class-name="hb-line"
|
|
||||||
@animation-complete="onLineDone(i)"
|
|
||||||
/>
|
|
||||||
<BlurText
|
|
||||||
v-else-if="lineStage > i"
|
|
||||||
:text="line"
|
|
||||||
:active="lineStage > i"
|
|
||||||
animate-by="words"
|
|
||||||
:delay="90"
|
|
||||||
class-name="hb-line"
|
|
||||||
@animation-complete="onLineDone(i)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hb-footer" :class="{ 'is-show': stage >= 5 }">
|
|
||||||
<BlurText
|
|
||||||
v-if="stage >= 5"
|
|
||||||
:text="cfg.footer"
|
|
||||||
:active="stage >= 5"
|
|
||||||
animate-by="letters"
|
|
||||||
:delay="35"
|
|
||||||
class-name="hb-footer-text"
|
|
||||||
@animation-complete="bumpStage(6)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="cfg.showInviteButton && stage >= 6" class="hb-cta is-show">
|
|
||||||
<button type="button" class="hb-cta-btn" @click="goInvite">
|
|
||||||
{{ cfg.inviteButtonText }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!cfg.enabled && phase === 'content'" class="hb-disabled">
|
<div v-if="!cfg.enabled && phase === 'content'" class="hb-disabled">
|
||||||
<p>海报暂未开放</p>
|
<p>海报暂未开放</p>
|
||||||
@@ -204,21 +87,16 @@ import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { getPosterConfig } from '@/api/wedding'
|
import { getPosterConfig } from '@/api/wedding'
|
||||||
import { normalizePosterConfig, resolvePosterSide } from '@/composables/posterDefaults'
|
import { normalizePosterConfig, resolvePosterSide } from '@/composables/posterDefaults'
|
||||||
import BlurText from '@/components/vue-bits/BlurText.vue'
|
import ClassicPoster from '@/views/hb/templates/ClassicPoster.vue'
|
||||||
import TextType from '@/components/vue-bits/TextType.vue'
|
import SplitPoster from '@/views/hb/templates/SplitPoster.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const posterSide = computed(() => resolvePosterSide(route.meta.posterSide) || 'male')
|
const posterSide = computed(() => resolvePosterSide(route.meta.posterSide) || 1)
|
||||||
const cfg = reactive(normalizePosterConfig(null, posterSide.value))
|
const cfg = reactive(normalizePosterConfig(null, posterSide.value))
|
||||||
const phase = ref('loading') // loading | enter | content
|
const phase = ref('loading')
|
||||||
const enterLeaving = ref(false)
|
const enterLeaving = ref(false)
|
||||||
const contentVisible = ref(false)
|
const contentVisible = ref(false)
|
||||||
const stage = ref(0)
|
|
||||||
const lineStage = ref(0)
|
|
||||||
const namesRightReady = ref(false)
|
|
||||||
const nameLeft = computed(() => `${cfg.sonLabel || ''}${cfg.sonName || ''}`)
|
|
||||||
const nameRight = computed(() => `${cfg.daughterInLawLabel || ''}${cfg.daughterInLawName || ''}`)
|
|
||||||
let timers = []
|
let timers = []
|
||||||
|
|
||||||
function later(fn, ms) {
|
function later(fn, ms) {
|
||||||
@@ -227,29 +105,6 @@ function later(fn, ms) {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
function bumpStage(n) {
|
|
||||||
if (stage.value < n) stage.value = n
|
|
||||||
if (n === 4) {
|
|
||||||
if (!cfg.lines?.length) {
|
|
||||||
later(() => bumpStage(5), 200)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lineStage.value = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNamesLeftDone() {
|
|
||||||
namesRightReady.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLineDone(i) {
|
|
||||||
if (i + 1 >= cfg.lines.length) {
|
|
||||||
bumpStage(5)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (lineStage.value === i + 1) lineStage.value = i + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
function goInvite() {
|
function goInvite() {
|
||||||
const path = cfg.invitePath || '/'
|
const path = cfg.invitePath || '/'
|
||||||
if (path.startsWith('http')) window.location.href = path
|
if (path.startsWith('http')) window.location.href = path
|
||||||
@@ -276,10 +131,7 @@ async function runSequence() {
|
|||||||
contentVisible.value = true
|
contentVisible.value = true
|
||||||
}, 80)
|
}, 80)
|
||||||
await new Promise((r) => later(r, enterMs))
|
await new Promise((r) => later(r, enterMs))
|
||||||
|
|
||||||
phase.value = 'content'
|
phase.value = 'content'
|
||||||
later(() => { stage.value = 1 }, 200)
|
|
||||||
later(() => { stage.value = 2 }, 700)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -323,130 +175,56 @@ onUnmounted(() => {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-family: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", serif;
|
font-family: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", serif;
|
||||||
}
|
}
|
||||||
|
.hb-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
|
||||||
.hb-sprite {
|
.hb-bg-decor { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 全屏背景装饰 —— */
|
|
||||||
.hb-bg-decor {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-bg-xi {
|
.hb-bg-xi {
|
||||||
position: absolute;
|
position: absolute; color: rgba(240, 215, 140, 0.14); font-weight: 700; line-height: 1;
|
||||||
color: rgba(240, 215, 140, 0.14);
|
user-select: none; animation: hb-pulse-soft 7s ease-in-out infinite;
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
user-select: none;
|
|
||||||
animation: hb-pulse-soft 7s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
.hb-bg-xi-1 { top: 6%; left: 4%; font-size: 56px; transform: rotate(-12deg); animation-delay: 0s; }
|
.hb-bg-xi-1 { top: 6%; left: 4%; font-size: 56px; transform: rotate(-12deg); }
|
||||||
.hb-bg-xi-2 { top: 14%; right: 6%; font-size: 42px; transform: rotate(10deg); animation-delay: 1.2s; color: rgba(196, 30, 58, 0.22); }
|
.hb-bg-xi-2 { top: 14%; right: 6%; font-size: 42px; transform: rotate(10deg); color: rgba(196, 30, 58, 0.22); animation-delay: 1.2s; }
|
||||||
.hb-bg-xi-3 { bottom: 18%; left: 8%; font-size: 48px; transform: rotate(8deg); animation-delay: 2s; }
|
.hb-bg-xi-3 { bottom: 18%; left: 8%; font-size: 48px; transform: rotate(8deg); animation-delay: 2s; }
|
||||||
.hb-bg-xi-4 { bottom: 10%; right: 10%; font-size: 64px; transform: rotate(-8deg); animation-delay: 0.6s; color: rgba(196, 30, 58, 0.18); }
|
.hb-bg-xi-4 { bottom: 10%; right: 10%; font-size: 64px; transform: rotate(-8deg); color: rgba(196, 30, 58, 0.18); animation-delay: 0.6s; }
|
||||||
.hb-bg-xi-5 { top: 42%; left: 2%; font-size: 36px; opacity: 0.8; animation-delay: 3s; }
|
.hb-bg-xi-5 { top: 42%; left: 2%; font-size: 36px; opacity: 0.8; animation-delay: 3s; }
|
||||||
.hb-bg-xi-6 { top: 48%; right: 3%; font-size: 40px; opacity: 0.75; animation-delay: 2.4s; }
|
.hb-bg-xi-6 { top: 48%; right: 3%; font-size: 40px; opacity: 0.75; animation-delay: 2.4s; }
|
||||||
|
.hb-bg-knot { position: absolute; color: rgba(212, 175, 55, 0.28); width: 72px; height: 72px; animation: hb-float 10s ease-in-out infinite; }
|
||||||
.hb-bg-knot {
|
.hb-bg-knot-1 { top: 8%; left: 14%; width: 64px; height: 64px; }
|
||||||
position: absolute;
|
|
||||||
color: rgba(212, 175, 55, 0.28);
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
animation: hb-float 10s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.hb-bg-knot-1 { top: 8%; left: 14%; width: 64px; height: 64px; animation-delay: 0.4s; }
|
|
||||||
.hb-bg-knot-2 { top: 22%; right: 12%; width: 56px; height: 56px; color: rgba(196, 30, 58, 0.28); animation-delay: 1.8s; }
|
.hb-bg-knot-2 { top: 22%; right: 12%; width: 56px; height: 56px; color: rgba(196, 30, 58, 0.28); animation-delay: 1.8s; }
|
||||||
.hb-bg-knot-3 { bottom: 22%; left: 10%; width: 60px; height: 60px; animation-delay: 2.6s; }
|
.hb-bg-knot-3 { bottom: 22%; left: 10%; width: 60px; height: 60px; animation-delay: 2.6s; }
|
||||||
.hb-bg-knot-4 { bottom: 12%; right: 14%; width: 70px; height: 70px; color: rgba(240, 215, 140, 0.22); animation-delay: 1s; }
|
.hb-bg-knot-4 { bottom: 12%; right: 14%; width: 70px; height: 70px; color: rgba(240, 215, 140, 0.22); animation-delay: 1s; }
|
||||||
|
.hb-bg-lantern { position: absolute; color: rgba(240, 215, 140, 0.35); width: 36px; height: 50px; animation: hb-swing 5.5s ease-in-out infinite; }
|
||||||
.hb-bg-lantern {
|
|
||||||
position: absolute;
|
|
||||||
color: rgba(240, 215, 140, 0.35);
|
|
||||||
width: 36px;
|
|
||||||
height: 50px;
|
|
||||||
animation: hb-swing 5.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.hb-bg-lantern-1 { top: 4%; left: 22%; }
|
.hb-bg-lantern-1 { top: 4%; left: 22%; }
|
||||||
.hb-bg-lantern-2 { top: 5%; right: 20%; animation-delay: 1.4s; }
|
.hb-bg-lantern-2 { top: 5%; right: 20%; animation-delay: 1.4s; }
|
||||||
|
|
||||||
.hb-bg-spark {
|
.hb-bg-spark {
|
||||||
position: absolute;
|
position: absolute; width: 6px; height: 6px; border-radius: 50%;
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle, var(--hb-gold-soft), transparent 70%);
|
background: radial-gradient(circle, var(--hb-gold-soft), transparent 70%);
|
||||||
box-shadow: 0 0 10px rgba(240, 215, 140, 0.55);
|
box-shadow: 0 0 10px rgba(240, 215, 140, 0.55); animation: hb-twinkle 2.8s ease-in-out infinite;
|
||||||
animation: hb-twinkle 2.8s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
.hb-bg-spark-1 { top: 18%; left: 30%; animation-delay: 0s; }
|
.hb-bg-spark-1 { top: 18%; left: 30%; }
|
||||||
.hb-bg-spark-2 { top: 28%; right: 28%; animation-delay: 0.7s; }
|
.hb-bg-spark-2 { top: 28%; right: 28%; animation-delay: 0.7s; }
|
||||||
.hb-bg-spark-3 { bottom: 30%; left: 26%; animation-delay: 1.3s; }
|
.hb-bg-spark-3 { bottom: 30%; left: 26%; animation-delay: 1.3s; }
|
||||||
.hb-bg-spark-4 { bottom: 24%; right: 24%; animation-delay: 2s; }
|
.hb-bg-spark-4 { bottom: 24%; right: 24%; animation-delay: 2s; }
|
||||||
|
|
||||||
.hb-loading {
|
.hb-loading {
|
||||||
position: fixed;
|
position: fixed; inset: 0; z-index: 40; display: flex; align-items: center; justify-content: center;
|
||||||
inset: 0;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--hb-red-deep);
|
background: var(--hb-red-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hb-seal {
|
.hb-seal {
|
||||||
width: 88px;
|
width: 88px; height: 88px; border-radius: 50%; border: 3px solid var(--hb-gold); color: var(--hb-gold);
|
||||||
height: 88px;
|
display: flex; align-items: center; justify-content: center; font-size: 42px; font-weight: 700;
|
||||||
border-radius: 50%;
|
animation: hb-seal-pop 1s ease both; box-shadow: 0 0 0 6px rgba(212, 175, 55, 0.15);
|
||||||
border: 3px solid var(--hb-gold);
|
|
||||||
color: var(--hb-gold);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 42px;
|
|
||||||
font-weight: 700;
|
|
||||||
animation: hb-seal-pop 1s ease both;
|
|
||||||
box-shadow: 0 0 0 6px rgba(212, 175, 55, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hb-shimmer {
|
.hb-shimmer {
|
||||||
font-size: 28px;
|
font-size: 28px; letter-spacing: 0.35em;
|
||||||
letter-spacing: 0.35em;
|
background: linear-gradient(90deg, #8a6a1a, var(--hb-gold-soft), #8a6a1a); background-size: 200% 100%;
|
||||||
background: linear-gradient(90deg, #8a6a1a, var(--hb-gold-soft), #8a6a1a);
|
-webkit-background-clip: text; background-clip: text; color: transparent; animation: hb-shimmer 1.2s linear infinite;
|
||||||
background-size: 200% 100%;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
animation: hb-shimmer 1.2s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hb-fade-dot {
|
.hb-fade-dot {
|
||||||
width: 12px;
|
width: 12px; height: 12px; border-radius: 50%; background: var(--hb-gold);
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--hb-gold);
|
|
||||||
animation: hb-fade-pulse 0.9s ease-in-out infinite;
|
animation: hb-fade-pulse 0.9s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
.hb-enter { position: fixed; inset: 0; z-index: 30; pointer-events: none; }
|
||||||
.hb-enter {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 30;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-enter--curtain .hb-enter-panel {
|
.hb-enter--curtain .hb-enter-panel {
|
||||||
position: absolute;
|
position: absolute; top: 0; bottom: 0; width: 50%;
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 50%;
|
|
||||||
background: linear-gradient(180deg, #8b0000, #5c0000);
|
background: linear-gradient(180deg, #8b0000, #5c0000);
|
||||||
transition: transform 0.95s cubic-bezier(0.65, 0, 0.35, 1);
|
transition: transform 0.95s cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
}
|
}
|
||||||
@@ -454,409 +232,33 @@ onUnmounted(() => {
|
|||||||
.hb-enter--curtain .hb-enter-right { right: 0; border-left: 1px solid rgba(212, 175, 55, 0.35); }
|
.hb-enter--curtain .hb-enter-right { right: 0; border-left: 1px solid rgba(212, 175, 55, 0.35); }
|
||||||
.hb-enter--curtain.is-done .hb-enter-left { transform: translateX(-105%); }
|
.hb-enter--curtain.is-done .hb-enter-left { transform: translateX(-105%); }
|
||||||
.hb-enter--curtain.is-done .hb-enter-right { transform: translateX(105%); }
|
.hb-enter--curtain.is-done .hb-enter-right { transform: translateX(105%); }
|
||||||
|
|
||||||
.hb-enter--fadeUp .hb-enter-veil,
|
.hb-enter--fadeUp .hb-enter-veil,
|
||||||
.hb-enter--scaleIn .hb-enter-veil {
|
.hb-enter--scaleIn .hb-enter-veil {
|
||||||
position: absolute;
|
position: absolute; inset: 0; background: var(--hb-red-deep);
|
||||||
inset: 0;
|
|
||||||
background: var(--hb-red-deep);
|
|
||||||
transition: opacity 0.7s ease, transform 0.8s ease;
|
transition: opacity 0.7s ease, transform 0.8s ease;
|
||||||
}
|
}
|
||||||
.hb-enter--fadeUp.is-done .hb-enter-veil {
|
.hb-enter--fadeUp.is-done .hb-enter-veil { opacity: 0; transform: translateY(-24px); }
|
||||||
opacity: 0;
|
.hb-enter--scaleIn.is-done .hb-enter-veil { opacity: 0; transform: scale(1.08); }
|
||||||
transform: translateY(-24px);
|
|
||||||
}
|
|
||||||
.hb-enter--scaleIn.is-done .hb-enter-veil {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-stage {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: min(420px, 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(18px) scale(0.98);
|
|
||||||
transition: opacity 0.6s ease, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
.hb-stage.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-frame {
|
|
||||||
position: relative;
|
|
||||||
border: 1.5px solid var(--hb-gold);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
background:
|
|
||||||
linear-gradient(135deg, rgba(212, 175, 55, 0.18), transparent 40%),
|
|
||||||
linear-gradient(180deg, rgba(155, 0, 0, 0.4), rgba(90, 0, 0, 0.25));
|
|
||||||
box-shadow:
|
|
||||||
0 12px 40px rgba(0, 0, 0, 0.35),
|
|
||||||
inset 0 0 0 1px rgba(240, 215, 140, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-frame-ornament {
|
|
||||||
position: absolute;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
color: var(--hb-gold-soft);
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.hb-frame-ornament svg { width: 100%; height: 100%; display: block; }
|
|
||||||
.hb-frame-ornament-tl { top: 2px; left: 2px; }
|
|
||||||
.hb-frame-ornament-tr { top: 2px; right: 2px; transform: scaleX(-1); }
|
|
||||||
.hb-frame-ornament-bl { bottom: 2px; left: 2px; transform: scaleY(-1); }
|
|
||||||
.hb-frame-ornament-br { bottom: 2px; right: 2px; transform: scale(-1); }
|
|
||||||
|
|
||||||
.hb-inner {
|
|
||||||
position: relative;
|
|
||||||
border: 1.5px solid rgba(212, 175, 55, 0.85);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 14px 14px 20px;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(130, 0, 0, 0.58), rgba(110, 0, 0, 0.38)),
|
|
||||||
radial-gradient(circle at 90% 8%, rgba(255, 255, 255, 0.08), transparent 28%),
|
|
||||||
radial-gradient(circle at 10% 92%, rgba(196, 30, 58, 0.2), transparent 30%);
|
|
||||||
min-height: min(780px, calc(100dvh - 40px));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-inner::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 5px;
|
|
||||||
border: 1px solid rgba(240, 215, 140, 0.35);
|
|
||||||
border-radius: 4px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
clip-path: polygon(
|
|
||||||
0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px,
|
|
||||||
100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-inner::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
border: 1px dashed rgba(212, 175, 55, 0.18);
|
|
||||||
border-radius: 2px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 纸面装饰 —— */
|
|
||||||
.hb-paper-decor {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-paper-watermark {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 52%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: min(220px, 58vw);
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(196, 30, 58, 0.1);
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-shadow: 0 0 1px rgba(212, 175, 55, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-paper-knot {
|
|
||||||
position: absolute;
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
color: rgba(212, 175, 55, 0.45);
|
|
||||||
}
|
|
||||||
.hb-paper-knot-tl { top: 10px; left: 10px; }
|
|
||||||
.hb-paper-knot-tr { top: 10px; right: 10px; transform: scaleX(-1); color: rgba(240, 215, 140, 0.4); }
|
|
||||||
.hb-paper-knot-bl { bottom: 12px; left: 10px; transform: scaleY(-1); color: rgba(240, 215, 140, 0.38); }
|
|
||||||
.hb-paper-knot-br { bottom: 12px; right: 10px; transform: scale(-1); }
|
|
||||||
|
|
||||||
.hb-paper-cloud {
|
|
||||||
position: absolute;
|
|
||||||
width: 96px;
|
|
||||||
height: 42px;
|
|
||||||
color: rgba(212, 175, 55, 0.32);
|
|
||||||
}
|
|
||||||
.hb-paper-cloud-t { top: 8px; left: 50%; transform: translateX(-50%); }
|
|
||||||
.hb-paper-cloud-b { bottom: 8px; left: 50%; transform: translateX(-50%) scaleY(-1); color: rgba(196, 30, 58, 0.28); }
|
|
||||||
|
|
||||||
.hb-paper-xi {
|
|
||||||
position: absolute;
|
|
||||||
top: 46%;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(196, 30, 58, 0.28);
|
|
||||||
text-shadow: 0 0 8px rgba(212, 175, 55, 0.15);
|
|
||||||
}
|
|
||||||
.hb-paper-xi-l { left: 8px; transform: rotate(-18deg); }
|
|
||||||
.hb-paper-xi-r { right: 8px; transform: rotate(18deg); }
|
|
||||||
|
|
||||||
.hb-paper-dot {
|
|
||||||
position: absolute;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(212, 175, 55, 0.45);
|
|
||||||
box-shadow: 0 0 0 3px rgba(196, 30, 58, 0.12);
|
|
||||||
}
|
|
||||||
.hb-paper-dot-1 { top: 22%; left: 14px; }
|
|
||||||
.hb-paper-dot-2 { top: 22%; right: 14px; }
|
|
||||||
.hb-paper-dot-3 { bottom: 26%; left: 16px; }
|
|
||||||
.hb-paper-dot-4 { bottom: 26%; right: 16px; }
|
|
||||||
|
|
||||||
.hb-paper-side {
|
|
||||||
position: absolute;
|
|
||||||
top: 28%;
|
|
||||||
bottom: 28%;
|
|
||||||
width: 10px;
|
|
||||||
background:
|
|
||||||
repeating-linear-gradient(
|
|
||||||
180deg,
|
|
||||||
transparent 0 10px,
|
|
||||||
rgba(212, 175, 55, 0.22) 10px 11px,
|
|
||||||
transparent 11px 20px
|
|
||||||
);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.hb-paper-side-l {
|
|
||||||
left: 4px;
|
|
||||||
border-left: 1px solid rgba(212, 175, 55, 0.25);
|
|
||||||
}
|
|
||||||
.hb-paper-side-r {
|
|
||||||
right: 4px;
|
|
||||||
border-right: 1px solid rgba(212, 175, 55, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-hero-wrap,
|
|
||||||
.hb-names,
|
|
||||||
.hb-title-wrap,
|
|
||||||
.hb-body,
|
|
||||||
.hb-footer,
|
|
||||||
.hb-cta {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-hero-wrap {
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(12px);
|
|
||||||
transition: opacity 0.55s ease, transform 0.55s ease;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
.hb-hero-wrap.is-show {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-hero {
|
|
||||||
position: relative;
|
|
||||||
border: 1.5px solid var(--hb-gold);
|
|
||||||
padding: 3px;
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
aspect-ratio: 16 / 11;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-hero-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-hero-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgba(240, 215, 140, 0.55);
|
|
||||||
letter-spacing: 0.3em;
|
|
||||||
font-size: 13px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-xi {
|
|
||||||
position: absolute;
|
|
||||||
top: 10%;
|
|
||||||
font-size: 28px;
|
|
||||||
color: #c41e3a;
|
|
||||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
|
|
||||||
font-weight: 700;
|
|
||||||
opacity: 0.92;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.hb-xi-l { left: 8%; }
|
|
||||||
.hb-xi-r { right: 8%; }
|
|
||||||
|
|
||||||
.hb-names {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 28px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
.hb-names.is-show { opacity: 1; }
|
|
||||||
|
|
||||||
.hb-name {
|
|
||||||
color: #f7efe0 !important;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-xi-circle {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--hb-gold);
|
|
||||||
color: var(--hb-gold);
|
|
||||||
font-size: 11px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: rgba(155, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-title-wrap {
|
|
||||||
margin: 4px 0 12px;
|
|
||||||
min-height: 48px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.35s ease;
|
|
||||||
}
|
|
||||||
.hb-title-wrap.is-show { opacity: 1; }
|
|
||||||
|
|
||||||
.hb-title {
|
|
||||||
color: var(--hb-gold-soft) !important;
|
|
||||||
font-size: clamp(30px, 9vw, 40px);
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
justify-content: center;
|
|
||||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-body {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 9px;
|
|
||||||
padding: 4px 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-line {
|
|
||||||
color: var(--hb-gold-soft) !important;
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
line-height: 1.55;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-cursor {
|
|
||||||
color: var(--hb-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-footer {
|
|
||||||
margin-top: 8px;
|
|
||||||
min-height: 24px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
.hb-footer.is-show { opacity: 1; }
|
|
||||||
|
|
||||||
.hb-footer-text {
|
|
||||||
color: #f7efe0 !important;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hb-cta {
|
|
||||||
margin-top: 16px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
animation: hb-cta-in 0.5s ease forwards;
|
|
||||||
}
|
|
||||||
.hb-cta.is-show { opacity: 1; }
|
|
||||||
|
|
||||||
.hb-cta-btn {
|
|
||||||
appearance: none;
|
|
||||||
border: 1px solid var(--hb-gold);
|
|
||||||
background: linear-gradient(180deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.08));
|
|
||||||
color: var(--hb-gold-soft);
|
|
||||||
padding: 8px 22px;
|
|
||||||
border-radius: 999px;
|
|
||||||
letter-spacing: 0.28em;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.hb-cta-btn:active { transform: scale(0.98); }
|
|
||||||
|
|
||||||
.hb-disabled {
|
.hb-disabled {
|
||||||
position: fixed;
|
position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
|
||||||
inset: 0;
|
align-items: center; justify-content: center; gap: 16px;
|
||||||
z-index: 50;
|
background: rgba(80, 0, 0, 0.92); color: var(--hb-cream);
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
.hb-cta-btn {
|
||||||
align-items: center;
|
appearance: none; border: 1px solid var(--hb-gold);
|
||||||
justify-content: center;
|
background: linear-gradient(180deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.08));
|
||||||
gap: 16px;
|
color: var(--hb-gold-soft); padding: 8px 22px; border-radius: 999px;
|
||||||
background: rgba(80, 0, 0, 0.92);
|
letter-spacing: 0.28em; font-size: 13px; cursor: pointer;
|
||||||
color: var(--hb-cream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes hb-seal-pop {
|
@keyframes hb-seal-pop {
|
||||||
0% { opacity: 0; transform: scale(0.4) rotate(-12deg); }
|
0% { opacity: 0; transform: scale(0.4) rotate(-12deg); }
|
||||||
60% { opacity: 1; transform: scale(1.08) rotate(2deg); }
|
60% { opacity: 1; transform: scale(1.08) rotate(2deg); }
|
||||||
100% { opacity: 1; transform: scale(1) rotate(0); }
|
100% { opacity: 1; transform: scale(1) rotate(0); }
|
||||||
}
|
}
|
||||||
@keyframes hb-shimmer {
|
@keyframes hb-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
||||||
0% { background-position: 100% 0; }
|
@keyframes hb-fade-pulse { 0%, 100% { opacity: 0.25; transform: scale(0.85); } 50% { opacity: 1; transform: scale(1.1); } }
|
||||||
100% { background-position: -100% 0; }
|
@keyframes hb-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||||
}
|
@keyframes hb-pulse-soft { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
||||||
@keyframes hb-fade-pulse {
|
@keyframes hb-swing { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
|
||||||
0%, 100% { opacity: 0.25; transform: scale(0.85); }
|
@keyframes hb-twinkle { 0%, 100% { opacity: 0.25; transform: scale(0.7); } 50% { opacity: 1; transform: scale(1.25); } }
|
||||||
50% { opacity: 1; transform: scale(1.1); }
|
|
||||||
}
|
|
||||||
@keyframes hb-cta-in {
|
|
||||||
to { opacity: 1; transform: none; }
|
|
||||||
}
|
|
||||||
@keyframes hb-float {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-8px); }
|
|
||||||
}
|
|
||||||
@keyframes hb-pulse-soft {
|
|
||||||
0%, 100% { opacity: 0.55; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
}
|
|
||||||
@keyframes hb-swing {
|
|
||||||
0%, 100% { transform: rotate(-4deg); }
|
|
||||||
50% { transform: rotate(4deg); }
|
|
||||||
}
|
|
||||||
@keyframes hb-twinkle {
|
|
||||||
0%, 100% { opacity: 0.25; transform: scale(0.7); }
|
|
||||||
50% { opacity: 1; transform: scale(1.25); }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
342
vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue
Normal file
342
vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hb-stage" :class="{ 'is-visible': ready }">
|
||||||
|
<div class="hb-frame">
|
||||||
|
<div class="hb-frame-ornament hb-frame-ornament-tl" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="hb-frame-ornament hb-frame-ornament-tr" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="hb-frame-ornament hb-frame-ornament-bl" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="hb-frame-ornament hb-frame-ornament-br" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 48 48"><use href="#hb-icon-corner" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="hb-inner">
|
||||||
|
<div class="hb-paper-decor" aria-hidden="true">
|
||||||
|
<span class="hb-paper-watermark">囍</span>
|
||||||
|
<svg class="hb-paper-knot hb-paper-knot-tl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
||||||
|
<svg class="hb-paper-knot hb-paper-knot-tr" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
||||||
|
<svg class="hb-paper-knot hb-paper-knot-bl" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
||||||
|
<svg class="hb-paper-knot hb-paper-knot-br" viewBox="0 0 64 64"><use href="#hb-icon-knot" /></svg>
|
||||||
|
<svg class="hb-paper-cloud hb-paper-cloud-t" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
|
||||||
|
<svg class="hb-paper-cloud hb-paper-cloud-b" viewBox="0 0 80 36"><use href="#hb-icon-cloud" /></svg>
|
||||||
|
<span class="hb-paper-xi hb-paper-xi-l">囍</span>
|
||||||
|
<span class="hb-paper-xi hb-paper-xi-r">囍</span>
|
||||||
|
<span class="hb-paper-dot hb-paper-dot-1"></span>
|
||||||
|
<span class="hb-paper-dot hb-paper-dot-2"></span>
|
||||||
|
<span class="hb-paper-dot hb-paper-dot-3"></span>
|
||||||
|
<span class="hb-paper-dot hb-paper-dot-4"></span>
|
||||||
|
<div class="hb-paper-side hb-paper-side-l"></div>
|
||||||
|
<div class="hb-paper-side hb-paper-side-r"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hb-hero-wrap" :class="{ 'is-show': stage >= 1 }">
|
||||||
|
<div class="hb-hero">
|
||||||
|
<img v-if="cfg.heroImg" :src="cfg.heroImg" alt="" class="hb-hero-img" />
|
||||||
|
<div v-else class="hb-hero-placeholder">婚礼照片</div>
|
||||||
|
<span class="hb-xi hb-xi-l">囍</span>
|
||||||
|
<span class="hb-xi hb-xi-r">囍</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hb-names" :class="{ 'is-show': stage >= 2 }">
|
||||||
|
<BlurText
|
||||||
|
v-if="stage >= 2"
|
||||||
|
:text="nameLeft"
|
||||||
|
:active="stage >= 2"
|
||||||
|
animate-by="letters"
|
||||||
|
:delay="40"
|
||||||
|
class-name="hb-name"
|
||||||
|
@animation-complete="namesRightReady = true"
|
||||||
|
/>
|
||||||
|
<span class="hb-xi-circle">囍</span>
|
||||||
|
<BlurText
|
||||||
|
v-if="namesRightReady"
|
||||||
|
:text="nameRight"
|
||||||
|
:active="namesRightReady"
|
||||||
|
animate-by="letters"
|
||||||
|
:delay="40"
|
||||||
|
class-name="hb-name"
|
||||||
|
@animation-complete="bumpStage(3)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hb-title-wrap" :class="{ 'is-show': stage >= 3 }">
|
||||||
|
<BlurText
|
||||||
|
v-if="stage >= 3"
|
||||||
|
:text="cfg.title"
|
||||||
|
:active="stage >= 3"
|
||||||
|
animate-by="letters"
|
||||||
|
direction="bottom"
|
||||||
|
:delay="70"
|
||||||
|
:step-duration="0.4"
|
||||||
|
class-name="hb-title calligraphy-strong"
|
||||||
|
@animation-complete="bumpStage(4)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hb-body">
|
||||||
|
<template v-for="(line, i) in cfg.lines" :key="`line-${i}-${line}`">
|
||||||
|
<TextType
|
||||||
|
v-if="lineStage > i && i % 2 === 0"
|
||||||
|
:text="line"
|
||||||
|
:active="lineStage > i"
|
||||||
|
:loop="false"
|
||||||
|
:show-cursor="lineStage === i + 1"
|
||||||
|
cursor-character="|"
|
||||||
|
cursor-class-name="hb-cursor"
|
||||||
|
:typing-speed="48"
|
||||||
|
:initial-delay="80"
|
||||||
|
class-name="hb-line"
|
||||||
|
@animation-complete="onLineDone(i)"
|
||||||
|
/>
|
||||||
|
<BlurText
|
||||||
|
v-else-if="lineStage > i"
|
||||||
|
:text="line"
|
||||||
|
:active="lineStage > i"
|
||||||
|
animate-by="words"
|
||||||
|
:delay="90"
|
||||||
|
class-name="hb-line"
|
||||||
|
@animation-complete="onLineDone(i)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hb-footer" :class="{ 'is-show': stage >= 5 }">
|
||||||
|
<BlurText
|
||||||
|
v-if="stage >= 5"
|
||||||
|
:text="cfg.footer"
|
||||||
|
:active="stage >= 5"
|
||||||
|
animate-by="letters"
|
||||||
|
:delay="35"
|
||||||
|
class-name="hb-footer-text"
|
||||||
|
@animation-complete="bumpStage(6)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cfg.showInviteButton && stage >= 6" class="hb-cta is-show">
|
||||||
|
<button type="button" class="hb-cta-btn" @click="$emit('invite')">
|
||||||
|
{{ cfg.inviteButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import BlurText from '@/components/vue-bits/BlurText.vue'
|
||||||
|
import TextType from '@/components/vue-bits/TextType.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
cfg: { type: Object, required: true },
|
||||||
|
ready: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
defineEmits(['invite'])
|
||||||
|
|
||||||
|
const stage = ref(0)
|
||||||
|
const lineStage = ref(0)
|
||||||
|
const namesRightReady = ref(false)
|
||||||
|
let timers = []
|
||||||
|
|
||||||
|
const nameLeft = computed(() => `${props.cfg.sonLabel || ''}${props.cfg.sonName || ''}`)
|
||||||
|
const nameRight = computed(() => `${props.cfg.daughterInLawLabel || ''}${props.cfg.daughterInLawName || ''}`)
|
||||||
|
|
||||||
|
function later(fn, ms) {
|
||||||
|
const id = setTimeout(fn, ms)
|
||||||
|
timers.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpStage(n) {
|
||||||
|
if (stage.value < n) stage.value = n
|
||||||
|
if (n === 4) {
|
||||||
|
if (!props.cfg.lines?.length) {
|
||||||
|
later(() => bumpStage(5), 200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lineStage.value = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLineDone(i) {
|
||||||
|
if (i + 1 >= (props.cfg.lines?.length || 0)) {
|
||||||
|
bumpStage(5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lineStage.value === i + 1) lineStage.value = i + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.ready,
|
||||||
|
(v) => {
|
||||||
|
if (!v) return
|
||||||
|
later(() => { stage.value = 1 }, 200)
|
||||||
|
later(() => { stage.value = 2 }, 700)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hb-stage {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(420px, 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(18px) scale(0.98);
|
||||||
|
transition: opacity 0.6s ease, transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
.hb-stage.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
.hb-frame {
|
||||||
|
position: relative;
|
||||||
|
border: 1.5px solid var(--hb-gold);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(212, 175, 55, 0.18), transparent 40%),
|
||||||
|
linear-gradient(180deg, rgba(155, 0, 0, 0.4), rgba(90, 0, 0, 0.25));
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(240, 215, 140, 0.2);
|
||||||
|
}
|
||||||
|
.hb-frame-ornament {
|
||||||
|
position: absolute;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
color: var(--hb-gold-soft);
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.hb-frame-ornament svg { width: 100%; height: 100%; display: block; }
|
||||||
|
.hb-frame-ornament-tl { top: 2px; left: 2px; }
|
||||||
|
.hb-frame-ornament-tr { top: 2px; right: 2px; transform: scaleX(-1); }
|
||||||
|
.hb-frame-ornament-bl { bottom: 2px; left: 2px; transform: scaleY(-1); }
|
||||||
|
.hb-frame-ornament-br { bottom: 2px; right: 2px; transform: scale(-1); }
|
||||||
|
.hb-inner {
|
||||||
|
position: relative;
|
||||||
|
border: 1.5px solid rgba(212, 175, 55, 0.85);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 14px 20px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(130, 0, 0, 0.58), rgba(110, 0, 0, 0.38)),
|
||||||
|
radial-gradient(circle at 90% 8%, rgba(255, 255, 255, 0.08), transparent 28%),
|
||||||
|
radial-gradient(circle at 10% 92%, rgba(196, 30, 58, 0.2), transparent 30%);
|
||||||
|
min-height: min(780px, calc(100dvh - 40px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hb-inner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 5px;
|
||||||
|
border: 1px solid rgba(240, 215, 140, 0.35);
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
clip-path: polygon(0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px));
|
||||||
|
}
|
||||||
|
.hb-inner::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
border: 1px dashed rgba(212, 175, 55, 0.18);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.hb-paper-decor { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
|
||||||
|
.hb-paper-watermark {
|
||||||
|
position: absolute; left: 50%; top: 52%; transform: translate(-50%, -50%);
|
||||||
|
font-size: min(220px, 58vw); font-weight: 700; color: rgba(196, 30, 58, 0.1); line-height: 1;
|
||||||
|
}
|
||||||
|
.hb-paper-knot { position: absolute; width: 42px; height: 42px; color: rgba(212, 175, 55, 0.45); }
|
||||||
|
.hb-paper-knot-tl { top: 10px; left: 10px; }
|
||||||
|
.hb-paper-knot-tr { top: 10px; right: 10px; transform: scaleX(-1); }
|
||||||
|
.hb-paper-knot-bl { bottom: 12px; left: 10px; transform: scaleY(-1); }
|
||||||
|
.hb-paper-knot-br { bottom: 12px; right: 10px; transform: scale(-1); }
|
||||||
|
.hb-paper-cloud { position: absolute; width: 96px; height: 42px; color: rgba(212, 175, 55, 0.32); }
|
||||||
|
.hb-paper-cloud-t { top: 8px; left: 50%; transform: translateX(-50%); }
|
||||||
|
.hb-paper-cloud-b { bottom: 8px; left: 50%; transform: translateX(-50%) scaleY(-1); color: rgba(196, 30, 58, 0.28); }
|
||||||
|
.hb-paper-xi { position: absolute; top: 46%; font-size: 22px; font-weight: 700; color: rgba(196, 30, 58, 0.28); }
|
||||||
|
.hb-paper-xi-l { left: 8px; transform: rotate(-18deg); }
|
||||||
|
.hb-paper-xi-r { right: 8px; transform: rotate(18deg); }
|
||||||
|
.hb-paper-dot {
|
||||||
|
position: absolute; width: 5px; height: 5px; border-radius: 50%;
|
||||||
|
background: rgba(212, 175, 55, 0.45); box-shadow: 0 0 0 3px rgba(196, 30, 58, 0.12);
|
||||||
|
}
|
||||||
|
.hb-paper-dot-1 { top: 22%; left: 14px; }
|
||||||
|
.hb-paper-dot-2 { top: 22%; right: 14px; }
|
||||||
|
.hb-paper-dot-3 { bottom: 26%; left: 16px; }
|
||||||
|
.hb-paper-dot-4 { bottom: 26%; right: 16px; }
|
||||||
|
.hb-paper-side {
|
||||||
|
position: absolute; top: 28%; bottom: 28%; width: 10px;
|
||||||
|
background: repeating-linear-gradient(180deg, transparent 0 10px, rgba(212, 175, 55, 0.22) 10px 11px, transparent 11px 20px);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.hb-paper-side-l { left: 4px; border-left: 1px solid rgba(212, 175, 55, 0.25); }
|
||||||
|
.hb-paper-side-r { right: 4px; border-right: 1px solid rgba(212, 175, 55, 0.25); }
|
||||||
|
.hb-hero-wrap, .hb-names, .hb-title-wrap, .hb-body, .hb-footer, .hb-cta { position: relative; z-index: 2; }
|
||||||
|
.hb-hero-wrap {
|
||||||
|
width: 100%; opacity: 0; transform: translateY(12px);
|
||||||
|
transition: opacity 0.55s ease, transform 0.55s ease; margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.hb-hero-wrap.is-show { opacity: 1; transform: none; }
|
||||||
|
.hb-hero {
|
||||||
|
position: relative; border: 1.5px solid var(--hb-gold); padding: 3px;
|
||||||
|
background: rgba(0, 0, 0, 0.15); aspect-ratio: 16 / 11; overflow: hidden;
|
||||||
|
}
|
||||||
|
.hb-hero-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
|
.hb-hero-placeholder {
|
||||||
|
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: rgba(240, 215, 140, 0.55); letter-spacing: 0.3em; font-size: 13px; background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.hb-xi {
|
||||||
|
position: absolute; top: 10%; font-size: 28px; color: #c41e3a; font-weight: 700;
|
||||||
|
opacity: 0.92; pointer-events: none;
|
||||||
|
}
|
||||||
|
.hb-xi-l { left: 8%; }
|
||||||
|
.hb-xi-r { right: 8%; }
|
||||||
|
.hb-names {
|
||||||
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
|
min-height: 28px; margin-bottom: 8px; opacity: 0; transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
.hb-names.is-show { opacity: 1; }
|
||||||
|
.hb-name { color: #f7efe0 !important; font-size: 12px; letter-spacing: 0.12em; justify-content: center; }
|
||||||
|
.hb-xi-circle {
|
||||||
|
width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--hb-gold);
|
||||||
|
color: var(--hb-gold); font-size: 11px; display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0; background: rgba(155, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.hb-title-wrap { margin: 4px 0 12px; min-height: 48px; opacity: 0; transition: opacity 0.35s ease; }
|
||||||
|
.hb-title-wrap.is-show { opacity: 1; }
|
||||||
|
.hb-title {
|
||||||
|
color: var(--hb-gold-soft) !important; font-size: clamp(30px, 9vw, 40px);
|
||||||
|
letter-spacing: 0.18em; justify-content: center; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.hb-body {
|
||||||
|
width: 100%; flex: 1; display: flex; flex-direction: column; align-items: center; gap: 9px; padding: 4px 4px 10px;
|
||||||
|
}
|
||||||
|
.hb-line {
|
||||||
|
color: var(--hb-gold-soft) !important; font-size: 13px; letter-spacing: 0.14em;
|
||||||
|
line-height: 1.55; text-align: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.hb-cursor { color: var(--hb-gold); }
|
||||||
|
.hb-footer { margin-top: 8px; min-height: 24px; opacity: 0; transition: opacity 0.4s ease; }
|
||||||
|
.hb-footer.is-show { opacity: 1; }
|
||||||
|
.hb-footer-text { color: #f7efe0 !important; font-size: 12px; letter-spacing: 0.2em; justify-content: center; }
|
||||||
|
.hb-cta { margin-top: 16px; opacity: 0; transform: translateY(8px); animation: hb-cta-in 0.5s ease forwards; }
|
||||||
|
.hb-cta-btn {
|
||||||
|
appearance: none; border: 1px solid var(--hb-gold);
|
||||||
|
background: linear-gradient(180deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.08));
|
||||||
|
color: var(--hb-gold-soft); padding: 8px 22px; border-radius: 999px;
|
||||||
|
letter-spacing: 0.28em; font-size: 13px; cursor: pointer;
|
||||||
|
}
|
||||||
|
@keyframes hb-cta-in { to { opacity: 1; transform: none; } }
|
||||||
|
</style>
|
||||||
337
vite-tailwindcss/src/views/hb/templates/SplitPoster.vue
Normal file
337
vite-tailwindcss/src/views/hb/templates/SplitPoster.vue
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sp" :class="{ 'is-visible': ready }">
|
||||||
|
<div class="sp-card">
|
||||||
|
<section class="sp-left">
|
||||||
|
<div class="sp-photo" :class="{ 'is-show': stage >= 1 }">
|
||||||
|
<img v-if="cfg.heroImg" :src="cfg.heroImg" alt="" />
|
||||||
|
<div v-else class="sp-photo-ph">婚礼照片</div>
|
||||||
|
<div class="sp-photo-veil"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sp-slogan-block" :class="{ 'is-show': stage >= 2 }">
|
||||||
|
<BlurText
|
||||||
|
v-if="stage >= 2"
|
||||||
|
:text="cfg.verticalSlogan || '吾家有喜'"
|
||||||
|
:active="stage >= 2"
|
||||||
|
animate-by="letters"
|
||||||
|
:delay="90"
|
||||||
|
class-name="sp-slogan calligraphy-strong"
|
||||||
|
@animation-complete="bumpStage(3)"
|
||||||
|
/>
|
||||||
|
<div v-if="stage >= 3" class="sp-subs">
|
||||||
|
<BlurText :text="cfg.subSlogan1 || ''" :active="true" animate-by="letters" :delay="50" class-name="sp-sub" />
|
||||||
|
<BlurText :text="cfg.subSlogan2 || ''" :active="true" animate-by="letters" :delay="50" class-name="sp-sub" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="sp-right">
|
||||||
|
<div class="sp-right-decor" aria-hidden="true">
|
||||||
|
<span class="sp-wm">囍</span>
|
||||||
|
</div>
|
||||||
|
<div class="sp-xi-top" :class="{ 'is-show': stage >= 3 }">囍</div>
|
||||||
|
|
||||||
|
<BlurText
|
||||||
|
v-if="stage >= 3"
|
||||||
|
:text="cfg.greeting || ''"
|
||||||
|
:active="stage >= 3"
|
||||||
|
animate-by="letters"
|
||||||
|
:delay="40"
|
||||||
|
class-name="sp-greeting"
|
||||||
|
@animation-complete="bumpStage(4)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="sp-body">
|
||||||
|
<template v-for="(line, i) in bodyLines" :key="`sp-${i}`">
|
||||||
|
<TextType
|
||||||
|
v-if="lineStage > i && i % 2 === 0"
|
||||||
|
:text="line"
|
||||||
|
:active="lineStage > i"
|
||||||
|
:loop="false"
|
||||||
|
:show-cursor="lineStage === i + 1"
|
||||||
|
cursor-character="|"
|
||||||
|
cursor-class-name="sp-cursor"
|
||||||
|
:typing-speed="42"
|
||||||
|
class-name="sp-line"
|
||||||
|
@animation-complete="onLineDone(i)"
|
||||||
|
/>
|
||||||
|
<BlurText
|
||||||
|
v-else-if="lineStage > i"
|
||||||
|
:text="line"
|
||||||
|
:active="lineStage > i"
|
||||||
|
animate-by="words"
|
||||||
|
:delay="70"
|
||||||
|
class-name="sp-line"
|
||||||
|
@animation-complete="onLineDone(i)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sp-couple" :class="{ 'is-show': stage >= 5 }">
|
||||||
|
<div class="sp-person">
|
||||||
|
<span class="sp-role">新郎</span>
|
||||||
|
<BlurText v-if="stage >= 5" :text="groom" :active="true" animate-by="letters" :delay="40" class-name="sp-name" />
|
||||||
|
</div>
|
||||||
|
<span class="sp-xi-mid">囍</span>
|
||||||
|
<div class="sp-person">
|
||||||
|
<span class="sp-role">新娘</span>
|
||||||
|
<BlurText v-if="stage >= 5" :text="bride" :active="true" animate-by="letters" :delay="40" class-name="sp-name" @animation-complete="bumpStage(6)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sp-meta" :class="{ 'is-show': stage >= 6 }">
|
||||||
|
<div class="sp-meta-title">新婚喜宴时间</div>
|
||||||
|
<div class="sp-meta-val">{{ cfg.dateText }}</div>
|
||||||
|
<div class="sp-meta-sub">{{ cfg.lunarText }}</div>
|
||||||
|
<div class="sp-meta-title mt">地点</div>
|
||||||
|
<div class="sp-meta-val">{{ cfg.venueText }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sp-love" :class="{ 'is-show': stage >= 6 }">
|
||||||
|
<span class="sp-love-text calligraphy">{{ cfg.loveFooter || 'Love you' }}</span>
|
||||||
|
<span class="sp-heart">♥</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cfg.showInviteButton && stage >= 6" class="sp-cta">
|
||||||
|
<button type="button" class="sp-cta-btn" @click="$emit('invite')">
|
||||||
|
{{ cfg.inviteButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import BlurText from '@/components/vue-bits/BlurText.vue'
|
||||||
|
import TextType from '@/components/vue-bits/TextType.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
cfg: { type: Object, required: true },
|
||||||
|
ready: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
defineEmits(['invite'])
|
||||||
|
|
||||||
|
const stage = ref(0)
|
||||||
|
const lineStage = ref(0)
|
||||||
|
let timers = []
|
||||||
|
|
||||||
|
const bodyLines = computed(() => {
|
||||||
|
const lines = Array.isArray(props.cfg.lines) ? props.cfg.lines.filter(Boolean) : []
|
||||||
|
if (lines.length) return lines.slice(0, 4)
|
||||||
|
return [
|
||||||
|
'良辰已定,吉日待访,吾有薄酒',
|
||||||
|
'以谢良友,敬备喜宴,随候尊驾',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const groom = computed(() => props.cfg.groomName || props.cfg.sonName || '')
|
||||||
|
const bride = computed(() => props.cfg.brideName || props.cfg.daughterInLawName || '')
|
||||||
|
|
||||||
|
function later(fn, ms) {
|
||||||
|
timers.push(setTimeout(fn, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpStage(n) {
|
||||||
|
if (stage.value < n) stage.value = n
|
||||||
|
if (n === 4) {
|
||||||
|
if (!bodyLines.value.length) {
|
||||||
|
later(() => bumpStage(5), 200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lineStage.value = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLineDone(i) {
|
||||||
|
if (i + 1 >= bodyLines.value.length) {
|
||||||
|
bumpStage(5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lineStage.value === i + 1) lineStage.value = i + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.ready,
|
||||||
|
(v) => {
|
||||||
|
if (!v) return
|
||||||
|
later(() => { stage.value = 1 }, 180)
|
||||||
|
later(() => { stage.value = 2 }, 550)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sp {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(720px, 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.98);
|
||||||
|
transition: opacity 0.55s ease, transform 0.65s ease;
|
||||||
|
}
|
||||||
|
.sp.is-visible { opacity: 1; transform: none; }
|
||||||
|
|
||||||
|
.sp-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1.5px solid var(--hb-gold);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
min-height: min(720px, calc(100dvh - 32px));
|
||||||
|
background: #8b0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.sp-card { flex-direction: row; min-height: min(560px, calc(100dvh - 40px)); }
|
||||||
|
.sp-left { width: 46%; }
|
||||||
|
.sp-right { width: 54%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-left {
|
||||||
|
position: relative;
|
||||||
|
min-height: 260px;
|
||||||
|
background: #c23a2b;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-photo {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 220px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
.sp-photo.is-show { opacity: 1; }
|
||||||
|
.sp-photo img { width: 100%; height: 100%; object-fit: cover; display: block; position: absolute; inset: 0; }
|
||||||
|
.sp-photo-ph {
|
||||||
|
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: rgba(255, 240, 220, 0.55); letter-spacing: 0.3em;
|
||||||
|
}
|
||||||
|
.sp-photo-veil {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(180deg, transparent 40%, rgba(140, 20, 20, 0.55) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-slogan-block {
|
||||||
|
position: absolute;
|
||||||
|
left: 10%;
|
||||||
|
top: 12%;
|
||||||
|
bottom: 12%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.45s ease;
|
||||||
|
z-index: 2;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
}
|
||||||
|
.sp-slogan-block.is-show { opacity: 1; }
|
||||||
|
|
||||||
|
.sp-slogan {
|
||||||
|
color: #f3e6c8 !important;
|
||||||
|
font-size: clamp(36px, 8vw, 52px);
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.sp-subs { display: flex; flex-direction: column; gap: 18px; margin-top: 8px; }
|
||||||
|
.sp-sub {
|
||||||
|
color: rgba(243, 230, 200, 0.88) !important;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-right {
|
||||||
|
position: relative;
|
||||||
|
padding: 22px 20px 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 80% 10%, rgba(255, 255, 255, 0.06), transparent 30%),
|
||||||
|
linear-gradient(180deg, #9b0000, #7a0000);
|
||||||
|
color: var(--hb-gold-soft);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sp-right-decor { position: absolute; inset: 0; pointer-events: none; }
|
||||||
|
.sp-wm {
|
||||||
|
position: absolute; left: 50%; top: 48%; transform: translate(-50%, -50%);
|
||||||
|
font-size: 180px; font-weight: 700; color: rgba(196, 30, 58, 0.12); line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-xi-top {
|
||||||
|
font-size: 48px; font-weight: 700; color: var(--hb-gold-soft);
|
||||||
|
opacity: 0; transform: scale(0.8); transition: opacity 0.4s ease, transform 0.4s ease;
|
||||||
|
margin-bottom: 8px; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); position: relative; z-index: 1;
|
||||||
|
}
|
||||||
|
.sp-xi-top.is-show { opacity: 1; transform: scale(1); }
|
||||||
|
|
||||||
|
.sp-greeting {
|
||||||
|
color: #f7efe0 !important; font-size: 14px; letter-spacing: 0.2em;
|
||||||
|
margin-bottom: 12px; justify-content: center; position: relative; z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-body {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||||
|
min-height: 72px; margin-bottom: 14px; width: 100%;
|
||||||
|
}
|
||||||
|
.sp-line {
|
||||||
|
color: var(--hb-gold-soft) !important; font-size: 13px; letter-spacing: 0.12em;
|
||||||
|
line-height: 1.55; justify-content: center; text-align: center;
|
||||||
|
}
|
||||||
|
.sp-cursor { color: var(--hb-gold); }
|
||||||
|
|
||||||
|
.sp-couple {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 14px;
|
||||||
|
margin: 6px 0 14px; opacity: 0; transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
.sp-couple.is-show { opacity: 1; }
|
||||||
|
.sp-person { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 72px; }
|
||||||
|
.sp-role { font-size: 11px; color: rgba(247, 239, 224, 0.7); letter-spacing: 0.3em; }
|
||||||
|
.sp-name { color: var(--hb-gold-soft) !important; font-size: 18px; letter-spacing: 0.2em; justify-content: center; }
|
||||||
|
.sp-xi-mid {
|
||||||
|
width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--hb-gold);
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 13px; color: var(--hb-gold); background: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-meta {
|
||||||
|
position: relative; z-index: 1; width: 100%;
|
||||||
|
opacity: 0; transition: opacity 0.45s ease; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.sp-meta.is-show { opacity: 1; }
|
||||||
|
.sp-meta-title {
|
||||||
|
font-size: 12px; letter-spacing: 0.35em; color: rgba(247, 239, 224, 0.75); margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.sp-meta-title.mt { margin-top: 12px; }
|
||||||
|
.sp-meta-val { font-size: 14px; letter-spacing: 0.12em; color: var(--hb-gold-soft); }
|
||||||
|
.sp-meta-sub { font-size: 12px; margin-top: 2px; color: rgba(240, 215, 140, 0.75); letter-spacing: 0.15em; }
|
||||||
|
|
||||||
|
.sp-love {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
margin-top: auto; padding-top: 10px;
|
||||||
|
opacity: 0; transition: opacity 0.4s ease;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.sp-love.is-show { opacity: 1; }
|
||||||
|
.sp-love-text { font-size: 22px; color: #f3e6c8; }
|
||||||
|
.sp-heart { color: #e85d5d; font-size: 14px; }
|
||||||
|
|
||||||
|
.sp-cta { position: relative; z-index: 1; margin-top: 12px; }
|
||||||
|
.sp-cta-btn {
|
||||||
|
appearance: none; border: 1px solid var(--hb-gold);
|
||||||
|
background: linear-gradient(180deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.08));
|
||||||
|
color: var(--hb-gold-soft); padding: 7px 18px; border-radius: 999px;
|
||||||
|
letter-spacing: 0.28em; font-size: 12px; cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user