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样式问题

相关推荐
GISer_Jing1 小时前
前端性能指标及优化策略——从加载、渲染和交互阶段分别解读详解并以Webpack+Vue项目为例进行解读
前端·javascript·vue
&白帝&5 小时前
vue右键显示菜单
前端·javascript·vue.js
Wannaer5 小时前
从 Vue3 回望 Vue2:事件总线的前世今生
前端·javascript·vue.js
羽球知道5 小时前
在Spark搭建YARN
前端·javascript·ajax
光影少年5 小时前
vue中,created和mounted两个钩子之间调用时差值受什么影响
前端·javascript·vue.js
Ten peaches6 小时前
Selenium-Java版(操作元素)
java·selenium·测试工具·html
cdcdhj6 小时前
vue用通过npm的webpack打包编译,这样更适合灵活配置的项目
vue.js·webpack·npm
恋猫de小郭6 小时前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
赵大仁7 小时前
React Native 与 Expo
javascript·react native·react.js
程序员与背包客_CoderZ8 小时前
Node.js异步编程——Callback回调函数实现
前端·javascript·node.js·web