diff --git a/apps/src/api/legacy/baishun-game.ts b/apps/src/api/legacy/baishun-game.ts index 3494bb0..9ce0907 100644 --- a/apps/src/api/legacy/baishun-game.ts +++ b/apps/src/api/legacy/baishun-game.ts @@ -60,3 +60,51 @@ export async function importBaishunCatalog(data: Record) { export async function syncBaishunGames(data: Record) { return requestClient.post>('/go/operate/baishun-game/sync', data); } + +export async function getLingxianProviderConfig(params: Record) { + return requestClient.get>('/go/operate/lingxian-game/config', { + params, + }); +} + +export async function saveLingxianProviderConfig(data: Record) { + return requestClient.post>( + '/go/operate/lingxian-game/config', + data, + ); +} + +export async function activateLingxianProviderProfile(data: Record) { + return requestClient.post>( + '/go/operate/lingxian-game/config/activate', + data, + ); +} + +export async function pageLingxianCatalog(params: Record) { + return requestClient.get>( + '/go/operate/lingxian-game/catalog/page', + { params }, + ); +} + +export async function fetchLingxianCatalog(data: Record) { + return requestClient.post>( + '/go/operate/lingxian-game/catalog/fetch', + data, + ); +} + +export async function importLingxianCatalog(data: Record) { + return requestClient.post>( + '/go/operate/lingxian-game/import-catalog', + data, + ); +} + +export async function syncLingxianGames(data: Record) { + return requestClient.post>( + '/go/operate/lingxian-game/sync', + data, + ); +} diff --git a/apps/src/api/legacy/datav.ts b/apps/src/api/legacy/datav.ts index 4c3de51..458cb79 100644 --- a/apps/src/api/legacy/datav.ts +++ b/apps/src/api/legacy/datav.ts @@ -4,16 +4,32 @@ export interface CountryDashboardMetric { countryCode: string; countryName: string; countryNewUser: number; + d1RetentionBaseUser: number; + d1RetentionRate: number | string; + d1RetentionUser: number; + d7RetentionBaseUser: number; + d7RetentionRate: number | string; + d7RetentionUser: number; + d30RetentionBaseUser: number; + d30RetentionRate: number | string; + d30RetentionUser: number; + dailyActiveUser: number; dealerRecharge: number | string; + gamePayoutRate: number | string; gamePayout: number | string; gameProfit: number | string; + gameProfitRate: number | string; gameTotalFlow: number | string; + gameUser: number; giftConsume: number | string; luckyGiftAnchorShare: number | string; luckyGiftPayout: number | string; + luckyGiftPayoutRate: number | string; luckyGiftProfit: number | string; luckyGiftProfitRate: number | string; luckyGiftTotalFlow: number | string; + luckyGiftUser: number; + luckyGiftUserRate: number | string; newUserRecharge: number | string; officialRecharge: number | string; periodKey: string; diff --git a/apps/src/router/routes/modules/game-manager.ts b/apps/src/router/routes/modules/game-manager.ts index 7604dd7..b37ae3c 100644 --- a/apps/src/router/routes/modules/game-manager.ts +++ b/apps/src/router/routes/modules/game-manager.ts @@ -23,6 +23,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/operate/baishun-config.vue'), meta: { title: '百顺配置' }, }, + { + name: 'OperateLingxianConfig', + path: 'lingxian-config', + component: () => import('#/views/operate/lingxian-config.vue'), + meta: { title: '灵仙配置' }, + }, ], }, ]; diff --git a/apps/src/views/operate/lingxian-config.vue b/apps/src/views/operate/lingxian-config.vue new file mode 100644 index 0000000..58e2d66 --- /dev/null +++ b/apps/src/views/operate/lingxian-config.vue @@ -0,0 +1,464 @@ + + + + + diff --git a/apps/src/views/statistics/datav.vue b/apps/src/views/statistics/datav.vue index 339330a..4f13af2 100644 --- a/apps/src/views/statistics/datav.vue +++ b/apps/src/views/statistics/datav.vue @@ -87,53 +87,100 @@ const periodTypeOptions = [ const metricColumns = new Set([ 'countryNewUser', + 'd1RetentionRate', + 'd7RetentionRate', + 'd30RetentionRate', + 'dailyActiveUser', 'dealerRecharge', 'gamePayout', + 'gamePayoutRate', 'gameProfit', + 'gameProfitRate', 'gameTotalFlow', + 'gameUser', 'giftConsume', 'luckyGiftPayout', + 'luckyGiftPayoutRate', 'luckyGiftProfit', 'luckyGiftProfitRate', 'luckyGiftTotalFlow', + 'luckyGiftUser', + 'luckyGiftUserRate', 'newUserRecharge', 'salaryExchange', 'totalRecharge', ]); +const integerMetricColumns = new Set([ + 'countryNewUser', + 'dailyActiveUser', + 'gameUser', + 'luckyGiftUser', +]); + +const percentMetricColumns = new Set([ + 'd1RetentionRate', + 'd7RetentionRate', + 'd30RetentionRate', + 'gamePayoutRate', + 'gameProfitRate', + 'luckyGiftPayoutRate', + 'luckyGiftProfitRate', + 'luckyGiftUserRate', +]); + const summaryMetricKeys = [ 'countryNewUser', + 'dailyActiveUser', 'newUserRecharge', 'dealerRecharge', 'salaryExchange', 'totalRecharge', 'giftConsume', 'luckyGiftTotalFlow', + 'luckyGiftUser', 'luckyGiftPayout', 'luckyGiftProfit', 'gameTotalFlow', + 'gameUser', 'gamePayout', 'gameProfit', + 'd1RetentionUser', + 'd1RetentionBaseUser', + 'd7RetentionUser', + 'd7RetentionBaseUser', + 'd30RetentionUser', + 'd30RetentionBaseUser', ] as const; const summaryMetricDefinitions = [ { key: 'countryNewUser', label: '新增' }, + { key: 'dailyActiveUser', label: '当日活跃' }, { key: 'newUserRecharge', label: '新增充值' }, { key: 'dealerRecharge', label: '币商充值' }, { key: 'salaryExchange', label: '工资兑换' }, { key: 'totalRecharge', label: '总充值' }, { key: 'giftConsume', label: '礼物消耗' }, { key: 'luckyGiftTotalFlow', label: '幸运礼物流水' }, + { key: 'luckyGiftUser', label: '幸运礼物参与人数' }, + { key: 'luckyGiftUserRate', label: '幸运礼物付费率' }, { key: 'luckyGiftPayout', label: '幸运礼物返奖' }, + { key: 'luckyGiftPayoutRate', label: '幸运礼物返奖率' }, { key: 'luckyGiftProfit', label: '幸运礼物利润' }, { key: 'luckyGiftProfitRate', label: '幸运礼物利润率' }, { key: 'gameTotalFlow', label: '游戏流水' }, + { key: 'gameUser', label: '游戏参与人数' }, { key: 'gamePayout', label: '游戏返奖' }, + { key: 'gamePayoutRate', label: '游戏返奖率' }, { key: 'gameProfit', label: '游戏利润' }, + { key: 'gameProfitRate', label: '游戏利润率' }, + { key: 'd1RetentionRate', label: '次日留存率' }, + { key: 'd7RetentionRate', label: '七日留存率' }, + { key: 'd30RetentionRate', label: '30 日留存率' }, ] as const; type SummaryMetricKey = (typeof summaryMetricDefinitions)[number]['key']; -type SummaryMetric = Record; +type SummaryMetric = Record; const summaryColumns = [ ...summaryMetricDefinitions.map((item) => ({ @@ -141,7 +188,7 @@ const summaryColumns = [ dataIndex: item.key, key: item.key, title: item.label, - width: item.key === 'countryNewUser' ? 110 : (item.key === 'luckyGiftProfitRate' ? 160 : 140), + width: item.key === 'countryNewUser' ? 110 : (String(item.key).includes('Rate') ? 160 : 150), })), ]; const summaryScrollX = summaryColumns.reduce((totalWidth, item) => totalWidth + item.width, 0); @@ -151,19 +198,30 @@ const columns: any[] = [ { dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 }, { dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 }, { align: 'right', dataIndex: 'countryNewUser', key: 'countryNewUser', title: '新增', width: 110 }, + { align: 'right', dataIndex: 'dailyActiveUser', key: 'dailyActiveUser', title: '当日活跃', width: 120 }, + { align: 'right', dataIndex: 'd1RetentionRate', key: 'd1RetentionRate', title: '次日留存率', width: 140 }, + { align: 'right', dataIndex: 'd7RetentionRate', key: 'd7RetentionRate', title: '七日留存率', width: 140 }, + { 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: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 }, { align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 130 }, { align: 'right', dataIndex: 'giftConsume', key: 'giftConsume', title: '礼物消耗', width: 130 }, { align: 'right', dataIndex: 'luckyGiftTotalFlow', key: 'luckyGiftTotalFlow', title: '幸运礼物流水', width: 150 }, + { align: 'right', dataIndex: 'luckyGiftUser', key: 'luckyGiftUser', title: '幸运礼物参与人数', width: 170 }, + { align: 'right', dataIndex: 'luckyGiftUserRate', key: 'luckyGiftUserRate', title: '幸运礼物付费率', width: 160 }, { align: 'right', dataIndex: 'luckyGiftPayout', key: 'luckyGiftPayout', title: '幸运礼物返奖', width: 150 }, + { align: 'right', dataIndex: 'luckyGiftPayoutRate', key: 'luckyGiftPayoutRate', title: '幸运礼物返奖率', width: 160 }, { align: 'right', dataIndex: 'luckyGiftProfit', key: 'luckyGiftProfit', title: '幸运礼物利润', width: 150 }, { align: 'right', dataIndex: 'luckyGiftProfitRate', key: 'luckyGiftProfitRate', title: '幸运礼物利润率', width: 160 }, { align: 'right', dataIndex: 'gameTotalFlow', key: 'gameTotalFlow', title: '游戏流水', width: 130 }, + { align: 'right', dataIndex: 'gameUser', key: 'gameUser', title: '游戏参与人数', width: 140 }, { align: 'right', dataIndex: 'gamePayout', key: 'gamePayout', title: '游戏返奖', width: 130 }, + { align: 'right', dataIndex: 'gamePayoutRate', key: 'gamePayoutRate', title: '游戏返奖率', width: 140 }, { align: 'right', dataIndex: 'gameProfit', key: 'gameProfit', title: '游戏利润', width: 130 }, + { align: 'right', dataIndex: 'gameProfitRate', key: 'gameProfitRate', title: '游戏利润率', width: 140 }, ]; +const tableScrollX = columns.reduce((totalWidth, item) => totalWidth + Number(item.width || 0), 0); const rawRecords = computed(() => dashboard.value?.records || []); const countryOptions = computed(() => { @@ -195,6 +253,26 @@ const summaryDataSource = computed(() => { } return [row]; }); +const luckyGameCards = computed(() => { + const summary = visibleSummary.value; + return [ + { label: '幸运礼物参与人数', value: formatInteger(summary.luckyGiftUser) }, + { label: '幸运礼物付费率', value: formatPercent(summary.luckyGiftUserRate) }, + { label: '幸运礼物返奖率', value: formatPercent(summary.luckyGiftPayoutRate) }, + { label: '游戏参与人数', value: formatInteger(summary.gameUser) }, + { label: '游戏返奖率', value: formatPercent(summary.gamePayoutRate) }, + { label: '游戏利润率', value: formatPercent(summary.gameProfitRate) }, + ]; +}); +const appDataCards = computed(() => { + const summary = visibleSummary.value; + return [ + { label: '当日活跃数', value: formatInteger(summary.dailyActiveUser) }, + { label: '次日留存率', value: formatPercent(summary.d1RetentionRate) }, + { label: '七日留存率', value: formatPercent(summary.d7RetentionRate) }, + { label: '30 日留存率', value: formatPercent(summary.d30RetentionRate) }, + ]; +}); function hasPermission(code: string) { const codes = accessCodes.value; @@ -282,6 +360,14 @@ function toAmount(value?: null | number | string) { return Number.isFinite(number) ? number : 0; } +function calcPercent(numerator?: null | number | string, denominator?: null | number | string) { + const fixedDenominator = toAmount(denominator); + if (fixedDenominator === 0) { + return 0; + } + return (toAmount(numerator) / fixedDenominator) * 100; +} + function formatInteger(value?: null | number | string) { const number = Number(value || 0); return Number.isFinite(number) ? number.toLocaleString('en-US', { maximumFractionDigits: 0 }) : '0'; @@ -332,18 +418,66 @@ function resolveLuckyGiftProfitRate(record?: null | { return (Number(record.luckyGiftProfit || 0) / flow) * 100; } +function resolveMetricRate(record: Record, key: string) { + if (key === 'luckyGiftProfitRate') { + return resolveLuckyGiftProfitRate(record); + } + const rate = Number(record[key]); + if (Number.isFinite(rate) && record[key] !== undefined && record[key] !== null && record[key] !== '') { + return rate; + } + if (key === 'luckyGiftUserRate') { + return calcPercent(record.luckyGiftUser, record.dailyActiveUser); + } + if (key === 'luckyGiftPayoutRate') { + return calcPercent(record.luckyGiftPayout, record.luckyGiftTotalFlow); + } + if (key === 'gamePayoutRate') { + return calcPercent(record.gamePayout, record.gameTotalFlow); + } + if (key === 'gameProfitRate') { + return calcPercent(record.gameProfit, record.gameTotalFlow); + } + if (key === 'd1RetentionRate') { + return calcPercent(record.d1RetentionUser, record.d1RetentionBaseUser); + } + if (key === 'd7RetentionRate') { + return calcPercent(record.d7RetentionUser, record.d7RetentionBaseUser); + } + if (key === 'd30RetentionRate') { + return calcPercent(record.d30RetentionUser, record.d30RetentionBaseUser); + } + return 0; +} + function emptySummary(): SummaryMetric { return { countryNewUser: 0, + d1RetentionBaseUser: 0, + d1RetentionRate: 0, + d1RetentionUser: 0, + d7RetentionBaseUser: 0, + d7RetentionRate: 0, + d7RetentionUser: 0, + d30RetentionBaseUser: 0, + d30RetentionRate: 0, + d30RetentionUser: 0, + dailyActiveUser: 0, dealerRecharge: 0, gamePayout: 0, + gamePayoutRate: 0, gameProfit: 0, + gameProfitRate: 0, gameTotalFlow: 0, + gameUser: 0, giftConsume: 0, luckyGiftPayout: 0, + luckyGiftPayoutRate: 0, luckyGiftProfit: 0, luckyGiftProfitRate: 0, luckyGiftTotalFlow: 0, + luckyGiftUser: 0, + luckyGiftUserRate: 0, newUserRecharge: 0, salaryExchange: 0, totalRecharge: 0, @@ -354,19 +488,26 @@ function buildVisibleSummary(list: CountryDashboardMetric[]) { const summary = emptySummary(); for (const item of list) { for (const key of summaryMetricKeys) { - summary[key] += toAmount(item[key] as number | string); + summary[key] = toAmount(summary[key]) + toAmount(item[key] as number | string); } } + summary.luckyGiftUserRate = calcPercent(summary.luckyGiftUser, summary.dailyActiveUser); + summary.luckyGiftPayoutRate = calcPercent(summary.luckyGiftPayout, summary.luckyGiftTotalFlow); summary.luckyGiftProfitRate = resolveLuckyGiftProfitRate(summary); + summary.gamePayoutRate = calcPercent(summary.gamePayout, summary.gameTotalFlow); + summary.gameProfitRate = calcPercent(summary.gameProfit, summary.gameTotalFlow); + summary.d1RetentionRate = calcPercent(summary.d1RetentionUser, summary.d1RetentionBaseUser); + summary.d7RetentionRate = calcPercent(summary.d7RetentionUser, summary.d7RetentionBaseUser); + summary.d30RetentionRate = calcPercent(summary.d30RetentionUser, summary.d30RetentionBaseUser); return summary; } function formatSummaryValue(key: SummaryMetricKey, summary: SummaryMetric) { - if (key === 'countryNewUser') { + if (integerMetricColumns.has(key)) { return formatInteger(summary[key]); } - if (key === 'luckyGiftProfitRate') { - return formatPercent(summary[key]); + if (percentMetricColumns.has(key)) { + return formatPercent(resolveMetricRate(summary, key)); } return formatAmount(summary[key]); } @@ -379,11 +520,11 @@ function filterCountry(input: string, option?: Record) { } function tableCellText(record: Record, key: string) { - if (key === 'countryNewUser') { - return formatInteger(record.countryNewUser); + if (integerMetricColumns.has(key)) { + return formatInteger(record[key] as number | string); } - if (key === 'luckyGiftProfitRate') { - return formatPercent(resolveLuckyGiftProfitRate(record)); + if (percentMetricColumns.has(key)) { + return formatPercent(resolveMetricRate(record, key)); } return formatAmount(record[key] as number | string); } @@ -637,6 +778,35 @@ onBeforeUnmount(() => { +
+
+
幸运礼物 / 游戏
+
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+
+
+
APP 数据
+
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+
+
+
{ :data-source="records" :pagination="{ pageSize: 20, showSizeChanger: true }" :row-key="rowKey" - :scroll="{ x: 2060 }" + :scroll="{ x: tableScrollX }" size="small" >