@@ -251,6 +288,8 @@ const DEFAULTS = {
pageSwitchDuration: 800, // 页面切换速度:翻页动画时长 ms
firstScreenDuration: 4500, // 首屏停留时间:首屏效果结束后再开始自动滚动
formalPageHeight: '100vh',
+ calendarDate: '2026-09-04',
+ introExitEffect: 'wind',
}
const data = ref({
@@ -295,6 +334,42 @@ const firstScreenDuration = computed(() => {
const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` }))
const endImage = computed(() => data.value.endImg || data.value.heroImg)
+const introExitClass = computed(() => {
+ const effect = ['wind', 'book', 'lift', 'doors'].includes(data.value.introExitEffect) ? data.value.introExitEffect : 'wind'
+ return `intro-exit-${effect}`
+})
+const introExitDuration = computed(() => ({ wind: 1280, book: 1220, lift: 980, doors: 1180 }[data.value.introExitEffect] || 1280))
+
+const calendarWeekdays = ['一', '二', '三', '四', '五', '六', '日']
+const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
+const parsedCalendarDate = computed(() => {
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(data.value.calendarDate || '')
+ const fallback = { year: 2026, month: 9, day: 4 }
+ if (!m) return fallback
+ const year = Number(m[1])
+ const month = Math.min(12, Math.max(1, Number(m[2])))
+ const last = new Date(year, month, 0).getDate()
+ const day = Math.min(last, Math.max(1, Number(m[3])))
+ return { year, month, day }
+})
+const weddingDay = computed(() => parsedCalendarDate.value.day)
+const calendarTitle = computed(() => ({
+ year: parsedCalendarDate.value.year,
+ monthText: monthNames[parsedCalendarDate.value.month - 1],
+}))
+const weddingDateText = computed(() => {
+ const d = parsedCalendarDate.value
+ return `${d.year}年${d.month}月${d.day}日`
+})
+const calendarDays = computed(() => {
+ const { year, month } = parsedCalendarDate.value
+ const firstDay = new Date(year, month - 1, 1).getDay()
+ const leading = (firstDay + 6) % 7
+ const days = new Date(year, month, 0).getDate()
+ const cells = [...Array.from({ length: leading }, () => null), ...Array.from({ length: days }, (_, i) => i + 1)]
+ const trailing = (7 - (cells.length % 7)) % 7
+ return [...cells, ...Array.from({ length: trailing }, () => null)]
+})
// 页面高度:支持 100vh / 80vh / 60vh / auto / 自定义 vh
const sectionHeightStyle = (height) => {
@@ -414,13 +489,6 @@ const completeIntro = () => {
startAutoScroll()
}
-const onIntroTransitionEnd = (e) => {
- if (!introOpening.value) return
- if (!e.target?.classList?.contains('curtain-door-right')) return
- if (e.propertyName !== 'transform') return
- completeIntro()
-}
-
const toggleAutoScroll = () => {
isAutoScroll.value = !isAutoScroll.value
const el = scrollContainerRef.value
@@ -433,7 +501,7 @@ const openInvitation = () => {
if (introOpening.value) return
clearTimeout(introTimer)
introOpening.value = true
- introTimer = setTimeout(() => completeIntro(), 1800)
+ introTimer = setTimeout(() => completeIntro(), introExitDuration.value)
// 点击揭幕是明确的用户手势:直接尝试播放背景音乐(最可靠的自动播放路径)
if (data.value.musicList.length) audio.play()
}
@@ -503,6 +571,8 @@ onMounted(async () => {
? Number(loaded.firstScreenDuration)
: DEFAULTS.firstScreenDuration
loaded.formalPageHeight = loaded.formalPageHeight || DEFAULTS.formalPageHeight
+ loaded.calendarDate = loaded.calendarDate || DEFAULTS.calendarDate
+ loaded.introExitEffect = loaded.introExitEffect || DEFAULTS.introExitEffect
data.value = { ...data.value, ...DEFAULTS, ...loaded }
}
} catch (e) { /* 使用默认配置 */ }
@@ -589,6 +659,96 @@ onUnmounted(() => {
font-family: var(--font-serif);
}
+/* ---------- 婚礼日历 ---------- */
+.wedding-calendar-page {
+ min-height: 76vh;
+ background:
+ linear-gradient(180deg, rgba(253,251,247,0.72), rgba(247,246,242,0.94)),
+ radial-gradient(circle at 18% 12%, rgba(232,201,210,0.18), transparent 30%),
+ radial-gradient(circle at 82% 82%, rgba(168,140,107,0.12), transparent 28%);
+}
+.calendar-card {
+ width: min(100%, 352px); margin: 0 auto; padding: 30px 24px 28px;
+ color: #4a3f30; text-align: center; position: relative;
+ background:
+ linear-gradient(135deg, rgba(255,255,255,0.66), rgba(255,255,255,0.28)),
+ repeating-linear-gradient(0deg, rgba(112,82,47,0.025) 0 1px, transparent 1px 8px),
+ rgba(253,251,247,0.72);
+ border: 1px solid rgba(168,140,107,0.24);
+ box-shadow: 0 18px 50px rgba(95,70,39,0.13), inset 0 0 0 8px rgba(255,255,255,0.34);
+}
+.calendar-card::before {
+ content: ''; position: absolute; inset: 12px;
+ border: 1px solid rgba(168,140,107,0.16); pointer-events: none;
+}
+.calendar-kicker {
+ color: #a88c6b; 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);
+ font-size: 30px; line-height: 1.2; letter-spacing: 0.12em;
+}
+.calendar-title i {
+ width: 34px; height: 1px; background: linear-gradient(90deg, transparent, rgba(168,140,107,0.8), transparent);
+}
+.calendar-date-line {
+ margin: 10px auto 22px; color: #806f5c; font-size: 11px;
+ letter-spacing: 0.16em; font-family: var(--font-serif);
+}
+.calendar-weekdays,
+.calendar-grid {
+ display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px;
+}
+.calendar-weekdays {
+ margin-bottom: 9px; color: #a88c6b; 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;
+}
+.calendar-day.is-empty { visibility: hidden; }
+.calendar-day-num {
+ position: relative; z-index: 2;
+}
+.calendar-day.is-selected .calendar-day-num {
+ color: #8f4d5a; font-weight: 700; text-shadow: 0 1px 0 rgba(255,255,255,0.75);
+}
+.heart-ring {
+ position: absolute; inset: -9px; display: flex; align-items: center; justify-content: center;
+ z-index: 1; animation: heartBeat 1.65s ease-in-out infinite;
+ filter: drop-shadow(0 6px 10px rgba(151,76,91,0.18));
+}
+.heart-ring svg {
+ width: 56px; height: 50px; overflow: visible;
+}
+.heart-brush {
+ fill: rgba(182,106,120,0.06);
+ stroke: #a94f62;
+ stroke-width: 4.2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-dasharray: 176;
+ stroke-dashoffset: 176;
+ animation: brushHeartDraw 1.25s cubic-bezier(0.18, 0.78, 0.24, 1) forwards;
+}
+.heart-brush-shadow {
+ stroke: rgba(126,76,54,0.22);
+ stroke-width: 6;
+ transform: translate(1px, 1px) rotate(-2deg);
+ animation-delay: .06s;
+}
+.calendar-note {
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ margin-top: 24px; color: #806f5c; font-family: var(--font-kai);
+ font-size: 13px; font-weight: 700; letter-spacing: 0.2em;
+}
+.calendar-note i {
+ width: 20px; height: 1px; background: rgba(168,140,107,0.45);
+}
+
/* ---------- 入场揭幕(雅致象牙金) ---------- */
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: var(--font-sans); background: #fbf6ef; perspective: 1200px; }
.curtain-bg {
@@ -629,7 +789,45 @@ onUnmounted(() => {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 3; padding: 28px;
transition: opacity 0.8s ease, transform 0.9s ease; animation: emblemIn 1.1s ease both;
}
-.curtain-root.is-opening .curtain-emblem { opacity: 0; transform: scale(1.25); }
+.curtain-root.is-opening .curtain-emblem { opacity: 0; transform: scale(0.98); }
+.curtain-root.is-opening .curtain-veil { animation: veilFade 1.2s ease both; }
+.curtain-root.is-opening.intro-exit-wind .curtain-emblem {
+ opacity: 1; transform: none;
+}
+.curtain-root.is-opening.intro-exit-wind .curtain-door-left { transform: translateX(-82%) rotateY(-8deg); }
+.curtain-root.is-opening.intro-exit-wind .curtain-door-right { transform: translateX(82%) rotateY(8deg); }
+.curtain-root.is-opening.intro-exit-wind .emblem-inner { animation: inviteContentExit 520ms cubic-bezier(0.4, 0, 1, 1) 90ms forwards; }
+.curtain-root.is-opening.intro-exit-wind .invite-cover {
+ transform-origin: 72% 24%;
+ will-change: transform, opacity, filter;
+ animation: inviteWindExit 1180ms cubic-bezier(0.4, 0, 1, 1) forwards;
+}
+.curtain-root.is-opening.intro-exit-book .curtain-emblem {
+ opacity: 1; transform: none;
+}
+.curtain-root.is-opening.intro-exit-book .emblem-inner { animation: inviteContentExit 480ms cubic-bezier(0.4, 0, 1, 1) 70ms forwards; }
+.curtain-root.is-opening.intro-exit-book .invite-cover {
+ transform-style: preserve-3d;
+ transform-origin: 0% 50%;
+ will-change: transform, opacity, filter;
+ animation: inviteBookExit 1120ms cubic-bezier(0.4, 0, 1, 1) forwards;
+}
+.curtain-root.is-opening.intro-exit-lift .curtain-emblem {
+ opacity: 1; transform: none;
+}
+.curtain-root.is-opening.intro-exit-lift .emblem-inner { animation: inviteContentExit 420ms cubic-bezier(0.4, 0, 1, 1) 50ms forwards; }
+.curtain-root.is-opening.intro-exit-lift .invite-cover {
+ transform-origin: 50% 18%;
+ will-change: transform, opacity, filter;
+ animation: inviteLiftExit 900ms cubic-bezier(0.4, 0, 1, 1) forwards;
+}
+.curtain-root.is-opening.intro-exit-doors .curtain-emblem {
+ opacity: 1; transform: none;
+}
+.curtain-root.is-opening.intro-exit-doors .emblem-inner { animation: inviteContentExit 430ms cubic-bezier(0.4, 0, 1, 1) forwards; }
+.curtain-root.is-opening.intro-exit-doors .invite-cover {
+ animation: inviteDoorsExit 860ms cubic-bezier(0.4, 0, 1, 1) forwards;
+}
.invite-cover {
position: relative; width: min(82vw, 360px); min-height: min(72vh, 560px);
display: flex; align-items: center; justify-content: center; padding: 34px 24px;
@@ -863,4 +1061,48 @@ onUnmounted(() => {
@keyframes emblemIn { from { opacity: 0; transform: scale(0.85) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@keyframes hintPulse { 0%,100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.7); opacity: 1; } }
+@keyframes heartBeat {
+ 0%, 100% { transform: scale(1); opacity: .92; }
+ 18% { transform: scale(1.16); opacity: 1; }
+ 34% { transform: scale(.96); }
+ 52% { transform: scale(1.1); }
+}
+@keyframes brushHeartDraw {
+ 0% { stroke-dashoffset: 176; opacity: .34; }
+ 66% { stroke-dashoffset: 0; opacity: 1; }
+ 100% { stroke-dashoffset: 0; opacity: 1; }
+}
+@keyframes veilFade {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+@keyframes inviteContentExit {
+ 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
+ 100% { opacity: 0; transform: translateY(-7px) scale(.975); filter: blur(1px); }
+}
+@keyframes inviteWindExit {
+ 0% { opacity: 1; transform: translate3d(0, 0, 0) rotate(0deg) scale(1); filter: blur(0); }
+ 8% { opacity: 1; transform: translate3d(-7px, 5px, 0) rotate(-1.4deg) scale(.982); filter: blur(0); }
+ 28% { opacity: 1; transform: translate3d(20px, -18px, 0) rotate(4deg) scale(.985); filter: blur(0); }
+ 58% { opacity: .82; transform: translate3d(46vw, -18vh, 0) rotate(13deg) scale(.93); filter: blur(.3px); }
+ 100% { opacity: 0; transform: translate3d(96vw, -42vh, 0) rotate(24deg) scale(.82); filter: blur(2px); }
+}
+@keyframes inviteBookExit {
+ 0% { opacity: 1; transform: rotateY(0deg) translateX(0) translateZ(0) scale(1); filter: brightness(1); }
+ 9% { opacity: 1; transform: rotateY(0deg) translateX(7px) translateZ(0) scale(.985); filter: brightness(.98); }
+ 38% { opacity: 1; transform: rotateY(-42deg) translateX(-10px) translateZ(36px) scale(.975); filter: brightness(.94); }
+ 72% { opacity: .72; transform: rotateY(-88deg) translateX(-34px) translateZ(26px) scale(.95); filter: brightness(.9) blur(.4px); }
+ 100% { opacity: 0; transform: rotateY(-112deg) translateX(-70px) translateZ(0) scale(.9); filter: brightness(.85) blur(1.8px); }
+}
+@keyframes inviteLiftExit {
+ 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
+ 10% { opacity: 1; transform: translateY(8px) scale(.975, .988); filter: blur(0); }
+ 46% { opacity: .96; transform: translateY(-28px) scale(1.018, 1.03); filter: blur(0); }
+ 100% { opacity: 0; transform: translateY(-116px) scale(1.08); filter: blur(2.4px); }
+}
+@keyframes inviteDoorsExit {
+ 0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
+ 12% { opacity: 1; transform: translateY(4px) scale(.98); filter: blur(0); }
+ 100% { opacity: 0; transform: translateY(-12px) scale(.94); filter: blur(1.4px); }
+}