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

@@ -35,27 +35,27 @@
} }
.scope-tab-label { .scope-tab-label {
position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: flex-start;
gap: 4px;
line-height: 1.2; line-height: 1.2;
} }
.scope-tab-badge { .scope-tab-badge {
position: absolute; position: static;
top: -8px; flex: 0 0 auto;
right: -15px; min-width: 14px;
min-width: 15px; height: 14px;
height: 15px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 4px; margin-top: -5px;
padding: 0 3px;
border: 1px solid #fff; border: 1px solid #fff;
border-radius: 999px; border-radius: 999px;
background: #ef4444; background: #ef4444;
color: #fff; color: #fff;
font-size: 10px; font-size: 9.5px;
font-weight: 850; font-weight: 850;
line-height: 1; line-height: 1;
box-shadow: box-shadow:

View File

@@ -216,14 +216,12 @@
flex: 1; flex: 1;
min-width: 0; min-width: 0;
max-width: 128px; max-width: 128px;
position: relative;
display: inline-flex;
align-items: center;
color: currentColor; color: currentColor;
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
white-space: nowrap; white-space: nowrap;
overflow: visible; overflow: hidden;
text-overflow: ellipsis;
opacity: 1; opacity: 1;
transition: transition:
max-width var(--rail-motion-duration) var(--rail-motion-ease), max-width var(--rail-motion-duration) var(--rail-motion-ease),
@@ -232,16 +230,6 @@
will-change: max-width, opacity, transform; will-change: max-width, opacity, transform;
} }
.nav-label-text {
position: relative;
min-width: 0;
max-width: 100%;
display: inline-flex;
align-items: center;
overflow: visible;
line-height: 1.2;
}
.nav-badge { .nav-badge {
flex: 0 0 auto; flex: 0 0 auto;
min-width: 34px; min-width: 34px;
@@ -263,27 +251,6 @@
will-change: min-width, max-width, padding, opacity; will-change: min-width, max-width, padding, opacity;
} }
.nav-unread-dot {
width: 9px;
height: 9px;
border: 2px solid #fff;
border-radius: 999px;
background: #ef4444;
box-shadow:
0 0 0 3px rgba(239, 68, 68, 0.12),
0 6px 14px rgba(239, 68, 68, 0.32);
}
.nav-unread-dot-label {
position: absolute;
top: -8px;
right: -10px;
}
.nav-unread-dot-collapsed {
display: none;
}
.rail-user { .rail-user {
position: relative; position: relative;
min-width: 0; min-width: 0;
@@ -493,19 +460,6 @@
transition-delay: 0ms; transition-delay: 0ms;
} }
.rail-collapsed .nav-unread-dot-label {
display: none;
}
.rail-collapsed .nav-unread-dot-collapsed {
position: absolute;
display: block;
top: 10px;
right: 11px;
width: 9px;
height: 9px;
}
.rail-collapsed { .rail-collapsed {
overflow: visible; overflow: visible;
} }

View File

