fix(notifications): refine bell notification center
This commit is contained in:
@@ -142,9 +142,24 @@
|
||||
<strong>通知中心</strong>
|
||||
<small>{{ unreadNotifications.length ? `${unreadNotifications.length} 条待处理` : '暂无待处理通知' }}</small>
|
||||
</span>
|
||||
<button type="button" aria-label="关闭通知" @click="notificationOpen = false">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
<span class="notification-head-actions">
|
||||
<button
|
||||
class="notification-clear-btn"
|
||||
type="button"
|
||||
:disabled="notificationItems.length === 0"
|
||||
@click="clearAllNotifications"
|
||||
>
|
||||
清空通知
|
||||
</button>
|
||||
<button
|
||||
class="notification-close-btn"
|
||||
type="button"
|
||||
aria-label="关闭通知"
|
||||
@click="notificationOpen = false"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="notification-tabs" role="tablist" aria-label="通知状态">
|
||||
@@ -374,63 +389,152 @@ const eyebrowLabel = computed(() => (
|
||||
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
|
||||
))
|
||||
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
||||
const NOTIFICATION_READ_STORAGE_KEY = 'x-financial.topbar.notifications.read'
|
||||
const NOTIFICATION_HIDDEN_STORAGE_KEY = 'x-financial.topbar.notifications.hidden'
|
||||
const MAX_NOTIFICATION_ITEMS = 30
|
||||
const {
|
||||
markDocumentInboxRowRead,
|
||||
markDocumentInboxRowsRead,
|
||||
notificationRows: documentInboxNotificationRows,
|
||||
refreshDocumentInbox,
|
||||
startDocumentInboxPolling,
|
||||
stopDocumentInboxPolling,
|
||||
unreadCount: documentInboxUnreadCount
|
||||
stopDocumentInboxPolling
|
||||
} = useDocumentCenterInbox()
|
||||
let documentInboxInitialRefreshTimer = null
|
||||
const workbenchNotificationCount = computed(() => {
|
||||
const summary = props.workbenchSummary ?? {}
|
||||
const count = Number(summary.unreadNotificationCount ?? 0)
|
||||
return Number.isFinite(count) && count > 0 ? count : 0
|
||||
})
|
||||
const topbarNotificationCount = computed(() => {
|
||||
const count = workbenchNotificationCount.value + Number(documentInboxUnreadCount.value || 0)
|
||||
return Number.isFinite(count) && count > 0 ? Math.min(count, 99) : 0
|
||||
})
|
||||
const readNotificationIds = ref(readNotificationIdSet(NOTIFICATION_READ_STORAGE_KEY))
|
||||
const hiddenNotificationIds = ref(readNotificationIdSet(NOTIFICATION_HIDDEN_STORAGE_KEY))
|
||||
const notificationOpen = ref(false)
|
||||
const notificationTab = ref('unread')
|
||||
const documentInboxBadgeText = computed(() => {
|
||||
const count = Number(documentInboxUnreadCount.value || 0)
|
||||
return count > 99 ? '99+' : String(count)
|
||||
})
|
||||
const documentInboxNotification = computed(() => {
|
||||
const count = Number(documentInboxUnreadCount.value || 0)
|
||||
if (!Number.isFinite(count) || count <= 0) {
|
||||
return null
|
||||
|
||||
function readNotificationIdSet(storageKey) {
|
||||
if (typeof window === 'undefined') {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'document-center-unread',
|
||||
title: '单据中心有新单据',
|
||||
description: `当前有 ${count} 条新单据待查看`,
|
||||
time: '刚刚更新',
|
||||
category: '单据中心',
|
||||
tone: 'danger',
|
||||
unread: true,
|
||||
icon: 'mdi mdi-file-document-alert-outline',
|
||||
badge: documentInboxBadgeText.value,
|
||||
target: { type: 'documents-center' }
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(storageKey) || '[]')
|
||||
return new Set(Array.isArray(parsed) ? parsed.map((item) => String(item || '').trim()).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 updateNotificationIdSet(targetRef, storageKey, updater) {
|
||||
const next = updater(new Set(targetRef.value))
|
||||
targetRef.value = next
|
||||
writeNotificationIdSet(storageKey, next)
|
||||
}
|
||||
|
||||
function normalizeNotificationId(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function isNotificationHidden(id) {
|
||||
return hiddenNotificationIds.value.has(normalizeNotificationId(id))
|
||||
}
|
||||
|
||||
function formatNotificationTime(value) {
|
||||
const date = new Date(value)
|
||||
if (!Number.isFinite(date.getTime())) {
|
||||
return '最近更新'
|
||||
}
|
||||
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
function resolveDocumentNotificationTone(row) {
|
||||
if (row?.source === 'approval') {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
return row?.isUnread ? 'info' : 'success'
|
||||
}
|
||||
|
||||
function resolveDocumentNotificationDescription(row) {
|
||||
return [
|
||||
row?.title,
|
||||
row?.initiatorName ? `发起人 ${row.initiatorName}` : '',
|
||||
row?.statusLabel ? `状态 ${row.statusLabel}` : ''
|
||||
].filter(Boolean).join(' · ') || '单据中心有新的单据状态'
|
||||
}
|
||||
|
||||
function resolveWorkbenchNotificationId(item, index) {
|
||||
return normalizeNotificationId(`workbench:${item?.id || [item?.title, item?.description, item?.time, index].join('|')}`)
|
||||
}
|
||||
|
||||
const documentNotificationItems = computed(() =>
|
||||
documentInboxNotificationRows.value
|
||||
.map((row) => {
|
||||
const id = normalizeNotificationId(`document:${row.documentKey || row.claimId || row.documentNo}`)
|
||||
if (!id || isNotificationHidden(id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
kind: 'document',
|
||||
title: `${row.documentTypeLabel || '单据'} ${row.documentNo || row.claimId || '待生成'}`,
|
||||
description: resolveDocumentNotificationDescription(row),
|
||||
time: formatNotificationTime(row.updatedAt || row.createdAt),
|
||||
category: row.sourceLabel || '单据中心',
|
||||
tone: resolveDocumentNotificationTone(row),
|
||||
unread: Boolean(row.isUnread),
|
||||
icon: row.source === 'approval' ? 'mdi mdi-clipboard-text-clock-outline' : 'mdi mdi-file-document-outline',
|
||||
badge: row.isUnread ? '新' : '',
|
||||
target: {
|
||||
type: 'document',
|
||||
id: row.claimId,
|
||||
claimNo: row.documentNo
|
||||
},
|
||||
documentRow: row
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const workbenchNotificationItems = computed(() => (
|
||||
Array.isArray(props.workbenchSummary?.notifications)
|
||||
? props.workbenchSummary.notifications
|
||||
? props.workbenchSummary.notifications.map((item, index) => {
|
||||
const id = resolveWorkbenchNotificationId(item, index)
|
||||
if (!id || isNotificationHidden(id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
id,
|
||||
kind: 'workbench',
|
||||
category: item.category || '个人工作台',
|
||||
unread: Boolean(item.unread) && !readNotificationIds.value.has(id),
|
||||
icon: item.icon || resolveNotificationIcon(item)
|
||||
}
|
||||
}).filter(Boolean)
|
||||
: []
|
||||
))
|
||||
const notificationItems = computed(() => {
|
||||
const inboxNotification = documentInboxNotification.value
|
||||
return inboxNotification
|
||||
? [inboxNotification, ...workbenchNotificationItems.value]
|
||||
: workbenchNotificationItems.value
|
||||
})
|
||||
const notificationItems = computed(() =>
|
||||
[...documentNotificationItems.value, ...workbenchNotificationItems.value].slice(0, MAX_NOTIFICATION_ITEMS)
|
||||
)
|
||||
const unreadNotifications = computed(() => notificationItems.value.filter((item) => item.unread))
|
||||
const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread))
|
||||
const activeNotifications = computed(() => (
|
||||
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value
|
||||
))
|
||||
const topbarNotificationCount = computed(() => {
|
||||
const count = unreadNotifications.value.length
|
||||
return count > 0 ? Math.min(count, 99) : 0
|
||||
})
|
||||
|
||||
function clearDocumentInboxInitialRefreshTimer() {
|
||||
if (documentInboxInitialRefreshTimer && typeof window !== 'undefined') {
|
||||
@@ -471,14 +575,47 @@ function resolveNotificationIcon(item) {
|
||||
return 'mdi mdi-bell-outline'
|
||||
}
|
||||
|
||||
function openNotification(item) {
|
||||
notificationOpen.value = false
|
||||
const target = item?.target || {}
|
||||
if (target.type === 'documents-center') {
|
||||
emit('navigate', 'documents')
|
||||
function markNotificationRead(item) {
|
||||
if (!item?.id || !item.unread) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.kind === 'document' && item.documentRow) {
|
||||
markDocumentInboxRowRead(item.documentRow)
|
||||
return
|
||||
}
|
||||
|
||||
updateNotificationIdSet(readNotificationIds, NOTIFICATION_READ_STORAGE_KEY, (next) => {
|
||||
next.add(item.id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function clearAllNotifications() {
|
||||
const currentItems = notificationItems.value
|
||||
if (!currentItems.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const documentRows = currentItems
|
||||
.filter((item) => item.kind === 'document' && item.documentRow)
|
||||
.map((item) => item.documentRow)
|
||||
|
||||
if (documentRows.length) {
|
||||
markDocumentInboxRowsRead(documentRows)
|
||||
}
|
||||
|
||||
updateNotificationIdSet(hiddenNotificationIds, NOTIFICATION_HIDDEN_STORAGE_KEY, (next) => {
|
||||
currentItems.forEach((item) => next.add(item.id))
|
||||
return next
|
||||
})
|
||||
notificationTab.value = 'unread'
|
||||
}
|
||||
|
||||
function openNotification(item) {
|
||||
markNotificationRead(item)
|
||||
notificationOpen.value = false
|
||||
const target = item?.target || {}
|
||||
if (target.type === 'document' && (target.id || target.claimNo)) {
|
||||
emit('openDocument', {
|
||||
claimId: target.id,
|
||||
|
||||
Reference in New Issue
Block a user