fix(frontend): resolve all eslint errors to zero (B1)
This commit is contained in:
@@ -35,6 +35,9 @@ export default tseslint.config(
|
|||||||
"jsx-a11y/click-events-have-key-events": "warn",
|
"jsx-a11y/click-events-have-key-events": "warn",
|
||||||
"jsx-a11y/no-static-element-interactions": "warn",
|
"jsx-a11y/no-static-element-interactions": "warn",
|
||||||
"jsx-a11y/no-noninteractive-element-interactions": "warn",
|
"jsx-a11y/no-noninteractive-element-interactions": "warn",
|
||||||
|
// 键盘焦点容器:CBZ/PDF 阅读器根 div 用 tabIndex={0} 接方向键翻页,
|
||||||
|
// 与上面三条同属 ② 改版范围(radix 原语自带焦点管理),本期随块降 warn
|
||||||
|
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token && meQ.isError) {
|
if (token && meQ.isError) {
|
||||||
setToken(null); // 非 401 失败也要清持久化 token,避免与 React 状态分叉导致 RequireAuth 永久加载
|
setToken(null); // 非 401 失败也要清持久化 token,避免与 React 状态分叉导致 RequireAuth 永久加载
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- 对「me 查询失败」异步结果的清理反应,与持久化 token 需同帧回收;改渲染期重置/事件驱动会改变鉴权失败时序,保守保留
|
||||||
setTok(null);
|
setTok(null);
|
||||||
}
|
}
|
||||||
}, [token, meQ.isError]);
|
}, [token, meQ.isError]);
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ export function useAuthedImage(url: string | undefined): { src: string; failed:
|
|||||||
});
|
});
|
||||||
const [nonce, setNonce] = useState(0);
|
const [nonce, setNonce] = useState(0);
|
||||||
const key = (url ?? "") + "#" + nonce;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
let dead = false;
|
let dead = false;
|
||||||
const k = url + "#" + nonce;
|
const k = url + "#" + nonce;
|
||||||
setState({ key: k, src: "", failed: false });
|
|
||||||
fetchObjectUrl(url)
|
fetchObjectUrl(url)
|
||||||
.then((src) => !dead && setState({ key: k, src, failed: false }))
|
.then((src) => !dead && setState({ key: k, src, failed: false }))
|
||||||
.catch(() => !dead && setState({ key: k, src: "", failed: true }));
|
.catch(() => !dead && setState({ key: k, src: "", failed: true }));
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function Login() {
|
|||||||
id="login-user"
|
id="login-user"
|
||||||
value={u}
|
value={u}
|
||||||
onChange={(e) => setU(e.target.value)}
|
onChange={(e) => setU(e.target.value)}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus -- 登录页整页即表单,首字段自动聚焦是通行 UX;规则无按场景豁免选项,保留行为
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -141,7 +141,13 @@ export default function Shelf() {
|
|||||||
const [qRaw, setQRaw] = useState(serverQ);
|
const [qRaw, setQRaw] = useState(serverQ);
|
||||||
const [confirmDel, setConfirmDel] = useState<Book | null>(null);
|
const [confirmDel, setConfirmDel] = useState<Book | null>(null);
|
||||||
|
|
||||||
useEffect(() => setQRaw(serverQ), [serverQ]);
|
// URL 是筛选词的事实来源:回退/前进/程序化改参时把输入框拉回 URL 值。
|
||||||
|
// 用 React 官方推荐的「渲染期按下发值重置本地 state」替代原先 effect 内的同步 setState
|
||||||
|
const [prevServerQ, setPrevServerQ] = useState(serverQ);
|
||||||
|
if (prevServerQ !== serverQ) {
|
||||||
|
setPrevServerQ(serverQ);
|
||||||
|
setQRaw(serverQ);
|
||||||
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (qRaw.trim() === serverQ) return;
|
if (qRaw.trim() === serverQ) return;
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
@@ -284,6 +290,7 @@ export default function Shelf() {
|
|||||||
{continueRows.length > 0 && !searching && (
|
{continueRows.length > 0 && !searching && (
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="mb-3 text-sm font-medium text-muted-foreground">继续阅读</h2>
|
<h2 className="mb-3 text-sm font-medium text-muted-foreground">继续阅读</h2>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- Tailwind preflight 移除了 ul 的 list 样式,Safari/VoiceOver 因此丢列表语义;role="list" 是官方推荐的补偿写法,非冗余 */}
|
||||||
<ul className="flex gap-3 overflow-x-auto pb-2" role="list">
|
<ul className="flex gap-3 overflow-x-auto pb-2" role="list">
|
||||||
{continueRows.map((r, i) => {
|
{continueRows.map((r, i) => {
|
||||||
const b = booksByKey.get(`${r.library_id}/${r.path}`);
|
const b = booksByKey.get(`${r.library_id}/${r.path}`);
|
||||||
|
|||||||
@@ -38,10 +38,17 @@ function PageImg({ url, width, onLoaded }: { url: string; width: number; onLoade
|
|||||||
const [src, setSrc] = useState("");
|
const [src, setSrc] = useState("");
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
const [nonce, setNonce] = useState(0);
|
const [nonce, setNonce] = useState(0);
|
||||||
useEffect(() => {
|
// url/nonce 变化时先落回占位态:React 官方推荐的「渲染期按下发 key 重置本地 state」,
|
||||||
let dead = false;
|
// 替代原先 effect 内的同步 setState(级联渲染);换页/重试时的表现与原先一致
|
||||||
|
const key = url + "#" + nonce;
|
||||||
|
const [prevKey, setPrevKey] = useState(key);
|
||||||
|
if (prevKey !== key) {
|
||||||
|
setPrevKey(key);
|
||||||
setSrc("");
|
setSrc("");
|
||||||
setFailed(false);
|
setFailed(false);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
let dead = false;
|
||||||
fetchObjectUrl(url)
|
fetchObjectUrl(url)
|
||||||
.then((s) => !dead && setSrc(s))
|
.then((s) => !dead && setSrc(s))
|
||||||
.catch(() => !dead && setFailed(true));
|
.catch(() => !dead && setFailed(true));
|
||||||
|
|||||||
@@ -38,12 +38,19 @@ export default function EpubReader({ book, initialLocator, initialPercent }: Rea
|
|||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [nonce, setNonce] = useState(0);
|
const [nonce, setNonce] = useState(0);
|
||||||
const [ready, setReady] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
let dead = false;
|
let dead = false;
|
||||||
let bookObj: EpubBookLike | null = null;
|
let bookObj: EpubBookLike | null = null;
|
||||||
setErr("");
|
|
||||||
setReady(false);
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const ePub = await loadEpub();
|
const ePub = await loadEpub();
|
||||||
const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer());
|
const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer());
|
||||||
|
|||||||
@@ -28,10 +28,17 @@ export default function PdfReader({ book, initialLocator, initialPercent }: Read
|
|||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [nonce, setNonce] = useState(0); // 仅加载重试
|
const [nonce, setNonce] = useState(0); // 仅加载重试
|
||||||
const [redraw, setRedraw] = useState(0); // 仅 resize 重绘(不重新下载文档)
|
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(() => {
|
useEffect(() => {
|
||||||
let dead = false;
|
let dead = false;
|
||||||
setErr("");
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer());
|
const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer());
|
||||||
if (dead) return;
|
if (dead) return;
|
||||||
|
|||||||
@@ -19,10 +19,17 @@ function useFileText(url: string) {
|
|||||||
const [text, setText] = useState<string | null>(null);
|
const [text, setText] = useState<string | null>(null);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [nonce, setNonce] = useState(0);
|
const [nonce, setNonce] = useState(0);
|
||||||
useEffect(() => {
|
// url/nonce 变化时先落回加载态:React 官方推荐的「渲染期按下发 key 重置本地 state」,
|
||||||
let dead = false;
|
// 替代原先 effect 内的同步 setState(级联渲染);重试时的表现与原先一致
|
||||||
|
const key = url + "#" + nonce;
|
||||||
|
const [prevKey, setPrevKey] = useState(key);
|
||||||
|
if (prevKey !== key) {
|
||||||
|
setPrevKey(key);
|
||||||
setText(null);
|
setText(null);
|
||||||
setErr("");
|
setErr("");
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
let dead = false;
|
||||||
apiRaw(url)
|
apiRaw(url)
|
||||||
.then((r) => r.arrayBuffer())
|
.then((r) => r.arrayBuffer())
|
||||||
.then((b) => !dead && setText(decodeText(b)))
|
.then((b) => !dead && setText(decodeText(b)))
|
||||||
@@ -238,6 +245,7 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps)
|
|||||||
const rawCh = Number(initialLocator?.ch);
|
const rawCh = Number(initialLocator?.ch);
|
||||||
if (Number.isFinite(rawCh) && rawCh >= 0 && rawCh < chapters.length) {
|
if (Number.isFinite(rawCh) && rawCh >= 0 && rawCh < chapters.length) {
|
||||||
targetCi.current = Math.floor(rawCh);
|
targetCi.current = Math.floor(rawCh);
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- 首章渲染后按缓存 locator 落位阅读进度;ci 与下方滚动定位 effect/refs 协作,改渲染期重置属高风险重构,保守保留
|
||||||
setCi(Math.floor(rawCh));
|
setCi(Math.floor(rawCh));
|
||||||
if (hasF) restoreFrac.current = f;
|
if (hasF) restoreFrac.current = f;
|
||||||
} else if (hasF) {
|
} else if (hasF) {
|
||||||
|
|||||||
Reference in New Issue
Block a user