@@ -408,21 +408,26 @@
display: inline-flex; display: inline-flex;
} }
.notification-btn {
position: relative;
overflow: visible;
}
.notification-badge { .notification-badge {
position: absolute; position: absolute;
top: 2px; top: 1px;
right: 1px; right: 0;
min-width: 13px; min-width: 15px;
height: 13px; height: 15px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 3px; padding: 0 4px;
border: 2px solid #fff; border: 2px solid #fff;
border-radius: 999px; border-radius: 999px;
background: #ef4444; background: #ef4444;
color: #fff; color: #fff;
font-size: 8px; font-size: 9px;
font-weight: 850; font-weight: 850;
line-height: 1; line-height: 1;
box-shadow: 0 5px 10px rgba(239, 68, 68, .22); box-shadow: 0 5px 10px rgba(239, 68, 68, .22);
@@ -433,15 +438,15 @@
top: calc(100% + 10px); top: calc(100% + 10px);
right: -8px; right: -8px;
z-index: 60; z-index: 60;
width: min(360px, calc(100vw - 32px)); width: min(392px, calc(100vw - 32px));
display: grid; display: grid;
gap: 10px; gap: 12px;
padding: 12px; padding: 14px;
border: 1px solid #e5edf5; border: 1px solid #e5edf5;
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 0.98); background: #fff;
box-shadow: box-shadow:
0 18px 42px rgba(15, 23, 42, 0.14), 0 18px 46px rgba(15, 23, 42, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.92); inset 0 1px 0 rgba(255, 255, 255, 0.92);
} }
@@ -469,14 +474,27 @@
.notification-head { .notification-head {
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #edf2f7;
}
.notification-head-copy {
display: grid;
gap: 3px;
} }
.notification-head strong { .notification-head strong {
color: #0f172a; color: #0f172a;
font-size: 14px; font-size: 15px;
font-weight: 850; font-weight: 850;
} }
.notification-head small {
color: #64748b;
font-size: 12px;
font-weight: 650;
}
.notification-head button { .notification-head button {
width: 26px; width: 26px;
height: 26px; height: 26px;
@@ -501,89 +519,175 @@
.notification-tabs button { .notification-tabs button {
flex: 1 1 0; flex: 1 1 0;
height: 28px; height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 3px; border-radius: 3px;
color: #64748b; color: #64748b;
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
} }
.notification-tabs button em {
min-width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 6px;
border-radius: 999px;
background: #e2e8f0;
color: #475569;
font-style: normal;
font-size: 11px;
line-height: 1;
}
.notification-tabs button.active { .notification-tabs button.active {
background: #fff; background: #fff;
color: var(--theme-primary-active); color: var(--theme-primary-active);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
} }
.notification-tabs button.active em {
background: var(--theme-primary-light-8);
color: var(--theme-primary-active);
}
.notification-list { .notification-list {
position: relative; position: relative;
z-index: 1; z-index: 1;
display: grid; display: grid;
gap: 8px;
max-height: 320px; max-height: 320px;
overflow: auto; overflow: auto;
} }
.notification-row { .notification-row {
display: grid; display: grid;
grid-template-columns: 8px minmax(0, 1fr) 16px; grid-template-columns: 34px minmax(0, 1fr) 16px;
align-items: center; align-items: start;
gap: 10px; gap: 10px;
padding: 10px 4px; min-height: 72px;
border-top: 1px solid #edf2f7; padding: 10px;
border: 1px solid #edf2f7;
border-radius: 4px;
background: #fff;
text-align: left; text-align: left;
} transition:
border-color 160ms var(--ease),
.notification-row:first-child { background 160ms var(--ease),
border-top: 0; box-shadow 160ms var(--ease);
} }
.notification-row:hover { .notification-row:hover {
background: #f8fafc; border-color: var(--theme-primary-light-5);
background: #f8fbff;
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.08);
} }
.notification-dot { .notification-type-icon {
width: 7px; width: 34px;
height: 7px; height: 34px;
border-radius: 999px; display: grid;
background: var(--theme-primary); place-items: center;
border: 1px solid var(--theme-primary-light-6);
border-radius: 4px;
background: var(--theme-primary-light-9);
color: var(--theme-primary-active);
font-size: 18px;
} }
.notification-dot.danger { background: #ef4444; } .notification-type-icon.danger {
.notification-dot.warning { background: #f59e0b; } border-color: #fecaca;
.notification-dot.success { background: var(--success); } background: #fff5f5;
.notification-dot.info { background: #3b82f6; } color: #dc2626;
}
.notification-type-icon.warning {
border-color: #fde68a;
background: #fffbeb;
color: #d97706;
}
.notification-type-icon.success {
border-color: #bbf7d0;
background: #f0fdf4;
color: #16a34a;
}
.notification-type-icon.info {
border-color: #bfdbfe;
background: #eff6ff;
color: #2563eb;
}
.notification-copy { .notification-copy {
min-width: 0; min-width: 0;
display: grid; display: grid;
gap: 2px; gap: 5px;
} }
.notification-copy strong, .notification-copy strong,
.notification-copy small, .notification-copy small {
.notification-copy em {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.notification-title-line {
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
}
.notification-copy strong { .notification-copy strong {
min-width: 0;
color: #0f172a; color: #0f172a;
font-size: 13px; font-size: 13px;
font-weight: 850; font-weight: 850;
} }
.notification-title-line b {
flex: 0 0 auto;
min-width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 6px;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 10px;
line-height: 1;
}
.notification-copy small { .notification-copy small {
color: #475569; color: #475569;
font-size: 12px; font-size: 12px;
} }
.notification-copy em { .notification-meta {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.notification-meta em,
.notification-meta time {
color: #94a3b8; color: #94a3b8;
font-size: 11px; font-size: 11px;
font-style: normal; font-style: normal;
} }
.notification-row > .mdi { .notification-row > .mdi {
align-self: center;
color: #94a3b8; color: #94a3b8;
font-size: 16px; font-size: 16px;
} }

View File

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

View File

@@ -138,7 +138,10 @@
<div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心"> <div v-if="notificationOpen" class="notification-popover" role="dialog" aria-label="通知中心">
<header class="notification-head"> <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"> <button type="button" aria-label="关闭通知" @click="notificationOpen = false">
<i class="mdi mdi-close"></i> <i class="mdi mdi-close"></i>
</button> </button>
@@ -152,7 +155,8 @@
:class="{ active: notificationTab === 'unread' }" :class="{ active: notificationTab === 'unread' }"
@click="notificationTab = 'unread'" @click="notificationTab = 'unread'"
> >
未读 {{ unreadNotifications.length }} <span>未读</span>
<em>{{ unreadNotifications.length }}</em>
</button> </button>
<button <button
type="button" type="button"
@@ -161,7 +165,8 @@
:class="{ active: notificationTab === 'read' }" :class="{ active: notificationTab === 'read' }"
@click="notificationTab = 'read'" @click="notificationTab = 'read'"
> >
已读 {{ readNotifications.length }} <span>已读</span>
<em>{{ readNotifications.length }}</em>
</button> </button>
</div> </div>
@@ -173,11 +178,19 @@
class="notification-row" class="notification-row"
@click="openNotification(item)" @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"> <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> <small>{{ item.description }}</small>
<em>{{ item.time }}</em> <span class="notification-meta">
<em>{{ item.category || '系统通知' }}</em>
<time>{{ item.time }}</time>
</span>
</span> </span>
<i class="mdi mdi-chevron-right"></i> <i class="mdi mdi-chevron-right"></i>
</button> </button>
@@ -274,8 +287,9 @@
</template> </template>
<script setup> <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' import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
const props = defineProps({ const props = defineProps({
@@ -342,7 +356,8 @@ const emit = defineEmits([
'batchApprove', 'batchApprove',
'openChat', 'openChat',
'newApplication', 'newApplication',
'openDocument' 'openDocument',
'navigate'
]) ])
const isChat = computed(() => props.activeView === 'chat') const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview') const isOverview = computed(() => props.activeView === 'overview')
@@ -359,27 +374,111 @@ const eyebrowLabel = computed(() => (
|| (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations') || (isChat.value ? 'Smart Finance Q&A' : 'Smart Expense Operations')
)) ))
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司') 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 summary = props.workbenchSummary ?? {}
const count = Number(summary.unreadNotificationCount ?? 0) 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 return Number.isFinite(count) && count > 0 ? Math.min(count, 99) : 0
}) })
const notificationOpen = ref(false) const notificationOpen = ref(false)
const notificationTab = ref('unread') 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) Array.isArray(props.workbenchSummary?.notifications)
? 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 unreadNotifications = computed(() => notificationItems.value.filter((item) => item.unread))
const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread)) const readNotifications = computed(() => notificationItems.value.filter((item) => !item.unread))
const activeNotifications = computed(() => ( const activeNotifications = computed(() => (
notificationTab.value === 'unread' ? unreadNotifications.value : readNotifications.value 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) { function openNotification(item) {
notificationOpen.value = false notificationOpen.value = false
const target = item?.target || {} const target = item?.target || {}
if (target.type === 'documents-center') {
emit('navigate', 'documents')
return
}
if (target.type === 'document' && (target.id || target.claimNo)) { if (target.type === 'document' && (target.id || target.claimNo)) {
emit('openDocument', { emit('openDocument', {
claimId: target.id, claimId: target.id,
@@ -566,16 +665,36 @@ const canApplyCustomRange = computed(() =>
Boolean(draftStart.value && draftEnd.value && draftStart.value <= draftEnd.value) Boolean(draftStart.value && draftEnd.value && draftStart.value <= draftEnd.value)
) )
watch( watch(
() => props.customRange, () => props.customRange,
(range) => { (range) => {
draftStart.value = range.start draftStart.value = range.start
draftEnd.value = range.end draftEnd.value = range.end
}, },
{ deep: true } { 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) emit('update:activeRange', range)
calendarOpen.value = false calendarOpen.value = false
} }

View File

@@ -86,6 +86,7 @@
@batch-approve="toast('已批量通过 23 条审批任务')" @batch-approve="toast('已批量通过 23 条审批任务')"
@new-application="openExpenseApplicationCreate" @new-application="openExpenseApplicationCreate"
@open-document="openWorkbenchDocument" @open-document="openWorkbenchDocument"
@navigate="handleNavigate"
/> />
<FilterBar <FilterBar

View File

@@ -198,8 +198,12 @@ test('documents center category tabs render bubble counts for new documents', ()
documentsCenterView, documentsCenterView,
/const scopeTabItems = computed\(\(\) =>[\s\S]*badgeCount: scopeNewCountMap\.value\[tab\] \|\| 0/ /const scopeTabItems = computed\(\(\) =>[\s\S]*badgeCount: scopeNewCountMap\.value\[tab\] \|\| 0/
) )
assert.match(documentListSharedStyles, /\.scope-tab-label\s*\{[\s\S]*position:\s*relative;/) const scopeTabBadgeBlock = documentListSharedStyles.match(/\.scope-tab-badge\s*\{[^}]*\}/)?.[0] || ''
assert.match(documentListSharedStyles, /\.scope-tab-badge\s*\{[\s\S]*position:\s*absolute;[\s\S]*top:\s*-8px;[\s\S]*height:\s*15px;/) assert.match(documentListSharedStyles, /\.scope-tab-label\s*\{[\s\S]*align-items:\s*flex-start;[\s\S]*gap:\s*4px;/)
assert.match(scopeTabBadgeBlock, /position:\s*static;/)
assert.match(scopeTabBadgeBlock, /height:\s*14px;/)
assert.match(scopeTabBadgeBlock, /margin-top:\s*-5px;/)
assert.doesNotMatch(scopeTabBadgeBlock, /position:\s*absolute;/)
}) })
test('documents center can mark all unread documents as read from toolbar', () => { test('documents center can mark all unread documents as read from toolbar', () => {

View File

@@ -13,6 +13,21 @@ const sidebarStyles = readFileSync(
'utf8' 'utf8'
) )
const topbar = readFileSync(
fileURLToPath(new URL('../src/components/layout/TopBar.vue', import.meta.url)),
'utf8'
)
const topbarStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/top-bar.css', import.meta.url)),
'utf8'
)
const appShellRouteView = readFileSync(
fileURLToPath(new URL('../src/views/AppShellRouteView.vue', import.meta.url)),
'utf8'
)
const documentInbox = readFileSync( const documentInbox = readFileSync(
fileURLToPath(new URL('../src/composables/useDocumentCenterInbox.js', import.meta.url)), fileURLToPath(new URL('../src/composables/useDocumentCenterInbox.js', import.meta.url)),
'utf8' 'utf8'
@@ -23,19 +38,37 @@ const documentNewState = readFileSync(
'utf8' 'utf8'
) )
test('sidebar renders a red dot for unread document center rows', () => { test('sidebar no longer renders document center unread indicators', () => {
assert.match(sidebar, /useDocumentCenterInbox/) assert.doesNotMatch(sidebar, /useDocumentCenterInbox/)
assert.match(sidebar, /hasUnread: documentInboxHasUnread/) assert.doesNotMatch(sidebar, /hasUnread: documentInboxHasUnread/)
assert.match(sidebar, /class="nav-label-text"[\s\S]*class="nav-unread-dot nav-unread-dot-label"/) assert.doesNotMatch(sidebar, /hasNewMessage/)
assert.match(sidebar, /class="nav-unread-dot nav-unread-dot-collapsed"/) assert.doesNotMatch(sidebar, /nav-label-text/)
assert.match(sidebar, /hasNewMessage: item\.id === 'documents' \? documentInboxHasUnread\.value : false/) assert.doesNotMatch(sidebar, /nav-unread-dot/)
assert.match(sidebar, /void refreshDocumentInbox\(\)/) assert.match(sidebar, /<span class="nav-label">\{\{ item\.displayLabel \}\}<\/span>/)
assert.match(sidebar, /startDocumentInboxPolling\(\)/) assert.doesNotMatch(sidebarStyles, /\.nav-label-text\s*\{/)
assert.match(sidebar, /stopDocumentInboxPolling\(\)/) assert.doesNotMatch(sidebarStyles, /\.nav-unread-dot/)
assert.match(sidebarStyles, /\.nav-label-text\s*\{[\s\S]*position:\s*relative;/) assert.match(sidebarStyles, /\.nav-label\s*\{[\s\S]*overflow:\s*hidden;[\s\S]*text-overflow:\s*ellipsis;/)
assert.match(sidebarStyles, /\.nav-unread-dot\s*\{[\s\S]*background:\s*#ef4444;/) })
assert.match(sidebarStyles, /\.nav-unread-dot-label\s*\{[\s\S]*position:\s*absolute;[\s\S]*top:\s*-8px;/)
assert.match(sidebarStyles, /\.rail-collapsed \.nav-unread-dot-collapsed\s*\{[\s\S]*position:\s*absolute;/) test('topbar bell owns document center unread notifications', () => {
assert.match(topbar, /useDocumentCenterInbox/)
assert.match(topbar, /unreadCount: documentInboxUnreadCount/)
assert.match(topbar, /const workbenchNotificationCount = computed/)
assert.match(topbar, /const count = workbenchNotificationCount\.value \+ Number\(documentInboxUnreadCount\.value \|\| 0\)/)
assert.match(topbar, /const documentInboxNotification = computed/)
assert.match(topbar, /id: 'document-center-unread'/)
assert.match(topbar, /title: '单据中心有新单据'/)
assert.match(topbar, /target: \{ type: 'documents-center' \}/)
assert.match(topbar, /emit\('navigate', 'documents'\)/)
assert.match(appShellRouteView, /@navigate="handleNavigate"/)
assert.match(topbar, /startDocumentInboxPolling\(\)/)
assert.match(topbar, /stopDocumentInboxPolling\(\)/)
assert.match(topbar, /class="notification-type-icon" :class="item\.tone"/)
assert.match(topbarStyles, /\.notification-head-copy\s*\{[\s\S]*display:\s*grid;/)
assert.match(topbarStyles, /\.notification-tabs button em\s*\{[\s\S]*border-radius:\s*999px;/)
assert.match(topbarStyles, /\.notification-row\s*\{[\s\S]*grid-template-columns:\s*34px minmax\(0,\s*1fr\) 16px;/)
assert.match(topbarStyles, /\.notification-type-icon\.danger\s*\{[\s\S]*background:\s*#fff5f5;/)
assert.doesNotMatch(topbarStyles, /\.notification-dot/)
}) })
test('document inbox reuses document center viewed-key state', () => { test('document inbox reuses document center viewed-key state', () => {