Formular.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. import React, {
  2. useState,
  3. useCallback,
  4. useRef,
  5. useEffect,
  6. forwardRef,
  7. useImperativeHandle,
  8. } from "react";
  9. import { Spin } from 'antd';
  10. import HintBox from "./HintBox";
  11. import styles from "./fomular.module.less";
  12. import { moveInTextNode, isFirefox } from "./utils";
  13. import oneNode from "./oneNode";
  14. import API2 from "../../../../api/alarm";
  15. const Formular = (props, ref) => {
  16. const formulaRef = useRef();
  17. const [hintVisible, setHintVisible] = useState(false);
  18. const [cursorTextRange, setCursorTextRange] = useState();
  19. const [cursorHtmlRange, setCursorHtmlRange] = useState();
  20. const [formularText, setFormularText] = useState();
  21. const [formularHtml, setFormularHtml] = useState();
  22. const [loading, setLoading] = useState(false);
  23. // const [globalRange, setGlobalRange] = useState();
  24. const rangeCurrent = useRef(); //历史光标range位置
  25. const globalRangeRef = useRef(); //全局range的记录
  26. const outStrRef = useRef(); //最终传给后端的公式str
  27. const saveRange = function () {
  28. var selection = window.getSelection
  29. ? window.getSelection()
  30. : document.selection;
  31. var range = selection.createRange
  32. ? selection.createRange()
  33. : selection.getRangeAt(0);
  34. // console.log('更新全局range', range);
  35. // setGlobalRange(range)
  36. globalRangeRef.current = range;
  37. // _range = range;
  38. };
  39. const dtInsertFormular = function (str) {
  40. if (!window.getSelection) {
  41. // document.getElementById('formulaId').focus();
  42. var selection = window.getSelection
  43. ? window.getSelection()
  44. : document.selection;
  45. var range = selection.createRange
  46. ? selection.createRange()
  47. : selection.getRangeAt(0);
  48. range.pasteHTML(str);
  49. range.collapse(false);
  50. range.select();
  51. } else {
  52. // document.getElementById('formulaId').focus();
  53. var selection = window.getSelection
  54. ? window.getSelection()
  55. : document.selection;
  56. // console.log(globalRangeRef.current)
  57. if (!globalRangeRef.current) {
  58. console.log("输入框没有焦点");
  59. return;
  60. }
  61. selection.addRange(globalRangeRef.current);
  62. range = globalRangeRef.current;
  63. range.collapse(false);
  64. var hasR = range.createContextualFragment(str);
  65. var hasR_lastChild = hasR.lastChild;
  66. while (
  67. hasR_lastChild &&
  68. hasR_lastChild.nodeName.toLowerCase() == "br" &&
  69. hasR_lastChild.previousSibling &&
  70. hasR_lastChild.previousSibling.nodeName.toLowerCase() == "br"
  71. ) {
  72. var e = hasR_lastChild;
  73. hasR_lastChild = hasR_lastChild.previousSibling;
  74. hasR.removeChild(e);
  75. }
  76. range.insertNode(hasR);
  77. if (hasR_lastChild) {
  78. range.setEndAfter(hasR_lastChild);
  79. range.setStartAfter(hasR_lastChild);
  80. }
  81. selection.removeAllRanges();
  82. selection.addRange(range);
  83. }
  84. updateCursorLocation();
  85. };
  86. const cleanBr = () => {
  87. const inHtml = document.getElementById("formulaId").innerHTML;
  88. document.getElementById("formulaId").innerHTML = inHtml.replace("<br>", ""); //清理无用的br
  89. };
  90. const [pointData, setPointData] = useState([]); //点位信息
  91. const backEndInput = useCallback(
  92. async (inputStr) => {
  93. // const inputStr = '([P_NB4_CW_1_bo_7_AQ]>15)-([P_NB4_CW_1_bo_7_AQ_DIFF_H]+1)';
  94. var vars = {};
  95. pointData.forEach((item, index) => {
  96. vars[item.point_id] = item.name;
  97. });
  98. // console.log(vars)
  99. const regx = new RegExp(`\\[([^\\[\\]]*)\\]`, "g");
  100. var match = inputStr.match(regx);
  101. match = match.map((item) => item.replace("[", "").replace("]", ""));
  102. // const res = await API.energySearchByPointsIds(match)
  103. setLoading(true)
  104. API2.searchPoint({
  105. point_ids: match,
  106. count: 20,
  107. type: 1,
  108. }).then(res => {
  109. if (res.state === 0) {
  110. res.data?.forEach((item) => {
  111. vars[item.point_id] = item.name;
  112. });
  113. } else {
  114. vars = {};
  115. }
  116. }).finally(() => {
  117. setLoading(false)
  118. })
  119. console.log(vars);
  120. initDisplay({ vars: vars, formula: inputStr });
  121. outStrRef.current = inputStr;
  122. },
  123. [pointData]
  124. );
  125. const initDisplay = (value) => {
  126. const { vars, formula } = value;
  127. console.log(value);
  128. let result = formula;
  129. Reflect.ownKeys(vars).forEach((key) => {
  130. const rule = new RegExp(`\\[${key}\\]`, "g");
  131. // const name = `<div contenteditable="false" data-point_id='${key}'>${vars[key]}</div>`
  132. const name = oneNode(key, vars[key]);
  133. result = result.replace(rule, (v, index, string) => {
  134. const { length } = v;
  135. const str = string.slice(index - 1, length + index + 1);
  136. if (str.startsWith("_") || str.endsWith("_")) {
  137. return key;
  138. } else {
  139. return name;
  140. }
  141. });
  142. // result = result.replace('&&', 'and').replace('||', 'or')
  143. });
  144. result = result.replace("<br>", ""); //去除br标签
  145. formulaRef.current.innerHTML = result;
  146. // const target = formulaRef.current
  147. // setFocus(target, result.length)
  148. // setLastHtmlEditRange();
  149. };
  150. // 获取光标位置,但这个函数是不足够的,因为如果插入img元素,光标位置会被分割。只能判断字符串在自身text元素的位置
  151. const getCursorIndex = () => {
  152. const selection = window.getSelection();
  153. return selection?.focusOffset;
  154. };
  155. const updateCursorLocation = () => {
  156. // console.log('HTML位置: ', getCursorHtmlPosition(document.getElementById('formulaId')))
  157. // console.log('Text位置: ', getCursorTextPosition(document.getElementById('formulaId')))
  158. saveRange();
  159. // console.log(getCursorIndex());
  160. // setCursorTextRange(getCursorIndex())
  161. };
  162. const handleClick = () => {
  163. updateCursorLocation();
  164. };
  165. const getRangeRect = () => {
  166. //三层逻辑,首先判断能不能找到光标的位置,再次判断历史光标有没有位置,最后默认一个位置。
  167. const selection = window.getSelection();
  168. const range = selection?.getRangeAt(0);
  169. console.log(range);
  170. let rect = range.getClientRects()[0];
  171. console.log(range.getClientRects());
  172. if (rect) {
  173. rangeCurrent.current = rect;
  174. } else {
  175. if (rangeCurrent.current) {
  176. rect = rangeCurrent.current;
  177. } else {
  178. rect = { x: 110, y: 160.5 };
  179. }
  180. }
  181. const LINE_HEIGHT = 30;
  182. return {
  183. x: rect.x,
  184. y: rect.y + LINE_HEIGHT,
  185. };
  186. };
  187. // 获取节点
  188. const getRangeNode = () => {
  189. const selection = window.getSelection();
  190. return selection?.focusNode;
  191. };
  192. // 匹配@后的内容
  193. const getAtContent = () => {
  194. const content = getRangeNode()?.textContent || "";
  195. const regx = /@([^@\s]*)$/;
  196. const match = regx.exec(content.slice(0, getCursorIndex()));
  197. if (match && match.length === 2) {
  198. return match[1];
  199. }
  200. return undefined;
  201. };
  202. const [position, setPosition] = useState({
  203. x: 0,
  204. y: 0,
  205. }); //提示框出现的位置
  206. const [queryString, setQueryString] = useState(""); //查询字符串
  207. const handleAt = (e) => {
  208. document.getElementById("formulaId").focus();
  209. const position = getRangeRect();
  210. setPosition(position);
  211. const user = getAtContent();
  212. setQueryString(user || "");
  213. setHintVisible(true);
  214. };
  215. const handleKeyUp = (e) => {
  216. updateCursorLocation(); //按下任意按键都会更新最新的光标位置
  217. const { key } = e;
  218. switch (key) {
  219. case "ArrowUp":
  220. case "ArrowDown":
  221. case "ArrowLeft":
  222. case "ArrowRight":
  223. // updateCursorLocation();
  224. break;
  225. case "Backspace":
  226. case "Delete":
  227. if (
  228. document.getElementById("formulaId").innerHTML.startsWith("<br>") &&
  229. isFirefox()
  230. ) {
  231. cleanBr();
  232. }
  233. break;
  234. default:
  235. }
  236. };
  237. const handleKeyDown = (e) => {
  238. if (hintVisible) {
  239. if (
  240. e.code === "ArrowUp" ||
  241. e.code === "ArrowDown" ||
  242. e.code === "Enter"
  243. ) {
  244. e.preventDefault();
  245. }
  246. if (e.code === "Backspace") {
  247. e.preventDefault();
  248. setHintVisible(false);
  249. }
  250. } else {
  251. const { key } = e;
  252. switch (key) {
  253. case "Enter":
  254. e.preventDefault();
  255. break;
  256. case "@":
  257. // case 'Process': // 中文输入法的 @
  258. handleAt();
  259. e.preventDefault();
  260. break;
  261. default:
  262. }
  263. }
  264. };
  265. useImperativeHandle(
  266. ref,
  267. () => {
  268. return {
  269. backEndInput: backEndInput,
  270. dtInsertFormular: dtInsertFormular,
  271. moveInTextNode: moveInTextNode,
  272. getFinalStr: () => outStrRef.current,
  273. };
  274. },
  275. [backEndInput]
  276. );
  277. const handlePickPoint = (poi) => {
  278. const { name, point_id } = poi;
  279. // const res = `<div contenteditable="false" data-point_id='${point_id}'>${name}</div> `
  280. const res = oneNode(point_id, name);
  281. dtInsertFormular(res);
  282. setHintVisible(false);
  283. updateCursorLocation();
  284. };
  285. const divBlur = () => {
  286. setValue();
  287. };
  288. const setValue = () => {
  289. let formula = "";
  290. const vars = {};
  291. const processNode = (node) => {
  292. if (node.dataset?.point_id) {
  293. // 处理点位节点
  294. formula += `[${node.dataset?.point_id}]`;
  295. vars[node.dataset?.point_id] = node.innerText || node.textContent;
  296. } else if (node.localName === "br") {
  297. // 忽略br标签
  298. } else if (node.nodeType === Node.TEXT_NODE) {
  299. // 处理文本节点
  300. formula += node.data || node.textContent;
  301. } else if (node.nodeType === Node.ELEMENT_NODE) {
  302. // 处理其他HTML元素(如span),提取其文本内容
  303. if (node.children && node.children.length > 0) {
  304. // 如果有子元素,递归处理
  305. Array.from(node.children).forEach(processNode);
  306. } else {
  307. // 如果没有子元素,直接提取文本内容
  308. formula += node.textContent || node.innerText || "";
  309. }
  310. }
  311. };
  312. formulaRef.current.childNodes.forEach(processNode);
  313. const res = {
  314. formula,
  315. vars,
  316. };
  317. outStrRef.current = formula;
  318. console.log(outStrRef.current);
  319. };
  320. const cleanPastedText = useCallback((text) => {
  321. if (!text || typeof text !== 'string') return ''
  322. /** 检测是否包含HTML标签和换行符(用于调试日志) */
  323. const hasHtmlTags = /<[^>]*>/g.test(text)
  324. const hasLineBreaks = /[\r\n\t]+/g.test(text)
  325. if (hasHtmlTags) {
  326. console.warn('检测到粘贴内容包含HTML标签,已自动清理')
  327. }
  328. if (hasLineBreaks) {
  329. console.warn('检测到粘贴内容包含换行符,已转换为空格')
  330. }
  331. return text
  332. /** 移除零宽字符 */
  333. .replace(/[\u200B-\u200D\uFEFF]/g, '')
  334. /** 移除控制字符 */
  335. .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
  336. /** 移除HTML标签 */
  337. .replace(/<[^>]*>/g, '')
  338. /** 统一处理所有空白字符:换行符、制表符、各种空格 → 单个普通空格 */
  339. .replace(/[\r\n\t\s\u00A0\u2000-\u200A\u202F\u205F\u3000]+/g, ' ')
  340. /** 移除首尾空格 */
  341. .trim()
  342. }, [])
  343. /**
  344. * 异步获取点位信息并转换文本为HTML,[point_id]转换为img节点,其他保持为文本
  345. * @param {string} text - 输入文本,如 "[123]+[456]"
  346. * @returns {Promise<string>} - 转换后的HTML字符串
  347. */
  348. const convertTextToHtmlWithApi = useCallback(async (text) => {
  349. // 匹配 [point_id] 模式
  350. const pointPattern = /\[([^\[\]]+)\]/g;
  351. let match;
  352. const matches = [];
  353. const pointIds = [];
  354. // 收集所有匹配项
  355. while ((match = pointPattern.exec(text)) !== null) {
  356. matches.push({
  357. fullMatch: match[0], // [123]
  358. pointId: match[1], // 123
  359. index: match.index
  360. });
  361. pointIds.push(match[1]);
  362. }
  363. if (pointIds.length === 0) {
  364. // 没有点位变量,直接返回原文本
  365. return text;
  366. }
  367. // 调用API获取点位信息
  368. let pointVars = {};
  369. try {
  370. const res = await API2.searchPoint({
  371. point_ids: pointIds,
  372. count: 20,
  373. type: 1,
  374. });
  375. if (res.state === 0) {
  376. res.data?.forEach((item) => {
  377. pointVars[item.point_id] = item.name;
  378. });
  379. }
  380. } catch (error) {
  381. console.error('获取点位信息失败:', error);
  382. // 如果API调用失败,使用point_id作为name
  383. pointIds.forEach(id => {
  384. pointVars[id] = id;
  385. });
  386. }
  387. let result = text;
  388. // 从后往前替换,避免索引位置变化
  389. for (let i = matches.length - 1; i >= 0; i--) {
  390. const matchItem = matches[i];
  391. const pointName = pointVars[matchItem.pointId] || matchItem.pointId;
  392. const imgHtml = oneNode(matchItem.pointId, pointName);
  393. result = result.substring(0, matchItem.index) +
  394. imgHtml +
  395. result.substring(matchItem.index + matchItem.fullMatch.length);
  396. }
  397. return result;
  398. }, []);
  399. const handlePaste = useCallback(async (e) => {
  400. e.preventDefault()
  401. /** 获取剪贴板数据 */
  402. const clipboardData = e.clipboardData
  403. if (!clipboardData) return false
  404. /** 获取并清理粘贴的文本内容 */
  405. const rawText = clipboardData.getData('text/plain')
  406. const cleanedText = cleanPastedText(rawText)
  407. /** 如果清理后没有有效文本内容,使用默认行为或阻止粘贴 */
  408. if (!cleanedText) {
  409. e.preventDefault()
  410. return true
  411. }
  412. console.log('原始文本:', cleanedText);
  413. try {
  414. setLoading(true);
  415. /** 异步获取点位信息并转换为HTML格式 */
  416. const htmlContent = await convertTextToHtmlWithApi(cleanedText);
  417. console.log('转换后的HTML:', htmlContent);
  418. /** 将转换后的HTML插入到编辑器中 */
  419. dtInsertFormular(htmlContent);
  420. /** 更新输出值 */
  421. setTimeout(() => {
  422. setValue();
  423. }, 0);
  424. } catch (error) {
  425. console.error('粘贴处理失败:', error);
  426. } finally {
  427. setLoading(false);
  428. }
  429. }, [convertTextToHtmlWithApi, cleanPastedText]);
  430. const handleCopy = useCallback((e) => {
  431. // 允许复制操作
  432. updateCursorLocation();
  433. }, []);
  434. const handleCut = useCallback((e) => {
  435. // 允许剪切,但在剪切后更新内容
  436. setTimeout(() => {
  437. setValue();
  438. updateCursorLocation();
  439. }, 0);
  440. }, []);
  441. return (
  442. <div className={styles.wrapper}>
  443. <Spin spinning={loading}>
  444. <div
  445. id="formulaId"
  446. ref={formulaRef}
  447. className="editor"
  448. contentEditable
  449. onClick={handleClick}
  450. onKeyUp={handleKeyUp}
  451. onKeyDown={handleKeyDown}
  452. onCut={handleCut}
  453. onBlur={divBlur}
  454. onPaste={handlePaste}
  455. onCopy={handleCopy}
  456. ></div>
  457. <div>
  458. <HintBox
  459. visible={hintVisible}
  460. setVis={() => setHintVisible(false)}
  461. // hintData={pointData}
  462. queryString={queryString}
  463. position={position}
  464. onPickPoint={handlePickPoint}
  465. onClickPoint={handlePickPoint}
  466. ></HintBox>
  467. </div>
  468. </Spin>
  469. {/* <button onClick={backEndInput}>后端输入</button>
  470. <button onClick={()=>dtInsertFormular('()')}>输入字符</button> */}
  471. </div>
  472. );
  473. };
  474. export default forwardRef(Formular);