feat(ui): 评论三态显式 — 有内容 / 等待中(灰斜体) / 失败(红)

- 之前: 只有 commentary 内容才显示 v-if 块,否则空白
- 现在: 评论卡永远显示,三态语义明确:
    ok + 有内容 → 评论文本(正常)
    pending / n/a / null → 等待中(灰斜体 + 提示 enrichment_loop 会跑)
    failed → 评论生成失败(红斜体 + 失败原因)
- 列表 Feed + 详情 ArticleDetail 同步改造
- 加 commentaryState() 辅助函数:status+content → 'ok'|'waiting'|'failed'
This commit is contained in:
xiaji
2026-06-12 23:24:30 +08:00
parent 16536fe3a0
commit 66e57c6e07
2 changed files with 154 additions and 29 deletions

View File

@@ -64,6 +64,24 @@ const publishedAt = computed(() => article.value?.published_at || article.value?
const isOwner = computed(() => auth.isOwner)
const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean))
// === 评论三态 ===
// 'ok' + 有内容 → 显示真实评论
// 'ok' 但无内容 → 等待(防御性)
// 'pending' / 'n/a' / null → 等待
// 'failed' → 失败
type CommentaryState = 'ok' | 'waiting' | 'failed'
function commentaryState(status?: string | null, content?: string | null): CommentaryState {
if (status === 'failed') return 'failed'
if (status === 'ok' && content) return 'ok'
return 'waiting'
}
const angelState = computed<CommentaryState>(() =>
commentaryState(article.value?.commentary_status, article.value?.commentary)
)
const meituanState = computed<CommentaryState>(() =>
commentaryState(article.value?.commentary_meituan_status, article.value?.commentary_meituan)
)
/** 把"一坨"译文/原文按"中文句号"切成 <p> 段,改善"挤在一起"的观感。
* 优先按 \n 切(LLM 排版过的),没有换行再按句号/问号/感叹号切。
* 句中常见的"Mr./U.S."等缩写不会出现在中文译文里,按 6+ 字符才切,避免半句话被切。
@@ -258,44 +276,81 @@ onMounted(load)
本条翻译失败,可点 "重译" 重试,或查看后端日志
</NAlert>
<!-- 1) 评论( provider:Angel + 美团,各自一张卡) -->
<NCard v-if="article.commentary" class="detail-card" style="margin-top: 16px">
<!-- 1) Angel 评论(永远显示,三态:有内容 / 等待中 / 失败) -->
<NCard class="detail-card" style="margin-top: 16px">
<template #header>
<span class="card-header-title">💬 Angel 评论</span>
</template>
<template #header-extra>
<NTag size="tiny" :type="statusTagType(article.commentary_status)" :bordered="false" round>
{{ article.commentary_status || 'n/a' }}
{{
angelState === 'ok' ? 'ok'
: angelState === 'failed' ? 'failed'
: '等待中'
}}
</NTag>
</template>
<p class="commentary-text-detail">{{ article.commentary }}</p>
<p
v-if="angelState === 'ok'"
class="commentary-text-detail"
>{{ article.commentary }}</p>
<p v-else-if="angelState === 'failed'" class="commentary-text-detail commentary-text-failed-detail">
评论生成失败
<NText v-if="article.commentary_status === 'failed'" :depth="3" style="display:block; font-size:12px; margin-top:6px">
enrichment_loop 会自动重试;如持续失败,检查后端 worker 日志
</NText>
</p>
<p v-else class="commentary-text-detail commentary-text-waiting-detail">
🕒 等待评论中
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
enrichment_loop 会在翻译完成后跑(每篇约 15-20 )
</NText>
</p>
</NCard>
<NCard v-if="article.commentary_meituan" class="detail-card" style="margin-top: 16px">
<!-- 2) 美团评论(永远显示,三态) -->
<NCard class="detail-card" style="margin-top: 16px">
<template #header>
<span class="card-header-title commentary-header-meituan">🐱 美团评论</span>
</template>
<template #header-extra>
<NTag size="tiny" :type="statusTagType(article.commentary_meituan_status)" :bordered="false" round>
{{ article.commentary_meituan_status || 'n/a' }}
{{
meituanState === 'ok' ? 'ok'
: meituanState === 'failed' ? 'failed'
: '等待中'
}}
</NTag>
</template>
<p class="commentary-text-detail">{{ article.commentary_meituan }}</p>
<NText v-if="article.commentary_meituan_model" :depth="3" style="font-size: 11px; display:block; margin-top:8px">
<p
v-if="meituanState === 'ok'"
class="commentary-text-detail"
>{{ article.commentary_meituan }}</p>
<p v-else-if="meituanState === 'failed'" class="commentary-text-detail commentary-text-failed-detail">
评论生成失败
<NText
v-if="article.commentary_meituan_error"
:depth="3"
style="display:block; font-size:12px; margin-top:6px"
>
{{ article.commentary_meituan_error }}
</NText>
</p>
<p v-else class="commentary-text-detail commentary-text-waiting-detail">
🕒 等待评论中
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
enrichment_loop 会在翻译完成后跑(每篇约 15-20 )
</NText>
</p>
<NText
v-if="meituanState === 'ok' && article.commentary_meituan_model"
:depth="3"
style="font-size: 11px; display:block; margin-top:8px"
>
模型: {{ article.commentary_meituan_model }}
</NText>
</NCard>
<NAlert
v-else-if="article.commentary_meituan_status === 'failed' && article.commentary_meituan_error"
type="warning"
style="margin-top: 16px"
:show-icon="false"
>
<div><strong>美团评论生成失败</strong></div>
<div style="font-size: 12px; margin-top: 4px">{{ article.commentary_meituan_error }}</div>
</NAlert>
<!-- 2) 译文(优先 LLM 排版版) -->
<div v-if="showTranslation" style="margin-top: 16px">
<NCard v-if="article.body_zh_formatted" class="detail-card">
@@ -379,6 +434,16 @@ onMounted(load)
font-size: 15px;
}
.commentary-text-waiting-detail {
color: var(--color-text-faint);
font-style: italic;
}
.commentary-text-failed-detail {
color: #d03050;
font-style: italic;
}
.article-body-fallback {
white-space: pre-wrap;
line-height: 1.95;

View File

@@ -99,6 +99,18 @@ function commentaryStatusType(s?: string | null): 'success' | 'warning' | 'error
return 'default'
}
// === 评论三态语义 ===
// status 'ok' + 有内容 → 显示评论
// status 'ok' + 无内容 → 视为等待(防御性,正常不会触发)
// status 'pending' / 'n/a' / null → 等待中
// status 'failed' → 显示失败提示
type CommentaryState = 'ok' | 'waiting' | 'failed'
function commentaryState(status?: string | null, content?: string | null): CommentaryState {
if (status === 'failed') return 'failed'
if (status === 'ok' && content) return 'ok'
return 'waiting'
}
// 正文摘要(取 body_zh_text 前 N 字;没有就 fallback 到 summary_zh)
function bodyExcerpt(text?: string | null, max = 200): string {
if (!text) return ''
@@ -224,35 +236,71 @@ onMounted(async () => {
{{ bodyExcerpt(a.body_zh_text || a.summary_zh, 220) }}
</div>
<!-- 评论钩子( provider:Angel + 美团,淡木色背景 + 木色左边框, Android 对齐) -->
<!-- 评论钩子( provider:Angel + 美团,三态显式显示:有内容 / 等待中 / 失败) -->
<div
v-if="a.commentary || a.commentary_meituan"
v-if="true"
class="commentary-box"
>
<!-- Angel 评论 -->
<template v-if="a.commentary">
<template>
<NSpace align="center" :size="6" style="margin-bottom: 6px">
<span class="commentary-label">💬 Angel 评论</span>
<NTag size="tiny" :type="commentaryStatusType(a.commentary_status)" round :bordered="false">
{{ a.commentary_status || 'n/a' }}
<NTag
size="tiny"
:type="commentaryStatusType(a.commentary_status)"
round
:bordered="false"
>
{{
commentaryState(a.commentary_status, a.commentary) === 'ok' ? 'ok'
: commentaryState(a.commentary_status, a.commentary) === 'failed' ? 'failed'
: '等待中'
}}
</NTag>
</NSpace>
<div class="commentary-text">
<div
v-if="commentaryState(a.commentary_status, a.commentary) === 'ok'"
class="commentary-text"
>
{{ previewCommentary(a.commentary, 140) }}
</div>
<div v-else-if="commentaryState(a.commentary_status, a.commentary) === 'failed'" class="commentary-text commentary-text-failed">
评论生成失败,后台重试中
</div>
<div v-else class="commentary-text commentary-text-waiting">
🕒 等待评论中
</div>
</template>
<!-- 美团评论 -->
<template v-if="a.commentary_meituan">
<div v-if="a.commentary" class="commentary-divider" />
<template>
<div class="commentary-divider" />
<NSpace align="center" :size="6" style="margin-bottom: 6px">
<span class="commentary-label commentary-label-meituan">🐱 美团评论</span>
<NTag size="tiny" :type="commentaryStatusType(a.commentary_meituan_status)" round :bordered="false">
{{ a.commentary_meituan_status || 'n/a' }}
<NTag
size="tiny"
:type="commentaryStatusType(a.commentary_meituan_status)"
round
:bordered="false"
>
{{
commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok' ? 'ok'
: commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed' ? 'failed'
: '等待中'
}}
</NTag>
</NSpace>
<div class="commentary-text">
<div
v-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok'"
class="commentary-text"
>
{{ previewCommentary(a.commentary_meituan, 140) }}
</div>
<div v-else-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed'" class="commentary-text commentary-text-failed">
评论生成失败,后台重试中
</div>
<div v-else class="commentary-text commentary-text-waiting">
🕒 等待评论中
</div>
</template>
</div>
</NSpace>
@@ -303,6 +351,18 @@ onMounted(async () => {
line-height: 1.7;
}
.commentary-text-waiting {
color: var(--color-text-faint);
font-style: italic;
font-size: 12px;
}
.commentary-text-failed {
color: #d03050;
font-size: 12px;
font-style: italic;
}
.commentary-divider {
height: 1px;
background: var(--color-primary-soft);