Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/项目笔记/代达罗斯/考试/2026-07-30-Finding反馈交互

EXAM-20260730-01 阅卷报告:Finding 反馈交互链路

12 分钟阅读 · Note

目录树 578 篇

              • EXAM-20260730-01 阅卷报告:Finding 反馈交互链路
              • EXAM-20260730-01:Finding 反馈交互链路
            • 代达罗斯前端考试索引
            • 前端能力画像
            • 深色模式
            • 阅卷报告:CrateDetailPageContent
            • 阅卷报告:ThemeProvider.tsx(第二次)
            • CrateDialog
          • 项目待做
          • 性能优化
          • UI设计
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗EXAM-20260730-01:Finding 反馈交互链路同一路径↗深色模式共同主题↗阅卷报告:CrateDetailPageContent共同主题↗阅卷报告:ThemeProvider.tsx(第二次)共同主题↗CrateDialog共同主题↗EXAM-20260730-02 答辩与现场接管记录共同主题
  • EXAM-20260730-01 阅卷报告:Finding 反馈交互链路

EXAM-20260730-01 阅卷报告:Finding 反馈交互链路

总览

  • 考生:Dano Day
  • 得分:60 / 80(75%)
  • 评级:🌕🌕🌕🌑🌑 — 及格,有需改进之处
  • 致命问题:2 个
  • Git 标准答案基准:4e03802a81989b0ae6da6ca775d9fa789a93e773
  • 试卷文件:
    • apps/app/src/hooks/use-finding-feedback.ts
    • apps/app/src/components/finding-feedback-buttons.tsx
    • apps/app/src/pages/RuleDetailPage/RuleDetailPageContent.tsx

本次继续遵守 i18n 不计分边界:翻译 key、直接文案和具体措辞不扣分。按钮能否理解、状态是否完整、事件是否隔离和数据能否刷新仍按功能评分。

成绩汇总

题号文件知识点得分结论
1use-finding-feedback.tsReact Query 查询与缓存键4/10首次查询可用,但查询 key 与失效 key 不一致,失败也没有真正收敛为 null
2use-finding-feedback.tsmutation payload 与缓存失效5/10提交代码行为正确,但考生明确标注为抄写,不计为独立掌握
3finding-feedback-buttons.tsx事件冒泡、pending 守卫10/10三个回调的事件隔离、值映射和依赖均正确
4finding-feedback-buttons.tsxJSX 事件接线、可访问名称8/10三个按钮接线正确;擅自移除图标及 import,留下调试说明
5RuleDetailPageContent.tsxEffect、异步加载、payload9/10加载生命周期和 { ruleId } 正确;双重断言掩盖了类型关系
6RuleDetailPageContent.tsx早返回、Refine update10/10前置条件、不可变 values 和依赖数组全部正确
7RuleDetailPageContent.tsx异步三态渲染10/10loading → empty → data 顺序正确,使用等价组件不扣分
8RuleDetailPageContent.tsx可点击行与反馈组件组合4/10Props 正确,但行没有 onClick,展开功能完全断开
合计60/8075%

逐题批改

第1题:读取单条 Finding 的反馈状态 — 4/10

一句话结论:当前代码能完成首次查询,但提交反馈后不会刷新这条查询,因为“存数据时使用的地址”和“通知刷新时使用的地址”不是同一个。

考生代码:

const { data } = useQuery({
  queryKey: [findingId],
  queryFn: () =>
    trpcClient.findings.getFeedbackByFinding.query({ findingId }) ?? null,
  enabled: findingId.length > 0,
}, queryClient);

必须修复:查询 key 与失效 key 不一致

概念是什么:查询键(queryKey)是 React Query 给缓存数据使用的结构化地址。数组第一项通常是业务命名空间,后续项用于区分具体实体。

本题运行过程:

FeedbackButtons 渲染
→ useQuery 把结果存到 [findingId]
→ 用户提交反馈
→ 第2题失效 ["finding-feedback", findingId]
→ React Query 找不到同一个缓存地址
→ 原来的 [findingId] 不重新查询
→ currentOutcome 可能继续显示旧值

这不是要求所有项目都必须使用某个固定字符串,而是要求同一份数据的查询和失效采用同一套 key 工厂或同一结构。

最小修复:

const feedbackKey = ["finding-feedback", findingId] as const;

useQuery({ queryKey: feedbackKey, /* ... */ });
queryClient.invalidateQueries({ queryKey: feedbackKey });

第一行建立唯一来源;查询和失效复用同一个值,避免两个地方手写后漂移。

迁移规则:凡是看到 invalidateQueries,都要反向找到创建该数据的 useQuery,比较完整 key 或前缀是否能够匹配。用户详情可使用 ["user", userId],用户列表可使用 ["users", filters];不要只用裸 id,因为不同业务实体可能碰巧拥有相同 id。

