diff --git a/src/views/home/components/Input.vue b/src/views/home/components/Input.vue index 36d958c..1827812 100644 --- a/src/views/home/components/Input.vue +++ b/src/views/home/components/Input.vue @@ -53,7 +53,7 @@ Upload files
-
+
Trending report
@@ -215,7 +215,7 @@ v-if="!isAgentMode" class="report-btn flex space-between align-center outer" :class="{ 'is-cn': isCn }" - @click="toogltReportTag" + @click="toogltReportTag()" > {{ $t('Input.trendingReport') }} @@ -337,8 +337,61 @@ const editorRef = ref(null) const inputValue = ref('') - const reportTags = ref([]) - const customPlaceholder = ref(null) + const reportTags = ref([]) + const reportPromptText = ref(null) + let reportTypewriterTimeout: ReturnType | null = null + + const stopReportTypewriter = () => { + if (reportTypewriterTimeout) { + clearTimeout(reportTypewriterTimeout) + reportTypewriterTimeout = null + } + } + + const moveCaretToTextEnd = (textNode: Text) => { + const selection = window.getSelection() + if (!selection || selection.anchorNode !== textNode) return + + const range = document.createRange() + range.setStart(textNode, textNode.data.length) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } + + const typeReportPromptText = (textNode: Text, text: string, index = 0) => { + if (reportPromptText.value !== textNode) return + + if (index >= text.length) { + reportTypewriterTimeout = null + return + } + + textNode.textContent = `${textNode.textContent || ''}${text.charAt(index)}` + handleEditorInput() + moveCaretToTextEnd(textNode) + + reportTypewriterTimeout = setTimeout(() => { + typeReportPromptText(textNode, text, index + 1) + }, 30) + } + + const removeReportPromptText = () => { + stopReportTypewriter() + const promptText = reportPromptText.value + if (promptText?.parentNode) { + const nextNode = promptText.nextSibling + if ( + nextNode && + nextNode.nodeType === Node.TEXT_NODE && + nextNode.textContent === '\u200B' + ) { + nextNode.remove() + } + promptText.parentNode.removeChild(promptText) + } + reportPromptText.value = null + } // 控制占位符显示 const showPlaceholder = computed(() => { @@ -356,25 +409,6 @@ return textContent === '' && !hasMeaningfulChildren && isEmptyHTML }) - // 打字机效果显示placeholder - let typewriterTimeout: ReturnType | null = null - const typeWriterEffect = (element: HTMLElement, text: string, index: number = 0) => { - if (index < text.length) { - element.innerText += text.charAt(index) - typewriterTimeout = setTimeout(() => { - typeWriterEffect(element, text, index + 1) - }, 30) // 每个字符间隔30ms - } - } - - // 停止打字机效果 - const stopTypewriter = () => { - if (typewriterTimeout) { - clearTimeout(typewriterTimeout) - typewriterTimeout = null - } - } - // 导出给父组件调用的方法 const addReportTag = (text?: string) => { // 使用传入的文本,如果没有传入则使用默认的翻译文本 @@ -404,15 +438,12 @@ imgClose.addEventListener('click', (ev) => { ev.stopPropagation() - stopTypewriter() // 关闭标签时停止打字机效果 // remove tag when close clicked + removeReportPromptText() tag.remove() const idx = reportTags.value.indexOf(tag) if (idx > -1) reportTags.value.splice(idx, 1) - if (customPlaceholder.value) { - customPlaceholder.value.remove() - customPlaceholder.value = null - } + handleEditorInput() }) // assemble @@ -431,75 +462,48 @@ const range = selection.getRangeAt(0) range.insertNode(tag) - // Insert a zero-width space text node after the tag so the caret can be placed there - const zwsp = document.createTextNode('\u200B') - if (tag.parentNode) tag.parentNode.insertBefore(zwsp, tag.nextSibling) - - // Create a new collapsed range positioned inside the zwsp (after the tag) - const newRange = document.createRange() - newRange.setStart(zwsp, 1) - newRange.collapse(true) - - selection.removeAllRanges() - selection.addRange(newRange) - // ensure editor has focus editorRef.value && (editorRef.value as HTMLElement).focus() } else if (editorRef.value) { // If no selection in editor, append directly to editor and place caret after editorRef.value.appendChild(tag) - const zwsp = document.createTextNode('\u200B') - editorRef.value.appendChild(zwsp) - const sel = window.getSelection() - if (sel) { - const r = document.createRange() - r.setStart(zwsp, 1) - r.collapse(true) - sel.removeAllRanges() - sel.addRange(r) - } editorRef.value && (editorRef.value as HTMLElement).focus() } reportTags.value.push(tag) - const placeholderSpan = document.createElement('span') - placeholderSpan.className = 'custom-placeholder' - // 初始为空字符串,稍后通过打字机效果填充 - placeholderSpan.innerText = '' + const promptText = document.createTextNode('') if (tag.parentNode) { - tag.parentNode.insertBefore(placeholderSpan, tag.nextSibling) + tag.parentNode.insertBefore(promptText, tag.nextSibling) const zwsp = document.createTextNode('\u200B') - tag.parentNode.insertBefore(zwsp, placeholderSpan.nextSibling) + tag.parentNode.insertBefore(zwsp, promptText.nextSibling) const newRange = document.createRange() - newRange.setStart(placeholderSpan, 0) + newRange.setStart(promptText, promptText.data.length) newRange.collapse(true) - selection.removeAllRanges() - selection.addRange(newRange) - selection.addRange(newRange) + const currentSelection = window.getSelection() + currentSelection?.removeAllRanges() + currentSelection?.addRange(newRange) } - customPlaceholder.value = placeholderSpan - - // 打字机效果显示placeholder文本 - const placeholderText = t('Input.reportPlaceholder') - typeWriterEffect(placeholderSpan, placeholderText) - - const removePlaceholderOnInput = () => { - stopTypewriter() // 用户输入时停止打字机效果 - if (placeholderSpan.parentNode) { - placeholderSpan.remove() - customPlaceholder.value = null - } - editorRef.value?.removeEventListener('input', removePlaceholderOnInput) - } - editorRef.value?.addEventListener('input', removePlaceholderOnInput) + reportPromptText.value = promptText + typeReportPromptText(promptText, t('Input.reportPlaceholder')) } const toogltReportTag = (clear = false) => { - stopTypewriter() // 移除标签时停止打字机效果 + const shouldClear = clear === true + // 清理掉已被删除的标签引用(从 DOM 中移除的元素) reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null) + if (shouldClear) { + removeReportPromptText() + reportTags.value.forEach((tag) => { + tag.remove() + }) + reportTags.value = [] + return + } + if (reportTags.value.length > 0) { + removeReportPromptText() // 移除所有标签及其关联的零宽空格 reportTags.value.forEach((tag) => { if ( @@ -512,10 +516,7 @@ tag.remove() }) reportTags.value = [] - if (customPlaceholder.value) { - customPlaceholder.value.remove() - customPlaceholder.value = null - } + handleEditorInput() } else { // 添加标签 addReportTag() @@ -538,8 +539,8 @@ editor.innerHTML = '' editor.textContent = '' reportTags.value = [] - customPlaceholder.value = null - stopTypewriter() + stopReportTypewriter() + reportPromptText.value = null } } } @@ -554,7 +555,6 @@ let node: Node | null while ((node = walker.nextNode())) { // 使用 closest() 检查当前节点的祖先元素是否包含需要排除的 class - if (node.parentElement?.closest('.custom-placeholder')) continue if (node.parentElement?.closest('.editor-tag')) continue text += node.textContent } @@ -569,12 +569,12 @@ const hasTextContent = editor.textContent?.replace(/[\s\u200B]/g, '').trim().length > 0 // 如果编辑器完全为空,清空它以显示占位符 - // 同时也要清空 reportTags 和 customPlaceholder + // 同时也要清空 reportTags 和 reportPromptText if (!hasChildElements && !hasTextContent) { editor.innerHTML = '' reportTags.value = [] - customPlaceholder.value = null - stopTypewriter() + stopReportTypewriter() + reportPromptText.value = null } // 自动调整高度 @@ -642,20 +642,14 @@ const element = nodeToDelete as Element const isEditorTag = element.classList.contains('editor-tag') const isReportTag = element.classList.contains('report-tag') - const isCustomPlaceholder = element.classList.contains('custom-placeholder') - if (isEditorTag || isReportTag || isCustomPlaceholder) { + if (isEditorTag || isReportTag) { e.preventDefault() + removeReportPromptText() element.remove() - // 如果删除的是 customPlaceholder,停止打字机效果 - if (isCustomPlaceholder) { - stopTypewriter() - customPlaceholder.value = null - } - // 从 reportTags 中移除 - const index = reportTags.value.indexOf(nodeToDelete as Element) + const index = reportTags.value.indexOf(nodeToDelete as HTMLElement) if (index > -1) { reportTags.value.splice(index, 1) } @@ -663,6 +657,7 @@ // 删除标签后清理零宽字符并检查是否为空 nextTick(() => { cleanupEditor() + handleEditorInput() }) return } @@ -677,10 +672,10 @@ // 检查是否完全为空,需要显示占位符 if (editorRef.value.children.length === 0) { reportTags.value = [] - customPlaceholder.value = null - stopTypewriter() + reportPromptText.value = null } } + handleEditorInput() } }) } @@ -837,6 +832,7 @@ }) }) onUnmounted(() => { + stopReportTypewriter() MyEvent.remove('quote', handleQuote) MyEvent.remove('projectChange', handleInitInput) }) @@ -1161,11 +1157,6 @@