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:
@@ -490,6 +490,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 使用 `standard` 壳层和统一 `PageHeader`。
|
||||
- 搜索、范围和时间筛选位于筛选工具栏。
|
||||
- 行项目统一显示标题、范围、最近更新时间和必要状态。
|
||||
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
|
||||
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
|
||||
|
||||
### 13.3 知识库
|
||||
|
||||
@@ -181,7 +181,11 @@ import type { ReleaseNotesSnapshot } from '../../shared/release-notes-contracts'
|
||||
import { ReleaseNotesDialog } from './ReleaseNotesDialog'
|
||||
import { scheduleIdleRoutePreload } from './idle-route-preload'
|
||||
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 {
|
||||
pruneKeepAliveEntries,
|
||||
@@ -6464,7 +6468,22 @@ function App(): React.JSX.Element {
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
{activeConversationIds.has(conversation.id) && (
|
||||
<span
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { formatTime } from './time-format'
|
||||
import { formatConversationListTime, formatTime } from './time-format'
|
||||
|
||||
describe('formatTime', () => {
|
||||
afterEach(() => {
|
||||
@@ -27,4 +27,47 @@ describe('formatTime', () => {
|
||||
expect(secondChinese).not.toBe('')
|
||||
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)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,14 @@ const timeFormatters: Partial<
|
||||
Record<TimeFormatLocale, Intl.DateTimeFormat>
|
||||
> = {}
|
||||
|
||||
const conversationDateFormatters: Partial<
|
||||
Record<TimeFormatLocale, Intl.DateTimeFormat>
|
||||
> = {}
|
||||
|
||||
const conversationDateWithYearFormatters: Partial<
|
||||
Record<TimeFormatLocale, Intl.DateTimeFormat>
|
||||
> = {}
|
||||
|
||||
export function formatTime(
|
||||
timestamp: number,
|
||||
locale: TimeFormatLocale
|
||||
@@ -16,3 +24,43 @@ export function formatTime(
|
||||
}))
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user