react-quill 富文本组件编写和应用

  •  index.tsx文件
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><Modalvisible={isModalVisible}onCancel={() => setIsModalVisible(false)}onOk={submitContent}><RichEditor ref={editorRef} /></Modal><Button onClick={openEditModal}>打开编辑对话框</Button><Modalvisible={isEditModalVisible}onCancel={() => setIsEditModalVisible(false)}onOk={submitEditContent}><RichEditor ref={editorRef} initialContent={contentHtml} /></Modal></div>);
};export default AnchorTouchHistory;
  • RichEditor.tsx
// 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 (<><ReactQuillref={quillRef}value={editorValue}onChange={handleEditorChange}modules={modules}formats={formats}/><Modaltitle="插入图片"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 && (<divstyle={{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"><Inputplaceholder="图片链接"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 && (<divstyle={{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><Inputplaceholder="设置宽度"value={width}onChange={(e) => setWidth(e.target.value)}onBlur={handleWidthBlur}style={{ marginTop: 10 }}/><Inputplaceholder="设置高度"value={height}onChange={(e) => setHeight(e.target.value)}onBlur={handleHeightBlur}style={{ marginTop: 10 }}/><div style={{ marginTop: 10, textAlign: 'right' }}><Buttontype="primary"onClick={handleOk}disabled={uploadMode === 'local'? !currentFile: !imageUrl || !isImageValid // 当图片无效时禁用按钮}>确认</Button><Button onClick={handleCancel} style={{ marginLeft: 10 }}>取消</Button></div></Modal><Modaltitle="添加链接"visible={isLinkModalVisible}onCancel={handleLinkCancel}onOk={handleLinkOk}>{urlError && <Alert message={urlError} type="error" />}<Inputplaceholder="链接地址"value={linkUrl}onChange={handleLinkUrlChange}style={{ marginBottom: 10 }}/><Inputplaceholder="备注"value={linkText}onChange={(e) => setLinkText(e.target.value)}/></Modal></>);
});export default RichEditor;
  • quillEditor.css
/* 字体风格 */
/* 处理下拉字体选择器中选项的文本溢出并显示省略号 */
.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;
}
  • golang后端接口,获取
    TxyCosConf:SecretId: 'xxxxx'SecretKey: 'xxxxx'Bucket: 'xxxxx'Region: 'xxxx'
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;});
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于mybatis-plus历史背景下的多租户平台改造

前言 别误会&#xff0c;本篇【并不是】 要用mybatis-plus自身的多租户方案&#xff1a;在表中加一个tenant_id字段来区分不同的租户数据。并不是的&#xff01; 而是在假设业务系统已经使用mybatis-plus多数据源的前提下&#xff0c;如何实现业务数据库隔开的多租户系统。 这…

大数据技术实训:Hadoop完全分布式运行模式配置

准备&#xff1a; 1&#xff09;准备3台客户机&#xff08;关闭防火墙、静态ip、主机名称&#xff09; 2&#xff09;安装JDK 3&#xff09;配置环境变量 4&#xff09;安装Hadoop 5&#xff09;配置环境变量 6&#xff09;配置集群 7&#xff09;单点启动 8&#xff09;配置ss…

计算机网络(五)运输层

5.1、运输层概述 概念 进程之间的通信 从通信和信息处理的角度看&#xff0c;运输层向它上面的应用层提供通信服务&#xff0c;它属于面向通信部分的最高层&#xff0c;同时也是用户功能中的最低层。 当网络的边缘部分中的两个主机使用网络的核心部分的功能进行端到端的通信时…

可视化-Visualization

可视化-Visualization 1.Introduction Visualization in Open CASCADE Technology is based on the separation of: on the one hand – the data which stores the geometry and topology of the entities you want to display and select, andon the other hand – its pr…

FPGA自学之路:到底有多崎岖?

FPGA&#xff0c;即现场可编程门阵列&#xff0c;被誉为硬件世界的“瑞士军刀”&#xff0c;其灵活性和可编程性让无数开发者为之倾倒。但谈及FPGA的学习难度&#xff0c;不少人望而却步。那么&#xff0c;FPGA自学之路到底有多崎岖呢&#xff1f; 几座大山那么高&#xff1f;…

关于扫描模型 拓扑 和 传递贴图工作流笔记

关于MAYA拓扑和传递贴图的操作笔记 一、拓扑低模: 1、拓扑工作区位置: 1、准备出 目标 高模。 (高模的状态如上 ↑ )。 2、打开顶点吸附,和建模工具区,选择四边形绘制. 2、拓扑快捷键使…