fix: clarify conversation list timestamps

The recent-conversation list previously showed only a time, making older entries ambiguous. It now shows local time for conversations updated today, a local date for older entries, and includes the year when needed; the full timestamp remains available on hover.

Release note: 会话列表现在当天显示时间、较早会话显示日期,跨年记录同时显示年份,更容易快速判断最近更新时间。
This commit is contained in:
mesalogo
2026-08-17 17:11:00 +08:00
parent 9773f3ceb4
commit ce15e7021c
4 changed files with 114 additions and 3 deletions
+1
View File
@@ -490,6 +490,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 使用 `standard` 壳层和统一 `PageHeader` - 使用 `standard` 壳层和统一 `PageHeader`
- 搜索、范围和时间筛选位于筛选工具栏。 - 搜索、范围和时间筛选位于筛选工具栏。
- 行项目统一显示标题、范围、最近更新时间和必要状态。 - 行项目统一显示标题、范围、最近更新时间和必要状态。
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。 - 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
### 13.3 知识库 ### 13.3 知识库
+21 -2
View File
@@ -181,7 +181,11 @@ import type { ReleaseNotesSnapshot } from '../../shared/release-notes-contracts'
import { ReleaseNotesDialog } from './ReleaseNotesDialog' import { ReleaseNotesDialog } from './ReleaseNotesDialog'
import { scheduleIdleRoutePreload } from './idle-route-preload' import { scheduleIdleRoutePreload } from './idle-route-preload'
import { createPreloadableComponent } from './preloadable-component' import { createPreloadableComponent } from './preloadable-component'
import { formatTime, type TimeFormatLocale } from './time-format' import {
formatConversationListTime,
type TimeFormatLocale
} from './time-format'
import { formatMediumDateTime } from './locale-formatters'
import { formatCompactTokens } from './token-format' import { formatCompactTokens } from './token-format'
import { import {
pruneKeepAliveEntries, pruneKeepAliveEntries,
@@ -6464,7 +6468,22 @@ function App(): React.JSX.Element {
/> />
)} )}
</span> </span>
<small>{formatTime(conversation.updatedAt, locale)}</small> <small>
<time
dateTime={new Date(
conversation.updatedAt
).toISOString()}
title={formatMediumDateTime(
conversation.updatedAt,
locale
)}
>
{formatConversationListTime(
conversation.updatedAt,
locale
)}
</time>
</small>
</button> </button>
{activeConversationIds.has(conversation.id) && ( {activeConversationIds.has(conversation.id) && (
<span <span
+44 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import { formatTime } from './time-format' import { formatConversationListTime, formatTime } from './time-format'
describe('formatTime', () => { describe('formatTime', () => {
afterEach(() => { afterEach(() => {
@@ -27,4 +27,47 @@ describe('formatTime', () => {
expect(secondChinese).not.toBe('') expect(secondChinese).not.toBe('')
expect(formatter).toHaveBeenCalledTimes(2) expect(formatter).toHaveBeenCalledTimes(2)
}) })
it('shows the time for a conversation updated today', () => {
const timestamp = new Date(2026, 7, 17, 9, 5).getTime()
const referenceTimestamp = new Date(2026, 7, 17, 23, 30).getTime()
expect(
formatConversationListTime(timestamp, 'zh-CN', referenceTimestamp)
).toBe(
new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit'
}).format(timestamp)
)
})
it('shows the date after the local calendar day changes', () => {
const timestamp = new Date(2026, 7, 16, 23, 30).getTime()
const referenceTimestamp = new Date(2026, 7, 17, 0, 15).getTime()
expect(
formatConversationListTime(timestamp, 'zh-CN', referenceTimestamp)
).toBe(
new Intl.DateTimeFormat('zh-CN', {
month: 'short',
day: 'numeric'
}).format(timestamp)
)
})
it('includes the year for a conversation from another year', () => {
const timestamp = new Date(2025, 11, 31, 23, 30).getTime()
const referenceTimestamp = new Date(2026, 0, 1, 0, 15).getTime()
expect(
formatConversationListTime(timestamp, 'en-US', referenceTimestamp)
).toBe(
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(timestamp)
)
})
}) })
+48
View File
@@ -4,6 +4,14 @@ const timeFormatters: Partial<
Record<TimeFormatLocale, Intl.DateTimeFormat> Record<TimeFormatLocale, Intl.DateTimeFormat>
> = {} > = {}
const conversationDateFormatters: Partial<
Record<TimeFormatLocale, Intl.DateTimeFormat>
> = {}
const conversationDateWithYearFormatters: Partial<
Record<TimeFormatLocale, Intl.DateTimeFormat>
> = {}
export function formatTime( export function formatTime(
timestamp: number, timestamp: number,
locale: TimeFormatLocale locale: TimeFormatLocale
@@ -16,3 +24,43 @@ export function formatTime(
})) }))
return formatter.format(timestamp) return formatter.format(timestamp)
} }
export function formatConversationListTime(
timestamp: number,
locale: TimeFormatLocale,
referenceTimestamp = Date.now()
): string {
const date = new Date(timestamp)
const referenceDate = new Date(referenceTimestamp)
const isSameYear = date.getFullYear() === referenceDate.getFullYear()
const isToday =
isSameYear &&
date.getMonth() === referenceDate.getMonth() &&
date.getDate() === referenceDate.getDate()
if (isToday) {
return formatTime(timestamp, locale)
}
if (isSameYear) {
const formatter =
conversationDateFormatters[locale] ??
(conversationDateFormatters[locale] = new Intl.DateTimeFormat(locale, {
month: 'short',
day: 'numeric'
}))
return formatter.format(timestamp)
}
const formatter =
conversationDateWithYearFormatters[locale] ??
(conversationDateWithYearFormatters[locale] = new Intl.DateTimeFormat(
locale,
{
year: 'numeric',
month: 'short',
day: 'numeric'
}
))
return formatter.format(timestamp)
}