Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd9c685723 | |||
| 700c4e3128 | |||
| 3f5356ad87 | |||
| c90775d09c |
@@ -10,6 +10,7 @@ import {
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
@@ -72,8 +73,16 @@ interface CouponDataRow {
|
||||
app_env: string | null;
|
||||
started_at: string;
|
||||
claimed_count: number | null;
|
||||
point_success_count: number | null;
|
||||
point_total_count: number | null;
|
||||
point_details: Array<{
|
||||
coupon_id: string;
|
||||
coupon_name: string | null;
|
||||
status: string;
|
||||
reason: string | null;
|
||||
}>;
|
||||
trace_url: string | null;
|
||||
ad_revenue_yuan: number; // 本次领券看的信息流广告预估收益(元)
|
||||
ad_revenue_yuan: number | null; // 本次领券看的信息流广告预估收益(元);未填充为 null
|
||||
}
|
||||
interface CouponDataReport {
|
||||
date_from: string;
|
||||
@@ -116,6 +125,13 @@ const STATUS_TAG: Record<string, { color: string; label: string }> = {
|
||||
abandoned: { color: 'orange', label: '中途退出' },
|
||||
};
|
||||
|
||||
const POINT_STATUS_TAG: Record<string, { color: string; label: string }> = {
|
||||
success: { color: 'success', label: '成功' },
|
||||
already_claimed: { color: 'processing', label: '已领' },
|
||||
failed: { color: 'error', label: '失败' },
|
||||
skipped: { color: 'default', label: '跳过' },
|
||||
};
|
||||
|
||||
// ms → "1.5s"(空值显示 -)
|
||||
const fmtSec = (ms: number | null | undefined): string =>
|
||||
ms == null ? '-' : `${(ms / 1000).toFixed(1)}s`;
|
||||
@@ -124,9 +140,9 @@ const fmtSec = (ms: number | null | undefined): string =>
|
||||
const fmtPct = (v: number | null | undefined): string =>
|
||||
v == null ? '-' : `${(v * 100).toFixed(1)}%`;
|
||||
|
||||
// 元(小数)→ "¥0.0050";空值显示 -,真实 0 显示 ¥0.0000,以区分“零收益”和“无数据”。
|
||||
// 元(小数)→ "¥0.0050";空值表示 eCPM 根本未填充,真实 0 仍显示 ¥0.0000。
|
||||
const fmtYuan = (v: number | null | undefined): string =>
|
||||
v == null ? '-' : `¥${v.toFixed(4)}`;
|
||||
v == null ? '未填充' : `¥${v.toFixed(4)}`;
|
||||
|
||||
// 一组按券行的 success_rate 算术平均(每张券等权;空值券跳过);无有效券 → null(显示 -)。
|
||||
// 汇总卡「分平台/合计点位成功率」据此由按券明细上卷,口径与下方按券表一致。
|
||||
@@ -137,11 +153,6 @@ const slotRateMean = (rows: CouponSlotRow[]): number | null => {
|
||||
return rates.length ? rates.reduce((a, b) => a + b, 0) / rates.length : null;
|
||||
};
|
||||
|
||||
// 汇总卡成功率口径 tooltip 文案
|
||||
const POINT_RATE_HINT =
|
||||
'点位成功率(合计):当前区间内全部券成功率的算术平均(每张券等权);' +
|
||||
'单券成功率=成功(含已领)÷尝试(不含跳过),源自领券每券记录。';
|
||||
|
||||
// 按券表:coupon_id 平台 → 中文
|
||||
const SLOT_PLATFORM: Record<string, string> = {
|
||||
'meituan-waimai': '美团',
|
||||
@@ -500,24 +511,62 @@ export default function CouponDataPage() {
|
||||
title: '点位成功率',
|
||||
key: 'point_success_rate',
|
||||
width: 110,
|
||||
render: (_: unknown, r: CouponDataRow) => (
|
||||
<Tooltip title="当前接口未返回本场应领点位及逐点结果,前端不使用全局均值冒充本场成功率。">
|
||||
{r.trace_url ? (
|
||||
<a href={r.trace_url} target="_blank" rel="noreferrer">
|
||||
查看明细
|
||||
</a>
|
||||
) : (
|
||||
<Typography.Text type="secondary">待埋点</Typography.Text>
|
||||
)}
|
||||
</Tooltip>
|
||||
),
|
||||
render: (_: unknown, r: CouponDataRow) => {
|
||||
if (r.point_success_count == null || r.point_total_count == null || r.point_total_count <= 0) {
|
||||
return <Typography.Text type="secondary">-</Typography.Text>;
|
||||
}
|
||||
const score = `${r.point_success_count}/${r.point_total_count}`;
|
||||
const details = r.point_details ?? [];
|
||||
return (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottomLeft"
|
||||
title={`点位明细(${score})`}
|
||||
content={(
|
||||
<div style={{ width: 380, maxHeight: 360, overflowY: 'auto' }}>
|
||||
{details.length > 0 ? details.map((point, index) => {
|
||||
const status = POINT_STATUS_TAG[point.status] ?? {
|
||||
color: 'default', label: point.status,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`${point.coupon_id}-${index}`}
|
||||
style={{
|
||||
padding: '8px 0',
|
||||
borderBottom: index < details.length - 1 ? '1px solid #f0f0f0' : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography.Text>{point.coupon_name || point.coupon_id}</Typography.Text>
|
||||
{point.coupon_name ? (
|
||||
<div><Typography.Text type="secondary">{point.coupon_id}</Typography.Text></div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tag color={status.color} style={{ marginInlineEnd: 0 }}>{status.label}</Tag>
|
||||
</div>
|
||||
{point.reason ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Typography.Text type="danger">原因:{point.reason}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}) : <Typography.Text type="secondary">暂无点位明细</Typography.Text>}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button type="link" size="small" style={{ height: 'auto', padding: 0 }}>{score}</Button>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '广告收益',
|
||||
dataIndex: 'ad_revenue_yuan',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: (v: number) => fmtYuan(v),
|
||||
render: (v: number | null) => fmtYuan(v),
|
||||
},
|
||||
{
|
||||
title: '美团耗时',
|
||||
@@ -597,6 +646,10 @@ export default function CouponDataPage() {
|
||||
const successDenominator = summary ? Math.max(0, summary.started_count - abandonedCount) : 0;
|
||||
const couponSuccessRate =
|
||||
summary && successDenominator > 0 ? summary.completed_count / successDenominator : null;
|
||||
const validCouponSlots = couponSlots.filter((r) => r.success_rate != null);
|
||||
const pointSuccessRate = slotRateMean(validCouponSlots);
|
||||
const pointSucceededTotal = validCouponSlots.reduce((total, r) => total + r.succeeded, 0);
|
||||
const pointTriedTotal = validCouponSlots.reduce((total, r) => total + r.tried, 0);
|
||||
const items = data?.items ?? [];
|
||||
|
||||
return (
|
||||
@@ -714,8 +767,19 @@ export default function CouponDataPage() {
|
||||
<Statistic
|
||||
title={
|
||||
<span>
|
||||
领券成功率{' '}
|
||||
<Tooltip title="领券完成数 ÷(领券发起数-中途退出数)">
|
||||
领券完成率{' '}
|
||||
<Tooltip
|
||||
overlayStyle={{ maxWidth: 520 }}
|
||||
title={
|
||||
<div>
|
||||
<div>计算逻辑:领券完成数 ÷(领券发起数-中途退出数)</div>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
本期:{summary.completed_count} ÷({summary.started_count}-{abandonedCount})
|
||||
= {fmtPct(couponSuccessRate)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined style={{ color: '#999' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
@@ -728,12 +792,33 @@ export default function CouponDataPage() {
|
||||
title={
|
||||
<span>
|
||||
点位成功率{' '}
|
||||
<Tooltip title={POINT_RATE_HINT}>
|
||||
<Tooltip
|
||||
overlayStyle={{ maxWidth: 560 }}
|
||||
title={
|
||||
<div>
|
||||
<div>计算逻辑:全部单券成功率的算术平均,每张券等权。</div>
|
||||
<div>单券成功率=成功数(含已领)÷ 尝试数(不含跳过)。</div>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
本期共 {validCouponSlots.length} 张有效券:
|
||||
</div>
|
||||
{validCouponSlots.map((slot) => (
|
||||
<div key={slot.coupon_id}>
|
||||
{slot.coupon_name || slot.coupon_id}:{slot.succeeded} ÷ {slot.tried}=
|
||||
{fmtPct(slot.success_rate)}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 6 }}>
|
||||
等权平均={fmtPct(pointSuccessRate)};累计成功/尝试=
|
||||
{pointSucceededTotal}/{pointTriedTotal}(累计数仅辅助核对,不作为当前加权公式)。
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined style={{ color: '#999' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
value={fmtPct(slotRateMean(couponSlots))}
|
||||
value={fmtPct(pointSuccessRate)}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="1 1 0">
|
||||
|
||||
Reference in New Issue
Block a user