feat: trending report不再作为Placeholder

This commit is contained in:
2026-04-30 10:11:19 +08:00
parent 0b8b4c7aeb
commit 5097e71311
2 changed files with 94 additions and 103 deletions

View File

@@ -53,7 +53,7 @@
<span>Upload files</span>
</div>
<div class="gap"></div>
<div class="report flex align-center" @click="toogltReportTag">
<div class="report flex align-center" @click="toogltReportTag()">
<SvgIcon color="#5A5A5A" name="light" size="11" />
<span>Trending report</span>
</div>
@@ -215,7 +215,7 @@
v-if="!isAgentMode"
class="report-btn flex space-between align-center outer"
:class="{ 'is-cn': isCn }"
@click="toogltReportTag"
@click="toogltReportTag()"
>
<SvgIcon class="light-icon" color="#FFDB56" name="light" size="16" />
<span>{{ $t('Input.trendingReport') }}</span>
@@ -337,8 +337,61 @@
const editorRef = ref<HTMLDivElement | null>(null)
const inputValue = ref<string>('')
const reportTags = ref([])
const customPlaceholder = ref<HTMLElement | null>(null)
const reportTags = ref<HTMLElement[]>([])
const reportPromptText = ref<Text | null>(null)
let reportTypewriterTimeout: ReturnType<typeof setTimeout> | 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<typeof setTimeout> | 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 @@
</style>
<style lang="less">
.custom-placeholder {
color: #999;
margin-left: 0.5rem;
pointer-events: none;
}
.fida-style-select-popover {
width: 34.2rem !important;
padding: 0 !important;

View File

@@ -1,7 +1,7 @@
<template>
<div class="main-input-container flex-1">
<div class="slogan">
<p>Creating Things with <span class="fiDA">FiDA</span> that</p>
<p>Creating Works with <span class="fiDA">FiDA</span> that</p>
<p>Bloom Your Creativity</p>
</div>
<Input />