Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
C
center-book
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
center-book
Commits
e793befe
提交
e793befe
authored
3月 13, 2026
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 新增插入图标
上级
dfc99e11
显示空白字符变更
内嵌
并排
正在显示
5 个修改的文件
包含
298 行增加
和
0 行删除
+298
-0
index.less
src/common/wangeditor-customer/components/index.less
+1
-0
index.jsx
src/common/wangeditor-customer/index.jsx
+4
-0
Icon.jsx
src/common/wangeditor-customer/menu/Icon.jsx
+22
-0
IconModal.jsx
src/common/wangeditor-customer/menu/common/IconModal.jsx
+182
-0
IconModal.less
src/common/wangeditor-customer/menu/common/IconModal.less
+89
-0
没有找到文件。
src/common/wangeditor-customer/components/index.less
浏览文件 @
e793befe
...
...
@@ -159,3 +159,4 @@
}
}
}
src/common/wangeditor-customer/index.jsx
浏览文件 @
e793befe
...
...
@@ -59,6 +59,7 @@ import AIWrite from './menu/AIWrite'
import
AIImage
from
'./menu/AIImage'
import
AIVideo
from
'./menu/AIVideo'
import
AIBaiduSearch
from
'./menu/AIBaiduSearch'
import
Icon
from
'./menu/Icon'
import
ImageModal
from
'./components/image'
import
VideoModal
from
'./components/video'
...
...
@@ -131,6 +132,7 @@ const module = {
AIImage
,
AIVideo
,
AIBaiduSearch
,
Icon
,
],
}
Boot
.
registerModule
(
module
)
...
...
@@ -358,6 +360,7 @@ const WangEditorCustomer = (props, ref) => {
'AIVideo'
,
'AudioAuto'
,
'insertTable'
,
'Icon'
,
'|'
,
'codeBlock'
,
// 代码块
'blockquote'
,
// 引用
...
...
@@ -999,6 +1002,7 @@ const WangEditorCustomer = (props, ref) => {
/>
</
Modal
>
<
Modal
open=
{
practiceVisible
}
footer=
{
null
}
...
...
src/common/wangeditor-customer/menu/Icon.jsx
0 → 100644
浏览文件 @
e793befe
import
BaseModalMenu
from
'./common/BaseModalMenu'
import
IconModal
from
'./common/IconModal'
class
Icon
extends
BaseModalMenu
{
constructor
()
{
super
()
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
>
}
}
export
default
{
key
:
'Icon'
,
factory
()
{
return
new
Icon
()
},
}
src/common/wangeditor-customer/menu/common/IconModal.jsx
0 → 100644
浏览文件 @
e793befe
import
{
useState
,
useMemo
}
from
'react'
import
{
Modal
,
message
,
Row
,
Col
,
Card
,
Tabs
,
Input
}
from
'antd'
import
{
SlateTransforms
}
from
'@wangeditor/editor'
import
*
as
AntIcons
from
'@ant-design/icons'
import
'./IconModal.less'
const
{
Search
}
=
Input
export
default
function
IconModal
(
props
)
{
const
{
editor
}
=
props
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
true
)
const
[
selectedIcon
,
setSelectedIcon
]
=
useState
(
null
)
const
[
activeTab
,
setActiveTab
]
=
useState
(
'image'
)
const
[
searchText
,
setSearchText
]
=
useState
(
''
)
// 精美图标 (JPG)
const
imageIcons
=
Array
.
from
({
length
:
11
},
(
_
,
i
)
=>
({
id
:
i
+
1
,
url
:
`https://webapp-pub.ezijing.com/book-app/icons/
${
i
+
1
}
.png`
,
type
:
'image'
}))
// 通用图标 (Ant Design Icons)
const
antIconNames
=
useMemo
(()
=>
{
return
Object
.
keys
(
AntIcons
).
filter
(
name
=>
name
.
endsWith
(
'Outlined'
)
&&
/^
[
A-Z
]
/
.
test
(
name
)
&&
(
typeof
AntIcons
[
name
]
===
'object'
||
typeof
AntIcons
[
name
]
===
'function'
)
)
},
[])
const
filteredAntIcons
=
useMemo
(()
=>
{
let
list
=
antIconNames
if
(
searchText
)
{
list
=
antIconNames
.
filter
(
name
=>
name
.
toLowerCase
().
includes
(
searchText
.
toLowerCase
())
)
}
return
list
.
slice
(
0
,
120
)
},
[
antIconNames
,
searchText
])
// 选择图标时的处理:如果是 Ant 图标,提前生成 Data URL
const
onSelect
=
(
icon
)
=>
{
if
(
icon
.
type
===
'ant'
)
{
const
iconElement
=
document
.
getElementById
(
`temp-icon-
${
icon
.
name
}
`
)
if
(
iconElement
)
{
const
svgElement
=
iconElement
.
querySelector
(
'svg'
)
if
(
svgElement
)
{
const
clonedSvg
=
svgElement
.
cloneNode
(
true
)
clonedSvg
.
setAttribute
(
'width'
,
'32'
)
clonedSvg
.
setAttribute
(
'height'
,
'32'
)
clonedSvg
.
setAttribute
(
'xmlns'
,
'http://www.w3.org/2000/svg'
)
// 确保图标有颜色
if
(
!
clonedSvg
.
getAttribute
(
'fill'
)
||
clonedSvg
.
getAttribute
(
'fill'
)
===
'currentColor'
)
{
clonedSvg
.
setAttribute
(
'fill'
,
'#333333'
)
}
const
svgString
=
new
XMLSerializer
().
serializeToString
(
clonedSvg
)
try
{
const
base64
=
btoa
(
unescape
(
encodeURIComponent
(
svgString
)))
setSelectedIcon
({
...
icon
,
url
:
`data:image/svg+xml;base64,
${
base64
}
`
})
}
catch
(
e
)
{
setSelectedIcon
({
...
icon
,
url
:
`data:image/svg+xml;charset=utf-8,
${
encodeURIComponent
(
svgString
)}
`
})
}
return
}
}
}
setSelectedIcon
(
icon
)
}
const
handleInsert
=
()
=>
{
if
(
!
selectedIcon
||
!
selectedIcon
.
url
)
{
message
.
warning
(
'请先选择一个图标'
)
return
}
if
(
editor
)
{
editor
.
restoreSelection
()
const
iconNode
=
{
type
:
'image'
,
src
:
selectedIcon
.
url
,
alt
:
'icon'
,
style
:
{
width
:
'32px'
,
height
:
'32px'
},
width
:
'32'
,
// 双重保障,部分渲染器读这个
height
:
'32'
,
children
:
[{
text
:
''
}],
}
// 很多时候直接插入 image node 可能会被某些编辑器逻辑过滤
// 我们尝试按照 AIImage 的习惯,如果是在段落里就直接插,否则尝试外层包一下
SlateTransforms
.
insertNodes
(
editor
,
iconNode
)
message
.
success
(
'图标已插入编辑器!'
)
setIsModalOpen
(
false
)
}
else
{
message
.
error
(
'编辑器未找到'
)
}
}
const
renderImageIcons
=
()
=>
(
<
Row
gutter=
{
[
16
,
16
]
}
>
{
imageIcons
.
map
((
icon
)
=>
(
<
Col
span=
{
6
}
key=
{
icon
.
id
}
>
<
Card
hoverable
className=
{
`icon-item-card ${selectedIcon?.url === icon.url ? 'selected' : ''}`
}
cover=
{
<
div
className=
"icon-container"
>
<
img
src=
{
icon
.
url
}
alt=
{
`icon-${icon.id}`
}
/>
</
div
>
}
onClick=
{
()
=>
onSelect
(
icon
)
}
/>
</
Col
>
))
}
</
Row
>
)
const
renderAntIcons
=
()
=>
(
<
div
className=
"ant-icons-tab"
>
<
Search
placeholder=
"搜索通用图标"
allowClear
onChange=
{
e
=>
setSearchText
(
e
.
target
.
value
)
}
style=
{
{
marginBottom
:
16
}
}
/>
<
div
className=
"ant-icons-grid"
>
<
Row
gutter=
{
[
8
,
8
]
}
>
{
filteredAntIcons
.
map
((
name
)
=>
{
const
IconComp
=
AntIcons
[
name
]
if
(
!
IconComp
)
return
null
return
(
<
Col
span=
{
4
}
key=
{
name
}
>
<
div
className=
{
`ant-icon-item ${selectedIcon?.name === name ? 'selected' : ''}`
}
onClick=
{
()
=>
onSelect
({
name
,
type
:
'ant'
})
}
>
<
div
id=
{
`temp-icon-${name}`
}
className=
"icon-wrapper"
>
<
IconComp
style=
{
{
fontSize
:
24
}
}
/>
</
div
>
<
div
className=
"ant-icon-name"
>
{
name
.
replace
(
'Outlined'
,
''
)
}
</
div
>
</
div
>
</
Col
>
)
})
}
</
Row
>
</
div
>
</
div
>
)
return
(
<
Modal
title=
"选择图标"
open=
{
isModalOpen
}
onOk=
{
handleInsert
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
okText=
"插入"
cancelText=
"取消"
width=
{
700
}
centered
destroyOnClose
>
<
Tabs
activeKey=
{
activeTab
}
onChange=
{
setActiveTab
}
items=
{
[
{
key
:
'image'
,
label
:
'精美图标'
,
children
:
<
div
className=
"icon-selection-grid"
>
{
renderImageIcons
()
}
</
div
>
},
{
key
:
'ant'
,
label
:
'通用图标'
,
children
:
<
div
className=
"ant-icons-container"
>
{
renderAntIcons
()
}
</
div
>
}
]
}
/>
</
Modal
>
)
}
src/common/wangeditor-customer/menu/common/IconModal.less
0 → 100644
浏览文件 @
e793befe
.icon-selection-grid {
padding: 16px 0;
.icon-item-card {
transition: all 0.3s;
border: 1px solid #d9d9d9;
&:hover, &.selected {
border-color: #ab1941;
background-color: #fff1f0;
}
.ant-card-body {
display: none;
}
.icon-container {
text-align: center;
display: flex;
justify-content: center;
align-items: center;
height: 80px;
img {
width: 48px;
height: 48px;
object-fit: contain;
}
}
}
}
.ant-icons-container {
padding: 8px 0;
.ant-icons-grid {
max-height: 400px;
overflow-y: auto;
padding: 8px;
background: #fafafa;
border-radius: 4px;
}
.ant-icon-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px 8px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s;
color: #333; // 确保默认颜色可见
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
background: #fff;
border-color: #ab1941;
color: #ab1941;
}
&.selected {
background: #fff1f0;
border-color: #ab1941;
color: #ab1941;
}
.ant-icon-name {
font-size: 11px;
margin-top: 8px;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #666;
}
&.selected .ant-icon-name {
color: #ab1941;
}
}
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论