vue中根据html动态渲染内容

需求:根据数据中的html,因为我是在做填空,所以是需要将html中的_____替换成input,由于具体需求我使用的是元素contenteditable代替的可编辑的input

html部分

复制代码
<div class="wrap">
      <component :is="renderedContent" ref="wrap_component" />
    </div>

js部分

复制代码
// 这个是为了保证输入的时候光标保持在最后
const moveCursorToEnd = (element: HTMLElement) => {
  const range = document.createRange();
  const selection = window.getSelection();

  // 找到最后一个文本节点
  let lastTextNode: Text | any = null;
  const traverseNodes = (node: Node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      lastTextNode = node as Text;
    }
    for (let i = 0; i < node.childNodes.length; i++) {
      traverseNodes(node.childNodes[i]);
    }
  };
  traverseNodes(element);

  if (lastTextNode) {
    range.setStart(lastTextNode, lastTextNode.textContent?.length || 0);
    range.collapse(true);
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  } else {
    range.setStart(element, 0);
    range.collapse(true);
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  // 兼容性处理:确保元素获取焦点
  element.focus();
  if (document.activeElement !== element) {
    element.focus();
  }
};

// 计算属性,用于生成渲染内容
const renderedContent = computed(() => {
  if (!itemConf.value.customConf?.inputHtml) return null;
  const parts = itemConf.value.customConf.inputHtml.split(/_{1,}/);
  let nodes: any = [];

  parts.forEach((part, index) => {
    if (part) {
      const replacedSpaces = part.replace(/ /g, '&nbsp;');
      const replacedPart = replacedSpaces.replace(/<div>/g, '<br>').replace(/<\/div>/g, '');
      nodes.push(h('span', { class: 'custom-span', innerHTML: replacedPart }));
    }
    if (index < parts.length - 1) {
      if (!inputValues.value[index]) {
        inputValues.value[index] = '';
      }
      if (!isInputFocused.value[index]) {
        isInputFocused.value[index] = false;
      }
      if (!isClearIconClicked.value[index]) {
        isClearIconClicked.value[index] = false;
      }
      if (!clearIconHideTimer.value[index]) {
        clearIconHideTimer.value[index] = 0;
      }
      const clearIcon = h(
        ElIcon,
        {
          class: [
            'clear_icon',
            {
              'is-hidden':
                inputValues.value[index].length === 0 ||
                itemConf.value.baseConf.isReadOnly ||
                !isInputFocused.value[index],
            },
          ],
          onClick: () => {
            if (!itemConf.value.baseConf.isReadOnly) {
              isClearIconClicked.value[index] = true;
              inputValues.value[index] = '';
              if (inputRefs.value[index]) {
                inputRefs.value[index].innerText = '';
              }
              adjustInputWidth(index);
              handleChange(itemConf.value.customConf.inputGroup[index], '');
              // 点击后清除隐藏定时器
              clearTimeout(clearIconHideTimer.value[index]);
            }
          },
        },
        { default: () => h(CircleClose) },
      );
      const inputNode = h(
        'p',
        {
          contenteditable: !itemConf.value.baseConf.isReadOnly,
          class: [
            'underline_input',
            {
              'is-disabled': itemConf.value.baseConf.isReadOnly,
            },
          ],
          disabled: itemConf.value.baseConf.isReadOnly,
          innerHTML: inputValues.value[index],
          placeholder: unref(itemConf).customConf?.inputGroup[index]?.placeholder || '请输入',
          onInput: async (event: InputEvent) => {
            const target = event.target as HTMLParagraphElement;
            adjustInputWidth(index);
            inputValues.value[index] = target.innerHTML;
            await nextTick(() => {
              moveCursorToEnd(target);
            });
          },
          onFocus: () => {
            if (!itemConf.value.baseConf.isReadOnly) {
              isInputFocused.value[index] = true;
              clearTimeout(clearIconHideTimer.value[index]);
            }
          },
          onBlur: () => {
            if (!itemConf.value.baseConf.isReadOnly) {
              handleChange(itemConf.value.customConf.inputGroup[index], inputValues.value[index]);
              clearIconHideTimer.value[index] = setTimeout(() => {
                if (!isClearIconClicked.value[index]) {
                  isInputFocused.value[index] = false;
                }
                isClearIconClicked.value[index] = false;
              }, 200);
            }
          },
          onMousedown: (event: MouseEvent) => {
            if (itemConf.value.baseConf.isReadOnly) {
              event.preventDefault();
              event.stopPropagation();
            }
          },
          onKeydown: (event: KeyboardEvent) => {
            if (itemConf.value.baseConf.isReadOnly) {
              event.preventDefault();
              event.stopPropagation();
            }
          },
          ref: (el) => (inputRefs.value[index] = el),
        },
        // [clearIcon],
      );

      nodes.push(h('p', { class: 'underline_input_wrap' }, [inputNode, clearIcon]));
    }
  });
  return h('div', nodes);
});