自检方法:打开 React Query Devtools,提交反馈前后观察 ["finding-feedback", findingId] 是否变为 stale 并重新 fetching;也可 mock queryFn,断言成功 mutation 后调用次数增加。

建议改进:?? null 没有捕获 Promise 拒绝

trpcClient...query() 返回 Promise。只要请求函数正常被调用,左侧就是一个 Promise 对象,因此 Promise ?? null 永远选左侧。网络错误发生在 Promise 之后的 rejected 状态,不会经过这个空值运算符。

执行顺序是:

调用 query()
→ 立即得到 Promise 对象
→ `?? null` 判断 Promise 不是 null
→ 稍后网络失败,Promise reject
→ 之前的 `?? null` 已经结束,无法处理错误

要把错误转换成 null,需要在异步边界使用 try/catch 或 .catch(() => null)。不过 React Query 默认会捕获 rejected Promise 并记录 isError,所以页面通常不会直接崩溃;这里的主要问题是没有实现题目要求的“失败收敛为 null”语义。

第2题:提交反馈并精确失效缓存 — 5/10

一句话结论:提交实现与标准行为一致,但你明确写了“这里是我抄的”,因此只计算可运行结果,不把它记录为独立掌握。

考生核心代码:

mutationFn: (outcome: "accepted" | "rejected" | "ignored") =>
  trpcClient.findings.addFeedback.mutate({ findingId, runId, outcome }),
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ["finding-feedback", findingId] });
  queryClient.invalidateQueries({ queryKey: ["findings"] });
  options?.onSuccess?.();
},

疑问一:outcome 类型应该从 Schema 导出吗?

直接答案:如果这个枚举被多个层共同使用,抽成共享类型或共享 Zod Schema 会更稳;但当前标准答案写显式联合类型并不是功能错误。

这里有三种层级:

  1. tRPC router 的输入 Schema 在服务端校验运行时数据;
  2. AppRouter 把输入输出类型传给 trpcClient,前端调用 mutate 时已经能获得类型检查;
  3. mutationFn 参数需要描述按钮允许提交的三个值,显式联合类型能完成这件事,但存在重复。

更可复用的前端写法可以是:

type FeedbackOutcome = FeedbackEntry["outcome"];

mutationFn: (outcome: FeedbackOutcome) =>
  trpcClient.findings.addFeedback.mutate({ findingId, runId, outcome });

如果需要让客户端、服务端和测试共同复用,则应把 feedbackOutcomeSchema 放入共享 Schema 包,再用 z.infer 得到类型。不要从服务端 router 文件反向导入到浏览器代码,那会破坏前后端边界。

如何确认:把鼠标放在 trpcClient.findings.addFeedback.mutate 的参数上,查看 IDE 是否已经显示 { findingId; runId; outcome; note? }。类型能正确出现,说明 tRPC 推导链仍在工作。

疑问二:finding-feedback 和 findings 是哪里来的?

它们不是 React Query 内置常量,而是项目开发者定义的缓存命名空间。

  • ["finding-feedback", findingId]:定位某条 Finding 的反馈;
  • ["findings"]:约定为 Findings 集合的前缀,理论上可同时匹配 ["findings"]、["findings", filters] 等查询。

注意:只有真正使用这些 key 的 React Query 查询才会被刷新。当前 Rule 详情页的 Findings 是通过直接 tRPC Promise 写入本地 state,并不使用 ["findings"] 缓存;所以第二次失效对该页面未必有作用。options.onSuccess 或显式重新请求才是通知这类本地 state 页面的方式。这是现有标准实现的设计局限,不对你额外扣分。

疑问三:失败时只 console.error 合适吗?

直接答案:作为开发期诊断可以,但作为完整产品反馈不够。

本 Hook 同时把 mutation 的 error 转成字符串返回,因此理想流程是:Hook 保留错误状态,组件渲染 role="alert" 或触发项目 toast,监控系统记录异常。当前 FeedbackButtons 没有消费 error,所以用户只看到按钮不更新,不知道失败原因。标准答案也有这项产品体验欠缺,本题不因此扣分。

第3题:隔离行内按钮事件并防止重复提交 — 10/10

一句话结论:三个回调均正确;你已经掌握了“先阻止冒泡,再判断 pending,再提交对应值”的顺序。

const handleAccept = useCallback((event: React.MouseEvent) => {
  event.stopPropagation();
  if (!isPending) submit("accepted");
}, [submit, isPending]);

事件从按钮开始,默认会继续向父元素传播。stopPropagation() 放在 pending 判断之前很重要:即使请求进行中不再提交,点击也不能穿透到父行。

疑问:不写 useCallback 会怎么样?

直接答案:本题功能仍然正确,通常也不会产生可感知性能问题。

