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

416 lines
12 KiB
Dart

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<RobotSquarePage> createState() => _RobotSquarePageState();
}
class _RobotSquarePageState extends State<RobotSquarePage> {
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 {
setState(() {
_loading = true;
current = 1;
_list = [];
});
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": false})
.setRequestType(RequestType.GET)
.execute();
log(result.toString());
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;
});
}
}
@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<String, dynamic> 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<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;
}
/// 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,
),
),
],
),
),
],
),
);
}
}