ai-course/app/backend/src/pages/exam/questions/compenents/detail-update.tsx

649 lines
20 KiB
TypeScript
Raw Normal View History

2025-11-18 13:32:46 +08:00
import React, { useState, useEffect } from 'react';
import { Modal, Form, Radio, Spin, message, Cascader } from 'antd';
import type { CascaderProps } from 'antd';
import { question, textbook } from '../../../../api/index';
2025-11-18 13:32:46 +08:00
import { QuestionInput } from '../../../../compenents';
import { QChoice } from './choice';
import { QSelect } from './select';
import { QInput } from './input';
import { QJudge } from './judge';
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;
}
2025-11-18 13:32:46 +08:00
interface PropInterface {
id: number;
qid: number;
open: boolean;
onCancel: () => void;
}
export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [init, setInit] = useState(true);
const [loading, setLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const [type, setType] = useState('1');
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [cascaderOptions, setCascaderOptions] = useState<CascaderOption[]>([]); // 级联选择器选项
2025-11-18 13:32:46 +08:00
const [formParams, setFormParams] = useState({
v: 'v1',
d: {
content: null,
remark: null,
},
});
useEffect(() => {
setInit(true);
if (open && qid > 0) {
getDetail();
}
}, [form, open, id, qid]);
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);
2025-11-18 13:32:46 +08:00
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);
}
}
2025-11-18 13:32:46 +08:00
form.setFieldsValue({
level: data.level,
type: String(data.type),
content: params.d.content,
remark: params.d.remark,
knowledge_cascader: cascaderValue,
2025-11-18 13:32:46 +08:00
});
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]);
2025-11-18 13:32:46 +08:00
});
};
const onFinish = (values: any) => {
if (loading) {
return;
}
const obj: any = formParams;
if (!obj.d.content) {
message.error(t('exam.question.detail.edit.text1'));
return;
}
if (!obj.d.content.text) {
message.error(t('exam.question.detail.edit.text2'));
return;
}
if ((type === '2' || type === '1') && !obj.d.answer) {
message.error(t('exam.question.detail.edit.text3'));
return;
}
if (type === '2' || type === '1') {
let isOk = true;
for (const key in obj.d.options) {
// 判断属性值是否为null
if (obj.d.options[key] === null) {
message.error(
t('exam.question.detail.edit.text4') + key + t('exam.question.detail.edit.text5')
);
isOk = false;
break;
}
if (
obj.d.options[key] &&
!obj.d.options[key].text &&
obj.d.options[key].image.length === 0 &&
obj.d.options[key].audio.length === 0 &&
obj.d.options[key].video.length === 0
) {
message.error(
t('exam.question.detail.edit.text4') + key + t('exam.question.detail.edit.text5')
);
isOk = false;
break;
}
}
if (!isOk) {
return;
}
}
if (type === '2' && obj.d.answer && obj.d.answer.length === 0) {
message.error(t('exam.question.detail.edit.text6'));
return;
}
if (type === '3' && !obj.d.answer) {
message.error(t('exam.question.detail.edit.text7'));
return;
}
if (type === '3' && obj.d.answer && obj.d.answer.items && obj.d.answer.items.length === 0) {
message.error(t('exam.question.detail.edit.text7'));
return;
}
if (type === '3' && obj.d.answer && obj.d.answer.items && obj.d.answer.items.length > 0) {
let isOk = true;
obj.d.answer.items.map((item: any) => {
if (item.items.length === 0) {
isOk = false;
} else {
item.items.map((it: any) => {
if (it === '') {
isOk = false;
}
});
}
});
if (!isOk) {
message.error(t('exam.question.detail.edit.text8'));
return;
}
}
if (type === '4' && obj.d.answer === undefined) {
message.error(t('exam.question.detail.edit.text9'));
return;
}
if (type === '4' && obj.d.answer && obj.d.answer.length === 0) {
message.error(t('exam.question.detail.edit.text3'));
return;
}
if (type === '5' && !obj.d.answer) {
message.error(t('exam.question.detail.edit.text10'));
return;
}
if (type === '5' && obj.d.answer && obj.d.answer.length === 0) {
message.error(t('exam.question.detail.edit.text10'));
return;
}
if (type === '5' && obj.d.answer && obj.d.answer.length > 0) {
let isOk = true;
obj.d.answer.map((item: any) => {
if (item.keywords.length === 0) {
isOk = false;
} else {
item.keywords.map((it: any) => {
if (it === '') {
isOk = false;
}
});
}
});
if (!isOk) {
message.error(t('exam.question.detail.edit.text11'));
return;
}
}
if (type === '6' && !obj.d.children) {
message.error(t('exam.question.detail.edit.text12'));
return;
}
if (type === '6' && obj.d.children === '[]') {
message.error(t('exam.question.detail.edit.text12'));
return;
}
if (type === '6' && obj.d.children) {
const result = JSON.parse(obj.d.children);
let isOk = true;
result.map((item: any) => {
if (!item.raw.d.content) {
isOk = false;
}
if (item.raw.d.content && !item.raw.d.content.text) {
isOk = false;
}
if ((item.type === 1 || item.type === 2 || item.type === 3) && !item.raw.d.answer) {
isOk = false;
}
if (item.type === 2 || item.type === 1) {
for (const key in item.raw.d.options) {
// 判断属性值是否为null
if (item.raw.d.options[key] === null) {
isOk = false;
break;
}
if (
item.raw.d.options[key] &&
!item.raw.d.options[key].text &&
item.raw.d.options[key].image.length === 0 &&
item.raw.d.options[key].audio.length === 0 &&
item.raw.d.options[key].video.length === 0
) {
isOk = false;
break;
}
}
}
if (item.type === 2 && item.raw.d.answer && item.raw.d.answer.length === 0) {
isOk = false;
}
if (
item.type === 3 &&
item.raw.d.answer &&
item.raw.d.answer.items &&
item.raw.d.answer.items.length === 0
) {
isOk = false;
}
if (
item.type === 3 &&
item.raw.d.answer &&
item.raw.d.answer.items &&
item.raw.d.answer.items.length > 0
) {
item.raw.d.answer.items.map((item: any) => {
if (item.items.length === 0) {
isOk = false;
} else {
item.items.map((it: any) => {
if (it === '') {
isOk = false;
}
});
}
});
}
if (item.type === 4 && item.raw.d.answer === undefined) {
isOk = false;
}
if (item.type === 4 && item.raw.d.answer && item.raw.d.answer.length === 0) {
isOk = false;
}
if (item.type === 5 && !item.raw.d.answer) {
isOk = false;
}
if (item.type === 5 && item.raw.d.answer && item.raw.d.answer.length === 0) {
isOk = false;
}
if (item.type === 5 && item.raw.d.answer && item.raw.d.answer.length > 0) {
item.raw.d.answer.map((item: any) => {
if (item.keywords.length === 0) {
isOk = false;
} else {
item.keywords.map((it: any) => {
if (it === '') {
isOk = false;
}
});
}
});
}
});
if (!isOk) {
message.error(t('exam.question.detail.edit.text13'));
return;
}
}
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(',');
}
}
2025-11-18 13:32:46 +08:00
setLoading(true);
question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => {
2025-11-18 13:32:46 +08:00
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const changeQuestionContent = (
text: string,
image: number[],
video: number[],
audio: number[]
) => {
const params = {
text: text,
image: image,
video: video,
audio: audio,
};
const obj: any = { ...formParams };
obj.d.content = params;
setFormParams(obj);
form.setFieldsValue({
content: params,
});
};
const changeQuestionRemark = (
text: string,
image: number[],
video: number[],
audio: number[]
) => {
const params = {
text: text,
image: image,
video: video,
audio: audio,
};
const obj: any = { ...formParams };
obj.d.remark = params;
setFormParams(obj);
};
const change = (question: any, list: any) => {
const obj = { ...formParams };
Object.assign(obj.d, question);
setFormParams(obj);
};
const choiceRender = (data: any) => {
if (data.options && data.answer) {
return {
options: data.options,
answer: data.answer,
};
} else {
return null;
}
};
const selectRender = (data: any) => {
if (data.options && data.answer) {
return {
options: data.options,
answer: data.answer,
};
} else {
return null;
}
};
const inputRender = (data: any) => {
if (data.answer) {
return {
answer: data.answer,
};
} else {
return null;
}
};
const judgeRender = (data: any) => {
if (data) {
return {
answer: data.answer,
};
} else {
return null;
}
};
const qaRender = (data: any) => {
if (data.answer) {
return {
answer: data.answer,
};
} else {
return null;
}
};
const capRender = (data: any) => {
if (data.children) {
return {
children: data.children,
};
} else {
return null;
}
};
return (
<>
{open ? (
<Modal
title={t('exam.question.detail.update')}
centered
forceRender
open={true}
width={1000}
onOk={() => form.submit()}
onCancel={() => onCancel()}
maskClosable={false}
okButtonProps={{ loading: loading }}
>
{init && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
{!init && (
<>
<div className="exam-tabs"></div>
<div className="float-left mt-10">
<Form
form={form}
name="question-detail-create"
labelCol={{ span: 2 }}
wrapperCol={{ span: 22 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label={t('exam.question.detail.edit.level')}
name="level"
rules={[
{
required: true,
message: t('exam.question.detail.edit.levelPlaceholder'),
},
]}
>
<Radio.Group>
<Radio value={1}>{t('exam.question.level1')}</Radio>
<Radio value={2} style={{ marginLeft: 8 }}>
{t('exam.question.level2')}
</Radio>
<Radio value={3} style={{ marginLeft: 8 }}>
{t('exam.question.level3')}
</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>
2025-11-18 13:32:46 +08:00
<Form.Item
label={t('exam.question.detail.edit.name')}
name="content"
labelCol={{ style: { marginTop: 4, marginLeft: 28 } }}
rules={[
{
required: true,
message: t('exam.question.detail.edit.namePlaceholder'),
},
]}
>
<QuestionInput
defautValue={formParams.d.content}
resourceUrl={resourceUrl}
height={40}
placeholder={t('exam.question.questionPlaceholder')}
setContent={(text, image, video, audio) => {
changeQuestionContent(text, image, video, audio);
}}
></QuestionInput>
</Form.Item>
{type === '1' && (
<QChoice
question={choiceRender(formParams.d)}
resourceUrl={resourceUrl}
index={null}
onChange={(question: any, list: any) => change(question, list)}
></QChoice>
)}
{type === '2' && (
<QSelect
question={selectRender(formParams.d)}
index={null}
resourceUrl={resourceUrl}
onChange={(question: any, list: any) => change(question, list)}
></QSelect>
)}
{type === '3' && (
<QInput
question={inputRender(formParams.d)}
index={null}
onChange={(question: any, list: any) => change(question, list)}
></QInput>
)}
{type === '4' && (
<QJudge
question={judgeRender(formParams.d)}
index={null}
onChange={(question: any, list: any) => change(question, list)}
></QJudge>
)}
{type === '5' && (
<QQa
question={qaRender(formParams.d)}
index={null}
onChange={(question: any, list: any) => change(question, list)}
></QQa>
)}
{type === '6' && (
<QCap
question={capRender(formParams.d)}
resourceUrl={resourceUrl}
index={null}
onChange={(question: any, list: any) => change(question, list)}
></QCap>
)}
<Form.Item
label={t('exam.question.detail.edit.remark')}
name="remark"
labelCol={{ style: { marginTop: 4, marginLeft: 39 } }}
>
<QuestionInput
defautValue={formParams.d.remark}
resourceUrl={resourceUrl}
height={120}
placeholder={t('exam.question.questionPlaceholder4')}
setContent={(text, image, video, audio) => {
changeQuestionRemark(text, image, video, audio);
}}
></QuestionInput>
</Form.Item>
</Form>
</div>
</>
)}
</Modal>
) : null}
</>
);
};