效果如下:支持连续输入
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 800px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.content {
padding: 30px;
}
.editor-container {
position: relative;
margin-bottom: 30px;
}
#editor {
min-height: 200px;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
font-size: 16px;
line-height: 1.6;
outline: none;
transition: border-color 0.3s;
background: white;
overflow-y: auto;
}
#editor:focus {
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.highlight {
background-color: #fff9c4;
border-radius: 3px;
padding: 0 2px;
color: #e74c3c;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="editor-container">
<div id="editor" contenteditable="true" placeholder="输入内容..."></div>
</div>
</div>
</div>
<script>
const editor = document.getElementById('editor');
function saveCursorPosition() {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return {
startOffset: preCaretRange.toString().length,
range: range
};
}
function restoreCursorPosition(startOffset) {
const selection = window.getSelection();
selection.removeAllRanges();
let charCount = 0;
let nodeStack = [editor];
let node, foundStart = false;
let range = document.createRange();
range.setStart(editor, 0);
range.collapse(true);
while (!foundStart && (node = nodeStack.pop())) {
if (node.nodeType === Node.TEXT_NODE) {
const nextCharCount = charCount + node.length;
if (!foundStart && startOffset >= charCount && startOffset <= nextCharCount) {
range.setStart(node, startOffset - charCount);
foundStart = true;
}
charCount = nextCharCount
} else {
const children = node.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
nodeStack.push(children[i])
}
}
}
if (foundStart) {
range.collapse(true);
selection.addRange(range);
}
}
function applyHighlighting() {
const cursorPosition = saveCursorPosition();
const text = editor.textContent;
const tags = [];
editor.innerHTML = text.replace(/([@#][^\s]+)/g, (match) => {
tags.push(match);
return `<span class="highlight">${match}</span>`;
});
// 恢复光标位置
if (cursorPosition) {
restoreCursorPosition(cursorPosition.startOffset);
}
}
let isComposing = false;
editor.addEventListener('compositionstart', () => {
isComposing = true;
});
editor.addEventListener('compositionend', () => {
isComposing = false;
applyHighlighting();
});
editor.addEventListener('input', () => {
if (!isComposing) {
applyHighlighting();
}
});
applyHighlighting();
</script>
</body>
</html>