diff --git a/frontend/src/views/ArticleDetail.vue b/frontend/src/views/ArticleDetail.vue index 9336800..484a03f 100644 --- a/frontend/src/views/ArticleDetail.vue +++ b/frontend/src/views/ArticleDetail.vue @@ -64,6 +64,64 @@ const publishedAt = computed(() => article.value?.published_at || article.value? const isOwner = computed(() => auth.isOwner) const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean)) +/** 把"一坨"译文/原文按"中文句号"切成

段,改善"挤在一起"的观感。 + * 优先按 \n 切(LLM 排版过的),没有换行再按句号/问号/感叹号切。 + * 句中常见的"Mr./U.S."等缩写不会出现在中文译文里,按 6+ 字符才切,避免半句话被切。 + */ +function splitIntoParagraphs(html: string): string { + // 已经是 HTML 标签(LLM 排版过),不动 + if (/<(p|div|br)\b/i.test(html)) return html + // 按 \n 切 + if (/\n/.test(html)) { + return html + .split(/\n+/) + .map((p) => p.trim()) + .filter(Boolean) + .map((p) => `

${escapeHtml(p)}

`) + .join('') + } + // 一坨纯文字:按句号/问号/感叹号切,每段最多 140 字 + const text = html.trim() + if (!text) return '' + const chunks: string[] = [] + const re = /([。!?!?])(?=[^。!?!?]{0,140})/g + let last = 0 + let m: RegExpExecArray | null + while ((m = re.exec(text)) !== null) { + const end = m.index + 1 + if (end - last > 6) { + chunks.push(text.slice(last, end)) + last = end + } + } + if (last < text.length) chunks.push(text.slice(last)) + return chunks.map((p) => `

${escapeHtml(p.trim())}

`).join('') +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, '&') + .replace(//g, '>') +} + +const translationBody = computed(() => { + const a = article.value + if (!a) return '' + if (a.body_zh_formatted) return a.body_zh_formatted + if (a.body_zh_html) return splitIntoParagraphs(a.body_zh_html) + if (a.body_zh_text) return splitIntoParagraphs(a.body_zh_text) + return '' +}) + +const originalBody = computed(() => { + const a = article.value + if (!a) return '' + if (a.body_html) return splitIntoParagraphs(a.body_html) + if (a.body_text) return splitIntoParagraphs(a.body_text) + return '' +}) + async function rerunTranslation() { if (!article.value) return if (!confirm('重新翻译会消耗配额,确认?')) return @@ -234,8 +292,7 @@ onMounted(load) -
-
{{ article.body_zh_text }}
+
暂无译文
@@ -254,8 +311,7 @@ onMounted(load) -
-
{{ article.body_text }}
+
@@ -293,10 +349,28 @@ onMounted(load) .article-body-fallback { white-space: pre-wrap; - line-height: 1.85; + line-height: 1.95; color: var(--color-letter); - font-size: 16px; + font-size: 19px; font-family: var(--font-sans); + max-width: 720px; + margin: 0 auto; + letter-spacing: 0.02em; + text-align: justify; + text-justify: inter-ideograph; +} + +/* 原始译文里的"中文句号"或"换行"在浏览器里经常被压成一坨, + 强制段间加空行 + 段首缩进 2 字符,接近纸质书阅读感 */ +.article-body-fallback :deep(p), +.article-body-fallback :deep(div) { + margin: 0 0 1.1em 0; + text-indent: 2em; +} + +.article-body-fallback :deep(p:last-child), +.article-body-fallback :deep(div:last-child) { + margin-bottom: 0; } .article-image {