From 19bd804a21cd49de59b953076047dc1a69a6503a Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 23 Dec 2025 19:45:51 -0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E5=89=8D=E7=AB=AF=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=98=BE=E7=A4=BA=E7=BB=9F=E4=B8=80=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=E6=9C=AC=E5=9C=B0=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mobile-app: 新增 DateTimeUtils 工具类处理 UTC -> 本地时间转换 - mobile-app: 修改 ledger_detail_page 和 profile_page 使用本地时间 - admin-web: 添加 dayjs 自动转换注释说明 - mobile-upgrade: 优化 toLocaleString 格式化选项 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/admin-web/src/utils/formatters.ts | 3 + .../mobile-app/lib/core/utils/date_utils.dart | 96 +++++++++++++++++++ .../presentation/pages/profile_page.dart | 11 ++- .../pages/ledger_detail_page.dart | 14 +-- .../presentation/components/version-card.tsx | 11 ++- 5 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 frontend/mobile-app/lib/core/utils/date_utils.dart diff --git a/frontend/admin-web/src/utils/formatters.ts b/frontend/admin-web/src/utils/formatters.ts index 576a0123..77b8cb74 100644 --- a/frontend/admin-web/src/utils/formatters.ts +++ b/frontend/admin-web/src/utils/formatters.ts @@ -41,6 +41,7 @@ export function formatPercentage(value: number, decimals = 1): string { /** * 格式化日期 + * dayjs 会自动将 UTC 时间转换为本地时间显示 */ export function formatDate(date: string | Date, format = 'YYYY-MM-DD'): string { return dayjs(date).format(format); @@ -48,6 +49,7 @@ export function formatDate(date: string | Date, format = 'YYYY-MM-DD'): string { /** * 格式化日期时间 + * dayjs 会自动将 UTC 时间转换为本地时间显示 */ export function formatDateTime(date: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string { return dayjs(date).format(format); @@ -55,6 +57,7 @@ export function formatDateTime(date: string | Date, format = 'YYYY-MM-DD HH:mm:s /** * 格式化相对时间 (如: 5分钟前, 2小时前) + * dayjs 会自动将 UTC 时间转换为本地时间计算 */ export function formatRelativeTime(date: string | Date): string { return dayjs(date).fromNow(); diff --git a/frontend/mobile-app/lib/core/utils/date_utils.dart b/frontend/mobile-app/lib/core/utils/date_utils.dart new file mode 100644 index 00000000..1d9a8799 --- /dev/null +++ b/frontend/mobile-app/lib/core/utils/date_utils.dart @@ -0,0 +1,96 @@ +import 'package:intl/intl.dart'; + +/// 日期时间工具类 +/// 处理 UTC 时间到本地时间的转换和格式化 +class DateTimeUtils { + DateTimeUtils._(); + + /// 格式化日期时间(完整格式) + /// 自动将 UTC 时间转换为本地时间 + static String formatDateTime(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + return DateFormat('yyyy-MM-dd HH:mm').format(localTime); + } + + /// 格式化日期时间(带秒) + static String formatDateTimeFull(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + return DateFormat('yyyy-MM-dd HH:mm:ss').format(localTime); + } + + /// 格式化日期(仅日期) + static String formatDate(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + return DateFormat('yyyy-MM-dd').format(localTime); + } + + /// 格式化短日期(月/日) + static String formatShortDate(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + return DateFormat('MM/dd').format(localTime); + } + + /// 从字符串解析日期并格式化短日期 + static String formatShortDateFromString(String? dateStr) { + if (dateStr == null || dateStr.isEmpty) return '-'; + try { + final parsed = DateTime.parse(dateStr); + return formatShortDate(parsed); + } catch (e) { + return dateStr; + } + } + + /// 格式化时间(仅时间) + static String formatTime(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + return DateFormat('HH:mm').format(localTime); + } + + /// 格式化相对时间(如:刚刚、5分钟前、1小时前、昨天) + static String formatRelativeTime(DateTime? dateTime) { + if (dateTime == null) return '-'; + final localTime = dateTime.toLocal(); + final now = DateTime.now(); + final diff = now.difference(localTime); + + if (diff.inSeconds < 60) { + return '刚刚'; + } else if (diff.inMinutes < 60) { + return '${diff.inMinutes}分钟前'; + } else if (diff.inHours < 24) { + return '${diff.inHours}小时前'; + } else if (diff.inDays < 2) { + return '昨天 ${formatTime(dateTime)}'; + } else if (diff.inDays < 7) { + return '${diff.inDays}天前'; + } else { + return formatDate(dateTime); + } + } + + /// 判断是否是今天 + static bool isToday(DateTime? dateTime) { + if (dateTime == null) return false; + final localTime = dateTime.toLocal(); + final now = DateTime.now(); + return localTime.year == now.year && + localTime.month == now.month && + localTime.day == now.day; + } + + /// 判断是否是昨天 + static bool isYesterday(DateTime? dateTime) { + if (dateTime == null) return false; + final localTime = dateTime.toLocal(); + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + return localTime.year == yesterday.year && + localTime.month == yesterday.month && + localTime.day == yesterday.day; + } +} diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index c07a4fa3..0e94cdc2 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -15,6 +15,7 @@ import '../../../../core/storage/storage_keys.dart'; import '../../../../core/services/referral_service.dart'; import '../../../../core/services/reward_service.dart'; import '../../../../core/services/notification_service.dart'; +import '../../../../core/utils/date_utils.dart'; import '../../../../routes/route_paths.dart'; import '../../../../routes/app_router.dart'; import '../../../auth/presentation/providers/auth_provider.dart'; @@ -2500,9 +2501,10 @@ class _ProfilePageState extends ConsumerState { /// 构建单条可结算奖励项 Widget _buildSettleableRewardItem(SettleableRewardItem item) { - // 格式化时间(优先使用 claimedAt,否则使用 createdAt) + // 格式化时间(优先使用 claimedAt,否则使用 createdAt)- UTC -> 本地时间 final displayDate = item.claimedAt ?? item.createdAt; - final settledDate = '${displayDate.month}/${displayDate.day} ${displayDate.hour.toString().padLeft(2, '0')}:${displayDate.minute.toString().padLeft(2, '0')}'; + final localDate = displayDate.toLocal(); + final settledDate = '${localDate.month}/${localDate.day} ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}'; // 构建金额显示文本 final List amountParts = []; @@ -2585,9 +2587,10 @@ class _ProfilePageState extends ConsumerState { /// 构建堆叠卡片样式的可结算奖励项 Widget _buildStackedSettleableRewardCard(SettleableRewardItem item, bool isSelected) { - // 格式化时间(优先使用 claimedAt,否则使用 createdAt) + // 格式化时间(优先使用 claimedAt,否则使用 createdAt)- UTC -> 本地时间 final displayDate = item.claimedAt ?? item.createdAt; - final settledDate = '${displayDate.month}/${displayDate.day} ${displayDate.hour.toString().padLeft(2, '0')}:${displayDate.minute.toString().padLeft(2, '0')}'; + final localDate = displayDate.toLocal(); + final settledDate = '${localDate.month}/${localDate.day} ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}'; // 构建金额显示文本 final List amountParts = []; diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart index 608a0b26..cfd7d9e8 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/services/wallet_service.dart'; +import '../../../../core/utils/date_utils.dart'; /// 账本明细页面 - 显示用户的流水账、统计图表和筛选功能 class LedgerDetailPage extends ConsumerStatefulWidget { @@ -186,19 +187,14 @@ class _LedgerDetailPageState extends ConsumerState return formatter.format(amount); } - /// 格式化日期 + /// 格式化日期 (UTC -> 本地时间) String _formatDate(DateTime date) { - return DateFormat('yyyy-MM-dd HH:mm').format(date); + return DateTimeUtils.formatDateTime(date); } - /// 格式化短日期 + /// 格式化短日期 (UTC -> 本地时间) String _formatShortDate(String date) { - try { - final parsed = DateTime.parse(date); - return DateFormat('MM/dd').format(parsed); - } catch (e) { - return date; - } + return DateTimeUtils.formatShortDateFromString(date); } @override diff --git a/frontend/mobile-upgrade/src/presentation/components/version-card.tsx b/frontend/mobile-upgrade/src/presentation/components/version-card.tsx index f85b97ab..fc5cf415 100644 --- a/frontend/mobile-upgrade/src/presentation/components/version-card.tsx +++ b/frontend/mobile-upgrade/src/presentation/components/version-card.tsx @@ -23,9 +23,18 @@ export function VersionCard({ version, onEdit, onDelete, onToggle }: VersionCard return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } + // 格式化日期时间 - UTC 自动转换为本地时间 const formatDate = (dateStr: string | null) => { if (!dateStr) return '-' - return new Date(dateStr).toLocaleString('zh-CN') + return new Date(dateStr).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) } return (