refactor: 返璞归真 - 剥离 Markdown 注入,利用 Halo 原生层级回复

- 后端 CommentReplyPublisher 删除 buildQuoteMarkdown 和 Markdown 拼接逻辑
- AI 回复直接存入纯净文本,由 Halo 原生 quoteReply 字段渲染前台层级关系
- 前端 ConversationMessage 恢复 quoteOwner/quoteContent 字段
- 前端对话弹窗添加原生 Tailwind 引用摘要框(灰色 border-l-[3px])
- 简化 renderContent,删除 Markdown 引用正则匹配
This commit is contained in:
bbb-lsy07
2026-06-21 10:40:12 +08:00
parent 11620cc113
commit dd123ee2f2
2 changed files with 32 additions and 60 deletions
+22 -16
View File
@@ -134,7 +134,17 @@
<div class="text-xs mb-1.5 px-1 font-medium" :class="msg.isAi ? 'text-blue-600' : 'text-gray-500 text-right'">
{{ msg.owner }}
</div>
<div class="rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm break-words" :class="msg.isAi ? 'bg-white border border-blue-100 text-gray-800 rounded-tl-sm' : 'bg-blue-600 text-white rounded-tr-sm'" v-html="renderContent(msg.content, msg.isAi)"></div>
<div class="rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm break-words" :class="msg.isAi ? 'bg-white border border-blue-100 text-gray-800 rounded-tl-sm' : 'bg-blue-600 text-white rounded-tr-sm'">
<!-- 引用摘要模块如果该条消息有回复对象则渲染 -->
<div v-if="msg.quoteOwner && msg.quoteContent"
class="mb-2.5 px-3 py-2 rounded-lg border-l-[3px] text-xs"
:class="msg.isAi ? 'bg-gray-50 border-gray-300 text-gray-500' : 'bg-black/10 border-white/40 text-blue-100'">
<span class="font-semibold" :class="msg.isAi ? 'text-gray-700' : 'text-white'">@{{ msg.quoteOwner }}</span>
{{ truncateQuote(msg.quoteContent, 40) }}
</div>
<!-- 实际发言内容 -->
<div v-html="renderContent(msg.content)"></div>
</div>
<div class="text-[11px] text-gray-400 mt-1.5 px-1" :class="msg.isAi ? 'text-left' : 'text-right'">
{{ formatDate(msg.time) }}
</div>
@@ -169,6 +179,8 @@ interface ConversationMessage {
content: string
time: string
isAi: boolean
quoteOwner?: string
quoteContent?: string
}
const replies = ref<AiCommentReplyItem[]>([])
@@ -269,24 +281,18 @@ const stripHtml = (html: string) => {
return html.replace(/<[^>]+>/g, "").replace(/\n+/g, " ").trim()
}
const renderContent = (content: string, isAi: boolean) => {
const truncateQuote = (content: string, length = 40) => {
if (!content) return ""
const plain = stripHtml(content)
return plain.length > length ? plain.substring(0, length) + "..." : plain
}
const renderContent = (content: string) => {
if (!content) return "<span class='opacity-50'>(空)</span>"
let parsed = content
return content
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, "")
parsed = parsed.replace(/^>\s*(?:💬\s*)?\*\*(.*?)\*\*\s*[:]\s*(.*?)$/gm, (match, name, text) => {
const quoteBg = isAi ? 'bg-blue-50 border-blue-200' : 'bg-white/20 border-white/30'
const nameColor = isAi ? 'text-blue-700' : 'text-white font-semibold'
const textColor = isAi ? 'text-gray-600' : 'text-blue-100'
return `<div class="mb-3 px-3 py-2 rounded-lg border-l-4 ${quoteBg} text-xs">
<span class="${nameColor}">@${name.replace(/💬|🤖/g, '').trim()}</span>:
<span class="${textColor} opacity-90">${text.trim()}</span>
</div>`
})
parsed = parsed.replace(/\n/g, "<br/>")
return parsed
.replace(/\n/g, "<br/>")
}
const resetFilters = () => { filterStatus.value = ""; filterSentiment.value = ""; filterKeyword.value = ""; page.value = 1; fetchReplies() }