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:
@@ -64,6 +64,24 @@ 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))
|
||||||
|
|
||||||
|
// === 评论三态 ===
|
||||||
|
// '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> 段,改善"挤在一起"的观感。
|
/** 把"一坨"译文/原文按"中文句号"切成 <p> 段,改善"挤在一起"的观感。
|
||||||
* 优先按 \n 切(LLM 排版过的),没有换行再按句号/问号/感叹号切。
|
* 优先按 \n 切(LLM 排版过的),没有换行再按句号/问号/感叹号切。
|
||||||
* 句中常见的"Mr./U.S."等缩写不会出现在中文译文里,按 6+ 字符才切,避免半句话被切。
|
* 句中常见的"Mr./U.S."等缩写不会出现在中文译文里,按 6+ 字符才切,避免半句话被切。
|
||||||
@@ -258,44 +276,81 @@ onMounted(load)
|
|||||||
本条翻译失败,可点 "重译" 重试,或查看后端日志。
|
本条翻译失败,可点 "重译" 重试,或查看后端日志。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
|
|
||||||
<!-- 1) 评论(双 provider:Angel + 美团,各自一张卡) -->
|
<!-- 1) Angel 评论(永远显示,三态:有内容 / 等待中 / 失败) -->
|
||||||
<NCard v-if="article.commentary" class="detail-card" style="margin-top: 16px">
|
<NCard class="detail-card" style="margin-top: 16px">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="card-header-title">💬 Angel 评论</span>
|
<span class="card-header-title">💬 Angel 评论</span>
|
||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<NTag size="tiny" :type="statusTagType(article.commentary_status)" :bordered="false" round>
|
<NTag size="tiny" :type="statusTagType(article.commentary_status)" :bordered="false" round>
|
||||||
{{ article.commentary_status || 'n/a' }}
|
{{
|
||||||
|
angelState === 'ok' ? 'ok'
|
||||||
|
: angelState === 'failed' ? 'failed'
|
||||||
|
: '等待中'
|
||||||
|
}}
|
||||||
</NTag>
|
</NTag>
|
||||||
</template>
|
</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>
|
||||||
|
|
||||||
<NCard v-if="article.commentary_meituan" class="detail-card" style="margin-top: 16px">
|
<!-- 2) 美团评论(永远显示,三态) -->
|
||||||
|
<NCard class="detail-card" style="margin-top: 16px">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="card-header-title commentary-header-meituan">🐱 美团评论</span>
|
<span class="card-header-title commentary-header-meituan">🐱 美团评论</span>
|
||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<NTag size="tiny" :type="statusTagType(article.commentary_meituan_status)" :bordered="false" round>
|
<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>
|
</NTag>
|
||||||
</template>
|
</template>
|
||||||
<p class="commentary-text-detail">{{ article.commentary_meituan }}</p>
|
<p
|
||||||
<NText v-if="article.commentary_meituan_model" :depth="3" style="font-size: 11px; display:block; margin-top:8px">
|
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 }}
|
模型: {{ article.commentary_meituan_model }}
|
||||||
</NText>
|
</NText>
|
||||||
</NCard>
|
</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 排版版) -->
|
<!-- 2) 译文(优先 LLM 排版版) -->
|
||||||
<div v-if="showTranslation" style="margin-top: 16px">
|
<div v-if="showTranslation" style="margin-top: 16px">
|
||||||
<NCard v-if="article.body_zh_formatted" class="detail-card">
|
<NCard v-if="article.body_zh_formatted" class="detail-card">
|
||||||
@@ -379,6 +434,16 @@ onMounted(load)
|
|||||||
font-size: 15px;
|
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 {
|
.article-body-fallback {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.95;
|
line-height: 1.95;
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ function commentaryStatusType(s?: string | null): 'success' | 'warning' | 'error
|
|||||||
return 'default'
|
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)
|
// 正文摘要(取 body_zh_text 前 N 字;没有就 fallback 到 summary_zh)
|
||||||
function bodyExcerpt(text?: string | null, max = 200): string {
|
function bodyExcerpt(text?: string | null, max = 200): string {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
@@ -224,35 +236,71 @@ onMounted(async () => {
|
|||||||
{{ bodyExcerpt(a.body_zh_text || a.summary_zh, 220) }}
|
{{ bodyExcerpt(a.body_zh_text || a.summary_zh, 220) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 评论钩子(双 provider:Angel + 美团,淡木色背景 + 木色左边框,与 Android 对齐) -->
|
<!-- 评论钩子(双 provider:Angel + 美团,三态显式显示:有内容 / 等待中 / 失败) -->
|
||||||
<div
|
<div
|
||||||
v-if="a.commentary || a.commentary_meituan"
|
v-if="true"
|
||||||
class="commentary-box"
|
class="commentary-box"
|
||||||
>
|
>
|
||||||
<!-- Angel 评论 -->
|
<!-- Angel 评论 -->
|
||||||
<template v-if="a.commentary">
|
<template>
|
||||||
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
||||||
<span class="commentary-label">💬 Angel 评论</span>
|
<span class="commentary-label">💬 Angel 评论</span>
|
||||||
<NTag size="tiny" :type="commentaryStatusType(a.commentary_status)" round :bordered="false">
|
<NTag
|
||||||
{{ a.commentary_status || 'n/a' }}
|
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>
|
</NTag>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<div class="commentary-text">
|
<div
|
||||||
|
v-if="commentaryState(a.commentary_status, a.commentary) === 'ok'"
|
||||||
|
class="commentary-text"
|
||||||
|
>
|
||||||
{{ previewCommentary(a.commentary, 140) }}
|
{{ previewCommentary(a.commentary, 140) }}
|
||||||
</div>
|
</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>
|
||||||
<!-- 美团评论 -->
|
<!-- 美团评论 -->
|
||||||
<template v-if="a.commentary_meituan">
|
<template>
|
||||||
<div v-if="a.commentary" class="commentary-divider" />
|
<div class="commentary-divider" />
|
||||||
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
||||||
<span class="commentary-label commentary-label-meituan">🐱 美团评论</span>
|
<span class="commentary-label commentary-label-meituan">🐱 美团评论</span>
|
||||||
<NTag size="tiny" :type="commentaryStatusType(a.commentary_meituan_status)" round :bordered="false">
|
<NTag
|
||||||
{{ a.commentary_meituan_status || 'n/a' }}
|
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>
|
</NTag>
|
||||||
</NSpace>
|
</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) }}
|
{{ previewCommentary(a.commentary_meituan, 140) }}
|
||||||
</div>
|
</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
@@ -303,6 +351,18 @@ onMounted(async () => {
|
|||||||
line-height: 1.7;
|
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 {
|
.commentary-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: var(--color-primary-soft);
|
background: var(--color-primary-soft);
|
||||||
|
|||||||
Reference in New Issue
Block a user