Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
da3f36f6
提交
da3f36f6
authored
7月 20, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
fix(admin): restore question bank rich text editor
上级
8d6aaa09
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
66 行增加
和
3 行删除
+66
-3
api.ts
apps/admin/src/modules/books/question-bank/api.ts
+1
-0
QuestionForm.tsx
...c/modules/books/question-bank/components/QuestionForm.tsx
+4
-3
RichTextEditor.tsx
...modules/books/question-bank/components/RichTextEditor.tsx
+61
-0
没有找到文件。
apps/admin/src/modules/books/question-bank/api.ts
浏览文件 @
da3f36f6
...
@@ -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
();
...
...
apps/admin/src/modules/books/question-bank/components/QuestionForm.tsx
浏览文件 @
da3f36f6
...
@@ -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
>;
...
...
apps/admin/src/modules/books/question-bank/components/RichTextEditor.tsx
0 → 100644
浏览文件 @
da3f36f6
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
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论