Files
customer-im-client-dev/apps/im_app/lib/features/chat/view/chat_page.dart
pp-bot e715a0673b feat(chat): 发收消息全量实现 (#25~#28)
- 移除 @riverpod/@freezed 注解依赖,全部改为手写 Provider(无需 build_runner)
  · LoginState 改为纯 Dart,LoginViewModel/ThemeViewModel/ChatViewModel 改为 Notifier
  · UserNotifier 改为 FamilyAsyncNotifier<User?,int>,mini_app_provider 改为手写 Provider
  · 15 个 StreamProvider/StreamProvider.family 从 @riverpod 迁移至手写

- 发送消息(#25)
  · SendMessageRequest/SendMessageResponse DTO
  · SendMessageUseCase:乐观写入 DB → HTTP POST → 更新 Chat 摘要

- 接收消息 WS(#26)
  · WsMessageService:监听 mode2 WS 帧 → HTTP 补拉 → DB 写入 → Chat 更新
  · FetchHistoryRequest/FetchHistoryResponse DTO(GET /app/api/chat/history)
  · FetchHistoryUseCase:拉取 → insertOrReplaceAll

- DI 装配(chat_service_providers.dart)
  · wsMessageServiceProvider、sendMessageUseCaseProvider、fetchHistoryUseCaseProvider

- 聊天列表页(#27)
  · ChatListViewModel(Notifier<void>)+ chat_page.dart 真实会话列表 UI
  · ListTile:头像首字母、最新消息摘要、未读角标、时间格式化

- 聊天详情页(#28)
  · ChatDetailViewModel(FamilyNotifier<ChatDetailState,int>)+ chat_detail_page.dart
  · 消息气泡(自己/他人分左右)、底部输入框、发送状态与错误提示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:16:44 +09:00

131 lines
4.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:im_app/domain/entities/chat.dart';
import 'package:im_app/features/chat/di/chat_provider.dart';
import 'package:im_app/features/chat/presentation/chat_list_view_model.dart';
/// 聊天列表页
///
/// 从本地 DB Stream 读取会话列表,实时反映 WS 推送的新消息摘要。
/// 点击任意会话进入 [ChatDetailPage]chatId + 名称作为路由参数)。
class ChatPage extends ConsumerWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.watch(chatListViewModelProvider.notifier);
final chatsAsync = ref.watch(allChatsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('消息'),
actions: [
IconButton(
icon: const Icon(Icons.storage_outlined),
tooltip: '数据库测试',
onPressed: () => vm.goToDatabaseTest(context),
),
],
),
body: chatsAsync.when(
data: (chats) => chats.isEmpty
? const Center(child: Text('暂无会话'))
: ListView.separated(
itemCount: chats.length,
separatorBuilder: (_, __) =>
const Divider(height: 1, indent: 72),
itemBuilder: (context, index) =>
_ChatTile(chat: chats[index], vm: vm),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
),
);
}
}
class _ChatTile extends StatelessWidget {
const _ChatTile({required this.chat, required this.vm});
final Chat chat;
final ChatListViewModel vm;
@override
Widget build(BuildContext context) {
final name = chat.name ?? 'Chat ${chat.chatId ?? chat.id}';
final lastMsg = chat.lastMsg ?? '';
final unread = chat.unreadNum ?? 0;
final sendTime = chat.lastTime;
final timeStr = sendTime != null ? _formatTime(sendTime) : '';
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : '?',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
title: Row(
children: [
Expanded(
child: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),
),
if (timeStr.isNotEmpty)
Text(
timeStr,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
],
),
subtitle: Row(
children: [
Expanded(
child: Text(
lastMsg,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
if (unread > 0)
Container(
margin: const EdgeInsets.only(left: 4),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(10),
),
child: Text(
unread > 99 ? '99+' : '$unread',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onError,
),
),
),
],
),
onTap: () => vm.openChat(context, chat),
);
}
String _formatTime(int unixSec) {
final dt = DateTime.fromMillisecondsSinceEpoch(unixSec * 1000);
final now = DateTime.now();
if (dt.year == now.year &&
dt.month == now.month &&
dt.day == now.day) {
return '${dt.hour.toString().padLeft(2, '0')}:'
'${dt.minute.toString().padLeft(2, '0')}';
}
return '${dt.month}/${dt.day}';
}
}