From f7e04de5e5ad25344fa19eddd5a69cae522f3388 Mon Sep 17 00:00:00 2001 From: menft <17554333016@163.com> Date: Sat, 29 Nov 2025 22:31:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(exam):=20=E8=AF=95=E9=A2=98=E5=85=B3?= =?UTF-8?q?=E8=81=94=E7=9F=A5=E8=AF=86=E7=82=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:ExamQuestion 实体新增 knowledge_code 字段存储关联知识点 - 后端:TextbookController 新增教材下拉列表、知识点列表接口 - 后端:KnowledgeController 新增按编码列表查询知识点接口 - 前端:试题创建/编辑页面增加教材-知识点两级级联选择器 - 支持多选知识点,编辑时自动回显已关联知识点 --- .../backend/exam/QuestionController.java | 6 + .../backend/jc/KnowledgeController.java | 16 +++ .../backend/jc/TextbookController.java | 27 +++- .../backend/exam/ExamQuestionRequest.java | 3 + .../paginate/ExamQuestionPaginateFilter.java | 2 + .../java/xyz/playedu/jc/domain/Knowledge.java | 3 +- .../jc/service/impl/KnowledgeServiceImpl.java | 3 +- .../xyz/playedu/exam/domain/ExamQuestion.java | 9 ++ .../exam/service/ExamQuestionService.java | 8 +- .../service/impl/ExamQuestionServiceImpl.java | 10 +- .../resources/mapper/ExamQuestionMapper.xml | 9 +- app/backend/src/api/question.ts | 17 ++- app/backend/src/api/textbook.ts | 19 +++ .../src/compenents/add-question/index.tsx | 86 +++++++++++- .../questions/compenents/detail-create.tsx | 89 +++++++++++- .../questions/compenents/detail-update.tsx | 128 +++++++++++++++++- 16 files changed, 410 insertions(+), 25 deletions(-) diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java index 031fbe5..c463bc9 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java @@ -74,6 +74,7 @@ public class QuestionController { String content = MapUtils.getString(params, "content"); Integer level = MapUtils.getInteger(params, "level"); Integer type = MapUtils.getInteger(params, "type"); + String knowledgeCode = MapUtils.getString(params, "knowledge_code"); ExamQuestionPaginateFilter filter = new ExamQuestionPaginateFilter(); filter.setSortAlgo(sortAlgo); @@ -82,6 +83,7 @@ public class QuestionController { filter.setContent(content); filter.setLevel(level); filter.setType(type); + filter.setKnowledgeCode(knowledgeCode); if (!backendBus.isSuperAdmin()) { // 非超管只能读取它自己的题库 filter.setAdminIds(backendBus.getSameDataPermissionAdminIds()); @@ -125,6 +127,7 @@ public class QuestionController { String content = MapUtils.getString(params, "content"); Integer level = MapUtils.getInteger(params, "level"); Integer type = MapUtils.getInteger(params, "type"); + String knowledgeCode = MapUtils.getString(params, "knowledge_code"); List questionCategories = new ArrayList<>(); if (StringUtil.isNotNull(categoryId)) { @@ -147,6 +150,7 @@ public class QuestionController { filter.setContent(content); filter.setLevel(level); filter.setType(type); + filter.setKnowledgeCode(knowledgeCode); if (!backendBus.isSuperAdmin()) { // 非超管只能读取它自己的题库 filter.setAdminIds(backendBus.getSameDataPermissionAdminIds()); @@ -169,6 +173,7 @@ public class QuestionController { req.getCategoryId(), req.getContent().replaceAll(" ", ""), req.getLevel(), + req.getKnowledgeCode(), req.getType(), BCtx.getId()); @@ -198,6 +203,7 @@ public class QuestionController { examQuestion.getId(), req.getContent().replaceAll(" ", ""), req.getLevel(), + req.getKnowledgeCode(), req.getType(), req.getCategoryId(), BCtx.getId()); diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java index 3b5c2c3..dcef4f1 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java @@ -72,4 +72,20 @@ public class KnowledgeController { knowledgeService.remove(queryWrapper); return JsonResponse.success(); } + + /** + * 根据知识点编码列表获取知识点详情(用于编辑回显) + * @param codes 逗号分隔的知识点编码 + */ + @GetMapping("/byCodes") + public JsonResponse getByCodes(@RequestParam("codes") String codes) { + if (codes == null || codes.trim().isEmpty()) { + return JsonResponse.data(List.of()); + } + String[] codeArray = codes.split(","); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(Knowledge::getKnowledgeCode, (Object[]) codeArray); + List list = knowledgeService.list(queryWrapper); + return JsonResponse.data(list); + } } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java index 9314fc3..5525fae 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java @@ -1,7 +1,6 @@ package xyz.playedu.api.controller.backend.jc; import cn.hutool.core.util.ObjectUtil; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,7 +36,10 @@ import xyz.playedu.jc.service.ITextbookService; import xyz.playedu.jc.service.JCIResourceService; import xyz.playedu.knowledge.domain.KnowledgeMessages; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -273,6 +275,22 @@ public class TextbookController { return JsonResponse.data(list); } + /** + * 获取教材下拉选择列表(轻量级,仅返回id和title) + */ + @GetMapping("/selectList") + public JsonResponse selectList() { + List list = textbookService.list(); + List> result = new ArrayList<>(); + for (Textbook textbook : list) { + Map item = new HashMap<>(); + item.put("id", textbook.getId()); + item.put("title", textbook.getTitle()); + result.add(item); + } + return JsonResponse.data(result); + } + // @GetMapping("/{id}") // public JsonResponse detail(@PathVariable("id") Integer id) { // Textbook one = textbookService.getById(id); @@ -347,7 +365,4 @@ public class TextbookController { textbookService.updateById(textbook); return JsonResponse.success(); } - - - -} \ No newline at end of file +} diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java index bcc132d..4619857 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java @@ -24,6 +24,9 @@ public class ExamQuestionRequest implements Serializable { @NotNull(message = "level参数为空") private Integer level; + @JsonProperty("knowledge_code") + private String knowledgeCode; + @NotNull(message = "type参数为空") private Integer type; } diff --git a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java index bdec8f8..bfd27cb 100644 --- a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java +++ b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java @@ -17,6 +17,8 @@ public class ExamQuestionPaginateFilter { private String categoryId; + private String knowledgeCode; + private List adminIds; private String sortField; diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java index eb5214a..ffb35c4 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java @@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import xyz.playedu.framework.tenant.core.db.TenantBaseDO; - import java.util.Date; import java.util.List; @@ -38,7 +37,7 @@ public class Knowledge extends TenantBaseDO { private String knowledgeCode; /** 知识点介绍 */ - @TableField("desc") + @TableField("`desc`") private String desc; /** 层级 */ diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java index 3c39430..dbfd480 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java @@ -47,7 +47,8 @@ public class KnowledgeServiceImpl extends ServiceImpl listVo(KnowledgeParam param) { //获取知识点卡片 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(Knowledge::getBookId, param.getBookId()) + // 当bookId不为空时才加条件,否则查询所有知识点 + queryWrapper.eq(param.getBookId() != null, Knowledge::getBookId, param.getBookId()) .eq(Knowledge::getIsReal,"1").orderByAsc(Knowledge::getOrderNum); return list(queryWrapper); } diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java index 687f936..3af51b2 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java @@ -36,6 +36,10 @@ public class ExamQuestion extends TenantBaseDO { /** 难度等级:1-4 */ private Integer level; + /** 知识点CODE(多个用逗号分隔) */ + @JsonProperty("knowledge_code") + private String knowledgeCode; + /** 内容 */ private String content; @@ -78,6 +82,9 @@ public class ExamQuestion extends TenantBaseDO { && (this.getLevel() == null ? other.getLevel() == null : this.getLevel().equals(other.getLevel())) + && (this.getKnowledgeCode() == null + ? other.getKnowledgeCode() == null + : this.getKnowledgeCode().equals(other.getKnowledgeCode())) && (this.getContent() == null ? other.getContent() == null : this.getContent().equals(other.getContent())) @@ -101,6 +108,7 @@ public class ExamQuestion extends TenantBaseDO { result = prime * result + ((getAdminId() == null) ? 0 : getAdminId().hashCode()); result = prime * result + ((getType() == null) ? 0 : getType().hashCode()); result = prime * result + ((getLevel() == null) ? 0 : getLevel().hashCode()); + result = prime * result + ((getKnowledgeCode() == null) ? 0 : getKnowledgeCode().hashCode()); result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode()); result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode()); result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode()); @@ -119,6 +127,7 @@ public class ExamQuestion extends TenantBaseDO { sb.append(", adminId=").append(adminId); sb.append(", type=").append(type); sb.append(", level=").append(level); + sb.append(", knowledgeCode=").append(knowledgeCode); sb.append(", content=").append(content); sb.append(", createdAt=").append(createdAt); sb.append(", updatedAt=").append(updatedAt); diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java index 4dd6dda..018daa7 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java @@ -20,12 +20,18 @@ public interface ExamQuestionService extends IService { PaginationResult paginate(int page, int size, ExamQuestionPaginateFilter filter); Integer create( - Integer categoryId, String content, Integer level, Integer type, Integer adminId); + Integer categoryId, + String content, + Integer level, + String knowledgeCode, + Integer type, + Integer adminId); void update( Integer id, String content, Integer level, + String knowledgeCode, Integer type, Integer categoryId, Integer adminId); diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java index 9ef5681..4affbb9 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java @@ -50,13 +50,19 @@ public class ExamQuestionServiceImpl extends ServiceImpl + @@ -18,7 +19,7 @@ id,category_id,admin_id,type, - level,content,created_at, + level,knowledge_code,content,created_at, updated_at,deleted @@ -42,6 +43,9 @@ AND `exam_question`.`level` = #{level} + + AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`) + @@ -87,6 +91,9 @@ AND `exam_question`.`level` = #{level} + + AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`) + diff --git a/app/backend/src/api/question.ts b/app/backend/src/api/question.ts index db028cc..3f8c254 100644 --- a/app/backend/src/api/question.ts +++ b/app/backend/src/api/question.ts @@ -54,7 +54,8 @@ export function questionList( sortAlgo: string, content: string, level: any, - type: any + type: any, + knowledgeCode?: string ) { return client.get('/backend/v1/exam/question/index', { category_id, @@ -65,15 +66,23 @@ export function questionList( content, level, type, + knowledge_code: knowledgeCode, }); } -export function questionStore(category_id: number, content: string, level: any, type: any) { +export function questionStore( + category_id: number, + content: string, + level: any, + type: any, + knowledge_code?: string +) { return client.post('/backend/v1/exam/question/create', { category_id, content, level, type, + knowledge_code, }); } @@ -86,13 +95,15 @@ export function questionUpdate( category_id: number, content: string, level: any, - type: any + type: any, + knowledge_code?: string ) { return client.put(`/backend/v1/exam/question/${id}`, { category_id, content, level, type, + knowledge_code, }); } diff --git a/app/backend/src/api/textbook.ts b/app/backend/src/api/textbook.ts index d2cd50e..1f5ff48 100644 --- a/app/backend/src/api/textbook.ts +++ b/app/backend/src/api/textbook.ts @@ -229,6 +229,25 @@ export function EditChapterApi( }); } +/* + * Knowledge 知识点 + * */ + +// 获取知识点列表 +export function getKnowledgeListApi(bookId?: number) { + return client.get('/backend/v1/jc/knowledge/list', { bookId: bookId }); +} + +// 获取教材下拉列表(不分页,用于选择器) +export function getTextbookSelectListApi() { + return client.get('/backend/v1/jc/textbook/selectList', {}); +} + +// 根据知识点编码列表获取知识点详情(用于编辑回显) +export function getKnowledgeByCodesApi(codes: string) { + return client.get('/backend/v1/jc/knowledge/byCodes', { codes }); +} + /* * resource List * */ diff --git a/app/backend/src/compenents/add-question/index.tsx b/app/backend/src/compenents/add-question/index.tsx index 28583af..81e60a0 100644 --- a/app/backend/src/compenents/add-question/index.tsx +++ b/app/backend/src/compenents/add-question/index.tsx @@ -13,7 +13,7 @@ import { Spin, message, } from 'antd'; -import { question, resourceCategory } from '../../api'; +import { question, resourceCategory, textbook } from '../../api'; import type { ColumnsType } from 'antd/es/table'; import styles from './index.module.less'; import { TreeQuestion } from '../../compenents'; @@ -63,6 +63,11 @@ export const AddQuestion = (props: PropsInterface) => { const [selectedIds, setSelectedIds] = useState([]); const [selectVideos, setSelectVideos] = useState([]); const [categories, setCategories] = useState([]); + const [textbookList, setTextbookList] = useState([]); // 教材列表 + const [selectedTextbookId, setSelectedTextbookId] = useState(undefined); // 选中的教材ID + const [knowledgeCode, setKnowledgeCode] = useState(''); + const [knowledgeList, setKnowledgeList] = useState([]); + const [knowledgeLoading, setKnowledgeLoading] = useState(false); // 知识点加载状态 const [resourceUrl, setResourceUrl] = useState({}); const types = [ { label: t('exam.question.choice.label2'), value: 1 }, @@ -92,10 +97,43 @@ export const AddQuestion = (props: PropsInterface) => { const initData = async () => { await getCategory(); + await getTextbookList(); await getList(); setInit(false); }; + // 加载教材列表 + const getTextbookList = async () => { + try { + const res: any = await textbook.getTextbookSelectListApi(); + if (res.data && Array.isArray(res.data)) { + setTextbookList(res.data); + } + } catch (err) { + console.error('加载教材列表失败:', err); + } + }; + + // 当选择教材时,加载对应的知识点列表 + const handleTextbookChange = async (textbookId: number | undefined) => { + setSelectedTextbookId(textbookId); + setKnowledgeCode(''); // 清空已选知识点 + setKnowledgeList([]); + + if (textbookId) { + setKnowledgeLoading(true); + try { + const res: any = await textbook.getKnowledgeListApi(textbookId); + if (res.data && Array.isArray(res.data)) { + setKnowledgeList(res.data); + } + } catch (err) { + console.error('加载知识点列表失败:', err); + } + setKnowledgeLoading(false); + } + }; + const getCategory = async () => { const res: any = await resourceCategory.resourceCategoryList(); const categories = res.data.categories; @@ -122,7 +160,8 @@ export const AddQuestion = (props: PropsInterface) => { '', name, level.length === 0 ? '' : level, - type.length === 0 ? '' : type + type.length === 0 ? '' : type, + knowledgeCode || undefined ); setResourceUrl(res.data.resource_url); const data = res.data.result.data; @@ -152,6 +191,9 @@ export const AddQuestion = (props: PropsInterface) => { setName(''); setType([]); setLevel([]); + setSelectedTextbookId(undefined); + setKnowledgeCode(''); + setKnowledgeList([]); setSelectedRowKeys([]); setSelectedIds([]); setRefresh(!refresh); @@ -380,6 +422,46 @@ export const AddQuestion = (props: PropsInterface) => { placeholder={t('exam.question.detail.namePlaceholder2')} /> + +
+
+ 教材 + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + onChange={(value: any) => setKnowledgeCode(value || '')} + options={knowledgeList.map((item: any) => ({ + label: item.name, + value: item.knowledgeCode || item.knowledge_code, + }))} + /> +
diff --git a/app/backend/src/pages/exam/questions/compenents/detail-create.tsx b/app/backend/src/pages/exam/questions/compenents/detail-create.tsx index 3602e97..e45cab2 100644 --- a/app/backend/src/pages/exam/questions/compenents/detail-create.tsx +++ b/app/backend/src/pages/exam/questions/compenents/detail-create.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Form, Tabs, Radio, Spin, message } from 'antd'; +import { Modal, Form, Tabs, Radio, Spin, message, Cascader } from 'antd'; import type { TabsProps } from 'antd'; -import { question } from '../../../../api/index'; +import type { CascaderProps } from 'antd'; +import { question, textbook } from '../../../../api/index'; import { QuestionInput } from '../../../../compenents'; import { QChoice } from './choice'; import { QSelect } from './select'; @@ -11,6 +12,15 @@ import { QQa } from './qa'; import { QCap } from './cap'; import { useTranslation } from 'react-i18next'; +// 级联选择器选项类型 +interface CascaderOption { + value: string | number; + label: string; + children?: CascaderOption[]; + isLeaf?: boolean; + loading?: boolean; +} + interface PropInterface { id: number; open: boolean; @@ -24,6 +34,7 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan const [loading, setLoading] = useState(false); const [refresh, setRefresh] = useState(false); const [type, setType] = useState('1'); + const [cascaderOptions, setCascaderOptions] = useState([]); // 级联选择器选项 const [formParams, setFormParams] = useState({ v: 'v1', d: { @@ -38,6 +49,7 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan setType('1'); form.setFieldsValue({ level: 1, + knowledge_cascader: [], }); setFormParams({ v: 'v1', @@ -46,10 +58,49 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan remark: null, }, }); + // 加载教材列表作为级联选择器第一级 + textbook.getTextbookSelectListApi().then((res: any) => { + if (res.data && Array.isArray(res.data)) { + const options: CascaderOption[] = res.data.map((item: any) => ({ + value: item.id, + label: item.title, + isLeaf: false, // 表示有子节点 + })); + setCascaderOptions(options); + } + }).catch((err) => { + console.error('加载教材列表失败:', err); + }); setInit(false); } }, [form, open, id]); + // 级联选择器动态加载知识点 + const loadKnowledgeData = (selectedOptions: CascaderOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + targetOption.loading = true; + + // 加载该教材下的知识点 + 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) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, // 知识点是叶子节点 + })); + } else { + targetOption.children = []; + } + setCascaderOptions([...cascaderOptions]); + }).catch((err) => { + console.error('加载知识点失败:', err); + targetOption.loading = false; + targetOption.children = []; + setCascaderOptions([...cascaderOptions]); + }); + }; + const items: TabsProps['items'] = [ { key: '1', @@ -289,8 +340,19 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan } } const params = JSON.stringify(formParams); + // 处理知识点:从级联选择器值中提取知识点编码(第二级的值) + let knowledgeCode: string | undefined = undefined; + if (values.knowledge_cascader && values.knowledge_cascader.length > 0) { + // 级联选择器多选值格式: [[textbook_id, knowledge_code], ...] + const codes = values.knowledge_cascader + .filter((item: any[]) => item && item.length === 2) + .map((item: any[]) => item[1]); // 取第二级的值(知识点编码) + if (codes.length > 0) { + knowledgeCode = codes.join(','); + } + } setLoading(true); - question.questionStore(id, params, values.level, Number(type)).then((res: any) => { + question.questionStore(id, params, values.level, Number(type), knowledgeCode).then((res: any) => { setLoading(false); message.success(t('commen.saveSuccess')); onCancel(); @@ -413,6 +475,27 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan + + ['loadData']} + multiple + maxTagCount="responsive" + placeholder="请选择教材和知识点(可多选)" + style={{ width: '100%' }} + showCheckedStrategy={Cascader.SHOW_CHILD} + showSearch={{ + filter: (inputValue: string, path: CascaderOption[]) => + path.some( + (option) => + (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ), + }} + /> + = ({ id, qid, open, const [refresh, setRefresh] = useState(false); const [type, setType] = useState('1'); const [resourceUrl, setResourceUrl] = useState({}); + const [cascaderOptions, setCascaderOptions] = useState([]); // 级联选择器选项 const [formParams, setFormParams] = useState({ v: 'v1', d: { @@ -40,20 +51,96 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } }, [form, open, id, qid]); - const getDetail = () => { - question.questionDetail(qid).then((res: any) => { + const getDetail = async () => { + try { + // 1. 加载教材列表作为级联选择器第一级 + const textbookRes = await textbook.getTextbookSelectListApi(); + let options: CascaderOption[] = []; + if (textbookRes.data && Array.isArray(textbookRes.data)) { + options = textbookRes.data.map((item: any) => ({ + value: item.id, + label: item.title, + isLeaf: false, + })); + setCascaderOptions(options); + } + + // 2. 加载试题详情 + const res: any = await question.questionDetail(qid); setResourceUrl(res.data.resource_url); const data = res.data.question; setType(String(data.type)); const params = JSON.parse(res.data.question.content); + + // 3. 处理知识点回显 + let cascaderValue: (string | number)[][] = []; + if (data.knowledge_code) { + try { + // 获取知识点详情(包含bookId) + const knowledgeRes: any = await textbook.getKnowledgeByCodesApi(data.knowledge_code); + 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); + if (targetOption && knowledgeListRes.data && Array.isArray(knowledgeListRes.data)) { + targetOption.children = knowledgeListRes.data.map((item: any) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, + })); + } + } + setCascaderOptions([...options]); + + // 构建级联选择器的值 [[bookId, knowledgeCode], ...] + cascaderValue = knowledgeRes.data.map((k: any) => [k.bookId, k.knowledgeCode || k.knowledge_code]); + } + } catch (err) { + console.error('加载知识点详情失败:', err); + } + } + form.setFieldsValue({ level: data.level, type: String(data.type), content: params.d.content, remark: params.d.remark, + knowledge_cascader: cascaderValue, }); setFormParams(params); setInit(false); + } catch (err) { + console.error('加载数据失败:', err); + setInit(false); + } + }; + + // 级联选择器动态加载知识点 + const loadKnowledgeData = (selectedOptions: CascaderOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + targetOption.loading = true; + + 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) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, + })); + } else { + targetOption.children = []; + } + setCascaderOptions([...cascaderOptions]); + }).catch((err) => { + console.error('加载知识点失败:', err); + targetOption.loading = false; + targetOption.children = []; + setCascaderOptions([...cascaderOptions]); }); }; @@ -268,8 +355,18 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } } const params = JSON.stringify(formParams); + // 处理知识点:从级联选择器值中提取知识点编码(第二级的值) + let knowledgeCode: string | undefined = undefined; + if (values.knowledge_cascader && values.knowledge_cascader.length > 0) { + const codes = values.knowledge_cascader + .filter((item: any[]) => item && item.length === 2) + .map((item: any[]) => item[1]); + if (codes.length > 0) { + knowledgeCode = codes.join(','); + } + } setLoading(true); - question.questionUpdate(qid, id, params, values.level, Number(type)).then((res: any) => { + question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => { setLoading(false); message.success(t('commen.saveSuccess')); onCancel(); @@ -438,6 +535,27 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, + + ['loadData']} + multiple + maxTagCount="responsive" + placeholder="请选择教材和知识点(可多选)" + style={{ width: '100%' }} + showCheckedStrategy={Cascader.SHOW_CHILD} + showSearch={{ + filter: (inputValue: string, path: CascaderOption[]) => + path.some( + (option) => + (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ), + }} + /> +