Sfoglia il codice sorgente

fix: 单日默认回昨日 + 按钮只显日期(去'单日/最新'),空就空

tianyu.chu 1 mese fa
parent
commit
6697a8c9bc

+ 7 - 0
CHANGELOG.md

@@ -2,6 +2,13 @@
 
 > 记录本项目每次改动。新条目追加到顶部。格式:`日期 — 改动概述`,正文按 `新增 / 变更 / 修复 / 移除` 分类。链接 PR / commit / 相关文档(如有)。
 
+## 2026-06-29
+
+### 变更
+- **单日默认改回「昨日」**(`apps/web`):去掉 06-26 引入的"最新可用日"默认(原为省略 `snapshot_dt` → 后端取 `MAX(dt)`)。改回默认查昨日并发 `snapshot_dt`;昨日无数据就如实显示「数据缺失」——**空就空,是上游问题,不在应用侧兜底**。
+  - 起因:上游 daily 跑批从 6/27 起写入全 0 行,`MAX(dt)` 落到空行 → 默认显示 0000。
+  - `TimePeriodSelect` 去掉「最新」文案与「最新可用日」快捷项,单日按钮直接显示所选日期(默认昨日);日历仍可选历史。
+
 ## 2026-06-26
 
 ### 新增

+ 12 - 14
apps/web/src/modules/funnel/FunnelView.tsx

@@ -6,7 +6,12 @@ import type {
   FunnelQueryRequest,
   FunnelQueryResponse,
 } from '../../api/types';
-import { DEFAULT_PERIOD, funnelRangeText, toSnapshotParam } from './period';
+import {
+  DEFAULT_PERIOD,
+  funnelRangeText,
+  toSnapshotParam,
+  yesterday,
+} from './period';
 import { TimePeriodSelect } from './components/TimePeriodSelect';
 import { FunnelResult } from './components/FunnelResult';
 
@@ -16,10 +21,9 @@ import { FunnelResult } from './components/FunnelResult';
  */
 export function FunnelView() {
   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);
