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

相关推荐
DFT计算杂谈2 分钟前
AMSET 设置多核并行计算
java·前端·css·html·css3
sheeta19981 小时前
Vue 前端基础笔记
前端·vue.js·笔记
前端那点事1 小时前
别再写垃圾组件!Vue3 如何设计「真正可复用」的高质量通用组件
前端·vue.js
卷帘依旧1 小时前
JavaScript 中的 Symbol
前端·javascript
JYeontu2 小时前
正方体翻滚Loading 2.0
前端·javascript·css
张元清2 小时前
React 与用户偏好:尊重用户已经在 OS 里设过的那些选项
前端·javascript·面试
RPGMZ2 小时前
RPGMZ 游戏场景全局提示框 带三秒隐藏插件
前端·javascript·游戏·rpgmz
宠..2 小时前
VS Code 修改 C++ 标准同时修改错误检测标准
java·linux·开发语言·javascript·c++·python·qt
Rkgua2 小时前
React中的赋值操作为什么不是=?
前端·javascript
heyCHEEMS2 小时前
记录一个 React 表单的小坑:缓存节流导致页面刷新
前端·javascript