fix(detail): 原始译文/原文标签字号 16→19px,加段落切分+行高 1.95
问题:'文章译文(原始)' 和 '文章原文' 标签下,内容是 body_zh_text / body_text 一坨没分段, 走 .article-body-fallback 16px,排版难看。 修复: 1) .article-body-fallback 字号 16→19,行高 1.85→1.95,加 max-width 720px 居中 + 0.02em letter-spacing 2) 加 splitIntoParagraphs: 已经是 HTML 标签的不动;否则按 \n 切;一坨纯文字按 [。!?!?] 切 3) 段首 text-indent 2em(中文书报排版) 4) translationBody / originalBody computed 替代内联三元 LLM 排版过的 (body_zh_formatted) 已经 <p> 段标签,不受影响。
This commit is contained in:
@@ -64,6 +64,64 @@ const publishedAt = computed(() => article.value?.published_at || article.value?
|
|||||||
const isOwner = computed(() => auth.isOwner)
|
const isOwner = computed(() => auth.isOwner)
|
||||||
const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean))
|
const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean))
|
||||||
|
|
||||||
|
/** 把"一坨"译文/原文按"中文句号"切成 <p> 段,改善"挤在一起"的观感。
|
||||||
|
* 优先按 \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) => `<p>${escapeHtml(p)}</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) => `<p>${escapeHtml(p.trim())}</p>`).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.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() {
|
async function rerunTranslation() {
|
||||||
if (!article.value) return
|
if (!article.value) return
|
||||||
if (!confirm('重新翻译会消耗配额,确认?')) return
|
if (!confirm('重新翻译会消耗配额,确认?')) return
|
||||||
@@ -234,8 +292,7 @@ onMounted(load)
|
|||||||
<template #header>
|
<template #header>
|
||||||
<span class="card-header-title">📖 文章译文(原始)</span>
|
<span class="card-header-title">📖 文章译文(原始)</span>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="article.body_zh_html" v-html="article.body_zh_html" />
|
<div v-if="translationBody" class="article-body-fallback" v-html="translationBody" />
|
||||||
<div v-else-if="article.body_zh_text" class="article-body-fallback">{{ article.body_zh_text }}</div>
|
|
||||||
<NText v-else :depth="3">暂无译文</NText>
|
<NText v-else :depth="3">暂无译文</NText>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,8 +311,7 @@ onMounted(load)
|
|||||||
<template #header>
|
<template #header>
|
||||||
<span class="card-header-title">📄 文章原文</span>
|
<span class="card-header-title">📄 文章原文</span>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="article.body_html" v-html="article.body_html" class="article-body-fallback" />
|
<div v-if="originalBody" class="article-body-fallback" v-html="originalBody" />
|
||||||
<div v-else class="article-body-fallback">{{ article.body_text }}</div>
|
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -293,10 +349,28 @@ onMounted(load)
|
|||||||
|
|
||||||
.article-body-fallback {
|
.article-body-fallback {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.85;
|
line-height: 1.95;
|
||||||
color: var(--color-letter);
|
color: var(--color-letter);
|
||||||
font-size: 16px;
|
font-size: 19px;
|
||||||
font-family: var(--font-sans);
|
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 {
|
.article-image {
|
||||||
|
|||||||
Reference in New Issue
Block a user