@@ -0,0 +1,90 @@
< template >
< div >
< div v-if = "isChannel" v-loading="loading" class="report-cards" >
< el -card v-for = "card in cards" :key="card.label" shadow="never"><div class="report-card-label" > {{ card.label }} < / div > < strong > { { card . value } } < / strong > < / el-card >
< / div >
< div class = "kra-search-box" >
< el-form :model = "query" :inline = "true" @submit.prevent ="search" >
< el -form -item label = "所属平台" > < el-select v-model = "sources" multiple collapse-tags clearable placeholder="选择平台" style="width: 210px" @change="sourceChanged"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" / > < / el-select > < / el-form-item >
< el-form-item label = "游戏" > < el-select v-model = "games" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @visible-change="open => open && loadGames('')"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" / > < / el-select > < / el-form-item >
< el-form-item v-if = "isChannel" label="渠道类型"><el-select v-model="query.promoteType" clearable placeholder="全部渠道类型" style="width: 150px"><el-option label="公会" :value="1" /><el-option label="个人" :value="0" / > < / el-select > < / el-form-item >
< el-form-item v-if = "isChannel" label="渠道账号"><el-input v-model="query.promoteName" clearable placeholder="渠道账号(模糊匹配)" / > < / el-form-item >
< el-form-item label = "日期" > < el-date-picker v-model = "dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" / > < / el-form-item >
< el-form-item > < el-button type = "primary" icon = "search" native -type = " submit " > { { isChannel ? '搜索' : '查询' } } < / el-button > < / el-form-item >
< / el-form >
< / div >
< div class = "kra-table-box" >
< div v-if = "!isChannel" class="report-heading"><strong>核心指标</strong><el-text type="info" > 统计周期 : {{ period }} < / 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="统计所需业务汇总尚未接入。" / >
< el-table v-loading = "loading" :data="rows" :row-key="row => `${row.sourceDb}:${isChannel ? row.promoteId : row.gameId}`" :default-sort="{ prop: 'totalPay', order: 'descending' }" :show-summary="!isChannel" :summary-method="summaries" @sort-change="sortChanged" >
< el -table -column prop = "sourceDbLabel" label = "所属平台" min -width = " 100 " / >
< el-table-column : prop = "isChannel ? 'promoteName' : 'gameName'" : label = "isChannel ? '渠道名称' : '游戏名称'" min -width = " 170 " show -overflow -tooltip / >
< el-table-column v-if = "isChannel" prop="busierName" label="归属商务" min-width="140" show -overflow -tooltip / >
< el-table-column v-if = "isChannel" label="渠道类型" min-width="100"><template #default="{ row }" > {{ { 0 : ' 个人 ' , 1 : ' 公会 ' } [ row.promoteType ] ? ? row.promoteType ? ? ' — ' }} < / template > < / el -table -column >
< el-table-column v-else prop = "gameId" label = "游戏ID" min -width = " 100 " / >
< el-table-column v-for = "column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.width || 110" sortable="custom"><template #default="{ row }" > {{ format ( column.type , row [ column.key ] ) }} < / template > < / el -table -column >
< / el-table >
< div class = "kra-pagination" > < el-pagination v -model :current-page = "query.pageNum" v -model :page-size = "query.pageSize" :total = "total" : page -sizes = " [ 10 , 20 , 50 , 100 ] " layout = "total, sizes, prev, pager, next, jumper" @ size -change = " search " @ current -change = " load " / > < / div >
< / div >
< / div >
< / template >
< script setup >
import { computed , onMounted , reactive , ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listAnalytics } from '@/api/analytics'
import { listLedgerGames } from '@/api/ledger'
import { channelCount , channelMoney } from '../channel-display'
const props = defineProps ( { kind : { type : String , required : true } } )
const isChannel = computed ( ( ) => props . kind === 'channel' )
const platforms = { lx : '龙翔' , hw : '汇玩' , yq : '游趣' }
const day = ( ) => { const n = new Date ( ) ; return new Date ( n . getFullYear ( ) , n . getMonth ( ) , n . getDate ( ) ) }
const ymd = value => ` ${ value . getFullYear ( ) } - ${ String ( value . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } - ${ String ( value . getDate ( ) ) . padStart ( 2 , '0' ) } `
const end = day ( ) , begin = day ( ) ; begin . setDate ( begin . getDate ( ) - 29 )
const dates = ref ( [ ymd ( begin ) , ymd ( end ) ] )
const shortcuts = [
{ text : '今日' , value : ( ) => [ day ( ) , day ( ) ] } , { text : '昨日' , value : ( ) => { const d = day ( ) ; d . setDate ( d . getDate ( ) - 1 ) ; return [ d , new Date ( d ) ] } } ,
{ text : '本周' , value : ( ) => { const e = day ( ) , b = day ( ) ; b . setDate ( b . getDate ( ) - ( b . getDay ( ) || 7 ) + 1 ) ; return [ b , e ] } } ,
{ text : '本月' , value : ( ) => { const e = day ( ) ; return [ new Date ( e . getFullYear ( ) , e . getMonth ( ) , 1 ) , e ] } }
]
const sources = ref ( Object . keys ( platforms ) ) , games = ref ( [ ] ) , gameOptions = ref ( [ ] ) , gameLoading = ref ( false )
const query = reactive ( { pageNum : 1 , pageSize : 10 , promoteType : undefined , promoteName : '' , sortField : 'totalPay' , sortOrder : 'desc' } )
const rows = ref ( [ ] ) , total = ref ( 0 ) , summary = ref ( null ) , result = ref ( null ) , loading = ref ( false ) , available = ref ( true ) , error = ref ( '' )
let version = 0 , gameVersion = 0
const period = computed ( ( ) => result . value ? . beginTime && result . value ? . endTime ? ` ${ result . value . beginTime } 至 ${ result . value . endTime } ` : dates . value . join ( ' 至 ' ) )
const fields = { newRegisterCount : { key : 'newRegisterCount' , label : '新增注册' , type : 'count' } , activeCount : { key : 'activeCount' , label : '活跃玩家' , type : 'count' } , payUserCount : { key : 'payUserCount' , label : '付费玩家' , type : 'count' } , newPayUserCount : { key : 'newPayUserCount' , label : '新增付费' , type : 'count' } , totalPay : { key : 'totalPay' , label : '充值金额' , type : 'money' , width : 145 } , payRate : { key : 'payRate' , label : '付费率' , type : 'percent' } , arpu : { key : 'arpu' , label : 'ARPU' , type : 'money' } , arppu : { key : 'arppu' , label : 'ARPPU' , type : 'money' } }
const columns = computed ( ( ) => ( isChannel . value ? [ 'newRegisterCount' , 'activeCount' , 'totalPay' , 'payUserCount' , 'newPayUserCount' , 'payRate' ] : [ 'newRegisterCount' , 'activeCount' , 'payUserCount' , 'totalPay' , 'payRate' , 'arpu' , 'arppu' , 'newPayUserCount' ] ) . map ( key => fields [ key ] ) )
const format = ( type , value ) => value == null ? '未接入' : type === 'money' ? channelMoney ( value ) : type === 'percent' ? ` ${ value } % ` : channelCount ( value )
const cards = computed ( ( ) => [ { label : '渠道总数' , key : 'channelCount' , type : 'count' } , { label : '渠道总注册玩家' , key : 'totalRegisterCount' , type : 'count' } , { label : '渠道总充值' , key : 'totalPayAmount' , type : 'money' } , { label : '平均付费率' , key : 'avgPayRate' , type : 'percent' } ] . map ( card => ( { label : card . label , value : error . value ? '未获取' : format ( card . type , result . value ? . [ card . key ] ) } ) ) )
const summaries = ( { columns : tableColumns } ) => tableColumns . map ( ( column , index ) => { if ( index === 0 ) return '合计' ; if ( index === 1 ) return summary . value ? . gameName || '' ; const field = fields [ column . property ] ; return field ? format ( field . type , summary . value ? . [ column . property ] ) : '' } )
const clear = ( ) => { rows . value = [ ] ; total . value = 0 ; summary . value = null ; result . value = null }
const load = async ( ) => {
const current = ++ version
if ( ! sources . value . length ) { clear ( ) ; loading . value = false ; available . value = true ; return }
loading . value = true ; error . value = ''
try {
const res = await listAnalytics ( props . kind , { ... query , promoteName : query . promoteName . trim ( ) , sourceDb : sources . value . join ( ',' ) , gameName : games . value . join ( ',' ) , beginTime : dates . value [ 0 ] , endTime : dates . value [ 1 ] } )
if ( current !== version ) return
if ( res . code === 0 ) { result . value = res . data ; rows . value = res . data . rows ; total . value = res . data . total ; summary . value = res . data . summary ; available . value = res . data . available !== false }
else { clear ( ) ; error . value = res . msg || '统计查询失败' }
} catch { if ( current === version ) { clear ( ) ; error . value = '统计查询失败,请重试' } } finally { if ( current === version ) loading . value = false }
}
const search = ( ) => { query . pageNum = 1 ; if ( ! sources . value . length ) ElMessage . warning ( '请选择平台' ) ; return load ( ) }
const sortChanged = ( { prop , order } ) => { query . sortField = prop || 'totalPay' ; query . sortOrder = order === 'ascending' ? 'asc' : 'desc' ; return search ( ) }
const sourceChanged = ( ) => { ++ gameVersion ; games . value = [ ] ; gameOptions . value = [ ] ; gameLoading . value = false }
const loadGames = async ( keyword = '' ) => {
const current = ++ gameVersion ; if ( ! sources . value . length ) return
gameLoading . value = true
try { const res = await listLedgerGames ( { sourceDb : sources . value . join ( ',' ) , keyword , limit : 50 } ) ; if ( current === gameVersion && res . code === 0 ) gameOptions . value = [ ... new Set ( [ ... games . value , ... res . data . map ( row => row . gameName ) . filter ( Boolean ) ] ) ] } finally { if ( current === gameVersion ) gameLoading . value = false }
}
onMounted ( load )
< / script >
< style scoped >
. report - cards { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : 16 px ; margin - bottom : 16 px ; }
. report - card - label { color : var ( -- el - text - color - secondary ) ; font - size : 13 px ; margin - bottom : 12 px ; }
. report - cards strong { font - size : 25 px ; overflow - wrap : anywhere ; }
. report - heading { display : flex ; justify - content : space - between ; gap : 12 px ; margin - bottom : 18 px ; flex - wrap : wrap ; }
@ media ( max - width : 800 px ) { . report - cards { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } }
< / style >