Keystone.js中Exam与ExamQuestion模型的最佳编辑实现方案问询
在Keystone.js中实现Exam关联试题的内联编辑器
嘿,我来帮你搞定这个问题!要在Keystone.js的Exam编辑页面里直接管理复杂的试题列表,咱们得从模型定义、自定义UI组件这两个核心部分入手,一步步来:
1. 先定义好Exam和Question模型
首先得把基础的模型结构搭好,明确两者的关联关系,同时给Question加上所有需要的复杂字段:
// schemas/Question.js import { list } from '@keystone-6/core'; import { select, text, textarea, array } from '@keystone-6/core/fields'; export const Question = list({ fields: { questionType: select({ options: [ { label: '单选题', value: 'single_choice' }, { label: '多选题', value: 'multiple_choice' }, { label: '图片题', value: 'picture' }, { label: '简答题', value: 'text' }, { label: '论述题', value: 'longtext' }, ], defaultValue: 'single_choice', isRequired: true, }), questionText: text({ isRequired: true }), questionChoices: array({ type: text(), ui: { createView: { fieldMode: 'hidden' }, itemView: { fieldMode: ({ item }) => ['single_choice', 'multiple_choice'].includes(item?.questionType) ? 'edit' : 'hidden' }, }, }), correctAnswer: text({ ui: { createView: { fieldMode: 'hidden' }, itemView: { fieldMode: ({ item }) => ['single_choice', 'multiple_choice'].includes(item?.questionType) ? 'edit' : 'hidden' }, }, }), explanationTextLong: textarea(), }, ui: { // 隐藏单独的Question列表页,只让用户在Exam页面编辑 isHidden: true, }, }); // schemas/Exam.js import { list } from '@keystone-6/core'; import { text, integer, textarea, relationship } from '@keystone-6/core/fields'; import ExamQuestionsEditor from '../components/ExamQuestionsEditor'; // 后面要写的自定义组件 export const Exam = list({ fields: { name: text({ isRequired: true }), durationMinutes: integer({ isRequired: true, defaultValue: 60 }), welcomeText: textarea(), questions: relationship({ ref: 'Question', many: true, ui: { // 指定自定义的编辑器组件 component: ExamQuestionsEditor, }, validation: { maxItems: 100, // 限制最多100道题 }, }), }, });
2. 编写自定义的试题内联编辑器组件
这是核心部分——咱们要做一个能在Exam编辑页里直接添加、编辑、删除试题的组件,还要根据试题类型动态显示对应的字段。下面是一个简化版的示例(基于React和Keystone的UI钩子):
// components/ExamQuestionsEditor.js import { useState } from 'react'; import { Button, FieldContainer, FieldLabel, Input, Select, Textarea, Stack } from '@keystone-ui/core'; import { useField, useItem } from '@keystone-6/core/admin-ui'; export default function ExamQuestionsEditor() { const { field, value, onChange } = useField(); const [localQuestions, setLocalQuestions] = useState(value || []); // 添加新试题 const addNewQuestion = () => { setLocalQuestions([...localQuestions, { questionType: 'single_choice', questionText: '', questionChoices: [], correctAnswer: '', explanationTextLong: '', }]); }; // 删除指定位置的试题 const removeQuestion = (index) => { const updated = [...localQuestions]; updated.splice(index, 1); setLocalQuestions(updated); onChange(updated); }; // 更新单个试题的字段 const updateQuestionField = (index, fieldKey, fieldValue) => { const updated = [...localQuestions]; updated[index] = { ...updated[index], [fieldKey]: fieldValue }; setLocalQuestions(updated); onChange(updated); }; // 给选择题添加选项 const addChoice = (index) => { const updated = [...localQuestions]; updated[index].questionChoices = [...updated[index].questionChoices, '']; setLocalQuestions(updated); onChange(updated); }; return ( <FieldContainer> <FieldLabel>{field.label}</FieldLabel> <Stack gap="medium"> {localQuestions.map((question, index) => ( <Stack key={index} gap="small" padding="small" border="1px solid #e2e8f0" borderRadius="4px"> <Select value={question.questionType} onChange={(e) => updateQuestionField(index, 'questionType', e.target.value)} > <option value="single_choice">单选题</option> <option value="multiple_choice">多选题</option> <option value="picture">图片题</option> <option value="text">简答题</option> <option value="longtext">论述题</option> </Select> <Input value={question.questionText} onChange={(e) => updateQuestionField(index, 'questionText', e.target.value)} placeholder="输入题目内容" required /> {/* 仅选择题显示选项和正确答案 */} {['single_choice', 'multiple_choice'].includes(question.questionType) && ( <Stack gap="small"> <Button onClick={() => addChoice(index)}>添加选项</Button> {question.questionChoices.map((choice, choiceIndex) => ( <Input key={choiceIndex} value={choice} onChange={(e) => { const updatedChoices = [...question.questionChoices]; updatedChoices[choiceIndex] = e.target.value; updateQuestionField(index, 'questionChoices', updatedChoices); }} placeholder={`选项 ${choiceIndex + 1}`} /> ))} <Input value={question.correctAnswer} onChange={(e) => updateQuestionField(index, 'correctAnswer', e.target.value)} placeholder="输入正确答案(与选项内容一致)" required /> </Stack> )} <Textarea value={question.explanationTextLong} onChange={(e) => updateQuestionField(index, 'explanationTextLong', e.target.value)} placeholder="输入题目解析" /> <Button variant="destructive" onClick={() => removeQuestion(index)}>删除题目</Button> </Stack> ))} <Button onClick={addNewQuestion} disabled={localQuestions.length >= 100}> {localQuestions.length >= 100 ? '已达最大题量(100道)' : '添加新题目'} </Button> </Stack> </FieldContainer> ); }
3. 关键细节和优化点
- 动态字段显示:上面的组件已经根据
questionType来控制选项、正确答案字段的显示/隐藏,你可以根据需求扩展(比如图片题添加图片上传字段,用Keystone的file或image字段)。 - 表单验证:可以添加额外的验证逻辑,比如选择题的正确答案必须存在于选项列表中,必填字段不能为空等,结合Keystone的
validation或者在组件里做前端校验。 - 性能优化:如果要支持100道题,建议用React的
memo来优化子组件渲染,避免不必要的重绘。 - 数据持久化:上面的组件直接通过
onChange更新关联字段的值,Keystone会自动处理这些关联数据的创建/更新,不需要额外的API调用。
这样一来,你就能在Exam的编辑页面里直接管理所有试题,不用跳转到单独的Question页面,完全符合你的需求啦!
内容的提问来源于stack exchange,提问作者Dimitry K
相关产品推荐
相关产品推荐

