From 882d232c35b3f66f77522a944f560b045f0a6d75 Mon Sep 17 00:00:00 2001 From: hy Date: Fri, 24 Apr 2026 15:33:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=A4=A7=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/src/views/statistics/datav.vue | 253 ++++++++++++------ .../effects/plugins/src/echarts/echarts.ts | 2 + 2 files changed, 180 insertions(+), 75 deletions(-) diff --git a/apps/src/views/statistics/datav.vue b/apps/src/views/statistics/datav.vue index fd87044..339330a 100644 --- a/apps/src/views/statistics/datav.vue +++ b/apps/src/views/statistics/datav.vue @@ -6,7 +6,15 @@ import type { CountryDashboardResult, } from '#/api/legacy/datav'; -import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; +import { + computed, + nextTick, + onBeforeUnmount, + onMounted, + reactive, + ref, + watch, +} from 'vue'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { useAccessStore } from '@vben/stores'; @@ -15,7 +23,6 @@ import { Button, DateRangePicker, Empty, - Input, message, Select, Space, @@ -45,9 +52,9 @@ const dateRange = ref<[string, string] | null>([ dayjs().subtract(6, 'day').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD'), ]); +const selectedCountryCodes = ref([]); const query = reactive({ - countryKeyword: '', periodType: 'DAY', sysOrigin: '', }); @@ -94,6 +101,51 @@ const metricColumns = new Set([ 'totalRecharge', ]); +const summaryMetricKeys = [ + 'countryNewUser', + 'newUserRecharge', + 'dealerRecharge', + 'salaryExchange', + 'totalRecharge', + 'giftConsume', + 'luckyGiftTotalFlow', + 'luckyGiftPayout', + 'luckyGiftProfit', + 'gameTotalFlow', + 'gamePayout', + 'gameProfit', +] as const; + +const summaryMetricDefinitions = [ + { key: 'countryNewUser', label: '新增' }, + { key: 'newUserRecharge', label: '新增充值' }, + { key: 'dealerRecharge', label: '币商充值' }, + { key: 'salaryExchange', label: '工资兑换' }, + { key: 'totalRecharge', label: '总充值' }, + { key: 'giftConsume', label: '礼物消耗' }, + { key: 'luckyGiftTotalFlow', label: '幸运礼物流水' }, + { key: 'luckyGiftPayout', label: '幸运礼物返奖' }, + { key: 'luckyGiftProfit', label: '幸运礼物利润' }, + { key: 'luckyGiftProfitRate', label: '幸运礼物利润率' }, + { key: 'gameTotalFlow', label: '游戏流水' }, + { key: 'gamePayout', label: '游戏返奖' }, + { key: 'gameProfit', label: '游戏利润' }, +] as const; + +type SummaryMetricKey = (typeof summaryMetricDefinitions)[number]['key']; +type SummaryMetric = Record; + +const summaryColumns = [ + ...summaryMetricDefinitions.map((item) => ({ + align: 'right' as const, + dataIndex: item.key, + key: item.key, + title: item.label, + width: item.key === 'countryNewUser' ? 110 : (item.key === 'luckyGiftProfitRate' ? 160 : 140), + })), +]; +const summaryScrollX = summaryColumns.reduce((totalWidth, item) => totalWidth + item.width, 0); + const columns: any[] = [ { dataIndex: 'countryName', fixed: 'left' as const, key: 'countryName', title: '国家', width: 150 }, { dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 }, @@ -113,21 +165,35 @@ const columns: any[] = [ { align: 'right', dataIndex: 'gameProfit', key: 'gameProfit', title: '游戏利润', width: 130 }, ]; -const total = computed(() => dashboard.value?.total || null); -const records = computed(() => dashboard.value?.records || []); +const rawRecords = computed(() => dashboard.value?.records || []); +const countryOptions = computed(() => { + const optionMap = new Map(); + for (const item of rawRecords.value) { + const code = String(item.countryCode || ''); + if (!code || code === 'ALL') { + continue; + } + optionMap.set(code, `${item.countryName || code} / ${code}`); + } + return [...optionMap.entries()] + .map(([value, label]) => ({ label, value })) + .toSorted((current, next) => current.label.localeCompare(next.label)); +}); +const records = computed(() => { + const selected = new Set(selectedCountryCodes.value); + if (selected.size === 0) { + return rawRecords.value; + } + return rawRecords.value.filter((item) => selected.has(String(item.countryCode || ''))); +}); const chartRecords = computed(() => records.value.filter((item) => item.countryCode !== 'ALL')); -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?.luckyGiftTotalFlow) }, - { label: '幸运礼物利润', value: formatAmount(item?.luckyGiftProfit) }, - { label: '幸运礼物利润率', value: formatPercent(resolveLuckyGiftProfitRate(item)) }, - { label: '游戏流水', value: formatAmount(item?.gameTotalFlow) }, - { label: '游戏利润', value: formatAmount(item?.gameProfit) }, - ]; +const visibleSummary = computed(() => buildVisibleSummary(records.value)); +const summaryDataSource = computed(() => { + const row: Record = { key: 'summary' }; + for (const item of summaryMetricDefinitions) { + row[item.key] = formatSummaryValue(item.key, visibleSummary.value); + } + return [row]; }); function hasPermission(code: string) { @@ -157,9 +223,9 @@ async function screen() { await element.requestFullscreen(); } -function toggleVisualMode() { +async function toggleVisualMode() { visualMode.value = !visualMode.value; - renderVisualCharts(); + await renderVisualCharts(); } function defaultRange(periodType: string): [string, string] | null { @@ -184,7 +250,6 @@ function handlePeriodChange(value: string) { function buildParams() { const params: Record = { - countryKeyword: query.countryKeyword || undefined, periodType: query.periodType, sysOrigin: query.sysOrigin || undefined, }; @@ -267,6 +332,52 @@ function resolveLuckyGiftProfitRate(record?: null | { return (Number(record.luckyGiftProfit || 0) / flow) * 100; } +function emptySummary(): SummaryMetric { + return { + countryNewUser: 0, + dealerRecharge: 0, + gamePayout: 0, + gameProfit: 0, + gameTotalFlow: 0, + giftConsume: 0, + luckyGiftPayout: 0, + luckyGiftProfit: 0, + luckyGiftProfitRate: 0, + luckyGiftTotalFlow: 0, + newUserRecharge: 0, + salaryExchange: 0, + totalRecharge: 0, + }; +} + +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.luckyGiftProfitRate = resolveLuckyGiftProfitRate(summary); + return summary; +} + +function formatSummaryValue(key: SummaryMetricKey, summary: SummaryMetric) { + if (key === 'countryNewUser') { + return formatInteger(summary[key]); + } + if (key === 'luckyGiftProfitRate') { + return formatPercent(summary[key]); + } + return formatAmount(summary[key]); +} + +function filterCountry(input: string, option?: Record) { + const keyword = input.toLowerCase(); + const label = String(option?.label || '').toLowerCase(); + const value = String(option?.value || '').toLowerCase(); + return label.includes(keyword) || value.includes(keyword); +} + function tableCellText(record: Record, key: string) { if (key === 'countryNewUser') { return formatInteger(record.countryNewUser); @@ -394,6 +505,9 @@ async function renderVisualCharts() { return; } await nextTick(); + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }); const list = chartRecords.value; if (list.length === 0) { return; @@ -433,6 +547,13 @@ watch([records, visualMode], () => { renderVisualCharts(); }); +watch(countryOptions, (options) => { + const optionValues = new Set(options.map((item) => item.value)); + selectedCountryCodes.value = selectedCountryCodes.value.filter((code) => + optionValues.has(code), + ); +}); + onMounted(() => { document.addEventListener('fullscreenchange', syncFullscreenState); loadDashboard(); @@ -498,12 +619,17 @@ onBeforeUnmount(() => { value-format="YYYY-MM-DD" @change="loadDashboard" /> -