普通函数会在每次渲染时得到新引用;useCallback 在依赖不变时复用旧引用。这里回调直接交给原生 button,button 不会因为函数引用变化而单独执行昂贵逻辑,因此 useCallback 的实际收益很小。

它更有价值的场景是:

  • 回调传给经过 React.memo 的子组件;
  • 回调进入其他 Hook 的依赖数组;
  • 第三方组件根据回调引用决定是否重新订阅。

迁移规则:先确认“稳定引用是否被下游观察”,再决定是否使用 useCallback。不要把它当成所有事件处理器的必选语法。

自检方法:临时去掉 useCallback,用 React DevTools Profiler 比较子组件渲染;如果没有 memo 子组件或订阅变化,行为通常完全一致。

第4题:把反馈按钮接到三个事件回调 — 8/10

一句话结论:按钮接线、当前态高亮和可访问名称正确;删除图标不影响提交,但属于超出题目范围的 UI 回退。

  • 必须行为:三个 onClick 与 accept/reject/ignore 一一对应,正确。
  • 可访问性:按钮仍有文字和 aria-label,即使图标缺失,读屏名称仍存在,不扣可访问性分。
  • 建议改进:不要因为 IDE 暂时报导入失败就删除已跟踪的 import 和 JSX;应先确认依赖安装、TypeScript Server 和路径解析。

本机验证显示 node_modules/lucide-react 存在,且仓库其他文件正常导入 Lucide。更可能的排查顺序是:重新安装依赖 → 重启 TypeScript Server → 查看真正的 typecheck 错误 → 再决定是否修改代码。

同类问题还出现在页面的返回按钮:ArrowLeft 被改成注释。它不属于本题目标,属于应在提交前恢复或解释的范围外改动。

第5题:按规则身份加载 Findings — 9/10

一句话结论:Effect 的依赖、loading 生命周期和 { ruleId: id } payload 都正确;仅有类型断言过强。

useEffect(() => {
  setFindingsLoading(true);
  trpcClient.findings.listByRuleId.query({ ruleId: id })
    .then((result) => setFindings(result as unknown as Finding[]))
    .finally(() => setFindingsLoading(false));
}, [id]);

as unknown as Finding[] 的含义不是“验证 result 是 Finding[]”,而是先主动丢掉原类型,再强行声明目标类型。它会隐藏真实接口变化。

迁移规则:如果两个类型兼容,优先不写断言;如果不兼容,先核对 tRPC 输出类型和前端 Finding 是否代表同一领域对象。只有明确知道运行时结构且暂时无法修正上游时才使用断言,并留下原因。

第6题:守住规则开关的 mutation 边界 — 10/10

一句话结论:这次早返回使用正确,修复了历史上“失败分支继续提交”的薄弱点。

if (!rule) return;
updateRule({
  resource: ResourceName.rules,
  id,
  values: { enabled: !rule.enabled },
});

运行过程是:先确认生成 payload 所需的 rule 已存在,再读取 rule.enabled,创建只包含更新字段的新对象,最后调用 mutation。没有修改原 rule,也不会在数据尚未加载时发请求。

这证明“前置条件守卫”已经掌握;但本题没有复测共享工厂生成完整表单对象,因此 Anatomy 创建表单中的领域对象边界仍需以后单独验证。

第7题:补齐 Findings 的异步三态骨架 — 10/10

一句话结论:三态优先级完全正确,LoadingState 与原来的 Loader 节点行为等价,因此不按标准答案的具体 JSX 扣分。

findingsLoading ? <LoadingState />
  : findings.length <= 0 ? <EmptyState />
  : <div>{/* data */}</div>

因为数组长度不可能小于 0,&lt;= 0 与 === 0 在这里等价。EmptyState 使用默认文案与原翻译文案不同,按 i18n 规则不扣分。

异步分支的关键不是组件名,而是互斥顺序:请求进行时,即使数组暂时为空也必须显示 loading;只有请求结束且数组为空才显示 empty;剩余情况显示 data。

第8题:组合可展开行与反馈子组件 — 4/10

一句话结论:反馈组件拿到了正确身份,但父行没有点击处理器,所以整行永远无法展开,isOpen 也不会因用户操作改变。

考生代码:

<div key={f.id} className="...cursor-pointer...">
  {/* ... */}
  <FeedbackButtons
    findingId={String(f?.id) ?? ""}
    runId={f?.runId ?? ""}
  />
</div>

必须修复:交互链少了起点

事件隔离不是只写 stopPropagation 就完成了,它要求父子两层各自承担职责:父行接收普通点击并展开,子按钮阻止自己的点击继续到父行。

运行过程:

当前代码点击行
→ 行没有 onClick
→ handleToggleExpand 从未执行
→ expanded 保持原值
→ isOpen 始终不能由用户切换

