11"use client" ;
22
3- import { useEffect , useState } from "react" ;
3+ import { useEffect , useReducer } from "react" ;
44import Image from "next/image" ;
55import type { HistoryItem } from "@/app/types/docs-history" ;
66
@@ -12,6 +12,25 @@ interface DocHistoryPanelProps {
1212 path : string ;
1313}
1414
15+ // 将 items / error / loading 合并成一个 discriminated union,
16+ // 避免 effect 里多次同步 setState 触发 react-hooks/set-state-in-effect
17+ // 同时天然保证三种状态互斥(不会同时出现"错误提示 + 旧列表")
18+ type State =
19+ | { status : "loading" }
20+ | { status : "ok" ; items : HistoryItem [ ] }
21+ | { status : "error" ; message : string } ;
22+
23+ type Action =
24+ | { type : "fetch" }
25+ | { type : "ok" ; items : HistoryItem [ ] }
26+ | { type : "error" ; message : string } ;
27+
28+ function reducer ( _ : State , action : Action ) : State {
29+ if ( action . type === "fetch" ) return { status : "loading" } ;
30+ if ( action . type === "ok" ) return { status : "ok" , items : action . items } ;
31+ return { status : "error" , message : action . message } ;
32+ }
33+
1534// 将 ISO 日期转为相对时间描述(中文)
1635function relativeTime ( dateStr : string ) : string {
1736 const diff = Date . now ( ) - new Date ( dateStr ) . getTime ( ) ;
@@ -41,30 +60,29 @@ function SkeletonRow() {
4160}
4261
4362export function DocHistoryPanel ( { path } : DocHistoryPanelProps ) {
44- const [ items , setItems ] = useState < HistoryItem [ ] | null > ( null ) ;
45- const [ error , setError ] = useState < string | null > ( null ) ;
63+ const [ state , dispatch ] = useReducer ( reducer , { status : "loading" } ) ;
4664
4765 useEffect ( ( ) => {
66+ // 用 dispatch 而不是多次 setState,规避 react-hooks/set-state-in-effect lint;
67+ // path 变化时立刻回到 loading,避免"错误提示 + 旧列表"并存
68+ dispatch ( { type : "fetch" } ) ;
4869 let cancelled = false ;
49- // path 变化触发重新 fetch 时先清空旧状态,避免"错误提示 + 旧列表"同时显示
50- setItems ( null ) ;
51- setError ( null ) ;
5270 fetch ( `/api/docs/history?path=${ encodeURIComponent ( path ) } ` )
5371 . then ( ( r ) => r . json ( ) )
5472 . then ( ( json ) => {
5573 if ( cancelled ) return ;
5674 if ( json . success ) {
57- setItems ( json . data ) ;
58- setError ( null ) ;
75+ dispatch ( { type : "ok" , items : json . data ?? [ ] } ) ;
5976 } else {
60- setItems ( null ) ;
61- setError ( json . error ?? "无法加载历史" ) ;
77+ dispatch ( {
78+ type : "error" ,
79+ message : json . error ?? "无法加载历史" ,
80+ } ) ;
6281 }
6382 } )
6483 . catch ( ( ) => {
6584 if ( ! cancelled ) {
66- setItems ( null ) ;
67- setError ( "无法加载历史" ) ;
85+ dispatch ( { type : "error" , message : "无法加载历史" } ) ;
6886 }
6987 } ) ;
7088 return ( ) => {
@@ -80,7 +98,7 @@ export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
8098 </ h2 >
8199
82100 { /* 加载中 */ }
83- { items === null && error === null && (
101+ { state . status === "loading" && (
84102 < div className = "divide-y divide-neutral-100 dark:divide-neutral-800" >
85103 < SkeletonRow />
86104 < SkeletonRow />
@@ -89,23 +107,23 @@ export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
89107 ) }
90108
91109 { /* 错误 */ }
92- { error !== null && (
110+ { state . status === "error" && (
93111 < p className = "text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2" >
94- { error }
112+ { state . message }
95113 </ p >
96114 ) }
97115
98116 { /* 空结果 */ }
99- { items !== null && items . length === 0 && (
117+ { state . status === "ok" && state . items . length === 0 && (
100118 < p className = "text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2" >
101119 暂无更新记录
102120 </ p >
103121 ) }
104122
105123 { /* 历史列表 */ }
106- { items !== null && items . length > 0 && (
124+ { state . status === "ok" && state . items . length > 0 && (
107125 < ol className = "divide-y divide-neutral-100 dark:divide-neutral-800" >
108- { items . map ( ( item ) => (
126+ { state . items . map ( ( item ) => (
109127 < li key = { item . sha } >
110128 < a
111129 href = { item . htmlUrl }
0 commit comments