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

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/项目笔记/代达罗斯/考试/2026-07-28-代码片段管理

EXAM-20260728-01 阅卷报告:代码片段管理

10 分钟阅读 · Note

目录树 578 篇

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

关联笔记 6

↗EXAM-20260728-01:代码片段管理同一路径↗深色模式共同主题↗阅卷报告:CrateDetailPageContent共同主题↗阅卷报告:ThemeProvider.tsx(第二次)共同主题↗CrateDialog共同主题↗EXAM-20260730-02 答辩与现场接管记录共同主题
  • EXAM-20260728-01 阅卷报告:代码片段管理

EXAM-20260728-01 阅卷报告:代码片段管理

总览

  • 考生:Dano Day
  • 得分:76 / 100(76%)
  • 评级:🌕🌕🌕🌑🌑 — 及格,有需改进之处
  • 致命问题:2 个
  • 试卷文件:
    • moveCrateCodeSnippet.ts
    • CrateCodeSnippetDialog.tsx
    • CrateCodeSnippetsPanel.tsx

本次按用户要求,不对 className、布局美化、翻译 key、t(...) 使用方式或具体 i18n 文案扣分。按钮是否可理解、错误/加载/空状态是否存在,以及图标按钮是否有可访问名称,仍按功能和可访问性评分。

成绩汇总

题号知识点得分主要结论
1不可变数组重排9/10核心行为正确,边界判断有无效条件且留有废弃注释
2表单默认值9/10行为正确,写法可直接用可选链简化
3React Hook Form + Zod + reset10/10与标准行为一致
4表单关闭与提交桥接9/10校验链正确,未复制提交值对象
5表单 JSX 与错误反馈8/10四字段和提交状态完整,服务端错误缺少 alert 语义
6Refine 查询与 mutation 声明9/10查询、分页、启用条件和泛型均正确,存在无关 import
7新增/编辑分流4/10缺少 return,编辑会同时 update 和 create
8删除、重排与重试5/10代码行为正确,但重排 mutation 明确为恢复答案,不计独立掌握
9不可变 Set 与 pending 状态10/10完成正确
10查询状态与管理面板3/10有基本列表和按钮,但状态分支、展开、边界禁用和可访问性未完成
合计76/100

逐题批改

第1题:实现不可变的代码片段上下移动 — 9/10

知识点:不可变更新、数组重排、边界条件。

考生核心代码:

const len = snippets.length;
const targetIndex = index + direction;
if (len < 0 || targetIndex < 0 || targetIndex >= len) return snippets;
const re = snippets.slice();
const targetSnippet = re.splice(index, 1)[0];
if (!targetSnippet) return snippets;
re.splice(targetIndex, 0, targetSnippet);
return re.map((snippet, index) => ({ ...snippet, sortOrder: index }));

标准答案核心:

const nextIndex = index + direction;
if (nextIndex < 0 || nextIndex >= snippets.length) return snippets;
const reordered = [...snippets];
const [snippet] = reordered.splice(index, 1);
if (!snippet) return snippets;
reordered.splice(nextIndex, 0, snippet);
return reordered.map((item, sortOrder) => ({ ...item, sortOrder }));
  • 功能:测试覆盖的移动、越界和 sortOrder 归一化全部正确。
  • 模式:slice() 同样满足不可变要求,不因写法不同扣功能分。
  • 洁癖:len &lt; 0 永远不成立;// if...、// const ans 是废弃注释;lint 还提示可优先使用展开语法。
  • 改进:保留真正参与判断的条件,提交前清掉探索性注释。

第2题:把编辑数据转换为表单默认值 — 9/10

知识点:受控表单默认值、空值处理。

考生代码:

if (initialData) {
  return {
    name: initialData.name ?? "",
    language: initialData.language ?? "",
    code: initialData.code ?? "",
    description: initialData.description ?? "",
  };
}
return { name: "", language: "", code: "", description: "" };

标准答案:

return {
  name: initialData?.name ?? "",
  language: initialData?.language ?? "",
  code: initialData?.code ?? "",
  description: initialData?.description ?? "",
};
  • 功能:完全正确,没有把 undefined 传给表单字段。
  • 模式:显式分支与标准答案等价,不扣功能分。
  • 洁癖:单行对象过长,可选链能减少重复。
  • 疑问答复:这题确实简单,你没有写错;简单题考的是能否守住受控输入的空值边界。

第3题:初始化表单并同步弹窗身份 — 10/10

