FunnelPage.tsx 3.2 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 {
  10. DEFAULT_PERIOD,
  11. funnelRangeText,
  12. toSnapshotParam,
  13. yesterday,
  14. } from './period';
  15. import { TimePeriodSelect } from './components/TimePeriodSelect';
  16. import { FunnelResult } from './components/FunnelResult';
  17. import { Badge } from '@/components/ui/badge';
  18. /**
  19. * 拼团 5-step funnel page (docs/02 §5 v3). The funnel is fixed. Inputs are the
  20. * standard period plus — for `day` only — a historical date. Selecting a period
  21. * (incl. the default on first load) or, in single-day mode, a date immediately
  22. * fires a query.
  23. */
  24. export function FunnelPage() {
  25. const [period, setPeriod] = useState<FunnelPeriod>(DEFAULT_PERIOD);
  26. // Selected calendar day for `period: 'day'`; defaults to yesterday (max).
  27. const [snapshotDt, setSnapshotDt] = useState<Date>(() => yesterday());
  28. const mutation = useMutation<FunnelQueryResponse, Error, FunnelQueryRequest>({
  29. mutationFn: queryFunnel,
  30. });
  31. const { mutate } = mutation;
  32. // Query on mount and whenever the period — or, for single-day, the chosen
  33. // date — changes. Send snapshot_dt ONLY for `day`; omit it for 7d/30d.
  34. const dayParam = period === 'day' ? toSnapshotParam(snapshotDt) : undefined;
  35. useEffect(() => {
  36. mutate(
  37. period === 'day'
  38. ? { period, snapshot_dt: dayParam }
  39. : { period },
  40. );
  41. }, [period, dayParam, mutate]);
  42. return (
  43. <div className="flex flex-col gap-5 w-full">
  44. {/* 标题行:标题 + Mock 徽章 在左,控件 ml-auto 落右;时间边界在控件正下方 */}
  45. <div className="flex items-start gap-3 flex-wrap">
  46. <h2 className="text-xl font-semibold tracking-tight m-0">漏斗分析</h2>
  47. {USE_MOCK && (
  48. <Badge
  49. variant="outline"
  50. className="text-xs font-normal text-muted-foreground border-dashed gap-1.5 pl-2"
  51. >
  52. <span className="size-1.5 rounded-full bg-amber-500" />
  53. Mock 模式
  54. </Badge>
  55. )}
  56. <div className="ml-auto flex flex-col items-end gap-1">
  57. {/* 单日 / 近7天 / 近30天 三选一,选中 mint 高亮;单日可选历史。 */}
  58. <TimePeriodSelect
  59. period={period}
  60. snapshotDt={snapshotDt}
  61. onPickDate={(d) => {
  62. setSnapshotDt(d);
  63. setPeriod('day');
  64. }}
  65. onPickRolling={(p) => setPeriod(p)}
  66. />
  67. {/* 时间边界:简短日期,不含今日 */}
  68. {mutation.data?.snapshot_dt && (
  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;