1. 礼物特效
This commit is contained in:
@@ -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_configs:1 男方 /hb-n,2 女方 /hb-nv,3 回门宴 /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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user