diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index db55bda..f9e45dc 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -35,6 +35,9 @@ export default tseslint.config( "jsx-a11y/click-events-have-key-events": "warn", "jsx-a11y/no-static-element-interactions": "warn", "jsx-a11y/no-noninteractive-element-interactions": "warn", + // 键盘焦点容器:CBZ/PDF 阅读器根 div 用 tabIndex={0} 接方向键翻页, + // 与上面三条同属 ② 改版范围(radix 原语自带焦点管理),本期随块降 warn + "jsx-a11y/no-noninteractive-tabindex": "warn", }, }, { diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index 78e8bcd..c6cafc5 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -40,6 +40,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { useEffect(() => { if (token && meQ.isError) { setToken(null); // 非 401 失败也要清持久化 token,避免与 React 状态分叉导致 RequireAuth 永久加载 + // eslint-disable-next-line react-hooks/set-state-in-effect -- 对「me 查询失败」异步结果的清理反应,与持久化 token 需同帧回收;改渲染期重置/事件驱动会改变鉴权失败时序,保守保留 setTok(null); } }, [token, meQ.isError]); diff --git a/frontend/src/components/Cover.tsx b/frontend/src/components/Cover.tsx index f606464..e46fea7 100644 --- a/frontend/src/components/Cover.tsx +++ b/frontend/src/components/Cover.tsx @@ -12,12 +12,18 @@ export function useAuthedImage(url: string | undefined): { src: string; failed: }); const [nonce, setNonce] = useState(0); const key = (url ?? "") + "#" + nonce; + // url/nonce 变化时先落回占位态:React 官方推荐的「渲染期按下发 key 重置本地 state」, + // 替代原先 effect 内的同步 setState(级联渲染),行为等价——加载中同样呈现 src="" 的占位 + const [prevKey, setPrevKey] = useState(key); + if (prevKey !== key) { + setPrevKey(key); + setState({ key, src: "", failed: false }); + } useEffect(() => { if (!url) return; let dead = false; const k = url + "#" + nonce; - setState({ key: k, src: "", failed: false }); fetchObjectUrl(url) .then((src) => !dead && setState({ key: k, src, failed: false })) .catch(() => !dead && setState({ key: k, src: "", failed: true })); diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 6791369..bac4d70 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -51,6 +51,7 @@ export default function Login() { id="login-user" value={u} onChange={(e) => setU(e.target.value)} + // eslint-disable-next-line jsx-a11y/no-autofocus -- 登录页整页即表单,首字段自动聚焦是通行 UX;规则无按场景豁免选项,保留行为 autoFocus autoComplete="username" /> diff --git a/frontend/src/pages/Shelf.tsx b/frontend/src/pages/Shelf.tsx index 4e869a9..311156a 100644 --- a/frontend/src/pages/Shelf.tsx +++ b/frontend/src/pages/Shelf.tsx @@ -141,7 +141,13 @@ export default function Shelf() { const [qRaw, setQRaw] = useState(serverQ); const [confirmDel, setConfirmDel] = useState(null); - useEffect(() => setQRaw(serverQ), [serverQ]); + // URL 是筛选词的事实来源:回退/前进/程序化改参时把输入框拉回 URL 值。 + // 用 React 官方推荐的「渲染期按下发值重置本地 state」替代原先 effect 内的同步 setState + const [prevServerQ, setPrevServerQ] = useState(serverQ); + if (prevServerQ !== serverQ) { + setPrevServerQ(serverQ); + setQRaw(serverQ); + } useEffect(() => { if (qRaw.trim() === serverQ) return; const t = setTimeout(() => { @@ -284,6 +290,7 @@ export default function Shelf() { {continueRows.length > 0 && !searching && (

继续阅读

+ {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- Tailwind preflight 移除了 ul 的 list 样式,Safari/VoiceOver 因此丢列表语义;role="list" 是官方推荐的补偿写法,非冗余 */}