Merge pull request '随机试卷支持难度与知识点筛选' (#3) from menft into master

Reviewed-on: http://114.55.243.137:6688/dianliang/ai-course/pulls/3
This commit is contained in:
menfutong 2025-11-30 10:54:57 +08:00
commit 3eacc73c8d
12 changed files with 559 additions and 39 deletions

View File

@ -39,6 +39,8 @@ import xyz.playedu.common.util.StringUtil;
import xyz.playedu.exam.constants.ExamConstant;
import xyz.playedu.exam.domain.*;
import xyz.playedu.exam.service.*;
import xyz.playedu.jc.domain.Knowledge;
import xyz.playedu.jc.service.IKnowledgeService;
import xyz.playedu.resource.service.ResourceService;
@RestController
@ -62,6 +64,8 @@ public class QuestionController {
@Autowired private ResourceService resourceService;
@Autowired private IKnowledgeService knowledgeService;
@GetMapping("/index")
@BackendPermission(slug = BPermissionConstant.EXAM_QUESTION)
@Log(title = "试题-列表", businessType = BusinessTypeConstant.GET)
@ -1178,4 +1182,54 @@ public class QuestionController {
.size());
examQuestionCategoryService.updateById(examQuestionCategory);
}
/**
* 根据题库ID题型难度获取关联的知识点列表去重
* 用于随机试卷配置时的知识点筛选
* 返回格式: [{code: "MATH001", name: "导数与微分"}, ...]
*
* @author menft
*/
@GetMapping("/knowledge-codes")
@BackendPermission(slug = BPermissionConstant.EXAM_QUESTION)
public JsonResponse getKnowledgeCodes(@RequestParam HashMap<String, Object> params) {
String categoryIds = MapUtils.getString(params, "category_ids");
Integer type = MapUtils.getInteger(params, "type");
Integer level = MapUtils.getInteger(params, "level");
if (StringUtil.isEmpty(categoryIds)) {
return JsonResponse.data(List.of());
}
// 获取去重的知识点编码
List<String> knowledgeCodes =
examQuestionService.getDistinctKnowledgeCodes(categoryIds, type, level);
if (knowledgeCodes.isEmpty()) {
return JsonResponse.data(List.of());
}
// 查询知识点名称
List<Knowledge> knowledgeList = knowledgeService.getByKnowledgeCodes(knowledgeCodes);
Map<String, String> codeToName =
knowledgeList.stream()
.collect(
Collectors.toMap(
Knowledge::getKnowledgeCode,
Knowledge::getName,
(v1, v2) -> v1));
// 构建返回结果
List<Map<String, String>> result =
knowledgeCodes.stream()
.map(
code -> {
Map<String, String> item = new HashMap<>();
item.put("code", code);
item.put("name", codeToName.getOrDefault(code, code));
return item;
})
.collect(Collectors.toList());
return JsonResponse.data(result);
}
}

View File