知识点:React Hook Form、Zod resolver、表单身份同步、Effect 依赖。

考生代码与标准答案等价:

const form = useForm<CrateCodeSnippetFormValues>({
  resolver: zodResolver(crateCodeSnippetFormSchema),
  defaultValues: getDefaultValues(initialData),
});

useEffect(() => {
  if (open) form.reset(getDefaultValues(initialData));
}, [form, initialData, open]);
  • 功能:正确。
  • 模式:resolver、默认值、打开时 reset 与依赖数组均正确。
  • 洁癖:无实质问题。
  • 疑问答复:你的“不安”可以放下,这里已经够了。defaultValues 只负责首次初始化,弹窗复用时由 reset 同步新身份。

第4题:桥接关闭与合法提交事件 — 9/10

知识点:稳定回调、表单校验入口、数据边界。

考生代码:

const handleClose = useCallback(() => onClose(), [onClose]);
const handleSubmit = form.handleSubmit((_values) => {
  onSubmit(_values);
});

标准答案:

const handleSubmit = form.handleSubmit((values) => {
  onSubmit({ ...values });
});
  • 功能:校验失败不会进入回调,关闭和提交行为正确。
  • 模式:题目要求向父级交付独立对象,少了 { ...values };当前父级不修改对象,所以暂不构成功能 Bug。
  • 洁癖:_values 实际被使用,不需要下划线前缀。

第5题:渲染可校验的新增/编辑弹窗 — 8/10

知识点:表单注册、错误反馈、提交状态、可访问性。

考生实现包含四个字段、FormProvider、onSubmit、取消按钮和提交禁用。标准答案额外保证:

{serverError && (
  <p className="text-sm text-destructive" role="alert">
    {serverError}
  </p>
)}
  • 功能:四个字段均已注册,代码框关闭拼写检查,提交状态存在。
  • 模式:description 使用 Input 而非 Textarea 不影响数据提交;当前没有子组件调用 useFormContext,因此 FormProvider 现在确实可以移除。保留它的价值主要是方便未来抽取字段子组件。
  • 可访问性:serverError 没有 role="alert";code/description 无错误时仍渲染空 alert 节点,建议条件渲染。
  • 洁癖:存在注释掉的无关 Schema import、action="" 和多处占位注释。
  • i18n:标题、标签和按钮使用直接文案不扣分;拼写也未计入本题分数。

第6题:建立列表查询与四类变更通道 — 9/10

知识点:Refine useList、查询状态、mutation 泛型、稳定派生数据。

考生代码与标准答案的查询配置一致:

const { result, query } = useList<CrateCodeSnippet>({
  resource: ResourceName.crateCodeSnippets,
  filters: [{ field: "crateId", value: crateId, operator: "eq" }],
  pagination: { currentPage: 1, pageSize: 0 },
  queryOptions: { enabled: crateId.length > 0 },
});
const snippets = useMemo(
  () => (result?.data ?? []) as CrateCodeSnippet[],
  [result?.data],
);
  • 功能:过滤、分页、启用条件、空数组回退和四种 mutation 类型均正确。
  • 模式:这次已经正确使用 query 承载加载/错误状态,说明历史薄弱点有改善。
  • 洁癖:误加了未使用的 import { map } from "zod",导致类型检查和 lint 报错。
  • 疑问答复:你写出的部分已经完整,没有漏配置。

第7题:实现新增与编辑弹窗的数据流 — 4/10

知识点:CRUD 分支、异步回调、早返回。

考生代码的关键问题:

if (editingSnippet) {
  updateMutation.mutate(...);
}
createMutation.mutate(...);

标准答案:

if (editingSnippet) {
  updateMutation.mutate(...);
  return;
}
createMutation.mutate(...);
  • 功能:新增、打开、编辑、关闭以及两个 mutation 参数本身都写对了。
  • 致命 Bug:编辑分支缺少 return,每次编辑都会同时创建一条新片段。
  • 模式:这是典型互斥分支问题;完成一个终止分支后必须显式提前返回,或使用 else。
  • 洁癖:无额外主要扣分。

第8题:实现删除、重排与重试控制 — 5/10

知识点:Refine mutation、自定义 action、失效后重取、确认框状态。

当前代码与标准答案行为一致,但注释明确说明重排 mutation 是直接恢复的标准代码:

