feat: pair community post translations

This commit is contained in:
hajimi
2026-08-02 02:27:04 +08:00
parent ac20d5d914
commit 18b7f843ae
3 changed files with 204 additions and 65 deletions
@@ -53,24 +53,36 @@
<text>付费内容</text>
</view>
<text v-if="postPreview.title" class="detail-post-card__title">{{ postPreview.title }}</text>
<view v-if="postPreview.summary" class="detail-post-card__summary-wrap">
<text class="detail-post-card__summary"
:class="{ 'detail-post-card__summary--collapsed': previewCanExpand && !previewExpanded }">
{{ postPreview.summary }}
</text>
<text v-if="previewCanExpand" class="detail-post-card__expand"
@tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
<view v-if="hasBilingualContent" class="detail-post-card__bilingual">
<view v-for="(block, index) in visibleBilingualBlocks" :key="`bilingual-${index}`"
class="detail-post-card__bilingual-pair">
<text v-if="block.original"
class="detail-post-card__bilingual-original">{{ block.original }}</text>
<text v-if="block.translated"
class="detail-post-card__bilingual-translated">{{ block.translated }}</text>
</view>
</view>
<view v-if="translatedLines.length" class="detail-post-card__translation">
<text class="detail-post-card__translation-label">译文</text>
<text v-for="(line, index) in translatedLines" :key="`translated-${index}`"
class="detail-post-card__translation-text">{{ line }}</text>
</view>
<template v-else>
<text v-if="postPreview.title" class="detail-post-card__title">{{ postPreview.title }}</text>
<view v-if="postPreview.summary" class="detail-post-card__summary-wrap">
<text class="detail-post-card__summary"
:class="{ 'detail-post-card__summary--collapsed': previewCanExpand && !previewExpanded }">
{{ postPreview.summary }}
</text>
<text v-if="previewCanExpand" class="detail-post-card__expand"
@tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
</view>
</template>
<text v-if="hasBilingualContent && previewCanExpand"
class="detail-post-card__expand detail-post-card__expand--bilingual"
@tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
<view v-if="post.images && post.images.length > 0" class="detail-images">
<view v-if="showExternalCover" class="detail-images__external" @tap="openSourceUrl">
@@ -335,19 +347,80 @@ const splitPostPreview = (value: unknown) => {
return { title: '', summary: content }
}
const splitContentLines = (value: unknown) => String(value || '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
const shouldKeepEnglishPeriod = (text: string, index: number) => {
const previous = text[index - 1] || ''
const next = text[index + 1] || ''
if (/\d/.test(previous) && /\d/.test(next)) return true
if (/[A-Za-z]/.test(previous) && /[A-Za-z]/.test(next)) return true
const token = text.slice(0, index).match(/([A-Za-z](?:\.[A-Za-z])*)$/)?.[1] || ''
const nextNonSpace = text.slice(index + 1).match(/\S/)?.[0] || ''
if (/^(?:[A-Za-z]\.)+[A-Za-z]$/.test(token)) return true
return /^[A-Z]$/.test(token) && /[A-Za-z]/.test(nextNonSpace)
}
const splitSentenceLine = (text: string) => {
const sentences: string[] = []
let start = 0
for (let index = 0; index < text.length; index += 1) {
const character = text[index]
const isStop = /[。!?!?]/.test(character) || character === '.'
if (!isStop || (character === '.' && shouldKeepEnglishPeriod(text, index))) continue
let end = index + 1
while (end < text.length && /[。!?.!?]/.test(text[end])) end += 1
while (end < text.length && /[”’"')】\]]/.test(text[end])) end += 1
const sentence = text.slice(start, end).trim()
if (sentence) sentences.push(sentence)
start = end
index = end - 1
}
const tail = text.slice(start).trim()
if (tail) sentences.push(tail)
return sentences
}
const splitContentLines = (value: unknown) => {
const normalized = String(value || '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.trim()
if (!normalized) return []
const manualLines = normalized
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter(Boolean)
const sentenceLines = manualLines.flatMap((line) => splitSentenceLine(line))
return sentenceLines.length ? sentenceLines : [normalized]
}
const postPreview = computed(() => splitPostPreview(post.value?.content))
const originalLines = computed(() => splitContentLines(post.value?.content))
const translatedLines = computed(() => splitContentLines(translatedText.value))
const bilingualBlocks = computed(() => {
const size = Math.max(originalLines.value.length, translatedLines.value.length)
return Array.from({ length: size }, (_, index) => ({
original: originalLines.value[index] || '',
translated: translatedLines.value[index] || ''
})).filter((block) => block.original || block.translated)
})
const hasBilingualContent = computed(() => originalLines.value.length > 0 && translatedLines.value.length > 0)
const previewCanExpand = computed(() => {
if (hasBilingualContent.value) return bilingualBlocks.value.length > 1
const summary = postPreview.value.summary
return summary.length > 90 || summary.split('\n').length > 3
})
const visibleBilingualBlocks = computed(() => {
if (!previewCanExpand.value || previewExpanded.value) return bilingualBlocks.value
return bilingualBlocks.value.slice(0, 1)
})
const pageTitle = computed(() => postPreview.value.title || postPreview.value.summary.slice(0, 40) || '帖子详情')
const userPointsText = computed(() => {
if (!getToken()) return '登录后查看'
@@ -555,6 +628,10 @@ const applyUnlockedContent = async (result: any) => {
post.value.is_purchased = true
if (!result.from_cache) post.value.paid_count = Number(post.value.paid_count || 0) + 1
previewExpanded.value = true
if (isTrumpPost.value && !translatedText.value && !autoTranslateStarted.value) {
autoTranslateStarted.value = true
void requestTranslate()
}
await loadUserPoints()
}
@@ -659,7 +736,7 @@ const openSourceUrl = () => {
uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(trumpSourceUrl.value)}` })
}
const requestTranslate = async () => {
async function requestTranslate() {
if (!post.value?.id) return
try {
const result = await translatePost({ post_id: post.value.id })
@@ -984,26 +1061,38 @@ const formatCommentTime = (value: any) => {
white-space: nowrap;
}
&__translation {
margin-top: 22rpx;
padding: 20rpx 22rpx;
&__bilingual {
margin-top: 20rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
border-radius: 14rpx;
background: #f6f8fc;
gap: 24rpx;
}
&__translation-label {
color: #1468f5;
font-size: 23rpx;
font-weight: 700;
&__bilingual-pair {
display: flex;
flex-direction: column;
gap: 8rpx;
}
&__translation-text {
color: #465164;
&__bilingual-original {
color: #20242b;
font-size: 29rpx;
font-weight: 500;
line-height: 1.72;
word-break: break-word;
}
&__bilingual-translated {
color: #647187;
font-size: 27rpx;
line-height: 1.72;
word-break: break-word;
}
&__expand--bilingual {
display: block;
margin-top: 12rpx;
text-align: right;
}
}