@ -961,41 +961,53 @@ public class PaperController {
JSONObject source = randomRules.getJSONObject("source");
List<Integer> categoryIds = source.getBeanList("category_ids", Integer.class);
filter.setCategoryIds(categoryIds);
// 题型数量
// 题型数量难度知识点
JSONObject score = randomRules.getJSONObject("score");
JSONObject type1 = score.getJSONObject(ExamConstant.TYPE_1 + "");
if (StringUtil.isNotNull(type1)) {
filter.setType1Number(type1.getInt("number"));
filter.setType1Level(type1.getInt("level"));
filter.setType1KnowledgeCodes(type1.getStr("knowledge_codes"));
} else {
filter.setType1Number(0);
}
JSONObject type2 = score.getJSONObject(ExamConstant.TYPE_2 + "");
if (StringUtil.isNotNull(type2)) {
filter.setType2Number(type2.getInt("number"));
filter.setType2Level(type2.getInt("level"));
filter.setType2KnowledgeCodes(type2.getStr("knowledge_codes"));
} else {
filter.setType2Number(0);
}
JSONObject type3 = score.getJSONObject(ExamConstant.TYPE_3 + "");
if (StringUtil.isNotNull(type3)) {
filter.setType3Number(type3.getInt("number"));
filter.setType3Level(type3.getInt("level"));
filter.setType3KnowledgeCodes(type3.getStr("knowledge_codes"));
} else {
filter.setType3Number(0);
}
JSONObject type4 = score.getJSONObject(ExamConstant.TYPE_4 + "");
if (StringUtil.isNotNull(type4)) {
filter.setType4Number(type4.getInt("number"));
filter.setType4Level(type4.getInt("level"));
filter.setType4KnowledgeCodes(type4.getStr("knowledge_codes"));
} else {
filter.setType4Number(0);
}
JSONObject type5 = score.getJSONObject(ExamConstant.TYPE_5 + "");
if (StringUtil.isNotNull(type5)) {
filter.setType5Number(type5.getInt("number"));
filter.setType5Level(type5.getInt("level"));
filter.setType5KnowledgeCodes(type5.getStr("knowledge_codes"));
} else {
filter.setType5Number(0);
}
JSONObject type6 = score.getJSONObject(ExamConstant.TYPE_6 + "");
if (StringUtil.isNotNull(type6)) {
filter.setType6Number(type6.getInt("number"));
filter.setType6Level(type6.getInt("level"));
filter.setType6KnowledgeCodes(type6.getStr("knowledge_codes"));
} else {
filter.setType6Number(0);
}

View File

@ -12,14 +12,26 @@ public class ExamQuestionFilter {
private List<Integer> categoryIds;
private Integer type1Number;
private Integer type1Level;
private String type1KnowledgeCodes;
private Integer type2Number;
private Integer type2Level;
private String type2KnowledgeCodes;
private Integer type3Number;
private Integer type3Level;
private String type3KnowledgeCodes;
private Integer type4Number;
private Integer type4Level;
private String type4KnowledgeCodes;
private Integer type5Number;
private Integer type5Level;
private String type5KnowledgeCodes;
private Integer type6Number;
private Integer type6Level;
private String type6KnowledgeCodes;
}

View File

@ -20,5 +20,12 @@ public interface IKnowledgeService extends IService<Knowledge> {
JSONObject getByIdVo(Integer id);
JSONObject stuGetByIdVo(Integer id);
/**
* 根据知识点编码列表批量查询知识点
*
* @param codes 知识点编码列表
* @return 知识点列表
* @author menft
*/
List<Knowledge> getByKnowledgeCodes(List<String> codes);
}

View File

@ -217,6 +217,16 @@ public class KnowledgeServiceImpl extends ServiceImpl<KnowledgeMapper, Knowledge
}
}
@Override
public List<Knowledge> getByKnowledgeCodes(List<String> codes) {
if (codes == null || codes.isEmpty()) {
return new ArrayList<>();
}
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Knowledge::getKnowledgeCode, codes);
return list(queryWrapper);
}
/**
* 递增编码
* @param code 当前编码

View File

@ -22,4 +22,6 @@ public interface ExamQuestionMapper extends BaseMapper<ExamQuestion> {
Long paginateCount(ExamQuestionPaginateFilter filter);
List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter);
List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level);
}

View File

@ -43,4 +43,11 @@ public interface ExamQuestionService extends IService<ExamQuestion> {
List<ExamQuestion> chunksByCategoryId(Integer categoryId);
List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter);
/**
* 根据题库ID题型难度获取去重的知识点编码列表
*
* @author menft
*/
List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level);
}

View File

@ -116,4 +116,22 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
public List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter) {
return getBaseMapper().chunksByCategoryIdAndLimit(filter);
}
@Override
public List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level) {
// 获取所有 knowledge_code 字段可能是逗号分隔的多个值
List<String> rawCodes = getBaseMapper().getDistinctKnowledgeCodes(categoryIds, type, level);
if (rawCodes == null || rawCodes.isEmpty()) {
return new ArrayList<>();
}
// 拆分并去重
return rawCodes.stream()
.filter(code -> code != null && !code.isEmpty())
.flatMap(code -> java.util.Arrays.stream(code.split(",")))
.map(String::trim)
.filter(code -> !code.isEmpty())
.distinct()
.sorted()
.collect(java.util.stream.Collectors.toList());
}
}

