feat(admin): show game consume ranking modal
This commit is contained in:
parent
6eb24bdd99
commit
790b0f9f7a
@ -116,6 +116,30 @@ export async function countryDashboardGameMetrics(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function countryDashboardGameSummaries(
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return requestClient.get<{
|
||||||
|
current: number;
|
||||||
|
records: CountryDashboardGameMetric[];
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
}>('/datav/country-dashboard/game-metrics/games', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function countryDashboardGameCountryRank(
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return requestClient.get<CountryDashboardGameMetric[]>(
|
||||||
|
'/datav/country-dashboard/game-metrics/country-rank',
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function latestActiveUserCountryCode(params: Record<string, any> = {}) {
|
export async function latestActiveUserCountryCode(params: Record<string, any> = {}) {
|
||||||
return requestClient.get<Array<Record<string, any>>>(
|
return requestClient.get<Array<Record<string, any>>>(
|
||||||
'/datav/active/user-country-code',
|
'/datav/active/user-country-code',
|
||||||
|
|||||||
@ -42,7 +42,8 @@ import utc from 'dayjs/plugin/utc';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
countryDashboard,
|
countryDashboard,
|
||||||
countryDashboardGameMetrics,
|
countryDashboardGameCountryRank,
|
||||||
|
countryDashboardGameSummaries,
|
||||||
countryDashboardRechargeDetails,
|
countryDashboardRechargeDetails,
|
||||||
} from '#/api/legacy/datav';
|
} from '#/api/legacy/datav';
|
||||||
import SysOriginSelect from '#/components/sys-origin-select.vue';
|
import SysOriginSelect from '#/components/sys-origin-select.vue';
|
||||||
@ -103,6 +104,10 @@ const gameMetricSorter = reactive<{
|
|||||||
field: 'consumeAmount',
|
field: 'consumeAmount',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
});
|
});
|
||||||
|
const gameCountryRankOpen = ref(false);
|
||||||
|
const gameCountryRankLoading = ref(false);
|
||||||
|
const gameCountryRankRecords = ref<CountryDashboardGameMetric[]>([]);
|
||||||
|
const selectedGameMetric = ref<null | Partial<CountryDashboardGameMetric>>(null);
|
||||||
const gameQuery = reactive<{
|
const gameQuery = reactive<{
|
||||||
gameKeyword: string;
|
gameKeyword: string;
|
||||||
gameProvider?: string;
|
gameProvider?: string;
|
||||||
@ -128,6 +133,13 @@ const { renderEcharts: renderLuckyChart } = useEcharts(luckyChartRef);
|
|||||||
type ChartOption = Parameters<typeof renderUserRechargeChart>[0];
|
type ChartOption = Parameters<typeof renderUserRechargeChart>[0];
|
||||||
|
|
||||||
const accessCodes = computed(() => accessStore.accessCodes || []);
|
const accessCodes = computed(() => accessStore.accessCodes || []);
|
||||||
|
const gameCountryRankTitle = computed(() => {
|
||||||
|
const game = selectedGameMetric.value;
|
||||||
|
if (!game) {
|
||||||
|
return '国家消耗排名';
|
||||||
|
}
|
||||||
|
return `${gameDisplayText(game)} 国家消耗排名`;
|
||||||
|
});
|
||||||
const sysOriginOptions = computed(() => {
|
const sysOriginOptions = computed(() => {
|
||||||
const options = getAllowedSysOrigins(accessCodes.value);
|
const options = getAllowedSysOrigins(accessCodes.value);
|
||||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||||
@ -337,12 +349,10 @@ const gameMetricIntegerColumns = new Set(['orderCount', 'userCount']);
|
|||||||
const gameMetricPercentColumns = new Set(['payoutRate', 'profitRate']);
|
const gameMetricPercentColumns = new Set(['payoutRate', 'profitRate']);
|
||||||
|
|
||||||
const gameMetricColumns: any[] = [
|
const gameMetricColumns: any[] = [
|
||||||
{ dataIndex: 'periodName', fixed: 'left' as const, key: 'periodName', title: '日期/周期', width: 120 },
|
{ align: 'right' as const, dataIndex: 'rank', fixed: 'left' as const, key: 'rank', title: '排名', width: 72 },
|
||||||
{ dataIndex: 'countryName', key: 'countryName', title: '国家', width: 150 },
|
{ dataIndex: 'gameName', fixed: 'left' as const, key: 'gameName', title: '游戏', width: 240 },
|
||||||
{ dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 },
|
{ dataIndex: 'gameProvider', key: 'gameProvider', title: '游戏厂商', width: 130 },
|
||||||
{ dataIndex: 'gameProvider', key: 'gameProvider', title: '游戏厂商', width: 120 },
|
{ dataIndex: 'gameId', key: 'gameId', title: '游戏 ID', width: 150 },
|
||||||
{ dataIndex: 'gameId', key: 'gameId', title: '游戏 ID', width: 130 },
|
|
||||||
{ dataIndex: 'gameName', key: 'gameName', title: '游戏名称', width: 180 },
|
|
||||||
{
|
{
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
dataIndex: 'consumeAmount',
|
dataIndex: 'consumeAmount',
|
||||||
@ -352,44 +362,6 @@ const gameMetricColumns: any[] = [
|
|||||||
title: '消耗',
|
title: '消耗',
|
||||||
width: 130,
|
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 },
|
{ dataIndex: 'refreshedAt', key: 'refreshedAt', title: '更新时间', width: 170 },
|
||||||
];
|
];
|
||||||
const gameTableScrollX = gameMetricColumns.reduce(
|
const gameTableScrollX = gameMetricColumns.reduce(
|
||||||
@ -397,6 +369,18 @@ const gameTableScrollX = gameMetricColumns.reduce(
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gameCountryRankColumns: any[] = [
|
||||||
|
{ align: 'right' as const, dataIndex: 'rank', key: 'rank', title: '排名', width: 72 },
|
||||||
|
{ dataIndex: 'countryName', key: 'countryName', title: '国家', width: 180 },
|
||||||
|
{ dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 100 },
|
||||||
|
{ align: 'right' as const, dataIndex: 'consumeAmount', key: 'consumeAmount', title: '消耗', width: 150 },
|
||||||
|
{ dataIndex: 'refreshedAt', key: 'refreshedAt', title: '更新时间', width: 170 },
|
||||||
|
];
|
||||||
|
const gameCountryRankScrollX = gameCountryRankColumns.reduce(
|
||||||
|
(totalWidth, item) => totalWidth + Number(item.width || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
const rechargeDetailColumns: any[] = [
|
const rechargeDetailColumns: any[] = [
|
||||||
{ dataIndex: 'createTime', key: 'createTime', title: '时间', width: 170 },
|
{ dataIndex: 'createTime', key: 'createTime', title: '时间', width: 170 },
|
||||||
{ dataIndex: 'sourceType', key: 'sourceType', title: '来源', width: 120 },
|
{ dataIndex: 'sourceType', key: 'sourceType', title: '来源', width: 120 },
|
||||||
@ -589,6 +573,9 @@ async function loadDashboard() {
|
|||||||
};
|
};
|
||||||
if (activeTableTab.value === 'game') {
|
if (activeTableTab.value === 'game') {
|
||||||
await loadGameMetrics();
|
await loadGameMetrics();
|
||||||
|
if (gameCountryRankOpen.value) {
|
||||||
|
await loadGameCountryRank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dashboard.value = null;
|
dashboard.value = null;
|
||||||
@ -624,7 +611,7 @@ async function loadGameMetrics() {
|
|||||||
}
|
}
|
||||||
gameMetricLoading.value = true;
|
gameMetricLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await countryDashboardGameMetrics(buildGameMetricParams());
|
const result = await countryDashboardGameSummaries(buildGameMetricParams());
|
||||||
gameMetricRecords.value = result.records || [];
|
gameMetricRecords.value = result.records || [];
|
||||||
gameMetricPagination.total = Number(result.total || 0);
|
gameMetricPagination.total = Number(result.total || 0);
|
||||||
gameMetricPagination.current = Number(result.current || gameMetricPagination.current || 1);
|
gameMetricPagination.current = Number(result.current || gameMetricPagination.current || 1);
|
||||||
@ -642,6 +629,39 @@ async function loadGameMetrics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGameCountryRankParams(record: Partial<CountryDashboardGameMetric>) {
|
||||||
|
return {
|
||||||
|
...buildParams(),
|
||||||
|
countryCodes: selectedCountryCodes.value.join(',') || undefined,
|
||||||
|
gameId: record.gameId,
|
||||||
|
gameProvider: record.gameProvider,
|
||||||
|
rankLimit: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGameCountryRank(record = selectedGameMetric.value) {
|
||||||
|
if (!record?.gameId || !record.gameProvider || !canQuery.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gameCountryRankLoading.value = true;
|
||||||
|
try {
|
||||||
|
gameCountryRankRecords.value = await countryDashboardGameCountryRank(
|
||||||
|
buildGameCountryRankParams(record),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
gameCountryRankRecords.value = [];
|
||||||
|
message.error('游戏国家排名加载失败');
|
||||||
|
} finally {
|
||||||
|
gameCountryRankLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openGameCountryRank(record: Partial<CountryDashboardGameMetric>) {
|
||||||
|
selectedGameMetric.value = record;
|
||||||
|
gameCountryRankOpen.value = true;
|
||||||
|
await loadGameCountryRank(record);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGameMetricTableChange(
|
async function handleGameMetricTableChange(
|
||||||
pagination: Record<string, any>,
|
pagination: Record<string, any>,
|
||||||
_filters: Record<string, any>,
|
_filters: Record<string, any>,
|
||||||
@ -697,13 +717,19 @@ function rechargeDetailRowKey(record: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gameMetricRowKey(record: Record<string, any>) {
|
function gameMetricRowKey(record: Record<string, any>) {
|
||||||
return [
|
return [record.gameProvider, record.gameId].join('-');
|
||||||
record.periodKey,
|
}
|
||||||
record.statTimezone,
|
|
||||||
record.countryCode,
|
function gameCountryRankRowKey(record: Record<string, any>) {
|
||||||
record.gameProvider,
|
return [record.countryCode, record.gameProvider, record.gameId].join('-');
|
||||||
record.gameId,
|
}
|
||||||
].join('-');
|
|
||||||
|
function gameMetricRank(index: number) {
|
||||||
|
return (gameMetricPagination.current - 1) * gameMetricPagination.pageSize + index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameDisplayText(record: Partial<CountryDashboardGameMetric>) {
|
||||||
|
return record.gameName || record.gameId || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function rechargeSourceName(sourceType: string) {
|
function rechargeSourceName(sourceType: string) {
|
||||||
@ -1131,6 +1157,9 @@ watch(selectedCountryCodes, () => {
|
|||||||
if (activeTableTab.value === 'game') {
|
if (activeTableTab.value === 'game') {
|
||||||
gameMetricPagination.current = 1;
|
gameMetricPagination.current = 1;
|
||||||
void loadGameMetrics();
|
void loadGameMetrics();
|
||||||
|
if (gameCountryRankOpen.value) {
|
||||||
|
void loadGameCountryRank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1357,9 +1386,22 @@ onBeforeUnmount(() => {
|
|||||||
size="small"
|
size="small"
|
||||||
@change="handleGameMetricTableChange"
|
@change="handleGameMetricTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, index, record }">
|
||||||
|
<template v-if="column.key === 'rank'">
|
||||||
|
<span class="number-cell">{{ gameMetricRank(index) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'gameName'">
|
||||||
|
<Button
|
||||||
|
class="game-link-button"
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="openGameCountryRank(record)"
|
||||||
|
>
|
||||||
|
{{ gameDisplayText(record) }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-else-if="
|
||||||
gameMetricNumberColumns.has(String(column.dataIndex)) ||
|
gameMetricNumberColumns.has(String(column.dataIndex)) ||
|
||||||
gameMetricPercentColumns.has(String(column.dataIndex))
|
gameMetricPercentColumns.has(String(column.dataIndex))
|
||||||
"
|
"
|
||||||
@ -1453,6 +1495,34 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
v-model:open="gameCountryRankOpen"
|
||||||
|
:footer="null"
|
||||||
|
:title="gameCountryRankTitle"
|
||||||
|
width="860px"
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
:columns="gameCountryRankColumns"
|
||||||
|
:data-source="gameCountryRankRecords"
|
||||||
|
:loading="gameCountryRankLoading"
|
||||||
|
:pagination="false"
|
||||||
|
:row-key="gameCountryRankRowKey"
|
||||||
|
:scroll="{ x: gameCountryRankScrollX, y: 520 }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, index, record }">
|
||||||
|
<template v-if="column.key === 'rank'">
|
||||||
|
<span class="number-cell">{{ index + 1 }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'consumeAmount'">
|
||||||
|
<span class="number-cell">{{ formatAmount(record.consumeAmount) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'countryName'">
|
||||||
|
{{ record.countryName || record.countryCode || '-' }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -1633,6 +1703,17 @@ onBeforeUnmount(() => {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-link-button {
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-link-button :deep(span) {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-value-cell {
|
.summary-value-cell {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user