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

相关推荐
OpenTiny社区19 分钟前
一行命令添加 AI 对话入口!TinyRobot 也太省事了~
前端·vue.js·ai编程
sagima_sdu21 分钟前
Vue 前端径向渐变背景制作
前端·javascript·vue.js
神奇小汤圆26 分钟前
Java 并发编程核心原理与生产级最佳实践
javascript
叶落阁主44 分钟前
Vue3 后台管理系统全局菜单搜索实战:Cmd/Ctrl + K、权限菜单与拼音过滤
前端·javascript·vue.js
MacroZheng1 小时前
阿里Qoder + GLM-5.1,夯爆了!
前端·vue.js·人工智能
橘猫走江湖2 小时前
Web 前端本地存储:localStorage 与 IndexedDB
前端·javascript·indexeddb
AKA__老方丈2 小时前
删除确认 Hook - 统一管理单删/批量删除的确认弹窗与执行
前端·javascript·vue.js
云间寄信2 小时前
JS:数据结构与集合
javascript
零度晚风2 小时前
JS:基础语法与控制结构
javascript
布兰妮甜3 小时前
Vue 项目 `localhost:3000` 打不开?404 常见原因排查指南
前端·javascript·vue.js·vuecli·4040排查