diff --git a/apps/src/api/legacy/datav.ts b/apps/src/api/legacy/datav.ts index 3fc363c..86cdee2 100644 --- a/apps/src/api/legacy/datav.ts +++ b/apps/src/api/legacy/datav.ts @@ -1,5 +1,41 @@ import { requestClient } from '#/api/request'; +export interface CountryDashboardMetric { + countryCode: string; + countryName: string; + countryNewUser: number; + dealerRecharge: number | string; + gamePayout: number | string; + gameProfit: number | string; + gameTotalFlow: number | string; + giftConsume: number | string; + luckyGiftAnchorShare: number | string; + luckyGiftPayout: number | string; + luckyGiftProfit: number | string; + luckyGiftTotalFlow: number | string; + newUserRecharge: number | string; + officialRecharge: number | string; + periodKey: string; + periodName: string; + salaryExchange: number | string; + totalRecharge: number | string; +} + +export interface CountryDashboardResult { + computedAt: string; + endDate?: string; + periodType: string; + records: CountryDashboardMetric[]; + startDate?: string; + total: CountryDashboardMetric; +} + +export async function countryDashboard(params: Record = {}) { + return requestClient.get('/datav/country-dashboard', { + params, + }); +} + export async function latestActiveUserCountryCode(params: Record = {}) { return requestClient.get>>( '/datav/active/user-country-code', diff --git a/apps/src/views/operate/baishun-config.vue b/apps/src/views/operate/baishun-config.vue index 7126666..e8bf917 100644 --- a/apps/src/views/operate/baishun-config.vue +++ b/apps/src/views/operate/baishun-config.vue @@ -36,6 +36,8 @@ function createProviderForm(sysOrigin = '') { sysOrigin, profile: 'PROD', platformBaseUrl: '', + gameListUrl: '', + luckyGiftUrl: '', appId: undefined as any, appName: '', appChannel: '', @@ -85,6 +87,16 @@ const activeProfileLabel = computed( activeProfile.value, ); +function defaultGameListUrl(platformBaseUrl: any) { + const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, ''); + return baseUrl ? `${baseUrl}/v1/api/gamelist` : ''; +} + +function defaultLuckyGiftUrl(platformBaseUrl: any) { + const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, ''); + return baseUrl ? `${baseUrl}/lucky_gift/start_game` : ''; +} + const catalogColumns = [ { dataIndex: 'cover', key: 'cover', title: '封面', width: 90 }, { dataIndex: 'name', key: 'name', title: '名称', width: 180 }, @@ -138,6 +150,9 @@ function applyProviderForm(record: Record) { activeProfile.value = String(record.activeProfile || activeProfile.value || 'PROD'); Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), { ...record, + gameListUrl: record.gameListUrl || defaultGameListUrl(record.platformBaseUrl), + luckyGiftUrl: + record.luckyGiftUrl || defaultLuckyGiftUrl(record.platformBaseUrl), profile: contextQuery.profile, sysOrigin: contextQuery.sysOrigin, appId: Number(record.appId || 0) || undefined, @@ -164,14 +179,24 @@ async function loadProviderConfig() { } async function persistProviderConfig(showSuccess = true) { + const gameListUrl = + String(providerForm.gameListUrl || '').trim() || + defaultGameListUrl(providerForm.platformBaseUrl); + const luckyGiftUrl = + String(providerForm.luckyGiftUrl || '').trim() || + defaultLuckyGiftUrl(providerForm.platformBaseUrl); + providerForm.gameListUrl = gameListUrl; + providerForm.luckyGiftUrl = luckyGiftUrl; if ( !contextQuery.sysOrigin || !providerForm.platformBaseUrl || + !gameListUrl || + !luckyGiftUrl || !providerForm.appId || !providerForm.appChannel || !providerForm.appKey ) { - message.warning('请补全百顺平台地址、AppId、AppChannel、AppKey'); + message.warning('请补全百顺平台地址、游戏列表地址、幸运礼物地址、AppId、AppChannel、AppKey'); return false; } providerSaving.value = true; @@ -322,6 +347,24 @@ async function handleImportSelected() { placeholder="https://game-cn-test.jieyou.shop" /> + + + + + + ({ label: item.name, value: item.value as any, })); + +function defaultGameListUrl(platformBaseUrl: any) { + const baseUrl = String(platformBaseUrl || '').trim().replace(/\/+$/, ''); + return baseUrl ? `${baseUrl}/v1/api/gamelist` : ''; +} + const launchModeOptions = [ { label: 'H5_REMOTE', value: 'H5_REMOTE' }, { label: 'H5_ZIP_LOCAL', value: 'H5_ZIP_LOCAL' }, @@ -222,6 +229,7 @@ function resetGameForm() { function applyProviderForm(record: Record) { Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), { ...record, + gameListUrl: record.gameListUrl || defaultGameListUrl(record.platformBaseUrl), sysOrigin: contextQuery.sysOrigin, appId: Number(record.appId || 0) || undefined, gsp: Number(record.gsp || 101) || 101, @@ -248,14 +256,19 @@ async function loadProviderConfig() { } async function persistProviderConfig(showSuccess = true) { + const gameListUrl = + String(providerForm.gameListUrl || '').trim() || + defaultGameListUrl(providerForm.platformBaseUrl); + providerForm.gameListUrl = gameListUrl; if ( !contextQuery.sysOrigin || !providerForm.platformBaseUrl || + !gameListUrl || !providerForm.appId || !providerForm.appChannel || !providerForm.appKey ) { - message.warning('请补全百顺平台地址、AppId、AppChannel、AppKey'); + message.warning('请补全百顺平台地址、游戏列表地址、AppId、AppChannel、AppKey'); return false; } providerSaving.value = true; @@ -458,6 +471,15 @@ async function handleImportSelected() { placeholder="https://game-cn-test.jieyou.shop" /> + + + -import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; +import type { + CountryDashboardResult, +} from '#/api/legacy/datav'; + +import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; -import { useWindowSize } from '@vueuse/core'; import { useAccessStore } from '@vben/stores'; +import { + Button, + DateRangePicker, + Empty, + Input, + message, + Select, + Space, + Spin, + Table, + Tooltip, +} from 'antdv-next'; +import dayjs from 'dayjs'; + +import { countryDashboard } from '#/api/legacy/datav'; +import SysOriginSelect from '#/components/sys-origin-select.vue'; import { getAllowedSysOrigins } from '#/views/system/shared'; import LatestPurchaseList from './components/latest-purchase-list.vue'; @@ -12,17 +31,83 @@ import PurchaseCount from './components/purchase-count.vue'; defineOptions({ name: 'StatisticsDatav' }); const accessStore = useAccessStore(); -const { width } = useWindowSize(); const datavFullscreen = ref(); const fullscreen = ref(false); +const loading = ref(false); +const dashboard = ref(null); +const dateRange = ref<[string, string] | null>([ + dayjs().subtract(6, 'day').format('YYYY-MM-DD'), + dayjs().format('YYYY-MM-DD'), +]); + +const query = reactive({ + countryKeyword: '', + periodType: 'DAY', + sysOrigin: '', +}); -const mobile = computed(() => width.value < 768); const accessCodes = computed(() => accessStore.accessCodes || []); -const permissionsSysOriginCode = computed(() => { +const sysOriginOptions = computed(() => { const options = getAllowedSysOrigins(accessCodes.value); - const finalOptions = options.length > 0 ? options : getAllowedSysOrigins([]); - return finalOptions.map((item) => String(item.value || '')); + return options.length > 0 ? options : getAllowedSysOrigins([]); +}); +const permissionsSysOriginCode = computed(() => + sysOriginOptions.value.map((item) => String(item.value || '')), +); + +const periodTypeOptions = [ + { label: '日', value: 'DAY' }, + { label: '周', value: 'WEEK' }, + { label: '月', value: 'MONTH' }, + { label: '全部', value: 'ALL' }, +]; + +const metricColumns = new Set([ + 'countryNewUser', + 'dealerRecharge', + 'gamePayout', + 'gameProfit', + 'gameTotalFlow', + 'giftConsume', + 'luckyGiftPayout', + 'luckyGiftProfit', + 'luckyGiftTotalFlow', + 'newUserRecharge', + 'salaryExchange', + 'totalRecharge', +]); + +const columns: 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: 'countryNewUser', key: 'countryNewUser', title: '新增', width: 110 }, + { 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: 'luckyGiftPayout', key: 'luckyGiftPayout', title: '幸运礼物返奖', width: 150 }, + { align: 'right', dataIndex: 'luckyGiftProfit', key: 'luckyGiftProfit', title: '幸运礼物利润', width: 150 }, + { align: 'right', dataIndex: 'gameTotalFlow', key: 'gameTotalFlow', title: '游戏流水', width: 130 }, + { align: 'right', dataIndex: 'gamePayout', key: 'gamePayout', title: '游戏返奖', width: 130 }, + { align: 'right', dataIndex: 'gameProfit', key: 'gameProfit', title: '游戏利润', width: 130 }, +]; + +const total = computed(() => dashboard.value?.total || null); +const records = computed(() => dashboard.value?.records || []); +const summaryCards = computed(() => { + const item = total.value; + return [ + { label: '新增用户', value: formatInteger(item?.countryNewUser) }, + { label: '总充值', value: formatAmount(item?.totalRecharge) }, + { label: '礼物消耗', value: formatAmount(item?.giftConsume) }, + { label: '幸运礼物利润', value: formatAmount(item?.luckyGiftProfit) }, + { label: '游戏流水', value: formatAmount(item?.gameTotalFlow) }, + { label: '游戏利润', value: formatAmount(item?.gameProfit) }, + ]; }); function hasPermission(code: string) { @@ -52,8 +137,94 @@ async function screen() { await element.requestFullscreen(); } +function defaultRange(periodType: string): [string, string] | null { + const now = dayjs(); + if (periodType === 'ALL') { + return null; + } + if (periodType === 'MONTH') { + return [now.startOf('month').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')]; + } + if (periodType === 'WEEK') { + const daysSinceMonday = now.day() === 0 ? 6 : now.day() - 1; + return [now.subtract(daysSinceMonday, 'day').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')]; + } + return [now.subtract(6, 'day').format('YYYY-MM-DD'), now.format('YYYY-MM-DD')]; +} + +function handlePeriodChange(value: string) { + dateRange.value = defaultRange(value); + loadDashboard(); +} + +function buildParams() { + const params: Record = { + countryKeyword: query.countryKeyword || undefined, + periodType: query.periodType, + sysOrigin: query.sysOrigin || undefined, + }; + if (dateRange.value?.[0] && dateRange.value?.[1]) { + params.startDate = dateRange.value[0]; + params.endDate = dateRange.value[1]; + } + return params; +} + +async function loadDashboard() { + if (!canQuery.value) { + return; + } + loading.value = true; + try { + dashboard.value = await countryDashboard(buildParams()); + } catch (error) { + dashboard.value = null; + message.error('数据大屏加载失败'); + throw error; + } finally { + loading.value = false; + } +} + +function formatInteger(value?: null | number | string) { + const number = Number(value || 0); + return Number.isFinite(number) ? number.toLocaleString('en-US', { maximumFractionDigits: 0 }) : '0'; +} + +function formatAmount(value?: null | number | string) { + const number = Number(value || 0); + return Number.isFinite(number) + ? number.toLocaleString('en-US', { + maximumFractionDigits: 2, + minimumFractionDigits: 0, + }) + : '0'; +} + +function tableCellText(record: Record, key: string) { + if (key === 'countryNewUser') { + return formatInteger(record.countryNewUser); + } + return formatAmount(record[key] as number | string); +} + +function rowKey(record: Record) { + return `${record.countryCode}-${record.periodKey}`; +} + +watch( + sysOriginOptions, + (options) => { + if (!query.sysOrigin && options[0]?.value) { + query.sysOrigin = String(options[0].value); + } + }, + { immediate: true }, +); + onMounted(() => { document.addEventListener('fullscreenchange', syncFullscreenState); + loadDashboard(); }); onBeforeUnmount(() => { @@ -65,33 +236,115 @@ onBeforeUnmount(() => {
-
-
数据大屏
+
+
+
数据大屏
+
+ 国家维度 · {{ dashboard?.computedAt || '-' }} +
+
+ + + +
-
实时数据预览
-
-
-
-
购买用户
-
- -
+
+
+ + + + + +
+ +
+
+
{{ item.label }}
+
{{ item.value }}
-
-
-
今日付费预览
-
- -
+
+ + + +
+ +
+
+ +
+
+
购买用户
+ +
+
+
今日付费预览
+
@@ -101,114 +354,156 @@ onBeforeUnmount(() => {