View File

@ -107,6 +107,16 @@
#{categoryId}
</foreach>
</if>
<if test="type1Level != null">
and `exam_question`.`level` = #{type1Level}
</if>
<if test="type1KnowledgeCodes != null and type1KnowledgeCodes != ''">
and (
<foreach collection="type1KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type1Number})
@ -122,6 +132,16 @@
#{categoryId}
</foreach>
</if>
<if test="type2Level != null">
and `exam_question`.`level` = #{type2Level}
</if>
<if test="type2KnowledgeCodes != null and type2KnowledgeCodes != ''">
and (
<foreach collection="type2KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type2Number})
@ -137,6 +157,16 @@
#{categoryId}
</foreach>
</if>
<if test="type3Level != null">
and `exam_question`.`level` = #{type3Level}
</if>
<if test="type3KnowledgeCodes != null and type3KnowledgeCodes != ''">
and (
<foreach collection="type3KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type3Number})
@ -152,6 +182,16 @@
#{categoryId}
</foreach>
</if>
<if test="type4Level != null">
and `exam_question`.`level` = #{type4Level}
</if>
<if test="type4KnowledgeCodes != null and type4KnowledgeCodes != ''">
and (
<foreach collection="type4KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type4Number})
@ -167,6 +207,16 @@
#{categoryId}
</foreach>
</if>
<if test="type5Level != null">
and `exam_question`.`level` = #{type5Level}
</if>
<if test="type5KnowledgeCodes != null and type5KnowledgeCodes != ''">
and (
<foreach collection="type5KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type5Number})
@ -182,8 +232,36 @@
#{categoryId}
</foreach>
</if>
<if test="type6Level != null">
and `exam_question`.`level` = #{type6Level}
</if>
<if test="type6KnowledgeCodes != null and type6KnowledgeCodes != ''">
and (
<foreach collection="type6KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type6Number})
</select>
<select id="getDistinctKnowledgeCodes" resultType="java.lang.String">
SELECT DISTINCT `knowledge_code`
FROM `exam_question`
<where>
AND `knowledge_code` IS NOT NULL
AND `knowledge_code` != ''
<if test="categoryIds != null and categoryIds != ''">
AND `category_id` IN (${categoryIds})
</if>
<if test="type != null">
AND `type` = #{type}
</if>
<if test="level != null">
AND `level` = #{level}
</if>
</where>
</select>
</mapper>

View File

@ -134,3 +134,17 @@ export function storeBatch(categoryId: number, startLine: number, questions: str
export function uploadTxt(params: any) {
return client.post('/backend/v1/exam/question/import', params);
}
/**
* ID
* @param categoryIds ID列表
* @param type
* @param level
*/
export function getKnowledgeCodes(categoryIds: string, type?: number, level?: number | null) {
return client.get('/backend/v1/exam/question/knowledge-codes', {
category_ids: categoryIds,
type: type,
level: level,
});
}

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import styles from './paper.module.less';
import { message, Button, Table, InputNumber, Spin, Modal } from 'antd';
import { paper } from '../../../../api';
import { message, Button, Table, InputNumber, Spin, Modal, Select } from 'antd';
import { paper, question } from '../../../../api';
import type { ColumnsType } from 'antd/es/table';
import { PlusOutlined } from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom';
@ -10,6 +10,14 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
const { confirm } = Modal;
// 难度选项
const levelOptions = [
{ value: null, label: '不限' },
{ value: 1, label: '简单' },
{ value: 2, label: '中等' },
{ value: 3, label: '困难' },
];
interface DataType {
id: React.Key;
admin_id: number;
@ -61,28 +69,49 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
const [choice, setChoice] = useState({
number: 0,
score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
const [select, setSelect] = useState({
number: 0,
score: 0,
missed_score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
const [input, setInput] = useState({
number: 0,
score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
const [judge, setJudge] = useState({
number: 0,
score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
const [qa, setQa] = useState({
number: 0,
score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
const [cap, setCap] = useState({
number: 0,
score: 0,
missed_score: 0,
level: null as number | null,
knowledge_codes: '' as string,
});
// 各题型的知识点选项
const [knowledgeOptions, setKnowledgeOptions] = useState<Record<number, any[]>>({
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
});
useEffect(() => {
@ -91,6 +120,23 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
setId(Number(result.get('id')));
}, [result.get('cid'), result.get('title'), result.get('id')]);
// 根据题库、题型、难度加载知识点列表
const loadKnowledgeCodes = async (type: number, level: number | null) => {
if (questions.length === 0) return;
try {
const res: any = await question.getKnowledgeCodes(questions.join(','), type, level);
const items = res.data || [];
// 后端返回 [{code: "xxx", name: "yyy"}, ...]
const options = items.map((item: { code: string; name: string }) => ({
value: item.code,
label: item.name,
}));
setKnowledgeOptions((prev) => ({ ...prev, [type]: options }));
} catch (e) {
console.error('加载知识点失败', e);
}
};
useEffect(() => {
if (id === 0) {
return;
@ -187,6 +233,8 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
setChoice({
number: score[1].number,
score: score[1].score,
level: score[1].level || null,
knowledge_codes: score[1].knowledge_codes || '',
});
}
if (score[2]) {
@ -194,24 +242,32 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
number: score[2].number,
score: score[2].score,
missed_score: score[2].missed_score,
level: score[2].level || null,
knowledge_codes: score[2].knowledge_codes || '',
});
}
if (score[3]) {
setInput({
number: score[3].number,
score: score[3].score,
level: score[3].level || null,
knowledge_codes: score[3].knowledge_codes || '',
});
}
if (score[4]) {
setJudge({
number: score[4].number,
score: score[4].score,
level: score[4].level || null,
knowledge_codes: score[4].knowledge_codes || '',
});
}
if (score[5]) {
setQa({
number: score[5].number,
score: score[5].score,
level: score[5].level || null,
knowledge_codes: score[5].knowledge_codes || '',
});
}
if (score[6]) {
@ -219,10 +275,33 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
number: score[6].number,
score: score[6].score,
missed_score: 0,
level: score[6].level || null,
knowledge_codes: score[6].knowledge_codes || '',
});
}
setSpinInit(false);
setInit(false);
// 预加载各题型的知识点选项(用于回显)
if (arr.length > 0) {
const categoryIds = arr.join(',');
[1, 2, 3, 4, 5, 6].forEach((type) => {
const typeScore = score[type];
if (typeScore && typeScore.knowledge_codes) {
question
.getKnowledgeCodes(categoryIds, type, typeScore.level || null)
.then((res: any) => {
const items = res.data || [];
const options = items.map((item: { code: string; name: string }) => ({
value: item.code,
label: item.name,
}));
setKnowledgeOptions((prev) => ({ ...prev, [type]: options }));
})
.catch(() => {});
}
});
}
});
};
@ -609,6 +688,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={choice.level}
onChange={(value) => {
const obj = { ...choice };
obj.level = value;
obj.knowledge_codes = '';
setChoice(obj);
loadKnowledgeCodes(1, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[1]}
value={choice.knowledge_codes ? choice.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...choice };
obj.knowledge_codes = values.join(',');
setChoice(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(1, choice.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -619,7 +734,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q2 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.select.label')}({t('exam.paper.compose.text1')}
@ -679,6 +794,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text5')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={select.level}
onChange={(value) => {
const obj = { ...select };
obj.level = value;
obj.knowledge_codes = '';
setSelect(obj);
loadKnowledgeCodes(2, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[2]}
value={select.knowledge_codes ? select.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...select };
obj.knowledge_codes = values.join(',');
setSelect(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(2, select.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -689,7 +840,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q3 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.input.label')}({t('exam.paper.compose.text1')}
@ -730,6 +881,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={input.level}
onChange={(value) => {
const obj = { ...input };
obj.level = value;
obj.knowledge_codes = '';
setInput(obj);
loadKnowledgeCodes(3, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[3]}
value={input.knowledge_codes ? input.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...input };
obj.knowledge_codes = values.join(',');
setInput(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(3, input.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -740,7 +927,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q4 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.judge.label')}({t('exam.paper.compose.text1')}
@ -781,6 +968,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={judge.level}
onChange={(value) => {
const obj = { ...judge };
obj.level = value;
obj.knowledge_codes = '';
setJudge(obj);
loadKnowledgeCodes(4, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[4]}
value={judge.knowledge_codes ? judge.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...judge };
obj.knowledge_codes = values.join(',');
setJudge(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(4, judge.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -791,7 +1014,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q5 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.qa.label')}({t('exam.paper.compose.text1')}
@ -832,6 +1055,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={qa.level}
onChange={(value) => {
const obj = { ...qa };
obj.level = value;
obj.knowledge_codes = '';
setQa(obj);
loadKnowledgeCodes(5, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[5]}
value={qa.knowledge_codes ? qa.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...qa };
obj.knowledge_codes = values.join(',');
setQa(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(5, qa.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -842,7 +1101,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q6 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.cap.label')}({t('exam.paper.compose.text1')}
@ -883,6 +1142,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={cap.level}
onChange={(value) => {
const obj = { ...cap };
obj.level = value;
obj.knowledge_codes = '';
setCap(obj);
loadKnowledgeCodes(6, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[6]}
value={cap.knowledge_codes ? cap.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...cap };
obj.knowledge_codes = values.join(',');
setCap(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(6, cap.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}

View File

@ -78,14 +78,18 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
try {
// 获取知识点详情包含bookId
const knowledgeRes: any = await textbook.getKnowledgeByCodesApi(data.knowledge_code);
if (knowledgeRes.data && Array.isArray(knowledgeRes.data) && knowledgeRes.data.length > 0) {
if (
knowledgeRes.data &&
Array.isArray(knowledgeRes.data) &&
knowledgeRes.data.length > 0
) {
// 获取所有需要预加载的教材ID去重
const bookIds = [...new Set(knowledgeRes.data.map((k: any) => k.bookId))];
// 预加载每个教材的知识点列表到级联选项中
for (const bookId of bookIds) {
const knowledgeListRes: any = await textbook.getKnowledgeListApi(bookId as number);
const targetOption = options.find(opt => opt.value === bookId);
const targetOption = options.find((opt) => opt.value === bookId);
if (targetOption && knowledgeListRes.data && Array.isArray(knowledgeListRes.data)) {
targetOption.children = knowledgeListRes.data.map((item: any) => ({
value: item.knowledgeCode || item.knowledge_code,
@ -97,7 +101,10 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
setCascaderOptions([...options]);
// 构建级联选择器的值 [[bookId, knowledgeCode], ...]
cascaderValue = knowledgeRes.data.map((k: any) => [k.bookId, k.knowledgeCode || k.knowledge_code]);
cascaderValue = knowledgeRes.data.map((k: any) => [
k.bookId,
k.knowledgeCode || k.knowledge_code,
]);
}
} catch (err) {
console.error('加载知识点详情失败:', err);
@ -124,7 +131,9 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
textbook.getKnowledgeListApi(targetOption.value as number).then((res: any) => {
textbook
.getKnowledgeListApi(targetOption.value as number)
.then((res: any) => {
targetOption.loading = false;
if (res.data && Array.isArray(res.data)) {
targetOption.children = res.data.map((item: any) => ({
@ -136,7 +145,8 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
targetOption.children = [];
}
setCascaderOptions([...cascaderOptions]);
}).catch((err) => {
})
.catch((err) => {
console.error('加载知识点失败:', err);
targetOption.loading = false;
targetOption.children = [];
@ -366,7 +376,9 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
}
}
setLoading(true);
question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => {
question
.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
@ -535,10 +547,7 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label="关联知识点"
name="knowledge_cascader"
>
<Form.Item label="关联知识点" name="knowledge_cascader">
<Cascader
options={cascaderOptions}
loadData={loadKnowledgeData as CascaderProps<CascaderOption>['loadData']}
@ -551,7 +560,9 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
filter: (inputValue: string, path: CascaderOption[]) =>
path.some(
(option) =>
(option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1
(option.label as string)
.toLowerCase()
.indexOf(inputValue.toLowerCase()) > -1
),
}}
/>