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