From 49b346d375e9b9b62c3a59c6f6970c7facfba94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=B7=A5=E7=A8=8B=E5=B8=88?= Date: Thu, 20 Aug 2026 03:51:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile-next):=20=E9=80=9A=E8=AE=AF?= =?UTF-8?q?=E5=BD=95=E6=8C=89=E9=83=A8=E9=97=A8=E5=88=86=E7=BB=84=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=E8=81=8C=E5=8A=A1=E8=A1=8C=E4=B8=8E=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E7=AA=84=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor Co-authored-by: multica-agent --- .../lib/pages/contacts/contacts_logic.dart | 89 +++++++- .../lib/pages/contacts/contacts_view.dart | 121 +++++++++-- .../friend_requests/friend_requests_view.dart | 190 ++++++++++-------- 3 files changed, 294 insertions(+), 106 deletions(-) diff --git a/mobile-next/lib/pages/contacts/contacts_logic.dart b/mobile-next/lib/pages/contacts/contacts_logic.dart index 6a68c33..46a4ed1 100644 --- a/mobile-next/lib/pages/contacts/contacts_logic.dart +++ b/mobile-next/lib/pages/contacts/contacts_logic.dart @@ -4,18 +4,45 @@ import 'package:openim/pages/contacts/group_profile_panel/group_profile_panel_lo import 'package:openim/routes/app_navigator.dart'; import 'package:openim_common/openim_common.dart'; +import '../../company/directory/directory_api.dart'; import '../../core/controller/im_controller.dart'; import '../home/home_logic.dart'; import 'select_contacts/select_contacts_logic.dart'; +class ColleagueRow { + ColleagueRow({ + required this.userID, + required this.nickname, + required this.faceURL, + required this.department, + required this.title, + }); + + final String userID; + final String nickname; + final String faceURL; + final String department; + final String title; + + String get subtitle { + if (title.isNotEmpty && department.isNotEmpty) { + return '$title · $department'; + } + if (title.isNotEmpty) return title; + return department; + } +} + class ContactsLogic extends GetxController implements ViewUserProfileBridge, SelectContactsBridge, ScanBridge { final imLogic = Get.find(); final homeLogic = Get.find(); + final _directory = DirectoryApi(); final friendApplicationList = []; final loading = true.obs; final loadFailed = false.obs; + final colleagues = [].obs; int get friendApplicationCount => homeLogic.unhandledFriendApplicationCount.value; @@ -23,6 +50,21 @@ class ContactsLogic extends GetxController int get groupApplicationCount => homeLogic.unhandledGroupApplicationCount.value; + Map> get groupedColleagues { + final map = >{}; + for (final row in colleagues) { + final key = row.department.isEmpty ? '未分组' : row.department; + map.putIfAbsent(key, () => []).add(row); + } + final keys = map.keys.toList() + ..sort((a, b) { + if (a == '未分组') return 1; + if (b == '未分组') return -1; + return a.compareTo(b); + }); + return {for (final k in keys) k: map[k]!}; + } + @override void onInit() { PackageBridge.selectContactsBridge = this; @@ -46,11 +88,7 @@ class ContactsLogic extends GetxController OpenIM.iMManager.friendshipManager .getFriendApplicationListAsRecipient(), OpenIM.iMManager.groupManager.getGroupApplicationListAsRecipient(), - OpenIM.iMManager.friendshipManager.getFriendListPage( - offset: 0, - count: 1, - filterBlack: true, - ), + _loadColleagues(), ]); homeLogic.getUnhandledFriendApplicationCount(); homeLogic.getUnhandledGroupApplicationCount(); @@ -62,6 +100,47 @@ class ContactsLogic extends GetxController } } + Future _loadColleagues() async { + final friends = []; + const page = 1000; + while (true) { + final temp = await OpenIM.iMManager.friendshipManager.getFriendListPage( + offset: friends.length, + count: page, + filterBlack: true, + ); + friends.addAll(temp); + if (temp.length < page) break; + } + + var dirById = {}; + try { + final items = await _directory.search(keyword: '', limit: 100); + dirById = {for (final e in items) e.userID: e}; + } catch (e, s) { + Logger.print('contacts directory e: $e $s'); + } + + colleagues.assignAll(friends.where((e) => e.userID != null).map((e) { + final dir = dirById[e.userID]; + return ColleagueRow( + userID: e.userID!, + nickname: (e.remark?.isNotEmpty == true) + ? e.remark! + : (e.nickname?.isNotEmpty == true ? e.nickname! : e.userID!), + faceURL: e.faceURL ?? dir?.faceURL ?? '', + department: dir?.department ?? '', + title: dir?.title ?? '', + ); + })); + } + + void openColleague(ColleagueRow row) => AppNavigator.startUserProfilePane( + userID: row.userID, + nickname: row.nickname, + faceURL: row.faceURL, + ); + @override void onClose() { PackageBridge.selectContactsBridge = null; diff --git a/mobile-next/lib/pages/contacts/contacts_view.dart b/mobile-next/lib/pages/contacts/contacts_view.dart index 445de25..279080e 100644 --- a/mobile-next/lib/pages/contacts/contacts_view.dart +++ b/mobile-next/lib/pages/contacts/contacts_view.dart @@ -32,31 +32,116 @@ class ContactsPage extends StatelessWidget { if (logic.loadFailed.value) { return CompanyFailView(onRetry: logic.reload); } - return Column( - children: [ - _entry( - icon: Icons.person_add_alt, - label: '新的同事', - count: logic.friendApplicationCount, - onTap: logic.newFriend, - ), - _entry( - icon: Icons.group_outlined, - label: '我的群聊', - count: logic.groupApplicationCount, - onTap: logic.myGroup, - ), - _entry( - icon: Icons.people_outline, - label: '同事', - onTap: logic.myFriend, + final grouped = logic.groupedColleagues; + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column( + children: [ + _entry( + icon: Icons.person_add_alt, + label: '新的同事', + count: logic.friendApplicationCount, + onTap: logic.newFriend, + ), + _entry( + icon: Icons.group_outlined, + label: '我的群聊', + count: logic.groupApplicationCount, + onTap: logic.myGroup, + ), + ], + ), ), + if (logic.colleagues.isEmpty) + const SliverFillRemaining( + hasScrollBody: false, + child: CompanyEmptyView( + icon: Icons.person_outline, + title: '暂时没有同事', + subtitle: '点右上角添加同事', + ), + ) + else + for (final entry in grouped.entries) ...[ + SliverToBoxAdapter(child: _groupHeader(entry.key)), + SliverList( + delegate: SliverChildBuilderDelegate( + (_, index) => _colleagueRow(entry.value[index]), + childCount: entry.value.length, + ), + ), + ], ], ); }), ); } + Widget _groupHeader(String name) { + return Container( + height: 28, + color: AppColors.chatBg, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: AppGap.x4), + child: Text( + name, + style: const TextStyle( + fontSize: AppFont.small, color: AppColors.textSecondary), + ), + ); + } + + Widget _colleagueRow(ColleagueRow row) { + return InkWell( + onTap: () => logic.openColleague(row), + splashColor: AppColors.chatBg, + highlightColor: AppColors.chatBg, + child: SizedBox( + height: 64, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppGap.x4), + child: Row( + children: [ + AvatarView( + url: row.faceURL, + text: row.nickname, + width: 48, + height: 48, + ), + const SizedBox(width: AppGap.x3), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + row.nickname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: AppFont.body, + color: AppColors.textPrimary), + ), + if (row.subtitle.isNotEmpty) + Text( + row.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: AppFont.sub, + color: AppColors.textSecondary), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + Widget _entry({ required IconData icon, required String label, diff --git a/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart index 38dcd79..2828182 100644 --- a/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart +++ b/mobile-next/lib/pages/contacts/friend_requests/friend_requests_view.dart @@ -34,19 +34,111 @@ class FriendRequestsPage extends StatelessWidget { return ListView.builder( itemCount: logic.applicationList.length, itemBuilder: (_, index) => - _buildItemView(logic.applicationList[index]), + _buildItemView(context, logic.applicationList[index]), ); }), ); } - Widget _buildItemView(FriendApplicationInfo info) { + Widget _buildItemView(BuildContext context, FriendApplicationInfo info) { final isISendRequest = info.fromUserID == OpenIM.iMManager.userID; final name = isISendRequest ? info.toNickname : info.fromNickname; final faceURL = isISendRequest ? info.toFaceURL : info.fromFaceURL; final waiting = info.isWaitingHandle; final rejected = info.isRejected; final agreed = info.isAgreed; + final narrow = MediaQuery.sizeOf(context).width < 360; + final pendingActions = waiting && !isISendRequest; + final stacked = narrow && pendingActions; + + Widget status; + if (waiting && isISendRequest) { + status = const Text('等待对方通过', + style: TextStyle( + fontSize: AppFont.sub, color: AppColors.textSecondary)); + } else if (pendingActions) { + status = SizedBox( + width: 132, + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => logic.refuseFriendApplication(info), + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 32), + padding: EdgeInsets.zero, + side: const BorderSide( + color: AppColors.textSecondary, width: 0.5), + foregroundColor: AppColors.textPrimary, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppRadius.control)), + ), + child: const Text('拒绝', + style: TextStyle(fontSize: AppFont.sub)), + ), + ), + const SizedBox(width: AppGap.x2), + Expanded( + child: FilledButton( + onPressed: () => logic.acceptFriendApplication(info), + style: FilledButton.styleFrom( + minimumSize: const Size(0, 32), + padding: EdgeInsets.zero, + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppRadius.control)), + ), + child: const Text('接受', + style: TextStyle( + fontSize: AppFont.sub, color: Colors.white)), + ), + ), + ], + ), + ); + } else if (agreed) { + status = const Text('已添加', + style: TextStyle( + fontSize: AppFont.sub, color: AppColors.textSecondary)); + } else if (rejected) { + status = const Text('已拒绝', + style: TextStyle( + fontSize: AppFont.sub, color: AppColors.textSecondary)); + } else { + status = const SizedBox.shrink(); + } + + final identity = Row( + children: [ + AvatarView(url: faceURL, text: name, width: 48, height: 48), + const SizedBox(width: AppGap.x3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: AppFont.body, color: AppColors.textPrimary), + ), + if (IMUtils.isNotNullEmptyStr(info.reqMsg)) + Text( + info.reqMsg ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: AppFont.sub, color: AppColors.textSecondary), + ), + ], + ), + ), + ], + ); return Container( constraints: const BoxConstraints(minHeight: 72), @@ -57,90 +149,22 @@ class FriendRequestsPage extends StatelessWidget { border: Border(bottom: BorderSide(color: AppColors.divider, width: 0.5)), ), - child: Row( - children: [ - AvatarView(url: faceURL, text: name, width: 48, height: 48), - const SizedBox(width: AppGap.x3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + child: stacked + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - name ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: AppFont.body, color: AppColors.textPrimary), - ), - if (IMUtils.isNotNullEmptyStr(info.reqMsg)) - Text( - info.reqMsg ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: AppFont.sub, color: AppColors.textSecondary), - ), + identity, + const SizedBox(height: AppGap.x2), + Align(alignment: Alignment.centerRight, child: status), + ], + ) + : Row( + children: [ + Expanded(child: identity), + const SizedBox(width: AppGap.x2), + status, ], ), - ), - const SizedBox(width: AppGap.x2), - if (waiting && isISendRequest) - const Text('等待对方通过', - style: TextStyle( - fontSize: AppFont.sub, color: AppColors.textSecondary)), - if (waiting && !isISendRequest) - SizedBox( - width: 132, - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => logic.refuseFriendApplication(info), - style: OutlinedButton.styleFrom( - minimumSize: const Size(0, 32), - padding: EdgeInsets.zero, - side: const BorderSide( - color: AppColors.textSecondary, width: 0.5), - foregroundColor: AppColors.textPrimary, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(AppRadius.control)), - ), - child: const Text('拒绝', - style: TextStyle(fontSize: AppFont.sub)), - ), - ), - const SizedBox(width: AppGap.x2), - Expanded( - child: FilledButton( - onPressed: () => logic.acceptFriendApplication(info), - style: FilledButton.styleFrom( - minimumSize: const Size(0, 32), - padding: EdgeInsets.zero, - backgroundColor: AppColors.primary, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(AppRadius.control)), - ), - child: const Text('接受', - style: TextStyle( - fontSize: AppFont.sub, color: Colors.white)), - ), - ), - ], - ), - ), - if (agreed) - const Text('已添加', - style: TextStyle( - fontSize: AppFont.sub, color: AppColors.textSecondary)), - if (rejected) - const Text('已拒绝', - style: TextStyle( - fontSize: AppFont.sub, color: AppColors.textSecondary)), - ], - ), ); } }