| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- import { useEffect, useState } from 'react';
- import { useMutation } from '@tanstack/react-query';
- import { queryFunnel, USE_MOCK } from '../../api/funnel';
- import type {
- FunnelPeriod,
- FunnelQueryRequest,
- FunnelQueryResponse,
- } from '../../api/types';
- import { DEFAULT_PERIOD, funnelRangeText, toSnapshotParam } from './period';
- import { TimePeriodSelect } from './components/TimePeriodSelect';
- import { FunnelResult } from './components/FunnelResult';
- import { Badge } from '@/components/ui/badge';
- /**
- * 拼团 5-step funnel page (docs/02 §5 v3). The funnel is fixed. Inputs are the
- * standard period plus — for `day` only — a historical date. Selecting a period
- * (incl. the default on first load) or, in single-day mode, a date immediately
- * fires a query.
- */
- export function FunnelPage() {
- const [period, setPeriod] = useState<FunnelPeriod>(DEFAULT_PERIOD);
- // Selected calendar day for `period: 'day'`. `null` = latest available day
- // (data is T+1 and may lag, so the default queries MAX(dt) server-side
- // rather than hard-coding yesterday); a picked date overrides it.
- const [snapshotDt, setSnapshotDt] = useState<Date | null>(null);
- const mutation = useMutation<FunnelQueryResponse, Error, FunnelQueryRequest>({
- mutationFn: queryFunnel,
- });
- const { mutate } = mutation;
- // Query on mount and whenever the period — or, for single-day, the chosen
- // date — changes. Send snapshot_dt ONLY for `day` with a picked date; omit it
- // for latest single-day and for 7d/30d (the server returns the latest row).
- const dayParam =
- period === 'day' && snapshotDt ? toSnapshotParam(snapshotDt) : undefined;
- useEffect(() => {
- mutate(dayParam ? { period: 'day', snapshot_dt: dayParam } : { period });
- }, [period, dayParam, mutate]);
- return (
- <div className="flex flex-col gap-5 w-full">
- {/* 标题行:标题 + Mock 徽章 在左,控件 ml-auto 落右;时间边界在控件正下方 */}
- <div className="flex items-start gap-3 flex-wrap">
- <h2 className="text-xl font-semibold tracking-tight m-0">漏斗分析</h2>
- {USE_MOCK && (
- <Badge
- variant="outline"
- className="text-xs font-normal text-muted-foreground border-dashed gap-1.5 pl-2"
- >
- <span className="size-1.5 rounded-full bg-amber-500" />
- Mock 模式
- </Badge>
- )}
- <div className="ml-auto flex flex-col items-end gap-1">
- {/* 单日 / 近7天 / 近30天 三选一,选中 mint 高亮;单日可选历史。 */}
- <TimePeriodSelect
- period={period}
- snapshotDt={snapshotDt}
- onPickDate={(d) => {
- setSnapshotDt(d);
- setPeriod('day');
- }}
- onPickLatest={() => {
- setSnapshotDt(null);
- setPeriod('day');
- }}
- onPickRolling={(p) => setPeriod(p)}
- />
- {/* 滚动周期显示日期范围;单日不显示。查询 pending 时不渲染,
- 避免用旧 snapshot_dt 配新 period 算出错误范围。 */}
- {!mutation.isPending && mutation.data?.snapshot_dt && period !== 'day' && (
- <span className="text-xs text-muted-foreground">
- {funnelRangeText(period, mutation.data.snapshot_dt)}
- </span>
- )}
- </div>
- </div>
- <p className="text-sm text-muted-foreground m-0">
- 拼团漏斗:启动 → 曝光 → 拼团详情 → 下单 → 成功
- </p>
- <FunnelResult
- data={mutation.data}
- isLoading={mutation.isPending}
- isError={mutation.isError}
- errorMessage={mutation.error?.message}
- />
- </div>
- );
- }
- export default FunnelPage;
|