303 lines
11 KiB
Dart
303 lines
11 KiB
Dart
![]() |
import 'package:easy_refresh/easy_refresh.dart';
|
|||
|
import 'package:flutter/material.dart';
|
|||
|
import 'package:get/get.dart';
|
|||
|
import 'package:loopin/IM/im_service.dart';
|
|||
|
import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart';
|
|||
|
import 'package:loopin/styles/index.dart';
|
|||
|
import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart';
|
|||
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart';
|
|||
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart';
|
|||
|
|
|||
|
class MemberActionSheet extends StatefulWidget {
|
|||
|
final String groupID;
|
|||
|
final Function(List<String> selected) onAction;
|
|||
|
final String title;
|
|||
|
final String actionLabel;
|
|||
|
final bool showButton;
|
|||
|
final bool showSelf;
|
|||
|
|
|||
|
const MemberActionSheet({
|
|||
|
super.key,
|
|||
|
required this.groupID,
|
|||
|
required this.onAction,
|
|||
|
this.title = "",
|
|||
|
this.actionLabel = "确认",
|
|||
|
this.showButton = true,
|
|||
|
this.showSelf = false,
|
|||
|
});
|
|||
|
|
|||
|
@override
|
|||
|
State<MemberActionSheet> createState() => _MemberActionSheetState();
|
|||
|
}
|
|||
|
|
|||
|
class _MemberActionSheetState extends State<MemberActionSheet> {
|
|||
|
String _query = "";
|
|||
|
final Set<String> _selectedIDs = {}; // 选中的 userID 集合
|
|||
|
late List<V2TimGroupMemberFullInfo> members = [];
|
|||
|
late List<V2TimGroupMemberFullInfo> searchMemberList = [];
|
|||
|
String nextSeq = '0';
|
|||
|
String searchCursor = '';
|
|||
|
bool hasMore = false; // 普通列表用
|
|||
|
bool loading = false;
|
|||
|
bool isFinished = false; // 搜索列表用
|
|||
|
bool loading2 = false; //搜索
|
|||
|
|
|||
|
//
|
|||
|
@override
|
|||
|
void initState() {
|
|||
|
super.initState();
|
|||
|
if (widget.showSelf) {
|
|||
|
final self = Get.find<GroupDetailController>().selfInfo.value!;
|
|||
|
members.insert(0, self);
|
|||
|
}
|
|||
|
getMemberData();
|
|||
|
}
|
|||
|
|
|||
|
//群成员
|
|||
|
Future<void> getMemberData() async {
|
|||
|
if (loading) return;
|
|||
|
loading = true;
|
|||
|
final res = await ImService.instance.getGroupMemberList(
|
|||
|
groupID: widget.groupID,
|
|||
|
filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_COMMON,
|
|||
|
nextSeq: nextSeq,
|
|||
|
count: 100,
|
|||
|
);
|
|||
|
if (res.success && res.data != null) {
|
|||
|
logger.e(res.data!.nextSeq);
|
|||
|
nextSeq = res.data!.nextSeq ?? '0';
|
|||
|
final mem = res.data!.memberInfoList ?? [];
|
|||
|
setState(() {
|
|||
|
members.addAll(mem);
|
|||
|
loading = false;
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
///搜索群成员
|
|||
|
Future<void> searchMember({bool loadMore = false}) async {
|
|||
|
if (loading2) return;
|
|||
|
if (_query.isEmpty) {
|
|||
|
setState(() {
|
|||
|
_query = '';
|
|||
|
});
|
|||
|
return;
|
|||
|
}
|
|||
|
loading2 = true;
|
|||
|
final param = V2TimGroupMemberSearchParam(
|
|||
|
keywordList: [_query],
|
|||
|
groupIDList: [widget.groupID],
|
|||
|
isSearchMemberUserID: true,
|
|||
|
isSearchMemberNickName: true,
|
|||
|
isSearchMemberRemark: true,
|
|||
|
isSearchMemberNameCard: true,
|
|||
|
keywordListMatchType: V2TimGroupMemberSearchParam.V2TIM_KEYWORD_LIST_MATCH_TYPE_OR,
|
|||
|
searchCount: 100,
|
|||
|
searchCursor: searchCursor,
|
|||
|
);
|
|||
|
final res = await ImService.instance.searchGroupMembers(param: param);
|
|||
|
if (res.success && res.data != null) {
|
|||
|
isFinished = res.data!.isFinished ?? true;
|
|||
|
final searchCursor = res.data!.nextCursor ?? '';
|
|||
|
final data = res.data!.groupMemberSearchResultItems;
|
|||
|
// 搜索结果群的成员
|
|||
|
if (data != null && data[widget.groupID] != null) {
|
|||
|
List<V2TimGroupMemberFullInfo> list = (data[widget.groupID] as List<dynamic>).cast<V2TimGroupMemberFullInfo>();
|
|||
|
if (!widget.showSelf) {
|
|||
|
// 过滤自己
|
|||
|
final self = Get.find<GroupDetailController>().selfInfo.value!;
|
|||
|
list = list.where((item) => item.userID != self.userID).toList();
|
|||
|
}
|
|||
|
if (loadMore) {
|
|||
|
searchMemberList.addAll(list);
|
|||
|
} else {
|
|||
|
searchMemberList = list;
|
|||
|
}
|
|||
|
}
|
|||
|
logger.w(hasMore);
|
|||
|
logger.w(searchCursor);
|
|||
|
logger.w(data);
|
|||
|
}
|
|||
|
setState(() {
|
|||
|
loading2 = false;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
String handleText(String? text, String defaultValue) {
|
|||
|
if (text == null || text.trim().isEmpty) return defaultValue;
|
|||
|
return text;
|
|||
|
}
|
|||
|
|
|||
|
@override
|
|||
|
Widget build(BuildContext context) {
|
|||
|
List filteredMembers;
|
|||
|
if (_query.isEmpty) {
|
|||
|
filteredMembers = members;
|
|||
|
} else {
|
|||
|
filteredMembers = searchMemberList;
|
|||
|
}
|
|||
|
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();
|
|||
|
searchMember();
|
|||
|
});
|
|||
|
},
|
|||
|
),
|
|||
|
),
|
|||
|
|
|||
|
const SizedBox(height: 8),
|
|||
|
|
|||
|
// 成员列表
|
|||
|
Expanded(
|
|||
|
child: EasyRefresh(
|
|||
|
footer: ClassicFooter(
|
|||
|
dragText: '加载更多',
|
|||
|
armedText: '释放加载',
|
|||
|
readyText: '加载中...',
|
|||
|
processingText: '加载中...',
|
|||
|
processedText: hasMore ? '加载完成' : '没有更多了~',
|
|||
|
failedText: '加载失败,请重试',
|
|||
|
messageText: '最后更新于 %T',
|
|||
|
),
|
|||
|
onLoad: () async {
|
|||
|
//
|
|||
|
if (hasMore) {
|
|||
|
if (_query.isNotEmpty) {
|
|||
|
await searchMember(loadMore: true);
|
|||
|
} else {
|
|||
|
await getMemberData();
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
child: ListView.builder(
|
|||
|
itemCount: filteredMembers.length,
|
|||
|
itemBuilder: (context, index) {
|
|||
|
final m = filteredMembers[index];
|
|||
|
final id = m.userID;
|
|||
|
final uname = handleText(m.nameCard, '');
|
|||
|
final nickname = handleText(m.nickName, '未知昵称');
|
|||
|
final showName = uname.isEmpty ? nickname : uname;
|
|||
|
return InkWell(
|
|||
|
onTap: () {
|
|||
|
setState(() {
|
|||
|
if (_selectedIDs.contains(id)) {
|
|||
|
_selectedIDs.remove(id);
|
|||
|
} else {
|
|||
|
_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,
|
|||
|
),
|
|||
|
),
|
|||
|
if (widget.showButton)
|
|||
|
// 复选框
|
|||
|
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,
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
],
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
);
|
|||
|
}
|
|||
|
}
|