diff --git a/UI-DESIGN.md b/UI-DESIGN.md index e045177..531e0c5 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -490,6 +490,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 使用 `standard` 壳层和统一 `PageHeader`。 - 搜索、范围和时间筛选位于筛选工具栏。 - 行项目统一显示标题、范围、最近更新时间和必要状态。 +- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。 - 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。 ### 13.3 知识库 diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 784be10..59299b4 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 { /> )} - {formatTime(conversation.updatedAt, locale)} + + + {activeConversationIds.has(conversation.id) && ( { 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) + ) + }) }) diff --git a/src/renderer/src/time-format.ts b/src/renderer/src/time-format.ts index ccef369..b5548e4 100644 --- a/src/renderer/src/time-format.ts +++ b/src/renderer/src/time-format.ts @@ -4,6 +4,14 @@ const timeFormatters: Partial< Record > = {} +const conversationDateFormatters: Partial< + Record +> = {} + +const conversationDateWithYearFormatters: Partial< + Record +> = {} + 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) +}