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