feat: 迁移渠道商务游戏排行榜及同期比较
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
export const listRankings = (dimension, params) => service({ url: `/sw/rank/${dimension}`, method: 'get', params, timeout: 30000, donNotShowLoading: true })
|
||||
@@ -59,6 +59,7 @@
|
||||
"/src/view/sw/ChannelDataStatistics.vue": "SwChannelDataStatistics",
|
||||
"/src/view/sw/ChannelList.vue": "SwChannelList",
|
||||
"/src/view/sw/GameAnalytics.vue": "SwGameAnalytics",
|
||||
"/src/view/sw/RankingAnalysis.vue": "RankingAnalysis",
|
||||
"/src/view/sw/RechargeDetails.vue": "SwRechargeDetails",
|
||||
"/src/view/sw/RegistrationDetails.vue": "SwRegistrationDetails",
|
||||
"/src/view/sw/ServerAnalysis.vue": "ServerAnalysis",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" @submit.prevent="load">
|
||||
<el-form-item label="所属平台"><el-select v-model="sources" multiple collapse-tags clearable placeholder="全部平台" style="width: 240px" @change="load"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item><el-radio-group :model-value="dimension" @change="changeDimension"><el-radio-button v-for="(label, value) in dimensions" :key="value" :value="value">{{ label }}排行</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item><el-radio-group v-model="memory[dimension].range" @change="changeRange"><el-radio-button v-for="item in ranges" :key="item.value" :value="item.value">{{ item.label }}</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item label="日期"><el-date-picker v-model="memory[dimension].dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" style="width: 300px" @change="customDate" /></el-form-item>
|
||||
</el-form>
|
||||
<el-text type="info">统计周期:{{ dates.join(' 至 ') }}</el-text>
|
||||
</div>
|
||||
<el-alert v-if="error" class="mb-4" type="error" :closable="false" :title="error" />
|
||||
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="排行榜所需业务数据尚未接入。" />
|
||||
<div v-loading="loading" class="ranking-panels">
|
||||
<div v-for="panel in panels" :key="panel.key" class="kra-table-box ranking-panel">
|
||||
<h3>{{ panel.title }}</h3>
|
||||
<el-table :data="displayRows(panel)" row-key="rowKey" :row-class-name="({ row }) => row.rank <= 3 && !row.placeholder ? 'ranking-top' : ''">
|
||||
<el-table-column label="所属平台" min-width="90"><template #default="{ row }">{{ row.sourceDbLabel || '—' }}</template></el-table-column>
|
||||
<el-table-column label="#" width="45"><template #default="{ row }">{{ row.placeholder ? '—' : row.rank }}</template></el-table-column>
|
||||
<el-table-column :label="`${dimensions[dimension]}名称`" min-width="130" show-overflow-tooltip><template #default="{ row }">{{ row.objectName || '—' }}</template></el-table-column>
|
||||
<el-table-column :label="panel.label" min-width="125"><template #default="{ row }">{{ metric(panel, row.metricValue) }}</template></el-table-column>
|
||||
<el-table-column label="上期环比" min-width="112"><template #default="{ row }"><el-text :type="row.momStatus > 0 ? 'success' : row.momStatus < 0 ? 'danger' : 'info'">{{ change(panel, row) }}</el-text></template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { listRankings } from '@/api/ranking'
|
||||
import { channelCount, channelMoney } from './channel-display'
|
||||
defineOptions({ name: 'RankingAnalysis' })
|
||||
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }, dimensions = { channel: '渠道', business: '商务', game: '游戏' }
|
||||
const sources = ref(Object.keys(platforms)), dimension = ref('channel')
|
||||
const ranges = [{ value: 'today', label: '今日' }, { value: 'yesterday', label: '昨日' }, { value: '7d', label: '近7天' }, { value: '30d', label: '近30天' }]
|
||||
const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`
|
||||
const dateRange = range => { const e = new Date(), b = new Date(); if (range === 'yesterday') { e.setDate(e.getDate() - 1); b.setDate(b.getDate() - 1) } else if (range === '7d') b.setDate(b.getDate() - 6); else if (range === '30d') b.setDate(b.getDate() - 29); return [ymd(b), ymd(e)] }
|
||||
const memory = reactive(Object.fromEntries(Object.keys(dimensions).map(key => [key, { range: '7d', dates: dateRange('7d') }])))
|
||||
const dates = computed(() => memory[dimension.value].dates)
|
||||
const result = ref(null), available = ref(true), loading = ref(false), error = ref('')
|
||||
let version = 0
|
||||
const panels = computed(() => [
|
||||
{ key: 'registerRankList', title: '玩家注册量', label: '注册量' },
|
||||
{ key: 'rechargeRankList', title: '充值金额', label: '充值金额', money: true },
|
||||
{ key: 'payRateRankList', title: '付费率', label: '付费率', rate: true },
|
||||
{ key: 'activeRankList', title: `${ranges.find(item => item.value === memory[dimension.value].range)?.label || ''}活跃玩家数`, label: '活跃人数' }
|
||||
])
|
||||
const displayRows = panel => {
|
||||
const seen = new Set(), rows = []
|
||||
for (const item of result.value?.[panel.key] || []) { const key = `${item.sourceDb || ''}:${item.objectId}`; if (!seen.has(key)) { seen.add(key); rows.push({ ...item, rowKey: key }) } }
|
||||
for (let i = rows.length; i < 20; i++) rows.push({ rowKey: `placeholder-${i}`, placeholder: true })
|
||||
return rows
|
||||
}
|
||||
const metric = (panel, value) => value == null ? '—' : panel.money ? channelMoney(value) : panel.rate ? `${value}%` : channelCount(value)
|
||||
const change = (panel, row) => row.momValue == null ? '—' : `${row.momStatus > 0 ? '+' : ''}${row.momValue}${panel.rate ? ' 个百分点' : '%'}`
|
||||
const load = async () => {
|
||||
const current = ++version; loading.value = true; result.value = null; error.value = ''; available.value = true
|
||||
try {
|
||||
// 旧排行榜空选平台表示全部,保留此语义;其他报表页面有各自的空选规则。
|
||||
const res = await listRankings(dimension.value, { sourceDb: sources.value.length ? sources.value.join(',') : undefined, beginTime: dates.value[0], endTime: dates.value[1] })
|
||||
if (current !== version) return
|
||||
if (res.code === 0) { result.value = res.data; available.value = res.data.available !== false } else error.value = res.msg || '排行榜查询失败'
|
||||
} catch { if (current === version) error.value = '排行榜查询失败,请重试' } finally { if (current === version) loading.value = false }
|
||||
}
|
||||
const changeDimension = value => { dimension.value = value; const m = memory[value]; if (m.range !== 'custom') m.dates = dateRange(m.range); return load() }
|
||||
const changeRange = value => { memory[dimension.value].dates = dateRange(value); return load() }
|
||||
const customDate = () => { memory[dimension.value].range = 'custom'; return load() }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ranking-panels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.ranking-panel { min-width: 0; }
|
||||
.ranking-panel h3 { margin: 0 0 16px; font-weight: 600; }
|
||||
:deep(.ranking-top) { --el-table-tr-bg-color: var(--el-color-primary-light-9); }
|
||||
@media (max-width: 1100px) { .ranking-panels { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user