reorderMutation.mutate(
  {
    url: DataProviderCustomAction.reorderCrateCodeSnippets,
    method: "post",
    values: { crateId, snippetIds: reordered.map((snippet) => snippet.id) },
  },
  {
    onSuccess: () => void query.refetch(),
    onError: (error) => setActionError(error.message),
  },
);
  • 功能:提交后的代码可正确运行。
  • 掌握度:恢复答案的部分不作为独立完成计分;删除目标判空、重试和确认框状态给予部分分。
  • 学习结论:mutate 的第一个参数描述“请求什么”,第二个参数描述“请求结束后怎么办”。普通删除使用 { resource, id };自定义操作使用 { url, method, values }。

第9题:维护多项展开状态与异步派生状态 — 10/10

知识点:函数式状态更新、不可变 Set、异步派生状态。

考生代码与标准答案等价:

setExpandedSnippetIds((current) => {
  const next = new Set(current);
  if (next.has(snippetId)) next.delete(snippetId);
  else next.add(snippetId);
  return next;
});

const isSaving =
  createMutation.mutation.isPending || updateMutation.mutation.isPending;
const isReordering = reorderMutation.mutation.isPending;
  • 功能:支持多项展开与独立收起,pending 派生正确。
  • 模式:没有原地修改只读 Set。
  • 洁癖:无实质问题。

第10题:组合完整的代码片段管理面板 — 3/10

知识点:异步状态机、条件渲染、列表 key、交互边界、可访问性。

考生条件分支:

{query.isLoading
  ? query.isError
    ? result.data.length > 0
      ? <LoadingState />
      : <ErrorState />
    : <EmptyState />
  : result.data.map(...)}

标准分支骨架:

{query.isLoading ? (
  <LoadingState />
) : query.isError ? (
  <ErrorState action={/* retry */} />
) : snippets.length === 0 ? (
  <EmptyState action={/* add */} />
) : (
  snippets.map(...)
)}
  • 功能:已提供添加、编辑、删除、上下移动和两个 Dialog 的基本绑定。
  • 致命 Bug:判断层级写反;常见的 isError=true, isLoading=false 会进入 result.data.map,可能因数据不存在而崩溃。
  • 缺失:重试入口、展开按钮/条件代码区、移动边界禁用、重排期间禁用、列表 key。
  • 可访问性:actionError 缺少 role="alert",操作按钮没有包含片段名称的可访问标签。
  • 洁癖:遗留 console.log、空 &lt;div&gt;、未使用的状态/回调/import;这些导致类型检查和 lint 失败。
  • 样式/i18n:所有 className、图标和翻译差异均未扣分。

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

moveCrateCodeSnippet → handleMove → reorderMutation 的类型和数据格式一致;Dialog 的 onSubmit 与 Panel 的 handleSubmit 类型也一致。主要断点不在接口,而在 Panel 内部流程:编辑分支没有终止,最终 JSX 没有消费第9题的展开与 pending 产物。

疑问解答

1. 怎么快速知道 useForm 配置对象有哪些属性?

最快顺序:

  1. 光标放在 useForm({ ... }) 对象内部按编辑器补全快捷键,TypeScript 会根据 UseFormProps&lt;T&gt; 列出属性;
  2. 悬停 useForm 或跳转到类型定义,直接查看 UseFormProps&lt;CrateCodeSnippetFormValues&gt;;
  3. 在仓库搜索 useForm&lt;,参考项目里已经通过检查的同类表单;
  4. 只有类型注释仍解释不清行为时再查官方文档。

类型补全适合回答“能填什么”,文档适合回答“这个选项具体在什么时候生效”。 这个提示确实很难读——它展示的是“完整函数类型”,不是给人看的配置说明。你不需要一次看懂全部,只要拆成三段。

useForm<表单字段类型>(配置对象): 表单操作对象

对应你的代码:

useForm<CrateCodeSnippetFormValues>({
  resolver: zodResolver(crateCodeSnippetFormSchema),
  defaultValues: getDefaultValues(initialData),
});

你截图里的类型是什么意思

useForm<
  { name; language; code; description }, // ① 表单输入类型
  any,                                   // ② Context 类型,一般不用管
  { name; language; code; description }  // ③ 校验转换后的输出类型
>(
  props?: Partial<{ ... }>               // ④ 可选配置
): UseFormReturn<...>                     // ⑤ 返回表单操作方法

关键是这一段:

props?: Partial<{
  mode: Mode;
  disabled: boolean;
  defaultValues: ...;
  values: ...;
  // ...
}>

它表达的是:

  • props?:整个配置对象可以不传。
  • Partial:配置对象里的每个属性都可以不写。
  • 花括号里的属性:所有可选配置项。
  • ... 11 more ...:IDE 把剩余属性折叠了。
  • UseFormReturn:最终返回 register、reset、handleSubmit、formState 等操作。

