1. 礼物特效

This commit is contained in:
李琦
2026-08-03 14:27:32 +08:00
parent eac87759f3
commit fe10dd3fa5
14 changed files with 2787 additions and 24 deletions

View File

@@ -1054,11 +1054,176 @@
<template v-else-if="column.key==='created_at'">
<span class="text-[12px] text-[#8A8680]">{{ formatTime(record.created_at) }}</span>
</template>
<template v-else-if="column.key==='actions'">
<a-button size="small" danger type="text" @click="onDeleteRsvp(record)">删除</a-button>
</template>
</template>
</a-table>
</div>
</section>
<!-- 礼金 -->
<section v-if="activeTab === 'cash'">
<div class="admin-panel">
<div class="flex justify-between items-center mb-4 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">礼金记录</div>
<div class="text-[11px] text-[#8A8680] mt-0.5">
{{ cashGiftCount }} · 合计 ¥{{ cashGiftTotal }}
· 前台录入 <a class="text-[#a88c6b]" href="/lijin" target="_blank">/lijin</a>
· 明细 <a class="text-[#a88c6b]" href="/lijin/list" target="_blank">/lijin/list</a>
</div>
</div>
<div class="flex gap-2">
<a-button size="small" @click="openCashGiftCreate">新增</a-button>
<a-button size="small" :loading="cashGiftLoading" @click="loadCashGifts">
<i class="fa-solid fa-rotate-right mr-1"></i>刷新
</a-button>
</div>
</div>
<a-table
class="admin-table"
:columns="cashGiftColumns"
:data-source="cashGiftList"
:loading="cashGiftLoading"
row-key="id"
size="middle"
:pagination="{ pageSize: 10, hideOnSinglePage: true }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key==='amount'">
<span class="text-[#a88c6b] font-medium">¥{{ record.amount }}</span>
</template>
<template v-else-if="column.key==='created_at'">
<span class="text-[12px] text-[#8A8680]">{{ formatTime(record.created_at) }}</span>
</template>
<template v-else-if="column.key==='actions'">
<a-space>
<a-button size="small" type="link" class="!text-[#a88c6b] !px-1" @click="openCashGiftEdit(record)">编辑</a-button>
<a-button size="small" danger type="text" @click="onDeleteCashGift(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<div class="admin-panel mt-4">
<div class="flex justify-between items-center mb-3 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">记账口令</div>
<div class="text-[11px] text-[#8A8680] mt-0.5">
前台 <a class="text-[#a88c6b]" href="/lijin" target="_blank">/lijin</a> 登记礼金前需输入此口令
</div>
</div>
<a-button
size="small"
type="primary"
class="!bg-[#a88c6b] !border-[#a88c6b]"
:loading="cashEntryPassSaving"
@click="saveCashEntryPass"
>保存口令</a-button>
</div>
<a-input-password
v-model:value="cashEntryPass"
placeholder="设置记账口令"
class="!max-w-[320px]"
:maxlength="64"
/>
</div>
<div class="admin-panel mt-4">
<div class="flex justify-between items-center mb-3 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">查看全部口令</div>
<div class="text-[11px] text-[#8A8680] mt-0.5">
前台 <a class="text-[#a88c6b]" href="/lijin/list" target="_blank">/lijin/list</a> 需输入口令后查看完整礼金列表
</div>
</div>
<a-button
size="small"
type="primary"
class="!bg-[#a88c6b] !border-[#a88c6b]"
:loading="cashViewPassSaving"
@click="saveCashViewPass"
>保存口令</a-button>
</div>
<a-input-password
v-model:value="cashViewPass"
placeholder="设置查看口令"
class="!max-w-[320px]"
:maxlength="64"
/>
</div>
<div class="admin-panel mt-4">
<div class="flex justify-between items-center mb-3 gap-3 flex-wrap">
<div>
<div class="text-sm text-[#2c2c2c]">备注快捷选项</div>
<div class="text-[11px] text-[#8A8680] mt-0.5">前台礼金页可一键点选男方 / 女方分开配置</div>
</div>
<div class="flex gap-2">
<a-button size="small" @click="resetCashNotePresets">恢复默认</a-button>
<a-button
size="small"
type="primary"
class="!bg-[#a88c6b] !border-[#a88c6b]"
:loading="cashNotesSaving"
@click="saveCashNotePresets"
>保存选项</a-button>
</div>
</div>
<div class="flex flex-wrap gap-2 mb-3">
<a-tag
v-for="(n, i) in cashNotePresets"
:key="`${n}-${i}`"
closable
color="gold"
@close.prevent="removeCashNotePreset(i)"
>{{ n }}</a-tag>
</div>
<div class="flex gap-2 items-center flex-wrap">
<a-input
v-model:value="cashNoteDraft"
placeholder="新增快捷备注,如:男方亲戚"
class="!max-w-[280px]"
:maxlength="30"
@pressEnter="addCashNotePreset"
/>
<a-button size="small" @click="addCashNotePreset">添加</a-button>
</div>
</div>
<a-modal
v-model:open="cashGiftModalOpen"
:title="cashGiftEditingId ? '编辑礼金' : '新增礼金'"
ok-text="保存"
cancel-text="取消"
:confirm-loading="cashGiftSaving"
@ok="saveCashGiftModal"
>
<a-form layout="vertical" class="mt-2">
<a-form-item label="姓名" required>
<a-input v-model:value="cashGiftForm.name" placeholder="宾客姓名" :maxlength="20" />
</a-form-item>
<a-form-item label="金额(元)" required>
<a-input-number v-model:value="cashGiftForm.amount" :min="1" :max="10000000" class="w-full" />
</a-form-item>
<a-form-item label="备注">
<div class="flex flex-wrap gap-1.5 mb-2">
<a-tag
v-for="n in cashNotePresets"
:key="'m-' + n"
class="!cursor-pointer"
:color="cashGiftForm.note === n ? 'gold' : undefined"
@click="cashGiftForm.note = n"
>{{ n }}</a-tag>
</div>
<a-input v-model:value="cashGiftForm.note" placeholder="可选" :maxlength="100" />
</a-form-item>
</a-form>
</a-modal>
</section>
<!-- 弹幕审核 -->
<section v-if="activeTab === 'danmaku'">
<div class="admin-panel">
@@ -1134,9 +1299,13 @@ import { PieChart } from 'echarts/charts'
import { TooltipComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import {
getConfigSection, saveConfigSection, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig,
getConfigSection, saveConfigSection, uploadImage, getRsvpList, deleteRsvp, getUploadConfig, saveUploadConfig,
getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitList,
getPosterConfig, savePosterConfig, getGiftStats, saveGiftCosts,
getCashGiftList, updateCashGift, deleteCashGift, submitCashGift,
getCashGiftNotes, saveCashGiftNotes,
getCashGiftViewPass, saveCashGiftViewPass,
getCashGiftEntryPass, saveCashGiftEntryPass,
} from '@/api/wedding'
import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
@@ -1158,7 +1327,7 @@ 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 VALID_TABS = ['overview', 'gifts', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'rsvp', 'cash', 'danmaku']
/** 后台菜单分组 */
const adminMenuGroups = [
@@ -1188,6 +1357,7 @@ const adminMenuGroups = [
label: '宾客互动',
items: [
{ key: 'rsvp', label: '出席回执', icon: 'fa-solid fa-envelope-open-text' },
{ key: 'cash', label: '礼金', icon: 'fa-solid fa-yen-sign' },
{ key: 'danmaku', label: '弹幕审核', icon: 'fa-solid fa-comments' },
],
},
@@ -1258,6 +1428,22 @@ const posterMusicOptions = ref([])
const tabLoading = ref(false)
const rsvpLoading = ref(false)
const rsvpList = ref([])
const cashGiftLoading = ref(false)
const cashGiftSaving = ref(false)
const cashGiftList = ref([])
const cashGiftTotal = ref(0)
const cashGiftCount = ref(0)
const cashGiftModalOpen = ref(false)
const cashGiftEditingId = ref(0)
const cashGiftForm = reactive({ name: '', amount: 1000, note: '' })
const cashNotePresets = ref([])
const cashNoteDefaults = ref([])
const cashNoteDraft = ref('')
const cashNotesSaving = ref(false)
const cashViewPass = ref('')
const cashViewPassSaving = ref(false)
const cashEntryPass = ref('')
const cashEntryPassSaving = ref(false)
const danmakuLoading = ref(false)
const danmakuList = ref([])
const danmakuFilter = ref('pending')
@@ -2167,6 +2353,10 @@ async function onTabActivate(tab) {
await loadRsvp()
return
}
if (tab === 'cash') {
await Promise.all([loadCashGifts(), loadCashNotePresets(), loadCashViewPass(), loadCashEntryPass()])
return
}
if (tab === 'danmaku') {
await loadDanmaku()
}
@@ -2190,6 +2380,7 @@ const rsvpColumns = [
{ title: '出席', dataIndex: 'guest_count', key: 'guest_count' },
{ title: '祝福', dataIndex: 'wishes', key: 'wishes', ellipsis: true },
{ title: '提交时间', dataIndex: 'created_at', key: 'created_at' },
{ title: '操作', key: 'actions', width: 90 },
]
const rsvpCountLabel = (v) => ({ '1': '1 人出席', '2': '2 人出席', '3': '3 人及以上', '0': '遗憾缺席' }[v] || v)
function formatTime(t) {
@@ -2204,6 +2395,182 @@ async function loadRsvp() {
catch (e) { /* 忽略 */ }
finally { rsvpLoading.value = false }
}
async function onDeleteRsvp(record) {
if (!record?.id) return
try {
await deleteRsvp(record.id)
message.success('已删除')
await loadRsvp()
} catch (e) {
message.error(e?.message || '删除失败')
}
}
const cashGiftColumns = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 120 },
{ title: '金额', key: 'amount', width: 110 },
{ title: '备注', dataIndex: 'note', key: 'note', ellipsis: true },
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
{ title: '操作', key: 'actions', width: 140 },
]
async function loadCashGifts() {
cashGiftLoading.value = true
try {
const res = await getCashGiftList()
const data = res.data || {}
cashGiftList.value = Array.isArray(data.list) ? data.list : []
cashGiftTotal.value = Number(data.total) || 0
cashGiftCount.value = Number(data.count) || cashGiftList.value.length
} catch {
cashGiftList.value = []
cashGiftTotal.value = 0
cashGiftCount.value = 0
} finally {
cashGiftLoading.value = false
}
}
function openCashGiftCreate() {
cashGiftEditingId.value = 0
cashGiftForm.name = ''
cashGiftForm.amount = 1000
cashGiftForm.note = ''
cashGiftModalOpen.value = true
}
function openCashGiftEdit(record) {
cashGiftEditingId.value = record.id
cashGiftForm.name = record.name || ''
cashGiftForm.amount = Number(record.amount) || 0
cashGiftForm.note = record.note || ''
cashGiftModalOpen.value = true
}
async function saveCashGiftModal() {
const name = String(cashGiftForm.name || '').trim()
const amount = Math.round(Number(cashGiftForm.amount))
if (!name) {
message.error('请填写姓名')
return Promise.reject()
}
if (!Number.isFinite(amount) || amount <= 0) {
message.error('请填写正确金额')
return Promise.reject()
}
cashGiftSaving.value = true
try {
const payload = { name, amount, note: String(cashGiftForm.note || '').trim() }
if (cashGiftEditingId.value) await updateCashGift(cashGiftEditingId.value, payload)
else await submitCashGift(payload)
message.success('已保存')
cashGiftModalOpen.value = false
await loadCashGifts()
} catch (e) {
if (e) message.error(e?.message || '保存失败')
return Promise.reject(e)
} finally {
cashGiftSaving.value = false
}
}
async function onDeleteCashGift(record) {
if (!record?.id) return
try {
await deleteCashGift(record.id)
message.success('已删除')
await loadCashGifts()
} catch (e) {
message.error(e?.message || '删除失败')
}
}
async function loadCashNotePresets() {
try {
const res = await getCashGiftNotes()
cashNotePresets.value = Array.isArray(res.data?.notes) ? [...res.data.notes] : []
cashNoteDefaults.value = Array.isArray(res.data?.defaults) ? [...res.data.defaults] : []
} catch {
cashNotePresets.value = []
cashNoteDefaults.value = []
}
}
async function loadCashViewPass() {
try {
const res = await getCashGiftViewPass()
cashViewPass.value = res.data?.password || ''
} catch {
cashViewPass.value = ''
}
}
async function saveCashViewPass() {
const password = String(cashViewPass.value || '').trim()
if (!password) return message.error('请设置查看口令')
cashViewPassSaving.value = true
try {
await saveCashGiftViewPass(password)
message.success('查看口令已保存')
} catch (e) {
message.error(e?.message || '保存失败')
} finally {
cashViewPassSaving.value = false
}
}
async function loadCashEntryPass() {
try {
const res = await getCashGiftEntryPass()
cashEntryPass.value = res.data?.password || ''
} catch {
cashEntryPass.value = ''
}
}
async function saveCashEntryPass() {
const password = String(cashEntryPass.value || '').trim()
if (!password) return message.error('请设置记账口令')
cashEntryPassSaving.value = true
try {
await saveCashGiftEntryPass(password)
message.success('记账口令已保存')
} catch (e) {
message.error(e?.message || '保存失败')
} finally {
cashEntryPassSaving.value = false
}
}
function addCashNotePreset() {
const v = String(cashNoteDraft.value || '').trim()
if (!v) return
if (cashNotePresets.value.includes(v)) {
message.warning('已存在该选项')
return
}
cashNotePresets.value.push(v)
cashNoteDraft.value = ''
}
function removeCashNotePreset(index) {
cashNotePresets.value.splice(index, 1)
}
function resetCashNotePresets() {
cashNotePresets.value = cashNoteDefaults.value.length
? [...cashNoteDefaults.value]
: ['男方亲戚', '女方亲戚', '男方朋友', '女方朋友', '男方同学', '女方同学', '男方同事', '女方同事', '男方邻居', '女方邻居', '其他']
}
async function saveCashNotePresets() {
if (!cashNotePresets.value.length) return message.error('至少保留一个快捷备注')
cashNotesSaving.value = true
try {
const res = await saveCashGiftNotes(cashNotePresets.value)
cashNotePresets.value = Array.isArray(res.data?.notes) ? [...res.data.notes] : [...cashNotePresets.value]
message.success('备注选项已保存')
} catch (e) {
message.error(e?.message || '保存失败')
} finally {
cashNotesSaving.value = false
}
}
const danmakuColumns = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
@@ -2353,7 +2720,8 @@ onUnmounted(() => {
box-sizing: border-box;
}
.admin-shell--top {
max-width: 1040px;
max-width: 80vw;
width: 80vw;
}
.admin-shell--side {
max-width: 1220px;