1111 * 不做 SWR 集成:交互简单(只关心自己的点击),直接 useState + fetch 更直白
1212 */
1313
14- import { useState } from "react" ;
14+ import { useEffect , useRef , useState } from "react" ;
1515import { useRouter } from "next/navigation" ;
1616import { useAuth } from "@/lib/use-auth" ;
1717
@@ -37,6 +37,24 @@ export function InterestButton({
3737 const [ count , setCount ] = useState ( initialCount ) ;
3838 const [ interested , setInterested ] = useState ( initialInterested ) ;
3939 const [ loading , setLoading ] = useState ( false ) ;
40+ // 失败提示(issue #302 P1-1):之前 catch 静默吞错,乐观 UI 回滚后用户
41+ // 看到数字"动了一下又回去"以为按钮坏了。短暂展示一行红字 3 秒消失,
42+ // 与 SettingsForm 的 toast 思路一致但内联化(按钮位置紧凑,不强插全局 toast)
43+ const [ errorMsg , setErrorMsg ] = useState < string | null > ( null ) ;
44+ const errorTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
45+
46+ // 卸载时清掉 timer,避免对已 unmount 的组件 setState 报警
47+ useEffect ( ( ) => {
48+ return ( ) => {
49+ if ( errorTimerRef . current ) clearTimeout ( errorTimerRef . current ) ;
50+ } ;
51+ } , [ ] ) ;
52+
53+ function flashError ( msg : string ) {
54+ if ( errorTimerRef . current ) clearTimeout ( errorTimerRef . current ) ;
55+ setErrorMsg ( msg ) ;
56+ errorTimerRef . current = setTimeout ( ( ) => setErrorMsg ( null ) , 3000 ) ;
57+ }
4058
4159 if ( status === "unauthenticated" ) {
4260 return (
@@ -74,28 +92,40 @@ export function InterestButton({
7492 // 用后端返回的权威值覆盖乐观值,避免竞争
7593 setCount ( json . data . count ) ;
7694 setInterested ( json . data . interested ) ;
77- } catch {
78- // 回滚
95+ } catch ( err ) {
96+ // 回滚 + 显式 toast,避免静默吞错(issue #302 P1-1)
7997 setInterested ( prevInterested ) ;
8098 setCount ( prevCount ) ;
99+ flashError ( err instanceof Error ? err . message : "操作失败,请重试" ) ;
81100 } finally {
82101 setLoading ( false ) ;
83102 }
84103 } ;
85104
86105 return (
87- < button
88- type = "button"
89- disabled = { loading }
90- onClick = { toggle }
91- className = { `font-mono text-xs uppercase tracking-widest px-4 py-2 border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
92- interested
93- ? "border-[#CC0000] bg-[#CC0000] text-white hover:bg-transparent hover:text-[#CC0000]"
94- : "border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)]"
95- } `}
96- >
97- { interested ? "已标记 · " : "感兴趣 · " }
98- { count }
99- </ button >
106+ < div className = "flex items-center gap-3" >
107+ < button
108+ type = "button"
109+ disabled = { loading }
110+ onClick = { toggle }
111+ className = { `font-mono text-xs uppercase tracking-widest px-4 py-2 border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
112+ interested
113+ ? "border-[#CC0000] bg-[#CC0000] text-white hover:bg-transparent hover:text-[#CC0000]"
114+ : "border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)]"
115+ } `}
116+ >
117+ { interested ? "已标记 · " : "感兴趣 · " }
118+ { count }
119+ </ button >
120+ { errorMsg && (
121+ < span
122+ role = "alert"
123+ aria-live = "polite"
124+ className = "font-mono text-xs text-red-600 dark:text-red-400"
125+ >
126+ { errorMsg }
127+ </ span >
128+ ) }
129+ </ div >
100130 ) ;
101131}
0 commit comments