feat: 按原型更新商务明细与游戏分析并清理临时资料

This commit is contained in:
Yvan
2026-11-04 18:13:03 +08:00
parent 19926d275c
commit f866cac768
41 changed files with 392 additions and 2955 deletions
+12 -9
View File
@@ -3,7 +3,7 @@
<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 :reserve-keyword="false" :loading="gameLoading" :disabled="!sources.length" placeholder="请选择游戏" style="width: 200px" @change="gameChanged"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item label="游戏"><el-select v-model="selectedGame" filterable :reserve-keyword="false" :loading="gameLoading" :disabled="!sources.length" placeholder="请选择游戏" style="width: 200px" @change="gameChanged"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item label="区服"><el-select v-model="query.serverNum" clearable filterable :loading="serverLoading" :disabled="!sources.length || !games.length" placeholder="全部区服" style="width: 200px"><el-option label="全部区服" value="" /><el-option v-for="row in serverOptions" :key="row.serverNum" :label="row.serverName ? `${row.serverName} (${row.serverNum})` : row.serverNum" :value="row.serverNum" /></el-select></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">搜索</el-button></el-form-item>
@@ -13,12 +13,12 @@
<div class="server-heading"><strong>区服数据</strong><el-text type="info">统计周期:{{ period }}</el-text></div>
<el-alert v-if="error || optionError" class="mb-4" type="error" :closable="false" :title="error || optionError" />
<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}:${row.gameId}:${row.serverNum}`" :default-sort="{ prop: 'totalPay', order: 'descending' }" show-summary :summary-method="summaries" @sort-change="sortChanged">
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.gameId}:${row.serverNum}`" show-summary :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column prop="serverNum" label="区服编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="serverName" label="区服名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="openTime" label="开服日期" min-width="120" sortable="custom"><template #default="{ row }">{{ openDate(row.openTime) }}</template></el-table-column>
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.money ? 140 : 110" sortable="custom"><template #default="{ row }">{{ format(column, row[column.key]) }}</template></el-table-column>
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.money ? 140 : 110" sortable="custom"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><template #default="{ row }">{{ format(column, 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>
@@ -31,6 +31,8 @@ import { ElMessage } from 'element-plus'
import { listLedgerGames } from '@/api/ledger'
import { listServerAnalysis, listServerOptions } from '@/api/serverAnalysis'
import { channelCount, channelMoney } from './channel-display'
import { metricTips } from './table-display'
import { analysisGameSelection } from './analysis-game-selection'
defineOptions({ name: 'ServerAnalysis' })
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
@@ -45,7 +47,8 @@ const shortcuts = [
{ text: '本月', value: () => { const e = day(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } }
]
const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), serverOptions = ref([])
const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: 'totalPay', sortOrder: 'desc' })
const selectedGame = computed({ get: () => games.value[0] || '', set: value => { games.value = value ? [value] : [] } })
const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: '', sortOrder: 'desc' })
const rows = ref([]), total = ref(0), summary = ref(null), result = ref(null), loading = ref(false), available = ref(true), error = ref(''), optionError = ref('')
const gameLoading = ref(false), serverLoading = ref(false)
let version = 0, gameVersion = 0, serverVersion = 0
@@ -60,7 +63,7 @@ const openDate = value => {
const summaries = ({ columns: tableColumns }) => tableColumns.map((column, index) => {
if (index === 0) return '合计'
const field = columns.find(item => item.key === column.property)
return field ? format(field, summary.value?.[column.property]) : '—'
return field ? format(field, summary.value?.[column.property]) : ''
})
const clear = () => { ++version; rows.value = []; total.value = 0; summary.value = null; result.value = null; loading.value = false; error.value = ''; available.value = true }
const params = () => ({ ...query, sourceDb: sources.value.join(','), gameName: games.value.join(','), beginTime: dates.value[0], endTime: dates.value[1] })
@@ -80,7 +83,7 @@ const search = () => {
else if (!games.value.length) ElMessage.warning('请选择游戏')
return load()
}
const sortChanged = ({ prop, order }) => { query.pageNum = 1; query.sortField = order ? prop : 'totalPay'; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return load() }
const sortChanged = ({ prop, order }) => { query.pageNum = 1; query.sortField = order ? prop : ''; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return load() }
const loadGames = async () => {
const current = ++gameVersion; gameLoading.value = true; optionError.value = ''
try {
@@ -104,13 +107,13 @@ const loadServers = async () => {
const sourceChanged = async () => {
++gameVersion; ++serverVersion; gameLoading.value = false; serverLoading.value = false
games.value = []; gameOptions.value = []; query.serverNum = ''; serverOptions.value = []; optionError.value = ''; clear()
if (sources.value.length) await loadGames()
if (sources.value.length) { await loadGames(); if (gameOptions.value.length) { selectedGame.value = gameOptions.value[0]; await gameChanged() } }
}
const gameChanged = () => { query.serverNum = ''; query.pageNum = 1; clear(); return loadServers() }
const gameChanged = () => { analysisGameSelection.value = selectedGame.value; query.serverNum = ''; query.pageNum = 1; clear(); return Promise.all([loadServers(), load()]) }
onMounted(async () => {
await loadGames()
if (!gameOptions.value.length) return
games.value = [gameOptions.value[0]]
selectedGame.value = gameOptions.value.includes(analysisGameSelection.value) ? analysisGameSelection.value : gameOptions.value[0]
await Promise.all([loadServers(), load()])
})
</script>
@@ -0,0 +1,5 @@
import { ref } from 'vue'
// Preserve the game while moving between analysis pages; validate it against
// the current account's available options before using it on a new page.
export const analysisGameSelection = ref(null)
+13 -8
View File
@@ -6,7 +6,7 @@
<el-form-item label="商务账号"><el-input v-model="query.busierAccount" clearable placeholder="商务账号" /></el-form-item>
<el-form-item label="玩家ID"><el-input v-model="playerID" clearable placeholder="玩家ID" /></el-form-item>
<el-form-item v-if="spend" label="角色名"><el-input v-model="query.gamePlayerName" clearable placeholder="角色名" /></el-form-item>
<el-form-item label="渠道账号"><el-input v-model="query.promoteAccount" clearable placeholder="渠道账号(精确匹配)" /></el-form-item>
<el-form-item label="渠道名称"><el-input v-model="query.promoteAccount" clearable placeholder="渠道名称或账号(模糊匹配)" /></el-form-item>
<el-form-item v-if="spend" label="订单号"><el-input v-model="query.payOrderNumber" clearable placeholder="订单号" /></el-form-item>
<el-form-item label="游戏"><el-select v-model="gameNames" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gamesLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 200px" @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="spend" label="方式"><el-select v-model="query.payWay" clearable placeholder="全部方式" style="width: 140px"><el-option v-for="(label, value) in payWays" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
@@ -19,17 +19,17 @@
<div class="kra-btn-list"><el-button v-if="canExport" type="warning" plain icon="download" :loading="exporting" @click="download">导出</el-button></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}:${row.id}`" :row-class-name="rowClass" :default-sort="{ prop: spend ? 'payTime' : 'registerTime', order: 'descending' }" @sort-change="sortChanged">
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`" :row-class-name="rowClass">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column v-if="spend" prop="id" label="ID" min-width="90" />
<el-table-column prop="userId" label="玩家ID" min-width="110" />
<el-table-column v-if="!spend" prop="registerTime" label="注册时间" min-width="145" sortable="custom"><template #default="{ row }">{{ unixTime(row.registerTime) }}</template></el-table-column>
<el-table-column v-if="!spend" prop="registerTime" label="注册时间" min-width="165" sortable :sort-method="(a, b) => Number(a.registerTime) - Number(b.registerTime)"><template #default="{ row }">{{ unixTime(row.registerTime) }}</template></el-table-column>
<el-table-column v-if="spend" prop="payOrderNumber" label="订单号" min-width="190" show-overflow-tooltip />
<el-table-column :prop="spend ? 'gameName' : 'fgameName'" label="游戏" min-width="150" show-overflow-tooltip />
<el-table-column v-if="spend" prop="serverName" label="区服" min-width="140" show-overflow-tooltip />
<el-table-column v-if="spend" prop="gamePlayerName" label="角色" min-width="130" show-overflow-tooltip />
<el-table-column :prop="spend ? 'payAmount' : 'totalPayAmount'" :label="spend ? '实付金额' : '累计充值'" min-width="150" sortable="custom"><template #default="{ row }">{{ channelMoney(spend ? row.payAmount ?? '0.00' : row.totalPayAmount) }}</template></el-table-column>
<el-table-column v-if="spend" prop="payTime" label="充值时间" min-width="145" sortable="custom"><template #default="{ row }">{{ unixTime(row.payTime) }}</template></el-table-column>
<el-table-column :prop="spend ? 'payAmount' : 'totalPayAmount'" :label="spend ? '实付金额' : '累计充值'" min-width="150" sortable :sort-method="(a, b) => compareAmounts(a[spend ? 'payAmount' : 'totalPayAmount'], b[spend ? 'payAmount' : 'totalPayAmount'])"><template #default="{ row }">{{ channelMoney(spend ? row.payAmount ?? '0.00' : row.totalPayAmount) }}</template></el-table-column>
<el-table-column v-if="spend" prop="payTime" label="充值时间" min-width="165" sortable :sort-method="(a, b) => Number(a.payTime) - Number(b.payTime)"><template #default="{ row }">{{ unixTime(row.payTime) }}</template></el-table-column>
<el-table-column v-if="spend" label="方式" min-width="100"><template #default="{ row }">{{ payWays[row.payWay] ?? row.payWay ?? '—' }}</template></el-table-column>
<el-table-column v-if="spend" label="状态" min-width="110"><template #default="{ row }"><el-tag :type="{ 1: 'success', 2: 'warning', 0: 'danger' }[row.payStatus] || 'info'">{{ statuses[row.payStatus] ?? row.payStatus ?? '—' }}</el-tag></template></el-table-column>
<el-table-column :label="spend ? 'IP' : '注册IP'" min-width="130"><template #default="{ row }">{{ (spend ? row.spendIp : row.registerIp) || '—' }}</template></el-table-column>
@@ -50,6 +50,7 @@ import { useUserStore } from '@/pinia/modules/user'
import { useBtnAuth } from '@/utils/btnAuth'
import { listLedger, exportLedger, listLedgerGames } from '@/api/ledger'
import { channelMoney } from '../channel-display'
import { compareAmounts } from '../table-display'
const props = defineProps({ kind: { type: String, required: true } })
const spend = computed(() => props.kind === 'spend')
const user = useUserStore(), buttons = useBtnAuth()
@@ -94,18 +95,22 @@ const load = async () => {
} catch { if (version === listVersion) { rows.value = []; total.value = 0; totalPayAmount.value = null; error.value = '明细查询失败,请重试' } } finally { if (version === listVersion) loading.value = false }
}
const search = () => { query.pageNum = 1; if (!sources.value.length) ElMessage.warning('请选择平台'); return load() }
const sortChanged = ({ prop, order }) => { query.sortField = prop || (spend.value ? 'payTime' : 'registerTime'); query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return search() }
const sourceChanged = () => { ++gameVersion; gameNames.value = []; gameOptions.value = []; gamesLoading.value = false }
const loadGames = async (keyword = '') => {
const version = ++gameVersion; if (!sources.value.length) return
gamesLoading.value = true
try { const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: 50 }); if (version === gameVersion && res.code === 0) gameOptions.value = [...new Set([...gameNames.value, ...res.data.map(row => row.gameName).filter(Boolean)])] } finally { if (version === gameVersion) gamesLoading.value = false }
try {
const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: 50 })
if (version !== gameVersion) return
if (res.code === 0) gameOptions.value = [...new Set([...gameNames.value, ...res.data.map(row => row.gameName).filter(Boolean)])]
else { gameOptions.value = []; ElMessage.error(res.msg || '游戏选项读取失败') }
} catch { if (version === gameVersion) { gameOptions.value = []; ElMessage.error('游戏选项读取失败,请重试') } } finally { if (version === gameVersion) gamesLoading.value = false }
}
const unixTime = value => {
if (value == null) return '—'
const date = new Date(Number(value) * 1000)
if (Number.isNaN(date.getTime())) return '—'
return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
return `${ymd(date)} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
const rowClass = ({ row }) => spend.value && Number(row.payAmount) >= 5000 ? 'large-recharge-row' : ''
const download = async () => {
+28 -10
View File
@@ -6,7 +6,10 @@
<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 label="游戏">
<el-select v-if="isChannel" 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-select v-else v-model="selectedGame" clearable filterable :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @change="gameChanged"><el-option label="全部游戏" value="" /><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>
@@ -17,13 +20,13 @@
<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 v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${isChannel ? row.promoteId : row.gameId}`" :default-sort="isChannel ? { 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-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.width || 110" sortable="custom"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><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>
@@ -36,12 +39,14 @@ import { ElMessage } from 'element-plus'
import { listAnalytics } from '@/api/analytics'
import { listLedgerGames } from '@/api/ledger'
import { channelCount, channelMoney } from '../channel-display'
import { metricTips } from '../table-display'
import { analysisGameSelection } from '../analysis-game-selection'
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 end = day(), begin = day(); if (isChannel.value) 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)] } },
@@ -49,7 +54,8 @@ const shortcuts = [
{ 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 selectedGame = computed({ get: () => games.value[0] || '', set: value => { games.value = value ? [value] : [] } })
const query = reactive({ pageNum: 1, pageSize: 10, promoteType: undefined, promoteName: '', sortField: isChannel.value ? '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(' 至 '))
@@ -57,7 +63,7 @@ const fields = { newRegisterCount: { key: 'newRegisterCount', label: '新增注
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 summaries = ({ columns: tableColumns }) => tableColumns.map((column, index) => { if (index === 0) return '全部游戏'; if (index === 1) return ''; 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
@@ -71,14 +77,26 @@ const load = async () => {
} 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 sortChanged = ({ prop, order }) => { query.sortField = order ? prop : isChannel.value ? 'totalPay' : ''; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return search() }
const gameChanged = () => { analysisGameSelection.value = selectedGame.value; return search() }
const sourceChanged = async () => {
++version; ++gameVersion; games.value = []; gameOptions.value = []; gameLoading.value = false; loading.value = false; error.value = ''; clear()
if (!isChannel.value) { analysisGameSelection.value = ''; if (sources.value.length) await loadGames(); await search() }
}
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 }
try {
const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: isChannel.value ? 50 : 10000 })
if (current !== gameVersion) return
if (res.code === 0) gameOptions.value = [...new Set(res.data.map(row => row.gameName).filter(Boolean))]
else { gameOptions.value = []; ElMessage.error(res.msg || '游戏选项读取失败') }
} catch { if (current === gameVersion) { gameOptions.value = []; ElMessage.error('游戏选项读取失败,请重试') } } finally { if (current === gameVersion) gameLoading.value = false }
}
onMounted(load)
onMounted(async () => {
if (!isChannel.value) { await loadGames(); if (gameOptions.value.includes(analysisGameSelection.value)) selectedGame.value = analysisGameSelection.value }
await load()
})
</script>
<style scoped>
+24
View File
@@ -0,0 +1,24 @@
// Amounts arrive as fixed-point strings; preserve cents beyond Number's safe range.
export const compareAmounts = (a, b) => {
const cents = value => {
const match = String(value ?? '0').match(/^(-?)(\d+)(?:\.(\d{1,2}))?$/)
if (!match) return { negative: false, digits: '0' }
const digits = (match[2] + (match[3] || '').padEnd(2, '0')).replace(/^0+/, '') || '0'
return { negative: !!match[1] && digits !== '0', digits }
}
const left = cents(a), right = cents(b)
if (left.negative !== right.negative) return left.negative ? -1 : 1
const order = left.digits.length - right.digits.length || (left.digits < right.digits ? -1 : left.digits > right.digits ? 1 : 0)
return left.negative ? -order : order
}
export const metricTips = {
newRegisterCount: '所选周期内新增注册玩家数;开启身份证去重后按实名身份证去重,无实名信息不计入。',
activeCount: '所选周期内登录过游戏的去重玩家数;受身份证去重开关影响。',
payUserCount: '所选周期内产生成功充值的去重玩家数。',
totalPay: '所选周期内成功充值金额汇总,不计入失败或取消订单。',
payRate: '付费玩家 ÷ 活跃玩家 × 100%。',
arpu: '充值金额 ÷ 活跃玩家。',
arppu: '充值金额 ÷ 付费玩家。',
newPayUserCount: '所选周期内首次产生成功充值的玩家数,此前从未付费。'
}