1. 礼物特效

This commit is contained in:
李琦
2026-08-03 13:21:22 +08:00
parent 8a4b3f4ab7
commit 43957f6547
2 changed files with 270 additions and 50 deletions

View File

@@ -1,15 +1,23 @@
<template>
<div class="min-h-screen bg-[#f6f4ef] text-[#2c2c2c]">
<!-- 顶部栏 -->
<header class="sticky top-0 z-30 bg-white/90 backdrop-blur border-b border-[#a88c6b]/15 px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xl text-[#a88c6b] font-serif"></span>
<div>
<div class="text-sm tracking-[0.2em] font-light">婚礼 · 内容编辑后台</div>
<div class="text-[10px] text-[#8A8680] tracking-[0.3em] uppercase">Invitation Admin</div>
<header class="sticky top-0 z-30 bg-white/90 backdrop-blur border-b border-[#a88c6b]/15 px-4 sm:px-6 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="text-xl text-[#a88c6b] font-serif shrink-0"></span>
<div class="min-w-0">
<div class="text-sm tracking-[0.18em] font-light truncate">婚礼邀请函 · 内容编辑后台</div>
<div class="text-[10px] text-[#8A8680] tracking-[0.12em] mt-0.5">
制作者 · <span class="text-[#a88c6b]">年糕崽崽</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-wrap justify-end">
<a-segmented
v-model:value="menuLayout"
size="small"
class="admin-layout-seg"
:options="menuLayoutOptions"
/>
<a-button size="small" @click="openPreview">
<i class="fa-solid fa-eye mr-1 text-[#a88c6b]"></i>预览请柬
</a-button>
@@ -46,11 +54,35 @@
</div>
</header>
<div class="max-w-[960px] mx-auto px-4 py-6">
<a-tabs v-model:activeKey="activeTab" type="card" class="admin-tabs" :destroyInactiveTabPane="true">
<div class="admin-shell" :class="menuLayout === 'side' ? 'admin-shell--side' : 'admin-shell--top'">
<nav class="admin-nav" aria-label="后台菜单">
<div
v-for="group in adminMenuGroups"
:key="group.key"
class="admin-nav-group"
>
<div class="admin-nav-group-title">{{ group.label }}</div>
<div class="admin-nav-items">
<button
v-for="item in group.items"
:key="item.key"
type="button"
class="admin-nav-item"
:class="{ active: activeTab === item.key }"
@click="activeTab = item.key"
>
<i :class="item.icon" class="admin-nav-icon" />
<span>{{ item.label }}</span>
</button>
</div>
</div>
<div class="admin-nav-credit">制作者 · 年糕崽崽</div>
</nav>
<main class="admin-main">
<!-- 概览 -->
<a-tab-pane key="overview" tab="概览">
<div v-if="activeTab === 'overview'" class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<section v-if="activeTab === 'overview'">
<div class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<div class="flex justify-between items-center mb-4 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">访客概览</div>
@@ -121,11 +153,11 @@
</a-table>
</div>
</div>
</a-tab-pane>
</section>
<!-- 礼物 -->
<a-tab-pane key="gifts" tab="礼物">
<div v-if="activeTab === 'gifts'" class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<section v-if="activeTab === 'gifts'">
<div class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<div class="flex justify-between items-center mb-4 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">礼物统计</div>
@@ -169,11 +201,11 @@
</div>
</div>
</div>
</a-tab-pane>
</section>
<!-- 访客记录 -->
<a-tab-pane key="visits" tab="访客记录">
<div v-if="activeTab === 'visits'" class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<section v-if="activeTab === 'visits'">
<div class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
<div class="flex justify-between items-center mb-4 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">访客记录</div>
@@ -215,11 +247,11 @@
</a-table>
</div>
</div>
</a-tab-pane>
</section>
<!-- 基础信息 -->
<a-tab-pane key="basic" tab="基础信息">
<a-card v-if="activeTab === 'basic'" :bordered="false" class="!shadow-sm" :class="{ 'opacity-60': tabLoading }">
<section v-if="activeTab === 'basic'">
<a-card :bordered="false" class="!shadow-sm" :class="{ 'opacity-60': tabLoading }">
<a-form layout="vertical">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<a-form-item label="新郎">
@@ -257,11 +289,11 @@
</div>
</a-form>
</a-card>
</a-tab-pane>
</section>
<!-- 文案 -->
<a-tab-pane key="copy" tab="邀请文案">
<a-card v-if="activeTab === 'copy'" :bordered="false" class="!shadow-sm">
<section v-if="activeTab === 'copy'">
<a-card :bordered="false" class="!shadow-sm">
<a-form layout="vertical">
<a-form-item label="竖排文案 · 上联">
<a-input v-model:value="cfg.formalText1" placeholder="如 两姓联姻 一堂缔约" />
@@ -282,11 +314,11 @@
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
</section>
<!-- 视觉 / 动效 -->
<a-tab-pane key="visual" tab="视觉与动效">
<a-card v-if="activeTab === 'visual'" :bordered="false" class="!shadow-sm">
<section v-if="activeTab === 'visual'">
<a-card :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>
@@ -354,11 +386,11 @@
</div>
</a-form>
</a-card>
</a-tab-pane>
</section>
<!-- 背景音乐 -->
<a-tab-pane key="music" tab="背景音乐">
<div v-if="activeTab === 'music'" class="admin-panel">
<section v-if="activeTab === 'music'">
<div class="admin-panel">
<div class="flex flex-wrap items-start justify-between gap-3 mb-5">
<div>
<div class="text-sm text-[#2c2c2c] tracking-wide">曲目列表</div>
@@ -456,11 +488,11 @@
</li>
</ul>
</div>
</a-tab-pane>
</section>
<!-- 上传设置 -->
<a-tab-pane key="upload" tab="上传设置">
<a-card v-if="activeTab === 'upload'" :bordered="false" class="!shadow-sm">
<section v-if="activeTab === 'upload'">
<a-card :bordered="false" class="!shadow-sm">
<a-alert
type="info"
show-icon
@@ -506,11 +538,11 @@
</a-button>
</a-form>
</a-card>
</a-tab-pane>
</section>
<!-- 海报独立 poster_configs1 男方 /hb-n2 女方 /hb-nv3 回门宴 /hb-hm -->
<a-tab-pane key="poster" tab="海报">
<a-card v-if="activeTab === 'poster'" :bordered="false" class="!shadow-sm">
<section v-if="activeTab === 'poster'">
<a-card :bordered="false" class="!shadow-sm">
<a-alert
type="info"
show-icon
@@ -736,9 +768,9 @@
</div>
</a-form>
</a-card>
</a-tab-pane>
</section>
<a-tab-pane key="photos" tab="相册页管理">
<section v-if="activeTab === 'photos'">
<div class="admin-panel !p-4 mb-3">
<div class="flex justify-between items-center gap-3 flex-wrap">
<div>
@@ -914,10 +946,10 @@
</div>
</a-card>
</template>
</a-tab-pane>
</section>
<!-- 婚礼流程 -->
<a-tab-pane key="schedule" tab="婚礼流程">
<section v-if="activeTab === 'schedule'">
<div class="admin-panel">
<div class="flex justify-between items-center mb-4">
<div>
@@ -957,10 +989,10 @@
</li>
</ol>
</div>
</a-tab-pane>
</section>
<!-- 回执列表 -->
<a-tab-pane key="rsvp" tab="出席回执">
<section v-if="activeTab === 'rsvp'">
<div class="admin-panel">
<div class="flex justify-between items-center mb-4">
<div>
@@ -991,10 +1023,10 @@
</template>
</a-table>
</div>
</a-tab-pane>
</section>
<!-- 弹幕审核 -->
<a-tab-pane key="danmaku" tab="弹幕审核">
<section v-if="activeTab === 'danmaku'">
<div class="admin-panel">
<div class="flex items-center justify-between mb-4 gap-3 flex-wrap">
<div>
@@ -1053,8 +1085,8 @@
</template>
</a-table>
</div>
</a-tab-pane>
</a-tabs>
</section>
</main>
</div>
</div>
</template>
@@ -1090,9 +1122,56 @@ const router = useRouter()
const adminStore = useAdminStore()
const TAB_KEY = 'wedding_admin_active_tab'
const MENU_LAYOUT_KEY = 'wedding_admin_menu_layout'
const CONFIG_SECTIONS = ['basic', 'copy', 'visual', 'music', 'photos', 'schedule']
const VALID_TABS = ['overview', 'gifts', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'rsvp', 'danmaku']
/** 后台菜单分组 */
const adminMenuGroups = [
{
key: 'data',
label: '数据看板',
items: [
{ key: 'overview', label: '概览', icon: 'fa-solid fa-chart-pie' },
{ key: 'visits', label: '访客记录', icon: 'fa-solid fa-users' },
{ key: 'gifts', label: '礼物', icon: 'fa-solid fa-gift' },
],
},
{
key: 'content',
label: '请柬内容',
items: [
{ key: 'basic', label: '基础信息', icon: 'fa-solid fa-heart' },
{ key: 'copy', label: '邀请文案', icon: 'fa-solid fa-pen-fancy' },
{ key: 'visual', label: '视觉动效', icon: 'fa-solid fa-wand-magic-sparkles' },
{ key: 'music', label: '背景音乐', icon: 'fa-solid fa-music' },
{ key: 'photos', label: '相册页', icon: 'fa-solid fa-images' },
{ key: 'schedule', label: '婚礼流程', icon: 'fa-solid fa-list-ol' },
],
},
{
key: 'interact',
label: '宾客互动',
items: [
{ key: 'rsvp', label: '出席回执', icon: 'fa-solid fa-envelope-open-text' },
{ key: 'danmaku', label: '弹幕审核', icon: 'fa-solid fa-comments' },
],
},
{
key: 'system',
label: '海报与系统',
items: [
{ key: 'poster', label: '海报', icon: 'fa-solid fa-scroll' },
{ key: 'upload', label: '上传设置', icon: 'fa-solid fa-cloud-arrow-up' },
],
},
]
const menuLayoutOptions = [
{ value: 'top', label: '顶部菜单' },
{ value: 'side', label: '侧边菜单' },
]
const SECTION_KEYS = {
basic: ['groom', 'bride', 'date', 'lunar', 'calendarDate', 'hotel', 'address', 'heroImg', 'endImg'],
copy: ['formalText1', 'formalText2', 'formalPageHeight'],
@@ -1113,7 +1192,19 @@ function readStoredTab() {
return 'overview'
}
function readStoredMenuLayout() {
try {
const v = localStorage.getItem(MENU_LAYOUT_KEY)
if (v === 'top' || v === 'side') return v
} catch { /* ignore */ }
return 'side'
}
const activeTab = ref(readStoredTab())
const menuLayout = ref(readStoredMenuLayout())
watch(menuLayout, (v) => {
try { localStorage.setItem(MENU_LAYOUT_KEY, v) } catch { /* ignore */ }
})
const activePhotoTab = ref('0')
const saving = ref(false)
const uploading = ref(false)
@@ -2154,9 +2245,138 @@ onUnmounted(() => {
white-space: pre-line;
word-break: break-all;
}
.admin-tabs :deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) { color: #a88c6b; }
.admin-tabs :deep(.ant-tabs-ink-bar) { background: #a88c6b; }
.admin-tabs :deep(.ant-tabs-tab:hover .ant-tabs-tab-btn) { color: #967b5c; }
.admin-layout-seg :deep(.ant-segmented-item-selected) {
color: #a88c6b;
}
.admin-shell {
margin: 0 auto;
padding: 20px 16px 40px;
box-sizing: border-box;
}
.admin-shell--top {
max-width: 1040px;
}
.admin-shell--side {
max-width: 1220px;
display: flex;
align-items: flex-start;
gap: 18px;
}
.admin-nav {
background: #fff;
border: 1px solid rgba(168, 140, 107, 0.14);
border-radius: 16px;
box-shadow: 0 8px 28px rgba(88, 68, 42, 0.05);
}
.admin-shell--top .admin-nav {
display: flex;
flex-wrap: wrap;
gap: 10px 18px;
padding: 14px 16px 12px;
margin-bottom: 16px;
}
.admin-shell--side .admin-nav {
position: sticky;
top: 68px;
width: 212px;
flex-shrink: 0;
padding: 14px 10px 16px;
max-height: calc(100dvh - 88px);
overflow: auto;
}
.admin-nav-group-title {
font-size: 11px;
letter-spacing: 0.12em;
color: #8A8680;
margin-bottom: 6px;
padding: 0 6px;
}
.admin-shell--top .admin-nav-group {
min-width: 0;
}
.admin-shell--side .admin-nav-group + .admin-nav-group {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed rgba(168, 140, 107, 0.22);
}
.admin-nav-items {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.admin-shell--side .admin-nav-items {
flex-direction: column;
flex-wrap: nowrap;
}
.admin-nav-item {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid transparent;
background: transparent;
color: #5c5348;
font-size: 13px;
line-height: 1.2;
padding: 7px 10px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
text-align: left;
}
.admin-shell--side .admin-nav-item {
width: 100%;
}
.admin-nav-item:hover {
background: rgba(168, 140, 107, 0.08);
color: #967b5c;
}
.admin-nav-item.active {
background: rgba(168, 140, 107, 0.14);
border-color: rgba(168, 140, 107, 0.28);
color: #a88c6b;
font-weight: 500;
}
.admin-nav-icon {
width: 1em;
text-align: center;
font-size: 12px;
opacity: 0.85;
}
.admin-nav-credit {
margin-top: 14px;
padding: 10px 8px 2px;
border-top: 1px dashed rgba(168, 140, 107, 0.22);
font-size: 11px;
letter-spacing: 0.08em;
color: #a88c6b;
text-align: center;
}
.admin-shell--top .admin-nav-credit {
width: 100%;
margin-top: 4px;
padding-top: 10px;
}
.admin-main {
min-width: 0;
flex: 1;
}
@media (max-width: 860px) {
.admin-shell--side {
flex-direction: column;
}
.admin-shell--side .admin-nav {
position: static;
width: 100%;
max-height: none;
}
.admin-shell--side .admin-nav-items {
flex-direction: row;
flex-wrap: wrap;
}
.admin-shell--side .admin-nav-item {
width: auto;
}
}
.admin-panel {
background: #fff;

View File

@@ -9,8 +9,8 @@
<div class="relative z-10 w-[360px] bg-white/80 backdrop-blur-xl border border-[#a88c6b]/15 rounded-3xl shadow-2xl p-9 animate-fade-up">
<div class="text-center mb-8">
<div class="text-3xl mb-3 text-[#a88c6b] font-serif"></div>
<h1 class="text-xl tracking-[0.3em] text-[#2c2c2c] font-light">婚礼 · 后台</h1>
<p class="text-[11px] text-[#8A8680] tracking-[0.3em] mt-2 uppercase">Admin Console</p>
<h1 class="text-xl tracking-[0.3em] text-[#2c2c2c] font-light">婚礼邀请函 · 后台</h1>
<p class="text-[11px] text-[#8A8680] tracking-[0.25em] mt-2">制作者 · 年糕崽崽</p>
</div>
<a-form :model="form" layout="vertical" @finish="onSubmit">