css部分

复制代码
.underline_input_wrap {
  display: inline-block;
  // max-width: calc(100% - 70px);
  position: relative;
  margin-top: 20px;
  margin-bottom: 0;
  max-width: calc(100% - 50px);
}
.underline_input {
  position: relative;
  height: 40px;
  min-width: 101px;
  // max-width: calc(100% - 70px);
  max-width: 100%;
  background: #f5f7fb;
  border-radius: 6px 6px 6px 6px;
  border: none;
  margin-left: 10px;
  margin-top: 0;
  margin-bottom: 0;
  display: inline-block;
  box-sizing: border-box;
  padding: 0 26px 0 12px;
  background: #f5f7fb;
  vertical-align: middle;
  color: #606266;
  background: #f5f7fb;
  vertical-align: middle;
  &:focus {
    outline: none;
    border: 1px solid #1a77ff;
    color: #606266;
  }
  &:disabled {
    color: #bbbfc4;
    cursor: not-allowed;
  }
  &::placeholder {
    color: #a8abb2;
    font-size: 14px;
  }
}

.underline_input.is-disabled {
  color: #bbbfc4;
  cursor: not-allowed;
}

.underline_input[contenteditable='true']:empty::before,
.underline_input.is-disabled:empty::before {
  content: attr(placeholder);
  color: #bbbfc4;
}
:deep(.clear_icon) {
  position: absolute;
  width: 14px;
  height: 14px;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #999;
  z-index: 10; /* 增加 z-index 确保在最上层 */
  &:hover {
    color: #666;
  }
  &.is-hidden {
    display: none;
  }
}

我们要模拟input可清除,所以需要我们去调整样式,以及placeholder样式问题

相关推荐
daols885 小时前
vue vxe-table 自适应列宽,根据内容自适应宽度的2种使用方式
vue.js·vxe-table
OEC小胖胖7 小时前
告别 undefined is not a function:TypeScript 前端开发优势与实践指南
前端·javascript·typescript·web
行云&流水7 小时前
Vue3 Lifecycle Hooks
前端·javascript·vue.js
老虎06277 小时前
JavaWeb(苍穹外卖)--学习笔记04(前端:HTML,CSS,JavaScript)
前端·javascript·css·笔记·学习·html
三水气象台8 小时前
用户中心Vue3网页开发(1.0版)
javascript·css·vue.js·typescript·前端框架·html·anti-design-vue
烛阴8 小时前
Babel 完全上手指南:从零开始解锁现代 JavaScript 开发的超能力!
前端·javascript
CN-Dust9 小时前
[FMZ][JS]第一个回测程序--让时间轴跑起来
javascript
盛夏绽放9 小时前
Vue3 中 Excel 导出的性能优化与实战指南
vue.js·excel
全宝10 小时前
🎨前端实现文字渐变的三种方式
前端·javascript·css
yanlele10 小时前
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
前端·javascript·面试