diff --git a/frontend/src/views/Feed.vue b/frontend/src/views/Feed.vue index 517e545..9f414fa 100644 --- a/frontend/src/views/Feed.vue +++ b/frontend/src/views/Feed.vue @@ -130,11 +130,35 @@ function splitCategory(c?: string | null): string[] { return c.split(',').map((s) => s.trim()).filter(Boolean) } -// 评论预览:长文截断 -function previewCommentary(c?: string | null, max = 120): string { +// === 搜索关键字高亮(用于标题/评论预览的渲染)=== +// 1) HTML escape — 防止 XSS(content 来自外部 RSS/ingest,不可信) +// 2) 不区分大小写匹配 q +// 3) 用 包裹匹配项(全局 mark 标签由 .commentary-text / 标题区样式接管) +// 返回 HTML 字符串,供 v-html 使用。 +function escapeHtml(s: string): string { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +function highlightHtml(text: string | null | undefined, q: string): string { + const safe = escapeHtml(text || '') + if (!q || !q.trim()) return safe + // 转义 q 里可能的正则元字符(用户搜 "*.x" 等不应该当 regex) + const escapedQ = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const re = new RegExp(escapedQ, 'gi') + return safe.replace(re, (m) => `${m}`) +} + +// 评论预览:长文截断 + 搜索时高亮 +// 返回 HTML 字符串(供 v-html 渲染)。q 为空时等价于纯文本(无高亮标签)。 +function previewCommentary(c?: string | null, max = 120, q = ''): string { if (!c) return '' const trimmed = c.replace(/\s+/g, ' ').trim() - return trimmed.length > max ? trimmed.slice(0, max) + '…' : trimmed + const text = trimmed.length > max ? trimmed.slice(0, max) + '…' : trimmed + return highlightHtml(text, q) } function commentaryStatusType(s?: string | null): 'success' | 'warning' | 'error' | 'default' { @@ -269,7 +293,7 @@ onMounted(async () => { - +
- {{ a.title_zh }} -
+ v-html="highlightHtml(a.title_zh, q)" + /> - +
- {{ a.title }} -
+ v-html="highlightHtml(a.title, q)" + /> { 正文摘要: - 长新闻:body_zh_text 截前 200 字(去多余空白) - 短新闻:body_zh_text(=body_text)完整展示,保留换行 + - 搜索时高亮 q(escape + 包裹,无 XSS) -->
{ font-size: 14px; line-height: 1.75; " - > - {{ + v-html="highlightHtml( a.is_short_news ? bodyExcerpt(a.body_zh_text || a.summary_zh || '', 5000, true) - : bodyExcerpt(a.body_zh_text || a.summary_zh, 200) - }} -
+ : bodyExcerpt(a.body_zh_text || a.summary_zh, 200), + q + )" + />
@@ -360,7 +383,8 @@ onMounted(async () => {
{{ previewCommentary(a.commentary, 140) }}
+ v-html="previewCommentary(a.commentary, 140, q)" + />
评论生成失败,后台 enrichment_loop 会重试
@@ -396,7 +420,8 @@ onMounted(async () => {
{{ previewCommentary(a.commentary_meituan, 140) }}
+ v-html="previewCommentary(a.commentary_meituan, 140, q)" + />
评论生成失败,后台 enrichment_loop 会重试
@@ -647,6 +672,15 @@ onMounted(async () => { font-size: 11px; } +/* === 搜索关键字高亮( 标签)=== */ +.feed-list :deep(mark) { + background: #fff3a0; /* 暖黄底,跟 Naive UI 主题协调 */ + color: inherit; /* 保持父级文字色,不被 默认色影响 */ + padding: 0 2px; + border-radius: 2px; + font-weight: 600; /* 命中后略加粗,视觉强调 */ +} + /* === 底部操作栏(浮在卡片右下角)=== */ .feed-actions { display: flex;