import React, { useRef, useState } from 'react';
import { Modal, Button } from 'antd';
import RichEditor from './RichEditor';
const AnchorTouchHistory: React.FC = () => {
const editorRef = useRef<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [contentHtml, setContentHtml] = useState('<p>heheda</p>' );
const openAddModal = () => setIsModalVisible(true);
const submitContent = () => {
const content = editorRef.current?.getRichContent();
console.log(content);
setIsModalVisible(false);
editorRef.current?.resetContent();
};
const openEditModal = () => setIsEditModalVisible(true);
const submitEditContent = () => {
const content = editorRef.current?.getRichContent();
console.log(content);
setIsEditModalVisible(false);
editorRef.current?.resetContent();
};
return (
<div>
<Button onClick={openAddModal}>打开添加对话框</Button>
<Modal
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
onOk={submitContent}
>
<RichEditor ref={editorRef} />
</Modal>
<Button onClick={openEditModal}>打开编辑对话框</Button>
<Modal
visible={isEditModalVisible}
onCancel={() => setIsEditModalVisible(false)}
onOk={submitEditContent}
>
<RichEditor ref={editorRef} initialContent={contentHtml} />
</Modal>
</div>
);
};
export default AnchorTouchHistory;
// RichEditor.tsx
import React, { useState, useEffect, useRef, useMemo, forwardRef } from 'react';
import ReactQuill, { Quill } from 'react-quill';
import COS from 'cos-js-sdk-v5';
import 'react-quill/dist/quill.snow.css';
import { Modal, Input, Upload, Button, Tabs, Alert } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import ImageResize from 'quill-image-resize-module-react';
import { getTxyCosConf } from '@/services/anchor-touch/history';
import '@/styles/quillEditor.css';
// 引入 Quill 的基础类
const BlockEmbed = Quill.import('blots/block/embed');
// 自定义图片 Blot,支持宽度和高度属性
class CustomImage extends BlockEmbed {
static create(value) {
const node = super.create();
node.setAttribute('src', value.src);
if (value.width) {
node.setAttribute('width', value.width);
}
if (value.height) {
node.setAttribute('height', value.height);
}
return node;
}
static value(node) {
return {
src: node.getAttribute('src'),
width: node.getAttribute('width'),
height: node.getAttribute('height'),
};
}
}
CustomImage.blotName = 'customImage';
CustomImage.tagName = 'img';
Quill.register(CustomImage);
Quill.register('modules/imageResize', ImageResize);
const RichEditor = forwardRef((props, ref) => {
const { value = '', onChange } = props;
const [editorValue, setEditorValue] = useState(value);
const [isCosReady, setIsCosReady] = useState(false);
const quillRef = useRef<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLinkModalVisible, setIsLinkModalVisible] = useState(false);
const [bucket, setBucket] = useState('');
const [region, setRegion] = useState('');
const [cos, setCos] = useState<COS | null>(null);
const [width, setWidth] = useState('');
const [height, setHeight] = useState('');
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [originalWidth, setOriginalWidth] = useState<number | null>(null);
const [originalHeight, setOriginalHeight] = useState<number | null>(null);
const [imageUrl, setImageUrl] = useState('');
const [uploadMode, setUploadMode] = useState<'local' | 'url'>('local');
const [linkUrl, setLinkUrl] = useState('');
const [linkText, setLinkText] = useState('');
const [urlError, setUrlError] = useState('');
const [isImageValid, setIsImageValid] = useState(false);
useEffect(() => {
setEditorValue(value);
}, [value]);
useEffect(() => {
const fetchCosConfig = async () => {
try {
const response = await getTxyCosConf();
setBucket(response.data.bucket);
setRegion(response.data.region);
const cosInstance = new COS({
SecretId: response.data.secretid,
SecretKey: response.data.secretkey,
});
setCos(cosInstance);
setIsCosReady(true);
} catch (error) {
console.error('获取 COS 配置失败:', error);
}
};
fetchCosConfig();
}, []);
const handleEditorChange = (content: string) => {
setEditorValue(content);
if (onChange) {
onChange(content);
}
};
const showImageUploadModal = () => {
setIsModalVisible(true);
};
const showLinkModal = () => {
setIsLinkModalVisible(true);
};
const handleLinkOk = () => {
if (!linkUrl.startsWith('http://') && !linkUrl.startsWith('https://')) {
setUrlError('链接地址格式不正确,请输入有效的链接地址。');
return;
}
const editor = quillRef.current?.getEditor();
if (editor) {
editor.focus();
const range = editor.getSelection();
const position = range ? range.index : editor.getLength();
editor.insertText(position, linkText, 'link', linkUrl);
editor.setSelection(position + linkText.length);
handleLinkCancel();
}
};
const handleLinkCancel = () => {
setIsLinkModalVisible(false);
setLinkUrl('');
setLinkText('');
setUrlError('');
};
const handleOk = () => {
if (uploadMode === 'local') {
if (!currentFile || !cos) {
handleCancel();
return;
}
const uniqueFileName = `${Date.now()}_${currentFile.name}`;
cos.uploadFile(
{
Bucket: bucket,
Region: region,
Key: uniqueFileName,
Body: currentFile,
SliceSize: 1024 * 1024,
},
(err, data) => {
if (err) {
console.error('上传失败:', err);
} else {
const imageUrl = `https://${data.Location}`;
insertImageToEditor(imageUrl);
}
}
);
} else {
insertImageToEditor(imageUrl);
}
};
const insertImageToEditor = (imageUrl: string) => {
const editor = quillRef.current?.getEditor();
if (editor) {
editor.focus();
const range = editor.getSelection();
const position = range ? range.index : editor.getLength();
editor.insertEmbed(position, 'customImage', {
src: imageUrl,
width: width,
height: height,
});
editor.setSelection(position + 1);
handleCancel();
}
};
const handleCancel = () => {
setIsModalVisible(false);
setPreviewUrl(null);
setCurrentFile(null);
setWidth('');
setHeight('');
setImageUrl('');
};
const beforeUpload = (file: File) => {
if (!file.type.startsWith('image/')) {
console.error('不是有效的图像文件');
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const preview = e.target?.result as string;
setPreviewUrl(preview);
setCurrentFile(file);
const img = new Image();
img.onload = () => {
setOriginalWidth(img.naturalWidth);
setOriginalHeight(img.naturalHeight);
setWidth(img.naturalWidth.toString());
setHeight(img.naturalHeight.toString());
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
};
img.src = preview;
};
reader.onerror = (error) => {
console.error('文件读取失败:', error);
};
reader.readAsDataURL(file);
return false;
};
const handleWidthBlur = () => {
const widthValue = parseFloat(width);
if (isNaN(widthValue)) {
console.error('无效的宽度: ', width);
return;
}
if (originalWidth && originalHeight && widthValue > 0) {
const calculatedHeight = (widthValue / originalWidth) * originalHeight;
setHeight(calculatedHeight.toFixed(0).toString());
}
};
const handleHeightBlur = () => {
const heightValue = parseFloat(height);
if (originalWidth && originalHeight && heightValue > 0) {
const calculatedWidth = (heightValue / originalHeight) * originalWidth;
setWidth(calculatedWidth.toFixed(0).toString());
}
};
const handleLinkUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const url = e.target.value;
setLinkUrl(url);
if (url.startsWith('http://') || url.startsWith('https://')) {
setUrlError('');
} else if (url) {
setUrlError('链接地址格式不正确,请输入有效的链接地址。');
}
};
const sizes = [false, '14px', '16px', '18px', '20px', '22px', '26px', '28px', '30px'];
const Size = Quill.import('formats/size');
Size.whitelist = sizes;
const fonts = [
'SimSun',
'SimHei',
'Microsoft-YaHei',
'KaiTi',
'FangSong',
'Arial',
'Times-New-Roman',
'sans-serif',
];
const Font = Quill.import('formats/font');
Font.whitelist = fonts;
Quill.register(Font, true);
const modules = useMemo(
() => ({
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ size: sizes }],
[{ header: [1, 2, 3, 4, 5, false] }],
[{ color: [] }, { background: [] }],
['link', 'image', 'clean'],
],
handlers: {
image: showImageUploadModal,
link: showLinkModal,
},
},
imageResize: {
modules: ['DisplaySize'],
handleStyles: {
backgroundColor: 'transparent',
border: 'none',
},
resizeWidth: false,
},
}),
[cos]
);
const formats = [
'font',
'header',
'size',
'bold',
'italic',
'underline',
'strike',
'list',
'bullet',
'link',
'customImage', // 使用自定义的 customImage
'width',
'height',
'color',
'background',
];
if (!isCosReady) {
return <div>加载中...</div>;
}
return (
<>
<ReactQuill
ref={quillRef}
value={editorValue}
onChange={handleEditorChange}
modules={modules}
formats={formats}
/>
<Modal
title="插入图片"
visible={isModalVisible}
onCancel={handleCancel}
footer={null}
>
<Tabs defaultActiveKey="local" onChange={(key) => setUploadMode(key as 'local' | 'url')}>
<Tabs.TabPane tab="本地图片" key="local">
<Upload beforeUpload={beforeUpload} showUploadList={false}>
<Button icon={<UploadOutlined />}>选择图片</Button>
</Upload>
{previewUrl && (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 10,
width: 150,
height: 150,
overflow: 'hidden',
border: '1px solid #e8e8e8',
}}
>
<img src={previewUrl} alt="预览" style={{ width: 150, maxHeight: '100%' }} />
</div>
)}
</Tabs.TabPane>
<Tabs.TabPane tab="链接图片" key="url">
<Input
placeholder="图片链接"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
onBlur={() => {
const img = new Image();
img.onload = () => {
setOriginalWidth(img.naturalWidth);
setOriginalHeight(img.naturalHeight);
setWidth(img.naturalWidth.toString());
setHeight(img.naturalHeight.toString());
setPreviewUrl(imageUrl);
setIsImageValid(true); // 图片有效
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
setPreviewUrl(null);
setIsImageValid(false); // 图片无效
};
img.src = imageUrl;
}}
/>
{previewUrl && (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 10,
width: 150,
height: 150,
overflow: 'hidden',
border: '1px solid #e8e8e8',
}}
>
<img src={previewUrl} alt="预览" style={{ width: 150, maxHeight: '100%' }} />
</div>
)}
</Tabs.TabPane>
</Tabs>
<Input
placeholder="设置宽度"
value={width}
onChange={(e) => setWidth(e.target.value)}
onBlur={handleWidthBlur}
style={{ marginTop: 10 }}
/>
<Input
placeholder="设置高度"
value={height}
onChange={(e) => setHeight(e.target.value)}
onBlur={handleHeightBlur}
style={{ marginTop: 10 }}
/>
<div style={{ marginTop: 10, textAlign: 'right' }}>
<Button
type="primary"
onClick={handleOk}
disabled={
uploadMode === 'local'
? !currentFile
: !imageUrl || !isImageValid // 当图片无效时禁用按钮
}
>
确认
</Button>
<Button onClick={handleCancel} style={{ marginLeft: 10 }}>
取消
</Button>
</div>
</Modal>
<Modal
title="添加链接"
visible={isLinkModalVisible}
onCancel={handleLinkCancel}
onOk={handleLinkOk}
>
{urlError && <Alert message={urlError} type="error" />}
<Input
placeholder="链接地址"
value={linkUrl}
onChange={handleLinkUrlChange}
style={{ marginBottom: 10 }}
/>
<Input
placeholder="备注"
value={linkText}
onChange={(e) => setLinkText(e.target.value)}
/>
</Modal>
</>
);
});
export default RichEditor;
/* 字体风格 */
/* 处理下拉字体选择器中选项的文本溢出并显示省略号 */
.ql-snow .ql-picker.ql-font .ql-picker-label::before {
width: 88px; /* 设置下拉选项宽度,可以根据需要调整 */
white-space: nowrap; /* 不换行显示 */
overflow: hidden; /* 隐藏溢出部分 */
text-overflow: ellipsis; /* 使用省略号显示溢出文本 */
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="SimSun"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="SimSun"]::before {
content: "宋体";
font-family: "SimSun";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="SimHei"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="SimHei"]::before {
content: "黑体";
font-family: "SimHei";
}
.ql-snow
.ql-picker.ql-font
.ql-picker-label[data-value="Microsoft-YaHei"]::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-item[data-value="Microsoft-YaHei"]::before {
content: "微软雅黑";
font-family: "Microsoft YaHei";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="KaiTi"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="KaiTi"]::before {
content: "楷体";
font-family: "KaiTi";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="FangSong"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="FangSong"]::before {
content: "仿宋";
font-family: "FangSong";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="Arial"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="Arial"]::before {
content: "Arial";
font-family: "Arial";
}
.ql-snow
.ql-picker.ql-font
.ql-picker-label[data-value="Times-New-Roman"]::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-item[data-value="Times-New-Roman"]::before {
content: "Times New Roman";
font-family: "Times New Roman";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="sans-serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="sans-serif"]::before {
content: "sans-serif";
font-family: "sans-serif";
}
.ql-font-SimSun { font-family: "SimSun"; }
.ql-font-SimHei { font-family: "SimHei"; }
.ql-font-Microsoft-YaHei { font-family: "Microsoft YaHei"; }
.ql-font-KaiTi { font-family: "KaiTi"; }
.ql-font-FangSong { font-family: "FangSong"; }
.ql-font-Arial { font-family: "Arial"; }
.ql-font-Times-New-Roman { font-family: "Times New Roman"; }
.ql-font-sans-serif { font-family: "sans-serif"; }
/* 字体大小 */
.ql-snow .ql-picker.ql-size .ql-picker-label::before { content: "字体大小"; }
.ql-snow .ql-picker.ql-size .ql-picker-item::before { content: "常规"; }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before{
content: "14px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before{
content: "16px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before{
content: "18px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before{
content: "20px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="22px"]::before{
content: "22px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="26px"]::before{
content: "26px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="30px"]::before {
content: "30px";
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
content: "14px";
font-size: 14px;
}
.ql-size-14px { font-size: 14px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
content: "16px";
font-size: 16px;
}
.ql-size-16px { font-size: 16px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
content: "18px";
font-size: 18px;
}
.ql-size-18px { font-size: 18px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
content: "20px";
font-size: 20px;
}
.ql-size-20px { font-size: 20px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="22px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="22px"]::before {
content: "22px";
font-size: 22px;
}
.ql-size-22px { font-size: 22px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="26px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="26px"]::before {
content: "26px";
font-size: 26px;
}
.ql-size-26px { font-size: 26px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before {
content: "28px";
font-size: 28px;
}
.ql-size-28px { font-size: 28px; }
/* .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="30px"]::before, */
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="30px"]::before {
content: "30px";
font-size: 30px;
}
.ql-size-30px { font-size: 30px; }
/* 段落大小 */
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "常规";
}
/* .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, */
.ql-snow .ql-picker.ql-header .ql-picker-label::before {
content: "标题大小";
}
/* 默认设置 */
.ql-snow .ql-editor { font-size: 14px; }
/* 查看样式 */
.view-editor .ql-toolbar { display: none; }
.view-editor .ql-container.ql-snow { border: 0; }
.view-editor .ql-container.ql-snow .ql-editor { padding: 0; }
/* 编辑样式 */
.edit-editor .ql-toolbar { display: block; }
.edit-editor .ql-container.ql-snow {
border: 1px solid #ccc;
min-height: inherit;
}
import {request} from "@@/plugin-request/request";
export function getTxyCosConf() {
return request('/api/v1/xxxx/getTxyCosConf', {
method: 'get',
})
.then(response => {
return response;
})
.catch(error => {
console.error('Error get data:', error);
throw error;
});
}