diff --git a/src/app/(main)/coupon-data/page.tsx b/src/app/(main)/coupon-data/page.tsx index 3f0a696..f7c339c 100644 --- a/src/app/(main)/coupon-data/page.tsx +++ b/src/app/(main)/coupon-data/page.tsx @@ -10,9 +10,11 @@ import { DatePicker, Divider, Input, + Popover, Row, Select, Space, + Spin, Statistic, Table, Tag, @@ -72,9 +74,23 @@ interface CouponDataRow { app_env: string | null; started_at: string; claimed_count: number | null; + point_success_count: number | null; + point_total_count: number | null; + // 兼容旧后端的内嵌明细;新后端不再返回,改为点击后按 trace 加载。 + point_details?: CouponPointDetail[]; trace_url: string | null; ad_revenue_yuan: number | null; // 本次领券看的信息流广告预估收益(元);未填充为 null } +interface CouponPointDetail { + coupon_id: string; + coupon_name: string | null; + status: string; + reason: string | null; +} +interface CouponPointDetailsOut { + trace_id: string; + items: CouponPointDetail[]; +} interface CouponDataReport { date_from: string; date_to: string; @@ -116,6 +132,94 @@ const STATUS_TAG: Record = { abandoned: { color: 'orange', label: '中途退出' }, }; +const POINT_STATUS_TAG: Record = { + success: { color: 'success', label: '成功' }, + already_claimed: { color: 'processing', label: '已领' }, + failed: { color: 'error', label: '失败' }, + skipped: { color: 'default', label: '跳过' }, +}; + +function PointScorePopover({ row }: { row: CouponDataRow }) { + const { message } = App.useApp(); + const [details, setDetails] = useState(row.point_details ?? null); + const [loading, setLoading] = useState(false); + const [loadError, setLoadError] = useState(null); + + if (row.point_success_count == null || row.point_total_count == null || row.point_total_count <= 0) { + return -; + } + const score = `${row.point_success_count}/${row.point_total_count}`; + const loadDetails = async () => { + if (details !== null || loading) return; + setLoading(true); + setLoadError(null); + try { + const response = await api.get('/admin/api/coupon-data/point-details', { + params: { trace_id: row.trace_id }, + }); + setDetails(response.data.items); + } catch (error) { + const errorMessage = errMsg(error); + setLoadError(errorMessage); + message.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + { + if (open) void loadDetails(); + }} + content={( +
+ {loading || (details === null && loadError === null) ? ( + + ) : loadError ? ( + 明细加载失败,请关闭后重试 + ) : details && details.length > 0 ? details.map((point, index) => { + const status = POINT_STATUS_TAG[point.status] ?? { + color: 'default', label: point.status, + }; + return ( +
+
+
+ {point.coupon_name || point.coupon_id} + {point.coupon_name ? ( +
{point.coupon_id}
+ ) : null} +
+ {status.label} +
+ {point.reason ? ( +
+ 原因:{point.reason} +
+ ) : null} +
+ ); + }) : ( + 暂无点位明细 + )} +
+ )} + > + +
+ ); +} + // ms → "1.5s"(空值显示 -) const fmtSec = (ms: number | null | undefined): string => ms == null ? '-' : `${(ms / 1000).toFixed(1)}s`; @@ -495,17 +599,7 @@ export default function CouponDataPage() { title: '点位成功率', key: 'point_success_rate', width: 110, - render: (_: unknown, r: CouponDataRow) => ( - - {r.trace_url ? ( - - 查看明细 - - ) : ( - 待埋点 - )} - - ), + render: (_: unknown, r: CouponDataRow) => , }, { title: '广告收益',