自定义评论面板

# 开发自定义评论面板时需要注意以下问题:

# 1、如何插入自定义面板?

websdk 给目标元素设置了 id 为 left_menu,用户可根据 id 找到目标元素,将自定义面板插入到页面中,详情请见下方示例 1 和 2。

# 2、自定义面板样式如何处理?

面板样式由用户自定义。

# 3、自定义面板能使用哪些组件?

自定义面板对组件没有硬性要求,websdk 适用于常见的前端框架,如 vue、angular、react 等,用户可使用惯用的组件库,根据使用场景选择合适的组件。 该样例是基于原生 js 和 html 开发的。

# 4、自定义面板如何绘制批注?

该问题涉及三个前端 SDK 接口:绘制批注、完成绘制的回调及删除批注。 (1)ZwSetDrawCommentBubble:触发绘制批注。 (2)ZwEvtDrawCommentBubble:批注绘制完成后的回调,此处会接收到批注的信息。以圆形批注为例,绘制结束后返回圆的位置信息、批注颜色、唯一标识符 id 等信息。 (3)ZwSetDeleteBubbles:删除绘制的批注,例如在输入框中删除圆形批注后,需要调用该方法同步清除画布中绘制的圆形批注。 其他批注相关的接口,例如批注的定位、气泡高亮、取消气泡高亮、中断气泡的绘制等接口,请查看**“前端接口对象及方法”**。

# 5、评论列表?

评论列表需用户自行开发,该样例仅演示了如何自定义评论面板,不包含评论列表。

# 该示例主要演示如何自定义评论面板。

1、自定义评论面板:编写 html,根据应用场景将需要的批注工具和评论字段添加上去,请查看示例代码。

<!-- 如右侧区域所示,在左上方增加一个自定义评论面板的按钮,用来打开或关闭“自定义评论面板”。 -->
<button
  id="comment_btn"
  style="width: 110px;position: fixed;top: 9px;right: 0;"
>
  自定义评论面板
</button>
<div id="custom_Comment" class="custom-area">
  <!-- 自定义评论面板的内容 -->
  <!-- websdk包中index.html包含了自定义评论面板的样例代码 -->
  ……
</div>

2、插入自定义面板:根据 id 找到目标元素,将 html 片段插入到目标元素前。

   <!-- 给“自定义评论面板按钮”绑定点击事件,点击后能展开或隐藏自定义面板 -->
   let comment_btn = document.getElementById("comment_btn");
    comment_btn.addEventListener('click', function () {
      <!-- 判断自定义面板是否需要隐藏 -->
      let custom_Comment = document.getElementById("custom_Comment");
      if (custom_Comment && custom_Comment.style.display && custom_Comment.style.display  !== 'none') {
        custom_Comment.style.display = 'none';
        return;
      }

      <!-- ************插入自定义面板开始************ -->

      <!-- main_c是右侧整个区域的元素id,left_menu是左侧面板的id -->
      let main_c = document.getElementById("main_c");
      let left_menu = document.getElementById("left_menu");

      <!-- 获取自定义评论面板的Dom -->
      let newNode = custom_Comment.cloneNode(custom_Comment);
      custom_Comment.remove();
      newNode.style.display = 'block';
      newNode.style.height = left_menu.style.height;

      <!-- 将自定义评论面板插入到main_c区域中left_menu的前面 -->
      main_c.insertBefore(newNode, left_menu);

      <!-- ************插入自定义面板结束************ -->

      <!-- 其他业务代码开始 -->
      <!-- websdk包中index.html包含了自定义评论面板的样例代码 -->
      ……
      <!-- 其他业务代码结束 -->
      }, 0);
    });

