f82c7ad8f8
- 侧边栏在「用户反馈」后、「广告配置」前加「领券数据」菜单(/coupon-data)。 - 两段式:汇总卡(领券发起数 / 完成数 / 平均耗时 + 耗时 P5/P50/P95/P99)+ 自研零依赖 SVG 趋势图 (发起 / 完成数双柱 + 平均耗时线,按天 / 按小时)+ 逐条领券明细表。 - 「发起平台」=发起来源(傻瓜比价 / 美团 / 淘宝 / 京东);trace 列有 trace_url 给可点链接; 手机号列可点 → 复用通用 UserRecordsDrawer 看该用户全部领券(为其加了可选 rewardSuffix prop)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
// 点列表里的手机号 → 抽屉列出该用户「同类型」的全部记录(反馈页=全部反馈;低价审核页=全部上报)。
|
|
// 顶部汇总该用户的「提交/上报总计」+「奖励总计」;复用各自的 list 接口按 user_id 过滤(无需新后端接口)。
|
|
import { useEffect, useState } from 'react';
|
|
import { Drawer, Empty, Space, Spin, Statistic, Table, Typography } from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { api } from '@/lib/api';
|
|
import type { CursorPage } from '@/lib/types';
|
|
|
|
const { Text } = Typography;
|
|
|
|
interface Props<T> {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
userId: number | null;
|
|
phone: string | null;
|
|
endpoint: string; // 列表接口,按 user_id 过滤,如 /admin/api/feedbacks
|
|
columns: ColumnsType<T>;
|
|
recordLabel: string; // 「反馈」/「低价上报」,用于标题与空态文案
|
|
countLabel: string; // 顶部计数卡标题,如「提交总计」/「上报总计」
|
|
rewardLabel: string; // 顶部奖励卡标题,如「提交奖励总计」/「上报奖励总计」
|
|
rewardOf: (item: T) => number; // 单条已发奖励金币(未采纳/未通过返回 0)
|
|
rewardSuffix?: string; // 第二个汇总卡单位,默认「金币」;领券场景可传「张」(领到券张数)等
|
|
}
|
|
|
|
export default function UserRecordsDrawer<T extends { id: number }>({
|
|
open,
|
|
onClose,
|
|
userId,
|
|
phone,
|
|
endpoint,
|
|
columns,
|
|
recordLabel,
|
|
countLabel,
|
|
rewardLabel,
|
|
rewardOf,
|
|
rewardSuffix = '金币',
|
|
}: Props<T>) {
|
|
const [items, setItems] = useState<T[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open || userId == null) return;
|
|
let alive = true;
|
|
setLoading(true);
|
|
api
|
|
.get<CursorPage<T>>(endpoint, {
|
|
params: { user_id: userId, limit: 100, sort_by: 'created_at', sort_order: 'desc' },
|
|
})
|
|
.then((r) => {
|
|
if (!alive) return;
|
|
setItems(r.data.items);
|
|
setTotal(r.data.total ?? r.data.items.length);
|
|
})
|
|
.catch(() => {
|
|
if (!alive) return;
|
|
setItems([]);
|
|
setTotal(0);
|
|
})
|
|
.finally(() => {
|
|
if (alive) setLoading(false);
|
|
});
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, [open, userId, endpoint]);
|
|
|
|
// 奖励总计在已加载记录上累加(单用户记录数远小于 100 上限,够用)
|
|
const rewardTotal = items.reduce((sum, it) => sum + rewardOf(it), 0);
|
|
|
|
return (
|
|
<Drawer
|
|
title={
|
|
<span>
|
|
用户 #{userId ?? '-'} 的全部{recordLabel}
|
|
{phone ? (
|
|
<Text type="secondary" style={{ fontSize: 13, marginLeft: 8 }}>
|
|
{phone}
|
|
</Text>
|
|
) : null}
|
|
</span>
|
|
}
|
|
width={760}
|
|
open={open}
|
|
onClose={onClose}
|
|
destroyOnHidden
|
|
>
|
|
{loading ? (
|
|
<Spin style={{ display: 'block', margin: '40px auto' }} />
|
|
) : (
|
|
<>
|
|
<Space size={48} style={{ marginBottom: 20 }}>
|
|
<Statistic title={countLabel} value={total} />
|
|
<Statistic title={rewardLabel} value={rewardTotal} suffix={rewardSuffix} />
|
|
</Space>
|
|
{items.length === 0 ? (
|
|
<Empty description={`该用户暂无${recordLabel}`} style={{ marginTop: 24 }} />
|
|
) : (
|
|
<Table<T>
|
|
rowKey="id"
|
|
size="small"
|
|
columns={columns}
|
|
dataSource={items}
|
|
pagination={false}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</Drawer>
|
|
);
|
|
}
|