1. 礼物特效

This commit is contained in:
李琦
2026-08-03 12:20:55 +08:00
parent 63fea7e903
commit 170102b0d8
12 changed files with 813 additions and 216 deletions

View File

@@ -76,22 +76,6 @@
</div>
</div>
<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">各礼物收到次数 · 合计 {{ giftStats.total }}</div>
</div>
<a-button size="small" :loading="giftStatsLoading" @click="loadGiftStats">
<i class="fa-solid fa-rotate-right mr-1"></i>刷新
</a-button>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-5">
<div v-for="g in giftTypeList" :key="g.key" class="stat-card">
<div class="stat-label calligraphy">{{ g.label }}</div>
<div class="stat-value">{{ giftStats.counts[g.key] || 0 }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-5">
<div class="chart-card">
<div class="text-[12px] text-[#7a6550] mb-2 tracking-wide">地区分布</div>
@@ -139,6 +123,54 @@
</div>
</a-tab-pane>
<!-- 礼物 -->
<a-tab-pane key="gifts" tab="礼物">
<div v-if="activeTab === 'gifts'" 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>
<div class="text-[11px] text-[#8A8680] mt-0.5">各礼物收到次数 · 合计 {{ giftStats.total }}</div>
</div>
<a-button size="small" :loading="giftStatsLoading" @click="loadGiftStats">
<i class="fa-solid fa-rotate-right mr-1"></i>刷新
</a-button>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
<div v-for="g in giftTypeList" :key="g.key" class="stat-card">
<div class="stat-label calligraphy">{{ g.label }}</div>
<div class="stat-value">{{ giftStats.counts[g.key] || 0 }}</div>
<div class="text-[10px] text-[#8A8680] mt-1">兑换 {{ giftCostsEdit[g.key] || g.cost }} </div>
</div>
</div>
<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">每个礼物消耗的点赞次数 · 默认阶梯 50 / 100 / 200</div>
</div>
<div class="flex gap-2">
<a-button size="small" @click="resetGiftCostsEdit">恢复默认</a-button>
<a-button size="small" type="primary" class="!bg-[#a88c6b]" :loading="giftCostsSaving" @click="saveGiftCostsEdit">
保存配置
</a-button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div v-for="g in giftTypeList" :key="'cost-' + g.key" class="stat-card !py-3">
<div class="stat-label calligraphy mb-2">{{ g.label }}</div>
<a-input-number
v-model:value="giftCostsEdit[g.key]"
:min="1"
:max="10000"
:step="10"
class="w-full"
addon-after=""
/>
</div>
</div>
</div>
</a-tab-pane>
<!-- 访客记录 -->
<a-tab-pane key="visits" tab="访客记录">
<div v-if="activeTab === 'visits'" class="admin-panel" :class="{ 'opacity-60 pointer-events-none': tabLoading }">
@@ -983,13 +1015,13 @@ import { CanvasRenderer } from 'echarts/renderers'
import {
getConfigSection, saveConfigSection, uploadImage, getRsvpList, getUploadConfig, saveUploadConfig,
getDanmakuList, updateDanmakuStatus, getVisitStats, getVisitList,
getPosterConfig, savePosterConfig, getGiftStats,
getPosterConfig, savePosterConfig, getGiftStats, saveGiftCosts,
} from '@/api/wedding'
import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
import { GIFT_TYPES } from '@/utils/giftTypes'
import { GIFT_TYPES, emptyGiftCosts, DEFAULT_GIFT_COSTS } from '@/utils/giftTypes'
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
import {
defaultPosterBundle, normalizePosterBundle, normalizePosterConfig,
@@ -1003,7 +1035,7 @@ const adminStore = useAdminStore()
const TAB_KEY = 'wedding_admin_active_tab'
const CONFIG_SECTIONS = ['basic', 'copy', 'visual', 'music', 'photos', 'schedule']
const VALID_TABS = ['overview', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'rsvp', 'danmaku']
const VALID_TABS = ['overview', 'gifts', 'visits', ...CONFIG_SECTIONS, 'upload', 'poster', 'rsvp', 'danmaku']
const SECTION_KEYS = {
basic: ['groom', 'bride', 'date', 'lunar', 'calendarDate', 'hotel', 'address', 'heroImg', 'endImg'],
@@ -1044,11 +1076,42 @@ const danmakuFilter = ref('pending')
const visitStatsLoading = ref(false)
const visitListLoading = ref(false)
const giftStatsLoading = ref(false)
const giftCostsSaving = ref(false)
const giftTypeList = GIFT_TYPES
const giftStats = reactive({
total: 0,
counts: Object.fromEntries(GIFT_TYPES.map((g) => [g.key, 0])),
})
const giftCostsEdit = reactive(emptyGiftCosts())
function applyGiftCostsFromServer(costs) {
if (!costs || typeof costs !== 'object') return
for (const g of GIFT_TYPES) {
const v = Number(costs[g.key])
if (Number.isFinite(v) && v > 0) giftCostsEdit[g.key] = v
}
}
function resetGiftCostsEdit() {
Object.assign(giftCostsEdit, emptyGiftCosts())
}
async function saveGiftCostsEdit() {
giftCostsSaving.value = true
try {
const payload = {}
for (const g of GIFT_TYPES) {
payload[g.key] = Number(giftCostsEdit[g.key]) || DEFAULT_GIFT_COSTS[g.key]
}
const res = await saveGiftCosts(payload)
applyGiftCostsFromServer(res.data?.costs || payload)
message.success('礼物兑换配置已保存')
} catch (e) {
message.error(e?.message || '保存失败')
} finally {
giftCostsSaving.value = false
}
}
const visitStats = reactive({ uv: 0, pv: 0, today_uv: 0, regions: [], devices: [] })
const visitList = ref([])
const visitPreviewList = ref([])
@@ -1728,6 +1791,7 @@ async function loadGiftStats() {
giftStats.counts[g.key] = Number(counts[g.key]) || 0
}
giftStats.total = Number(d.total) || Object.values(giftStats.counts).reduce((a, b) => a + b, 0)
applyGiftCostsFromServer(d.costs)
} catch (e) {
message.error(e?.message || '加载礼物统计失败')
} finally {
@@ -1787,7 +1851,7 @@ async function loadVisitList() {
}
async function loadOverview() {
await Promise.all([loadVisitStats(), loadVisitPreview(), loadGiftStats()])
await Promise.all([loadVisitStats(), loadVisitPreview()])
}
function onVisitTableChange(pag) {
@@ -1803,6 +1867,10 @@ async function onTabActivate(tab) {
await loadOverview()
return
}
if (tab === 'gifts') {
await loadGiftStats()
return
}
if (tab === 'visits') {
await loadVisitList()
return