3、样例简介 websdk 包中 index.html 的样例代码包含了批注工具的使用、颜色的交互选择、@功能、快捷回复等基本功能,供用户参考。 点击“提交”按钮,将评论批注信息提取出来,用户根据实际场景保存即可。点击取消按钮将清空输入的评论内容。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <base href="/pages/" />
    <title>Html Demo</title>
  </head>

  <style>
    body {
      margin: 0;
      overflow: hidden;
    }

    .custom-area {
      display: none;
      width: 330px;
      background-color: #535353;
      border-right: 1px solid #424242;
      border-bottom: 1px solid #424242;
      color: #fff;
    }

    .panel-title {
      background-color: #424242;
      color: #ddd;
      height: 25px;
      line-height: 25px;
      font-weight: bold;
    }

    .custom-comment-tool {
      height: 35px;
      border-bottom: 1px solid #424242;
      margin-bottom: 10px;
    }

    .custom-comment-tool svg {
      cursor: pointer;
      font-size: 20px;
      margin-right: 5px;
    }

    .component {
      height: 24px;
      line-height: 24px;
    }

    .align-center {
      display: flex;
      align-items: center;
    }

    label {
      width: 80px;
    }

    .width-220 {
      width: 220px;
    }

    .comment-input-box {
      height: 85px;
      width: 222px;
    }

    .fast-reply {
      display: flex;
      justify-content: end;
      align-items: center;
    }

    .fast-reply div {
      cursor: pointer;
      font-size: 12px;
      margin-right: 5px;
    }

    .quick-reply {
      display: none;
      position: absolute;
      margin-top: 100px;
      border: 1px solid #535353;
      background-color: #535353;
      height: auto;
      /* padding: 2px 5px 2px 5px; */
    }

    .quick-reply option:hover {
      background-color: #424242;
    }

    .comment-input::-webkit-scrollbar {
      width: 10px;
      height: 10px;
    }

    .comment-input::-webkit-scrollbar-thumb {
      border-radius: 10px;
      background-color: #000000;
    }

    .comment-input::-webkit-scrollbar-track {
      background-color: #363636;
    }

    .comment-input::-webkit-scrollbar-corner {
      width: 10px;
      height: 10px;
      background-color: #535353;
    }

    .comment-input-box,
    .comment-input,
    textarea {
      border-radius: 5px;
    }

    .comment-input {
      height: 60px;
      overflow-y: auto;
    }

    .comment-input-box,
    .comment-input,
    textarea,
    select {
      outline: none;
      border: 1px solid #424242;
      background-color: #424242;
    }

    .comment-input-box:hover,
    .comment-input-box:focus,
    textarea:hover,
    textarea:focus,
    select:hover,
    select:focus {
      border-color: #40a9ff;
    }

    .flex-end {
      display: flex;
      justify-content: flex-end;
    }

    button {
      margin-right: 10px;
      width: 60px;
      height: 32px;
      cursor: pointer;
      border-radius: 3px;
    }

    .default {
      border: 1px solid #535353;
      background: #535353;
    }

    .default:hover {
      border: 1px solid rgba(66, 66, 66, 0.2);
      background: rgba(66, 66, 66, 0.2);
    }

    .primary {
      border: 1px solid #424242;
      background: #424242;
    }

    .primary:hover {
      border: 1px solid #1890ff;
      background: #1890ff;
    }

    .margin-left-15 {
      margin-left: 15px;
    }

    .margin-bottom-15 {
      margin-bottom: 15px;
    }

    .palette-box:hover .palette {
      display: block;
    }

    .select-color {
      width: 18px;
      height: 18px;
      border-radius: 9px;
      margin-top: -3px;
      cursor: pointer;
      background-color: red;
    }

    .palette {
      position: absolute;
      margin-left: -125px;
      padding-top: 2px;
      display: none;
    }

    .rectangle {
      height: 24px;
      width: 24px;
      cursor: pointer;
    }

    .rectangle:hover {
      border: 2px solid #fff;
    }

    .bg-red {
      background-color: red;
    }

    .bg-yellow {
      background-color: yellow;
    }

    .bg-green {
      background-color: green;
    }

    .bg-cyan {
      background-color: cyan;
    }

    .bg-blue {
      background-color: blue;
    }

    .bg-color1 {
      background-color: rgba(255, 0, 255, 1);
    }

    .member-list-box {
      display: none;
      margin-left: 64px;
    }

    .member-list {
      border: 1px solid #999;
      background: #424242;
      margin: 0px 2px 15px 15px;
    }

    .userName {
      margin: 5px 10px 5px 10px;
    }

    .userName-box:hover {
      background-color: #535353;
      cursor: pointer;
    }
  </style>

  <script
    type="text/javascript"
    src="../assets/ZwCloud2DSDK/ZwCloud2D.js"
  ></script>

  <body id="wholeFrame">
    <div id="container" style="width: 100vw; height: 100vh"></div>

    <button
      id="comment_btn"
      style="width: 110px; position: fixed; top: 9px; right: 0"
    >
      自定义评论面板
    </button>
    <div id="custom_Comment" class="custom-area">
      <div class="panel-title">
        <span class="margin-left-15">评论</span>
      </div>
      <div class="align-center custom-comment-tool">
        <span class="margin-left-15" style="width: 80px">批注工具:</span>
        <svg
          id="circle_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-comment-circle"></use>
        </svg>
        <svg
          id="juxing_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-rectangle"></use>
        </svg>
        <svg
          id="yunxian_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-revcloud"></use>
        </svg>
        <svg
          id="zidingyi_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-custom-shape"></use>
        </svg>
        <svg
          id="jiantou_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-comment-arrow"></use>
        </svg>
        <svg
          id="tuding_comment"
          class="icon custom-comment-panel"
          aria-hidden="true"
        >
          <use xlink:href="#icon-zw-thumbtack"></use>
        </svg>
        <div class="palette-box">
          <div class="select-color" id="selectedColor"></div>
          <div id="palette" class="palette">
            <div class="flex-end">
              <div class="rectangle bg-red"></div>
              <div class="rectangle bg-yellow"></div>
              <div class="rectangle bg-green"></div>
              <div class="rectangle bg-cyan"></div>
              <div class="rectangle bg-blue"></div>
              <div class="rectangle bg-color1"></div>
            </div>
          </div>
        </div>
      </div>
      <div style="padding: 5px 10px 0 15px">
        <div style="display: flex" class="margin-bottom-15">
          <label>评论内容</label>
          <div class="comment-input-box">
            <div
              contenteditable="true"
              id="commentInput"
              class="comment-input width-220"
            ></div>
            <div class="fast-reply">
              <div id="triggerQuickReply" style="color: #40a9ff">快捷回复</div>
              <div id="quickReply" class="component quick-reply">
                <option class="select-quick-reply">快捷回复01快捷回复01</option>
                <option class="select-quick-reply">快捷回复02</option>
                <option class="select-quick-reply">
                  快捷回复03快捷回复03快捷回复03
                </option>
              </div>
            </div>
          </div>
        </div>
        <div>
          <div class="member-list-box" id="memberList">
            <div class="member-list">
              <div class="userName-box">
                <div class="userName">张三</div>
              </div>
              <div class="userName-box">
                <div class="userName">李四</div>
              </div>
              <div class="userName-box">
                <div class="userName">王五</div>
              </div>
            </div>
          </div>
        </div>
        <div class="align-center margin-bottom-15">
          <label>问题等级</label>
          <select id="select" class="component width-220" required>
            <option value="轻微">轻微</option>
            <option value=" 严重">严重</option>
            <option value=" 严重">严重</option>
          </select>
        </div>
        <div style="display: flex; margin-bottom: 10px">
          <label>备注</label>
          <div></div>
          <textarea
            rows="5"
            id="textarea"
            required
            class="width-220"
            style="resize: none"
          ></textarea>
        </div>
        <div class="flex-end">
          <button class="default" id="cancel">取消</button>
          <button class="primary" id="submit">提交</button>
        </div>
      </div>
    </div>

    <script type="module">
      // 初始化配置
      const initializeViewer = async () => {
        try {
          // 设置云服务连接配置
          window.ZwCloud2D.ZwDataProcessor.ZwSetConnectUrl(
            process.env.HTML_APP_ZW_URL
          );
          // 设置认证令牌
          window.ZwCloud2D.ZwDataProcessor.ZwSetToken(
            process.env.HTML_APP_ZW_TOKEN
          );
          // 初始化编辑器
          await window.ZwCloud2D.ZwEditor.ZwInit(
            document.getElementById("container")
          );
        } catch (error) {
          console.error("初始化失败:", error);
          // 可以添加错误处理逻辑
        }
      };

      initializeViewer().then(() => {
        // 从路由参数获取文档ID
        const params = new URLSearchParams(location.search);
        const docId = params.get("docId");

        if (docId) {
          // 配置要加载的文档
          window.ZwCloud2D.ZwDataProcessor.ZwSetLoadDwg(docId);

          // 加载文档
          window.ZwCloud2D.ZwDataProcessor.ZwLoad();
        }

        // 初始化面板
        initPanel();
      });

      /*************自定义评论面板 样例开始*************/
      // 批注颜色选项
      let paletteColors = [
        {
          str: "rgba(255,0,0,1)", //red
          num: 0xffff0000,
        },
        {
          str: "rgba(255,255,0,1)", //yellow
          num: 0xffffff00,
        },
        {
          str: "rgba(0,128,0,1)", //green
          num: 0xff008000,
        },
        {
          str: "rgba(0,255,255,1)", //cyan
          num: 0xff00ffff,
        },
        {
          str: "rgba(0,0,255,1)", //blue
          num: 0xff0000ff,
        },
        {
          str: "rgba(255,0,255,1)",
          num: 0xffff00ff,
        },
      ];
      memberList = [
        {
          memberId: "1",
          memberName: "张三",
        },
        {
          memberId: "2",
          memberName: "李四",
        },
        {
          memberId: "3",
          memberName: "王五",
        },
        {
          memberId: "4",
          memberName: "小李",
        },
      ];
      // 记录@符号的位置
      let focusNode = null;
      let focusOffset = 0;
      let clickAtMemberEl = undefined;

      let selectedColor = { str: "red", num: 0xffff0000 };
      let allInputMarks = {};

      function initPanel() {
        let comment_btn = document.getElementById("comment_btn");
        comment_btn.addEventListener("click", function () {
          let custom_Comment = document.getElementById("custom_Comment");
          if (
            custom_Comment &&
            custom_Comment.style.display &&
            custom_Comment.style.display !== "none"
          ) {
            custom_Comment.style.display = "none";
            return;
          }

          let main_c = document.getElementById("main_c");
          custom_Comment.style.display = "block";
          main_c.insertBefore(custom_Comment, null);

          setTimeout(() => {
            let inputEl = document.getElementById("commentInput");
            // 使用后退格键删除的时候需要删除增加的富文本
            inputEl.addEventListener("keydown", (e) => {
              if (e.code === "Digit2" && e.shiftKey) {
                const selection = window.getSelection();
                if (selection !== null) {
                  // 记录@符号的位置
                  focusNode = selection.focusNode;
                  focusOffset = selection.focusOffset;
                  setDisplay("memberList");
                }
              } else if (e.code === "Backspace") {
                setDisplay("quickReply", false);
                setDisplay("memberList", false);
                deleteRichText();
              } else if (e.code === "Delete") {
                setDisplay("quickReply", false);
                setDisplay("memberList", false);
                deleteRichText();
              } else if (e.ctrlKey && (e.keyCode === 67 || e.keyCode === 88)) {
                // 禁止复制和剪切
                e.preventDefault();
              } else if (
                e.code === "ArrowLeft" ||
                e.code === "ArrowRight" ||
                e.code === "ArrowDown" ||
                e.code === "ArrowUp"
              ) {
                if (target) {
                  const range = document.createRange();
                  range.selectNodeContents(target);
                  const selection = window.getSelection();
                  if (selection !== null) {
                    if (selection.rangeCount > 0) {
                      selection.removeAllRanges();
                    }
                    selection.addRange(range);
                    selection.selectAllChildren(target);
                  }
                }
              }
            });

            inputEl.addEventListener("click", (e) => {
              setDisplay("quickReply", false);
              setDisplay("memberList", false);
              let target = e.target;
              if (
                target.className === "atFont" ||
                target.className === "bubble" ||
                target.nodeName === "svg" ||
                target.nodeName === "use"
              ) {
                const selection = window.getSelection();
                if (selection && selection.rangeCount <= 0) {
                  return;
                }
                const range = selection.getRangeAt(0);
                range.selectNode(target);
                selection && selection.selectAllChildren(target);
                clickAtMemberEl = target;
              } else {
                clickAtMemberEl = undefined;
              }
            });

            let commandEles = document.getElementsByClassName(
              "custom-comment-panel"
            );
            for (let index = 0; index < commandEles.length; index++) {
              const element = commandEles[index];
              element &&
                element.addEventListener("click", (e) => {
                  let id = element.id;
                  triggerCommand(id);
                });
            }

            let selectColorEles = document.getElementsByClassName("rectangle");
            for (let index = 0; index < selectColorEles.length; index++) {
              const element = selectColorEles[index];
              element &&
                element.addEventListener("click", (e) => {
                  selectColor(index);
                });
            }

            let cancelBtn = document.getElementById("cancel");
            cancelBtn &&
              cancelBtn.addEventListener("click", (e) => {
                cancel();
              });

            let submitBtn = document.getElementById("submit");
            submitBtn &&
              submitBtn.addEventListener("click", (e) => {
                submit();
              });

            let triggerQuickReplyBtn =
              document.getElementById("triggerQuickReply");
            triggerQuickReplyBtn &&
              triggerQuickReplyBtn.addEventListener("click", (e) => {
                setDisplay("quickReply");
              });

            let selectQuickReplyEles =
              document.getElementsByClassName("select-quick-reply");
            for (let index = 0; index < selectQuickReplyEles.length; index++) {
              const element = selectQuickReplyEles[index];
              element &&
                element.addEventListener("click", (e) => {
                  switch (index) {
                    case 0:
                      selectQuickReply("快捷回复01快捷回复01");
                      break;
                    case 1:
                      selectQuickReply("快捷回复02");
                      break;
                    case 2:
                      selectQuickReply("快捷回复03快捷回复03快捷回复03");
                      break;
                    default:
                      break;
                  }
                });
            }

            let userNameEles = document.getElementsByClassName("userName");
            for (let index = 0; index < userNameEles.length; index++) {
              const element = userNameEles[index];
              element &&
                element.addEventListener("click", (e) => {
                  selectMember(index);
                });
            }

            custom_Comment = document.getElementById("custom_Comment");
            custom_Comment &&
              custom_Comment.addEventListener("click", (e) => {
                closeAllDivs();
              });
          }, 0);
        });
      }

      // 点击批注工具,触发绘制
      function triggerCommand(id) {
        let svgEl = document.getElementById(id);
        if (svgEl) {
          svgEl.style.color = selectedColor.str;
        }

        let command = "circle_v";
        let svgIcon = "#icon-yuanxin";
        switch (id) {
          case "circle_comment":
            command = "circle_v";
            svgIcon = "#icon-zw-comment-circle";
            break;
          case "juxing_comment":
            command = "rectangle_v";
            svgIcon = "#icon-zw-rectangle";
            break;
          case "yunxian_comment":
            command = "revcloud_v;2";
            svgIcon = "#icon-zw-revcloud";
            break;
          case "zidingyi_comment":
            command = "pline_v";
            svgIcon = "#icon-zw-custom-shape";
            break;
          case "jiantou_comment":
            command = "leader_v";
            svgIcon = "#icon-zw-comment-arrow";
            break;
          case "tuding_comment":
            command = "bubble_v";
            svgIcon = "#icon-zw-thumbtack";
            break;
          default:
            break;
        }
        ZwCloud2D.ZwDataManager.ZwSetDrawCommentBubble(command, selectedColor);

        // 批注结束后,将数据回填到输入框中:例如圆形批注结束后,将圆形图标回填到输入框中
        ZwCloud2D.ZwMessageCallback.ZwEvtDrawCommentBubble = (data) => {
          // 绘制结束后清除批注工具的颜色
          svgEl.style.color = "";

          console.log("ZwEvtDrawCommentBubble: ", data);
          if (data.data && data.data.id) {
            allInputMarks[data.data.id] = data.data;
            allInputMarks[data.data.id].type = data.type;
            let el = document.getElementById("commentInput");
            let newHtml =
              '<svg class="icon bubble" contenteditable="false" id="' +
              data.data.id +
              '" style="color:' +
              data.data.color +
              ';" aria-hidden="true" style="margin-right: 5px;cursor:pointer;"><use xlink:href="' +
              svgIcon +
              '"></use></svg>';
            el.innerHTML += "&nbsp;" + newHtml + "&nbsp;";
          }
        };
      }

      // 选择颜色
      function selectColor(index) {
        selectedColor = paletteColors[index];
        let target = document.getElementById("selectedColor");
        if (target) {
          target.style.backgroundColor = selectedColor.str;
        }
      }

      function closeAllDivs() {
        setDisplay("quickReply", false);
        setDisplay("memberList", false);
      }

      // 设置面板显示或隐藏
      function setDisplay(id, show) {
        let target = document.getElementById(id);
        if (
          (target && target.style.display && target.style.display !== "none") ||
          show === false
        ) {
          target.style.display = "none";
        } else {
          target.style.display = "block";
        }
        if (id === "quickReply") {
          setDisplay("memberList", false);
        }
        stopPropagaton();
      }

      // 快捷回复
      function selectQuickReply(params) {
        let commentInput = document.getElementById("commentInput");
        commentInput.innerHTML += "&nbsp;" + params + "&nbsp;";
        let target = document.getElementById("quickReply");
        target.style.display = "none";
        stopPropagaton();
      }

      function stopPropagaton(params) {
        let e = window.event;
        if (e.stopPropagaton) {
          e.stopPropagation();
        } else {
          e.cancelBubble = true;
        }
      }

      // 选择@的用户
      function selectMember(index) {
        let obj = memberList[index];
        const selection = window.getSelection();
        if (selection !== null) {
          let inputEl = document.getElementById("commentInput");
          if (inputEl === null || (selection && selection.rangeCount <= 0))
            return;
          const range = selection.getRangeAt(0);
          // 选中输入的@符号
          let offset = focusOffset + 1;
          range.setStart(focusNode, offset - 1);
          range.setEnd(focusNode, offset);
          // 删除输入的@符号
          range.deleteContents();

          // 创建元素节点
          const element = document.createElement("span");
          element.style.color = "orange";
          element.className = "atFont";
          element.setAttribute("id", obj.memberId);
          element.setAttribute("contenteditable", "false");
          element.innerText = `@${obj.memberName}`;

          // 创建一个空元素
          const emptyEl = document.createElement("span");
          emptyEl.innerHTML = "&nbsp;";
          let frag = document.createDocumentFragment();
          frag.appendChild(element);
          let lastNode;
          if (emptyEl && emptyEl.firstChild) {
            lastNode = frag.appendChild(emptyEl.firstChild);
          }
          range.insertNode(frag);
          lastNode && selection.extend(lastNode, 1);
          selection.collapseToEnd();

          // 聚焦输入框
          inputEl && inputEl.focus();
          // 关闭选项
          setDisplay("memberList", false);
        }
      }

      /**
       * 选中后删除
       */
      function clickToDelete() {
        let atInput = document.getElementById("commentInput");
        if (clickAtMemberEl) {
          atInput && atInput.removeChild(clickAtMemberEl);
          let id = clickAtMemberEl.id;
          if (id && id.length === 36) {
            ZwCloud2D.ZwDataManager.ZwSetDeleteBubbles([id]);
            delete allInputMarks[id];
          }
          clickAtMemberEl = undefined;
          return true;
        }
        return false;
      }

      // 删除富文本
      function deleteRichText() {
        let isMobile = false;
        let atInput = document.getElementById("commentInput");
        const selection = window.getSelection();

        if (!clickToDelete()) {
          if (atInput && selection !== null && selection.rangeCount > 0) {
            const range = selection.getRangeAt(0);
            let removeNode = null;

            let startContainer = range.startContainer;
            if (!startContainer.parentElement) {
              return;
            }
            if (startContainer.parentElement.className !== "atFont") {
              //移动端光标移动发生在删除之前
              if (isMobile) {
                removeNode = startContainer.nextElementSibling;
              } else {
                //获取当前锚点元素
                //富文本前后会带有&nbsp; 借此判断是否是需要执行手动删除
                let anchorNode = selection.anchorNode;
                if (anchorNode && selection.anchorOffset === 1) {
                  //是否为文字node
                  if (anchorNode.nodeType === 3) {
                    if (anchorNode.textContent) {
                      //判断是否包含&nbsp;
                      let text = anchorNode.textContent;
                      if (text.charCodeAt(0) === 160) {
                        removeNode = startContainer.previousSibling;
                      }
                    }
                  }
                }
              }

              if (removeNode) {
                //删除元素node
                let id = removeNode.id
                  ? removeNode.id
                  : removeNode.firstChild
                  ? removeNode.firstChild.id
                  : undefined;
                if (id && id.length === 36) {
                  // this.mGs.deleteBubble([id]);
                  ZwCloud2D.ZwDataManager.ZwSetDeleteBubbles([id]);
                  if (removeNode) {
                    //删除多余的&nbsp;
                    if (
                      removeNode.nextSibling &&
                      removeNode.nextSibling.textContent.charCodeAt(0) === 160
                    ) {
                      let newText = removeNode.nextSibling.nodeValue.slice(1);
                      removeNode.nextSibling.nodeValue = newText;
                    }
                    // 同时删除@和name
                    selection.selectAllChildren(removeNode);
                    atInput.removeChild(removeNode);
                  }
                }
              }
            } else if (startContainer.parentElement.className === "atFont") {
              removeNode = startContainer.parentElement;
              if (removeNode) {
                // 同时删除@和name
                selection.selectAllChildren(removeNode);
                atInput.removeChild(removeNode);
              }
            }
          }
        }
      }

      function checkSpecialChar(src) {
        let notAllowedChars = /[\\/:*"<>#~$%^&*(){}|]/g;
        notAllowedChars.lastIndex = 0;
        if (src && notAllowedChars.test(src)) {
          return true;
        }
        return false;
      }

      function getMarkById(id) {
        let res = null;
        for (const key in allInputMarks) {
          if (Object.prototype.hasOwnProperty.call(allInputMarks, key)) {
            const mark = allInputMarks[key];
            if (mark.id === id) {
              res = JSON.parse(JSON.stringify(mark));
              break;
            }
          }
        }
        if (res) {
          switch (res.type) {
            case 0: //BubbleType.Pin:
              res.pt = [res.pt.mX, res.pt.mY, res.pt.mZ];
              break;
            case 1: //BubbleType.Circle:
              res.center = [res.center.mX, res.center.mY, res.center.mZ];
              res.endPt = [res.endPt.mX, res.endPt.mY, res.endPt.mZ];
              break;
            case 2: //BubbleType.Rectangle:
              res.points = [
                res.points[0].mX,
                res.points[0].mY,
                res.points[0].mZ,
                res.points[1].mX,
                res.points[1].mY,
                res.points[1].mZ,
              ];
              break;
            case 3: //BubbleType.Polyline:
              let arr = [];
              res.points.forEach((element) => {
                arr.push(element.mX, element.mY, element.mZ);
              });
              res.points = arr;
              break;
            case 4: //BubbleType.Leader:
              let pts = [];
              res.points.forEach((element) => {
                pts.push(element.mX, element.mY, element.mZ);
              });
              res.points = pts;
              let polygonPts = [];
              res.polygonPts.forEach((element) => {
                polygonPts.push(element.mX, element.mY, element.mZ);
              });
              res.polygonPts = polygonPts;
              break;
            case 6: //BubbleType.Image:
              res.originPoint = [res.origin.mX, res.origin.mY, res.origin.mZ];
              res.uvector = [res.uVec.mX, res.uVec.mY, res.uVec.mZ];
              res.vvector = [res.vVec.mX, res.vVec.mY, res.vVec.mZ];
              break;
            case 5: //BubbleType.Revcloud:
              res.originPt = [
                res.originPt.mX,
                res.originPt.mY,
                res.originPt.mZ,
              ];
              res.diagonalPt = [
                res.diagonalPt.mX,
                res.diagonalPt.mY,
                res.diagonalPt.mZ,
              ];
              break;
            default:
              break;
          }
        }
        return res;
      }

      // 提交评论
      function submit() {
        let input = document.getElementById("commentInput");
        if (input === null) {
          return false;
        }

        let inputContent = {};
        let str = input.innerHTML;
        let matchRegSpan = /<span.*?<\/span>&nbsp;/g;
        let matchBracketsFront = /<span/g;
        let matchBracketsBack = /<\/span>/g;
        if (str) {
          let spanArr = str.match(matchRegSpan);
          // 指定开头和结尾,获取中间部分 /(?<=指定开头").*?(?=指定结尾)/g
          let matchId = /id="(?:[^"\\]|\\.)*"/g;

          let id = "";
          let ids = [];
          spanArr &&
            spanArr.forEach((span) => {
              id = span.match(matchId);
              if (id && id[0]) {
                ids.push(id[0].slice(4, id[0].length - 1));
              }
            });
          if (ids.length > 0) {
            inputContent.atUserIds = "";
            inputContent.atUserIds += ids.join(",");
            inputContent.withAt = "YES";
          }

          let marks = [];

          let strArr = str.split("&nbsp;");
          let template = "";
          let realContent = "";
          let userIds = [];
          let hasSpecialChar = false;
          for (let index = 0; index < strArr.length; index++) {
            const substr = strArr[index];
            if (substr) {
              let tempArr = [];
              if (substr.indexOf('<div class="bubble"') > -1) {
                tempArr = substr.split('<div class="bubble"');
              } else if (substr.indexOf('<svg class="icon') > -1) {
                tempArr = substr.split('<svg class="icon');
              }
              if (tempArr.length === 2) {
                template += tempArr[0];
              }
              let bracketsFront = substr
                .split(matchBracketsFront)
                .filter((item) => {
                  return item !== "";
                });

              if (bracketsFront.length > 0) {
                for (
                  let innerindex = 0;
                  innerindex < bracketsFront.length;
                  innerindex++
                ) {
                  const element = bracketsFront[innerindex];
                  let ids = element.match(matchId);
                  if (ids && ids[0]) {
                    let id = ids[0].slice(4, ids[0].length - 1);
                    if (id && id.length === 36) {
                      // 气泡多了一层<div>
                      if (element.indexOf("</svg></div>") > -1) {
                        tempArr = element.split("</svg></div>");
                      } else {
                        // 其他类型的气泡
                        tempArr = element.split("</svg>");
                      }
                      if (checkSpecialChar(tempArr[1])) {
                        hasSpecialChar = true;
                        break;
                      }
                      template += "#";
                      let res = getMarkById(id);
                      res && marks.push(res);
                      if (tempArr.length === 2) {
                        template += tempArr[1];
                      }
                    } else {
                      tempArr = element.split("</span>");
                      if (checkSpecialChar(tempArr[1])) {
                        hasSpecialChar = true;
                        break;
                      }
                      // @字符串
                      template += "$";
                      userIds.push(id);
                      if (tempArr.length === 2) {
                        template += tempArr[1];
                      }
                    }
                  } else {
                    if (checkSpecialChar(element)) {
                      hasSpecialChar = true;
                      break;
                    }
                    template += element;
                    realContent += element;
                  }
                }

                if (hasSpecialChar) {
                  break;
                }
              }
            }
          }
          if (hasSpecialChar) {
            console.log("不能包含特殊字符");
            return false;
          }

          if (marks.length > 0) {
            inputContent.withMark = "YES";
            inputContent.mark = JSON.stringify(marks);
          }

          inputContent.content = template;
          inputContent.realContent = realContent;
          if (inputContent.content.length > 1000) {
            console.log("字符长度不超过1000");
            return false;
          }

          console.log("inputContent: ", inputContent);
          let commentList = {
            code: 0,
            data: {
              rows: [inputContent],
            },
          };
          // 测试ZwSetCommentData接口
          // ZwCloud2D.ZwDataManager.ZwSetCommentData({ commentList: commentList });
        }

        let select = document.getElementById("select");
        console.log("select:", select.value);
        let textarea = document.getElementById("textarea");
        console.log("textarea:", textarea.value);
      }

      // 取消评论
      function cancel() {
        let input = document.getElementById("commentInput");
        if (input === null) {
          return false;
        }
        input.innerHTML = "";
        for (const key in allInputMarks) {
          if (Object.hasOwnProperty.call(allInputMarks, key)) {
            const element = allInputMarks[key];
            if (element.id && element.id.length === 36) {
              ZwCloud2D.ZwDataManager.ZwSetDeleteBubbles([element.id]);
            }
          }
        }
        allInputMarks = {};
      }
      /*************自定义评论面板 样例结束*************/
    </script>
  </body>
</html>