@@ -168,7 +256,13 @@ function StatCard({
{value}
{unit && value !== '--' && {unit}}
- {spark &&
{title}
- {delta &&
{delta}}
+ {delta &&
{delta}}
{value}
{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
('yesterday');
const [data, setData] = useState(null);
+ const [previousData, setPreviousData] = useState(null);
const [adReport, setAdReport] = useState(null);
+ const [previousAdReport, setPreviousAdReport] = useState(null);
const [loading, setLoading] = useState(true);
const [adLoading, setAdLoading] = useState(false);
const [overviewError, setOverviewError] = useState(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('/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('/admin/api/stats/overview', { params: currentParams }),
+ api.get('/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('/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('/admin/api/ad-revenue-report', { params: currentParams }),
+ api.get('/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 ;
@@ -464,7 +613,7 @@ export default function DashboardPage() {
return ;
}
- 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() {
核心趋势
先看用户、留存与比价活跃度
-
-
+
@@ -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}
/>
@@ -571,23 +721,43 @@ export default function DashboardPage() {
发起、成功、下单与效率