1. 礼物特效
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user