2025-09-13 17:01:01 +08:00
|
|
|
|
import 'package:easy_refresh/easy_refresh.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:loopin/IM/im_service.dart';
|
|
|
|
|
import 'package:loopin/styles/index.dart';
|
|
|
|
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart';
|
|
|
|
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
|
|
|
|
|
|
|
|
|
class InviteActionSheet extends StatefulWidget {
|
|
|
|
|
final String groupID;
|
|
|
|
|
|
|
|
|
|
final Function(List<String> selected) onAction;
|
|
|
|
|
final String title;
|
|
|
|
|
final String actionLabel;
|
|
|
|
|
final bool showButton;
|
|
|
|
|
|
|
|
|
|
const InviteActionSheet({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.groupID,
|
|
|
|
|
required this.onAction,
|
|
|
|
|
this.title = "",
|
|
|
|
|
this.actionLabel = "确认",
|
|
|
|
|
this.showButton = true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<InviteActionSheet> createState() => _MemberActionSheetState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MemberActionSheetState extends State<InviteActionSheet> {
|
|
|
|
|
String _query = "";
|
|
|
|
|
final Set<String> _selectedIDs = {}; // 选中的 userID 集合
|
|
|
|
|
late List<V2TimUserFullInfo> members = [];
|
|
|
|
|
String nextSeq = '';
|
|
|
|
|
bool hasMore = false;
|
|
|
|
|
bool loading = false;
|
|
|
|
|
//
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
getMemberData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//互关好友
|
|
|
|
|
Future<void> getMemberData({bool reset = false}) async {
|
|
|
|
|
if (loading) return;
|
|
|
|
|
loading = true;
|
|
|
|
|
final res = await ImService.instance.getMutualFollowersList(
|
|
|
|
|
nextCursor: nextSeq,
|
|
|
|
|
);
|
|
|
|
|
if (res.success && res.data != null) {
|
|
|
|
|
final userInfoList = res.data!.userFullInfoList ?? [];
|
|
|
|
|
final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty;
|
|
|
|
|
logger.w('获取成功:${res.data!.nextCursor},是否还有更多:$isFinished');
|
|
|
|
|
if (isFinished) {
|
|
|
|
|
hasMore = false;
|
|
|
|
|
nextSeq = '';
|
|
|
|
|
} else {
|
|
|
|
|
nextSeq = res.data!.nextCursor ?? '';
|
|
|
|
|
hasMore = nextSeq.isNotEmpty ? true : false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final ids = userInfoList.map((item) => item.userID).whereType<String>().toList();
|
|
|
|
|
if (ids.isNotEmpty) {
|
|
|
|
|
getGroupMemberData(userIDs: ids, userList: userInfoList);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logger.e('获取数据失败:${res.desc}');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//群成员
|
|
|
|
|
Future<void> getGroupMemberData({required List<String> userIDs, required List<V2TimUserFullInfo> userList}) async {
|
|
|
|
|
final res = await ImService.instance.getGroupMembersInfo(
|
|
|
|
|
groupID: widget.groupID,
|
|
|
|
|
memberList: userIDs,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (res.success && res.data != null) {
|
|
|
|
|
final List<V2TimGroupMemberFullInfo> groupMemberList = res.data ?? [];
|
|
|
|
|
// 已经在群里的 userID
|
|
|
|
|
final inGroupIds = groupMemberList.map((m) => m.userID).whereType<String>().where((id) => id.isNotEmpty).toSet();
|
|
|
|
|
// 过滤掉已经在群里的
|
|
|
|
|
final notInGroupUsers = userList.where((user) => user.userID != null && !inGroupIds.contains(user.userID)).toList();
|
|
|
|
|
setState(() {
|
|
|
|
|
members.addAll(notInGroupUsers);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setState(() {
|
|
|
|
|
loading = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String handleText(String? text, String defaultValue) {
|
|
|
|
|
if (text == null || text.trim().isEmpty) return defaultValue;
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
// 搜索过滤
|
|
|
|
|
final filteredMembers = members.where((m) {
|
|
|
|
|
final name = m.nickName ?? '';
|
|
|
|
|
return name.contains(_query);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
return SafeArea(
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
behavior: HitTestBehavior.translucent, // 点击空白区域也能触发
|
|
|
|
|
onTap: () {
|
|
|
|
|
FocusScope.of(context).unfocus();
|
|
|
|
|
},
|
|
|
|
|
child: Scaffold(
|
|
|
|
|
backgroundColor: Colors.white,
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
centerTitle: true,
|
|
|
|
|
forceMaterialTransparency: true,
|
|
|
|
|
bottom: PreferredSize(
|
|
|
|
|
preferredSize: const Size.fromHeight(1.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
color: Colors.grey[300],
|
|
|
|
|
height: 1.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
title: Text(
|
|
|
|
|
widget.title,
|
|
|
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
leading: IconButton(
|
|
|
|
|
icon: const Icon(Icons.arrow_back),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
body: Column(
|
|
|
|
|
children: [
|
|
|
|
|
SizedBox(height: 10),
|
|
|
|
|
// 搜索框
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
child: TextField(
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
prefixIcon: const Icon(Icons.search),
|
|
|
|
|
hintText: "搜索",
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
|
|
|
),
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_query = value.trim();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
|
|
|
|
// 成员列表
|
|
|
|
|
Expanded(
|
|
|
|
|
child: EasyRefresh(
|
|
|
|
|
footer: ClassicFooter(
|
|
|
|
|
dragText: '加载更多',
|
|
|
|
|
armedText: '释放加载',
|
|
|
|
|
readyText: '加载中...',
|
|
|
|
|
processingText: '加载中...',
|
2025-09-22 14:41:47 +08:00
|
|
|
|
processedText: '加载完成',
|
|
|
|
|
noMoreText: '没有更多了~',
|
2025-09-13 17:01:01 +08:00
|
|
|
|
failedText: '加载失败,请重试',
|
|
|
|
|
messageText: '最后更新于 %T',
|
|
|
|
|
),
|
|
|
|
|
onLoad: () async {
|
|
|
|
|
if (hasMore) {
|
|
|
|
|
await getMemberData();
|
2025-09-22 14:41:47 +08:00
|
|
|
|
return hasMore ? IndicatorResult.success : IndicatorResult.noMore;
|
2025-09-13 17:01:01 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: ListView.builder(
|
|
|
|
|
itemCount: filteredMembers.length,
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
final m = filteredMembers[index];
|
|
|
|
|
final id = m.userID;
|
|
|
|
|
final uname = handleText(m.nickName, '');
|
|
|
|
|
final nickname = handleText(m.nickName, '未知昵称');
|
|
|
|
|
final showName = uname.isEmpty ? nickname : uname;
|
|
|
|
|
return InkWell(
|
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
if (_selectedIDs.contains(id)) {
|
|
|
|
|
_selectedIDs.remove(id);
|
|
|
|
|
} else if (id != null && id.isNotEmpty) {
|
|
|
|
|
_selectedIDs.add(id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
// 左侧圆形头像
|
|
|
|
|
CircleAvatar(
|
|
|
|
|
radius: 20,
|
|
|
|
|
backgroundImage: m.faceUrl != null ? NetworkImage(m.faceUrl!) : null,
|
|
|
|
|
child: m.faceUrl == null ? const Icon(Icons.person) : null,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
|
|
|
|
// 用户名
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
showName,
|
|
|
|
|
style: const TextStyle(fontSize: 14),
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 复选框
|
|
|
|
|
Container(
|
|
|
|
|
width: 24,
|
|
|
|
|
height: 24,
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: Border.all(color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.grey),
|
|
|
|
|
color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.transparent,
|
|
|
|
|
),
|
|
|
|
|
child: _selectedIDs.contains(id) ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 底部操作按钮
|
|
|
|
|
if (widget.showButton)
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
minimumSize: const Size(double.infinity, 48),
|
|
|
|
|
backgroundColor: FStyle.primaryColor,
|
|
|
|
|
),
|
|
|
|
|
onPressed: _selectedIDs.isEmpty
|
|
|
|
|
? null
|
|
|
|
|
: () {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
widget.onAction(_selectedIDs.toList());
|
|
|
|
|
},
|
|
|
|
|
child: Text(
|
|
|
|
|
"${widget.actionLabel}(${_selectedIDs.length})",
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: _selectedIDs.isNotEmpty ? Colors.white : Colors.black,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|