fix(documents): move unread notice into bell

This commit is contained in:
caoxiaozhu
2026-06-03 17:05:34 +08:00
parent 8c2f301d85
commit c73178b65d
8 changed files with 342 additions and 180 deletions

View File

@@ -50,13 +50,7 @@
@click="emit('navigate', item.id)"
>
<span class="nav-icon" v-html="item.icon"></span>
<span class="nav-label">
<span class="nav-label-text">
{{ item.displayLabel }}
<span v-if="item.hasNewMessage" class="nav-unread-dot nav-unread-dot-label" aria-hidden="true"></span>
</span>
</span>
<span v-if="item.hasNewMessage" class="nav-unread-dot nav-unread-dot-collapsed" aria-hidden="true"></span>
<span class="nav-label">{{ item.displayLabel }}</span>
<span v-if="item.badge" class="nav-badge">{{ item.badge }}</span>
</button>
</ElTooltip>
@@ -118,9 +112,7 @@
<script setup>
import { ElTooltip } from 'element-plus/es/components/tooltip/index.mjs'
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
const props = defineProps({
navItems: { type: Array, required: true },
@@ -149,14 +141,6 @@ const props = defineProps({
const emit = defineEmits(['navigate', 'openChat', 'logout', 'toggle-collapse'])
const {
hasUnread: documentInboxHasUnread,
refreshDocumentInbox,
startDocumentInboxPolling,
stopDocumentInboxPolling
} = useDocumentCenterInbox()
let inboxInitialRefreshTimer = null
const sidebarMeta = {
overview: { label: '分析看板' },
workbench: { label: '个人工作台' },
@@ -173,35 +157,10 @@ const decoratedNavItems = computed(() =>
props.navItems.map((item) => ({
...item,
displayLabel: sidebarMeta[item.id]?.label ?? item.label,
hasNewMessage: item.id === 'documents' ? documentInboxHasUnread.value : false,
badge: sidebarMeta[item.id]?.badge
}))
)
function clearInboxInitialRefreshTimer() {
if (inboxInitialRefreshTimer && typeof window !== 'undefined') {
window.clearTimeout(inboxInitialRefreshTimer)
inboxInitialRefreshTimer = null
}
}
function scheduleInboxInitialRefresh() {
if (typeof window === 'undefined') {
return
}
clearInboxInitialRefreshTimer()
inboxInitialRefreshTimer = window.setTimeout(() => {
inboxInitialRefreshTimer = null
void refreshDocumentInbox()
}, props.activeView === 'documents' ? 1200 : 6000)
}
onMounted(() => {
scheduleInboxInitialRefresh()
startDocumentInboxPolling()
})
const displayUser = computed(() => ({
name: props.currentUser?.name || '系统管理员',
@@ -295,19 +254,7 @@ watch(
}
)
watch(
() => props.activeView,
(activeView, previousView) => {
if (activeView === 'documents' && previousView !== 'documents') {
clearInboxInitialRefreshTimer()
void refreshDocumentInbox({ force: true })
}
}
)
onBeforeUnmount(() => {
clearInboxInitialRefreshTimer()
stopDocumentInboxPolling()
closeCollapsedUserMenuNow()
})
</script>

View File

@@ -138,7 +138,10 @@
<div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心">
<header class="notification-head">
<strong>通知</strong>
<span class="notification-head-copy">
<strong>通知中心</strong>
<small>{{ unreadNotifications.length ? `${unreadNotifications.length} 条待处理` : '暂无待处理通知' }}</small>
</span>
<button type="button" aria-label="关闭通知" @click="notificationOpen = false">
<i class="mdi mdi-close"></i>
</button>
@@ -152,7 +155,8 @@
:class="{ active: notificationTab === 'unread' }"
@click="notificationTab = 'unread'"
>
未读 {{ unreadNotifications.length }}
<span>未读</span>
<em>{{ unreadNotifications.length }}</em>
</button>
<button
type="button"
@@ -161,7 +165,8 @@
:class="{ active: notificationTab === 'read' }"
@click="notificationTab = 'read'"
>
已读 {{ readNotifications.length }}
<span>已读</span>
<em>{{ readNotifications.length }}</em>
</button>
</div>
@@ -173,11 +178,19 @@
class="notification-row"
@click="openNotification(item)"
>
<span class="notification-dot" :class="item.tone"></span>
<span class="notification-type-icon" :class="item.tone">
<i :class="resolveNotificationIcon(item)"></i>
</span>
<span class="notification-copy">
<strong>{{ item.title }}</strong>
<span class="notification-title-line">
<strong>{{ item.title }}</strong>
<b v-if="item.badge">{{ item.badge }}</b>
</span>
<small>{{ item.description }}</small>
<em>{{ item.time }}</em>
<span class="notification-meta">
<em>{{ item.category || '系统通知' }}</em>
<time>{{ item.time }}</time>
</span>
</span>
<i class="mdi mdi-chevron-right"></i>
</button>
@@ -274,8 +287,9 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useDocumentCenterInbox } from '../../composables/useDocumentCenterInbox.js'
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
const props = defineProps({
@@ -342,7 +356,8 @@ const emit = defineEmits([
'batchApprove',
'openChat',
'newApplication',
'openDocument'
'openDocument',
'navigate'
])
const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview')
@@ -359,27 +374,111 @@ const eyebrowLabel = computed(() => (
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
))
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
const topbarNotificationCount = computed(() => {
const {
refreshDocumentInbox,
startDocumentInboxPolling,
stopDocumentInboxPolling,
unreadCount: documentInboxUnreadCount
} = 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 notificationOpen = ref(false)
const notificationTab = ref('unread')
const notificationItems = computed(() => (
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
}
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' }
}
})
const workbenchNotificationItems = computed(() => (
Array.isArray(props.workbenchSummary?.notifications)
? props.workbenchSummary.notifications
: []
))
const notificationItems = computed(() => {
const inboxNotification = documentInboxNotification.value
return inboxNotification
? [inboxNotification, ...workbenchNotificationItems.value]
: workbenchNotificationItems.value
})
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
))
function clearDocumentInboxInitialRefreshTimer() {
if (documentInboxInitialRefreshTimer && typeof window !== 'undefined') {
window.clearTimeout(documentInboxInitialRefreshTimer)
documentInboxInitialRefreshTimer = null
}
}
function scheduleDocumentInboxInitialRefresh() {
if (typeof window === 'undefined') {
return
}
clearDocumentInboxInitialRefreshTimer()
documentInboxInitialRefreshTimer = window.setTimeout(() => {
documentInboxInitialRefreshTimer = null
void refreshDocumentInbox()
}, props.activeView === 'workbench' ? 1200 : 6000)
}
function resolveNotificationIcon(item) {
if (item?.icon) {
return item.icon
}
if (item?.tone === 'danger') {
return 'mdi mdi-alert-circle-outline'
}
if (item?.tone === 'warning') {
return 'mdi mdi-alert-outline'
}
if (item?.tone === 'success') {
return 'mdi mdi-check-circle-outline'
}
return 'mdi mdi-bell-outline'
}
function openNotification(item) {
notificationOpen.value = false
const target = item?.target || {}
if (target.type === 'documents-center') {
emit('navigate', 'documents')
return
}
if (target.type === 'document' && (target.id || target.claimNo)) {
emit('openDocument', {
claimId: target.id,
@@ -566,16 +665,36 @@ const canApplyCustomRange = computed(() =>
Boolean(draftStart.value && draftEnd.value && draftStart.value <= draftEnd.value)
)
watch(
() => props.customRange,
(range) => {
draftStart.value = range.start
draftEnd.value = range.end
watch(
() => props.customRange,
(range) => {
draftStart.value = range.start
draftEnd.value = range.end
},
{ deep: true }
)
function setRange(range) {
)
watch(
() => props.activeView,
(activeView, previousView) => {
if (activeView === 'workbench' && previousView !== 'workbench') {
clearDocumentInboxInitialRefreshTimer()
void refreshDocumentInbox({ force: true })
}
}
)
onMounted(() => {
scheduleDocumentInboxInitialRefresh()
startDocumentInboxPolling()
})
onBeforeUnmount(() => {
clearDocumentInboxInitialRefreshTimer()
stopDocumentInboxPolling()
})
function setRange(range) {
emit('update:activeRange', range)
calendarOpen.value = false
}