Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
fe79c9e1
提交
fe79c9e1
authored
8月 24, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 添加章节加载失败处理,优化章节编辑器的加载逻辑
上级
3da7f60b
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
218 行增加
和
1 行删除
+218
-1
index.jsx
apps/admin/src/modules/books/management/chapters/index.jsx
+18
-1
index.less
apps/admin/src/modules/books/management/chapters/index.less
+9
-0
index.test.tsx
...dmin/src/modules/books/management/chapters/index.test.tsx
+191
-0
没有找到文件。
apps/admin/src/modules/books/management/chapters/index.jsx
浏览文件 @
fe79c9e1
...
...
@@ -57,6 +57,7 @@ const Examine = () => {
const
[
parentId
,
setParentId
]
=
useState
(
null
)
const
[
editValue
,
setEditValue
]
=
useState
(
''
)
const
[
loading
,
setLoading
]
=
useState
(
false
)
const
[
chapterLoadFailed
,
setChapterLoadFailed
]
=
useState
(
false
)
const
contentMd5Ref
=
useRef
(
md5
(
''
))
const
chapterRequestRef
=
useRef
(
0
)
const
chapterSwitchingRef
=
useRef
(
false
)
...
...
@@ -106,6 +107,7 @@ const Examine = () => {
const
editorRef
=
useRef
()
const
[
html
,
setHtml
]
=
useState
(
''
)
const
[
contentId
,
setContentId
]
=
useState
(
false
)
const
[
loadedChapterId
,
setLoadedChapterId
]
=
useState
(
0
)
useEffect
(()
=>
{
if
(
!
id
||
!
userInfo
?.
id
)
{
...
...
@@ -177,6 +179,7 @@ const Examine = () => {
// 获取内容
const
getChapterVal
=
async
()
=>
{
const
requestId
=
++
chapterRequestRef
.
current
setChapterLoadFailed
(
false
)
setLoading
(
true
)
try
{
const
data
=
await
getInfoByChapterId
({
chapter_id
:
chapterId
})
...
...
@@ -193,8 +196,10 @@ const Examine = () => {
contentMd5Ref
.
current
=
md5
(
content
)
setHtml
(
content
)
setContentId
(
cId
)
setLoadedChapterId
(
chapterId
)
}
catch
{
// 请求层已统一展示错误提示。
if
(
requestId
===
chapterRequestRef
.
current
)
setChapterLoadFailed
(
true
)
}
finally
{
if
(
requestId
===
chapterRequestRef
.
current
)
setLoading
(
false
)
}
...
...
@@ -236,7 +241,6 @@ const Examine = () => {
useEffect
(()
=>
{
if
(
chapterId
)
{
setContentId
(
false
)
setHtml
(
''
)
void
getChapterVal
()
}
},
[
chapterId
])
...
...
@@ -262,6 +266,10 @@ const Examine = () => {
setAutosaveTime
(
Date
.
now
())
}
const
editorReady
=
Boolean
(
chapterId
&&
contentId
&&
Number
(
loadedChapterId
)
===
Number
(
chapterId
),
)
const
addChapterParent
=
()
=>
{
setEditKey
(
-
1
)
setEditValue
(
''
)
...
...
@@ -530,7 +538,9 @@ const Examine = () => {
<
div
className=
'book-content-editor'
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
className=
'editor-right'
>
<
Spin
spinning=
{
loading
}
>
{
editorReady
&&
(
<
WangEditorCustomer
key=
{
chapterId
}
ref=
{
editorRef
}
chapterId=
{
chapterId
}
bookId=
{
id
}
...
...
@@ -553,6 +563,13 @@ const Examine = () => {
/>
}
/>
)
}
{
!
loading
&&
chapterLoadFailed
&&
(
<
div
className=
'chapter-load-error'
>
<
p
>
章节内容加载失败
</
p
>
<
Button
onClick=
{
()
=>
void
getChapterVal
()
}
>
重新加载
</
Button
>
</
div
>
)
}
</
Spin
>
</
div
>
</
div
>
...
...
apps/admin/src/modules/books/management/chapters/index.less
浏览文件 @
fe79c9e1
...
...
@@ -83,6 +83,15 @@
height: 100%;
}
}
.chapter-load-error {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: #6f7886;
}
}
.draggable-tree {
flex: 1;
...
...
apps/admin/src/modules/books/management/chapters/index.test.tsx
0 → 100644
浏览文件 @
fe79c9e1
import
{
act
,
fireEvent
,
render
,
screen
,
waitFor
}
from
'@testing-library/react'
import
type
{
ComponentProps
,
ReactNode
}
from
'react'
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
Examine
from
'./index'
const
mocks
=
vi
.
hoisted
(()
=>
({
getAllList
:
vi
.
fn
(),
getInfoByChapterId
:
vi
.
fn
(),
sectionEdit
:
vi
.
fn
(),
setAutosaveTime
:
vi
.
fn
(),
setTreeChapter
:
vi
.
fn
(),
}))
vi
.
mock
(
'antd'
,
()
=>
{
const
PassThrough
=
({
children
}:
{
children
?:
ReactNode
})
=>
<>
{
children
}
</>
return
{
App
:
{
useApp
:
()
=>
({
modal
:
{
confirm
:
vi
.
fn
()
}
})
},
Button
:
({
children
,
onClick
,
...
props
}:
ComponentProps
<
'button'
>
)
=>
(
<
button
onClick=
{
onClick
}
{
...
props
}
>
{
children
}
</
button
>
),
Col
:
PassThrough
,
Descriptions
:
({
items
=
[]
}:
{
items
?:
Array
<
{
key
:
string
;
label
?:
ReactNode
}
>
})
=>
(
<>
{
items
.
map
((
item
)
=>
(
<
div
key=
{
item
.
key
}
>
{
item
.
label
}
</
div
>
))
}
</>
),
Dropdown
:
PassThrough
,
Modal
:
({
children
,
open
}:
{
children
?:
ReactNode
;
open
?:
boolean
})
=>
open
?
<>
{
children
}
</>
:
null
,
Row
:
PassThrough
,
Spin
:
PassThrough
,
Tooltip
:
PassThrough
,
Tree
:
({
disabled
,
onSelect
,
treeData
=
[],
}:
{
disabled
?:
boolean
onSelect
:
(
keys
:
number
[],
info
:
{
node
:
{
key
:
number
;
title
:
string
}
})
=>
void
treeData
?:
Array
<
{
key
:
number
;
title
:
string
}
>
})
=>
(
<
div
>
{
treeData
.
map
((
node
)
=>
(
<
button
key=
{
node
.
key
}
disabled=
{
disabled
}
onClick=
{
()
=>
onSelect
([
node
.
key
],
{
node
})
}
>
{
node
.
title
}
</
button
>
))
}
</
div
>
),
}
})
vi
.
mock
(
'@ant-design/icons'
,
()
=>
({
DiffOutlined
:
()
=>
null
,
EllipsisOutlined
:
()
=>
null
,
MenuFoldOutlined
:
()
=>
null
,
MenuUnfoldOutlined
:
()
=>
null
,
}))
vi
.
mock
(
'@/components/editor/wangeditor-customer'
,
()
=>
({
default
:
({
chapterId
,
html
}:
{
chapterId
:
number
;
html
:
string
})
=>
(
<
div
data
-
chapter
-
id=
{
chapterId
}
data
-
html=
{
html
}
data
-
testid=
'chapter-editor'
/>
),
}))
vi
.
mock
(
'../../review-comments/views/EditorReviewComments'
,
()
=>
({
default
:
()
=>
null
}))
vi
.
mock
(
'./components/form-chapter-title'
,
()
=>
({
default
:
()
=>
null
}))
vi
.
mock
(
'./components/EditChapterEditors'
,
()
=>
({
default
:
()
=>
null
}))
vi
.
mock
(
'./components/BindKnowledgeGraph'
,
()
=>
({
default
:
()
=>
null
}))
vi
.
mock
(
'@/stores/editorStore'
,
()
=>
({
useEditorStore
:
(
selector
:
(
state
:
{
setAutosaveTime
:
typeof
mocks
.
setAutosaveTime
})
=>
unknown
,
)
=>
selector
({
setAutosaveTime
:
mocks
.
setAutosaveTime
}),
}))
vi
.
mock
(
'@/stores/userStore'
,
()
=>
({
useUserStore
:
(
selector
:
(
state
:
{
treeChapter
:
null
userInfo
:
{
id
:
number
;
type
:
number
}
setTreeChapter
:
typeof
mocks
.
setTreeChapter
})
=>
unknown
,
)
=>
selector
({
treeChapter
:
null
,
userInfo
:
{
id
:
1
,
type
:
1
},
setTreeChapter
:
mocks
.
setTreeChapter
,
}),
}))
vi
.
mock
(
'react-router'
,
()
=>
({
useNavigate
:
()
=>
vi
.
fn
(),
useSearchParams
:
()
=>
[
new
URLSearchParams
(
'book_id=10'
)],
}))
vi
.
mock
(
'@/utils/common'
,
()
=>
({
convertToAntdTreeData
:
(
data
:
Array
<
{
id
:
number
;
name
:
string
}
>
)
=>
data
.
map
(({
id
,
name
})
=>
({
key
:
id
,
title
:
name
})),
findFirstNotHasChildren
:
(
data
:
Array
<
{
key
:
number
;
title
:
string
}
>
)
=>
data
[
0
],
findNodeById
:
()
=>
[],
findParentLevelOne
:
()
=>
null
,
findTreeElementByKey
:
(
data
:
Array
<
{
key
:
number
;
title
:
string
}
>
,
_field
:
string
,
value
?:
number
,
)
=>
data
.
find
((
item
)
=>
item
.
key
===
Number
(
value
)),
findTreeToIndex
:
()
=>
0
,
}))
vi
.
mock
(
'./api'
,
()
=>
({
chapterDel
:
vi
.
fn
(),
dragOrder
:
vi
.
fn
(),
getAllList
:
mocks
.
getAllList
,
getInfoByChapterId
:
mocks
.
getInfoByChapterId
,
sectionEdit
:
mocks
.
sectionEdit
,
}))
vi
.
mock
(
'../api'
,
()
=>
({
getBook
:
vi
.
fn
()
}))
const
deferred
=
<
T
,
>
() =
>
{
let
resolve
!
:
(
value
:
T
)
=>
void
let
reject
!
:
(
reason
?:
unknown
)
=>
void
const
promise
=
new
Promise
<
T
>
((
promiseResolve
,
promiseReject
)
=>
{
resolve
=
promiseResolve
reject
=
promiseReject
})
return
{
promise
,
reject
,
resolve
}
}
describe('chapter editor lifecycle', () =
>
{
beforeEach
(()
=>
{
vi
.
clearAllMocks
()
mocks
.
getAllList
.
mockResolvedValue
({
code
:
200
,
data
:
[
{
id
:
1
,
name
:
'章节一'
},
{
id
:
2
,
name
:
'章节二'
},
],
})
})
it
(
'creates a fresh editor only after the selected chapter loads successfully'
,
async
()
=>
{
const
secondChapterRequest
=
deferred
<
{
id
:
number
content
:
string
selected_editor_ids
:
number
[]
}
>
()
mocks
.
getInfoByChapterId
.
mockResolvedValueOnce
({
id
:
101
,
content
:
'<p>章节一内容</p>'
,
selected_editor_ids
:
[
1
],
})
.
mockReturnValueOnce
(
secondChapterRequest
.
promise
)
.
mockResolvedValueOnce
({
id
:
102
,
content
:
'<p>章节二内容</p>'
,
selected_editor_ids
:
[
1
],
})
render
(<
Examine
/>)
const
firstEditor
=
await
screen
.
findByTestId
(
'chapter-editor'
)
expect
(
firstEditor
).
toHaveAttribute
(
'data-chapter-id'
,
'1'
)
fireEvent
.
click
(
screen
.
getByRole
(
'button'
,
{
name
:
'章节二'
}))
await
waitFor
(()
=>
expect
(
screen
.
queryByTestId
(
'chapter-editor'
)).
not
.
toBeInTheDocument
())
await
act
(
async
()
=>
{
secondChapterRequest
.
reject
(
new
Error
(
'load failed'
))
await
secondChapterRequest
.
promise
.
catch
(()
=>
undefined
)
})
expect
(
await
screen
.
findByText
(
'章节内容加载失败'
)).
toBeInTheDocument
()
fireEvent
.
click
(
screen
.
getByRole
(
'button'
,
{
name
:
'重新加载'
}))
const
secondEditor
=
await
screen
.
findByTestId
(
'chapter-editor'
)
expect
(
secondEditor
).
not
.
toBe
(
firstEditor
)
expect
(
secondEditor
).
toHaveAttribute
(
'data-chapter-id'
,
'2'
)
expect
(
secondEditor
).
toHaveAttribute
(
'data-html'
,
'<p>章节二内容</p>'
)
})
}
)
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论