You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:13:44