feat(feed): 首页列表展示分类标签 + LLM 评论预览

- 后端 ArticleListItem schema 加 commentary / commentary_status / image_ai_url
- 后端 articles.list 接口把以上字段写入响应
- 前端 API 类型同步
- 前端 Feed.vue 卡片:
  * 分类 tag(逗号分隔,多 tag)
  * 评论预览(蓝色引线块,140 字截断,带状态点)
  * 用户点进详情页前就能看到 LLM 点评钩子
This commit is contained in:
Mavis
2026-06-09 15:59:48 +08:00
parent 76e95908e8
commit 474299baf9
4 changed files with 64 additions and 1 deletions

View File

@@ -127,6 +127,10 @@ async def list_articles(
published_at=art.published_at,
fetched_at=art.fetched_at,
image_url=art.image_url,
# 列表预览钩子:分类 + LLM 点评 + AI 插图 缩略图
commentary=art.commentary,
commentary_status=art.commentary_status,
image_ai_url=art.image_ai_url,
is_starred=art.id in starred_ids,
)
items.append(item)

View File

@@ -16,7 +16,7 @@ class SourceBrief(BaseModel):
class ArticleListItem(BaseModel):
"""列表项:精简字段。"""
"""列表项:精简字段(首页只露钩子,详细阅读进详情页)"""
model_config = ConfigDict(from_attributes=True)
@@ -31,6 +31,10 @@ class ArticleListItem(BaseModel):
published_at: datetime | None = None
fetched_at: datetime
image_url: str | None = None
# === 列表预览钩子:点击进详情前的"诱导点" ===
commentary: str | None = None # LLM 点评(列表里截断显示)
commentary_status: str | None = None # ok/failed/pending/n/a
image_ai_url: str | None = None # AI 插图(列表里缩略图)
is_starred: bool = False

View File

@@ -31,6 +31,10 @@ export interface ArticleListItem {
published_at?: string | null
fetched_at: string
image_url?: string | null
// 列表预览钩子(首页展示用,详情页看完整版)
commentary?: string | null
commentary_status?: string | null
image_ai_url?: string | null
is_starred: boolean
}

View File

@@ -68,6 +68,26 @@ function fmtTime(s?: string | null) {
return dayjs(s).fromNow()
}
// category 是逗号分隔字符串(LLM 输出),拆成多个 tag
function splitCategory(c?: string | null): string[] {
if (!c) return []
return c.split(',').map((s) => s.trim()).filter(Boolean)
}
// 评论预览:长文截断,带状态点
function previewCommentary(c?: string | null, max = 120): string {
if (!c) return ''
const trimmed = c.replace(/\s+/g, ' ').trim()
return trimmed.length > max ? trimmed.slice(0, max) + '…' : trimmed
}
function commentaryStatusType(s?: string | null): 'success' | 'warning' | 'error' | 'default' {
if (s === 'ok') return 'success'
if (s === 'failed') return 'error'
if (s === 'pending') return 'warning'
return 'default'
}
onMounted(async () => {
await loadSources()
await load()
@@ -112,6 +132,15 @@ onMounted(async () => {
<NTag v-if="a.translation_status !== 'ok'" size="small" type="warning">
{{ a.translation_status }}
</NTag>
<!-- 分类标签(LLM classify 输出,多分类逗号分隔) -->
<NTag
v-for="c in splitCategory(a.category)"
:key="c"
size="small"
type="success"
>
{{ c }}
</NTag>
<NText depth="3" style="font-size: 12px">{{ fmtTime(a.published_at || a.fetched_at) }}</NText>
</NSpace>
<div style="font-size: 16px; font-weight: 600; color: #333">{{ a.title }}</div>
@@ -121,6 +150,28 @@ onMounted(async () => {
<div v-if="a.summary_zh" style="color: #666; font-size: 13px; margin-top: 4px">
{{ a.summary_zh.slice(0, 200) }}{{ a.summary_zh.length > 200 ? '…' : '' }}
</div>
<!-- 评论预览(列表钩子,详情页有完整版) -->
<div
v-if="a.commentary"
style="
margin-top: 8px;
padding: 8px 10px;
background: #f6f8ff;
border-left: 3px solid #2080f0;
border-radius: 4px;
color: #444;
font-size: 13px;
line-height: 1.6;
"
>
<NSpace align="center" :size="6" style="margin-bottom: 4px">
<NText depth="2" style="font-size: 12px; font-weight: 600">💬 评论</NText>
<NTag size="tiny" :type="commentaryStatusType(a.commentary_status)">
{{ a.commentary_status || 'n/a' }}
</NTag>
</NSpace>
<span>{{ previewCommentary(a.commentary, 140) }}</span>
</div>
</NSpace>
</NCard>
<NSpace v-if="!exhausted" justify="center" style="margin: 16px 0">