+  // Selected calendar day for `period: 'day'`; defaults to yesterday (max, T+1).
+  // 空就空:昨日若无数据,如实显示「数据缺失」(上游问题,不在应用侧兜底)。
+  const [snapshotDt, setSnapshotDt] = useState<Date>(() => yesterday());
 
   const mutation = useMutation<FunnelQueryResponse, Error, FunnelQueryRequest>({
     mutationFn: queryFunnel,
@@ -28,12 +32,10 @@ export function FunnelView() {
   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;
+  // date — changes. Send snapshot_dt ONLY for `day`; omit it for 7d/30d.
+  const dayParam = period === 'day' ? toSnapshotParam(snapshotDt) : undefined;
   useEffect(() => {
-    mutate(dayParam ? { period: 'day', snapshot_dt: dayParam } : { period });
+    mutate(period === 'day' ? { period, snapshot_dt: dayParam } : { period });
   }, [period, dayParam, mutate]);
 
   return (
@@ -51,10 +53,6 @@ export function FunnelView() {
               setSnapshotDt(d);
               setPeriod('day');
             }}
-            onPickLatest={() => {
-              setSnapshotDt(null);
-              setPeriod('day');
-            }}
             onPickRolling={(p) => setPeriod(p)}
           />
           {/* 日期范围槽:始终占位(固定行高),避免显示/隐藏时挤动下方内容。

+ 20 - 30
apps/web/src/modules/funnel/__tests__/FunnelPage.test.tsx

@@ -84,10 +84,10 @@ describe('FunnelPage — fixed funnel + period selection', () => {
     for (const label of ['近 7 天', '近 30 天']) {
       expect(screen.getByRole('button', { name: label })).toBeInTheDocument();
     }
-    // 单日 segment opens the calendar (aria-label) and shows "单日".
+    // 单日 segment opens the calendar (aria-label) and shows the date (默认昨日).
     const picker = screen.getByRole('button', { name: '选择历史日期' });
     expect(picker).toBeInTheDocument();
-    expect(picker).toHaveTextContent('单日');
+    expect(picker).toHaveTextContent(yesterdayParam());
   });
 
   /** Last request body the spy was invoked with. */
@@ -101,14 +101,22 @@ describe('FunnelPage — fixed funnel + period selection', () => {
     return lastReq()?.period;
   }
 
-  it('auto-queries with default 单日 (latest available day) on first render', async () => {
+  /** Yesterday as "yyyy-MM-dd" (the picker default + request snapshot_dt). */
+  function yesterdayParam(): string {
+    const d = new Date();
+    d.setDate(d.getDate() - 1);
+    const y = d.getFullYear();
+    const m = String(d.getMonth() + 1).padStart(2, '0');
+    const day = String(d.getDate()).padStart(2, '0');
+    return `${y}-${m}-${day}`;
+  }
+
+  it('auto-queries with default 单日 (day = yesterday) on first render', async () => {
     queryFunnelMock.mockResolvedValue(readyResponse());
     renderPage();
 
-    // Data is T+1 and may lag, so the default omits snapshot_dt and the server
-    // returns MAX(dt) — no hard-coded yesterday that could be "数据缺失".
     await waitFor(() => expect(lastPeriod()).toBe('day'));
-    expect(lastReq()).toEqual({ period: 'day' });
+    expect(lastReq()).toEqual({ period: 'day', snapshot_dt: yesterdayParam() });
     expect(queryFunnelMock).toHaveBeenCalledTimes(1);
   });
 
@@ -180,44 +188,26 @@ describe('FunnelPage — fixed funnel + period selection', () => {
     await waitFor(() => expect(lastPeriod()).toBe('day'));
   });
 
-  it('单日 segment defaults to 最新 (latest available day)', async () => {
+  it('单日 segment defaults to yesterday', async () => {
     queryFunnelMock.mockResolvedValue(readyResponse());
     renderPage();
 
     await waitFor(() => expect(lastPeriod()).toBe('day'));
     const picker = screen.getByRole('button', { name: '选择历史日期' });
     expect(picker).toBeInTheDocument();
-    expect(picker).toHaveTextContent('最新');
+    expect(picker).toHaveTextContent(yesterdayParam());
   });
 
-  it('default day omits snapshot_dt; a picked date sends it, then 最新 clears it', async () => {
+  it('default day sends snapshot_dt = yesterday; rolling periods omit it', async () => {
     queryFunnelMock.mockResolvedValue(readyResponse());
     const user = userEvent.setup();
     renderPage();
 
-    // default 单日 (latest) omits snapshot_dt
+    // default 单日 sends snapshot_dt = yesterday
     await waitFor(() => expect(lastPeriod()).toBe('day'));
-    expect(lastReq()).toEqual({ period: 'day' });
-
-    // picking a historical date sends that snapshot_dt
-    await pickFirstOfMonth(user, await openCalendar(user));
-    await waitFor(() => {
-      const req = lastReq();
-      expect(req?.period).toBe('day');
-      expect(req?.snapshot_dt).toMatch(/^\d{4}-\d{2}-\d{2}$/);
-    });
-
-    // 最新可用日 quick-pick clears snapshot_dt again
-    await user.click(screen.getByRole('button', { name: '选择历史日期' }));
-    await user.click(await screen.findByRole('button', { name: '最新可用日' }));
-    await waitFor(() => expect(lastReq()).toEqual({ period: 'day' }));
-  });
-
-  it('rolling periods omit snapshot_dt', async () => {
-    queryFunnelMock.mockResolvedValue(readyResponse());
-    const user = userEvent.setup();
-    renderPage();
+    expect(lastReq()).toEqual({ period: 'day', snapshot_dt: yesterdayParam() });
 
+    // rolling periods omit snapshot_dt
     await user.click(screen.getByRole('button', { name: '近 7 天' }));
     await waitFor(() => expect(lastPeriod()).toBe('last_7d'));
     expect(lastReq()).toEqual({ period: 'last_7d' });

+ 9 - 35
apps/web/src/modules/funnel/components/TimePeriodSelect.tsx

@@ -1,4 +1,3 @@
-import { useState } from 'react';
 import { CalendarIcon } from 'lucide-react';
 import type { FunnelPeriod } from '../../../api/types';
 import { toSnapshotParam, yesterday } from '../period';
@@ -12,12 +11,10 @@ import { cn } from '@/lib/utils';
 
 interface Props {
   period: FunnelPeriod;
-  /** Selected day for `period: 'day'`; `null` = latest available day. */
-  snapshotDt: Date | null;
+  /** Selected day for `period: 'day'` (defaults to yesterday). */
+  snapshotDt: Date;
   /** Pick a historical day → switch to single-day mode. */
   onPickDate: (d: Date) => void;
-  /** Reset single-day back to the latest available day. */
-  onPickLatest: () => void;
   /** Pick a rolling window. */
   onPickRolling: (p: 'last_7d' | 'last_30d') => void;
 }
@@ -37,16 +34,14 @@ export function TimePeriodSelect({
   period,
   snapshotDt,
   onPickDate,
-  onPickLatest,
   onPickRolling,
 }: Props) {
   const max = yesterday();
-  const [open, setOpen] = useState(false);
 
   return (
     <div className="inline-flex items-center gap-1 rounded-lg border border-border bg-muted/60 p-1">
-      {/* 单日 —— 日历 popover;未选具体日 = 最新可用日 */}
-      <Popover open={open} onOpenChange={setOpen}>
+      {/* 单日 —— 日历 popover(默认昨日,可选历史日) */}
+      <Popover>
         <PopoverTrigger asChild>
           <button
             type="button"
@@ -55,39 +50,18 @@ export function TimePeriodSelect({
             className={cn(SEG, period === 'day' ? ON : OFF)}
           >
             <CalendarIcon className="size-4" />
-            <span>单日</span>
-            <span className="tabular-nums opacity-90">
-              {snapshotDt ? toSnapshotParam(snapshotDt) : '最新'}
-            </span>
+            <span className="tabular-nums">{toSnapshotParam(snapshotDt)}</span>
           </button>
         </PopoverTrigger>
         <PopoverContent className="w-auto p-0" align="start">
-          {/* 最新可用日快捷项:回到默认(后端取 MAX(dt)) */}
-          <button
-            type="button"
-            onClick={() => {
-              onPickLatest();
-              setOpen(false);
-            }}
-            className={cn(
-              'w-full px-3 py-2 text-left text-sm border-b border-border transition-colors',
-              !snapshotDt
-                ? 'text-primary font-medium'
-                : 'text-foreground hover:bg-muted',
-            )}
-          >
-            最新可用日
-          </button>
           <Calendar
             mode="single"
-            selected={snapshotDt ?? undefined}
-            defaultMonth={snapshotDt ?? max}
+            selected={snapshotDt}
+            defaultMonth={snapshotDt}
             disabled={{ after: max }}
+            required
             onSelect={(d) => {
-              if (d) {
-                onPickDate(d);
-                setOpen(false);
-              }
+              if (d) onPickDate(d);
             }}
           />
         </PopoverContent>