import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../chat/data/models/chat_list_item.dart'; import '../../../chat/presentation/bloc/chat_bloc.dart'; import '../../../chat/ui/widgets/ui_schema_renderer.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import 'home_sheet.dart'; /// 布局常量 const _headerHeight = 60.0; const _defaultPadding = 20.0; const _itemSpacing = 16.0; const _inputPadding = 16.0; const _iconSize = 24.0; const _messagePaddingH = 13.0; const _messagePaddingV = 9.0; const _cornerRadius = 12.0; const _inputMinHeight = 48.0; const _inputRadius = 24.0; const _scrollDurationMs = 300; /// 颜色常量 const _chatBgColor = Color(0xFFF8FAFC); const _userBubbleColor = Color(0xFFEAF1FB); class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); late final ChatBloc _chatBloc; bool get _hasMessage => _messageController.text.trim().isNotEmpty; @override void initState() { super.initState(); _messageController.addListener(_onMessageChanged); _chatBloc = ChatBloc(); _chatBloc.loadHistory(); } @override void dispose() { _messageController.removeListener(_onMessageChanged); _messageController.dispose(); _scrollController.dispose(); _chatBloc.close(); super.dispose(); } void _onMessageChanged() { setState(() {}); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _chatBloc, child: BlocConsumer( listener: (context, state) { if (state.error != null) { Toast.show(context, state.error!, type: ToastType.error); } }, builder: (context, state) { return Scaffold( backgroundColor: _chatBgColor, body: SafeArea( child: Column( children: [ _buildHeader(context), Expanded(child: _buildChatArea(context, state)), _buildInputContainer(context), ], ), ), ); }, ), ); } Widget _buildHeader(BuildContext context) { return SizedBox( height: _headerHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: _defaultPadding), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon( LucideIcons.settings, size: _iconSize, color: AppColors.slate900, ), onPressed: () => context.push('/settings'), ), Row( children: [ IconButton( icon: const Icon( LucideIcons.calendar, size: _iconSize, color: AppColors.slate900, ), onPressed: () => context.push('/calendar/dayweek?from=home'), ), const SizedBox(width: _itemSpacing), IconButton( icon: const Icon( LucideIcons.messageSquare, size: _iconSize, color: AppColors.slate900, ), onPressed: () => context.push('/messages/invites'), ), ], ), ], ), ), ); } Widget _buildChatArea(BuildContext context, ChatState state) { if (state.isLoading && state.items.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (state.items.isEmpty) { return const Center( child: Text( '开始对话吧', style: TextStyle(fontSize: 16, color: AppColors.slate400), ), ); } return RefreshIndicator( onRefresh: () => _onRefresh(context), child: ListView.builder( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(_defaultPadding), itemCount: state.items.length + (state.hasEarlierHistory ? 1 : 0), itemBuilder: (context, index) { if (index == 0 && state.hasEarlierHistory) { return _buildLoadMoreButton(context, state.isLoading); } final itemIndex = state.hasEarlierHistory ? index - 1 : index; final item = state.items[itemIndex]; final showDateDivider = itemIndex == 0 || !_isSameDay(state.items[itemIndex - 1].timestamp, item.timestamp); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (showDateDivider) _buildDateDivider(item.timestamp), Padding( padding: const EdgeInsets.only(bottom: _itemSpacing), child: _buildChatItem(item), ), ], ); }, ), ); } bool _isSameDay(DateTime a, DateTime b) { return a.year == b.year && a.month == b.month && a.day == b.day; } Widget _buildDateDivider(DateTime date) { final now = DateTime.now(); final weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; final weekday = weekdays[date.weekday - 1]; // For all dates (today/yesterday/this year), use the same format // Only add year prefix for dates from previous years final label = date.year == now.year ? '${date.month}月${date.day}日 $weekday' : '${date.year}年${date.month}月${date.day}日 $weekday'; return Container( padding: const EdgeInsets.symmetric(vertical: 12), alignment: Alignment.center, child: Text( label, style: const TextStyle(fontSize: 12, color: AppColors.slate400), ), ); } Widget _buildLoadMoreButton(BuildContext context, bool isLoading) { return GestureDetector( onTap: isLoading ? null : () => _onLoadMore(context), child: Container( padding: const EdgeInsets.symmetric(vertical: 8), alignment: Alignment.center, child: isLoading ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 1.5, color: AppColors.slate400, ), ) : const Text( '查看历史', style: TextStyle(fontSize: 12, color: AppColors.slate400), ), ), ); } Future _onRefresh(BuildContext context) async { await context.read().loadMoreHistory(); } void _onLoadMore(BuildContext context) { context.read().loadMoreHistory(); } Widget _buildChatItem(ChatListItem item) { switch (item.type) { case ChatItemType.message: return _buildMessageItem(item as TextMessageItem); case ChatItemType.toolCall: return _buildToolCallItem(item as ToolCallItem); case ChatItemType.toolResult: return _buildToolResultItem(item as ToolResultItem); } } Widget _buildMessageItem(TextMessageItem item) { final isUser = item.sender == MessageSender.user; return Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Container( padding: const EdgeInsets.symmetric( horizontal: _messagePaddingH, vertical: _messagePaddingV, ), decoration: BoxDecoration( color: isUser ? _userBubbleColor : AppColors.white, borderRadius: BorderRadius.only( topLeft: const Radius.circular(_cornerRadius), topRight: const Radius.circular(_cornerRadius), bottomLeft: Radius.circular(isUser ? _cornerRadius : 0), bottomRight: Radius.circular(isUser ? 0 : _cornerRadius), ), border: isUser ? null : Border.all(color: AppColors.slate300), ), child: Text( item.content, style: const TextStyle(fontSize: 14, color: AppColors.slate900), ), ), ), ], ); } Widget _buildToolCallItem(ToolCallItem item) { final (statusText, statusColor, statusIcon) = switch (item.status) { ToolCallStatus.pending => ( '准备中...', AppColors.slate500, LucideIcons.clock, ), ToolCallStatus.executing => ( '执行中...', AppColors.blue600, LucideIcons.loader, ), ToolCallStatus.error => ( item.errorMessage ?? '执行失败', AppColors.red600, LucideIcons.alertCircle, ), ToolCallStatus.completed => ( '已完成', AppColors.emerald600, LucideIcons.checkCircle, ), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.blue50, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.slate300), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(statusIcon, size: 16, color: statusColor), const SizedBox(width: 8), Text( item.toolName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate700, ), ), const SizedBox(width: 8), Text(statusText, style: TextStyle(fontSize: 12, color: statusColor)), ], ), ); } Widget _buildToolResultItem(ToolResultItem item) { return UiSchemaRenderer.render(item.uiCard); } Widget _buildInputContainer(BuildContext context) { return Container( padding: const EdgeInsets.all(_inputPadding), color: _chatBgColor, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( onTap: () => _showBottomSheet(context), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.white, shape: BoxShape.circle, border: Border.all(color: AppColors.slate300), ), child: const Icon( LucideIcons.plus, size: 20, color: AppColors.slate500, ), ), ), const SizedBox(width: 8), Expanded( child: Container( constraints: const BoxConstraints(minHeight: _inputMinHeight), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(_inputRadius), border: Border.all(color: AppColors.slate300), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: TextField( controller: _messageController, minLines: 1, maxLines: 3, decoration: const InputDecoration( hintText: '输入消息...', border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, filled: false, ), onSubmitted: (_) => _sendMessage(context), ), ), const SizedBox(width: 8), GestureDetector( onTap: _hasMessage ? () => _sendMessage(context) : null, child: Icon( _hasMessage ? LucideIcons.send : LucideIcons.mic, size: _iconSize, color: _hasMessage ? AppColors.blue600 : AppColors.slate500, ), ), ], ), ), ), ], ), ); } Future _sendMessage(BuildContext context) async { final content = _messageController.text.trim(); if (content.isEmpty) return; _messageController.clear(); context.read().sendMessage(content); WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: _scrollDurationMs), curve: Curves.easeOut, ); } }); } void _showBottomSheet(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => const HomeSheet(), ); } }