From a427aa1c4ab8ad9d983dfe1a2caaa0dc468c042d Mon Sep 17 00:00:00 2001 From: XingfenD Date: Tue, 15 Sep 2026 11:39:21 +0800 Subject: [PATCH] fix(frontend): resolve all eslint errors to zero (B1) --- frontend/eslint.config.js | 3 +++ frontend/src/auth/AuthContext.tsx | 1 + frontend/src/components/Cover.tsx | 8 +++++++- frontend/src/pages/Login.tsx | 1 + frontend/src/pages/Shelf.tsx | 9 ++++++++- frontend/src/readers/CbzReader.tsx | 11 +++++++++-- frontend/src/readers/EpubReader.tsx | 11 +++++++++-- frontend/src/readers/PdfReader.tsx | 9 ++++++++- frontend/src/readers/TextReader.tsx | 12 ++++++++++-- 9 files changed, 56 insertions(+), 9 deletions(-) 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" 是官方推荐的补偿写法,非冗余 */}
    {continueRows.map((r, i) => { const b = booksByKey.get(`${r.library_id}/${r.path}`); diff --git a/frontend/src/readers/CbzReader.tsx b/frontend/src/readers/CbzReader.tsx index 1a786a5..34d701b 100644 --- a/frontend/src/readers/CbzReader.tsx +++ b/frontend/src/readers/CbzReader.tsx @@ -38,10 +38,17 @@ function PageImg({ url, width, onLoaded }: { url: string; width: number; onLoade const [src, setSrc] = useState(""); const [failed, setFailed] = useState(false); const [nonce, setNonce] = useState(0); - useEffect(() => { - let dead = false; + // url/nonce 变化时先落回占位态:React 官方推荐的「渲染期按下发 key 重置本地 state」, + // 替代原先 effect 内的同步 setState(级联渲染);换页/重试时的表现与原先一致 + const key = url + "#" + nonce; + const [prevKey, setPrevKey] = useState(key); + if (prevKey !== key) { + setPrevKey(key); setSrc(""); setFailed(false); + } + useEffect(() => { + let dead = false; fetchObjectUrl(url) .then((s) => !dead && setSrc(s)) .catch(() => !dead && setFailed(true)); diff --git a/frontend/src/readers/EpubReader.tsx b/frontend/src/readers/EpubReader.tsx index 530388f..122b504 100644 --- a/frontend/src/readers/EpubReader.tsx +++ b/frontend/src/readers/EpubReader.tsx @@ -38,12 +38,19 @@ export default function EpubReader({ book, initialLocator, initialPercent }: Rea const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); const [ready, setReady] = useState(false); + // 重新加载(换书/重试)时先清错误与就绪态:React 官方推荐的「渲染期按下发 key 重置本地 state」, + // 替代原先 effect 内的同步 setState(级联渲染);首挂载 key 相等,重置为空操作,行为等价 + const loadKey = (book.file_url ?? "") + "#" + nonce; + const [prevLoadKey, setPrevLoadKey] = useState(loadKey); + if (prevLoadKey !== loadKey) { + setPrevLoadKey(loadKey); + setErr(""); + setReady(false); + } useEffect(() => { let dead = false; let bookObj: EpubBookLike | null = null; - setErr(""); - setReady(false); (async () => { const ePub = await loadEpub(); const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer()); diff --git a/frontend/src/readers/PdfReader.tsx b/frontend/src/readers/PdfReader.tsx index f8b9cdf..e1f23a7 100644 --- a/frontend/src/readers/PdfReader.tsx +++ b/frontend/src/readers/PdfReader.tsx @@ -28,10 +28,17 @@ export default function PdfReader({ book, initialLocator, initialPercent }: Read const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); // 仅加载重试 const [redraw, setRedraw] = useState(0); // 仅 resize 重绘(不重新下载文档) + // 重新加载(换书/重试)时先清错误:React 官方推荐的「渲染期按下发 key 重置本地 state」, + // 替代原先 effect 内的同步 setState(级联渲染);首挂载 key 相等,重置为空操作,行为等价 + const loadKey = (book.file_url ?? "") + "#" + nonce; + const [prevLoadKey, setPrevLoadKey] = useState(loadKey); + if (prevLoadKey !== loadKey) { + setPrevLoadKey(loadKey); + setErr(""); + } useEffect(() => { let dead = false; - setErr(""); (async () => { const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer()); if (dead) return; diff --git a/frontend/src/readers/TextReader.tsx b/frontend/src/readers/TextReader.tsx index 249b09e..11047bc 100644 --- a/frontend/src/readers/TextReader.tsx +++ b/frontend/src/readers/TextReader.tsx @@ -19,10 +19,17 @@ function useFileText(url: string) { const [text, setText] = useState(null); const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); - useEffect(() => { - let dead = false; + // url/nonce 变化时先落回加载态:React 官方推荐的「渲染期按下发 key 重置本地 state」, + // 替代原先 effect 内的同步 setState(级联渲染);重试时的表现与原先一致 + const key = url + "#" + nonce; + const [prevKey, setPrevKey] = useState(key); + if (prevKey !== key) { + setPrevKey(key); setText(null); setErr(""); + } + useEffect(() => { + let dead = false; apiRaw(url) .then((r) => r.arrayBuffer()) .then((b) => !dead && setText(decodeText(b))) @@ -238,6 +245,7 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps) const rawCh = Number(initialLocator?.ch); if (Number.isFinite(rawCh) && rawCh >= 0 && rawCh < chapters.length) { targetCi.current = Math.floor(rawCh); + // eslint-disable-next-line react-hooks/set-state-in-effect -- 首章渲染后按缓存 locator 落位阅读进度;ci 与下方滚动定位 effect/refs 协作,改渲染期重置属高风险重构,保守保留 setCi(Math.floor(rawCh)); if (hasF) restoreFrac.current = f; } else if (hasF) {