更新页面效果
This commit is contained in:
@@ -50,6 +50,14 @@ export function saveConfig(payload) {
|
||||
return service.post('/config', payload)
|
||||
}
|
||||
|
||||
export function getUploadConfig() {
|
||||
return service.get('/upload/config')
|
||||
}
|
||||
|
||||
export function saveUploadConfig(payload) {
|
||||
return service.post('/upload/config', payload)
|
||||
}
|
||||
|
||||
export function uploadImage(file) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
</header>
|
||||
|
||||
<div class="max-w-[960px] mx-auto px-4 py-6">
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs">
|
||||
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs" :destroyInactiveTabPane="true">
|
||||
<!-- 基础信息 -->
|
||||
<a-tab-pane key="basic" tab="基础信息">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-card v-if="activeTab === 'basic'" :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="新郎">
|
||||
@@ -54,16 +54,23 @@
|
||||
<a-textarea v-model:value="cfg.address" :rows="2" placeholder="可换行" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<a-form-item label="封面 / 尾页主图">
|
||||
<image-field v-model="cfg.heroImg" @pick="openUpload(u => cfg.heroImg = u)" :uploading="uploading" />
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="封面图片">
|
||||
<image-field v-model="cfg.heroImg" label="封面" @pick="openUpload(u => cfg.heroImg = u)" :uploading="uploading" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-2">用于首页封面和入场揭幕背景。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="尾页图片">
|
||||
<image-field v-model="cfg.endImg" label="尾页" @pick="openUpload(u => cfg.endImg = u)" :uploading="uploading" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-2">用于最后的回执尾页,可与封面分开设置。</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 文案 -->
|
||||
<a-tab-pane key="copy" tab="邀请文案">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-card v-if="activeTab === 'copy'" :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="竖排文案 · 上联">
|
||||
<a-input v-model:value="cfg.formalText1" placeholder="如 两姓联姻 一堂缔约" />
|
||||
@@ -77,7 +84,7 @@
|
||||
|
||||
<!-- 视觉 / 动效 -->
|
||||
<a-tab-pane key="visual" tab="视觉与动效">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-card v-if="activeTab === 'visual'" :bordered="false" class="!shadow-sm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="背景音乐">
|
||||
<div class="text-[12px] text-[#8A8680] leading-relaxed">多首背景音乐的上传、试听与「设为播放」请在 <span class="text-[#a88c6b]">背景音乐</span> 标签页管理。</div>
|
||||
@@ -122,7 +129,7 @@
|
||||
|
||||
<!-- 背景音乐 -->
|
||||
<a-tab-pane key="music" tab="背景音乐">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-card v-if="activeTab === 'music'" :bordered="false" class="!shadow-sm">
|
||||
<div class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
|
||||
上传多首背景音乐(支持 mp3 等格式),访客首次交互后自动播放当前曲目,可在前台随时切换或暂停。
|
||||
</div>
|
||||
@@ -147,14 +154,67 @@
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 相册页 -->
|
||||
<!-- 上传设置 -->
|
||||
<a-tab-pane key="upload" tab="上传设置">
|
||||
<a-card v-if="activeTab === 'upload'" :bordered="false" class="!shadow-sm">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="上传设置仅后台可见,OSS 密钥不会写入公开请柬配置。"
|
||||
/>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="上传方式">
|
||||
<a-radio-group v-model:value="uploadCfg.provider">
|
||||
<a-radio value="local">本地上传</a-radio>
|
||||
<a-radio value="oss">阿里云 OSS</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<div v-if="uploadCfg.provider === 'oss'" class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="AccessKeyId">
|
||||
<a-input v-model:value="uploadCfg.accessKeyId" placeholder="请输入 AccessKeyId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="AccessKeySecret">
|
||||
<a-input-password v-model:value="uploadCfg.accessKeySecret" :placeholder="uploadCfg.hasSecret ? '已配置,留空则不修改' : '请输入 AccessKeySecret'" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Bucket">
|
||||
<a-input v-model:value="uploadCfg.bucket" placeholder="如 wedding-assets" />
|
||||
</a-form-item>
|
||||
<a-form-item label="文件夹">
|
||||
<a-input v-model:value="uploadCfg.folder" placeholder="如 invitations" />
|
||||
</a-form-item>
|
||||
<a-form-item label="访问域名">
|
||||
<a-input v-model:value="uploadCfg.domain" placeholder="如 https://cdn.example.com,可留空" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Region">
|
||||
<a-input v-model:value="uploadCfg.region" placeholder="如 cn-hangzhou" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Endpoint">
|
||||
<a-input v-model:value="uploadCfg.endpoint" placeholder="如 https://oss-cn-hangzhou.aliyuncs.com" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div v-else class="text-[12px] text-[#8A8680] leading-relaxed mb-4">
|
||||
本地上传会继续保存到后端 uploads 目录,返回 http://localhost:8080/uploads/ 开头的地址。
|
||||
</div>
|
||||
<a-button type="primary" :loading="uploadConfigSaving" @click="onSaveUploadConfig"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存上传设置
|
||||
</a-button>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="photos" tab="相册页管理">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-[12px] text-[#8A8680]">共 {{ cfg.photoPages.length }} 个相册页</span>
|
||||
<a-button size="small" @click="addPage"><i class="fa-solid fa-plus mr-1 text-[#a88c6b]"></i>新增相册页</a-button>
|
||||
</div>
|
||||
<a-empty v-if="!cfg.photoPages.length" description="暂无相册页" />
|
||||
<a-card v-for="(page, idx) in cfg.photoPages" :key="idx" :bordered="false" class="!shadow-sm mb-4">
|
||||
<a-tabs v-if="cfg.photoPages.length" v-model:activeKey="activePhotoTab" size="small" type="card" class="photo-page-tabs" :destroyInactiveTabPane="true">
|
||||
<a-tab-pane v-for="(page, idx) in cfg.photoPages" :key="String(idx)" :tab="`第 ${idx + 1} 页`" />
|
||||
</a-tabs>
|
||||
<template v-for="(page, idx) in cfg.photoPages" :key="idx">
|
||||
<a-card v-if="idx === currentPhotoIndex" :bordered="false" class="!shadow-sm mb-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-[#a88c6b] text-sm">第 {{ idx + 1 }} 页</span>
|
||||
<div class="flex gap-1">
|
||||
@@ -257,6 +317,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 婚礼流程 -->
|
||||
@@ -303,10 +364,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getConfig, saveConfig, uploadImage, getRsvpList } from '@/api/wedding'
|
||||
import { getConfig, saveConfig, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig } from '@/api/wedding'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
|
||||
@@ -315,12 +376,16 @@ const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
const activeTab = ref('basic')
|
||||
const activePhotoTab = ref('0')
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const uploadConfigSaving = ref(false)
|
||||
const rsvpLoading = ref(false)
|
||||
const rsvpList = ref([])
|
||||
|
||||
const cfg = reactive(defaultConfig())
|
||||
const uploadCfg = reactive(defaultUploadConfig())
|
||||
const currentPhotoIndex = computed(() => Math.min(Number(activePhotoTab.value) || 0, Math.max(cfg.photoPages.length - 1, 0)))
|
||||
|
||||
function defaultConfig() {
|
||||
return {
|
||||
@@ -328,6 +393,7 @@ function defaultConfig() {
|
||||
hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅',
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
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,
|
||||
@@ -346,6 +412,20 @@ function defaultConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function defaultUploadConfig() {
|
||||
return {
|
||||
provider: 'local',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: '',
|
||||
hasSecret: false,
|
||||
bucket: '',
|
||||
folder: '',
|
||||
domain: '',
|
||||
region: '',
|
||||
endpoint: '',
|
||||
}
|
||||
}
|
||||
|
||||
function fill(n) {
|
||||
return Array.from({ length: n }, () => 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=400&q=80')
|
||||
}
|
||||
@@ -411,9 +491,24 @@ function moveImage(page, i, dir) {
|
||||
}
|
||||
|
||||
// 相册页操作
|
||||
function addPage() { cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' })) }
|
||||
function removePage(i) { cfg.photoPages.splice(i, 1) }
|
||||
function movePage(i, dir) { const j = i + dir; if (j < 0 || j >= cfg.photoPages.length) return; const t = cfg.photoPages[i]; cfg.photoPages[i] = cfg.photoPages[j]; cfg.photoPages[j] = t }
|
||||
function setActivePhotoIndex(i) {
|
||||
const max = Math.max(cfg.photoPages.length - 1, 0)
|
||||
activePhotoTab.value = String(Math.min(Math.max(i, 0), max))
|
||||
}
|
||||
function addPage() {
|
||||
cfg.photoPages.push(ensurePageShape({ type: 'single', title: '新页面', subtitle: 'NEW', desc: '', img1: '' }))
|
||||
setActivePhotoIndex(cfg.photoPages.length - 1)
|
||||
}
|
||||
function removePage(i) {
|
||||
cfg.photoPages.splice(i, 1)
|
||||
setActivePhotoIndex(Math.min(i, cfg.photoPages.length - 1))
|
||||
}
|
||||
function movePage(i, dir) {
|
||||
const j = i + dir
|
||||
if (j < 0 || j >= cfg.photoPages.length) return
|
||||
const t = cfg.photoPages[i]; cfg.photoPages[i] = cfg.photoPages[j]; cfg.photoPages[j] = t
|
||||
setActivePhotoIndex(j)
|
||||
}
|
||||
// 流程操作
|
||||
function addSchedule() { cfg.schedule.push({ time: '', event: '', desc: '' }) }
|
||||
function removeSchedule(i) { cfg.schedule.splice(i, 1) }
|
||||
@@ -473,6 +568,40 @@ function shortUrl(url) {
|
||||
return parts[parts.length - 1] || url
|
||||
}
|
||||
|
||||
async function loadUploadConfig() {
|
||||
try {
|
||||
const res = await getUploadConfig()
|
||||
Object.assign(uploadCfg, defaultUploadConfig(), res.data || {})
|
||||
uploadCfg.accessKeySecret = ''
|
||||
} catch (e) {
|
||||
// 上传设置仅后台使用,读取失败不影响内容编辑。
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveUploadConfig() {
|
||||
uploadConfigSaving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
provider: uploadCfg.provider,
|
||||
accessKeyId: uploadCfg.accessKeyId,
|
||||
accessKeySecret: uploadCfg.accessKeySecret,
|
||||
bucket: uploadCfg.bucket,
|
||||
folder: uploadCfg.folder,
|
||||
domain: uploadCfg.domain,
|
||||
region: uploadCfg.region,
|
||||
endpoint: uploadCfg.endpoint,
|
||||
}
|
||||
const res = await saveUploadConfig(payload)
|
||||
Object.assign(uploadCfg, defaultUploadConfig(), res.data || {})
|
||||
uploadCfg.accessKeySecret = ''
|
||||
message.success('上传设置已保存')
|
||||
} catch (e) {
|
||||
message.error(e?.message || '上传设置保存失败')
|
||||
} finally {
|
||||
uploadConfigSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
async function onSave() {
|
||||
saving.value = true
|
||||
@@ -526,14 +655,19 @@ onMounted(async () => {
|
||||
loaded.musicList = [{ url: loaded.musicUrl, name: '背景音乐' }]
|
||||
loaded.activeMusicUrl = loaded.musicUrl
|
||||
}
|
||||
loaded.endImg = loaded.endImg || loaded.heroImg || cfg.endImg
|
||||
Object.assign(cfg, loaded)
|
||||
cfg.photoPages = (cfg.photoPages || []).map(ensurePageShape)
|
||||
setActivePhotoIndex(currentPhotoIndex.value)
|
||||
// 数值型参数兜底(兼容旧配置)
|
||||
cfg.freeScrollInterval = Number(cfg.freeScrollInterval) || 50
|
||||
cfg.pageSwitchDuration = Number(cfg.pageSwitchDuration) || 800
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
if (adminStore.isAdmin) loadRsvp()
|
||||
if (adminStore.isAdmin) {
|
||||
loadRsvp()
|
||||
loadUploadConfig()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -162,18 +162,27 @@
|
||||
</div>
|
||||
|
||||
<!-- 尾页 -->
|
||||
<div class="page-section p-6 flex flex-col items-center justify-center relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
|
||||
<div class="absolute top-10 bottom-10 left-6 right-6 border-[0.5px] border-[#a88c6b]/40 rounded-t-[10rem] z-0 bg-[#Fdfbf7]/30"></div>
|
||||
<div class="w-full flex flex-col items-center z-10 relative py-10">
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="w-full max-w-[200px] aspect-[3/4] rounded-t-full overflow-hidden mb-10 shadow-lg border-[4px] border-white relative">
|
||||
<img :src="data.heroImg" class="w-full h-full bg-[#F7F6F2] object-cover" />
|
||||
<div class="page-section ending-page px-6 py-8 flex flex-col items-center justify-center relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''">
|
||||
<div class="ending-frame" aria-hidden="true"></div>
|
||||
<div class="ending-corners" aria-hidden="true"><i></i><i></i></div>
|
||||
<div class="w-full max-w-[340px] flex flex-col items-center z-10 relative">
|
||||
<div v-reveal="'down'" class="ending-kicker">
|
||||
<span>With Love</span>
|
||||
<i></i>
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 200 }" class="text-center space-y-2 mb-10 px-4">
|
||||
<span class="text-xl text-[#a88c6b] tracking-[0.3em] font-light block mb-4">期待您的光临</span>
|
||||
<span class="text-[14px] text-[#3A3A3A] tracking-widest font-medium block">{{ data.hotel }}</span>
|
||||
<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 v-reveal="{ variant: 'scale', delay: 100 }" class="ending-photo">
|
||||
<img :src="endImage" class="w-full h-full bg-[#F7F6F2] object-cover" alt="婚礼尾页照片" />
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 300 }" class="flex flex-col gap-4 w-full px-8 max-w-[280px]">
|
||||
<div v-reveal="{ variant: 'up', delay: 180 }" class="ending-copy">
|
||||
<span class="ending-title">期待您的光临</span>
|
||||
<span class="ending-names">{{ data.groom }} & {{ data.bride }}</span>
|
||||
<span class="ending-date">{{ data.date }}</span>
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 240 }" class="ending-venue">
|
||||
<span class="ending-hotel">{{ data.hotel }}</span>
|
||||
<span class="ending-address">{{ data.address }}</span>
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'up', delay: 300 }" class="w-full max-w-[280px]">
|
||||
<button class="rsvp-cta" @click="isDrawerOpen = true">
|
||||
<span class="rsvp-cta-deco">❀</span>
|
||||
<span class="rsvp-cta-text">填写出席回执</span>
|
||||
@@ -238,6 +247,7 @@ const data = ref({
|
||||
hotel: '杭州星光万丽大酒店', address: '杭州市西湖区星光大道88号\n3楼大宴会厅',
|
||||
formalText1: '两姓联姻 一堂缔约', formalText2: '良缘永结 匹配同称',
|
||||
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',
|
||||
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', 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' },
|
||||
@@ -310,6 +320,7 @@ const petals = Array.from({ length: 16 }).map((_, i) => {
|
||||
})
|
||||
|
||||
const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` }))
|
||||
const endImage = computed(() => data.value.endImg || data.value.heroImg)
|
||||
|
||||
// 相册页页面高度:支持 100vh / 80vh / 60vh / auto / 自定义 vh
|
||||
const pageHeightStyle = (page) => {
|
||||
@@ -319,7 +330,7 @@ const pageHeightStyle = (page) => {
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const authorizeMusic = () => { audio.play(); showMusicAuth.value = false }
|
||||
|
||||
audio.play()
|
||||
const handleScroll = () => {
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
@@ -420,6 +431,7 @@ onMounted(async () => {
|
||||
loaded.musicList = [{ url: loaded.musicUrl, name: '背景音乐' }]
|
||||
loaded.activeMusicUrl = loaded.musicUrl
|
||||
}
|
||||
loaded.endImg = loaded.endImg || loaded.heroImg || data.value.endImg
|
||||
data.value = { ...data.value, ...DEFAULTS, ...loaded }
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
@@ -497,6 +509,72 @@ onUnmounted(() => {
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.emblem-hint .dot { width: 6px; height: 6px; border-radius: 9999px; background: #b89a78; animation: hintPulse 1.6s infinite; }
|
||||
|
||||
/* ---------- 尾页 ---------- */
|
||||
.ending-page {
|
||||
background:
|
||||
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(168,140,107,0.26); border-radius: 160px 160px 24px 24px;
|
||||
background: rgba(255,255,255,0.28); pointer-events: none;
|
||||
}
|
||||
.ending-frame::before {
|
||||
content: ''; position: absolute; inset: 9px;
|
||||
border: 0.5px solid rgba(168,140,107,0.18); border-radius: 150px 150px 18px 18px;
|
||||
}
|
||||
.ending-corners i {
|
||||
position: absolute; width: 54px; height: 54px; border-color: rgba(168,140,107,0.28);
|
||||
}
|
||||
.ending-corners i:first-child { top: 44px; left: 38px; border-top: 1px solid; border-left: 1px solid; }
|
||||
.ending-corners i:last-child { right: 38px; bottom: 44px; border-right: 1px solid; border-bottom: 1px solid; }
|
||||
.ending-kicker {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 10px;
|
||||
margin-bottom: 18px; color: #a88c6b; font-size: 10px; letter-spacing: 0.45em;
|
||||
text-transform: uppercase; padding-left: 0.45em;
|
||||
}
|
||||
.ending-kicker i {
|
||||
display: block; width: 34px; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(168,140,107,0.75), transparent);
|
||||
}
|
||||
.ending-photo {
|
||||
width: min(54vw, 188px); aspect-ratio: 3 / 4; overflow: hidden;
|
||||
border-radius: 9999px 9999px 16px 16px; border: 5px solid rgba(255,255,255,0.86);
|
||||
box-shadow: 0 18px 42px rgba(100,78,48,0.18), 0 0 0 1px rgba(168,140,107,0.18);
|
||||
background: #F7F6F2;
|
||||
}
|
||||
.ending-copy {
|
||||
display: flex; flex-direction: column; align-items: center; text-align: center;
|
||||
margin-top: 22px; margin-bottom: 18px; padding: 0 12px;
|
||||
}
|
||||
.ending-title {
|
||||
display: block; color: #a88c6b; font-size: 20px; line-height: 1.3;
|
||||
letter-spacing: 0.28em; padding-left: 0.28em; font-weight: 300;
|
||||
}
|
||||
.ending-names {
|
||||
display: block; margin-top: 10px; color: #3A3A3A; font-size: 14px;
|
||||
letter-spacing: 0.16em; font-family: "Noto Serif SC", serif;
|
||||
}
|
||||
.ending-date {
|
||||
display: block; margin-top: 8px; color: #8A8680; font-size: 10px;
|
||||
letter-spacing: 0.28em; padding-left: 0.28em;
|
||||
}
|
||||
.ending-venue {
|
||||
width: 100%; margin-bottom: 24px; padding: 14px 18px;
|
||||
border-top: 1px solid rgba(168,140,107,0.18);
|
||||
border-bottom: 1px solid rgba(168,140,107,0.18);
|
||||
text-align: center; background: rgba(255,255,255,0.32);
|
||||
}
|
||||
.ending-hotel {
|
||||
display: block; color: #3A3A3A; font-size: 13px; line-height: 1.6;
|
||||
letter-spacing: 0.18em; font-weight: 500;
|
||||
}
|
||||
.ending-address {
|
||||
display: block; max-width: 260px; margin: 7px auto 0; color: #706b64;
|
||||
font-size: 11px; line-height: 1.8; letter-spacing: 0.08em; white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ---------- 回执 CTA 按钮 ---------- */
|
||||
.rsvp-cta {
|
||||
position: relative; width: 100%; padding: 16px 0; border-radius: 9999px;
|
||||
|
||||
Reference in New Issue
Block a user