style(ui): 评论三态视觉化 — 等待/失败卡片增强辨识度
旧版问题:等待中卡片视觉太低调(灰小斜体一行),失败也不显眼, 用户截图反馈:看不出状态,无法判断是 bug 还是没跑。 新版: - 等待中: 浅蓝底 + 1.5px 虚线边框 + 旋转 spinner + '生成中' 徽章 - 失败: 浅红底 + 1.5px 实线红边框 + '失败' 徽章(详情页显示原因) - 成功: 保留原木色左边框样式 - 加 commentary-badge(带 spinner / 状态文字) 替代原 NTag,辨识度更高 - 列表/详情 min-height 60px,避免占位卡片太矮导致视觉跳 - 失败时详情页显示 commentary_meituan_error(给 owner 排查用)
This commit is contained in:
@@ -282,30 +282,36 @@ onMounted(load)
|
|||||||
<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>
|
<span
|
||||||
{{
|
class="commentary-badge"
|
||||||
angelState === 'ok' ? 'ok'
|
:class="`commentary-badge-${angelState}`"
|
||||||
: angelState === 'failed' ? 'failed'
|
>
|
||||||
: '等待中'
|
<span v-if="angelState === 'waiting'" class="commentary-spinner" />
|
||||||
}}
|
{{ angelState === 'ok' ? '已生成' : angelState === 'failed' ? '失败' : '生成中' }}
|
||||||
</NTag>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<p
|
<div
|
||||||
v-if="angelState === 'ok'"
|
v-if="angelState === 'ok'"
|
||||||
class="commentary-text-detail"
|
class="commentary-state-box commentary-state-ok"
|
||||||
>{{ article.commentary }}</p>
|
>{{ article.commentary }}</div>
|
||||||
<p v-else-if="angelState === 'failed'" class="commentary-text-detail commentary-text-failed-detail">
|
<div
|
||||||
⚠️ 评论生成失败
|
v-else-if="angelState === 'failed'"
|
||||||
|
class="commentary-state-box commentary-state-failed"
|
||||||
|
>
|
||||||
|
评论生成失败
|
||||||
<NText v-if="article.commentary_status === 'failed'" :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
<NText v-if="article.commentary_status === 'failed'" :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
||||||
enrichment_loop 会自动重试;如持续失败,检查后端 worker 日志
|
enrichment_loop 会自动重试;如持续失败,检查后端 worker 日志
|
||||||
</NText>
|
</NText>
|
||||||
</p>
|
</div>
|
||||||
<p v-else class="commentary-text-detail commentary-text-waiting-detail">
|
<div
|
||||||
🕒 等待评论中
|
v-else
|
||||||
|
class="commentary-state-box commentary-state-waiting"
|
||||||
|
>
|
||||||
|
正在生成评论…
|
||||||
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
||||||
enrichment_loop 会在翻译完成后跑(每篇约 15-20 秒)
|
enrichment_loop 会在翻译完成后跑(每篇约 15-20 秒)
|
||||||
</NText>
|
</NText>
|
||||||
</p>
|
</div>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<!-- 2) 美团评论(永远显示,三态) -->
|
<!-- 2) 美团评论(永远显示,三态) -->
|
||||||
@@ -314,20 +320,23 @@ onMounted(load)
|
|||||||
<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>
|
<span
|
||||||
{{
|
class="commentary-badge"
|
||||||
meituanState === 'ok' ? 'ok'
|
:class="`commentary-badge-${meituanState}`"
|
||||||
: meituanState === 'failed' ? 'failed'
|
>
|
||||||
: '等待中'
|
<span v-if="meituanState === 'waiting'" class="commentary-spinner" />
|
||||||
}}
|
{{ meituanState === 'ok' ? '已生成' : meituanState === 'failed' ? '失败' : '生成中' }}
|
||||||
</NTag>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<p
|
<div
|
||||||
v-if="meituanState === 'ok'"
|
v-if="meituanState === 'ok'"
|
||||||
class="commentary-text-detail"
|
class="commentary-state-box commentary-state-ok"
|
||||||
>{{ article.commentary_meituan }}</p>
|
>{{ article.commentary_meituan }}</div>
|
||||||
<p v-else-if="meituanState === 'failed'" class="commentary-text-detail commentary-text-failed-detail">
|
<div
|
||||||
⚠️ 评论生成失败
|
v-else-if="meituanState === 'failed'"
|
||||||
|
class="commentary-state-box commentary-state-failed"
|
||||||
|
>
|
||||||
|
评论生成失败
|
||||||
<NText
|
<NText
|
||||||
v-if="article.commentary_meituan_error"
|
v-if="article.commentary_meituan_error"
|
||||||
:depth="3"
|
:depth="3"
|
||||||
@@ -335,13 +344,16 @@ onMounted(load)
|
|||||||
>
|
>
|
||||||
{{ article.commentary_meituan_error }}
|
{{ article.commentary_meituan_error }}
|
||||||
</NText>
|
</NText>
|
||||||
</p>
|
</div>
|
||||||
<p v-else class="commentary-text-detail commentary-text-waiting-detail">
|
<div
|
||||||
🕒 等待评论中
|
v-else
|
||||||
|
class="commentary-state-box commentary-state-waiting"
|
||||||
|
>
|
||||||
|
正在生成评论…
|
||||||
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
<NText :depth="3" style="display:block; font-size:12px; margin-top:6px">
|
||||||
enrichment_loop 会在翻译完成后跑(每篇约 15-20 秒)
|
enrichment_loop 会在翻译完成后跑(每篇约 15-20 秒)
|
||||||
</NText>
|
</NText>
|
||||||
</p>
|
</div>
|
||||||
<NText
|
<NText
|
||||||
v-if="meituanState === 'ok' && article.commentary_meituan_model"
|
v-if="meituanState === 'ok' && article.commentary_meituan_model"
|
||||||
:depth="3"
|
:depth="3"
|
||||||
@@ -444,6 +456,72 @@ onMounted(load)
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === 评论三态(详情页)=== */
|
||||||
|
.commentary-state-box {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.85;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.commentary-state-ok {
|
||||||
|
color: var(--color-letter);
|
||||||
|
background: var(--color-primary-soft);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
.commentary-state-waiting {
|
||||||
|
color: #64748b;
|
||||||
|
background: #f0f4fa;
|
||||||
|
border: 1.5px dashed #94a3b8;
|
||||||
|
border-left: 4px solid #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.commentary-state-failed {
|
||||||
|
color: #991b1b;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1.5px solid #fca5a5;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.commentary-badge-ok {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
.commentary-badge-waiting {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.commentary-badge-failed {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: commentary-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes commentary-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.article-body-fallback {
|
.article-body-fallback {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.95;
|
line-height: 1.95;
|
||||||
|
|||||||
@@ -237,71 +237,81 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 评论钩子(双 provider:Angel + 美团,三态显式显示:有内容 / 等待中 / 失败) -->
|
<!-- 评论钩子(双 provider:Angel + 美团,三态显式显示:有内容 / 等待中 / 失败) -->
|
||||||
<div
|
<div class="commentary-stack">
|
||||||
v-if="true"
|
|
||||||
class="commentary-box"
|
|
||||||
>
|
|
||||||
<!-- Angel 评论 -->
|
<!-- Angel 评论 -->
|
||||||
<template>
|
<div
|
||||||
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
class="commentary-item"
|
||||||
|
:class="{
|
||||||
|
'commentary-item-waiting': commentaryState(a.commentary_status, a.commentary) === 'waiting',
|
||||||
|
'commentary-item-failed': commentaryState(a.commentary_status, a.commentary) === 'failed',
|
||||||
|
'commentary-item-ok': commentaryState(a.commentary_status, a.commentary) === 'ok',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="commentary-head">
|
||||||
<span class="commentary-label">💬 Angel 评论</span>
|
<span class="commentary-label">💬 Angel 评论</span>
|
||||||
<NTag
|
<span
|
||||||
size="tiny"
|
class="commentary-badge"
|
||||||
:type="commentaryStatusType(a.commentary_status)"
|
:class="`commentary-badge-${commentaryState(a.commentary_status, a.commentary)}`"
|
||||||
round
|
|
||||||
:bordered="false"
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="commentaryState(a.commentary_status, a.commentary) === 'waiting'"
|
||||||
|
class="commentary-spinner"
|
||||||
|
/>
|
||||||
{{
|
{{
|
||||||
commentaryState(a.commentary_status, a.commentary) === 'ok' ? 'ok'
|
commentaryState(a.commentary_status, a.commentary) === 'ok' ? '已生成'
|
||||||
: commentaryState(a.commentary_status, a.commentary) === 'failed' ? 'failed'
|
: commentaryState(a.commentary_status, a.commentary) === 'failed' ? '失败'
|
||||||
: '等待中'
|
: '生成中'
|
||||||
}}
|
}}
|
||||||
</NTag>
|
</span>
|
||||||
</NSpace>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="commentaryState(a.commentary_status, a.commentary) === 'ok'"
|
v-if="commentaryState(a.commentary_status, a.commentary) === 'ok'"
|
||||||
class="commentary-text"
|
class="commentary-text"
|
||||||
>
|
>{{ previewCommentary(a.commentary, 140) }}</div>
|
||||||
{{ previewCommentary(a.commentary, 140) }}
|
|
||||||
</div>
|
|
||||||
<div v-else-if="commentaryState(a.commentary_status, a.commentary) === 'failed'" class="commentary-text commentary-text-failed">
|
<div v-else-if="commentaryState(a.commentary_status, a.commentary) === 'failed'" class="commentary-text commentary-text-failed">
|
||||||
⚠️ 评论生成失败,后台重试中
|
评论生成失败,后台 enrichment_loop 会重试
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="commentary-text commentary-text-waiting">
|
<div v-else class="commentary-text commentary-text-waiting">
|
||||||
🕒 等待评论中
|
正在生成评论…
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<!-- 美团评论 -->
|
<!-- 美团评论 -->
|
||||||
<template>
|
<div
|
||||||
<div class="commentary-divider" />
|
class="commentary-item"
|
||||||
<NSpace align="center" :size="6" style="margin-bottom: 6px">
|
:class="{
|
||||||
|
'commentary-item-waiting': commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'waiting',
|
||||||
|
'commentary-item-failed': commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed',
|
||||||
|
'commentary-item-ok': commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="commentary-head">
|
||||||
<span class="commentary-label commentary-label-meituan">🐱 美团评论</span>
|
<span class="commentary-label commentary-label-meituan">🐱 美团评论</span>
|
||||||
<NTag
|
<span
|
||||||
size="tiny"
|
class="commentary-badge"
|
||||||
:type="commentaryStatusType(a.commentary_meituan_status)"
|
:class="`commentary-badge-${commentaryState(a.commentary_meituan_status, a.commentary_meituan)}`"
|
||||||
round
|
|
||||||
:bordered="false"
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'waiting'"
|
||||||
|
class="commentary-spinner"
|
||||||
|
/>
|
||||||
{{
|
{{
|
||||||
commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok' ? 'ok'
|
commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok' ? '已生成'
|
||||||
: commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed' ? 'failed'
|
: commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed' ? '失败'
|
||||||
: '等待中'
|
: '生成中'
|
||||||
}}
|
}}
|
||||||
</NTag>
|
</span>
|
||||||
</NSpace>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok'"
|
v-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'ok'"
|
||||||
class="commentary-text"
|
class="commentary-text"
|
||||||
>
|
>{{ previewCommentary(a.commentary_meituan, 140) }}</div>
|
||||||
{{ 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 v-else-if="commentaryState(a.commentary_meituan_status, a.commentary_meituan) === 'failed'" class="commentary-text commentary-text-failed">
|
||||||
⚠️ 评论生成失败,后台重试中
|
评论生成失败,后台 enrichment_loop 会重试
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="commentary-text commentary-text-waiting">
|
<div v-else class="commentary-text commentary-text-waiting">
|
||||||
🕒 等待评论中
|
正在生成评论…
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NCard>
|
</NCard>
|
||||||
@@ -323,51 +333,133 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 评论钩子(项目级统一样式,与 Android ArticleCard.kt 的 CommentaryBox 对齐) */
|
/* === 评论钩子(双 provider,三态视觉化) === */
|
||||||
.commentary-box {
|
.commentary-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 12px 14px;
|
}
|
||||||
|
|
||||||
|
.commentary-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
min-height: 56px; /* 避免占位卡片太矮导致视觉跳 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === OK 状态(有内容)== */
|
||||||
|
.commentary-item-ok {
|
||||||
background: var(--color-primary-soft);
|
background: var(--color-primary-soft);
|
||||||
border-left: 3px solid var(--color-primary);
|
border-left: 3px solid var(--color-primary);
|
||||||
border-radius: 6px;
|
color: var(--color-letter);
|
||||||
|
}
|
||||||
|
.commentary-item-ok .commentary-label {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.commentary-item-ok .commentary-text {
|
||||||
color: var(--color-letter);
|
color: var(--color-letter);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === 等待中(生成中)== */
|
||||||
|
.commentary-item-waiting {
|
||||||
|
background: #f0f4fa; /* 浅蓝底 */
|
||||||
|
border: 1.5px dashed #94a3b8; /* 虚线边框,提示"未完成" */
|
||||||
|
border-left: 4px solid #64748b; /* 左侧稍粗的灰条,跟 ok 状态一致位置 */
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.commentary-item-waiting .commentary-label {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.commentary-item-waiting .commentary-text {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 失败 === */
|
||||||
|
.commentary-item-failed {
|
||||||
|
background: #fef2f2; /* 浅红底 */
|
||||||
|
border: 1.5px solid #fca5a5;
|
||||||
|
border-left: 4px solid #ef4444;
|
||||||
|
color: #991b1b;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.commentary-item-failed .commentary-label {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
.commentary-item-failed .commentary-text {
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.commentary-label {
|
.commentary-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentary-label-meituan {
|
.commentary-label-meituan {
|
||||||
color: #c2410c; /* 橙色,与 Angel 区分 */
|
color: #c2410c; /* 橙色,与 Angel 区分 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentary-text {
|
.commentary-text-failed,
|
||||||
color: var(--color-letter);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentary-text-waiting {
|
.commentary-text-waiting {
|
||||||
color: var(--color-text-faint);
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentary-text-failed {
|
|
||||||
color: #d03050;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentary-divider {
|
.commentary-head {
|
||||||
height: 1px;
|
display: flex;
|
||||||
background: var(--color-primary-soft);
|
align-items: center;
|
||||||
margin: 10px 0;
|
gap: 8px;
|
||||||
opacity: 0.6;
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentary-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.commentary-badge-ok {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
.commentary-badge-waiting {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.commentary-badge-failed {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转的 spinner(等待中) */
|
||||||
|
.commentary-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: commentary-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes commentary-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 桌面端默认宽度 ===== */
|
/* ===== 桌面端默认宽度 ===== */
|
||||||
|
|||||||
Reference in New Issue
Block a user