import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:oktoast/oktoast.dart'; import '../../plugin/C.dart'; import '../../plugin/CacheAavatar.dart'; import '../../plugin/HTTP.dart'; import '../../plugin/LoadingOverlay.dart'; import '../../plugin/RouteAnimation.dart'; import 'RobotProfilePage.dart'; class RobotSquarePage extends StatefulWidget { const RobotSquarePage({Key? key}) : super(key: key); @override State createState() => _RobotSquarePageState(); } class _RobotSquarePageState 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 { setState(() { _loading = true; current = 1; _list = []; }); 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": false}) .setRequestType(RequestType.GET) .execute(); log(result.toString()); 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; }); } } @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), ); }, ), ), ), ) ], ), ), ); } // 加载更多指示器 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 creatorUsername; final String template; final String systemPrompt; final bool follow; RobotItem({ required this.robotId, required this.name, required this.avatar, required this.describe, required this.creatorUsername, required this.template, required this.systemPrompt, required this.follow, }); factory RobotItem.fromJson(Map json) { return RobotItem( robotId: json['robotId'] ?? -1, name: json['name'] ?? '', avatar: json['avatar'] ?? '', describe: json['describe'] ?? '', creatorUsername: json['creatorUsername'] ?? '', template: json['template'] ?? '', systemPrompt: json['systemPrompt'] ?? '', follow: json['follow'] ?? false, ); } RobotItem copyWith({ int? robotId, String? name, String? avatar, String? describe, bool? follow, }) { return RobotItem( robotId: robotId ?? this.robotId, name: name ?? this.name, avatar: avatar ?? this.avatar, describe: describe ?? this.describe, systemPrompt: describe ?? this.systemPrompt, creatorUsername: describe ?? this.creatorUsername, template: describe ?? this.template, follow: follow ?? this.follow, ); } } /// 单个数据行 class RobotTile extends StatelessWidget { final RobotItem item; final VoidCallback onFollow; const RobotTile({Key? key, required this.item, required this.onFollow}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), child: GestureDetector(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: [ Row(children: [ Text( item.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), Text(" (${item.template})", style: const TextStyle( fontSize: 12, color: Colors.grey ), ) ],), const SizedBox(height: 5), Text( item.creatorUsername, style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), item.follow ? const Icon(Icons.favorite, color: Colors.redAccent) : GestureDetector( onTap: onFollow, child: const Icon( Icons.favorite_border, color: Colors.grey, ), ), ], ), ), ),onTap: (){ Navigator.push( context, RouteAnimation( RobotProfilePage( robotId: item.robotId, name: item.name, avatar: item.avatar, systemPrompt: item.systemPrompt, creatorUsername: item.creatorUsername, template: item.template, description: item.describe, followList: false, ), Offset(1, 0), ), ); },), ); } } 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; } /// 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, ), ), ], ), ), ], ), ); } }