@@ -1,7 +1,7 @@
'use client' ;
import { useEffect , useMemo , useState } from 'react' ;
import { Alert , Select , Spin , Tag , Tooltip , message } from 'antd' ;
import { useEffect , useMemo , useState , type ReactNode } from 'react' ;
import { Alert , App , Select , Spin , Tag , Tooltip } from 'antd' ;
import { CalendarOutlined , InfoCircleOutlined } from '@ant-design/icons' ;
import dayjs from 'dayjs' ;
import { api } from '@/lib/api' ;
@@ -10,6 +10,8 @@ import type { AdRevenueReport, DashboardOverview } from '@/lib/types';
type PeriodKey = 'yesterday' | '7d' | '30d' ;
type TrendSeriesKey = 'dau' | 'newUsers' | 'comparisons' ;
type DashboardTrendPoint = DashboardOverview [ 'period' ] [ 'trend' ] [ number ] ;
type DeltaTone = 'muted' | 'up' | 'down' | 'flat' ;
type DeltaInfo = { value : ReactNode ; tone : DeltaTone } ;
const PERIODS : { key : PeriodKey ; label : string ; days : number } [ ] = [
{ key : 'yesterday' , label : '昨日' , days : 1 } ,
@@ -35,6 +37,72 @@ const fmtYuan = (yuan: number | null | undefined) => (yuan == null ? '--' : `¥$
const fmtMsAsSeconds = ( ms : number | null | undefined ) = > ( ms == null ? '--' : ( ms / 1000 ) . toFixed ( 1 ) ) ;
const pct = ( v : number | null | undefined ) = > ( v == null ? '--' : ` ${ ( v * 100 ) . toFixed ( 1 ) } % ` ) ;
const compact = ( v : number ) = > ( v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( v >= 10000 ? 0 : 1 ) } k ` : String ( v ) ) ;
const fmtCoinAmount = ( v : number | null | undefined ) = > {
if ( v == null ) return '--' ;
if ( Math . abs ( v ) >= 10000 ) return ` ${ ( v / 10000 ) . toFixed ( 2 ) . replace ( /\.?0+$/ , '' ) } 万 ` ;
return numberFmt . format ( v ) ;
} ;
const CMP_LABEL = 'vs 上周期' ;
function withCompare ( main : string ) {
return (
< >
{ main }
< span className = "cmp" > { CMP_LABEL } < / span >
< / >
) ;
}
function percentDelta ( current : number | null | undefined , previous : number | null | undefined ) : DeltaInfo {
if ( current == null || previous == null ) return { value : '暂无上期' , tone : 'flat' } ;
if ( previous === 0 ) {
if ( current === 0 ) return { value : withCompare ( '持平' ) , tone : 'flat' } ;
return { value : '上期为 0' , tone : current > 0 ? 'up' : 'down' } ;
}
const diff = ( current - previous ) / Math . abs ( previous ) ;
if ( Math . abs ( diff ) < 0.0005 ) return { value : withCompare ( '持平' ) , tone : 'flat' } ;
const arrow = diff > 0 ? '▲' : '▼' ;
return {
value : withCompare ( ` ${ arrow } ${ ( Math . abs ( diff ) * 100 ) . toFixed ( 1 ) } % ` ) ,
tone : diff > 0 ? 'up' : 'down' ,
} ;
}
function pointDelta ( current : number | null | undefined , previous : number | null | undefined ) : DeltaInfo {
if ( current == null || previous == null ) return { value : '暂无上期' , tone : 'flat' } ;
const diff = ( current - previous ) * 100 ;
if ( Math . abs ( diff ) < 0.05 ) return { value : withCompare ( '持平' ) , tone : 'flat' } ;
const arrow = diff > 0 ? '▲' : '▼' ;
return {
value : withCompare ( ` ${ arrow } ${ Math . abs ( diff ) . toFixed ( 1 ) } pt ` ) ,
tone : diff > 0 ? 'up' : 'down' ,
} ;
}
function durationDelta ( currentMs : number | null | undefined , previousMs : number | null | undefined ) : DeltaInfo {
if ( currentMs == null || previousMs == null ) return { value : '暂无上期' , tone : 'flat' } ;
const diffSeconds = ( currentMs - previousMs ) / 1000 ;
if ( Math . abs ( diffSeconds ) < 0.05 ) return { value : withCompare ( '持平' ) , tone : 'flat' } ;
return diffSeconds < 0
? { value : ` ▼ ${ Math . abs ( diffSeconds ) . toFixed ( 1 ) } s 更快 ` , tone : 'up' }
: { value : ` ▲ ${ Math . abs ( diffSeconds ) . toFixed ( 1 ) } s 更慢 ` , tone : 'down' } ;
}
function moneyDeltaCents ( current : number | null | undefined , previous : number | null | undefined ) : DeltaInfo {
if ( current == null || previous == null ) return { value : '暂无上期' , tone : 'flat' } ;
const diff = current - previous ;
if ( Math . abs ( diff ) < 0.5 ) return { value : withCompare ( '持平' ) , tone : 'flat' } ;
const arrow = diff > 0 ? '▲' : '▼' ;
return {
value : withCompare ( ` ${ arrow } ${ fmtCents ( Math . abs ( diff ) ) } ` ) ,
tone : diff > 0 ? 'up' : 'down' ,
} ;
}
function ratioBadge ( value : number | null | undefined , total : number | null | undefined ) : DeltaInfo {
if ( value == null || total == null || total <= 0 ) return { value : '占 --' , tone : 'flat' } ;
return { value : ` 占 ${ ( ( value / total ) * 100 ) . toFixed ( 1 ) } % ` , tone : 'flat' } ;
}
function periodRange ( period : PeriodKey ) {
const p = PERIODS . find ( ( it ) = > it . key === period ) ? ? PERIODS [ 0 ] ;
@@ -43,6 +111,12 @@ function periodRange(period: PeriodKey) {
return { start , end , label : ` ${ start . format ( 'MM-DD' ) } ~ ${ end . format ( 'MM-DD' ) } ` , days : p.days } ;
}
function previousPeriodRange ( range : ReturnType < typeof periodRange > ) {
const end = range . start . subtract ( 1 , 'day' ) ;
const start = end . subtract ( range . days - 1 , 'day' ) ;
return { start , end , label : ` ${ start . format ( 'MM-DD' ) } ~ ${ end . format ( 'MM-DD' ) } ` } ;
}
function retentionTitle ( period : PeriodKey ) {
if ( period === '7d' ) return '近 7 日新增留存' ;
if ( period === '30d' ) return '近 30 日新增留存' ;
@@ -92,6 +166,16 @@ function sceneAdEcpm(summary: ReturnType<typeof adSceneSummary>) {
return fmtYuan ( ( summary . revenue_yuan / summary . impressions ) * 1000 ) ;
}
function cpsCommissionCents ( data : DashboardOverview | null | undefined ) {
if ( ! data ? . cps . available ) return null ;
return ( data . cps . meituan_commission_cents ? ? 0 ) + ( data . cps . jd_commission_cents ? ? 0 ) ;
}
function revenueYuan ( ad : AdRevenueReport | null | undefined , cpsCents : number | null | undefined ) {
if ( ad ? . total_revenue_yuan == null && cpsCents == null ) return null ;
return ( ad ? . total_revenue_yuan ? ? 0 ) + ( ( cpsCents ? ? 0 ) / 100 ) ;
}
function niceMax ( v : number ) {
if ( v <= 5 ) return 5 ;
if ( v <= 10 ) return 10 ;
@@ -99,7 +183,7 @@ function niceMax(v: number) {
return Math . ceil ( v / base ) * base ;
}
function StatusText ( { children , tone = 'muted' } : { children : React.ReactNode ; tone ? : 'muted' | 'ok' | 'warn' } ) {
function StatusText ( { children , tone = 'muted' } : { children : ReactNode ; tone ? : 'muted' | 'ok' | 'warn' } ) {
return < span className = { ` status status- ${ tone } ` } > { children } < / span > ;
}
@@ -107,8 +191,8 @@ function DeltaBadge({
children ,
tone = 'muted' ,
} : {
children : React.ReactNode ;
tone ? : 'muted' | 'up' | 'down' ;
children : ReactNode ;
tone? : DeltaTone ;
} ) {
return < span className = { ` delta delta- ${ tone } ` } > { children } < / span > ;
}
@@ -116,11 +200,15 @@ function DeltaBadge({
function MiniSpark ( {
color = '#2F6BFF' ,
down ,
flat ,
} : {
color? : string ;
down? : boolean ;
flat? : boolean ;
} ) {
const points = down
const points = flat
? '0,16 13,16 26,15 39,16 52,15 65,16 78,15'
: down
? '0,8 13,10 26,9 39,14 52,12 65,18 78,20'
: '0,24 13,22 26,20 39,15 52,13 65,8 78,5' ;
return (
@@ -149,9 +237,9 @@ function StatCard({
status? : string ;
tone ? : 'muted' | 'ok' | 'warn' ;
emphasis? : boolean ;
spark ? : 'up' | 'down' ;
delta? : string ;
deltaTone ? : 'muted' | 'up' | 'down' ;
spark ? : 'up' | 'down' | 'flat' ;
delta? : ReactNode ;
deltaTone? : DeltaTone ;
} ) {
return (
< div className = { ` stat-card ${ emphasis ? ' stat-card-emphasis' : '' } ` } >
@@ -168,7 +256,13 @@ function StatCard({
{ value }
{ unit && value !== '--' && < span > { unit } < / span > }
< / div >
{ spark && < MiniSpark color = { spark === 'down' ? '#EF4444' : '#2F6BFF' } down = { spark === 'down' } / > }
{ spark && (
< MiniSpark
color = { spark === 'down' ? '#EF4444' : '#2F6BFF' }
down = { spark === 'down' }
flat = { spark === 'flat' }
/ >
) }
< / div >
{ delta ? < DeltaBadge tone = { deltaTone } > { delta } < / DeltaBadge > : status && < StatusText tone = { tone } > { status } < / StatusText > }
< / div >
@@ -181,18 +275,20 @@ function RevenueCard({
meta ,
accent ,
delta ,
deltaTone = 'up' ,
} : {
title : string ;
value : string ;
meta ? : { label : string ; value : string } [ ] ;
accent ? : 'blue' | 'green' | 'yellow' | 'red' ;
delta? : string ;
accent ? : 'blue' | 'green' | 'yellow' | 'red' | 'jd' ;
delta? : ReactNode ;
deltaTone? : DeltaTone ;
} ) {
return (
< div className = { ` revenue-card ${ accent ? ` rev- ${ accent } ` : '' } ` } >
< div className = "revenue-top" >
< div className = "revenue-tag" > { title } < / div >
{ delta && < DeltaBadge tone = "up" > { delta } < / DeltaBadge > }
{ delta && < DeltaBadge tone = { deltaTone } > { delta } < / DeltaBadge > }
< / div >
< div className = "revenue-value" > { value } < / div >
{ meta && meta . length > 0 && (
@@ -349,9 +445,12 @@ function TrendChart({ rangeLabel, points }: { rangeLabel: string; points: Dashbo
}
export default function DashboardPage() {
const { message } = App . useApp ( ) ;
const [ period , setPeriod ] = useState < PeriodKey > ( 'yesterday' ) ;
const [ data , setData ] = useState < DashboardOverview | null > ( null ) ;
const [ previousData , setPreviousData ] = useState < DashboardOverview | null > ( null ) ;
const [ adReport , setAdReport ] = useState < AdRevenueReport | null > ( null ) ;
const [ previousAdReport , setPreviousAdReport ] = useState < AdRevenueReport | null > ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ adLoading , setAdLoading ] = useState ( false ) ;
const [ overviewError , setOverviewError ] = useState < string | null > ( null ) ;
@@ -361,7 +460,9 @@ export default function DashboardPage() {
const [ cpsSyncing , setCpsSyncing ] = useState ( false ) ;
const range = useMemo ( ( ) = > periodRange ( period ) , [ period ] ) ;
const previousRange = useMemo ( ( ) = > previousPeriodRange ( range ) , [ range ] ) ;
const periodData = data ? . period ? ? null ;
const previousPeriodData = previousData ? . period ? ? null ;
const adAvgEcpm = avgEcpm ( adReport ) ;
const rewardVideoAdSummary = adTypeSummary ( adReport , 'reward_video' ) ;
const couponAdSummary = adSceneSummary ( adReport , 'coupon' ) ;
@@ -381,44 +482,84 @@ export default function DashboardPage() {
const regularTaskCoinTotal =
periodData ? . coins . regular_task_coin_total ? ?
( ( signinCoinTotal ? ? 0 ) + ( signinBoostCoinTotal ? ? 0 ) + ( taskCoinTotal ? ? 0 ) ) ;
const cpsCommissionCents = data ? . cps . meituan_commission_cents ;
const cpsAvailable = data ? . cps . available === true ;
const totalRevenueYuan =
adReport ? . total_revenue_yuan == null && ( ! cpsAvailable || cpsCommissionCents == null )
? null
: ( adReport ? . total_revenue_yuan ? ? 0 ) + ( cpsAvailable ? ( cpsCommissionCents ? ? 0 ) / 100 : 0 ) ;
const meituanCpsCommissionCents = data ? . cps . meituan_commission_cents ;
const jdCpsCommissionCents = data ? . cps . jd_commission_cents ;
const totalCpsCommissionCents = cpsCommissionCents ( data ) ;
const previousTotalCpsCommissionCents = cpsCommissionCents ( previousData ) ;
const totalRevenueYuan = revenueYuan ( adReport , totalCpsCommissionCents ) ;
const previousTotalRevenueYuan = revenueYuan ( previousAdReport , previousTotalCpsCommissionCents ) ;
const yesterdayTrendPoint = periodData ? . trend ? . [ periodData . trend . length - 1 ] ;
const yesterdayActiveUsers = yesterdayTrendPoint ? . active_users ? ? null ;
const arpuYuan =
totalRevenueYuan == null || yesterdayActiveUsers == null || yesterdayActiveUsers <= 0
? null
: totalRevenueYuan / yesterdayActiveUsers ;
const cpsRevenueText = cpsAvailable ? fmtCents ( cpsCommissionCents ) : '--' ;
const cpsRevenueText = cpsAvailable ? fmtCents ( totalCpsCommissionCents ) : '--' ;
const meituanCpsRevenueText = cpsAvailable ? fmtCents ( meituanCpsCommissionCents ) : '--' ;
const jdCpsRevenueText = cpsAvailable ? fmtCents ( jdCpsCommissionCents ) : '--' ;
const cpsHitRate = ! cpsAvailable || data ? . cps . meituan_hit_rate == null ? '--' : pct ( data . cps . meituan_hit_rate ) ;
const retentionValue =
periodData ? . users . retention_rate == null
? '--'
: ( periodData . users . retention_rate * 100 ) . toFixed ( 1 ) ;
const activeUserDelta = percentDelta ( periodData ? . users . active , previousPeriodData ? . users . active ) ;
const newUserDelta = percentDelta ( periodData ? . users . new , previousPeriodData ? . users . new ) ;
const retentionDelta = pointDelta ( periodData ? . users . retention_rate , previousPeriodData ? . users . retention_rate ) ;
const retentionSpark : 'up' | 'down' | 'flat' | undefined =
periodData ? . users . retention_rate == null
? undefined
: retentionDelta . tone === 'down'
? 'down'
: retentionDelta . tone === 'up' || periodData . users . retention_rate >= 1
? 'up'
: 'flat' ;
const comparisonTotalDelta = percentDelta ( periodData ? . comparison . total , previousPeriodData ? . comparison . total ) ;
const comparisonSuccessDelta = percentDelta ( periodData ? . comparison . success , previousPeriodData ? . comparison . success ) ;
const comparisonOrderedDelta = percentDelta ( periodData ? . comparison . ordered , previousPeriodData ? . comparison . ordered ) ;
const durationDeltaInfo = durationDelta (
periodData ? . comparison . average_duration_ms ,
previousPeriodData ? . comparison . average_duration_ms ,
) ;
const averageSavedDelta = moneyDeltaCents (
periodData ? . comparison . average_saved_cents ,
previousPeriodData ? . comparison . average_saved_cents ,
) ;
const totalRevenueDelta = percentDelta ( totalRevenueYuan , previousTotalRevenueYuan ) ;
const adRevenueDelta = percentDelta ( adReport ? . total_revenue_yuan , previousAdReport ? . total_revenue_yuan ) ;
const cpsRevenueDelta = percentDelta ( totalCpsCommissionCents , previousTotalCpsCommissionCents ) ;
const grantedCoinDelta = percentDelta ( periodData ? . coins . granted_total , previousPeriodData ? . coins . granted_total ) ;
const couponCoinRatio = ratioBadge ( couponRewardCoinTotal , periodData ? . coins . granted_total ) ;
const comparisonCoinRatio = ratioBadge ( comparisonRewardCoinTotal , periodData ? . coins . granted_total ) ;
const rewardVideoCoinRatio = ratioBadge ( periodData ? . coins . reward_video_coin_total , periodData ? . coins . granted_total ) ;
const regularTaskCoinRatio = ratioBadge ( regularTaskCoinTotal , periodData ? . coins . granted_total ) ;
useEffect ( ( ) = > {
let alive = true ;
setLoading ( true ) ;
setOverviewError ( null ) ;
api
. get < DashboardOverview > ( '/admin/api/stats/overview' , {
params : {
date_from : range.start.format ( 'YYYY-MM-DD' ) ,
date_to : range.end.format ( 'YYYY-MM-DD' ) ,
} ,
} )
. then ( ( r ) = > {
const currentParams = {
date_from : range.start.format ( 'YYYY-MM-DD' ) ,
date_to : range.end.format ( 'YYYY-MM-DD' ) ,
} ;
const previousParams = {
date_from : previousRange.start.format ( 'YYYY-MM-DD' ) ,
date_to : previousRange.end.format ( 'YYYY-MM-DD' ) ,
} ;
Promise . allSettled ( [
api . get < DashboardOverview > ( '/admin/api/stats/overview' , { params : currentParams } ) ,
api . get < DashboardOverview > ( '/admin/api/stats/overview' , { params : previousParams } ) ,
] )
. then ( ( [ current , previous ] ) = > {
if ( ! alive ) return ;
setData ( r . data ) ;
setUpdatedAt ( dayjs ( ) . format ( 'YYYY-MM-DD HH:mm' ) ) ;
} )
. catch ( ( ) = > {
if ( ! alive ) return ;
setOverviewError ( '总览接口加载失败' ) ;
if ( current . status === 'fulfilled' ) {
setData ( current . value . data ) ;
setUpdatedAt ( dayjs ( ) . format ( 'YYYY-MM-DD HH:mm' ) ) ;
} else {
setData ( null ) ;
setOverviewError ( '总览接口加载失败' ) ;
}
setPreviousData ( previous . status === 'fulfilled' ? previous.value.data : null ) ;
} )
. finally ( ( ) = > {
if ( alive ) setLoading ( false ) ;
@@ -426,29 +567,37 @@ export default function DashboardPage() {
return ( ) = > {
alive = false ;
} ;
} , [ range . start , range . end , refreshKey ] ) ;
} , [ range . start , range . end , previousRange . start , previousRange . end , refreshKey ] ) ;
useEffect ( ( ) = > {
let alive = true ;
setAdLoading ( true ) ;
setAdError ( null ) ;
api
. get < AdRevenueReport > ( '/admin/api/ad-revenue-report' , {
params : {
date_from : range.start.format ( 'YYYY-MM-DD' ) ,
date_to : range.end.format ( 'YYYY-MM-DD' ) ,
granularity : 'day' ,
limit : 1000 ,
} ,
} )
. then ( ( r ) = > {
const currentParams = {
date_from : range.start.format ( 'YYYY-MM-DD' ) ,
date_to : range.end.format ( 'YYYY-MM-DD' ) ,
granularity : 'day' ,
limit : 1000 ,
} ;
const previousParams = {
date_from : previousRange.start.format ( 'YYYY-MM-DD' ) ,
date_to : previousRange.end.format ( 'YYYY-MM-DD' ) ,
granularity : 'day' ,
limit : 1000 ,
} ;
Promise . allSettled ( [
api . get < AdRevenueReport > ( '/admin/api/ad-revenue-report' , { params : currentParams } ) ,
api . get < AdRevenueReport > ( '/admin/api/ad-revenue-report' , { params : previousParams } ) ,
] )
. then ( ( [ current , previous ] ) = > {
if ( ! alive ) return ;
setAdReport ( r . data ) ;
} )
. catch ( ( ) = > {
if ( ! alive ) return ;
setAdReport ( null ) ;
setAdError ( '广告收益接口加载失败' ) ;
if ( current . status === 'fulfilled' ) {
setAdReport ( current . value . data ) ;
} else {
setAdReport ( null ) ;
setAdError ( '广告收益接口加载失败' ) ;
}
setPreviousAdReport ( previous . status === 'fulfilled' ? previous.value.data : null ) ;
} )
. finally ( ( ) = > {
if ( alive ) setAdLoading ( false ) ;
@@ -456,7 +605,7 @@ export default function DashboardPage() {
return ( ) = > {
alive = false ;
} ;
} , [ range . start , range . end ] ) ;
} , [ range . start , range . end , previousRange . start , previousRange . end ] ) ;
if ( loading ) return < Spin style = { { display : 'block' , marginTop : 80 } } / > ;
@@ -464,7 +613,7 @@ export default function DashboardPage() {
return < Alert type = "error" showIcon message = { overviewError ? ? '数据大盘加载失败' } / > ;
}
const syncMeituanCps = async ( ) = > {
const syncCpsOrders = async ( ) = > {
setCpsSyncing ( true ) ;
try {
const resp = await api . post < {
@@ -476,12 +625,13 @@ export default function DashboardPage() {
params : {
date_from : range.start.format ( 'YYYY-MM-DD' ) ,
date_to : range.end.format ( 'YYYY-MM-DD' ) ,
platform : 'all' ,
} ,
} ) ;
message . success ( ` 已同步 ${ resp . data . fetched } 单 ` ) ;
setRefreshKey ( ( v ) = > v + 1 ) ;
} catch {
message . error ( '美团 订单刷新失败' ) ;
message . error ( 'CPS 订单刷新失败' ) ;
} finally {
setCpsSyncing ( false ) ;
}
@@ -527,20 +677,20 @@ export default function DashboardPage() {
< h3 > 核 心 趋 势 < / h3 >
< span > 先 看 用 户 、 留 存 与 比 价 活 跃 度 < / span >
< / div >
< div className = "grid g5 " >
< StatCard title = "总用户" value = { fmtInt ( data . users . total ) } status = "累计用户数" tone = "ok" spark = "up" / >
< div className = "grid g4 " >
< StatCard
title = "活跃用户"
value = { fmtInt ( periodData ? . users . active ) }
status = "本期活跃去重"
tone = "ok"
hint = "当前按本期内进入 App、完成比价记录 、开始领券流程 任一满足去重;未完成上报的比价开始事件待后续补埋点 。"
delta = { activeUserDelta . value }
deltaTone = { activeUserDelta . tone }
hint = "当前按本期内登录、开始比价 、开始领券任一满足去重;比价按 real_compare_start,领券按 real_coupon_start/claim_started 。"
spark = "up"
/ >
< StatCard
title = "新增用户"
value = { fmtInt ( periodData ? . users . new ) }
status = "本期新用户"
delta = { newUserDelta . value }
deltaTone = { newUserDelta . tone }
hint = "当前按注册时间落在所选日期范围统计;若产品要按首次进入 App/首次流程启动定义新增,需要补首次行为聚合。"
spark = "up"
/ >
@@ -548,16 +698,16 @@ export default function DashboardPage() {
title = { retentionTitle ( period ) }
value = { retentionValue }
unit = "%"
status = { ` ${ fmtInt ( periodData ? . users . retained_new_users ) } / ${ fmtInt ( periodData ? . users . new ) } 新用户 ` }
tone = "warn"
delta = { retentionDelta . value }
deltaTone = { retentionDelta . tone }
hint = { periodData ? . users . retention_note ? ? '暂无留存数据' }
spark = "down"
spark = { retentionSpark }
/ >
< StatCard
title = "比价次数"
value = { fmtInt ( periodData ? . comparison . total ) }
status = "本期比价次数"
tone = "muted"
delta = { comparisonTotalDelta . value }
deltaTone = { comparisonTotalDelta . tone }
spark = "up"
/ >
< / div >
@@ -571,23 +721,43 @@ export default function DashboardPage() {
< span > 发 起 、 成 功 、 下 单 与 效 率 < / span >
< / div >
< div className = "grid g5" >
< StatCard title = "发起比价" value = { fmtInt ( periodData ? . comparison . total ) } unit = "次" status = "本期发起次数" / >
< StatCard title = "比价成功" value = { fmtInt ( periodData ? . comparison . success ) } unit = "次" status = { pct ( compareSuccessRate ) } tone = "ok" / >
< StatCard
title = "发起比价"
value = { fmtInt ( periodData ? . comparison . total ) }
unit = "次"
delta = { comparisonTotalDelta . value }
deltaTone = { comparisonTotalDelta . tone }
/ >
< StatCard
title = "比价成功"
value = { fmtInt ( periodData ? . comparison . success ) }
unit = "次"
delta = { comparisonSuccessDelta . value }
deltaTone = { comparisonSuccessDelta . tone }
hint = { ` 当前成功率 ${ pct ( compareSuccessRate ) } ` }
/ >
< StatCard
title = "成功下单"
value = { fmtInt ( periodData ? . comparison . ordered ) }
unit = "单"
status = "已下单记录"
delta = { comparisonOrderedDelta . value }
deltaTone = { comparisonOrderedDelta . tone }
hint = "按比价记录页同一口径:用户有通过比价产生的下单记录,且店名与比价记录店名一致,则这条比价记录记为已下单。"
/ >
< StatCard
title = "平均耗时"
value = { fmtMsAsSeconds ( averageDurationMs ) }
unit = "秒"
status = { averageDurationMs == null ? '暂无耗时数据' : '本期平均耗时' }
delta = { durationDeltaInfo . value }
deltaTone = { durationDeltaInfo . tone }
hint = "按比价记录页同一口径:取所选日期范围内已上报耗时的比价记录,求平均后换算为秒。"
/ >
< StatCard title = "平均节省金额" value = { fmtCents ( periodData ? . comparison . average_saved_cents ) } status = "有节省的成功比价" / >
< StatCard
title = "平均节省金额"
value = { fmtCents ( periodData ? . comparison . average_saved_cents ) }
delta = { averageSavedDelta . value }
deltaTone = { averageSavedDelta . tone }
/ >
< / div >
< / section >
@@ -597,7 +767,7 @@ export default function DashboardPage() {
< h3 > 变 现 < / h3 >
< span >
{ cpsAvailable
? ( adError ? '广告收益接口当前后端不可用,美团 CPS 已接入' : '广告收益与美团 CPS 已接入' )
? ( adError ? '广告收益接口当前后端不可用,美团/京东 CPS 已接入' : '广告收益与美团/京东 CPS 已接入' )
: ( adError ? '广告收益接口当前后端不可用,订单/佣金数据待接' : '广告收益已接入,订单/佣金数据待接' ) }
< / span >
< / div >
@@ -607,6 +777,8 @@ export default function DashboardPage() {
title = "总收入"
value = { adLoading ? '--' : fmtYuan ( totalRevenueYuan ) }
accent = "blue"
delta = { totalRevenueDelta . value }
deltaTone = { totalRevenueDelta . tone }
meta = { [
{ label : '广告收入' , value : adLoading ? '--' : fmtYuan ( adReport ? . total_revenue_yuan ) } ,
{ label : 'CPS 收入' , value : cpsRevenueText } ,
@@ -615,38 +787,58 @@ export default function DashboardPage() {
< RevenueCard
title = "广告收入"
value = { adLoading ? '--' : fmtYuan ( adReport ? . total_revenue_yuan ) }
meta = { [ { label : '展示数' , value : adLoading ? '--' : fmtInt ( adReport ? . total_impressions ) } ] }
delta = { adRevenueDelta . value }
deltaTone = { adRevenueDelta . tone }
meta = { [
{
label : '占总收入' ,
value :
totalRevenueYuan && totalRevenueYuan > 0 && adReport ? . total_revenue_yuan != null
? ` ${ ( ( adReport . total_revenue_yuan / totalRevenueYuan ) * 100 ) . toFixed ( 1 ) } % `
: '--' ,
} ,
] }
/ >
< RevenueCard
title = "CPS 收入"
value = { cpsRevenueText }
meta = { [ { label : '来源' , value : cpsAvailable ? '美团已接,淘宝/京东暂空' : '订单/佣金待接' } ] }
delta = { cpsRevenueDelta . value }
deltaTone = { cpsRevenueDelta . tone }
meta = { [
{
label : '占总收入' ,
value :
totalRevenueYuan && totalRevenueYuan > 0 && totalCpsCommissionCents != null
? ` ${ ( ( ( totalCpsCommissionCents / 100 ) / totalRevenueYuan ) * 100 ) . toFixed ( 1 ) } % `
: '--' ,
} ,
] }
/ >
< RevenueCard
title = "ARPU"
value = { adLoading ? '--' : fmtYuan ( arpuYuan ) }
meta = { [
{ label : '口径' , value : '收入 / 活跃用户' } ,
{ label : 'ARPPU ' , value : '待确认付费用户口径' } ,
{ label : '口径' , value : '总 收入 / 昨日 活跃用户' } ,
{ label : '分母 ' , value : yesterdayActiveUsers == null ? '--' : ` ${ fmtInt ( yesterdayActiveUsers ) } 人 ` } ,
] }
/ >
< / div >
< div className = "sub-label cps-sub-label" >
< span > CPS 收 益 < / span >
< button type = "button" onClick = { syncMeituanCps } disabled = { cpsSyncing } >
{ cpsSyncing ? '刷新中' : '刷新美团 订单' }
< button type = "button" onClick = { syncCpsOrders } disabled = { cpsSyncing } >
{ cpsSyncing ? '刷新中' : '刷新 CPS 订单' }
< / button >
< / div >
< div className = "grid g4" >
< RevenueCard
title = "总 CPS 收益"
value = { cpsRevenueText }
meta = { [ { label : '来源' , value : cpsAvailable ? '美团订单佣金,淘宝/京东暂空 ' : '订单/佣金待接' } ] }
meta = { [ { label : '来源' , value : cpsAvailable ? '美团+京东 订单佣金,淘宝暂未接入 ' : '订单/佣金待接' } ] }
/ >
< RevenueCard
title = "美团 CPS"
value = { cpsRevenueText }
value = { meituanCpsRevenueText }
meta = { [
{ label : '订单量' , value : cpsAvailable ? fmtInt ( data ? . cps . meituan_order_count ) : '待订单数据' } ,
{
@@ -658,8 +850,16 @@ export default function DashboardPage() {
] }
accent = "yellow"
/ >
< RevenueCard title = "淘宝 CPS" value = "--" meta = { [ { label : '状态' , value : '后续接入后填写 ' } ] } accent = "red" / >
< RevenueCard title = "京东 CPS" value = "--" meta = { [ { label : '状态' , value : '后续接入后填写' } ] } accent = "red" / >
< RevenueCard title = "淘宝 CPS" value = "--" meta = { [ { label : '状态' , value : '暂未接入 ' } ] } accent = "red" / >
< RevenueCard
title = "京东 CPS"
value = { jdCpsRevenueText }
meta = { [
{ label : '有效订单' , value : cpsAvailable ? fmtInt ( data ? . cps . jd_order_count ) : '待订单数据' } ,
{ label : '无效订单' , value : cpsAvailable ? fmtInt ( data ? . cps . jd_invalid_count ? ? 0 ) : '待订单数据' } ,
] }
accent = "jd"
/ >
< / div >
< div className = "sub-label" > 广 告 收 益 < / div >
@@ -705,32 +905,58 @@ export default function DashboardPage() {
< span className = "bar" / >
< h3 > 金 币 经 济 < / h3 >
< Tag color = "warning" > 风 控 重 点 < / Tag >
< span > 发 放 是 账 面 负 债 , 提 现 是 真 实 现 金 成 本 < / span >
< span > 发 放 = 平 台 负 债 , 提 现 = 真 实 现 金 成 本 < / span >
< / div >
< div className = "grid g6" >
< StatCard title = "本期发放金币" value = { fmtInt ( periodData ? . coins . granted_total ) } status = "本期发放合计" / >
< StatCard
title = "本期发放金币"
value = { fmtInt ( periodData ? . coins . granted_total ) }
delta = { grantedCoinDelta . value }
deltaTone = { grantedCoinDelta . tone }
emphasis
/ >
< StatCard
title = "领券奖励金币"
value = { fmtInt ( couponRewardCoinTotal ) }
status = "领券相关发放"
delta = { couponCoinRatio . value }
deltaTone = { couponCoinRatio . tone }
/ >
< StatCard
title = "比价奖励金币"
value = { fmtInt ( comparisonRewardCoinTotal ) }
status = "比价相关发放"
delta = { comparisonCoinRatio . value }
deltaTone = { comparisonCoinRatio . tone }
/ >
< StatCard
title = "激励视频金币"
value = { fmtInt ( periodData ? . coins . reward_video_coin_total ) }
delta = { rewardVideoCoinRatio . value }
deltaTone = { rewardVideoCoinRatio . tone }
hint = "激励视频金币已按产品口径计入领券奖励金币,这里单独展示来源占比。"
/ >
< StatCard title = "激励视频金币" value = "--" status = "并入领券奖励" / >
< StatCard
title = "常规任务金币"
value = { fmtInt ( regularTaskCoinTotal ) }
status = "任务等发放"
delta = { regularTaskCoinRatio . value }
deltaTone = { regularTaskCoinRatio . tone }
/ >
< StatCard
title = "本期提现金额"
value = { fmtCents ( periodData ? . cash . withdraw_success_cents ) }
delta = "实付现金"
deltaTone = "flat"
/ >
< StatCard title = "本期提现金额" value = { fmtCents ( periodData ? . cash . withdraw_success_cents ) } status = "成功提现" / >
< / div >
< div className = "risk-note" >
本 期 已 发 放 { fmtInt ( periodData ? . coins . granted_total ) } 金 币 , 成 功 提 现 { fmtCents ( periodData ? . cash . withdraw_success_cents ) } ;
累 计 已 发 放 { fmtInt ( data . coins . granted_total ) } 金 币 , 累 计 成 功 提 现 { fmtCents ( data . cash . withdraw_success_cents ) } 。
金 币 分 类 已 按 产 品 口 径 归 类 ; 邀 请 、 后 台 手 动 加 金 币 、 福 利 页 广 告 、 无 法 判 断 来 源 的 广 告 不 进 入 上 述 分 类 。
⚠ ️
< div >
本 期 4 项 奖 励 合 计 发 放 < b > { fmtCoinAmount ( periodData ? . coins . granted_total ) } < / b > 金 币
( 账 面 负 债 ≈ < b > { fmtYuan ( ( periodData ? . coins . granted_total ? ? 0 ) / 1000 ) } < / b > ) ,
实 际 提 现 兑 付 < b > { fmtCents ( periodData ? . cash . withdraw_success_cents ) } < / b > 。
累 计 已 发 放 < b > { fmtCoinAmount ( data . coins . granted_total ) } < / b > 金 币 但 累 计 真 实 提 现 仅
< b > { fmtCents ( data . cash . withdraw_success_cents ) } < / b > , 发 放 与 现 金 兑 付 背 离 属 正 常 ( 金 币 沉 淀 / 过 期 ) ;
需 盯 紧 各 来 源 占 比 突 变 与 异 常 领 取 , 防 薅 羊 毛 。
< / div >
< / div >
< / section >
@@ -772,7 +998,7 @@ export default function DashboardPage() {
--coin: #ffb300;
--p-mt-waimai: #ffc300;
--p-taobao: #ff4400;
--p-jd: #e1251b ;
--p-jd: #b91c1c ;
--radius: 10px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.04);
color: var(--ink);
@@ -967,10 +1193,17 @@ export default function DashboardPage() {
font-weight: 600;
}
.status-muted,
.delta-muted {
.delta-muted,
.delta-flat {
color: var(--ink-3);
background: #f1f3f7;
}
.delta .cmp {
margin-left: 3px;
color: var(--ink-3);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
font-weight: 400;
}
.status-ok,
.delta-up {
color: var(--up);
@@ -1144,6 +1377,10 @@ export default function DashboardPage() {
background: var(--p-taobao);
color: #fff;
}
.rev-jd .revenue-tag {
background: var(--p-jd);
color: #fff;
}
.revenue-value {
color: var(--ink);
font-family: "DIN Alternate", "Helvetica Neue", sans-serif;