Compare commits

...

3 Commits

Author SHA1 Message Date
lowmaster-chen 1db0c3f834 fix: 同步数据大盘活跃用户口径文案 2026-06-30 19:49:49 +08:00
lowmaster-chen aaea458f1a feat: 优化数据大盘环比展示并移除总用户卡 2026-06-30 15:50:32 +08:00
lowmaster-chen 464d8dd183 feat: 补齐数据大盘京东 CPS 与金币展示
改了什么:数据大盘总收入/CPS 收入纳入京东 CPS;京东 CPS 卡片展示有效/无效订单;刷新按钮改为同步全部 CPS 订单;ARPU 展示产品确认的昨日活跃用户分母;激励视频金币展示真实值并标注已计入领券奖励。

验证方式:npx tsc --noEmit;浏览器验证 /dashboard 已显示京东 CPS、ARPU 分母和激励视频金币。
2026-06-30 10:23:49 +08:00
2 changed files with 334 additions and 92 deletions
+329 -92
View File
@@ -1,7 +1,7 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { Alert, Select, Spin, Tag, Tooltip, message } from 'antd';
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { Alert, App, Select, Spin, Tag, Tooltip } from 'antd';
import { CalendarOutlined, InfoCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@/lib/api';
@@ -10,6 +10,8 @@ import type { AdRevenueReport, DashboardOverview } from '@/lib/types';
type PeriodKey = 'yesterday' | '7d' | '30d';
type TrendSeriesKey = 'dau' | 'newUsers' | 'comparisons';
type DashboardTrendPoint = DashboardOverview['period']['trend'][number];
type DeltaTone = 'muted' | 'up' | 'down' | 'flat';
type DeltaInfo = { value: ReactNode; tone: DeltaTone };
const PERIODS: { key: PeriodKey; label: string; days: number }[] = [
{ key: 'yesterday', label: '昨日', days: 1 },
@@ -35,6 +37,72 @@ const fmtYuan = (yuan: number | null | undefined) => (yuan == null ? '--' : `¥$
const fmtMsAsSeconds = (ms: number | null | undefined) => (ms == null ? '--' : (ms / 1000).toFixed(1));
const pct = (v: number | null | undefined) => (v == null ? '--' : `${(v * 100).toFixed(1)}%`);
const compact = (v: number) => (v >= 1000 ? `${(v / 1000).toFixed(v >= 10000 ? 0 : 1)}k` : String(v));
const fmtCoinAmount = (v: number | null | undefined) => {
if (v == null) return '--';
if (Math.abs(v) >= 10000) return `${(v / 10000).toFixed(2).replace(/\.?0+$/, '')}`;
return numberFmt.format(v);
};
const CMP_LABEL = 'vs 上周期';
function withCompare(main: string) {
return (
<>
{main}
<span className="cmp">{CMP_LABEL}</span>
</>
);
}
function percentDelta(current: number | null | undefined, previous: number | null | undefined): DeltaInfo {
if (current == null || previous == null) return { value: '暂无上期', tone: 'flat' };
if (previous === 0) {
if (current === 0) return { value: withCompare('持平'), tone: 'flat' };
return { value: '上期为 0', tone: current > 0 ? 'up' : 'down' };
}
const diff = (current - previous) / Math.abs(previous);
if (Math.abs(diff) < 0.0005) return { value: withCompare('持平'), tone: 'flat' };
const arrow = diff > 0 ? '▲' : '▼';
return {
value: withCompare(`${arrow} ${(Math.abs(diff) * 100).toFixed(1)}%`),
tone: diff > 0 ? 'up' : 'down',
};
}
function pointDelta(current: number | null | undefined, previous: number | null | undefined): DeltaInfo {
if (current == null || previous == null) return { value: '暂无上期', tone: 'flat' };
const diff = (current - previous) * 100;
if (Math.abs(diff) < 0.05) return { value: withCompare('持平'), tone: 'flat' };
const arrow = diff > 0 ? '▲' : '▼';
return {
value: withCompare(`${arrow} ${Math.abs(diff).toFixed(1)}pt`),
tone: diff > 0 ? 'up' : 'down',
};
}
function durationDelta(currentMs: number | null | undefined, previousMs: number | null | undefined): DeltaInfo {
if (currentMs == null || previousMs == null) return { value: '暂无上期', tone: 'flat' };
const diffSeconds = (currentMs - previousMs) / 1000;
if (Math.abs(diffSeconds) < 0.05) return { value: withCompare('持平'), tone: 'flat' };
return diffSeconds < 0
? { value: `${Math.abs(diffSeconds).toFixed(1)}s 更快`, tone: 'up' }
: { value: `${Math.abs(diffSeconds).toFixed(1)}s 更慢`, tone: 'down' };
}
function moneyDeltaCents(current: number | null | undefined, previous: number | null | undefined): DeltaInfo {
if (current == null || previous == null) return { value: '暂无上期', tone: 'flat' };
const diff = current - previous;
if (Math.abs(diff) < 0.5) return { value: withCompare('持平'), tone: 'flat' };
const arrow = diff > 0 ? '▲' : '▼';
return {
value: withCompare(`${arrow} ${fmtCents(Math.abs(diff))}`),
tone: diff > 0 ? 'up' : 'down',
};
}
function ratioBadge(value: number | null | undefined, total: number | null | undefined): DeltaInfo {
if (value == null || total == null || total <= 0) return { value: '占 --', tone: 'flat' };
return { value: `${((value / total) * 100).toFixed(1)}%`, tone: 'flat' };
}
function periodRange(period: PeriodKey) {
const p = PERIODS.find((it) => it.key === period) ?? PERIODS[0];
@@ -43,6 +111,12 @@ function periodRange(period: PeriodKey) {
return { start, end, label: `${start.format('MM-DD')} ~ ${end.format('MM-DD')}`, days: p.days };
}
function previousPeriodRange(range: ReturnType<typeof periodRange>) {
const end = range.start.subtract(1, 'day');
const start = end.subtract(range.days - 1, 'day');
return { start, end, label: `${start.format('MM-DD')} ~ ${end.format('MM-DD')}` };
}
function retentionTitle(period: PeriodKey) {
if (period === '7d') return '近 7 日新增留存';
if (period === '30d') return '近 30 日新增留存';
@@ -92,6 +166,16 @@ function sceneAdEcpm(summary: ReturnType<typeof adSceneSummary>) {
return fmtYuan((summary.revenue_yuan / summary.impressions) * 1000);
}
function cpsCommissionCents(data: DashboardOverview | null | undefined) {
if (!data?.cps.available) return null;
return (data.cps.meituan_commission_cents ?? 0) + (data.cps.jd_commission_cents ?? 0);
}
function revenueYuan(ad: AdRevenueReport | null | undefined, cpsCents: number | null | undefined) {
if (ad?.total_revenue_yuan == null && cpsCents == null) return null;
return (ad?.total_revenue_yuan ?? 0) + ((cpsCents ?? 0) / 100);
}
function niceMax(v: number) {
if (v <= 5) return 5;
if (v <= 10) return 10;
@@ -99,7 +183,7 @@ function niceMax(v: number) {
return Math.ceil(v / base) * base;
}
function StatusText({ children, tone = 'muted' }: { children: React.ReactNode; tone?: 'muted' | 'ok' | 'warn' }) {
function StatusText({ children, tone = 'muted' }: { children: ReactNode; tone?: 'muted' | 'ok' | 'warn' }) {
return <span className={`status status-${tone}`}>{children}</span>;
}
@@ -107,8 +191,8 @@ function DeltaBadge({
children,
tone = 'muted',
}: {
children: React.ReactNode;
tone?: 'muted' | 'up' | 'down';
children: ReactNode;
tone?: DeltaTone;
}) {
return <span className={`delta delta-${tone}`}>{children}</span>;
}
@@ -116,11 +200,15 @@ function DeltaBadge({
function MiniSpark({
color = '#2F6BFF',
down,
flat,
}: {
color?: string;
down?: boolean;
flat?: boolean;
}) {
const points = down
const points = flat
? '0,16 13,16 26,15 39,16 52,15 65,16 78,15'
: down
? '0,8 13,10 26,9 39,14 52,12 65,18 78,20'
: '0,24 13,22 26,20 39,15 52,13 65,8 78,5';
return (
@@ -149,9 +237,9 @@ function StatCard({
status?: string;
tone?: 'muted' | 'ok' | 'warn';
emphasis?: boolean;
spark?: 'up' | 'down';
delta?: string;
deltaTone?: 'muted' | 'up' | 'down';
spark?: 'up' | 'down' | 'flat';
delta?: ReactNode;
deltaTone?: DeltaTone;
}) {
return (
<div className={`stat-card${emphasis ? ' stat-card-emphasis' : ''}`}>
@@ -168,7 +256,13 @@ function StatCard({
{value}
{unit && value !== '--' && <span>{unit}</span>}
</div>
{spark && <MiniSpark color={spark === 'down' ? '#EF4444' : '#2F6BFF'} down={spark === 'down'} />}
{spark && (
<MiniSpark
color={spark === 'down' ? '#EF4444' : '#2F6BFF'}
down={spark === 'down'}
flat={spark === 'flat'}
/>
)}
</div>
{delta ? <DeltaBadge tone={deltaTone}>{delta}</DeltaBadge> : status && <StatusText tone={tone}>{status}</StatusText>}
</div>
@@ -181,18 +275,20 @@ function RevenueCard({
meta,
accent,
delta,
deltaTone = 'up',
}: {
title: string;
value: string;
meta?: { label: string; value: string }[];
accent?: 'blue' | 'green' | 'yellow' | 'red';
delta?: string;
accent?: 'blue' | 'green' | 'yellow' | 'red' | 'jd';
delta?: ReactNode;
deltaTone?: DeltaTone;
}) {
return (
<div className={`revenue-card ${accent ? `rev-${accent}` : ''}`}>
<div className="revenue-top">
<div className="revenue-tag">{title}</div>
{delta && <DeltaBadge tone="up">{delta}</DeltaBadge>}
{delta && <DeltaBadge tone={deltaTone}>{delta}</DeltaBadge>}
</div>
<div className="revenue-value">{value}</div>
{meta && meta.length > 0 && (
@@ -349,9 +445,12 @@ function TrendChart({ rangeLabel, points }: { rangeLabel: string; points: Dashbo
}
export default function DashboardPage() {
const { message } = App.useApp();
const [period, setPeriod] = useState<PeriodKey>('yesterday');
const [data, setData] = useState<DashboardOverview | null>(null);
const [previousData, setPreviousData] = useState<DashboardOverview | null>(null);
const [adReport, setAdReport] = useState<AdRevenueReport | null>(null);
const [previousAdReport, setPreviousAdReport] = useState<AdRevenueReport | null>(null);
const [loading, setLoading] = useState(true);
const [adLoading, setAdLoading] = useState(false);
const [overviewError, setOverviewError] = useState<string | null>(null);
@@ -361,7 +460,9 @@ export default function DashboardPage() {
const [cpsSyncing, setCpsSyncing] = useState(false);
const range = useMemo(() => periodRange(period), [period]);
const previousRange = useMemo(() => previousPeriodRange(range), [range]);
const periodData = data?.period ?? null;
const previousPeriodData = previousData?.period ?? null;
const adAvgEcpm = avgEcpm(adReport);
const rewardVideoAdSummary = adTypeSummary(adReport, 'reward_video');
const couponAdSummary = adSceneSummary(adReport, 'coupon');
@@ -381,44 +482,84 @@ export default function DashboardPage() {
const regularTaskCoinTotal =
periodData?.coins.regular_task_coin_total ??
((signinCoinTotal ?? 0) + (signinBoostCoinTotal ?? 0) + (taskCoinTotal ?? 0));
const cpsCommissionCents = data?.cps.meituan_commission_cents;
const cpsAvailable = data?.cps.available === true;
const totalRevenueYuan =
adReport?.total_revenue_yuan == null && (!cpsAvailable || cpsCommissionCents == null)
? null
: (adReport?.total_revenue_yuan ?? 0) + (cpsAvailable ? (cpsCommissionCents ?? 0) / 100 : 0);
const meituanCpsCommissionCents = data?.cps.meituan_commission_cents;
const jdCpsCommissionCents = data?.cps.jd_commission_cents;
const totalCpsCommissionCents = cpsCommissionCents(data);
const previousTotalCpsCommissionCents = cpsCommissionCents(previousData);
const totalRevenueYuan = revenueYuan(adReport, totalCpsCommissionCents);
const previousTotalRevenueYuan = revenueYuan(previousAdReport, previousTotalCpsCommissionCents);
const yesterdayTrendPoint = periodData?.trend?.[periodData.trend.length - 1];
const yesterdayActiveUsers = yesterdayTrendPoint?.active_users ?? null;
const arpuYuan =
totalRevenueYuan == null || yesterdayActiveUsers == null || yesterdayActiveUsers <= 0
? null
: totalRevenueYuan / yesterdayActiveUsers;
const cpsRevenueText = cpsAvailable ? fmtCents(cpsCommissionCents) : '--';
const cpsRevenueText = cpsAvailable ? fmtCents(totalCpsCommissionCents) : '--';
const meituanCpsRevenueText = cpsAvailable ? fmtCents(meituanCpsCommissionCents) : '--';
const jdCpsRevenueText = cpsAvailable ? fmtCents(jdCpsCommissionCents) : '--';
const cpsHitRate = !cpsAvailable || data?.cps.meituan_hit_rate == null ? '--' : pct(data.cps.meituan_hit_rate);
const retentionValue =
periodData?.users.retention_rate == null
? '--'
: (periodData.users.retention_rate * 100).toFixed(1);
const activeUserDelta = percentDelta(periodData?.users.active, previousPeriodData?.users.active);
const newUserDelta = percentDelta(periodData?.users.new, previousPeriodData?.users.new);
const retentionDelta = pointDelta(periodData?.users.retention_rate, previousPeriodData?.users.retention_rate);
const retentionSpark: 'up' | 'down' | 'flat' | undefined =
periodData?.users.retention_rate == null
? undefined
: retentionDelta.tone === 'down'
? 'down'
: retentionDelta.tone === 'up' || periodData.users.retention_rate >= 1
? 'up'
: 'flat';
const comparisonTotalDelta = percentDelta(periodData?.comparison.total, previousPeriodData?.comparison.total);
const comparisonSuccessDelta = percentDelta(periodData?.comparison.success, previousPeriodData?.comparison.success);
const comparisonOrderedDelta = percentDelta(periodData?.comparison.ordered, previousPeriodData?.comparison.ordered);
const durationDeltaInfo = durationDelta(
periodData?.comparison.average_duration_ms,
previousPeriodData?.comparison.average_duration_ms,
);
const averageSavedDelta = moneyDeltaCents(
periodData?.comparison.average_saved_cents,
previousPeriodData?.comparison.average_saved_cents,
);
const totalRevenueDelta = percentDelta(totalRevenueYuan, previousTotalRevenueYuan);
const adRevenueDelta = percentDelta(adReport?.total_revenue_yuan, previousAdReport?.total_revenue_yuan);
const cpsRevenueDelta = percentDelta(totalCpsCommissionCents, previousTotalCpsCommissionCents);
const grantedCoinDelta = percentDelta(periodData?.coins.granted_total, previousPeriodData?.coins.granted_total);
const couponCoinRatio = ratioBadge(couponRewardCoinTotal, periodData?.coins.granted_total);
const comparisonCoinRatio = ratioBadge(comparisonRewardCoinTotal, periodData?.coins.granted_total);
const rewardVideoCoinRatio = ratioBadge(periodData?.coins.reward_video_coin_total, periodData?.coins.granted_total);
const regularTaskCoinRatio = ratioBadge(regularTaskCoinTotal, periodData?.coins.granted_total);
useEffect(() => {
let alive = true;
setLoading(true);
setOverviewError(null);
api
.get<DashboardOverview>('/admin/api/stats/overview', {
params: {
date_from: range.start.format('YYYY-MM-DD'),
date_to: range.end.format('YYYY-MM-DD'),
},
})
.then((r) => {
const currentParams = {
date_from: range.start.format('YYYY-MM-DD'),
date_to: range.end.format('YYYY-MM-DD'),
};
const previousParams = {
date_from: previousRange.start.format('YYYY-MM-DD'),
date_to: previousRange.end.format('YYYY-MM-DD'),
};
Promise.allSettled([
api.get<DashboardOverview>('/admin/api/stats/overview', { params: currentParams }),
api.get<DashboardOverview>('/admin/api/stats/overview', { params: previousParams }),
])
.then(([current, previous]) => {
if (!alive) return;
setData(r.data);
setUpdatedAt(dayjs().format('YYYY-MM-DD HH:mm'));
})
.catch(() => {
if (!alive) return;
setOverviewError('总览接口加载失败');
if (current.status === 'fulfilled') {
setData(current.value.data);
setUpdatedAt(dayjs().format('YYYY-MM-DD HH:mm'));
} else {
setData(null);
setOverviewError('总览接口加载失败');
}
setPreviousData(previous.status === 'fulfilled' ? previous.value.data : null);
})
.finally(() => {
if (alive) setLoading(false);
@@ -426,29 +567,37 @@ export default function DashboardPage() {
return () => {
alive = false;
};
}, [range.start, range.end, refreshKey]);
}, [range.start, range.end, previousRange.start, previousRange.end, refreshKey]);
useEffect(() => {
let alive = true;
setAdLoading(true);
setAdError(null);
api
.get<AdRevenueReport>('/admin/api/ad-revenue-report', {
params: {
date_from: range.start.format('YYYY-MM-DD'),
date_to: range.end.format('YYYY-MM-DD'),
granularity: 'day',
limit: 1000,
},
})
.then((r) => {
const currentParams = {
date_from: range.start.format('YYYY-MM-DD'),
date_to: range.end.format('YYYY-MM-DD'),
granularity: 'day',
limit: 1000,
};
const previousParams = {
date_from: previousRange.start.format('YYYY-MM-DD'),
date_to: previousRange.end.format('YYYY-MM-DD'),
granularity: 'day',
limit: 1000,
};
Promise.allSettled([
api.get<AdRevenueReport>('/admin/api/ad-revenue-report', { params: currentParams }),
api.get<AdRevenueReport>('/admin/api/ad-revenue-report', { params: previousParams }),
])
.then(([current, previous]) => {
if (!alive) return;
setAdReport(r.data);
})
.catch(() => {
if (!alive) return;
setAdReport(null);
setAdError('广告收益接口加载失败');
if (current.status === 'fulfilled') {
setAdReport(current.value.data);
} else {
setAdReport(null);
setAdError('广告收益接口加载失败');
}
setPreviousAdReport(previous.status === 'fulfilled' ? previous.value.data : null);
})
.finally(() => {
if (alive) setAdLoading(false);
@@ -456,7 +605,7 @@ export default function DashboardPage() {
return () => {
alive = false;
};
}, [range.start, range.end]);
}, [range.start, range.end, previousRange.start, previousRange.end]);
if (loading) return <Spin style={{ display: 'block', marginTop: 80 }} />;
@@ -464,7 +613,7 @@ export default function DashboardPage() {
return <Alert type="error" showIcon message={overviewError ?? '数据大盘加载失败'} />;
}
const syncMeituanCps = async () => {
const syncCpsOrders = async () => {
setCpsSyncing(true);
try {
const resp = await api.post<{
@@ -476,12 +625,13 @@ export default function DashboardPage() {
params: {
date_from: range.start.format('YYYY-MM-DD'),
date_to: range.end.format('YYYY-MM-DD'),
platform: 'all',
},
});
message.success(`已同步 ${resp.data.fetched}`);
setRefreshKey((v) => v + 1);
} catch {
message.error('美团订单刷新失败');
message.error('CPS 订单刷新失败');
} finally {
setCpsSyncing(false);
}
@@ -527,20 +677,20 @@ export default function DashboardPage() {
<h3></h3>
<span></span>
</div>
<div className="grid g5">
<StatCard title="总用户" value={fmtInt(data.users.total)} status="累计用户数" tone="ok" spark="up" />
<div className="grid g4">
<StatCard
title="活跃用户"
value={fmtInt(periodData?.users.active)}
status="本期活跃去重"
tone="ok"
hint="当前按本期内进入 App、完成比价记录、开始领券流程任一满足去重;未完成上报的比价开始事件待后续补埋点。"
delta={activeUserDelta.value}
deltaTone={activeUserDelta.tone}
hint="当前按本期内登录、开始比价、开始领券任一满足去重;比价按 real_compare_start,领券按 real_coupon_start/claim_started。"
spark="up"
/>
<StatCard
title="新增用户"
value={fmtInt(periodData?.users.new)}
status="本期新用户"
delta={newUserDelta.value}
deltaTone={newUserDelta.tone}
hint="当前按注册时间落在所选日期范围统计;若产品要按首次进入 App/首次流程启动定义新增,需要补首次行为聚合。"
spark="up"
/>
@@ -548,16 +698,16 @@ export default function DashboardPage() {
title={retentionTitle(period)}
value={retentionValue}
unit="%"
status={`${fmtInt(periodData?.users.retained_new_users)} / ${fmtInt(periodData?.users.new)} 新用户`}
tone="warn"
delta={retentionDelta.value}
deltaTone={retentionDelta.tone}
hint={periodData?.users.retention_note ?? '暂无留存数据'}
spark="down"
spark={retentionSpark}
/>
<StatCard
title="比价次数"
value={fmtInt(periodData?.comparison.total)}
status="本期比价次数"
tone="muted"
delta={comparisonTotalDelta.value}
deltaTone={comparisonTotalDelta.tone}
spark="up"
/>
</div>
@@ -571,23 +721,43 @@ export default function DashboardPage() {
<span></span>
</div>
<div className="grid g5">
<StatCard title="发起比价" value={fmtInt(periodData?.comparison.total)} unit="次" status="本期发起次数" />
<StatCard title="比价成功" value={fmtInt(periodData?.comparison.success)} unit="次" status={pct(compareSuccessRate)} tone="ok" />
<StatCard
title="发起比价"
value={fmtInt(periodData?.comparison.total)}
unit="次"
delta={comparisonTotalDelta.value}
deltaTone={comparisonTotalDelta.tone}
/>
<StatCard
title="比价成功"
value={fmtInt(periodData?.comparison.success)}
unit="次"
delta={comparisonSuccessDelta.value}
deltaTone={comparisonSuccessDelta.tone}
hint={`当前成功率 ${pct(compareSuccessRate)}`}
/>
<StatCard
title="成功下单"
value={fmtInt(periodData?.comparison.ordered)}
unit="单"
status="已下单记录"
delta={comparisonOrderedDelta.value}
deltaTone={comparisonOrderedDelta.tone}
hint="按比价记录页同一口径:用户有通过比价产生的下单记录,且店名与比价记录店名一致,则这条比价记录记为已下单。"
/>
<StatCard
title="平均耗时"
value={fmtMsAsSeconds(averageDurationMs)}
unit="秒"
status={averageDurationMs == null ? '暂无耗时数据' : '本期平均耗时'}
delta={durationDeltaInfo.value}
deltaTone={durationDeltaInfo.tone}
hint="按比价记录页同一口径:取所选日期范围内已上报耗时的比价记录,求平均后换算为秒。"
/>
<StatCard title="平均节省金额" value={fmtCents(periodData?.comparison.average_saved_cents)} status="有节省的成功比价" />
<StatCard
title="平均节省金额"
value={fmtCents(periodData?.comparison.average_saved_cents)}
delta={averageSavedDelta.value}
deltaTone={averageSavedDelta.tone}
/>
</div>
</section>
@@ -597,7 +767,7 @@ export default function DashboardPage() {
<h3></h3>
<span>
{cpsAvailable
? (adError ? '广告收益接口当前后端不可用,美团 CPS 已接入' : '广告收益与美团 CPS 已接入')
? (adError ? '广告收益接口当前后端不可用,美团/京东 CPS 已接入' : '广告收益与美团/京东 CPS 已接入')
: (adError ? '广告收益接口当前后端不可用,订单/佣金数据待接' : '广告收益已接入,订单/佣金数据待接')}
</span>
</div>
@@ -607,6 +777,8 @@ export default function DashboardPage() {
title="总收入"
value={adLoading ? '--' : fmtYuan(totalRevenueYuan)}
accent="blue"
delta={totalRevenueDelta.value}
deltaTone={totalRevenueDelta.tone}
meta={[
{ label: '广告收入', value: adLoading ? '--' : fmtYuan(adReport?.total_revenue_yuan) },
{ label: 'CPS 收入', value: cpsRevenueText },
@@ -615,38 +787,58 @@ export default function DashboardPage() {
<RevenueCard
title="广告收入"
value={adLoading ? '--' : fmtYuan(adReport?.total_revenue_yuan)}
meta={[{ label: '展示数', value: adLoading ? '--' : fmtInt(adReport?.total_impressions) }]}
delta={adRevenueDelta.value}
deltaTone={adRevenueDelta.tone}
meta={[
{
label: '占总收入',
value:
totalRevenueYuan && totalRevenueYuan > 0 && adReport?.total_revenue_yuan != null
? `${((adReport.total_revenue_yuan / totalRevenueYuan) * 100).toFixed(1)}%`
: '--',
},
]}
/>
<RevenueCard
title="CPS 收入"
value={cpsRevenueText}
meta={[{ label: '来源', value: cpsAvailable ? '美团已接,淘宝/京东暂空' : '订单/佣金待接' }]}
delta={cpsRevenueDelta.value}
deltaTone={cpsRevenueDelta.tone}
meta={[
{
label: '占总收入',
value:
totalRevenueYuan && totalRevenueYuan > 0 && totalCpsCommissionCents != null
? `${(((totalCpsCommissionCents / 100) / totalRevenueYuan) * 100).toFixed(1)}%`
: '--',
},
]}
/>
<RevenueCard
title="ARPU"
value={adLoading ? '--' : fmtYuan(arpuYuan)}
meta={[
{ label: '口径', value: '收入 / 活跃用户' },
{ label: 'ARPPU', value: '待确认付费用户口径' },
{ label: '口径', value: '收入 / 昨日活跃用户' },
{ label: '分母', value: yesterdayActiveUsers == null ? '--' : `${fmtInt(yesterdayActiveUsers)}` },
]}
/>
</div>
<div className="sub-label cps-sub-label">
<span>CPS </span>
<button type="button" onClick={syncMeituanCps} disabled={cpsSyncing}>
{cpsSyncing ? '刷新中' : '刷新美团订单'}
<button type="button" onClick={syncCpsOrders} disabled={cpsSyncing}>
{cpsSyncing ? '刷新中' : '刷新 CPS 订单'}
</button>
</div>
<div className="grid g4">
<RevenueCard
title="总 CPS 收益"
value={cpsRevenueText}
meta={[{ label: '来源', value: cpsAvailable ? '美团订单佣金,淘宝/京东暂空' : '订单/佣金待接' }]}
meta={[{ label: '来源', value: cpsAvailable ? '美团+京东订单佣金,淘宝暂未接入' : '订单/佣金待接' }]}
/>
<RevenueCard
title="美团 CPS"
value={cpsRevenueText}
value={meituanCpsRevenueText}
meta={[
{ label: '订单量', value: cpsAvailable ? fmtInt(data?.cps.meituan_order_count) : '待订单数据' },
{
@@ -658,8 +850,16 @@ export default function DashboardPage() {
]}
accent="yellow"
/>
<RevenueCard title="淘宝 CPS" value="--" meta={[{ label: '状态', value: '后续接入后填写' }]} accent="red" />
<RevenueCard title="京东 CPS" value="--" meta={[{ label: '状态', value: '后续接入后填写' }]} accent="red" />
<RevenueCard title="淘宝 CPS" value="--" meta={[{ label: '状态', value: '暂未接入' }]} accent="red" />
<RevenueCard
title="京东 CPS"
value={jdCpsRevenueText}
meta={[
{ label: '有效订单', value: cpsAvailable ? fmtInt(data?.cps.jd_order_count) : '待订单数据' },
{ label: '无效订单', value: cpsAvailable ? fmtInt(data?.cps.jd_invalid_count ?? 0) : '待订单数据' },
]}
accent="jd"
/>
</div>
<div className="sub-label">广</div>
@@ -705,32 +905,58 @@ export default function DashboardPage() {
<span className="bar" />
<h3></h3>
<Tag color="warning"></Tag>
<span></span>
<span> = = </span>
</div>
<div className="grid g6">
<StatCard title="本期发放金币" value={fmtInt(periodData?.coins.granted_total)} status="本期发放合计" />
<StatCard
title="本期发放金币"
value={fmtInt(periodData?.coins.granted_total)}
delta={grantedCoinDelta.value}
deltaTone={grantedCoinDelta.tone}
emphasis
/>
<StatCard
title="领券奖励金币"
value={fmtInt(couponRewardCoinTotal)}
status="领券相关发放"
delta={couponCoinRatio.value}
deltaTone={couponCoinRatio.tone}
/>
<StatCard
title="比价奖励金币"
value={fmtInt(comparisonRewardCoinTotal)}
status="比价相关发放"
delta={comparisonCoinRatio.value}
deltaTone={comparisonCoinRatio.tone}
/>
<StatCard
title="激励视频金币"
value={fmtInt(periodData?.coins.reward_video_coin_total)}
delta={rewardVideoCoinRatio.value}
deltaTone={rewardVideoCoinRatio.tone}
hint="激励视频金币已按产品口径计入领券奖励金币,这里单独展示来源占比。"
/>
<StatCard title="激励视频金币" value="--" status="并入领券奖励" />
<StatCard
title="常规任务金币"
value={fmtInt(regularTaskCoinTotal)}
status="任务等发放"
delta={regularTaskCoinRatio.value}
deltaTone={regularTaskCoinRatio.tone}
/>
<StatCard
title="本期提现金额"
value={fmtCents(periodData?.cash.withdraw_success_cents)}
delta="实付现金"
deltaTone="flat"
/>
<StatCard title="本期提现金额" value={fmtCents(periodData?.cash.withdraw_success_cents)} status="成功提现" />
</div>
<div className="risk-note">
{fmtInt(periodData?.coins.granted_total)} {fmtCents(periodData?.cash.withdraw_success_cents)}
{fmtInt(data.coins.granted_total)} {fmtCents(data.cash.withdraw_success_cents)}
广广
<div>
4 <b>{fmtCoinAmount(periodData?.coins.granted_total)}</b>
<b>{fmtYuan((periodData?.coins.granted_total ?? 0) / 1000)}</b>
<b>{fmtCents(periodData?.cash.withdraw_success_cents)}</b>
<b>{fmtCoinAmount(data.coins.granted_total)}</b>
<b>{fmtCents(data.cash.withdraw_success_cents)}</b>/
</div>
</div>
</section>
@@ -772,7 +998,7 @@ export default function DashboardPage() {
--coin: #ffb300;
--p-mt-waimai: #ffc300;
--p-taobao: #ff4400;
--p-jd: #e1251b;
--p-jd: #b91c1c;
--radius: 10px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.04);
color: var(--ink);
@@ -967,10 +1193,17 @@ export default function DashboardPage() {
font-weight: 600;
}
.status-muted,
.delta-muted {
.delta-muted,
.delta-flat {
color: var(--ink-3);
background: #f1f3f7;
}
.delta .cmp {
margin-left: 3px;
color: var(--ink-3);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
font-weight: 400;
}
.status-ok,
.delta-up {
color: var(--up);
@@ -1144,6 +1377,10 @@ export default function DashboardPage() {
background: var(--p-taobao);
color: #fff;
}
.rev-jd .revenue-tag {
background: var(--p-jd);
color: #fff;
}
.revenue-value {
color: var(--ink);
font-family: "DIN Alternate", "Helvetica Neue", sans-serif;
+5
View File
@@ -536,6 +536,11 @@ export interface DashboardOverview {
meituan_miss_count: number;
meituan_unknown_rate_count: number;
meituan_hit_rate: number | null;
jd_order_count?: number;
jd_commission_cents?: number;
jd_actual_commission_cents?: number;
jd_estimated_commission_cents?: number;
jd_invalid_count?: number;
};
}