@@ -0,0 +1,677 @@
'use client' ;
import { useCallback , useEffect , useState } from 'react' ;
import type { ColumnsType } from 'antd/es/table' ;
import {
App ,
Button ,
Card ,
Col ,
DatePicker ,
Divider ,
Input ,
Row ,
Select ,
Space ,
Statistic ,
Table ,
Tag ,
Typography ,
} from 'antd' ;
import dayjs , { type Dayjs } from 'dayjs' ;
import { api , errMsg } from '@/lib/api' ;
import { formatUtcTime } from '@/lib/format' ;
import UserRecordsDrawer from '@/components/UserRecordsDrawer' ;
const { RangePicker } = DatePicker ;
// ── 领券数据看板类型(仅本页用,内联定义)──
interface CouponDataSummary {
started_count : number ;
completed_count : number ;
avg_elapsed_ms : number | null ;
p5_ms : number | null ;
p50_ms : number | null ;
p95_ms : number | null ;
p99_ms : number | null ;
}
interface CouponDataDaily {
date : string ;
started_count : number ;
completed_count : number ;
avg_elapsed_ms : number | null ;
}
interface CouponDataHourly {
hour : number ;
started_count : number ;
completed_count : number ;
avg_elapsed_ms : number | null ;
}
interface CouponDataRow {
id : number ;
trace_id : string ;
user_id : number | null ;
user_phone : string | null ;
user_nickname : string | null ;
status : string ;
platforms : string [ ] | null ;
origin_package : string | null ;
elapsed_ms : number | null ;
platform_elapsed : Record < string , number > | null ;
device_model : string | null ;
rom : string | null ;
app_env : string | null ;
started_at : string ;
claimed_count : number | null ;
trace_url : string | null ;
}
interface CouponDataReport {
date_from : string ;
date_to : string ;
summary : CouponDataSummary ;
daily : CouponDataDaily [ ] ;
hourly : CouponDataHourly [ ] ;
total : number ;
items : CouponDataRow [ ] ;
}
// 发起来源 App 包名 → 中文(「发起平台」列):空=傻瓜比价首页发起,外卖 App 包名→对应平台。
function originLabel ( pkg : string | null ) : string {
if ( ! pkg ) return '傻瓜比价' ;
if ( pkg . includes ( 'meituan' ) || pkg . includes ( 'sankuai' ) ) return '美团' ;
if ( pkg . includes ( 'taobao' ) || pkg . includes ( 'ele' ) ) return '淘宝' ;
if ( pkg . includes ( 'jingdong' ) || pkg . includes ( 'jd' ) ) return '京东' ;
return pkg ;
}
// 领券状态 → 颜色 + 中文(started 无终态 = 中途流失)
const STATUS_TAG : Record < string , { color : string ; label : string } > = {
started : { color : 'default' , label : '未完成' } ,
completed : { color : 'green' , label : '完成' } ,
failed : { color : 'red' , label : '失败' } ,
abandoned : { color : 'orange' , label : '中途退出' } ,
} ;
// ms → "1.5s"(空值显示 -)
const fmtSec = ( ms : number | null | undefined ) : string = >
ms == null ? '-' : ` ${ ( ms / 1000 ) . toFixed ( 1 ) } s ` ;
// 点手机号弹出的「该用户全部领券」抽屉列(精简版:单用户,不含用户/手机号列)。
const RECORD_COLUMNS : ColumnsType < CouponDataRow > = [
{ title : '时间' , dataIndex : 'started_at' , width : 160 , render : ( v : string ) = > formatUtcTime ( v ) } ,
{
title : '状态' ,
dataIndex : 'status' ,
width : 80 ,
render : ( s : string ) = > {
const t = STATUS_TAG [ s ] ? ? { color : 'default' , label : s } ;
return < Tag color = { t . color } > { t . label } < / Tag > ;
} ,
} ,
{ title : '发起平台' , dataIndex : 'origin_package' , width : 90 , render : ( pkg : string | null ) = > originLabel ( pkg ) } ,
{ title : '耗时' , dataIndex : 'elapsed_ms' , width : 80 , align : 'right' , render : ( v : number | null ) = > fmtSec ( v ) } ,
{ title : '美团' , key : 'mt' , width : 70 , align : 'right' , render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'meituan-waimai' ] ) } ,
{ title : '淘宝' , key : 'tb' , width : 70 , align : 'right' , render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'taobao-shanguang' ] ) } ,
{ title : '京东' , key : 'jd' , width : 70 , align : 'right' , render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'jd-waimai' ] ) } ,
{
title : 'trace' ,
dataIndex : 'trace_id' ,
width : 90 ,
render : ( v : string , r : CouponDataRow ) = >
r . trace_url ? (
< a href = { r . trace_url } target = "_blank" rel = "noreferrer" title = { v } > trace < / a >
) : (
< Typography.Text type = "secondary" style = { { fontSize : 12 } } > { v . slice ( 0 , 6 ) } … < / Typography.Text >
) ,
} ,
] ;
// ── 趋势图(纯 SVG,零依赖,复用广告报表同款):浅蓝柱=发起数、深蓝柱=完成数(左轴 次数),橙线=平均耗时(右轴 秒)──
const CHART_BAR_STARTED = '#bae0ff' ;
const CHART_BAR_COMPLETED = '#1677ff' ;
const CHART_LINE = '#fa8c16' ;
interface TrendPoint {
label : string ; // x 轴刻度(小时数 / MM-DD)
tip : string ; // hover 原生 tooltip
started : number ;
completed : number ;
avgSec : number | null ; // 平均耗时(秒);该桶无完成为 null(不画线点)
}
// 按天聚合:用 from..to 补齐空缺日为 0,轴连续。
function aggregateDaily ( dateFrom : string , dateTo : string , daily : CouponDataDaily [ ] ) : TrendPoint [ ] {
const map = new Map ( daily . map ( ( d ) = > [ d . date , d ] ) ) ;
const out : TrendPoint [ ] = [ ] ;
let cur = dayjs ( dateFrom ) ;
let guard = 0 ;
while ( cur . format ( 'YYYY-MM-DD' ) <= dateTo && guard < 400 ) {
const ds = cur . format ( 'YYYY-MM-DD' ) ;
const d = map . get ( ds ) ;
const started = d ? . started_count ? ? 0 ;
const completed = d ? . completed_count ? ? 0 ;
const avgSec = d ? . avg_elapsed_ms != null ? d . avg_elapsed_ms / 1000 : null ;
out . push ( {
label : ds.slice ( 5 ) ,
tip : ` ${ ds } 发起 ${ started } · 完成 ${ completed } · 平均耗时 ${ avgSec == null ? '-' : avgSec . toFixed ( 1 ) + 's' } ` ,
started ,
completed ,
avgSec ,
} ) ;
cur = cur . add ( 1 , 'day' ) ;
guard += 1 ;
}
return out ;
}
// 按小时聚合(0– 23)。
function aggregateHourly ( rows : CouponDataHourly [ ] ) : TrendPoint [ ] {
const byHour = new Map ( rows . map ( ( r ) = > [ r . hour , r ] ) ) ;
return Array . from ( { length : 24 } , ( _ , h ) = > {
const r = byHour . get ( h ) ;
const started = r ? . started_count ? ? 0 ;
const completed = r ? . completed_count ? ? 0 ;
const avgSec = r ? . avg_elapsed_ms != null ? r . avg_elapsed_ms / 1000 : null ;
return {
label : String ( h ) ,
tip : ` ${ String ( h ) . padStart ( 2 , '0' ) } :00 发起 ${ started } · 完成 ${ completed } · 平均耗时 ${ avgSec == null ? '-' : avgSec . toFixed ( 1 ) + 's' } ` ,
started ,
completed ,
avgSec ,
} ;
} ) ;
}
function TrendChart ( { points } : { points : TrendPoint [ ] } ) {
const n = points . length ;
const maxBar = Math . max ( 1 , . . . points . map ( ( p ) = > p . started ) ) ;
const maxSec = Math . max ( 1 e - 9 , . . . points . map ( ( p ) = > p . avgSec ? ? 0 ) ) ;
const W = 960 ;
const H = 280 ;
const padL = 48 ;
const padR = 56 ;
const padT = 16 ;
const padB = 32 ;
const plotW = W - padL - padR ;
const plotH = H - padT - padB ;
const step = plotW / Math . max ( 1 , n ) ;
const barW = Math . min ( 24 , step * 0.5 ) ;
const yBase = padT + plotH ;
const barX = ( i : number ) = > padL + i * step + ( step - barW ) / 2 ;
const cx = ( i : number ) = > padL + i * step + step / 2 ;
const barH = ( v : number ) = > ( v / maxBar ) * plotH ;
const secY = ( v : number ) = > yBase - ( v / maxSec ) * plotH ;
// X 轴刻度按天/按小时尽量每格都标(≤31 个点每个都标,超出才抽稀),避免跨度稍大就隔天显示。
const labelEvery = Math . max ( 1 , Math . ceil ( n / 31 ) ) ;
// 平均耗时线:仅 avgSec 非 null 的点连线(无完成的桶断开)。
const linePts = points
. map ( ( p , i ) = > ( p . avgSec == null ? null : ` ${ cx ( i ) } , ${ secY ( p . avgSec ) } ` ) )
. filter ( ( x ) : x is string = > x !== null )
. join ( ' ' ) ;
return (
< svg viewBox = { ` 0 0 ${ W } ${ H } ` } width = "100%" style = { { display : 'block' } } role = "img" aria-label = "趋势图" >
{ [ 0 , 0.25 , 0.5 , 0.75 , 1 ] . map ( ( t ) = > {
const y = yBase - t * plotH ;
return (
< g key = { t } >
< line x1 = { padL } y1 = { y } x2 = { W - padR } y2 = { y } stroke = "#f0f0f0" / >
< text x = { padL - 8 } y = { y + 4 } textAnchor = "end" fontSize = { 11 } fill = { CHART_BAR_COMPLETED } >
{ Math . round ( maxBar * t ) }
< / text >
< text x = { W - padR + 8 } y = { y + 4 } textAnchor = "start" fontSize = { 11 } fill = { CHART_LINE } >
{ ( maxSec * t ) . toFixed ( 1 ) }
< / text >
< / g >
) ;
} ) }
{ /* 发起数柱(浅) */ }
{ points . map ( ( p , i ) = > (
< rect
key = { ` s- ${ i } ` }
x = { barX ( i ) }
y = { yBase - barH ( p . started ) }
width = { barW }
height = { barH ( p . started ) }
fill = { CHART_BAR_STARTED }
rx = { 2 }
>
< title > { p . tip } < / title >
< / rect >
) ) }
{ /* 完成数柱(深,窄,叠中间) */ }
{ points . map ( ( p , i ) = > {
const cw = barW * 0.55 ;
return (
< rect
key = { ` c- ${ i } ` }
x = { cx ( i ) - cw / 2 }
y = { yBase - barH ( p . completed ) }
width = { cw }
height = { barH ( p . completed ) }
fill = { CHART_BAR_COMPLETED }
rx = { 1 }
>
< title > { p . tip } < / title >
< / rect >
) ;
} ) }
{ /* 平均耗时线(橙,右轴) */ }
< polyline points = { linePts } fill = "none" stroke = { CHART_LINE } strokeWidth = { 2 } / >
{ points . map ( ( p , i ) = >
p . avgSec == null ? null : (
< circle key = { ` p- ${ i } ` } cx = { cx ( i ) } cy = { secY ( p . avgSec ) } r = { 2.5 } fill = { CHART_LINE } >
< title > { p . tip } < / title >
< / circle >
) ,
) }
{ points . map ( ( p , i ) = >
i % labelEvery === 0 || i === n - 1 ? (
< text key = { ` x- ${ i } ` } x = { cx ( i ) } y = { H - 14 } textAnchor = "middle" fontSize = { 11 } fill = "#999" >
{ p . label }
< / text >
) : null ,
) }
< / svg >
) ;
}
// 领券数据看板:上半汇总卡 + 按天/小时趋势(参考广告收益大盘),下半逐条领券明细。
// 数据源 coupon_session(客户端 /api/v1/coupon/session 两段上报);耗时统计只算 completed。
export default function CouponDataPage() {
const { message } = App . useApp ( ) ;
const [ range , setRange ] = useState < [ Dayjs , Dayjs ] > ( [ dayjs ( ) . subtract ( 6 , 'day' ) , dayjs ( ) ] ) ;
const [ user , setUser ] = useState < string > ( '' ) ;
const [ appEnv , setAppEnv ] = useState < string > ( 'prod' ) ;
const [ granularity , setGranularity ] = useState < 'day' | 'hour' > ( 'day' ) ;
const [ sortBy , setSortBy ] = useState < 'time' | 'elapsed' > ( 'time' ) ;
const [ limit , setLimit ] = useState < number > ( 100 ) ;
const [ page , setPage ] = useState < number > ( 1 ) ;
const [ queriedGranularity , setQueriedGranularity ] = useState < 'day' | 'hour' > ( 'day' ) ;
const [ queriedMultiDay , setQueriedMultiDay ] = useState ( false ) ;
const [ queriedLimit , setQueriedLimit ] = useState < number > ( 100 ) ;
const [ data , setData ] = useState < CouponDataReport | null > ( null ) ;
const [ loading , setLoading ] = useState ( false ) ;
// 点手机号:抽屉看该用户全部领券(总次数 + 记录)
const [ recordsUser , setRecordsUser ] = useState < { userId : number ; phone : string | null } | null > ( null ) ;
const openUserRecords = ( r : CouponDataRow ) = > {
if ( r . user_id != null ) setRecordsUser ( { userId : r.user_id , phone : r.user_phone } ) ;
} ;
// 跨多天时「按小时」无意义,粒度强制按天(同广告报表)。
const rangeMultiDay = range [ 0 ] . format ( 'YYYY-MM-DD' ) !== range [ 1 ] . format ( 'YYYY-MM-DD' ) ;
const load = useCallback (
async ( targetPage = 1 , targetLimit = limit , targetSort = sortBy ) = > {
const from = range [ 0 ] . format ( 'YYYY-MM-DD' ) ;
const to = range [ 1 ] . format ( 'YYYY-MM-DD' ) ;
const multiDay = from !== to ;
const gran = multiDay ? 'day' : granularity ;
setLoading ( true ) ;
try {
const res = await api . get < CouponDataReport > ( '/admin/api/coupon-data' , {
params : {
date_from : from ,
date_to : to ,
user : user.trim ( ) || undefined ,
app_env : appEnv ,
granularity : gran ,
limit : targetLimit ,
offset : ( targetPage - 1 ) * targetLimit ,
sort : targetSort ,
} ,
} ) ;
setData ( res . data ) ;
setPage ( targetPage ) ;
setQueriedLimit ( targetLimit ) ;
setQueriedGranularity ( gran ) ;
setQueriedMultiDay ( multiDay ) ;
} catch ( e ) {
message . error ( errMsg ( e ) ) ;
} finally {
setLoading ( false ) ;
}
} ,
[ range , user , appEnv , granularity , limit , sortBy , message ] ,
) ;
useEffect ( ( ) = > {
load ( ) ;
// 仅首次自动拉近 7 天;之后由「查询」按钮触发
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ ] ) ;
const columns : ColumnsType < CouponDataRow > = [
{
title : '用户' ,
dataIndex : 'user_nickname' ,
width : 140 ,
render : ( _ : unknown , r : CouponDataRow ) = > {
if ( r . user_nickname ) {
return (
< span >
{ r . user_nickname }
{ r . user_id != null && (
< Typography.Text type = "secondary" style = { { fontSize : 11 , marginLeft : 6 } } >
# { r . user_id }
< / Typography.Text >
) }
< / span >
) ;
}
if ( r . user_id != null ) return < Typography.Text type = "secondary" > # { r . user_id } < / Typography.Text > ;
return < Typography.Text type = "secondary" > 游 客 < / Typography.Text > ;
} ,
} ,
{
title : '手机号' ,
dataIndex : 'user_phone' ,
width : 130 ,
// 点手机号 → 抽屉看该用户全部领券(参考低价审核/反馈页)。游客(无 user_id)不可点。
render : ( phone : string | null , r : CouponDataRow ) = >
r . user_id != null ? (
< a onClick = { ( ) = > openUserRecords ( r ) } > { phone || ` # ${ r . user_id } ` } < / a >
) : (
< Typography.Text type = "secondary" > 游 客 < / Typography.Text >
) ,
} ,
{
title : '状态' ,
dataIndex : 'status' ,
width : 90 ,
render : ( s : string ) = > {
const t = STATUS_TAG [ s ] ? ? { color : 'default' , label : s } ;
return < Tag color = { t . color } > { t . label } < / Tag > ;
} ,
} ,
{
title : '发起平台' ,
dataIndex : 'origin_package' ,
width : 110 ,
render : ( pkg : string | null ) = > originLabel ( pkg ) ,
} ,
{
title : '耗时' ,
dataIndex : 'elapsed_ms' ,
width : 90 ,
align : 'right' ,
render : ( v : number | null ) = > fmtSec ( v ) ,
} ,
{
title : '美团耗时' ,
key : 'mt_elapsed' ,
width : 90 ,
align : 'right' ,
render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'meituan-waimai' ] ) ,
} ,
{
title : '淘宝耗时' ,
key : 'tb_elapsed' ,
width : 90 ,
align : 'right' ,
render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'taobao-shanguang' ] ) ,
} ,
{
title : '京东耗时' ,
key : 'jd_elapsed' ,
width : 90 ,
align : 'right' ,
render : ( _ : unknown , r : CouponDataRow ) = > fmtSec ( r . platform_elapsed ? . [ 'jd-waimai' ] ) ,
} ,
{
title : '机型/ROM' ,
key : 'device' ,
width : 200 ,
render : ( _ : unknown , r : CouponDataRow ) = > {
const parts = [ r . device_model , r . rom ] . filter ( Boolean ) ;
return parts . length ? parts . join ( ' / ' ) : < Typography.Text type = "secondary" > - < / Typography.Text > ;
} ,
} ,
{
title : '时间' ,
dataIndex : 'started_at' ,
width : 165 ,
render : ( v : string ) = > formatUtcTime ( v ) ,
} ,
{
title : '领券 trace' ,
dataIndex : 'trace_id' ,
width : 150 ,
// 有 trace_url(pricebot 上云,仅 completed)→ 可点链接打开完整现场;否则降级为可复制 trace_id。
render : ( v : string , r : CouponDataRow ) = >
r . trace_url ? (
< a href = { r . trace_url } target = "_blank" rel = "noreferrer" title = { v } >
trace ( { v . slice ( 0 , 8 ) } … )
< / a >
) : (
< Typography.Text copyable = { { text : v } } style = { { fontSize : 12 } } >
{ v . slice ( 0 , 8 ) } …
< / Typography.Text >
) ,
} ,
] ;
const summary = data ? . summary ;
const items = data ? . items ? ? [ ] ;
return (
< div >
< Space align = "center" style = { { display : 'flex' , marginBottom : 16 } } >
< h2 style = { { margin : 0 } } > 领 券 数 据 < / h2 >
< Typography.Text type = "secondary" style = { { fontSize : 12 } } >
发 起 → 完 成 耗 时 口 径 为 客 户 端 全 程 计 时 ; 均 值 / 分 位 仅 统 计 「 完 成 」 的 领 券
< / Typography.Text >
< / Space >
< Card size = "small" style = { { marginBottom : 16 } } >
< Space wrap size = "middle" align = "center" >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 日 期 < / Typography.Text >
< RangePicker
value = { range }
onChange = { ( v ) = > v && v [ 0 ] && v [ 1 ] && setRange ( [ v [ 0 ] , v [ 1 ] ] ) }
allowClear = { false }
presets = { [
{ label : '今天' , value : [ dayjs ( ) , dayjs ( ) ] } ,
{ label : '近 7 天' , value : [ dayjs ( ) . subtract ( 6 , 'day' ) , dayjs ( ) ] } ,
{ label : '近 30 天' , value : [ dayjs ( ) . subtract ( 29 , 'day' ) , dayjs ( ) ] } ,
] }
/ >
< / Space >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 用 户 < / Typography.Text >
< Input
placeholder = "手机号/昵称"
value = { user }
onChange = { ( e ) = > setUser ( e . target . value ) }
onPressEnter = { ( ) = > load ( 1 ) }
allowClear
style = { { width : 150 } }
/ >
< / Space >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 环 境 < / Typography.Text >
< Select
value = { appEnv }
onChange = { setAppEnv }
style = { { width : 120 } }
options = { [
{ value : 'prod' , label : '正式(prod)' } ,
{ value : 'dev' , label : '测试(dev)' } ,
{ value : 'all' , label : '全部' } ,
] }
/ >
< / Space >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 粒 度 < / Typography.Text >
< Select
value = { rangeMultiDay ? 'day' : granularity }
onChange = { setGranularity }
disabled = { rangeMultiDay }
style = { { width : 110 } }
title = { rangeMultiDay ? '跨多天仅支持按天' : undefined }
options = { [
{ value : 'day' , label : '按天' } ,
{ value : 'hour' , label : '按小时' } ,
] }
/ >
< / Space >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 排 序 < / Typography.Text >
< Select
value = { sortBy }
onChange = { ( v ) = > {
setSortBy ( v ) ;
load ( 1 , limit , v ) ;
} }
style = { { width : 130 } }
options = { [
{ value : 'time' , label : '时间倒序' } ,
{ value : 'elapsed' , label : '耗时倒序' } ,
] }
/ >
< / Space >
< Space size = { 6 } >
< Typography.Text type = "secondary" > 每 页 < / Typography.Text >
< Select
value = { limit }
onChange = { ( v ) = > {
setLimit ( v ) ;
load ( 1 , v ) ;
} }
style = { { width : 120 } }
options = { [ 20 , 50 , 100 , 200 , 500 ] . map ( ( nn ) = > ( { value : nn , label : ` ${ nn } 条/页 ` } ) ) }
/ >
< / Space >
< Button type = "primary" onClick = { ( ) = > load ( 1 ) } loading = { loading } >
查 询
< / Button >
< / Space >
< / Card >
{ summary && (
< Card size = "small" style = { { marginBottom : 16 } } >
< Row gutter = { [ 16 , 12 ] } >
< Col flex = "1 1 0" >
< Statistic title = "领券发起数" value = { summary . started_count } / >
< / Col >
< Col flex = "1 1 0" >
< Statistic title = "领券完成数" value = { summary . completed_count } / >
< / Col >
< Col flex = "1 1 0" >
< Statistic title = "平均耗时" value = { fmtSec ( summary . avg_elapsed_ms ) } / >
< / Col >
< / Row >
< Divider style = { { marginTop : 16 , marginBottom : 16 } } / >
< Row gutter = { [ 16 , 12 ] } >
< Col flex = "1 1 0" >
< Statistic title = "耗时 5 分位" value = { fmtSec ( summary . p5_ms ) } / >
< / Col >
< Col flex = "1 1 0" >
< Statistic title = "耗时 50 分位" value = { fmtSec ( summary . p50_ms ) } / >
< / Col >
< Col flex = "1 1 0" >
< Statistic title = "耗时 95 分位" value = { fmtSec ( summary . p95_ms ) } / >
< / Col >
< Col flex = "1 1 0" >
< Statistic title = "耗时 99 分位" value = { fmtSec ( summary . p99_ms ) } / >
< / Col >
< / Row >
< / Card >
) }
{ data &&
( queriedMultiDay
? ( data . daily ? . length ? ? 0 ) > 0
: queriedGranularity === 'hour' && ( data . hourly ? . length ? ? 0 ) > 0 ) && (
< Card
size = "small"
title = { queriedMultiDay ? '按天趋势' : '按小时趋势' }
style = { { marginBottom : 16 } }
extra = {
< Space size = { 16 } >
< span style = { { fontSize : 12 , color : '#666' } } >
< span
style = { {
display : 'inline-block' ,
width : 10 ,
height : 10 ,
background : CHART_BAR_STARTED ,
borderRadius : 2 ,
marginRight : 4 ,
} }
/ >
发 起 数
< / span >
< span style = { { fontSize : 12 , color : '#666' } } >
< span
style = { {
display : 'inline-block' ,
width : 10 ,
height : 10 ,
background : CHART_BAR_COMPLETED ,
borderRadius : 2 ,
marginRight : 4 ,
} }
/ >
完 成 数
< / span >
< span style = { { fontSize : 12 , color : '#666' } } >
< span
style = { {
display : 'inline-block' ,
width : 14 ,
borderTop : ` 2px solid ${ CHART_LINE } ` ,
marginRight : 4 ,
verticalAlign : 'middle' ,
} }
/ >
平 均 耗 时 ( 秒 )
< / span >
< / Space >
}
>
{ queriedMultiDay ? (
< TrendChart points = { aggregateDaily ( data . date_from , data . date_to , data . daily ) } / >
) : (
< TrendChart points = { aggregateHourly ( data . hourly ) } / >
) }
< / Card >
) }
< Table
rowKey = "trace_id"
columns = { columns }
dataSource = { items }
loading = { loading }
pagination = { {
current : page ,
pageSize : queriedLimit ,
total : data?.total ? ? 0 ,
showSizeChanger : false ,
showTotal : ( t ) = > ` 共 ${ t } 条 ` ,
onChange : ( p ) = > load ( p , queriedLimit ) ,
} }
size = "small"
scroll = { { x : 1450 } }
/ >
< UserRecordsDrawer < CouponDataRow >
open = { recordsUser != null }
onClose = { ( ) = > setRecordsUser ( null ) }
userId = { recordsUser ? . userId ? ? null }
phone = { recordsUser ? . phone ? ? null }
endpoint = "/admin/api/coupon-data/user-records"
columns = { RECORD_COLUMNS }
recordLabel = "领券"
countLabel = "领券次数"
rewardLabel = "领到券张数"
rewardSuffix = "张"
rewardOf = { ( r ) = > r . claimed_count ? ? 0 }
/ >
< / div >
) ;
}