From 723d88c02c8679f84c9397b53ea792df3e4a2c81 Mon Sep 17 00:00:00 2001 From: guke Date: Fri, 10 Jul 2026 11:10:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(coupon-data):=20=E7=82=B9=E4=BD=8D?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E7=8E=87=E6=8C=87=E6=A0=87=E5=8D=A1=E6=94=B9?= =?UTF-8?q?=E7=89=88(=E5=93=81=E7=89=8C=E8=89=B2=E5=9C=86=E7=82=B9+?= =?UTF-8?q?=E7=8E=AF=E6=AF=94+=E6=9F=A5=E7=9C=8B=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E4=B8=8B=E9=92=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 三平台点位成功率卡:平台名前品牌色小圆点 + 大数字 + 环比涨跌徽标 + 「查看明细」按钮 - 仅「查看明细」按钮触发下钻(原整卡整块可点),下钻表改为卡内浅灰分层区域 - 新增环比:并行拉上一等长周期汇总,按百分点(pt)展示涨跌;无上期/失败显示「暂无上期」(对齐数据大盘 pointDelta 口径) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/coupon-data/page.tsx | 151 ++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 33 deletions(-) diff --git a/src/app/(main)/coupon-data/page.tsx b/src/app/(main)/coupon-data/page.tsx index 659b512..0c38767 100644 --- a/src/app/(main)/coupon-data/page.tsx +++ b/src/app/(main)/coupon-data/page.tsx @@ -19,7 +19,12 @@ import { Tooltip, Typography, } from 'antd'; -import { InfoCircleOutlined } from '@ant-design/icons'; +import { + CaretDownOutlined, + CaretUpOutlined, + DownOutlined, + InfoCircleOutlined, +} from '@ant-design/icons'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; import { formatUtcTime } from '@/lib/format'; @@ -137,6 +142,31 @@ const SLOT_PLATFORM: Record = { }; const slotPlatformLabel = (p: string | null): string => (p ? SLOT_PLATFORM[p] ?? p : '其他'); +// 三平台品牌色(指标卡左侧色块用):美团黄 / 淘宝闪购橙 / 京东红。 +const SLOT_PLATFORM_COLOR: Record = { + 'meituan-waimai': '#FFB300', + 'taobao-shanguang': '#FF6A00', + 'jd-waimai': '#E1251B', +}; + +// 点位成功率「环比上一等长周期」徽标:绿↑/红↓/灰持平/灰暂无上期。 +// 口径对齐数据大盘 pointDelta:成功率为 0..1,差值换算成百分点(pt)。 +function SlotRateDelta({ rate, prev }: { rate: number | null | undefined; prev: number | null | undefined }) { + if (rate == null || prev == null) + return 暂无上期; + const diffPt = (rate - prev) * 100; + if (Math.abs(diffPt) < 0.05) return 持平; + const up = diffPt > 0; + return ( + + {up ? : } + {Math.abs(diffPt).toFixed(1)}pt + + ); +} + // 点手机号弹出的「该用户全部领券」抽屉列(精简版:单用户,不含用户/手机号列)。 const RECORD_COLUMNS: ColumnsType = [ { title: '时间', dataIndex: 'started_at', width: 160, render: (v: string) => formatUtcTime(v) }, @@ -340,6 +370,8 @@ export default function CouponDataPage() { const [couponSlots, setCouponSlots] = useState([]); // 按券表:默认隐藏,点某平台点位成功率卡 → 展开该平台的券表(null=隐藏)。 const [selectedSlotPlatform, setSelectedSlotPlatform] = useState(null); + // 上一等长周期各平台点位成功率(算环比涨跌;拉取失败为 null → 显示「暂无上期」)。 + const [prevPerPlatform, setPrevPerPlatform] = useState | null>(null); const [loading, setLoading] = useState(false); // 点手机号:抽屉看该用户全部领券(总次数 + 记录) const [recordsUser, setRecordsUser] = useState<{ userId: number; phone: string | null } | null>(null); @@ -356,6 +388,10 @@ export default function CouponDataPage() { const to = range[1].format('YYYY-MM-DD'); const multiDay = from !== to; const gran = multiDay ? 'day' : granularity; + // 环比:上一个等长周期(对齐数据大盘「环比上周期」口径)。 + const spanDays = range[1].diff(range[0], 'day') + 1; + const prevTo = range[0].subtract(1, 'day'); + const prevFrom = prevTo.subtract(spanDays - 1, 'day'); setLoading(true); try { const res = await api.get('/admin/api/coupon-data', { @@ -381,6 +417,23 @@ export default function CouponDataPage() { }) .then((r) => setCouponSlots(r.data.items)) .catch(() => setCouponSlots([])); + // 并行拉「上一周期」汇总算环比(同筛选条件、上一等长周期);只用 summary,limit=1 省流量,失败按「暂无上期」。 + api + .get('/admin/api/coupon-data', { + params: { + date_from: prevFrom.format('YYYY-MM-DD'), + date_to: prevTo.format('YYYY-MM-DD'), + user: user.trim() || undefined, + app_env: appEnv, + status: statuses, + granularity: 'day', + limit: 1, + offset: 0, + }, + paramsSerializer: { indexes: null }, + }) + .then((r) => setPrevPerPlatform(r.data.summary?.per_platform ?? null)) + .catch(() => setPrevPerPlatform(null)); setPage(targetPage); setQueriedLimit(targetLimit); setQueriedGranularity(gran); @@ -704,51 +757,83 @@ export default function CouponDataPage() { - {( - [ - ['meituan-waimai', '美团点位成功率'], - ['taobao-shanguang', '淘宝点位成功率'], - ['jd-waimai', '京东点位成功率'], - ] as const - ).map(([pid, title]) => { + {(['meituan-waimai', 'taobao-shanguang', 'jd-waimai'] as const).map((pid) => { const active = selectedSlotPlatform === pid; + const rate = summary.per_platform?.[pid]; + // 左侧品牌色块 | 中间大数字+环比 | 底部「查看明细」按钮;仅按钮可展开表格。 return (
setSelectedSlotPlatform(active ? null : pid)} - title="点击查看该平台各券成功率" style={{ - cursor: 'pointer', - padding: '4px 8px', - borderRadius: 6, - border: active ? '1px solid #1677ff' : '1px solid transparent', - background: active ? '#e6f4ff' : undefined, + border: `1px solid ${active ? '#91caff' : '#f0f0f0'}`, + borderRadius: 8, + background: active ? '#f0f7ff' : '#fff', transition: 'all .2s', + padding: '10px 14px', }} > - + {/* 平台名前放小品牌色圆点(取代原左侧竖条),克制地标记平台身份,不打破页面灰度。 */} +
+ + {`${SLOT_PLATFORM[pid]}点位成功率`} +
+
+ {fmtPct(rate)} + +
+
); })}
- - )} - - {selectedSlotPlatform && ( - setSelectedSlotPlatform(null)}>收起} - style={{ marginBottom: 16 }} - > - - rowKey="coupon_id" - size="small" - pagination={false} - dataSource={couponSlots.filter((r) => r.platform === selectedSlotPlatform)} - columns={slotColumns} - /> + {selectedSlotPlatform && ( + // 表格区域:浅灰底 + 内边距,和上方指标卡主体分层区分。 +
+
+ {`${slotPlatformLabel(selectedSlotPlatform)} · 按券成功率`} + setSelectedSlotPlatform(null)}>收起 +
+ + rowKey="coupon_id" + size="small" + pagination={false} + dataSource={couponSlots.filter((r) => r.platform === selectedSlotPlatform)} + columns={slotColumns} + /> +
+ )}
)}