实现类似word 文档下划线输入功能
效果图:
组件:
<template><span class="underline-input-wrapper" @click="focusEditable"><spanref="editable"class="underline-editable"contenteditable="true":style="{minWidth: minWidth,'--min-chars': 3,}"@input="handleInput"@blur="handleBlur"@keydown.enter.prevent="insertLineBreak"@compositionstart="isComposing = true"@compositionend="handleCompositionEnd"></span></span>
</template><script setup lang="ts">
import { ref, onMounted, watch, nextTick, toRefs } from "vue";const props = defineProps({modelValue: {type: String,default: () => " ",},minWidth: {type: String,default: "20px",},dashed: {type: Boolean,default: false,},lineType: {type: String,default: () => "dashed",},
});
const { lineType } = toRefs(props);const emit = defineEmits(["blur", "update:value"]);const editable = ref<any>(null);
const isComposing = ref(false);onMounted(() => {if (props.modelValue) {renderContent(props.modelValue);}
});watch(() => props.modelValue,(newVal) => {if (newVal !== getDisplayText() && !isComposing.value) {renderContent(newVal);}},
);const renderContent = async (text: string) => {if (!editable.value) return;const cursorPos = getCursorPosition();editable.value.innerHTML = text.split("").map((char) =>char === "\n"? `<span class="underline-char ${lineType.value}-line"><br></span>`: `<span class="underline-char ${lineType.value}-line">${char}</span>`,).join("");await nextTick();setCursorPosition(cursorPos);
};const getDisplayText = () => {return editable.value?.textContent || "";
};const handleInput = () => {if (isComposing.value) return;const text = getDisplayText();emit("update:value", text.trim());if (text !== props.modelValue) {renderContent(text);}
};const handleCompositionEnd = () => {isComposing.value = false;handleInput();
};const insertLineBreak = () => {const selection = window.getSelection();if (!selection || selection.rangeCount === 0) return;// 获取当前选区范围const range = selection.getRangeAt(0);range.deleteContents(); // 清除当前选中的内容// 创建要插入的元素const span = document.createElement("span");span.className = `underline-char ${lineType.value}-line`;span.innerHTML = "<br>";// 插入元素到文档中range.insertNode(span);// 移动光标到插入的元素后面range.setStartAfter(span);range.setEndAfter(span);// 更新选区selection.removeAllRanges();selection.addRange(range);handleInput();
};const getCursorPosition = () => {const selection: any = window.getSelection();if (selection.rangeCount === 0) return 0;const range = selection.getRangeAt(0);let pos = 0;const walker = document.createTreeWalker(editable.value, NodeFilter.SHOW_TEXT, null);let node: any;while ((node = walker.nextNode())) {if (node === range.startContainer) {pos += range.startOffset;break;}pos += node.length;}return pos;
};const setCursorPosition = (pos: any) => {const selection: any = window.getSelection();const range = document.createRange();let count = 0;const walker = document.createTreeWalker(editable.value, NodeFilter.SHOW_TEXT, null);let node: any;while ((node = walker.nextNode())) {if (count + node.length >= pos) {range.setStart(node, pos - count);range.collapse(true);selection.removeAllRanges();selection.addRange(range);break;}count += node.length;}
};const focusEditable = () => {editable.value.focus();
};const handleBlur = () => {emit("blur", getDisplayText());
};
</script><style lang="less">
.underline-input-wrapper {cursor: text;line-height: 1.5;
}.underline-editable {display: inline;min-width: v-bind("minWidth");outline: none;white-space: nowrap;
}.underline-char {display: inline;padding-bottom: 0px;white-space: pre-wrap;position: relative;
}
.dashed-line {margin-right: 2px;border-bottom: 1px dashed #000;
}
.solid-line {border-bottom: 1px solid #000;
}.underline-char:empty::after,
.underline-char br::after {content: "\200B";display: inline;
}/* 处理换行情况 */
.underline-char br {display: block;content: "";margin-top: 1em;
}
</style>
引用案例:一个是默认展示内容,一个是绑定的值
<dsahedInputComp:modelValue="` ${mctCertApplysValue?.officeUser} `"v-model:value="saveData.officeUser"/>