当前代码点击反馈按钮
→ 第3题 stopPropagation 正常执行
→ 但父行本来也没有处理器
→ 无法证明完整的父子事件边界已经接通

最小修复:

<div
  key={f.id}
  onClick={() => handleToggleExpand(f.id)}
>
  <FeedbackButtons findingId={String(f.id)} runId={f.runId ?? ""} />
</div>

第一处建立父行行为;第二处依赖第3题在按钮内部停止冒泡。两部分共同构成复合交互。

String(f?.id) ?? "" 还有一个机械问题:String(...) 永远返回字符串,即使输入 undefined 也会得到字符串 "undefined",所以右侧 ?? "" 永远不会执行。并且 f 来自 findings.map,本身不需要可选链。目标写法是 String(f.id)。

迁移规则:遇到“可点击卡片/行内部还有按钮或链接”,同时检查两点:父容器是否有明确主行为;内部交互是否用 stopPropagation 或父级 closest() 守卫排除。只完成一层不能算完整。

自检方法:依次点击行空白处、反馈按钮、行内链接;预期只有空白处切换展开,反馈只提交,链接只导航。目标 lint 也会通过 handleToggleExpand 是否未使用暴露这类断链。

多文件数据流与接口一致性

RuleDetailPageContent → FeedbackButtons → useFindingFeedback 的 Props 和 mutation payload 已接通。三个按钮提交的 outcome 正确,findingId/runId 也能传入 Hook。

存在两个跨文件断点:

  1. Hook 的查询使用 [findingId],成功 mutation 却失效 ["finding-feedback", findingId],反馈状态无法按预期刷新;
  2. 页面声明了 handleToggleExpand,但 Finding 行没有消费它,展开状态链在 JSX 处断开。

疑问集中解答

1. currentOutcome 是 any 吗?

不应默认是 any。trpcClient 由 AppRouter 提供类型,getFeedbackByFinding 的输出会传给 useQuery,再传到 data 和 feedback。如果 IDE 实际显示 any,应沿 trpcClient → AppRouter → findings router 返回值 检查哪一层丢失类型,而不是仅给 currentOutcome 补断言。

当前文件中的 FeedbackEntry 只是一个未被 Hook 使用的本地类型,不会自动约束 query 输出。需要在前端明确收口时,可给 useQuery&lt;FeedbackEntry | null&gt; 泛型,或者从共享 Schema 导出输出类型。

2. 为什么 key 不集中定义?

小项目可以就地写数组;一旦同一 key 同时用于查询、失效、预取和测试,就适合建立 key factory:

const feedbackKeys = {
  all: ["finding-feedback"] as const,
  detail: (id: string) => [...feedbackKeys.all, id] as const,
};

这样查询和失效都调用 feedbackKeys.detail(findingId),避免本次出现的漂移。

3. 为什么标准答案也可能不够完整?

标准答案是评分基准,不代表架构上不可改进。比如本题的 console.error 没有用户反馈,["findings"] 也不会自动刷新直接写入本地 state 的列表。阅卷按题目验收行为判断等价实现,同时会把标准实现的局限单独说明,不因你没有超出题目修复这些设计债而扣分。

验证结果

检查结果说明
bun run check-types失败1 个目标错误:handleToggleExpand 声明但未使用,对应第8题展开链断开
目标文件 oxlint失败1 error、2 warnings:return 前多余空行、未使用 handleToggleExpand、String(...) ?? "" 左侧恒不为空
相关前端测试无仓库没有这三个 Hook/组件/页面的前端测试;发现的 findings.test.ts 属于 tRPC router,按考试边界未运行
git diff --check失败第2题“这里是我抄的”注释存在尾随空格
依赖确认通过lucide-react 已安装并可从当前 app 的 node_modules 解析
调试与疑问扫描需清理保留多条 TODO/抄写说明、注释图标和 console.error;后者来自标准答案,不作为个人调试遗留扣分

致命问题

  1. 查询 key 和 mutation 失效 key 不一致,反馈提交后当前高亮可能保持旧值。
  2. Finding 行缺少 onClick,展开/收起功能完全不可用。

三大改进方向

  1. 把 queryKey 当成缓存地址:查询、失效、预取和测试统一从 key factory 获取。
  2. 检查完整交互链,而不是单个处理器:父行主行为和子按钮事件隔离必须同时存在。
  3. 不用断言和删除 import 掩盖工具报错;先从类型源、依赖解析和真实检查结果定位原因。

下一次训练建议

下一场不再采用挖空填空。建议给出一个完整 Issue:“反馈提交后高亮不更新、点击反馈会影响行展开”,由考生自主定位文件、提出方案、实现、补测试并提交验证说明。专项填空只在同一基础概念再次失败时短暂使用。