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 createState() => _MyRobotPageState(); } class _MyRobotPageState extends State { List _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 _onRefresh() async { current = 1; _hasMore = true; await fetchRobotList(current, isRefresh: true); } // 上拉加载更多 Future loadMore() async { if (_loadingMore || !_hasMore) return; setState(() { _loadingMore = true; }); current++; await fetchRobotList(current, isRefresh: false); } Future 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> listOfMap = (result['data']['result'] as List).cast>(); List 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 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 onDelete(int robotId, int index) async { bool status = await deleteRobot(robotId,context); if (status) { setState(() { _list.removeAt(index); _hasFollowed = true; }); } } Future 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 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 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 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, ), ), ], ), ), ], ), ); } }