style(web): 通知中心列表行布局重构与时间标签格式化
- TopBar 通知行改为 row-main/row-head/row-foot 结构化布局,标题加粗、分类改为 pill、箭头随标题右侧 - formatNotificationTime 改名 formatNotificationTimeLabel,新增 ISO/短日期直通匹配与超长截断兜底 - 更新 sidebar-document-unread-dot 测试
This commit is contained in:
@@ -204,18 +204,22 @@
|
||||
<span class="notification-type-icon" :class="item.tone">
|
||||
<i :class="resolveNotificationIcon(item)"></i>
|
||||
</span>
|
||||
<span class="notification-copy">
|
||||
<span class="notification-title-line">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<b v-if="item.badge">{{ item.badge }}</b>
|
||||
<span class="notification-row-main">
|
||||
<span class="notification-row-head">
|
||||
<span class="notification-title-line">
|
||||
<strong class="notification-row-title">{{ item.title }}</strong>
|
||||
<b v-if="item.badge">{{ item.badge }}</b>
|
||||
</span>
|
||||
<span class="notification-row-action" aria-hidden="true">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</span>
|
||||
</span>
|
||||
<small>{{ item.description }}</small>
|
||||
<span class="notification-meta">
|
||||
<em>{{ item.category || '系统通知' }}</em>
|
||||
<time>{{ item.time }}</time>
|
||||
<small class="notification-context">{{ item.description }}</small>
|
||||
<span class="notification-row-foot">
|
||||
<span class="notification-category-pill">{{ item.category || '系统通知' }}</span>
|
||||
<time class="notification-time">{{ item.timeLabel || item.time }}</time>
|
||||
</span>
|
||||
</span>
|
||||
<i class="mdi mdi-chevron-right notification-row-arrow"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="notification-empty">
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, /<time>\{\{ item\.time \}\}<\/time>/)
|
||||
assert.match(topbarStyles, /\.notification-row\s*\{[\s\S]*grid-template-columns:\s*36px minmax\(0,\s*1fr\);/)
|
||||
assert.match(topbarStyles, /\.notification-context\s*\{[\s\S]*-webkit-line-clamp:\s*2;/)
|
||||
assert.match(topbarStyles, /\.notification-row-foot\s*\{[\s\S]*justify-content:\s*space-between;/)
|
||||
assert.match(topbarStyles, /\.notification-time\s*\{[\s\S]*font-variant-numeric:\s*tabular-nums;/)
|
||||
assert.match(topbarStyles, /\.notification-row-action\s*\{[\s\S]*width:\s*28px;[\s\S]*height:\s*28px;/)
|
||||
})
|
||||
|
||||
test('topbar notification state is persisted through backend API with local fallback', () => {
|
||||
assert.match(notificationStatesService, /apiRequest\('\/notification-states'\)/)
|
||||
assert.match(notificationStatesService, /apiRequest\('\/notification-states',\s*\{[\s\S]*method:\s*'POST'/)
|
||||
|
||||
Reference in New Issue
Block a user