FunnelPage.tsx 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { useEffect, useState } from 'react';
  2. import { useMutation } from '@tanstack/react-query';
  3. import { queryFunnel, USE_MOCK } from '../../api/funnel';
  4. import type {
  5. FunnelPeriod,
  6. FunnelQueryRequest,
  7. FunnelQueryResponse,
  8. } from '../../api/types';
  9. import { DEFAULT_PERIOD, funnelRangeText, toSnapshotParam } from './period';
  10. import { TimePeriodSelect } from './components/TimePeriodSelect';
  11. import { FunnelResult } from './components/FunnelResult';
  12. import { Badge } from '@/components/ui/badge';
  13. /**
  14. * 拼团 5-step funnel page (docs/02 §5 v3). The funnel is fixed. Inputs are the
  15. * standard period plus — for `day` only — a historical date. Selecting a period
  16. * (incl. the default on first load) or, in single-day mode, a date immediately
  17. * fires a query.
  18. */
  19. export function FunnelPage() {
  20. const [period, setPeriod] = useState<FunnelPeriod>(DEFAULT_PERIOD);
  21. // Selected calendar day for `period: 'day'`. `null` = latest available day
  22. // (data is T+1 and may lag, so the default queries MAX(dt) server-side
  23. // rather than hard-coding yesterday); a picked date overrides it.
  24. const [snapshotDt, setSnapshotDt] = useState<Date | null>(null);
  25. const mutation = useMutation<FunnelQueryResponse, Error, FunnelQueryRequest>({
  26. mutationFn: queryFunnel,
  27. });
  28. const { mutate } = mutation;
  29. // Query on mount and whenever the period — or, for single-day, the chosen
  30. // date — changes. Send snapshot_dt ONLY for `day` with a picked date; omit it
  31. // for latest single-day and for 7d/30d (the server returns the latest row).
  32. const dayParam =
  33. period === 'day' && snapshotDt ? toSnapshotParam(snapshotDt) : undefined;
  34. useEffect(() => {
  35. mutate(dayParam ? { period: 'day', snapshot_dt: dayParam } : { period });
  36. }, [period, dayParam, mutate]);
  37. return (
  38. <div className="flex flex-col gap-5 w-full">
  39. {/* 标题行:标题 + Mock 徽章 在左,控件 ml-auto 落右;时间边界在控件正下方 */}
  40. <div className="flex items-start gap-3 flex-wrap">
  41. <h2 className="text-xl font-semibold tracking-tight m-0">漏斗分析</h2>
  42. {USE_MOCK && (
  43. <Badge
  44. variant="outline"
  45. className="text-xs font-normal text-muted-foreground border-dashed gap-1.5 pl-2"
  46. >
  47. <span className="size-1.5 rounded-full bg-amber-500" />
  48. Mock 模式
  49. </Badge>
  50. )}
  51. <div className="ml-auto flex flex-col items-end gap-1">
  52. {/* 单日 / 近7天 / 近30天 三选一,选中 mint 高亮;单日可选历史。 */}
  53. <TimePeriodSelect
  54. period={period}
  55. snapshotDt={snapshotDt}
  56. onPickDate={(d) => {
  57. setSnapshotDt(d);
  58. setPeriod('day');
  59. }}
  60. onPickLatest={() => {
  61. setSnapshotDt(null);
  62. setPeriod('day');
  63. }}
  64. onPickRolling={(p) => setPeriod(p)}
  65. />
  66. {/* 滚动周期显示日期范围;单日不显示。查询 pending 时不渲染,
  67. 避免用旧 snapshot_dt 配新 period 算出错误范围。 */}
  68. {!mutation.isPending && mutation.data?.snapshot_dt && period !== 'day' && (
  69. <span className="text-xs text-muted-foreground">
  70. {funnelRangeText(period, mutation.data.snapshot_dt)}
  71. </span>
  72. )}
  73. </div>
  74. </div>
  75. <p className="text-sm text-muted-foreground m-0">
  76. 拼团漏斗:启动 → 曝光 → 拼团详情 → 下单 → 成功
  77. </p>
  78. <FunnelResult
  79. data={mutation.data}
  80. isLoading={mutation.isPending}
  81. isError={mutation.isError}
  82. errorMessage={mutation.error?.message}
  83. />
  84. </div>
  85. );
  86. }
  87. export default FunnelPage;