fix: 升级了一些视觉、各种单图相册效果、日历效果、多种模板任选

This commit is contained in:
2026-08-14 21:02:24 +08:00
parent 5e609d5ee4
commit 1914dad39a
22 changed files with 6929 additions and 330 deletions

View File

@@ -29,9 +29,13 @@ var configSectionKeys = map[string][]string{
"copy": {
"formalText1", "formalText2", "formalPageHeight",
},
"theme": {
"siteTheme", "sectionIndexEnabled", "calendarStyle", "endingFrameStyle",
},
"visual": {
"introExitEffect", "scrollMode", "freeScrollInterval", "pageSwitchDuration", "firstScreenDuration",
"petalEnabled", "bubbleEnabled", "introEnabled", "danmakuEnabled", "danmakuShowTime", "nineGridAnimate",
"giftEffectMode",
},
"music": {
"musicList", "activeMusicUrl", "musicPlayMode", "musicRandomEnabled",

View File

@@ -10,13 +10,18 @@ const props = defineProps({
bubbleEnabled: { type: Boolean, default: true },
/** 预览/抽屉等遮罩时关闭粒子绘制 */
active: { type: Boolean, default: true },
/** 主题 fx 色板注入:空则用默认暖彩 */
petalPalette: { type: Array, default: null },
heartPalette: { type: Array, default: null },
})
const canvasRef = ref(null)
const petals = []
const hearts = []
const petalColors = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
const heartColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
const DEFAULT_PETALS = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6']
const DEFAULT_HEARTS = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
const petalColorsOf = () => (props.petalPalette?.length ? props.petalPalette : DEFAULT_PETALS)
const heartColorsOf = () => (props.heartPalette?.length ? props.heartPalette : DEFAULT_HEARTS)
let ctx = null
let rafId = 0
@@ -39,7 +44,8 @@ function resetPetal(p, initial = false) {
p.rotation = rand(0, Math.PI * 2)
p.rotationSpeed = rand(-0.9, 0.9)
p.opacity = rand(0.28, 0.7)
p.color = petalColors[Math.floor(rand(0, petalColors.length))]
const pc = petalColorsOf()
p.color = pc[Math.floor(rand(0, pc.length))]
}
function resetHeart(h, initial = false) {
@@ -50,7 +56,8 @@ function resetHeart(h, initial = false) {
h.sway = rand(8, 26)
h.phase = rand(0, Math.PI * 2)
h.opacity = rand(0.1, 0.32)
h.color = heartColors[Math.floor(rand(0, heartColors.length))]
const hc = heartColorsOf()
h.color = hc[Math.floor(rand(0, hc.length))]
}
function seedParticles() {

View File

@@ -30,7 +30,7 @@ const GLOW_BLUR = isIOS ? 4 : 10
const GLOW_ALPHA = 0.55
const DPR_CAP = isIOS ? 1.5 : 2
/** 多色爱心:主色 */
/** 多色爱心:主色(可被主题 fx 色板覆盖) */
const HEART_COLORS = [
'#ff2442', '#ff2d55', '#ff4770', '#ff6b8a',
'#f472b6', '#ec4899', '#e879f9', '#c084fc',
@@ -47,6 +47,14 @@ const GLOW_COLORS = [
'#fdba74', '#f5d0b0', '#ffffff',
]
const props = defineProps({
/** 主题色板注入:空则用默认 */
colors: { type: Array, default: null },
glowColors: { type: Array, default: null },
})
const heartPalette = () => (props.colors?.length ? props.colors : HEART_COLORS)
const glowPalette = () => (props.glowColors?.length ? props.glowColors : GLOW_COLORS)
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]
const rand = (min, max) => min + Math.random() * (max - min)
@@ -199,8 +207,8 @@ function createHeart(x, y, now) {
birthTime: now,
maxAge: rand(HEART_MIN_LIFE, HEART_MAX_LIFE),
baseSize: rand(HEART_MIN_SIZE, HEART_MAX_SIZE),
color: pick(HEART_COLORS),
glowColor: pick(GLOW_COLORS),
color: pick(heartPalette()),
glowColor: pick(glowPalette()),
peakScale: rand(0.75, 1.35),
riseSpeed: rand(RISE_SPEED_MIN, RISE_SPEED_MAX),
driftAmp: rand(DRIFT_AMP_MIN, DRIFT_AMP_MAX),

View File

@@ -214,8 +214,8 @@
:class="{ 'g-heart-done': heartDone }"
>
<div
v-for="(slot, imgIdx) in HEART_SLOTS"
:key="imgIdx"
v-for="(slot, imgIdx) in heartSlots"
:key="`${page.heartLayout || 'classic'}-${imgIdx}`"
class="g-heart-slot"
:class="{ 'g-heart-placeholder': imgIdx >= shownCount }"
:style="heartSlotStyle(slot)"
@@ -225,7 +225,7 @@
v-reveal="{ variant: heartReveal(imgIdx), immediate: true }"
class="absolute inset-0 g-heart-cell"
:class="frameClass"
:style="heartCellShape(imgIdx)"
:style="heartCellShape(slot)"
>
<img
:src="displayAt(imgIdx)"
@@ -309,7 +309,7 @@
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { borderClass, borderStyleMeta } from '@/composables/borderStyles'
import { createSequentialMount } from '@/composables/useSequentialMount'
import { HEART_SLOTS, heartSlotStyle, heartFocusCssAt, heartCellShape } from '@/composables/heartCollage'
import { heartSlotsFor, heartSlotStyle, heartFocusCssAt, heartCellShape } from '@/composables/heartCollage'
import SectionHeading from '@/components/themes/SectionHeading.vue'
const NINE_STEP_MS = 300
@@ -735,16 +735,21 @@ function polaroidTransform(i) {
}
/** 爱心:左右对称入场,中轴与心尖用缩放 */
/** 当前爱心拼接方案的槽位page.heartLayout 可选classic 默认) */
const heartSlots = computed(() => heartSlotsFor(props.page))
function heartReveal(i) {
if (i === 5 || i === 12) return 'scale'
if (i === 0 || i === 2 || i === 3 || i === 4 || i === 9) return 'left'
if (i === 1 || i === 6 || i === 7 || i === 8 || i === 11) return 'right'
return 'up'
// 按槽位横向位置决定入场方向:左半从左入、右半从右入、中轴缩放
const slot = heartSlots.value[i]
if (!slot) return 'up'
const cx = slot.l + slot.w / 2
if (cx > 42 && cx < 58) return 'scale'
return cx <= 42 ? 'left' : 'right'
}
/** 爱心拼合完成:触发一次心跳 + 小心心飘散 */
const heartDone = computed(() =>
props.page?.type === 'heart-collage' && shownCount.value >= HEART_SLOTS.length
props.page?.type === 'heart-collage' && shownCount.value >= heartSlots.value.length
)
</script>

View File

@@ -201,6 +201,156 @@ html[data-theme='pixel'] .sh-note {
font-size: 10px;
}
/* —— 水墨:楷体 + 墨横 + 朱砂点 —— */
html[data-theme='inkwash'] .sh-kicker {
font-family: var(--font-kai);
letter-spacing: 0.4em;
}
html[data-theme='inkwash'] .sh-title {
font-family: var(--font-kai);
letter-spacing: 0.36em;
text-indent: 0.36em;
}
html[data-theme='inkwash'] .sh-rule-bottom {
display: block;
width: 46px;
height: 3px;
background: linear-gradient(90deg, transparent, var(--c-ink) 30%, var(--c-ink) 70%, transparent);
border-radius: 2px;
}
/* —— 电影:场记板 SCENE —— */
html[data-theme='cinema'] .sh-index {
display: block;
font-family: var(--font-number);
font-style: normal;
font-size: 13px;
letter-spacing: 0.2em;
color: var(--c-on-primary);
background: var(--c-primary);
padding: 3px 10px;
border-radius: 3px;
transform: rotate(-2deg);
}
html[data-theme='cinema'] .sh-index::before {
content: 'SCENE ';
}
html[data-theme='cinema'] .sh-title {
font-family: var(--font-serif);
font-weight: 700;
letter-spacing: 0.2em;
}
html[data-theme='cinema'] .sh-kicker {
font-family: var(--font-cinzel);
}
/* —— 胶片:帧号标记 —— */
html[data-theme='kodak'] .sh-index {
display: block;
font-family: var(--font-number);
font-style: normal;
font-size: 12px;
letter-spacing: 0.16em;
color: #ff9a3c;
text-shadow: 0 0 6px rgba(255, 130, 30, 0.55);
}
html[data-theme='kodak'] .sh-index::before {
content: '▸ ';
}
html[data-theme='kodak'] .sh-index::after {
content: 'A';
}
html[data-theme='kodak'] .sh-title {
font-weight: 700;
letter-spacing: 0.14em;
}
/* —— 文艺:罗马分幕 —— */
html[data-theme='arthouse'] .sh {
gap: 10px;
margin-bottom: 30px;
}
html[data-theme='arthouse'] .sh-kicker {
font-family: var(--font-display);
letter-spacing: 0.56em;
}
html[data-theme='arthouse'] .sh-title {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.3em;
text-indent: 0.3em;
}
html[data-theme='arthouse'] .sh-rule-bottom {
display: block;
width: 1px;
height: 22px;
background: rgba(var(--c-line-rgb), 0.6);
}
/* —— 旅行:邮戳圆章 —— */
html[data-theme='passport'] .sh-index {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-family: var(--font-number);
font-style: normal;
font-size: 13px;
color: var(--c-primary);
border: 2px solid rgba(var(--c-primary-rgb), 0.55);
border-radius: 50%;
transform: rotate(-10deg);
}
html[data-theme='passport'] .sh-kicker {
font-family: var(--font-number);
letter-spacing: 0.34em;
}
html[data-theme='passport'] .sh-title {
letter-spacing: 0.22em;
font-weight: 600;
}
/* —— 手账:标签贴 + 胶带 —— */
html[data-theme='scrapbook'] .sh-title {
font-family: var(--font-longcang);
padding: 6px 20px;
background: var(--c-card);
border: 1px dashed rgba(var(--c-primary-rgb), 0.45);
border-radius: 6px;
transform: rotate(-1.4deg);
box-shadow: 0 6px 16px rgba(69, 58, 48, 0.1);
}
html[data-theme='scrapbook'] .sh-kicker {
font-family: var(--font-liujian);
text-transform: none;
letter-spacing: 0.18em;
}
/* —— Y2K窗口标题条 —— */
html[data-theme='y2k'] .sh-title {
font-weight: 800;
letter-spacing: 0.1em;
background: linear-gradient(180deg, #7a4ae0 10%, #4a90e0 50%, #e04ad0 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
html[data-theme='y2k'] .sh-kicker {
font-family: var(--font-pixel);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--c-line);
text-transform: none;
}
html[data-theme='y2k'] .sh-rule-bottom {
display: block;
width: 60px;
height: 4px;
border-radius: 2px;
background: repeating-linear-gradient(90deg, #7a4ae0 0 8px, #4adce0 8px 16px);
}
/* —— 极简主义:小字 + 一条细线 + 大留白 —— */
html[data-theme='zen'] .sh {
gap: 12px;

View File

@@ -0,0 +1,98 @@
<template>
<div class="ca-root">
<span v-reveal="'down'" class="ca-chapter">CHAPITRE UN</span>
<div class="ca-mid">
<i v-reveal="'down'" class="ca-line" aria-hidden="true"></i>
<div v-reveal="{ variant: 'scale', delay: 180 }" class="ca-photo">
<img :src="heroImg" class="ca-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<i v-reveal="'up'" class="ca-line" aria-hidden="true"></i>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="ca-copy">
<span class="ca-names">{{ groom }} <em>et</em> {{ bride }}</span>
<span class="ca-date">{{ date }} {{ lunar }}</span>
</div>
</div>
</template>
<script setup>
/** 文艺影调封面:严格对称构图、上下轴线、法式分幕字 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.ca-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
padding: max(40px, env(safe-area-inset-top)) 30px 48px;
background: var(--c-bg);
overflow: hidden;
}
.ca-chapter {
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 0.66em;
text-indent: 0.66em;
color: var(--c-muted);
}
.ca-mid {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.ca-line {
width: 1px;
height: 30px;
background: rgba(var(--c-line-rgb), 0.7);
}
.ca-photo {
width: min(58vw, 215px);
aspect-ratio: 4 / 5;
overflow: hidden;
}
.ca-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: saturate(0.82) contrast(1.03);
}
.ca-copy {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
.ca-names {
font-family: var(--font-display);
font-size: clamp(23px, 7vw, 30px);
letter-spacing: 0.16em;
color: var(--c-ink);
}
.ca-names em {
font-style: italic;
font-size: 0.62em;
color: var(--c-muted);
margin: 0 0.3em;
}
.ca-date {
font-family: var(--font-serif);
font-size: 10.5px;
letter-spacing: 0.3em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="cc-root">
<div v-reveal="'down'" class="cc-top">
<span class="cc-badge">NOW SHOWING</span>
<span class="cc-studio">LOVE PICTURES 出品</span>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cc-poster">
<img :src="heroImg" class="cc-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="cc-poster-veil" aria-hidden="true"></span>
<span class="cc-corner" aria-hidden="true">彩色 · 宽银幕</span>
</div>
<div v-reveal="{ variant: 'up', delay: 280 }" class="cc-title-wrap">
<span class="cc-title">THE WEDDING</span>
<span class="cc-starring">领衔主演</span>
<span class="cc-names">{{ groom }} · {{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="cc-release">
<i aria-hidden="true"></i>
<span>{{ date }} 隆重献映 · {{ lunar }}</span>
<i aria-hidden="true"></i>
</div>
</div>
</template>
<script setup>
/** 复古电影海报封面NOW SHOWING 角标 + 主演字幕 + 上映日期 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cc-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
padding: max(24px, env(safe-area-inset-top)) 26px 34px;
background:
var(--t-bg-texture),
radial-gradient(ellipse at 50% 0%, rgba(212, 162, 74, 0.16), transparent 55%),
var(--c-bg);
overflow: hidden;
}
.cc-top {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.cc-badge {
padding: 4px 10px;
font-family: var(--font-cinzel);
font-size: 10px;
letter-spacing: 0.24em;
color: var(--c-on-primary);
background: var(--c-primary);
border-radius: 3px;
transform: rotate(-3deg);
box-shadow: 2px 3px 0 rgba(58, 44, 34, 0.25);
}
.cc-studio {
font-family: var(--font-serif);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cc-poster {
position: relative;
width: min(70vw, 260px);
aspect-ratio: 3 / 4;
border: 3px solid var(--c-ink);
outline: 1px solid rgba(var(--c-line-rgb), 0.8);
outline-offset: 5px;
overflow: hidden;
box-shadow: 0 18px 44px rgba(58, 44, 34, 0.3);
}
.cc-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: sepia(0.16) saturate(1.12) contrast(1.04);
}
.cc-poster-veil {
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 62%, rgba(38, 24, 16, 0.42));
pointer-events: none;
}
.cc-corner {
position: absolute;
right: 8px;
bottom: 8px;
font-family: var(--font-serif);
font-size: 9px;
letter-spacing: 0.16em;
color: rgba(248, 240, 226, 0.92);
}
.cc-title-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
}
.cc-title {
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(30px, 9.4vw, 42px);
letter-spacing: 0.1em;
color: var(--c-primary);
text-shadow: 2px 2px 0 rgba(212, 162, 74, 0.4);
}
.cc-starring {
font-family: var(--font-serif);
font-size: 10px;
letter-spacing: 0.5em;
text-indent: 0.5em;
color: var(--c-muted);
}
.cc-names {
font-family: var(--font-serif);
font-weight: 700;
font-size: clamp(19px, 5.6vw, 24px);
letter-spacing: 0.2em;
color: var(--c-ink);
}
.cc-release {
display: flex;
align-items: center;
gap: 12px;
font-family: var(--font-serif);
font-size: 11px;
letter-spacing: 0.16em;
color: var(--c-ink-soft);
}
.cc-release i {
width: 34px;
height: 1px;
background: rgba(var(--c-line-rgb), 0.8);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col items-center justify-between py-12 flex-1 w-full relative border-[0.5px] border-[var(--c-primary)]/20 bg-[var(--c-card)]/40">
<div class="flex flex-col items-center justify-between py-12 flex-1 w-full relative border-[0.5px] border-[var(--c-primary)]/20 bg-[var(--c-card)]/20">
<div v-reveal="'down'" class="text-center mt-6 z-10">
<span class="cover-kicker calligraphy text-[10px] tracking-[0.5em] uppercase text-[var(--c-primary)] font-bold block">Wedding Invitation</span>
<div class="w-6 h-[0.5px] bg-[var(--c-primary)] mx-auto opacity-50 mt-4"></div>

View File

@@ -0,0 +1,114 @@
<template>
<div class="ci-root">
<svg class="ci-mount" viewBox="0 0 400 160" preserveAspectRatio="none" aria-hidden="true">
<path d="M0 128C60 84 120 108 170 92 230 72 260 108 320 96 360 88 380 104 400 96V160H0Z" fill="var(--c-primary)" fill-opacity="0.1" />
<path d="M0 144C80 112 150 136 220 118 290 100 340 128 400 116V160H0Z" fill="var(--c-primary)" fill-opacity="0.16" />
</svg>
<span v-reveal="'down'" class="ci-kicker"> </span>
<div v-reveal="{ variant: 'scale', delay: 160 }" class="ci-photo">
<img :src="heroImg" class="ci-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div v-reveal="{ variant: 'up', delay: 280 }" class="ci-names">
<span class="vertical-text">{{ groom }}</span>
<b class="ci-seal" aria-hidden="true"></b>
<span class="vertical-text">{{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 360 }" class="ci-date">{{ date }} · {{ lunar }}</div>
</div>
</template>
<script setup>
/** 水墨山水封面:宣纸底 + 泼墨山影 + 竖排名 + 朱砂印 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.ci-root {
position: relative;
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 26px;
padding: max(30px, env(safe-area-inset-top)) 26px 90px;
background: var(--c-bg);
overflow: hidden;
}
.ci-mount {
position: absolute;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 150px;
pointer-events: none;
}
.ci-kicker {
font-family: var(--font-kai);
font-size: 13px;
letter-spacing: 0.6em;
text-indent: 0.6em;
color: var(--c-muted);
}
.ci-photo {
width: min(62vw, 235px);
aspect-ratio: 3 / 4;
padding: 8px;
background: var(--c-card);
border: 1px solid rgba(var(--c-primary-rgb), 0.28);
box-shadow: 0 16px 40px rgba(42, 46, 42, 0.14);
}
.ci-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: saturate(0.85) contrast(1.02);
}
.ci-names {
display: flex;
align-items: flex-start;
gap: 20px;
font-family: var(--font-kai);
font-size: clamp(21px, 6vw, 26px);
letter-spacing: 0.3em;
color: var(--c-ink);
}
.ci-names .vertical-text {
writing-mode: vertical-rl;
max-height: 120px;
}
.ci-seal {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 6px;
font-family: var(--font-kai);
font-size: 15px;
font-weight: 400;
letter-spacing: 0;
color: #fdf6f0;
background: #b5493f;
border-radius: 3px;
}
.ci-date {
font-family: var(--font-serif);
font-size: 11px;
letter-spacing: 0.24em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,127 @@
<template>
<div class="ck-root">
<div v-reveal="'down'" class="ck-strip" aria-hidden="true">
<i></i><i></i><i></i>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="ck-frame">
<span class="ck-holes ck-holes-top" aria-hidden="true"></span>
<div class="ck-photo">
<img :src="heroImg" class="ck-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="ck-stamp">{{ dateStamp }}</span>
</div>
<span class="ck-holes ck-holes-bottom" aria-hidden="true"></span>
<span class="ck-label">WEDDING FILM · 400</span>
</div>
<div v-reveal="{ variant: 'up', delay: 300 }" class="ck-names">{{ groom }} {{ bride }}</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="ck-sub">{{ date }} · {{ lunar }}</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
/** 胶片日记封面:齿孔胶片框 + 复古日期水印 + 柯达色条 */
const props = defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
/** 提取 'YY MM DD 形式的复古水印 */
const dateStamp = computed(() => {
const m = /(\d{4})年(\d{1,2})月(\d{1,2})日/.exec(props.date || '')
if (!m) return "'26 09 04"
return `'${m[1].slice(2)} ${m[2].padStart(2, '0')} ${m[3].padStart(2, '0')}`
})
</script>
<style scoped>
.ck-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: max(28px, env(safe-area-inset-top)) 26px 40px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
/* 柯达色条 */
.ck-strip {
display: flex;
gap: 4px;
}
.ck-strip i {
width: 26px;
height: 8px;
border-radius: 2px;
}
.ck-strip i:nth-child(1) { background: #e0782e; }
.ck-strip i:nth-child(2) { background: #d84438; }
.ck-strip i:nth-child(3) { background: #e8b02e; }
/* 胶片框 */
.ck-frame {
width: min(76vw, 280px);
background: #201d18;
border-radius: 8px;
padding: 6px 10px 8px;
box-shadow: 0 18px 42px rgba(56, 48, 42, 0.34);
}
.ck-holes {
display: block;
height: 9px;
margin: 4px 2px;
background-image: repeating-linear-gradient(90deg, rgba(250, 246, 236, 0.9) 0 10px, transparent 10px 21px);
border-radius: 2px;
}
.ck-photo {
position: relative;
overflow: hidden;
}
.ck-img {
width: 100%;
aspect-ratio: 3 / 4;
object-fit: cover;
display: block;
filter: sepia(0.18) saturate(1.15) contrast(1.02) brightness(1.02);
}
/* 复古橙色日期水印 */
.ck-stamp {
position: absolute;
right: 8px;
bottom: 8px;
font-family: var(--font-number);
font-size: 15px;
letter-spacing: 0.08em;
color: #ff9a3c;
text-shadow: 0 0 7px rgba(255, 130, 30, 0.75);
}
.ck-label {
display: block;
text-align: center;
padding-top: 3px;
font-size: 8.5px;
letter-spacing: 0.3em;
color: rgba(250, 246, 236, 0.6);
}
.ck-names {
font-size: clamp(21px, 6.4vw, 27px);
font-weight: 600;
letter-spacing: 0.14em;
color: var(--c-ink);
}
.ck-sub {
font-size: 11px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,209 @@
<template>
<div class="cpp-root">
<div v-reveal="'down'" class="cpp-head">
<span class="cpp-air">LOVE AIRLINES</span>
<span class="cpp-class">FIRST CLASS</span>
</div>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cpp-ticket">
<div class="cpp-photo">
<img :src="heroImg" class="cpp-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div class="cpp-info">
<div class="cpp-route">
<span class="cpp-port"><b>SGL</b><i>单身</i></span>
<span class="cpp-plane" aria-hidden="true">
<svg viewBox="0 0 90 20"><path d="M2 12h30M58 12h30" stroke="var(--c-line)" stroke-width="1" stroke-dasharray="3 3" fill="none"/><path d="M40 10l12 2-12 2 3-2z" fill="var(--c-primary)"/></svg>
</span>
<span class="cpp-port"><b>MRD</b><i>已婚</i></span>
</div>
<div class="cpp-row">
<span class="cpp-cell"><i>PASSENGERS</i><b>{{ groom }} / {{ bride }}</b></span>
</div>
<div class="cpp-row">
<span class="cpp-cell"><i>DATE</i><b>{{ date }}</b></span>
<span class="cpp-cell"><i>GATE</i><b>FOREVER</b></span>
</div>
<div class="cpp-barcode" aria-hidden="true"><i v-for="n in 22" :key="n"></i></div>
</div>
<span class="cpp-stamp" aria-hidden="true">APPROVED </span>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="cpp-foot">{{ lunar }} · 一起飞往余生</div>
</div>
</template>
<script setup>
/** 环球旅行封面:登机牌(单身 → 已婚 航线、乘客栏、条码、盖章) */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cpp-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 26px;
padding: max(28px, env(safe-area-inset-top)) 22px 40px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
.cpp-head {
width: min(100%, 330px);
display: flex;
justify-content: space-between;
font-family: var(--font-number);
font-size: 11px;
letter-spacing: 0.26em;
color: var(--c-primary);
}
.cpp-class {
color: var(--c-line);
}
.cpp-ticket {
position: relative;
width: min(100%, 330px);
border-radius: 12px;
background: var(--c-card);
border: 1px solid rgba(var(--c-primary-rgb), 0.3);
box-shadow: 0 16px 40px rgba(44, 54, 64, 0.16);
overflow: hidden;
}
/* 登机牌撕边 */
.cpp-ticket::before,
.cpp-ticket::after {
content: '';
position: absolute;
top: 58%;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--c-bg);
border: 1px solid rgba(var(--c-primary-rgb), 0.3);
}
.cpp-ticket::before { left: -9px; }
.cpp-ticket::after { right: -9px; }
.cpp-photo {
height: 168px;
overflow: hidden;
}
.cpp-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cpp-info {
padding: 14px 16px 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.cpp-route {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.cpp-port {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.cpp-port b {
font-family: var(--font-number);
font-size: 24px;
letter-spacing: 0.08em;
color: var(--c-primary);
}
.cpp-port i {
font-style: normal;
font-size: 9px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cpp-plane {
flex: 1;
}
.cpp-plane svg {
width: 100%;
height: 20px;
display: block;
}
.cpp-row {
display: flex;
gap: 14px;
border-top: 1px dashed rgba(var(--c-primary-rgb), 0.25);
padding-top: 9px;
}
.cpp-cell {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.cpp-cell i {
font-style: normal;
font-family: var(--font-number);
font-size: 8px;
letter-spacing: 0.22em;
color: var(--c-muted);
}
.cpp-cell b {
font-size: 13px;
letter-spacing: 0.06em;
color: var(--c-ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cpp-barcode {
display: flex;
align-items: flex-end;
gap: 2px;
height: 20px;
margin-top: 2px;
}
.cpp-barcode i {
width: 2px;
height: 100%;
background: var(--c-ink);
opacity: 0.8;
}
.cpp-barcode i:nth-child(2n) { height: 72%; width: 1px; }
.cpp-barcode i:nth-child(3n) { height: 86%; width: 3px; }
/* 邮戳 */
.cpp-stamp {
position: absolute;
right: 12px;
top: 130px;
padding: 5px 10px;
font-family: var(--font-number);
font-size: 11px;
letter-spacing: 0.14em;
color: #b5493f;
border: 2px solid #b5493f;
border-radius: 6px;
transform: rotate(-12deg);
opacity: 0.85;
background: rgba(255, 252, 246, 0.55);
}
.cpp-foot {
font-size: 11px;
letter-spacing: 0.24em;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<div class="cs-root">
<span v-reveal="'down'" class="cs-kicker"> my wedding journal </span>
<div v-reveal="{ variant: 'scale', delay: 150 }" class="cs-photo-wrap">
<span class="cs-tape cs-tape-l" aria-hidden="true"></span>
<span class="cs-tape cs-tape-r" aria-hidden="true"></span>
<div class="cs-photo">
<img :src="heroImg" class="cs-img" fetchpriority="high" decoding="async" alt="封面" />
<span class="cs-caption">us </span>
</div>
<span class="cs-sticker cs-sticker-1" aria-hidden="true"></span>
<span class="cs-sticker cs-sticker-2" aria-hidden="true"></span>
</div>
<div v-reveal="{ variant: 'up', delay: 300 }" class="cs-names">
<span>{{ groom }}</span>
<em>&amp;</em>
<span>{{ bride }}</span>
</div>
<div v-reveal="{ variant: 'up', delay: 380 }" class="cs-date-tag">
<span>{{ date }}</span>
<small>{{ lunar }}</small>
</div>
</div>
</template>
<script setup>
/** 手账拼贴封面:胶带斜贴拍立得 + 贴纸 + 手写标签 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cs-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: max(28px, env(safe-area-inset-top)) 26px 42px;
background:
var(--t-bg-texture),
var(--c-bg);
overflow: hidden;
}
.cs-kicker {
font-family: var(--font-liujian);
font-size: 14px;
letter-spacing: 0.2em;
color: var(--c-muted);
}
.cs-photo-wrap {
position: relative;
transform: rotate(-2.5deg);
}
.cs-photo {
width: min(64vw, 240px);
padding: 10px 10px 34px;
background: var(--c-card);
box-shadow: 0 12px 32px rgba(69, 58, 48, 0.18);
border: 1px solid rgba(69, 58, 48, 0.08);
}
.cs-img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
.cs-caption {
display: block;
padding-top: 8px;
text-align: center;
font-family: var(--font-liujian);
font-size: 15px;
color: var(--c-ink-soft);
}
.cs-tape {
position: absolute;
width: 74px;
height: 24px;
background: rgba(138, 168, 138, 0.5);
box-shadow: 0 1px 4px rgba(69, 58, 48, 0.16);
z-index: 2;
}
.cs-tape-l { top: -12px; left: -22px; transform: rotate(-38deg); }
.cs-tape-r { top: -10px; right: -24px; transform: rotate(32deg); background: rgba(216, 168, 72, 0.45); }
.cs-sticker {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
box-shadow: 0 2px 6px rgba(69, 58, 48, 0.24);
}
.cs-sticker-1 {
right: -16px;
bottom: 22px;
width: 34px;
height: 34px;
font-size: 15px;
background: #c47a5a;
transform: rotate(10deg);
}
.cs-sticker-2 {
left: -14px;
bottom: 52px;
width: 26px;
height: 26px;
font-size: 12px;
background: #8aa88a;
transform: rotate(-14deg);
}
.cs-names {
display: flex;
align-items: baseline;
gap: 12px;
font-family: var(--font-longcang);
font-size: clamp(26px, 8vw, 34px);
color: var(--c-ink);
}
.cs-names em {
font-style: normal;
font-size: 0.6em;
color: var(--c-primary);
}
.cs-date-tag {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 8px 18px;
background: var(--c-card);
border: 1px dashed rgba(var(--c-primary-rgb), 0.5);
border-radius: 8px;
transform: rotate(1.6deg);
font-family: var(--font-liujian);
}
.cs-date-tag span {
font-size: 14px;
color: var(--c-ink-soft);
}
.cs-date-tag small {
font-size: 11px;
color: var(--c-muted);
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="cy-root">
<div v-reveal="{ variant: 'scale', delay: 100 }" class="cy-window">
<div class="cy-titlebar">
<span class="cy-title-text">wedding.exe</span>
<span class="cy-btns" aria-hidden="true"><i></i><i></i><i></i></span>
</div>
<div class="cy-body">
<div class="cy-photo">
<img :src="heroImg" class="cy-img" fetchpriority="high" decoding="async" alt="封面" />
</div>
<div class="cy-names">{{ groom }} {{ bride }}</div>
<div class="cy-date">{{ date }} {{ lunar }}</div>
<div class="cy-progress" aria-hidden="true">
<span class="cy-progress-label">loading love 99%</span>
<span class="cy-progress-track"><i></i></span>
</div>
</div>
</div>
<div v-reveal="{ variant: 'up', delay: 320 }" class="cy-cursor" aria-hidden="true">
<span class="cy-star"></span>
<span class="cy-hint">click to continue</span>
</div>
</div>
</template>
<script setup>
/** 千禧蒸汽波封面:复古 UI 窗口(标题栏/加载条)+ 渐变镜面字 + 星星光标 */
defineProps({
heroImg: { type: String, default: '' },
groom: { type: String, default: '' },
bride: { type: String, default: '' },
date: { type: String, default: '' },
lunar: { type: String, default: '' },
})
</script>
<style scoped>
.cy-root {
width: 100%;
flex: 1 1 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
padding: max(28px, env(safe-area-inset-top)) 24px 44px;
background:
var(--t-bg-texture),
linear-gradient(160deg, #ece8f8 0%, #e0ecf8 48%, #f4e4f4 100%);
overflow: hidden;
}
.cy-window {
width: min(100%, 330px);
border: 2px solid #9088b8;
border-radius: 10px;
background: var(--c-card);
box-shadow:
inset 1px 1px 0 rgba(255, 255, 255, 0.9),
6px 8px 0 rgba(122, 74, 224, 0.18),
0 20px 46px rgba(84, 74, 112, 0.22);
overflow: hidden;
}
.cy-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 10px;
background: linear-gradient(90deg, #7a4ae0, #4a90e0 55%, #4adce0);
color: #fff;
}
.cy-title-text {
font-family: var(--font-pixel);
font-size: 11px;
letter-spacing: 0.1em;
}
.cy-btns {
display: flex;
gap: 4px;
}
.cy-btns i {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-style: normal;
background: rgba(255, 255, 255, 0.92);
color: #544a70;
border-radius: 3px;
}
.cy-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 18px 18px 20px;
}
.cy-photo {
width: 100%;
border: 2px solid #b8b0d8;
border-radius: 6px;
padding: 4px;
background: #fff;
}
.cy-img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
border-radius: 3px;
filter: saturate(1.15) contrast(1.02);
}
/* 渐变镜面字 */
.cy-names {
font-size: clamp(23px, 7vw, 29px);
font-weight: 800;
letter-spacing: 0.08em;
background: linear-gradient(180deg, #7a4ae0 8%, #4a90e0 44%, #4adce0 58%, #e04ad0 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 2px 0 rgba(255, 255, 255, 0.8)) drop-shadow(0 4px 10px rgba(122, 74, 224, 0.3));
}
.cy-date {
font-family: var(--font-pixel);
font-size: 11px;
letter-spacing: 0.08em;
color: var(--c-ink-soft);
}
.cy-progress {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.cy-progress-label {
font-family: var(--font-pixel);
font-size: 9px;
color: var(--c-muted);
}
.cy-progress-track {
display: block;
height: 12px;
border: 2px solid #9088b8;
border-radius: 4px;
padding: 1px;
background: #fff;
}
.cy-progress-track i {
display: block;
width: 99%;
height: 100%;
border-radius: 2px;
background: repeating-linear-gradient(90deg, #7a4ae0 0 8px, #4adce0 8px 16px);
animation: cyLoad 1.6s steps(8) infinite;
}
@keyframes cyLoad {
0% { background-position: 0 0; }
100% { background-position: 32px 0; }
}
.cy-cursor {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.cy-star {
font-size: 20px;
color: #7a4ae0;
animation: cyTwinkle 1.2s steps(2) infinite;
}
@keyframes cyTwinkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.85); }
}
.cy-hint {
font-family: var(--font-pixel);
font-size: 9px;
letter-spacing: 0.16em;
color: var(--c-muted);
}
</style>

View File

@@ -16,7 +16,11 @@ export const BORDER_STYLES = [
{ value: 'glow', label: '柔光金晕', group: 'base' },
{ value: 'stamp', label: '邮票齿孔', group: 'postcard', shaped: true },
{ value: 'postcard', label: '复古明信片', group: 'postcard' },
{ value: 'arch', label: '拱门', group: 'postcard', ratio: '3 / 4' },
{ value: 'wreath', label: '花环相框', group: 'postcard', ratio: '3 / 4' },
{ value: 'baroque', label: '巴洛克金镜', group: 'postcard', ratio: '3 / 4' },
{ value: 'fan', label: '折扇扇面', group: 'postcard', shaped: true, ratio: '4 / 3' },
{ value: 'moonwin', label: '月洞圆窗', group: 'postcard', ratio: '1 / 1' },
{ value: 'ribbon', label: '蝴蝶结缎带', group: 'postcard', ratio: '3 / 4' },
{ value: 'oval', label: '椭圆相框', group: 'postcard', ratio: '3 / 4' },
{ value: 'circle', label: '正圆相框', group: 'postcard', ratio: '1 / 1' },
{ value: 'heart', label: '心形', group: 'postcard', shaped: true, ratio: '1 / 1' },
@@ -44,12 +48,17 @@ export const BORDER_STYLE_GROUPS = [
{ key: 'scenic', label: '横幅 · 云雾花', items: BORDER_STYLES.filter((b) => b.group === 'scenic') },
]
/** 旧配置值映射:拱门样式已下线,历史数据自动呈现为花环相框 */
const LEGACY_BORDER_MAP = { arch: 'wreath' }
/** 取样式元数据(未知值回落白卡纸) */
export function borderStyleMeta(style) {
return BORDER_STYLES.find((b) => b.value === style) || BORDER_STYLES.find((b) => b.value === 'mat')
const v = LEGACY_BORDER_MAP[style] || style
return BORDER_STYLES.find((b) => b.value === v) || BORDER_STYLES.find((b) => b.value === 'mat')
}
export function borderClass(style) {
const v = BORDER_STYLES.some((b) => b.value === style) ? style : 'mat'
const mapped = LEGACY_BORDER_MAP[style] || style
const v = BORDER_STYLES.some((b) => b.value === mapped) ? mapped : 'mat'
return 'gf-frame gf-' + v
}

View File

@@ -1,32 +1,157 @@
import { clampHeroFocus, heroFocusCss } from '@/composables/posterDefaults'
/** 爱心拼接槽位:百分比绝对定位(顶开叉、中最宽、底收尖)
* r : 格子圆角(外缘格用大弧度圆角把方块阶梯"磨"成心形弧线
* clip : 格子裁切(心尖用 V 形三角收出尖端)
* 槽位索引与角色保持不变,存量配置的图片顺序不受影响 */
export const HEART_SLOTS = [
{ l: 6, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px' }, // 0 左上叶(半圆顶
{ l: 56, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px' }, // 1 右上叶(半圆顶)
{ l: 0, t: 14.8, w: 13, h: 15, r: '55% 6px 6px 6px' }, // 2 左外上(肩部外弧)
{ l: 0, t: 30.6, w: 13, h: 14.6, r: '6px 6px 6px 55%' }, // 3 左外下(收腰起弧)
{ l: 14, t: 14.8, w: 26, h: 30.4 }, // 4 左大图
{ l: 41, t: 14.8, w: 18, h: 30.4 }, // 5 中轴
{ l: 60, t: 14.8, w: 26, h: 30.4 }, // 6 右大图
{ l: 87, t: 14.8, w: 13, h: 15, r: '6px 55% 6px 6px' }, // 7 右外上(肩部外弧)
{ l: 87, t: 30.6, w: 13, h: 14.6, r: '6px 6px 55% 6px' }, // 8 右外下(收腰起弧)
{ l: 8, t: 46.2, w: 16, h: 16, r: '6px 6px 6px 65%' }, // 9 左下收腰(底外弧)
{ l: 25, t: 46.2, w: 50, h: 16 }, // 10 中下横
{ l: 76, t: 46.2, w: 16, h: 16, r: '6px 6px 65% 6px' }, // 11 右下收腰(底外弧)
{ l: 34, t: 63, w: 32, h: 16.5, clip: 'polygon(0 0, 100% 0, 50% 100%)' }, // 12 心尖V 形)
/**
* 爱心拼接多套拼法方案后台可选classic 为默认
* 槽位字段:
* - l/t/w/h : 百分比绝对定位
* - r : 圆角(弧形磨边)
* - clip : clip-pathV 形心尖等
* - shape : 'heart' —— 整格心形 mask 裁切
* - label : 后台拼图板槽位名
*/
export const HEART_MASK_CSS = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E\") no-repeat 50% 50% / 100% 100%"
const HEART_MASK_SVG = HEART_MASK_CSS
/* —— classic 经典阶梯v1 原始矩形,默认) —— */
const SLOTS_CLASSIC = [
{ l: 10, t: 0, w: 32, h: 13, label: '左上叶' },
{ l: 58, t: 0, w: 32, h: 13, label: '右上叶' },
{ l: 0, t: 14, w: 14, h: 15.5, label: '左外上' },
{ l: 0, t: 30.5, w: 14, h: 15.5, label: '左外下' },
{ l: 15, t: 14, w: 26, h: 32, label: '左大图' },
{ l: 42, t: 14, w: 16, h: 32, label: '中轴' },
{ l: 59, t: 14, w: 26, h: 32, label: '右大图' },
{ l: 86, t: 14, w: 14, h: 15.5, label: '右外上' },
{ l: 86, t: 30.5, w: 14, h: 15.5, label: '右外下' },
{ l: 10, t: 47, w: 13, h: 16, label: '左下' },
{ l: 24, t: 47, w: 52, h: 16, label: '中下横' },
{ l: 77, t: 47, w: 13, h: 16, label: '右下' },
{ l: 38, t: 64, w: 24, h: 14, label: '心尖' },
]
export const HEART_SLOT_COUNT = HEART_SLOTS.length
export const HEART_SLOT_LABELS = [
'左上叶', '右上叶', '左外上', '左外下', '左大图', '中轴', '右大图',
'右外上', '右外下', '左下', '中下横', '右下', '心尖',
/* —— soft 圆弧心(外缘大圆角 + V 形心尖) —— */
const SLOTS_SOFT = [
{ l: 6, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px', label: '左上叶' },
{ l: 56, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px', label: '右上叶' },
{ l: 0, t: 14.8, w: 13, h: 15, r: '55% 6px 6px 6px', label: '左外上' },
{ l: 0, t: 30.6, w: 13, h: 14.6, r: '6px 6px 6px 55%', label: '左外下' },
{ l: 14, t: 14.8, w: 26, h: 30.4, label: '左大图' },
{ l: 41, t: 14.8, w: 18, h: 30.4, label: '中轴' },
{ l: 60, t: 14.8, w: 26, h: 30.4, label: '右大图' },
{ l: 87, t: 14.8, w: 13, h: 15, r: '6px 55% 6px 6px', label: '右外上' },
{ l: 87, t: 30.6, w: 13, h: 14.6, r: '6px 6px 55% 6px', label: '右外下' },
{ l: 8, t: 46.2, w: 16, h: 16, r: '6px 6px 6px 65%', label: '左下' },
{ l: 25, t: 46.2, w: 50, h: 16, label: '中下横' },
{ l: 76, t: 46.2, w: 16, h: 16, r: '6px 6px 65% 6px', label: '右下' },
{ l: 34, t: 63, w: 32, h: 16.5, clip: 'polygon(0 0, 100% 0, 50% 100%)', label: '心尖' },
]
/* —— ring 环绕心(中央大圆 + 12 小圆沿心形轮廓) —— */
const SLOTS_RING = [
{ l: 30, t: 24, w: 40, h: 32, r: '50%', label: '中央主图' },
{ l: 43, t: 5.4, w: 14, h: 11.2, r: '50%', label: '凹口' },
{ l: 28, t: 0, w: 14, h: 11.2, r: '50%', label: '左叶顶' },
{ l: 58, t: 0, w: 14, h: 11.2, r: '50%', label: '右叶顶' },
{ l: 13, t: 3.4, w: 14, h: 11.2, r: '50%', label: '左叶外' },
{ l: 73, t: 3.4, w: 14, h: 11.2, r: '50%', label: '右叶外' },
{ l: 3, t: 20.4, w: 14, h: 11.2, r: '50%', label: '左肩' },
{ l: 83, t: 20.4, w: 14, h: 11.2, r: '50%', label: '右肩' },
{ l: 9, t: 39.4, w: 14, h: 11.2, r: '50%', label: '左腰' },
{ l: 77, t: 39.4, w: 14, h: 11.2, r: '50%', label: '右腰' },
{ l: 25, t: 57.4, w: 14, h: 11.2, r: '50%', label: '左下' },
{ l: 61, t: 57.4, w: 14, h: 11.2, r: '50%', label: '右下' },
{ l: 43, t: 72, w: 14, h: 11.2, r: '50%', label: '心尖' },
]
/* —— grid 格子心(等大方格阶梯,像素心) —— */
const SLOTS_GRID = (() => {
const W = 18
const H = 14.4
const colX = [5, 24, 43, 62, 81]
const rowY = [2, 20, 38, 56]
const cells = [
[0, 1], [0, 3],
[1, 0], [1, 1], [1, 2], [1, 3], [1, 4],
[2, 1], [2, 2], [2, 3],
[3, 2],
]
const labels = ['左叶', '右叶', '左耳', '左颊', '中心', '右颊', '右耳', '左收', '中收', '右收', '心尖']
return cells.map(([row, col], i) => ({
l: colX[col], t: rowY[row], w: W, h: H, r: '4px', label: labels[i] || `${i + 1}`,
}))
})()
/* —— wings 比翼心(两大翼 + 中缝收尖列,少图大脸友好) —— */
const SLOTS_WINGS = [
{ l: 2, t: 3, w: 46, h: 54, r: '48% 46% 8% 62% / 42% 38% 8% 34%', label: '左翼' },
{ l: 52, t: 3, w: 46, h: 54, r: '46% 48% 62% 8% / 38% 42% 34% 8%', label: '右翼' },
{ l: 42.5, t: 59, w: 15, h: 12, r: '50%', label: '收心一' },
{ l: 44, t: 72.5, w: 12, h: 9.6, r: '50%', label: '收心二' },
{ l: 45.5, t: 83.5, w: 9, h: 7.2, r: '50%', label: '心尖' },
]
/* —— mosaic 马赛克心(大小方块混拼) —— */
const SLOTS_MOSAIC = [
{ l: 8, t: 2, w: 22, h: 15, r: '6px', label: '左叶大' },
{ l: 70, t: 2, w: 22, h: 15, r: '6px', label: '右叶大' },
{ l: 32, t: 6, w: 16, h: 11, r: '5px', label: '左叶内' },
{ l: 52, t: 6, w: 16, h: 11, r: '5px', label: '右叶内' },
{ l: 4, t: 19, w: 30, h: 21, r: '6px', label: '左脸' },
{ l: 66, t: 19, w: 30, h: 21, r: '6px', label: '右脸' },
{ l: 36, t: 19, w: 28, h: 14, r: '5px', label: '中上' },
{ l: 36, t: 35, w: 28, h: 10, r: '4px', label: '中带' },
{ l: 12, t: 42, w: 24, h: 15, r: '5px', label: '左收' },
{ l: 64, t: 42, w: 24, h: 15, r: '5px', label: '右收' },
{ l: 38, t: 47, w: 24, h: 12, r: '4px', label: '中横' },
{ l: 26, t: 59, w: 16, h: 11, r: '4px', label: '左下' },
{ l: 58, t: 59, w: 16, h: 11, r: '4px', label: '右下' },
{ l: 43, t: 61, w: 14, h: 14, r: '4px', label: '中下' },
{ l: 44, t: 77, w: 12, h: 8.5, r: '4px', label: '心尖' },
]
/* —— orbit 星环心(心形大图 + 8 小圆环绕) —— */
const SLOTS_ORBIT = [
{ l: 20, t: 16, w: 60, h: 46, shape: 'heart', label: '心形主图' },
{ l: 22, t: 0, w: 12, h: 9.6, r: '50%', label: '左上星' },
{ l: 66, t: 0, w: 12, h: 9.6, r: '50%', label: '右上星' },
{ l: 4, t: 18, w: 12, h: 9.6, r: '50%', label: '左星' },
{ l: 84, t: 18, w: 12, h: 9.6, r: '50%', label: '右星' },
{ l: 12, t: 42, w: 12, h: 9.6, r: '50%', label: '左下星' },
{ l: 76, t: 42, w: 12, h: 9.6, r: '50%', label: '右下星' },
{ l: 30, t: 63, w: 12, h: 9.6, r: '50%', label: '尖左星' },
{ l: 58, t: 63, w: 12, h: 9.6, r: '50%', label: '尖右星' },
]
export const HEART_LAYOUTS = [
{ key: 'classic', label: '经典阶梯', desc: '矩形块阶梯拼合(默认)', slots: SLOTS_CLASSIC },
{ key: 'soft', label: '圆弧心', desc: '外缘弧形磨边 + V 形心尖', slots: SLOTS_SOFT },
{ key: 'ring', label: '环绕心', desc: '中央大圆 + 小圆沿轮廓环绕', slots: SLOTS_RING },
{ key: 'grid', label: '格子心', desc: '等大方格像素心', slots: SLOTS_GRID },
{ key: 'wings', label: '比翼心', desc: '两大翼 + 收尖圆列(图少脸大)', slots: SLOTS_WINGS },
{ key: 'mosaic', label: '马赛克心', desc: '大小方块混拼', slots: SLOTS_MOSAIC },
{ key: 'orbit', label: '星环心', desc: '心形整图 + 星点环绕', slots: SLOTS_ORBIT },
]
const HEART_LAYOUT_MAP = Object.fromEntries(HEART_LAYOUTS.map((x) => [x.key, x]))
export function normalizeHeartLayout(key) {
return HEART_LAYOUT_MAP[key] ? key : 'classic'
}
export function heartLayoutMeta(key) {
return HEART_LAYOUT_MAP[normalizeHeartLayout(key)]
}
/** 取页面配置对应的槽位数组 */
export function heartSlotsFor(page) {
return heartLayoutMeta(page?.heartLayout).slots
}
/* —— 兼容旧引用:默认方案 —— */
export const HEART_SLOTS = SLOTS_CLASSIC
export const HEART_SLOT_COUNT = SLOTS_CLASSIC.length
export function heartSlotStyle(slot) {
return {
left: `${slot.l}%`,
@@ -36,17 +161,21 @@ export function heartSlotStyle(slot) {
}
}
export function heartSlotLabel(i) {
return HEART_SLOT_LABELS[i] || `${i + 1}`
export function heartSlotLabel(page, i) {
const slots = heartSlotsFor(page)
return slots[i]?.label || `${i + 1}`
}
/** 格子形状样式(圆角/裁切),作用在展示格上让整体轮廓贴近心形 */
export function heartCellShape(i) {
const s = HEART_SLOTS[i]
if (!s) return {}
/** 格子形状样式(圆角/裁切/心形 mask),作用在展示格上 */
export function heartCellShape(slot) {
if (!slot) return {}
const style = {}
if (s.r) style.borderRadius = s.r
if (s.clip) style.clipPath = s.clip
if (slot.r) style.borderRadius = slot.r
if (slot.clip) style.clipPath = slot.clip
if (slot.shape === 'heart') {
style.webkitMask = HEART_MASK_SVG
style.mask = HEART_MASK_SVG
}
return style
}

View File

@@ -57,11 +57,11 @@ html[data-theme='guofeng-new'] {
--c-muted: #8b8a80;
--c-ink: #2c302c;
--c-ink-soft: #4a4f48;
--c-paper: #faf8f2;
--c-bg: #f6f3ec;
--c-bg-deep: #ece7db;
--c-paper: #f7f5e8;
--c-bg: #f2efe0;
--c-bg-deep: #e6e2ce;
--c-card: #fffefa;
--c-on-primary: #f6f3ec;
--c-on-primary: #f2efe0;
--t-font-title: var(--font-serif);
--t-font-kicker: var(--font-kai);
--t-font-body: var(--font-serif);
@@ -79,9 +79,9 @@ html[data-theme='guofeng-old'] {
--c-muted: #9a8578;
--c-ink: #3a2428;
--c-ink-soft: #5c3a40;
--c-paper: #fdf6f0;
--c-bg: #f9efe6;
--c-bg-deep: #f2e3d4;
--c-paper: #fcf4e6;
--c-bg: #f8ecda;
--c-bg-deep: #f0dec4;
--c-card: #fffaf4;
--c-on-primary: #f7e7c3;
--t-font-title: var(--font-mashan);
@@ -101,9 +101,9 @@ html[data-theme='french'] {
--c-muted: #9c8b90;
--c-ink: #3d3136;
--c-ink-soft: #5c454e;
--c-paper: #fdfafa;
--c-bg: #fdf6f4;
--c-bg-deep: #f7e9e7;
--c-paper: #fdf4f6;
--c-bg: #fceef0;
--c-bg-deep: #f7e2e4;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-calligraphy);
@@ -115,17 +115,17 @@ html[data-theme='french'] {
}
/* 现代简约:暖灰低饱和 · 无衬线 · 细几何 */
html[data-theme='modern'] {
--c-primary: #8a8578;
--c-primary-deep: #757064;
--c-primary-rgb: 138, 133, 120;
--c-line: #a39e91;
--c-line-rgb: 163, 158, 145;
--c-muted: #8f8c85;
--c-ink: #2e2d2a;
--c-ink-soft: #4a4842;
--c-paper: #fbfaf8;
--c-bg: #f7f6f3;
--c-bg-deep: #edebe6;
--c-primary: #6e7b84;
--c-primary-deep: #5c6a74;
--c-primary-rgb: 110, 123, 132;
--c-line: #93a4ae;
--c-line-rgb: 147, 164, 174;
--c-muted: #84909a;
--c-ink: #2a3238;
--c-ink-soft: #46525c;
--c-paper: #f8fafb;
--c-bg: #f1f4f6;
--c-bg-deep: #e4eaee;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-sans);
@@ -145,9 +145,9 @@ html[data-theme='forest'] {
--c-muted: #86928a;
--c-ink: #29322c;
--c-ink-soft: #3d4a40;
--c-paper: #f9fcf9;
--c-bg: #f3f8f2;
--c-bg-deep: #e9f0e7;
--c-paper: #f3f9f2;
--c-bg: #ecf4ea;
--c-bg-deep: #dfeadd;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-longcang);
@@ -189,9 +189,9 @@ html[data-theme='magazine'] {
--c-muted: #7d786f;
--c-ink: #1f1c19;
--c-ink-soft: #3a352e;
--c-paper: #faf8f4;
--c-bg: #f5f2ec;
--c-bg-deep: #eae6dd;
--c-paper: #f7f4ec;
--c-bg: #f2eee4;
--c-bg-deep: #e4dfd0;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-display);
@@ -211,11 +211,11 @@ html[data-theme='newspaper'] {
--c-muted: #6f6b62;
--c-ink: #262420;
--c-ink-soft: #423e35;
--c-paper: #fbf9f2;
--c-bg: #f6f3ea;
--c-bg-deep: #ebe7da;
--c-paper: #f9f5e4;
--c-bg: #f4efdc;
--c-bg-deep: #e7e0c8;
--c-card: #fffdf6;
--c-on-primary: #fbf9f2;
--c-on-primary: #f9f5e4;
--t-font-title: var(--font-serif);
--t-font-kicker: var(--font-display);
--t-font-body: var(--font-serif);
@@ -233,9 +233,9 @@ html[data-theme='pixel'] {
--c-muted: #8a7f74;
--c-ink: #33261f;
--c-ink-soft: #54433a;
--c-paper: #fdf9ee;
--c-bg: #faf3e6;
--c-bg-deep: #f0e6d2;
--c-paper: #fcf5e0;
--c-bg: #f9f0da;
--c-bg-deep: #eee0c0;
--c-card: #fffdf4;
--c-on-primary: #fffdf4;
--t-font-title: var(--font-pixel);
@@ -245,21 +245,219 @@ html[data-theme='pixel'] {
--t-radius-btn: 0px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Crect x='0' y='0' width='4' height='4' fill='%2333261f' fill-opacity='0.02'/%3E%3Crect x='4' y='4' width='4' height='4' fill='%2333261f' fill-opacity='0.02'/%3E%3C/svg%3E");
}
/* 极简主义:黑白留白 · 细线 · 轴线 */
html[data-theme='zen'] {
--c-primary: #2a2a28;
--c-primary-deep: #111111;
--c-primary-rgb: 42, 42, 40;
--c-line: #8f8d88;
--c-line-rgb: 143, 141, 136;
--c-muted: #85837e;
--c-ink: #262624;
--c-ink-soft: #444442;
--c-paper: #fdfdfc;
--c-bg: #fafaf8;
--c-bg-deep: #f0f0ed;
/* 水彩画境:雾蓝淡彩 · 晕染 · 柔圆 */
html[data-theme='watercolor'] {
--c-primary: #7f9bb3;
--c-primary-deep: #6a86a0;
--c-primary-rgb: 127, 155, 179;
--c-line: #a8c0d0;
--c-line-rgb: 168, 192, 208;
--c-muted: #8b939c;
--c-ink: #33404a;
--c-ink-soft: #52626e;
--c-paper: #f5f9fc;
--c-bg: #edf4fa;
--c-bg-deep: #dde9f2;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-calligraphy);
--t-font-kicker: var(--font-sans);
--t-font-body: var(--font-sans);
--t-radius-card: 20px;
--t-radius-btn: 999px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cg fill-opacity='0.05'%3E%3Ccircle cx='40' cy='48' r='26' fill='%237f9bb3'/%3E%3Ccircle cx='138' cy='30' r='18' fill='%23c8a8c0'/%3E%3Ccircle cx='120' cy='128' r='30' fill='%2388b8a8'/%3E%3Ccircle cx='34' cy='140' r='16' fill='%23a8c0d0'/%3E%3C/g%3E%3C/svg%3E");
}
/* 樱花物语:粉白渐彩 · 花瓣 */
html[data-theme='sakura'] {
--c-primary: #d98a9e;
--c-primary-deep: #c67689;
--c-primary-rgb: 217, 138, 158;
--c-line: #e8b4c2;
--c-line-rgb: 232, 180, 194;
--c-muted: #a08d93;
--c-ink: #43333a;
--c-ink-soft: #64505a;
--c-paper: #fdf3f6;
--c-bg: #fcedf1;
--c-bg-deep: #f6dfe6;
--c-card: #ffffff;
--c-on-primary: #ffffff;
--t-font-title: var(--font-calligraphy);
--t-font-kicker: "Pinyon Script", cursive;
--t-font-body: var(--font-sans);
--t-radius-card: 16px;
--t-radius-btn: 999px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cg fill='%23e8a8bc' fill-opacity='0.08'%3E%3Cellipse cx='26' cy='30' rx='5' ry='3.2' transform='rotate(-24 26 30)'/%3E%3Cellipse cx='104' cy='18' rx='4' ry='2.6' transform='rotate(18 104 18)'/%3E%3Cellipse cx='120' cy='96' rx='5.5' ry='3.4' transform='rotate(-40 120 96)'/%3E%3Cellipse cx='48' cy='112' rx='4.4' ry='2.8' transform='rotate(30 48 112)'/%3E%3Cellipse cx='78' cy='64' rx='3.6' ry='2.2' transform='rotate(-12 78 64)'/%3E%3C/g%3E%3C/svg%3E");
}
/* 水墨山水:宣纸墨韵 · 朱砂印 */
html[data-theme='inkwash'] {
--c-primary: #3a3f3a;
--c-primary-deep: #262a26;
--c-primary-rgb: 58, 63, 58;
--c-line: #8a9088;
--c-line-rgb: 138, 144, 136;
--c-muted: #82887e;
--c-ink: #2a2e2a;
--c-ink-soft: #4a504a;
--c-paper: #f7f7f0;
--c-bg: #f2f2ea;
--c-bg-deep: #e2e2d6;
--c-card: #fdfdfa;
--c-on-primary: #f2f2ea;
--t-font-title: var(--font-kai);
--t-font-kicker: var(--font-kai);
--t-font-body: var(--font-serif);
--t-radius-card: 2px;
--t-radius-btn: 2px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cg fill='%233a3f3a' fill-opacity='0.028'%3E%3Cpath d='M0 158q50-36 100-10t100-16v68H0z'/%3E%3C/g%3E%3Cg fill='%233a3f3a' fill-opacity='0.02'%3E%3Ccircle cx='150' cy='40' r='2'/%3E%3Ccircle cx='36' cy='60' r='1.4'/%3E%3C/g%3E%3C/svg%3E");
}
/* 复古电影:上映海报 · 做旧奶油 */
html[data-theme='cinema'] {
--c-primary: #b8322e;
--c-primary-deep: #962622;
--c-primary-rgb: 184, 50, 46;
--c-line: #d4a24a;
--c-line-rgb: 212, 162, 74;
--c-muted: #97887a;
--c-ink: #3a2c22;
--c-ink-soft: #5c4738;
--c-paper: #f5ead4;
--c-bg: #efe0c8;
--c-bg-deep: #e2d0ae;
--c-card: #fbf4e8;
--c-on-primary: #fbf4e8;
--t-font-title: var(--font-display);
--t-font-kicker: var(--font-cinzel);
--t-font-body: var(--font-serif);
--t-radius-card: 6px;
--t-radius-btn: 4px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cg fill='%233a2c22' fill-opacity='0.026'%3E%3Ccircle cx='16' cy='26' r='1.1'/%3E%3Ccircle cx='58' cy='12' r='1.5'/%3E%3Ccircle cx='80' cy='46' r='1'/%3E%3Ccircle cx='34' cy='62' r='1.3'/%3E%3Ccircle cx='68' cy='80' r='1.1'/%3E%3C/g%3E%3C/svg%3E");
}
/* 胶片日记:柯达暖调 · 齿孔框 */
html[data-theme='kodak'] {
--c-primary: #e0782e;
--c-primary-deep: #c86420;
--c-primary-rgb: 224, 120, 46;
--c-line: #3c3a34;
--c-line-rgb: 60, 58, 52;
--c-muted: #96897a;
--c-ink: #38302a;
--c-ink-soft: #5c5044;
--c-paper: #fbf4de;
--c-bg: #f8f0d8;
--c-bg-deep: #eddfba;
--c-card: #fffdf6;
--c-on-primary: #fffdf6;
--t-font-title: var(--font-sans);
--t-font-kicker: var(--font-number);
--t-font-body: var(--font-sans);
--t-radius-card: 8px;
--t-radius-btn: 8px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cg fill='%2338302a' fill-opacity='0.022'%3E%3Ccircle cx='20' cy='30' r='1.2'/%3E%3Ccircle cx='66' cy='14' r='1'/%3E%3Ccircle cx='84' cy='60' r='1.3'/%3E%3Ccircle cx='38' cy='78' r='1'/%3E%3C/g%3E%3C/svg%3E");
}
/* 文艺影调:对称构图 · 衬线留白 */
html[data-theme='arthouse'] {
--c-primary: #4a4640;
--c-primary-deep: #322f2a;
--c-primary-rgb: 74, 70, 64;
--c-line: #9a948a;
--c-line-rgb: 154, 148, 138;
--c-muted: #8c877d;
--c-ink: #363330;
--c-ink-soft: #5a564e;
--c-paper: #f4efdc;
--c-bg: #f1ede2;
--c-bg-deep: #e3decd;
--c-card: #fdfbf7;
--c-on-primary: #f4efdc;
--t-font-title: var(--font-display);
--t-font-kicker: var(--font-display);
--t-font-body: var(--font-serif);
--t-radius-card: 0px;
--t-radius-btn: 0px;
--t-bg-texture: none;
}
/* 环球旅行:护照蓝金 · 登机牌 */
html[data-theme='passport'] {
--c-primary: #2e4a68;
--c-primary-deep: #223a54;
--c-primary-rgb: 46, 74, 104;
--c-line: #b08a4a;
--c-line-rgb: 176, 138, 74;
--c-muted: #85898c;
--c-ink: #2c3640;
--c-ink-soft: #4a5662;
--c-paper: #f4f7fa;
--c-bg: #eceff2;
--c-bg-deep: #dde3ea;
--c-card: #fcfaf4;
--c-on-primary: #eceff2;
--t-font-title: var(--font-display);
--t-font-kicker: var(--font-number);
--t-font-body: var(--font-sans);
--t-radius-card: 10px;
--t-radius-btn: 6px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cg fill='none' stroke='%232e4a68' stroke-opacity='0.04' stroke-width='1' stroke-dasharray='4 5'%3E%3Cpath d='M-10 120C40 60 120 100 170 30'/%3E%3C/g%3E%3Cg fill='%232e4a68' fill-opacity='0.05'%3E%3Cpath d='M96 66l10 4-10 4 2-4z'/%3E%3C/g%3E%3C/svg%3E");
}
/* 手账拼贴:牛皮纸 · 胶带贴纸 */
html[data-theme='scrapbook'] {
--c-primary: #c47a5a;
--c-primary-deep: #a86446;
--c-primary-rgb: 196, 122, 90;
--c-line: #8aa88a;
--c-line-rgb: 138, 168, 138;
--c-muted: #9a8f80;
--c-ink: #453a30;
--c-ink-soft: #665748;
--c-paper: #fbf2dc;
--c-bg: #f7ecd4;
--c-bg-deep: #ecdcba;
--c-card: #fffcf4;
--c-on-primary: #fffcf4;
--t-font-title: var(--font-longcang);
--t-font-kicker: var(--font-liujian);
--t-font-body: var(--font-sans);
--t-radius-card: 6px;
--t-radius-btn: 8px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cg fill='%23453a30' fill-opacity='0.03'%3E%3Ccircle cx='12' cy='12' r='1.4'/%3E%3Ccircle cx='42' cy='30' r='1.4'/%3E%3Ccircle cx='20' cy='48' r='1.4'/%3E%3C/g%3E%3C/svg%3E");
}
/* 千禧蒸汽波:紫青渐变 · 复古窗口 */
html[data-theme='y2k'] {
--c-primary: #7a4ae0;
--c-primary-deep: #6238c0;
--c-primary-rgb: 122, 74, 224;
--c-line: #4adce0;
--c-line-rgb: 74, 220, 224;
--c-muted: #8d87a0;
--c-ink: #322a48;
--c-ink-soft: #544a70;
--c-paper: #efeafa;
--c-bg: #e7e1f8;
--c-bg-deep: #d4cbee;
--c-card: #faf8ff;
--c-on-primary: #faf8ff;
--t-font-title: var(--font-sans);
--t-font-kicker: var(--font-pixel);
--t-font-body: var(--font-sans);
--t-radius-card: 8px;
--t-radius-btn: 6px;
--t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cg fill='%237a4ae0' fill-opacity='0.05'%3E%3Cpath d='M30 20l2.4 6 6 2.4-6 2.4-2.4 6-2.4-6-6-2.4 6-2.4z'/%3E%3Cpath d='M92 74l1.8 4.4 4.4 1.8-4.4 1.8-1.8 4.4-1.8-4.4-4.4-1.8 4.4-1.8z'/%3E%3C/g%3E%3Cg fill='%234adce0' fill-opacity='0.05'%3E%3Ccircle cx='72' cy='28' r='2.4'/%3E%3Ccircle cx='24' cy='92' r='2'/%3E%3C/g%3E%3C/svg%3E");
}
/* 极简主义:暖白留白 · 细线 · 轴线 */
html[data-theme='zen'] {
--c-primary: #57503f;
--c-primary-deep: #3e392c;
--c-primary-rgb: 87, 80, 63;
--c-line: #a89f8e;
--c-line-rgb: 168, 159, 142;
--c-muted: #8f887a;
--c-ink: #3c362e;
--c-ink-soft: #5c5548;
--c-paper: #fdfbf7;
--c-bg: #faf7f2;
--c-bg-deep: #f0ebe2;
--c-card: #fffef9;
--c-on-primary: #fdfbf7;
--t-font-title: var(--font-sans);
--t-font-kicker: var(--font-sans);
--t-font-body: var(--font-sans);

View File

@@ -127,20 +127,24 @@
:where(.gf-postcard, .gf-film, .gf-stamp, .gf-arch, .gf-oval, .gf-circle, .gf-leaf,
.gf-heart, .gf-flower, .gf-hexagon, .gf-scallop, .gf-wave, .gf-zigzag,
.gf-torn, .gf-ticket, .gf-bevel,
.gf-wreath, .gf-baroque, .gf-fan, .gf-moonwin, .gf-ribbon,
.gf-cloud, .gf-mist, .gf-bloom, .gf-panorama, .gf-ovalwide) {
position: relative;
}
/* 边缘渐隐内晕:照片边缘向纸色柔和过渡,与边框/裁切形状融为一体
mask/clip 类的伪元素会随形状一并裁切,晕自动贴合轮廓 */
/* 边缘渐隐内晕:照片边缘向页面纸色柔和过渡,与边框/裁切形状融为一体
mask/clip 类的伪元素会随形状一并裁切,晕自动贴合轮廓
颜色用 color-mix 跟随主题纸色,深浅主题都能融入页面) */
.gf-heart::after, .gf-flower::after, .gf-scallop::after, .gf-wave::after,
.gf-zigzag::after, .gf-torn::after, .gf-stamp::after, .gf-ticket::after,
.gf-hexagon::after, .gf-bevel::after, .gf-arch::after, .gf-oval::after,
.gf-circle::after, .gf-leaf::after, .gf-cloud::after, .gf-ovalwide::after {
.gf-circle::after, .gf-leaf::after, .gf-cloud::after, .gf-ovalwide::after,
.gf-wreath::after, .gf-baroque::after, .gf-fan::after, .gf-moonwin::after,
.gf-ribbon::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 62% 62% at 50% 50%, rgba(255, 252, 246, 0) 55%, rgba(255, 252, 246, 0.48) 100%);
background: radial-gradient(ellipse 64% 64% at 50% 50%, transparent 50%, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent) 100%);
pointer-events: none;
}
@@ -205,39 +209,143 @@
pointer-events: none;
}
/* 拱门(顶部全圆,婚礼穹顶造型 */
.gf-arch {
padding: 8px;
border: 1px solid rgba(201, 169, 120, 0.5);
border-radius: 999px 999px 14px 14px;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.65), 0 14px 32px rgba(120, 90, 60, 0.16);
background: #fff;
}
.gf-arch > img {
border-radius: 992px 992px 8px 8px;
}
/* 拱顶花饰圆章(呼应尾页门框 keystone */
.gf-arch::before {
content: '❀';
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
width: 26px;
height: 26px;
/* 花环相框月桂双枝环抱椭圆照片arch 为历史别名 */
.gf-arch,
.gf-wreath {
padding: 22px;
border: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
color: #b08e63;
background: rgba(253, 251, 247, 0.92);
box-shadow: 0 0 0 1px rgba(201, 169, 120, 0.45), 0 2px 6px rgba(120, 90, 60, 0.18);
box-shadow: none;
background: transparent;
}
.gf-arch > img,
.gf-wreath > img {
border-radius: 50%;
}
.gf-arch::before,
.gf-wreath::before {
content: '';
position: absolute;
inset: -4px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 250'%3E%3Cg fill='none' stroke='%238a9a76' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M104 240C48 230 16 178 20 112 22 70 38 38 66 18'/%3E%3Cpath d='M96 240c56-10 88-62 84-128-2-42-18-74-46-94'/%3E%3C/g%3E%3Cg fill='%239ab48a'%3E%3Cellipse cx='34' cy='196' rx='11' ry='4.6' transform='rotate(38 34 196)'/%3E%3Cellipse cx='23' cy='164' rx='11' ry='4.6' transform='rotate(64 23 164)'/%3E%3Cellipse cx='19' cy='128' rx='11' ry='4.6' transform='rotate(82 19 128)'/%3E%3Cellipse cx='24' cy='92' rx='11' ry='4.6' transform='rotate(102 24 92)'/%3E%3Cellipse cx='38' cy='56' rx='11' ry='4.6' transform='rotate(124 38 56)'/%3E%3Cellipse cx='60' cy='30' rx='11' ry='4.6' transform='rotate(146 60 30)'/%3E%3Cellipse cx='166' cy='196' rx='11' ry='4.6' transform='rotate(-38 166 196)'/%3E%3Cellipse cx='177' cy='164' rx='11' ry='4.6' transform='rotate(-64 177 164)'/%3E%3Cellipse cx='181' cy='128' rx='11' ry='4.6' transform='rotate(-82 181 128)'/%3E%3Cellipse cx='176' cy='92' rx='11' ry='4.6' transform='rotate(-102 176 92)'/%3E%3Cellipse cx='162' cy='56' rx='11' ry='4.6' transform='rotate(-124 162 56)'/%3E%3Cellipse cx='140' cy='30' rx='11' ry='4.6' transform='rotate(-146 140 30)'/%3E%3C/g%3E%3Cg fill='%23b8cba6' opacity='.85'%3E%3Cellipse cx='46' cy='222' rx='9' ry='3.8' transform='rotate(22 46 222)'/%3E%3Cellipse cx='154' cy='222' rx='9' ry='3.8' transform='rotate(-22 154 222)'/%3E%3Cellipse cx='84' cy='10' rx='9' ry='3.8' transform='rotate(168 84 10)'/%3E%3Cellipse cx='116' cy='10' rx='9' ry='3.8' transform='rotate(12 116 10)'/%3E%3C/g%3E%3Cg fill='%23e4aeb6'%3E%3Cg transform='translate(100 244)'%3E%3Ccircle cx='0' cy='-4' r='3.4'/%3E%3Ccircle cx='3.8' cy='1.4' r='3.4'/%3E%3Ccircle cx='-3.8' cy='1.4' r='3.4'/%3E%3Ccircle r='1.8' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(30 40) scale(.8)'%3E%3Ccircle cx='0' cy='-4' r='3.4'/%3E%3Ccircle cx='3.8' cy='1.4' r='3.4'/%3E%3Ccircle cx='-3.8' cy='1.4' r='3.4'/%3E%3Ccircle r='1.8' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(170 40) scale(.8)'%3E%3Ccircle cx='0' cy='-4' r='3.4'/%3E%3Ccircle cx='3.8' cy='1.4' r='3.4'/%3E%3Ccircle cx='-3.8' cy='1.4' r='3.4'/%3E%3Ccircle r='1.8' fill='%23c9a24a'/%3E%3C/g%3E%3C/g%3E%3Cg fill='%23d98a9e' opacity='.7'%3E%3Ccircle cx='52' cy='210' r='2.6'/%3E%3Ccircle cx='148' cy='210' r='2.6'/%3E%3Ccircle cx='26' cy='76' r='2.4'/%3E%3Ccircle cx='174' cy='76' r='2.4'/%3E%3C/g%3E%3C/svg%3E") center / 100% 100% no-repeat;
pointer-events: none;
z-index: 1;
}
/* 巴洛克金镜(凡尔赛宫廷椭圆镜:金渐变边 + 顶部卷草冠饰) */
.gf-baroque {
padding: 9px;
border: 3.5px solid transparent;
border-radius: 50%;
background:
linear-gradient(180deg, #fffdf8, #fbf4e6) padding-box,
linear-gradient(140deg, #ead7aa 0%, #b08d54 26%, #f6ecd0 48%, #a8834a 72%, #e2cb98 100%) border-box;
box-shadow:
0 0 0 1px rgba(140, 108, 60, 0.5),
0 0 0 6px rgba(246, 236, 208, 0.55),
0 0 0 7.5px rgba(176, 141, 84, 0.5),
0 16px 36px rgba(120, 90, 50, 0.22);
}
.gf-baroque > img {
border-radius: 50%;
}
.gf-baroque::before {
content: '';
position: absolute;
top: -21px;
left: 50%;
transform: translateX(-50%);
width: 84px;
height: 26px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 26'%3E%3Cg fill='none' stroke='%23b08d54' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M42 22c-6-2-8-7-5-11 2.6-3.4 7-2.4 7 1 0 2.6-2.6 3.4-4 1.6'/%3E%3Cpath d='M42 22c6-2 8-7 5-11-2.6-3.4-7-2.4-7 1 0 2.6 2.6 3.4 4 1.6'/%3E%3Cpath d='M30 21C20 19 14 13 16 7c4 2 8 6 10 11'/%3E%3Cpath d='M54 21c10-2 16-8 14-14-4 2-8 6-10 11'/%3E%3Cpath d='M14 8C10 6 6 6 3 9c3 2 6 2 9 1'/%3E%3Cpath d='M70 8c4-2 8-2 11 1-3 2-6 2-9 1'/%3E%3C/g%3E%3Ccircle cx='42' cy='6' r='2.6' fill='%23c9a24a'/%3E%3C/svg%3E") center / contain no-repeat;
pointer-events: none;
z-index: 1;
}
/* 折扇扇面底心圆点展开的扇形clip 扇角 ∩ mask 环带,附扇骨放射线)
环带 mask 内外缘放宽渐变即自带羽化clip 斜边与顶边由 ::after 贴边雾软化 */
.gf-fan {
padding: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: transparent;
clip-path: polygon(50% 126%, -14% -6%, 114% -6%);
-webkit-clip-path: polygon(50% 126%, -14% -6%, 114% -6%);
-webkit-mask: radial-gradient(146% 146% at 50% 126%, transparent 27%, #000 33%, #000 93%, transparent 98.5%);
mask: radial-gradient(146% 146% at 50% 126%, transparent 27%, #000 33%, #000 93%, transparent 98.5%);
}
/* 扇骨:自底心放射的细白线 + 弧沿描边 */
.gf-fan::before {
content: '';
position: absolute;
inset: 0;
background:
repeating-conic-gradient(from -57deg at 50% 126%, rgba(255, 253, 246, 0.6) 0 0.5deg, transparent 0.5deg 16.2deg),
radial-gradient(146% 146% at 50% 126%, transparent 91%, rgba(190, 155, 105, 0.45) 92.5%, rgba(190, 155, 105, 0.45) 95%, transparent 96.5%),
radial-gradient(146% 146% at 50% 126%, transparent 30%, rgba(190, 155, 105, 0.4) 31.5%, rgba(190, 155, 105, 0.4) 34%, transparent 35.5%);
pointer-events: none;
z-index: 1;
}
/* 扇面贴边雾:顶边下坠 + 左右斜边 + 内弧收口(覆盖通用椭圆晕,全部贴合扇形轮廓) */
.gf-fan::after {
background:
linear-gradient(180deg, color-mix(in srgb, var(--c-paper, #fffcf6) 78%, transparent), transparent 15%),
linear-gradient(68deg, color-mix(in srgb, var(--c-paper, #fffcf6) 72%, transparent), transparent 26%),
linear-gradient(292deg, color-mix(in srgb, var(--c-paper, #fffcf6) 72%, transparent), transparent 26%),
radial-gradient(146% 146% at 50% 126%, transparent 28%, color-mix(in srgb, var(--c-paper, #fffcf6) 55%, transparent) 34%, transparent 42%);
}
/* 月洞圆窗(正圆开窗 + 双环窗线 + 右上探入梅枝) */
.gf-moonwin {
padding: 9px;
border: 0;
border-radius: 50%;
box-shadow: none;
background: transparent;
}
.gf-moonwin > img {
border-radius: 50%;
}
.gf-moonwin::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 1.5px solid rgba(110, 120, 105, 0.6);
box-shadow:
inset 0 0 0 4px rgba(255, 255, 255, 0.6),
inset 0 0 0 5px rgba(110, 120, 105, 0.35);
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%236e7869' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M96 22C84 30 72 34 58 34'/%3E%3Cpath d='M84 27c-2-5-1-9 3-12'/%3E%3Cpath d='M70 32c-1-4 0-7 3-9'/%3E%3C/g%3E%3Cg fill='%23d9a0a5'%3E%3Ccircle cx='80' cy='24' r='2.6'/%3E%3Ccircle cx='68' cy='30' r='2.2'/%3E%3Ccircle cx='58' cy='33' r='1.8'/%3E%3C/g%3E%3Ccircle cx='74' cy='21' r='1.2' fill='%23c9a24a'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
pointer-events: none;
z-index: 1;
}
/* 蝴蝶结缎带框(顶部系结垂带的礼物相框) */
.gf-ribbon {
padding: 13px 11px 11px;
border: 1px solid rgba(217, 160, 165, 0.55);
border-radius: 12px;
background: linear-gradient(180deg, #fffafa, #fdf1f3);
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.7), 0 12px 28px rgba(190, 120, 130, 0.18);
}
.gf-ribbon > img {
border-radius: 7px;
}
.gf-ribbon::before {
content: '';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
width: 66px;
height: 44px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 66 44'%3E%3Cg fill='%23e093a3'%3E%3Cpath d='M33 12C26 4 14 2 8 8c-5 5-3 13 4 15 8 2 17-4 21-11z' fill='%23e8a8b2'/%3E%3Cpath d='M33 12c7-8 19-10 25-4 5 5 3 13-4 15-8 2-17-4-21-11z' fill='%23e8a8b2'/%3E%3Cpath d='M33 12C28 7 20 6 16 10c-3 3-2 8 2 9 6 2 12-2 15-7z' fill='%23f2c4cc' opacity='.9'/%3E%3Cpath d='M33 12c5-5 13-6 17-2 3 3 2 8-2 9-6 2-12-2-15-7z' fill='%23f2c4cc' opacity='.9'/%3E%3Cpath d='M27 20l-7 20 8-3 3 5 4-22z' fill='%23e8a8b2'/%3E%3Cpath d='M39 20l7 20-8-3-3 5-4-22z' fill='%23dfa0aa'/%3E%3Ccircle cx='33' cy='13' r='5' fill='%23d9899a'/%3E%3Ccircle cx='33' cy='13' r='2.2' fill='%23f2c4cc'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
filter: drop-shadow(0 3px 5px rgba(190, 120, 130, 0.3));
pointer-events: none;
z-index: 2;
}
/* 椭圆相框 */
.gf-oval {
padding: 7px;
@@ -305,6 +413,16 @@
clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
-webkit-clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}
/* 六向贴形边雾:每条边各一道向内渐隐,取代不贴形的通用椭圆晕 */
.gf-hexagon::after {
background:
linear-gradient(210deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%),
linear-gradient(270deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%),
linear-gradient(330deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%),
linear-gradient(30deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%),
linear-gradient(90deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%),
linear-gradient(150deg, color-mix(in srgb, var(--c-paper, #fffcf6) 62%, transparent), transparent 17%);
}
/* 八瓣花(多层 radial mask 合成花形,推荐 1:1 */
.gf-flower {
@@ -313,35 +431,35 @@
border-radius: 0;
box-shadow: none;
-webkit-mask:
radial-gradient(19% 19% at 50% 19%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 71.9% 28.1%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 81% 50%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 71.9% 71.9%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 50% 81%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 28.1% 71.9%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 19% 50%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 28.1% 28.1%, #000 98%, #0000 100%),
radial-gradient(34% 34% at 50% 50%, #000 98%, #0000 100%);
radial-gradient(19% 19% at 50% 19%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 71.9% 28.1%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 81% 50%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 71.9% 71.9%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 50% 81%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 28.1% 71.9%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 19% 50%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 28.1% 28.1%, #000 68%, #0000 99%),
radial-gradient(34% 34% at 50% 50%, #000 82%, #0000 100%);
mask:
radial-gradient(19% 19% at 50% 19%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 71.9% 28.1%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 81% 50%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 71.9% 71.9%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 50% 81%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 28.1% 71.9%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 19% 50%, #000 98%, #0000 100%),
radial-gradient(19% 19% at 28.1% 28.1%, #000 98%, #0000 100%),
radial-gradient(34% 34% at 50% 50%, #000 98%, #0000 100%);
radial-gradient(19% 19% at 50% 19%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 71.9% 28.1%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 81% 50%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 71.9% 71.9%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 50% 81%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 28.1% 71.9%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 19% 50%, #000 68%, #0000 99%),
radial-gradient(19% 19% at 28.1% 28.1%, #000 68%, #0000 99%),
radial-gradient(34% 34% at 50% 50%, #000 82%, #0000 100%);
}
/* 心形SVG mask 裁切,推荐 1:1 */
/* 心形SVG mask 裁切 + 内嵌高斯模糊羽化边,推荐 1:1 */
.gf-heart {
padding: 0;
border: 0;
border-radius: 0;
box-shadow: none;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cdefs%3E%3Cfilter id='f' x='-20%25' y='-20%25' width='140%25' height='140%25'%3E%3CfeGaussianBlur stdDeviation='12'/%3E%3C/filter%3E%3C/defs%3E%3Cpath filter='url(%23f)' fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cdefs%3E%3Cfilter id='f' x='-20%25' y='-20%25' width='140%25' height='140%25'%3E%3CfeGaussianBlur stdDeviation='12'/%3E%3C/filter%3E%3C/defs%3E%3Cpath filter='url(%23f)' fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%;
}
/* 扇贝花边(四边外凸圆弧波浪) */
@@ -541,12 +659,12 @@
box-shadow: none;
background: #fff;
-webkit-mask:
radial-gradient(var(--gf-c), #000 98%, #0000) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(calc(0.68 * var(--gf-c)), #000 98%, #0000) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(var(--gf-c), #000 62%, #0000 99%) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(calc(0.68 * var(--gf-c)), #000 62%, #0000 99%) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
linear-gradient(#000 0 0) 50% / calc(100% - 2.4 * var(--gf-c)) calc(100% - 2.4 * var(--gf-c)) no-repeat;
mask:
radial-gradient(var(--gf-c), #000 98%, #0000) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(calc(0.68 * var(--gf-c)), #000 98%, #0000) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(var(--gf-c), #000 62%, #0000 99%) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
radial-gradient(calc(0.68 * var(--gf-c)), #000 62%, #0000 99%) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round,
linear-gradient(#000 0 0) 50% / calc(100% - 2.4 * var(--gf-c)) calc(100% - 2.4 * var(--gf-c)) no-repeat;
}
/* 右上飘着的小云 */
@@ -692,9 +810,45 @@
z-index: 1;
}
/* 异形裁切类的投影mask 会裁掉 box-shadow由父级 drop-shadow 跟随轮廓补投影 */
/* ———— 云雾加持:拱门/椭圆/云朵边缘的飘动雾层 ————
覆盖上方通用渐隐层:顶部柔光 + 两团缓慢漂移的白雾 + 底部升雾 + 周缘主题色淡晕。
雾团层带显式 background-size靠 background-position 关键帧漂移。 */
.gf-arch::after,
.gf-wreath::after,
.gf-moonwin::after,
.gf-oval::after,
.gf-ovalwide::after,
.gf-cloud::after {
background:
radial-gradient(120% 60% at 50% 0%, rgba(255, 255, 255, 0.32), transparent 46%) 50% 0 / 100% 100%,
radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, 0.5), transparent 72%) 8% 96% / 64% 34%,
radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, 0.4), transparent 74%) 92% 99% / 56% 28%,
linear-gradient(0deg, color-mix(in srgb, var(--c-paper, #fffcf6) 58%, transparent), transparent 30%) 0 0 / 100% 100%,
radial-gradient(ellipse 72% 72% at 50% 46%, transparent 56%, color-mix(in srgb, var(--c-paper, #fffcf6) 46%, transparent) 100%) 0 0 / 100% 100%;
background-repeat: no-repeat;
animation: gfFogDrift 15s ease-in-out infinite alternate;
}
@keyframes gfFogDrift {
from {
background-position: 50% 0, 0% 96%, 100% 99%, 0 0, 0 0;
}
to {
background-position: 50% 0, 34% 90%, 64% 96%, 0 0, 0 0;
}
}
/* 晨雾框的底部双雾团:缓慢涌动 */
.gf-mist::before {
animation: gfMistSway 11s ease-in-out infinite alternate;
}
@keyframes gfMistSway {
from { transform: translateX(-7px) scaleY(1); }
to { transform: translateX(7px) scaleY(1.12); }
}
/* 异形裁切类的投影mask 会裁掉 box-shadow由父级 drop-shadow 跟随轮廓补投影
(投影刻意压淡:裁切形状靠边缘虚化融入页面,重阴影会显得生硬) */
.gf-shape-shadow {
filter: drop-shadow(0 10px 18px rgba(96, 72, 50, 0.22));
filter: drop-shadow(0 6px 12px rgba(96, 72, 50, 0.12));
}
/* 辅助:图片保持原始宽高比(单图铺满+保比例时使用,覆盖 gf-frame 的 height:100% */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
/**
* 日历Save The Date 屏)视觉模板
* - 配置值 'auto'(默认)= 跟随整站风格模板的推荐样式
* - 也可手动指定任意样式,与主题解耦
* 样式类挂在 .calendar-card 上cal-style-<key>CSS 见 theme-packs.css
*/
export const DEFAULT_CALENDAR_STYLE = 'auto'
export const CALENDAR_STYLES = [
{ key: 'classic', label: '香槟金卡', desc: '经典白卡金线(默认样式)' },
{ key: 'french', label: '法式蕾丝', desc: '扇贝蕾丝边 · 花体粉金' },
{ key: 'mono', label: '瑞士网格', desc: '粗黑标题 · 红黑双色' },
{ key: 'picnic', label: '野餐格纹', desc: '绿白格布 · 樱桃婚期' },
{ key: 'greenhouse', label: '玻璃温室', desc: '尖顶花房 · 玻璃格线' },
{ key: 'herbarium', label: '植物标本', desc: '压花蕨叶 · 胶带标签' },
{ key: 'fullmoon', label: '林间满月', desc: '夜空满月 · 桂枝剪影' },
{ key: 'gardensign', label: '花园吊牌', desc: '木牌麻绳 · 白漆手写' },
{ key: 'starry', label: '星夜弯月', desc: '闪烁星空 · 金月婚期' },
{ key: 'aquarelle', label: '水彩晕染', desc: '多彩晕斑 · 水痕婚期' },
{ key: 'hanami', label: '樱吹雪', desc: '飘落花瓣 · 樱花婚期' },
{ key: 'redgold', label: '中式红金', desc: '宫墙红底 · 通栏金幅' },
{ key: 'neochina', label: '月洞窗', desc: '圆窗题字 · 黛绿玉环' },
{ key: 'ink', label: '水墨素卡', desc: '文人左排 · 朱砂闲章' },
{ key: 'paper', label: '报刊粗格', desc: '表格网格 · 铅字排印' },
{ key: 'magazine', label: '杂志内页', desc: '巨字刊号 · 黑白对比' },
{ key: 'pixel', label: '像素方格', desc: '游戏窗口 · 标题栏' },
{ key: 'minimal', label: '极简数字', desc: '非对称 · 超大细字' },
{ key: 'journal', label: '手账贴纸', desc: '胶带贴纸 · 手绘圈' },
{ key: 'ticket', label: '复古票根', desc: '票号条码 · 撕票口' },
{ key: 'boarding', label: '登机牌', desc: '航司蓝条 · 舱位信息' },
{ key: 'film', label: '胶片场记', desc: '上下齿孔 · 对焦婚期' },
{ key: 'marquee', label: '影院灯牌', desc: '灯泡流转 · 金星婚期' },
{ key: 'y2k', label: '千禧玻璃', desc: '玻璃拟态 · 光泽扫过' },
{ key: 'gallery', label: '画廊白盒', desc: '画框标签 · 红点已定' },
]
/** 各风格模板的专属日历样式20 主题一对一ticket 保留为手动可选) */
const THEME_CALENDAR_MAP = {
champagne: 'classic',
french: 'french',
modern: 'mono',
forest: 'picnic',
night: 'starry',
watercolor: 'aquarelle',
sakura: 'hanami',
'guofeng-old': 'redgold',
'guofeng-new': 'neochina',
inkwash: 'ink',
newspaper: 'paper',
magazine: 'magazine',
pixel: 'pixel',
zen: 'minimal',
scrapbook: 'journal',
passport: 'boarding',
cinema: 'marquee',
kodak: 'film',
arthouse: 'gallery',
y2k: 'y2k',
}
export function resolveCalendarStyle(value, themeKey) {
if (value && value !== 'auto' && CALENDAR_STYLES.some((s) => s.key === value)) return value
return THEME_CALENDAR_MAP[themeKey] || 'classic'
}

View File

@@ -33,6 +33,16 @@ export const SITE_THEMES = [
desc: '深蓝烫金 · 暗色主题 · 星月点缀',
primary: '#c9a96a', line: '#8fa3c0', bg: '#101b2e',
},
{
key: 'watercolor', kind: 'pack', label: '水彩画境',
desc: '清透淡彩 · 晕染纹理 · 柔圆卡片',
primary: '#7f9bb3', line: '#a8c0d0', bg: '#f7fafc',
},
{
key: 'sakura', kind: 'pack', label: '樱花物语',
desc: '粉白渐彩 · 花瓣纷落 · 春日气息',
primary: '#d98a9e', line: '#e8b4c2', bg: '#fdf5f7',
},
// ———— 完整模板 ————
{
key: 'guofeng-new', kind: 'layout', label: '新中式',
@@ -61,8 +71,43 @@ export const SITE_THEMES = [
},
{
key: 'zen', kind: 'layout', label: '极简主义',
desc: '白留白 · 细线 · 轴线排版',
primary: '#2a2a28', line: '#8f8d88', bg: '#fafaf8',
desc: '白留白 · 细线 · 轴线排版',
primary: '#57503f', line: '#a89f8e', bg: '#faf7f2',
},
{
key: 'inkwash', kind: 'layout', label: '水墨山水',
desc: '宣纸墨韵 · 卷轴排版 · 朱砂印',
primary: '#3a3f3a', line: '#8a9088', bg: '#f6f6f2',
},
{
key: 'cinema', kind: 'layout', label: '复古电影',
desc: '上映海报 · 主演字幕 · 场记章节',
primary: '#b8322e', line: '#d4a24a', bg: '#f2e8d8',
},
{
key: 'kodak', kind: 'layout', label: '胶片日记',
desc: '胶卷齿孔 · 日期水印 · 柯达暖调',
primary: '#e0782e', line: '#3c3a34', bg: '#faf6ec',
},
{
key: 'arthouse', kind: 'layout', label: '文艺影调',
desc: '对称构图 · 分幕章节 · 衬线留白',
primary: '#4a4640', line: '#9a948a', bg: '#f4f1ea',
},
{
key: 'passport', kind: 'layout', label: '环球旅行',
desc: '护照登机牌 · 邮戳航线 · 出发到永远',
primary: '#2e4a68', line: '#b08a4a', bg: '#f2f0e8',
},
{
key: 'scrapbook', kind: 'layout', label: '手账拼贴',
desc: '胶带贴纸 · 牛皮纸 · 手写标签',
primary: '#c47a5a', line: '#8aa88a', bg: '#faf4e8',
},
{
key: 'y2k', kind: 'layout', label: '千禧蒸汽波',
desc: '复古窗口 · 渐变镜面字 · 星星光标',
primary: '#7a4ae0', line: '#4adce0', bg: '#ece8f8',
},
]
@@ -79,6 +124,7 @@ const LEGACY_THEME_MAP = {
ocean: 'night',
lilac: 'french',
sunset: 'champagne',
filmnoir: 'arthouse',
}
export function normalizeSiteTheme(key) {
@@ -102,13 +148,151 @@ export function isDarkTheme(key) {
return !!siteThemeMeta(key).dark
}
/* ============================================================
主题特效色板fx点赞爱心 / 高光 / 花瓣 / 漂浮爱心
未定义专属色板的主题回落到默认(香槟婚礼暖彩)
============================================================ */
const DEFAULT_FX = {
like: [
'#ff2442', '#ff2d55', '#ff4770', '#ff6b8a',
'#f472b6', '#ec4899', '#e879f9', '#c084fc',
'#a78bfa', '#818cf8', '#60a5fa', '#38bdf8',
'#2dd4bf', '#34d399', '#fbbf24', '#f59e0b',
'#fb923c', '#a88c6b', '#d4a574', '#e8b4b8',
'#f43f5e', '#fb7185', '#fda4af',
],
likeGlow: [
'#ff8da6', '#ffb0c4', '#f9a8d4', '#e9d5ff',
'#c4b5fd', '#93c5fd', '#6ee7b7', '#fde68a',
'#fdba74', '#f5d0b0', '#ffffff',
],
petal: ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6'],
bubble: ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2'],
}
const THEME_FX = {
night: {
like: ['#c9a96a', '#e8cf96', '#8fa3c0', '#d4b060', '#f0e0b0', '#a8b8d8'],
likeGlow: ['#f5e6b8', '#ffffff', '#c7d4ea'],
petal: ['#c9a96a', '#8fa3c0', '#e8cf96', '#5a7099', '#d8c8a0'],
bubble: ['#c9a96a', '#8fa3c0', '#ac9060'],
},
'guofeng-old': {
like: ['#c9414e', '#e05a66', '#c9a24a', '#e8c878', '#a8323e', '#f0a0a0'],
likeGlow: ['#f7e7c3', '#ffd9d0', '#ffe9a8'],
petal: ['#f2c4c4', '#e8a8a8', '#e8d0a0', '#f0dcc8'],
bubble: ['#c9a24a', '#d98a8a', '#b84a55'],
},
'guofeng-new': {
like: ['#486156', '#6f8f80', '#b5493f', '#b0885a', '#8fae9c'],
likeGlow: ['#d8e4dc', '#f0d8d0', '#e8d8c0'],
petal: ['#d8e4d8', '#e8ded0', '#f0d0c8', '#dce8dc'],
bubble: ['#8fae9c', '#c9a080', '#b5493f'],
},
magazine: {
like: ['#1f1c19', '#4a453e', '#8a8378', '#b8ac9c', '#d4c8b8'],
likeGlow: ['#ffffff', '#e8e0d4'],
petal: ['#e8e2d8', '#d8d2c6', '#c8c2b6'],
bubble: ['#8a8378', '#b0a898'],
},
newspaper: {
like: ['#2b2822', '#57524a', '#8a8478', '#6b675e', '#a8a294'],
likeGlow: ['#f6f3ea', '#dcd6c8'],
petal: ['#e8e2d2', '#dcd6c4', '#d0cab8'],
bubble: ['#8a8478', '#a8a294'],
},
pixel: {
like: ['#c94f7c', '#5f9ea0', '#e8a03c', '#7a4ae0', '#4aa34a', '#e0557a'],
likeGlow: ['#ffd9e8', '#c8f0f0', '#ffe9b8'],
petal: ['#f7c8da', '#c0e4e4', '#f5e0b8', '#e0d0f5'],
bubble: ['#c94f7c', '#5f9ea0', '#e8a03c'],
},
zen: {
like: ['#57503f', '#7a7260', '#a89f8e', '#c4bcac'],
likeGlow: ['#fffef9', '#ede8dc'],
petal: ['#ece7dc', '#e0dace', '#d6cfc0'],
bubble: ['#a89f8e', '#c0b8a8'],
},
watercolor: {
like: ['#7f9bb3', '#a8c0d0', '#c8a8c0', '#88b8a8', '#d0b8d8'],
likeGlow: ['#e8f0f8', '#ffffff'],
petal: ['#dce8f0', '#e8dce8', '#d8e8e0', '#f0e8f0'],
bubble: ['#a8c0d0', '#c8a8c0', '#88b8a8'],
},
sakura: {
like: ['#d98a9e', '#e8a8b8', '#f0c0cc', '#c67689', '#f5d5dc'],
likeGlow: ['#ffe4ec', '#ffffff'],
petal: ['#f8d5de', '#f5c8d4', '#fce4ea', '#f0b8c8'],
bubble: ['#e8b4c2', '#d98a9e', '#f0c0cc'],
},
inkwash: {
like: ['#3a3f3a', '#5c625c', '#8a9088', '#b5493f', '#b8bcb4'],
likeGlow: ['#eef0ec', '#ffffff'],
petal: ['#dfe2dc', '#d0d4cc', '#e8eae4'],
bubble: ['#8a9088', '#b5493f'],
},
cinema: {
like: ['#b8322e', '#d4552e', '#d4a24a', '#8c2320', '#e8c878'],
likeGlow: ['#ffe9c8', '#ffd9c0'],
petal: ['#f0dcc0', '#e8d0b0', '#f5e6cc'],
bubble: ['#d4a24a', '#b8322e'],
},
kodak: {
like: ['#e0782e', '#e89a3c', '#c85a28', '#f0b860', '#a84a20'],
likeGlow: ['#ffe0b8', '#ffefd0'],
petal: ['#f5dcbe', '#f0d0a8', '#f8e8cc'],
bubble: ['#e0782e', '#e89a3c'],
},
arthouse: {
like: ['#4a4640', '#6e6a62', '#9a948a', '#b8b2a6'],
likeGlow: ['#f4f1ea', '#ffffff'],
petal: ['#e6e2d8', '#dcd8cc', '#d0ccc0'],
bubble: ['#9a948a', '#b8b2a6'],
},
passport: {
like: ['#2e4a68', '#4a6a8c', '#b08a4a', '#6a88a8', '#d0a860'],
likeGlow: ['#e0ecf5', '#f0e4c8'],
petal: ['#d8e2ec', '#e8e0cc', '#ccd8e4'],
bubble: ['#4a6a8c', '#b08a4a'],
},
scrapbook: {
like: ['#c47a5a', '#8aa88a', '#d8a848', '#a86a8c', '#6a9ab0'],
likeGlow: ['#ffe8d0', '#e0f0e0'],
petal: ['#f0dcc8', '#d8e8d0', '#f5e8c8', '#e8d0dc'],
bubble: ['#c47a5a', '#8aa88a', '#d8a848'],
},
y2k: {
like: ['#7a4ae0', '#4adce0', '#e04ad0', '#8c8cff', '#4a90e0'],
likeGlow: ['#e0d8ff', '#c8f8ff', '#ffd8f8'],
petal: ['#e0d8f8', '#d0f0f8', '#f0d8f0'],
bubble: ['#7a4ae0', '#4adce0', '#e04ad0'],
},
}
/** 取主题特效色板(点赞/花瓣/漂浮爱心) */
export function themeFx(key) {
const k = normalizeSiteTheme(key)
return { ...DEFAULT_FX, ...(THEME_FX[k] || {}) }
}
/** 应用主题到整页html[data-theme]),前台按配置、后台跟随编辑值 */
const THEME_CACHE_KEY = 'wedding-site-theme'
export function applySiteTheme(key) {
const k = normalizeSiteTheme(key)
document.documentElement.dataset.theme = k
try { localStorage.setItem(THEME_CACHE_KEY, k) } catch { /* 隐私模式等场景忽略 */ }
if (k === 'pixel') ensurePixelCjkFont()
}
/**
* 上次访问缓存的主题:配置接口返回前先用它渲染 Loading 屏与首屏,
* 二次访问即可看到专属主题 Loading且避免主题闪变首次访问回退默认
*/
export function cachedSiteTheme() {
try { return normalizeSiteTheme(localStorage.getItem(THEME_CACHE_KEY) || DEFAULT_SITE_THEME) } catch { return DEFAULT_SITE_THEME }
}
/**
* 中文像素字形按需加载:
* 本地 @fontsource 包只含 latin 子集(英文/数字像素),中日韩全量字形

File diff suppressed because it is too large Load Diff

View File

@@ -29,16 +29,16 @@
<div class="boot-frame boot-frame-br" aria-hidden="true"></div>
<div class="boot-stage">
<span class="boot-kicker calligraphy">WEDDING INVITATION</span>
<span class="boot-kicker calligraphy">{{ bootKickerText }}</span>
<div class="boot-rule" aria-hidden="true"><span></span><i></i><span></span></div>
<div class="boot-seal-wrap">
<i class="boot-seal-ring boot-seal-ring-a" aria-hidden="true"></i>
<i class="boot-seal-ring boot-seal-ring-b" aria-hidden="true"></i>
<div class="boot-seal calligraphy"></div>
<div class="boot-seal calligraphy">{{ bootSealChar }}</div>
</div>
<div class="boot-rule boot-rule-flip" aria-hidden="true"><span></span><i></i><span></span></div>
<p class="boot-names calligraphy-strong">{{ data.groom }}<span>&amp;</span>{{ data.bride }}</p>
<p class="boot-hint calligraphy">请柬装载中</p>
<p class="boot-hint calligraphy">{{ bootHintText }}</p>
<div class="boot-dots" aria-hidden="true"><i></i><i></i><i></i></div>
</div>
</div>
@@ -82,6 +82,77 @@
<div class="em-go-date em-go-lunar">{{ data.lunar }} 恭候光临</div>
<div class="emblem-hint"><span class="dot"></span>轻触展开婚书</div>
</template>
<!-- 水墨山水卷轴帖 -->
<template v-else-if="siteThemeKey === 'inkwash'">
<i class="em-ink-rod" aria-hidden="true"></i>
<div class="em-ink-title"> </div>
<div class="em-ink-names">{{ data.groom }} {{ data.bride }}</div>
<div class="em-ink-date">{{ data.date }} · {{ data.lunar }}</div>
<span class="em-ink-seal" aria-hidden="true"></span>
<i class="em-ink-rod" aria-hidden="true"></i>
<div class="emblem-hint"><span class="dot"></span>轻触展卷</div>
</template>
<!-- 复古电影三段式上映海报横幅 / 主标 / 制作名单 -->
<template v-else-if="siteThemeKey === 'cinema'">
<div class="em-cn-top">
<span class="em-cn-badge">COMING SOON</span>
<span class="em-cn-studio">LOVE PICTURES 荣誉出品</span>
</div>
<div class="em-cn-mid">
<div class="em-cn-title">THE WEDDING</div>
<div class="em-cn-star">领衔主演</div>
<div class="em-cn-names">{{ data.groom }} · {{ data.bride }}</div>
<div class="em-cn-date">{{ data.date }} 隆重献映</div>
</div>
<div class="em-cn-bottom">
<div class="em-cn-credits">出品·缘分 / 监制·时光 / 特别出演·各位亲友</div>
<div class="emblem-hint"><span class="dot"></span>轻触入场</div>
</div>
</template>
<!-- 胶片日记胶卷盒 -->
<template v-else-if="siteThemeKey === 'kodak'">
<span class="em-kd-strip" aria-hidden="true"><i></i><i></i><i></i></span>
<div class="em-kd-title">WEDDING FILM</div>
<div class="em-kd-sub">COLOR · 35mm · 24 EXP</div>
<div class="em-kd-names">{{ data.groom }} {{ data.bride }}</div>
<div class="em-kd-date">{{ data.date }}</div>
<div class="emblem-hint"><span class="dot"></span>轻触冲印</div>
</template>
<!-- 文艺影调分幕页 -->
<template v-else-if="siteThemeKey === 'arthouse'">
<span class="em-ah-chapter"> CHAPITRE UN </span>
<div class="em-ah-names">{{ data.groom }} <em>et</em> {{ data.bride }}</div>
<i class="em-ah-line" aria-hidden="true"></i>
<div class="em-ah-date">{{ data.date }}</div>
<div class="emblem-hint"><span class="dot"></span>轻触开场</div>
</template>
<!-- 环球旅行护照封面 -->
<template v-else-if="siteThemeKey === 'passport'">
<span class="em-pp-top">LOVE REPUBLIC</span>
<span class="em-pp-crest" aria-hidden="true"></span>
<div class="em-pp-title">PASSPORT</div>
<div class="em-pp-sub">TO FOREVER</div>
<div class="em-pp-names">{{ data.groom }} / {{ data.bride }}</div>
<div class="emblem-hint"><span class="dot"></span>轻触通关</div>
</template>
<!-- 手账拼贴日记封面 -->
<template v-else-if="siteThemeKey === 'scrapbook'">
<span class="em-sb-tape" aria-hidden="true"></span>
<div class="em-sb-label">
<span class="em-sb-small">our story </span>
<div class="em-sb-names">{{ data.groom }} &amp; {{ data.bride }}</div>
<span class="em-sb-date">{{ data.date }}</span>
</div>
<div class="emblem-hint"><span class="dot"></span>轻触翻开手账</div>
</template>
<!-- 千禧蒸汽波开机窗口 -->
<template v-else-if="siteThemeKey === 'y2k'">
<div class="em-yk-bar"><span>welcome.exe</span><i aria-hidden="true"></i></div>
<div class="em-yk-names">{{ data.groom }} {{ data.bride }}</div>
<div class="em-yk-date">{{ data.date }} {{ data.lunar }}</div>
<div class="em-yk-load" aria-hidden="true"><i></i></div>
<div class="emblem-hint"><span class="dot"></span>click to start</div>
</template>
<!-- 杂志封面刊头式幕布 -->
<template v-else-if="siteThemeKey === 'magazine'">
<span class="em-mag-logo">VOWS</span>
@@ -142,6 +213,8 @@
:petal-enabled="data.petalEnabled"
:bubble-enabled="data.bubbleEnabled"
:active="effectsActive"
:petal-palette="themeFxObj.petal"
:heart-palette="themeFxObj.bubble"
/>
<DanmakuLayer
@@ -447,7 +520,7 @@
</div>
</div>
<LikeBurst ref="likeBurstRef" />
<LikeBurst ref="likeBurstRef" :colors="themeFxObj.like" :glow-colors="themeFxObj.likeGlow" />
<GiftEffect v-if="giftFxBackend === 'classic'" ref="giftEffectRef" @play-start="onGiftPlayStart" />
<GiftEffect3D v-else ref="giftEffectRef" @play-start="onGiftPlayStart" @fallback="onGiftFxFallback" />
<GiftFeed ref="giftFeedRef" />
@@ -569,7 +642,7 @@
</div>
<!-- 2. 邀请文案页风格包=经典竖排完整模板=各自专属排版 -->
<div class="page-section bg-[var(--c-bg)] items-center px-8 relative justify-center" :class="data.scrollMode === 'snap' ? 'snap-start' : ''" :style="formalPageHeightStyle">
<div class="page-section items-center px-8 relative justify-center" :class="data.scrollMode === 'snap' ? 'snap-start' : ''" :style="formalPageHeightStyle">
<template v-if="!isLayoutTheme">
<div class="absolute top-0 left-0 w-32 h-32 border-l-[0.5px] border-t-[0.5px] border-[var(--c-primary)]/30 m-6"></div>
<div class="absolute bottom-0 right-0 w-32 h-32 border-r-[0.5px] border-b-[0.5px] border-[var(--c-primary)]/30 m-6"></div>
@@ -596,6 +669,56 @@
<span class="fgo-foot"> · </span>
</div>
</div>
<!-- 水墨竖排 + 墨线 + 朱印 -->
<div v-else-if="siteThemeKey === 'inkwash'" v-reveal="'blur'" class="formal-ink">
<div class="fi-cols">
<span class="vertical-text fi-main">{{ data.formalText1 }}</span>
<span class="vertical-text fi-main fi-main-2">{{ data.formalText2 }}</span>
</div>
<span class="fi-seal" aria-hidden="true"></span>
</div>
<!-- 电影台词字幕卡 -->
<div v-else-if="siteThemeKey === 'cinema'" v-reveal="'up'" class="formal-cn">
<span class="fc-time">00:52:13,600 00:52:20,140</span>
<p class="fc-line">{{ data.formalText1 }}</p>
<p class="fc-line fc-line-2">{{ data.formalText2 }}</p>
<span class="fc-note"> 选自我们的婚礼未删减版</span>
</div>
<!-- 胶片相纸背书 -->
<div v-else-if="siteThemeKey === 'kodak'" v-reveal="'up'" class="formal-kd">
<span class="fk-head"> 相纸背面的话 </span>
<p class="fk-line">{{ data.formalText1 }}</p>
<p class="fk-line">{{ data.formalText2 }}</p>
<span class="fk-stamp">{{ data.date }}</span>
</div>
<!-- 文艺居中引言 + 罗马数字 -->
<div v-else-if="siteThemeKey === 'arthouse'" v-reveal="'up'" class="formal-ah">
<span class="fa-num"></span>
<p class="fa-line">{{ data.formalText1 }}</p>
<p class="fa-line fa-line-2">{{ data.formalText2 }}</p>
</div>
<!-- 旅行签证页 -->
<div v-else-if="siteThemeKey === 'passport'" v-reveal="'up'" class="formal-pp">
<span class="fp-visa" aria-hidden="true">VISA · APPROVED</span>
<p class="fp-line">{{ data.formalText1 }}</p>
<p class="fp-line">{{ data.formalText2 }}</p>
<span class="fp-foot"> DESTINATION: FOREVER</span>
</div>
<!-- 手账便签纸 -->
<div v-else-if="siteThemeKey === 'scrapbook'" v-reveal="'up'" class="formal-sb">
<span class="fs-clip" aria-hidden="true"></span>
<p class="fs-line">{{ data.formalText1 }}</p>
<p class="fs-line">{{ data.formalText2 }}</p>
</div>
<!-- Y2K对话框窗口 -->
<div v-else-if="siteThemeKey === 'y2k'" v-reveal="'up'" class="formal-yk">
<div class="fy-bar"><span>message.txt</span><i aria-hidden="true"></i></div>
<div class="fy-body">
<p class="fy-line">{{ data.formalText1 }}</p>
<p class="fy-line">{{ data.formalText2 }}</p>
<span class="fy-btn" aria-hidden="true">OK </span>
</div>
</div>
<!-- 杂志编辑部引言pull-quote -->
<div v-else-if="siteThemeKey === 'magazine'" v-reveal="'up'" class="formal-mag">
<span class="fm-quote" aria-hidden="true"></span>
@@ -643,7 +766,7 @@
class="page-section p-6 relative"
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
:style="pageHeightStyle(page)">
<div class="absolute inset-0 bg-[var(--c-card)]/40 -z-10"></div>
<div class="absolute inset-0 bg-[var(--c-card)]/20 -z-10"></div>
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[var(--c-primary)]/30 m-6 pointer-events-none z-0"></div>
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[var(--c-primary)]/30 m-6 pointer-events-none z-0"></div>
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
@@ -651,7 +774,7 @@
:page="page"
:nine-grid-animate="!!data.nineGridAnimate"
:layout-theme="isLayoutTheme"
:page-index="pIdx + 2"
:page-index="sectionIndex(pIdx + 2)"
@preview-image="openImagePreview"
@ready="onGalleryReady"
/>
@@ -660,10 +783,10 @@
<!-- 婚礼日历 -->
<div class="page-section wedding-calendar-page px-6 py-10 relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
<div v-reveal="'up'" class="calendar-card">
<div v-reveal="'up'" class="calendar-card" :class="calStyleClass">
<SectionHeading
v-if="isLayoutTheme"
:index="(data.photoPages?.length || 0) + 2"
:index="sectionIndex((data.photoPages?.length || 0) + 2)"
kicker="Save The Date"
:title="`${calendarTitle.year} · ${calendarTitle.monthText}`"
/>
@@ -730,7 +853,7 @@
<SectionHeading
v-if="isLayoutTheme"
v-reveal="'up'"
:index="(data.photoPages?.length || 0) + 3"
:index="sectionIndex((data.photoPages?.length || 0) + 3)"
kicker="Wedding Schedule"
title="婚礼流程"
/>
@@ -756,18 +879,11 @@
<!-- 尾页 -->
<div class="page-section ending-page px-6 py-8 flex flex-col items-center justify-center relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
<!-- 门框装饰仅风格包主题渲染;完整模板走各自的收尾排版 -->
<!-- 尾页装饰仅风格包主题渲染(幕布/花环/格窗/星环/缎带,后台可选);完整模板走各自的收尾排版 -->
<template v-if="!isLayoutTheme">
<div v-reveal="'blur'" class="ending-frame" aria-hidden="true">
<span class="ending-keystone">
<i class="ek-line"></i>
<span class="ek-mark">❀</span>
<i class="ek-line ek-line-r"></i>
</span>
<span class="ending-vine ending-vine-l"></span>
<span class="ending-vine ending-vine-r"></span>
<div v-reveal="'blur'" class="ending-decor" :class="`ending-style-${endingStyleKey}`" aria-hidden="true">
<i class="ed-a"></i><i class="ed-b"></i><i class="ed-c"></i><i class="ed-d"></i>
</div>
<div v-reveal="{ variant: 'scale', delay: 220 }" class="ending-corners" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
</template>
<div class="w-full max-w-[340px] flex flex-col items-center z-10 relative">
<SectionHeading v-if="isLayoutTheme" v-reveal="'down'" title="With Love" kicker="Ending" />
@@ -809,6 +925,13 @@
<span v-else-if="siteThemeKey === 'newspaper'" class="elf elf-news">── 本版终 · THE END ──</span>
<span v-else-if="siteThemeKey === 'magazine'" class="elf elf-mag">BACK COVER · VOWS MAGAZINE</span>
<span v-else-if="siteThemeKey === 'pixel'" class="elf elf-pixel">♥ GAME CLEAR · THANK YOU FOR PLAYING ♥</span>
<span v-else-if="siteThemeKey === 'inkwash'" class="elf elf-ink">水墨长卷 <b>囍</b> 落款于此</span>
<span v-else-if="siteThemeKey === 'cinema'" class="elf elf-cn">THE END · 全 剧 终</span>
<span v-else-if="siteThemeKey === 'kodak'" class="elf elf-kd">DEVELOPED WITH LOVE · 24 EXP</span>
<span v-else-if="siteThemeKey === 'arthouse'" class="elf elf-ah">fin — merci</span>
<span v-else-if="siteThemeKey === 'passport'" class="elf elf-pp">BON VOYAGE · 旅途愉快 ✈</span>
<span v-else-if="siteThemeKey === 'scrapbook'" class="elf elf-sb">❀ to be continued…</span>
<span v-else-if="siteThemeKey === 'y2k'" class="elf elf-yk">☆ see u at the wedding ☆</span>
<span v-else class="elf elf-zen">fin</span>
</div>
</div>
@@ -1029,7 +1152,8 @@ import {
import { sendVisitBeacon, getFingerprint } from '@/utils/visitBeacon'
import { optimizeImageUrl, isIOS } from '@/composables/useAndroidOptimize'
import { DANMAKU_COLORS, DANMAKU_GRADIENT_COLORS, DEFAULT_DANMAKU_COLOR, BLESSING_STYLES } from '@/utils/danmakuColors'
import { applySiteTheme, normalizeSiteTheme, themeLayoutKind } from '@/utils/themes'
import { applySiteTheme, normalizeSiteTheme, themeLayoutKind, themeFx, cachedSiteTheme } from '@/utils/themes'
import { resolveCalendarStyle } from '@/utils/calendarStyles'
import SectionHeading from '@/components/themes/SectionHeading.vue'
import CoverClassic from '@/components/themes/covers/CoverClassic.vue'
import CoverMagazine from '@/components/themes/covers/CoverMagazine.vue'
@@ -1038,6 +1162,13 @@ import CoverPixel from '@/components/themes/covers/CoverPixel.vue'
import CoverZen from '@/components/themes/covers/CoverZen.vue'
import CoverGuofengNew from '@/components/themes/covers/CoverGuofengNew.vue'
import CoverGuofengOld from '@/components/themes/covers/CoverGuofengOld.vue'
import CoverInkwash from '@/components/themes/covers/CoverInkwash.vue'
import CoverCinema from '@/components/themes/covers/CoverCinema.vue'
import CoverKodak from '@/components/themes/covers/CoverKodak.vue'
import CoverArthouse from '@/components/themes/covers/CoverArthouse.vue'
import CoverPassport from '@/components/themes/covers/CoverPassport.vue'
import CoverScrapbook from '@/components/themes/covers/CoverScrapbook.vue'
import CoverY2k from '@/components/themes/covers/CoverY2k.vue'
import { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc'
import CanvasEffects from '@/components/CanvasEffects.vue'
import DanmakuLayer from '@/components/DanmakuLayer.vue'
@@ -1055,7 +1186,10 @@ const DEFAULTS = {
musicCacheEnabled: true, musicCacheHours: 24,
danmakuEnabled: true, danmakuShowTime: true, nineGridAnimate: false,
giftEffectMode: 'classic', // 礼物特效方案classic 经典粒子 / three 立体炫效 / auto 按设备
siteTheme: 'champagne', // 整站主题champagne/crimson/sage/ocean/lilac/sunset
siteTheme: cachedSiteTheme(), // 整站风格模板初始取上次缓存Loading 屏即主题化)
endingFrameStyle: 'curtain', // 尾页装饰curtain 幕布 / moonwreath 花环 / frwindow 格窗 / starring 星环 / ribbons 缎带
sectionIndexEnabled: true, // 完整模板的章节编号01/02…是否显示
calendarStyle: 'auto', // 日历样式auto=跟随主题,或指定 calendarStyles.js 中的样式
freeScrollInterval: 50, // 自由滚动速度:滚动间隔 ms越小越快
pageSwitchDuration: 800, // 页面切换速度:翻页动画时长 ms
@@ -1225,6 +1359,26 @@ watch(() => data.value.siteTheme, (t) => applySiteTheme(t), { immediate: true })
/** 当前主题归一化后与布局类型pack=风格包沿用经典排版 / layout=完整模板) */
const siteThemeKey = computed(() => normalizeSiteTheme(data.value.siteTheme))
const isLayoutTheme = computed(() => themeLayoutKind(siteThemeKey.value) === 'layout')
/** Loading 屏专属文案:中心印记 / 加载提示 / 顶部小字(缺省回退经典请柬) */
const BOOT_THEME_TEXT = {
'guofeng-old': { hint: '花轿启程中', kicker: '大喜之日' },
'guofeng-new': { hint: '良辰将至', kicker: '喜结良缘' },
magazine: { seal: 'V', hint: 'COVER STORY LOADING', kicker: 'VOWS MAGAZINE' },
newspaper: { seal: '', hint: '头版排印中', kicker: 'THE WEDDING TIMES' },
pixel: { seal: '', hint: 'NOW LOADING', kicker: 'WEDDING QUEST' },
zen: { seal: '', hint: '静候片刻', kicker: 'SLOW WEDDING' },
inkwash: { seal: '', hint: '墨韵晕染中', kicker: '水墨婚书' },
cinema: { seal: '', hint: '影片装带中', kicker: 'NOW SHOWING' },
kodak: { seal: '35', hint: 'DEVELOPING', kicker: 'FILM MEMORIES' },
arthouse: { seal: 'A', hint: '布展中', kicker: 'ART HOUSE' },
passport: { seal: '', hint: '安检放行中', kicker: 'BOARDING PASS' },
scrapbook: { seal: '', hint: '手帐拼贴中', kicker: 'OUR SCRAPBOOK' },
y2k: { seal: '', hint: 'LOADING 98%', kicker: 'WEDDING.EXE' },
}
const bootThemeText = computed(() => BOOT_THEME_TEXT[siteThemeKey.value] || {})
const bootSealChar = computed(() => bootThemeText.value.seal || '')
const bootHintText = computed(() => bootThemeText.value.hint || '请柬装载中')
const bootKickerText = computed(() => bootThemeText.value.kicker || 'WEDDING INVITATION')
/** 封面组件:风格包共用经典封面,完整模板各用专属排版 */
const coverComponent = computed(() => ({
'guofeng-new': CoverGuofengNew,
@@ -1233,13 +1387,31 @@ const coverComponent = computed(() => ({
newspaper: CoverNewspaper,
pixel: CoverPixel,
zen: CoverZen,
inkwash: CoverInkwash,
cinema: CoverCinema,
kodak: CoverKodak,
arthouse: CoverArthouse,
passport: CoverPassport,
scrapbook: CoverScrapbook,
y2k: CoverY2k,
}[siteThemeKey.value] || CoverClassic))
/** 章节编号:可在后台关闭(传 0 时 SectionHeading 不渲染编号) */
const sectionIndex = (n) => (data.value.sectionIndexEnabled === false ? 0 : n)
/** 主题特效色板:点赞爱心 / 花瓣 / 漂浮爱心随模板换色 */
const themeFxObj = computed(() => themeFx(siteThemeKey.value))
/** 日历样式auto 跟随主题推荐,可在后台手动指定 */
const calStyleClass = computed(() => `cal-style-${resolveCalendarStyle(data.value.calendarStyle, siteThemeKey.value)}`)
/** 尾页装饰样式(风格包主题下生效,未知值回落幕布) */
const ENDING_FRAME_STYLES = ['curtain', 'moonwreath', 'frwindow', 'starring', 'ribbons']
const endingStyleKey = computed(() => (ENDING_FRAME_STYLES.includes(data.value.endingFrameStyle) ? data.value.endingFrameStyle : 'curtain'))
/** 幕布印章符号:各风格包的辨识元素(空 = 默认羽毛图标) */
const curtainSealChar = computed(() => ({
french: '',
forest: '',
night: '',
modern: '',
watercolor: '',
sakura: '',
}[siteThemeKey.value] || ''))
/** 邀请文案页首符号:随风格包变化 */
const formalMark = computed(() => ({
@@ -1247,6 +1419,8 @@ const formalMark = computed(() => ({
forest: '',
night: '',
modern: '',
watercolor: '',
sakura: '',
}[siteThemeKey.value] || ''))
const likeCount = ref(0)
const likePulse = ref(false)
@@ -3735,6 +3909,16 @@ watch(
radial-gradient(circle at 82% 82%, rgba(var(--c-primary-rgb),0.12), transparent 28%);
}
.calendar-card {
/* 成套配色变量:默认为香槟金卡观感,各 cal-style-* 皮肤整组覆盖(见 theme-packs.css */
--cal-title: #3f3428;
--cal-sub: #806f5c;
--cal-kicker: var(--c-primary);
--cal-week: var(--c-primary);
--cal-day: #4b443c;
--cal-sel-ink: #763342;
--cal-sel-bg: rgba(253, 251, 247, 0.9);
--cal-rule: rgba(var(--c-primary-rgb), 0.45);
--cal-title-rule: rgba(var(--c-primary-rgb), 0.8);
width: min(100%, 352px); margin: 0 auto; padding: 30px 24px 28px;
color: var(--c-ink-soft); text-align: center; position: relative;
background:
@@ -3749,19 +3933,19 @@ watch(
border: 1px solid rgba(var(--c-primary-rgb),0.16); pointer-events: none;
}
.calendar-kicker {
color: var(--c-primary); font-family: var(--font-display);
color: var(--cal-kicker); font-family: var(--font-display);
font-size: 10px; letter-spacing: 0.46em; padding-left: 0.46em; text-transform: uppercase;
}
.calendar-title {
display: flex; align-items: center; justify-content: center; gap: 14px;
margin-top: 12px; color: #3f3428; font-family: var(--font-display);
margin-top: 12px; color: var(--cal-title); font-family: var(--font-display);
font-size: 30px; line-height: 1.2; letter-spacing: 0.12em;
}
.calendar-title i {
width: 34px; height: 1px; background: linear-gradient(90deg, transparent, rgba(var(--c-primary-rgb),0.8), transparent);
width: 34px; height: 1px; background: linear-gradient(90deg, transparent, var(--cal-title-rule), transparent);
}
.calendar-date-line {
margin: 10px auto 22px; color: #806f5c; font-size: 11px;
margin: 10px auto 22px; color: var(--cal-sub); font-size: 11px;
letter-spacing: 0.16em; font-family: var(--font-serif);
}
.calendar-weekdays,
@@ -3769,12 +3953,12 @@ watch(
display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px;
}
.calendar-weekdays {
margin-bottom: 9px; color: var(--c-primary); font-size: 10px;
margin-bottom: 9px; color: var(--cal-week); font-size: 10px;
font-family: var(--font-kai); font-weight: 700;
}
.calendar-day {
position: relative; aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
color: #4b443c; font-family: var(--font-number); font-size: 15px;
color: var(--cal-day); font-family: var(--font-number); font-size: 15px;
}
.calendar-day.is-empty { visibility: hidden; }
.calendar-day-num {
@@ -3783,8 +3967,8 @@ watch(
.calendar-day.is-selected .calendar-day-num {
width: 24px; height: 24px; border-radius: 9999px;
display: flex; align-items: center; justify-content: center;
color: #763342; font-weight: 900; font-size: 16px;
background: rgba(253,251,247,0.9);
color: var(--cal-sel-ink); font-weight: 900; font-size: 16px;
background: var(--cal-sel-bg);
box-shadow: 0 0 0 1px rgba(151,64,82,0.12), 0 4px 10px rgba(91,47,36,0.1);
text-shadow: 0 1px 0 rgba(255,255,255,0.82);
}
@@ -3843,11 +4027,11 @@ watch(
}
.calendar-note {
display: flex; align-items: center; justify-content: center; gap: 10px;
margin-top: 24px; color: #806f5c; font-family: var(--font-kai);
margin-top: 24px; color: var(--cal-sub); font-family: var(--font-kai);
font-size: 13px; font-weight: 700; letter-spacing: 0.2em;
}
.calendar-note i {
width: 20px; height: 1px; background: rgba(var(--c-primary-rgb),0.45);
width: 20px; height: 1px; background: var(--cal-rule);
}
/* ---------- 揭幕前 Loading象牙金主题 ---------- */
@@ -3864,10 +4048,10 @@ watch(
justify-content: center;
overflow: hidden;
background:
radial-gradient(ellipse at 50% 42%, rgba(255, 252, 246, 0.96), transparent 55%),
radial-gradient(ellipse at 50% 42%, rgba(255, 252, 246, 0.72), transparent 55%),
radial-gradient(ellipse at 18% 78%, rgba(var(--c-line-rgb), 0.14), transparent 42%),
radial-gradient(ellipse at 82% 18%, rgba(var(--c-primary-rgb), 0.12), transparent 40%),
linear-gradient(165deg, var(--c-paper) 0%, #f7efe4 46%, #eddcc6 100%);
linear-gradient(165deg, var(--c-paper) 0%, var(--c-bg) 46%, var(--c-bg-deep) 100%);
transition: opacity 0.48s ease, visibility 0.48s ease, transform 0.48s ease;
}
.boot-loading.is-leaving {
@@ -4022,7 +4206,7 @@ watch(
color: var(--boot-gold);
background:
radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.65), transparent 46%),
linear-gradient(160deg, #fffaf3, #f3e6d6);
linear-gradient(160deg, var(--c-card), var(--c-bg-deep));
border: 1.5px solid rgba(var(--c-primary-rgb), 0.55);
box-shadow:
inset 0 0 0 5px rgba(var(--c-primary-rgb), 0.08),
@@ -4050,7 +4234,8 @@ watch(
font-size: 12px;
letter-spacing: 0.38em;
padding-left: 0.38em;
color: rgba(128, 106, 77, 0.78);
color: var(--boot-ink);
opacity: 0.78;
}
.boot-dots {
display: flex;
@@ -4510,76 +4695,209 @@ watch(
linear-gradient(180deg, rgba(253,251,247,0.92) 0%, rgba(247,246,242,0.98) 100%),
radial-gradient(circle at 50% 12%, rgba(232,201,210,0.2), transparent 34%);
}
.ending-frame {
position: absolute; top: 28px; right: 22px; bottom: 28px; left: 22px;
border: 1px solid rgba(var(--c-primary-rgb),0.26); border-radius: 160px 160px 24px 24px;
background: rgba(255,255,255,0.28); pointer-events: none;
/* 内侧淡金光晕,增强"门内"纵深感 */
box-shadow: inset 0 0 46px rgba(201, 169, 120, 0.13);
}
.ending-frame::before {
content: ''; position: absolute; inset: 9px;
border: 0.5px solid rgba(var(--c-primary-rgb),0.18); border-radius: 150px 150px 18px 18px;
}
/* 拱顶 keystone花饰圆章 + 两侧延伸细线,圆章用页面底色垫片盖住穿过的拱线 */
.ending-keystone {
position: absolute; top: 0; left: 50%;
transform: translate(-50%, -50%);
display: flex; align-items: center; gap: 7px;
color: #b08e63;
}
.ek-mark {
width: 34px; height: 34px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 15px; line-height: 1;
background: var(--c-paper);
border: 1px solid rgba(var(--c-primary-rgb), 0.38);
box-shadow: 0 0 0 4px var(--c-paper), inset 0 0 8px rgba(201, 169, 120, 0.2);
}
.ek-line {
width: 24px; height: 1px;
background: linear-gradient(90deg, transparent, rgba(var(--c-primary-rgb), 0.55));
}
.ek-line-r { transform: scaleX(-1); }
.reveal--in .ek-mark {
animation: keystonePop 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.35s both;
}
@keyframes keystonePop {
0% { opacity: 0; transform: scale(0.4) rotate(-90deg); }
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
/* 门框底部两侧对称藤蔓花枝 */
.ending-vine {
position: absolute; bottom: 10px;
width: 74px; height: 48px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 60'%3E%3Cg fill='none' stroke='%23a88c6b' stroke-width='1.2'%3E%3Cpath d='M4 56C24 50 44 40 60 26 70 17 78 10 92 6'/%3E%3Cpath d='M24 48q-3-9 4-14 4 7-4 14z' fill='%23c9a978' stroke='none' opacity='.6'/%3E%3Cpath d='M42 38q-1-9 7-12 2 8-7 12z' fill='%23c9a978' stroke='none' opacity='.55'/%3E%3Cpath d='M60 26q0-9 8-11 1 8-8 11z' fill='%23c9a978' stroke='none' opacity='.5'/%3E%3Cpath d='M76 15q1-8 9-9 0 8-9 9z' fill='%23c9a978' stroke='none' opacity='.45'/%3E%3Ccircle cx='92' cy='6' r='2' fill='%23d9a0a5' stroke='none' opacity='.65'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
opacity: 0.55;
}
.ending-vine-l { left: 10px; }
.ending-vine-r { right: 10px; transform: scaleX(-1); }
/* 四角雕花:细线 L 角 + 圆点收尾
容器必须自带 absolute 尺寸v-reveal 的 transform 会把它变成
absolute 子元素的 containing block没有 inset 时入场动画期间四角会错位 */
.ending-corners {
/* ---------- 尾页装饰(幕布/花环/格窗/星环/缎带,后台可选) ----------
容器自带 inset:0v-reveal 的 transform 会让它成为 absolute 子元素的
containing block无尺寸时入场动画期间装饰会错位 */
.ending-decor {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.ending-corners i {
position: absolute; width: 46px; height: 46px; border-color: rgba(var(--c-primary-rgb),0.3);
.ending-decor i {
position: absolute;
}
.ending-corners i::after {
content: ''; position: absolute; width: 4px; height: 4px;
border-radius: 50%; background: rgba(var(--c-primary-rgb), 0.42);
/* —— 舞台幕布:顶部帷幔垂坠 + 两侧束幕 + 金色束带 —— */
.ending-style-curtain {
--ed-cloth: color-mix(in srgb, var(--c-primary) 78%, #6e2836);
--ed-cloth-dark: color-mix(in srgb, var(--c-primary) 58%, #431722);
--ed-gold: rgba(216, 178, 110, 0.9);
}
.ending-style-curtain::before {
content: '';
position: absolute;
top: 0;
left: -6px;
right: -6px;
height: 84px;
background:
radial-gradient(43px 52px at 46px 2px, var(--ed-cloth) 68%, transparent 70%) 0 4px / 92px 80px repeat-x,
radial-gradient(43px 48px at 0 0, var(--ed-cloth-dark) 68%, transparent 70%) 46px 8px / 92px 74px repeat-x,
linear-gradient(180deg, var(--ed-cloth-dark), var(--ed-cloth) 82%) 0 0 / 100% 30px no-repeat;
filter: drop-shadow(0 6px 10px rgba(60, 20, 30, 0.3));
}
.ending-style-curtain::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, rgba(216, 178, 110, 0.4), var(--ed-gold) 30%, var(--ed-gold) 70%, rgba(216, 178, 110, 0.4));
}
.ending-style-curtain .ed-a,
.ending-style-curtain .ed-b {
top: -8px;
bottom: -8px;
width: 74px;
background:
repeating-linear-gradient(90deg, var(--ed-cloth) 0 9px, var(--ed-cloth-dark) 9px 17px, var(--ed-cloth) 17px 26px);
filter: drop-shadow(0 0 14px rgba(60, 20, 30, 0.35));
}
.ending-style-curtain .ed-a {
left: -10px;
clip-path: polygon(0 0, 100% 0, 56% 40%, 42% 50%, 78% 62%, 100% 100%, 0 100%);
}
.ending-style-curtain .ed-b {
right: -10px;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 22% 62%, 58% 50%, 44% 40%);
}
.ending-style-curtain .ed-c,
.ending-style-curtain .ed-d {
top: 47%;
width: 34px;
height: 13px;
border-radius: 8px;
background: linear-gradient(180deg, #ecd2a0, #c9a054 70%);
box-shadow: 0 2px 5px rgba(90, 60, 20, 0.35);
}
.ending-style-curtain .ed-c {
left: 2px;
transform: rotate(9deg);
}
.ending-style-curtain .ed-d {
right: 2px;
transform: rotate(-9deg);
}
/* —— 满月花环:正圆双枝花环环抱内容 —— */
.ending-style-moonwreath::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: min(92vw, 400px);
height: min(92vw, 400px);
transform: translate(-50%, -50%);
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23a8b894' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M100 190a90 90 0 0 1-88-72'/%3E%3Cpath d='M100 190a90 90 0 0 0 88-72'/%3E%3Cpath d='M12 82A90 90 0 0 1 96 10'/%3E%3Cpath d='M188 82A90 90 0 0 0 104 10'/%3E%3C/g%3E%3Cg fill='%23b0c29a'%3E%3Cellipse cx='22' cy='140' rx='9' ry='3.8' transform='rotate(58 22 140)'/%3E%3Cellipse cx='12' cy='108' rx='9' ry='3.8' transform='rotate(84 12 108)'/%3E%3Cellipse cx='14' cy='72' rx='9' ry='3.8' transform='rotate(108 14 72)'/%3E%3Cellipse cx='30' cy='40' rx='9' ry='3.8' transform='rotate(134 30 40)'/%3E%3Cellipse cx='58' cy='18' rx='9' ry='3.8' transform='rotate(156 58 18)'/%3E%3Cellipse cx='178' cy='140' rx='9' ry='3.8' transform='rotate(-58 178 140)'/%3E%3Cellipse cx='188' cy='108' rx='9' ry='3.8' transform='rotate(-84 188 108)'/%3E%3Cellipse cx='186' cy='72' rx='9' ry='3.8' transform='rotate(-108 186 72)'/%3E%3Cellipse cx='170' cy='40' rx='9' ry='3.8' transform='rotate(-134 170 40)'/%3E%3Cellipse cx='142' cy='18' rx='9' ry='3.8' transform='rotate(-156 142 18)'/%3E%3Cellipse cx='58' cy='182' rx='8' ry='3.4' transform='rotate(22 58 182)'/%3E%3Cellipse cx='142' cy='182' rx='8' ry='3.4' transform='rotate(-22 142 182)'/%3E%3C/g%3E%3Cg fill='%23e4aeb6'%3E%3Cg transform='translate(100 12)'%3E%3Ccircle cy='-4' r='3.2'/%3E%3Ccircle cx='3.6' cy='1.2' r='3.2'/%3E%3Ccircle cx='-3.6' cy='1.2' r='3.2'/%3E%3Ccircle r='1.7' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(100 188)'%3E%3Ccircle cy='-4' r='3.2'/%3E%3Ccircle cx='3.6' cy='1.2' r='3.2'/%3E%3Ccircle cx='-3.6' cy='1.2' r='3.2'/%3E%3Ccircle r='1.7' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(14 96) scale(.82)'%3E%3Ccircle cy='-4' r='3.2'/%3E%3Ccircle cx='3.6' cy='1.2' r='3.2'/%3E%3Ccircle cx='-3.6' cy='1.2' r='3.2'/%3E%3Ccircle r='1.7' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(186 96) scale(.82)'%3E%3Ccircle cy='-4' r='3.2'/%3E%3Ccircle cx='3.6' cy='1.2' r='3.2'/%3E%3Ccircle cx='-3.6' cy='1.2' r='3.2'/%3E%3Ccircle r='1.7' fill='%23c9a24a'/%3E%3C/g%3E%3C/g%3E%3Cg fill='%23d9a0a5' opacity='.6'%3E%3Ccircle cx='40' cy='166' r='2.2'/%3E%3Ccircle cx='160' cy='166' r='2.2'/%3E%3Ccircle cx='40' cy='30' r='2.2'/%3E%3Ccircle cx='160' cy='30' r='2.2'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
opacity: 0.85;
}
.ending-style-moonwreath::after {
content: '';
position: absolute;
left: 50%;
bottom: 6%;
width: 60%;
height: 26px;
transform: translateX(-50%);
background: radial-gradient(50% 100% at 50% 50%, rgba(168, 184, 148, 0.2), transparent 75%);
}
/* —— 法式格窗:细格线落地窗 + 窗台横板 —— */
.ending-style-frwindow::before {
content: '';
position: absolute;
inset: 26px 20px 44px;
border: 2px solid rgba(var(--c-primary-rgb), 0.4);
border-radius: 3px;
background:
linear-gradient(rgba(var(--c-primary-rgb), 0.35), rgba(var(--c-primary-rgb), 0.35)) 50% 0 / 2.5px 100% no-repeat,
repeating-linear-gradient(0deg, transparent 0 calc(25% - 1px), rgba(var(--c-primary-rgb), 0.24) calc(25% - 1px) 25%),
repeating-linear-gradient(90deg, transparent 0 calc(16.666% - 1px), rgba(var(--c-primary-rgb), 0.16) calc(16.666% - 1px) 16.666%),
radial-gradient(120% 60% at 50% 0%, rgba(255, 255, 255, 0.35), transparent 50%);
box-shadow: inset 0 0 40px rgba(255, 255, 255, 0.35), 0 14px 30px rgba(var(--c-primary-rgb), 0.12);
}
.ending-style-frwindow::after {
content: '';
position: absolute;
left: 12px;
right: 12px;
bottom: 32px;
height: 7px;
border-radius: 3px;
background: linear-gradient(180deg, rgba(var(--c-primary-rgb), 0.4), rgba(var(--c-primary-rgb), 0.26));
box-shadow: 0 5px 8px rgba(var(--c-primary-rgb), 0.16);
}
.ending-style-frwindow .ed-a {
left: 50%;
top: 49%;
width: 5px;
height: 22px;
margin-left: 5px;
border-radius: 4px;
background: rgba(var(--c-primary-rgb), 0.5);
}
/* —— 星光圆环:光点环缓旋 + 四角光斑 —— */
.ending-style-starring::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(34% 26% at 6% 4%, rgba(232, 201, 135, 0.22), transparent 70%),
radial-gradient(30% 24% at 96% 8%, rgba(232, 201, 135, 0.16), transparent 70%),
radial-gradient(32% 24% at 4% 94%, rgba(232, 201, 135, 0.16), transparent 70%),
radial-gradient(36% 28% at 96% 96%, rgba(232, 201, 135, 0.22), transparent 70%);
}
.ending-style-starring .ed-a {
left: 50%;
top: 50%;
width: min(88vw, 384px);
height: min(88vw, 384px);
transform: translate(-50%, -50%);
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='100' cy='100' r='92' fill='none' stroke='%23d9c288' stroke-width='2.6' stroke-dasharray='0.1 15.5' stroke-linecap='round'/%3E%3Ccircle cx='100' cy='100' r='80' fill='none' stroke='%23d9c288' stroke-width='1.4' stroke-dasharray='0.1 10.4' stroke-linecap='round' opacity='.55'/%3E%3Cg fill='%23d9c288'%3E%3Cpath d='M100 2l1.8 4.4L106 8l-4.2 1.6L100 14l-1.8-4.4L94 8l4.2-1.6z'/%3E%3Cpath d='M100 186l1.8 4.4 4.2 1.6-4.2 1.6-1.8 4.4-1.8-4.4-4.2-1.6 4.2-1.6z'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
animation: edRingSpin 70s linear infinite;
filter: drop-shadow(0 0 10px rgba(217, 194, 136, 0.4));
}
@keyframes edRingSpin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.ending-style-starring::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 60%;
height: 46%;
transform: translate(-50%, -50%);
background: radial-gradient(50% 50% at 50% 50%, rgba(255, 250, 236, 0.3), transparent 72%);
}
/* —— 缎带环绕:上下飘带 + 对角蝴蝶结 —— */
.ending-style-ribbons::before,
.ending-style-ribbons::after {
content: '';
position: absolute;
left: -12px;
right: -12px;
height: 46px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 46' preserveAspectRatio='none'%3E%3Cpath d='M-4 30C56 8 116 8 178 24s124 18 246-8' fill='none' stroke='%23e8a8b2' stroke-width='7' stroke-linecap='round' opacity='.8'/%3E%3Cpath d='M-4 34C56 12 116 12 178 28s124 18 246-8' fill='none' stroke='%23d9c288' stroke-width='2.4' stroke-linecap='round' opacity='.75'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.ending-style-ribbons::before {
top: 22px;
}
.ending-style-ribbons::after {
bottom: 22px;
transform: scaleX(-1) scaleY(-1);
}
.ending-style-ribbons .ed-a,
.ending-style-ribbons .ed-b {
width: 56px;
height: 38px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 66 44'%3E%3Cg fill='%23e093a3'%3E%3Cpath d='M33 12C26 4 14 2 8 8c-5 5-3 13 4 15 8 2 17-4 21-11z' fill='%23e8a8b2'/%3E%3Cpath d='M33 12c7-8 19-10 25-4 5 5 3 13-4 15-8 2-17-4-21-11z' fill='%23e8a8b2'/%3E%3Cpath d='M27 20l-7 20 8-3 3 5 4-22z' fill='%23e8a8b2'/%3E%3Cpath d='M39 20l7 20-8-3-3 5-4-22z' fill='%23dfa0aa'/%3E%3Ccircle cx='33' cy='13' r='5' fill='%23d9899a'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
filter: drop-shadow(0 3px 5px rgba(190, 120, 130, 0.35));
}
.ending-style-ribbons .ed-a {
top: 40px;
left: 16px;
transform: rotate(-14deg);
}
.ending-style-ribbons .ed-b {
bottom: 40px;
right: 16px;
transform: rotate(168deg);
}
.ending-corners i:nth-child(1) { top: 44px; left: 38px; border-top: 1px solid; border-left: 1px solid; }
.ending-corners i:nth-child(1)::after { top: -2.5px; left: -2.5px; }
.ending-corners i:nth-child(2) { top: 44px; right: 38px; border-top: 1px solid; border-right: 1px solid; }
.ending-corners i:nth-child(2)::after { top: -2.5px; right: -2.5px; }
.ending-corners i:nth-child(3) { left: 38px; bottom: 44px; border-bottom: 1px solid; border-left: 1px solid; }
.ending-corners i:nth-child(3)::after { bottom: -2.5px; left: -2.5px; }
.ending-corners i:nth-child(4) { right: 38px; bottom: 44px; border-right: 1px solid; border-bottom: 1px solid; }
.ending-corners i:nth-child(4)::after { bottom: -2.5px; right: -2.5px; }
.ending-kicker {
display: flex; flex-direction: column; align-items: center; gap: 10px;
margin-bottom: 18px; color: var(--c-primary); font-size: 10px; letter-spacing: 0.45em;