瀏覽代碼

code-review 修复:范围文案 pending 守卫 + period.ts 清理 + serve.py 回退收窄 + look.sh 推当前分支

tianyu.chu 1 月之前
父節點
當前提交
a57f731d46

+ 3 - 2
apps/web/src/modules/funnel/FunnelPage.tsx

@@ -69,8 +69,9 @@ export function FunnelPage() {
             }}
             onPickRolling={(p) => setPeriod(p)}
           />
-          {/* 滚动周期显示日期范围;单日不显示(日期已在按钮上) */}
-          {mutation.data?.snapshot_dt && period !== 'day' && (
+          {/* 滚动周期显示日期范围;单日不显示。查询 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>

+ 4 - 2
apps/web/src/modules/funnel/__tests__/FunnelPage.test.tsx

@@ -143,10 +143,12 @@ describe('FunnelPage — fixed funnel + period selection', () => {
     expect(screen.queryByText(/不含今日/)).not.toBeInTheDocument();
     expect(screen.queryByText(/~/)).not.toBeInTheDocument();
 
-    // 近 7 天 → 显示日期范围,且无"不含今日"
+    // 近 7 天 → 加载完显示日期范围(pending 期间不渲染),且无"不含今日"
     await user.click(screen.getByRole('button', { name: '近 7 天' }));
     await waitFor(() => expect(lastPeriod()).toBe('last_7d'));
-    expect(screen.getByText(/2026-06-17 ~ 2026-06-23/)).toBeInTheDocument();
+    expect(
+      await screen.findByText(/2026-06-17 ~ 2026-06-23/),
+    ).toBeInTheDocument();
     expect(screen.queryByText(/不含今日/)).not.toBeInTheDocument();
   });
 

+ 3 - 5
apps/web/src/modules/funnel/period.ts

@@ -29,23 +29,21 @@ function parseSnapshot(dt: string): Date {
 }
 
 /**
- * 时间边界文案(精简)—— 只给日期,周期标签已在按钮上(`snapshot_dt` 为止/快照日,yyyyMMdd):
- *   单日   → 「2026-06-24」
+ * 滚动周期的时间范围文案(`snapshot_dt` 为止/快照日,yyyyMMdd):
  *   近7天  → 「2026-06-18 ~ 2026-06-24」
  *   近30天 → 「2026-05-26 ~ 2026-06-24」
+ * 仅供 last_7d / last_30d 使用(单日不展示文案,调用处已守卫)。
  */
 export function funnelRangeText(
   period: FunnelPeriod,
   snapshotDt: string,
 ): string {
   if (!/^\d{8}$/.test(snapshotDt)) return formatSnapshotDt(snapshotDt);
-  if (period === 'day') return formatSnapshotDt(snapshotDt);
   const end = parseSnapshot(snapshotDt);
   const span = period === 'last_7d' ? 7 : 30;
   const start = new Date(end);
   start.setDate(start.getDate() - (span - 1));
-  const startStr = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}-${String(start.getDate()).padStart(2, '0')}`;
-  return `${startStr} ~ ${formatSnapshotDt(snapshotDt)}`;
+  return `${toSnapshotParam(start)} ~ ${formatSnapshotDt(snapshotDt)}`;
 }
 
 /** Local-midnight `Date` for yesterday — the latest selectable / queryable day (data is T+1). */

+ 4 - 2
infra/look.sh

@@ -13,10 +13,12 @@ echo "==> 1/5 验证 + 构建"
 corepack pnpm@10 --filter @hs-data/web test
 corepack pnpm@10 --filter @hs-data/web build
 
-echo "==> 2/5 源码 → feature"
+echo "==> 2/5 源码 → 当前分支"
+BR="$(git branch --show-current)"
+[ "$BR" = "feature" ] || echo "  ⚠️ 当前分支是 '$BR'(预期 feature),仍推到它以保持源码/部署同步"
 git add -A
 git diff --cached --quiet || git commit -q -m "$MSG"
-git push -q origin feature
+git push -q origin "$BR"
 
 echo "==> 3/5 产物 → deploy 分支(worktree 快进推送,无 force)"
 git fetch -q origin deploy

+ 4 - 3
infra/serve-start.sh

@@ -1,6 +1,7 @@
 #!/bin/bash
-# 服务器静态服务守护启动器:没在跑就拉起(cron 每分钟调 + @reboot)。
-# 托管 ~/hs-data(deploy 分支 clone)的静态产物,端口 6666,用户空间、不动系统。
+# 服务器静态服务守护启动器(可选,cron 未默认启用)。没在跑就拉起。
+# 注:服务器上的脚本名为 ~/serve-hs.py(由仓库 infra/serve.py 拷贝部署而来,
+#     见 README 部署说明);端口 8080;托管 ~/hs-data(deploy 分支 clone)。用户空间、不动系统。
 pgrep -f "serve-hs.py" >/dev/null 2>&1 || \
-  setsid python3 -u "$HOME/serve-hs.py" "$HOME/hs-data" 6666 \
+  setsid python3 -u "$HOME/serve-hs.py" "$HOME/hs-data" 8080 \
     >"$HOME/hs-web.log" 2>&1 </dev/null &

+ 9 - 5
infra/serve.py

@@ -21,16 +21,20 @@ os.chdir(ROOT)
 
 
 class SpaHandler(SimpleHTTPRequestHandler):
-    def do_GET(self):
-        # 路径不是真实文件 → 回退到 index.html(SPA 客户端路由)。
+    # 覆盖 send_head(GET 与 HEAD 都走它),统一做 SPA 回退。
+    def send_head(self):
         rel = self.path.split("?", 1)[0].split("#", 1)[0].lstrip("/")
         if rel and not os.path.isfile(os.path.join(ROOT, rel)):
-            self.path = "/index.html"
-        return SimpleHTTPRequestHandler.do_GET(self)
+            last = rel.rsplit("/", 1)[-1]
+            # 仅"看起来像路由"的路径(末段无扩展名)回退到 index.html;
+            # 缺失的资源文件(如 /assets/x.js)让其正常 404,避免返回 HTML 触发 MIME 错误。
+            if "." not in last:
+                self.path = "/index.html"
+        return SimpleHTTPRequestHandler.send_head(self)
 
     def end_headers(self):
         # index.html 不缓存(构建产物自带 hash,可长缓存)。
-        if self.path in ("/", "/index.html"):
+        if self.path.split("?", 1)[0] in ("/", "/index.html"):
             self.send_header("Cache-Control", "no-cache")
         SimpleHTTPRequestHandler.end_headers(self)