feat(mobile-next): 通讯录按部门分组并补齐职务行与申请窄屏

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-20 03:51:27 +08:00
co-authored by Cursor multica-agent
parent 7f131457f7
commit 49b346d375
3 changed files with 294 additions and 106 deletions
@@ -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<IMController>();
final homeLogic = Get.find<HomeLogic>();
final _directory = DirectoryApi();
final friendApplicationList = <UserInfo>[];
final loading = true.obs;
final loadFailed = false.obs;
final colleagues = <ColleagueRow>[].obs;
int get friendApplicationCount =>
homeLogic.unhandledFriendApplicationCount.value;
@@ -23,6 +50,21 @@ class ContactsLogic extends GetxController
int get groupApplicationCount =>
homeLogic.unhandledGroupApplicationCount.value;
Map<String, List<ColleagueRow>> get groupedColleagues {
final map = <String, List<ColleagueRow>>{};
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<void> _loadColleagues() async {
final friends = <FriendInfo>[];
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 = <String, DirectoryUser>{};
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;
+103 -18
View File
@@ -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,
@@ -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)),
],
),
);
}
}