347 lines
11 KiB
Dart
347 lines
11 KiB
Dart
import 'package:elysia/plugin/Remind.dart';
|
||
import 'package:elysia/plugin/RouteAnimation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:shimmer/shimmer.dart';
|
||
import '../bean/ChatCacheItem.dart';
|
||
import '../plugin/CacheAavatar.dart';
|
||
import '../plugin/HTTP.dart';
|
||
import '../plugin/C.dart';
|
||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||
import 'package:oktoast/oktoast.dart';
|
||
import '../plugin/HiverCache.dart';
|
||
import '../plugin/LoadingOverlay.dart';
|
||
import '../plugin/StringTool.dart';
|
||
import 'child/ChatPage.dart';
|
||
|
||
class ChatRoomPage extends StatefulWidget {
|
||
const ChatRoomPage({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<ChatRoomPage> createState() => _ChatRoomPageState();
|
||
|
||
static Function(dynamic?)? flushData;
|
||
}
|
||
|
||
class _ChatRoomPageState extends State<ChatRoomPage>
|
||
with AutomaticKeepAliveClientMixin {
|
||
List<ChatCacheItem> _chatList = [];
|
||
bool _loading = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
ChatRoomPage.flushData = (data) {
|
||
if (data == null) {
|
||
getCache();
|
||
} else {
|
||
int index = _chatList.indexWhere(
|
||
(item) => item.roomId == data["roomId"].toString(),
|
||
);
|
||
if (index == -1) return; // 找不到就直接返回
|
||
// 深拷贝并更新数据
|
||
ChatCacheItem chatItem = _chatList[index].copy();
|
||
|
||
chatItem.lastChatMessage = data["message"] ?? data["content"];
|
||
chatItem.updateTime(DateTime.now());
|
||
setState(() {
|
||
// 先移除旧 item
|
||
_chatList.removeAt(index);
|
||
// 插入到列表头
|
||
_chatList.insert(0, chatItem);
|
||
});
|
||
HiverCache.cache(C.HIVE_CACHE_CACHE_LIST, _chatList);
|
||
}
|
||
};
|
||
getCache();
|
||
}
|
||
|
||
Future<void> getCache() async {
|
||
List<ChatCacheItem>? result = await HiverCache.getCache<ChatCacheItem>(
|
||
C.HIVE_CACHE_CACHE_LIST,
|
||
ChatCacheItem.fromJson,
|
||
);
|
||
setState(() {
|
||
_loading = false;
|
||
});
|
||
await fetchChatList(result??[]);
|
||
startFlushData(result??[]);
|
||
}
|
||
|
||
Future<void> startFlushData(List<ChatCacheItem> cache) async {
|
||
if(cache?.length==0){
|
||
_loading=true;
|
||
}
|
||
dynamic result = await loadData();
|
||
try {
|
||
if (result['code'] == 200) {
|
||
List<Map<String, dynamic>> listOfMap = result["data"]
|
||
.cast<Map<String, dynamic>>();
|
||
List<ChatCacheItem> dbData = listOfMap
|
||
.map((e) => ChatCacheItem.fromJson(e))
|
||
.toList();
|
||
syncLists(cache ?? [], dbData);
|
||
fetchChatList(cache);
|
||
HiverCache.cache(C.HIVE_CACHE_CACHE_LIST, cache);
|
||
} else {
|
||
setState(() {
|
||
_loading = false;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
setState(() {
|
||
_loading = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
void syncLists(List<ChatCacheItem> cacheList, List<ChatCacheItem> dbList) {
|
||
// 提取 bList 的所有 id
|
||
final bIds = dbList.map((e) => e.roomId).toSet();
|
||
// 删除 A 中多余的项(ID 不在 B 里)
|
||
cacheList.removeWhere((a) => !bIds.contains(a.roomId));
|
||
// 提取 A 的所有 id
|
||
final aIds = cacheList.map((e) => e.roomId).toSet();
|
||
// 添加 B 中缺少的项
|
||
for (var b in dbList) {
|
||
if (!aIds.contains(b.roomId)) {
|
||
cacheList.add(b);
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> fetchChatList(List<ChatCacheItem> result) async {
|
||
print(result);
|
||
setState(() {
|
||
_chatList = result;
|
||
_loading = false;
|
||
});
|
||
}
|
||
|
||
Future<dynamic> loadData() async {
|
||
dynamic result = await HTTP
|
||
.create("${C.BASE_URL}/chat/list")
|
||
.setHeader(C.TOKEN)
|
||
.setRequestType(RequestType.GET)
|
||
.execute();
|
||
return result;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: _loading
|
||
? ListView.builder(
|
||
itemCount: 10, // 加载前显示 10 个 Shimmer
|
||
itemBuilder: (context, index) {
|
||
return ShimmerChatTile();
|
||
},
|
||
)
|
||
: ListView.builder(
|
||
itemCount: _chatList.length,
|
||
itemBuilder: (context, index) {
|
||
final chat = _chatList[index];
|
||
return ChatTile(
|
||
chat: chat,
|
||
onDelete: () {
|
||
Remind.show(
|
||
context,
|
||
"提示",
|
||
"是否删除聊天,这是不可恢复的!!!",
|
||
onTap: (status) async {
|
||
if (status) {
|
||
LoadingOverlay.show(
|
||
context: context,
|
||
barrierColor: Colors.black54,
|
||
);
|
||
try {
|
||
dynamic result = await HTTP
|
||
.create("${C.BASE_URL}/chat/del/${chat.roomId}")
|
||
.setHeader(C.TOKEN)
|
||
.setRequestType(RequestType.DELETE)
|
||
.execute();
|
||
LoadingOverlay.hide();
|
||
if (result['code'] == 200) {
|
||
setState(() {
|
||
_chatList.removeAt(index);
|
||
});
|
||
HiverCache.cache(C.HIVE_CACHE_CACHE_LIST, _chatList);
|
||
} else {
|
||
showToast(
|
||
result["message"],
|
||
position: ToastPosition.bottom,
|
||
);
|
||
}
|
||
} catch (e) {
|
||
LoadingOverlay.hide();
|
||
}
|
||
}
|
||
},
|
||
);
|
||
},
|
||
onClear: () {
|
||
Remind.show(
|
||
context,
|
||
"提示",
|
||
"是否清空聊天记录",
|
||
onTap: (status) async {
|
||
if (status) {
|
||
LoadingOverlay.show(
|
||
context: context,
|
||
barrierColor: Colors.black54,
|
||
);
|
||
try {
|
||
dynamic result = await HTTP
|
||
.create(
|
||
"${C.BASE_URL}/chat/clear/${chat.roomId}",
|
||
)
|
||
.setHeader(C.TOKEN)
|
||
.setRequestType(RequestType.DELETE)
|
||
.execute();
|
||
LoadingOverlay.hide();
|
||
if (result['code'] == 200) {
|
||
setState(() {
|
||
_chatList[index].lastChatTime = "";
|
||
_chatList[index].lastChatMessage = "";
|
||
});
|
||
HiverCache.cache(C.HIVE_CACHE_CACHE_LIST, _chatList);
|
||
} else {
|
||
showToast(
|
||
result["message"],
|
||
position: ToastPosition.bottom,
|
||
);
|
||
}
|
||
} catch (e) {
|
||
LoadingOverlay.hide();
|
||
}
|
||
}
|
||
},
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool get wantKeepAlive => true;
|
||
}
|
||
|
||
/// 单条聊天列表项
|
||
class ChatTile extends StatelessWidget {
|
||
final ChatCacheItem chat;
|
||
final VoidCallback? onDelete;
|
||
final VoidCallback? onClear;
|
||
|
||
const ChatTile({Key? key, required this.chat, this.onDelete, this.onClear})
|
||
: super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Slidable(
|
||
key: ValueKey(chat.roomId),
|
||
endActionPane: ActionPane(
|
||
motion: const DrawerMotion(), // 滑动动画
|
||
extentRatio: 0.45, // 右滑按钮占宽度比例
|
||
children: [
|
||
Expanded(
|
||
child: SlidableAction(
|
||
onPressed: (context) {
|
||
if (onDelete != null) onDelete!();
|
||
},
|
||
backgroundColor: Colors.red,
|
||
foregroundColor: Colors.white,
|
||
icon: Icons.delete,
|
||
label: '删除',
|
||
),
|
||
flex: 4,
|
||
),
|
||
Expanded(
|
||
child: SlidableAction(
|
||
onPressed: (context) {
|
||
if (onClear != null) onClear!();
|
||
},
|
||
backgroundColor: Colors.amberAccent,
|
||
foregroundColor: Colors.white,
|
||
icon: Icons.cleaning_services_rounded,
|
||
label: '清空聊天记录',
|
||
),
|
||
flex: 6,
|
||
),
|
||
],
|
||
),
|
||
child: ListTile(
|
||
leading: CacheAvatar(url: chat.roomImage),
|
||
title: Text(
|
||
chat.roomName,
|
||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
subtitle: Text(
|
||
StringTool.stripMarkdown(chat.lastChatMessage),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
trailing: Text(
|
||
chat.lastChatTime,
|
||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
onTap: () {
|
||
C.PUSH_STATUS = false;
|
||
Navigator.push(
|
||
context,
|
||
RouteAnimation(
|
||
ChatPage(
|
||
roomId: chat.roomId,
|
||
robotAvatar: chat.roomImage,
|
||
robotName: chat.roomName,
|
||
),
|
||
Offset(1, 0),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Shimmer 占位列表项
|
||
class ShimmerChatTile extends StatelessWidget {
|
||
const ShimmerChatTile({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ListTile(
|
||
leading: Shimmer.fromColors(
|
||
baseColor: Colors.grey.shade300,
|
||
highlightColor: Colors.white,
|
||
child: Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
),
|
||
title: Shimmer.fromColors(
|
||
baseColor: Colors.grey.shade300,
|
||
highlightColor: Colors.grey.shade100,
|
||
child: Container(
|
||
height: 16,
|
||
color: Colors.grey,
|
||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||
),
|
||
),
|
||
subtitle: Shimmer.fromColors(
|
||
baseColor: Colors.grey.shade300,
|
||
highlightColor: Colors.grey.shade100,
|
||
child: Container(
|
||
height: 14,
|
||
color: Colors.grey,
|
||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|