Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
3da7f60b
提交
3da7f60b
authored
8月 24, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
chore: update
上级
a9fbd6f2
全部展开
显示空白字符变更
内嵌
并排
正在显示
117 个修改的文件
包含
892 行增加
和
1374 行删除
+892
-1374
AppList.tsx
apps/admin/src/components/AppList/AppList.tsx
+0
-1
editor-feature-events.test.tsx
...geditor-customer/__tests__/editor-feature-events.test.tsx
+0
-27
feature-registry.test.ts
...or/wangeditor-customer/__tests__/feature-registry.test.ts
+4
-10
image-menu.test.ts
...s/editor/wangeditor-customer/__tests__/image-menu.test.ts
+43
-0
normalize-inline-void.test.ts
...ngeditor-customer/__tests__/normalize-inline-void.test.ts
+44
-0
remove-spaces-menu.test.ts
.../wangeditor-customer/__tests__/remove-spaces-menu.test.ts
+32
-0
source-boundary.test.ts
...tor/wangeditor-customer/__tests__/source-boundary.test.ts
+5
-8
with-trailing-paragraph.test.ts
...editor-customer/__tests__/with-trailing-paragraph.test.ts
+51
-0
DialogMenu.js
.../components/editor/wangeditor-customer/core/DialogMenu.js
+21
-0
LegacyDialogMenu.js
...nents/editor/wangeditor-customer/core/LegacyDialogMenu.js
+0
-41
create-feature-registry.js
...ditor/wangeditor-customer/core/create-feature-registry.js
+0
-7
define-editor-feature.js
.../editor/wangeditor-customer/core/define-editor-feature.js
+5
-6
register-editor-features.js
...itor/wangeditor-customer/core/register-editor-features.js
+1
-10
useEditorFeatureEvents.js
...editor/wangeditor-customer/core/useEditorFeatureEvents.js
+0
-18
AiImageDialog.jsx
...r/wangeditor-customer/features/ai-media/AiImageDialog.jsx
+3
-11
AiVideoDialog.jsx
...r/wangeditor-customer/features/ai-media/AiVideoDialog.jsx
+3
-11
DigitalHumanDialog.jsx
...geditor-customer/features/ai-media/DigitalHumanDialog.jsx
+2
-3
digital-human-menu.jsx
...geditor-customer/features/ai-media/digital-human-menu.jsx
+3
-7
feature.js
...s/editor/wangeditor-customer/features/ai-media/feature.js
+6
-1
image-menu.jsx
...itor/wangeditor-customer/features/ai-media/image-menu.jsx
+3
-8
video-menu.jsx
...itor/wangeditor-customer/features/ai-media/video-menu.jsx
+3
-8
AiQuestionDialog.jsx
...editor-customer/features/ai-question/AiQuestionDialog.jsx
+5
-6
feature.js
...ditor/wangeditor-customer/features/ai-question/feature.js
+6
-1
judge-menu.jsx
...r/wangeditor-customer/features/ai-question/judge-menu.jsx
+3
-7
multiple-menu.jsx
...angeditor-customer/features/ai-question/multiple-menu.jsx
+3
-7
single-menu.jsx
.../wangeditor-customer/features/ai-question/single-menu.jsx
+3
-7
AiWriteDialog.jsx
...angeditor-customer/features/ai-research/AiWriteDialog.jsx
+3
-10
BaiduSearchDialog.jsx
...ditor-customer/features/ai-research/BaiduSearchDialog.jsx
+0
-160
SearchDialog.jsx
...wangeditor-customer/features/ai-research/SearchDialog.jsx
+3
-10
baidu-search-menu.jsx
...ditor-customer/features/ai-research/baidu-search-menu.jsx
+0
-21
feature.js
...ditor/wangeditor-customer/features/ai-research/feature.js
+7
-10
search-menu.jsx
.../wangeditor-customer/features/ai-research/search-menu.jsx
+3
-7
write-menu.jsx
...r/wangeditor-customer/features/ai-research/write-menu.jsx
+3
-7
AiChatDialog.jsx
...tor/wangeditor-customer/features/ai-text/AiChatDialog.jsx
+3
-4
chat-menu.jsx
...editor/wangeditor-customer/features/ai-text/chat-menu.jsx
+3
-7
content-inspect-menu.jsx
...editor-customer/features/ai-text/content-inspect-menu.jsx
+3
-7
expand-menu.jsx
...itor/wangeditor-customer/features/ai-text/expand-menu.jsx
+3
-7
polishing-menu.jsx
...r/wangeditor-customer/features/ai-text/polishing-menu.jsx
+3
-7
punctuation-menu.jsx
...wangeditor-customer/features/ai-text/punctuation-menu.jsx
+3
-7
rewrite-menu.jsx
...tor/wangeditor-customer/features/ai-text/rewrite-menu.jsx
+3
-7
summary-menu.jsx
...tor/wangeditor-customer/features/ai-text/summary-menu.jsx
+3
-7
translate-menu.jsx
...r/wangeditor-customer/features/ai-text/translate-menu.jsx
+3
-7
feature.js
...ents/editor/wangeditor-customer/features/audio/feature.js
+0
-8
menu.js
...ponents/editor/wangeditor-customer/features/audio/menu.js
+4
-3
node.js
...ponents/editor/wangeditor-customer/features/audio/node.js
+4
-31
feature.js
...wangeditor-customer/features/chapter-structure/feature.js
+0
-11
section-menu.js
...ditor-customer/features/chapter-structure/section-menu.js
+3
-2
section-node.js
...ditor-customer/features/chapter-structure/section-node.js
+3
-36
title-menu.js
...geditor-customer/features/chapter-structure/title-menu.js
+3
-2
title-node.js
...geditor-customer/features/chapter-structure/title-node.js
+3
-35
remove-spaces-menu.js
...tor-customer/features/content-tools/remove-spaces-menu.js
+22
-27
feature.js
...ditor/wangeditor-customer/features/expand-read/feature.js
+0
-11
menu.js
...s/editor/wangeditor-customer/features/expand-read/menu.js
+6
-2
node.js
...s/editor/wangeditor-customer/features/expand-read/node.js
+4
-43
simple-node.js
...r/wangeditor-customer/features/expand-read/simple-node.js
+10
-46
feature.js
...ts/editor/wangeditor-customer/features/formula/feature.js
+0
-7
menu.js
...nents/editor/wangeditor-customer/features/formula/menu.js
+3
-2
node.js
...nents/editor/wangeditor-customer/features/formula/node.js
+9
-21
feature.js
...ts/editor/wangeditor-customer/features/gallery/feature.js
+0
-20
menu.js
...nents/editor/wangeditor-customer/features/gallery/menu.js
+7
-2
node.js
...nents/editor/wangeditor-customer/features/gallery/node.js
+3
-36
online-menu.js
...ditor/wangeditor-customer/features/gallery/online-menu.js
+6
-1
FeatureDialog.jsx
...or/wangeditor-customer/features/history/FeatureDialog.jsx
+1
-2
HistoryDialog.jsx
...or/wangeditor-customer/features/history/HistoryDialog.jsx
+10
-13
menu.jsx
...ponents/editor/wangeditor-customer/features/icon/menu.jsx
+3
-8
edit-menu.js
...ts/editor/wangeditor-customer/features/image/edit-menu.js
+14
-2
feature.js
...ents/editor/wangeditor-customer/features/image/feature.js
+0
-36
menu.js
...ponents/editor/wangeditor-customer/features/image/menu.js
+49
-43
node.jsx
...onents/editor/wangeditor-customer/features/image/node.jsx
+10
-76
online-menu.js
.../editor/wangeditor-customer/features/image/online-menu.js
+11
-84
feature.js
...nents/editor/wangeditor-customer/features/link/feature.js
+0
-12
menu.js
...mponents/editor/wangeditor-customer/features/link/menu.js
+7
-2
node.js
...mponents/editor/wangeditor-customer/features/link/node.js
+9
-22
PracticeSettingsDialog.jsx
...tor-customer/features/practice/PracticeSettingsDialog.jsx
+2
-2
feature.js
...s/editor/wangeditor-customer/features/practice/feature.js
+0
-7
menu.js
...ents/editor/wangeditor-customer/features/practice/menu.js
+3
-2
node.js
...ents/editor/wangeditor-customer/features/practice/node.js
+4
-37
FeatureDialog.jsx
...or/wangeditor-customer/features/preview/FeatureDialog.jsx
+0
-1
PracticePreview.jsx
.../wangeditor-customer/features/preview/PracticePreview.jsx
+87
-0
PracticePreview.test.tsx
...editor-customer/features/preview/PracticePreview.test.tsx
+36
-0
PreviewDialog.jsx
...or/wangeditor-customer/features/preview/PreviewDialog.jsx
+0
-0
PreviewDialog.test.tsx
...ngeditor-customer/features/preview/PreviewDialog.test.tsx
+60
-0
feature.js
...ts/editor/wangeditor-customer/features/tooltip/feature.js
+0
-9
menu.js
...nents/editor/wangeditor-customer/features/tooltip/menu.js
+6
-2
node.js
...nents/editor/wangeditor-customer/features/tooltip/node.js
+9
-27
feature.js
...ents/editor/wangeditor-customer/features/video/feature.js
+0
-8
menu.js
...ponents/editor/wangeditor-customer/features/video/menu.js
+4
-3
node.js
...ponents/editor/wangeditor-customer/features/video/node.js
+4
-37
index.less
...dmin/src/components/editor/wangeditor-customer/index.less
+1
-15
normalize-inline-void.js
...ditor/wangeditor-customer/shared/normalize-inline-void.js
+17
-0
with-trailing-paragraph.js
...tor/wangeditor-customer/shared/with-trailing-paragraph.js
+36
-0
EditorHeader.jsx
...ponents/editor/wangeditor-customer/shell/EditorHeader.jsx
+11
-39
EditorSidebar.jsx
...onents/editor/wangeditor-customer/shell/EditorSidebar.jsx
+17
-7
EditorSidebar.test.tsx
...s/editor/wangeditor-customer/shell/EditorSidebar.test.tsx
+48
-0
WangEditorCustomer.jsx
...s/editor/wangeditor-customer/shell/WangEditorCustomer.jsx
+43
-29
message.jsx
apps/admin/src/components/layout/components/message.jsx
+1
-1
index.jsx
apps/admin/src/components/layout/index.jsx
+2
-6
index.less
apps/admin/src/components/layout/index.less
+0
-4
DeleteAdminModal.tsx
.../src/modules/access/admin/components/DeleteAdminModal.tsx
+1
-1
DeleteRoleModal.tsx
...in/src/modules/access/role/components/DeleteRoleModal.tsx
+1
-1
AccountView.jsx
apps/admin/src/modules/account/views/AccountView.jsx
+0
-0
DeleteAdvertModal.tsx
...es/advertisements/advert/components/DeleteAdvertModal.tsx
+0
-0
index.jsx
apps/admin/src/modules/books/management/chapters/index.jsx
+0
-0
DeleteBookModal.tsx
...c/modules/books/management/components/DeleteBookModal.tsx
+0
-0
ExportBookModal.tsx
...c/modules/books/management/components/ExportBookModal.tsx
+0
-0
ImportBookDrawer.tsx
.../modules/books/management/components/ImportBookDrawer.tsx
+0
-0
ManagementAddEdit.jsx
...modules/books/management/components/ManagementAddEdit.jsx
+0
-0
EditorReviewComments.tsx
...ules/books/review-comments/views/EditorReviewComments.tsx
+0
-0
SaleDiscussionDetail.jsx
...rc/modules/books/sale/components/SaleDiscussionDetail.jsx
+0
-0
SaleEdit.jsx
apps/admin/src/modules/books/sale/components/SaleEdit.jsx
+0
-0
DeleteMemberLevelModal.tsx
...dules/members/level/components/DeleteMemberLevelModal.tsx
+0
-0
AppVersionListView.jsx
...modules/settings/app-version/views/AppVersionListView.jsx
+0
-0
AppliedListView.jsx
...in/src/modules/settings/applied/views/AppliedListView.jsx
+0
-0
HelpListView.jsx
apps/admin/src/modules/settings/help/views/HelpListView.jsx
+0
-0
StudentBookView.jsx
.../admin/src/modules/student-book/views/StudentBookView.jsx
+0
-0
TeacherListView.jsx
...admin/src/modules/teachers/list/views/TeacherListView.jsx
+0
-0
editorStore.ts
apps/admin/src/stores/editorStore.ts
+0
-0
没有找到文件。
apps/admin/src/components/AppList/AppList.tsx
浏览文件 @
3da7f60b
...
...
@@ -82,7 +82,6 @@ export function AppList<Row extends object, Filters extends object = Record<stri
{
hasRefreshError
?
(
<
Alert
className=
'app-list__error'
type=
'error'
showIcon
title=
{
data
.
error
?.
message
}
...
...
apps/admin/src/components/editor/wangeditor-customer/__tests__/editor-feature-events.test.tsx
deleted
100644 → 0
浏览文件 @
a9fbd6f2
import
{
render
}
from
'@testing-library/react'
import
{
createElement
}
from
'react'
import
{
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
useEditorFeatureEvents
}
from
'@/components/editor/wangeditor-customer/core/useEditorFeatureEvents'
describe
(
'useEditorFeatureEvents'
,
()
=>
{
it
(
'subscribes registry menu events to the current editor and cleans them up'
,
()
=>
{
const
editor
=
{
on
:
vi
.
fn
(),
off
:
vi
.
fn
()
}
const
handler
=
vi
.
fn
()
const
registry
=
{
events
:
[{
name
:
'ImageMenuClick'
,
handler
}]
}
const
Subscriber
=
()
=>
{
useEditorFeatureEvents
({
editor
,
registry
,
runtime
:
{}
})
return
null
}
const
view
=
render
(
createElement
(
Subscriber
))
expect
(
editor
.
on
).
toHaveBeenCalledWith
(
'ImageMenuClick'
,
expect
.
any
(
Function
))
const
listener
=
editor
.
on
.
mock
.
calls
[
0
][
1
]
listener
({
source
:
'menu'
})
expect
(
handler
).
toHaveBeenCalledWith
({
editor
,
payload
:
{
source
:
'menu'
},
runtime
:
{}
})
view
.
unmount
()
expect
(
editor
.
off
).
toHaveBeenCalledWith
(
'ImageMenuClick'
,
listener
)
})
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/feature-registry.test.ts
浏览文件 @
3da7f60b
...
...
@@ -44,7 +44,6 @@ const expectedMenuKeys = [
'AIWrite'
,
'AIImage'
,
'AIVideo'
,
'AIBaiduSearch'
,
'Icon'
,
]
...
...
@@ -90,7 +89,6 @@ describe('createFeatureRegistry', () => {
menus
:
[{
order
:
10
,
config
:
menu
(
'menu-a'
)
}],
nodeModules
:
[{
order
:
10
,
types
:
[
'node-a'
],
module
:
{}
}],
dialogs
:
[{
id
:
'dialog-a'
,
component
:
dialog
}],
events
:
[{
name
:
'event-a'
,
handler
:
()
=>
{}
}],
...
overrides
,
})
...
...
@@ -104,12 +102,6 @@ describe('createFeatureRegistry', () => {
expect
(()
=>
createFeatureRegistry
([
make
(),
make
({
id
:
'feature-b'
,
menus
:
[],
nodeModules
:
[]
})]),
).
toThrow
(
'Duplicate dialog id: dialog-a'
)
expect
(()
=>
createFeatureRegistry
([
make
(),
make
({
id
:
'feature-b'
,
menus
:
[],
nodeModules
:
[],
dialogs
:
[]
}),
]),
).
toThrow
(
'Duplicate editor event: event-a'
)
})
it
(
'keeps the legacy menu and node registration orders'
,
()
=>
{
...
...
@@ -130,13 +122,15 @@ describe('createFeatureRegistry', () => {
expect
(
calls
[
0
].
menus
.
map
((
menu
)
=>
menu
.
key
)).
toEqual
(
expectedMenuKeys
)
})
it
(
'opens
legacy dialogs by the stable menu key
'
,
()
=>
{
it
(
'opens
the dialog configured by the menu
'
,
()
=>
{
const
calls
=
[]
const
Boot
=
{
registerModule
:
(
module
)
=>
calls
.
push
(
module
)
}
const
emit
=
vi
.
fn
()
registerEditorFeatures
(
Boot
)
calls
[
0
].
menus
.
find
((
menu
)
=>
menu
.
key
===
'AIRewrite'
).
factory
().
exec
({
emit
})
const
menu
=
calls
[
0
].
menus
.
find
((
menu
)
=>
menu
.
key
===
'AIRewrite'
).
factory
()
expect
(
menu
.
getValue
()).
toBe
(
''
)
menu
.
exec
({
emit
})
expect
(
emit
).
toHaveBeenCalledWith
(
'wangeditor-customer:dialog'
,
{
id
:
'ai-text.action'
,
...
...
apps/admin/src/components/editor/wangeditor-customer/__tests__/image-menu.test.ts
0 → 100644
浏览文件 @
3da7f60b
import
{
afterEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
ImageMenu
,
{
imageWidth50MenuChapterConf
,
}
from
'@/components/editor/wangeditor-customer/features/image/menu'
afterEach
(()
=>
vi
.
restoreAllMocks
())
describe
(
'image menus'
,
()
=>
{
it
(
'opens the image dialog without trying to resize an image'
,
()
=>
{
const
editor
=
{
emit
:
vi
.
fn
()
}
const
menu
=
ImageMenu
.
factory
()
vi
.
spyOn
(
menu
,
'isDisabled'
).
mockReturnValue
(
false
)
const
resize
=
vi
.
spyOn
(
SlateTransforms
,
'setNodes'
)
menu
.
exec
(
editor
)
expect
(
editor
.
emit
).
toHaveBeenCalledWith
(
'wangeditor-customer:dialog'
,
{
id
:
'image.insert'
,
payload
:
{
isOnline
:
false
},
})
expect
(
resize
).
not
.
toHaveBeenCalled
()
})
it
(
'resizes only the selected chapter image without opening the image dialog'
,
()
=>
{
const
image
=
{
type
:
'chapterImage'
,
style
:
{
margin
:
'auto'
}
}
const
editor
=
{
emit
:
vi
.
fn
()
}
vi
.
spyOn
(
DomEditor
,
'getSelectedNodeByType'
).
mockReturnValue
(
image
)
vi
.
spyOn
(
DomEditor
,
'getHoverbar'
).
mockReturnValue
(
null
)
vi
.
spyOn
(
DomEditor
,
'findPath'
).
mockReturnValue
([
2
])
const
resize
=
vi
.
spyOn
(
SlateTransforms
,
'setNodes'
).
mockImplementation
(()
=>
{})
imageWidth50MenuChapterConf
.
factory
().
exec
(
editor
)
expect
(
editor
.
emit
).
not
.
toHaveBeenCalled
()
expect
(
resize
).
toHaveBeenCalledWith
(
editor
,
{
style
:
{
margin
:
'auto'
,
width
:
'50%'
,
height
:
''
}
},
{
at
:
[
2
]
},
)
})
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/normalize-inline-void.test.ts
0 → 100644
浏览文件 @
3da7f60b
import
{
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
normalizeInlineVoidNode
}
from
'../shared/normalize-inline-void'
describe
(
'normalizeInlineVoidNode'
,
()
=>
{
it
(
'adds one trailing text node after an inline void at the end of a paragraph'
,
()
=>
{
const
inlineNode
=
{
type
:
'chapterExpandReadSimple'
,
children
:
[{
text
:
''
}]
}
const
paragraph
=
{
type
:
'paragraph'
,
children
:
[{
text
:
'正文'
},
inlineNode
]
}
const
editor
=
{
children
:
[
paragraph
]
}
const
insertNodes
=
vi
.
fn
((
_editor
,
node
,
options
)
=>
{
paragraph
.
children
.
splice
(
options
.
at
[
1
],
0
,
node
)
})
const
input
=
{
editor
,
node
:
inlineNode
,
path
:
[
0
,
1
],
type
:
'chapterExpandReadSimple'
,
normalizeNode
:
vi
.
fn
(),
transforms
:
{
insertNodes
},
}
normalizeInlineVoidNode
(
input
)
normalizeInlineVoidNode
(
input
)
expect
(
insertNodes
).
toHaveBeenCalledOnce
()
expect
(
paragraph
.
children
).
toEqual
([{
text
:
'正文'
},
inlineNode
,
{
text
:
''
}])
})
it
(
'delegates nodes owned by another normalizer'
,
()
=>
{
const
normalizeNode
=
vi
.
fn
()
const
node
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
normalizeInlineVoidNode
({
editor
:
{
children
:
[
node
]
},
node
,
path
:
[
0
],
type
:
'chapterFormula'
,
normalizeNode
,
transforms
:
{
insertNodes
:
vi
.
fn
()
},
})
expect
(
normalizeNode
).
toHaveBeenCalledWith
([
node
,
[
0
]])
})
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/remove-spaces-menu.test.ts
0 → 100644
浏览文件 @
3da7f60b
import
{
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
RemoveSpacesMenu
,
{
removeSpacesFromChineseParagraphs
,
}
from
'@/components/editor/wangeditor-customer/features/content-tools/remove-spaces-menu'
describe
(
'removeSpacesFromChineseParagraphs'
,
()
=>
{
it
(
'removes spaces and non-breaking spaces from Chinese paragraphs'
,
()
=>
{
const
html
=
'<p>这 是 <strong>中文 段落</strong>。</p><p>English text stays.</p>'
expect
(
removeSpacesFromChineseParagraphs
(
html
)).
toBe
(
'<p>这是<strong>中文段落</strong>。</p><p>English text stays.</p>'
,
)
})
it
(
'does not change content outside paragraphs'
,
()
=>
{
const
html
=
'<pre><code>const value = 1</code></pre><div>中文 内容</div>'
expect
(
removeSpacesFromChineseParagraphs
(
html
)).
toBe
(
html
)
})
it
(
'updates the editor only when content changes'
,
()
=>
{
const
editor
=
{
getHtml
:
vi
.
fn
(()
=>
'<p>中文 段落</p>'
),
setHtml
:
vi
.
fn
(),
}
RemoveSpacesMenu
.
factory
().
exec
(
editor
)
expect
(
editor
.
setHtml
).
toHaveBeenCalledWith
(
'<p>中文段落</p>'
)
})
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/source-boundary.test.ts
浏览文件 @
3da7f60b
...
...
@@ -13,13 +13,10 @@ describe('WangEditorCustomer shell boundary', () => {
expect
(
shellSource
).
not
.
toMatch
(
/from
\s
+
[
'"
][^
'"
]
*features
\/
/
)
})
it
.
each
([
'ImageMenuClick'
,
'GalleryMenuClick'
,
'PracticeMenuClick'
,
'window.dispatchEvent'
,
'document.querySelector('
,
])(
'does not contain %s'
,
(
forbidden
)
=>
{
it
.
each
([
'window.dispatchEvent'
,
'document.querySelector('
])(
'does not contain %s'
,
(
forbidden
)
=>
{
expect
(
shellSource
).
not
.
toContain
(
forbidden
)
})
},
)
})
apps/admin/src/components/editor/wangeditor-customer/__tests__/with-trailing-paragraph.test.ts
0 → 100644
浏览文件 @
3da7f60b
import
{
afterEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
SlateTransforms
}
from
'@wangeditor/editor'
import
{
withTrailingParagraph
}
from
'@/components/editor/wangeditor-customer/shared/with-trailing-paragraph'
afterEach
(()
=>
vi
.
restoreAllMocks
())
describe
(
'withTrailingParagraph'
,
()
=>
{
it
(
'adds an editable paragraph after a custom block at the document end'
,
()
=>
{
const
node
=
{
type
:
'chapterVideo'
,
children
:
[{
text
:
''
}]
}
const
editor
=
{
children
:
[
node
],
normalizeNode
:
vi
.
fn
()
}
const
insert
=
vi
.
spyOn
(
SlateTransforms
,
'insertNodes'
).
mockImplementation
(()
=>
{})
withTrailingParagraph
(
editor
,
'chapterVideo'
).
normalizeNode
([
node
,
[
0
]])
expect
(
insert
).
toHaveBeenCalledWith
(
editor
,
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
},
{
at
:
[
1
]
},
)
})
it
(
'keeps allowed following blocks and delegates unrelated nodes'
,
()
=>
{
const
node
=
{
type
:
'chapterVideo'
,
children
:
[{
text
:
''
}]
}
const
next
=
{
type
:
'chapterGallery'
,
children
:
[{
text
:
''
}]
}
const
normalizeNode
=
vi
.
fn
()
const
editor
=
{
children
:
[
node
,
next
],
normalizeNode
}
const
insert
=
vi
.
spyOn
(
SlateTransforms
,
'insertNodes'
).
mockImplementation
(()
=>
{})
withTrailingParagraph
(
editor
,
'chapterVideo'
)
editor
.
normalizeNode
([
node
,
[
0
]])
editor
.
normalizeNode
([
next
,
[
1
]])
expect
(
insert
).
not
.
toHaveBeenCalled
()
expect
(
normalizeNode
).
toHaveBeenCalledWith
([
next
,
[
1
]])
})
it
(
'supports the narrower image and audio rule without duplicating the normalizer'
,
()
=>
{
const
node
=
{
type
:
'chapterAudio'
,
children
:
[{
text
:
''
}]
}
const
next
=
{
type
:
'chapterGallery'
,
children
:
[{
text
:
''
}]
}
const
editor
=
{
children
:
[
node
,
next
],
normalizeNode
:
vi
.
fn
()
}
const
insert
=
vi
.
spyOn
(
SlateTransforms
,
'insertNodes'
).
mockImplementation
(()
=>
{})
withTrailingParagraph
(
editor
,
'chapterAudio'
,
[
'paragraph'
,
'blockquote'
]).
normalizeNode
([
node
,
[
0
],
])
expect
(
insert
).
toHaveBeenCalledOnce
()
})
})
apps/admin/src/components/editor/wangeditor-customer/core/DialogMenu.js
0 → 100644
浏览文件 @
3da7f60b
import
{
openEditorDialog
}
from
'./editor-dialog'
export
default
class
DialogMenu
{
constructor
(
dialogId
,
payload
=
{})
{
this
.
dialogId
=
dialogId
this
.
payload
=
payload
this
.
tag
=
'button'
}
getValue
()
{
return
''
}
isActive
()
{
return
false
}
isDisabled
()
{
return
false
}
exec
(
editor
)
{
if
(
!
this
.
isDisabled
(
editor
))
openEditorDialog
(
editor
,
this
.
dialogId
,
this
.
payload
)
}
}
apps/admin/src/components/editor/wangeditor-customer/core/LegacyDialogMenu.js
deleted
100644 → 0
浏览文件 @
a9fbd6f2
import
{
openEditorDialog
}
from
'./editor-dialog'
const
dialogByMenu
=
{
AIChat
:
[
'ai-text.chat'
,
{}],
AIRewrite
:
[
'ai-text.action'
,
{
action
:
'rewrite'
}],
AIExpand
:
[
'ai-text.action'
,
{
action
:
'expand'
}],
AISummary
:
[
'ai-text.action'
,
{
action
:
'summary'
}],
AIPolishing
:
[
'ai-text.action'
,
{
action
:
'abbreviate'
}],
AIPunctuation
:
[
'ai-text.action'
,
{
action
:
'punctuation'
}],
AIContentInspect
:
[
'ai-text.action'
,
{
action
:
'contentInspect'
}],
AITranslate
:
[
'ai-text.action'
,
{
action
:
'translate'
}],
AIQuestionSingle
:
[
'ai-question.generate'
,
{
action
:
'1'
}],
AIQuestionMultiple
:
[
'ai-question.generate'
,
{
action
:
'2'
}],
AIQuestionJudge
:
[
'ai-question.generate'
,
{
action
:
'3'
}],
AIQuestionGapFill
:
[
'ai-question.generate'
,
{
action
:
'4'
}],
AIQuestionOpenEnded
:
[
'ai-question.generate'
,
{
action
:
'5'
}],
AIDigitalHuman
:
[
'ai-media.digital-human'
,
{}],
AIImage
:
[
'ai-media.image'
,
{}],
AIVideo
:
[
'ai-media.video'
,
{}],
AISearch
:
[
'ai-research.search'
,
{
provider
:
'search'
}],
AIBaiduSearch
:
[
'ai-research.search'
,
{
provider
:
'baidu'
}],
AIWrite
:
[
'ai-research.write'
,
{}],
Icon
:
[
'icon.insert'
,
{}],
}
export
default
class
BaseModalMenu
{
constructor
()
{
this
.
tag
=
'button'
}
isActive
()
{
return
false
// or true
}
isDisabled
()
{
return
false
// or true
}
exec
(
editor
)
{
const
request
=
dialogByMenu
[
this
.
menuKey
]
if
(
!
request
)
return
openEditorDialog
(
editor
,
request
[
0
],
request
[
1
])
}
}
apps/admin/src/components/editor/wangeditor-customer/core/create-feature-registry.js
浏览文件 @
3da7f60b
...
...
@@ -18,7 +18,6 @@ export const createFeatureRegistry = (features) => {
const
menus
=
features
.
flatMap
((
feature
)
=>
feature
.
menus
).
sort
(
byOrder
)
const
nodeModules
=
features
.
flatMap
((
feature
)
=>
feature
.
nodeModules
).
sort
(
byOrder
)
const
dialogEntries
=
features
.
flatMap
((
feature
)
=>
feature
.
dialogs
)
const
events
=
features
.
flatMap
((
feature
)
=>
feature
.
events
)
assertUnique
(
menus
.
map
((
entry
)
=>
entry
.
config
.
key
),
...
...
@@ -32,16 +31,10 @@ export const createFeatureRegistry = (features) => {
dialogEntries
.
map
((
entry
)
=>
entry
.
id
),
'dialog id'
,
)
assertUnique
(
events
.
map
((
entry
)
=>
entry
.
name
),
'editor event'
,
)
return
{
features
:
[...
features
],
menus
,
nodeModules
,
events
,
dialogs
:
new
Map
(
dialogEntries
.
map
((
entry
)
=>
[
entry
.
id
,
entry
.
component
])),
}
}
apps/admin/src/components/editor/wangeditor-customer/core/define-editor-feature.js
浏览文件 @
3da7f60b
export
const
defineEditorFeature
=
({
export
const
defineEditorFeature
=
({
id
,
menus
=
[],
nodeModules
=
[],
dialogs
=
[]
})
=>
({
id
,
menus
=
[],
nodeModules
=
[],
dialogs
=
[],
events
=
[],
})
=>
({
id
,
menus
,
nodeModules
,
dialogs
,
events
})
menus
,
nodeModules
,
dialogs
,
})
apps/admin/src/components/editor/wangeditor-customer/core/register-editor-features.js
浏览文件 @
3da7f60b
...
...
@@ -2,20 +2,11 @@ import { editorFeatureRegistry } from '../features'
const
registrationKey
=
Symbol
.
for
(
'ebook.admin.wangeditor-customer.registered'
)
const
bindMenuKey
=
(
config
)
=>
({
...
config
,
factory
(...
args
)
{
const
menu
=
config
.
factory
.
apply
(
config
,
args
)
menu
.
menuKey
=
config
.
key
return
menu
},
})
export
const
registerEditorFeatures
=
(
Boot
)
=>
{
if
(
Boot
[
registrationKey
])
return
Boot
.
registerModule
({
menus
:
editorFeatureRegistry
.
menus
.
map
((
entry
)
=>
bindMenuKey
(
entry
.
config
)
),
menus
:
editorFeatureRegistry
.
menus
.
map
((
entry
)
=>
entry
.
config
),
})
editorFeatureRegistry
.
nodeModules
.
forEach
((
entry
)
=>
Boot
.
registerModule
(
entry
.
module
))
...
...
apps/admin/src/components/editor/wangeditor-customer/core/useEditorFeatureEvents.js
deleted
100644 → 0
浏览文件 @
a9fbd6f2
import
{
useEffect
,
useRef
}
from
'react'
export
const
useEditorFeatureEvents
=
({
editor
,
registry
,
runtime
})
=>
{
const
runtimeRef
=
useRef
(
runtime
)
runtimeRef
.
current
=
runtime
useEffect
(()
=>
{
if
(
!
editor
)
return
const
listeners
=
registry
.
events
.
map
(({
name
,
handler
})
=>
({
name
,
listener
:
(
payload
)
=>
handler
({
editor
,
payload
,
runtime
:
runtimeRef
.
current
}),
}))
listeners
.
forEach
(({
name
,
listener
})
=>
editor
.
on
(
name
,
listener
))
return
()
=>
listeners
.
forEach
(({
name
,
listener
})
=>
editor
.
off
(
name
,
listener
))
},
[
editor
,
registry
])
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/AiImageDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -25,9 +25,7 @@ import './image.less'
import
{
useImage
}
from
'@ezijing/ai-react'
import
{
uploadFileByUrl
}
from
'@/utils/oss'
export
default
function
AIImageModal
(
props
)
{
const
{
editor
}
=
props
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
export
default
function
AIImageModal
({
editor
,
open
,
onClose
})
{
const
[
prompt
,
setPrompt
]
=
useState
(
''
)
const
[
size
,
setSize
]
=
useState
(
'2048x2048'
)
const
[
style
,
setStyle
]
=
useState
(
'default'
)
...
...
@@ -152,7 +150,7 @@ export default function AIImageModal(props) {
insertBlockNodesAndFocus
(
editor
,
SlateTransforms
,
[
oImg
,
oP
])
message
.
success
(
'图片已插入编辑器!'
)
setIsModalOpen
(
false
)
onClose
(
)
}
else
{
message
.
error
(
'编辑器未找到'
)
}
...
...
@@ -208,13 +206,7 @@ export default function AIImageModal(props) {
<
ConfigProvider
theme=
{
{
components
:
{
Modal
:
{
headerBg
:
'#f7f8fa'
,
contentBg
:
'#f7f8fa'
}
}
}
}
>
<
Modal
title=
'AI图片生成'
open=
{
isModalOpen
}
footer=
{
null
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
width=
{
1200
}
>
<
Modal
title=
'AI图片生成'
open=
{
open
}
footer=
{
null
}
onCancel=
{
onClose
}
width=
{
1200
}
>
<
div
className=
'ai-image-generator'
>
{
/* 输入区域 */
}
<
div
className=
'input-container'
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/AiVideoDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -31,9 +31,7 @@ import './video.less'
const
{
TextArea
}
=
Input
export
default
function
AIVideoModal
(
props
)
{
const
{
editor
}
=
props
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
export
default
function
AIVideoModal
({
editor
,
open
,
onClose
})
{
const
[
prompt
,
setPrompt
]
=
useState
(
''
)
const
[
ratio
,
setRatio
]
=
useState
(
'16:9'
)
const
[
duration
,
setDuration
]
=
useState
(
5
)
...
...
@@ -138,7 +136,7 @@ export default function AIVideoModal(props) {
children
:
[{
text
:
''
}],
})
message
.
success
(
'视频已插入编辑器!'
)
setIsModalOpen
(
false
)
onClose
(
)
}
else
{
message
.
error
(
'编辑器未找到'
)
}
...
...
@@ -192,13 +190,7 @@ export default function AIVideoModal(props) {
<
ConfigProvider
theme=
{
{
components
:
{
Modal
:
{
headerBg
:
'#f7f8fa'
,
contentBg
:
'#f7f8fa'
}
}
}
}
>
<
Modal
title=
'AI视频生成'
open=
{
isModalOpen
}
footer=
{
null
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
width=
{
1200
}
>
<
Modal
title=
'AI视频生成'
open=
{
open
}
footer=
{
null
}
onCancel=
{
onClose
}
width=
{
1200
}
>
<
div
className=
'ai-video-generator'
>
<
div
className=
'input-container'
>
<
div
className=
'input-box'
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/DigitalHumanDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -13,12 +13,11 @@ import './digital-human.less'
const
CUSTOMIZE_WORKSPACE
=
'customize'
export
default
function
DigitalHumanDialog
({
editor
})
{
const
[
open
,
setOpen
]
=
useState
(
true
)
export
default
function
DigitalHumanDialog
({
editor
,
open
,
onClose
})
{
const
[
activeWorkspace
,
setActiveWorkspace
]
=
useState
(
getConfiguredDigitalHumanType
)
const
[
historySelection
,
setHistorySelection
]
=
useState
(
null
)
const
digitalHuman
=
useDigitalHuman
()
const
close
=
()
=>
setOpen
(
false
)
const
close
=
onClose
const
clearTaskSelection
=
useCallback
(()
=>
setHistorySelection
(
null
),
[])
const
sharedProps
=
{
editor
,
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/digital-human-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIDigitalHumanModal
from
'./DigitalHumanDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIDigitalHuman
extends
BaseModal
Menu
{
class
AIDigitalHuman
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-media.digital-human'
)
this
.
title
=
'数字人'
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 28 28"><path fill="currentColor" d="M12 5.5a2 2 0 0 0 1.491 1.935c.337.053.68.053 1.018 0A2 2 0 1 0 12 5.5m-1.337 1.058a3.5 3.5 0 1 1 6.675 0l4.419-1.436a2.477 2.477 0 1 1 1.53 4.712L18 11.552v3.822c0 .16.03.32.091.468l2.728 6.752a2.477 2.477 0 0 1-4.594 1.856l-2.243-5.553l-2.232 5.56a2.46 2.46 0 0 1-3.21 1.362a2.477 2.477 0 0 1-1.364-3.215l2.734-6.812q.09-.224.09-.466v-3.774L4.712 9.834a2.477 2.477 0 0 1 1.531-4.712zm2.518 2.346a5 5 0 0 1-.649-.162L5.78 6.548a.977.977 0 0 0-.604 1.859l5.46 1.774c.515.168.864.648.864 1.189v3.957c0 .35-.067.698-.198 1.024l-2.734 6.811a.977.977 0 0 0 .538 1.267a.96.96 0 0 0 1.252-.531l2.463-6.136c.42-1.045 1.897-1.047 2.319-.003l2.476 6.129a.977.977 0 1 0 1.812-.732L16.7 16.404a2.8 2.8 0 0 1-.2-1.03V11.37c0-.541.349-1.021.864-1.189l5.46-1.774a.977.977 0 1 0-.604-1.859l-6.752 2.194q-.32.104-.649.162a3.5 3.5 0 0 1-1.639 0"/></svg>`
}
getValue
(
editor
)
{
return
<
AIDigitalHumanModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AIDigitalHumanModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/feature.js
浏览文件 @
3da7f60b
...
...
@@ -7,7 +7,12 @@ import DigitalHumanDialog from './DigitalHumanDialog'
import
AiImageDialog
from
'./AiImageDialog'
import
AiVideoDialog
from
'./AiVideoDialog'
const
dialog
=
(
Component
)
=>
(
props
)
=>
createElement
(
Component
,
{
editor
:
props
.
runtime
.
editor
})
const
dialog
=
(
Component
)
=>
(
props
)
=>
createElement
(
Component
,
{
editor
:
props
.
runtime
.
editor
,
open
:
props
.
open
,
onClose
:
props
.
onClose
,
})
export
default
defineEditorFeature
({
id
:
'ai-media'
,
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/image-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/Legacy
DialogMenu'
import
DialogMenu
from
'../../core/
DialogMenu'
import
{
SlateRange
}
from
'@wangeditor/editor'
import
AIImageModal
from
'./AiImageDialog'
class
AIImage
extends
BaseModal
Menu
{
class
AIImage
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-media.image'
)
this
.
title
=
'AI图片'
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 8.12811L20.4668 8.69379C20.2864 9.10792 19.7136 9.10792 19.5331 8.69379L19.2866 8.12811C18.8471 7.11947 18.0555 6.31641 17.0677 5.87708L16.308 5.53922C15.8973 5.35653 15.8973 4.75881 16.308 4.57612L17.0252 4.25714C18.0384 3.80651 18.8442 2.97373 19.2761 1.93083L19.5293 1.31953C19.7058 0.893489 20.2942 0.893489 20.4706 1.31953L20.7238 1.93083C21.1558 2.97373 21.9616 3.80651 22.9748 4.25714L23.6919 4.57612C24.1027 4.75881 24.1027 5.35653 23.6919 5.53922L22.9323 5.87708C21.9445 6.31641 21.1529 7.11947 20.7134 8.12811ZM2.9918 3H14V5H4V19L14 9L20 15V11H22V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"></path></svg>`
}
getValue
(
editor
)
{
return
<
AIImageModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AIImageModal
>
}
isDisabled
(
editor
)
{
const
{
selection
}
=
editor
||
{}
return
!
selection
||
SlateRange
.
isExpanded
(
selection
)
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-media/video-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/Legacy
DialogMenu'
import
DialogMenu
from
'../../core/
DialogMenu'
import
{
SlateRange
}
from
'@wangeditor/editor'
import
AIVideoModal
from
'./AiVideoDialog'
class
AIVideo
extends
BaseModal
Menu
{
class
AIVideo
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-media.video'
)
this
.
title
=
'AI视频'
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.7134 8.12811L19.4668 8.69379C19.2864 9.10792 18.7136 9.10792 18.5331 8.69379L18.2866 8.12811C17.8471 7.11947 17.0555 6.31641 16.0677 5.87708L15.308 5.53922C14.8973 5.35653 14.8973 4.75881 15.308 4.57612L16.0252 4.25714C17.0384 3.80651 17.8442 2.97373 18.2761 1.93083L18.5293 1.31953C18.7058 0.893489 19.2942 0.893489 19.4706 1.31953L19.7238 1.93083C20.1558 2.97373 20.9616 3.80651 21.9748 4.25714L22.6919 4.57612C23.1027 4.75881 23.1027 5.35653 22.6919 5.53922L21.9323 5.87708C20.9445 6.31641 20.1529 7.11947 19.7134 8.12811ZM3.9934 3H13V5H5V19H19V11H21V20.0066C21 20.5552 20.5551 21 20.0066 21H3.9934C3.44476 21 3 20.5551 3 20.0066V3.9934C3 3.44476 3.44495 3 3.9934 3ZM10.6219 8.41459L15.5008 11.6672C15.6846 11.7897 15.7343 12.0381 15.6117 12.2219C15.5824 12.2658 15.5447 12.3035 15.5008 12.3328L10.6219 15.5854C10.4381 15.708 10.1897 15.6583 10.0672 15.4745C10.0234 15.4088 10 15.3316 10 15.2526V8.74741C10 8.52649 10.1791 8.34741 10.4 8.34741C10.479 8.34741 10.5562 8.37078 10.6219 8.41459Z"></path></svg>`
}
getValue
(
editor
)
{
return
<
AIVideoModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AIVideoModal
>
}
isDisabled
(
editor
)
{
const
{
selection
}
=
editor
||
{}
return
!
selection
||
SlateRange
.
isExpanded
(
selection
)
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-question/AiQuestionDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -65,9 +65,8 @@ const actionMap = {
},
}
export
default
function
AIQuestionModal
({
editor
,
action
})
{
export
default
function
AIQuestionModal
({
editor
,
action
,
open
,
onClose
})
{
const
{
message
}
=
App
.
useApp
()
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
const
[
content
,
setContent
]
=
useState
(
''
)
const
{
text
,
fetch
,
isLoading
}
=
useAIEdit
()
const
actionText
=
actionMap
[
action
]?.
name
...
...
@@ -156,17 +155,17 @@ export default function AIQuestionModal({ editor, action }) {
question_list
:
JSON
.
stringify
(
question
.
question_list
),
})
message
.
success
(
res
.
message
)
setIsModalOpen
(
false
)
onClose
(
)
}
return
(
<
Modal
title=
'AI创建试题'
open=
{
isModalO
pen
}
open=
{
o
pen
}
footer=
{
null
}
width=
{
800
}
onOk=
{
handlePrimary
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
onCancel=
{
onClose
}
>
<
Spin
spinning=
{
isLoading
}
>
<
Form
labelCol=
{
{
span
:
2
}
}
>
...
...
@@ -198,7 +197,7 @@ export default function AIQuestionModal({ editor, action }) {
</
Spin
>
<
br
/>
<
Flex
gap=
'small'
justify=
'end'
>
<
Button
onClick=
{
()
=>
setIsModalOpen
(
false
)
}
>
取消
</
Button
>
<
Button
onClick=
{
onClose
}
>
取消
</
Button
>
<
Button
type=
'primary'
onClick=
{
handleFetch
}
>
重新出题
</
Button
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-question/feature.js
浏览文件 @
3da7f60b
...
...
@@ -8,7 +8,12 @@ import AIQuestionSingle from './single-menu'
import
AiQuestionDialog
from
'./AiQuestionDialog'
const
dialog
=
(
props
)
=>
createElement
(
AiQuestionDialog
,
{
editor
:
props
.
runtime
.
editor
,
action
:
props
.
payload
.
action
})
createElement
(
AiQuestionDialog
,
{
editor
:
props
.
runtime
.
editor
,
action
:
props
.
payload
.
action
,
open
:
props
.
open
,
onClose
:
props
.
onClose
,
})
export
default
defineEditorFeature
({
id
:
'ai-question'
,
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-question/judge-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIQuestionModal
from
'./AiQuestionDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIQuestionJudge
extends
BaseModal
Menu
{
class
AIQuestionJudge
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-question.generate'
,
{
action
:
'3'
}
)
this
.
title
=
'判断题'
this
.
iconSvg
=
`<svg t="1729144923785" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="713" width="200" height="200"><path d="M804.672 27.968H219.328A182.528 182.528 0 0 0 36.672 210.624v585.408a182.528 182.528 0 0 0 182.656 182.592h585.408a182.528 182.528 0 0 0 182.656-182.656V210.624A182.144 182.144 0 0 0 804.672 27.968z m110.08 768a109.952 109.952 0 0 1-110.08 110.08H219.328a109.952 109.952 0 0 1-110.08-110.08V210.624a109.952 109.952 0 0 1 110.08-110.08h585.408a109.952 109.952 0 0 1 110.08 110.08z m-172.096-573.632l-511.04 511.04-4.096 5.248a37.248 37.248 0 0 0 4.096 46.848 36.736 36.736 0 0 0 51.2 0l511.04-511.04 4.096-5.248a37.248 37.248 0 0 0-4.096-46.848 35.712 35.712 0 0 0-51.2 0z m-4.672 468.288l56.192-56.192 4.096-4.672a30.336 30.336 0 0 0-46.848-38.08l-56.192 56.192-56.192-56.192-4.672-4.096a30.336 30.336 0 0 0-38.08 46.848l56.192 56.192-56.192 56.192-4.096 4.672a30.336 30.336 0 0 0 46.848 38.08l56.192-56.192 56.192 56.192 4.672 4.096a30.336 30.336 0 0 0 38.08-46.848z m-459.52-255.36a32.704 32.704 0 0 0 22.272 11.136h1.152a34.496 34.496 0 0 0 22.848-9.344l148.032-148.608 2.368-2.944a35.968 35.968 0 0 0 0-43.328 28.544 28.544 0 0 0-22.272-11.136 36.288 36.288 0 0 0-24 9.344L309.568 359.296l-38.4-62.656-2.368-2.944a30.4 30.4 0 0 0-42.432-4.032 35.392 35.392 0 0 0-8.768 46.272l58.56 96.576 2.368 2.944z" fill="#999999" p-id="714"></path></svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
<
AIQuestionModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'3'
></
AIQuestionModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-question/multiple-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIQuestionModal
from
'./AiQuestionDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIQuestionMultiple
extends
BaseModal
Menu
{
class
AIQuestionMultiple
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-question.generate'
,
{
action
:
'2'
}
)
this
.
title
=
'多选题'
this
.
iconSvg
=
`<svg t="1729144537633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4530" width="200" height="200"><path d="M358.4 256h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 1 1 0-102.4zM204.8 358.4a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4z m0 204.8a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4z m153.6-102.4h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 0 1 0-102.4z m0 204.8h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 0 1 0-102.4z" fill="#4C4C4C" p-id="4531"></path><path d="M204.8 51.2a153.6 153.6 0 0 0-153.6 153.6v614.4a153.6 153.6 0 0 0 153.6 153.6h614.4a153.6 153.6 0 0 0 153.6-153.6V204.8a153.6 153.6 0 0 0-153.6-153.6H204.8z m0-51.2h614.4a204.8 204.8 0 0 1 204.8 204.8v614.4a204.8 204.8 0 0 1-204.8 204.8H204.8a204.8 204.8 0 0 1-204.8-204.8V204.8a204.8 204.8 0 0 1 204.8-204.8z" fill="#4C4C4C" p-id="4532"></path></svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
<
AIQuestionModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'2'
></
AIQuestionModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-question/single-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIQuestionModal
from
'./AiQuestionDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIQuestionSingle
extends
BaseModal
Menu
{
class
AIQuestionSingle
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-question.generate'
,
{
action
:
'1'
}
)
this
.
title
=
'单选题'
this
.
iconSvg
=
`<svg t="1729144945895" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="860" width="200" height="200"><path d="M358.4 256h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 1 1 0-102.4zM204.8 358.4a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4z m153.6 102.4h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 0 1 0-102.4z m0 204.8h460.8a51.2 51.2 0 0 1 0 102.4H358.4a51.2 51.2 0 0 1 0-102.4z" fill="#4C4C4C" p-id="861"></path><path d="M204.8 51.2a153.6 153.6 0 0 0-153.6 153.6v614.4a153.6 153.6 0 0 0 153.6 153.6h614.4a153.6 153.6 0 0 0 153.6-153.6V204.8a153.6 153.6 0 0 0-153.6-153.6H204.8z m0-51.2h614.4a204.8 204.8 0 0 1 204.8 204.8v614.4a204.8 204.8 0 0 1-204.8 204.8H204.8a204.8 204.8 0 0 1-204.8-204.8V204.8a204.8 204.8 0 0 1 204.8-204.8z" fill="#4C4C4C" p-id="862"></path></svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
<
AIQuestionModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'1'
></
AIQuestionModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/AiWriteDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -17,8 +17,7 @@ import { usePaper } from '@/components/editor/hooks/useWenku'
import
{
useCopyToClipboard
}
from
'react-use'
import
{
RiMore2Line
}
from
'@remixicon/react'
export
default
function
AIModal
()
{
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
export
default
function
AIModal
({
open
,
onClose
})
{
const
[
content
,
setContent
]
=
useState
(
''
)
const
[
textIndent
,
setTextIndent
]
=
useState
(
0
)
const
prePromptRef
=
useRef
(
null
)
...
...
@@ -40,7 +39,7 @@ export default function AIModal() {
const
width
=
prePromptRef
.
current
.
offsetWidth
+
10
setTextIndent
(
width
)
}
},
[
isModalO
pen
])
},
[
o
pen
])
const
[
isEditMessage
,
setIsEditMessage
]
=
useState
(
false
)
useEffect
(()
=>
{
if
(
messageScrollRef
.
current
&&
!
isEditMessage
)
{
...
...
@@ -237,13 +236,7 @@ export default function AIModal() {
<
ConfigProvider
theme=
{
{
components
:
{
Modal
:
{
headerBg
:
'#f7f8fa'
,
contentBg
:
'#f7f8fa'
}
}
}
}
>
<
Modal
title=
'长文写作/课程大纲:'
open=
{
isModalOpen
}
footer=
{
null
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
width=
{
1000
}
>
<
Modal
title=
'长文写作/课程大纲:'
open=
{
open
}
footer=
{
null
}
onCancel=
{
onClose
}
width=
{
1000
}
>
<
div
className=
'message-scroll'
ref=
{
messageScrollRef
}
>
{
messages
.
map
((
msg
)
=>
{
return
<
MessageRender
msg=
{
msg
}
key=
{
msg
.
id
}
></
MessageRender
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/BaiduSearchDialog.jsx
deleted
100644 → 0
浏览文件 @
a9fbd6f2
import
{
useState
,
useEffect
,
useRef
}
from
'react'
import
{
SendOutlined
,
UndoOutlined
,
CopyOutlined
,
LinkOutlined
}
from
'@ant-design/icons'
import
{
ConfigProvider
,
Modal
,
Input
,
Button
}
from
'antd'
const
{
TextArea
}
=
Input
import
'./search.less'
import
{
useSearch
}
from
'@/components/editor/hooks/useWenku'
import
{
useCopyToClipboard
}
from
'react-use'
export
default
function
AIModal
()
{
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
const
[
content
,
setContent
]
=
useState
(
''
)
const
[
textIndent
,
setTextIndent
]
=
useState
(
0
)
const
prePromptRef
=
useRef
(
null
)
const
messageScrollRef
=
useRef
(
null
)
const
{
messages
,
isLoading
,
query
}
=
useSearch
()
useEffect
(()
=>
{
if
(
prePromptRef
.
current
)
{
const
width
=
prePromptRef
.
current
.
offsetWidth
+
10
setTextIndent
(
width
)
}
},
[
isModalOpen
])
useEffect
(()
=>
{
if
(
messageScrollRef
.
current
)
{
const
scrollContainer
=
messageScrollRef
.
current
scrollContainer
.
scrollTop
=
scrollContainer
.
scrollHeight
}
},
[
messages
])
const
prePrompt
=
'帮我找一些文献资料,主题是:'
const
handleEnterSearch
=
(
e
)
=>
{
if
(
e
.
key
===
'Enter'
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
()
!
isLoading
&&
handleSearch
()
}
}
const
handleSearch
=
()
=>
{
query
(
prePrompt
+
content
)
setContent
(
''
)
}
const
[,
copyToClipboard
]
=
useCopyToClipboard
()
const
handleCopy
=
(
content
)
=>
{
copyToClipboard
(
content
)
}
const
MessageRender
=
({
msg
})
=>
{
if
(
msg
.
role
===
'ai'
)
{
const
copyMessageContent
=
msg
.
searchReferList
?.
map
((
refer
,
index
)
=>
`
${
index
+
1
}
.《
${
refer
.
title
}
》\n
${
refer
.
abstract
}
\n`
)
.
join
(
'
\
n'
)
return
(
msg
.
searchReferList
?.
length
>
0
&&
(
<
div
className=
{
`message-item ${msg.role}`
}
>
<
div
className=
'message-box'
>
<
div
className=
'message-content'
>
{
msg
.
searchReferList
.
map
((
refer
,
index
)
=>
{
return
(
<
div
key=
{
index
}
className=
'result-item'
>
<
p
>
<
strong
>
{
index
+
1
}
.《
{
refer
.
title
}
》
</
strong
>
</
p
>
<
p
>
{
refer
.
abstract
}
<
a
href=
{
refer
.
orgUrl
}
target=
'_blank'
rel=
'noreferrer'
>
<
LinkOutlined
/>
</
a
>
</
p
>
</
div
>
)
})
}
</
div
>
<
div
className=
'message-tools'
>
<
Button
type=
'text'
size=
'small'
icon=
{
<
UndoOutlined
/>
}
disabled=
{
isLoading
}
onClick=
{
()
=>
query
(
msg
.
userQuery
)
}
>
重新生成
</
Button
>
<
Button
type=
'text'
size=
'small'
icon=
{
<
CopyOutlined
/>
}
onClick=
{
()
=>
handleCopy
(
copyMessageContent
)
}
>
复制内容
</
Button
>
</
div
>
</
div
>
</
div
>
)
)
}
else
{
return
(
<
div
className=
{
`message-item ${msg.role}`
}
>
<
div
className=
'message-box'
>
<
div
className=
'message-content'
>
{
msg
.
content
}
</
div
>
</
div
>
</
div
>
)
}
}
return
(
<
ConfigProvider
theme=
{
{
components
:
{
Modal
:
{
headerBg
:
'#f7f8fa'
,
contentBg
:
'#f7f8fa'
}
}
}
}
>
<
Modal
title=
'学术/文献搜索:'
open=
{
isModalOpen
}
footer=
{
null
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
width=
{
1000
}
>
<
div
className=
'message-scroll'
ref=
{
messageScrollRef
}
>
{
messages
.
map
((
msg
,
index
)
=>
{
return
<
MessageRender
msg=
{
msg
}
key=
{
index
}
></
MessageRender
>
})
}
</
div
>
<
div
className=
'input-container'
>
<
div
className=
'input-box'
>
<
div
className=
'edit-area'
>
{
prePrompt
&&
(
<
span
className=
'pre-prompt'
ref=
{
prePromptRef
}
>
{
prePrompt
}
</
span
>
)
}
<
TextArea
className=
'content'
autoSize
value=
{
content
}
placeholder=
'请输入关键词'
onChange=
{
(
e
)
=>
setContent
(
e
.
target
.
value
)
}
onKeyDown=
{
handleEnterSearch
}
style=
{
{
textIndent
}
}
/>
</
div
>
<
div
className=
'input-tools'
>
<
Button
type=
'primary'
size=
'large'
icon=
{
<
SendOutlined
/>
}
onClick=
{
handleSearch
}
loading=
{
isLoading
}
/>
</
div
>
</
div
>
</
div
>
</
Modal
>
</
ConfigProvider
>
)
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/SearchDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -7,8 +7,7 @@ import { useSearch } from '@/components/editor/hooks/useWenku'
import
{
useCopyToClipboard
}
from
'react-use'
export
default
function
AIModal
()
{
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
export
default
function
AIModal
({
open
,
onClose
})
{
const
[
content
,
setContent
]
=
useState
(
''
)
const
[
textIndent
,
setTextIndent
]
=
useState
(
0
)
const
prePromptRef
=
useRef
(
null
)
...
...
@@ -20,7 +19,7 @@ export default function AIModal() {
const
width
=
prePromptRef
.
current
.
offsetWidth
+
10
setTextIndent
(
width
)
}
},
[
isModalO
pen
])
},
[
o
pen
])
useEffect
(()
=>
{
if
(
messageScrollRef
.
current
)
{
...
...
@@ -113,13 +112,7 @@ export default function AIModal() {
<
ConfigProvider
theme=
{
{
components
:
{
Modal
:
{
headerBg
:
'#f7f8fa'
,
contentBg
:
'#f7f8fa'
}
}
}
}
>
<
Modal
title=
'学术/文献搜索:'
open=
{
isModalOpen
}
footer=
{
null
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
width=
{
1000
}
>
<
Modal
title=
'学术/文献搜索:'
open=
{
open
}
footer=
{
null
}
onCancel=
{
onClose
}
width=
{
1000
}
>
<
div
className=
'message-scroll'
ref=
{
messageScrollRef
}
>
{
messages
.
map
((
msg
,
index
)
=>
{
return
<
MessageRender
msg=
{
msg
}
key=
{
index
}
></
MessageRender
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/baidu-search-menu.jsx
deleted
100644 → 0
浏览文件 @
a9fbd6f2
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AISearchModal
from
'./BaiduSearchDialog'
class
AIBaiduSearch
extends
BaseModalMenu
{
constructor
()
{
super
()
this
.
title
=
'学术搜索'
this
.
iconSvg
=
`<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-file-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M12 21h-5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v4.5" /><path d="M16.5 17.5m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0" /><path d="M18.5 19.5l2.5 2.5" /></svg>`
}
getValue
(
editor
)
{
return
<
AISearchModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AISearchModal
>
}
}
export
default
{
key
:
'AIBaiduSearch'
,
factory
()
{
return
new
AIBaiduSearch
()
},
}
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
{
createElement
}
from
'react'
import
AIBaiduSearch
from
'./baidu-search-menu'
import
AISearch
from
'./search-menu'
import
AIWrite
from
'./write-menu'
import
SearchDialog
from
'./SearchDialog'
import
BaiduSearchDialog
from
'./BaiduSearchDialog'
import
AiWriteDialog
from
'./AiWriteDialog'
const
searchDialog
=
(
props
)
=>
{
c
onst
Component
=
props
.
payload
.
provider
===
'baidu'
?
BaiduSearchDialog
:
SearchDialog
return
createElement
(
Component
,
{
editor
:
props
.
runtime
.
editor
})
}
const
writeDialog
=
(
props
)
=>
createElement
(
AiWriteDialog
,
{
editor
:
props
.
runtime
.
editor
})
const
dialog
=
(
Component
)
=>
(
props
)
=>
c
reateElement
(
Component
,
{
open
:
props
.
open
,
onClose
:
props
.
onClose
,
})
export
default
defineEditorFeature
({
id
:
'ai-research'
,
menus
:
[
{
order
:
300
,
config
:
AISearch
},
{
order
:
310
,
config
:
AIWrite
},
{
order
:
340
,
config
:
AIBaiduSearch
},
],
dialogs
:
[
{
id
:
'ai-research.search'
,
component
:
searchDialog
},
{
id
:
'ai-research.write'
,
component
:
writeDialog
},
{
id
:
'ai-research.search'
,
component
:
dialog
(
SearchDialog
)
},
{
id
:
'ai-research.write'
,
component
:
dialog
(
AiWriteDialog
)
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/search-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AISearchModal
from
'./SearchDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AISearch
extends
BaseModal
Menu
{
class
AISearch
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-research.search'
,
{
provider
:
'search'
}
)
this
.
title
=
'学术搜索'
this
.
iconSvg
=
`<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-file-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M12 21h-5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v4.5" /><path d="M16.5 17.5m-2.5 0a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0" /><path d="M18.5 19.5l2.5 2.5" /></svg>`
}
getValue
(
editor
)
{
return
<
AISearchModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AISearchModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-research/write-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIWriteModal
from
'./AiWriteDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIWrite
extends
BaseModal
Menu
{
class
AIWrite
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-research.write'
)
this
.
title
=
'写作/大纲'
this
.
iconSvg
=
`<svg style="fill:none" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pen"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>`
}
getValue
(
editor
)
{
return
<
AIWriteModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AIWriteModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/AiChatDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -8,9 +8,8 @@ import { useAI } from '@/components/editor/hooks/useAI'
import
'./styles.less'
const
AIChatDrawer
=
(
props
)
=>
{
const
selectText
=
props
.
editor
.
getSelectionText
()
const
[
open
,
setOpen
]
=
useState
(
true
)
const
AIChatDrawer
=
({
editor
,
open
,
onClose
})
=>
{
const
selectText
=
editor
.
getSelectionText
()
const
{
ai
,
setAI
,
options
,
messages
,
post
,
isLoading
}
=
useAI
()
...
...
@@ -48,7 +47,7 @@ const AIChatDrawer = (props) => {
size=
{
600
}
title=
'AI对话'
destroyOnHidden
onClose=
{
()
=>
setOpen
(
false
)
}
onClose=
{
onClose
}
rootClassName=
'ai-drawer-wrapper'
className=
'ai-drawer-container'
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/chat-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIChatDrawer
from
'./AiChatDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIChat
extends
BaseModal
Menu
{
class
AIChat
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.chat'
)
this
.
title
=
'AI'
this
.
iconSvg
=
`<svg t="1713246032755" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12352" width="16" height="16"><path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#cdcdcd" p-id="12353"></path></svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
<
AIChatDrawer
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
AIChatDrawer
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/content-inspect-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIContentInspect
extends
BaseModal
Menu
{
class
AIContentInspect
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'contentInspect'
}
)
this
.
title
=
'内容检查'
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11.5 13.5q.525 0 .988-.137t.912-.413l1.525 1.575q.2.225.513.213t.537-.213q.225-.225.238-.537T16 13.45l-1.55-1.55q.275-.425.413-.9T15 10q0-1.475-1.038-2.488T11.5 6.5T9.037 7.513T8 10t1.038 2.488T11.5 13.5m0-1.5q-.825 0-1.412-.587T9.5 10t.588-1.412T11.5 8q.8 0 1.4.588T13.5 10t-.587 1.413T11.5 12M2 21q-.425 0-.712-.288T1 20t.288-.712T2 19h20q.425 0 .713.288T23 20t-.288.713T22 21zm2-3q-.825 0-1.412-.587T2 16V5q0-.825.588-1.412T4 3h16q.825 0 1.413.588T22 5v11q0 .825-.587 1.413T20 18zm0-2h16V5H4zm0 0V5z"/></svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'contentInspect'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/expand-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
class
AIExpand
extends
BaseModalMenu
{
import
DialogMenu
from
'../../core/DialogMenu'
class
AIExpand
extends
DialogMenu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'expand'
}
)
this
.
title
=
'扩写'
this
.
iconSvg
=
`<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
...
...
@@ -14,9 +13,6 @@ class AIExpand extends BaseModalMenu {
</g>
</svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'expand'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/polishing-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIPolishing
extends
BaseModal
Menu
{
class
AIPolishing
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'abbreviate'
}
)
this
.
title
=
'润色'
this
.
iconSvg
=
`<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
...
...
@@ -19,9 +18,6 @@ class AIPolishing extends BaseModalMenu {
</g>
</svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'abbreviate'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/punctuation-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIPunctuation
extends
BaseModal
Menu
{
class
AIPunctuation
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'punctuation'
}
)
this
.
title
=
'标点校对'
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M920 416H616c-4.4 0-8 3.6-8 8v112c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-56h60v320h-46c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h164c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8h-46V480h60v56c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V424c0-4.4-3.6-8-8-8M656 296V168c0-4.4-3.6-8-8-8H104c-4.4 0-8 3.6-8 8v128c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-64h168v560h-92c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-92V232h168v64c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8"/></svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'punctuation'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/rewrite-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AIRewrite
extends
BaseModal
Menu
{
class
AIRewrite
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'rewrite'
}
)
this
.
title
=
'改写'
this
.
iconSvg
=
`<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
...
...
@@ -18,9 +17,6 @@ class AIRewrite extends BaseModalMenu {
</svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'rewrite'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/summary-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AISummary
extends
BaseModal
Menu
{
class
AISummary
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'summary'
}
)
this
.
title
=
'总结'
this
.
iconSvg
=
`<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
...
...
@@ -18,9 +17,6 @@ class AISummary extends BaseModalMenu {
</g>
</svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'summary'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/ai-text/translate-menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/LegacyDialogMenu'
import
AIModal
from
'./AiTextDialog'
import
DialogMenu
from
'../../core/DialogMenu'
class
AITranslate
extends
BaseModal
Menu
{
class
AITranslate
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'ai-text.action'
,
{
action
:
'translate'
}
)
this
.
title
=
'翻译'
this
.
iconSvg
=
`<svg fill="#000000" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><defs><style>.cls-1{fill:none;}</style></defs><title>translate</title><path d="M27.85,29H30L24,14H21.65l-6,15H17.8l1.6-4h6.85ZM20.2,23l2.62-6.56L25.45,23Z"></path><path d="M18,7V5H11V2H9V5H2V7H12.74a14.71,14.71,0,0,1-3.19,6.18A13.5,13.5,0,0,1,7.26,9H5.16a16.47,16.47,0,0,0,3,5.58A16.84,16.84,0,0,1,3,18l.75,1.86A18.47,18.47,0,0,0,9.53,16a16.92,16.92,0,0,0,5.76,3.84L16,18a14.48,14.48,0,0,1-5.12-3.37A17.64,17.64,0,0,0,14.8,7Z"></path><rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"></rect></g></svg>`
}
getValue
(
editor
)
{
return
<
AIModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
action=
'translate'
></
AIModal
>
}
}
export
default
{
...
...
apps/admin/src/components/editor/wangeditor-customer/features/audio/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
AudioDialog
from
'./AudioDialog'
export
default
defineEditorFeature
({
...
...
@@ -12,11 +11,4 @@ export default defineEditorFeature({
{
id
:
'audio.insert'
,
component
:
AudioDialog
},
{
id
:
'audio.edit'
,
component
:
AudioDialog
},
],
events
:
[
{
name
:
'AudioMenuClick'
,
handler
:
({
editor
,
payload
})
=>
openEditorDialog
(
editor
,
payload
?.
path
?
'audio.edit'
:
'audio.insert'
,
payload
),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/audio/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
AudioAuto
{
constructor
()
{
...
...
@@ -61,14 +62,14 @@ class AudioAuto {
const
chapterAudio
=
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterAudio'
)
if
(
chapterAudio
)
{
const
path
=
DomEditor
.
findPath
(
editor
,
chapterAudio
)
editor
.
emit
(
'AudioMenuClick
'
,
{
openEditorDialog
(
editor
,
'audio.edit
'
,
{
path
,
audioUrl
:
chapterAudio
.
audioUrl
||
''
,
title
:
chapterAudio
.
title
||
''
,
})
return
}
editor
.
emit
(
'AudioMenuClick
'
)
openEditorDialog
(
editor
,
'audio.insert
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/audio/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
DomEditor
}
from
'@wangeditor/editor'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withAudioNode
=
(
editor
)
=>
{
const
{
isVoid
,
normalizeNode
}
=
editor
const
{
isVoid
}
=
editor
const
newEditor
=
editor
newEditor
.
isVoid
=
(
elem
)
=>
{
...
...
@@ -12,35 +13,7 @@ const withAudioNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterAudio'
)
{
// 未命中 chapterAudio ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterAudio'
,
[
'paragraph'
,
'blockquote'
])
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/chapter-structure/feature.js
浏览文件 @
3da7f60b
...
...
@@ -3,7 +3,6 @@ import itemMenu from './section-menu'
import
titleMenu
from
'./title-menu'
import
sectionNodeModule
from
'./section-node'
import
headerNodeModule
from
'./title-node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
ChapterTitleFeatureDialog
,
ChapterSectionFeatureDialog
}
from
'./FeatureDialogs'
export
default
defineEditorFeature
({
...
...
@@ -20,14 +19,4 @@ export default defineEditorFeature({
{
id
:
'chapter-structure.title'
,
component
:
ChapterTitleFeatureDialog
},
{
id
:
'chapter-structure.section'
,
component
:
ChapterSectionFeatureDialog
},
],
events
:
[
{
name
:
'ChapterItemMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'chapter-structure.section'
),
},
{
name
:
'ChapterTitleClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'chapter-structure.title'
),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/chapter-structure/section-menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
ChapterItem
{
constructor
()
{
...
...
@@ -55,7 +56,7 @@ class ChapterItem {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'ChapterItemMenuClick
'
)
openEditorDialog
(
editor
,
'chapter-structure.section
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/chapter-structure/section-node.js
浏览文件 @
3da7f60b
...
...
@@ -3,9 +3,10 @@ import iconClose from '@/assets/images/icon_chapter_close.png'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
findNodeWithParent
}
from
'../../shared/editor-settings'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withChapterSectionNode
=
(
editor
)
=>
{
const
{
is
Inline
,
isVoid
,
normalizeNode
}
=
editor
const
{
is
Void
}
=
editor
const
newEditor
=
editor
// newEditor.isInline = (elem) => {
...
...
@@ -20,41 +21,7 @@ const withChapterSectionNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterSection'
)
{
// 未命中 chapterSection ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterSection'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/chapter-structure/title-menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
ChapterTitle
{
constructor
()
{
...
...
@@ -55,7 +56,7 @@ class ChapterTitle {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'ChapterTitleClick
'
)
openEditorDialog
(
editor
,
'chapter-structure.title
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/chapter-structure/title-node.js
浏览文件 @
3da7f60b
...
...
@@ -3,9 +3,10 @@ import iconClose from '@/assets/images/icon_chapter_close.png'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
findNodeWithParent
}
from
'../../shared/editor-settings'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withChapterHeaderNode
=
(
editor
)
=>
{
const
{
is
Inline
,
isVoid
,
normalizeNode
}
=
editor
const
{
is
Void
}
=
editor
const
newEditor
=
editor
// newEditor.isInline = (elem) => {
...
...
@@ -20,40 +21,7 @@ const withChapterHeaderNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterHeader'
)
{
// 未命中 chapterHeader ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote (否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterHeader'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/content-tools/remove-spaces-menu.js
浏览文件 @
3da7f60b
const
CHINESE_TEXT
=
/
[\u
3400-
\u
4dbf
\u
4e00-
\u
9fff
\u
f900-
\u
faff
]
/
const
PARAGRAPH
=
/<p
\b[^
>
]
*>
[\s\S]
*
?
<
\/
p>/gi
const
TEXT_BETWEEN_TAGS
=
/>
([^
<
]
*
)
</g
const
SPACES
=
/
(?:
| |�*a0;|
[
\u
00a0
])
+/gi
export
const
removeSpacesFromChineseParagraphs
=
(
html
)
=>
html
.
replace
(
PARAGRAPH
,
(
paragraph
)
=>
{
const
text
=
paragraph
.
replace
(
/<
[^
>
]
+>/g
,
''
)
if
(
!
CHINESE_TEXT
.
test
(
text
))
return
paragraph
return
paragraph
.
replace
(
TEXT_BETWEEN_TAGS
,
(
_
,
content
)
=>
{
return
`>
${
content
.
replace
(
SPACES
,
''
)}
<`
})
})
class
RemoveSpaces
{
constructor
()
{
this
.
title
=
'去空格'
...
...
@@ -5,49 +20,29 @@ class RemoveSpaces {
this
.
tag
=
'button'
}
// eslint-disable-next-line no-unused-vars
getValue
(
editor
)
{
getValue
()
{
return
''
}
// eslint-disable-next-line no-unused-vars
isActive
(
editor
)
{
isActive
()
{
return
false
}
// eslint-disable-next-line no-unused-vars
isDisabled
(
editor
)
{
isDisabled
()
{
return
false
}
exec
(
editor
)
{
// 获取当前 HTML
const
html
=
editor
.
getHtml
()
if
(
!
html
)
return
if
(
!
html
)
{
return
}
// 使用正则表达式去除 HTML 标签之间文本的空格
// 匹配 >文本< 之间的内容,去除空格字符(包括多个相邻的空格)和 ,保留其他字符和标签结构
const
htmlWithoutSpaces
=
html
.
replace
(
/>
([^
<
]
*
?)
</g
,
(
match
,
text
)
=>
{
// 先去除所有的 HTML 实体
let
textWithoutSpaces
=
text
.
replace
(
/ +/gi
,
''
)
// 然后去除所有空格字符(包括多个相邻的空格),保留其他字符
textWithoutSpaces
=
textWithoutSpaces
.
replace
(
/ +/g
,
''
)
return
`>
${
textWithoutSpaces
}
<`
})
// 如果内容有变化,设置回去
if
(
htmlWithoutSpaces
!==
html
)
{
// 设置处理后的 HTML
editor
.
setHtml
(
htmlWithoutSpaces
)
}
const
nextHtml
=
removeSpacesFromChineseParagraphs
(
html
)
if
(
nextHtml
!==
html
)
editor
.
setHtml
(
nextHtml
)
}
}
export
default
{
key
:
'RemoveSpaces'
,
// 定义 menu key :要保证唯一、不重复(重要)
key
:
'RemoveSpaces'
,
factory
()
{
return
new
RemoveSpaces
()
},
...
...
apps/admin/src/components/editor/wangeditor-customer/features/expand-read/feature.js
浏览文件 @
3da7f60b
...
...
@@ -2,8 +2,6 @@ import { defineEditorFeature } from '../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
simpleNodeModule
from
'./simple-node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
import
ExpandReadFeatureDialog
from
'./FeatureDialog'
export
default
defineEditorFeature
({
...
...
@@ -17,13 +15,4 @@ export default defineEditorFeature({
{
id
:
'expand-read.insert'
,
component
:
ExpandReadFeatureDialog
},
{
id
:
'expand-read.edit'
,
component
:
ExpandReadFeatureDialog
},
],
events
:
[
{
name
:
'ExpandReadClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'expand-read.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
),
}),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/expand-read/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
class
ExpandRead
{
constructor
()
{
...
...
@@ -54,7 +56,9 @@ class ExpandRead {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'ExpandReadClick'
)
openEditorDialog
(
editor
,
'expand-read.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
),
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/expand-read/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateEditor
,
SlateElement
,
SlateNode
,
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
iconExpand
from
'@/assets/images/editor/icon_expand_read.png'
import
iconClose
from
'@/assets/images/icon_chapter_close.png'
import
{
findNodeWithParent
}
from
'../../shared/editor-settings'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withExpandReadNode
=
(
editor
)
=>
{
const
{
is
Inline
,
isVoid
,
normalizeNode
}
=
editor
const
{
is
Void
}
=
editor
const
newEditor
=
editor
// newEditor.isInline = (elem) => {
...
...
@@ -27,41 +22,7 @@ const withExpandReadNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterExpandRead'
)
{
// 未命中 chapterExpandRead ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterExpandRead'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/expand-read/simple-node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateEditor
,
SlateElement
,
SlateNode
,
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
iconExpand
from
'@/assets/images/editor/icon_expand_tips.png'
import
iconExpandT
from
'@/assets/images/editor/icon_expand_editor.png'
import
{
normalizeInlineVoidNode
}
from
'../../shared/normalize-inline-void'
const
withExpandReadNode
=
(
editor
)
=>
{
const
{
isInline
,
isVoid
,
normalizeNode
}
=
editor
...
...
@@ -28,43 +21,14 @@ const withExpandReadNode = (editor) => {
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterExpandReadSimple'
)
{
// 未命中 chapterExpandReadSimple ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]]
||
{}
if
(
nextNode
.
type
===
'paragraph'
)
{
const
lastChild
=
nextNode
.
children
[
nextNode
.
children
.
length
-
1
]
if
(
lastChild
.
type
===
'chapterExpandReadSimple'
)
{
SlateTransforms
.
insertNodes
(
newEditor
,
{
type
:
'span'
,
children
:
[{
text
:
' '
}]
},
{
at
:
path
,
// 在 link-card 后面插入
},
)
}
}
// const nextNodeType = DomEditor.getNodeType(nextNode);
// console.log(nextNode, nextNodeType);
// if (
// nextNodeType !== 'paragraph' &&
// nextNodeType !== 'blockquote' &&
// !nextNodeType.startsWith('header')
// ) {
// // link-card node 后面不是 p 或 header ,则插入一个空 p
// const p = { type: 'paragraph', children: [{ text: '' }] };
// const insertPath = [path[0] + 1];
// SlateTransforms.insertNodes(newEditor, p, {
// at: insertPath, // 在 link-card 后面插入
// });
// }
return
normalizeInlineVoidNode
({
editor
:
newEditor
,
node
,
path
,
type
:
'chapterExpandReadSimple'
,
normalizeNode
,
transforms
:
SlateTransforms
,
})
}
return
newEditor
// 返回 newEditor ,重要!!!
...
...
apps/admin/src/components/editor/wangeditor-customer/features/formula/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
FormulaDialog
from
'./FormulaDialog'
export
default
defineEditorFeature
({
...
...
@@ -9,10 +8,4 @@ export default defineEditorFeature({
menus
:
[{
order
:
60
,
config
:
menu
}],
nodeModules
:
[{
order
:
80
,
types
:
[
'chapterFormula'
],
module
:
nodeModule
}],
dialogs
:
[{
id
:
'formula.insert'
,
component
:
FormulaDialog
}],
events
:
[
{
name
:
'FormulaMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'formula.insert'
),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/formula/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
FormulaAuto
{
constructor
()
{
...
...
@@ -58,7 +59,7 @@ class FormulaAuto {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'FormulaMenuClick
'
)
openEditorDialog
(
editor
,
'formula.insert
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/formula/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
icon_tooltip_editor
from
'@/assets/images/editor/icon_tooltip_editor.png'
import
{
h
}
from
'snabbdom'
import
{
normalizeInlineVoidNode
}
from
'../../shared/normalize-inline-void'
const
withChapterFormulaNode
=
(
editor
)
=>
{
const
{
isInline
,
isVoid
,
normalizeNode
}
=
editor
...
...
@@ -20,26 +20,14 @@ const withChapterFormulaNode = (editor) => {
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterFormula'
)
{
// 未命中 chapterHeader ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
const
topLevelNodes
=
newEditor
.
children
||
[]
const
nextNode
=
topLevelNodes
[
path
[
0
]]
||
{}
if
(
nextNode
.
type
===
'paragraph'
)
{
const
lastChild
=
nextNode
.
children
[
nextNode
.
children
.
length
-
1
]
if
(
lastChild
.
type
===
'chapterExpandReadSimple'
)
{
SlateTransforms
.
insertNodes
(
newEditor
,
{
type
:
'span'
,
children
:
[{
text
:
''
}]
},
{
at
:
path
,
// 在 link-card 后面插入
},
)
}
}
return
normalizeInlineVoidNode
({
editor
:
newEditor
,
node
,
path
,
type
:
'chapterFormula'
,
normalizeNode
,
transforms
:
SlateTransforms
,
})
}
return
newEditor
// 返回 newEditor ,重要!!!
...
...
apps/admin/src/components/editor/wangeditor-customer/features/gallery/feature.js
浏览文件 @
3da7f60b
...
...
@@ -3,8 +3,6 @@ import menu from './menu'
import
onlineMenu
from
'./online-menu'
import
nodeModule
from
'./node'
import
inlineNodeModule
from
'./inline-node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
import
GalleryFeatureDialog
from
'./FeatureDialog'
export
default
defineEditorFeature
({
...
...
@@ -21,22 +19,4 @@ export default defineEditorFeature({
{
id
:
'gallery.insert'
,
component
:
GalleryFeatureDialog
},
{
id
:
'gallery.edit'
,
component
:
GalleryFeatureDialog
},
],
events
:
[
{
name
:
'GalleryMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'gallery.insert'
,
{
isOnline
:
false
,
selectionSize
:
getSelectionFontSize
(
editor
),
}),
},
{
name
:
'GalleryOnlineMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'gallery.insert'
,
{
isOnline
:
true
,
selectionSize
:
getSelectionFontSize
(
editor
),
}),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/gallery/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
class
GalleryAuto
{
constructor
()
{
...
...
@@ -60,7 +62,10 @@ class GalleryAuto {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'GalleryMenuClick'
)
openEditorDialog
(
editor
,
'gallery.insert'
,
{
isOnline
:
false
,
selectionSize
:
getSelectionFontSize
(
editor
),
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/gallery/node.js
浏览文件 @
3da7f60b
...
...
@@ -4,9 +4,10 @@ import iconGallery from '@/assets/images/editor/icon_gallery_editor.png'
import
iconClose
from
'@/assets/images/icon_chapter_close.png'
import
{
findNodeWithParent
}
from
'../../shared/editor-settings'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withGalleryNode
=
(
editor
)
=>
{
const
{
isVoid
,
normalizeNode
}
=
editor
const
{
isVoid
}
=
editor
const
newEditor
=
editor
// newEditor.isInline = (elem) => {
...
...
@@ -21,41 +22,7 @@ const withGalleryNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterGallery'
)
{
// 未命中 chapterGallery ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterGallery'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/gallery/online-menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
class
GalleryAutoOnline
{
constructor
()
{
...
...
@@ -65,7 +67,10 @@ class GalleryAutoOnline {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'GalleryOnlineMenuClick'
)
openEditorDialog
(
editor
,
'gallery.insert'
,
{
isOnline
:
true
,
selectionSize
:
getSelectionFontSize
(
editor
),
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/history/FeatureDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -21,8 +21,7 @@ export default function HistoryFeatureDialog({ open, onClose, runtime }) {
<
HistoryDialog
chapterId=
{
runtime
.
chapterId
}
bookId=
{
runtime
.
bookId
}
saveContent=
{
runtime
.
saveContent
}
setContent=
{
runtime
.
setContent
}
onRestore=
{
runtime
.
restoreContent
}
setHistoryVisible=
{
(
visible
)
=>
!
visible
&&
onClose
()
}
/>
</
Modal
>
...
...
apps/admin/src/components/editor/wangeditor-customer/features/history/HistoryDialog.jsx
浏览文件 @
3da7f60b
import
React
,
{
useState
}
from
'react'
import
{
Divider
,
Button
,
Spin
,
Modal
,
Row
,
Col
,
Space
}
from
'antd'
import
{
useEffect
,
useState
}
from
'react'
import
{
Button
,
Spin
,
Modal
,
Row
,
Space
}
from
'antd'
import
{
CloseOutlined
}
from
'@ant-design/icons'
import
{
Editor
,
Toolbar
}
from
'@wangeditor/editor-for-react'
import
{
getRecordList
,
getRecordInfo
}
from
'../../shared/request'
import
'./styles.less'
import
{
useEffect
}
from
'react'
const
HistoryEditor
=
(
props
)
=>
{
const
{
setHistoryVisible
,
chapterId
,
bookId
,
setContent
,
saveContent
}
=
props
const
{
setHistoryVisible
,
chapterId
,
bookId
,
onRestore
}
=
props
const
[
loading
,
setLoading
]
=
useState
(
false
)
const
[
delModal
,
setDelModal
]
=
useState
(
false
)
...
...
@@ -16,9 +14,8 @@ const HistoryEditor = (props) => {
const
[
recordId
,
setRecordId
]
=
useState
(
null
)
// 当前的记录id
// 确认还原
const
handleSuccess
=
()
=>
{
setContent
(
recordContent
)
saveContent
()
const
handleSuccess
=
async
()
=>
{
await
onRestore
(
recordContent
)
setDelModal
(
false
)
setHistoryVisible
(
false
)
}
...
...
@@ -34,7 +31,7 @@ const HistoryEditor = (props) => {
})
if
(
data
.
length
>
0
)
{
setRecordList
(
data
)
await
getRecordInfoVal
(
data
[
0
].
name
)
setRecordId
(
data
[
0
].
name
)
}
setLoading
(
false
)
}
...
...
@@ -48,7 +45,7 @@ const HistoryEditor = (props) => {
name
:
name
||
recordId
,
})
if
(
name
)
{
setRecordId
[
name
]
setRecordId
(
name
)
}
setRecordContent
(
data
.
content
)
setLoading
(
false
)
...
...
@@ -140,10 +137,10 @@ const HistoryEditor = (props) => {
onCancel=
{
()
=>
setDelModal
(
false
)
}
footer=
{
(
_
,
{
OkBtn
,
CancelBtn
})
=>
{
return
(
<
Row
justify=
{
'end'
}
className=
'delModal'
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Row
justify=
{
'end'
}
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Space
size=
{
20
}
>
<
CancelBtn
className=
'cancel'
/>
<
OkBtn
className=
'true'
/>
<
CancelBtn
/>
<
OkBtn
/>
</
Space
>
</
Row
>
)
...
...
apps/admin/src/components/editor/wangeditor-customer/features/icon/menu.jsx
浏览文件 @
3da7f60b
import
BaseModalMenu
from
'../../core/Legacy
DialogMenu'
import
DialogMenu
from
'../../core/
DialogMenu'
import
{
SlateRange
}
from
'@wangeditor/editor'
import
IconModal
from
'./IconDialog'
class
Icon
extends
BaseModal
Menu
{
class
Icon
extends
Dialog
Menu
{
constructor
()
{
super
()
super
(
'icon.insert'
)
this
.
title
=
'插入图标'
// Using a smiley/icon-like SVG
this
.
iconSvg
=
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10.5199 19.8634C10.5955 18.6615 10.8833 17.5172 11.3463 16.4676C9.81124 16.3252 8.41864 15.6867 7.33309 14.7151L8.66691 13.2248C9.55217 14.0172 10.7188 14.4978 12 14.4978C12.1763 14.4978 12.3501 14.4887 12.5211 14.471C14.227 12.2169 16.8661 10.7083 19.8634 10.5199C19.1692 6.80877 15.9126 4 12 4C7.58172 4 4 7.58172 4 12C4 15.9126 6.80877 19.1692 10.5199 19.8634ZM19.0233 12.636C15.7891 13.2396 13.2396 15.7891 12.636 19.0233L19.0233 12.636ZM22 12C22 12.1677 21.9959 12.3344 21.9877 12.5L12.5 21.9877C12.3344 21.9959 12.1677 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM10 10C10 10.8284 9.32843 11.5 8.5 11.5C7.67157 11.5 7 10.8284 7 10C7 9.17157 7.67157 8.5 8.5 8.5C9.32843 8.5 10 9.17157 10 10ZM17 10C17 10.8284 16.3284 11.5 15.5 11.5C14.6716 11.5 14 10.8284 14 10C14 9.17157 14.6716 8.5 15.5 8.5C16.3284 8.5 17 9.17157 17 10Z"></path></svg>`
}
getValue
(
editor
)
{
return
<
IconModal
key=
{
Date
.
now
()
}
editor=
{
editor
}
></
IconModal
>
}
isDisabled
(
editor
)
{
const
{
selection
}
=
editor
||
{}
return
!
selection
||
SlateRange
.
isExpanded
(
selection
)
...
...
apps/admin/src/components/editor/wangeditor-customer/features/image/edit-menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
const
getSelectedImageInfo
=
(
editor
)
=>
{
const
image
=
DomEditor
.
getSelectedNodeByType
(
editor
,
'image'
)
if
(
!
image
)
return
{}
const
[
node
,
path
]
=
SlateEditor
.
parent
(
editor
,
DomEditor
.
findPath
(
editor
,
image
))
return
{
image
,
node
,
path
}
}
class
ImageEditor
{
constructor
()
{
...
...
@@ -37,7 +46,10 @@ class ImageEditor {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'ImageEditorClick'
)
openEditorDialog
(
editor
,
'image.edit'
,
{
isOnline
:
false
,
imageInfo
:
getSelectedImageInfo
(
editor
),
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/image/feature.js
浏览文件 @
3da7f60b
...
...
@@ -2,26 +2,8 @@ import { defineEditorFeature } from '../../core/define-editor-feature'
import
editMenu
from
'./edit-menu'
import
onlineMenu
from
'./online-menu'
import
nodeModule
from
'./node'
import
{
SlateEditor
,
SlateElement
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
ImageFeatureDialog
from
'./FeatureDialog'
const
getSelectedImageInfo
=
(
editor
)
=>
{
const
entries
=
SlateEditor
.
nodes
(
editor
,
{
match
:
(
node
)
=>
SlateElement
.
isElement
(
node
)
&&
node
.
type
===
'paragraph'
,
universal
:
true
,
})
const
info
=
{}
for
(
const
[
node
,
path
]
of
entries
)
{
node
.
children
.
forEach
((
item
)
=>
{
if
(
item
.
type
===
'image'
)
info
.
image
=
item
})
info
.
node
=
node
info
.
path
=
path
}
return
info
}
export
default
defineEditorFeature
({
id
:
'image'
,
menus
:
[
...
...
@@ -33,22 +15,4 @@ export default defineEditorFeature({
{
id
:
'image.insert'
,
component
:
ImageFeatureDialog
},
{
id
:
'image.edit'
,
component
:
ImageFeatureDialog
},
],
events
:
[
{
name
:
'ImageMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'image.insert'
,
{
isOnline
:
false
}),
},
{
name
:
'ImageOnlineMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'image.insert'
,
{
isOnline
:
true
}),
},
{
name
:
'ImageEditorClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'image.edit'
,
{
isOnline
:
false
,
imageInfo
:
getSelectedImageInfo
(
editor
),
}),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/image/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
as
Transforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
// Extend menu
class
ImageAuto
{
...
...
@@ -16,11 +17,11 @@ class ImageAuto {
</svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
'
hello, 图片, , , 图片
'
getValue
()
{
return
''
}
isActive
(
editor
)
{
return
false
// or true
isActive
()
{
return
false
}
isDisabled
(
editor
)
{
const
{
selection
}
=
editor
...
...
@@ -32,69 +33,74 @@ class ImageAuto {
const
hasVoidElem
=
selectedElems
.
some
((
elem
)
=>
editor
.
isVoid
(
elem
))
if
(
hasVoidElem
)
return
true
// 选中了 void 元素,禁用
// eslint-disable-next-line array-callback-return
const
hasPreElem
=
selectedElems
.
some
((
elem
)
=>
{
const
type
=
DomEditor
.
getNodeType
(
elem
)
// 代码块 引用 表格 禁用
if
(
type
===
'pre'
||
type
===
'blockquote'
)
return
true
return
type
===
'pre'
||
type
===
'blockquote'
})
if
(
hasPreElem
)
return
true
// 选中了 pre 原则,禁用
return
false
return
hasPreElem
}
getSelectedNode
(
editor
)
{
return
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterImage'
)
exec
(
editor
)
{
if
(
!
this
.
isDisabled
(
editor
))
openEditorDialog
(
editor
,
'image.insert'
,
{
isOnline
:
false
}
)
}
exec
(
editor
,
value
)
{
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
editor
.
emit
(
'ImageMenuClick'
)
}
if
(
this
.
isDisabled
(
editor
))
{
return
class
ImageWidthMenu
{
constructor
(
title
,
width
)
{
this
.
title
=
title
this
.
width
=
width
this
.
tag
=
'button'
}
getValue
()
{
return
''
}
isActive
()
{
return
false
}
isDisabled
(
editor
)
{
return
!
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterImage'
)
}
exec
(
editor
)
{
const
imageNode
=
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterImage'
)
if
(
!
imageNode
)
return
const
imageNode
=
this
.
getSelectedNode
(
editor
)
if
(
imageNode
===
null
)
return
// 隐藏 hoverbar
const
hoverbar
=
DomEditor
.
getHoverbar
(
editor
)
if
(
hoverbar
)
hoverbar
.
hideAndClean
()
const
{
style
=
{}
}
=
imageNode
const
props
=
{
style
:
{
...
style
,
width
:
this
.
value
,
// 修改 width
height
:
''
,
// 清空 height
Transforms
.
setNodes
(
editor
,
{
style
:
{
...
style
,
width
:
this
.
width
,
height
:
''
},
},
}
Transforms
.
setNodes
(
editor
,
props
,
{
match
:
(
n
)
=>
DomEditor
.
checkNodeType
(
n
,
'chapterImage'
),
})
{
at
:
DomEditor
.
findPath
(
editor
,
imageNode
),
},
)
}
}
class
ImageWidthCustomer100
extends
ImageAuto
{
title
=
'100%'
// 菜单标题
value
=
'100%'
// css width 的值
class
ImageWidthCustomer100
extends
ImageWidthMenu
{
constructor
()
{
super
(
'100%'
,
'100%'
)
}
}
class
ImageWidthCustomer50
extends
ImageAuto
{
title
=
'50%'
// 菜单标题
value
=
'50%'
// css width 的值
class
ImageWidthCustomer50
extends
ImageWidthMenu
{
constructor
()
{
super
(
'50%'
,
'50%'
)
}
}
class
ImageWidthCustomer30
extends
ImageAuto
{
title
=
'30%'
// 菜单标题
value
=
'30%'
// css width 的值
class
ImageWidthCustomer30
extends
ImageWidthMenu
{
constructor
()
{
super
(
'30%'
,
'30%'
)
}
}
export
default
{
key
:
'ImageAuto'
,
// 定义 menu key :要保证唯一、不重复(重要)
key
:
'ImageAuto'
,
factory
()
{
return
new
ImageAuto
()
// 把 `YourMenuClass` 替换为你菜单的 class
return
new
ImageAuto
()
},
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/image/node.jsx
浏览文件 @
3da7f60b
...
...
@@ -3,6 +3,7 @@ import { h } from 'snabbdom'
import
{
throttle
}
from
'lodash-es'
import
$
from
'jquery'
import
{
getStyleValue
}
from
'@/utils/common'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
import
ImageAutoConf
,
{
imageWidth100MenuChapterConf
,
...
...
@@ -11,7 +12,7 @@ import ImageAutoConf, {
}
from
'./menu'
const
withImageNode
=
(
editor
)
=>
{
const
{
isInline
,
isVoid
,
normalizeNode
}
=
editor
const
{
isInline
,
isVoid
}
=
editor
const
newEditor
=
editor
newEditor
.
isInline
=
(
elem
)
=>
{
...
...
@@ -26,35 +27,7 @@ const withImageNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterImage'
)
{
// 未命中 chapterImage ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterImage'
,
[
'paragraph'
,
'blockquote'
])
}
function
genContainerId
(
editor
,
elemNode
)
{
...
...
@@ -100,6 +73,8 @@ function renderResizeContainer(editor, elemNode, imageVnode, imageInfo) {
*/
function
init
(
clientX
)
{
$container
=
getContainerElem
()
$body
.
off
(
'mousemove'
,
onMousemove
)
$body
.
off
(
'mouseup'
,
onMouseup
)
// 记录当前 x 坐标值
originalX
=
clientX
...
...
@@ -138,9 +113,11 @@ function renderResizeContainer(editor, elemNode, imageVnode, imageInfo) {
$container
.
css
(
'height'
,
`
${
newHeight
}
px`
)
},
100
)
function
onMouseup
(
e
)
{
function
onMouseup
()
{
// 取消监听 mousemove
$body
.
off
(
'mousemove'
,
onMousemove
)
$body
.
off
(
'mouseup'
,
onMouseup
)
onMousemove
.
cancel
()
if
(
$container
===
null
)
return
const
newWidth
=
$container
.
width
().
toFixed
(
2
)
...
...
@@ -154,10 +131,7 @@ function renderResizeContainer(editor, elemNode, imageVnode, imageInfo) {
height
:
`
${
newHeight
}
px`
,
},
}
Transforms
.
setNodes
(
editor
,
props
,
{
at
:
DomEditor
.
findPath
(
editor
,
elemNode
)
})
// 取消监听 mouseup
$body
.
off
(
'mouseup'
,
onMouseup
)
SlateTransforms
.
setNodes
(
editor
,
props
,
{
at
:
DomEditor
.
findPath
(
editor
,
elemNode
)
})
}
const
style
=
{}
...
...
@@ -165,14 +139,6 @@ function renderResizeContainer(editor, elemNode, imageVnode, imageInfo) {
if
(
height
)
style
.
height
=
height
// style.boxShadow = '0 0 0 1px #B4D5FF' // 自定义 selected 样式,因为有拖拽触手
const
normalStyle
=
'width: 7px; height: 7px; background-color: #4290f7; position: absolute; cursor: nwse-resize; z-index: 12;'
const
strDom
=
`
${
imageVnode
}
<div className='w-e-image-dragger left-top' style="
${
normalStyle
}
left: 0; top: 0;"></div>
<div className='w-e-image-dragger right-top' style="
${
normalStyle
}
right: 0; top: 0;"></div>
<div className='w-e-image-dragger left-bottom' style="
${
normalStyle
}
left: 0; bottom: 0;"></div>
<div className='w-e-image-dragger right-bottom' style="
${
normalStyle
}
right: 0; bottom: 0;"></div>`
return
h
(
'div'
,
{
...
...
@@ -301,7 +267,7 @@ function renderResizeContainer(editor, elemNode, imageVnode, imageInfo) {
// 定义 renderElem 函数
const
renderImage
=
(
elem
,
children
,
editor
)
=>
{
// 获取“附件”的数据,参考上文 myResume 数据结构
const
{
title
=
''
,
imgUrl
=
''
,
imgDescript
=
''
,
style
=
{}
}
=
elem
const
{
imgUrl
=
''
,
imgDescript
=
''
,
style
=
{}
}
=
elem
const
{
width
=
''
,
height
=
''
}
=
style
let
imageStyle
=
''
...
...
@@ -332,34 +298,7 @@ const renderImage = (elem, children, editor) => {
},
[
str
],
)
const
titleNode
=
h
(
'p'
,
{
props
:
{
className
:
'chapter-image-title'
,
},
style
:
{
color
:
'#666'
,
fontSize
:
'14px'
,
margin
:
'0'
,
lineHeight
:
'20px'
},
},
[
title
],
)
const
titleDescriptNode
=
h
(
'p'
,
{
props
:
{
className
:
'chapter-image-descript'
,
},
style
:
{
color
:
'#999'
,
fontSize
:
'14px'
,
margin
:
'0'
,
lineHeight
:
'20px'
},
},
[
imgDescript
],
)
const
element
=
[
imgNode
]
if
(
title
)
{
// element.push(titleNode);
}
if
(
imgDescript
)
{
// element.push(titleDescriptNode);
}
// 附件元素 vnode
const
attachVnode
=
h
(
...
...
@@ -373,11 +312,6 @@ const renderImage = (elem, children, editor) => {
className
:
'chapter-image-container'
,
},
style
:
{
padding
:
'10px 0px'
},
// style ,驼峰式写法
on
:
{
click
()
{
console
.
log
(
'clicked'
)
}
/* 其他... */
,
},
},
// 子节点
[...
element
],
...
...
apps/admin/src/components/editor/wangeditor-customer/features/image/online-menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
as
Transforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
// Extend menu
class
ImageAutoOnline
{
...
...
@@ -30,11 +31,11 @@ class ImageAutoOnline {
</svg>`
this
.
tag
=
'button'
}
getValue
(
editor
)
{
return
'
hello, 图片, , , 图片
'
getValue
()
{
return
''
}
isActive
(
editor
)
{
return
false
// or true
isActive
()
{
return
false
}
isDisabled
(
editor
)
{
const
{
selection
}
=
editor
...
...
@@ -46,63 +47,15 @@ class ImageAutoOnline {
const
hasVoidElem
=
selectedElems
.
some
((
elem
)
=>
editor
.
isVoid
(
elem
))
if
(
hasVoidElem
)
return
true
// 选中了 void 元素,禁用
// eslint-disable-next-line array-callback-return
const
hasPreElem
=
selectedElems
.
some
((
elem
)
=>
{
const
type
=
DomEditor
.
getNodeType
(
elem
)
// 代码块 引用 表格 禁用
if
(
type
===
'pre'
||
type
===
'blockquote'
)
return
true
return
type
===
'pre'
||
type
===
'blockquote'
})
if
(
hasPreElem
)
return
true
// 选中了 pre 原则,禁用
return
false
return
hasPreElem
}
getSelectedNode
(
editor
)
{
return
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterImage'
)
exec
(
editor
)
{
if
(
!
this
.
isDisabled
(
editor
))
openEditorDialog
(
editor
,
'image.insert'
,
{
isOnline
:
true
}
)
}
exec
(
editor
,
value
)
{
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
editor
.
emit
(
'ImageOnlineMenuClick'
)
if
(
this
.
isDisabled
(
editor
))
{
return
}
const
imageNode
=
this
.
getSelectedNode
(
editor
)
if
(
imageNode
===
null
)
return
// 隐藏 hoverbar
const
hoverbar
=
DomEditor
.
getHoverbar
(
editor
)
if
(
hoverbar
)
hoverbar
.
hideAndClean
()
const
{
style
=
{}
}
=
imageNode
const
props
=
{
style
:
{
...
style
,
width
:
this
.
value
,
// 修改 width
height
:
''
,
// 清空 height
},
}
Transforms
.
setNodes
(
editor
,
props
,
{
match
:
(
n
)
=>
DomEditor
.
checkNodeType
(
n
,
'chapterImage'
),
})
}
}
class
ImageWidthCustomer100
extends
ImageAutoOnline
{
title
=
'100%'
// 菜单标题
value
=
'100%'
// css width 的值
}
class
ImageWidthCustomer50
extends
ImageAutoOnline
{
title
=
'50%'
// 菜单标题
value
=
'50%'
// css width 的值
}
class
ImageWidthCustomer30
extends
ImageAutoOnline
{
title
=
'30%'
// 菜单标题
value
=
'30%'
// css width 的值
}
export
default
{
...
...
@@ -112,30 +65,4 @@ export default {
},
}
const
imageWidth100MenuChapterConf
=
{
key
:
'imageWidthChpater100'
,
factory
()
{
return
new
ImageWidthCustomer100
()
},
}
const
imageWidth50MenuChapterConf
=
{
key
:
'imageWidthChpater50'
,
factory
()
{
return
new
ImageWidthCustomer50
()
},
}
const
imageWidth30MenuChapterConf
=
{
key
:
'imageWidthChpater30'
,
factory
()
{
return
new
ImageWidthCustomer30
()
},
}
export
{
ImageAutoOnline
,
imageWidth100MenuChapterConf
,
imageWidth50MenuChapterConf
,
imageWidth30MenuChapterConf
,
}
export
{
ImageAutoOnline
}
apps/admin/src/components/editor/wangeditor-customer/features/link/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
import
LinkFeatureDialog
from
'./FeatureDialog'
export
default
defineEditorFeature
({
...
...
@@ -13,14 +11,4 @@ export default defineEditorFeature({
{
id
:
'link.insert'
,
component
:
LinkFeatureDialog
},
{
id
:
'link.edit'
,
component
:
LinkFeatureDialog
},
],
events
:
[
{
name
:
'CustomerLinkClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'link.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
),
theme
:
'#ab1941'
,
}),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/link/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
class
CustomerLink
{
constructor
()
{
...
...
@@ -44,7 +46,10 @@ class CustomerLink {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'CustomerLinkClick'
)
openEditorDialog
(
editor
,
'link.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
),
theme
:
'#ab1941'
,
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/link/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
icon_tooltip_editor
from
'@/assets/images/editor/icon_tooltip_editor.png'
import
{
h
}
from
'snabbdom'
import
iconLink
from
'@/assets/images/editor/icon_link_editor.png'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
normalizeInlineVoidNode
}
from
'../../shared/normalize-inline-void'
const
withChapterLinkNode
=
(
editor
)
=>
{
const
{
isInline
,
isVoid
,
normalizeNode
}
=
editor
...
...
@@ -23,26 +22,14 @@ const withChapterLinkNode = (editor) => {
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterLink'
)
{
// 未命中 chapterHeader ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
const
topLevelNodes
=
newEditor
.
children
||
[]
const
nextNode
=
topLevelNodes
[
path
[
0
]]
||
{}
if
(
nextNode
.
type
===
'paragraph'
)
{
const
lastChild
=
nextNode
.
children
[
nextNode
.
children
.
length
-
1
]
if
(
lastChild
.
type
===
'chapterExpandReadSimple'
)
{
SlateTransforms
.
insertNodes
(
newEditor
,
{
type
:
'span'
,
children
:
[{
text
:
''
}]
},
{
at
:
path
,
// 在 link-card 后面插入
},
)
}
}
return
normalizeInlineVoidNode
({
editor
:
newEditor
,
node
,
path
,
type
:
'chapterLink'
,
normalizeNode
,
transforms
:
SlateTransforms
,
})
}
return
newEditor
// 返回 newEditor ,重要!!!
...
...
apps/admin/src/components/editor/wangeditor-customer/features/practice/PracticeSettingsDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -85,7 +85,7 @@ const PracticeSettingsDialog = ({ editor, chapterId, bookId, closePanel }) => {
}
return
(
<
Row
gutter=
{
20
}
className=
'practice-topic-row'
>
<
Row
gutter=
{
20
}
>
<
Col
span=
{
12
}
className=
'common practice-left'
>
<
div
className=
'practice-header'
>
<
div
className=
'practice-l-img'
>
...
...
@@ -123,7 +123,7 @@ const PracticeSettingsDialog = ({ editor, chapterId, bookId, closePanel }) => {
/>
</
div
>
</
Col
>
<
Col
span=
{
12
}
className=
'common
practice-right
'
>
<
Col
span=
{
12
}
className=
'common'
>
<
div
className=
'practice-header right'
>
<
Button
onClick=
{
()
=>
setManualAdd
((
value
)
=>
!
value
)
}
>
{
manualAdd
?
'从题库选择'
:
'新增试题'
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/practice/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
PracticeInsertFeatureDialog
,
PracticeSettingsFeatureDialog
}
from
'./FeatureDialogs'
export
default
defineEditorFeature
({
...
...
@@ -12,10 +11,4 @@ export default defineEditorFeature({
{
id
:
'practice.insert'
,
component
:
PracticeInsertFeatureDialog
},
{
id
:
'practice.settings'
,
component
:
PracticeSettingsFeatureDialog
},
],
events
:
[
{
name
:
'PracticeMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'practice.insert'
),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/practice/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
Practice
{
constructor
()
{
...
...
@@ -55,7 +56,7 @@ class Practice {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'PracticeMenuClick
'
)
openEditorDialog
(
editor
,
'practice.insert
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/practice/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateEditor
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
iconClose
from
'@/assets/images/icon_chapter_close.png'
...
...
@@ -6,9 +6,10 @@ import { delChapterTopic } from '../../shared/request'
import
{
hexToRgb
}
from
'@/utils/common'
import
{
findNodeWithParent
}
from
'../../shared/editor-settings'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withPracticeNode
=
(
editor
)
=>
{
const
{
is
Inline
,
isVoid
,
normalizeNode
}
=
editor
const
{
is
Void
}
=
editor
const
newEditor
=
editor
// newEditor.isInline = (elem) => {
...
...
@@ -23,41 +24,7 @@ const withPracticeNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterPractice'
)
{
// 未命中 chapterPractice ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterPractice'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/features/preview/FeatureDialog.jsx
浏览文件 @
3da7f60b
...
...
@@ -24,7 +24,6 @@ export default function PreviewFeatureDialog({ open, onClose, runtime }) {
onCancel=
{
onClose
}
>
<
PreviewDialog
gData=
{
runtime
.
gData
}
editor=
{
runtime
.
editor
}
chapterId=
{
runtime
.
chapterId
}
bookId=
{
runtime
.
bookId
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/preview/PracticePreview.jsx
0 → 100644
浏览文件 @
3da7f60b
import
{
Input
}
from
'antd'
const
QUESTION_TYPES
=
[
{
style
:
1
,
title
:
'单选题'
},
{
style
:
2
,
title
:
'多选题'
},
{
style
:
3
,
title
:
'判断题'
},
{
style
:
4
,
title
:
'填空题'
},
{
style
:
5
,
title
:
'简答题'
},
]
const
SECTION_NUMBERS
=
[
'一'
,
'二'
,
'三'
,
'四'
,
'五'
]
export
function
buildTopicGroups
(
topics
)
{
const
normalizedTopics
=
(
Array
.
isArray
(
topics
)
?
topics
:
[]).
map
((
topic
)
=>
{
const
questionStyle
=
Number
(
topic
.
question_style
)
let
questionList
=
[]
if
([
1
,
2
,
3
].
includes
(
questionStyle
))
{
try
{
questionList
=
Array
.
isArray
(
topic
.
question_list
)
?
topic
.
question_list
:
JSON
.
parse
(
topic
.
question_list
)
}
catch
{
questionList
=
[]
}
}
return
{
...
topic
,
question_style
:
questionStyle
,
question_list
:
questionList
}
})
return
QUESTION_TYPES
.
map
((
type
)
=>
({
...
type
,
topics
:
normalizedTopics
.
filter
((
topic
)
=>
topic
.
question_style
===
type
.
style
),
})).
filter
((
group
)
=>
group
.
topics
.
length
>
0
)
}
function
Question
({
topic
,
index
})
{
const
isChoice
=
[
1
,
2
,
3
].
includes
(
topic
.
question_style
)
return
(
<
div
className=
'practice-item'
>
<
div
className=
'topic-choose-title'
dangerouslySetInnerHTML=
{
{
__html
:
`<span class="index">${index + 1}.</span>${topic.titles}`
,
}
}
/>
<
div
className=
'topic_choose-list'
>
{
isChoice
&&
topic
.
question_list
.
map
((
option
,
optionIndex
)
=>
(
<
div
className=
'topic_choose-item'
key=
{
optionIndex
}
>
<
div
className=
'choose-ico'
>
<
i
className=
{
`ico ${topic.question_style === 2 ? 'checkbox' : 'radio'}`
}
/>
</
div
>
<
div
className=
'topic_content'
dangerouslySetInnerHTML=
{
{
__html
:
`<span class="correct">${String.fromCharCode(65 + optionIndex)}.</span> ${option.option}`
,
}
}
/>
</
div
>
))
}
{
topic
.
question_style
===
5
&&
(
<
Input
.
TextArea
disabled
autoSize=
{
{
minRows
:
4
,
maxRows
:
6
}
}
/>
)
}
</
div
>
</
div
>
)
}
export
default
function
PracticePreview
({
groups
})
{
return
(
<
div
className=
'practice-insert-topic'
>
{
groups
.
map
((
group
,
groupIndex
)
=>
(
<
div
className=
'topic_style'
key=
{
group
.
style
}
>
<
h3
>
{
SECTION_NUMBERS
[
groupIndex
]
}
.
{
group
.
title
}
</
h3
>
<
div
className=
'practice-list'
>
{
group
.
topics
.
map
((
topic
,
index
)
=>
(
<
Question
topic=
{
topic
}
index=
{
index
}
key=
{
topic
.
id
??
index
}
/>
))
}
</
div
>
</
div
>
))
}
</
div
>
)
}
apps/admin/src/components/editor/wangeditor-customer/features/preview/PracticePreview.test.tsx
0 → 100644
浏览文件 @
3da7f60b
import
{
cleanup
,
render
,
screen
}
from
'@testing-library/react'
import
{
afterEach
,
describe
,
expect
,
it
}
from
'vitest'
import
PracticePreview
,
{
buildTopicGroups
}
from
'./PracticePreview'
describe
(
'PracticePreview'
,
()
=>
{
afterEach
(
cleanup
)
it
(
'normalizes and groups questions returned by the API'
,
()
=>
{
const
groups
=
buildTopicGroups
([
{
id
:
1
,
question_style
:
'2'
,
titles
:
'多选题'
,
question_list
:
'[{"option":"选项 A"}]'
,
},
{
id
:
2
,
question_style
:
5
,
titles
:
'简答题'
},
])
expect
(
groups
.
map
((
group
)
=>
group
.
style
)).
toEqual
([
2
,
5
])
expect
(
groups
[
0
].
topics
[
0
].
question_list
).
toEqual
([{
option
:
'选项 A'
}])
})
it
(
'numbers only the question groups that have content'
,
()
=>
{
const
groups
=
buildTopicGroups
([
{
id
:
1
,
question_style
:
3
,
titles
:
'判断题'
,
question_list
:
[]
},
{
id
:
2
,
question_style
:
5
,
titles
:
'简答题'
},
])
render
(<
PracticePreview
groups=
{
groups
}
/>)
expect
(
screen
.
getByRole
(
'heading'
,
{
name
:
'一. 判断题'
})).
toBeInTheDocument
()
expect
(
screen
.
getByRole
(
'heading'
,
{
name
:
'二. 简答题'
})).
toBeInTheDocument
()
expect
(
screen
.
getByRole
(
'textbox'
)).
toBeDisabled
()
})
})
apps/admin/src/components/editor/wangeditor-customer/features/preview/PreviewDialog.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/components/editor/wangeditor-customer/features/preview/PreviewDialog.test.tsx
0 → 100644
浏览文件 @
3da7f60b
import
{
cleanup
,
fireEvent
,
render
,
screen
,
waitFor
}
from
'@testing-library/react'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
PreviewDialog
from
'./PreviewDialog'
import
{
expandReadInfo
}
from
'../../shared/request'
import
hljs
from
'highlight.js'
vi
.
mock
(
'@/modules/books/management/chapters/api'
,
()
=>
({
getInfoByChapterId
:
vi
.
fn
(),
}))
vi
.
mock
(
'../../shared/request'
,
()
=>
({
expandReadInfo
:
vi
.
fn
(),
getChapterTopic
:
vi
.
fn
(),
}))
vi
.
mock
(
'highlight.js'
,
()
=>
({
default
:
{
highlightElement
:
vi
.
fn
()
},
}))
describe
(
'PreviewDialog'
,
()
=>
{
beforeEach
(()
=>
{
vi
.
clearAllMocks
()
})
afterEach
(
cleanup
)
it
(
'processes current chapter HTML after React renders it'
,
async
()
=>
{
const
editor
=
{
getHtml
:
()
=>
'<pre><code>const value = 1</code></pre><video></video>'
,
}
render
(<
PreviewDialog
editor=
{
editor
}
bookId=
{
86
}
chapterId=
{
10
}
nowTitle=
'当前章节'
/>)
await
waitFor
(()
=>
expect
(
hljs
.
highlightElement
).
toHaveBeenCalledOnce
())
const
video
=
document
.
querySelector
(
'.preview-content-html video'
)
expect
(
video
).
toHaveAttribute
(
'controls'
)
expect
(
video
).
toHaveAttribute
(
'controlslist'
,
'nodownload'
)
})
it
(
'handles images loaded later inside expand-read content'
,
async
()
=>
{
vi
.
mocked
(
expandReadInfo
).
mockResolvedValue
({
content
:
'<img src="/expand.png" alt="扩展图">'
,
})
const
editor
=
{
getHtml
:
()
=>
'<div class="chapter-expand" data-name="扩展阅读" data-random="position-1">查看扩展阅读</div>'
,
}
render
(<
PreviewDialog
editor=
{
editor
}
bookId=
{
86
}
chapterId=
{
10
}
nowTitle=
'当前章节'
/>)
fireEvent
.
click
(
await
screen
.
findByText
(
'查看扩展阅读'
))
fireEvent
.
click
(
await
screen
.
findByAltText
(
'扩展图'
))
expect
(
screen
.
getByText
(
'扩展图片'
)).
toBeInTheDocument
()
expect
(
screen
.
getByAltText
(
'预览图片'
)).
toHaveAttribute
(
'src'
,
'/expand.png'
)
fireEvent
.
click
(
screen
.
getByRole
(
'img'
,
{
name
:
'left'
}))
expect
(
screen
.
getByText
(
'扩展阅读'
)).
toBeInTheDocument
()
expect
(
screen
.
getByAltText
(
'扩展图'
)).
toBeInTheDocument
()
})
})
apps/admin/src/components/editor/wangeditor-customer/features/tooltip/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
import
TooltipFeatureDialog
from
'./FeatureDialog'
export
default
defineEditorFeature
({
...
...
@@ -13,11 +11,4 @@ export default defineEditorFeature({
{
id
:
'tooltip.insert'
,
component
:
TooltipFeatureDialog
},
{
id
:
'tooltip.edit'
,
component
:
TooltipFeatureDialog
},
],
events
:
[
{
name
:
'TooltipMenuClick'
,
handler
:
({
editor
})
=>
openEditorDialog
(
editor
,
'tooltip.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
)
}),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/tooltip/menu.js
浏览文件 @
3da7f60b
// Extend menu
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
getSelectionFontSize
}
from
'../../core/selection'
class
TooltipAuto
{
constructor
()
{
...
...
@@ -58,7 +60,9 @@ class TooltipAuto {
if
(
this
.
isDisabled
(
editor
))
{
return
}
editor
.
emit
(
'TooltipMenuClick'
)
openEditorDialog
(
editor
,
'tooltip.insert'
,
{
selectionSize
:
getSelectionFontSize
(
editor
),
})
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/tooltip/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
icon_tooltip_editor
from
'@/assets/images/editor/icon_tooltip_editor.png'
import
{
h
}
from
'snabbdom'
import
{
escapeHtml
}
from
'../../shared/html'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
normalizeInlineVoidNode
}
from
'../../shared/normalize-inline-void'
const
withChapterTooltipNode
=
(
editor
)
=>
{
const
{
isInline
,
isVoid
,
normalizeNode
}
=
editor
...
...
@@ -23,30 +23,14 @@ const withChapterTooltipNode = (editor) => {
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterTooltip'
)
{
// 未命中 chapterHeader ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// SlateTransforms.insertNodes(newEditor, {type: 'paragraph', children: [{ text: ' ' }]}, {
// at: path, // 在 link-card 后面插入
// });
const
topLevelNodes
=
newEditor
.
children
||
[]
const
nextNode
=
topLevelNodes
[
path
[
0
]]
||
{}
if
(
nextNode
.
type
===
'paragraph'
)
{
const
lastChild
=
nextNode
.
children
[
nextNode
.
children
.
length
-
1
]
if
(
lastChild
.
type
===
'chapterExpandReadSimple'
)
{
SlateTransforms
.
insertNodes
(
newEditor
,
{
type
:
'span'
,
children
:
[{
text
:
''
}]
},
{
at
:
path
,
// 在 link-card 后面插入
},
)
}
}
return
normalizeInlineVoidNode
({
editor
:
newEditor
,
node
,
path
,
type
:
'chapterTooltip'
,
normalizeNode
,
transforms
:
SlateTransforms
,
})
}
return
newEditor
// 返回 newEditor ,重要!!!
...
...
@@ -61,7 +45,6 @@ const renderChapterTooltip = (elem, children, editor) => {
tooltipType
=
1
,
link
=
''
,
content
=
''
,
icon
=
''
,
random
=
''
,
theme
=
'#ab1941'
,
fontsize
=
18
,
...
...
@@ -150,7 +133,6 @@ const chapterTooltipToHtml = (elem, childrenHtml) => {
tooltipType
=
1
,
link
=
''
,
content
=
''
,
icon
=
''
,
random
=
''
,
theme
=
'#ab1941'
,
fontsize
=
18
,
...
...
apps/admin/src/components/editor/wangeditor-customer/features/video/feature.js
浏览文件 @
3da7f60b
import
{
defineEditorFeature
}
from
'../../core/define-editor-feature'
import
menu
from
'./menu'
import
nodeModule
from
'./node'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
VideoDialog
from
'./VideoDialog'
export
default
defineEditorFeature
({
...
...
@@ -12,11 +11,4 @@ export default defineEditorFeature({
{
id
:
'video.insert'
,
component
:
VideoDialog
},
{
id
:
'video.edit'
,
component
:
VideoDialog
},
],
events
:
[
{
name
:
'VideoMenuClick'
,
handler
:
({
editor
,
payload
})
=>
openEditorDialog
(
editor
,
payload
?.
path
?
'video.edit'
:
'video.insert'
,
payload
),
},
],
})
apps/admin/src/components/editor/wangeditor-customer/features/video/menu.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
,
SlateRange
}
from
'@wangeditor/editor'
import
{
DomEditor
,
SlateRange
}
from
'@wangeditor/editor'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
class
VideoAuto
{
constructor
()
{
...
...
@@ -61,14 +62,14 @@ class VideoAuto {
const
chapterVideo
=
DomEditor
.
getSelectedNodeByType
(
editor
,
'chapterVideo'
)
if
(
chapterVideo
)
{
const
path
=
DomEditor
.
findPath
(
editor
,
chapterVideo
)
editor
.
emit
(
'VideoMenuClick
'
,
{
openEditorDialog
(
editor
,
'video.edit
'
,
{
path
,
videoUrl
:
chapterVideo
.
videoUrl
||
''
,
title
:
chapterVideo
.
title
||
''
,
})
return
}
editor
.
emit
(
'VideoMenuClick
'
)
openEditorDialog
(
editor
,
'video.insert
'
)
}
}
...
...
apps/admin/src/components/editor/wangeditor-customer/features/video/node.js
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
DomEditor
}
from
'@wangeditor/editor'
import
{
h
}
from
'snabbdom'
import
{
openEditorDialog
}
from
'../../core/editor-dialog'
import
{
withTrailingParagraph
}
from
'../../shared/with-trailing-paragraph'
const
withVideoNode
=
(
editor
)
=>
{
const
{
isVoid
,
normalizeNode
}
=
editor
const
{
isVoid
}
=
editor
const
newEditor
=
editor
newEditor
.
isVoid
=
(
elem
)
=>
{
...
...
@@ -12,41 +13,7 @@ const withVideoNode = (editor) => {
return
isVoid
(
elem
)
}
// 重新 normalize
newEditor
.
normalizeNode
=
([
node
,
path
])
=>
{
const
type
=
DomEditor
.
getNodeType
(
node
)
if
(
type
!==
'chapterVideo'
)
{
// 未命中 chapterVideo ,执行默认的 normalizeNode
return
normalizeNode
([
node
,
path
])
}
// editor 顶级 node
const
topLevelNodes
=
newEditor
.
children
||
[]
// 后面必须跟一个 p header blockquote(否则后面无法继续输入文字)
const
nextNode
=
topLevelNodes
[
path
[
0
]
+
1
]
||
{}
const
nextNodeType
=
DomEditor
.
getNodeType
(
nextNode
)
if
(
nextNodeType
!==
'paragraph'
&&
nextNodeType
!==
'blockquote'
&&
nextNodeType
!==
'header'
&&
nextNodeType
!==
'chapterPractice'
&&
nextNodeType
!==
'chapterExpandRead'
&&
nextNodeType
!==
'chapterSection'
&&
nextNodeType
!==
'chapterHeader'
&&
nextNodeType
!==
'chapterGallery'
&&
!
nextNodeType
.
startsWith
(
'header'
)
)
{
// link-card node 后面不是 p 或 header ,则插入一个空 p
const
p
=
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
}
const
insertPath
=
[
path
[
0
]
+
1
]
SlateTransforms
.
insertNodes
(
newEditor
,
p
,
{
at
:
insertPath
,
// 在 link-card 后面插入
})
}
}
return
newEditor
// 返回 newEditor ,重要!!!
return
withTrailingParagraph
(
newEditor
,
'chapterVideo'
)
}
// 在编辑器中渲染新元素
...
...
apps/admin/src/components/editor/wangeditor-customer/index.less
浏览文件 @
3da7f60b
...
...
@@ -265,19 +265,6 @@
line-height: 14px;
}
}
.view {
&:disabled {
color: #666666 !important;
}
}
.history {
color: #666666;
&:hover {
.ant-btn-icon {
color: #ab1941;
}
}
}
}
}
}
...
...
@@ -341,9 +328,8 @@
height: 50px;
display: flex;
width: 100%;
justify-content: center;
.tabs-item {
flex: 1;
flex: 1
1 auto
;
text-align: center;
border-bottom: 1px solid #e5e5e5;
color: #333;
...
...
apps/admin/src/components/editor/wangeditor-customer/shared/normalize-inline-void.js
0 → 100644
浏览文件 @
3da7f60b
export
const
normalizeInlineVoidNode
=
({
editor
,
node
,
path
,
type
,
normalizeNode
,
transforms
,
})
=>
{
if
(
node
.
type
!==
type
)
return
normalizeNode
([
node
,
path
])
const
[
parentIndex
,
childIndex
]
=
path
const
parent
=
editor
.
children
?.[
parentIndex
]
if
(
path
.
length
!==
2
||
parent
?.
type
!==
'paragraph'
)
return
if
(
childIndex
!==
parent
.
children
.
length
-
1
)
return
transforms
.
insertNodes
(
editor
,
{
text
:
''
},
{
at
:
[
parentIndex
,
childIndex
+
1
]
})
}
apps/admin/src/components/editor/wangeditor-customer/shared/with-trailing-paragraph.js
0 → 100644
浏览文件 @
3da7f60b
import
{
DomEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
const
DEFAULT_ALLOWED_NEXT_TYPES
=
new
Set
([
'paragraph'
,
'blockquote'
,
'chapterPractice'
,
'chapterExpandRead'
,
'chapterSection'
,
'chapterHeader'
,
'chapterGallery'
,
])
export
const
withTrailingParagraph
=
(
editor
,
nodeType
,
allowedNextTypes
=
DEFAULT_ALLOWED_NEXT_TYPES
,
)
=>
{
const
normalizeNode
=
editor
.
normalizeNode
const
allowedTypes
=
allowedNextTypes
instanceof
Set
?
allowedNextTypes
:
new
Set
(
allowedNextTypes
)
editor
.
normalizeNode
=
([
node
,
path
])
=>
{
if
(
DomEditor
.
getNodeType
(
node
)
!==
nodeType
)
return
normalizeNode
([
node
,
path
])
const
nextType
=
DomEditor
.
getNodeType
(
editor
.
children
?.[
path
[
0
]
+
1
]
||
{})
if
(
allowedTypes
.
has
(
nextType
)
||
nextType
.
startsWith
(
'header'
))
return
SlateTransforms
.
insertNodes
(
editor
,
{
type
:
'paragraph'
,
children
:
[{
text
:
''
}]
},
{
at
:
[
path
[
0
]
+
1
]
},
)
}
return
editor
}
apps/admin/src/components/editor/wangeditor-customer/shell/EditorHeader.jsx
浏览文件 @
3da7f60b
...
...
@@ -4,61 +4,33 @@ import dayjs from 'dayjs'
import
timesave
from
'@/assets/images/timesave.png'
export
const
EditorHeader
=
({
title
,
autosaveTime
,
isView
,
disabled
,
contentId
,
onSave
,
onPreview
,
onHistory
,
})
=>
(
<
div
className=
'title-head'
>
<
div
className=
'left'
>
export
const
EditorHeader
=
({
title
,
autosaveTime
,
isView
,
disabled
,
contentId
,
onSave
,
onPreview
,
onHistory
})
=>
(
<
div
className=
"title-head"
>
<
div
className=
"left"
>
<
h4
>
{
title
}
</
h4
>
</
div
>
<
div
className=
'right'
>
<
div
className=
"right"
>
<
Space
>
{
!
isView
&&
(
<>
<
div
className=
'save-time'
>
<
div
className=
'img'
>
<
div
className=
"save-time"
>
<
div
className=
"img"
>
<
img
src=
{
timesave
}
/>
<
span
>
自动保存
</
span
>
</
div
>
<
span
className=
'time'
>
{
autosaveTime
>
0
?
`上次保存发生在 ${dayjs(autosaveTime).format('YYYY-MM-DD HH:mm:ss')}`
:
''
}
<
span
className=
"time"
>
{
autosaveTime
>
0
?
`上次保存发生在 ${dayjs(autosaveTime).format('YYYY-MM-DD HH:mm:ss')}`
:
''
}
</
span
>
</
div
>
<
Button
type=
'primary'
icon=
{
<
SaveOutlined
/>
}
className=
'view'
onClick=
{
onSave
}
style=
{
{
color
:
'#fff'
}
}
disabled=
{
disabled
||
!
contentId
}
>
<
Button
type=
"primary"
icon=
{
<
SaveOutlined
/>
}
onClick=
{
onSave
}
disabled=
{
disabled
||
!
contentId
}
>
保存
</
Button
>
</>
)
}
<
Button
icon=
{
<
EyeOutlined
/>
}
className=
'history'
onClick=
{
onPreview
}
disabled=
{
!
contentId
}
>
<
Button
icon=
{
<
EyeOutlined
/>
}
onClick=
{
onPreview
}
disabled=
{
!
contentId
}
>
预览
</
Button
>
{
!
isView
&&
(
<
Button
icon=
{
<
HistoryOutlined
/>
}
className=
'history'
onClick=
{
onHistory
}
disabled=
{
disabled
||
!
contentId
}
>
<
Button
icon=
{
<
HistoryOutlined
/>
}
onClick=
{
onHistory
}
disabled=
{
disabled
||
!
contentId
}
>
历史
</
Button
>
)
}
...
...
apps/admin/src/components/editor/wangeditor-customer/shell/EditorSidebar.jsx
浏览文件 @
3da7f60b
import
{
cloneElement
,
isValidElement
,
useState
}
from
'react'
import
{
Button
}
from
'antd'
import
{
Toolbar
}
from
'@wangeditor/editor-for-react'
...
...
@@ -10,7 +11,17 @@ export const EditorSidebar = ({
colorList
,
onApplyColor
,
reviewCommentPanel
,
})
=>
(
})
=>
{
const
[
reviewCommentMounted
,
setReviewCommentMounted
]
=
useState
(
tabKey
===
'comment'
)
const
handleTabChange
=
(
key
)
=>
{
if
(
key
===
'comment'
)
setReviewCommentMounted
(
true
)
onTabChange
(
key
)
}
const
commentPanel
=
isValidElement
(
reviewCommentPanel
)
?
cloneElement
(
reviewCommentPanel
,
{
active
:
tabKey
===
'comment'
})
:
reviewCommentPanel
return
(
<
div
className=
'menu-tabs-key'
>
<
div
className=
'tabs'
>
{
[
...
...
@@ -21,7 +32,7 @@ export const EditorSidebar = ({
<
div
className=
{
`tabs-item ${item.key === tabKey ? 'active' : ''}`
}
key=
{
item
.
key
}
onClick=
{
()
=>
on
TabChange
(
item
.
key
)
}
onClick=
{
()
=>
handle
TabChange
(
item
.
key
)
}
>
{
item
.
title
}
<
span
/>
...
...
@@ -57,11 +68,10 @@ export const EditorSidebar = ({
</
ul
>
</
div
>
)
}
{
reviewCommentPanel
&&
(
<
div
className=
'editor-review-comment-tab'
hidden=
{
tabKey
!==
'comment'
}
>
{
reviewCommentPanel
}
</
div
>
{
reviewCommentMounted
&&
reviewCommentPanel
&&
(
<
div
hidden=
{
tabKey
!==
'comment'
}
>
{
commentPanel
}
</
div
>
)
}
</
div
>
</
div
>
)
)
}
apps/admin/src/components/editor/wangeditor-customer/shell/EditorSidebar.test.tsx
0 → 100644
浏览文件 @
3da7f60b
import
{
fireEvent
,
render
,
screen
}
from
'@testing-library/react'
import
{
useEffect
,
useState
}
from
'react'
import
{
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
EditorSidebar
}
from
'./EditorSidebar'
vi
.
mock
(
'@wangeditor/editor-for-react'
,
()
=>
({
Toolbar
:
()
=>
<
div
>
编辑工具栏
</
div
>,
}))
describe
(
'EditorSidebar'
,
()
=>
{
it
(
'mounts the comment panel on first open and keeps it mounted when hidden'
,
()
=>
{
const
mounted
=
vi
.
fn
()
const
CommentPanel
=
({
active
})
=>
{
useEffect
(()
=>
mounted
(),
[])
return
<
div
>
评论面板:
{
active
?
'显示'
:
'隐藏'
}
</
div
>
}
const
Harness
=
()
=>
{
const
[
tabKey
,
setTabKey
]
=
useState
(
'text'
)
return
(
<
EditorSidebar
editor=
{
null
}
toolbarConfig=
{
{}
}
toolbarRef=
{
{
current
:
null
}
}
tabKey=
{
tabKey
}
onTabChange=
{
setTabKey
}
colorList=
{
[]
}
onApplyColor=
{
()
=>
{}
}
reviewCommentPanel=
{
<
CommentPanel
/>
}
/>
)
}
render
(<
Harness
/>)
expect
(
screen
.
queryByText
(
/评论面板/
)).
not
.
toBeInTheDocument
()
fireEvent
.
click
(
screen
.
getByText
(
'评论'
))
expect
(
screen
.
getByText
(
'评论面板:显示'
)).
toBeInTheDocument
()
fireEvent
.
click
(
screen
.
getByText
(
'文本设置'
))
expect
(
screen
.
getByText
(
'评论面板:隐藏'
)).
not
.
toBeVisible
()
fireEvent
.
click
(
screen
.
getByText
(
'评论'
))
expect
(
screen
.
getByText
(
'评论面板:显示'
)).
toBeInTheDocument
()
expect
(
mounted
).
toHaveBeenCalledOnce
()
})
})
apps/admin/src/components/editor/wangeditor-customer/shell/WangEditorCustomer.jsx
浏览文件 @
3da7f60b
import
{
useState
,
useEffect
,
useRef
,
forwardRef
,
useImperativeHandle
}
from
'react'
import
{
Button
,
Divider
,
Space
,
Modal
}
from
'antd'
import
{
EyeOutlined
,
SaveOutlined
,
HistoryOutlined
}
from
'@ant-design/icons'
import
dayjs
from
'dayjs'
import
{
Divider
}
from
'antd'
import
{
useEditorStore
}
from
'@/stores/editorStore'
// import formulaEditor from 'easy-formula-editor'
import
'../shared/jax.less'
import
{
Boot
}
from
'@wangeditor/editor'
import
{
registerEditorFeatures
}
from
'../core/register-editor-features'
...
...
@@ -15,16 +12,12 @@ import { editorFeatureRegistry } from '../features'
import
{
DomEditor
,
SlateEditor
,
SlateTransforms
}
from
'@wangeditor/editor'
import
{
Editor
}
from
'@wangeditor/editor-for-react'
import
'@wangeditor/editor/dist/css/style.css'
// 引入 css
import
timesave
from
'@/assets/images/timesave.png'
import
relativeTime
from
'dayjs/plugin/relativeTime'
dayjs
.
extend
(
relativeTime
)
import
{
EditorHeader
}
from
'./EditorHeader'
import
{
EditorSidebar
}
from
'./EditorSidebar'
import
{
useToolbarPresentation
}
from
'./useToolbarPresentation'
import
{
useEditorLifecycle
}
from
'./useEditorLifecycle'
import
{
createEditorConfig
,
defaultEditorContent
,
toolbarConfig
}
from
'./editor-config'
import
{
useEditorFeatureEvents
}
from
'../core/useEditorFeatureEvents'
import
{
createAutosaveScheduler
}
from
'./autosave'
import
{
getEmptyParagraphPath
,
normalizeEmptyParagraph
}
from
'./paragraph-behavior'
...
...
@@ -55,25 +48,28 @@ const WangEditorCustomer = (props, ref) => {
const
[
tabKey
,
setTabKey
]
=
useState
(
'text'
)
// editor 实例
const
[
editor
,
setEditor
]
=
useState
(
null
)
const
[
content
,
setContent
]
=
useState
(
html
)
const
saveContentRef
=
useRef
(
saveContent
)
const
autosaveRef
=
useRef
(
)
if
(
!
autosaveRef
.
current
)
{
autosaveRef
.
current
=
createAutosaveScheduler
(()
=>
saveContentRef
.
current
?.(),
1500
)
}
const
latestHtmlRef
=
useRef
(
html
)
const
[
autosave
]
=
useState
(()
=>
createAutosaveScheduler
(()
=>
saveContentRef
.
current
?.(),
1500
))
const
[
htmlSync
]
=
useState
(()
=>
createAutosaveScheduler
(()
=>
setHtml
(
latestHtmlRef
.
current
),
500
),
)
useEffect
(()
=>
{
saveContentRef
.
current
=
saveContent
},
[
saveContent
])
useEffect
(()
=>
{
return
()
=>
autosaveRef
.
current
?.
cancel
()
},
[])
return
()
=>
{
autosave
.
cancel
()
htmlSync
.
cancel
()
}
},
[
autosave
,
htmlSync
])
useEffect
(()
=>
{
autosaveRef
.
current
?.
cancel
()
},
[
chapterId
,
contentId
,
disabled
,
isView
])
autosave
.
cancel
()
htmlSync
.
cancel
()
},
[
autosave
,
chapterId
,
contentId
,
disabled
,
htmlSync
,
isView
])
useImperativeHandle
(
ref
,
()
=>
{
return
{
...
...
@@ -82,6 +78,12 @@ const WangEditorCustomer = (props, ref) => {
})
const
updateContent
=
(
editor
)
=>
{
const
shouldSyncVideoAlignment
=
(
editor
.
operations
||
[]).
some
(
(
operation
)
=>
(
operation
.
type
===
'insert_node'
&&
operation
.
node
?.
type
===
'video'
)
||
(
operation
.
type
===
'set_node'
&&
'align'
in
(
operation
.
newProperties
||
{})),
)
if
(
shouldSyncVideoAlignment
)
{
for
(
const
[
node
]
of
SlateEditor
.
nodes
(
editor
,
{
match
:
(
node
)
=>
node
.
type
===
'video'
,
universal
:
true
,
...
...
@@ -89,8 +91,10 @@ const WangEditorCustomer = (props, ref) => {
const
dom
=
DomEditor
.
toDOMNode
(
editor
,
node
)
Object
.
assign
(
dom
.
style
,
{
textAlign
:
node
.
align
||
'center'
})
}
setHtml
(
editor
.
getHtml
())
setContent
(
editor
.
getHtml
())
}
latestHtmlRef
.
current
=
editor
.
getHtml
()
htmlSync
.
schedule
()
}
const
editorConfig
=
createEditorConfig
({
...
...
@@ -100,10 +104,10 @@ const WangEditorCustomer = (props, ref) => {
editorConfig
.
onChange
=
(
currentEditor
)
=>
{
updateContent
(
currentEditor
)
if
(
!
disabled
&&
!
isView
&&
contentId
)
autosave
Ref
.
current
.
schedule
()
if
(
!
disabled
&&
!
isView
&&
contentId
)
autosave
.
schedule
()
}
useEditor
FeatureEvents
({
editor
,
registry
:
editorFeatureRegistry
,
runtime
:
{}
})
useEditor
Lifecycle
({
editor
,
gData
,
disabled
:
disabled
||
isView
})
useEffect
(()
=>
{
if
(
!
editor
)
return
...
...
@@ -149,7 +153,6 @@ const WangEditorCustomer = (props, ref) => {
editor
.
addMark
(
'fontSize'
,
'18px'
)
editor
.
addMark
(
'fontFamily'
,
'黑体'
)
editor
.
addMark
(
'lineHeight'
,
1.5
)
setContent
(
html
)
return
()
=>
{
editor
.
destroy
()
setEditor
(
null
)
...
...
@@ -166,7 +169,7 @@ const WangEditorCustomer = (props, ref) => {
// 预览
const
previewIt
=
async
()
=>
{
if
(
!
isView
)
{
autosave
Ref
.
current
.
cancel
()
autosave
.
cancel
()
await
saveContent
()
}
openEditorDialog
(
editor
,
'preview.open'
)
...
...
@@ -178,10 +181,20 @@ const WangEditorCustomer = (props, ref) => {
}
const
saveNow
=
()
=>
{
autosave
Ref
.
current
.
cancel
()
autosave
.
cancel
()
return
saveContent
()
}
const
restoreContent
=
async
(
contentHtml
)
=>
{
if
(
!
editor
||
!
saveContent
)
return
editor
.
setHtml
(
contentHtml
)
autosave
.
cancel
()
htmlSync
.
cancel
()
setHtml
(
contentHtml
)
await
saveContent
(
contentHtml
)
}
const
colorList
=
[
{
color
:
'#ab1941'
,
name
:
'默认'
},
{
color
:
'#2970f6'
,
name
:
'蓝色'
},
...
...
@@ -229,8 +242,9 @@ const WangEditorCustomer = (props, ref) => {
<
Divider
style=
{
{
margin
:
0
}
}
/>
<
Editor
defaultConfig=
{
editorConfig
}
defaultContent=
{
defaultEditorContent
}
value=
{
content
}
defaultContent=
{
html
?
[]
:
defaultEditorContent
}
defaultHtml=
{
html
}
value=
{
html
}
onCreated=
{
setEditor
}
mode=
'default'
style=
{
{
flex
:
1
,
minHeight
:
0
,
overflowY
:
'auto'
,
overflowX
:
'hidden'
}
}
...
...
@@ -251,7 +265,7 @@ const WangEditorCustomer = (props, ref) => {
<
EditorDialogHost
editor=
{
editor
}
registry=
{
editorFeatureRegistry
}
runtime=
{
{
editor
,
chapterId
,
bookId
,
gData
,
nowTitle
,
saveContent
,
set
Content
}
}
runtime=
{
{
editor
,
chapterId
,
bookId
,
gData
,
nowTitle
,
saveContent
,
restore
Content
}
}
/>
</
div
>
)
...
...
apps/admin/src/components/layout/components/message.jsx
浏览文件 @
3da7f60b
...
...
@@ -108,7 +108,7 @@ function MessageListItem({ item, onRead }) {
<
BellOutlined
/>
</
Badge
>
</
Col
>
<
Col
span=
{
22
}
className=
'process'
>
<
Col
span=
{
22
}
>
<
div
className=
'head'
style=
{
{
display
:
'flex'
,
justifyContent
:
'space-between'
,
marginBottom
:
10
}
}
...
...
apps/admin/src/components/layout/index.jsx
浏览文件 @
3da7f60b
...
...
@@ -177,8 +177,6 @@ export default function AppLayout() {
const
[
collapsed
,
setCollapsed
]
=
useState
(
()
=>
localStorage
.
getItem
(
SIDER_COLLAPSED_STORAGE_KEY
)
!==
'false'
,
)
const
[
siderHovered
,
setSiderHovered
]
=
useState
(
false
)
const
siderCollapsed
=
collapsed
&&
!
siderHovered
const
headerOverride
=
pageHeaderOverrides
[
pathname
]
const
pageTitle
=
headerOverride
?.
title
||
getCurrentMenuTitle
(
menuList
,
pathname
)
...
...
@@ -232,11 +230,9 @@ export default function AppLayout() {
className=
'layout-sider'
width=
{
240
}
collapsedWidth=
{
64
}
collapsed=
{
siderC
ollapsed
}
collapsed=
{
c
ollapsed
}
theme=
'light'
trigger=
{
null
}
onMouseEnter=
{
()
=>
setSiderHovered
(
true
)
}
onMouseLeave=
{
()
=>
setSiderHovered
(
false
)
}
>
<
div
className=
'layout-brand'
>
<
Link
className=
'layout-brand__logo'
to=
'/'
aria
-
label=
'返回工作台首页'
>
...
...
@@ -244,7 +240,7 @@ export default function AppLayout() {
</
Link
>
</
div
>
<
div
className=
'layout-sider__body'
>
<
Menu
menuList=
{
menuList
}
collapsed=
{
siderC
ollapsed
}
></
Menu
>
<
Menu
menuList=
{
menuList
}
collapsed=
{
c
ollapsed
}
></
Menu
>
</
div
>
<
Tooltip
title=
{
collapsed
?
'固定展开侧边菜单'
:
'收起侧边菜单'
}
placement=
'right'
>
<
Button
...
...
apps/admin/src/components/layout/index.less
浏览文件 @
3da7f60b
...
...
@@ -14,10 +14,6 @@
.layout-sider-slot--collapsed {
flex-basis: 64px;
width: 64px;
.layout-sider:not(.ant-layout-sider-collapsed) {
z-index: 102;
}
}
.layout-sider {
...
...
apps/admin/src/modules/access/admin/components/DeleteAdminModal.tsx
浏览文件 @
3da7f60b
...
...
@@ -15,7 +15,7 @@ export default function DeleteAdminModal({ open, onCancel, onConfirm }: DeleteAd
onOk=
{
onConfirm
}
onCancel=
{
onCancel
}
footer=
{
(
_
,
{
OkBtn
,
CancelBtn
})
=>
(
<
Row
justify=
'center'
className=
'delModal'
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Row
justify=
'center'
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Space
size=
{
20
}
>
<
CancelBtn
/>
<
OkBtn
/>
...
...
apps/admin/src/modules/access/role/components/DeleteRoleModal.tsx
浏览文件 @
3da7f60b
...
...
@@ -15,7 +15,7 @@ export default function DeleteRoleModal({ open, onCancel, onConfirm }: DeleteRol
onOk=
{
onConfirm
}
onCancel=
{
onCancel
}
footer=
{
(
_
,
{
OkBtn
,
CancelBtn
})
=>
(
<
Row
justify=
'center'
className=
'delModal'
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Row
justify=
'center'
style=
{
{
marginBottom
:
'30px'
}
}
>
<
Space
size=
{
20
}
>
<
CancelBtn
/>
<
OkBtn
/>
...
...
apps/admin/src/modules/account/views/AccountView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/advertisements/advert/components/DeleteAdvertModal.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/management/chapters/index.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/management/components/DeleteBookModal.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/management/components/ExportBookModal.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/management/components/ImportBookDrawer.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/management/components/ManagementAddEdit.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/review-comments/views/EditorReviewComments.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/sale/components/SaleDiscussionDetail.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/books/sale/components/SaleEdit.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/members/level/components/DeleteMemberLevelModal.tsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/settings/app-version/views/AppVersionListView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/settings/applied/views/AppliedListView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/settings/help/views/HelpListView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/student-book/views/StudentBookView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/modules/teachers/list/views/TeacherListView.jsx
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
apps/admin/src/stores/editorStore.ts
浏览文件 @
3da7f60b
差异被折叠。
点击展开。
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论