/** * 1.引入组件 import RichText from '@wisdom-components/RichText'; * 示例:<RichText content={this.state.content} personList={this.state.personList} placeholder={'placeholder属性值'} onChange={val => { this.setState({ content: val }); }} onChangeFile={arr => { this.setState({ fileList: arr }); }} fileList={this.state.fileList} projectId={19} ref={this.myRichText} /> * * 2.传递方法 onChange 每次更改内容回调 * * 3.传值接收 可选值 projectId 项目id,根据项目id获取项目参与人员, * 可选值 personList 人员列表 示例:[{userId:1,userName:'xxx'}] * 可选值 config 框架wangEditor的配置参数 * * 4.注意事项 projectId和personList只用传一个,projectId优先级高于personList * content内容如果不是初始有的,可调用setHtml设置内容 * * 2022-03-21新增图片预览,附件上传功能 * 新增方法:onChangeFile 每次附件更改回调 若不传则不显示附件上传按钮 * fileList 附件列表 示例:[{name:'xxx.jpg',type:'image/jpg',size:8192,path:'xxxx'}] * 其中name和path是必传的,type为图片可以预览,其它类型文件直接下载 * * 2022-04-29 修改@人员列表逻辑 * personList 传任务相关人员列表(如 创建、负责、跟进人),同时传入projectId,personList * 下拉列表默认显示为任务相关人员,加项目人员(做了去重,任务相关人员在最上面) * @搜索时,搜索全部人员 */ import { Image, message, Spin } from 'antd'; import classNames from 'classnames'; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import request from 'umi-request'; import FileListItem from './fileListItem'; import styles from './index.less'; import WangEditor from './wangEditor.js'; const baseUrl = ''; const API = { POST_UPLOADERFILES: `${baseUrl}/PandaWorkFlow/WorkFlow/AccountManage/UploaderFiles`, GET_DOWNLOADFILES: `${baseUrl}/PandaWorkFlow/WorkFlow/AccountManage/DownloadFiles`, GET_GETALLPERSONNELS: `${baseUrl}/PandaInformatization/PandaWork/MeetingTask/GetAllPersonnels`, // 获取人员 GET_WORKHOURUSERLIST: `${baseUrl}/PandaInformatization/PandaWork/ProjectManage/GetWorkHourUserList`, //根据项目id获取项目参与人员 }; let editor = null; let range; const selection = window.getSelection(); let startOffset; let tempList = []; let allPeople = []; // 全部人员 let selectPersonIndex; let selectPerson = []; const RichText = forwardRef((props, ref) => { const [loading, setLoading] = useState(false); const [zIndex, setZIndex] = useState(500); const [selectIndex, setSelectIndex] = useState(null); const [selectList, setSelectList] = useState([]); const [selectSearchList, setSelectSearchList] = useState([]); const [fileList, setFileList] = useState([]); const [imgVisible, setImgVisible] = useState(false); const [imgPreviewSrc, setImgPreviewSrc] = useState(''); const richTextRef = useRef(); const selectBoxRef = useRef(); const fileInputRef = useRef(); const getData = () => { request(API.GET_WORKHOURUSERLIST, { method: 'get', params: { projectId: props.projectId }, }).then((res) => { setSelectList(res.data || []); }); }; // 获取全部人员信息 const getAllPeople = async () => { request(API.GET_GETALLPERSONNELS, { method: 'get', params: {}, }).then((res) => { allPeople = res?.data?.data || []; }); }; // 图片上传 const uploadImg = (file) => { const formData = new FormData(); formData.append('file', file); setLoading(true); request(API.POST_UPLOADERFILES, { method: 'POST', data: formData, }) .then((res) => { if (!res.data) { setLoading(false); return; } const img = res.data.replace(/[\\ \/=]/g, '/'); const imgHtml = ` <img contenteditable="false" style="display: block;" width="50%" src="${API.GET_DOWNLOADFILES}?filePath=${img}" > `; editor.cmd.do('insertHTML', imgHtml); setLoading(false); }) .catch((err) => { setLoading(false); }); }; const init = () => { const { BtnMenu } = WangEditor; editor = new WangEditor('#RichTextToolbar', '#RichTextContainer'); // 自定义菜单 const menuKey = 'fileMenuKey'; if (props.onChangeFile) { class InsertABCMenu extends BtnMenu { constructor(editor) { const $elem = WangEditor.$( `<div class="w-e-menu"> <i class="w-e-icon-link"> </i> </div>`, ); super($elem, editor); } // 菜单点击事件 clickHandler() { // 触发选择文件 fileInputRef.current.click(); } // 菜单激活状态 tryChangeActive() { // this.active(); // 菜单激活 } } editor.menus.extend(menuKey, InsertABCMenu); } editor.config = Object.assign( {}, editor.config, { placeholder: props.placeholder ?? '', focus: false, pasteFilterStyle: true, // 忽略粘贴样式 pasteIgnoreImg: true, // 忽略粘贴的图片 styleWithCSS: false, zIndex: 500, menus: [ 'bold', 'fontSize', 'italic', 'underline', 'strikeThrough', 'foreColor', 'backColor', 'list', 'justify', 'table', menuKey, 'undo', 'redo', 'image', ], }, props.config || {}, ); setZIndex(Number(editor.config.zIndex)); // 内容变更 editor.config.onchange = (newHtml, e) => { props.onChange(newHtml); }; // 粘贴前置处理 editor.config.pasteTextHandle = (pasteStr) => pasteStr; editor.config.onblur = (newHtml) => { selectBoxRef.current.style.display = 'none'; }; // 点击事件 editor.txt.eventHooks.clickEvents.push((e) => { // 图片预览 // 已弃用 if (e.target.getAttribute('type') === 'preview') { const imgSrc = e.target?.parentNode?.parentNode ?.getElementsByTagName('img')?.[0] ?.getAttribute('src'); setImgPreviewSrc(imgSrc); setImgVisible(true); } // 关闭选人的下拉框 selectBoxRef.current.style.display = 'none'; }); editor.txt.eventHooks.onPreviewEvents.push((link) => { // 图片预览 if (link) { setImgPreviewSrc(link); setImgVisible(true); } }); editor.txt.eventHooks.imgClickEvents.push((e) => {}); // 粘贴图片上传 editor.txt.eventHooks.pasteEvents.push((e) => { const file = e?.clipboardData?.items[0]?.getAsFile() || null; if (!file) return; uploadImg(file); }); editor.create(); editor.txt.html(props.content || ''); richTextRef.current.onkeydown = keyDownEvent; richTextRef.current.addEventListener('input', (e) => { if (range) { // 判断节点是否在选区及光标是否在@后面 const type = selection.containsNode(selection.getRangeAt(0).commonAncestorContainer, false); if (!type || startOffset > selection.focusOffset) { closeList(); return; } range.setEnd(selection.getRangeAt(0).commonAncestorContainer, selection.focusOffset); const str = range.toString() || ''; moveListBox(); handleChange(str, tempList); } if (e.data !== '@') return; if (range) { closeList(); } range = document.createRange(); startOffset = selection.focusOffset; range.setStart(selection.getRangeAt(0).commonAncestorContainer, selection.focusOffset); selection.addRange(range); moveListBox(); // 清空搜索 handleChange('', tempList); }); }; // 跟据光标位置移动下拉框 const moveListBox = () => { // 获取光标位置 const cursor = window?.getSelection()?.getRangeAt(0)?.getBoundingClientRect() || null; const containerRect = document.querySelector('#RichText').getBoundingClientRect(); selectBoxRef.current.style.display = 'block'; selectBoxRef.current.style.left = `${parseInt(cursor.x - containerRect.x, 10) + 5}px`; selectBoxRef.current.style.top = `${parseInt(cursor.y - containerRect.y, 10) + 25}px`; }; // 键盘事件 const keyDownEvent = (evet) => { // 上下方向键 if (evet.key === 'ArrowDown' || evet.key === 'ArrowUp') { if (selectBoxRef.current?.style?.display === 'block') { evet.preventDefault(); const max = selectBoxRef.current.querySelectorAll('.selectItem')?.length || 1000; let val = selectPersonIndex; if (evet.key === 'ArrowDown') { if (!val && val != 0) { val = 0; } else { val += 1; } } if (evet.key === 'ArrowUp') val -= 1; if (isNaN(val) || !val || val < 0) val = 0; if (val > max - 1) val = max - 1; selectPersonIndex = val; setSelectIndex(selectPersonIndex); } } if (evet.key === 'Enter') { // 解决无法回车换行的bug if (selectBoxRef.current.style.display === 'block') { evet.preventDefault(); if (selectPerson[selectPersonIndex]) { onSelect(selectPerson[selectPersonIndex]); } return false; } } }; useEffect(() => { richTextRef.current && richTextRef.current.removeEventListener('input', (e) => {}); init(); getAllPeople(); return () => { richTextRef.current && richTextRef.current.removeEventListener('input', (e) => {}); editor && editor.destroy(); editor = null; }; }, []); useEffect(() => { selectPersonIndex = null; setSelectIndex(null); selectPerson = selectSearchList || []; }, [selectSearchList]); useEffect(() => { if (props.projectId) getData(); }, [props.projectId]); useEffect(() => { const keys = []; const arr = []; if (props.personList) { props.personList.forEach((i) => { i.userId = Number(i.userId); if (!keys.includes(i.userId)) { keys.push(i.userId); arr.push(i); } }); } if (selectList) { selectList.forEach((i) => { i.userId = Number(i.userId); if (!keys.includes(i.userId)) { arr.push(i); keys.push(i.userId); } }); } tempList = arr; setSelectSearchList(arr); }, [selectList, props.personList]); useEffect(() => { setFileList(props.fileList); }, [props.fileList]); const getHtml = (val) => editor.txt.html(); // 获取文本,不含标签 const getText = (val) => editor.txt.text(); // 清除 const onClear = () => { editor.txt.clear(); }; // 设置内容 const setHtml = (val) => { editor.txt.html(val || ''); }; // 关闭人员下拉选框 const closeList = () => { selectPersonIndex = null; setSelectIndex(null); selectBoxRef.current.style.display = 'none'; if (range) { selection.removeRange(range); range = null; } }; // @某人 const onSelect = (item) => { if (range) { range.deleteContents(); // 删除前一个@符号 editor.cmd.do('delete'); const _html = `<span><span data-userId="${item.userId}" data-type="person" >@${item.userName}</span><span> </span></span>`; editor.cmd.do('insertElem', _html); closeList(); selection.collapseToEnd(); } }; let timer = null; const filterList = (val, list) => { if (!val) { if (list.length === 0) { selectBoxRef.current.style.display = 'none'; } setSelectSearchList(list); } else { const arr = getArrayByName(val, allPeople); if (arr.length === 0) { selectBoxRef.current.style.display = 'none'; } setSelectSearchList(arr); } }; const handleChange = (val, list) => { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { timer = null; filterList(val, list); }, 200); // filterList(val, list); }; /** * 根据字符串模糊搜索返回符合条件的数据 * name 搜索字符串 * array 检索json数组 */ const getArrayByName = (name, array) => { const result = []; array.forEach((i) => { if (i.name.indexOf(name) != -1) result.push({ userName: i.name, userId: i.id, port: i.port }); }); return result; }; const addFile = (e) => { if (e.target) { let file = e.target.files[0]; const pattern = /[`~!@#$^\-&*()+=|{}':;',\\\[\]\<>\/?~!@#¥……&*()——|{}【】';:""'。,、?\s]/g; let name = file.name.replace(pattern, ''); const renameFile = new File([file], name); const formData = new FormData(); formData.append('file', renameFile); setLoading(true); request(API.POST_UPLOADERFILES, { method: 'POST', data: formData, }) .then((res) => { if (res.data) { const arr = [...fileList]; const url = res.data.replace(/[\\ \/=]/g, '/'); arr.unshift({ name: name, type: file.type ? file.type.toLowerCase() : '', size: file.size, path: `${API.GET_DOWNLOADFILES}?filePath=${url}`, }); // setFileList(arr); props.onChangeFile(arr); setLoading(false); } else { res && message.error(res.msg); setLoading(false); } }) .catch((err) => { setLoading(false); }); } }; const onDelFile = (item) => { const arr = []; fileList.forEach((i) => { if (i.path !== item.path) { arr.push(i); } }); // setFileList(arr); props.onChangeFile(arr); }; useImperativeHandle(ref, () => ({ setHtml, onClear, getHtml, getText, })); return ( <div className={styles.RichText} id="RichText"> {loading ? ( <div className={styles.loadingWrap} style={{ zIndex: zIndex + 20 }}> <Spin spinning={loading} /> </div> ) : null} <div id="RichTextToolbar" className={styles.RichTextToolbar} /> <div ref={richTextRef} id="RichTextContainer" className={styles.RichTextContainer} /> <div className={styles.RichTextFileList}> <FileListItem list={fileList} onDel={(val) => { onDelFile(val); }} type="edit" onPreview={(val) => { if (!val) return; setImgPreviewSrc(val.path); setImgVisible(true); }} /> </div> <div ref={selectBoxRef} className={styles.selectBox} style={{ maxWidth: '300px', minWidth: '150px', zIndex: zIndex + 10 }} > {selectSearchList.length ? ( <div className={styles.selectList}> {selectSearchList.map((item, index) => ( <div key={item.userId} onClick={() => { onSelect(item); }} className={classNames( 'selectItem', styles.selectItem, selectIndex === index ? styles.selectActiveItem : '', )} > {item.userName} </div> ))} </div> ) : null} </div> <input style={{ display: 'none' }} type="file" ref={fileInputRef} onChange={(e) => { addFile(e); }} name="file" /> <Image width={200} style={{ display: 'none' }} src={imgPreviewSrc} preview={{ visible: imgVisible, src: imgPreviewSrc, onVisibleChange: (value) => { setImgVisible(value); if (!value) setImgPreviewSrc(''); }, }} /> </div> ); }); export default RichText;