@@ -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 < HTMLElemen t | null > ( null )
const reportTags = ref < HTMLElement [ ] > ([ ] )
const reportPromptText = ref < Tex t | 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 ( )
s election. add Range( newRange )
s election. addRange ( newRange )
const currentSelection = window . getSelection ( )
currentS election? . removeAll Ranges ( )
currentS election? . 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 = [ ]
cu stomPlaceholder . 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 = [ ]
cu stomPlaceholder . 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 HTML Element)
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.5 rem ;
pointer - events : none ;
}
. fida - style - select - popover {
width : 34.2 rem ! important ;
padding : 0 ! important ;