diff --git a/apps/src/api/legacy/datav.ts b/apps/src/api/legacy/datav.ts index 458cb79..e551fd8 100644 --- a/apps/src/api/legacy/datav.ts +++ b/apps/src/api/legacy/datav.ts @@ -15,6 +15,7 @@ export interface CountryDashboardMetric { d30RetentionUser: number; dailyActiveUser: number; dealerRecharge: number | string; + googleRecharge: number | string; gamePayoutRate: number | string; gamePayout: number | string; gameProfit: number | string; @@ -30,6 +31,7 @@ export interface CountryDashboardMetric { luckyGiftTotalFlow: number | string; luckyGiftUser: number; luckyGiftUserRate: number | string; + mifapayRecharge: number | string; newUserRecharge: number | string; officialRecharge: number | string; periodKey: string; @@ -47,12 +49,40 @@ export interface CountryDashboardResult { total: CountryDashboardMetric; } +export interface CountryDashboardRechargeDetail { + amount: number | string; + coinQuantity: number | string; + countryCode: string; + countryName: string; + createTime: string; + recordId: string; + remark: string; + sourceChannel: string; + sourceType: string; + userAccount: string; + userId: number | string; + userNickname: string; +} + export async function countryDashboard(params: Record = {}) { return requestClient.get('/datav/country-dashboard', { params, }); } +export async function countryDashboardRechargeDetails( + params: Record = {}, +) { + return requestClient.get<{ + current: number; + records: CountryDashboardRechargeDetail[]; + size: number; + total: number; + }>('/datav/country-dashboard/recharge-details', { + 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 4b4eb53..3257723 100644 --- a/apps/src/views/statistics/datav.vue +++ b/apps/src/views/statistics/datav.vue @@ -3,6 +3,7 @@ import type { EchartsUIType } from '@vben/plugins/echarts'; import type { CountryDashboardMetric, + CountryDashboardRechargeDetail, CountryDashboardResult, } from '#/api/legacy/datav'; @@ -24,15 +25,21 @@ import { DateRangePicker, Empty, message, + Modal, Select, Space, Spin, + TabPane, Table, + Tabs, Tooltip, } from 'antdv-next'; import dayjs from 'dayjs'; -import { countryDashboard } from '#/api/legacy/datav'; +import { + countryDashboard, + countryDashboardRechargeDetails, +} from '#/api/legacy/datav'; import SysOriginSelect from '#/components/sys-origin-select.vue'; import { getAllowedSysOrigins } from '#/views/system/shared'; @@ -48,6 +55,7 @@ const fullscreen = ref(false); const visualMode = ref(false); const loading = ref(false); const dashboard = ref(null); +const activeTableTab = ref<'overview' | 'recharge'>('overview'); const arpuDashboards = ref<{ day: CountryDashboardResult | null; threeDay: CountryDashboardResult | null; @@ -62,6 +70,14 @@ const dateRange = ref<[string, string] | null>([ dayjs().format('YYYY-MM-DD'), ]); const selectedCountryCodes = ref([]); +const rechargeDetailOpen = ref(false); +const rechargeDetailLoading = ref(false); +const rechargeDetailRecords = ref([]); +const rechargeDetailPagination = reactive({ + current: 1, + pageSize: 100, + total: 0, +}); const query = reactive({ periodType: 'DAY', @@ -101,6 +117,7 @@ const metricColumns = new Set([ 'd30RetentionRate', 'dailyActiveUser', 'dealerRecharge', + 'googleRecharge', 'gamePayout', 'gamePayoutRate', 'gameProfit', @@ -115,6 +132,7 @@ const metricColumns = new Set([ 'luckyGiftTotalFlow', 'luckyGiftUser', 'luckyGiftUserRate', + 'mifapayRecharge', 'newUserRecharge', 'salaryExchange', 'totalRecharge', @@ -143,6 +161,8 @@ const summaryMetricKeys = [ 'dailyActiveUser', 'newUserRecharge', 'dealerRecharge', + 'mifapayRecharge', + 'googleRecharge', 'salaryExchange', 'totalRecharge', 'giftConsume', @@ -167,6 +187,8 @@ const summaryMetricDefinitions = [ { key: 'dailyActiveUser', label: '当日活跃' }, { key: 'newUserRecharge', label: '新增充值' }, { key: 'dealerRecharge', label: '币商充值' }, + { key: 'mifapayRecharge', label: 'MifaPay 充值' }, + { key: 'googleRecharge', label: 'Google 充值' }, { key: 'salaryExchange', label: '工资兑换' }, { key: 'totalRecharge', label: '总充值' }, { key: 'giftConsume', label: '礼物消耗' }, @@ -202,7 +224,7 @@ const summaryColumns = [ ]; const summaryScrollX = summaryColumns.reduce((totalWidth, item) => totalWidth + item.width, 0); -const columns: any[] = [ +const overviewColumns: any[] = [ { dataIndex: 'countryName', fixed: 'left' as const, key: 'countryName', title: '国家', width: 150 }, { dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 }, { dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 }, @@ -213,6 +235,8 @@ const columns: any[] = [ { align: 'right', dataIndex: 'd30RetentionRate', key: 'd30RetentionRate', title: '30 日留存率', width: 150 }, { align: 'right', dataIndex: 'newUserRecharge', key: 'newUserRecharge', title: '新增充值', width: 130 }, { align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 }, + { align: 'right', dataIndex: 'mifapayRecharge', key: 'mifapayRecharge', title: 'MifaPay 充值', width: 150 }, + { align: 'right', dataIndex: 'googleRecharge', key: 'googleRecharge', title: 'Google 充值', width: 140 }, { align: 'right', dataIndex: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 }, { align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 130 }, { align: 'right', dataIndex: 'giftConsume', key: 'giftConsume', title: '礼物消耗', width: 130 }, @@ -234,7 +258,47 @@ const columns: any[] = [ sortDirections: ['ascend', 'descend'], sorter: compareTableColumn(String(column.dataIndex)), })); -const tableScrollX = columns.reduce((totalWidth, item) => totalWidth + Number(item.width || 0), 0); + +const rechargeColumns: any[] = [ + { dataIndex: 'countryName', fixed: 'left' as const, key: 'countryName', title: '国家', width: 150 }, + { dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 }, + { dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 }, + { align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 140 }, + { align: 'right', dataIndex: 'mifapayRecharge', key: 'mifapayRecharge', title: 'MifaPay 充值', width: 160 }, + { align: 'right', dataIndex: 'googleRecharge', key: 'googleRecharge', title: 'Google 充值', width: 150 }, + { align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 140 }, +].map((column) => ({ + ...column, + sortDirections: ['ascend', 'descend'], + sorter: compareTableColumn(String(column.dataIndex)), +})); + +const rechargeDetailColumns: any[] = [ + { dataIndex: 'createTime', key: 'createTime', title: '时间', width: 170 }, + { dataIndex: 'sourceType', key: 'sourceType', title: '来源', width: 120 }, + { dataIndex: 'user', key: 'user', title: '用户', width: 240 }, + { dataIndex: 'country', key: 'country', title: '国家', width: 160 }, + { align: 'right', dataIndex: 'amount', key: 'amount', title: '充值金额', width: 120 }, + { align: 'right', dataIndex: 'coinQuantity', key: 'coinQuantity', title: '金币', width: 130 }, + { dataIndex: 'sourceChannel', key: 'sourceChannel', title: '渠道/类型', width: 150 }, + { dataIndex: 'remark', key: 'remark', title: '备注', width: 220 }, +]; +const rechargeDetailScrollX = rechargeDetailColumns.reduce( + (totalWidth, item) => totalWidth + Number(item.width || 0), + 0, +); +const rechargeSourceNameMap: Record = { + DEALER: '币商充值', + MIFAPAY: 'MifaPay 充值', + OFFICIAL: '官方充值', +}; + +const activeColumns = computed(() => + activeTableTab.value === 'recharge' ? rechargeColumns : overviewColumns, +); +const tableScrollX = computed(() => + activeColumns.value.reduce((totalWidth, item) => totalWidth + Number(item.width || 0), 0), +); const rawRecords = computed(() => dashboard.value?.records || []); const countryOptions = computed(() => { @@ -290,6 +354,10 @@ const arpuCards = computed(() => { { label: '筛选 ARPU', value: formatAmount(calcArpu(visibleSummary.value)) }, ]; }); +const totalRechargeCard = computed(() => ({ + label: '总充值', + value: formatAmount(visibleSummary.value.totalRecharge), +})); function hasPermission(code: string) { const codes = accessCodes.value; @@ -400,6 +468,54 @@ async function loadDashboard() { renderVisualCharts(); } +async function loadRechargeDetails() { + rechargeDetailLoading.value = true; + try { + const result = await countryDashboardRechargeDetails({ + ...buildParams(), + countryCodes: selectedCountryCodes.value.join(',') || undefined, + cursor: rechargeDetailPagination.current, + limit: rechargeDetailPagination.pageSize, + }); + rechargeDetailRecords.value = result.records || []; + rechargeDetailPagination.total = Number(result.total || 0); + } finally { + rechargeDetailLoading.value = false; + } +} + +async function openRechargeDetails() { + rechargeDetailOpen.value = true; + rechargeDetailPagination.current = 1; + await loadRechargeDetails(); +} + +async function handleRechargeDetailTableChange(pagination: Record) { + rechargeDetailPagination.current = Number(pagination.current || 1); + await loadRechargeDetails(); +} + +function rechargeDetailRowKey(record: Record) { + return `${record.sourceType}-${record.recordId}`; +} + +function rechargeSourceName(sourceType: string) { + return rechargeSourceNameMap[sourceType] || sourceType || '-'; +} + +function rechargeDetailUserText(record: Record) { + const account = record.userAccount || record.userId; + return [account, record.userNickname].filter(Boolean).join(' / ') || '-'; +} + +function rechargeDetailCountryText(record: Record) { + return [record.countryName, record.countryCode].filter(Boolean).join(' / ') || '-'; +} + +function showRechargeDetailTotal(total: number) { + return `共 ${total} 条`; +} + function toAmount(value?: null | number | string) { const number = Number(value || 0); return Number.isFinite(number) ? number : 0; @@ -522,6 +638,7 @@ function emptySummary(): SummaryMetric { d30RetentionUser: 0, dailyActiveUser: 0, dealerRecharge: 0, + googleRecharge: 0, gamePayout: 0, gamePayoutRate: 0, gameProfit: 0, @@ -536,6 +653,7 @@ function emptySummary(): SummaryMetric { luckyGiftTotalFlow: 0, luckyGiftUser: 0, luckyGiftUserRate: 0, + mifapayRecharge: 0, newUserRecharge: 0, salaryExchange: 0, totalRecharge: 0, @@ -738,6 +856,8 @@ async function renderVisualCharts() { amountSeries('新增用户', 'countryNewUser', list), amountSeries('新增充值', 'newUserRecharge', list), amountSeries('币商充值', 'dealerRecharge', list), + amountSeries('MifaPay 充值', 'mifapayRecharge', list), + amountSeries('Google 充值', 'googleRecharge', list), amountSeries('工资兑换', 'salaryExchange', list), amountSeries('总充值', 'totalRecharge', list), ])); @@ -859,6 +979,22 @@ onBeforeUnmount(() => { +
+
充值
+
+
+
{{ totalRechargeCard.label }}
+
{{ totalRechargeCard.value }}
+
+
+
+
幸运礼物 / 游戏
@@ -938,9 +1074,13 @@ onBeforeUnmount(() => { @@ -1078,6 +1260,10 @@ onBeforeUnmount(() => { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.metric-card-grid--recharge { + grid-template-columns: minmax(180px, 260px); +} + .metric-card { background: #111827; border: 1px solid rgba(45, 212, 191, 0.18); @@ -1086,6 +1272,18 @@ onBeforeUnmount(() => { padding: 12px; } +.metric-card--clickable { + cursor: pointer; + transition: border-color 0.16s ease, transform 0.16s ease; +} + +.metric-card--clickable:focus-visible, +.metric-card--clickable:hover { + border-color: rgba(45, 212, 191, 0.72); + outline: none; + transform: translateY(-1px); +} + .metric-card__label { color: #cbd5e1; font-size: 12px; @@ -1108,6 +1306,22 @@ onBeforeUnmount(() => { padding: 12px; } +.dashboard-tabs { + margin-bottom: 10px; +} + +.dashboard-tabs :deep(.ant-tabs-nav) { + margin-bottom: 8px; +} + +.dashboard-tabs :deep(.ant-tabs-tab) { + color: #cbd5e1; +} + +.dashboard-tabs :deep(.ant-tabs-tab-active .ant-tabs-tab-btn) { + color: #2dd4bf; +} + .chart-grid { display: grid; gap: 12px; diff --git a/apps/src/views/team/bd-lead.vue b/apps/src/views/team/bd-lead.vue index d31d809..461bfc8 100644 --- a/apps/src/views/team/bd-lead.vue +++ b/apps/src/views/team/bd-lead.vue @@ -56,6 +56,7 @@ const query = reactive({ const columns = [ { dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 }, { dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 220 }, + { dataIndex: 'shortId', key: 'shortId', title: '短ID', width: 120 }, { dataIndex: 'regionName', key: 'regionName', title: '区域', width: 120 }, { dataIndex: 'bdQuantity', key: 'bdQuantity', title: 'BD数量', width: 100 }, { dataIndex: 'contact', key: 'contact', title: '联系方式', width: 180 }, @@ -181,9 +182,6 @@ loadData(true); + @@ -283,11 +284,6 @@ loadData(true); gap: 6px; } -.user-short-id { - color: #64748b; - font-size: 12px; -} - .user-sub { color: #64748b; margin-top: 4px;