fix(web): stale-closure + focus trap + 去边界强转 + a11y/性能打磨 + 类型化客户端

P1-6 useStyleLearn/useKimiOauth 修 stale-closure 依赖。
P1-7 CommandPalette/Drawer 加 focus trap(新 lib/a11y/focusTrap)。
P1-8 SSE 解析与 server.ts 去 as 强转改类型守卫/运行时校验。
P2 删冗余强转、开 noUncheckedIndexedAccess、Toast 清理+稳定 key、列表 key 稳定化、
  rel=noopener、useMemo 大文本、ConflictCard role=group、useInjection 加锁、
  client.test 真断言。
codegen 重生成 schema.d.ts + 消费 DimensionEntry/ReviewConflictView 强类型。
This commit is contained in:
Yaojia Wang
2026-06-21 19:32:49 +02:00
parent 016509c5c6
commit c6651b74b9
36 changed files with 792 additions and 140 deletions

View File

@@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import type { StyleFingerprintResponse } from "@/lib/api/types";
import { useJobPoll } from "@/lib/jobs/useJobPoll";
import { styleLearnResult } from "@/lib/jobs/job";
import {
@@ -41,16 +40,20 @@ export function useStyleLearn(initial: Fingerprint | null): UseStyleLearn {
const [pollStatus, setPollStatus] = useState<
ReturnType<typeof useJobPoll>["status"] | "idle"
>("idle");
const [projectId, setProjectId] = useState<string | null>(null);
// projectId 用 ref 而非 statedone 边沿的 effect 须读到「最近一次 learn 传入的」
// 而非闭包捕获的旧值(修 stale-closure。learn 同步写 refeffect 同步读。
const projectIdRef = useRef<string | null>(null);
const poll = useJobPoll();
const toast = useToast();
// 仅在提交过一次学文风后才反映轮询状态initialPollState.status 默认 "polling")。
const startedRef = useRef(false);
// 轮询完成 → 拉最新指纹;失败 → toast。
// 依赖 poll.status / poll.error / toast均稳定或随状态变化projectId 经 ref 取最新。
useEffect(() => {
if (!startedRef.current) return;
setPollStatus(poll.status);
const projectId = projectIdRef.current;
if (poll.status === "done" && projectId) {
void refetchFingerprint(projectId).then((fp) => {
if (fp) setFingerprint(fp);
@@ -60,14 +63,12 @@ export function useStyleLearn(initial: Fingerprint | null): UseStyleLearn {
if (poll.status === "error") {
toast(`学文风失败:${poll.error ?? "未知原因"}`, "error");
}
// 仅在 poll.status 变化时反应。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [poll.status]);
}, [poll.status, poll.error, toast]);
const learn = useCallback<UseStyleLearn["learn"]>(
async (pid, samples, mode) => {
setSubmitting(true);
setProjectId(pid);
projectIdRef.current = pid;
try {
const { data, error } = await api.POST("/projects/{project_id}/style", {
params: { path: { project_id: pid } },
@@ -111,7 +112,7 @@ async function refetchFingerprint(
params: { path: { project_id: projectId } },
});
if (error || !data) return null;
return normalizeFingerprint(data as StyleFingerprintResponse);
return normalizeFingerprint(data);
}
// 仅供测试/复用:保证 job done result 的版本回显(不阻断 UI