更新页面效果
This commit is contained in:
@@ -9,51 +9,52 @@
|
||||
|
||||
<!-- 单图(满幅带留白卡纸) -->
|
||||
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
|
||||
class="w-full max-w-[300px] mx-auto aspect-[4/5] bg-white p-2 shadow-sm border-[0.5px] border-[#a88c6b]/20 relative mt-6">
|
||||
<img :src="page.img1" class="w-full h-full object-cover" />
|
||||
class="w-full max-w-[300px] mx-auto aspect-[4/5] mt-6">
|
||||
<div :class="frameClass"><img :src="page.img1" /></div>
|
||||
</div>
|
||||
|
||||
<!-- 错落双图 -->
|
||||
<div v-else-if="page.type === 'overlap'" class="relative w-full h-[50vh] mt-4">
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }" class="absolute top-0 right-4 w-[70%] h-[65%] shadow-md bg-white p-1 z-10 border border-[#a88c6b]/10">
|
||||
<img :src="page.img1" class="w-full h-full object-cover" />
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }" class="absolute top-0 right-4 w-[70%] h-[65%] z-10" :class="frameClass">
|
||||
<img :src="page.img1" />
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-4 left-4 w-[60%] h-[60%] shadow-lg bg-white p-1 z-20 border border-[#a88c6b]/10">
|
||||
<img :src="page.img2" class="w-full h-full object-cover" />
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }" class="absolute bottom-4 left-4 w-[60%] h-[60%] z-20" :class="frameClass">
|
||||
<img :src="page.img2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一大五小 -->
|
||||
<div v-else-if="page.type === 'one-large-five-small'"
|
||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative overflow-hidden bg-[#Fdfbf7] shadow-sm">
|
||||
<img :src="page.images[0]" class="w-full h-full object-cover hover:scale-105 transition-transform duration-1000" />
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
||||
<img :src="page.images[0]" class="hover:scale-105 transition-transform duration-1000" />
|
||||
</div>
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative bg-[#Fdfbf7]">
|
||||
<img :src="page.images[i]" class="w-full h-full object-cover" />
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
||||
<img :src="page.images[i]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 九宫格 -->
|
||||
<div v-else-if="page.type === 'nine-grid'" class="grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 60 }"
|
||||
class="aspect-square bg-[#Fdfbf7] relative rounded-sm overflow-hidden">
|
||||
<img :src="img" class="w-full h-full object-cover hover:scale-105 transition-transform duration-700" />
|
||||
class="aspect-square relative" :class="frameClass">
|
||||
<img :src="img" class="hover:scale-105 transition-transform duration-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 瀑布流(错落多列) -->
|
||||
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }" class="g-masonry-item">
|
||||
<img :src="img" class="w-full block" />
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
||||
class="g-masonry-item" :class="frameClass">
|
||||
<img :src="img" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 轮播滑动 -->
|
||||
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
|
||||
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide">
|
||||
<img :src="img" class="w-full h-full object-cover" />
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
|
||||
<img :src="img" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张">‹</button>
|
||||
@@ -64,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍立得拼贴 -->
|
||||
<!-- 拍立得拼贴(自带边框,不受 borderStyle 影响) -->
|
||||
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
|
||||
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
|
||||
@@ -78,12 +79,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
})
|
||||
|
||||
// 边框样式:前台按 page.borderStyle 套用全局 gf-* 类
|
||||
const frameClass = computed(() => borderClass(props.page.borderStyle))
|
||||
|
||||
/* ---------- 轮播状态 ---------- */
|
||||
const index = ref(0)
|
||||
const trackRef = ref(null)
|
||||
@@ -117,8 +122,7 @@ function polaroidTransform(i) {
|
||||
|
||||
/* 瀑布流 */
|
||||
.g-masonry { column-count: 2; column-gap: 10px; }
|
||||
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; overflow: hidden; background: #Fdfbf7; box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
|
||||
.g-masonry-item img { display: block; }
|
||||
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; }
|
||||
|
||||
/* 轮播 */
|
||||
.g-carousel-track {
|
||||
|
||||
14
vite-tailwindcss/src/composables/borderStyles.js
Normal file
14
vite-tailwindcss/src/composables/borderStyles.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// 相册边框样式定义:供前台 PhotoGallery 渲染与后台 AdminEditor 配置/预览共用
|
||||
export const BORDER_STYLES = [
|
||||
{ value: 'none', label: '无边框' },
|
||||
{ value: 'thin', label: '细金线' },
|
||||
{ value: 'mat', label: '白卡纸' },
|
||||
{ value: 'rounded', label: '柔圆角' },
|
||||
{ value: 'double', label: '双线框' },
|
||||
{ value: 'gold', label: '古典金框' },
|
||||
]
|
||||
|
||||
export function borderClass(style) {
|
||||
const v = BORDER_STYLES.some((b) => b.value === style) ? style : 'mat'
|
||||
return 'gf-frame gf-' + v
|
||||
}
|
||||
@@ -62,6 +62,18 @@ export function useAudio() {
|
||||
}
|
||||
|
||||
const play = () => playUrl(activeUrl.value)
|
||||
|
||||
// 尝试自动播放,返回是否成功(被浏览器拦截则返回 false,由调用方决定是否弹授权框)
|
||||
const attemptAutoplay = async () => {
|
||||
if (!activeUrl.value) return false
|
||||
try {
|
||||
await playUrl(activeUrl.value)
|
||||
return isPlaying.value
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (audio) audio.pause()
|
||||
isPlaying.value = false
|
||||
@@ -98,5 +110,5 @@ export function useAudio() {
|
||||
}
|
||||
})
|
||||
|
||||
return { isPlaying, isReady, tracks, activeUrl, currentName, setTracks, setActive, play, pause, toggle, armAutoplay }
|
||||
return { isPlaying, isReady, tracks, activeUrl, currentName, setTracks, setActive, play, pause, toggle, attemptAutoplay, armAutoplay }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import './style.css'
|
||||
import './styles/animations.css'
|
||||
import './styles/frames.css'
|
||||
import App from './App.vue'
|
||||
import '@fortawesome/fontawesome-free/css/all.css' // 引入所有样式
|
||||
// 注册路由
|
||||
|
||||
@@ -25,36 +25,41 @@
|
||||
.reveal { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; }
|
||||
}
|
||||
|
||||
/* ---------- 飘落花瓣 ---------- */
|
||||
/* ---------- 飘落花瓣(优雅:柔和渐变 + 轻微摇曳) ---------- */
|
||||
@keyframes petalFall {
|
||||
0% { transform: translate3d(0, -12vh, 0) rotate(0deg); opacity: 0; }
|
||||
8% { opacity: 0.9; }
|
||||
90% { opacity: 0.85; }
|
||||
100% { transform: translate3d(8vw, 112vh, 0) rotate(420deg); opacity: 0; }
|
||||
10% { opacity: 0.85; }
|
||||
90% { opacity: 0.7; }
|
||||
100% { transform: translate3d(6vw, 112vh, 0) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
@keyframes petalDrift {
|
||||
0%, 100% { margin-left: -14px; }
|
||||
50% { margin-left: 14px; }
|
||||
}
|
||||
.petal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
animation: petalFall linear infinite;
|
||||
animation:
|
||||
petalFall var(--p-dur, 12s) linear infinite,
|
||||
petalDrift var(--p-sway, 6s) ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* ---------- 漂浮爱心 ---------- */
|
||||
/* ---------- 漂浮爱心(全屏自然上升,随机分布) ---------- */
|
||||
@keyframes heartFloat {
|
||||
0% { transform: translateY(110%) translateX(0) scale(0.8); opacity: 0; }
|
||||
10% { opacity: 0.85; }
|
||||
50% { transform: translateY(45%) translateX(14px) scale(1); }
|
||||
90% { opacity: 0.7; }
|
||||
100% { transform: translateY(-15%) translateX(-10px) scale(1.1); opacity: 0; }
|
||||
0% { transform: translateY(20px) translateX(0) scale(0.7); opacity: 0; }
|
||||
12% { opacity: 0.8; }
|
||||
85% { opacity: 0.6; }
|
||||
100% { transform: translateY(-118vh) translateX(24px) scale(1.1); opacity: 0; }
|
||||
}
|
||||
@keyframes heartSway {
|
||||
0% { margin-left: -18px; }
|
||||
100% { margin-left: 18px; }
|
||||
0%, 100% { margin-left: -16px; }
|
||||
50% { margin-left: 16px; }
|
||||
}
|
||||
.heart-bubble {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
will-change: transform, opacity;
|
||||
animation:
|
||||
heartFloat var(--float-duration, 18s) linear infinite,
|
||||
heartSway var(--sway-duration, 5s) ease-in-out infinite alternate;
|
||||
|
||||
71
vite-tailwindcss/src/styles/frames.css
Normal file
71
vite-tailwindcss/src/styles/frames.css
Normal file
@@ -0,0 +1,71 @@
|
||||
/* ============================================================
|
||||
相册边框样式(全局)
|
||||
- 前台 PhotoGallery 按 page.borderStyle 套用
|
||||
- 后台 AdminEditor 预览复用同一套类
|
||||
结构约定:<div class="gf-frame gf-xxx"><img/></div>
|
||||
gf-frame 负责定位与裁切,gf-xxx 负责边框/卡纸/投影
|
||||
============================================================ */
|
||||
.gf-frame {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gf-frame > img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 无边框 */
|
||||
.gf-none {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 细金线 */
|
||||
.gf-thin {
|
||||
padding: 0;
|
||||
border: 1px solid #cdb89c;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(120, 90, 60, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 白卡纸(经典裱框) */
|
||||
.gf-mat {
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 6px 20px rgba(120, 90, 60, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 柔圆角 */
|
||||
.gf-rounded {
|
||||
padding: 0;
|
||||
border: 1px solid rgba(168, 140, 107, 0.4);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 24px rgba(120, 90, 60, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 双线框 */
|
||||
.gf-double {
|
||||
padding: 6px;
|
||||
border: 3px double #b89a78;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 16px rgba(120, 90, 60, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 古典金框(外白 + 金线双层) */
|
||||
.gf-gold {
|
||||
padding: 7px;
|
||||
border: 2px solid #c9a978;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 4px #fff, 0 0 0 5px #e7d6bb, 0 8px 22px rgba(120, 90, 60, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
@@ -170,6 +170,21 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 items-start">
|
||||
<a-form-item label="边框样式">
|
||||
<a-select v-model:value="page.borderStyle">
|
||||
<a-select-option v-for="b in BORDER_STYLES" :key="b.value" :value="b.value">{{ b.label }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11px] text-[#a88c6b]">预览</span>
|
||||
<div class="w-[70px] h-[88px] shrink-0">
|
||||
<div :class="borderClass(page.borderStyle)" style="width:100%;height:100%">
|
||||
<img src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=200&q=80" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="小标题 (英文)">
|
||||
<a-input v-model:value="page.subtitle" />
|
||||
@@ -258,6 +273,7 @@ import { message } from 'ant-design-vue'
|
||||
import { getConfig, saveConfig, uploadImage, getRsvpList } from '@/api/wedding'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
|
||||
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
@@ -280,10 +296,10 @@ function defaultConfig() {
|
||||
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
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' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6), height: '100vh' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9), height: '100vh' },
|
||||
{ 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' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: fill(6), height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: fill(9), height: '100vh', borderStyle: 'mat' },
|
||||
],
|
||||
schedule: [
|
||||
{ time: '16:00', event: '签到迎宾', desc: 'Welcome' },
|
||||
@@ -337,6 +353,7 @@ function ensurePageShape(p) {
|
||||
}
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
p.borderStyle = p.borderStyle || 'mat'
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="wrapper relative w-full h-[100vh] bg-[#Fdfbf7] overflow-hidden text-[#2c2c2c]">
|
||||
|
||||
<!-- 入场揭幕帘幕(双开门 + 印章徽标) -->
|
||||
<!-- 入场揭幕帘幕(雅致双开门 + 徽标) -->
|
||||
<div v-show="showIntro" class="curtain-root" :class="{ 'is-opening': introOpening }" @click="openInvitation">
|
||||
<div class="curtain-bg" :style="curtainBg"></div>
|
||||
<div class="curtain-veil"></div>
|
||||
<div class="curtain-door curtain-door-left"></div>
|
||||
<div class="curtain-door curtain-door-right"></div>
|
||||
<div class="curtain-door curtain-door-left"><span class="door-sheen"></span></div>
|
||||
<div class="curtain-door curtain-door-right"><span class="door-sheen"></span></div>
|
||||
<div class="curtain-emblem">
|
||||
<div class="emblem-card">
|
||||
<span class="emblem-label">WEDDING INVITATION</span>
|
||||
<div class="emblem-seal">❀</div>
|
||||
<div class="emblem-names">{{ data.groom }} <span class="amp">&</span> {{ data.bride }}</div>
|
||||
<div class="emblem-inner">
|
||||
<span class="emblem-floral">❀</span>
|
||||
<span class="emblem-kicker">WEDDING INVITATION</span>
|
||||
<div class="emblem-names">{{ data.groom }}<span class="amp">&</span>{{ data.bride }}</div>
|
||||
<div class="emblem-rule"></div>
|
||||
<div class="emblem-date">{{ data.date }}</div>
|
||||
<div class="emblem-hint"><span class="dot"></span> 点击开启请柬</div>
|
||||
<div class="emblem-hint"><span class="dot"></span>轻触开启请柬</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,14 +22,19 @@
|
||||
<!-- 飘落花瓣层 -->
|
||||
<div v-if="data.petalEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="p in petals" :key="'p'+p.id" class="petal"
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, background: p.color, animationDuration: p.duration, animationDelay: p.delay, transform: `rotate(${p.rotate}deg)` }"></div>
|
||||
:style="{ left: p.left, width: p.size+'px', height: p.size+'px', opacity: p.opacity, animationDelay: p.delay, '--p-dur': p.duration, '--p-sway': p.sway }">
|
||||
<svg viewBox="0 0 100 100" class="w-full h-full" :style="{ filter: 'drop-shadow(0 2px 3px rgba(150,110,90,0.12))' }">
|
||||
<path d="M50 3 C28 26 20 62 50 97 C80 62 72 26 50 3 Z" :fill="p.color" opacity="0.9" />
|
||||
<path d="M50 9 C50 36 50 70 50 91" stroke="rgba(255,255,255,0.55)" stroke-width="1.4" fill="none" opacity="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 漂浮爱心层 -->
|
||||
<div v-if="data.bubbleEnabled" class="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<div v-for="b in bubbles" :key="'b'+b.id"
|
||||
class="heart-bubble flex items-center justify-center"
|
||||
:style="{ left: b.left, color: b.color, fontSize: b.size+'px', opacity: b.opacity, '--float-duration': b.floatDuration, '--sway-duration': b.swayDuration }">♥</div>
|
||||
:style="{ left: b.left, top: b.top, color: b.color, fontSize: b.size+'px', opacity: b.opacity, animationDelay: b.delay, '--float-duration': b.floatDuration, '--sway-duration': b.swayDuration }">♥</div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部滚动进度条 -->
|
||||
@@ -66,9 +72,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 低调管理入口 -->
|
||||
<router-link to="/login" class="absolute bottom-4 right-4 z-50 text-[10px] text-[#a88c6b]/50 hover:text-[#a88c6b] tracking-widest">管理后台</router-link>
|
||||
|
||||
<!-- 主滚动区域 -->
|
||||
<div ref="scrollContainerRef"
|
||||
class="w-full h-full overflow-y-auto no-scrollbar relative scroll-smooth"
|
||||
@@ -165,10 +168,11 @@
|
||||
<span class="text-[11px] text-[#8A8680] font-light tracking-[0.1em] leading-loose block max-w-[240px] mx-auto whitespace-pre-wrap">{{ data.address }}</span>
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 300 }" class="flex flex-col gap-4 w-full px-8 max-w-[280px]">
|
||||
<div @click="isDrawerOpen = true" class="w-full bg-[#a88c6b] text-white py-4 text-[11px] tracking-[0.2em] flex items-center justify-center gap-3 shadow-md uppercase rounded-full cursor-pointer hover:bg-[#967b5c] transition-all">
|
||||
<img src="https://api.iconify.design/lucide:send.svg?color=%23ffffff" class="w-3.5 h-3.5" />
|
||||
<span>填写出席回执</span>
|
||||
</div>
|
||||
<button class="rsvp-cta" @click="isDrawerOpen = true">
|
||||
<span class="rsvp-cta-deco">❀</span>
|
||||
<span class="rsvp-cta-text">填写出席回执</span>
|
||||
<span class="rsvp-cta-line"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,26 +182,24 @@
|
||||
<div v-if="isDrawerOpen" class="absolute inset-0 z-[60] bg-black/40 backdrop-blur-sm transition-opacity duration-500" @click="closeDrawer"></div>
|
||||
<div class="absolute bottom-0 left-0 w-full bg-white rounded-t-[2.5rem] shadow-2xl transition-transform duration-500 z-[70] p-10 flex flex-col" :style="{ transform: isDrawerOpen ? 'translateY(0)' : 'translateY(100%)' }">
|
||||
<div @click="closeDrawer" class="absolute top-6 right-6 p-2 text-[#a88c6b] cursor-pointer hover:text-[#2c2c2c] transition-colors text-xl">✕</div>
|
||||
<span class="text-xl text-center text-[#a88c6b] mb-1 tracking-[0.3em] font-light mt-2 uppercase">Guest RSVP</span>
|
||||
<span class="text-center text-[10px] text-[#8A8680] mb-10 uppercase tracking-[0.4em]">R . S . V . P</span>
|
||||
<span class="text-xl text-center text-[#a88c6b] mb-1 tracking-[0.3em] font-light mt-2">出席回执</span>
|
||||
<span class="text-center text-[10px] text-[#8A8680] mb-10 tracking-[0.4em]">敬 请 回 复</span>
|
||||
|
||||
<div v-if="!isSubmitted" class="space-y-6">
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Name</span>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest pl-1">姓名</span>
|
||||
<input v-model="rsvpForm.name" placeholder="您的姓名" class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] focus:outline-none focus:border-[#a88c6b] shadow-[inset_0_2px_4px_rgba(0,0,0,0.02)]" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Attendance</span>
|
||||
<div @click="isSelectOpen = !isSelectOpen" class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] flex items-center justify-between cursor-pointer">
|
||||
<span>{{ rsvpOptions.find(o => o.value === rsvpForm.guest_count)?.label || '请选择出席人数' }}</span>
|
||||
<span class="text-[#a88c6b] transition-transform" :style="{ transform: isSelectOpen ? 'rotate(180deg)' : '' }">▼</span>
|
||||
</div>
|
||||
<div v-show="isSelectOpen" class="absolute left-10 right-10 bottom-full mb-3 bg-white border border-[#a88c6b]/20 rounded-2xl shadow-2xl overflow-hidden z-[80]">
|
||||
<div v-for="opt in rsvpOptions" :key="opt.value" @click="selectRsvpCount(opt.value)" class="px-5 py-4 text-[13px] hover:bg-[#Fdfbf7] cursor-pointer" :class="rsvpForm.guest_count === opt.value ? 'text-[#a88c6b] bg-[#Fdfbf7]/50' : ''">{{ opt.label }}</div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-2 tracking-widest pl-1">出席情况</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button v-for="opt in rsvpOptions" :key="opt.value" type="button"
|
||||
class="rsvp-tag" :class="{ active: rsvpForm.guest_count === opt.value }"
|
||||
@click="rsvpForm.guest_count = opt.value">{{ opt.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest uppercase pl-1">Wishes</span>
|
||||
<span class="block text-[10px] text-[#a88c6b] mb-1.5 tracking-widest pl-1">祝福语</span>
|
||||
<textarea v-model="rsvpForm.wishes" placeholder="写下您的祝福..." class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/30 rounded-xl px-5 py-4 text-[13px] h-24 focus:outline-none resize-none shadow-[inset_0_2px_4px_rgba(0,0,0,0.02)]"></textarea>
|
||||
</div>
|
||||
<div @click="handleSubmitRsvp" class="w-full bg-[#a88c6b] text-white py-4 rounded-full text-center tracking-[0.3em] text-[12px] cursor-pointer hover:bg-[#967b5c] transition-all shadow-md">确认发送</div>
|
||||
@@ -208,6 +210,7 @@
|
||||
<span class="text-[13px] text-[#8A8680] leading-loose tracking-widest font-light">回执已妥投,期待与您共享喜悦</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -228,19 +231,19 @@ const data = ref({
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
heroImg: 'https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=800&q=80',
|
||||
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' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: Array.from({ length: 6 }, () => 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=600&q=80') },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: Array.from({ length: 9 }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80') },
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', borderStyle: 'mat' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', borderStyle: 'mat' },
|
||||
{ type: 'one-large-five-small', title: '相恋', subtitle: 'ROMANCE', desc: '星光坠入眼眸,晚风轻抚过裙摆。\n浪漫不止于此,还有无数个明天。', images: Array.from({ length: 6 }, () => 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=600&q=80'), borderStyle: 'mat' },
|
||||
{ type: 'nine-grid', title: '相守', subtitle: 'FOREVER', desc: '往后余生,风雪是你,平淡是你。', images: Array.from({ length: 9 }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80'), borderStyle: 'mat' },
|
||||
],
|
||||
schedule: [ { time: '16:00', event: '签到迎宾', desc: 'Welcome' }, { time: '17:08', event: '仪式开始', desc: 'Ceremony' }, { time: '18:00', event: '敬备喜宴', desc: 'Banquet' } ],
|
||||
...DEFAULTS,
|
||||
})
|
||||
|
||||
const audio = useAudio()
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSelectOpen = ref(false), isSubmitted = ref(false)
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)
|
||||
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0), parallax = ref(0)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMusicAuth = ref(false)
|
||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||
const rsvpOptions = [ { value: '1', label: '1 人出席' }, { value: '2', label: '2 人出席' }, { value: '3', label: '3 人及以上' }, { value: '0', label: '遗憾缺席' } ]
|
||||
|
||||
@@ -248,8 +251,18 @@ let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null
|
||||
|
||||
const loveColors = ['#a88c6b', '#e8a5b2', '#f4c2c2', '#d4af37']
|
||||
const petalColors = ['#f7d9e0', '#fbeee3', '#f3c6d3', '#efd9c4', '#f9e7d6']
|
||||
const bubbles = Array.from({ length: 15 }).map((_, i) => ({ id: i, left: Math.random() * 100 + '%', color: loveColors[i % 4], size: 14 + Math.random() * 10, opacity: 0.2, floatDuration: 14 + Math.random() * 10 + 's', swayDuration: 4 + Math.random() * 2 + 's' }))
|
||||
const petals = Array.from({ length: 18 }).map((_, i) => ({ id: i, left: Math.random() * 100 + '%', size: 10 + Math.random() * 14, opacity: 0.4 + Math.random() * 0.4, color: petalColors[i % petalColors.length], duration: 9 + Math.random() * 8 + 's', delay: Math.random() * 10 + 's', rotate: Math.random() * 360 }))
|
||||
const bubbles = Array.from({ length: 15 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', top: Math.random() * 100 + '%',
|
||||
color: loveColors[i % 4], size: 13 + Math.random() * 10, opacity: 0.25 + Math.random() * 0.15,
|
||||
floatDuration: (14 + Math.random() * 10) + 's', swayDuration: (4 + Math.random() * 2) + 's',
|
||||
delay: '-' + (Math.random() * 18).toFixed(1) + 's',
|
||||
}))
|
||||
const petals = Array.from({ length: 18 }).map((_, i) => ({
|
||||
id: i, left: Math.random() * 100 + '%', size: 10 + Math.random() * 14,
|
||||
opacity: 0.4 + Math.random() * 0.4, color: petalColors[i % petalColors.length],
|
||||
duration: (9 + Math.random() * 8) + 's', delay: (Math.random() * 10) + 's',
|
||||
sway: (4 + Math.random() * 4) + 's',
|
||||
}))
|
||||
|
||||
const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` }))
|
||||
|
||||
@@ -259,8 +272,8 @@ const pageHeightStyle = (page) => {
|
||||
return h === 'auto' ? { minHeight: 'auto' } : { minHeight: h }
|
||||
}
|
||||
|
||||
const selectRsvpCount = (val) => { rsvpForm.guest_count = val; isSelectOpen.value = false }
|
||||
const closeDrawer = () => { isDrawerOpen.value = false; isSelectOpen.value = false }
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const authorizeMusic = () => { audio.play(); showMusicAuth.value = false }
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = scrollContainerRef.value
|
||||
@@ -315,6 +328,13 @@ const openInvitation = () => {
|
||||
if (introOpening.value) return
|
||||
introOpening.value = true
|
||||
introTimer = setTimeout(() => { showIntro.value = false }, 1000)
|
||||
// 点击揭幕是明确的用户手势:直接尝试播放背景音乐(最可靠的自动播放路径)
|
||||
if (data.value.musicList.length) audio.play()
|
||||
}
|
||||
const onMusicAuthSkip = () => {
|
||||
showMusicAuth.value = false
|
||||
// 用户暂不开启:挂起首次交互解锁,后续任意点击仍可播放
|
||||
audio.armAutoplay()
|
||||
}
|
||||
const selectTrack = (url) => { audio.setActive(url); showTrackList.value = false }
|
||||
|
||||
@@ -342,10 +362,14 @@ onMounted(async () => {
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
|
||||
audio.setTracks(data.value.musicList, data.value.activeMusicUrl)
|
||||
// 先静默尝试自动播放;被浏览器拦截(无用户手势)时,再弹出授权模态框
|
||||
if (data.value.musicList.length) {
|
||||
const autoOk = await audio.attemptAutoplay()
|
||||
if (!autoOk) showMusicAuth.value = true
|
||||
}
|
||||
if (data.value.introEnabled) {
|
||||
showIntro.value = true
|
||||
introTimer = setTimeout(() => openInvitation(), 3600)
|
||||
audio.armAutoplay()
|
||||
}
|
||||
|
||||
startAutoScroll()
|
||||
@@ -369,46 +393,77 @@ onUnmounted(() => {
|
||||
.vertical-text { writing-mode: vertical-rl; letter-spacing: 0.4em; text-orientation: upright; }
|
||||
.page-section { min-height: 100vh; width: 100%; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
/* 花瓣形状 */
|
||||
.petal { border-radius: 0 100% 0 100%; }
|
||||
.heart-bubble { animation: heartFloat var(--float-duration, 18s) linear infinite, heartSway var(--sway-duration, 5s) ease-in-out infinite alternate; position: absolute; bottom: 0; }
|
||||
/* 花瓣形状由 SVG 决定,无需额外形状样式 */
|
||||
|
||||
/* ---------- 入场揭幕 ---------- */
|
||||
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: "PingFang SC", sans-serif; }
|
||||
/* ---------- 入场揭幕(雅致象牙金) ---------- */
|
||||
.curtain-root { position: fixed; inset: 0; z-index: 100; overflow: hidden; cursor: pointer; font-family: "PingFang SC", sans-serif; background: #fbf6ef; }
|
||||
.curtain-bg {
|
||||
position: absolute; inset: -24px; background-size: cover; background-position: center;
|
||||
filter: blur(14px) brightness(0.5) saturate(1.1); transform: scale(1.12);
|
||||
filter: blur(16px) brightness(0.7) saturate(1.05); transform: scale(1.12);
|
||||
}
|
||||
.curtain-veil { position: absolute; inset: 0; background: radial-gradient(circle at 50% 42%, rgba(0,0,0,0.15), rgba(0,0,0,0.5)); }
|
||||
.curtain-veil { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(251,246,239,0.35), rgba(232,214,194,0.25)); }
|
||||
.curtain-door {
|
||||
position: absolute; top: 0; bottom: 0; width: 50%; z-index: 2;
|
||||
background: linear-gradient(160deg, #b89a78 0%, #a88c6b 45%, #8a704f 100%);
|
||||
box-shadow: inset 0 0 60px rgba(0,0,0,0.25);
|
||||
transition: transform 1s cubic-bezier(0.76, 0, 0.24, 1);
|
||||
background: linear-gradient(160deg, #fbf6ef 0%, #f5e9dc 45%, #ecd9c4 100%);
|
||||
box-shadow: inset 0 0 50px rgba(184,154,120,0.12);
|
||||
transition: transform 1.4s cubic-bezier(0.76, 0, 0.24, 1);
|
||||
}
|
||||
.curtain-door::after { content: ''; position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(255,255,255,0.18); }
|
||||
.curtain-door::before { content: ''; position: absolute; top: 0; bottom: 0; width: 1px; background: linear-gradient(180deg, transparent, rgba(184,154,120,0.5), transparent); }
|
||||
.curtain-door-left { left: 0; }
|
||||
.curtain-door-right { right: 0; }
|
||||
.curtain-door-right::after { right: 0; }
|
||||
.curtain-door-left::before { right: 0; }
|
||||
.curtain-door-right::before { left: 0; }
|
||||
.door-sheen { position: absolute; inset: 0; background: linear-gradient(115deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.25) 100%); }
|
||||
.curtain-root.is-opening .curtain-door-left { transform: translateX(-100%); }
|
||||
.curtain-root.is-opening .curtain-door-right { transform: translateX(100%); }
|
||||
.curtain-emblem {
|
||||
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 3;
|
||||
transition: opacity 0.7s ease, transform 0.9s ease; animation: emblemIn 1s ease both;
|
||||
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.3); }
|
||||
.emblem-card { text-align: center; color: #fff; padding: 30px 34px; border-radius: 18px;
|
||||
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); backdrop-filter: blur(8px);
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||
.emblem-label { display: block; letter-spacing: 0.55em; font-size: 10px; opacity: 0.85; text-transform: uppercase; }
|
||||
.emblem-seal { width: 66px; height: 66px; margin: 16px auto; border: 1px solid rgba(255,255,255,0.6); border-radius: 9999px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 24px; color: #fff; }
|
||||
.emblem-names { font-family: "Noto Serif SC", serif; font-size: 26px; letter-spacing: 0.15em; font-weight: 300; }
|
||||
.emblem-names .amp { font-style: italic; opacity: 0.85; margin: 0 6px; }
|
||||
.emblem-date { font-size: 11px; letter-spacing: 0.3em; opacity: 0.8; margin-top: 10px; }
|
||||
.emblem-hint { margin-top: 22px; font-size: 11px; letter-spacing: 0.3em; opacity: 0.85;
|
||||
.curtain-root.is-opening .curtain-emblem { opacity: 0; transform: scale(1.25); }
|
||||
.emblem-inner { text-align: center; color: #5b4a36; padding: 10px; }
|
||||
.emblem-floral { display: block; font-size: 22px; color: #b89a78; margin-bottom: 14px; }
|
||||
.emblem-kicker { display: block; letter-spacing: 0.6em; font-size: 9px; color: #b89a78; text-transform: uppercase; margin-bottom: 18px; padding-left: 0.6em; }
|
||||
.emblem-names { font-family: "Noto Serif SC", serif; font-size: 30px; letter-spacing: 0.18em; font-weight: 300; color: #4a3f30; }
|
||||
.emblem-names .amp { font-style: italic; color: #b89a78; margin: 0 8px; }
|
||||
.emblem-rule { width: 46px; height: 1px; background: linear-gradient(90deg, transparent, #b89a78, transparent); margin: 18px auto; }
|
||||
.emblem-date { font-size: 11px; letter-spacing: 0.3em; color: #8a7458; }
|
||||
.emblem-hint { margin-top: 26px; font-size: 11px; letter-spacing: 0.3em; color: #a08a66;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.emblem-hint .dot { width: 6px; height: 6px; border-radius: 9999px; background: #fff; animation: hintPulse 1.6s infinite; }
|
||||
.emblem-hint .dot { width: 6px; height: 6px; border-radius: 9999px; background: #b89a78; animation: hintPulse 1.6s infinite; }
|
||||
|
||||
/* ---------- 回执 CTA 按钮 ---------- */
|
||||
.rsvp-cta {
|
||||
position: relative; width: 100%; padding: 16px 0; border-radius: 9999px;
|
||||
border: 1px solid #c9a978; background: rgba(255,255,255,0.6);
|
||||
color: #8a704f; font-size: 12px; letter-spacing: 0.28em; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; gap: 10px;
|
||||
backdrop-filter: blur(8px); transition: all .35s ease;
|
||||
box-shadow: 0 6px 18px rgba(184,154,120,0.12);
|
||||
}
|
||||
.rsvp-cta:hover { background: #a88c6b; color: #fff; border-color: #a88c6b; box-shadow: 0 10px 26px rgba(184,154,120,0.28); }
|
||||
.rsvp-cta-deco { font-size: 13px; opacity: .9; }
|
||||
.rsvp-cta-line { width: 18px; height: 1px; background: currentColor; opacity: .6; }
|
||||
|
||||
/* ---------- 出席情况单选标签 ---------- */
|
||||
.rsvp-tag {
|
||||
flex: 1 1 auto; min-width: 72px; padding: 10px 12px; border-radius: 12px;
|
||||
border: 1px solid rgba(168,140,107,0.35); background: #Fdfbf7; color: #6b6863;
|
||||
font-size: 12px; letter-spacing: 0.1em; cursor: pointer; transition: all .25s ease;
|
||||
}
|
||||
.rsvp-tag:hover { border-color: #a88c6b; }
|
||||
.rsvp-tag.active { background: #a88c6b; border-color: #a88c6b; color: #fff; box-shadow: 0 4px 12px rgba(168,140,107,0.25); }
|
||||
|
||||
/* ---------- 音乐授权模态框 ---------- */
|
||||
.music-auth-card { width: 240px; background: #fff; border-radius: 22px; padding: 28px 24px 22px; text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.28); border: 1px solid rgba(168,140,107,0.2); }
|
||||
.music-auth-note { font-size: 26px; color: #a88c6b; margin-bottom: 12px; }
|
||||
.music-auth-title { font-size: 16px; color: #4a3f30; letter-spacing: 0.2em; margin-bottom: 6px; }
|
||||
.music-auth-sub { font-size: 11px; color: #8A8680; letter-spacing: 0.15em; margin-bottom: 20px; }
|
||||
.music-auth-btn { width: 100%; padding: 11px 0; border-radius: 9999px; background: #a88c6b; color: #fff;
|
||||
font-size: 12px; letter-spacing: 0.2em; cursor: pointer; border: none; transition: background .3s; }
|
||||
.music-auth-btn:hover { background: #967b5c; }
|
||||
.music-auth-skip { margin-top: 10px; background: none; border: none; color: #b0a99e; font-size: 11px; letter-spacing: 0.1em; cursor: pointer; }
|
||||
|
||||
/* ---------- 音乐曲目弹层 ---------- */
|
||||
.music-wrap { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user