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

feat: 新增插入图标

上级 dfc99e11
......@@ -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}
......
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()
},
}
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>
)
}
.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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论