imgPre.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import React, { useState, useRef, useEffect } from 'react';
  2. import ReactQuill from 'react-quill';
  3. import 'react-quill/dist/quill.snow.css';
  4. import { Form } from 'antd';
  5. import { EyeOutlined, CloseOutlined } from '@ant-design/icons';
  6. // 提取纯文本工具函数
  7. const extractPlainText = (html: string): string => {
  8. if (!html) return '';
  9. const tempDiv = document.createElement('div');
  10. tempDiv.innerHTML = html;
  11. return tempDiv.textContent || '';
  12. };
  13. interface LinkData {
  14. id: string;
  15. originalText: string;
  16. url: string;
  17. isDeleted: boolean;
  18. }
  19. const PersistentImagePreviewEditor = () => {
  20. const [form] = Form.useForm();
  21. const [editorHtml, setEditorHtml] = useState<string>(`
  22. <p>系统工作人员联系方式</p>
  23. <p>系统工作人员联系方式表:
  24. <a href="http://xia0miduo.gicp.net:9000/papbtest//pdf/a2965738189226381312/a2966033634305507328/【示意图序号_a2966033634305507328_3】.jpg" target="_blank">【示意图序号_a2966033634305507328_2】</a>,
  25. 第二个标签 <a href="https://jkeckms.ryuiso.com/chat-bg.jpg" target="_blank">【示意图序号_a2966033634305507328_2】</a>,
  26. 第三个标签 <a href="https://jkeckms.ryuiso.com/chat-bg.jpg" target="_blank">【示意图序号_a2966033634305507328_2】</a>
  27. </p>
  28. `);
  29. const [plainText, setPlainText] = useState<string>(extractPlainText(editorHtml));
  30. const [linkDataList, setLinkDataList] = useState<LinkData[]>([]);
  31. const [activeImageUrl, setActiveImageUrl] = useState<string | null>(null);
  32. // 跟踪鼠标是否在链接或预览区上
  33. const [isHoveringLink, setIsHoveringLink] = useState(false);
  34. const [isHoveringPreview, setIsHoveringPreview] = useState(false);
  35. const editorContainerRef = useRef<HTMLDivElement>(null);
  36. const previewRef = useRef<HTMLDivElement>(null);
  37. // 初始化链接数据
  38. useEffect(() => {
  39. const parser = new DOMParser();
  40. const doc = parser.parseFromString(editorHtml, 'text/html');
  41. const links = doc.getElementsByTagName('a');
  42. const linksData: LinkData[] = [];
  43. Array.from(links).forEach((link, index) => {
  44. linksData.push({
  45. id: `link-${index}-${Date.now()}`,
  46. originalText: link.textContent || '',
  47. url: link.href,
  48. isDeleted: false
  49. });
  50. });
  51. setLinkDataList(linksData);
  52. }, []);
  53. // 处理内容变化
  54. const handleEditorChange = (html: string) => {
  55. setEditorHtml(html);
  56. const text = extractPlainText(html);
  57. setPlainText(text);
  58. form.setFieldsValue({ slice_text: text });
  59. if (linkDataList.length > 0) {
  60. const updatedLinks = [...linkDataList];
  61. updatedLinks.forEach(link => {
  62. const feature = link.originalText.slice(0, 5);
  63. link.isDeleted = !text.includes(feature);
  64. });
  65. setLinkDataList(updatedLinks);
  66. }
  67. };
  68. // 处理鼠标悬停链接事件
  69. useEffect(() => {
  70. if (!editorContainerRef.current) return;
  71. const handleMouseOverLink = (e: MouseEvent) => {
  72. const target = e.target as HTMLAnchorElement;
  73. if (target.tagName === 'A') {
  74. const url = target.href;
  75. const isKnownLink = linkDataList.some(link =>
  76. !link.isDeleted && url.includes(link.url)
  77. );
  78. if (isKnownLink) {
  79. console.log('图片地址:', url);
  80. setActiveImageUrl(url);
  81. setIsHoveringLink(true);
  82. }
  83. }
  84. };
  85. const handleMouseOutLink = () => {
  86. setIsHoveringLink(false);
  87. };
  88. const container = editorContainerRef.current;
  89. container.addEventListener('mouseover', handleMouseOverLink);
  90. container.addEventListener('mouseout', handleMouseOutLink);
  91. return () => {
  92. container.removeEventListener('mouseover', handleMouseOverLink);
  93. container.removeEventListener('mouseout', handleMouseOutLink);
  94. };
  95. }, [linkDataList]);
  96. // 控制预览区显示/隐藏的核心逻辑
  97. useEffect(() => {
  98. // 当鼠标既不在链接上也不在预览区上时,才隐藏预览
  99. if (!isHoveringLink && !isHoveringPreview) {
  100. setActiveImageUrl(null);
  101. }
  102. }, [isHoveringLink, isHoveringPreview]);
  103. // 配置编辑器(无工具栏)
  104. const modules = {
  105. toolbar: false
  106. };
  107. return (
  108. <Form form={form} initialValues={{ slice_text: plainText }}>
  109. <Form.Item
  110. name="slice_text"
  111. rules={[{ required: true, message: '内容不能为空' }]}
  112. >
  113. {/* 容器:编辑器 + 右侧图片预览区 */}
  114. <div style={{
  115. display: 'flex',
  116. gap: '16px',
  117. alignItems: 'flex-start'
  118. }}>
  119. {/* 500x500的编辑器 */}
  120. <div
  121. ref={editorContainerRef}
  122. style={{
  123. width: '500px',
  124. height: '500px',
  125. border: '1px solid #d9d9d9',
  126. borderRadius: '4px',
  127. overflow: 'hidden'
  128. }}
  129. >
  130. <ReactQuill
  131. value={editorHtml}
  132. onChange={handleEditorChange}
  133. modules={modules}
  134. style={{ height: '100%' }}
  135. placeholder="请输入内容..."
  136. />
  137. </div>
  138. {/* 右侧图片预览区(固定位置) */}
  139. {activeImageUrl && (
  140. <div
  141. ref={previewRef}
  142. style={{
  143. width: '300px',
  144. height: '500px',
  145. backgroundColor: 'white',
  146. borderRadius: '8px',
  147. boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
  148. padding: '12px',
  149. border: '1px solid #eee',
  150. display: 'flex',
  151. flexDirection: 'column'
  152. }}
  153. onMouseOver={() => setIsHoveringPreview(true)}
  154. onMouseOut={() => setIsHoveringPreview(false)}
  155. >
  156. <div style={{
  157. display: 'flex',
  158. justifyContent: 'space-between',
  159. alignItems: 'center',
  160. marginBottom: '12px',
  161. paddingBottom: '8px',
  162. borderBottom: '1px solid #f0f0f0'
  163. }}>
  164. <div style={{ display: 'flex', alignItems: 'center' }}>
  165. <EyeOutlined style={{ color: '#1890ff', marginRight: '8px' }} />
  166. <span style={{ fontSize: '14px', color: '#333' }}>图片预览</span>
  167. </div>
  168. <CloseOutlined
  169. style={{ color: '#999', cursor: 'pointer', fontSize: '16px' }}
  170. onClick={() => setActiveImageUrl(null)}
  171. />
  172. </div>
  173. <div style={{
  174. flex: 1,
  175. display: 'flex',
  176. alignItems: 'center',
  177. justifyContent: 'center',
  178. overflow: 'hidden'
  179. }}>
  180. <img
  181. src={activeImageUrl}
  182. alt="示意图预览"
  183. style={{
  184. maxWidth: '100%',
  185. maxHeight: '100%',
  186. objectFit: 'contain'
  187. }}
  188. onError={() => setActiveImageUrl(null)}
  189. />
  190. </div>
  191. </div>
  192. )}
  193. </div>
  194. </Form.Item>
  195. </Form>
  196. );
  197. };
  198. export default PersistentImagePreviewEditor;