feat(cps): 每日明细按天下钻弹窗(按用户领券/点击 + 券明细 tooltip)

- 每日明细表加「明细」列 → 弹窗按用户展示当天领券(copy)/点击(visit)
- 点击次数悬停 tooltip 展示该用户当天 visit 过的券(券×次数)
- 日期列加宽显示全 YYYY-MM-DD(配合后端 /daily date 改全日期)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
guke
2026-06-26 14:35:58 +08:00
parent c5534504c7
commit 1b9fdd2fc0
+127 -3
View File
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
import type { ColumnsType } from 'antd/es/table';
import { Button, Card, Col, Empty, Row, Segmented, Space, Spin, Statistic, Table, message } from 'antd';
import { Button, Card, Col, Empty, Modal, Row, Segmented, Space, Spin, Statistic, Table, Tooltip, message } from 'antd';
import { api, errMsg } from '@/lib/api';
import { formatUtcTime, yuan } from '@/lib/format';
@@ -57,6 +57,20 @@ interface WxUser {
copy_count: number;
}
// 某天该群按用户领券下钻(/day-users)
interface DayCoupon {
name: string;
count: number;
}
interface DayUser {
openid: string;
nickname: string | null;
headimgurl: string | null;
copy_count: number;
visit_count: number;
coupons: DayCoupon[];
}
const RANGES = [
{ label: '近 3 天', value: 'd3' },
{ label: '近 7 天', value: 'd7' },
@@ -139,6 +153,30 @@ export default function GroupDetailPage() {
loadWxUsers();
}, [loadWxUsers]);
// 某天领券下钻弹窗
const [dayDetail, setDayDetail] = useState<{
open: boolean;
date: string;
loading: boolean;
users: DayUser[];
}>({ open: false, date: '', loading: false, users: [] });
const openDayDetail = useCallback(
async (date: string) => {
setDayDetail({ open: true, date, loading: true, users: [] });
try {
const r = await api.get<{ users: DayUser[] }>(
`/admin/api/cps/groups/${id}/day-users?date=${date}`,
);
setDayDetail((s) => ({ ...s, loading: false, users: r.data.users || [] }));
} catch (e) {
message.error(errMsg(e, '明细加载失败'));
setDayDetail((s) => ({ ...s, loading: false }));
}
},
[id],
);
const points = useMemo(() => data?.points ?? [], [data]);
// 展平成「长表」给折线图:每个 (时间桶 × 指标) 一行,colorField 按指标分 4 条线。
const chartData = useMemo(
@@ -171,7 +209,7 @@ export default function GroupDetailPage() {
};
const dailyColumns: ColumnsType<DailyRow> = [
{ title: '日期', dataIndex: 'date', width: 64, fixed: 'left' },
{ title: '日期', dataIndex: 'date', width: 96, fixed: 'left' },
{ title: '点击', dataIndex: 'click_pv', width: 56 },
{ title: '独立', dataIndex: 'click_uv', width: 56 },
{ title: '复制', dataIndex: 'copy_pv', width: 56, render: (v: number) => v || '-' },
@@ -224,6 +262,17 @@ export default function GroupDetailPage() {
width: 84,
render: (v: number | null) => (v == null ? '-' : <span style={{ color: '#3f8600' }}>{yuan(v)}</span>),
},
{
title: '明细',
key: 'drill',
width: 64,
fixed: 'right',
render: (_, r) => (
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => openDayDetail(r.date)}>
</Button>
),
},
];
const wxColumns: ColumnsType<WxUser> = [
@@ -258,6 +307,59 @@ export default function GroupDetailPage() {
{ title: '首次进入', dataIndex: 'first_seen', width: 160, render: (v: string) => formatUtcTime(v) },
];
const dayUserColumns: ColumnsType<DayUser> = [
{
title: '用户',
key: 'user',
render: (_, u) => (
<Space>
{u.headimgurl ? (
<img
src={u.headimgurl}
alt=""
style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }}
/>
) : (
<span
style={{ width: 28, height: 28, borderRadius: '50%', background: '#f0f0f0', display: 'inline-block' }}
/>
)}
<span>{u.nickname || '(未授权昵称)'}</span>
</Space>
),
},
{
title: '领券次数',
dataIndex: 'copy_count',
width: 100,
sorter: (a, b) => a.copy_count - b.copy_count,
render: (v: number) => (v ? <b style={{ color: '#ff4d4f' }}>{v}</b> : 0),
},
{
title: '点击次数',
dataIndex: 'visit_count',
width: 100,
render: (v: number, u) =>
u.coupons.length ? (
<Tooltip
title={
<div>
{u.coupons.map((c) => (
<div key={c.name}>
{c.name} ×{c.count}
</div>
))}
</div>
}
>
<span style={{ cursor: 'help', textDecoration: 'underline dotted' }}>{v}</span>
</Tooltip>
) : (
v
),
},
];
return (
<div>
<Space style={{ marginBottom: 16 }} align="center" wrap>
@@ -307,7 +409,7 @@ export default function GroupDetailPage() {
columns={dailyColumns}
dataSource={daily?.rows ?? []}
pagination={false}
scroll={{ x: 900 }}
scroll={{ x: 1000 }}
/>
</Card>
)}
@@ -326,6 +428,28 @@ export default function GroupDetailPage() {
locale={{ emptyText: '暂无授权用户' }}
/>
</Card>
<Modal
open={dayDetail.open}
title={`群「${data?.group_name ?? ''}${dayDetail.date} · 用户领券明细`}
footer={null}
width={640}
height={600}
onCancel={() => setDayDetail((s) => ({ ...s, open: false }))}
>
<div style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>
(openid);/,
</div>
<Table<DayUser>
rowKey="openid"
size="small"
loading={dayDetail.loading}
columns={dayUserColumns}
dataSource={dayDetail.users}
pagination={false}
locale={{ emptyText: '当天无授权用户领券/点击' }}
/>
</Modal>
</div>
);
}