feat(cps): 群详情页加「群内微信用户」区块(头像/昵称/领券次数)

微信落地页授权来的用户列表:头像+昵称+领券次数(点复制)+点击次数+首次进入。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 11:44:14 +08:00
parent 5ae87fc11e
commit b7957e58c0
+72 -1
View File
@@ -8,7 +8,7 @@ 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 { api, errMsg } from '@/lib/api';
import { yuan } from '@/lib/format';
import { formatUtcTime, yuan } from '@/lib/format';
// @ant-design/plots 依赖 DOM,关掉 SSR 仅在客户端渲染(否则 Next 预渲染报 document undefined)。
const Line = dynamic(() => import('@ant-design/plots').then((m) => m.Line), { ssr: false });
@@ -47,6 +47,16 @@ interface DailyResp {
rows: DailyRow[];
}
// 群内微信用户(落地页授权拿到;nickname/headimgurl 仅 userinfo 授权后有)
interface WxUser {
openid: string;
nickname: string | null;
headimgurl: string | null;
first_seen: string;
visit_count: number;
copy_count: number;
}
const RANGES = [
{ label: '近 3 天', value: 'd3' },
{ label: '近 7 天', value: 'd7' },
@@ -115,6 +125,20 @@ export default function GroupDetailPage() {
loadDaily();
}, [loadDaily]);
// 群内微信用户(独立于时间范围,累计画像)
const [wxUsers, setWxUsers] = useState<WxUser[]>([]);
const loadWxUsers = useCallback(async () => {
try {
const r = await api.get<{ users: WxUser[] }>(`/admin/api/cps/groups/${id}/wx-users`);
setWxUsers(r.data.users || []);
} catch (e) {
message.error(errMsg(e, '用户列表加载失败'));
}
}, [id]);
useEffect(() => {
loadWxUsers();
}, [loadWxUsers]);
const points = useMemo(() => data?.points ?? [], [data]);
// 展平成「长表」给折线图:每个 (时间桶 × 指标) 一行,colorField 按指标分 4 条线。
const chartData = useMemo(
@@ -202,6 +226,38 @@ export default function GroupDetailPage() {
},
];
const wxColumns: ColumnsType<WxUser> = [
{
title: '用户',
key: 'user',
render: (_, u) => (
<Space>
{u.headimgurl ? (
<img
src={u.headimgurl}
alt=""
style={{ width: 32, height: 32, borderRadius: '50%', objectFit: 'cover' }}
/>
) : (
<span
style={{ width: 32, height: 32, borderRadius: '50%', background: '#f0f0f0', display: 'inline-block' }}
/>
)}
<span>{u.nickname || '(未授权昵称)'}</span>
</Space>
),
},
{
title: '领券次数',
dataIndex: 'copy_count',
width: 90,
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: 90 },
{ title: '首次进入', dataIndex: 'first_seen', width: 160, render: (v: string) => formatUtcTime(v) },
];
return (
<div>
<Space style={{ marginBottom: 16 }} align="center" wrap>
@@ -255,6 +311,21 @@ export default function GroupDetailPage() {
/>
</Card>
)}
<Card title={`群内微信用户(${wxUsers.length})`} style={{ marginTop: 16 }}>
<div style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>
= ;/
</div>
<Table<WxUser>
rowKey="openid"
size="small"
columns={wxColumns}
dataSource={wxUsers}
pagination={false}
scroll={{ x: 560 }}
locale={{ emptyText: '暂无授权用户' }}
/>
</Card>
</div>
);
}