不要靠 Hover 找配置项

Hover 适合确认类型,不适合探索 API。

把光标放到对象内部:

const form = useForm<CrateCodeSnippetFormValues>({
  // 光标放这里
});

然后按:

  • Ctrl + Space:列出所有可以填写的配置项。
  • Ctrl + Shift + Space:显示当前函数参数提示。
  • 选中某个配置项后再看右侧说明。
  • 对不理解的类型按 F12 或“转到类型定义”。

也就是说,不需要先读懂那一大坨类型。让 IDE 先列出选项,再逐个查看。

最常用的配置其实只有几个

useForm<FormValues>({
  // 初始值
  defaultValues: {},

  // 接入 Zod 等校验器
  resolver: zodResolver(schema),

  // 什么时候首次校验
  mode: "onSubmit",

  // 首次失败后,什么时候重新校验
  reValidateMode: "onChange",

  // 卸载字段时是否删除其值
  shouldUnregister: false,

  // 出错时是否自动聚焦
  shouldFocusError: true,

  // 禁用整个表单
  disabled: false,
});

日常表单大约 80% 的场景只需要:

resolver
defaultValues

所以你第3题写的:

const form = useForm<CrateCodeSnippetFormValues>({
  resolver: zodResolver(crateCodeSnippetFormSchema),
  defaultValues: getDefaultValues(initialData),
});

已经完整正确,不需要因为类型提示里还有十几个配置项就把它们全部写上。

一个实用判断方法

先从需求反推配置,而不是从类型列表猜:

需求使用什么
设置初始值defaultValues
接入 Zodresolver
控制校验时机mode
编辑对象变化后重新填值form.reset()
注册输入框form.register()
包装提交校验form.handleSubmit()
获取错误form.formState.errors
观察某个字段form.watch()

你的题目需求只有“Zod 校验、初始化、切换编辑对象”,所以答案自然就是:

resolver
defaultValues
useEffect + form.reset

重点不是看懂整个 useForm 类型,而是学会:需求 → 找对应 API → 用 Ctrl + Space 验证。

2. defaultValues + reset 真的就够了吗?

够。defaultValues 只在表单实例首次建立时读取;同一弹窗切换编辑对象时,必须用 reset 应用新的快照。你把 form、initialData、open 都放进依赖数组也正确。

3. FormProvider 没有子组件消费时是不是一样?

是。当前字段全部直接使用 form.register,没有调用 useFormContext 的后代,因此移除 FormProvider 不改变行为。若之后把字段拆成独立组件,并在子组件中通过 useFormContext 取表单,它才是必需的。

4. 错误节点不写 && 行不行?

画面上通常只是空白,但会长期渲染空的 role="alert" 节点,给辅助技术制造无意义的提醒区域。建议只在错误存在时渲染完整 alert。

5. Refine 的 mutate 怎么记?

把它拆成两部分:

  • 第一个参数:请求身份和请求数据;
  • 第二个参数:onSuccess / onError 生命周期。

先由 Hook 泛型确定 values 类型,再让编辑器从 mutate( 的参数位置给出补全,不需要背整个对象结构。

验证结果

检查结果说明
bun run check-types失败11 个未使用声明错误;其中两个 t 未使用按新 i18n 规则不扣分,其余主要来自第10题未消费导入/状态及无关 zod.map import
目标文件 oxlint失败7 个 error、25 个 warning;包括 return 空行、Props 顺序、未使用项、缺少列表 key 和调试遗留
两个相关测试文件通过2 个测试文件、4 个测试全部通过

致命问题

  1. 第7题编辑提交同时触发 update 与 create,会产生重复数据。
  2. 第10题异步状态分支错误,错误状态可能访问不存在的 result.data 并崩溃。

三大改进方向

  1. 把 mutation 当成“请求参数 + 生命周期”两段练习,并强化互斥分支的提前返回。
  2. 固定使用 loading → error → empty → data 顺序编写异步 UI,渲染统一消费安全的 snippets,不要回退访问原始可空结果。
  3. 提交前做一次机械清理:console.log、无关 import、空节点、未使用回调、列表 key 和占位注释。

下一次建议训练

下一场优先使用不同业务场景复测 Refine mutation、互斥 CRUD 分支和异步状态渲染;不可变数组与表单 reset 已建立较好基线,可降为组合题中的次要考点。