+
+
+ {{ item.title }}
+ {{ item.badge }}
+
+
+
+
- {{ item.description }}
-
- {{ item.category || '系统通知' }}
-
+ {{ item.description }}
+
-
@@ -516,12 +520,28 @@ function normalizeNotificationId(value) {
return String(value || '').trim()
}
-function formatNotificationTime(value) {
- const date = new Date(value)
- if (!Number.isFinite(date.getTime())) {
+function formatNotificationTimeLabel(value) {
+ const raw = String(value || '').trim()
+ if (!raw) {
return '最近更新'
}
+ const normalized = raw.replace('T', ' ')
+ const isoMatched = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/)
+ if (isoMatched) {
+ return `${isoMatched[2]}-${isoMatched[3]} ${isoMatched[4]}:${isoMatched[5]}`
+ }
+
+ const shortMatched = normalized.match(/^(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/)
+ if (shortMatched) {
+ return `${shortMatched[1]}-${shortMatched[2]} ${shortMatched[3]}:${shortMatched[4]}`
+ }
+
+ const date = new Date(raw)
+ if (!Number.isFinite(date.getTime())) {
+ return raw.length > 16 ? `${raw.slice(0, 16)}...` : raw
+ }
+
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')
@@ -560,13 +580,14 @@ const documentNotificationItems = computed(() =>
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, isUnread: unread }),
- unread,
+ kind: 'document',
+ title: `${row.documentTypeLabel || '单据'} ${row.documentNo || row.claimId || '待生成'}`,
+ description: resolveDocumentNotificationDescription(row),
+ time: row.updatedAt || row.createdAt,
+ timeLabel: formatNotificationTimeLabel(row.updatedAt || row.createdAt),
+ category: row.sourceLabel || '单据中心',
+ tone: resolveDocumentNotificationTone({ ...row, isUnread: unread }),
+ unread,
icon: row.source === 'approval' ? 'mdi mdi-clipboard-text-clock-outline' : 'mdi mdi-file-document-outline',
badge: unread ? '新' : '',
target: {
@@ -587,12 +608,15 @@ const workbenchNotificationItems = computed(() => (
if (!id || isNotificationHidden(id)) {
return null
}
+ const notificationTime = item.time || item.updatedAt || item.due
return {
...item,
id,
kind: 'workbench',
category: item.category || '个人工作台',
+ time: notificationTime,
+ timeLabel: formatNotificationTimeLabel(item.time || item.updatedAt || item.due),
unread: Boolean(item.unread) && !readNotificationIds.value.has(id),
icon: item.icon || resolveNotificationIcon(item)
}
diff --git a/web/tests/sidebar-document-unread-dot.test.mjs b/web/tests/sidebar-document-unread-dot.test.mjs
index 93e9de1..0eee735 100644
--- a/web/tests/sidebar-document-unread-dot.test.mjs
+++ b/web/tests/sidebar-document-unread-dot.test.mjs
@@ -96,6 +96,25 @@ test('topbar bell owns document center unread notifications', () => {
assert.doesNotMatch(topbarStyles, /\.notification-dot/)
})
+test('topbar notification popover uses inbox-style rows with formatted time labels', () => {
+ assert.match(topbar, /class="notification-row-main"/)
+ assert.match(topbar, /class="notification-row-head"/)
+ assert.match(topbar, /class="notification-row-title"/)
+ assert.match(topbar, /class="notification-context"/)
+ assert.match(topbar, /class="notification-row-foot"/)
+ assert.match(topbar, /class="notification-category-pill"/)
+ assert.match(topbar, /class="notification-time"/)
+ assert.match(topbar, /class="notification-row-action"/)
+ assert.match(topbar, /timeLabel:\s*formatNotificationTimeLabel\(row\.updatedAt \|\| row\.createdAt\)/)
+ assert.match(topbar, /timeLabel:\s*formatNotificationTimeLabel\(item\.time \|\| item\.updatedAt \|\| item\.due\)/)
+ assert.doesNotMatch(topbar, /