import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../core/app_colors.dart'; import '../core/app_design_tokens.dart'; import '../core/elder_mode_scope.dart'; import '../core/app_module_visuals.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/chat_provider.dart'; import '../providers/conversation_history_provider.dart'; import 'app_toast.dart'; import 'authenticated_network_image.dart'; import '../providers/data_providers.dart'; import 'drawer_shell.dart'; class HealthDrawer extends ConsumerWidget { const HealthDrawer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return DrawerShell( widthFactor: 0.9, showBackground: false, child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ const SliverToBoxAdapter(child: _DrawerHeroConsumer()), SliverToBoxAdapter( child: Container( padding: const EdgeInsets.fromLTRB(14, 10, 14, 18), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _NavigationSection(ref: ref), const SizedBox(height: 22), _HistorySection(ref: ref), ], ), ), ), ], ), ); } } class _DrawerHeroConsumer extends ConsumerWidget { const _DrawerHeroConsumer(); @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(authProvider.select((state) => state.user)); final latestHealth = ref.watch(latestHealthProvider); return _DrawerHero(user: user, latestHealth: latestHealth, ref: ref); } } class _DrawerHero extends StatelessWidget { final dynamic user; final AsyncValue> latestHealth; final WidgetRef ref; const _DrawerHero({ required this.user, required this.latestHealth, required this.ref, }); @override Widget build(BuildContext context) { final topPadding = MediaQuery.paddingOf(context).top; return Stack( children: [ Positioned.fill( child: Image.asset( 'assets/branding/drawer_background_v2.png', fit: BoxFit.cover, alignment: Alignment.topLeft, ), ), Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white.withValues(alpha: 0.06), Colors.white.withValues(alpha: 0.18), Colors.white.withValues(alpha: 0.78), ], stops: const [0.0, 0.58, 1.0], ), ), ), ), Padding( padding: EdgeInsets.fromLTRB(16, topPadding + 16, 16, 12), child: Column( children: [ _AccountHeader(user: user, ref: ref), const SizedBox(height: 16), _HealthDashboard(latestHealth: latestHealth, ref: ref), ], ), ), ], ); } } class _AccountHeader extends StatelessWidget { final dynamic user; final WidgetRef ref; const _AccountHeader({required this.user, required this.ref}); @override Widget build(BuildContext context) { final name = (user?.name?.toString().isNotEmpty ?? false) ? user.name.toString() : '未设置昵称'; final phone = (user?.phone?.toString().isNotEmpty ?? false) ? user.phone.toString() : '未登录'; return Row( children: [ InkWell( onTap: () => pushRoute(ref, 'profile'), borderRadius: BorderRadius.circular(33), child: Container( width: 66, height: 66, decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: const Color(0xFF7C5CFF).withValues(alpha: 0.16), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Stack( fit: StackFit.expand, children: [ ClipOval( clipBehavior: Clip.antiAlias, child: ColoredBox( color: const Color(0xFFF1F5F9), child: user?.avatarUrl?.toString().isNotEmpty == true ? AuthenticatedNetworkImage( imageUrl: user.avatarUrl.toString(), fit: BoxFit.cover, width: 66, height: 66, errorBuilder: (_, _, _) => const Icon( Icons.person_rounded, color: Color(0xFF94A3B8), size: 34, ), ) : const Icon( Icons.person_rounded, color: Color(0xFF94A3B8), size: 34, ), ), ), ], ), ), ), const SizedBox(width: 14), Expanded( child: InkWell( onTap: () => pushRoute(ref, 'profile'), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 5), Text( phone, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ], ), ), ), ), _IconButton( icon: Icons.settings_rounded, onTap: () => pushRoute(ref, 'settings'), ), ], ); } } class _HealthDashboard extends StatelessWidget { final AsyncValue> latestHealth; final WidgetRef ref; const _HealthDashboard({required this.latestHealth, required this.ref}); @override Widget build(BuildContext context) { return _Panel( title: '健康仪表盘', titleColor: Colors.white, backgroundPainter: const _HealthDashboardBackgroundPainter(), child: latestHealth.when( data: (data) => _DashboardMetrics(data: data, ref: ref), loading: () => const SizedBox( height: 118, child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), error: (error, stackTrace) => _DashboardMetrics(data: const {}, ref: ref), ), ); } } class _DashboardMetrics extends StatelessWidget { final Map data; final WidgetRef ref; const _DashboardMetrics({required this.data, required this.ref}); @override Widget build(BuildContext context) { final items = [ _MetricInfo( label: '血压', value: _bpText(data['BloodPressure']), unit: 'mmHg', routeType: 'blood_pressure', ), _MetricInfo( label: '心率', value: _metricVal(data['HeartRate']), unit: 'bpm', routeType: 'heart_rate', ), _MetricInfo( label: '血糖', value: _metricVal(data['Glucose']), unit: 'mmol/L', routeType: 'glucose', ), _MetricInfo( label: '血氧', value: _metricVal(data['SpO2']), unit: '%', routeType: 'spo2', ), ]; return Row( children: [ for (var i = 0; i < items.length; i++) ...[ Expanded( child: _MetricTile( info: items[i], onTap: () => pushRoute(ref, 'trend', params: {'type': items[i].routeType}), ), ), if (i != items.length - 1) const SizedBox(width: 6), ], ], ); } static String _bpText(dynamic bp) { if (bp is Map) { final systolic = bp['systolic'] ?? bp['value'] ?? '--'; final diastolic = bp['diastolic']; if (diastolic == null) return systolic.toString(); return '$systolic/$diastolic'; } return '--'; } static String _metricVal(dynamic val, {String unit = ''}) { if (val == null) return '--'; dynamic raw = val; if (val is Map) raw = val['value'] ?? val['val'] ?? val['data']; if (raw == null) return '--'; final text = raw is num ? raw.toStringAsFixed(raw is double ? 1 : 0) : raw.toString(); if (text.isEmpty || text == '--') return '--'; return unit.isEmpty ? text : '$text $unit'; } } class _MetricTile extends StatelessWidget { final _MetricInfo info; final VoidCallback onTap; const _MetricTile({required this.info, required this.onTap}); @override Widget build(BuildContext context) { final elderMode = ElderModeScope.enabledOf(context); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( height: elderMode ? 108 : 100, padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white.withValues(alpha: 0.40)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 32, child: FittedBox( fit: BoxFit.scaleDown, child: Text( info.value, maxLines: 1, textAlign: TextAlign.center, style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ), if (info.unit != null) Text( info.unit!, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF), ), ), const SizedBox(height: 2), Text( info.label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ); } } class _NavigationSection extends StatelessWidget { final WidgetRef ref; const _NavigationSection({required this.ref}); @override Widget build(BuildContext context) { final elderMode = ElderModeScope.enabledOf(context); final items = [ _NavItem( icon: LucideIcons.folderHeart, title: '档案', route: 'healthArchive', colors: AppColors.healthGradient.colors, ), _NavItem( icon: LucideIcons.fileText, title: '报告', route: 'reports', colors: AppColors.reportGradient.colors, ), _NavItem( icon: LucideIcons.pill, title: '用药', route: 'medications', colors: AppColors.medicationGradient.colors, ), _NavItem( icon: AppModuleVisuals.diet.icon, title: '饮食', route: 'dietRecords', colors: AppColors.dietGradient.colors, ), _NavItem( icon: LucideIcons.calendarDays, title: '日历', route: 'calendar', colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)], ), _NavItem( icon: LucideIcons.calendarCheck, title: '随访', route: 'followups', colors: const [Color(0xFFFBCFE8), Color(0xFFF9A8D4)], ), _NavItem( icon: AppModuleVisuals.exercise.icon, title: '运动', route: 'exercisePlan', colors: AppColors.exerciseGradient.colors, ), _NavItem( icon: LucideIcons.bluetooth, title: '设备', route: 'devices', colors: AppColors.deviceGradient.colors, ), ]; return _LightSection( title: '常用功能', child: GridView.builder( padding: const EdgeInsets.only(top: 3), itemCount: items.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: elderMode ? 2 : 4, mainAxisExtent: elderMode ? 82 : 82, mainAxisSpacing: elderMode ? 10 : 7, crossAxisSpacing: elderMode ? 10 : 8, ), itemBuilder: (context, index) { final item = items[index]; return _NavTile( item: item, elderMode: elderMode, onTap: () => pushRoute(ref, item.route), ); }, ), ); } } class _NavTile extends StatelessWidget { final _NavItem item; final bool elderMode; final VoidCallback onTap; const _NavTile({ required this.item, required this.elderMode, required this.onTap, }); String get _title => switch (item.route) { 'healthArchive' => '健康档案', 'reports' => '报告管理', 'medications' => '用药管理', 'dietRecords' => '饮食记录', 'calendar' => '健康日历', 'followups' => '复查随访', 'exercisePlan' => '运动计划', 'devices' => '蓝牙设备', _ => item.title, }; List get _colors => switch (item.route) { 'healthArchive' => AppColors.healthGradient.colors, 'reports' => AppColors.reportGradient.colors, 'medications' => AppColors.medicationGradient.colors, 'dietRecords' => AppColors.dietGradient.colors, 'calendar' => const [AppColors.calendarLight, AppColors.calendar], 'followups' => const [AppColors.followupLight, AppColors.followup], 'exercisePlan' => AppColors.exerciseGradient.colors, 'devices' => AppColors.deviceGradient.colors, _ => item.colors, }; @override Widget build(BuildContext context) { final isDevice = item.route == 'devices'; if (elderMode) { return InkWell( onTap: onTap, borderRadius: AppRadius.lgBorder, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFF8F8FC), borderRadius: AppRadius.lgBorder, border: Border.all(color: const Color(0xFFE7E8F0)), ), child: Row( children: [ Container( width: 54, height: 54, decoration: BoxDecoration( color: isDevice ? AppColors.device : null, gradient: isDevice ? null : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: _colors, ), borderRadius: AppRadius.mdBorder, ), child: Icon(item.icon, color: Colors.white, size: 29), ), const SizedBox(width: 10), Expanded( child: Text( _title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.15, ), ), ), ], ), ), ); } return InkWell( onTap: onTap, borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: isDevice ? AppColors.device : null, gradient: isDevice ? null : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: _colors, ), borderRadius: AppRadius.mdBorder, border: null, boxShadow: [ BoxShadow( color: _colors.last.withValues(alpha: 0.10), blurRadius: 6, offset: const Offset(0, 3), ), ], ), child: Icon(item.icon, color: Colors.white, size: 24), ), const SizedBox(height: 6), Text( _title, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary, height: 1.1, ), ), ], ), ), ); } } class _LightSection extends StatelessWidget { final String title; final Widget child; final Widget? trailing; const _LightSection({ required this.title, required this.child, this.trailing, }); @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Row( children: [ Expanded( child: Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ), ?trailing, ], ), ), const SizedBox(height: 8), child, ], ); } class _Panel extends StatelessWidget { final String title; final Widget child; final CustomPainter? backgroundPainter; final Color titleColor; const _Panel({ required this.title, required this.child, this.backgroundPainter, this.titleColor = AppColors.textPrimary, }); @override Widget build(BuildContext context) { return Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: backgroundPainter == null ? null : const Color(0xFFBAE6FD), gradient: backgroundPainter != null ? null : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white.withValues(alpha: 0.92), const Color(0xFFF8F4FF).withValues(alpha: 0.84), ], ), borderRadius: AppRadius.xlBorder, boxShadow: AppShadows.soft, ), child: CustomPaint( painter: backgroundPainter, child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: titleColor, ), ), const SizedBox(height: 12), child, ], ), ), ), ); } } class _HealthDashboardBackgroundPainter extends CustomPainter { const _HealthDashboardBackgroundPainter(); @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; final paint = Paint() ..shader = const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF00C6FB), Color(0xFF005BEA)], ).createShader(rect); canvas.drawRect(rect, paint); } @override bool shouldRepaint(_HealthDashboardBackgroundPainter oldDelegate) => false; } class _IconButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; const _IconButton({required this.icon, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.84), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white, width: 1.2), ), child: Icon(icon, size: 22, color: const Color(0xFF475569)), ), ); } } class _MetricInfo { final String label; final String value; final String? unit; final String routeType; const _MetricInfo({ required this.label, required this.value, this.unit, required this.routeType, }); } class _NavItem { final IconData icon; final String title; final String route; final List colors; const _NavItem({ required this.icon, required this.title, required this.route, required this.colors, }); } class _HistorySection extends ConsumerStatefulWidget { final WidgetRef ref; const _HistorySection({required this.ref}); static const int _previewCount = 7; @override ConsumerState<_HistorySection> createState() => _HistorySectionState(); } class _HistorySectionState extends ConsumerState<_HistorySection> { String? _selectedId; bool _selecting = false; bool _deleting = false; bool _expanded = false; bool _clearing = false; String? _loadingConversationId; void _enterSelect(String id) { setState(() { _selecting = true; _selectedId = id; }); } void _toggleSelect(String id) { setState(() { if (_selectedId == id) { _selectedId = null; _selecting = false; } else { _selectedId = id; } }); } void _exitSelect() { setState(() { _selecting = false; _selectedId = null; }); } @override Widget build(BuildContext context) { final async = ref.watch(conversationHistoryProvider); final currentConversationId = ref.watch( chatProvider.select((state) => state.conversationId), ); return _LightSection( title: '对话记录', trailing: _buildHeaderAction(context, async), child: async.when( loading: () => const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Center( child: SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), error: (error, stackTrace) => const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Text( '加载失败', style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), ), data: (allItems) { final list = allItems .where((item) => item.id != currentConversationId) .toList(); if (list.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Text( '暂无历史,发起对话后会显示在这里', style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), ); } final visible = _expanded ? list : list.take(_HistorySection._previewCount).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (var i = 0; i < visible.length; i++) _DrawerHistoryTile( item: visible[i], selecting: _selecting, selected: _selectedId == visible[i].id, loading: _loadingConversationId == visible[i].id, onTap: () => _openConversation(context, visible[i].id), onLongPress: _loadingConversationId == null && !_deleting ? () => _enterSelect(visible[i].id) : null, ), if (list.length > _HistorySection._previewCount) ...[ const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: InkWell( onTap: _selecting ? null : () => setState(() => _expanded = !_expanded), borderRadius: BorderRadius.circular(999), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 6, ), child: Text( _expanded ? '收起' : '显示全部', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ), ), ), ], ], ); }, ), ); } Widget? _buildHeaderAction( BuildContext context, AsyncValue> async, ) { if (_selecting && _selectedId != null) { return _HistorySelectionBar( deleting: _deleting, onCancel: _exitSelect, onDelete: () => _deleteSelected(context), ); } final hasHistory = async.asData?.value.isNotEmpty == true; if (!_expanded || !hasHistory) return null; return TextButton( onPressed: _clearing ? null : () => _confirmClearAll(context), style: TextButton.styleFrom( foregroundColor: AppColors.error, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( _clearing ? '清空中' : '清空全部', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), ); } Future _openConversation(BuildContext context, String id) async { if (_loadingConversationId != null || _deleting || _clearing) return; if (_selecting) { _toggleSelect(id); return; } setState(() => _loadingConversationId = id); final error = await ref.read(chatProvider.notifier).loadConversation(id); if (!mounted) return; setState(() => _loadingConversationId = null); if (error != null) { if (context.mounted) { AppToast.show(context, error, type: AppToastType.error); } return; } if (context.mounted) Navigator.of(context).maybePop(); } Future _confirmClearAll(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('清空全部对话'), content: const Text('清空后无法恢复,确定继续吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), style: TextButton.styleFrom(foregroundColor: AppColors.error), child: const Text('清空'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _clearing = true); try { final count = await ref .read(conversationHistoryProvider.notifier) .clearAll(); if (!context.mounted) return; setState(() => _expanded = false); AppToast.show(context, '已清空 $count 条对话', type: AppToastType.success); } catch (_) { if (context.mounted) { AppToast.show(context, '清空失败,请稍后重试', type: AppToastType.error); } } finally { if (mounted) setState(() => _clearing = false); } } Future _deleteSelected(BuildContext context) async { final id = _selectedId; if (id == null || _deleting) return; setState(() => _deleting = true); try { await ref.read(conversationHistoryProvider.notifier).deleteOne(id); _exitSelect(); } catch (_) { if (context.mounted) { AppToast.show(context, '删除失败,请稍后重试', type: AppToastType.error); } } finally { if (mounted) setState(() => _deleting = false); } } } class _HistorySelectionBar extends StatelessWidget { final bool deleting; final VoidCallback onCancel; final VoidCallback onDelete; const _HistorySelectionBar({ required this.deleting, required this.onCancel, required this.onDelete, }); @override Widget build(BuildContext context) { return Align( alignment: Alignment.centerRight, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), border: Border.all(color: const Color(0xFFE5E7EB)), boxShadow: AppColors.cardShadowLight, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _HistoryActionButton( icon: Icons.delete_outline_rounded, label: deleting ? '删除中' : '删除', color: AppColors.error, onTap: deleting ? null : onDelete, ), const SizedBox(width: 8), Container(width: 1, height: 22, color: const Color(0xFFE5E7EB)), const SizedBox(width: 8), _HistoryActionButton( icon: Icons.close_rounded, label: '取消', color: AppColors.textSecondary, onTap: onCancel, ), ], ), ), ); } } class _HistoryActionButton extends StatelessWidget { final IconData icon; final String label; final Color color; final VoidCallback? onTap; const _HistoryActionButton({ required this.icon, required this.label, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 18, color: onTap == null ? AppColors.textHint : color, ), const SizedBox(width: 5), Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: onTap == null ? AppColors.textHint : color, ), ), ], ), ), ); } } class _DrawerHistoryTile extends StatelessWidget { final ConversationListItem item; final bool selecting; final bool selected; final bool loading; final VoidCallback onTap; final VoidCallback? onLongPress; const _DrawerHistoryTile({ required this.item, required this.selecting, required this.selected, required this.loading, required this.onTap, required this.onLongPress, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, onLongPress: onLongPress, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 2), decoration: selected ? BoxDecoration( color: const Color(0xFFF1F3F7), borderRadius: BorderRadius.circular(8), ) : null, child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 11), child: Row( children: [ Expanded( child: Text( _displaySummary(item), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: selected ? AppColors.textSecondary : AppColors.textPrimary, height: 1.2, ), ), ), const SizedBox(width: 10), if (loading) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) else Text( _shortDate(item.updatedAt), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textHint, ), ), ], ), ), ], ), ), ); } static String _displayTitle(ConversationListItem item) { final s = item.summary?.trim(); if (s != null && s.isNotEmpty) return s; final t = item.title?.trim(); if (t != null && t.isNotEmpty) return t; return '未命名对话'; } static String _displaySummary(ConversationListItem item) { final s = item.summary?.trim(); if (s != null && s.isNotEmpty) return s.replaceAll(RegExp(r'\s+'), ' '); return _displayTitle(item); } static String _shortDate(DateTime time) { final now = DateTime.now(); final local = time.toLocal(); if (local.year == now.year && local.month == now.month && local.day == now.day) { return '今天'; } final yesterday = now.subtract(const Duration(days: 1)); if (local.year == yesterday.year && local.month == yesterday.month && local.day == yesterday.day) { return '昨天'; } return '${local.month}/${local.day}'; } }