feat: 迁移渠道与游戏统计及全量汇总

This commit is contained in:
Yvan
2026-11-04 01:08:33 +08:00
parent 6703a66f0d
commit fc27120950
31 changed files with 1492 additions and 6 deletions
+2
View File
@@ -0,0 +1,2 @@
import service from '@/utils/request'
export const listAnalytics = (kind, params) => service({ url: `/sw/${kind === 'channel' ? 'channelData' : 'gameData'}/list`, method: 'get', params })
+2
View File
@@ -56,7 +56,9 @@
"/src/view/sw/AlertRules.vue": "SwAlertRules",
"/src/view/sw/BusinessList.vue": "SwBusinessList",
"/src/view/sw/ChannelActivationList.vue": "SwChannelActivationList",
"/src/view/sw/ChannelDataStatistics.vue": "SwChannelDataStatistics",
"/src/view/sw/ChannelList.vue": "SwChannelList",
"/src/view/sw/GameAnalytics.vue": "SwGameAnalytics",
"/src/view/sw/RechargeDetails.vue": "SwRechargeDetails",
"/src/view/sw/RegistrationDetails.vue": "SwRegistrationDetails",
"/src/view/sw/components/BusinessBindings.vue": "SwBusinessBindings",
@@ -0,0 +1,5 @@
<template><report-analytics kind="channel" /></template>
<script setup>
import ReportAnalytics from './components/ReportAnalytics.vue'
defineOptions({ name: 'SwChannelDataStatistics' })
</script>
+5
View File
@@ -0,0 +1,5 @@
<template><report-analytics kind="game" /></template>
<script setup>
import ReportAnalytics from './components/ReportAnalytics.vue'
defineOptions({ name: 'SwGameAnalytics' })
</script>
@@ -0,0 +1,90 @@
<template>
<div>
<div v-if="isChannel" v-loading="loading" class="report-cards">
<el-card v-for="card in cards" :key="card.label" shadow="never"><div class="report-card-label">{{ card.label }}</div><strong>{{ card.value }}</strong></el-card>
</div>
<div class="kra-search-box">
<el-form :model="query" :inline="true" @submit.prevent="search">
<el-form-item label="所属平台"><el-select v-model="sources" multiple collapse-tags clearable placeholder="选择平台" style="width: 210px" @change="sourceChanged"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="游戏"><el-select v-model="games" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @visible-change="open => open && loadGames('')"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item v-if="isChannel" label="渠道类型"><el-select v-model="query.promoteType" clearable placeholder="全部渠道类型" style="width: 150px"><el-option label="公会" :value="1" /><el-option label="个人" :value="0" /></el-select></el-form-item>
<el-form-item v-if="isChannel" label="渠道账号"><el-input v-model="query.promoteName" clearable placeholder="渠道账号(模糊匹配)" /></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">{{ isChannel ? '搜索' : '查询' }}</el-button></el-form-item>
</el-form>
</div>
<div class="kra-table-box">
<div v-if="!isChannel" class="report-heading"><strong>核心指标</strong><el-text type="info">统计周期:{{ period }}</el-text></div>
<el-alert v-if="error" class="mb-4" type="error" :closable="false" :title="error" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="统计所需业务汇总尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${isChannel ? row.promoteId : row.gameId}`" :default-sort="{ prop: 'totalPay', order: 'descending' }" :show-summary="!isChannel" :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column :prop="isChannel ? 'promoteName' : 'gameName'" :label="isChannel ? '渠道名称' : '游戏名称'" min-width="170" show-overflow-tooltip />
<el-table-column v-if="isChannel" prop="busierName" label="归属商务" min-width="140" show-overflow-tooltip />
<el-table-column v-if="isChannel" label="渠道类型" min-width="100"><template #default="{ row }">{{ { 0: '个人', 1: '公会' }[row.promoteType] ?? row.promoteType ?? '—' }}</template></el-table-column>
<el-table-column v-else prop="gameId" label="游戏ID" min-width="100" />
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.width || 110" sortable="custom"><template #default="{ row }">{{ format(column.type, row[column.key]) }}</template></el-table-column>
</el-table>
<div class="kra-pagination"><el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listAnalytics } from '@/api/analytics'
import { listLedgerGames } from '@/api/ledger'
import { channelCount, channelMoney } from '../channel-display'
const props = defineProps({ kind: { type: String, required: true } })
const isChannel = computed(() => props.kind === 'channel')
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
const day = () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()) }
const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`
const end = day(), begin = day(); begin.setDate(begin.getDate() - 29)
const dates = ref([ymd(begin), ymd(end)])
const shortcuts = [
{ text: '今日', value: () => [day(), day()] }, { text: '昨日', value: () => { const d = day(); d.setDate(d.getDate() - 1); return [d, new Date(d)] } },
{ text: '本周', value: () => { const e = day(), b = day(); b.setDate(b.getDate() - (b.getDay() || 7) + 1); return [b, e] } },
{ text: '本月', value: () => { const e = day(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } }
]
const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), gameLoading = ref(false)
const query = reactive({ pageNum: 1, pageSize: 10, promoteType: undefined, promoteName: '', sortField: 'totalPay', sortOrder: 'desc' })
const rows = ref([]), total = ref(0), summary = ref(null), result = ref(null), loading = ref(false), available = ref(true), error = ref('')
let version = 0, gameVersion = 0
const period = computed(() => result.value?.beginTime && result.value?.endTime ? `${result.value.beginTime} 至 ${result.value.endTime}` : dates.value.join(' 至 '))
const fields = { newRegisterCount: { key: 'newRegisterCount', label: '新增注册', type: 'count' }, activeCount: { key: 'activeCount', label: '活跃玩家', type: 'count' }, payUserCount: { key: 'payUserCount', label: '付费玩家', type: 'count' }, newPayUserCount: { key: 'newPayUserCount', label: '新增付费', type: 'count' }, totalPay: { key: 'totalPay', label: '充值金额', type: 'money', width: 145 }, payRate: { key: 'payRate', label: '付费率', type: 'percent' }, arpu: { key: 'arpu', label: 'ARPU', type: 'money' }, arppu: { key: 'arppu', label: 'ARPPU', type: 'money' } }
const columns = computed(() => (isChannel.value ? ['newRegisterCount', 'activeCount', 'totalPay', 'payUserCount', 'newPayUserCount', 'payRate'] : ['newRegisterCount', 'activeCount', 'payUserCount', 'totalPay', 'payRate', 'arpu', 'arppu', 'newPayUserCount']).map(key => fields[key]))
const format = (type, value) => value == null ? '未接入' : type === 'money' ? channelMoney(value) : type === 'percent' ? `${value}%` : channelCount(value)
const cards = computed(() => [{ label: '渠道总数', key: 'channelCount', type: 'count' }, { label: '渠道总注册玩家', key: 'totalRegisterCount', type: 'count' }, { label: '渠道总充值', key: 'totalPayAmount', type: 'money' }, { label: '平均付费率', key: 'avgPayRate', type: 'percent' }].map(card => ({ label: card.label, value: error.value ? '未获取' : format(card.type, result.value?.[card.key]) })))
const summaries = ({ columns: tableColumns }) => tableColumns.map((column, index) => { if (index === 0) return '合计'; if (index === 1) return summary.value?.gameName || ''; const field = fields[column.property]; return field ? format(field.type, summary.value?.[column.property]) : '' })
const clear = () => { rows.value = []; total.value = 0; summary.value = null; result.value = null }
const load = async () => {
const current = ++version
if (!sources.value.length) { clear(); loading.value = false; available.value = true; return }
loading.value = true; error.value = ''
try {
const res = await listAnalytics(props.kind, { ...query, promoteName: query.promoteName.trim(), sourceDb: sources.value.join(','), gameName: games.value.join(','), beginTime: dates.value[0], endTime: dates.value[1] })
if (current !== version) return
if (res.code === 0) { result.value = res.data; rows.value = res.data.rows; total.value = res.data.total; summary.value = res.data.summary; available.value = res.data.available !== false }
else { clear(); error.value = res.msg || '统计查询失败' }
} catch { if (current === version) { clear(); error.value = '统计查询失败,请重试' } } finally { if (current === version) loading.value = false }
}
const search = () => { query.pageNum = 1; if (!sources.value.length) ElMessage.warning('请选择平台'); return load() }
const sortChanged = ({ prop, order }) => { query.sortField = prop || 'totalPay'; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return search() }
const sourceChanged = () => { ++gameVersion; games.value = []; gameOptions.value = []; gameLoading.value = false }
const loadGames = async (keyword = '') => {
const current = ++gameVersion; if (!sources.value.length) return
gameLoading.value = true
try { const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: 50 }); if (current === gameVersion && res.code === 0) gameOptions.value = [...new Set([...games.value, ...res.data.map(row => row.gameName).filter(Boolean)])] } finally { if (current === gameVersion) gameLoading.value = false }
}
onMounted(load)
</script>
<style scoped>
.report-cards { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
.report-card-label { color: var(--el-text-color-secondary); font-size: 13px; margin-bottom: 12px; }
.report-cards strong { font-size: 25px; overflow-wrap: anywhere; }
.report-heading { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
@media (max-width: 800px) { .report-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>