实现了文章的已读/未读状态管理,当用户阅读文章后,对应的未读气泡(badge)将自动消失,提供更清晰的阅读体验。
- 用户点击文章时自动标记为已读
- 无需手动操作,提升用户体验
- 侧边栏"全部"显示总未读数量
- 每个 RSS 源显示对应的未读数量
- 未读数量为 0 时不显示气泡
- 阅读文章后立即更新未读数量
- 统计数据实时同步
添加字段:在 articles 表添加 is_read 字段
ALTER TABLE articles ADD COLUMN is_read INTEGER DEFAULT 00= 未读1= 已读
添加索引:优化查询性能
CREATE INDEX idx_articles_is_read ON articles(is_read)标记已读:
def mark_article_as_read(self, article_id: int) -> bool:
"""标记文章为已读"""
cursor.execute("""
UPDATE articles
SET is_read = 1
WHERE id = ?
""", (article_id,))获取未读数量:
def get_unread_count(self, source: Optional[str] = None, feed_source_id: Optional[int] = None) -> int:
"""获取未读文章数量(支持按来源或feed_source_id筛选)"""修改统计方法:
def get_statistics(self) -> Dict[str, Any]:
"""返回统计信息,包括:
- total_articles: 总文章数
- unread_articles: 未读文章数
- sources_unread: 每个来源的未读数量
"""标记文章已读:
POST /api/articles/{article_id}/read
获取未读数量:
GET /api/articles/unread/count?source=xxx&feed_source_id=xxx
统计信息:
GET /api/statistics
返回数据新增字段:
unread_articles: 总未读数sources_unread: 每个来源的未读数
Article 接口:
export interface Article {
id: number
title: string
// ... 其他字段
is_read?: number // 0=未读, 1=已读
}Statistics 接口:
export interface Statistics {
total_articles: number
unread_articles: number // 新增
sources: Record<string, number>
sources_unread: Record<string, number> // 新增
}// src/api/articles.ts
export const markArticleAsRead = (id: number): Promise<{ success: boolean; message: string }> =>
api.post(`/api/articles/${id}/read`)
export const getUnreadCount = (params?: { source?: string; feed_source_id?: number }): Promise<{ count: number }> =>
api.get('/api/articles/unread/count', { params })// src/stores/article.ts
const markAsRead = async (articleId: number) => {
try {
await markArticleAsRead(articleId)
// 更新本地状态
const article = articles.value.find(a => a.id === articleId)
if (article) {
article.is_read = 1
}
} catch (error) {
console.error('Failed to mark article as read:', error)
}
}ArticleItem.vue:点击文章时标记已读
const openArticle = async () => {
// 标记为已读
await articleStore.markAsRead(props.article.id)
// 跳转到详情页
router.push(`/article/${props.article.id}`)
}Sidebar.vue:显示未读数量
<!-- 全部 - 显示总未读数 -->
<span class="count" v-if="statistics && statistics.unread_articles > 0">
{{ statistics.unread_articles }}
</span>
<!-- RSS 源 - 显示来源未读数 -->
<span class="count" v-if="getFeedUnreadCount(feed.title) > 0">
{{ getFeedUnreadCount(feed.title) }}
</span>用户点击文章
↓
ArticleItem.openArticle()
↓
articleStore.markAsRead(id)
↓
API: POST /api/articles/{id}/read
↓
数据库: UPDATE articles SET is_read=1
↓
更新本地 article.is_read = 1
↓
跳转到文章详情页
页面加载
↓
statisticsStore.fetchStatistics()
↓
API: GET /api/statistics
↓
数据库: 查询未读数量
↓
返回统计数据(含未读数)
↓
侧边栏显示未读气泡
.count {
margin-left: auto;
font-size: 11px;
color: var(--text-tertiary);
background: rgba(255, 107, 53, 0.1); // 橙色淡背景
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
}- 未读数 > 0: 显示橙色气泡
- 未读数 = 0: 不显示气泡(视觉更清爽)
- 收藏: 始终显示数量(不受已读影响)
- 侧边栏显示"全部 23"(23篇未读)
- 用户点击某篇文章
- 文章自动标记为已读
- 刷新统计后显示"全部 22"
- 侧边栏显示"TechCrunch 5"(5篇未读)
- 用户点击进入 TechCrunch
- 阅读一篇文章
- 返回后显示"TechCrunch 4"
- 用户阅读完所有未读文章
- 侧边栏不再显示任何未读气泡
- 界面清爽,阅读体验佳
- ✅ 在
is_read字段上创建索引 - ✅ 统计查询使用 SUM(CASE) 而非多次查询
- ✅ 避免 N+1 查询问题
- ✅ 标记已读后立即更新本地状态
- ✅ 避免频繁刷新统计数据
- ✅ 使用计算属性缓存未读数量
- 批量标记已读 - 支持"全部标记为已读"
- 未读筛选 - 添加"仅显示未读"选项
- 标记未读 - 支持重新标记为未读
- 键盘快捷键 -
m键标记已读/未读 - 滚动自动标记 - 滚动到文章时自动标记
- 时间衰减 - 超过 N 天自动标记为已读
- 多设备同步 - 跨设备同步已读状态
- 阅读进度 - 记录文章阅读百分比
- 阅读历史 - 完整的阅读记录
- 点击文章是否正确标记为已读
- 未读数量是否正确更新
- 刷新页面后状态是否保持
- 侧边栏气泡显示是否正确
- 未读数为 0 时不显示气泡
- 已读文章再次点击不重复标记
- 数据库连接失败的错误处理
- API 调用失败的降级处理
- 大量文章(10000+)的查询性能
- 统计查询的响应时间
- 并发标记已读的处理
backend/modules/database.py- 数据库方法backend/api_server.py- API 接口
frontend/src/types/article.ts- 文章类型定义frontend/src/types/api.ts- 统计类型定义frontend/src/api/articles.ts- API 调用frontend/src/stores/article.ts- 文章状态管理frontend/src/components/Article/ArticleItem.vue- 文章项组件frontend/src/components/Layout/Sidebar.vue- 侧边栏组件
- 版本: v1.0
- 实现日期: 2025-11-02
- 状态: ✅ 已完成
注意: 此功能需要重启后端服务以应用数据库变更。旧数据的 is_read 字段默认为 0(未读)。