From cfb297ac39ebe15f38fbdad867082c74ef75d6c4 Mon Sep 17 00:00:00 2001 From: xiaji Date: Tue, 16 Jun 2026 07:48:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(feed):=20=E5=88=86=E7=B1=BB=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E5=B7=B2=E8=AF=BB=E6=8F=90=E7=A4=BA=E6=9D=A1=E5=8A=A0?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ frontend/src/views/Feed.vue | 41 ++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 17310e9..80fa957 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,9 @@ alembic/versions/__pycache__/ # 临时调试脚本(下划线开头,不进仓库) scripts/_*.py scripts/_*/ +# docs/ 下的临时调试产物(截图 + 临时 js 测试脚本) +docs/*.js +docs/*.png # 敏感 secrets/ diff --git a/frontend/src/views/Feed.vue b/frontend/src/views/Feed.vue index b8de657..4dc5854 100644 --- a/frontend/src/views/Feed.vue +++ b/frontend/src/views/Feed.vue @@ -192,13 +192,26 @@ async function toggleRead(a: ArticleListItem) { } else { 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((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) { - // 等 leave 动画跑完再从 items 数组里移除(TransitionGroup 才能触发动画) const idx = items.value.findIndex((x) => x.id === a.id) if (idx >= 0) { - // 触发 leave 动画:Vue 会保留 DOM 元素直到 transition 结束 - // 但 splice(items, idx, 1) 会立即从 v-for 移除 → 用 markPending 标记 → 350ms 后再真正移除 pendingRemoval.value.add(a.id) setTimeout(() => { const i = items.value.findIndex((x) => x.id === a.id) @@ -208,13 +221,6 @@ async function toggleRead(a: ArticleListItem) { }, 360) } } - - // === 新增:刚标为已读 → 查该文章前 2 个 category 的 24h 未读数 === - // unmark 路径不触发(用户反悔了,不该再骚扰);hide_read 模式仍可触发 - // (滑出动画只影响当前这一条,提示条展示的是"这个分类下还有别的未读") - if (!wasRead && a.category) { - await maybePromptCategoryRead(a) - } } catch (e: any) { // 失败回滚 a.is_read = wasRead @@ -713,6 +719,12 @@ onMounted(async () => { :key="`prompt-wrapper-${a.id}`" class="feed-category-prompt-wrapper" > + +
+ + ✓ 已将《{{ a.title_zh || a.title }}》标记为已读 + +
{ > @@ -1102,6 +1113,12 @@ onMounted(async () => { flex-direction: column; gap: 8px; } +/* 标题行(标已读的确认)— 跟下面的 NAlert 视觉分层 */ +.feed-category-prompt-header { + padding: 6px 4px 0; + font-size: 13px; + color: var(--color-text-faint); +} .feed-category-prompt { background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%); border-left: 3px solid var(--color-primary, #5b86e5);