diff --git a/src/pages/chat/components/ChatInput.tsx b/src/pages/chat/components/ChatInput.tsx index 326e587..0c15144 100644 --- a/src/pages/chat/components/ChatInput.tsx +++ b/src/pages/chat/components/ChatInput.tsx @@ -52,6 +52,50 @@ export default function ChatInput(props: { const [mentionPos, setMentionPos] = useState<{ top: number; left: number } | null>(null); const inputRef = useRef(null); + const getCaretPos = (textarea: HTMLTextAreaElement, caretIndex: number) => { + const rect = textarea.getBoundingClientRect(); + const cs = window.getComputedStyle(textarea); + const div = document.createElement('div'); + + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; + div.style.top = '0'; + div.style.left = '-9999px'; + div.style.whiteSpace = 'pre-wrap'; + div.style.wordWrap = 'break-word'; + div.style.overflow = 'hidden'; + div.style.boxSizing = cs.boxSizing; + div.style.width = rect.width + 'px'; + div.style.fontFamily = cs.fontFamily; + div.style.fontSize = cs.fontSize; + div.style.fontWeight = cs.fontWeight; + div.style.fontStyle = cs.fontStyle; + div.style.letterSpacing = cs.letterSpacing; + div.style.textTransform = cs.textTransform; + div.style.lineHeight = cs.lineHeight; + div.style.padding = cs.padding; + div.style.border = cs.border; + div.style.tabSize = (cs as any).tabSize || '8'; + + const before = textarea.value.slice(0, caretIndex).replace(/ /g, '\u00a0'); + div.textContent = before; + const span = document.createElement('span'); + span.textContent = '\u200b'; + div.appendChild(span); + + document.body.appendChild(div); + const divRect = div.getBoundingClientRect(); + const spanRect = span.getBoundingClientRect(); + document.body.removeChild(div); + + const lineHeight = Number.parseFloat(cs.lineHeight) || Number.parseFloat(cs.fontSize) * 1.2; + return { + top: rect.top + (spanRect.top - divRect.top) - textarea.scrollTop, + left: rect.left + (spanRect.left - divRect.left) - textarea.scrollLeft, + lineHeight + }; + }; + // 检测 @ 触发提及选择 const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -82,10 +126,11 @@ export default function ChatInput(props: { console.log('[@mention] cannot get textarea DOM from inputRef:', inputRef.current); return; } - const rect = textarea.getBoundingClientRect(); - console.log('[@mention] textarea rect:', rect); - setMentionPos({ top: rect.bottom, left: rect.left + 10 }); - console.log('[@mention] show popover:', { top: rect.bottom, left: rect.left + 10, query }); + const caret = getCaretPos(textarea, cursorPos); + const top = Math.min(window.innerHeight - 8, caret.top + caret.lineHeight + 8); + const left = Math.min(window.innerWidth - 160, Math.max(8, caret.left)); + setMentionPos({ top, left }); + console.log('[@mention] show popover:', { top, left, query }); setShowMentionPopover(true); }); return;