feat(workbench): persist topbar notification state

This commit is contained in:
caoxiaozhu
2026-06-03 21:43:35 +08:00
parent b9826a1985
commit 75d5c178e1
15 changed files with 799 additions and 59 deletions

View File

@@ -0,0 +1,163 @@
import { ref } from 'vue'
import { fetchNotificationStates, patchNotificationStates } from '../services/notificationStates.js'
const NOTIFICATION_READ_STORAGE_KEY = 'x-financial.topbar.notifications.read'
const NOTIFICATION_HIDDEN_STORAGE_KEY = 'x-financial.topbar.notifications.hidden'
function normalizeNotificationId(value) {
return String(value || '').trim()
}
function readNotificationIdSet(storageKey) {
if (typeof window === 'undefined') {
return new Set()
}
try {
const parsed = JSON.parse(window.localStorage.getItem(storageKey) || '[]')
return new Set(Array.isArray(parsed) ? parsed.map(normalizeNotificationId).filter(Boolean) : [])
} catch {
return new Set()
}
}
function writeNotificationIdSet(storageKey, values) {
if (typeof window === 'undefined') {
return
}
window.localStorage.setItem(storageKey, JSON.stringify(Array.from(values).filter(Boolean)))
}
function mergeRemoteStateIntoSets(states, readIds, hiddenIds) {
for (const item of Array.isArray(states) ? states : []) {
const id = normalizeNotificationId(item?.notification_id || item?.notificationId)
if (!id) {
continue
}
if (item.read_at || item.readAt) {
readIds.add(id)
}
if (item.hidden_at || item.hiddenAt) {
hiddenIds.add(id)
}
}
}
function buildContext(item) {
const target = item?.target || {}
return {
kind: String(item?.kind || '').trim(),
category: String(item?.category || '').trim(),
target_type: String(target?.type || '').trim()
}
}
function buildPatch(item, flags) {
const notificationId = normalizeNotificationId(item?.id || item?.notification_id || item?.notificationId)
if (!notificationId) {
return null
}
return {
notification_id: notificationId,
read: Boolean(flags?.read),
hidden: Boolean(flags?.hidden),
context_json: buildContext(item)
}
}
export function useTopBarNotificationStates() {
const readNotificationIds = ref(readNotificationIdSet(NOTIFICATION_READ_STORAGE_KEY))
const hiddenNotificationIds = ref(readNotificationIdSet(NOTIFICATION_HIDDEN_STORAGE_KEY))
const notificationStateSyncing = ref(false)
const notificationStateError = ref('')
function persistLocalSets() {
writeNotificationIdSet(NOTIFICATION_READ_STORAGE_KEY, readNotificationIds.value)
writeNotificationIdSet(NOTIFICATION_HIDDEN_STORAGE_KEY, hiddenNotificationIds.value)
}
function applyLocalPatch(patch) {
if (!patch?.notification_id) {
return
}
if (patch.read) {
readNotificationIds.value.add(patch.notification_id)
}
if (patch.hidden) {
hiddenNotificationIds.value.add(patch.notification_id)
}
readNotificationIds.value = new Set(readNotificationIds.value)
hiddenNotificationIds.value = new Set(hiddenNotificationIds.value)
persistLocalSets()
}
function applyRemoteStates(states) {
const nextReadIds = new Set(readNotificationIds.value)
const nextHiddenIds = new Set(hiddenNotificationIds.value)
mergeRemoteStateIntoSets(states, nextReadIds, nextHiddenIds)
readNotificationIds.value = nextReadIds
hiddenNotificationIds.value = nextHiddenIds
persistLocalSets()
}
async function loadNotificationStates() {
notificationStateSyncing.value = true
notificationStateError.value = ''
try {
applyRemoteStates(await fetchNotificationStates())
} catch (error) {
notificationStateError.value = error?.message || '通知状态同步失败'
} finally {
notificationStateSyncing.value = false
}
}
async function syncNotificationPatches(patches) {
const normalizedPatches = (Array.isArray(patches) ? patches : []).filter(Boolean)
if (!normalizedPatches.length) {
return
}
normalizedPatches.forEach(applyLocalPatch)
notificationStateError.value = ''
try {
applyRemoteStates(await patchNotificationStates(normalizedPatches))
} catch (error) {
notificationStateError.value = error?.message || '通知状态同步失败'
}
}
function isNotificationHidden(id) {
return hiddenNotificationIds.value.has(normalizeNotificationId(id))
}
function isNotificationRead(id) {
return readNotificationIds.value.has(normalizeNotificationId(id))
}
function markNotificationStateRead(item) {
return syncNotificationPatches([buildPatch(item, { read: true })])
}
function hideNotificationStates(items) {
const patches = (Array.isArray(items) ? items : [])
.map((item) => buildPatch(item, { read: true, hidden: true }))
.filter(Boolean)
return syncNotificationPatches(patches)
}
return {
hiddenNotificationIds,
readNotificationIds,
notificationStateSyncing,
notificationStateError,
hideNotificationStates,
isNotificationHidden,
isNotificationRead,
loadNotificationStates,
markNotificationStateRead
}
}