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 c463bc9..86fcb9d 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 @@ -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 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 knowledgeCodes = + examQuestionService.getDistinctKnowledgeCodes(categoryIds, type, level); + if (knowledgeCodes.isEmpty()) { + return JsonResponse.data(List.of()); + } + + // 查询知识点名称 + List knowledgeList = knowledgeService.getByKnowledgeCodes(knowledgeCodes); + Map codeToName = + knowledgeList.stream() + .collect( + Collectors.toMap( + Knowledge::getKnowledgeCode, + Knowledge::getName, + (v1, v2) -> v1)); + + // 构建返回结果 + List> result = + knowledgeCodes.stream() + .map( + code -> { + Map item = new HashMap<>(); + item.put("code", code); + item.put("name", codeToName.getOrDefault(code, code)); + return item; + }) + .collect(Collectors.toList()); + + return JsonResponse.data(result); + } } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java index 11a113c..15a77a3 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java @@ -961,41 +961,53 @@ public class PaperController { JSONObject source = randomRules.getJSONObject("source"); List 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); } diff --git a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java index 47a18ef..c4e472a 100644 --- a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java +++ b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java @@ -12,14 +12,26 @@ public class ExamQuestionFilter { private List 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; } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java index dbf4149..8322572 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java @@ -20,5 +20,12 @@ public interface IKnowledgeService extends IService { JSONObject getByIdVo(Integer id); JSONObject stuGetByIdVo(Integer id); - + /** + * 根据知识点编码列表批量查询知识点 + * + * @param codes 知识点编码列表 + * @return 知识点列表 + * @author menft + */ + List getByKnowledgeCodes(List codes); } 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 dbfd480..aff443a 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 @@ -217,6 +217,16 @@ public class KnowledgeServiceImpl extends ServiceImpl getByKnowledgeCodes(List codes) { + if (codes == null || codes.isEmpty()) { + return new ArrayList<>(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(Knowledge::getKnowledgeCode, codes); + return list(queryWrapper); + } + /** * 递增编码 * @param code 当前编码 diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java index 98b62cd..377b5dd 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java @@ -22,4 +22,6 @@ public interface ExamQuestionMapper extends BaseMapper { Long paginateCount(ExamQuestionPaginateFilter filter); List chunksByCategoryIdAndLimit(ExamQuestionFilter filter); + + List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level); } 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 018daa7..a823ac4 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 @@ -43,4 +43,11 @@ public interface ExamQuestionService extends IService { List chunksByCategoryId(Integer categoryId); List chunksByCategoryIdAndLimit(ExamQuestionFilter filter); + + /** + * 根据题库ID、题型、难度获取去重的知识点编码列表 + * + * @author menft + */ + List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level); } 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 4affbb9..bfbec1a 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 @@ -116,4 +116,22 @@ public class ExamQuestionServiceImpl extends ServiceImpl chunksByCategoryIdAndLimit(ExamQuestionFilter filter) { return getBaseMapper().chunksByCategoryIdAndLimit(filter); } + + @Override + public List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level) { + // 获取所有 knowledge_code 字段(可能是逗号分隔的多个值) + List 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()); + } } diff --git a/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml b/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml index b949770..57522bb 100644 --- a/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml +++ b/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml @@ -107,6 +107,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type1Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type1Number}) @@ -122,6 +132,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type2Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type2Number}) @@ -137,6 +157,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type3Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type3Number}) @@ -152,6 +182,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type4Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type4Number}) @@ -167,6 +207,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type5Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type5Number}) @@ -182,8 +232,36 @@ #{categoryId} + + and `exam_question`.`level` = #{type6Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type6Number}) + + diff --git a/app/backend/CLAUDE.md b/app/backend/CLAUDE.md new file mode 100644 index 0000000..76981f3 --- /dev/null +++ b/app/backend/CLAUDE.md @@ -0,0 +1,207 @@ +[根目录](../../CLAUDE.md) > **app/backend** + +--- + +# 管理后台前端(PlayEdu Backend) + +## 变更记录 (Changelog) + +### 2025-11-24 22:45:26 +- 初始化模块文档 + +--- + +## 模块职责 + +PlayEdu 管理后台是供平台管理员使用的 Web 应用,提供: +- 课程管理(线上课程、线下课程、实验课程、软件模块) +- 教材与知识点管理 +- 考试与题库管理 +- 学员与部门管理 +- 资源库管理(视频、文档、音频、图片、附件) +- AI 知识库管理 +- 系统配置与权限管理 +- 数据统计与日志查看 + +--- + +## 入口与启动 + +**主入口文件**:`src/main.tsx` + +**启动命令**: +```bash +cd app/backend +npm install +npm run dev +``` + +**构建命令**: +```bash +npm run build +``` + +**开发服务器**:`http://localhost:5173`(Vite 默认端口) + +**生产构建输出**:`dist/` + +--- + +## 对外接口 + +### API 通信 +- **API Base URL**:配置在 `src/api/internal/httpClient.ts` +- **认证方式**:Bearer Token(存储在 localStorage) + +### ⚠️ httpClient 响应数据结构(重要) + +**httpClient 已经解包了一层**,调用 `client.get/post/put` 返回的是 `response.data`,即: + +```typescript +// 后端原始响应 +{ "code": 0, "msg": "", "data": [...] } + +// httpClient.get() 返回的 res 就是上面这个对象 +const res = await client.get('/api/xxx'); +// res = { code: 0, msg: "", data: [...] } +// res.data = [...] ✅ 正确 +// res.data.data ❌ 错误!多解了一层 +``` + +**正确用法**: +```typescript +const res: any = await someApi.getData(); +const items = res.data || []; // ✅ 直接用 res.data +``` + +**错误用法**: +```typescript +const res: any = await someApi.getData(); +const items = res.data.data || []; // ❌ 多解了一层,永远是 undefined +``` + +### 主要 API 模块(位于 `src/api/`): + - `login.ts` - 管理员登录与认证 + - `course.ts` - 课程管理 + - `paper.ts` / `question.ts` - 考试与题库 + - `user.ts` / `department.ts` - 学员与部门 + - `resource.ts` / `resource-category.ts` - 资源管理 + - `knowledge-*.ts` - AI 知识库 + - `system.ts` / `app-config.ts` - 系统配置 + - `upload.ts` - 文件上传(支持 S3/OSS) + +### 路由结构 +- **公开路由**:`/login` - 登录页 +- **管理员路由**(需认证): + - `/dashboard` - 数据看板 + - `/course/*` - 课程管理 + - `/offline-course/*` - 线下课程 + - `/lab-course/*` - 实验课程 + - `/textbook/*` - 教材管理 + - `/exam/*` - 考试管理 + - `/question/*` - 题库管理 + - `/resource/*` - 资源库 + - `/repository/*` - 仓库管理 + - `/knowledge/*` - AI 知识库 + - `/user/*` - 学员管理 + - `/department/*` - 部门管理 + - `/system/*` - 系统配置 + +--- + +## 关键依赖与配置 + +### 技术栈 +- **框架**:React 18.2 + TypeScript 4.9 +- **构建工具**:Vite 7.1.3 +- **UI 库**:Ant Design 5.12.2 +- **状态管理**:Redux Toolkit + React-Redux +- **路由**:React Router DOM 6.9 +- **HTTP 客户端**:Axios 1.3.4 +- **富文本编辑器**:Quill 2.0.3 + Braft Editor +- **图表**:ECharts 5.4.2 + ECharts for React +- **视频播放器**:XGPlayer 3.0.13 + HLS 支持 +- **文件上传**:Uppy 4.x(支持 AWS S3) +- **国际化**:i18next + react-i18next +- **工具库**:Day.js、Moment.js、Lodash、XLSX、FileSaver + +### 配置文件 +- `package.json` - 依赖与脚本定义 +- `vite.config.ts` - Vite 构建配置(未找到,可能使用默认配置) +- `tsconfig.json` - TypeScript 配置 +- `eslint.config.js` - ESLint 配置(使用 Prettier 集成) + +### 代码规范工具 +- ESLint 9.x(@typescript-eslint/eslint-plugin) +- Prettier 3.6.2 +- 格式化命令:`npm run format` + +--- + +## 数据模型 + +### Redux Store 结构(`src/store/`) +- `user.ts` / `userSlice` - 当前管理员信息 +- `system.ts` / `systemSlice` - 系统配置(API URL、logo、名称等) +- `resource.ts` / `resourceSlice` - 资源 URL 映射 + +### 本地存储(localStorage) +- `token` - 认证 Token +- `api_url` - API 服务器地址 +- `system_name` - 系统名称 +- `language` - 界面语言(zh-CN / zh-TC) + +--- + +## 测试与质量 + +- **代码检查**:ESLint(`npm run lint` / `npm run lint:quiet`) +- **代码格式化**:Prettier(`npm run format`) +- **类型检查**:TypeScript 严格模式 +- **浏览器兼容性**:通过 Vite 自动处理 polyfills + +--- + +## 常见问题 (FAQ) + +### Q1:如何添加新的管理页面? +1. 在 `src/pages/` 下创建新页面组件 +2. 在路由配置中添加路由(通常在 `App.tsx` 或独立的路由文件中) +3. 添加对应的 API 调用(在 `src/api/` 中) +4. 更新导航菜单(如果需要) + +### Q2:如何配置 API 服务器地址? +- 开发环境:在 `src/api/internal/httpClient.ts` 中配置 `baseURL` +- 生产环境:通过环境变量或运行时配置 + +### Q3:文件上传如何工作? +- 使用 Uppy 组件(`@uppy/react`) +- 支持直传到 S3/OSS(通过 `@uppy/aws-s3`) +- 上传接口:`POST /backend/v1/upload/*` + +### Q4:如何支持国际化? +- 配置文件:`src/i18n/config.ts` +- 语言包:`src/i18n/locales/` 目录 +- 使用 `useTranslation()` Hook 进行翻译 + +--- + +## 相关文件清单 + +### 配置文件 +- `app/backend/package.json` - NPM 依赖配置 +- `app/backend/README.md` - 模块说明 + +### 核心代码 +- `src/main.tsx` - 应用入口 +- `src/App.tsx` - 根组件与路由配置 +- `src/api/` - API 接口定义 +- `src/store/` - Redux 状态管理 +- `src/pages/` - 页面组件 +- `src/compenents/` - 可复用组件 +- `src/assets/` - 静态资源(图片、字体、样式) + +### 样式文件 +- `src/index.less` - 全局样式 +- `src/App.module.less` - 根组件样式 +- 组件样式:`*.module.less` / `*.module.scss` diff --git a/app/backend/src/api/question.ts b/app/backend/src/api/question.ts index 3f8c254..d747253 100644 --- a/app/backend/src/api/question.ts +++ b/app/backend/src/api/question.ts @@ -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, + }); +} diff --git a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx index f18be2f..00b072f 100644 --- a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx +++ b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx @@ -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 = ({ 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>({ + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], }); useEffect(() => { @@ -91,6 +120,24 @@ export const RendomPaper: React.FC = ({ 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); + // httpClient.get 已经 resolve(res.data),所以 res = {code, msg, data} + 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 +234,8 @@ export const RendomPaper: React.FC = ({ 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 +243,32 @@ export const RendomPaper: React.FC = ({ 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 +276,33 @@ export const RendomPaper: React.FC = ({ 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 +689,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')} +
+ 难度 + { + const obj = { ...choice }; + obj.knowledge_codes = values.join(','); + setChoice(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(1, choice.level)} + /> +
{t('exam.paper.compose.text1')} @@ -619,7 +735,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q2 > 0 && (
-
+
* {t('exam.question.select.label')}({t('exam.paper.compose.text1')} @@ -679,6 +795,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text5')}
+
+ 难度 + { + const obj = { ...select }; + obj.knowledge_codes = values.join(','); + setSelect(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(2, select.level)} + /> +
{t('exam.paper.compose.text1')} @@ -689,7 +841,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q3 > 0 && (
-
+
* {t('exam.question.input.label')}({t('exam.paper.compose.text1')} @@ -730,6 +882,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...input }; + obj.knowledge_codes = values.join(','); + setInput(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(3, input.level)} + /> +
{t('exam.paper.compose.text1')} @@ -740,7 +928,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q4 > 0 && (
-
+
* {t('exam.question.judge.label')}({t('exam.paper.compose.text1')} @@ -781,6 +969,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...judge }; + obj.knowledge_codes = values.join(','); + setJudge(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(4, judge.level)} + /> +
{t('exam.paper.compose.text1')} @@ -791,7 +1015,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q5 > 0 && (
-
+
* {t('exam.question.qa.label')}({t('exam.paper.compose.text1')} @@ -832,6 +1056,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...qa }; + obj.knowledge_codes = values.join(','); + setQa(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(5, qa.level)} + /> +
{t('exam.paper.compose.text1')} @@ -842,7 +1102,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q6 > 0 && (
-
+
* {t('exam.question.cap.label')}({t('exam.paper.compose.text1')} @@ -883,6 +1143,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...cap }; + obj.knowledge_codes = values.join(','); + setCap(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(6, cap.level)} + /> +
{t('exam.paper.compose.text1')} diff --git a/app/backend/src/pages/exam/questions/compenents/detail-update.tsx b/app/backend/src/pages/exam/questions/compenents/detail-update.tsx index 755b5e3..14b2390 100644 --- a/app/backend/src/pages/exam/questions/compenents/detail-update.tsx +++ b/app/backend/src/pages/exam/questions/compenents/detail-update.tsx @@ -78,14 +78,18 @@ export const QuestionsDetailUpdate: React.FC = ({ 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 = ({ 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,24 +131,27 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, 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 { + 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]); - }).catch((err) => { - console.error('加载知识点失败:', err); - targetOption.loading = false; - targetOption.children = []; - setCascaderOptions([...cascaderOptions]); - }); + setCascaderOptions([...cascaderOptions]); + }); }; const onFinish = (values: any) => { @@ -366,11 +376,13 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } } setLoading(true); - question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => { - setLoading(false); - message.success(t('commen.saveSuccess')); - onCancel(); - }); + question + .questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode) + .then((res: any) => { + setLoading(false); + message.success(t('commen.saveSuccess')); + onCancel(); + }); }; const onFinishFailed = (errorInfo: any) => { @@ -535,10 +547,7 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, - + ['loadData']} @@ -551,7 +560,9 @@ export const QuestionsDetailUpdate: React.FC = ({ 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 ), }} />