diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 25a4c92..cd99a6c 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -47,6 +47,10 @@ + + +
前台婚礼日历会展示该日期所在月份,并用爱心圈出这一天。
+
@@ -100,6 +104,12 @@
多首背景音乐的上传、试听与「设为播放」请在 背景音乐 标签页管理。
+ + + {{ opt.label }} + +
点击开启请柬时的退场动画,可按婚礼调性选择更轻盈或更有仪式感的方式。
+
整页 / 分屏吸附滚动 @@ -410,11 +420,13 @@ function defaultConfig() { groom: '李明', bride: '王华', date: '2026-05-20', lunar: '农历四月初四 星期三', hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅', formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称', formalPageHeight: '100vh', + calendarDate: '2026-09-04', heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80', endImg: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=800&q=80', musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }], activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true, + introExitEffect: 'wind', freeScrollInterval: 50, pageSwitchDuration: 800, firstScreenDuration: 4500, photoPages: [ { type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' }, @@ -456,6 +468,12 @@ const HEIGHT_PRESETS = [ { label: '自适应(内容高度)', value: 'auto' }, { label: '自定义', value: '__custom__' }, ] +const INTRO_EXIT_EFFECTS = [ + { label: '风吹纸页', value: 'wind' }, + { label: '翻书开启', value: 'book' }, + { label: '上提淡出', value: 'lift' }, + { label: '双门揭幕', value: 'doors' }, +] function heightSelectValue(page) { return heightFieldSelectValue(page.height) } @@ -688,6 +706,8 @@ onMounted(async () => { loaded.endImg = loaded.endImg || loaded.heroImg || cfg.endImg Object.assign(cfg, loaded) cfg.formalPageHeight = cfg.formalPageHeight || '100vh' + cfg.calendarDate = cfg.calendarDate || '2026-09-04' + cfg.introExitEffect = cfg.introExitEffect || 'wind' cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape) setActivePhotoIndex(currentPhotoIndex.value) // 数值型参数兜底(兼容旧配置) diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 9d6dff8..03caf16 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -2,7 +2,7 @@
-
+
@@ -129,6 +129,43 @@
+ +
+
+
Save The Date
+
+ {{ calendarTitle.year }} + + {{ calendarTitle.monthText }} +
+ +
+ {{ w }} +
+
+ + {{ day }} + + +
+
+ 良辰已定 + + 敬候光临 +
+
+
+
@@ -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); } +}