Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f43450afa | |||
| 694944cb5e |
@@ -661,11 +661,17 @@ export default function AdRevenueReportPage() {
|
||||
: data.date_from
|
||||
: `${data.date_from} ~ ${data.date_to}`;
|
||||
|
||||
// 第二行大盘「分广告类型」:draw / 激励视频 各自 收益 / eCPM / 展示条数;eCPM = 收益÷展示×1000。
|
||||
// eCPM 优先使用后端按真实展示加权的经营分类口径;兼容旧后端时才用收益÷展示数回退。
|
||||
const drawStat = data?.type_stats?.draw;
|
||||
const rvStat = data?.type_stats?.reward_video;
|
||||
const withdrawalVideoStat = data?.type_stats?.withdrawal_video;
|
||||
const drawEcpmStat = data?.category_stats?.draw ?? drawStat;
|
||||
const videoEcpmStat = data?.category_stats?.video ?? {
|
||||
impressions: (rvStat?.impressions ?? 0) + (withdrawalVideoStat?.impressions ?? 0),
|
||||
revenue_yuan: (rvStat?.revenue_yuan ?? 0) + (withdrawalVideoStat?.revenue_yuan ?? 0),
|
||||
};
|
||||
const ecpmOf = (s?: AdRevenueTypeStat) =>
|
||||
s && s.impressions > 0 ? (s.revenue_yuan / s.impressions) * 1000 : 0;
|
||||
s?.ecpm_yuan ?? (s && s.impressions > 0 ? (s.revenue_yuan / s.impressions) * 1000 : 0);
|
||||
|
||||
// 明细直接用后端返回的当前页 items(「场景」已由后端 feed_scene 全局过滤,前端不再二次筛)。
|
||||
const items = data?.items ?? [];
|
||||
@@ -981,7 +987,7 @@ export default function AdRevenueReportPage() {
|
||||
<Statistic title="Draw 信息流收益(元)" value={drawStat?.revenue_yuan ?? 0} precision={4} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic title="Draw 信息流 eCPM(元/千次)" value={ecpmOf(drawStat)} precision={2} />
|
||||
<Statistic title="Draw 信息流 eCPM(元/千次)" value={ecpmOf(drawEcpmStat)} precision={2} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic title="Draw 信息流条数" value={drawStat?.impressions ?? 0} />
|
||||
@@ -990,7 +996,7 @@ export default function AdRevenueReportPage() {
|
||||
<Statistic title="看视频收益(元)" value={rvStat?.revenue_yuan ?? 0} precision={4} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic title="看视频 eCPM(元/千次)" value={ecpmOf(rvStat)} precision={2} />
|
||||
<Statistic title="看视频 eCPM(元/千次)" value={ecpmOf(videoEcpmStat)} precision={2} />
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic title="看视频条数" value={rvStat?.impressions ?? 0} />
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
CheckCircleOutlined,
|
||||
ClearOutlined,
|
||||
CloseCircleOutlined,
|
||||
FilterOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SortAscendingOutlined,
|
||||
@@ -86,42 +85,6 @@ const STATUS_TABS = [
|
||||
];
|
||||
|
||||
const REJECT_TEMPLATES = ['账号异常', '提现金额异常', '微信实名不匹配', '风控拦截', '其他原因'];
|
||||
const QUICK_FILTER_OPTIONS = [
|
||||
{ value: 'abnormal', label: '异常单' },
|
||||
{ value: 'failed', label: '打款失败' },
|
||||
{ value: 'overdue_reviewing', label: '待审核超30分钟' },
|
||||
{ value: 'pending_overdue', label: '打款中超15分钟' },
|
||||
{ value: 'today', label: '今日申请' },
|
||||
{ value: 'high_risk', label: '高风险用户' },
|
||||
];
|
||||
const QUICK_FILTER_STATUS: Record<string, string> = {
|
||||
abnormal: 'all',
|
||||
failed: 'failed',
|
||||
overdue_reviewing: 'reviewing',
|
||||
pending_overdue: 'pending',
|
||||
high_risk: 'all',
|
||||
};
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'created_at', label: '申请时间' },
|
||||
{ value: 'amount_cents', label: '提现金额' },
|
||||
];
|
||||
// 提现类型:coin_cash=福利页提现(金币兑换现金) / invite_cash=邀请提现(邀请奖励金)
|
||||
const SOURCE_LABEL: Record<string, string> = {
|
||||
coin_cash: '福利页提现',
|
||||
invite_cash: '邀请提现',
|
||||
};
|
||||
const SOURCE_COLOR: Record<string, string> = {
|
||||
coin_cash: 'blue',
|
||||
invite_cash: 'purple',
|
||||
};
|
||||
const SOURCE_OPTIONS = [
|
||||
{ value: 'coin_cash', label: '福利页提现' },
|
||||
{ value: 'invite_cash', label: '邀请提现' },
|
||||
];
|
||||
const DATE_FIELD_OPTIONS = [
|
||||
{ value: 'created_at', label: '申请时间' },
|
||||
{ value: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
function statusTag(status: string) {
|
||||
return <Tag color={STATUS_COLOR[status]}>{STATUS_LABEL[status] || status}</Tag>;
|
||||
@@ -197,21 +160,15 @@ export default function WithdrawsPage() {
|
||||
const [searchDraft, setSearchDraft] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||
const [dateField, setDateField] = useState<'created_at' | 'updated_at'>('created_at');
|
||||
const [sortBy, setSortBy] = useState<'created_at' | 'amount_cents'>('created_at');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [quickFilter, setQuickFilter] = useState<string | undefined>(undefined);
|
||||
const [source, setSource] = useState<string | undefined>(undefined);
|
||||
|
||||
const filters: Record<string, unknown> = {};
|
||||
if (activeStatus !== 'all') filters.status = activeStatus;
|
||||
if (source) filters.source = source;
|
||||
if (keyword.trim()) filters.keyword = keyword.trim();
|
||||
if (dateRange?.[0]) filters.date_from = dateRange[0].startOf('day').toISOString();
|
||||
if (dateRange?.[1]) filters.date_to = dateRange[1].endOf('day').toISOString();
|
||||
if (quickFilter) filters.quick_filter = quickFilter;
|
||||
filters.date_field = dateField;
|
||||
filters.sort_by = sortBy;
|
||||
filters.date_field = 'created_at';
|
||||
filters.sort_by = 'created_at';
|
||||
filters.sort_order = sortOrder;
|
||||
const filterKey = JSON.stringify(filters);
|
||||
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||
@@ -226,27 +183,7 @@ export default function WithdrawsPage() {
|
||||
setSearchDraft('');
|
||||
setKeyword('');
|
||||
setDateRange(null);
|
||||
setDateField('created_at');
|
||||
setSortBy('created_at');
|
||||
setSortOrder('desc');
|
||||
setQuickFilter(undefined);
|
||||
setSource(undefined);
|
||||
};
|
||||
|
||||
const applyQuickFilter = (value?: string) => {
|
||||
setQuickFilter(value);
|
||||
const targetStatus = value ? QUICK_FILTER_STATUS[value] : undefined;
|
||||
if (targetStatus) setActiveStatus(targetStatus);
|
||||
};
|
||||
|
||||
const changeStatus = (status: string) => {
|
||||
setActiveStatus(status);
|
||||
setQuickFilter((current) => {
|
||||
if (!current) return current;
|
||||
const targetStatus = QUICK_FILTER_STATUS[current];
|
||||
if (!targetStatus || status === targetStatus) return current;
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
@@ -520,12 +457,6 @@ export default function WithdrawsPage() {
|
||||
width: 120,
|
||||
render: (v: number) => <Text strong>{yuan(v)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '提现类型',
|
||||
dataIndex: 'source',
|
||||
width: 110,
|
||||
render: (v: string) => <Tag color={SOURCE_COLOR[v] || 'default'}>{SOURCE_LABEL[v] || v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '累计提现',
|
||||
dataIndex: 'cumulative_success_cents',
|
||||
@@ -798,7 +729,7 @@ export default function WithdrawsPage() {
|
||||
<Card>
|
||||
<Tabs
|
||||
activeKey={activeStatus}
|
||||
onChange={changeStatus}
|
||||
onChange={setActiveStatus}
|
||||
items={STATUS_TABS.map((tab) => {
|
||||
const count = summaryCount(summary, tab.key);
|
||||
return {
|
||||
@@ -820,89 +751,55 @@ export default function WithdrawsPage() {
|
||||
align="start"
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
paddingBottom: 12,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
allowClear
|
||||
enterButton="搜索"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="用户手机号"
|
||||
value={searchDraft}
|
||||
style={{ width: 240 }}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchDraft(value);
|
||||
if (!value) setKeyword('');
|
||||
}}
|
||||
onSearch={(value) => setKeyword(value.trim())}
|
||||
/>
|
||||
<Select
|
||||
value={source}
|
||||
allowClear
|
||||
placeholder="提现类型"
|
||||
style={{ width: 140 }}
|
||||
options={SOURCE_OPTIONS}
|
||||
onChange={(value?: string) => setSource(value)}
|
||||
/>
|
||||
<Select
|
||||
value={quickFilter}
|
||||
allowClear
|
||||
placeholder="快捷筛选"
|
||||
style={{ width: 170 }}
|
||||
suffixIcon={<FilterOutlined />}
|
||||
options={QUICK_FILTER_OPTIONS}
|
||||
onChange={applyQuickFilter}
|
||||
/>
|
||||
<Select
|
||||
value={dateField}
|
||||
style={{ width: 120 }}
|
||||
options={DATE_FIELD_OPTIONS}
|
||||
onChange={(value: 'created_at' | 'updated_at') => setDateField(value)}
|
||||
/>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
presets={[
|
||||
{ label: '今天', value: [dayjs().startOf('day'), dayjs().endOf('day')] },
|
||||
{
|
||||
label: '最近7天',
|
||||
value: [dayjs().subtract(6, 'day').startOf('day'), dayjs().endOf('day')],
|
||||
},
|
||||
{
|
||||
label: '最近30天',
|
||||
value: [dayjs().subtract(29, 'day').startOf('day'), dayjs().endOf('day')],
|
||||
},
|
||||
]}
|
||||
onChange={(values) => setDateRange(values as [Dayjs, Dayjs] | null)}
|
||||
/>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={sortBy}
|
||||
style={{ width: 130 }}
|
||||
suffixIcon={<SortAscendingOutlined />}
|
||||
options={SORT_OPTIONS}
|
||||
onChange={(value: 'created_at' | 'amount_cents') => setSortBy(value)}
|
||||
/>
|
||||
<Select
|
||||
value={sortOrder}
|
||||
style={{ width: 96 }}
|
||||
options={[
|
||||
{ value: 'desc', label: '倒序' },
|
||||
{ value: 'asc', label: '正序' },
|
||||
]}
|
||||
onChange={(value: 'asc' | 'desc') => setSortOrder(value)}
|
||||
/>
|
||||
<Button icon={<ClearOutlined />} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
<Input.Search
|
||||
allowClear
|
||||
enterButton="搜索"
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="用户手机号"
|
||||
value={searchDraft}
|
||||
style={{ width: 240 }}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchDraft(value);
|
||||
if (!value) setKeyword('');
|
||||
}}
|
||||
onSearch={(value) => setKeyword(value.trim())}
|
||||
/>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
presets={[
|
||||
{ label: '今天', value: [dayjs().startOf('day'), dayjs().endOf('day')] },
|
||||
{
|
||||
label: '最近7天',
|
||||
value: [dayjs().subtract(6, 'day').startOf('day'), dayjs().endOf('day')],
|
||||
},
|
||||
{
|
||||
label: '最近30天',
|
||||
value: [dayjs().subtract(29, 'day').startOf('day'), dayjs().endOf('day')],
|
||||
},
|
||||
]}
|
||||
onChange={(values) => setDateRange(values as [Dayjs, Dayjs] | null)}
|
||||
/>
|
||||
<Select
|
||||
value={sortOrder}
|
||||
style={{ width: 96 }}
|
||||
suffixIcon={<SortAscendingOutlined />}
|
||||
options={[
|
||||
{ value: 'desc', label: '倒序' },
|
||||
{ value: 'asc', label: '正序' },
|
||||
]}
|
||||
onChange={(value: 'asc' | 'desc') => setSortOrder(value)}
|
||||
/>
|
||||
<Button icon={<ClearOutlined />} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
{canManage && selectedOrders.length > 0 && (
|
||||
<Space
|
||||
@@ -974,7 +871,7 @@ export default function WithdrawsPage() {
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1460 }}
|
||||
scroll={{ x: 1350 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => openDetail(record),
|
||||
style: { cursor: 'pointer' },
|
||||
@@ -1006,11 +903,6 @@ export default function WithdrawsPage() {
|
||||
<Descriptions.Item label="金额">
|
||||
<Text strong>{yuan(detail.order.amount_cents)}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提现类型">
|
||||
<Tag color={SOURCE_COLOR[detail.order.source] || 'default'}>
|
||||
{SOURCE_LABEL[detail.order.source] || detail.order.source}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提现实名">
|
||||
{detail.order.user_name || '-'}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -568,6 +568,7 @@ export interface AdRevenueHourly {
|
||||
export interface AdRevenueTypeStat {
|
||||
impressions: number; // 该类型展示条数合计
|
||||
revenue_yuan: number; // 该类型预估收益合计(元)
|
||||
ecpm_yuan?: number; // 按真实展示次数加权的 SDK eCPM(元/千次)
|
||||
}
|
||||
|
||||
// 广告收益报表:一次广告事件(逐条一行)。激励视频展示+发奖按 ad_session_id 合并;信息流展示/发奖各自成行。
|
||||
@@ -607,6 +608,7 @@ export interface AdRevenueReport {
|
||||
daily: AdRevenueDaily[]; // 按日期汇总序列(全量,供按天趋势图)
|
||||
hourly: AdRevenueHourly[]; // 按小时汇总序列(全量,供按小时趋势图;按天查询时为空)
|
||||
type_stats: Record<string, AdRevenueTypeStat>; // 按广告类型小计;前端取 draw / reward_video
|
||||
category_stats?: Record<string, AdRevenueTypeStat>; // draw=draw+历史feed;video=福利视频+提现视频
|
||||
// 按信息流场景小计(comparison/coupon/welfare;全量,不受分页截断)。数据大盘「领券广告/比价广告」卡
|
||||
// 优先读它:2026-07-02 后端起 items 不再含信息流逐条展示行(唯一带收益+场景的行),按 items 现算恒 0。
|
||||
// 可选(`?.` 探测)以兼容未升级后端,缺失时回退 items 现算。
|
||||
|
||||
Reference in New Issue
Block a user