diff --git a/src/components/common/index.ts b/src/components/common/index.ts index d94722ec8..3b1943945 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -38,7 +38,7 @@ export type { // Keep local components that are not yet migrated export { AmountDisplay, AmountWithFiat, formatAmount } from './amount-display' export { AnimatedNumber, AnimatedAmount } from './animated-number' -export { TimeDisplay, formatDate, formatDateTime, formatTime, toDate } from './time-display' +export { TimeDisplay, formatDate, formatDateTime, formatTime, getLocale, toDate } from './time-display' export { FormField } from './form-field' export { ErrorBoundary } from './error-boundary' export { CopyableText } from './copyable-text' diff --git a/src/components/common/time-display.tsx b/src/components/common/time-display.tsx index 0eeed6cf2..be823711b 100644 --- a/src/components/common/time-display.tsx +++ b/src/components/common/time-display.tsx @@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; -type TimeFormat = 'relative' | 'date' | 'datetime' | 'time'; +type TimeFormat = 'relative' | 'date' | 'datetime' | 'time' | 'monthDayTime'; interface TimeDisplayProps { value: Date | string | number; @@ -17,7 +17,7 @@ function toDate(value: Date | string | number): Date { } // 获取当前语言的 locale -function getLocale(lang: string): string { +export function getLocale(lang: string): string { const localeMap: Record = { 'zh-CN': 'zh-CN', en: 'en-US', @@ -40,7 +40,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl switch (format) { case 'relative': - formatted = formatRelativeTime(date, t); + formatted = formatRelativeTime(date, t, locale); break; case 'date': formatted = date.toLocaleDateString(locale, { @@ -65,6 +65,15 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl minute: '2-digit', }); break; + case 'monthDayTime': + formatted = date.toLocaleString(locale, { + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + break; } const titleText = date.toLocaleString(locale, { @@ -84,7 +93,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl } // 相对时间格式化(支持 i18n) -function formatRelativeTime(date: Date, t: TFunction): string { +function formatRelativeTime(date: Date, t: TFunction, locale: string): string { const now = new Date(); const diff = now.getTime() - date.getTime(); const absDiff = Math.abs(diff); @@ -108,7 +117,7 @@ function formatRelativeTime(date: Date, t: TFunction): string { } // 超过一周显示日期 - return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); + return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' }); } // 导出工具函数(用于非组件场景) diff --git a/src/components/transaction/transaction-item.test.tsx b/src/components/transaction/transaction-item.test.tsx index 0f97d009e..5a1fe6c12 100644 --- a/src/components/transaction/transaction-item.test.tsx +++ b/src/components/transaction/transaction-item.test.tsx @@ -10,6 +10,17 @@ function renderWithProvider(ui: React.ReactElement) { return render({ui}) } +const LOCALE = 'zh-CN' +const FIXED_TIME = new Date('2024-01-15T14:30:45') +const formatMonthDayTime = (date: Date) => + date.toLocaleString(LOCALE, { + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + const mockTransaction: TransactionInfo = { id: '1', type: 'send', @@ -17,7 +28,7 @@ const mockTransaction: TransactionInfo = { amount: Amount.fromFormatted('100', 6, 'USDT'), symbol: 'USDT', address: '0x1234567890abcdef1234567890abcdef12345678', - timestamp: new Date(), + timestamp: FIXED_TIME, } describe('TransactionItem', () => { @@ -98,8 +109,8 @@ describe('TransactionItem', () => { expect(screen.getByText(t('transaction:type.exchange'))).toBeInTheDocument() }) - it('formats recent timestamp', () => { + it('formats timestamp with month/day and seconds', () => { renderWithProvider() - expect(screen.getByText(t('time.justNow'))).toBeInTheDocument() + expect(screen.getByText(formatMonthDayTime(FIXED_TIME))).toBeInTheDocument() }) }) diff --git a/src/components/transaction/transaction-item.tsx b/src/components/transaction/transaction-item.tsx index cb9b17aaa..c09e38b01 100644 --- a/src/components/transaction/transaction-item.tsx +++ b/src/components/transaction/transaction-item.tsx @@ -132,7 +132,11 @@ export function TransactionItem({ size="sm" className={cn('@xs:text-base', typeMeta.color)} /> - + )} diff --git a/src/components/transaction/transaction-list.test.tsx b/src/components/transaction/transaction-list.test.tsx index d7d14cc76..c07fd6600 100644 --- a/src/components/transaction/transaction-list.test.tsx +++ b/src/components/transaction/transaction-list.test.tsx @@ -12,8 +12,11 @@ function renderWithProvider(ui: React.ReactElement) { } // Use fixed date to avoid timezone/midnight flakiness -// Set to noon on a specific date so "today" and "yesterday" are stable +// Set to noon on a specific date so local date grouping is stable const FIXED_NOW = new Date('2025-06-15T12:00:00Z').getTime() +const LOCALE = 'zh-CN' +const formatGroupDate = (date: Date) => + date.toLocaleDateString(LOCALE, { year: 'numeric', month: 'long', day: 'numeric' }) const mockTransactions: TransactionInfo[] = [ { @@ -55,8 +58,8 @@ describe('TransactionList', () => { it('groups transactions by date', () => { renderWithProvider() - expect(screen.getByText('今天')).toBeInTheDocument() - expect(screen.getByText('昨天')).toBeInTheDocument() + expect(screen.getByText(formatGroupDate(mockTransactions[0]!.timestamp as Date))).toBeInTheDocument() + expect(screen.getByText(formatGroupDate(mockTransactions[1]!.timestamp as Date))).toBeInTheDocument() }) it('shows loading skeleton', () => { diff --git a/src/components/transaction/transaction-list.tsx b/src/components/transaction/transaction-list.tsx index 71b5a5572..bf9502efd 100644 --- a/src/components/transaction/transaction-list.tsx +++ b/src/components/transaction/transaction-list.tsx @@ -1,6 +1,8 @@ import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; import { TransactionItem, type TransactionInfo } from './transaction-item'; import { EmptyState, SkeletonList } from '@biochain/key-ui'; +import { getLocale } from '../common'; interface TransactionListProps { transactions: TransactionInfo[]; @@ -15,25 +17,17 @@ interface TransactionListProps { testId?: string | undefined; } -function groupByDate(transactions: TransactionInfo[]): Map { +function groupByDate(transactions: TransactionInfo[], locale: string): Map { const groups = new Map(); - const now = new Date(); - const today = now.toDateString(); - const yesterday = new Date(now.getTime() - 86400000).toDateString(); transactions.forEach((tx) => { // timestamp 可能是 number (毫秒), string, 或 Date 对象 const date = tx.timestamp instanceof Date ? tx.timestamp : new Date(tx.timestamp); - const dateStr = date.toDateString(); - - let key: string; - if (dateStr === today) { - key = '今天'; // i18n-ignore: date grouping - } else if (dateStr === yesterday) { - key = '昨天'; // i18n-ignore: date grouping - } else { - key = date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' }); - } + const key = date.toLocaleDateString(locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + }); if (!groups.has(key)) { groups.set(key, []); @@ -55,6 +49,9 @@ export function TransactionList({ showChainIcon = false, testId, }: TransactionListProps) { + const { i18n } = useTranslation(); + const locale = getLocale(i18n.language); + if (loading) { return ; } @@ -80,7 +77,7 @@ export function TransactionList({ ); } - const grouped = groupByDate(transactions); + const grouped = groupByDate(transactions, locale); return (