feat(feed): 分类批量已读提示条加标题行

This commit is contained in:
xiaji
2026-06-16 07:48:16 +08:00
parent de4a88ce8b
commit cfb297ac39
2 changed files with 32 additions and 12 deletions

3
.gitignore vendored
View File

@@ -55,6 +55,9 @@ alembic/versions/__pycache__/
# 临时调试脚本(下划线开头,不进仓库) # 临时调试脚本(下划线开头,不进仓库)
scripts/_*.py scripts/_*.py
scripts/_*/ scripts/_*/
# docs/ 下的临时调试产物(截图 + 临时 js 测试脚本)
docs/*.js
docs/*.png
# 敏感 # 敏感
secrets/ secrets/

View File

@@ -192,13 +192,26 @@ async function toggleRead(a: ArticleListItem) {
} else { } else {
await readsApi.mark(a.id) await readsApi.mark(a.id)
} }
// 标记为已读后,如果当前在 hide_read 模式,卡片要从列表里消失
// === 关键:先 await 提示条 fetch,再 splice ===
// 原因:hide_read 模式下,350ms 后 items.splice 会把 a 移除,wrapper 跟着走。
// 如果 category-count 比 350ms 慢,wrapper 还没渲染就被切走 → 用户看不到。
if (!wasRead && a.category) {
try {
const fetchPromise = maybePromptCategoryRead(a)
const timeout = new Promise<void>((resolve) => setTimeout(resolve, 1500))
await Promise.race([fetchPromise, timeout])
// eslint-disable-next-line no-console
console.debug('[category-prompt] mounted for article', a.id, 'cats=', categoryPromptsByArticle.value.get(a.id))
} catch {
// 静默失败
}
}
// hide_read 模式:卡片从列表里消失(用户预期行为)
if (!wasRead && hideRead.value) { if (!wasRead && hideRead.value) {
// 等 leave 动画跑完再从 items 数组里移除(TransitionGroup 才能触发动画)
const idx = items.value.findIndex((x) => x.id === a.id) const idx = items.value.findIndex((x) => x.id === a.id)
if (idx >= 0) { if (idx >= 0) {
// 触发 leave 动画:Vue 会保留 DOM 元素直到 transition 结束
// 但 splice(items, idx, 1) 会立即从 v-for 移除 → 用 markPending 标记 → 350ms 后再真正移除
pendingRemoval.value.add(a.id) pendingRemoval.value.add(a.id)
setTimeout(() => { setTimeout(() => {
const i = items.value.findIndex((x) => x.id === a.id) const i = items.value.findIndex((x) => x.id === a.id)
@@ -208,13 +221,6 @@ async function toggleRead(a: ArticleListItem) {
}, 360) }, 360)
} }
} }
// === 新增:刚标为已读 → 查该文章前 2 个 category 的 24h 未读数 ===
// unmark 路径不触发(用户反悔了,不该再骚扰);hide_read 模式仍可触发
// (滑出动画只影响当前这一条,提示条展示的是"这个分类下还有别的未读")
if (!wasRead && a.category) {
await maybePromptCategoryRead(a)
}
} catch (e: any) { } catch (e: any) {
// 失败回滚 // 失败回滚
a.is_read = wasRead a.is_read = wasRead
@@ -713,6 +719,12 @@ onMounted(async () => {
:key="`prompt-wrapper-${a.id}`" :key="`prompt-wrapper-${a.id}`"
class="feed-category-prompt-wrapper" class="feed-category-prompt-wrapper"
> >
<!-- 标题行:标记已读的确认 + 1~2 条分类提示的容器 -->
<div class="feed-category-prompt-header">
<NSpace align="center" :size="8" :wrap="true">
<NTag type="success" size="small" round :bordered="false">✓ 已将《{{ a.title_zh || a.title }}》标记为已读</NTag>
</NSpace>
</div>
<NAlert <NAlert
v-for="p in (categoryPromptsByArticle.get(a.id) || [])" v-for="p in (categoryPromptsByArticle.get(a.id) || [])"
:key="`${a.id}-${p.category}`" :key="`${a.id}-${p.category}`"
@@ -724,7 +736,6 @@ onMounted(async () => {
> >
<template #header> <template #header>
<NSpace align="center" :size="8" :wrap="true"> <NSpace align="center" :size="8" :wrap="true">
<NTag type="success" size="small" round :bordered="false">✓ 已读</NTag>
<NText>「{{ p.category }}」分类下还有 {{ p.unreadCount }} 条 24 小时未读</NText> <NText>「{{ p.category }}」分类下还有 {{ p.unreadCount }} 条 24 小时未读</NText>
</NSpace> </NSpace>
</template> </template>
@@ -1102,6 +1113,12 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
/* 标题行(标已读的确认) 跟下面的 NAlert 视觉分层 */
.feed-category-prompt-header {
padding: 6px 4px 0;
font-size: 13px;
color: var(--color-text-faint);
}
.feed-category-prompt { .feed-category-prompt {
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%); background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%);
border-left: 3px solid var(--color-primary, #5b86e5); border-left: 3px solid var(--color-primary, #5b86e5);