elysia/lib/page/child/MyRobotPage.dart
2025-11-04 09:53:47 +08:00

464 lines
13 KiB
Dart

import 'dart:developer';
import 'package:elysia/page/FollowListPage.dart';
import 'package:elysia/page/child/EditorRobotPage.dart';
import 'package:elysia/plugin/RouteAnimation.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:oktoast/oktoast.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../plugin/C.dart';
import '../../plugin/CacheAavatar.dart';
import '../../plugin/HTTP.dart';
import '../../plugin/LoadingOverlay.dart';
import '../../plugin/Remind.dart';
class MyRobotPage extends StatefulWidget {
const MyRobotPage({Key? key}) : super(key: key);
@override
State<MyRobotPage> createState() => _MyRobotPageState();
}
class _MyRobotPageState extends State<MyRobotPage> {
List<RobotItem> _list = [];
bool _loading = true;
bool _loadingMore = false;
bool _hasMore = true;
int size = 20; // 每页数量减少以便测试分页
int current = 1;
final ScrollController _scrollController = ScrollController();
final TextEditingController _controller = TextEditingController();
bool _hasFollowed = false;
@override
void initState() {
super.initState();
fetchRobotList(current, isRefresh: false);
// 添加滚动监听器,实现上拉加载更多
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
if (_hasMore && !_loadingMore) {
loadMore();
}
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
// 下拉刷新
Future<void> _onRefresh() async {
current = 1;
_hasMore = true;
await fetchRobotList(current, isRefresh: true);
}
// 上拉加载更多
Future<void> loadMore() async {
if (_loadingMore || !_hasMore) return;
setState(() {
_loadingMore = true;
});
current++;
await fetchRobotList(current, isRefresh: false);
}
Future<void> fetchRobotList(int current, {bool isRefresh = false}) async {
try {
dynamic result = await HTTP
.create("${C.BASE_URL}/robot/list")
.setHeader(C.TOKEN)
.setParam({"size": size, "current": current, "private": true})
.setRequestType(RequestType.GET)
.execute();
if (result['code'] == 200) {
List<Map<String, dynamic>> listOfMap =
(result['data']['result'] as List).cast<Map<String, dynamic>>();
List<RobotItem> newItems =
listOfMap.map((e) => RobotItem.fromJson(e)).toList();
// 检查是否还有更多数据
bool hasMoreData = current < (result['data']['pages'] ?? current);
setState(() {
if (isRefresh) {
_list = newItems;
} else {
_list.addAll(newItems);
}
_hasMore = hasMoreData;
_loading = false;
_loadingMore = false;
});
} else {
setState(() {
_loading = false;
_loadingMore = false;
if (isRefresh) {
_hasMore = true;
}
});
}
} catch (e) {
setState(() {
_loading = false;
_loadingMore = false;
if (isRefresh) {
_hasMore = true;
}
});
}
}
Future<void> handleFollow(int robotId, int index) async {
bool status = await followRobot(robotId,context);
if (status) {
setState(() {
_list[index] = _list[index].copyWith(follow: true);
_hasFollowed = true;
});
}
}
Future<void> onDelete(int robotId, int index) async {
bool status = await deleteRobot(robotId,context);
if (status) {
setState(() {
_list.removeAt(index);
_hasFollowed = true;
});
}
}
Future<void> onEditor(RobotItem item,int index) async {
RobotItem editorData = await Navigator.push(context, RouteAnimation(EditorRobotPage(robotItem: item), Offset(1,0)));
setState(() {
_list[index]=editorData;
});
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
return;
}
Navigator.pop(context, _hasFollowed);
},
child: Scaffold(
appBar: AppBar(
title: const Text("我的机器人", style: TextStyle(color: Colors.black)),
centerTitle: true,
backgroundColor: Colors.grey[100],
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context,_hasFollowed),
),
elevation: 0,
),
body: Column(
children: [
Expanded(
child: _loading
? ListView.builder(
itemCount: 10,
itemBuilder: (context, index) =>
const ShimmerRobotTile(),
)
: RefreshIndicator(
onRefresh: _onRefresh,
child: ClipRect(
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), // 关键
itemCount: _list.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _list.length && _hasMore) {
return _buildLoadMoreIndicator();
}
if (index >= _list.length) {
return const SizedBox();
}
final item = _list[index];
return RobotTile(
item: item,
onFollow: () => handleFollow(item.robotId, index),
onDelete: () => onDelete(item.robotId, index),
onEditor: () => onEditor(item, index),
);
},
),
),
)
,
),
],
),
));
}
// 加载更多指示器
Widget _buildLoadMoreIndicator() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: _loadingMore
? const CircularProgressIndicator()
: Text(_hasMore ? '上拉加载更多' : '没有更多数据了'),
),
);
}
}
/// 数据模型
class RobotItem {
final int robotId;
final String name;
final String avatar;
final String describe;
final String systemPrompt;
final bool isPrivate;
final bool follow;
RobotItem({
required this.robotId,
required this.name,
required this.avatar,
required this.describe,
required this.systemPrompt,
required this.isPrivate,
required this.follow,
});
factory RobotItem.fromJson(Map<String, dynamic> json) {
return RobotItem(
robotId: json['robotId'] ?? -1,
name: json['name'] ?? '',
avatar: json['avatar'] ?? '',
describe: json['describe'] ?? '',
systemPrompt: json['systemPrompt'] ?? '',
isPrivate: json['isPrivate'] ?? false,
follow: json['follow'] ?? false,
);
}
RobotItem copyWith({
int? robotId,
String? name,
String? avatar,
String? describe,
String? systemPrompt,
bool? isPrivate,
bool? follow,
}) {
return RobotItem(
robotId: robotId ?? this.robotId,
name: name ?? this.name,
avatar: avatar ?? this.avatar,
describe: describe ?? this.describe,
systemPrompt: describe ?? this.systemPrompt,
isPrivate: isPrivate ?? this.isPrivate,
follow: follow ?? this.follow,
);
}
}
/// 单个数据行
class RobotTile extends StatelessWidget {
final RobotItem item;
final VoidCallback onFollow;
final VoidCallback onDelete;
final VoidCallback onEditor;
const RobotTile({Key? key, required this.item, required this.onFollow, required this.onDelete, required this.onEditor})
: super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
child:Slidable(
key: ValueKey(item.robotId),
endActionPane: ActionPane(
motion: const DrawerMotion(),
extentRatio: 0.45,
children: [
SlidableAction(
onPressed: (ctx) {
if (onEditor != null) onEditor!();
},
backgroundColor: Colors.lightBlueAccent,
foregroundColor: Colors.white,
icon: Icons.mode_edit_sharp,
label: '修改',
spacing: 4,
borderRadius: BorderRadius.circular(5),
autoClose: true,
),
SlidableAction(
onPressed: (ctx) {
Remind.show(
context,
"提示",
"是否删除聊天,这是不可恢复的!!!",
onTap: (status) async {
if (status){
onDelete();
}
},
);
},
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
icon: Icons.delete_forever,
label: '删除',
spacing: 4,
borderRadius: BorderRadius.circular(5),
autoClose: true,
)
],
),
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
children: [
CacheAvatar(url:item.avatar,),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 5),
Text(item.describe,
style:
const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
const SizedBox(width: 5),
item.follow ? const Icon(Icons.favorite, color: Colors.redAccent) : GestureDetector(
onTap: onFollow,
child: const Icon(Icons.favorite_border,
color: Colors.grey),
),
],
),
),
)),
);
}
}
Future<bool> followRobot(int robotId,BuildContext context) async {
LoadingOverlay.show(
context: context,
barrierColor: Colors.black54,
);
try{
dynamic result = await HTTP
.create("${C.BASE_URL}/robot/follow/$robotId")
.setHeader(C.TOKEN)
.setRequestType(RequestType.GET)
.execute();
LoadingOverlay.hide();
if (result["code"] == 200) {
return true;
}
}catch(e){
LoadingOverlay.hide();
}
return false;
}
Future<bool> deleteRobot(int robotId,BuildContext context) async {
LoadingOverlay.show(
context: context,
barrierColor: Colors.black54,
);
try{
dynamic result = await HTTP
.create("${C.BASE_URL}/robot/delete/$robotId")
.setHeader(C.TOKEN)
.setRequestType(RequestType.DELETE)
.execute();
LoadingOverlay.hide();
if (result["code"] == 200) {
FollowListPage.flushData!;
return true;
}
}catch(e){
LoadingOverlay.hide();
}
return false;
}
/// Shimmer 占位行(加载时显示)
class ShimmerRobotTile extends StatelessWidget {
const ShimmerRobotTile({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 16,
width: double.infinity,
color: Colors.white,
),
),
const SizedBox(height: 8),
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
height: 14,
width: MediaQuery.of(context).size.width * 0.5,
color: Colors.white,
),
),
],
),
),
],
),
);
}
}