z
数据大屏,灵仙游戏增加
This commit is contained in:
parent
e02a6ff92f
commit
18d13b6172
@ -60,3 +60,51 @@ export async function importBaishunCatalog(data: Record<string, any>) {
|
|||||||
export async function syncBaishunGames(data: Record<string, any>) {
|
export async function syncBaishunGames(data: Record<string, any>) {
|
||||||
return requestClient.post<Record<string, any>>('/go/operate/baishun-game/sync', data);
|
return requestClient.post<Record<string, any>>('/go/operate/baishun-game/sync', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLingxianProviderConfig(params: Record<string, any>) {
|
||||||
|
return requestClient.get<Record<string, any>>('/go/operate/lingxian-game/config', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveLingxianProviderConfig(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/config',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activateLingxianProviderProfile(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/config/activate',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pageLingxianCatalog(params: Record<string, any>) {
|
||||||
|
return requestClient.get<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/catalog/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLingxianCatalog(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/catalog/fetch',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importLingxianCatalog(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/import-catalog',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncLingxianGames(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Record<string, any>>(
|
||||||
|
'/go/operate/lingxian-game/sync',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -4,16 +4,32 @@ export interface CountryDashboardMetric {
|
|||||||
countryCode: string;
|
countryCode: string;
|
||||||
countryName: string;
|
countryName: string;
|
||||||
countryNewUser: number;
|
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;
|
dealerRecharge: number | string;
|
||||||
|
gamePayoutRate: number | string;
|
||||||
gamePayout: number | string;
|
gamePayout: number | string;
|
||||||
gameProfit: number | string;
|
gameProfit: number | string;
|
||||||
|
gameProfitRate: number | string;
|
||||||
gameTotalFlow: number | string;
|
gameTotalFlow: number | string;
|
||||||
|
gameUser: number;
|
||||||
giftConsume: number | string;
|
giftConsume: number | string;
|
||||||
luckyGiftAnchorShare: number | string;
|
luckyGiftAnchorShare: number | string;
|
||||||
luckyGiftPayout: number | string;
|
luckyGiftPayout: number | string;
|
||||||
|
luckyGiftPayoutRate: number | string;
|
||||||
luckyGiftProfit: number | string;
|
luckyGiftProfit: number | string;
|
||||||
luckyGiftProfitRate: number | string;
|
luckyGiftProfitRate: number | string;
|
||||||
luckyGiftTotalFlow: number | string;
|
luckyGiftTotalFlow: number | string;
|
||||||
|
luckyGiftUser: number;
|
||||||
|
luckyGiftUserRate: number | string;
|
||||||
newUserRecharge: number | string;
|
newUserRecharge: number | string;
|
||||||
officialRecharge: number | string;
|
officialRecharge: number | string;
|
||||||
periodKey: string;
|
periodKey: string;
|
||||||
|
|||||||
@ -23,6 +23,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('#/views/operate/baishun-config.vue'),
|
component: () => import('#/views/operate/baishun-config.vue'),
|
||||||
meta: { title: '百顺配置' },
|
meta: { title: '百顺配置' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'OperateLingxianConfig',
|
||||||
|
path: 'lingxian-config',
|
||||||
|
component: () => import('#/views/operate/lingxian-config.vue'),
|
||||||
|
meta: { title: '灵仙配置' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
464
apps/src/views/operate/lingxian-config.vue
Normal file
464
apps/src/views/operate/lingxian-config.vue
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
activateLingxianProviderProfile,
|
||||||
|
fetchLingxianCatalog,
|
||||||
|
getLingxianProviderConfig,
|
||||||
|
importLingxianCatalog,
|
||||||
|
pageLingxianCatalog,
|
||||||
|
saveLingxianProviderConfig,
|
||||||
|
} from '#/api/legacy/baishun-game';
|
||||||
|
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Collapse,
|
||||||
|
CollapsePanel,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
message,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OperateLingxianConfig' });
|
||||||
|
|
||||||
|
const defaultGameListUrl =
|
||||||
|
'https://sg-test.leadercc.com/yumichat_games/test_game_list.json';
|
||||||
|
|
||||||
|
function createProviderForm(sysOrigin = '') {
|
||||||
|
return {
|
||||||
|
appKey: '',
|
||||||
|
gameListUrl: defaultGameListUrl,
|
||||||
|
profile: 'PROD',
|
||||||
|
sysOrigin,
|
||||||
|
testToken: '',
|
||||||
|
testUid: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const sysOriginOptions = computed(() => {
|
||||||
|
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||||||
|
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||||
|
});
|
||||||
|
const profileOptions = [
|
||||||
|
{ label: '正式环境', value: 'PROD' },
|
||||||
|
{ label: '测试环境', value: 'TEST' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const contextQuery = reactive({ profile: 'PROD', sysOrigin: '' });
|
||||||
|
const providerForm = reactive(createProviderForm());
|
||||||
|
const catalogQuery = reactive<Record<string, any>>({
|
||||||
|
cursor: 1,
|
||||||
|
keyword: '',
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const providerLoading = ref(false);
|
||||||
|
const providerSaving = ref(false);
|
||||||
|
const catalogLoading = ref(false);
|
||||||
|
const catalogFetching = ref(false);
|
||||||
|
const catalogImporting = ref(false);
|
||||||
|
const profileActivating = ref(false);
|
||||||
|
const totalCatalog = ref(0);
|
||||||
|
const catalogList = ref<Array<Record<string, any>>>([]);
|
||||||
|
const selectedCatalogKeys = ref<Array<string>>([]);
|
||||||
|
const activeProfile = ref('PROD');
|
||||||
|
|
||||||
|
const isCurrentProfileActive = computed(
|
||||||
|
() => activeProfile.value === contextQuery.profile,
|
||||||
|
);
|
||||||
|
const activeProfileLabel = computed(
|
||||||
|
() =>
|
||||||
|
profileOptions.find((item) => item.value === activeProfile.value)?.label ||
|
||||||
|
activeProfile.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const catalogColumns = [
|
||||||
|
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 },
|
||||||
|
{ dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 },
|
||||||
|
{ dataIndex: 'vendorGameId', key: 'vendorGameId', title: '灵仙ID', width: 120 },
|
||||||
|
{ dataIndex: 'status', key: 'status', title: '目录状态', width: 110 },
|
||||||
|
{ dataIndex: 'packageVersion', key: 'packageVersion', title: '版本', width: 90 },
|
||||||
|
{ dataIndex: 'added', key: 'added', title: '统一游戏列表', width: 140 },
|
||||||
|
{ dataIndex: 'downloadUrl', key: 'downloadUrl', title: '入口地址', width: 440 },
|
||||||
|
{ dataIndex: 'previewUrl', key: 'previewUrl', title: '半屏地址', width: 440 },
|
||||||
|
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const catalogRowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedCatalogKeys.value,
|
||||||
|
onChange: (keys: Array<number | string>) => {
|
||||||
|
selectedCatalogKeys.value = keys.map((item) => String(item));
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: Record<string, any>) => ({
|
||||||
|
disabled: Boolean(record.added),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
watch(
|
||||||
|
sysOriginOptions,
|
||||||
|
(options) => {
|
||||||
|
if (!contextQuery.sysOrigin) {
|
||||||
|
contextQuery.sysOrigin = String(options[0]?.value || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [contextQuery.sysOrigin, contextQuery.profile],
|
||||||
|
([sysOrigin, profile]) => {
|
||||||
|
if (!sysOrigin || !profile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(providerForm, createProviderForm(sysOrigin), {
|
||||||
|
profile,
|
||||||
|
sysOrigin,
|
||||||
|
});
|
||||||
|
selectedCatalogKeys.value = [];
|
||||||
|
void Promise.all([loadProviderConfig(), loadCatalog(true)]);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function applyProviderForm(record: Record<string, any>) {
|
||||||
|
activeProfile.value = String(record.activeProfile || activeProfile.value || 'PROD');
|
||||||
|
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
|
||||||
|
...record,
|
||||||
|
gameListUrl: record.gameListUrl || defaultGameListUrl,
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProviderConfig() {
|
||||||
|
if (!contextQuery.sysOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
providerLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getLingxianProviderConfig({
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
applyProviderForm(result || {});
|
||||||
|
} finally {
|
||||||
|
providerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistProviderConfig(showSuccess = true) {
|
||||||
|
providerForm.gameListUrl =
|
||||||
|
String(providerForm.gameListUrl || '').trim() || defaultGameListUrl;
|
||||||
|
if (!contextQuery.sysOrigin || !providerForm.gameListUrl || !providerForm.appKey) {
|
||||||
|
message.warning('请补全灵仙游戏列表地址和 Key');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
providerSaving.value = true;
|
||||||
|
try {
|
||||||
|
const result = await saveLingxianProviderConfig({
|
||||||
|
...providerForm,
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
applyProviderForm(result || providerForm);
|
||||||
|
if (showSuccess) {
|
||||||
|
message.success('灵仙配置已保存');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
providerSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCatalog(reset = false) {
|
||||||
|
if (!contextQuery.sysOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
catalogQuery.cursor = 1;
|
||||||
|
}
|
||||||
|
catalogLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await pageLingxianCatalog({
|
||||||
|
...catalogQuery,
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
catalogList.value = Array.isArray(result.records) ? result.records : [];
|
||||||
|
totalCatalog.value = Number(result.total || 0);
|
||||||
|
} finally {
|
||||||
|
catalogLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCatalogPageChange(page: number, pageSize: number) {
|
||||||
|
catalogQuery.cursor = page;
|
||||||
|
catalogQuery.limit = pageSize;
|
||||||
|
void loadCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActivateProfile() {
|
||||||
|
if (!(await persistProviderConfig(false))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profileActivating.value = true;
|
||||||
|
try {
|
||||||
|
const result = await activateLingxianProviderProfile({
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
applyProviderForm(result || providerForm);
|
||||||
|
message.success(`已切换到${activeProfileLabel.value}`);
|
||||||
|
await loadCatalog(true);
|
||||||
|
} finally {
|
||||||
|
profileActivating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFetchCatalog() {
|
||||||
|
if (!(await persistProviderConfig(false))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catalogFetching.value = true;
|
||||||
|
try {
|
||||||
|
const result = await fetchLingxianCatalog({
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
});
|
||||||
|
message.success(
|
||||||
|
`获取完成:新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},跳过 ${Number(result.skipped || 0)}`,
|
||||||
|
);
|
||||||
|
await loadCatalog(true);
|
||||||
|
} finally {
|
||||||
|
catalogFetching.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportSelected() {
|
||||||
|
if (selectedCatalogKeys.value.length === 0) {
|
||||||
|
message.warning('请选择要添加到统一游戏列表的灵仙游戏');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catalogImporting.value = true;
|
||||||
|
try {
|
||||||
|
const result = await importLingxianCatalog({
|
||||||
|
profile: contextQuery.profile,
|
||||||
|
sysOrigin: contextQuery.sysOrigin,
|
||||||
|
vendorGameIds: selectedCatalogKeys.value,
|
||||||
|
});
|
||||||
|
selectedCatalogKeys.value = [];
|
||||||
|
message.success(
|
||||||
|
`添加完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`,
|
||||||
|
);
|
||||||
|
await loadCatalog(true);
|
||||||
|
} finally {
|
||||||
|
catalogImporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page title="灵仙配置">
|
||||||
|
<Card :loading="providerLoading" title="灵仙接入配置">
|
||||||
|
<Form layout="vertical">
|
||||||
|
<div class="form-grid provider-grid">
|
||||||
|
<FormItem label="系统">
|
||||||
|
<SysOriginSelect
|
||||||
|
v-model:value="contextQuery.sysOrigin"
|
||||||
|
:options="sysOriginOptions"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="配置环境" extra="app 侧只读取当前启用环境。">
|
||||||
|
<Select
|
||||||
|
v-model:value="contextQuery.profile"
|
||||||
|
:options="profileOptions"
|
||||||
|
option-label-prop="label"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="游戏列表地址">
|
||||||
|
<Input
|
||||||
|
v-model:value="providerForm.gameListUrl"
|
||||||
|
:placeholder="defaultGameListUrl"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="Key">
|
||||||
|
<Input
|
||||||
|
v-model:value="providerForm.appKey"
|
||||||
|
placeholder="请输入灵仙签名 Key"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
<Collapse class="test-user-collapse" ghost>
|
||||||
|
<CollapsePanel key="test-user" header="测试用户">
|
||||||
|
<div class="form-grid">
|
||||||
|
<FormItem label="UID">
|
||||||
|
<Input v-model:value="providerForm.testUid" placeholder="可选" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="Token">
|
||||||
|
<Input
|
||||||
|
v-model:value="providerForm.testToken"
|
||||||
|
placeholder="可选"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</CollapsePanel>
|
||||||
|
</Collapse>
|
||||||
|
<div class="toolbar">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="providerSaving"
|
||||||
|
@click="persistProviderConfig(true)"
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="isCurrentProfileActive"
|
||||||
|
:loading="profileActivating"
|
||||||
|
@click="handleActivateProfile"
|
||||||
|
>
|
||||||
|
启用此环境
|
||||||
|
</Button>
|
||||||
|
<Button :loading="catalogFetching" @click="handleFetchCatalog">
|
||||||
|
获取灵仙游戏列表
|
||||||
|
</Button>
|
||||||
|
<Tag :color="isCurrentProfileActive ? 'success' : 'warning'">
|
||||||
|
当前启用:{{ activeProfileLabel }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="content-card" title="灵仙目录">
|
||||||
|
<div class="toolbar">
|
||||||
|
<Input
|
||||||
|
v-model:value="catalogQuery.keyword"
|
||||||
|
placeholder="搜索目录名称 / 内部ID / 灵仙ID"
|
||||||
|
style="width: 280px"
|
||||||
|
@pressEnter="loadCatalog(true)"
|
||||||
|
/>
|
||||||
|
<Button :loading="catalogLoading" type="primary" @click="loadCatalog(true)">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button :loading="catalogFetching" @click="handleFetchCatalog">
|
||||||
|
重新获取目录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="catalogImporting"
|
||||||
|
@click="handleImportSelected"
|
||||||
|
>
|
||||||
|
添加已选游戏
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
:columns="catalogColumns"
|
||||||
|
:data-source="catalogList"
|
||||||
|
:loading="catalogLoading"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="vendorGameId"
|
||||||
|
:row-selection="catalogRowSelection"
|
||||||
|
:scroll="{ x: 1780 }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'status'">
|
||||||
|
<Tag :color="record.status === 'ENABLED' ? 'success' : 'default'">
|
||||||
|
{{ record.status || '-' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'added'">
|
||||||
|
<Tag :color="record.added ? 'success' : 'default'">
|
||||||
|
{{ record.added ? '已进入统一游戏列表' : '未添加' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'downloadUrl' || column.key === 'previewUrl'">
|
||||||
|
<a
|
||||||
|
v-if="record[column.key]"
|
||||||
|
:href="record[column.key]"
|
||||||
|
class="link-cell"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ record[column.key] }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div v-if="totalCatalog > 0" class="pager">
|
||||||
|
<Pagination
|
||||||
|
:current="catalogQuery.cursor"
|
||||||
|
:page-size="catalogQuery.limit"
|
||||||
|
:total="totalCatalog"
|
||||||
|
show-size-changer
|
||||||
|
@change="handleCatalogPageChange"
|
||||||
|
@showSizeChange="handleCatalogPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-user-collapse {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-cell {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 420px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.form-grid,
|
||||||
|
.provider-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -87,53 +87,100 @@ const periodTypeOptions = [
|
|||||||
|
|
||||||
const metricColumns = new Set([
|
const metricColumns = new Set([
|
||||||
'countryNewUser',
|
'countryNewUser',
|
||||||
|
'd1RetentionRate',
|
||||||
|
'd7RetentionRate',
|
||||||
|
'd30RetentionRate',
|
||||||
|
'dailyActiveUser',
|
||||||
'dealerRecharge',
|
'dealerRecharge',
|
||||||
'gamePayout',
|
'gamePayout',
|
||||||
|
'gamePayoutRate',
|
||||||
'gameProfit',
|
'gameProfit',
|
||||||
|
'gameProfitRate',
|
||||||
'gameTotalFlow',
|
'gameTotalFlow',
|
||||||
|
'gameUser',
|
||||||
'giftConsume',
|
'giftConsume',
|
||||||
'luckyGiftPayout',
|
'luckyGiftPayout',
|
||||||
|
'luckyGiftPayoutRate',
|
||||||
'luckyGiftProfit',
|
'luckyGiftProfit',
|
||||||
'luckyGiftProfitRate',
|
'luckyGiftProfitRate',
|
||||||
'luckyGiftTotalFlow',
|
'luckyGiftTotalFlow',
|
||||||
|
'luckyGiftUser',
|
||||||
|
'luckyGiftUserRate',
|
||||||
'newUserRecharge',
|
'newUserRecharge',
|
||||||
'salaryExchange',
|
'salaryExchange',
|
||||||
'totalRecharge',
|
'totalRecharge',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const integerMetricColumns = new Set([
|
||||||
|
'countryNewUser',
|
||||||
|
'dailyActiveUser',
|
||||||
|
'gameUser',
|
||||||
|
'luckyGiftUser',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const percentMetricColumns = new Set([
|
||||||
|
'd1RetentionRate',
|
||||||
|
'd7RetentionRate',
|
||||||
|
'd30RetentionRate',
|
||||||
|
'gamePayoutRate',
|
||||||
|
'gameProfitRate',
|
||||||
|
'luckyGiftPayoutRate',
|
||||||
|
'luckyGiftProfitRate',
|
||||||
|
'luckyGiftUserRate',
|
||||||
|
]);
|
||||||
|
|
||||||
const summaryMetricKeys = [
|
const summaryMetricKeys = [
|
||||||
'countryNewUser',
|
'countryNewUser',
|
||||||
|
'dailyActiveUser',
|
||||||
'newUserRecharge',
|
'newUserRecharge',
|
||||||
'dealerRecharge',
|
'dealerRecharge',
|
||||||
'salaryExchange',
|
'salaryExchange',
|
||||||
'totalRecharge',
|
'totalRecharge',
|
||||||
'giftConsume',
|
'giftConsume',
|
||||||
'luckyGiftTotalFlow',
|
'luckyGiftTotalFlow',
|
||||||
|
'luckyGiftUser',
|
||||||
'luckyGiftPayout',
|
'luckyGiftPayout',
|
||||||
'luckyGiftProfit',
|
'luckyGiftProfit',
|
||||||
'gameTotalFlow',
|
'gameTotalFlow',
|
||||||
|
'gameUser',
|
||||||
'gamePayout',
|
'gamePayout',
|
||||||
'gameProfit',
|
'gameProfit',
|
||||||
|
'd1RetentionUser',
|
||||||
|
'd1RetentionBaseUser',
|
||||||
|
'd7RetentionUser',
|
||||||
|
'd7RetentionBaseUser',
|
||||||
|
'd30RetentionUser',
|
||||||
|
'd30RetentionBaseUser',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const summaryMetricDefinitions = [
|
const summaryMetricDefinitions = [
|
||||||
{ key: 'countryNewUser', label: '新增' },
|
{ key: 'countryNewUser', label: '新增' },
|
||||||
|
{ key: 'dailyActiveUser', label: '当日活跃' },
|
||||||
{ key: 'newUserRecharge', label: '新增充值' },
|
{ key: 'newUserRecharge', label: '新增充值' },
|
||||||
{ key: 'dealerRecharge', label: '币商充值' },
|
{ key: 'dealerRecharge', label: '币商充值' },
|
||||||
{ key: 'salaryExchange', label: '工资兑换' },
|
{ key: 'salaryExchange', label: '工资兑换' },
|
||||||
{ key: 'totalRecharge', label: '总充值' },
|
{ key: 'totalRecharge', label: '总充值' },
|
||||||
{ key: 'giftConsume', label: '礼物消耗' },
|
{ key: 'giftConsume', label: '礼物消耗' },
|
||||||
{ key: 'luckyGiftTotalFlow', label: '幸运礼物流水' },
|
{ key: 'luckyGiftTotalFlow', label: '幸运礼物流水' },
|
||||||
|
{ key: 'luckyGiftUser', label: '幸运礼物参与人数' },
|
||||||
|
{ key: 'luckyGiftUserRate', label: '幸运礼物付费率' },
|
||||||
{ key: 'luckyGiftPayout', label: '幸运礼物返奖' },
|
{ key: 'luckyGiftPayout', label: '幸运礼物返奖' },
|
||||||
|
{ key: 'luckyGiftPayoutRate', label: '幸运礼物返奖率' },
|
||||||
{ key: 'luckyGiftProfit', label: '幸运礼物利润' },
|
{ key: 'luckyGiftProfit', label: '幸运礼物利润' },
|
||||||
{ key: 'luckyGiftProfitRate', label: '幸运礼物利润率' },
|
{ key: 'luckyGiftProfitRate', label: '幸运礼物利润率' },
|
||||||
{ key: 'gameTotalFlow', label: '游戏流水' },
|
{ key: 'gameTotalFlow', label: '游戏流水' },
|
||||||
|
{ key: 'gameUser', label: '游戏参与人数' },
|
||||||
{ key: 'gamePayout', label: '游戏返奖' },
|
{ key: 'gamePayout', label: '游戏返奖' },
|
||||||
|
{ key: 'gamePayoutRate', label: '游戏返奖率' },
|
||||||
{ key: 'gameProfit', label: '游戏利润' },
|
{ key: 'gameProfit', label: '游戏利润' },
|
||||||
|
{ key: 'gameProfitRate', label: '游戏利润率' },
|
||||||
|
{ key: 'd1RetentionRate', label: '次日留存率' },
|
||||||
|
{ key: 'd7RetentionRate', label: '七日留存率' },
|
||||||
|
{ key: 'd30RetentionRate', label: '30 日留存率' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type SummaryMetricKey = (typeof summaryMetricDefinitions)[number]['key'];
|
type SummaryMetricKey = (typeof summaryMetricDefinitions)[number]['key'];
|
||||||
type SummaryMetric = Record<SummaryMetricKey, number>;
|
type SummaryMetric = Record<string, number>;
|
||||||
|
|
||||||
const summaryColumns = [
|
const summaryColumns = [
|
||||||
...summaryMetricDefinitions.map((item) => ({
|
...summaryMetricDefinitions.map((item) => ({
|
||||||
@ -141,7 +188,7 @@ const summaryColumns = [
|
|||||||
dataIndex: item.key,
|
dataIndex: item.key,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
title: item.label,
|
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);
|
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: 'countryCode', key: 'countryCode', title: 'Code', width: 86 },
|
||||||
{ dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 },
|
{ dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 },
|
||||||
{ align: 'right', dataIndex: 'countryNewUser', key: 'countryNewUser', 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: 'newUserRecharge', key: 'newUserRecharge', title: '新增充值', width: 130 },
|
||||||
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 },
|
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 },
|
||||||
{ align: 'right', dataIndex: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 },
|
{ align: 'right', dataIndex: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 },
|
||||||
{ align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 130 },
|
{ align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 130 },
|
||||||
{ align: 'right', dataIndex: 'giftConsume', key: 'giftConsume', title: '礼物消耗', width: 130 },
|
{ align: 'right', dataIndex: 'giftConsume', key: 'giftConsume', title: '礼物消耗', width: 130 },
|
||||||
{ align: 'right', dataIndex: 'luckyGiftTotalFlow', key: 'luckyGiftTotalFlow', title: '幸运礼物流水', width: 150 },
|
{ 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: '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: 'luckyGiftProfit', key: 'luckyGiftProfit', title: '幸运礼物利润', width: 150 },
|
||||||
{ align: 'right', dataIndex: 'luckyGiftProfitRate', key: 'luckyGiftProfitRate', title: '幸运礼物利润率', width: 160 },
|
{ align: 'right', dataIndex: 'luckyGiftProfitRate', key: 'luckyGiftProfitRate', title: '幸运礼物利润率', width: 160 },
|
||||||
{ align: 'right', dataIndex: 'gameTotalFlow', key: 'gameTotalFlow', title: '游戏流水', width: 130 },
|
{ 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: '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: '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 rawRecords = computed(() => dashboard.value?.records || []);
|
||||||
const countryOptions = computed(() => {
|
const countryOptions = computed(() => {
|
||||||
@ -195,6 +253,26 @@ const summaryDataSource = computed(() => {
|
|||||||
}
|
}
|
||||||
return [row];
|
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) {
|
function hasPermission(code: string) {
|
||||||
const codes = accessCodes.value;
|
const codes = accessCodes.value;
|
||||||
@ -282,6 +360,14 @@ function toAmount(value?: null | number | string) {
|
|||||||
return Number.isFinite(number) ? number : 0;
|
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) {
|
function formatInteger(value?: null | number | string) {
|
||||||
const number = Number(value || 0);
|
const number = Number(value || 0);
|
||||||
return Number.isFinite(number) ? number.toLocaleString('en-US', { maximumFractionDigits: 0 }) : '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;
|
return (Number(record.luckyGiftProfit || 0) / flow) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMetricRate(record: Record<string, any>, 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 {
|
function emptySummary(): SummaryMetric {
|
||||||
return {
|
return {
|
||||||
countryNewUser: 0,
|
countryNewUser: 0,
|
||||||
|
d1RetentionBaseUser: 0,
|
||||||
|
d1RetentionRate: 0,
|
||||||
|
d1RetentionUser: 0,
|
||||||
|
d7RetentionBaseUser: 0,
|
||||||
|
d7RetentionRate: 0,
|
||||||
|
d7RetentionUser: 0,
|
||||||
|
d30RetentionBaseUser: 0,
|
||||||
|
d30RetentionRate: 0,
|
||||||
|
d30RetentionUser: 0,
|
||||||
|
dailyActiveUser: 0,
|
||||||
dealerRecharge: 0,
|
dealerRecharge: 0,
|
||||||
gamePayout: 0,
|
gamePayout: 0,
|
||||||
|
gamePayoutRate: 0,
|
||||||
gameProfit: 0,
|
gameProfit: 0,
|
||||||
|
gameProfitRate: 0,
|
||||||
gameTotalFlow: 0,
|
gameTotalFlow: 0,
|
||||||
|
gameUser: 0,
|
||||||
giftConsume: 0,
|
giftConsume: 0,
|
||||||
luckyGiftPayout: 0,
|
luckyGiftPayout: 0,
|
||||||
|
luckyGiftPayoutRate: 0,
|
||||||
luckyGiftProfit: 0,
|
luckyGiftProfit: 0,
|
||||||
luckyGiftProfitRate: 0,
|
luckyGiftProfitRate: 0,
|
||||||
luckyGiftTotalFlow: 0,
|
luckyGiftTotalFlow: 0,
|
||||||
|
luckyGiftUser: 0,
|
||||||
|
luckyGiftUserRate: 0,
|
||||||
newUserRecharge: 0,
|
newUserRecharge: 0,
|
||||||
salaryExchange: 0,
|
salaryExchange: 0,
|
||||||
totalRecharge: 0,
|
totalRecharge: 0,
|
||||||
@ -354,19 +488,26 @@ function buildVisibleSummary(list: CountryDashboardMetric[]) {
|
|||||||
const summary = emptySummary();
|
const summary = emptySummary();
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
for (const key of summaryMetricKeys) {
|
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.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;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSummaryValue(key: SummaryMetricKey, summary: SummaryMetric) {
|
function formatSummaryValue(key: SummaryMetricKey, summary: SummaryMetric) {
|
||||||
if (key === 'countryNewUser') {
|
if (integerMetricColumns.has(key)) {
|
||||||
return formatInteger(summary[key]);
|
return formatInteger(summary[key]);
|
||||||
}
|
}
|
||||||
if (key === 'luckyGiftProfitRate') {
|
if (percentMetricColumns.has(key)) {
|
||||||
return formatPercent(summary[key]);
|
return formatPercent(resolveMetricRate(summary, key));
|
||||||
}
|
}
|
||||||
return formatAmount(summary[key]);
|
return formatAmount(summary[key]);
|
||||||
}
|
}
|
||||||
@ -379,11 +520,11 @@ function filterCountry(input: string, option?: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tableCellText(record: Record<string, any>, key: string) {
|
function tableCellText(record: Record<string, any>, key: string) {
|
||||||
if (key === 'countryNewUser') {
|
if (integerMetricColumns.has(key)) {
|
||||||
return formatInteger(record.countryNewUser);
|
return formatInteger(record[key] as number | string);
|
||||||
}
|
}
|
||||||
if (key === 'luckyGiftProfitRate') {
|
if (percentMetricColumns.has(key)) {
|
||||||
return formatPercent(resolveLuckyGiftProfitRate(record));
|
return formatPercent(resolveMetricRate(record, key));
|
||||||
}
|
}
|
||||||
return formatAmount(record[key] as number | string);
|
return formatAmount(record[key] as number | string);
|
||||||
}
|
}
|
||||||
@ -637,6 +778,35 @@ onBeforeUnmount(() => {
|
|||||||
</Space>
|
</Space>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="metric-section-grid">
|
||||||
|
<section class="panel metric-section">
|
||||||
|
<div class="metric-section__title">幸运礼物 / 游戏</div>
|
||||||
|
<div class="metric-card-grid">
|
||||||
|
<div
|
||||||
|
v-for="item in luckyGameCards"
|
||||||
|
:key="item.label"
|
||||||
|
class="metric-card"
|
||||||
|
>
|
||||||
|
<div class="metric-card__label">{{ item.label }}</div>
|
||||||
|
<div class="metric-card__value">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="panel metric-section">
|
||||||
|
<div class="metric-section__title">APP 数据</div>
|
||||||
|
<div class="metric-card-grid metric-card-grid--app">
|
||||||
|
<div
|
||||||
|
v-for="item in appDataCards"
|
||||||
|
:key="item.label"
|
||||||
|
class="metric-card"
|
||||||
|
>
|
||||||
|
<div class="metric-card__label">{{ item.label }}</div>
|
||||||
|
<div class="metric-card__value">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="panel summary-table-panel">
|
<section class="panel summary-table-panel">
|
||||||
<Table
|
<Table
|
||||||
:columns="summaryColumns"
|
:columns="summaryColumns"
|
||||||
@ -680,7 +850,7 @@ onBeforeUnmount(() => {
|
|||||||
:data-source="records"
|
:data-source="records"
|
||||||
:pagination="{ pageSize: 20, showSizeChanger: true }"
|
:pagination="{ pageSize: 20, showSizeChanger: true }"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
:scroll="{ x: 2060 }"
|
:scroll="{ x: tableScrollX }"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@ -783,6 +953,55 @@ onBeforeUnmount(() => {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-section-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-section {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-section__title {
|
||||||
|
color: #2dd4bf;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card-grid--app {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
background: #111827;
|
||||||
|
border: 1px solid rgba(45, 212, 191, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card__label {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card__value {
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-table-panel {
|
.summary-table-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -874,5 +1093,11 @@ onBeforeUnmount(() => {
|
|||||||
.legacy-grid {
|
.legacy-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-section-grid,
|
||||||
|
.metric-card-grid,
|
||||||
|
.metric-card-grid--app {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user