feat(exam): 试题关联知识点功能 #1
@ -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<ExamQuestionCategory> 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());
|
||||
|
||||
@ -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<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(Knowledge::getKnowledgeCode, (Object[]) codeArray);
|
||||
List<Knowledge> list = knowledgeService.list(queryWrapper);
|
||||
return JsonResponse.data(list);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Textbook> list = textbookService.list();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Textbook textbook : list) {
|
||||
Map<String, Object> 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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ public class ExamQuestionPaginateFilter {
|
||||
|
||||
private String categoryId;
|
||||
|
||||
private String knowledgeCode;
|
||||
|
||||
private List<Integer> adminIds;
|
||||
|
||||
private String sortField;
|
||||
|
||||
@ -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;
|
||||
|
||||
/** 层级 */
|
||||
|
||||
@ -47,7 +47,8 @@ public class KnowledgeServiceImpl extends ServiceImpl<KnowledgeMapper, Knowledge
|
||||
public List<Knowledge> listVo(KnowledgeParam param) {
|
||||
//获取知识点卡片
|
||||
LambdaQueryWrapper<Knowledge> 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -20,12 +20,18 @@ public interface ExamQuestionService extends IService<ExamQuestion> {
|
||||
PaginationResult<ExamQuestion> 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);
|
||||
|
||||
@ -50,13 +50,19 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
|
||||
|
||||
@Override
|
||||
public Integer create(
|
||||
Integer categoryId, String content, Integer level, Integer type, Integer adminId) {
|
||||
Integer categoryId,
|
||||
String content,
|
||||
Integer level,
|
||||
String knowledgeCode,
|
||||
Integer type,
|
||||
Integer adminId) {
|
||||
ExamQuestion examQuestion =
|
||||
new ExamQuestion() {
|
||||
{
|
||||
setCategoryId(categoryId);
|
||||
setAdminId(adminId);
|
||||
setLevel(level);
|
||||
setKnowledgeCode(knowledgeCode);
|
||||
setType(type);
|
||||
setContent(content);
|
||||
setCreatedAt(new Date());
|
||||
@ -72,6 +78,7 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
|
||||
Integer id,
|
||||
String content,
|
||||
Integer level,
|
||||
String knowledgeCode,
|
||||
Integer type,
|
||||
Integer categoryId,
|
||||
Integer adminId) {
|
||||
@ -82,6 +89,7 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
|
||||
question.setCategoryId(categoryId);
|
||||
question.setContent(content);
|
||||
question.setLevel(level);
|
||||
question.setKnowledgeCode(knowledgeCode);
|
||||
question.setType(type);
|
||||
question.setUpdatedAt(new Date());
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
<result property="adminId" column="admin_id" jdbcType="INTEGER"/>
|
||||
<result property="type" column="type" jdbcType="INTEGER"/>
|
||||
<result property="level" column="level" jdbcType="TINYINT"/>
|
||||
<result property="knowledgeCode" column="knowledge_code" jdbcType="VARCHAR"/>
|
||||
<result property="content" column="content" jdbcType="VARCHAR"/>
|
||||
<result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
|
||||
<result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
|
||||
@ -18,7 +19,7 @@
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,category_id,admin_id,type,
|
||||
level,content,created_at,
|
||||
level,knowledge_code,content,created_at,
|
||||
updated_at,deleted
|
||||
</sql>
|
||||
|
||||
@ -42,6 +43,9 @@
|
||||
<if test="level != null">
|
||||
AND `exam_question`.`level` = #{level}
|
||||
</if>
|
||||
<if test="knowledgeCode != null and knowledgeCode != ''">
|
||||
AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`)
|
||||
</if>
|
||||
</where>
|
||||
|
||||
<if test="sortAlgo == 'asc'">
|
||||
@ -87,6 +91,9 @@
|
||||
<if test="level != null">
|
||||
AND `exam_question`.`level` = #{level}
|
||||
</if>
|
||||
<if test="knowledgeCode != null and knowledgeCode != ''">
|
||||
AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`)
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
* */
|
||||
|
||||
@ -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<any>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<any[]>([]);
|
||||
const [categories, setCategories] = useState<Option[]>([]);
|
||||
const [textbookList, setTextbookList] = useState<any[]>([]); // 教材列表
|
||||
const [selectedTextbookId, setSelectedTextbookId] = useState<number | undefined>(undefined); // 选中的教材ID
|
||||
const [knowledgeCode, setKnowledgeCode] = useState('');
|
||||
const [knowledgeList, setKnowledgeList] = useState<any[]>([]);
|
||||
const [knowledgeLoading, setKnowledgeLoading] = useState(false); // 知识点加载状态
|
||||
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
|
||||
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')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex mb-24">
|
||||
<div className="d-flex mr-16">
|
||||
<Typography.Text>教材</Typography.Text>
|
||||
<Select
|
||||
style={{ width: 150 }}
|
||||
placeholder="请选择教材"
|
||||
value={selectedTextbookId}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input: string, option: any) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={handleTextbookChange}
|
||||
options={textbookList.map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex mr-16">
|
||||
<Typography.Text>知识点</Typography.Text>
|
||||
<Select
|
||||
style={{ width: 150 }}
|
||||
placeholder={selectedTextbookId ? "请选择知识点" : "请先选择教材"}
|
||||
value={knowledgeCode || undefined}
|
||||
disabled={!selectedTextbookId}
|
||||
loading={knowledgeLoading}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input: string, option: any) =>
|
||||
(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,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<Button className="mr-16" onClick={resetList}>
|
||||
{t('commen.reset')}
|
||||
</Button>
|
||||
|
||||
@ -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<PropInterface> = ({ id, open, onCan
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [type, setType] = useState('1');
|
||||
const [cascaderOptions, setCascaderOptions] = useState<CascaderOption[]>([]); // 级联选择器选项
|
||||
const [formParams, setFormParams] = useState({
|
||||
v: 'v1',
|
||||
d: {
|
||||
@ -38,6 +49,7 @@ export const QuestionsDetailCreate: React.FC<PropInterface> = ({ id, open, onCan
|
||||
setType('1');
|
||||
form.setFieldsValue({
|
||||
level: 1,
|
||||
knowledge_cascader: [],
|
||||
});
|
||||
setFormParams({
|
||||
v: 'v1',
|
||||
@ -46,10 +58,49 @@ export const QuestionsDetailCreate: React.FC<PropInterface> = ({ 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<PropInterface> = ({ 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<PropInterface> = ({ id, open, onCan
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="关联知识点"
|
||||
name="knowledge_cascader"
|
||||
>
|
||||
<Cascader
|
||||
options={cascaderOptions}
|
||||
loadData={loadKnowledgeData as CascaderProps<CascaderOption>['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
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('exam.question.detail.edit.name')}
|
||||
name="content"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Radio, Spin, message } from 'antd';
|
||||
import { question } from '../../../../api/index';
|
||||
import { Modal, Form, Radio, Spin, message, Cascader } from 'antd';
|
||||
import type { CascaderProps } from 'antd';
|
||||
import { question, textbook } from '../../../../api/index';
|
||||
import { QuestionInput } from '../../../../compenents';
|
||||
import { QChoice } from './choice';
|
||||
import { QSelect } from './select';
|
||||
@ -10,6 +11,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;
|
||||
qid: number;
|
||||
@ -25,6 +35,7 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [type, setType] = useState('1');
|
||||
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
|
||||
const [cascaderOptions, setCascaderOptions] = useState<CascaderOption[]>([]); // 级联选择器选项
|
||||
const [formParams, setFormParams] = useState({
|
||||
v: 'v1',
|
||||
d: {
|
||||
@ -40,20 +51,96 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ 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<PropInterface> = ({ 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<PropInterface> = ({ id, qid, open,
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="关联知识点"
|
||||
name="knowledge_cascader"
|
||||
>
|
||||
<Cascader
|
||||
options={cascaderOptions}
|
||||
loadData={loadKnowledgeData as CascaderProps<CascaderOption>['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
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('exam.question.detail.edit.name')}
|
||||
name="content"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user