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

chore: 修改编辑器AI相关内容

上级 1cc62424
......@@ -584,6 +584,7 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@fortaine/fetch-event-source/-/fetch-event-source-3.0.6.tgz",
"integrity": "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==",
"license": "MIT",
"engines": {
"node": ">=16.15"
}
......
import { useState, useEffect } from 'react'
import { SendOutlined } from '@ant-design/icons'
import { Input, Button, message } from 'antd'
import md5 from 'js-md5'
import dayjs from 'dayjs'
import { sleep } from '@/utils/common'
import './index.less'
import { useSelector, useDispatch } from 'react-redux'
import { setEditorAi } from '@/store/modules/editor'
import normalAvatar from '@/assets/images/icon-normal-avatar.png'
const UUID = 'f3846153ba784b6d86bdcd5533259c88'
const auth_key = 'f3846153ba784b6d86bdcd5533259c88'
const auth_secret = 'HO4IyLEwEOHpeOXBxaLQUOqWslJRGs1M'
const AIDrawerComponent = (props) => {
const { selectText } = props
const dispatch = useDispatch()
const { userInfo } = useSelector((state) => state.user) // 用户状态信息
const { editorAi } = useSelector((state) => state.editor) // ai信息设置
const [value, setValue] = useState(selectText) // 输入值
const [loading, setLoading] = useState(false) // loading
const [chatId, setChatId] = useState(null) // 对话的id
const [chatContentList, setChatContentList] = useState([]) // 对话数据
const element = document.querySelector('#chat-content')
let str = ''
useEffect(() => {
if (editorAi && Object.entries(editorAi).length > 0) {
let tempJSON = JSON.parse(JSON.stringify(chatContentList))
let conversationId = editorAi.conversationId
const item = tempJSON.filter((item) => item.conversationId === conversationId)
if (item && item.length > 0) {
tempJSON[tempJSON.length - 1] = editorAi
} else {
tempJSON.push(editorAi)
}
setChatId(editorAi.chatId)
setChatContentList(tempJSON)
element.scrollTo(0, 999999)
} else {
setLoading(false)
}
}, [editorAi])
useEffect(() => {
if (selectText) {
targetChat()
}
return () => {
setValue('')
setChatContentList([])
setChatId(null)
}
}, [])
let buffer = ''
const readBuffer = (reader, decoder) => {
return reader.read().then(async ({ done, value }) => {
if (done) {
// 处理最后的缓冲数据
setLoading(false)
setValue('')
dispatch(setEditorAi({}))
str = ''
processBuffer(buffer)
return
}
await sleep(80)
buffer += decoder.decode(value, { stream: true })
// 检查缓冲区中是否有完整的消息
const messages = buffer.split('\n\n') // 假设每个消息以换行符结束
messages.forEach((message, index) => {
if (index < messages.length - 1) {
processBuffer(message)
}
})
buffer = messages[messages.length - 1] // 更新缓冲区
readBuffer(reader, decoder)
})
}
const processBuffer = (text) => {
let aiContent = {}
try {
const content = text.replace(/\n+/g, '').split('data:')
if (content instanceof Array) {
const itemContent = JSON.parse(content[1])
if (!(itemContent.finish && itemContent.finish === true)) {
str += itemContent.content
aiContent = {
type: 'ai',
content: str,
chatId: itemContent.chatId,
avatar: itemContent.role.avatar,
conversationId: itemContent.conversationId
}
dispatch(setEditorAi(aiContent))
}
}
} catch {}
}
// 提交对话
const targetChat = async () => {
if (value) {
setLoading(true)
const timestamp = Date.now()
fetch('/api/tiangong/openapi/agent/chat/stream/v1', {
method: 'POST',
responseType: 'stream', // 设置响应类型为 'stream'
headers: {
'Content-Type': 'application/json',
authKey: auth_key,
timestamp: timestamp,
sign: md5(`${auth_key}${auth_secret}${timestamp}`),
stream: true // or change to "false" 不处理流式返回内容
},
body: JSON.stringify({
agentId: UUID,
chatId: chatId,
userChatInput: value
}),
mode: 'cors'
})
.then((response) => {
// response.body 是一个 ReadableStream 对象
const stream = response.body
// 使用流式数据
const reader = stream.getReader()
const decoder = new TextDecoder() // 用于解码二进制数据流
let userContent = {
type: 'user',
content: value,
time: Date.now()
}
let tempJSON = JSON.parse(JSON.stringify(chatContentList))
tempJSON.push(userContent)
setChatContentList(tempJSON)
// 开始读取流数据
readBuffer(reader, decoder)
})
.catch((error) => {
// 处理请求错误
console.error('Request failed:', error)
})
} else {
message.error('请输入对话内容!')
}
}
return (
<div className="ai-drawer-content">
<div className="ai-chat-container">
<div className="chat-content" id="chat-content">
<div className="chat-content-padd">
{chatContentList &&
chatContentList.length > 0 &&
chatContentList.map((item, index) => {
return (
<div className={`chat-content-item`} key={index}>
{item.type === 'user' && <div className="time">{dayjs(item.time).format('YYYY-MM-DD HH:mm')}</div>}
{item.type === 'ai' && (
<div className="inside">
<div className="ai-in-content">
<div className="img">
<img src={item.avatar} alt="" />
</div>
<div className="ask-content">{item.content}</div>
</div>
</div>
)}
{item.type === 'user' && (
<div className="inside inside-user">
<div className="user-in-content">
<div className="ask-content">{item.content}</div>
<div className="img">
<img src={userInfo.pic ? userInfo.pic : normalAvatar} alt="用户头像" />
</div>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</div>
<div className="ai-chat-ask">
<div className="text">
<Input.TextArea
value={value}
defaultValue={value}
allowClear
disabled={loading}
onChange={(e) => setValue(e.target.value)}
palceholder="请输入内容"
style={{ height: '80px', resize: 'none' }}></Input.TextArea>
</div>
<div className="button">
<Button type="primary" icon={<SendOutlined />} loading={loading} size="large" onClick={targetChat}></Button>
</div>
</div>
</div>
)
}
export default AIDrawerComponent
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import {
Modal,
Divider,
Upload,
Input,
Space,
Button,
Form,
Spin,
message,
ColorPicker,
Select,
InputNumber,
Row,
Col,
} from 'antd';
import { DomEditor, SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor';
import { fontFamilyList, fontFizeList } from '../utils/setting';
import $ from 'jquery';
import './index.less';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Modal, Divider, Upload, Input, Space, Button, Form, Spin, message, ColorPicker, Select, InputNumber, Row, Col } from 'antd'
import { DomEditor, SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor'
import { fontFamilyList, fontSizeList } from '../utils/setting'
import $ from 'jquery'
import './index.less'
const ChapterItemModal = forwardRef((props, ref) => {
const { editor, setSectionInfo, sectionInfo, setSectionVisible } = props;
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const { editor, setSectionInfo, sectionInfo, setSectionVisible } = props
const [form] = Form.useForm()
const [visible, setVisible] = useState(false)
// 是否更新的判断
const [toolStatus, setToolStatus] = useState(false);
const [bgColorValue, setBgColorValue] = useState('#000000');
const [textColor, setTextColor] = useState('#ffffff');
const [nowRandom, setNowRandom] = useState(null);
const [toolStatus, setToolStatus] = useState(false)
const [bgColorValue, setBgColorValue] = useState('#000000')
const [textColor, setTextColor] = useState('#ffffff')
const [nowRandom, setNowRandom] = useState(null)
const [initValues, setInitValues] = useState({
title: '',
textColor: textColor,
......@@ -37,34 +22,34 @@ const ChapterItemModal = forwardRef((props, ref) => {
size: 24,
bgColor: bgColorValue,
height: 100,
family: '黑体',
});
family: '黑体'
})
useImperativeHandle(ref, () => {
return {
setVisible,
};
});
setVisible
}
})
useEffect(() => {
if (sectionInfo && Object.entries(sectionInfo).length > 0) {
setInitValues({ ...sectionInfo });
form.setFieldsValue({ ...sectionInfo });
setNowRandom(sectionInfo.random);
setTextColor(sectionInfo.textColor);
setBgColorValue(sectionInfo.bgColor);
setSectionInfo({});
setInitValues({ ...sectionInfo })
form.setFieldsValue({ ...sectionInfo })
setNowRandom(sectionInfo.random)
setTextColor(sectionInfo.textColor)
setBgColorValue(sectionInfo.bgColor)
setSectionInfo({})
}
}, [sectionInfo]);
}, [sectionInfo])
const bgColorChange = (value, hex) => {
setBgColorValue(hex);
form.setFieldsValue({ bgColor: hex });
};
setBgColorValue(hex)
form.setFieldsValue({ bgColor: hex })
}
const textColorChange = (value, hex) => {
setTextColor(hex);
form.setFieldsValue({ textColor: hex });
};
setTextColor(hex)
form.setFieldsValue({ textColor: hex })
}
const callback = ({ title, random, bgColor, textColor, align, height, size }) => {
form.setFieldsValue({
......@@ -73,82 +58,82 @@ const ChapterItemModal = forwardRef((props, ref) => {
textColor: textColor,
align: align,
height: height,
size: size,
});
setToolStatus(true);
setNowRandom(random);
};
size: size
})
setToolStatus(true)
setNowRandom(random)
}
// 获取当前节点的上一个节点
function getPreviousNode(editor) {
const { selection } = editor;
if (!selection) return null;
const { selection } = editor
if (!selection) return null
const [start] = SlateEditor.nodes(editor, { at: selection });
const previous = SlateEditor.previous(editor, { at: start[1] });
const [start] = SlateEditor.nodes(editor, { at: selection })
const previous = SlateEditor.previous(editor, { at: start[1] })
if (previous) {
return previous;
return previous
}
return null;
return null
}
const onFinish = (values) => {
editor.restoreSelection();
const onFinish = values => {
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
return true; // 匹配 paragraph
return true // 匹配 paragraph
}
}
return false;
return false
},
universal: true,
});
universal: true
})
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
if (node.children[0].text === '') {
SlateTransforms.removeNodes(editor);
SlateTransforms.removeNodes(editor)
}
}
if (nowRandom) {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterSection') {
return true; // 匹配 chapterHeader
return true // 匹配 chapterHeader
}
}
return false;
return false
},
universal: true,
});
universal: true
})
if (nodeEntries) {
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
// console.log(node, path);
if (parseInt(node.random) === parseInt(nowRandom)) {
SlateTransforms.setNodes(editor, { ...node, ...values }, { at: path });
SlateTransforms.setNodes(editor, { ...node, ...values }, { at: path })
form.resetFields();
setSectionVisible(false);
return false;
form.resetFields()
setSectionVisible(false)
return false
}
}
}
return;
return
}
const html = editor.getHtml();
const parser = new DOMParser();
let random = Math.random().toString(10).substring(2, 10);
const docBody = parser.parseFromString(html, 'text/html');
const section = docBody.body.querySelectorAll('.chapter-item-section'); // 节头标签
const html = editor.getHtml()
const parser = new DOMParser()
let random = Math.random().toString(10).substring(2, 10)
const docBody = parser.parseFromString(html, 'text/html')
const section = docBody.body.querySelectorAll('.chapter-item-section') // 节头标签
editor.insertNode({
type: 'chapterSection',
......@@ -161,103 +146,74 @@ const ChapterItemModal = forwardRef((props, ref) => {
size: values.size ? values.size : 20,
family: values.family,
callback: callback,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
form.resetFields();
setSectionVisible(false);
};
form.resetFields()
setSectionVisible(false)
}
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initValues}
>
<Form.Item
label='节头标题'
name='title'
rules={[{ required: true, message: '请输入节头标题' }]}
extra='最多输入30字'
>
<Input maxLength={30} placeholder='' allowClear />
<div className="editor-content-form">
<Form layout="vertical" name="validate_other" form={form} onFinish={onFinish} initialValues={initValues}>
<Form.Item label="节头标题" name="title" rules={[{ required: true, message: '请输入节头标题' }]} extra="最多输入30字">
<Input maxLength={30} placeholder="" allowClear />
</Form.Item>
<Form.Item>
<Row gutter={20}>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='背景颜色'
name='bgColor'
className='flex-max'
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}
>
<Input maxLength={100} placeholder='' allowClear />
label="背景颜色"
name="bgColor"
className="flex-max"
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}>
<Input maxLength={100} placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
format='hex'
value={bgColorValue}
defaultValue={bgColorValue}
onChange={bgColorChange}
/>
<ColorPicker disabledAlpha format="hex" value={bgColorValue} defaultValue={bgColorValue} onChange={bgColorChange} />
</Form.Item>
</div>
</Col>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='文本颜色'
name='textColor'
className='flex-max'
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}
>
<Input placeholder='' allowClear />
label="文本颜色"
name="textColor"
className="flex-max"
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
format='hex'
value={textColor}
defaultValue={textColor}
onChange={textColorChange}
/>
<ColorPicker disabledAlpha format="hex" value={textColor} defaultValue={textColor} onChange={textColorChange} />
</Form.Item>
</div>
</Col>
<Col span={12}>
<Form.Item label='字体' name='family'>
<Form.Item label="字体" name="family">
<Select>
{fontFamilyList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label='文字大小'
name='size'
rules={[{ required: true, message: '请输入文字大小' }]}
>
<Form.Item label="文字大小" name="size" rules={[{ required: true, message: '请输入文字大小' }]}>
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
......@@ -271,37 +227,28 @@ const ChapterItemModal = forwardRef((props, ref) => {
/> */}
</Col>
<Col span={12}>
<Form.Item
label='对齐方式'
name='align'
rules={[{ required: true, message: '请选择对齐方式' }]}
>
<Form.Item label="对齐方式" name="align" rules={[{ required: true, message: '请选择对齐方式' }]}>
<Select>
<Select.Option value='left'>左对齐</Select.Option>
<Select.Option value='center'>居中对齐</Select.Option>
<Select.Option value='right'>右对齐</Select.Option>
<Select.Option value="left">左对齐</Select.Option>
<Select.Option value="center">居中对齐</Select.Option>
<Select.Option value="right">右对齐</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='高度' name='height'>
<InputNumber
controls={false}
placeholder=''
style={{ width: '100%', textAlign: 'left' }}
allowClear
/>
<Form.Item label="高度" name="height">
<InputNumber controls={false} placeholder="" style={{ width: '100%', textAlign: 'left' }} allowClear />
</Form.Item>
</Col>
<Col span={12}></Col>
</Row>
</Form.Item>
<Form.Item className='editor-form-buttons'>
<Form.Item className="editor-form-buttons">
<Space>
<Button type='default' onClick={() => setSectionVisible(false)}>
<Button type="default" onClick={() => setSectionVisible(false)}>
取消
</Button>
<Button type='primary' htmlType='submit'>
<Button type="primary" htmlType="submit">
{nowRandom ? '更新' : '插入'}
</Button>
</Space>
......@@ -309,7 +256,7 @@ const ChapterItemModal = forwardRef((props, ref) => {
</Form>
</div>
</div>
);
});
)
})
export default ChapterItemModal;
export default ChapterItemModal
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import {
Modal,
Divider,
Upload,
Input,
Space,
Button,
Form,
Spin,
message,
ColorPicker,
Select,
InputNumber,
Row,
Col,
} from 'antd';
import { CloudUploadOutlined } from '@ant-design/icons';
import { DomEditor, SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor';
import { fontFamilyList, fontFizeList } from '../utils/setting';
import './index.less';
import $ from 'jquery';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Modal, Divider, Upload, Input, Space, Button, Form, Spin, message, ColorPicker, Select, InputNumber, Row, Col } from 'antd'
import { CloudUploadOutlined } from '@ant-design/icons'
import { DomEditor, SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor'
import { fontFamilyList, fontSizeList } from '../utils/setting'
import './index.less'
import $ from 'jquery'
import { partSize, normalUploader, multipartUploader } from '../utils/upload';
import { partSize, normalUploader, multipartUploader } from '../utils/upload'
const { Dragger } = Upload;
const { Dragger } = Upload
const imgUrl2 =
'http://zxts-book-file.zijingebook.com/2024/03/05/image-1709606013683-6k5qmzf75zj.png';
const imgUrl2 = 'http://zxts-book-file.zijingebook.com/2024/03/05/image-1709606013683-6k5qmzf75zj.png'
const ImageModal = forwardRef((props, ref) => {
const { editor, ossClient, setTitleVisible, titleInfo, setTitleInfo } = props;
const { editor, ossClient, setTitleVisible, titleInfo, setTitleInfo } = props
const fileAccept = ['.png', '.jpg', '.jpeg', '.svg'];
const [form] = Form.useForm();
const [bgColorValue, setBgColorValue] = useState('#000000');
const [textColor, setTextColor] = useState('#ffffff');
const [uploading, setUploading] = useState(false); // 文件上传状态
const [progress, setProgress] = useState(0);
const [file, setFile] = useState({});
const [fileList, setFileList] = useState([]);
const [imgUrl, setImgUrl] = useState('');
const fileAccept = ['.png', '.jpg', '.jpeg', '.svg']
const [form] = Form.useForm()
const [bgColorValue, setBgColorValue] = useState('#000000')
const [textColor, setTextColor] = useState('#ffffff')
const [uploading, setUploading] = useState(false) // 文件上传状态
const [progress, setProgress] = useState(0)
const [file, setFile] = useState({})
const [fileList, setFileList] = useState([])
const [imgUrl, setImgUrl] = useState('')
const [initValues, setInitValues] = useState({
title: '',
size: 32,
......@@ -48,39 +32,39 @@ const ImageModal = forwardRef((props, ref) => {
align: 'left',
bgColor: bgColorValue,
height: 200,
family: '黑体',
});
family: '黑体'
})
// 是否更新的判断
const [toolStatus, setToolStatus] = useState(false);
const [nowRandom, setNowRandom] = useState(null);
const [toolStatus, setToolStatus] = useState(false)
const [nowRandom, setNowRandom] = useState(null)
useEffect(() => {
if (titleInfo && Object.entries(titleInfo).length > 0) {
setInitValues({ ...titleInfo });
setImgUrl(titleInfo.bgImgUrl);
form.setFieldsValue({ ...titleInfo });
setTextColor(titleInfo.textColor);
setBgColorValue(titleInfo.bgColor);
setNowRandom(titleInfo.random);
setTitleInfo({});
setInitValues({ ...titleInfo })
setImgUrl(titleInfo.bgImgUrl)
form.setFieldsValue({ ...titleInfo })
setTextColor(titleInfo.textColor)
setBgColorValue(titleInfo.bgColor)
setNowRandom(titleInfo.random)
setTitleInfo({})
}
}, [titleInfo]);
}, [titleInfo])
useImperativeHandle(ref, () => {
return {
form,
setInitValues,
};
});
setInitValues
}
})
const textColorChange = (value, hex) => {
setTextColor(hex);
form.setFieldsValue({ textColor: hex });
};
setTextColor(hex)
form.setFieldsValue({ textColor: hex })
}
const bgColorChange = (value, hex) => {
setBgColorValue(hex);
form.setFieldsValue({ bgColor: hex });
};
setBgColorValue(hex)
form.setFieldsValue({ bgColor: hex })
}
const callback = ({ title, random, bgImgUrl, textColor, align, height, size, family }) => {
form.setFieldsValue({
......@@ -90,116 +74,116 @@ const ImageModal = forwardRef((props, ref) => {
align: align,
height: height,
size: size,
family: family,
});
setImgUrl(bgImgUrl);
setToolStatus(true);
setNowRandom(random);
};
family: family
})
setImgUrl(bgImgUrl)
setToolStatus(true)
setNowRandom(random)
}
const normFile = (e) => {
const normFile = e => {
if (Array.isArray(e)) {
return e;
return e
}
return e?.fileList
}
return e?.fileList;
};
const uploadProps = {
name: 'file',
maxCount: 1,
showUploadList: false,
accept: fileAccept.join(','),
beforeUpload: (file, fileList) => {
const fileExt = file.name.substring(file.name.lastIndexOf('.'));
const fileExt = file.name.substring(file.name.lastIndexOf('.'))
if (!fileAccept.includes(fileExt.toLowerCase())) {
message.error('请上传正确格式的图片');
return false;
message.error('请上传正确格式的图片')
return false
}
setFile(file);
setFileList(fileList);
setFile(file)
setFileList(fileList)
},
customRequest: async () => {
setUploading(true);
let data = null;
setUploading(true)
let data = null
if (file.size >= partSize) {
data = await multipartUploader(file, 'image', ossClient, (progress, checkpoint) => {
console.log(`上传进度 ${progress}`);
setProgress(parseInt(progress * 100));
});
console.log('multipartUploader --> ', data);
console.log(`上传进度 ${progress}`)
setProgress(parseInt(progress * 100))
})
console.log('multipartUploader --> ', data)
} else {
data = await normalUploader(file, 'image', ossClient);
console.log('normalUploader --> ', data);
data = await normalUploader(file, 'image', ossClient)
console.log('normalUploader --> ', data)
}
if (data.status === 200 && data.statusCode === 200) {
const { url, name } = data;
setImgUrl(url);
form.setFieldsValue({ bgImgUrl: url });
const { url, name } = data
setImgUrl(url)
form.setFieldsValue({ bgImgUrl: url })
}
setUploading(false)
}
}
setUploading(false);
},
};
const onFinish = (values) => {
editor.restoreSelection();
const onFinish = values => {
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
return true; // 匹配 paragraph
return true // 匹配 paragraph
}
}
return false;
return false
},
universal: true,
});
universal: true
})
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
if (node.children[0].text === '') {
SlateTransforms.removeNodes(editor);
SlateTransforms.removeNodes(editor)
}
}
if (nowRandom) {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterHeader') {
return true; // 匹配 chapterHeader
return true // 匹配 chapterHeader
}
}
return false;
return false
},
universal: true,
});
universal: true
})
if (nodeEntries) {
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
if (parseInt(node.random) === parseInt(nowRandom)) {
SlateTransforms.setNodes(editor, { ...node, ...values }, { at: path });
setImgUrl('');
setProgress(0);
setUploading(false);
form.resetFields();
setTitleVisible(false);
return false;
SlateTransforms.setNodes(editor, { ...node, ...values }, { at: path })
setImgUrl('')
setProgress(0)
setUploading(false)
form.resetFields()
setTitleVisible(false)
return false
}
}
}
return false;
return false
}
const html = editor.getHtml();
const parser = new DOMParser();
let random = Math.random().toString(10).substring(2, 10);
const html = editor.getHtml()
const parser = new DOMParser()
let random = Math.random().toString(10).substring(2, 10)
if (nowRandom) {
random = nowRandom;
random = nowRandom
}
const docBody = parser.parseFromString(html, 'text/html');
const section = docBody.body.querySelectorAll('.chapter-item-header'); // 是否存在章头
const docBody = parser.parseFromString(html, 'text/html')
const section = docBody.body.querySelectorAll('.chapter-item-header') // 是否存在章头
editor.insertNode({
type: 'chapterHeader',
......@@ -213,73 +197,60 @@ const ImageModal = forwardRef((props, ref) => {
size: values.size ? values.size : 26,
family: values.family,
callback: callback,
children: [{ text: '' }],
});
setImgUrl('');
setProgress(0);
setUploading(false);
form.resetFields();
setTitleVisible(false);
};
children: [{ text: '' }]
})
setImgUrl('')
setProgress(0)
setUploading(false)
form.resetFields()
setTitleVisible(false)
}
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initValues}
>
<Form.Item
label='章头背景图片'
valuePropName='fileList'
getValueFromEvent={normFile}
name='bgImgUrl'
>
<div className="editor-content-form">
<Form layout="vertical" name="validate_other" form={form} onFinish={onFinish} initialValues={initValues}>
<Form.Item label="章头背景图片" valuePropName="fileList" getValueFromEvent={normFile} name="bgImgUrl">
<Spin spinning={uploading} tip={`${progress}%`}>
<div className='editor-dragger'>
<div className="editor-dragger">
<Dragger {...uploadProps} showUploadList={false}>
{!imgUrl && (
<div className='editor-uploader-process'>
<p className='ant-upload-drag-icon'>
<div className="editor-uploader-process">
<p className="ant-upload-drag-icon">
<CloudUploadOutlined style={{ fontSize: 40 }} />
</p>
<p className='ant-upload-text'>点击上传或拖拽到此处上传</p>
<p className='ant-upload-hint'>支持上传 .png、.jpg、.jpeg、.svg格式的图片</p>
<p className="ant-upload-text">点击上传或拖拽到此处上传</p>
<p className="ant-upload-hint">支持上传 .png、.jpg、.jpeg、.svg格式的图片</p>
</div>
)}
{imgUrl && (
<>
<div className='editor-uploader-result'>
<div className='editor-uploader-result-img'>
<img src={imgUrl} alt='' />
<div className="editor-uploader-result">
<div className="editor-uploader-result-img">
<img src={imgUrl} alt="" />
</div>
<div className='editor-uploader-result-tips'>
<div className="editor-uploader-result-tips">
<Space>
<Button
size='small'
type='primary'
size="small"
type="primary"
ghost
onClick={() => {
setImgUrl(null);
form.setFieldsValue({ bgImgUrl: '' });
}}
>
setImgUrl(null)
form.setFieldsValue({ bgImgUrl: '' })
}}>
替换
</Button>
<Button
size='small'
type='default'
onClick={(ev) => {
ev.stopPropagation();
ev.preventDefault();
setImgUrl(null);
form.setFieldsValue({ bgImgUrl: '' });
}}
>
size="small"
type="default"
onClick={ev => {
ev.stopPropagation()
ev.preventDefault()
setImgUrl(null)
form.setFieldsValue({ bgImgUrl: '' })
}}>
移除
</Button>
</Space>
......@@ -291,119 +262,87 @@ const ImageModal = forwardRef((props, ref) => {
</div>
</Spin>
</Form.Item>
<Form.Item
label='标题'
name='title'
rules={[{ required: true, message: '请输入标题' }]}
extra='最多输入30字'
>
<Input maxLength={30} placeholder='' allowClear />
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]} extra="最多输入30字">
<Input maxLength={30} placeholder="" allowClear />
</Form.Item>
<Form.Item>
<Row gutter={20}>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='章头背景色'
name='bgColor'
className='flex-max'
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}
>
<Input placeholder='' allowClear />
label="章头背景色"
name="bgColor"
className="flex-max"
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={bgColorValue}
defaultValue={bgColorValue}
format='hex'
onChange={bgColorChange}
/>
<ColorPicker disabledAlpha value={bgColorValue} defaultValue={bgColorValue} format="hex" onChange={bgColorChange} />
</Form.Item>
</div>
</Col>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='文本颜色'
name='textColor'
className='flex-max'
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}
>
<Input maxLength={100} placeholder='' allowClear />
label="文本颜色"
name="textColor"
className="flex-max"
rules={[{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }]}>
<Input maxLength={100} placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
value={textColor}
defaultValue={textColor}
disabledAlpha
format='hex'
onChange={textColorChange}
/>
<ColorPicker value={textColor} defaultValue={textColor} disabledAlpha format="hex" onChange={textColorChange} />
</Form.Item>
</div>
</Col>
<Col span={12}>
<Form.Item label='字体' name='family'>
<Form.Item label="字体" name="family">
<Select>
{fontFamilyList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label='文字大小'
name='size'
rules={[{ required: true, message: '请输入文字大小' }]}
>
<Form.Item label="文字大小" name="size" rules={[{ required: true, message: '请输入文字大小' }]}>
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label='对齐方式'
name='align'
rules={[{ required: true, message: '请选择对齐方式' }]}
>
<Form.Item label="对齐方式" name="align" rules={[{ required: true, message: '请选择对齐方式' }]}>
<Select>
<Select.Option value='left'>左对齐</Select.Option>
<Select.Option value='center'>居中对齐</Select.Option>
<Select.Option value='right'>右对齐</Select.Option>
<Select.Option value="left">左对齐</Select.Option>
<Select.Option value="center">居中对齐</Select.Option>
<Select.Option value="right">右对齐</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='高度' name='height'>
<InputNumber
controls={false}
placeholder=''
style={{ width: '100%', textAlign: 'left' }}
allowClear
/>
<Form.Item label="高度" name="height">
<InputNumber controls={false} placeholder="" style={{ width: '100%', textAlign: 'left' }} allowClear />
</Form.Item>
</Col>
</Row>
</Form.Item>
<Form.Item className='editor-form-buttons'>
<Form.Item className="editor-form-buttons">
<Space>
<Button type='default' disabled={uploading} onClick={() => setTitleVisible(false)}>
<Button type="default" disabled={uploading} onClick={() => setTitleVisible(false)}>
取消
</Button>
<Button type='primary' disabled={uploading} htmlType='submit'>
<Button type="primary" disabled={uploading} htmlType="submit">
{nowRandom ? '更新' : '插入'}
</Button>
</Space>
......@@ -411,7 +350,7 @@ const ImageModal = forwardRef((props, ref) => {
</Form>
</div>
</div>
);
});
)
})
export default ImageModal;
export default ImageModal
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import {
Modal,
Divider,
ColorPicker,
Input,
Space,
Button,
Form,
Spin,
message,
Select,
Row,
Col,
} from 'antd';
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Modal, Divider, ColorPicker, Input, Space, Button, Form, Spin, message, Select, Row, Col } from 'antd'
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor'
import { fontFizeList } from '../utils/setting';
import EditorSimple from '@/common/editor-simple';
import './index.less';
import { addExpandRead, expandReadInfo } from '../utils/request';
import { fontSizeList } from '../utils/setting'
import EditorSimple from '@/common/editor-simple'
import './index.less'
import { addExpandRead, expandReadInfo } from '../utils/request'
const ExpandModal = forwardRef((props, ref) => {
const {
editor,
ossClient,
bookId,
chapterId,
setExpandVisible,
setExpandInfo,
expandInfo,
selectionSize = 18,
} = props;
const { editor, ossClient, bookId, chapterId, setExpandVisible, setExpandInfo, expandInfo, selectionSize = 18 } = props
const [form] = Form.useForm();
const flexValue = Form.useWatch('flex', form);
const [loadLoading, setLoadLoading] = useState(false);
const [content, setContent] = useState('');
const [themeValue, setThemeValue] = useState('#ab1941');
const [oldFlex, setOldFlex] = useState(null);
const [form] = Form.useForm()
const flexValue = Form.useWatch('flex', form)
const [loadLoading, setLoadLoading] = useState(false)
const [content, setContent] = useState('')
const [themeValue, setThemeValue] = useState('#ab1941')
const [oldFlex, setOldFlex] = useState(null)
const [initvalues, setInitValue] = useState({
flex: 1,
name: '',
title: '',
content: '',
theme: '#ab1941',
fontSize: selectionSize || 18,
});
const [nowRandom, setNowRandom] = useState(null);
fontSize: selectionSize || 18
})
const [nowRandom, setNowRandom] = useState(null)
// const [fontsize, setFontsize] = useState(selectionSize);
const getExpandInfo = async (random) => {
setLoadLoading(true);
const getExpandInfo = async random => {
setLoadLoading(true)
const data = await expandReadInfo({
book_id: bookId,
chapter_id: chapterId,
position: nowRandom || random,
});
position: nowRandom || random
})
if (data) {
setContent(data.content);
setContent(data.content)
// setInitValue({ ...data });
// form.setFieldsValue({ ...data });
}
setLoadLoading(false);
};
setLoadLoading(false)
}
const textColorChange = (value, hex) => {
setThemeValue(hex);
form.setFieldsValue({ theme: hex });
};
setThemeValue(hex)
form.setFieldsValue({ theme: hex })
}
useEffect(() => {
if (Object.entries(expandInfo).length > 0) {
(async () => {
setLoadLoading(true);
let obj = { ...expandInfo, flex: parseInt(expandInfo.flex), fontSize: expandInfo.fontsize };
setThemeValue(expandInfo.theme);
setOldFlex(parseInt(expandInfo.flex));
setInitValue(obj);
form.setFieldsValue(obj);
setNowRandom(expandInfo.random);
await getExpandInfo(expandInfo.random);
setExpandInfo({});
;(async () => {
setLoadLoading(true)
let obj = { ...expandInfo, flex: parseInt(expandInfo.flex), fontSize: expandInfo.fontsize }
setThemeValue(expandInfo.theme)
setOldFlex(parseInt(expandInfo.flex))
setInitValue(obj)
form.setFieldsValue(obj)
setNowRandom(expandInfo.random)
await getExpandInfo(expandInfo.random)
setExpandInfo({})
// setFontsize(expandInfo.fontsize);
setLoadLoading(false);
})();
setLoadLoading(false)
})()
}
}, [expandInfo]);
}, [expandInfo])
const removeInlineElement = (editor, type) => {
const { selection } = editor;
if (!selection) return;
const { selection } = editor
if (!selection) return
const [inlineNode] = SlateEditor.nodes(editor, {
match: (node) => SlateElement.isElement(node) && node.type === type,
});
console.log('inlineNode', inlineNode);
if (!inlineNode) return;
const [parent] = SlateEditor.parent(editor, inlineNode[1]);
match: node => SlateElement.isElement(node) && node.type === type
})
console.log('inlineNode', inlineNode)
if (!inlineNode) return
const [parent] = SlateEditor.parent(editor, inlineNode[1])
if (!parent || SlateElement.isElement(parent)) {
Transforms.removeNodes(editor, { match: (node) => node.type === type });
Transforms.removeNodes(editor, { match: node => node.type === type })
} else {
Transforms.removeNodes(editor, { at: inlineNode[1] });
Transforms.removeNodes(editor, { at: inlineNode[1] })
}
}
};
const onFinish = async (values) => {
editor.restoreSelection();
const onFinish = async values => {
editor.restoreSelection()
if (parseInt(values.flex) === 1) {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
return true; // 匹配 paragraph
return true // 匹配 paragraph
}
}
return false;
return false
},
universal: true,
});
universal: true
})
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
if (node.children[0].text === '') {
SlateTransforms.removeNodes(editor);
SlateTransforms.removeNodes(editor)
}
}
}
setLoadLoading(true);
setLoadLoading(true)
const { flex, theme, fontSize, ...other } = values;
const { flex, theme, fontSize, ...other } = values
if (nowRandom) {
let props = {
flex: parseInt(values.flex),
......@@ -135,34 +113,34 @@ const ExpandModal = forwardRef((props, ref) => {
title: values.title,
random: nowRandom,
theme: values.theme,
fontsize: values.fontSize,
};
fontsize: values.fontSize
}
if (parseInt(oldFlex) !== flex) {
SlateTransforms.removeNodes(editor);
SlateTransforms.removeNodes(editor)
if (parseInt(values.flex) === 1) {
editor.insertNode({
type: 'chapterExpandRead',
...props,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
} else {
editor.insertNode({
type: 'chapterExpandReadSimple',
...props,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
}
} else {
if (parseInt(flex) === 2) {
editor.move(1);
editor.deleteBackward();
editor.move(1)
editor.deleteBackward()
editor.insertNode({
type: 'chapterExpandReadSimple',
...props,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
// const nodeEntries = SlateEditor.nodes(editor, {
// match: (node) => {
......@@ -187,22 +165,22 @@ const ExpandModal = forwardRef((props, ref) => {
// }
} else {
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterExpandRead') {
return true; // 匹配 chapterToolTip
return true // 匹配 chapterToolTip
}
}
return false;
return false
},
universal: true,
});
universal: true
})
// 遍历匹配的节点迭代器,获取匹配的节点路径
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
const [node, path] = nodeEntry
if (node.random === nowRandom) {
SlateTransforms.setNodes(editor, props, { at: path });
SlateTransforms.setNodes(editor, props, { at: path })
}
}
}
......@@ -213,22 +191,22 @@ const ExpandModal = forwardRef((props, ref) => {
chapter_id: chapterId,
position: nowRandom,
type: flex,
...other,
});
setNowRandom(null);
setExpandVisible(false);
setLoadLoading(false);
return false;
...other
})
setNowRandom(null)
setExpandVisible(false)
setLoadLoading(false)
return false
}
let random = Math.random().toString(10).substring(2, 10);
let random = Math.random().toString(10).substring(2, 10)
const data = await addExpandRead({
book_id: bookId,
chapter_id: chapterId,
position: random,
type: flex,
...other,
});
...other
})
if (parseInt(values.flex) === 1) {
editor.insertNode({
......@@ -239,8 +217,8 @@ const ExpandModal = forwardRef((props, ref) => {
random: random,
theme: values.theme,
fontsize: values.fontSize || selectionSize,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
} else {
editor.insertNode({
type: 'chapterExpandReadSimple',
......@@ -250,85 +228,57 @@ const ExpandModal = forwardRef((props, ref) => {
random: random,
theme: values.theme,
fontsize: values.fontSize || selectionSize,
children: [{ text: '' }],
});
children: [{ text: '' }]
})
}
setNowRandom(null);
setExpandVisible(false);
setLoadLoading(false);
};
setNowRandom(null)
setExpandVisible(false)
setLoadLoading(false)
}
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initvalues}
>
<Form.Item
label='扩展类型'
name='flex'
rules={[{ required: true, message: '请选择扩展类型' }]}
>
<div className="editor-content-form">
<Form layout="vertical" name="validate_other" form={form} onFinish={onFinish} initialValues={initvalues}>
<Form.Item label="扩展类型" name="flex" rules={[{ required: true, message: '请选择扩展类型' }]}>
<Select disabled={oldFlex > 0 ? true : false}>
<Select.Option value={1}>独立扩展</Select.Option>
<Select.Option value={2}>行内扩展</Select.Option>
</Select>
</Form.Item>
<Form.Item
label='扩展名称'
name='name'
rules={[{ required: true, message: '请输入扩展名称' }]}
extra='最多输入100字'
>
<Input maxLength={100} placeholder='' allowClear />
<Form.Item label="扩展名称" name="name" rules={[{ required: true, message: '请输入扩展名称' }]} extra="最多输入100字">
<Input maxLength={100} placeholder="" allowClear />
</Form.Item>
{flexValue === 1 && (
<Form.Item
label='扩展标题'
name='title'
label="扩展标题"
name="title"
rules={[
{ required: flexValue === 1 ? true : false, message: '请输入扩展标题' },
{ max: 100, message: '最多输入100个字符!' },
{ max: 100, message: '最多输入100个字符!' }
]}
extra='最多输入100字'
>
<Input.TextArea
maxLength={100}
autoSize={{ minRows: 2, maxRows: 3 }}
placeholder=''
allowClear
/>
extra="最多输入100字">
<Input.TextArea maxLength={100} autoSize={{ minRows: 2, maxRows: 3 }} placeholder="" allowClear />
</Form.Item>
)}
{flexValue === 1 ? (
<Form.Item>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='扩展主题色'
name='theme'
className='flex-max'
label="扩展主题色"
name="theme"
className="flex-max"
rules={[
{ required: true, message: '请选择颜色' },
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={themeValue}
defaultValue={themeValue}
format='hex'
onChange={textColorChange}
/>
<ColorPicker disabledAlpha value={themeValue} defaultValue={themeValue} format="hex" onChange={textColorChange} />
</Form.Item>
</div>
</Form.Item>
......@@ -336,59 +286,47 @@ const ExpandModal = forwardRef((props, ref) => {
<Form.Item>
<Row gutter={20}>
<Col span={12}>
<Form.Item label='字号' name='fontSize'>
<Form.Item label="字号" name="fontSize">
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='扩展主题色'
name='theme'
className='flex-max'
label="扩展主题色"
name="theme"
className="flex-max"
rules={[
{ required: true, message: '请选择颜色' },
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={themeValue}
defaultValue={themeValue}
format='hex'
onChange={textColorChange}
/>
<ColorPicker disabledAlpha value={themeValue} defaultValue={themeValue} format="hex" onChange={textColorChange} />
</Form.Item>
</div>
</Col>
</Row>
</Form.Item>
)}
<Form.Item label='扩展内容' name='content'>
<EditorSimple
form={form}
fieldName={'content'}
content={content}
loadLoading={loadLoading}
/>
<Form.Item label="扩展内容" name="content">
<EditorSimple form={form} fieldName={'content'} content={content} loadLoading={loadLoading} />
</Form.Item>
<Form.Item className='editor-form-buttons'>
<Form.Item className="editor-form-buttons">
<Space>
<Button type='default' onClick={() => setExpandVisible(false)}>
<Button type="default" onClick={() => setExpandVisible(false)}>
取消
</Button>
<Button type='primary' loadLoading={loadLoading} htmlType='submit'>
<Button type="primary" loadLoading={loadLoading} htmlType="submit">
{nowRandom ? '更新' : '插入'}
</Button>
</Space>
......@@ -396,7 +334,7 @@ const ExpandModal = forwardRef((props, ref) => {
</Form>
</div>
</div>
);
});
)
})
export default ExpandModal;
export default ExpandModal
......@@ -4,11 +4,11 @@ import './index.less'
import { addGallery } from '../utils/request'
import { SlateEditor, SlateTransforms, SlateElement } from '@wangeditor/editor'
import { findNodeWithParent, fontFizeList } from '../utils/setting'
import { findNodeWithParent, fontSizeList } from '../utils/setting'
import GalleryFormItem from './galleryItem'
const randomOne = Math.random().toString(16).substring(2, 10)
const GalleryModal = (props) => {
const GalleryModal = props => {
const { editor, ossClient, galleryInfo, bookId, chapterId, setGalleryVisible, setGalleryInfo, selectionSize = 18, isOnline = false } = props
const [form] = Form.useForm()
......@@ -57,7 +57,7 @@ const GalleryModal = (props) => {
const [activeKey, setActiveKey] = useState(initialItems[0].key)
const [items, setItems] = useState(initialItems)
const onChange = (newActiveKey) => {
const onChange = newActiveKey => {
setActiveKey(newActiveKey)
}
const add = () => {
......@@ -81,8 +81,8 @@ const GalleryModal = (props) => {
setItems(newPanes)
setActiveKey(newActiveKey)
}
const remove = async (targetKey) => {
const tempGallery = picList.filter((item) => item.key !== targetKey)
const remove = async targetKey => {
const tempGallery = picList.filter(item => item.key !== targetKey)
await setPicList(tempGallery)
console.log(tempGallery)
form.setFieldsValue({ gallery: tempGallery })
......@@ -94,7 +94,7 @@ const GalleryModal = (props) => {
lastIndex = i - 1
}
})
let newPanes = items.filter((item) => item.key !== targetKey)
let newPanes = items.filter(item => item.key !== targetKey)
if (newPanes.length && newActiveKey === targetKey) {
if (lastIndex >= 0) {
newActiveKey = newPanes[lastIndex].key
......@@ -184,7 +184,7 @@ const GalleryModal = (props) => {
}
}, [galleryInfo])
const onFinish = async (values) => {
const onFinish = async values => {
editor.restoreSelection()
// setLoading(true);
const { galleryTitle, flex, gallery, fontSize, theme = '' } = values
......@@ -193,7 +193,7 @@ const GalleryModal = (props) => {
if (parseInt(flex) !== 2) {
// 删除空白的p标签
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
......@@ -310,7 +310,7 @@ const GalleryModal = (props) => {
<Col span={12}>
<Form.Item label="字号" name="fontSize">
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
......
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import {
Modal,
Divider,
Upload,
Input,
Space,
Button,
Form,
Spin,
message,
Select,
Cascader,
ColorPicker,
Row,
Col
} from 'antd';
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor';
import './index.less';
import { addChapterTooltip, delChapterTooltip } from '../utils/request';
import { fontFizeList } from '../utils/setting';
import { findTreeElementByKey } from '@/utils/common';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Modal, Divider, Upload, Input, Space, Button, Form, Spin, message, Select, Cascader, ColorPicker, Row, Col } from 'antd'
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor'
import './index.less'
import { addChapterTooltip, delChapterTooltip } from '../utils/request'
import { fontSizeList } from '../utils/setting'
import { findTreeElementByKey } from '@/utils/common'
const LinkModal = forwardRef((props, ref) => {
const { editor, ossClient, bookId, chapterId, setLinkVisible, setLinkInfo, linkInfo, gData, selectionSize = 18 } =
props;
const { editor, ossClient, bookId, chapterId, setLinkVisible, setLinkInfo, linkInfo, gData, selectionSize = 18 } = props
const [form] = Form.useForm();
const linktype = Form.useWatch('linktype', form);
const [loading, setLoading] = useState(false);
const [themeValue, setThemeValue] = useState('#ab1941');
const [form] = Form.useForm()
const linktype = Form.useWatch('linktype', form)
const [loading, setLoading] = useState(false)
const [themeValue, setThemeValue] = useState('#ab1941')
const [initvalues, setInitValue] = useState({
linktype: 1,
title: '',
link: '',
theme: '#ab1941',
fontSize: selectionSize || 18
});
const [toolStatus, setToolStatus] = useState(false);
const [nowRandom, setNowRandom] = useState(null);
const [fontsize, setFontsize] = useState(selectionSize);
})
const [toolStatus, setToolStatus] = useState(false)
const [nowRandom, setNowRandom] = useState(null)
const [fontsize, setFontsize] = useState(selectionSize)
const linkChange = (value) => {
const linkChange = value => {
if (value === 2) {
form.setFieldsValue({ chapters : [] });
form.setFieldsValue({ chapters: [] })
}
}
useEffect(() => {
if (editor.getSelectionText()) {
setInitValue({...initvalues, title: editor.getSelectionText() });
form.setFieldsValue({ title: editor.getSelectionText() });
setInitValue({ ...initvalues, title: editor.getSelectionText() })
form.setFieldsValue({ title: editor.getSelectionText() })
}
}, [])
useEffect(() => {
if (Object.entries(linkInfo).length > 0) {
let chapters = [];
let chapters = []
try {
chapters = linkInfo.chapters.split(',').map((item) => parseInt(item));
chapters = linkInfo.chapters.split(',').map(item => parseInt(item))
} catch (e) {
chapters = [];
chapters = []
}
let obj = { ...linkInfo, linktype: parseInt(linkInfo.linktype), chapters, fontSize: parseInt(linkInfo.fontsize) };
setInitValue(obj);
setThemeValue(linkInfo.theme);
form.setFieldsValue(obj);
setNowRandom(linkInfo.random);
let obj = { ...linkInfo, linktype: parseInt(linkInfo.linktype), chapters, fontSize: parseInt(linkInfo.fontsize) }
setInitValue(obj)
setThemeValue(linkInfo.theme)
form.setFieldsValue(obj)
setNowRandom(linkInfo.random)
// setFontsize(linkInfo.fontsize);
setLinkInfo({});
setLinkInfo({})
}
}, [linkInfo]);
}, [linkInfo])
const textColorChange = (value, hex) => {
setThemeValue(hex);
form.setFieldsValue({ theme: hex });
};
setThemeValue(hex)
form.setFieldsValue({ theme: hex })
}
const onFinish = async (values) => {
editor.restoreSelection();
const onFinish = async values => {
editor.restoreSelection()
let child = {};
let child = {}
if (values.linktype === 2) {
const last = values.chapters[values.chapters.length - 1];
child = findTreeElementByKey(gData, 'key', last);
const last = values.chapters[values.chapters.length - 1]
child = findTreeElementByKey(gData, 'key', last)
if (child.children && child.children.length) {
form.setFields([{ chapters: { errors: ['请选择正确的子节!'] } }]);
return false;
form.setFields([{ chapters: { errors: ['请选择正确的子节!'] } }])
return false
}
}
setLoading(true);
const { linktype, link, ...other } = values;
let newLink = '';
setLoading(true)
const { linktype, link, ...other } = values
let newLink = ''
if (link) {
if (!/^https*:\/\//.test(link)) {
newLink = `http://${link}`;
newLink = `http://${link}`
} else {
newLink = link;
newLink = link
}
}
......@@ -108,40 +92,40 @@ const LinkModal = forwardRef((props, ref) => {
random: nowRandom,
title: parseInt(values.linktype) === 1 ? values.title : child.title,
chapters: values.chapters instanceof Array ? values.chapters.join(',') : '',
fontsize: values.fontSize,
};
fontsize: values.fontSize
}
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterLink') {
return true;
return true
}
}
return false;
return false
},
universal: true,
});
universal: true
})
// 遍历匹配的节点迭代器,获取匹配的节点路径
if (nodeEntries === null) {
console.log('当前未选中的 chapterLink');
console.log('当前未选中的 chapterLink')
} else {
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
console.log('linkedit', node);
const [node, path] = nodeEntry
console.log('linkedit', node)
if (node.random === nowRandom) {
SlateTransforms.setNodes(editor, props, { at: path });
SlateTransforms.setNodes(editor, props, { at: path })
}
}
}
message.success('更新成功!');
setLinkVisible(false);
setNowRandom(null);
setLoading(false);
return false;
message.success('更新成功!')
setLinkVisible(false)
setNowRandom(null)
setLoading(false)
return false
}
let random = Math.random().toString(10).substring(2, 10);
let random = Math.random().toString(10).substring(2, 10)
editor.insertNode({
type: 'chapterLink',
title: parseInt(values.linktype) === 1 ? values.title : child.title,
......@@ -151,71 +135,61 @@ const LinkModal = forwardRef((props, ref) => {
chapters: values.chapters instanceof Array ? values.chapters.join(',') : '',
random,
fontsize: values.fontSize,
children: [{ text: '' }],
});
message.success('添加成功!');
setNowRandom(null);
setLinkVisible(false);
setLoading(false);
};
children: [{ text: '' }]
})
message.success('添加成功!')
setNowRandom(null)
setLinkVisible(false)
setLoading(false)
}
const deleteNowTooltip = async () => {
const data = await delChapterTooltip({
book_id: bookId,
chapter_id: chapterId,
position: nowRandom,
});
position: nowRandom
})
if (data) {
editor.restoreSelection();
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterLink') {
return true; // 匹配 chapterToolTip
return true // 匹配 chapterToolTip
}
}
return false;
return false
},
universal: true,
});
universal: true
})
// 遍历匹配的节点迭代器,获取匹配的节点路径
let nodePaths = [];
let nodes = [];
let nodePaths = []
let nodes = []
if (nodeEntries === null) {
console.log('当前未选中的 chapterLink');
console.log('当前未选中的 chapterLink')
} else {
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
nodePaths.push(path);
nodes.push(node);
const [node, path] = nodeEntry
nodePaths.push(path)
nodes.push(node)
}
// 将属性应用到匹配的节点
for (const path of nodePaths) {
SlateTransforms.removeNodes(editor, { at: path });
SlateTransforms.removeNodes(editor, { at: path })
}
}
setNowRandom(null);
setLinkVisible(false);
setNowRandom(null)
setLinkVisible(false)
}
}
};
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initvalues}
>
<Form.Item
label='链接类型'
name='linktype'
rules={[{ required: true, message: '请选择链接类型' }]}
>
<div className="editor-content-form">
<Form layout="vertical" name="validate_other" form={form} onFinish={onFinish} initialValues={initvalues}>
<Form.Item label="链接类型" name="linktype" rules={[{ required: true, message: '请选择链接类型' }]}>
<Select onChange={linkChange}>
<Select.Option value={1}>普通链接</Select.Option>
<Select.Option value={2}>章节链接</Select.Option>
......@@ -223,40 +197,25 @@ const LinkModal = forwardRef((props, ref) => {
</Form.Item>
{linktype === 1 && (
<>
<Form.Item
label='链接标题'
name='title'
rules={[{ required: true, message: '请输入链接标题' }]}
extra='最多输入100字'
>
<Input maxLength={100} placeholder='' allowClear />
<Form.Item label="链接标题" name="title" rules={[{ required: true, message: '请输入链接标题' }]} extra="最多输入100字">
<Input maxLength={100} placeholder="" allowClear />
</Form.Item>
<Form.Item
label='链接'
name='link'
label="链接"
name="link"
rules={[
{ required: true, message: '请输入链接地址' },
{ pattern: /^https*:\/\//gi, message: '链接地址需要http(s)://开头' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^https*:\/\//gi, message: '链接地址需要http(s)://开头' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
</>
)}
{linktype === 2 && (
<>
<Form.Item
label='子节'
name='chapters'
rules={[{ required: true, message: '请选择子节' }]}
>
<Cascader
options={gData}
fieldNames={{ label: 'title', value: 'key', children: 'children' }}
placeholder='请选择子节'
allowClear
/>
<Form.Item label="子节" name="chapters" rules={[{ required: true, message: '请选择子节' }]}>
<Cascader options={gData} fieldNames={{ label: 'title', value: 'key', children: 'children' }} placeholder="请选择子节" allowClear />
</Form.Item>
</>
)}
......@@ -264,52 +223,45 @@ const LinkModal = forwardRef((props, ref) => {
<Form.Item>
<Row gutter={20}>
<Col span={12}>
<Form.Item label='字号' name='fontSize'>
<Form.Item label="字号" name="fontSize">
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='扩展主题色'
name='theme'
className='flex-max'
label="扩展主题色"
name="theme"
className="flex-max"
rules={[
{ required: true, message: '请选择颜色' },
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={themeValue}
defaultValue={themeValue}
format='hex'
onChange={textColorChange}
/>
<ColorPicker disabledAlpha value={themeValue} defaultValue={themeValue} format="hex" onChange={textColorChange} />
</Form.Item>
</div>
</Col>
</Row>
</Form.Item>
<Form.Item className='editor-form-buttons'>
<Form.Item className="editor-form-buttons">
<Space>
{toolStatus && (
<Button type='default' onClick={deleteNowTooltip}>
<Button type="default" onClick={deleteNowTooltip}>
删除
</Button>
)}
<Button type='primary' loading={loading} htmlType='submit'>
<Button type="primary" loading={loading} htmlType="submit">
{nowRandom ? '更新' : '插入'}
</Button>
</Space>
......@@ -317,7 +269,7 @@ const LinkModal = forwardRef((props, ref) => {
</Form>
</div>
</div>
);
});
)
})
export default LinkModal;
export default LinkModal
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import {
Modal,
Divider,
Upload,
Input,
Space,
Button,
Form,
Spin,
message,
Select,
ColorPicker,
Row,
Col,
} from 'antd';
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor';
import './index.less';
import { addChapterTooltip, delChapterTooltip } from '../utils/request';
import { findNodeWithParent, fontFizeList } from '../utils/setting';
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import { Modal, Divider, Upload, Input, Space, Button, Form, Spin, message, Select, ColorPicker, Row, Col } from 'antd'
import { SlateTransforms, SlateEditor, DomEditor, SlateElement } from '@wangeditor/editor'
import './index.less'
import { addChapterTooltip, delChapterTooltip } from '../utils/request'
import { findNodeWithParent, fontSizeList } from '../utils/setting'
const TooltipModal = forwardRef((props, ref) => {
const { editor, ossClient, bookId, chapterId, setTooltipVisible, setTooltipInfo, tooltipInfo, selectionSize = 18 } =
props;
const { editor, ossClient, bookId, chapterId, setTooltipVisible, setTooltipInfo, tooltipInfo, selectionSize = 18 } = props
const [form] = Form.useForm();
const tooltipTypeValue = Form.useWatch('tooltipType', form);
const [loading, setLoading] = useState(false);
const [themeValue, setThemeValue] = useState('#ab1941');
const [form] = Form.useForm()
const tooltipTypeValue = Form.useWatch('tooltipType', form)
const [loading, setLoading] = useState(false)
const [themeValue, setThemeValue] = useState('#ab1941')
const [initvalues, setInitValue] = useState({
tooltipType: 1,
title: '',
......@@ -34,38 +19,38 @@ const TooltipModal = forwardRef((props, ref) => {
content: '',
theme: '#ab1941',
fontSize: selectionSize || 18
});
const [toolStatus, setToolStatus] = useState(false);
const [nowRandom, setNowRandom] = useState(null);
const [fontsize, setFontsize] = useState(selectionSize);
})
const [toolStatus, setToolStatus] = useState(false)
const [nowRandom, setNowRandom] = useState(null)
const [fontsize, setFontsize] = useState(selectionSize)
useEffect(() => {
if (Object.entries(tooltipInfo).length > 0) {
let obj = { ...tooltipInfo, tooltipType: parseInt(tooltipInfo.tooltipType), fontSize: parseInt(tooltipInfo.fontsize) };
setInitValue(obj);
form.setFieldsValue(obj);
setThemeValue(tooltipInfo.theme);
setNowRandom(tooltipInfo.random);
setFontsize(parseInt(tooltipInfo.fontsize));
setTooltipInfo({});
let obj = { ...tooltipInfo, tooltipType: parseInt(tooltipInfo.tooltipType), fontSize: parseInt(tooltipInfo.fontsize) }
setInitValue(obj)
form.setFieldsValue(obj)
setThemeValue(tooltipInfo.theme)
setNowRandom(tooltipInfo.random)
setFontsize(parseInt(tooltipInfo.fontsize))
setTooltipInfo({})
}
}, [tooltipInfo]);
}, [tooltipInfo])
const textColorChange = (value, hex) => {
setThemeValue(hex);
form.setFieldsValue({ theme: hex });
};
setThemeValue(hex)
form.setFieldsValue({ theme: hex })
}
const onFinish = async (values) => {
editor.restoreSelection();
setLoading(true);
const { tooltipType, link, theme, fontSize, ...other } = values;
let newLink = '';
const onFinish = async values => {
editor.restoreSelection()
setLoading(true)
const { tooltipType, link, theme, fontSize, ...other } = values
let newLink = ''
if (link) {
if (!/^https*:\/\//.test(link)) {
newLink = `http://${link}`;
newLink = `http://${link}`
} else {
newLink = link;
newLink = link
}
}
......@@ -76,33 +61,33 @@ const TooltipModal = forwardRef((props, ref) => {
position: nowRandom,
link: newLink,
...other,
type: tooltipType,
});
type: tooltipType
})
if (data) {
const props = { ...values, link: newLink, random: nowRandom, theme, fontsize: values.fontSize || selectionSize || fontsize };
const props = { ...values, link: newLink, random: nowRandom, theme, fontsize: values.fontSize || selectionSize || fontsize }
const aPath = findNodeWithParent(editor.children, 'chapterTooltip', 'random', nowRandom);
SlateTransforms.setNodes(editor, props, { at: aPath.reverse() });
message.success('更新成功!');
setTooltipVisible(false);
setNowRandom(null);
const aPath = findNodeWithParent(editor.children, 'chapterTooltip', 'random', nowRandom)
SlateTransforms.setNodes(editor, props, { at: aPath.reverse() })
message.success('更新成功!')
setTooltipVisible(false)
setNowRandom(null)
}
setLoading(false);
return;
setLoading(false)
return
}
const text = editor.getSelectionText();
let random = Math.random().toString(10).substring(2, 10);
const text = editor.getSelectionText()
let random = Math.random().toString(10).substring(2, 10)
const data = await addChapterTooltip({
book_id: bookId,
chapter_id: chapterId,
position: random,
link: newLink,
...other,
type: tooltipType,
});
type: tooltipType
})
if (data) {
editor.insertNode({
......@@ -114,72 +99,62 @@ const TooltipModal = forwardRef((props, ref) => {
random,
theme,
fontsize: values.fontSize || selectionSize || fontsize,
children: [{ text: '' }],
});
message.success('添加成功!');
children: [{ text: '' }]
})
message.success('添加成功!')
}
setNowRandom(null)
setTooltipVisible(false)
setLoading(false)
}
setNowRandom(null);
setTooltipVisible(false);
setLoading(false);
};
const deleteNowTooltip = async () => {
const data = await delChapterTooltip({
book_id: bookId,
chapter_id: chapterId,
position: nowRandom,
});
position: nowRandom
})
if (data) {
editor.restoreSelection();
editor.restoreSelection()
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'chapterTooltip') {
return true; // 匹配 chapterToolTip
return true // 匹配 chapterToolTip
}
}
return false;
return false
},
universal: true,
});
universal: true
})
// 遍历匹配的节点迭代器,获取匹配的节点路径
let nodePaths = [];
let nodes = [];
let nodePaths = []
let nodes = []
if (nodeEntries === null) {
console.log('当前未选中的 chapterTooltip');
console.log('当前未选中的 chapterTooltip')
} else {
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
nodePaths.push(path);
nodes.push(node);
const [node, path] = nodeEntry
nodePaths.push(path)
nodes.push(node)
}
// 将属性应用到匹配的节点
for (const path of nodePaths) {
SlateTransforms.removeNodes(editor, { at: path });
SlateTransforms.removeNodes(editor, { at: path })
}
}
setNowRandom(null)
setTooltipVisible(false)
}
setNowRandom(null);
setTooltipVisible(false);
}
};
return (
<div>
<Divider />
<div className='editor-content-form'>
<Form
layout='vertical'
name='validate_other'
form={form}
onFinish={onFinish}
initialValues={initvalues}
>
<Form.Item
label='气泡类型'
name='tooltipType'
rules={[{ required: true, message: '请选择气泡类型' }]}
>
<div className="editor-content-form">
<Form layout="vertical" name="validate_other" form={form} onFinish={onFinish} initialValues={initvalues}>
<Form.Item label="气泡类型" name="tooltipType" rules={[{ required: true, message: '请选择气泡类型' }]}>
<Select disabled={nowRandom ? true : false}>
<Select.Option value={1}>文字气泡</Select.Option>
<Select.Option value={2}>图标气泡</Select.Option>
......@@ -187,82 +162,67 @@ const TooltipModal = forwardRef((props, ref) => {
</Form.Item>
{tooltipTypeValue === 1 && (
<Form.Item
label='气泡标题'
name='title'
rules={[
{ required: tooltipTypeValue === 1 ? true : false, message: '请输入气泡标题' },
]}
extra='最多输入100字'
>
<Input maxLength={100} placeholder='' allowClear />
label="气泡标题"
name="title"
rules={[{ required: tooltipTypeValue === 1 ? true : false, message: '请输入气泡标题' }]}
extra="最多输入100字">
<Input maxLength={100} placeholder="" allowClear />
</Form.Item>
)}
<Form.Item
label='描述'
name='content'
rules={[{ required: true, message: '请输入描述内容' }]}
>
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} placeholder='' allowClear />
<Form.Item label="描述" name="content" rules={[{ required: true, message: '请输入描述内容' }]}>
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} placeholder="" allowClear />
</Form.Item>
<Form.Item>
<Row gutter={20}>
<Col span={12}>
<Form.Item label='字号' name='fontSize'>
<Form.Item label="字号" name="fontSize">
<Select>
{fontFizeList.map((item, index) => {
{fontSizeList.map((item, index) => {
return (
<Select.Option value={item.value} key={index}>
{item.name}
</Select.Option>
);
)
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<div className='justcontent-color-inline'>
<div className="justcontent-color-inline">
<Form.Item
label='气泡主题色'
name='theme'
className='flex-max'
label="气泡主题色"
name="theme"
className="flex-max"
rules={[
{ required: true, message: '请选择颜色' },
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^#[0-9A-Fa-f]{6}$/i, message: '请输入正确的16进制色值' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item label={` `}>
<ColorPicker
disabledAlpha
value={themeValue}
defaultValue={themeValue}
format='hex'
onChange={textColorChange}
/>
<ColorPicker disabledAlpha value={themeValue} defaultValue={themeValue} format="hex" onChange={textColorChange} />
</Form.Item>
</div>
</Col>
</Row>
</Form.Item>
<Form.Item
label='链接(非必须)'
name='link'
label="链接(非必须)"
name="link"
rules={[
{ required: false, message: '请输入气泡内容' },
{ pattern: /^https*:\/\//gi, message: '链接地址需要http(s)://开头' },
]}
>
<Input placeholder='' allowClear />
{ pattern: /^https*:\/\//gi, message: '链接地址需要http(s)://开头' }
]}>
<Input placeholder="" allowClear />
</Form.Item>
<Form.Item className='editor-form-buttons'>
<Form.Item className="editor-form-buttons">
<Space>
{toolStatus && (
<Button type='default' onClick={deleteNowTooltip}>
<Button type="default" onClick={deleteNowTooltip}>
删除
</Button>
)}
<Button type='primary' loading={loading} htmlType='submit'>
<Button type="primary" loading={loading} htmlType="submit">
{nowRandom ? '更新' : '插入'}
</Button>
</Space>
......@@ -270,7 +230,7 @@ const TooltipModal = forwardRef((props, ref) => {
</Form>
</div>
</div>
);
});
)
})
export default TooltipModal;
export default TooltipModal
import { DomEditor, SlateTransforms, SlateRange } from '@wangeditor/editor';
import { DomEditor, SlateTransforms, SlateRange } from '@wangeditor/editor'
class ChapterItem {
constructor() {
this.title = '章节'
this.title = '节头'
this.iconSvg = `<svg width="22px" height="23px" viewBox="0 0 22 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
......@@ -14,14 +14,14 @@ class ChapterItem {
</g>
</g>
</g>
</svg>`;
</svg>`
this.tag = 'button'
}
getValue(editor) {
return 'hello, 图片, , , 图片'
}
isActive(editor) {
return false;
return false
}
isDisabled(editor) {
const { selection } = editor
......@@ -35,7 +35,7 @@ class ChapterItem {
// eslint-disable-next-line array-callback-return
const hasPreElem = selectedElems.some(elem => {
const type = DomEditor.getNodeType(elem);
const type = DomEditor.getNodeType(elem)
// 代码块 引用 表格 禁用
if (type === 'pre' || type === 'blockquote' || type === 'table' || type === 'table-row' || type === 'table-cell') return true
})
......@@ -45,10 +45,10 @@ class ChapterItem {
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if(this.isDisabled(editor)){
return;
if (this.isDisabled(editor)) {
return
}
editor.emit('ChapterItemMenuClick');
editor.emit('ChapterItemMenuClick')
}
}
......@@ -56,9 +56,7 @@ export default {
key: 'ChapterItem', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new ChapterItem() // 把 `YourMenuClass` 替换为你菜单的 class
},
}
}
export {
ChapterItem
}
\ No newline at end of file
export { ChapterItem }
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import { Input, Spin, message, Button } from 'antd';
import { EyeOutlined } from '@ant-design/icons';
import AliOSS from 'ali-oss';
import dayjs from 'dayjs';
import { Boot } from '@wangeditor/editor';
import { useSelector } from 'react-redux';
import { SlateEditor, SlateNode, SlateElement, SlateText } from '@wangeditor/editor';
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import './node/index.less';
import timesave from '@/assets/images/timesave.png';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { storageChange } from '@/utils/storage.js';
import linkCardModule from '@wangeditor/plugin-link-card';
import PaddingSpace from './customer/padding';
import ImageAutoConf from './customer/Image';
import GalleryAutoConf from './customer/Gallery';
import VideoAutoConf from './customer/Video';
import AudioAutoConf from './customer/Audio';
import ChapterTitleAutoConf from './customer/ChapterTitle';
import ChapterItemAutoConf from './customer/ChapterItem';
import PracticeAutoConf from './customer/Practice';
import FormulaAutoConf from './customer/Formula';
import TooltipAutoConf from './customer/Tooltip';
import ImageModal from './components/image';
import VideoModal from './components/video';
import GalleryModal from './components/gallery';
import AudioModal from './components/audio';
import ChapterTitleModal from './components/chapter-title';
import ChapterItemModal from './components/chapter-item';
import PracticeModal from './components/practice';
import FormulaModal from './components/formula';
import TooltipModal from './components/tooltip';
import chapterSectionModule from './node/chapterItem';
import chapterHeaderModule from './node/chapterTitle';
import chapterImageModule from './node/image';
import chapterVideoModule from './node/video';
import chapterAudioModule from './node/audio';
import chapterGalleryModule from './node/gallery';
import chapterPracticeModule from './node/practice';
import formulaModule from './node/formula';
import tooltipModule from './node/tooltip';
import PracticeSettingModal from './practice/index';
import $ from 'jquery';
import { getInfoByChapterId } from '@/pages/books/section/request';
import { getAliOSSSTSToken } from './utils/request';
import './index.less';
const menuArr1 = ['字体样式', '字号'];
const menuArr2 = ['图片', '画廊', '视频', '音频'];
const menuArr3 = ['代码块', '链接', '引用', '公式', '章头', '章节', '交互练习', '气泡'];
const colorList = ['#ab1941', '#2970f6', '#2ad882', '#eb3351'];
const bookBucketName = 'zxts-book-file';
const module = {
menus: [
ImageAutoConf,
GalleryAutoConf,
VideoAutoConf,
AudioAutoConf,
FormulaAutoConf,
TooltipAutoConf,
ChapterTitleAutoConf,
ChapterItemAutoConf,
PracticeAutoConf,
],
};
Boot.registerModule(module);
// 注册节头内容
Boot.registerModule(chapterSectionModule);
// 注册章头内容
Boot.registerModule(chapterHeaderModule);
// 注册图片内容
Boot.registerModule(chapterImageModule);
// 注册视频内容
Boot.registerModule(chapterVideoModule);
// 注册音频内容
Boot.registerModule(chapterAudioModule);
// 注册画廊
Boot.registerModule(chapterGalleryModule);
// 注册公式
Boot.registerModule(formulaModule);
// 注册练习
Boot.registerModule(chapterPracticeModule);
// 注册气泡
Boot.registerModule(tooltipModule);
Boot.registerModule(linkCardModule);
const tabsMenu = [
{ key: 'text', title: '文本设置' },
{ key: 'style', title: '样式模版' },
];
storageChange();
const WangEditorCustomer = forwardRef((props, ref) => {
const { chapterId, bookId, contentId, html, setHtml, saveContent } = props;
// 自动保存时间
const { autosaveTime } = useSelector((state) => state.editor);
const toolbarRef = useRef();
const paddingSpaceRef = useRef();
const [cId, setCId] = useState(contentId);
const [ossClient, setOssClient] = useState(null); // oss 操作
const [STSToken, setSTSToken] = useState(null); // oss 过期设置
const [tabKey, setTabKey] = useState('text');
const [loading, setLoading] = useState(true);
// editor 实例
const [editor, setEditor] = useState(null);
const [editorNodes, setEditorNodes] = useState(null);
const [content, setContent] = useState(html);
const saveRef = useRef();
// 工具栏配置
const toolbarConfig = {
toolbarKeys: [
// '|',
// 'redo',
// 'undo',
// 'emotion',
// 'todo',
// 'fullScreen',
// '|',
// 'headerSelect',
// 'lineHeight',
// '|',
'fontFamily',
// 'uploadImage',
'fontSize',
'|',
'justifyLeft',
'justifyRight',
'justifyCenter',
'justifyJustify',
'divider',
'|',
'lineHeight',
'|',
'bold',
'italic',
'through',
'underline',
'sub',
'sup',
'color',
'bgColor',
'bulletedList',
'numberedList',
'indent',
'delIndent',
'|',
// 'insertImage',
// 'uploadImage',
// 'insertVideo',
// 'uploadVideo',
'insertTable',
'|',
'codeBlock', // 代码块
'insertLink', // 链接
// 'insertFormula', // 公式
'blockquote', // 引用
// 'code',
// 'clearStyle',
],
};
useImperativeHandle(ref, () => {
return {
editor,
};
});
const imageRef = useRef();
const galleryRef = useRef();
const videoRef = useRef();
const audioRef = useRef();
const formulaRef = useRef();
const tooltipRef = useRef();
const chapterTitleRef = useRef();
const chapterItemRef = useRef();
const practiceRef = useRef();
const practiceSettingRef = useRef();
toolbarConfig.insertKeys = {
index: 30,
keys: [
'ImageAuto',
'GalleryAuto',
'VideoAuto',
'AudioAuto',
'FormulaAuto',
'ChapterTitle',
'ChapterItem',
'Practice',
'TooltipAuto',
],
};
// 编辑器配置
const editorConfig = {
placeholder: '请输入内容...',
// 选中公式时的悬浮菜单
hoverbarKeys: {
// formula: {
// menuKeys: ['editFormula'], // “编辑公式”菜单
// },
// 在编辑器中,选中链接文本时,要弹出的菜单
link: {
menuKeys: [
'editLink',
'unLink',
'viewLink', // 默认的配置可以通过 `editor.getConfig().hoverbarKeys.link` 获取
'convertToLinkCard', // 增加 '转为链接卡片'菜单
],
},
MENU_CONF: {
// '转为链接卡片'菜单的配置
convertToLinkCard: {
// 自定义获取 link-card 信息,可选
// 返回 { title, iconImgSrc }
async getLinkCardInfo(linkText, linkUrl) {
// 1. 可通过 iframe 加载网页,然后获取网页 title 和其中的图片
// 2. 服务端获取(有些网页会设置 `X-Frame-Options` ,无法通过 iframe 加载)
// // 模拟异步返回
// return new Promise(resolve => {
// setTimeout(() => {
// const info = { title: linkText, iconImgSrc: '' }
// resolve(info)
// }, 100)
// })
},
},
// 其他...
},
},
};
// 编辑器按钮重排
const toolSetttingReplace = () => {
setTimeout(() => {
const toolbarElement = toolbarRef.current && toolbarRef.current.children[1].children[0];
const allChildren = toolbarElement.children;
const oH6_1 = document.createElement('h4');
oH6_1.setAttribute('class', 'w-auto type-heading');
oH6_1.innerHTML = '常用格式';
toolbarElement.insertBefore(oH6_1, allChildren[0]);
const oHDiv_3 = document.createElement('div');
oHDiv_3.setAttribute('class', 'custom-bar-box two');
toolbarElement.insertBefore(oHDiv_3, allChildren[1]);
$(allChildren[1]).append($(allChildren[2]).detach());
$(allChildren[1]).append($(allChildren[2]).detach());
const oH6_2 = document.createElement('h4');
oH6_2.setAttribute('class', 'w-auto type-heading');
oH6_2.innerHTML = '媒体资源';
toolbarElement.insertBefore(oH6_2, allChildren[22]);
const oHDiv_1 = document.createElement('div');
oHDiv_1.setAttribute('class', 'custom-bar-box');
toolbarElement.insertBefore(oHDiv_1, allChildren[23]);
const itemBox = '<div class="w-e-bar-boxitem"></div>';
$(itemBox).insertBefore($(allChildren[4]));
const itemBox2 = '<div class="w-e-bar-boxitem you"></div>';
$(itemBox2).insertBefore($(allChildren[10]));
$(allChildren[4]).append($(allChildren[3]).detach());
$(allChildren[3]).append($(allChildren[5]).detach());
$(allChildren[3]).append($(allChildren[5]).detach());
$(allChildren[3]).append($(allChildren[5]).detach());
$(allChildren[3]).append($(allChildren[4]).detach());
const oHDivInput_1 = document.createElement('div');
oHDivInput_1.setAttribute('class', 'custom-bar-box input');
oHDivInput_1.setAttribute('id', 'custom-bar-box-input');
toolbarElement.insertBefore(oHDivInput_1, allChildren[4]);
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[6]).append($(allChildren[7]).detach());
$(allChildren[9]).append($(allChildren[15]).detach());
$(allChildren[9]).append($(allChildren[15]).detach());
$(allChildren[9]).append($(allChildren[15]).detach());
$(allChildren[9]).append($(allChildren[15]).detach());
$(allChildren[9]).append($(allChildren[10]).detach());
const oH6_3 = document.createElement('h4');
oH6_3.setAttribute('class', 'w-auto type-heading');
oH6_3.innerHTML = '高级模块';
toolbarElement.insertBefore(oH6_3, allChildren[11]);
const oHDiv_2 = document.createElement('div');
oHDiv_2.setAttribute('class', 'custom-bar-box');
toolbarElement.insertBefore(oHDiv_2, allChildren[12]);
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$(allChildren[12]).append($(allChildren[13]).detach());
$('.custom-bar-box').each((index, item) => {
$(item)
.find('.w-e-bar-item')
.each((cIndex, cItem) => {
if (index === 2 && cIndex === 4) {
$(cItem).hide();
} else {
const oP = document.createElement('p');
if (index === 0) {
$(oP).insertBefore($(cItem).find('button'));
oP.innerHTML = menuArr1[cIndex];
} else if (index === 2) {
oP.innerHTML = menuArr2[cIndex];
$(cItem).append(oP);
} else if (index === 3) {
oP.innerHTML = menuArr3[cIndex];
$(cItem).append(oP);
}
}
});
});
setLoading(false);
}, 200);
};
editorConfig.onCreated = (editor) => {
setLoading(true);
toolSetttingReplace();
};
editorConfig.onFocus = (editor) => {
clearTimeout(saveRef.current);
};
editorConfig.onBlur = (editor) => {
// 失焦保存
// setHtml(editor.getHtml());
saveContent();
};
editorConfig.onChange = (editor) => {
// setHtml(editor.getHtml());
// clearTimeout(saveRef.current);
// saveRef.current = setTimeout(() => {
// saveContent();
// }, 800);
};
// 及时销毁 editor ,重要!
useEffect(() => {
if (editor) {
// 图片上传
editor.on('ImageMenuClick', () => {
console.log('ImageMenuClick', '----');
imageRef.current.setVisible(true);
});
// 画廊上传
editor.on('GalleryMenuClick', () => {
console.log('GalleryMenuClick', '----');
galleryRef.current.setVisible(true);
});
// 视频上传
editor.on('VideoMenuClick', () => {
console.log('VideoMenuClick', '----');
videoRef.current.setVisible(true);
});
// 音频上传
editor.on('AudioMenuClick', () => {
console.log('AudioMenuClick', '----');
audioRef.current.setVisible(true);
});
// 章节
editor.on('ChapterItemMenuClick', () => {
console.log('ChapterItemMenuClick', '----');
chapterItemRef.current.setVisible(true);
});
// 章头
editor.on('ChapterTitleClick', () => {
console.log('ChapterTitleClick', '----');
chapterTitleRef.current.setVisible(true);
});
// 交互练习
editor.on('PracticeMenuClick', () => {
console.log('PracticeMenuClick', '----');
practiceRef.current.setVisible(true);
});
// 公式
editor.on('FormulaMenuClick', () => {
console.log('FormulaMenuClick', '----');
formulaRef.current.setVisible(true);
});
// 气泡
editor.on('TooltipMenuClick', () => {
if (editor.getSelectionText()) {
const textSelection = editor.selection;
if (textSelection.anchor.path[0] === textSelection.focus.path[0]) {
tooltipRef.current.setVisible(true);
} else {
message.error('气泡操作不能跨段落进行!');
}
} else {
message.error('气泡操作需要选中内容!');
}
});
}
return () => {
if (editor === null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
useEffect(() => {
if (editor && html) {
setContent(html);
}
}, [html, editor]);
const tabKeyChange = (key) => {
if (key === 'text') {
toolSetttingReplace();
}
setTabKey(key);
};
const getStsAuthToken = async () => {
const data = await getAliOSSSTSToken();
if (data) {
window.sessionStorage.setItem('sts', JSON.stringify(data));
setSTSToken(data);
const ossClientTemp = await new AliOSS({
accessKeyId: data.AccessKeyId,
accessKeySecret: data.AccessKeySecret,
stsToken: data.SecurityToken,
endpoint: 'zijingebook.com',
// region: 'cn-beijing',
// endpoint: data.endpoint,
bucket: bookBucketName,
timeout: 180000,
refreshSTSToken: async () => {
const info = await getAliOSSSTSToken();
return {
AccessKeyId: info.AccessKeyId,
AccessKeySecret: info.AccessKeySecret,
SecurityToken: info.SecurityToken,
};
},
refreshSTSTokenInterval: 14 * 60 * 1000,
});
setOssClient(ossClientTemp);
}
};
useEffect(() => {
(async () => {
const tempStsToken = window.sessionStorage ? window.sessionStorage.getItem('sts') : '';
try {
const stsToken = JSON.parse(tempStsToken);
// 15 分钟过期
if (dayjs(stsToken.Expiration).valueOf() - dayjs().valueOf() >= 14 * 60 * 1000) {
getStsAuthToken();
} else {
const ossClientTemp = await new AliOSS({
accessKeyId: data.AccessKeyId,
accessKeySecret: data.AccessKeySecret,
stsToken: data.SecurityToken,
// endpoint: data.endpoint,
// region: 'cn-beijing',
endpoint: 'zijingebook.com',
bucket: bookBucketName,
timeout: 180000,
refreshSTSToken: async () => {
const info = await getAliOSSSTSToken();
return {
AccessKeyId: info.AccessKeyId,
AccessKeySecret: info.AccessKeySecret,
SecurityToken: info.SecurityToken,
};
},
refreshSTSTokenInterval: 14 * 60 * 1000,
});
setOssClient(ossClientTemp);
setSTSToken(stsToken);
}
} catch (e) {
getStsAuthToken();
}
})();
}, []);
const setColor = (type) => {
$(`#chapter-item-header`).css({ color: '#fff', backgroundColor: colorList[type - 1] });
$(`#chapter-item-section`).css({ color: '#fff', backgroundColor: colorList[type - 1] });
};
return (
<div className='wangeditor-customer-container'>
<div
style={{ height: 'calc(100vh - 250px)', overflowY: 'hidden' }}
className='editor-content-container'
>
<div className='title-head'>
<div className='right'>
<div className='save-time time-on'>
<Button
danger
type='link'
className='timepsave'
onClick={saveContent}
icon={<img src={timesave} />}
>
自动保存
</Button>
<span className='time'>{autosaveTime > 0 ? dayjs(autosaveTime).fromNow() : ''}</span>
</div>
<div className='save-time'>
<Button icon={<EyeOutlined />} className='view'>
预览
</Button>
<span className='time'>&nbsp;</span>
</div>
</div>
</div>
<Spin spinning={loading} className='editor-content-container-loading'>
<Editor
defaultConfig={editorConfig}
value={content}
onCreated={setEditor}
// onChange={(editor) => setHtml(editor.getHtml())}
mode='default'
style={{ height: 'calc(100vh - 250px)', overflowY: 'hidden' }}
/>
</Spin>
</div>
{/* <CustomerMenu editor={editor} /> */}
<div className='menu-tabs-key'>
<div className='tabs'>
{tabsMenu &&
tabsMenu.length &&
tabsMenu.map((item) => {
return (
<div
className={`tabs-item ${item.key === tabKey ? 'active' : ''}`}
key={item.key}
onClick={() => tabKeyChange(item.key)}
>
{item.title}
<span></span>
</div>
);
})}
</div>
<div className='menu-tabs-content'>
{tabKey === 'text' && (
<div ref={toolbarRef} className='toolbox-parent'>
<PaddingSpace ref={paddingSpaceRef} editor={editor} editorNodes={editorNodes} />
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode='default'
style={{ borderBottom: '1px solid #ccc' }}
className='editor-toolbar-container'
></Toolbar>
</div>
)}
{tabKey === 'style' && (
<div className='styletem'>
<p>样式模板</p>
<ul>
<li>
<div className='left'>
<span className='color color1'></span>
<b className='type'>默认</b>
</div>
<Button type='link' className='use' onClick={() => setColor(1)}>
使用
</Button>
</li>
<li>
<div className='left'>
<span className='color color2'></span>
<b className='type'>蓝色</b>
</div>
<Button type='link' className='use' onClick={() => setColor(2)}>
使用
</Button>
</li>
<li>
<div className='left'>
<span className='color color3'></span>
<b className='type'>绿色</b>
</div>
<Button type='link' className='use' onClick={() => setColor(3)}>
使用
</Button>
</li>
<li>
<div className='left'>
<span className='color color4'></span>
<b className='type'>红色</b>
</div>
<Button type='link' className='use' onClick={() => setColor(4)}>
使用
</Button>
</li>
</ul>
</div>
)}
</div>
</div>
<ImageModal ref={imageRef} editor={editor} ossClient={ossClient} STSToken={STSToken} />
<VideoModal ref={videoRef} editor={editor} ossClient={ossClient} STSToken={STSToken} />
<GalleryModal ref={galleryRef} editor={editor} ossClient={ossClient} STSToken={STSToken} />
<AudioModal ref={audioRef} editor={editor} ossClient={ossClient} STSToken={STSToken} />
<ChapterTitleModal
ref={chapterTitleRef}
editor={editor}
ossClient={ossClient}
STSToken={STSToken}
/>
<ChapterItemModal
ref={chapterItemRef}
editor={editor}
ossClient={ossClient}
STSToken={STSToken}
/>
<PracticeModal
ref={practiceRef}
chapterId={chapterId}
bookId={bookId}
editor={editor}
ossClient={ossClient}
STSToken={STSToken}
/>
<FormulaModal ref={formulaRef} editor={editor} ossClient={ossClient} STSToken={STSToken} />
<TooltipModal
ref={tooltipRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
ossClient={ossClient}
STSToken={STSToken}
/>
<PracticeSettingModal
ref={practiceSettingRef}
nodes={practiceRef.current && practiceRef.current.nodes ? practiceRef.current.nodes : null}
chapterId={chapterId}
bookId={bookId}
editor={editor}
/>
</div>
);
});
export default WangEditorCustomer;
......@@ -13,7 +13,7 @@ import { Boot } from '@wangeditor/editor'
import { SlateEditor, DomEditor, SlateElement, SlateTransforms } from '@wangeditor/editor'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { fontFamilyList } from './utils/setting'
import { fontFamilyList, fontSizeList, lineHeightList } from './utils/setting'
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import timesave from '@/assets/images/timesave.png'
......@@ -22,8 +22,6 @@ dayjs.extend(relativeTime)
import { storageChange } from '@/utils/storage.js'
// import PaddingSpace from './customer/padding';
import ImageAutoOnlineConf from './customer/ImageOnline'
import GalleryAutoConf from './customer/Gallery'
import GalleryAutoOnlineConf from './customer/GalleryOnline'
......@@ -37,11 +35,18 @@ import TooltipAutoConf from './customer/Tooltip'
import ImageEditorConf from './customer/ImageEditor'
import CustomerLinkConf from './customer/CustomerLink'
import ExpandReadConf from './customer/ExpandRead'
import PolishingConf from './ai/Polishing'
import ExpandArticleConf from './ai/ExpandArticle'
import RewriteConf from './ai/Rewrite'
import SummaryConf from './ai/Summary'
import AISelectTextRef from './ai/AI'
// AI对话
import AIChat from './menu/AIChat'
// AI辅助
import AIRewrite from './menu/AIRewrite'
import AIExpand from './menu/AIExpand'
import AISummary from './menu/AISummary'
import AIPolishing from './menu/AIPolishing'
import AIPunctuation from './menu/AIPunctuation'
import AIContentInspect from './menu/AIContentInspect'
import AIDigitalHuman from './menu/AIDigitalHuman'
import ImageModal from './components/image'
import VideoModal from './components/video'
......@@ -54,8 +59,6 @@ import FormulaModal from './components/formula'
import TooltipModal from './components/tooltip'
import LinkModal from './components/link'
import ExpandModal from './components/expand'
import AIDrawerComponent from './ai-drawer/index'
import AIWrite from './components/aiWrite'
import chapterSectionModule from './node/chapterItem'
import chapterHeaderModule from './node/chapterTitle'
......@@ -79,13 +82,6 @@ import $ from 'jquery'
import { getAliOSSSTSToken } from './utils/request'
import './index.less'
const menuArr0 = ['重做', '撤销']
const menuArr1 = ['字体样式', '字号', '行高']
const menuArr2 = ['离线图片', '在线图片', '离线画廊', '在线画廊', '视频', '音频', '表格']
const menuArr3 = ['代码块', '引用', '链接', '公式', '章头', '节头', '交互练习', '气泡', '扩展阅读']
const menuArr4 = ['改写', '扩写', '缩写', '总结']
const colorList = ['#ab1941', '#2970f6', '#2ad882', '#eb3351']
const bookBucketName = 'zxts-book-file'
const module = {
......@@ -104,11 +100,14 @@ const module = {
ImageEditorConf,
CustomerLinkConf,
ExpandReadConf,
PolishingConf,
ExpandArticleConf,
RewriteConf,
SummaryConf,
AISelectTextRef
AIChat,
AIRewrite,
AIExpand,
AISummary,
AIPolishing,
AIPunctuation,
AIContentInspect,
AIDigitalHuman
]
}
Boot.registerModule(module)
......@@ -149,7 +148,7 @@ const WangEditorCustomer = (props, ref) => {
const dispatch = useDispatch()
// 自动保存时间
const { autosaveTime } = useSelector((state) => state.editor)
const { autosaveTime } = useSelector(state => state.editor)
const toolbarRef = useRef()
......@@ -157,7 +156,6 @@ const WangEditorCustomer = (props, ref) => {
const [STSToken, setSTSToken] = useState(null) // oss 过期设置
const [tabKey, setTabKey] = useState('text')
const [loading, setLoading] = useState(true)
// editor 实例
const [editor, setEditor] = useState(null)
const [content, setContent] = useState(html)
......@@ -183,7 +181,7 @@ const WangEditorCustomer = (props, ref) => {
const [aiVisible, setAIVisible] = useState(false) // ai对话弹窗
const [priviewVisible, setPriviewVisible] = useState(false)
const [priviewVisible, setPreviewVisible] = useState(false)
const [historyVisible, setHistoryVisible] = useState(false) //点击历史
const [selectionSize, setSelectionSize] = useState(16) // 当前字号大小
......@@ -248,7 +246,7 @@ const WangEditorCustomer = (props, ref) => {
}
})
const listenNodeStyle = (path) => {
const listenNodeStyle = path => {
const children = editor.children
let node = null
if (path[1] === 0) {
......@@ -303,16 +301,6 @@ const WangEditorCustomer = (props, ref) => {
// 工具栏配置
const toolbarConfig = {
toolbarKeys: [
// '|',
// 'redo',
// 'undo',
// 'emotion',
// 'todo',
// 'fullScreen',
// '|',
// 'headerSelect',
// 'lineHeight',
// '|',
'redo',
'undo',
'|',
......@@ -337,18 +325,32 @@ const WangEditorCustomer = (props, ref) => {
'justifyJustify',
'divider',
'|',
// 'insertImage',
// 'uploadImage',
// 'insertVideo',
// 'uploadVideo',
'ImageAuto',
'ImageAutoOnline',
'GalleryAuto',
'GalleryAutoOnline',
'VideoAuto',
'AudioAuto',
'insertTable',
'|',
'codeBlock', // 代码块
// 'insertLink', // 链接
// 'insertFormula', // 公式
'blockquote' // 引用
// 'code',
// 'clearStyle',
'blockquote', // 引用
'CustomerLink',
'FormulaAuto',
'ChapterTitle',
'ChapterItem',
'Practice',
'TooltipAuto',
'ExpandRead',
'|',
'AIRewrite',
'AIExpand',
'AISummary',
'AIPolishing',
'AIPunctuation',
'AIContentInspect',
'|',
'AIDigitalHuman'
]
}
......@@ -372,47 +374,12 @@ const WangEditorCustomer = (props, ref) => {
}
]
toolbarConfig.insertKeys = {
index: 30,
keys: [
'ImageAuto',
'ImageAutoOnline',
'GalleryAuto',
'GalleryAutoOnline',
'VideoAuto',
'AudioAuto',
'CustomerLink',
'FormulaAuto',
'ChapterTitle',
'ChapterItem',
'Practice',
'TooltipAuto',
'ExpandRead',
'RewriteAuto',
'ExpandArticleAuto',
'PolishingAuto',
'SummaryAuto'
]
}
// 编辑器配置
let editorConfig = {
placeholder: '请输入内容...',
// 选中公式时的悬浮菜单
hoverbarKeys: {
// formula: {
// menuKeys: ['editFormula'], // “编辑公式”菜单
// },
// link: {
// menuKeys: [
// 'editLink',
// 'unLink',
// 'viewLink', // 默认的配置可以通过 `editor.getConfig().hoverbarKeys.link` 获取
// ],
// },
text: {
menuKeys: [
// 'headerSelect',
'CustomerLink',
'bulletedList',
'numberedList',
......@@ -427,196 +394,58 @@ const WangEditorCustomer = (props, ref) => {
'color',
'bgColor',
'clearStyle',
'AISelectTextAuto'
'AIChat'
]
},
image: {
menuKeys: [
'imageWidth30',
'imageWidth50',
'imageWidth100',
'ImageEditor',
// 'viewImageLink',
'deleteImage'
]
menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'ImageEditor', 'deleteImage']
},
ImageAuto: {
menuKeys: ['imageWidthChpater100', 'imageWidthChpater50', 'imageWidthChpater30', 'convertToLinkCard']
}
},
MENU_CONF: {
fontSize: {
fontSizeList: [
{ name: '初号', value: '56px' },
{ name: '小初', value: '48px' },
{ name: '一号', value: '34px' },
{ name: '小一', value: '32px' },
{ name: '二号', value: '29px' },
{ name: '小二', value: '24px' },
{ name: '三号', value: '21px' },
{ name: '小三', value: '20px' },
{ name: '四号', value: '18px' },
{ name: '小四', value: '16px' },
{ name: '五号', value: '14px' },
{ name: '小五', value: '12px' },
{ name: '六号', value: '10px' },
{ name: '小六', value: '8px' },
{ name: '七号', value: '7px' },
{ name: '八号', value: '6px' }
]
},
fontFamily: {
fontFamilyList: fontFamilyList
},
lineHeight: {
lineHeightList: ['1', '1.25', '1.5', '2', '2.5', '3']
}
// 其他...
fontSize: { fontSizeList },
fontFamily: { fontFamilyList },
lineHeight: { lineHeightList }
}
}
// 编辑器按钮重排
const toolSetttingReplace = () => {
const toolSettingReplace = () => {
setTimeout(() => {
const toolbarElement = toolbarRef.current && toolbarRef.current.children[0].children[0]
const allChildren = toolbarElement.children
const oHDiv_1 = document.createElement('div')
oHDiv_1.setAttribute('class', 'custom-bar-box two')
toolbarElement.insertBefore(oHDiv_1, allChildren[0])
$(allChildren[0]).append($(allChildren[1]).detach())
$(allChildren[0]).append($(allChildren[1]).detach())
const oH6_1 = document.createElement('h4')
oH6_1.setAttribute('class', 'w-auto type-heading')
oH6_1.innerHTML = '常用格式'
toolbarElement.insertBefore(oH6_1, allChildren[2])
// 行高字体字号设置
const itemBox = '<div class="custom-bar-box two"></div>'
$(itemBox).insertBefore($(allChildren[3]))
$(allChildren[3]).append($(allChildren[4]).detach())
$(allChildren[3]).append($(allChildren[4]).detach())
$(allChildren[3]).append($(allChildren[4]).detach())
// 加粗之类的
const itemBox2 = '<div class="w-e-bar-boxitem you"></div>'
$(itemBox2).insertBefore($(allChildren[4]))
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
$(allChildren[4]).append($(allChildren[5]).detach())
// 对齐
const itemBox3 = '<div class="w-e-bar-boxitem you"></div>'
$(itemBox3).insertBefore($(allChildren[5]))
$(allChildren[5]).append($(allChildren[6]).detach())
$(allChildren[5]).append($(allChildren[6]).detach())
$(allChildren[5]).append($(allChildren[6]).detach())
$(allChildren[5]).append($(allChildren[6]).detach())
$(allChildren[5]).append($(allChildren[6]).detach())
// 媒体资源
const oH6_2 = document.createElement('h4')
oH6_2.setAttribute('class', 'w-auto type-heading')
oH6_2.innerHTML = '媒体资源'
toolbarElement.insertBefore(oH6_2, allChildren[7])
const itemBox4 = document.createElement('div')
itemBox4.setAttribute('class', 'custom-bar-box media')
toolbarElement.insertBefore(itemBox4, allChildren[8])
console.log($(allChildren[8]), $(allChildren[13]), $(allChildren[9]))
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[13]).detach())
$(allChildren[8]).append($(allChildren[9]).detach())
// 高级模块
const oH6_3 = document.createElement('h4')
oH6_3.setAttribute('class', 'w-auto type-heading')
oH6_3.innerHTML = '高级模块'
toolbarElement.insertBefore(oH6_3, allChildren[10])
const itemBox5 = document.createElement('div')
itemBox5.setAttribute('class', 'custom-bar-box hight')
toolbarElement.insertBefore(itemBox5, allChildren[11])
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
$(allChildren[11]).append($(allChildren[12]).detach())
// ai
const dividerline = document.createElement('div')
dividerline.setAttribute('class', 'w-e-bar-divider')
toolbarElement.insertBefore(dividerline, allChildren[12])
const oH6_4 = document.createElement('h4')
oH6_4.setAttribute('class', 'w-auto type-heading')
oH6_4.innerHTML = 'AI辅助'
toolbarElement.insertBefore(oH6_4, allChildren[13])
const oHDiv_4 = document.createElement('div')
oHDiv_4.setAttribute('class', 'custom-bar-box hight')
toolbarElement.insertBefore(oHDiv_4, allChildren[14])
$(allChildren[14]).append($(allChildren[15]).detach())
$(allChildren[14]).append($(allChildren[15]).detach())
$(allChildren[14]).append($(allChildren[15]).detach())
$(allChildren[14]).append($(allChildren[15]).detach())
$('.custom-bar-box').each((index, item) => {
$(item)
.find('.w-e-bar-item')
.each((cIndex, cItem) => {
const oP = document.createElement('p')
if (index === 0) {
oP.innerHTML = menuArr0[cIndex]
$(oP).insertBefore($(cItem).find('button'))
} else if (index === 1) {
oP.innerHTML = menuArr1[cIndex]
$(oP).insertBefore($(cItem).find('button'))
} else if (index === 2) {
oP.innerHTML = menuArr2[cIndex]
// $(cItem).append(oP);
$(cItem).find('button').append(oP)
} else if (index === 3) {
oP.innerHTML = menuArr3[cIndex]
// $(cItem).append(oP);
$(cItem).find('button').append(oP)
} else if (index === 4) {
oP.innerHTML = menuArr4[cIndex]
// $(cItem).append(oP);
$(cItem).find('button').append(oP)
}
const editorToolbar = document.querySelector('.editor-toolbar-container')
// 设置菜单模块标题
const dividerElements = editorToolbar.querySelectorAll('.w-e-bar-divider')
const dividerTitles = ['常用格式', '媒体资源', '高级模块', 'AI辅助', 'AI数字人']
dividerElements.forEach((element, index) => {
element.innerHTML = dividerTitles[index]
})
// 设置菜单标题
const menuButtonElements = editorToolbar.querySelectorAll('.w-e-bar-item button')
menuButtonElements.forEach((element, index) => {
if (index > 1 && index < 22) return
const width = index === 0 || index === 1 ? '50%' : '25%'
element.parentElement.style.width = width
element.classList.add('has-title')
const title = element.getAttribute('data-tooltip')
const span = document.createElement('span')
span.innerHTML = title
span.className = 'title'
element.appendChild(span)
})
setLoading(false)
}, 350)
}, 50)
}
editorConfig.onCreated = (editor) => {
setLoading(true)
}
editorConfig.onFocus = (editor) => {
editorConfig.onFocus = editor => {
clearTimeout(saveRef.current)
}
editorConfig.onBlur = (editor) => {
editorConfig.onBlur = editor => {
// 失焦保存
setHtml(editor.getHtml())
setContent(editor.getHtml())
}
editorConfig.onChange = (editor) => {
editorConfig.onChange = editor => {
setHtml(editor.getHtml())
setContent(editor.getHtml())
}
......@@ -624,10 +453,7 @@ const WangEditorCustomer = (props, ref) => {
// 及时销毁 editor ,重要!
useEffect(() => {
if (editor) {
// console.log(editor.getConfig().hoverbarKeys.image);
// console.log(editor, editorConfig);
toolSetttingReplace()
toolSettingReplace()
// 图片上传
editor.on('ImageMenuClick', () => {
console.log('ImageMenuClick', '----')
......@@ -695,7 +521,7 @@ const WangEditorCustomer = (props, ref) => {
console.log('ImageEditorClick', '----')
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
match: node => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === 'paragraph') {
......@@ -745,45 +571,12 @@ const WangEditorCustomer = (props, ref) => {
}
setExpandVisible(true)
})
// 改写
editor.on('RewriteMenuClick', () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path)
}
setAiWriteOpen(true)
setAiWriteAction('rewrite')
})
// 扩写
editor.on('ExpandArticleMenuClick', () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path)
}
setAiWriteOpen(true)
setAiWriteAction('expand')
})
// 缩写
editor.on('PolishingMenuClick', () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path)
}
setAiWriteOpen(true)
setAiWriteAction('abbreviate')
})
// 总结
editor.on('SummaryMenuClick', () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path)
}
setAiWriteOpen(true)
setAiWriteAction('summary')
})
// ai对话
editor.on('AISelectTextClick', () => {
setSelectText(editor.getSelectionText())
setAIVisible(true)
})
const oldHtml = editor.getHtml()
editor.addMark('fontSize', '18px')
editor.addMark('fontFamily', '黑体')
......@@ -814,9 +607,9 @@ const WangEditorCustomer = (props, ref) => {
}
}, [gData, editor])
const tabKeyChange = (key) => {
const tabKeyChange = key => {
if (key === 'text') {
toolSetttingReplace()
toolSettingReplace()
}
setTabKey(key)
editor.focus()
......@@ -825,7 +618,7 @@ const WangEditorCustomer = (props, ref) => {
// 预览
const previewIt = async () => {
await saveContent()
setPriviewVisible(true)
setPreviewVisible(true)
}
// 历史
......@@ -838,7 +631,7 @@ const WangEditorCustomer = (props, ref) => {
if (data) {
window.sessionStorage.setItem('sts', JSON.stringify(data))
setSTSToken(data)
const ossClientTemp = await new AliOSS({
const ossClientTemp = new AliOSS({
accessKeyId: data.AccessKeyId,
accessKeySecret: data.AccessKeySecret,
stsToken: data.SecurityToken,
......@@ -898,23 +691,27 @@ const WangEditorCustomer = (props, ref) => {
})()
}, [])
const setColor = (type) => {
const colorList = [
{ color: '#ab1941', name: '默认' },
{ color: '#2970f6', name: '蓝色' },
{ color: '#2ad882', name: '绿色' },
{ color: '#eb3351', name: '红色' }
]
const setColor = color => {
const headers = document.querySelectorAll(`.w-e-scroll .chapter-item-header`)
const sections = document.querySelectorAll(`.w-e-scroll .chapter-item-section`)
headers.forEach((item) => {
headers.forEach(item => {
const node = DomEditor.toSlateNode(editor, item)
const path = DomEditor.findPath(editor, node)
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: colorList[type - 1] }, { at: path })
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: color }, { at: path })
})
sections.forEach((item) => {
sections.forEach(item => {
const node = DomEditor.toSlateNode(editor, item)
const path = DomEditor.findPath(editor, node)
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: colorList[type - 1] }, { at: path })
SlateTransforms.setNodes(editor, { ...node, textColor: '#ffffff', bgColor: color }, { at: path })
})
}
const [aiWriteOpen, setAiWriteOpen] = useState(false)
const [aiWriteAction, setAiWriteAction] = useState('rewrite')
return (
<div className="wangeditor-customer-container">
<div className="editor-content-container">
......@@ -963,7 +760,7 @@ const WangEditorCustomer = (props, ref) => {
<div className="tabs">
{tabsMenu &&
tabsMenu.length &&
tabsMenu.map((item) => {
tabsMenu.map(item => {
return (
<div className={`tabs-item ${item.key === tabKey ? 'active' : ''}`} key={item.key} onClick={() => tabKeyChange(item.key)}>
{item.title}
......@@ -975,54 +772,26 @@ const WangEditorCustomer = (props, ref) => {
<div className="menu-tabs-content">
{tabKey === 'text' && (
<div ref={toolbarRef} className="toolbox-parent">
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: '1px solid #ccc' }}
className="editor-toolbar-container"></Toolbar>
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" className="editor-toolbar-container"></Toolbar>
</div>
)}
{tabKey === 'style' && (
<div className="styletem">
<p>样式模板</p>
<ul>
<li>
<div className="left">
<span className="color color1"></span>
<b className="type">默认</b>
</div>
<Button type="link" className="use" onClick={() => setColor(1)}>
使用
</Button>
</li>
<li>
<div className="left">
<span className="color color2"></span>
<b className="type">蓝色</b>
</div>
<Button type="link" className="use" onClick={() => setColor(2)}>
使用
</Button>
</li>
<li>
<div className="left">
<span className="color color3"></span>
<b className="type">绿色</b>
</div>
<Button type="link" className="use" onClick={() => setColor(3)}>
使用
</Button>
</li>
<li>
{colorList.map(item => {
return (
<li key={item.color}>
<div className="left">
<span className="color color4"></span>
<b className="type">红色</b>
<span className="color" style={{ backgroundColor: item.color }}></span>
<b className="type">{item.name}</b>
</div>
<Button type="link" className="use" onClick={() => setColor(4)}>
<Button type="link" className="use" onClick={() => setColor(item.color)}>
使用
</Button>
</li>
)
})}
</ul>
</div>
)}
......@@ -1275,7 +1044,7 @@ const WangEditorCustomer = (props, ref) => {
classNames={{ body: 'phone-body', wrapper: 'phone-wrapper' }}
wrapClassName="wrap-phone-privew"
width="494px"
onCancel={() => setPriviewVisible(false)}>
onCancel={() => setPreviewVisible(false)}>
<PreviewModal ref={previewRef} gData={gData} editor={editor} chapterId={chapterId} bookId={bookId} nowTitle={nowTitle} />
</Modal>
......@@ -1302,25 +1071,6 @@ const WangEditorCustomer = (props, ref) => {
setContent={setContent}
/>
</Modal>
{/* ai对话 */}
<Drawer
open={aiVisible}
width="600px"
title="AI对话"
destroyOnClose
onClose={() => setAIVisible(false)}
rootClassName="ai-drawer-wrapper"
className="ai-drawer-container">
<AIDrawerComponent setAIVisible={setAIVisible} selectText={selectText} />
</Drawer>
<AIWrite
open={aiWriteOpen}
docAction={aiWriteAction}
onCancel={() => setAiWriteOpen(false)}
editor={editor}
chapterId={chapterId}
bookId={bookId}></AIWrite>
</div>
)
}
......
......@@ -239,173 +239,41 @@
}
}
.editor-toolbar-container {
flex: 0 0 306px;
border: none !important;
margin: 10px;
.w-e-bar {
background-color: transparent;
padding: 5px 15px;
}
.w-e-bar-boxitem {
background-color: #fff;
width: 100%;
display: flex;
justify-content: space-around;
&.you {
flex-flow: row wrap;
.w-e-bar-item {
width: 16.6%;
flex: 0 0 16.6%;
}
}
&.line-height {
justify-content: flex-start;
padding: 0;
svg {
width: 18px;
height: 18px;
}
}
.custom-bar-box {
background-color: #fff;
.w-e-bar-divider {
margin: 0;
padding-top: 10px;
width: 100%;
display: flex;
justify-content: flex-start;
flex-flow: row wrap;
.w-e-menu-tooltip-v5 {
height: 50px;
height: auto;
background: #fafafa;
font-size: 14px;
font-weight: 600;
line-height: 40px;
}
&.two {
.w-e-bar-item {
width: 33%;
flex: 0 0 33%;
justify-content: flex-start;
text-align: left;
display: block;
p {
color: #333;
font-weight: bold;
}
height: auto;
button {
padding-left: 0;
}
}
svg:nth-child(2) {
display: none;
}
&.input {
justify-content: space-around;
padding: 10px 0;
height: 62px;
.input-item {
flex: 1;
padding: 0 10px;
text-align: center;
input {
&.has-title {
width: 100%;
color: #333;
text-align: center;
border: 1px solid #d9d9d9;
border-radius: 5px;
height: 30px;
line-height: 30px;
&:hover {
border-color: #b83956;
}
}
.text {
color: #999;
font-size: 12px;
margin-top: 8px;
}
}
}
&.media {
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
p {
font-size: 10px;
-webkit-transform: scale(1.2);
-moz-transform: scale(1.2);
-o-transform: scale(1.2);
-ms-transform: scale(1.2);
transform: scale(1.2);
}
}
}
&.hight {
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
p {
font-size: 10px;
-webkit-transform: scale(1.2);
-moz-transform: scale(1.2);
-o-transform: scale(1.2);
-ms-transform: scale(1.2);
transform: scale(1.2);
}
}
}
&.media,
&.hight {
.w-e-bar-item {
button {
display: block;
flex-direction: column;
height: 100%;
padding: 10px;
height: 50px;
svg {
font-size: 22px;
}
}
p {
display: block;
width: 100%;
margin: 0px;
}
}
}
.w-e-bar-item {
width: 25%;
flex: 0 0 25%;
display: flex;
flex-direction: column;
height: 60px;
}
}
.w-e-bar-divider {
display: block;
height: 1px;
width: 100%;
margin: 8px 0;
&:nth-of-type(16) {
height: 0;
}
// &:nth-child(odd) {
// height: 0;
// }
}
.w-auto {
display: block;
width: 100%;
height: 40px;
line-height: 40px;
&.type-heading {
font-weight: 600;
}
}
.w-e-bar {
svg {
width: 18px;
height: 18px;
&:nth-child(2) {
display: none;
}
align-items: center;
.title {
margin: 5px 0 0 0;
}
}
.w-e-select-list {
z-index: 10001;
width: 180px !important;
}
.w-e-menu-tooltip-v5:before {
z-index: 10001;
}
}
.ant-spin-nested-loading,
......@@ -417,7 +285,7 @@
}
.menu-tabs-key {
flex: 1;
flex: 0 0 306px;
background: #fafafa;
border: 1px solid #e5e5e5;
min-width: 300px;
......
// Extend menu
class AISelectTextAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIChatDrawer from './common/AIChatDrawer'
class AIChat extends BaseModalMenu {
constructor() {
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';
super()
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 'hello, AI';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('AISelectTextClick');
return <AIChatDrawer key={Date.now()} editor={editor}></AIChatDrawer>
}
}
export default {
key: 'AISelectTextAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIChat', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new AISelectTextAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { AISelectTextAuto };
return new AIChat() // 把 `YourMenuClass` 替换为你菜单的 class
}
}
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIContentInspect extends BaseModalMenu {
constructor() {
super()
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} docAction="contentInspect"></AIModal>
}
}
export default {
key: 'AIContentInspect',
factory() {
return new AIContentInspect()
}
}
// Extend menu
class AIDigitalHuman {
constructor() {
this.title = '数字人'
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" 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>`
this.tag = 'button'
}
getValue() {
return 'hello, 音频'
}
isActive() {
return false
}
isDisabled() {
return true
}
exec() {
return
}
}
export default {
key: 'AIDigitalHuman',
factory() {
return new AIDigitalHuman()
}
}
// Extend menu
class ExpandArticleAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIExpand extends BaseModalMenu {
constructor() {
this.title = '扩写';
super()
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">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-622, -216)" fill="#333333" fill-rule="nonzero">
<g id="文章扩写" transform="translate(622, 216)">
......@@ -11,32 +12,16 @@ class ExpandArticleAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('ExpandArticleMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="expand"></AIModal>
}
}
export default {
key: 'ExpandArticleAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIExpand',
factory() {
return new ExpandArticleAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { ExpandArticleAuto };
return new AIExpand()
}
}
// Extend menu
class PolishingAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIPolishing extends BaseModalMenu {
constructor() {
this.title = '缩写';
super()
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">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-536, -221)" fill="#333333" fill-rule="nonzero">
<g id="缩写-(1)" transform="translate(536, 221)">
<g id="润色-(1)" transform="translate(536, 221)">
<path d="M1.95471575,0 L44.9584623,0 C46.2616061,0 46.913178,0.678713837 46.913178,2.03614151 L46.913178,2.03614151 C46.913178,3.39356919 46.2616061,4.07228302 44.9584623,4.07228302 L1.95471575,4.07228302 C0.651571917,4.07228302 0,3.39356919 0,2.03614151 L0,2.03614151 C0,0.678713837 0.651571917,0 1.95471575,0 Z" id="路径"></path>
<path d="M1.95471575,12.2168491 L29.3207363,12.2168491 C30.6238801,12.2168491 31.275452,12.8955629 31.275452,14.2529906 L31.275452,14.2529906 C31.275452,15.6104183 30.6238801,16.2891321 29.3207363,16.2891321 L1.95471575,16.2891321 C0.651571917,16.2891321 0,15.6104183 0,14.2529906 L0,14.2529906 C0,12.8955629 0.651571917,12.2168491 1.95471575,12.2168491 Z" id="路径"></path>
<path d="M1.95471575,24.4336981 L17.5924418,24.4336981 C18.8955856,24.4336981 19.5471575,25.112412 19.5471575,26.4698397 L19.5471575,26.4698397 C19.5471575,27.8272673 18.8955856,28.5059812 17.5924418,28.5059812 L1.95471575,28.5059812 C0.651571917,28.5059812 0,27.8272673 0,26.4698397 L0,26.4698397 C0,25.112412 0.651571917,24.4336981 1.95471575,24.4336981 Z" id="路径"></path>
......@@ -14,32 +17,16 @@ class PolishingAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
}
getValue(editor) {
return 'hello, 缩写';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('PolishingMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="abbreviate"></AIModal>
}
}
export default {
key: 'PolishingAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIPolishing', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new PolishingAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { PolishingAuto };
return new AIPolishing() // 把 `YourMenuClass` 替换为你菜单的 class
}
}
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIPunctuation extends BaseModalMenu {
constructor() {
super()
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} docAction="punctuation"></AIModal>
}
}
export default {
key: 'AIPunctuation',
factory() {
return new AIPunctuation()
}
}
// Extend menu
class RewriteAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AIRewrite extends BaseModalMenu {
constructor() {
this.title = '改写';
super()
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">
<g id="图标" transform="translate(-731, -218)" fill="#333333" fill-rule="nonzero">
......@@ -11,32 +15,17 @@ class RewriteAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
this.tag = 'button'
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('RewriteMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="rewrite"></AIModal>
}
}
export default {
key: 'RewriteAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AIRewrite',
factory() {
return new RewriteAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { RewriteAuto };
return new AIRewrite()
}
}
// Extend menu
class SummaryAuto {
import BaseModalMenu from './common/BaseModalMenu'
import AIModal from './common/AIModal'
class AISummary extends BaseModalMenu {
constructor() {
this.title = '总结';
super()
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">
<title>图标</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图标" transform="translate(-821, -216)" fill="#333333" fill-rule="nonzero">
<g id="总结" transform="translate(821, 216)">
......@@ -13,32 +16,16 @@ class SummaryAuto {
</g>
</g>
</g>
</svg>`;
this.tag = 'button';
</svg>`
}
getValue(editor) {
return 'hello, 音频';
}
isActive(editor) {
return false; // or true
}
isDisabled(editor) {
return false; // or true
}
exec(editor, value) {
// editor.insertText(value) // value 即 this.getValue(editor) 的返回值
if (this.isDisabled(editor)) {
return;
}
editor.emit('SummaryMenuClick');
return <AIModal key={Date.now()} editor={editor} docAction="summary"></AIModal>
}
}
export default {
key: 'SummaryAuto', // 定义 menu key :要保证唯一、不重复(重要)
key: 'AISummary',
factory() {
return new SummaryAuto(); // 把 `YourMenuClass` 替换为你菜单的 class
},
};
export { SummaryAuto };
return new AISummary()
}
}
import { useState, useEffect } from 'react'
import { SendOutlined } from '@ant-design/icons'
import { Drawer, Input, Button, message } from 'antd'
import dayjs from 'dayjs'
import { useAIChat } from './useAIChat'
import normalAvatar from '@/assets/images/icon-normal-avatar.png'
import './AIChatDrawer.less'
const AIChatDrawer = props => {
const selectText = props.editor.getSelectionText()
const [open, setOpen] = useState(true)
const { messages, post, isLoading } = useAIChat()
const [value, setValue] = useState(selectText) // 输入值
useEffect(() => {
if (selectText) {
targetChat()
}
return () => {
setValue('')
}
}, [selectText])
const element = document.querySelector('#chat-content')
useEffect(() => {
element?.scrollTo(0, 999999)
}, [messages, element])
// 提交对话
const targetChat = async () => {
if (value) {
post({ userChatInput: value })
setValue('')
} else {
message.error('请输入对话内容!')
}
}
return (
<Drawer
open={open}
width="600px"
title="AI对话"
destroyOnClose
onClose={() => setOpen(false)}
rootClassName="ai-drawer-wrapper"
className="ai-drawer-container">
<div className="ai-drawer-content">
<div className="ai-chat-container">
<div className="chat-content" id="chat-content">
<div className="chat-content-padd">
{messages.map((item, index) => {
return (
<div className={`chat-content-item`} key={index}>
{item.role_type === 'user' && <div className="time">{dayjs(item.time).format('YYYY-MM-DD HH:mm')}</div>}
{item.role_type === 'ai' && (
<div className="inside">
<div className="ai-in-content">
<div className="img">
<img src={item.role?.avatar} />
</div>
<div className="ask-content">{item.content}</div>
</div>
</div>
)}
{item.role_type === 'user' && (
<div className="inside inside-user">
<div className="user-in-content">
<div className="ask-content">{item.content}</div>
<div className="img">
<img src={normalAvatar} />
</div>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</div>
<div className="ai-chat-ask">
<div className="text">
<Input.TextArea
value={value}
defaultValue={value}
allowClear
disabled={isLoading}
onChange={e => setValue(e.target.value)}
placeholder="请输入内容"
style={{ height: '80px', resize: 'none' }}></Input.TextArea>
</div>
<div className="button">
<Button type="primary" icon={<SendOutlined />} loading={isLoading} size="large" onClick={targetChat}></Button>
</div>
</div>
</div>
</Drawer>
)
}
export default AIChatDrawer
......@@ -102,7 +102,7 @@
flex: 1;
}
.button {
flex: 0 0 60px;
flex: 0 0 40px;
padding-left: 10px;
.ant-btn {
height: 80px;
......
......@@ -6,25 +6,27 @@ const { TextArea } = Input
const actionMap = {
rewrite: { name: '改写', prompt: '帮我改写以下文字内容:' },
expand: { name: '扩写', prompt: '帮我在以下文字内容基础上进行扩写:' },
abbreviate: { name: '缩写', prompt: '帮我缩写以下文字内容:' },
summary: { name: '总结', prompt: '帮我总结以下文字内容:' }
abbreviate: { name: '润色', prompt: '帮我润色以下文字内容:' },
summary: { name: '总结', prompt: '帮我总结以下文字内容:' },
punctuation: { name: '标点校对', prompt: '帮我校对以下文字内容中的标点符号:' },
contentInspect: { name: '内容检查', prompt: '帮我检查以下文字内容中的敏感词和错别字:' }
}
export default function AIWrite({ editor, docAction, ...rest }) {
export default function AIModal({ editor, docAction }) {
const [isModalOpen, setIsModalOpen] = useState(true)
const [content, setContent] = useState('')
const { text, fetch, isLoading } = useAIEdit()
const actionText = actionMap[docAction]?.name
const [selectionText, setSelectionText] = useState('')
useEffect(() => {
if (rest.open) {
const selection = editor.getSelectionText()
if (selection) {
setSelectionText(selection)
setContent(selection)
fetch({ messages: [{ role: 'user', content: actionMap[docAction].prompt + selection }] })
}
}
}, [rest.open])
}, [docAction, editor, fetch])
useEffect(() => {
setContent(text)
......@@ -37,21 +39,19 @@ export default function AIWrite({ editor, docAction, ...rest }) {
const handlePrimary = () => {
editor.restoreSelection()
editor.insertText(text)
rest.onCancel()
setIsModalOpen(false)
}
return (
<Modal
title={`以下是AI${actionText}结果:`}
{...rest}
open={isModalOpen}
footer={null}
classNames={{
header: 'editor-header-customer',
body: 'editor-body-customer',
wrapper: 'editor-wrapper-customer'
}}>
classNames={{ header: 'editor-header-customer', body: 'editor-body-customer', wrapper: 'editor-wrapper-customer' }}
onOk={handlePrimary}
onCancel={() => setIsModalOpen(false)}>
<Spin spinning={isLoading}>
<TextArea autoSize={{ minRows: 4 }} value={content} onChange={(e) => setContent(e.target.value)} />
<TextArea autoSize={{ minRows: 4 }} value={content} onChange={e => setContent(e.target.value)} />
</Spin>
<br />
<Flex gap="small" justify="center">
......@@ -61,7 +61,7 @@ export default function AIWrite({ editor, docAction, ...rest }) {
<Button type="primary" onClick={handleFetch}>
重新{actionText}
</Button>
<Button type="primary" onClick={rest.onCancel}>
<Button type="primary" onClick={() => setIsModalOpen(false)}>
取消
</Button>
</Flex>
......
import ReactDOM from 'react-dom/client'
import { ConfigProvider, App } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export default class BaseModalMenu {
constructor() {
this.tag = 'button'
this.$el = document.createElement('div')
document.body.appendChild(this.$el)
this.$root = ReactDOM.createRoot(this.$el)
}
isActive() {
return false // or true
}
isDisabled() {
return false // or true
}
exec(editor, value) {
this.$root.render(
<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#ab1941' } }}>
<App>{value}</App>
</ConfigProvider>
)
}
}
import md5 from 'js-md5'
import { fetchEventSource } from '@fortaine/fetch-event-source'
import { useState, useCallback } from 'react'
export function useAIChat() {
const authKey = 'f3846153ba784b6d86bdcd5533259c88'
const authSecret = 'HO4IyLEwEOHpeOXBxaLQUOqWslJRGs1M'
const [messages, setMessages] = useState([])
const [chatId, setChatId] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const addMessage = useCallback(message => {
setMessages(prevMessages => [...prevMessages, message])
}, [])
const updateMessages = useCallback(newMessage => {
setMessages(prevMessages => {
const existingMessage = prevMessages.find(msg => msg.conversationId === newMessage.conversationId)
const content = newMessage.content === '\n' ? '<br/>' : newMessage.content || ''
if (existingMessage) {
// 更新现有消息
return prevMessages.map(msg => (msg.conversationId === newMessage.conversationId ? { ...msg, content: msg.content + content } : msg))
} else {
// 新增消息
return [...prevMessages, { ...newMessage, content, role_type: 'ai', time: Date.now() }]
}
})
}, [])
const post = useCallback(
async data => {
const timestamp = Date.now()
const sign = md5(`${authKey}${authSecret}${timestamp}`)
// 插入用户消息
addMessage({
role_type: 'user',
content: data.userChatInput,
conversationId: `user-${timestamp}`, // 生成一个唯一的 conversationId
time: Date.now()
})
setIsLoading(true)
try {
await fetchEventSource('/api/tiangong/openapi/agent/chat/stream/v1', {
method: 'POST',
headers: {
authKey,
timestamp,
sign,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...data, chatId, agentId: authKey }),
onopen(response) {
if (!response.ok) {
throw new Error('Network response was not ok')
}
},
onmessage(event) {
const message = JSON.parse(event.data)
setChatId(message.chatId)
updateMessages(message)
},
onerror(error) {
console.error('Fetch error:', error)
},
onclose() {
setIsLoading(false)
}
})
} catch (error) {
console.error('Post error:', error)
setIsLoading(false)
}
},
[authKey, authSecret, chatId, addMessage, updateMessages]
)
return { messages, isLoading, post }
}
......@@ -4,10 +4,10 @@ export const fontFamilyList = [
{ name: '黑体', value: '黑体' },
{ name: '宋体', value: '宋体' },
{ name: '楷体', value: '楷体' },
{ name: '仿宋', value: '仿宋' },
];
{ name: '仿宋', value: '仿宋' }
]
export const fontFizeList = [
export const fontSizeList = [
{ name: '初号', value: 56 },
{ name: '小初', value: 48 },
{ name: '一号', value: 34 },
......@@ -23,55 +23,58 @@ export const fontFizeList = [
{ name: '六号', value: 10 },
{ name: '小六', value: 8 },
{ name: '七号', value: 7 },
{ name: '八号', value: 6 },
];
{ name: '八号', value: 6 }
]
export const lineHeightList = ['1', '1.25', '1.5', '2', '2.5', '3']
export const findElementPath = (editor, elementType) => {
const { children } = editor;
const { children } = editor
// 递归函数来遍历节点
function traverseNodes(node) {
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
const child = node[i];
traverseNodes(child);
const child = node[i]
traverseNodes(child)
if (child.type === elementType) {
// 当找到匹配的元素类型时,返回其路径
return [i, ...traverseNodes(child)];
return [i, ...traverseNodes(child)]
}
}
} else if (node.type === elementType) {
// 如果当前节点是元素节点且类型匹配,则返回其路径
return [traverseNodes(node.parent)];
return [traverseNodes(node.parent)]
}
return null;
return null
}
// 从文档的根节点开始遍历
const path = traverseNodes(children);
return path;
};
const path = traverseNodes(children)
return path
}
export const findNodeWithParent = (node, type, key = 'random', value) => {
const path = []; // 用于存储当前节点的路径
const path = [] // 用于存储当前节点的路径
// 递归遍历节点的辅助函数
function traverse(nodes, type, key, value) {
for (let i = 0; i < nodes.length; i++) {
const current = nodes[i];
const current = nodes[i]
if (current.type === type && String(current[key]) === String(value)) {
// 找到匹配的节点,将索引添加到路径数组中
path.push(i);
return true;
path.push(i)
return true
}
if (Array.isArray(current.children) && traverse(current.children, type, key, value)) {
// 如果子节点中找到匹配的节点,添加当前节点的索引到路径
path.push(i);
return true;
path.push(i)
return true
}
}
return false; // 未找到匹配的节点
return false // 未找到匹配的节点
}
// 从根节点开始遍历
traverse(node, type, key, value);
return path; // 返回找到的路径,如果没有找到则返回空数组
};
traverse(node, type, key, value)
return path // 返回找到的路径,如果没有找到则返回空数组
}
import { useState, useEffect } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import './styles.less'
function MyEditor() {
function WangEditorCustomer() {
// editor 实例
const [editor, setEditor] = useState(null) // JS 语法
......@@ -17,7 +18,38 @@ function MyEditor() {
}, [])
// 工具栏配置
const toolbarConfig = {}
const toolbarConfig = {
toolbarKeys: [
'redo',
'undo',
'|',
'fontFamily',
'fontSize',
'lineHeight',
'color',
'bold',
'italic',
'through',
'underline',
'bgColor',
'bulletedList',
'numberedList',
'indent',
'delIndent',
'sub',
'sup',
'justifyLeft',
'justifyCenter',
'justifyRight',
'justifyJustify',
'divider',
'|',
'insertTable',
'|',
'codeBlock', // 代码块
'blockquote' // 引用
]
}
// 编辑器配置
const editorConfig = {
......@@ -32,21 +64,52 @@ function MyEditor() {
}
}, [editor])
const onCreated = editor => {
setEditor(editor)
setTimeout(() => {
const editorToolbar = document.querySelector('.editor-toolbar')
// 设置菜单模块标题
const dividerElements = editorToolbar.querySelectorAll('.w-e-bar-divider')
const dividerTitles = ['常用格式', '媒体资源', '高级模块', 'AI辅助', 'AI数字人']
dividerElements.forEach((element, index) => {
element.innerHTML = dividerTitles[index]
})
// 设置菜单标题
const menuButtonElements = editorToolbar.querySelectorAll('.w-e-bar-item button')
menuButtonElements.forEach((element, index) => {
if (index > 1 && index < 22) return
element.classList.add('has-title')
const title = element.getAttribute('data-tooltip')
const span = document.createElement('span')
span.innerHTML = title
span.className = 'title'
element.appendChild(span)
})
}, 50)
}
return (
<>
<div style={{ border: '1px solid #ccc', zIndex: 100 }}>
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" style={{ borderBottom: '1px solid #ccc' }} />
<div className="editor-wrapper">
<div className="editor-left">
<div className="editor-header"></div>
<div className="editor-main">
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onCreated={onCreated}
onChange={editor => setHtml(editor.getHtml())}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
/>
</div>
</div>
<div className="editor-right">
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" className="editor-toolbar" />
</div>
</div>
</>
)
}
export default MyEditor
export default WangEditorCustomer
.editor-wrapper {
display: flex;
}
.editor-left {
flex: 1;
border: 1px solid #ccc;
}
.editor-right {
flex: 0 0 300px;
background: #fafafa;
border: 1px solid #e5e5e5;
min-width: 300px;
margin-left: 10px;
border-radius: 6px;
}
.editor-toolbar {
margin: 10px;
.w-e-bar {
padding: 0;
svg {
width: 18px;
height: 18px;
}
}
.w-e-bar-divider {
margin: 0;
padding-top: 10px;
width: 100%;
height: auto;
background: #fafafa;
font-size: 14px;
font-weight: 600;
line-height: 40px;
}
.w-e-bar-item {
height: auto;
button {
svg:nth-child(2) {
display: none;
}
&.has-title {
width: 100%;
height: auto;
padding: 10px;
flex-direction: column;
align-items: center;
.title {
margin: 5px 0 0 0;
}
}
}
}
}
import React, { useState, useEffect, useRef } from 'react';
import {
Divider,
Button,
Row,
Col,
Descriptions,
Tree,
Tooltip,
Dropdown,
Space,
Input,
Popconfirm,
Modal,
Spin,
} from 'antd';
import React, { useState, useEffect, useRef } from 'react'
import { Divider, Button, Row, Col, Descriptions, Tree, Tooltip, Dropdown, Space, Input, Popconfirm, Modal, Spin } from 'antd'
import {
DiffOutlined,
MenuFoldOutlined,
......@@ -22,251 +8,235 @@ import {
DashOutlined,
CloseOutlined,
CheckOutlined,
EllipsisOutlined,
} from '@ant-design/icons';
import WangEditorCustomer from '@/common/wangeditor-customer';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { setAutosaveTime } from '@/store/modules/editor';
import { setTreeChapter } from '@/store/modules/user';
import EditChapterTitle from './components/form-chapter-title';
import { get } from 'lodash-es';
import md5 from 'js-md5';
import {
convertToAntdTreeData,
findTreeElementByKey,
findFirstNotHasChildren,
findParentLevelOne,
findTreeToIndex,
findNodeById,
} from '@/utils/common';
import {
getAllList,
getInfoByChapterId,
sectionEdit,
chapterDel,
dragOrder,
getRecordList,
getUserInfo,
getPowerByRoleId,
} from './request';
import './index.less';
EllipsisOutlined
} from '@ant-design/icons'
import WangEditorCustomer from '@/common/wangeditor-customer'
import { useLocation, useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { setAutosaveTime } from '@/store/modules/editor'
import { setTreeChapter } from '@/store/modules/user'
import EditChapterTitle from './components/form-chapter-title'
import { get } from 'lodash-es'
import md5 from 'js-md5'
import { convertToAntdTreeData, findTreeElementByKey, findFirstNotHasChildren, findParentLevelOne, findTreeToIndex, findNodeById } from '@/utils/common'
import { getAllList, getInfoByChapterId, sectionEdit, chapterDel, dragOrder, getRecordList, getUserInfo, getPowerByRoleId } from './request'
import './index.less'
const Examine = () => {
const location = useLocation();
const id = get(location, 'state.id', '');
const location = useLocation()
const id = get(location, 'state.id', '')
const { treeChapter } = useSelector((state) => state.user);
const navigate = useNavigate();
const dispatch = useDispatch();
const { treeChapter } = useSelector(state => state.user)
const navigate = useNavigate()
const dispatch = useDispatch()
const [gData, setGData] = useState([]);
const [chapterId, setChapterId] = useState(0);
const [bookId, setBookId] = useState(0);
const [nameList, setNameList] = useState([]);
const [openDel, setOpenDel] = useState(false);
const [delNode, setDelNode] = useState({});
const [recordList, setRecordList] = useState([]);
const [gData, setGData] = useState([])
const [chapterId, setChapterId] = useState(0)
const [bookId, setBookId] = useState(0)
const [nameList, setNameList] = useState([])
const [openDel, setOpenDel] = useState(false)
const [delNode, setDelNode] = useState({})
const [recordList, setRecordList] = useState([])
// 树节点设置
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [nowTitle, setNowTitle] = useState('');
const [expandedKeys, setExpandedKeys] = useState([])
const [checkedKeys, setCheckedKeys] = useState([])
const [nowTitle, setNowTitle] = useState('')
// 编辑操作
const [isCollapse, setisCollapse] = useState(false);
const [editKey, setEditKey] = useState(null);
const [parentId, setParentId] = useState(null);
const [editValue, setEditValue] = useState('');
const [loading, setLoading] = useState(false);
const [delLoading, setDelLoading] = useState(false);
const [contentMd5, setContentMd5] = useState('');
const [quanXian, setQuanXian] = useState(false);
const [isCollapse, setisCollapse] = useState(false)
const [editKey, setEditKey] = useState(null)
const [parentId, setParentId] = useState(null)
const [editValue, setEditValue] = useState('')
const [loading, setLoading] = useState(false)
const [delLoading, setDelLoading] = useState(false)
const [contentMd5, setContentMd5] = useState('')
const [quanXian, setQuanXian] = useState(false)
// 编辑器内容
const editorRef = useRef();
const saveInterRef = useRef();
const [html, setHtml] = useState('');
const [contentId, setContentId] = useState(false);
const editorRef = useRef()
const saveInterRef = useRef()
const [html, setHtml] = useState('')
const [contentId, setContentId] = useState(false)
// 获取目录结构
const getChapterTreeList = async () => {
setLoading(true);
const { data = [], code } = await getAllList({ book_id: id });
setLoading(true)
const { data = [], code } = await getAllList({ book_id: id })
if (code !== 200) {
setLoading(false);
navigate('/books/management/list', { replace: true });
return;
setLoading(false)
navigate('/books/management/list', { replace: true })
return
}
if (data.length === 0) {
setLoading(false);
return;
setLoading(false)
return
}
const uniqueBookList = [...new Set(data.map((item) => item.book_id))][0];
setBookId(uniqueBookList);
const arr = convertToAntdTreeData(data, 'name');
setGData(arr);
const allKeys = getAllNodeKeys(arr);
setExpandedKeys(allKeys);
const uniqueBookList = [...new Set(data.map(item => item.book_id))][0]
setBookId(uniqueBookList)
const arr = convertToAntdTreeData(data, 'name')
setGData(arr)
const allKeys = getAllNodeKeys(arr)
setExpandedKeys(allKeys)
const first = findFirstNotHasChildren(arr);
setNowTitle(first.title);
const first = findFirstNotHasChildren(arr)
setNowTitle(first.title)
if (treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(arr, 'key', treeChapter.saveChapterId);
const childInKey = findTreeElementByKey(arr, 'key', treeChapter.saveChapterId)
if (childInKey) {
setChapterId(childInKey.key);
setCheckedKeys([childInKey.key]);
setNowTitle(childInKey.title);
setChapterId(childInKey.key)
setCheckedKeys([childInKey.key])
setNowTitle(childInKey.title)
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
setChapterId(first.key)
setCheckedKeys([first.key])
setNowTitle(first.title)
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }));
setChapterId(first.key)
setCheckedKeys([first.key])
setNowTitle(first.title)
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: first.key }))
}
} else {
setChapterId(first.key);
setCheckedKeys([first.key]);
setNowTitle(first.title);
setChapterId(first.key)
setCheckedKeys([first.key])
setNowTitle(first.title)
}
setLoading(false)
}
setLoading(false);
};
//选中了章节管理才展示编辑
const getQuanXian = async () => {
const data = await getUserInfo();
const data1 = await getPowerByRoleId({ role_id: data.id });
const data = await getUserInfo()
const data1 = await getPowerByRoleId({ role_id: data.id })
if (!data1.includes(37)) {
setQuanXian(true);
setQuanXian(true)
}
}
};
const newChapterChange = (id, title) => {
setChapterId(parseInt(id));
setCheckedKeys([parseInt(id)]);
setNowTitle(title);
};
setChapterId(parseInt(id))
setCheckedKeys([parseInt(id)])
setNowTitle(title)
}
// 递归函数,获取所有节点的key
const getAllNodeKeys = (nodes) => {
let keys = [];
nodes.forEach((node) => {
keys.push(node.key);
const getAllNodeKeys = nodes => {
let keys = []
nodes.forEach(node => {
keys.push(node.key)
if (node.children && node.children.length > 0) {
keys = keys.concat(getAllNodeKeys(node.children));
keys = keys.concat(getAllNodeKeys(node.children))
}
})
return keys
}
});
return keys;
};
// 获取内容
const getChapterVal = async () => {
setLoading(true);
const data = await getInfoByChapterId({ chapter_id: chapterId });
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId }));
setLoading(true)
const data = await getInfoByChapterId({ chapter_id: chapterId })
dispatch(setTreeChapter({ saveBookId: id, saveChapterId: chapterId }))
const { content, id: cId } = data;
const { content, id: cId } = data
if (content) {
setContentMd5(md5(content));
setContentMd5(md5(content))
}
setHtml(content);
setContentId(cId);
setHtml(content)
setContentId(cId)
if (editorRef.current && editorRef.current.editor) {
editorRef.current.editor.setHtml(content);
editorRef.current.editor.setHtml(content)
}
setLoading(false)
}
setLoading(false);
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
};
const onExpand = expandedKeys => {
setExpandedKeys(expandedKeys)
}
const handleSelect = async (checkedKeys, info) => {
const { key } = info.node;
const childInKey = findTreeElementByKey(gData, 'key', key);
const { key } = info.node
const childInKey = findTreeElementByKey(gData, 'key', key)
if (childInKey.children && childInKey.children.length > 0) {
clearInterval(saveInterRef.current);
clearInterval(saveInterRef.current)
// 进行展开操作
const tempExpandeds = JSON.parse(JSON.stringify(expandedKeys));
let newExpand = '';
const tempExpandeds = JSON.parse(JSON.stringify(expandedKeys))
let newExpand = ''
if (expandedKeys.includes(key)) {
newExpand = tempExpandeds.filter((item) => parseInt(item) !== parseInt(key));
newExpand = tempExpandeds.filter(item => parseInt(item) !== parseInt(key))
} else {
newExpand = [...tempExpandeds, key];
newExpand = [...tempExpandeds, key]
}
setExpandedKeys(newExpand);
setCheckedKeys([key]);
return;
setExpandedKeys(newExpand)
setCheckedKeys([key])
return
} else {
const { key, title } = info.node;
const { key, title } = info.node
if (info.selected === false) {
setLoading(true);
setChapterId(key);
setCheckedKeys([key]);
setNowTitle(title);
setLoading(false);
setLoading(true)
setChapterId(key)
setCheckedKeys([key])
setNowTitle(title)
setLoading(false)
} else {
setLoading(true);
clearInterval(saveInterRef.current);
await saveContent();
setLoading(true)
clearInterval(saveInterRef.current)
await saveContent()
if (key !== chapterId) {
editorRef.current.editor.clear();
editorRef.current.editor.clear()
}
setLoading(true)
setChapterId(checkedKeys[0])
setCheckedKeys([checkedKeys[0]])
setNowTitle(info.node.title)
setLoading(false)
}
setLoading(true);
setChapterId(checkedKeys[0]);
setCheckedKeys([checkedKeys[0]]);
setNowTitle(info.node.title);
setLoading(false);
}
}
};
useEffect(() => {
getChapterTreeList();
getQuanXian();
}, []);
getChapterTreeList()
getQuanXian()
}, [])
useEffect(() => {
if (chapterId) {
setCheckedKeys([chapterId]);
getChapterVal();
setCheckedKeys([chapterId])
getChapterVal()
}
}, [chapterId]);
}, [chapterId])
// 保存方法
const saveContent = async () => {
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml();
const newMd5 = md5(contentHtml);
clearInterval(saveInterRef.current);
const word_count = editorRef.current.editor.getText().length;
const contentHtml = editorRef.current.editor.getHtml()
const newMd5 = md5(contentHtml)
clearInterval(saveInterRef.current)
const word_count = editorRef.current.editor.getText().length
// 保存
const data = await sectionEdit({
id: contentId,
content: contentHtml,
word_count,
});
word_count
})
if (data) {
if ((data.code && data.code === 3000) || data.code === '3000') {
navigate('/books/management/list');
return;
navigate('/books/management/list')
return
} else {
setContentMd5(newMd5);
autoSaveContent(newMd5);
dispatch(setAutosaveTime(Date.now()));
setContentMd5(newMd5)
autoSaveContent(newMd5)
dispatch(setAutosaveTime(Date.now()))
}
}
}
}
};
// 定时器轮询
const autoSaveContent = (newMd5) => {
const autoSaveContent = newMd5 => {
// clearInterval(saveInterRef.current);
// saveInterRef.current = setInterval(async () => {
// if (!contentMd5 || (newMd5 !== contentMd5 && contentMd5)) {
......@@ -276,149 +246,153 @@ const Examine = () => {
// // console.log('save', newMd5, '一样的');
// }
// }, 5000);
};
}
useEffect(() => {
if (!loading && contentId) {
let newMd5 = md5(html);
clearInterval(saveInterRef.current);
autoSaveContent(newMd5);
let newMd5 = md5(html)
clearInterval(saveInterRef.current)
autoSaveContent(newMd5)
} else {
clearInterval(saveInterRef.current);
clearInterval(saveInterRef.current)
}
}, [contentId, loading, contentMd5, html]);
}, [contentId, loading, contentMd5, html])
const addChapterParent = () => {
setEditKey(-1);
setEditValue('');
setParentId(-1);
};
setEditKey(-1)
setEditValue('')
setParentId(-1)
}
const dropDownMenuHandler = async (e, node) => {
e.domEvent.stopPropagation();
e.domEvent.preventDefault();
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
if (parseInt(e.key) === 1) {
// 展开
const ids = findNodeById(gData, 'key', node.key);
const expandedKeysTemp = [...expandedKeys, node.key];
const ids = findNodeById(gData, 'key', node.key)
const expandedKeysTemp = [...expandedKeys, node.key]
ids.forEach((item, index) => {
if (!expandedKeysTemp.includes(item)) {
expandedKeysTemp.push(item);
expandedKeysTemp.push(item)
}
});
setExpandedKeys(expandedKeysTemp);
})
setExpandedKeys(expandedKeysTemp)
} else if (parseInt(e.key) === 2) {
// 插入子节
// 编辑
setEditKey(-1);
setEditValue('');
setParentId(node.key);
setEditKey(-1)
setEditValue('')
setParentId(node.key)
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key);
setEditValue(node.title);
setParentId(false);
setEditKey(node.key)
setEditValue(node.title)
setParentId(false)
} else if (parseInt(e.key) === 4) {
setDelNode(node);
setOpenDel(true);
setDelNode(node)
setOpenDel(true)
}
}
};
const delChapter = async (node) => {
setDelLoading(true);
const childInKey = findParentLevelOne(gData, 'key', node.key);
let current = null;
const delChapter = async node => {
setDelLoading(true)
const childInKey = findParentLevelOne(gData, 'key', node.key)
let current = null
if (childInKey && childInKey.children && childInKey.children.length > 0) {
const index = findTreeToIndex(childInKey.children, 'key', node.key);
const index = findTreeToIndex(childInKey.children, 'key', node.key)
if (childInKey.children.length === 1) {
current = childInKey;
current = childInKey
} else {
// 前面还有
if (index <= childInKey.children.length) {
if (index === 0) {
current = childInKey.children[index + 1];
current = childInKey.children[index + 1]
} else {
current = childInKey.children[index - 1];
current = childInKey.children[index - 1]
}
}
}
} else {
const index = findTreeToIndex(gData, 'key', node.key);
let child = null;
const index = findTreeToIndex(gData, 'key', node.key)
let child = null
if (index === 0) {
child = gData[index + 1];
child = gData[index + 1]
} else {
child = gData[index - 1];
child = gData[index - 1]
}
if (child && child.children && child.children.length > 0) {
current = child.children[0];
current = child.children[0]
} else {
current = child;
current = child
}
}
const data = await chapterDel({ id: node.key });
const data = await chapterDel({ id: node.key })
if (data) {
await dispatch(
setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }),
);
setDelNode({});
setOpenDel(false);
await getChapterTreeList();
await dispatch(setTreeChapter({ saveBookId: id, saveChapterId: current ? current.key : null }))
setDelNode({})
setOpenDel(false)
await getChapterTreeList()
}
setDelLoading(false)
}
setDelLoading(false);
};
useEffect(() => {
if (gData && gData.length > 0 && treeChapter && Object.entries(treeChapter).length > 0) {
if (parseInt(treeChapter.saveBookId) === parseInt(id)) {
const childInKey = findTreeElementByKey(gData, 'key', treeChapter.saveChapterId);
const childInKey = findTreeElementByKey(gData, 'key', treeChapter.saveChapterId)
if (childInKey && Object.entries(childInKey).length > 0) {
setChapterId(parseInt(treeChapter.saveChapterId));
setCheckedKeys([parseInt(treeChapter.saveChapterId)]);
setNowTitle(childInKey.title);
setChapterId(parseInt(treeChapter.saveChapterId))
setCheckedKeys([parseInt(treeChapter.saveChapterId)])
setNowTitle(childInKey.title)
}
}
}
}, [gData, treeChapter]);
}, [gData, treeChapter])
useEffect(() => {
console.log(expandedKeys);
}, [expandedKeys]);
console.log(expandedKeys)
}, [expandedKeys])
// 编辑章节名称
const titleRenderDom = (node) => {
const titleRenderDom = node => {
return (
<div className='tree-customer-item'>
<div className='title'>
<div className='title-node'>{node.title}</div>
<div className="tree-customer-item">
<div className="title">
<div className="title-node">{node.title}</div>
</div>
<div className='opaeration'>
<div className="opaeration">
{quanXian ? (
<Dropdown
trigger={['click']}
overlayClassName='dropmenu_list'
overlayClassName="dropmenu_list"
destroyPopupOnHide={true}
menu={{
items: [
{
key: '1',
label: <Button type='text'>展开全部</Button>,
label: <Button type="text">展开全部</Button>
},
{
key: '2',
label: <Button type='text'>添加子节</Button>,
label: <Button type="text">添加子节</Button>
},
{
key: '4',
label: <Button type='text'>删除</Button>,
key: '5',
label: <Button type="text">设置编者</Button>
},
{
key: '3',
label: <Button type="text">重命名</Button>
},
{
key: '4',
label: <Button type="text">删除</Button>
}
],
onClick: (e) => dropDownMenuHandler(e, node),
}}
>
<div className='dashed'>
onClick: e => dropDownMenuHandler(e, node)
}}>
<div className="dashed">
<span></span>
<span></span>
<span></span>
......@@ -428,31 +402,30 @@ const Examine = () => {
) : (
<Dropdown
trigger={['click']}
overlayClassName='dropmenu_list'
overlayClassName="dropmenu_list"
destroyPopupOnHide={true}
menu={{
items: [
{
key: '1',
label: <Button type='text'>展开全部</Button>,
label: <Button type="text">展开全部</Button>
},
{
key: '2',
label: <Button type='text'>添加子节</Button>,
label: <Button type="text">添加子节</Button>
},
{
key: '3',
label: <Button type='text'>编辑</Button>,
label: <Button type="text">编辑</Button>
},
{
key: '4',
label: <Button type='text'>删除</Button>,
},
label: <Button type="text">删除</Button>
}
],
onClick: (e) => dropDownMenuHandler(e, node),
}}
>
<div className='dashed'>
onClick: e => dropDownMenuHandler(e, node)
}}>
<div className="dashed">
<span></span>
<span></span>
<span></span>
......@@ -462,20 +435,20 @@ const Examine = () => {
)}
</div>
</div>
);
};
)
}
const onDrop = async (info) => {
const dropToGap = info.dropToGap; // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key;
const sideDropNode = info.node.key;
const onDrop = async info => {
const dropToGap = info.dropToGap // true 为同级或最大级 false 为子级
const nowDropNode = info.dragNode.key
const sideDropNode = info.node.key
let data = null;
let data = null
if (dropToGap) {
// 同级
const dragOverGapBottom = info.node.dragOverGapBottom;
const dragOver = info.node.dragOver;
const dragOverGapTop = info.node.dragOverGapTop;
const dragOverGapBottom = info.node.dragOverGapBottom
const dragOver = info.node.dragOver
const dragOverGapTop = info.node.dragOverGapTop
if (!dragOverGapTop && !dragOver && !dragOverGapBottom) {
// 拖到最底部
// 拖拽到最顶层
......@@ -483,8 +456,8 @@ const Examine = () => {
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: 'after',
});
drop_type: 'after'
})
} else {
if (dragOverGapTop) {
// 拖拽到最顶层
......@@ -492,15 +465,15 @@ const Examine = () => {
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: 'before',
});
drop_type: 'before'
})
} else if (dragOverGapBottom) {
data = await dragOrder({
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: 'after',
});
drop_type: 'after'
})
}
}
} else {
......@@ -509,58 +482,46 @@ const Examine = () => {
book_id: id,
drag_node: nowDropNode,
position_node: sideDropNode,
drop_type: 'inner',
});
drop_type: 'inner'
})
}
if (data) {
await getChapterTreeList();
await getChapterTreeList()
}
}
};
return (
<div className='examine'>
<div className='content-box'>
<Row gutter={10} style={{ height: '100%' }} className='book-content-row'>
<Col span={isCollapse ? 1 : 4} className='book-content-tree'>
<div className='border'>
<div className="examine">
<div className="content-box">
<Row gutter={10} style={{ height: '100%' }} className="book-content-row">
<Col span={isCollapse ? 1 : 4} className="book-content-tree">
<div className="border">
{!isCollapse ? (
<>
<Descriptions
layout='vertical'
rootClassName='section-left-top'
layout="vertical"
rootClassName="section-left-top"
bordered
items={[
{
key: '1',
label: (
<>
<Row
gutter={5}
justify={'space-between'}
style={{ alignItems: 'center' }}
>
<Row gutter={5} justify={'space-between'} style={{ alignItems: 'center' }}>
<Col>章节目录</Col>
<Col>
<Button
type='text'
icon={<DiffOutlined />}
onClick={addChapterParent}
></Button>
<Button
type='text'
icon={<MenuFoldOutlined />}
onClick={() => setisCollapse(true)}
></Button>
<Button type="text" icon={<DiffOutlined />} onClick={addChapterParent}></Button>
<Button type="text" icon={<MenuFoldOutlined />} onClick={() => setisCollapse(true)}></Button>
</Col>
</Row>
</>
),
},
)
}
]}
/>
{gData && gData.length > 0 && (
<Tree
className='draggable-tree'
className="draggable-tree"
onSelect={handleSelect}
defaultExpandAll
defaultExpandedKeys={expandedKeys}
......@@ -573,42 +534,34 @@ const Examine = () => {
disabled={loading}
treeData={gData}
onExpand={onExpand}
titleRender={(nodeData) => titleRenderDom(nodeData)}
titleRender={nodeData => titleRenderDom(nodeData)}
/>
)}
</>
) : (
<Descriptions
layout='vertical'
layout="vertical"
bordered
items={[
{
key: '1',
label: (
<>
<Row
gutter={5}
justify={'space-between'}
style={{ alignItems: 'center' }}
>
<Row gutter={5} justify={'space-between'} style={{ alignItems: 'center' }}>
<Col>
<Button
type='text'
icon={<MenuUnfoldOutlined />}
onClick={() => setisCollapse(false)}
></Button>
<Button type="text" icon={<MenuUnfoldOutlined />} onClick={() => setisCollapse(false)}></Button>
</Col>
</Row>
</>
),
},
)
}
]}
/>
)}
</div>
</Col>
<Col span={isCollapse ? 23 : 20} className='book-content-tree'>
<div className='editor-right'>
<Col span={isCollapse ? 23 : 20} className="book-content-tree">
<div className="editor-right">
<Spin spinning={loading}>
<WangEditorCustomer
ref={editorRef}
......@@ -639,9 +592,8 @@ const Examine = () => {
closeIcon={false} // 添加这一行以隐藏关闭按钮
onCancel={() => setEditKey(false)}
classNames={{
wrapper: 'chapter-title-modal',
}}
>
wrapper: 'chapter-title-modal'
}}>
<EditChapterTitle
setEditKey={setEditKey}
editValue={editValue}
......@@ -668,30 +620,28 @@ const Examine = () => {
closeIcon={false} // 添加这一行以隐藏关闭按钮
onCancel={() => setOpenDel(false)}
classNames={{
wrapper: 'chapter-title-modal',
}}
>
wrapper: 'chapter-title-modal'
}}>
<Divider />
<div className=''>确认删除子节【{delNode.title}】?</div>
<div className='' style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div className="">确认删除子节【{delNode.title}】?</div>
<div className="" style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button type='primary' danger loading={delLoading} onClick={() => delChapter(delNode)}>
<Button type="primary" danger loading={delLoading} onClick={() => delChapter(delNode)}>
确定
</Button>
<Button
type='default'
type="default"
onClick={() => {
setDelNode({});
setOpenDel(false);
}}
>
setDelNode({})
setOpenDel(false)
}}>
取消
</Button>
</Space>
</div>
</Modal>
</div>
);
};
)
}
export default Examine;
export default Examine
import Editor from '@/components/editor/index'
export default Editor
......@@ -307,6 +307,10 @@ const baseRouter = [
{
path: 'userinfo',
Component: lazy(() => import('@/pages/user-module/userInfo'))
},
{
path: 'editor',
Component: lazy(() => import('@/pages/editor/index'))
}
]
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论