新增 mobile/:畅联手机端 Flutter 工程(B-58)
- 登录:工号+密码走 account-service /api/login,自动登录、被踢下线回登录页 - 消息:会话列表(未读角标/免打扰/时间)、单聊群聊、文字/语音/图片/文件消息、失败重发、历史分页 - 通讯录:新的同事/我的群聊入口、按部门分组(好友 ex 字段)、搜索 - 通话:一对一语音通话(信令走 OpenIM 自定义消息 + LiveKit,token 走 /api/rtc_token) - 界面按确认效果图 m1/m2/m3 实现,主色 #3B87F5,四态视图齐全,无英文残留
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
import '../services/im_service.dart';
|
||||
import '../theme.dart';
|
||||
import '../utils/format.dart';
|
||||
import '../widgets/avatar.dart';
|
||||
import '../widgets/search_box.dart';
|
||||
import '../widgets/state_views.dart';
|
||||
import 'add_friend_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
|
||||
/// 消息列表页(效果图 m1):
|
||||
/// 标题「畅联」+ 圆形加号、搜索框、会话行(头像/名称/预览/时间/未读角标/免打扰铃铛)、
|
||||
/// 置顶置灰,空/加载/失败/断网四态按 states.png。
|
||||
class ConversationListScreen extends StatefulWidget {
|
||||
/// 空态「去找同事」按钮:切到通讯录 Tab
|
||||
final VoidCallback? onFindContacts;
|
||||
|
||||
const ConversationListScreen({super.key, this.onFindContacts});
|
||||
|
||||
@override
|
||||
State<ConversationListScreen> createState() => _ConversationListScreenState();
|
||||
}
|
||||
|
||||
class _ConversationListScreenState extends State<ConversationListScreen> {
|
||||
String _keyword = '';
|
||||
|
||||
bool get _offline => IMService.instance.connectStatus != ConnectStatus.success;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('畅联'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 26),
|
||||
onPressed: () {
|
||||
// 加号:从简只保留「添加同事」入口
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.person_add_alt),
|
||||
title: const Text('添加同事'),
|
||||
onTap: () {
|
||||
Navigator.of(ctx).pop();
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AddFriendScreen()));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: AnimatedBuilder(
|
||||
animation: IMService.instance,
|
||||
builder: (context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_offline) const OfflineBanner(),
|
||||
SearchBox(onChanged: (v) => setState(() => _keyword = v)),
|
||||
Expanded(child: _buildBody()),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final im = IMService.instance;
|
||||
// 四态:加载中 / 加载失败 / 空 / 列表
|
||||
if (!im.conversationsLoaded && im.syncing && im.conversations.isEmpty) {
|
||||
return const LoadingConversations();
|
||||
}
|
||||
if (im.syncFailed && im.conversations.isEmpty) {
|
||||
return ErrorConversations(onRetry: () => im.refreshConversations());
|
||||
}
|
||||
final list = _keyword.isEmpty
|
||||
? im.conversations
|
||||
: im.conversations
|
||||
.where((c) => (c.showName ?? '').toLowerCase().contains(_keyword.toLowerCase()))
|
||||
.toList();
|
||||
if (list.isEmpty) {
|
||||
if (_keyword.isNotEmpty) {
|
||||
return const Center(
|
||||
child: Text('没有找到相关会话', style: TextStyle(fontSize: AppFont.sub, color: AppColors.textSecondary)),
|
||||
);
|
||||
}
|
||||
return EmptyConversations(onFindContacts: widget.onFindContacts);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: list.length + (_offline ? 1 : 0),
|
||||
separatorBuilder: (_, i) => const Divider(indent: 80),
|
||||
itemBuilder: (context, i) {
|
||||
if (i >= list.length) return const OfflineFooter();
|
||||
return _conversationTile(list[i]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _conversationTile(ConversationInfo c) {
|
||||
final isGroup = c.conversationType == ConversationType.superGroup || c.conversationType == ConversationType.group;
|
||||
final muted = (c.recvMsgOpt ?? 0) != 0;
|
||||
return Material(
|
||||
color: c.isPinned == true ? AppColors.pinnedBg : AppColors.pageBg,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (_) => ChatScreen(conversation: c)))
|
||||
.then((_) => IMService.instance.refreshConversations());
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppGap.x3, vertical: AppGap.x3),
|
||||
child: Row(
|
||||
children: [
|
||||
_avatarWithBadge(c, isGroup, muted),
|
||||
const SizedBox(width: AppGap.x3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
c.showName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: AppFont.body, color: AppColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: AppGap.x1),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
FormatUtils.messagePreview(c.latestMsg, isGroup: isGroup),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: AppFont.sub, color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
if (muted)
|
||||
const Icon(Icons.notifications_off_outlined, size: 16, color: AppColors.textSecondary),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppGap.x2),
|
||||
Text(
|
||||
FormatUtils.conversationTime(c.latestMsgSendTime),
|
||||
style: const TextStyle(fontSize: AppFont.small, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _avatarWithBadge(ConversationInfo c, bool isGroup, bool muted) {
|
||||
final unread = c.unreadCount;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NameAvatar(name: c.showName ?? '', faceURL: c.faceURL, isGroup: isGroup, size: 56),
|
||||
if (unread > 0)
|
||||
Positioned(
|
||||
right: -6,
|
||||
top: -6,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
height: 18,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
// 免打扰会话的角标弱化(参考微信习惯)
|
||||
color: muted ? AppColors.textSecondary : AppColors.danger,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Text(
|
||||
unread > 99 ? '99+' : '$unread',
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user