464 lines
13 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|