Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 342fd0cf5d | |||
| c83c1421f5 |
@@ -47,6 +47,13 @@ const TYPE_TAG: Record<string, { color: string; label: string }> = {
|
|||||||
withdrawal_video: { color: 'gold', label: '提现激励视频' },
|
withdrawal_video: { color: 'gold', label: '提现激励视频' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Draw 信息流投放场景标签(后端 AdRevenueRow.feed_scene:comparison/coupon/welfare)
|
||||||
|
const SCENE_TAG: Record<string, { text: string; color: string }> = {
|
||||||
|
comparison: { text: '比价', color: 'blue' },
|
||||||
|
coupon: { text: '领券', color: 'gold' },
|
||||||
|
welfare: { text: '福利', color: 'green' },
|
||||||
|
};
|
||||||
|
|
||||||
// 我们的应用环境标签
|
// 我们的应用环境标签
|
||||||
const APP_TAG: Record<string, { color: string; label: string }> = {
|
const APP_TAG: Record<string, { color: string; label: string }> = {
|
||||||
prod: { color: 'green', label: '傻瓜比价(正式)' },
|
prod: { color: 'green', label: '傻瓜比价(正式)' },
|
||||||
@@ -256,6 +263,8 @@ export default function AdRevenueReportPage() {
|
|||||||
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
|
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
|
||||||
const [userId, setUserId] = useState<number | null>(null);
|
const [userId, setUserId] = useState<number | null>(null);
|
||||||
const [adType, setAdType] = useState<string | undefined>();
|
const [adType, setAdType] = useState<string | undefined>();
|
||||||
|
// 「场景」为纯前端过滤(后端 query 暂不支持 feed_scene),只作用于下方明细表,不重新请求、不影响趋势/日汇总。
|
||||||
|
const [scene, setScene] = useState<string | undefined>();
|
||||||
const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
|
const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
|
||||||
const [limit, setLimit] = useState<number>(500);
|
const [limit, setLimit] = useState<number>(500);
|
||||||
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
|
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
|
||||||
@@ -332,6 +341,16 @@ export default function AdRevenueReportPage() {
|
|||||||
return <Tag color={t.color}>{t.label}</Tag>;
|
return <Tag color={t.color}>{t.label}</Tag>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '场景',
|
||||||
|
dataIndex: 'feed_scene',
|
||||||
|
width: 80,
|
||||||
|
render: (v: string | null | undefined) => {
|
||||||
|
if (!v) return <Typography.Text type="secondary">-</Typography.Text>;
|
||||||
|
const t = SCENE_TAG[v];
|
||||||
|
return t ? <Tag color={t.color}>{t.text}</Tag> : <Tag>{v}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '来源应用',
|
title: '来源应用',
|
||||||
dataIndex: 'app_env',
|
dataIndex: 'app_env',
|
||||||
@@ -428,6 +447,16 @@ export default function AdRevenueReportPage() {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// 「场景」纯前端过滤:只对已加载的明细 items 生效(后端 query 暂不支持 feed_scene);趋势图/日汇总仍用全量 daily/total_*。
|
||||||
|
const allItems = data?.items ?? [];
|
||||||
|
const filteredItems = scene ? allItems.filter((r) => r.feed_scene === scene) : allItems;
|
||||||
|
|
||||||
|
// 选中比价/领券且类型=draw 时,给出该场景在已加载明细内的实发金币小计(仅明细口径,可能受 limit 截断)。
|
||||||
|
const sceneCoinSubtotal =
|
||||||
|
adType === 'draw' && (scene === 'comparison' || scene === 'coupon')
|
||||||
|
? filteredItems.reduce((s, r) => s + (r.has_reward ? r.actual_coin : 0), 0)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space align="center" style={{ display: 'flex', marginBottom: 16 }}>
|
<Space align="center" style={{ display: 'flex', marginBottom: 16 }}>
|
||||||
@@ -497,6 +526,22 @@ export default function AdRevenueReportPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
<Space size={6}>
|
||||||
|
<Typography.Text type="secondary">场景</Typography.Text>
|
||||||
|
<Select
|
||||||
|
placeholder="全部"
|
||||||
|
value={scene}
|
||||||
|
onChange={setScene}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 130 }}
|
||||||
|
title="仅前端过滤下方明细,不影响趋势图/汇总"
|
||||||
|
options={[
|
||||||
|
{ value: 'comparison', label: '比价' },
|
||||||
|
{ value: 'coupon', label: '领券' },
|
||||||
|
{ value: 'welfare', label: '福利' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
<Space size={6}>
|
<Space size={6}>
|
||||||
<Typography.Text type="secondary">粒度</Typography.Text>
|
<Typography.Text type="secondary">粒度</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
@@ -599,6 +644,22 @@ export default function AdRevenueReportPage() {
|
|||||||
message={`明细已截断到 ${queriedLimit} 条,还有更多未显示——请按用户/类型/日期缩小范围,或调大「条数」。合计数字不受影响。`}
|
message={`明细已截断到 ${queriedLimit} 条,还有更多未显示——请按用户/类型/日期缩小范围,或调大「条数」。合计数字不受影响。`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{sceneCoinSubtotal != null && (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message={
|
||||||
|
<span>
|
||||||
|
当前已加载明细中,
|
||||||
|
<Tag color={SCENE_TAG[scene!].color} style={{ marginInline: 4 }}>
|
||||||
|
{SCENE_TAG[scene!].text}
|
||||||
|
</Tag>
|
||||||
|
Draw 信息流 实发金币小计 <b>{sceneCoinSubtotal}</b>(共 {filteredItems.length} 条;
|
||||||
|
仅明细口径,若已截断可能偏少;上方汇总/趋势仍为全量)。
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -640,7 +701,7 @@ export default function AdRevenueReportPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 按小时图由 items 聚合,会被 limit 截断;按天图用 daily(全量)不受影响,故只在按小时时警告 */}
|
{/* 趋势图与日汇总恒为全量:按天图用 daily(全量),按小时图用全量 items;「场景」前端过滤只作用于下方明细表,不影响此图。 */}
|
||||||
{!queriedMultiDay && data.truncated && (
|
{!queriedMultiDay && data.truncated && (
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -649,6 +710,14 @@ export default function AdRevenueReportPage() {
|
|||||||
message={`图表按小时聚合的是已截断到 ${queriedLimit} 组的明细,可能少算——调大「条数」或缩小筛选可还原真实趋势。`}
|
message={`图表按小时聚合的是已截断到 ${queriedLimit} 组的明细,可能少算——调大「条数」或缩小筛选可还原真实趋势。`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{scene && (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
message="趋势图为全量数据,不随「场景」筛选变化;场景过滤仅作用于下方明细表与其金币小计。"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{queriedMultiDay ? (
|
{queriedMultiDay ? (
|
||||||
<TrendChart points={aggregateDaily(data.date_from, data.date_to, data.daily)} />
|
<TrendChart points={aggregateDaily(data.date_from, data.date_to, data.daily)} />
|
||||||
) : (
|
) : (
|
||||||
@@ -660,11 +729,11 @@ export default function AdRevenueReportPage() {
|
|||||||
<Table
|
<Table
|
||||||
rowKey="event_key"
|
rowKey="event_key"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={filteredItems}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="small"
|
size="small"
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1280 }}
|
||||||
rowClassName={(r) => (r.has_reward && !r.matched ? 'row-mismatch' : '')}
|
rowClassName={(r) => (r.has_reward && !r.matched ? 'row-mismatch' : '')}
|
||||||
expandable={{
|
expandable={{
|
||||||
// 只有发了奖的事件可展开,看「这条广告金币怎么算的」;纯展示行无复算、不可展开。
|
// 只有发了奖的事件可展开,看「这条广告金币怎么算的」;纯展示行无复算、不可展开。
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { api, errMsg } from '@/lib/api';
|
|||||||
interface AdCfg {
|
interface AdCfg {
|
||||||
app_id: string;
|
app_id: string;
|
||||||
reward_code_id: string;
|
reward_code_id: string;
|
||||||
compare_feed_code_id: string;
|
compare_draw_code_id: string;
|
||||||
coupon_feed_code_id: string;
|
coupon_draw_code_id: string;
|
||||||
reward_mkey: string;
|
reward_mkey: string;
|
||||||
reward_enabled: boolean;
|
reward_enabled: boolean;
|
||||||
compare_ad_enabled: boolean;
|
compare_ad_enabled: boolean;
|
||||||
@@ -74,11 +74,11 @@ export default function AdConfigPage() {
|
|||||||
>
|
>
|
||||||
<Input.Password placeholder="GroMore S2S 回调验签密钥" autoComplete="off" />
|
<Input.Password placeholder="GroMore S2S 回调验签密钥" autoComplete="off" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="compare_feed_code_id" label="比价 · 信息流 广告位ID" rules={[{ required: true }]}>
|
<Form.Item name="compare_draw_code_id" label="比价Draw信息流代码位" rules={[{ required: true }]}>
|
||||||
<Input placeholder="如 104090333" />
|
<Input placeholder="比价Draw信息流代码位,如 104098712" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="coupon_feed_code_id" label="领券 · 信息流 广告位ID" rules={[{ required: true }]}>
|
<Form.Item name="coupon_draw_code_id" label="领券Draw信息流代码位" rules={[{ required: true }]}>
|
||||||
<Input placeholder="如 104090333" />
|
<Input placeholder="领券Draw信息流代码位,如 104098712" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="reward_enabled" label="福利激励视频开关" valuePropName="checked">
|
<Form.Item name="reward_enabled" label="福利激励视频开关" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import type { SorterResult } from 'antd/es/table/interface';
|
||||||
|
import { Button, DatePicker, Input, Space, Switch, Table, Tag, Typography } from 'antd';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { formatUtcTime } from '@/lib/format';
|
||||||
|
import { usePagedList } from '@/lib/usePagedList';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
/** 一条埋点日志(对应后端 analytics_event,五维)。 */
|
||||||
|
type EventLog = {
|
||||||
|
id: number;
|
||||||
|
event: string;
|
||||||
|
device_id: string;
|
||||||
|
user_id: number | null;
|
||||||
|
session_id: string | null;
|
||||||
|
client_ts: number;
|
||||||
|
sent_at: number | null;
|
||||||
|
created_at: string;
|
||||||
|
page: string | null;
|
||||||
|
client_ip: string | null;
|
||||||
|
oem: string | null;
|
||||||
|
os: string | null;
|
||||||
|
model: string | null;
|
||||||
|
app_ver: string | null;
|
||||||
|
network: string | null;
|
||||||
|
channel: string | null;
|
||||||
|
props: Record<string, string> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortField = 'id' | 'created_at';
|
||||||
|
|
||||||
|
// 端时间是 epoch ms;格式化成「月-日 时:分:秒.毫秒」便于核对节流/顺序
|
||||||
|
const fmtMs = (ms: number | null) => (ms ? dayjs(ms).format('MM-DD HH:mm:ss.SSS') : '-');
|
||||||
|
|
||||||
|
export default function EventLogsPage() {
|
||||||
|
// 筛选草稿:点「查询」才应用
|
||||||
|
const [event, setEvent] = useState('');
|
||||||
|
const [deviceId, setDeviceId] = useState('');
|
||||||
|
const [userId, setUserId] = useState('');
|
||||||
|
const [sessionId, setSessionId] = useState('');
|
||||||
|
const [createdRange, setCreatedRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||||
|
const [applied, setApplied] = useState<Record<string, unknown>>({});
|
||||||
|
// 排序(服务端)
|
||||||
|
const [sortBy, setSortBy] = useState<SortField>('id');
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
|
// 近实时:自动刷新开关
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
|
|
||||||
|
const filters: Record<string, unknown> = { ...applied, sort_by: sortBy, sort_order: sortOrder };
|
||||||
|
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
|
||||||
|
usePagedList<EventLog>('/admin/api/event-logs', filters);
|
||||||
|
|
||||||
|
// 开自动刷新后每 5s 拉当前页;reload 闭包随渲染更新,用 ref 取最新,避免 interval captured 过期闭包。
|
||||||
|
const reloadRef = useRef(reload);
|
||||||
|
reloadRef.current = reload;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return;
|
||||||
|
const t = setInterval(() => reloadRef.current(), 5000);
|
||||||
|
return () => clearInterval(t);
|
||||||
|
}, [autoRefresh]);
|
||||||
|
|
||||||
|
const search = () =>
|
||||||
|
setApplied({
|
||||||
|
event: event.trim() || undefined,
|
||||||
|
device_id: deviceId.trim() || undefined,
|
||||||
|
user_id: userId.trim() ? Number(userId.trim()) : undefined,
|
||||||
|
session_id: sessionId.trim() || undefined,
|
||||||
|
created_from: createdRange?.[0] ? createdRange[0].startOf('day').toISOString() : undefined,
|
||||||
|
created_to: createdRange?.[1] ? createdRange[1].endOf('day').toISOString() : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setEvent('');
|
||||||
|
setDeviceId('');
|
||||||
|
setUserId('');
|
||||||
|
setSessionId('');
|
||||||
|
setCreatedRange(null);
|
||||||
|
setSortBy('id');
|
||||||
|
setSortOrder('desc');
|
||||||
|
setApplied({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTableChange = (
|
||||||
|
_p: unknown,
|
||||||
|
_f: unknown,
|
||||||
|
sorter: SorterResult<EventLog> | SorterResult<EventLog>[],
|
||||||
|
) => {
|
||||||
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
if (s && s.order && s.field) {
|
||||||
|
setSortBy(s.field as SortField);
|
||||||
|
setSortOrder(s.order === 'ascend' ? 'asc' : 'desc');
|
||||||
|
} else {
|
||||||
|
setSortBy('id');
|
||||||
|
setSortOrder('desc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOrderOf = (field: SortField) =>
|
||||||
|
sortBy === field ? (sortOrder === 'asc' ? 'ascend' : 'descend') : null;
|
||||||
|
|
||||||
|
const columns: ColumnsType<EventLog> = [
|
||||||
|
{
|
||||||
|
title: '接收时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
width: 170,
|
||||||
|
sorter: true,
|
||||||
|
sortOrder: sortOrderOf('created_at'),
|
||||||
|
render: (v: string) => formatUtcTime(v),
|
||||||
|
},
|
||||||
|
{ title: 'ID', dataIndex: 'id', width: 70, sorter: true, sortOrder: sortOrderOf('id') },
|
||||||
|
{
|
||||||
|
title: '事件',
|
||||||
|
dataIndex: 'event',
|
||||||
|
width: 170,
|
||||||
|
render: (v: string) => <Tag color="blue">{v}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '页面',
|
||||||
|
dataIndex: 'page',
|
||||||
|
width: 100,
|
||||||
|
render: (v: string | null) => v || <Text type="secondary">-</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
dataIndex: 'user_id',
|
||||||
|
width: 80,
|
||||||
|
render: (v: number | null) => v ?? <Text type="secondary">未登录</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设备',
|
||||||
|
dataIndex: 'device_id',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (v: string) => (
|
||||||
|
<Text style={{ fontSize: 12 }} copyable={{ text: v }}>
|
||||||
|
{v}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '网络', dataIndex: 'network', width: 70, render: (v: string | null) => v || '-' },
|
||||||
|
{
|
||||||
|
title: '机型',
|
||||||
|
key: 'oem',
|
||||||
|
width: 130,
|
||||||
|
render: (_: unknown, r: EventLog) => (
|
||||||
|
<Text style={{ fontSize: 12 }}>{[r.oem, r.os].filter(Boolean).join(' / ') || '-'}</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '版本', dataIndex: 'app_ver', width: 100, render: (v: string | null) => v || '-' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>埋点日志</h2>
|
||||||
|
<Space style={{ marginBottom: 16 }} wrap>
|
||||||
|
<Input
|
||||||
|
placeholder="事件名 (如 video_play)"
|
||||||
|
value={event}
|
||||||
|
onChange={(e) => setEvent(e.target.value)}
|
||||||
|
onPressEnter={search}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 180 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="设备ID(前缀)"
|
||||||
|
value={deviceId}
|
||||||
|
onChange={(e) => setDeviceId(e.target.value)}
|
||||||
|
onPressEnter={search}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 160 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="用户ID"
|
||||||
|
value={userId}
|
||||||
|
onChange={(e) => setUserId(e.target.value.replace(/\D/g, ''))}
|
||||||
|
onPressEnter={search}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="会话ID"
|
||||||
|
value={sessionId}
|
||||||
|
onChange={(e) => setSessionId(e.target.value)}
|
||||||
|
onPressEnter={search}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 160 }}
|
||||||
|
/>
|
||||||
|
<RangePicker
|
||||||
|
placeholder={['接收起', '接收止']}
|
||||||
|
value={createdRange}
|
||||||
|
onChange={(v) => setCreatedRange(v as [Dayjs, Dayjs] | null)}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={search}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button onClick={resetFilters}>重置</Button>
|
||||||
|
<Button onClick={reload}>刷新</Button>
|
||||||
|
<Space size={4}>
|
||||||
|
<Switch checked={autoRefresh} onChange={setAutoRefresh} />
|
||||||
|
<Text type="secondary">自动刷新(5s)</Text>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={items}
|
||||||
|
loading={loading}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: (r) => (
|
||||||
|
<Space direction="vertical" size={2} style={{ fontSize: 12 }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
会话: {r.session_id || '-'} | 设备型号: {r.model || '-'} | 渠道: {r.channel || '-'} | IP:{' '}
|
||||||
|
{r.client_ip || '-'}
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary">
|
||||||
|
端事件时间: {fmtMs(r.client_ts)} | 端上报时间: {fmtMs(r.sent_at)}
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary">
|
||||||
|
属性: {r.props && Object.keys(r.props).length ? JSON.stringify(r.props) : '无'}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
onChange: onPageChange,
|
||||||
|
}}
|
||||||
|
onChange={onTableChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
|||||||
import {
|
import {
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
FlagOutlined,
|
FlagOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
@@ -37,6 +38,7 @@ const MENU = [
|
|||||||
{ key: '/cps', icon: <ShareAltOutlined />, label: 'CPS 分发' },
|
{ key: '/cps', icon: <ShareAltOutlined />, label: 'CPS 分发' },
|
||||||
{ key: '/config', icon: <SettingOutlined />, label: '系统配置' },
|
{ key: '/config', icon: <SettingOutlined />, label: '系统配置' },
|
||||||
{ key: '/admins', icon: <TeamOutlined />, label: '管理员', superOnly: true },
|
{ key: '/admins', icon: <TeamOutlined />, label: '管理员', superOnly: true },
|
||||||
|
{ key: '/event-logs', icon: <DatabaseOutlined />, label: '埋点日志' },
|
||||||
{ key: '/audit-logs', icon: <FileSearchOutlined />, label: '审计日志' },
|
{ key: '/audit-logs', icon: <FileSearchOutlined />, label: '审计日志' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ export interface AdRevenueRow {
|
|||||||
user_id: number;
|
user_id: number;
|
||||||
user_phone: string | null; // 用户手机号(admin 展示;查不到为空)
|
user_phone: string | null; // 用户手机号(admin 展示;查不到为空)
|
||||||
ad_type: string; // reward_video / feed / draw
|
ad_type: string; // reward_video / feed / draw
|
||||||
|
feed_scene?: string | null; // Draw 信息流投放场景 comparison(比价) / coupon(领券) / welfare(福利);非信息流或旧数据为 null
|
||||||
app_env: string | null; // prod(傻瓜比价) / test(测试);旧数据为空
|
app_env: string | null; // prod(傻瓜比价) / test(测试);旧数据为空
|
||||||
our_code_id: string | null; // 我们配置的代码位 104xxx;旧数据为空
|
our_code_id: string | null; // 我们配置的代码位 104xxx;旧数据为空
|
||||||
hour: number | null; // 北京时间小时 0–23(按小时粒度时有值;按天为 null)
|
hour: number | null; // 北京时间小时 0–23(按小时粒度时有值;按天为 null)
|
||||||
|
|||||||
Reference in New Issue
Block a user