refactor(chat): 重构聊天模块并集成历史消息加载功能

- 删除冗余的 chat_history_repository 和 home_mock_data
- 简化 ag_ui_event fromJson 使用工厂映射表
- 提取 ChatBloc 事件处理方法,添加 loadHistory/loadMoreHistory
- HomeScreen 集成 ChatBloc 实现历史消息加载和下拉刷新
- 更新 AGENTS.md 文档约束
This commit is contained in:
qzl
2026-03-02 15:05:10 +08:00
parent 6b32990986
commit e161ca22c4
16 changed files with 915 additions and 752 deletions
@@ -16,8 +16,6 @@ const _defaultPadding = 20.0;
const _itemSpacing = 16.0;
const _inputPadding = 16.0;
const _iconSize = 24.0;
const _avatarSize = 32.0;
const _botIconSize = 18.0;
const _messagePaddingH = 13.0;
const _messagePaddingV = 9.0;
const _cornerRadius = 12.0;
@@ -39,6 +37,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
late final ChatBloc _chatBloc;
bool get _hasMessage => _messageController.text.trim().isNotEmpty;
@@ -46,6 +45,8 @@ class _HomeScreenState extends State<HomeScreen> {
void initState() {
super.initState();
_messageController.addListener(_onMessageChanged);
_chatBloc = ChatBloc();
_chatBloc.loadHistory();
}
@override
@@ -53,6 +54,7 @@ class _HomeScreenState extends State<HomeScreen> {
_messageController.removeListener(_onMessageChanged);
_messageController.dispose();
_scrollController.dispose();
_chatBloc.close();
super.dispose();
}
@@ -62,8 +64,8 @@ class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChatBloc(),
return BlocProvider.value(
value: _chatBloc,
child: BlocConsumer<ChatBloc, ChatState>(
listener: (context, state) {
if (state.error != null) {
@@ -132,6 +134,10 @@ class _HomeScreenState extends State<HomeScreen> {
}
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(
@@ -141,30 +147,96 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: _scrollDurationMs),
curve: Curves.easeOut,
);
}
});
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);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(_defaultPadding),
itemCount: state.items.length,
itemBuilder: (context, index) {
final item = state.items[index];
return Padding(
padding: const EdgeInsets.only(bottom: _itemSpacing),
child: _buildChatItem(item),
);
},
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<void> _onRefresh(BuildContext context) async {
await context.read<ChatBloc>().loadMoreHistory();
}
void _onLoadMore(BuildContext context) {
context.read<ChatBloc>().loadMoreHistory();
}
Widget _buildChatItem(ChatListItem item) {
switch (item.type) {
case ChatItemType.message:
@@ -182,24 +254,8 @@ class _HomeScreenState extends State<HomeScreen> {
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
Container(
width: _avatarSize,
height: _avatarSize,
decoration: BoxDecoration(
color: AppColors.blue100,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.bot,
size: _botIconSize,
color: AppColors.blue600,
),
),
const SizedBox(width: 8),
],
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
@@ -222,8 +278,6 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
),
if (isUser) const SizedBox(width: 40),
if (!isUser) const SizedBox(width: 40),
],
);
}
@@ -365,6 +419,16 @@ class _HomeScreenState extends State<HomeScreen> {
if (content.isEmpty) return;
_messageController.clear();
context.read<ChatBloc>().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) {