File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 33import { usePathname } from "next/navigation" ;
44import { useEffect } from "react" ;
55
6+ /**
7+ * 文档页面访问埋点组件。
8+ *
9+ * 放在 app/docs/layout.tsx 下,pathname 变化时向自家 /api/analytics 上报一次 page_view,
10+ * 供将来基于 AnalyticsEvent 表做文档热度分析(当前 A-2 功能的热榜是用 GA4 数据,此处并行积累自家数据)。
11+ *
12+ * 去重策略:同一浏览器会话内同一 path 只报一次(sessionStorage key = "pv_reported:<path>")。
13+ * 为什么用 sessionStorage 不用 localStorage:关闭标签页后应当算新会话,否则长期复访的用户会被严重低估。
14+ *
15+ * 无返回 UI(return null),仅作副作用组件使用。
16+ */
617export function DocsPageViewTracker ( ) {
718 const pathname = usePathname ( ) ;
819
920 useEffect ( ( ) => {
1021 if ( ! pathname ) return ;
1122
23+ // 同会话同 path 已上报则跳过,避免刷新/快速切换重复计数
1224 const key = `pv_reported:${ pathname } ` ;
1325 if ( sessionStorage . getItem ( key ) ) return ;
1426
1527 sessionStorage . setItem ( key , "1" ) ;
1628
29+ // 如果用户登录了,带上 Sa-Token 让后端能把事件关联到 userId;匿名用户后端会写入 userId=null
1730 const token =
1831 typeof window !== "undefined" ? localStorage . getItem ( "satoken" ) : null ;
1932 const headers : Record < string , string > = {
2033 "Content-Type" : "application/json" ,
2134 } ;
2235 if ( token ) headers [ "x-satoken" ] = token ;
2336
37+ // 埋点失败静默吞掉:不能因为分析接口挂了影响文档页的正常阅读体验
2438 fetch ( "/api/analytics" , {
2539 method : "POST" ,
2640 headers,
Original file line number Diff line number Diff line change @@ -7,11 +7,25 @@ type Tab = "contributors" | "hot";
77type Window = "7d" | "30d" | "all" ;
88
99interface RankTabsProps {
10+ /** Contributors tab 的静态内容,由 /rank/page.tsx SSR 渲染后以 children 传入 */
1011 children : React . ReactNode ;
12+ /** SSR 决定的初始 tab,来自 URL query ?tab=;客户端挂载后以 searchParams 为准 */
1113 initialTab : Tab ;
14+ /** SSR 决定的初始窗口,Hot Docs tab 用 */
1215 initialWindow : Window ;
1316}
1417
18+ /**
19+ * /rank 页的 Tab 壳子:Contributors(贡献者榜,静态 JSON)/ Hot Docs(热门文档榜,后端 API)。
20+ *
21+ * Tab 和窗口状态都写进 URL query(?tab=&window=),而不是组件内 state,这样:
22+ * 1. 分享链接能直接定位到具体视图
23+ * 2. 浏览器前进/后退正常切换
24+ * 3. 刷新不丢状态
25+ *
26+ * 用 router.push 而非 replaceState 是为了让返回键能回到上一个 tab;窗口切换在 HotDocsTab 内部用
27+ * replaceState,避免每切一次就污染历史栈。
28+ */
1529export function RankTabs ( {
1630 children,
1731 initialTab,
@@ -25,6 +39,7 @@ export function RankTabs({
2539 const switchTab = ( tab : Tab ) => {
2640 const params = new URLSearchParams ( searchParams . toString ( ) ) ;
2741 params . set ( "tab" , tab ) ;
42+ // 首次切到 Hot Docs 还没选过窗口时默认 30d,避免 HotDocsTab 拿到 undefined
2843 if ( tab === "hot" && ! params . get ( "window" ) ) {
2944 params . set ( "window" , "30d" ) ;
3045 }
You can’t perform that action at this time.
0 commit comments