diff --git a/apps/src/api/legacy/datav.ts b/apps/src/api/legacy/datav.ts index 4b67133..297e789 100644 --- a/apps/src/api/legacy/datav.ts +++ b/apps/src/api/legacy/datav.ts @@ -65,6 +65,25 @@ export interface CountryDashboardRechargeDetail { userNickname: string; } +export interface CountryDashboardGameMetric { + consumeAmount: number | string; + countryCode: string; + countryName: string; + gameId: string; + gameName: string; + gameProvider: string; + orderCount: number; + payoutAmount: number | string; + payoutRate: number | string; + periodKey: string; + periodName: string; + profitAmount: number | string; + profitRate: number | string; + refreshedAt: string; + statTimezone: string; + userCount: number; +} + export async function countryDashboard(params: Record = {}) { return requestClient.get('/datav/country-dashboard', { params, @@ -84,6 +103,19 @@ export async function countryDashboardRechargeDetails( }); } +export async function countryDashboardGameMetrics( + params: Record = {}, +) { + return requestClient.get<{ + current: number; + records: CountryDashboardGameMetric[]; + size: number; + total: number; + }>('/datav/country-dashboard/game-metrics', { + params, + }); +} + export async function latestActiveUserCountryCode(params: Record = {}) { return requestClient.get>>( '/datav/active/user-country-code', diff --git a/apps/src/views/statistics/datav.vue b/apps/src/views/statistics/datav.vue index ceae0a1..3f6ef21 100644 --- a/apps/src/views/statistics/datav.vue +++ b/apps/src/views/statistics/datav.vue @@ -2,6 +2,7 @@ import type { EchartsUIType } from '@vben/plugins/echarts'; import type { + CountryDashboardGameMetric, CountryDashboardMetric, CountryDashboardRechargeDetail, CountryDashboardResult, @@ -24,13 +25,14 @@ import { Button, DateRangePicker, Empty, + Input, message, Modal, Select, Space, Spin, - TabPane, Table, + TabPane, Tabs, Tooltip, } from 'antdv-next'; @@ -40,6 +42,7 @@ import utc from 'dayjs/plugin/utc'; import { countryDashboard, + countryDashboardGameMetrics, countryDashboardRechargeDetails, } from '#/api/legacy/datav'; import SysOriginSelect from '#/components/sys-origin-select.vue'; @@ -62,7 +65,7 @@ const fullscreen = ref(false); const visualMode = ref(false); const loading = ref(false); const dashboard = ref(null); -const activeTableTab = ref<'overview' | 'recharge'>('overview'); +const activeTableTab = ref<'game' | 'overview' | 'recharge'>('overview'); const arpuDashboards = ref<{ day: CountryDashboardResult | null; threeDay: CountryDashboardResult | null; @@ -86,6 +89,27 @@ const rechargeDetailPagination = reactive({ pageSize: 100, total: 0, }); +const gameMetricLoading = ref(false); +const gameMetricRecords = ref([]); +const gameMetricPagination = reactive({ + current: 1, + pageSize: 50, + total: 0, +}); +const gameMetricSorter = reactive<{ + field?: string; + order?: 'ascend' | 'descend'; +}>({ + field: 'consumeAmount', + order: 'descend', +}); +const gameQuery = reactive<{ + gameKeyword: string; + gameProvider?: string; +}>({ + gameKeyword: '', + gameProvider: undefined, +}); const query = reactive({ periodType: 'DAY', @@ -125,6 +149,18 @@ const statTimezoneOptions = [ { label: '北京', value: 'Asia/Shanghai' }, ]; +const gameProviderOptions = [ + { label: '全部游戏厂商', value: '' }, + { label: '站内游戏', value: 'INTERNAL' }, + { label: '百顺', value: 'BAISHUN' }, + { label: 'Game Open', value: 'GAME_OPEN' }, + { label: '灵犀', value: 'LINGXIAN' }, + { label: 'Yomi', value: 'YOMI' }, + { label: 'HKYS', value: 'HKYS' }, + { label: 'Hot', value: 'HOT' }, + { label: '未识别', value: 'UNKNOWN' }, +]; + const metricColumns = new Set([ 'countryNewUser', 'd1RetentionRate', @@ -132,7 +168,6 @@ const metricColumns = new Set([ 'd30RetentionRate', 'dailyActiveUser', 'dealerRecharge', - 'googleRecharge', 'gamePayout', 'gamePayoutRate', 'gameProfit', @@ -140,6 +175,7 @@ const metricColumns = new Set([ 'gameTotalFlow', 'gameUser', 'giftConsume', + 'googleRecharge', 'luckyGiftPayout', 'luckyGiftPayoutRate', 'luckyGiftProfit', @@ -288,6 +324,79 @@ const rechargeColumns: any[] = [ sorter: compareTableColumn(String(column.dataIndex)), })); +const gameMetricNumberColumns = new Set([ + 'consumeAmount', + 'orderCount', + 'payoutAmount', + 'profitAmount', + 'userCount', +]); + +const gameMetricIntegerColumns = new Set(['orderCount', 'userCount']); + +const gameMetricPercentColumns = new Set(['payoutRate', 'profitRate']); + +const gameMetricColumns: any[] = [ + { dataIndex: 'periodName', fixed: 'left' as const, key: 'periodName', title: '日期/周期', width: 120 }, + { dataIndex: 'countryName', key: 'countryName', title: '国家', width: 150 }, + { dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 }, + { dataIndex: 'gameProvider', key: 'gameProvider', title: '游戏厂商', width: 120 }, + { dataIndex: 'gameId', key: 'gameId', title: '游戏 ID', width: 130 }, + { dataIndex: 'gameName', key: 'gameName', title: '游戏名称', width: 180 }, + { + align: 'right' as const, + dataIndex: 'consumeAmount', + key: 'consumeAmount', + sortDirections: ['descend', 'ascend'], + sorter: true, + title: '消耗', + width: 130, + }, + { + align: 'right' as const, + dataIndex: 'payoutAmount', + key: 'payoutAmount', + sortDirections: ['descend', 'ascend'], + sorter: true, + title: '返奖', + width: 130, + }, + { + align: 'right' as const, + dataIndex: 'profitAmount', + key: 'profitAmount', + sortDirections: ['descend', 'ascend'], + sorter: true, + title: '收益', + width: 130, + }, + { align: 'right' as const, dataIndex: 'payoutRate', key: 'payoutRate', title: '返奖率', width: 110 }, + { align: 'right' as const, dataIndex: 'profitRate', key: 'profitRate', title: '收益率', width: 110 }, + { + align: 'right' as const, + dataIndex: 'userCount', + key: 'userCount', + sortDirections: ['descend', 'ascend'], + sorter: true, + title: '游戏用户数', + width: 130, + }, + { + align: 'right' as const, + dataIndex: 'orderCount', + key: 'orderCount', + sortDirections: ['descend', 'ascend'], + sorter: true, + title: '流水笔数', + width: 120, + }, + { dataIndex: 'refreshedAt', key: 'refreshedAt', title: '更新时间', width: 170 }, +]; +const gameTableScrollX = gameMetricColumns.reduce( + (totalWidth, item) => totalWidth + Number(item.width || 0), + 0, +); + const rechargeDetailColumns: any[] = [ { dataIndex: 'createTime', key: 'createTime', title: '时间', width: 170 }, { dataIndex: 'sourceType', key: 'sourceType', title: '来源', width: 120 }, @@ -478,6 +587,9 @@ async function loadDashboard() { threeDay: threeDayArpuDashboard, week: weekArpuDashboard, }; + if (activeTableTab.value === 'game') { + await loadGameMetrics(); + } } catch (error) { dashboard.value = null; arpuDashboards.value = { @@ -493,6 +605,66 @@ async function loadDashboard() { renderVisualCharts(); } +function buildGameMetricParams() { + return { + ...buildParams(), + countryCodes: selectedCountryCodes.value.join(',') || undefined, + cursor: gameMetricPagination.current, + gameKeyword: gameQuery.gameKeyword.trim() || undefined, + gameProvider: gameQuery.gameProvider || undefined, + limit: gameMetricPagination.pageSize, + sortField: gameMetricSorter.field || undefined, + sortOrder: gameMetricSorter.order || undefined, + }; +} + +async function loadGameMetrics() { + if (!canQuery.value) { + return; + } + gameMetricLoading.value = true; + try { + const result = await countryDashboardGameMetrics(buildGameMetricParams()); + gameMetricRecords.value = result.records || []; + gameMetricPagination.total = Number(result.total || 0); + gameMetricPagination.current = Number(result.current || gameMetricPagination.current || 1); + const resultPageSize = Number(result.size); + gameMetricPagination.pageSize = + Number.isFinite(resultPageSize) && resultPageSize > 0 + ? resultPageSize + : gameMetricPagination.pageSize; + } catch { + gameMetricRecords.value = []; + gameMetricPagination.total = 0; + message.error('游戏数据加载失败'); + } finally { + gameMetricLoading.value = false; + } +} + +async function handleGameMetricTableChange( + pagination: Record, + _filters: Record, + sorter: Record | Record[], +) { + const activeSorter = Array.isArray(sorter) ? sorter[0] : sorter; + gameMetricPagination.current = Number(pagination.current || 1); + gameMetricPagination.pageSize = Number(pagination.pageSize || gameMetricPagination.pageSize || 50); + gameMetricSorter.field = activeSorter?.field ? String(activeSorter.field) : undefined; + gameMetricSorter.order = + activeSorter?.order === 'ascend' || activeSorter?.order === 'descend' + ? activeSorter.order + : undefined; + await loadGameMetrics(); +} + +async function handleGameMetricFilterChange() { + gameMetricPagination.current = 1; + if (activeTableTab.value === 'game') { + await loadGameMetrics(); + } +} + async function loadRechargeDetails() { rechargeDetailLoading.value = true; try { @@ -524,6 +696,16 @@ function rechargeDetailRowKey(record: Record) { return `${record.sourceType}-${record.recordId}`; } +function gameMetricRowKey(record: Record) { + return [ + record.periodKey, + record.statTimezone, + record.countryCode, + record.gameProvider, + record.gameId, + ].join('-'); +} + function rechargeSourceName(sourceType: string) { return rechargeSourceNameMap[sourceType] || sourceType || '-'; } @@ -541,6 +723,10 @@ function showRechargeDetailTotal(total: number) { return `共 ${total} 条`; } +function showGameMetricTotal(total: number) { + return `共 ${total} 条`; +} + function toAmount(value?: null | number | string) { const number = Number(value || 0); return Number.isFinite(number) ? number : 0; @@ -730,6 +916,19 @@ function tableCellText(record: Record, key: string) { return formatAmount(record[key] as number | string); } +function gameMetricCellText(record: Record, key: string) { + if (gameMetricIntegerColumns.has(key)) { + return formatInteger(record[key] as number | string); + } + if (gameMetricPercentColumns.has(key)) { + return formatPercent(record[key] as number | string); + } + if (gameMetricNumberColumns.has(key)) { + return formatAmount(record[key] as number | string); + } + return record[key] || '-'; +} + function compareTableColumn(key: string) { return (current: Record, next: Record) => { if (metricColumns.has(key)) { @@ -921,6 +1120,20 @@ watch(countryOptions, (options) => { ); }); +watch(activeTableTab, (value) => { + if (value === 'game') { + gameMetricPagination.current = 1; + void loadGameMetrics(); + } +}); + +watch(selectedCountryCodes, () => { + if (activeTableTab.value === 'game') { + gameMetricPagination.current = 1; + void loadGameMetrics(); + } +}); + onMounted(() => { document.addEventListener('fullscreenchange', syncFullscreenState); loadDashboard(); @@ -1004,7 +1217,24 @@ onBeforeUnmount(() => { show-search style="width: 300px" /> - @@ -1105,12 +1335,43 @@ onBeforeUnmount(() => {