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:
@@ -127,6 +127,10 @@ async def list_articles(
|
|||||||
published_at=art.published_at,
|
published_at=art.published_at,
|
||||||
fetched_at=art.fetched_at,
|
fetched_at=art.fetched_at,
|
||||||
image_url=art.image_url,
|
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,
|
is_starred=art.id in starred_ids,
|
||||||
)
|
)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SourceBrief(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ArticleListItem(BaseModel):
|
class ArticleListItem(BaseModel):
|
||||||
"""列表项:精简字段。"""
|
"""列表项:精简字段(首页只露钩子,详细阅读进详情页)。"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@@ -31,6 +31,10 @@ class ArticleListItem(BaseModel):
|
|||||||
published_at: datetime | None = None
|
published_at: datetime | None = None
|
||||||
fetched_at: datetime
|
fetched_at: datetime
|
||||||
image_url: str | None = None
|
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
|
is_starred: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export interface ArticleListItem {
|
|||||||
published_at?: string | null
|
published_at?: string | null
|
||||||
fetched_at: string
|
fetched_at: string
|
||||||
image_url?: string | null
|
image_url?: string | null
|
||||||
|
// 列表预览钩子(首页展示用,详情页看完整版)
|
||||||
|
commentary?: string | null
|
||||||
|
commentary_status?: string | null
|
||||||
|
image_ai_url?: string | null
|
||||||
is_starred: boolean
|
is_starred: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ function fmtTime(s?: string | null) {
|
|||||||
return dayjs(s).fromNow()
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadSources()
|
await loadSources()
|
||||||
await load()
|
await load()
|
||||||
@@ -112,6 +132,15 @@ onMounted(async () => {
|
|||||||
<NTag v-if="a.translation_status !== 'ok'" size="small" type="warning">
|
<NTag v-if="a.translation_status !== 'ok'" size="small" type="warning">
|
||||||
{{ a.translation_status }}
|
{{ a.translation_status }}
|
||||||
</NTag>
|
</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>
|
<NText depth="3" style="font-size: 12px">{{ fmtTime(a.published_at || a.fetched_at) }}</NText>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
<div style="font-size: 16px; font-weight: 600; color: #333">{{ a.title }}</div>
|
<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">
|
<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 ? '…' : '' }}
|
{{ a.summary_zh.slice(0, 200) }}{{ a.summary_zh.length > 200 ? '…' : '' }}
|
||||||
</div>
|
</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>
|
</NSpace>
|
||||||
</NCard>
|
</NCard>
|
||||||
<NSpace v-if="!exhausted" justify="center" style="margin: 16px 0">
|
<NSpace v-if="!exhausted" justify="center" style="margin: 16px 0">
|
||||||
|
|||||||
Reference in New Issue
Block a user