feat(workbench): persist topbar notification state
This commit is contained in:
163
web/src/composables/useTopBarNotificationStates.js
Normal file
163
web/src/composables/useTopBarNotificationStates.js
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user