| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- import React, {
- useState,
- useCallback,
- useRef,
- useEffect,
- forwardRef,
- useImperativeHandle,
- } from "react";
- import { Spin } from 'antd';
- import HintBox from "./HintBox";
- import styles from "./fomular.module.less";
- import { moveInTextNode, isFirefox } from "./utils";
- import oneNode from "./oneNode";
- import API2 from "../../../../api/alarm";
- const Formular = (props, ref) => {
- const formulaRef = useRef();
- const [hintVisible, setHintVisible] = useState(false);
- const [cursorTextRange, setCursorTextRange] = useState();
- const [cursorHtmlRange, setCursorHtmlRange] = useState();
- const [formularText, setFormularText] = useState();
- const [formularHtml, setFormularHtml] = useState();
- const [loading, setLoading] = useState(false);
- // const [globalRange, setGlobalRange] = useState();
- const rangeCurrent = useRef(); //历史光标range位置
- const globalRangeRef = useRef(); //全局range的记录
- const outStrRef = useRef(); //最终传给后端的公式str
- const saveRange = function () {
- var selection = window.getSelection
- ? window.getSelection()
- : document.selection;
- var range = selection.createRange
- ? selection.createRange()
- : selection.getRangeAt(0);
- // console.log('更新全局range', range);
- // setGlobalRange(range)
- globalRangeRef.current = range;
- // _range = range;
- };
- const dtInsertFormular = function (str) {
- if (!window.getSelection) {
- // document.getElementById('formulaId').focus();
- var selection = window.getSelection
- ? window.getSelection()
- : document.selection;
- var range = selection.createRange
- ? selection.createRange()
- : selection.getRangeAt(0);
- range.pasteHTML(str);
- range.collapse(false);
- range.select();
- } else {
- // document.getElementById('formulaId').focus();
- var selection = window.getSelection
- ? window.getSelection()
- : document.selection;
- // console.log(globalRangeRef.current)
- if (!globalRangeRef.current) {
- console.log("输入框没有焦点");
- return;
- }
- selection.addRange(globalRangeRef.current);
- range = globalRangeRef.current;
- range.collapse(false);
- var hasR = range.createContextualFragment(str);
- var hasR_lastChild = hasR.lastChild;
- while (
- hasR_lastChild &&
- hasR_lastChild.nodeName.toLowerCase() == "br" &&
- hasR_lastChild.previousSibling &&
- hasR_lastChild.previousSibling.nodeName.toLowerCase() == "br"
- ) {
- var e = hasR_lastChild;
- hasR_lastChild = hasR_lastChild.previousSibling;
- hasR.removeChild(e);
- }
- range.insertNode(hasR);
- if (hasR_lastChild) {
- range.setEndAfter(hasR_lastChild);
- range.setStartAfter(hasR_lastChild);
- }
- selection.removeAllRanges();
- selection.addRange(range);
- }
- updateCursorLocation();
- };
- const cleanBr = () => {
- const inHtml = document.getElementById("formulaId").innerHTML;
- document.getElementById("formulaId").innerHTML = inHtml.replace("<br>", ""); //清理无用的br
- };
- const [pointData, setPointData] = useState([]); //点位信息
- const backEndInput = useCallback(
- async (inputStr) => {
- // const inputStr = '([P_NB4_CW_1_bo_7_AQ]>15)-([P_NB4_CW_1_bo_7_AQ_DIFF_H]+1)';
- var vars = {};
- pointData.forEach((item, index) => {
- vars[item.point_id] = item.name;
- });
- // console.log(vars)
- const regx = new RegExp(`\\[([^\\[\\]]*)\\]`, "g");
- var match = inputStr.match(regx);
- match = match.map((item) => item.replace("[", "").replace("]", ""));
- // const res = await API.energySearchByPointsIds(match)
- setLoading(true)
- API2.searchPoint({
- point_ids: match,
- count: 20,
- type: 1,
- }).then(res => {
- if (res.state === 0) {
- res.data?.forEach((item) => {
- vars[item.point_id] = item.name;
- });
- } else {
- vars = {};
- }
- }).finally(() => {
- setLoading(false)
- })
- console.log(vars);
- initDisplay({ vars: vars, formula: inputStr });
- outStrRef.current = inputStr;
- },
- [pointData]
- );
- const initDisplay = (value) => {
- const { vars, formula } = value;
- console.log(value);
- let result = formula;
- Reflect.ownKeys(vars).forEach((key) => {
- const rule = new RegExp(`\\[${key}\\]`, "g");
- // const name = `<div contenteditable="false" data-point_id='${key}'>${vars[key]}</div>`
- const name = oneNode(key, vars[key]);
- result = result.replace(rule, (v, index, string) => {
- const { length } = v;
- const str = string.slice(index - 1, length + index + 1);
- if (str.startsWith("_") || str.endsWith("_")) {
- return key;
- } else {
- return name;
- }
- });
- // result = result.replace('&&', 'and').replace('||', 'or')
- });
- result = result.replace("<br>", ""); //去除br标签
- formulaRef.current.innerHTML = result;
- // const target = formulaRef.current
- // setFocus(target, result.length)
- // setLastHtmlEditRange();
- };
- // 获取光标位置,但这个函数是不足够的,因为如果插入img元素,光标位置会被分割。只能判断字符串在自身text元素的位置
- const getCursorIndex = () => {
- const selection = window.getSelection();
- return selection?.focusOffset;
- };
- const updateCursorLocation = () => {
- // console.log('HTML位置: ', getCursorHtmlPosition(document.getElementById('formulaId')))
- // console.log('Text位置: ', getCursorTextPosition(document.getElementById('formulaId')))
- saveRange();
- // console.log(getCursorIndex());
- // setCursorTextRange(getCursorIndex())
- };
- const handleClick = () => {
- updateCursorLocation();
- };
- const getRangeRect = () => {
- //三层逻辑,首先判断能不能找到光标的位置,再次判断历史光标有没有位置,最后默认一个位置。
- const selection = window.getSelection();
- const range = selection?.getRangeAt(0);
- console.log(range);
- let rect = range.getClientRects()[0];
- console.log(range.getClientRects());
- if (rect) {
- rangeCurrent.current = rect;
- } else {
- if (rangeCurrent.current) {
- rect = rangeCurrent.current;
- } else {
- rect = { x: 110, y: 160.5 };
- }
- }
- const LINE_HEIGHT = 30;
- return {
- x: rect.x,
- y: rect.y + LINE_HEIGHT,
- };
- };
- // 获取节点
- const getRangeNode = () => {
- const selection = window.getSelection();
- return selection?.focusNode;
- };
- // 匹配@后的内容
- const getAtContent = () => {
- const content = getRangeNode()?.textContent || "";
- const regx = /@([^@\s]*)$/;
- const match = regx.exec(content.slice(0, getCursorIndex()));
- if (match && match.length === 2) {
- return match[1];
- }
- return undefined;
- };
- const [position, setPosition] = useState({
- x: 0,
- y: 0,
- }); //提示框出现的位置
- const [queryString, setQueryString] = useState(""); //查询字符串
- const handleAt = (e) => {
- document.getElementById("formulaId").focus();
- const position = getRangeRect();
- setPosition(position);
- const user = getAtContent();
- setQueryString(user || "");
- setHintVisible(true);
- };
- const handleKeyUp = (e) => {
- updateCursorLocation(); //按下任意按键都会更新最新的光标位置
- const { key } = e;
- switch (key) {
- case "ArrowUp":
- case "ArrowDown":
- case "ArrowLeft":
- case "ArrowRight":
- // updateCursorLocation();
- break;
- case "Backspace":
- case "Delete":
- if (
- document.getElementById("formulaId").innerHTML.startsWith("<br>") &&
- isFirefox()
- ) {
- cleanBr();
- }
- break;
- default:
- }
- };
- const handleKeyDown = (e) => {
- if (hintVisible) {
- if (
- e.code === "ArrowUp" ||
- e.code === "ArrowDown" ||
- e.code === "Enter"
- ) {
- e.preventDefault();
- }
- if (e.code === "Backspace") {
- e.preventDefault();
- setHintVisible(false);
- }
- } else {
- const { key } = e;
- switch (key) {
- case "Enter":
- e.preventDefault();
- break;
- case "@":
- // case 'Process': // 中文输入法的 @
- handleAt();
- e.preventDefault();
- break;
- default:
- }
- }
- };
- useImperativeHandle(
- ref,
- () => {
- return {
- backEndInput: backEndInput,
- dtInsertFormular: dtInsertFormular,
- moveInTextNode: moveInTextNode,
- getFinalStr: () => outStrRef.current,
- };
- },
- [backEndInput]
- );
- const handlePickPoint = (poi) => {
- const { name, point_id } = poi;
- // const res = `<div contenteditable="false" data-point_id='${point_id}'>${name}</div> `
- const res = oneNode(point_id, name);
- dtInsertFormular(res);
- setHintVisible(false);
- updateCursorLocation();
- };
- const divBlur = () => {
- setValue();
- };
- const setValue = () => {
- let formula = "";
- const vars = {};
- const processNode = (node) => {
- if (node.dataset?.point_id) {
- // 处理点位节点
- formula += `[${node.dataset?.point_id}]`;
- vars[node.dataset?.point_id] = node.innerText || node.textContent;
- } else if (node.localName === "br") {
- // 忽略br标签
- } else if (node.nodeType === Node.TEXT_NODE) {
- // 处理文本节点
- formula += node.data || node.textContent;
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- // 处理其他HTML元素(如span),提取其文本内容
- if (node.children && node.children.length > 0) {
- // 如果有子元素,递归处理
- Array.from(node.children).forEach(processNode);
- } else {
- // 如果没有子元素,直接提取文本内容
- formula += node.textContent || node.innerText || "";
- }
- }
- };
- formulaRef.current.childNodes.forEach(processNode);
- const res = {
- formula,
- vars,
- };
- outStrRef.current = formula;
- console.log(outStrRef.current);
- };
- const cleanPastedText = useCallback((text) => {
- if (!text || typeof text !== 'string') return ''
- /** 检测是否包含HTML标签和换行符(用于调试日志) */
- const hasHtmlTags = /<[^>]*>/g.test(text)
- const hasLineBreaks = /[\r\n\t]+/g.test(text)
- if (hasHtmlTags) {
- console.warn('检测到粘贴内容包含HTML标签,已自动清理')
- }
- if (hasLineBreaks) {
- console.warn('检测到粘贴内容包含换行符,已转换为空格')
- }
- return text
- /** 移除零宽字符 */
- .replace(/[\u200B-\u200D\uFEFF]/g, '')
- /** 移除控制字符 */
- .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
- /** 移除HTML标签 */
- .replace(/<[^>]*>/g, '')
- /** 统一处理所有空白字符:换行符、制表符、各种空格 → 单个普通空格 */
- .replace(/[\r\n\t\s\u00A0\u2000-\u200A\u202F\u205F\u3000]+/g, ' ')
- /** 移除首尾空格 */
- .trim()
- }, [])
- /**
- * 异步获取点位信息并转换文本为HTML,[point_id]转换为img节点,其他保持为文本
- * @param {string} text - 输入文本,如 "[123]+[456]"
- * @returns {Promise<string>} - 转换后的HTML字符串
- */
- const convertTextToHtmlWithApi = useCallback(async (text) => {
- // 匹配 [point_id] 模式
- const pointPattern = /\[([^\[\]]+)\]/g;
-
- let match;
- const matches = [];
- const pointIds = [];
-
- // 收集所有匹配项
- while ((match = pointPattern.exec(text)) !== null) {
- matches.push({
- fullMatch: match[0], // [123]
- pointId: match[1], // 123
- index: match.index
- });
- pointIds.push(match[1]);
- }
-
- if (pointIds.length === 0) {
- // 没有点位变量,直接返回原文本
- return text;
- }
-
- // 调用API获取点位信息
- let pointVars = {};
- try {
- const res = await API2.searchPoint({
- point_ids: pointIds,
- count: 20,
- type: 1,
- });
-
- if (res.state === 0) {
- res.data?.forEach((item) => {
- pointVars[item.point_id] = item.name;
- });
- }
- } catch (error) {
- console.error('获取点位信息失败:', error);
- // 如果API调用失败,使用point_id作为name
- pointIds.forEach(id => {
- pointVars[id] = id;
- });
- }
-
- let result = text;
-
- // 从后往前替换,避免索引位置变化
- for (let i = matches.length - 1; i >= 0; i--) {
- const matchItem = matches[i];
- const pointName = pointVars[matchItem.pointId] || matchItem.pointId;
- const imgHtml = oneNode(matchItem.pointId, pointName);
- result = result.substring(0, matchItem.index) +
- imgHtml +
- result.substring(matchItem.index + matchItem.fullMatch.length);
- }
-
- return result;
- }, []);
- const handlePaste = useCallback(async (e) => {
- e.preventDefault()
- /** 获取剪贴板数据 */
- const clipboardData = e.clipboardData
- if (!clipboardData) return false
- /** 获取并清理粘贴的文本内容 */
- const rawText = clipboardData.getData('text/plain')
- const cleanedText = cleanPastedText(rawText)
- /** 如果清理后没有有效文本内容,使用默认行为或阻止粘贴 */
- if (!cleanedText) {
- e.preventDefault()
- return true
- }
- console.log('原始文本:', cleanedText);
-
- try {
- setLoading(true);
-
- /** 异步获取点位信息并转换为HTML格式 */
- const htmlContent = await convertTextToHtmlWithApi(cleanedText);
- console.log('转换后的HTML:', htmlContent);
- /** 将转换后的HTML插入到编辑器中 */
- dtInsertFormular(htmlContent);
- /** 更新输出值 */
- setTimeout(() => {
- setValue();
- }, 0);
-
- } catch (error) {
- console.error('粘贴处理失败:', error);
- } finally {
- setLoading(false);
- }
- }, [convertTextToHtmlWithApi, cleanPastedText]);
- const handleCopy = useCallback((e) => {
- // 允许复制操作
- updateCursorLocation();
- }, []);
- const handleCut = useCallback((e) => {
- // 允许剪切,但在剪切后更新内容
- setTimeout(() => {
- setValue();
- updateCursorLocation();
- }, 0);
- }, []);
- return (
- <div className={styles.wrapper}>
- <Spin spinning={loading}>
- <div
- id="formulaId"
- ref={formulaRef}
- className="editor"
- contentEditable
- onClick={handleClick}
- onKeyUp={handleKeyUp}
- onKeyDown={handleKeyDown}
- onCut={handleCut}
- onBlur={divBlur}
- onPaste={handlePaste}
- onCopy={handleCopy}
- ></div>
- <div>
- <HintBox
- visible={hintVisible}
- setVis={() => setHintVisible(false)}
- // hintData={pointData}
- queryString={queryString}
- position={position}
- onPickPoint={handlePickPoint}
- onClickPoint={handlePickPoint}
- ></HintBox>
- </div>
- </Spin>
- {/* <button onClick={backEndInput}>后端输入</button>
- <button onClick={()=>dtInsertFormular('()')}>输入字符</button> */}
- </div>
- );
- };
- export default forwardRef(Formular);
|