fix(achievement): 弹窗队列区分成就与题单奖章的 id 空间

两者的 id 来自不同的表、数值会重叠,却共用同一个队列:
- 去重只按 id,队列里有奖章 5 时成就 5 会被当成重复项丢掉
- 弹完奖章后调 markAchievementsRead(奖章id),后端按成就 id 解释,
  把同号且未弹过的成就静默标记为已弹,那个奖杯再也不会出现

载荷里本来就有 kind 字段,前端此前完全忽略。现在去重按 kind:id,
标记已读跳过奖章,弹窗标题也按类型区分。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 07:22:42 -06:00
parent a9278589c9
commit 7e498a1c1c
3 changed files with 22 additions and 7 deletions

View File

@@ -22,7 +22,7 @@ function playNext() {
visible.value = true
timer = setTimeout(async () => {
visible.value = false
await store.markRead(item.id)
await store.markRead(item)
gapTimer = setTimeout(playNext, 400)
}, 3000)
}
@@ -49,7 +49,9 @@ onUnmounted(() => {
>
<div class="icon">{{ current.icon }}</div>
<div class="body">
<div class="label">成就解锁</div>
<div class="label">
{{ current.kind === "badge" ? "获得奖章" : "成就解锁" }}
</div>
<div class="name">{{ current.name }}</div>
<div class="desc">{{ current.description }}</div>
</div>

View File

@@ -18,14 +18,20 @@ export const useAchievementStore = defineStore("achievement", () => {
const queue = ref<PendingAchievement[]>([])
const current = ref<PendingAchievement | null>(null)
// 成就和题单奖章的 id 来自两张不同的表,数值会重叠,
// 只按 id 去重会让奖章 5 把成就 5 挤掉
function keyOf(item: PendingAchievement) {
return `${item.kind ?? "achievement"}:${item.id}`
}
function enqueue(items: PendingAchievement[]) {
if (!items?.length) return
// 去重WebSocket 推来的和 pending 拉来的可能是同一批
const known = new Set([
...queue.value.map((i) => i.id),
...(current.value ? [current.value.id] : []),
...queue.value.map(keyOf),
...(current.value ? [keyOf(current.value)] : []),
])
queue.value.push(...items.filter((i) => !known.has(i.id)))
queue.value.push(...items.filter((i) => !known.has(keyOf(i))))
}
async function fetchPending() {
@@ -43,9 +49,12 @@ export const useAchievementStore = defineStore("achievement", () => {
return current.value
}
async function markRead(id: number) {
async function markRead(item: PendingAchievement) {
// 奖章不在 UserAchievement 表里,它的 id 传给标记接口会被当成成就 id
// 把一个恰好同号、还没弹过的成就静默标记为已弹——那个奖杯就再也不会出现
if (item.kind === "badge") return
try {
await markAchievementsRead([id])
await markAchievementsRead([item.id])
} catch {
// 标记失败下次会重复弹一次,可接受
}

View File

@@ -759,6 +759,10 @@ export interface PendingAchievement {
description: string
icon: string
rarity: AchievementRarity
// 弹窗队列里混着两种东西:全站成就和题单奖章。它们的 id 来自两张不同的表,
// 数值会重叠,所以去重和标记已读都必须带上 kind 一起判断。
// pending 接口只返回成就,不带这个字段,缺省按 achievement 处理。
kind?: "achievement" | "badge"
}
export interface AchievementSummary {