提交 da3f36f6 authored 作者: 王鹏飞's avatar 王鹏飞

fix(admin): restore question bank rich text editor

上级 8d6aaa09
...@@ -25,6 +25,7 @@ export const deleteQuestion = (id: string | number) => post("/book/question/del" ...@@ -25,6 +25,7 @@ export const deleteQuestion = (id: string | number) => post("/book/question/del"
export const addQuestionType = (typename: string) => post("/book/question/type/add", { typename }, true); export const addQuestionType = (typename: string) => post("/book/question/type/add", { typename }, true);
export const getQuestionTypes = () => post("/book/question/type/getList", { page: 1, page_size: 999 }); export const getQuestionTypes = () => post("/book/question/type/getList", { page: 1, page_size: 999 });
export const getBookNames = () => post("book/teacher/getListAll"); export const getBookNames = () => post("book/teacher/getListAll");
export const getAliOssToken = () => post("/common/OssUpload/getUploadToken");
export async function importQuestions(file: File) { export async function importQuestions(file: File) {
try { try {
const form = new FormData(); const form = new FormData();
......
...@@ -2,6 +2,7 @@ import { Button, Checkbox, Drawer, Form, Input, Select, Space } from "antd"; ...@@ -2,6 +2,7 @@ import { Button, Checkbox, Drawer, Form, Input, Select, Space } from "antd";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"; import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { addQuestion, addQuestionType, editQuestion } from "../api"; import { addQuestion, addQuestionType, editQuestion } from "../api";
import RichTextEditor from "./RichTextEditor";
type Props = { open: boolean; onClose: () => void; question: any; types: any[]; books: any[]; onTypesChange: () => void; onSaved: () => void }; type Props = { open: boolean; onClose: () => void; question: any; types: any[]; books: any[]; onTypesChange: () => void; onSaved: () => void };
const styles = ["单选题", "多选题", "判断题", "填空题", "简答题"]; const styles = ["单选题", "多选题", "判断题", "填空题", "简答题"];
export default function QuestionForm({ open, onClose, question, types, books, onTypesChange, onSaved }: Props) { export default function QuestionForm({ open, onClose, question, types, books, onTypesChange, onSaved }: Props) {
...@@ -31,10 +32,10 @@ export default function QuestionForm({ open, onClose, question, types, books, on ...@@ -31,10 +32,10 @@ export default function QuestionForm({ open, onClose, question, types, books, on
<Form.Item name="book_id" label="书籍名称" rules={[{ required: true, message: "请选择书籍名称" }]}><Select showSearch optionFilterProp="children" placeholder="请选择书籍名称">{books.map((book) => <Select.Option key={book.book_id} value={book.book_id}>{book.name}</Select.Option>)}</Select></Form.Item> <Form.Item name="book_id" label="书籍名称" rules={[{ required: true, message: "请选择书籍名称" }]}><Select showSearch optionFilterProp="children" placeholder="请选择书籍名称">{books.map((book) => <Select.Option key={book.book_id} value={book.book_id}>{book.name}</Select.Option>)}</Select></Form.Item>
<Form.Item name="question_style" label="题目类型" rules={[{ required: true, message: "请选择题目类型" }]}><Select placeholder="请选择题目类型">{styles.map((name, index) => <Select.Option key={index + 1} value={index + 1}>{name}</Select.Option>)}</Select></Form.Item> <Form.Item name="question_style" label="题目类型" rules={[{ required: true, message: "请选择题目类型" }]}><Select placeholder="请选择题目类型">{styles.map((name, index) => <Select.Option key={index + 1} value={index + 1}>{name}</Select.Option>)}</Select></Form.Item>
<Form.Item name="type_id" label="题目分类" rules={[{ required: true, message: "请选择题目分类" }]}>{addingType ? <Space><Input value={typeName} onChange={(e) => setTypeName(e.target.value)} placeholder="请输入题目分类"/><Button type="link" onClick={addType}>添加</Button><Button type="link" danger onClick={() => setAddingType(false)}>取消</Button></Space> : <Space.Compact><Select style={{ minWidth: 220 }} placeholder="请选择题目分类">{types.map((type) => <Select.Option key={type.id} value={type.id}>{type.typename}</Select.Option>)}</Select><Button type="link" onClick={() => setAddingType(true)}>添加分类</Button></Space.Compact>}</Form.Item> <Form.Item name="type_id" label="题目分类" rules={[{ required: true, message: "请选择题目分类" }]}>{addingType ? <Space><Input value={typeName} onChange={(e) => setTypeName(e.target.value)} placeholder="请输入题目分类"/><Button type="link" onClick={addType}>添加</Button><Button type="link" danger onClick={() => setAddingType(false)}>取消</Button></Space> : <Space.Compact><Select style={{ minWidth: 220 }} placeholder="请选择题目分类">{types.map((type) => <Select.Option key={type.id} value={type.id}>{type.typename}</Select.Option>)}</Select><Button type="link" onClick={() => setAddingType(true)}>添加分类</Button></Space.Compact>}</Form.Item>
<Form.Item name="titles" label="题干名称" rules={[{ required: true, message: "请输入题干名称" }]} extra={questionStyle === 4 ? "请在需要填空的位置使用2个或以上的下划线进行区分!" : ""}><Input.TextArea autoSize={{ minRows: 4, maxRows: 8 }} placeholder="请输入题干名称" /></Form.Item> <Form.Item name="titles" label="题干名称" rules={[{ required: true, message: "请输入题干名称" }]} extra={questionStyle === 4 ? "请在需要填空的位置使用2个或以上的下划线进行区分!" : ""}><RichTextEditor height={200} /></Form.Item>
<Form.Item name="analysis" label="题目解析"><Input.TextArea autoSize={{ minRows: 3, maxRows: 6 }} placeholder="请输入题目解析" /></Form.Item> <Form.Item name="analysis" label="题目解析"><Input.TextArea autoSize={{ minRows: 3, maxRows: 6 }} placeholder="请输入题目解析" /></Form.Item>
{needsOptions && <Form.List name="question_list">{(fields, { add, remove }) => <Form.Item label="题目选项">{fields.map((field) => <Space key={field.key} align="start" style={{ display: "flex", marginBottom: 8 }}><Form.Item {...field} name={[field.name, "option"]} rules={[{ required: true, message: "请输入内容" }]}><Input.TextArea placeholder="请输入选项" /></Form.Item><Form.Item {...field} name={[field.name, "correct"]} valuePropName="checked"><Checkbox>正确答案</Checkbox></Form.Item><Button icon={<MinusOutlined />} onClick={() => remove(field.name)} /></Space>)}{(questionStyle !== 3 || fields.length < 2) && <Button type="dashed" icon={<PlusOutlined />} onClick={() => add({ option: "", correct: false })} />}</Form.Item>}</Form.List>} {needsOptions && <Form.List name="question_list">{(fields, { add, remove }) => <Form.Item label="题目选项">{fields.map((field) => <Space key={field.key} align="start" style={{ display: "flex", marginBottom: 8 }}><Form.Item {...field} name={[field.name, "option"]} rules={[{ required: true, message: "请输入内容" }]}><RichTextEditor height={100} /></Form.Item><Form.Item {...field} name={[field.name, "correct"]} valuePropName="checked"><Checkbox>正确答案</Checkbox></Form.Item><Button icon={<MinusOutlined />} onClick={() => remove(field.name)} /></Space>)}{(questionStyle !== 3 || fields.length < 2) && <Button type="dashed" icon={<PlusOutlined />} onClick={() => add({ option: "", correct: false })} />}</Form.Item>}</Form.List>}
{[4, 5].includes(questionStyle) && <Form.Item name="answers" label="正确答案"><Input.TextArea autoSize={{ minRows: 4, maxRows: 8 }} placeholder="请输入正确答案" /></Form.Item>} {[4, 5].includes(questionStyle) && <Form.Item name="answers" label="正确答案"><RichTextEditor height={100} /></Form.Item>}
<Form.Item wrapperCol={{ offset: 5 }}><Space><Button onClick={onClose}>取消</Button><Button type="primary" htmlType="submit">提交</Button></Space></Form.Item> <Form.Item wrapperCol={{ offset: 5 }}><Space><Button onClick={onClose}>取消</Button><Button type="primary" htmlType="submit">提交</Button></Space></Form.Item>
</Form> </Form>
</Drawer>; </Drawer>;
......
import "@wangeditor/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import AliOSS from "ali-oss";
import dayjs from "dayjs";
import { Spin } from "antd";
import { useEffect, useMemo, useRef, useState } from "react";
import { getAliOssToken } from "../api";
type Props = { value?: string; onChange?: (value: string) => void; height?: number };
export default function RichTextEditor({ value = "", onChange, height = 140 }: Props) {
const [editor, setEditor] = useState<any>();
const [client, setClient] = useState<any>();
const [loading, setLoading] = useState(true);
const clientRef = useRef<any>();
useEffect(() => {
let active = true;
const createClient = async () => {
const token: any = await getAliOssToken();
if (!active || !token) return;
window.sessionStorage.setItem("sts", JSON.stringify(token));
const oss = new AliOSS({
accessKeyId: token.AccessKeyId, accessKeySecret: token.AccessKeySecret, stsToken: token.SecurityToken,
endpoint: token.endpoint, bucket: "zxts-book-file", timeout: 180000,
refreshSTSToken: async () => {
const next: any = await getAliOssToken();
return { AccessKeyId: next.AccessKeyId, AccessKeySecret: next.AccessKeySecret, SecurityToken: next.SecurityToken };
}, refreshSTSTokenInterval: 14 * 60 * 1000,
});
clientRef.current = oss; setClient(oss);
};
void createClient();
return () => { active = false; };
}, []);
useEffect(() => () => { editor?.destroy(); }, [editor]);
const config = useMemo(() => ({
placeholder: "请输入内容...", hoverbarKeys: { text: { menuKeys: [] } },
MENU_CONF: { uploadImage: {
timeout: 5000, fieldName: "image", maxFileSize: 10 * 1024 * 1024, base64LimitSize: 5 * 1024,
headers: { "Content-Type": "multipart/form-data", Authorization: `Bearer ${localStorage.getItem("token") || ""}` },
customUpload: async (file: File, insertFn: (url: string, alt: string) => void) => {
const oss = clientRef.current;
if (!oss) return;
const ext = file.name.substring(file.name.lastIndexOf("."));
const name = `practice-${dayjs().valueOf()}-${Math.random().toString(36).substring(2)}${ext}`;
const result = await oss.put(`${dayjs().format("YYYY/MM/DD")}/${name}`, file);
insertFn(result.url, "题库图片");
},
} },
customPaste: (instance: any, event: ClipboardEvent) => {
const text = event.clipboardData?.getData("text/plain");
if (text) instance.insertText(text);
event.preventDefault(); return false;
},
}), []);
return <div style={{ border: "1px solid #ccc", zIndex: 100 }}>
<Toolbar editor={editor} defaultConfig={{ toolbarKeys: ["uploadImage"] }} mode="default" style={{ borderBottom: "1px solid #ccc" }} />
<Spin spinning={loading || !client}>
<Editor defaultConfig={config} value={value} onCreated={(instance) => { setEditor(instance); setLoading(false); }} onChange={(instance) => onChange?.(instance.getHtml())} mode="default" style={{ height, overflowY: "hidden" }} />
</Spin>
</div>;
}
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论