import 'package:elysia/page/child/EditorRobotPage.dart'; import 'package:elysia/page/child/RobotProfilePage.dart'; import 'package:elysia/page/child/RobotSquarePage.dart'; import 'package:elysia/plugin/HiverCache.dart'; import 'package:elysia/plugin/RouteAnimation.dart'; import 'package:elysia/plugin/Toast.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../bean/Mappable.dart'; import '../plugin/C.dart'; import '../plugin/CacheAavatar.dart'; import '../plugin/HTTP.dart'; import 'package:oktoast/oktoast.dart'; import '../plugin/LoadingOverlay.dart'; class FollowListPage extends StatefulWidget { const FollowListPage({Key? key}) : super(key: key); static Function(bool)? flushData; static Function(int)? delete; @override State createState() => _FollowListPageState(); } class _FollowListPageState extends State with AutomaticKeepAliveClientMixin { List _list = []; bool _loading = true; @override void initState() { super.initState(); FollowListPage.flushData = (status) { _list = []; _loading = true; fetchFollowList(status); }; FollowListPage.delete = (id) { _removeById(id); }; fetchFollowList(false); } Future fetchFollowList(bool status) async { try { _loading = false; List cacheId = []; List cacheData = await HiverCache.getCache( C.HIVE_CACHE_FETCH_FOLLOW, FollowItem.fromJson, ) ?? []; setState(() { _list = cacheData; }); if (_list.length == 0 || status) { _loading = true; } else { if (!status) { for (FollowItem followItem in cacheData) { cacheId.add(followItem.robotId); } } } dynamic result = await HTTP .create("${C.BASE_URL}/robot/follow-list") .setHeader(C.TOKEN) .setParam({"cacheId": cacheId}) .setRequestType(RequestType.GET) .execute(); if (result['code'] == 200) { setState(() { if (status) { _list = []; } //判定是否有要删除的ID List excludeIdList = result['data']["excludeIdList"] ?? []; if (excludeIdList != null || excludeIdList.length != 0) { for (dynamic id in excludeIdList) { int index = -1; for (int i = 0; i < _list.length; i++) { if (_list[i].robotId.toString() == id.toString()) { index = i; break; } } if (index != -1) { _list.removeAt(index); } } } List> listOfMap = (result['data']["data"] as List).cast>(); if (listOfMap.length != 0) { for (Map d in listOfMap) { _list.add(FollowItem.fromJson(d)); } } _loading = false; HiverCache.cache(C.HIVE_CACHE_FETCH_FOLLOW, _list); }); } else { setState(() => _loading = false); } } catch (e) { setState(() => _loading = false); } } void _removeAt(int index) { setState(() { _list.removeAt(index); HiverCache.cache(C.HIVE_CACHE_FETCH_FOLLOW, _list); }); } void _removeById(int id) { int index = -1; for (int i = 0; i < _list.length; i++) { if (_list[i].robotId == id) { index = i; break; } } if (index != -1) { _removeAt(index); } } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ SizedBox( child: Column( children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 5), child: GestureDetector( onTap: () async { bool status = await Navigator.push( context, RouteAnimation(RobotSquarePage(), Offset(1, 0)), ); if (status) { setState(() { _list = []; _loading = true; fetchFollowList(false); }); } }, child: Container( decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(5), ), child: Padding( padding: EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), child: Row( children: [ // Avatar(带圆角和 Shimmer) ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 48, height: 50, child: Container( decoration: BoxDecoration(color: Colors.blue), child: Icon(Icons.group, color: Colors.white), ), ), ), const SizedBox(width: 12), // 名称与其它信息(这里仅显示 name) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "机器人广场", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ], ), ), Expanded(child: SizedBox()), Icon(Icons.chevron_right, color: Colors.grey), ], ), ), ), ), ), SizedBox(height: 10), Divider(height: 1, thickness: 1, color: Colors.grey.shade300), ], ), ), SizedBox(height: 10), Expanded( child: _loading ? ListView.builder( itemCount: 10, itemBuilder: (context, index) => const ShimmerFollowTile(), ) : ClipRect( child: ListView.builder( itemCount: _list.length, itemBuilder: (context, index) { final item = _list[index]; return FollowTile( item: item, onDelete: () => unfollowRobot(index), onClick: () { Navigator.push( context, RouteAnimation( RobotProfilePage( robotId: item.robotId, name: item.name, avatar: item.avatar, systemPrompt: item.systemPrompt, creatorUsername: "开发中...", template: "开发中...", description: item.describe, followList: true, ), Offset(1, 0), ), ); }, ); }, ), ), ), ], ), ); } Future unfollowRobot(int index) async { LoadingOverlay.show(context: context, barrierColor: Colors.black54); try { dynamic result = await HTTP .create("${C.BASE_URL}/robot/unfollow/${_list[index].robotId}") .setHeader(C.TOKEN) .setRequestType(RequestType.GET) .execute(); LoadingOverlay.hide(); if (result["code"] == 200) { _removeAt(index); } } catch (e) { LoadingOverlay.hide(); } } @override bool get wantKeepAlive => true; } /// 数据模型 class FollowItem extends Mappable { final int robotId; final String name; final String avatar; // 头像 URL final bool creator; final String systemPrompt; final String describe; FollowItem({ required this.robotId, required this.name, required this.avatar, required this.systemPrompt, required this.creator, required this.describe, }); factory FollowItem.fromJson(Map json) { return FollowItem( robotId: json['robotId'] ?? -1, name: json['name'] ?? '', avatar: json['avatar'] ?? '', describe: json['describe'] ?? '', systemPrompt: json['systemPrompt'] ?? '', creator: json['creator'] == 1 || json['creator'] == true, ); } @override Map toMap() { return { "robotId": robotId, "name": name, "avatar": avatar, "describe": describe, "systemPrompt": systemPrompt, "creator": creator, }; } } /// 真正的数据行(支持 Slidable 删除) class FollowTile extends StatelessWidget { final FollowItem item; final VoidCallback? onDelete; final VoidCallback? onClick; const FollowTile({Key? key, required this.item, this.onDelete, this.onClick}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.symmetric(vertical: 2), child: Slidable( key: ValueKey(item.robotId), endActionPane: ActionPane( motion: const DrawerMotion(), extentRatio: 0.25, children: [ SlidableAction( onPressed: (ctx) { if (onDelete != null) onDelete!(); // ✅ 这里会正确调用 unfollowRobot }, backgroundColor: Colors.amber, foregroundColor: Colors.white, icon: Icons.heart_broken, label: '取消关注', spacing: 4, borderRadius: BorderRadius.circular(5), autoClose: true, ) ], ), child: GestureDetector( onTap: () { if (onClick != null) onClick!(); }, child: Stack( children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 5), child: Container( decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(5), ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Row( children: [ // Avatar(带圆角和 Shimmer) CacheAvatar(url: item.avatar), const SizedBox(width: 12), // 名称与其它信息(这里仅显示 name) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), SizedBox(height: 5), Text( item.describe, style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), ), ], ), ), ), ), if (item.creator) Positioned( right: 5, top: 0, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.orange, borderRadius: BorderRadius.only( topRight: Radius.circular(5), bottomLeft: Radius.circular(5), ), ), child: const Icon( Icons.person, // 人头图标 size: 14, color: Colors.white, ), ), ), ], ), ), ), ); } } /// Shimmer 占位行(加载时显示) class ShimmerFollowTile extends StatelessWidget { const ShimmerFollowTile({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, ), ), ], ), ), ], ), ); } } class ChatRoomDialog extends StatefulWidget { final List> options; // 下拉选择的数据 final Function(String roomName, String value) onConfirm; const ChatRoomDialog({ Key? key, required this.options, required this.onConfirm, }) : super(key: key); @override State createState() => _ChatRoomDialogState(); } class _ChatRoomDialogState extends State { final TextEditingController _roomController = TextEditingController(); String? _selectedValue; @override void initState() { super.initState(); if (widget.options.isNotEmpty) { _selectedValue = widget.options[0]['value']; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), elevation: 0, backgroundColor: Colors.transparent, insetPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 24), child: _dialogContent(context, theme, isDark), ); } Widget _dialogContent(BuildContext context, ThemeData theme, bool isDark) { return Container( padding: EdgeInsets.all(24), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 20, spreadRadius: 0, offset: Offset(0, 8), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title Text( "创建聊天室", style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: theme.colorScheme.onSurface, ), ), SizedBox(height: 8), Text( "设置您的聊天室名称并选择AI引擎", style: TextStyle( fontSize: 14, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), SizedBox(height: 24), // Chat room name field Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "聊天室名称", style: TextStyle( fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface.withOpacity(0.8), fontSize: 14, ), ), SizedBox(height: 8), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: theme.colorScheme.primary.withOpacity(0.1), blurRadius: 4, offset: Offset(0, 2), ), ], ), child: TextField( controller: _roomController, decoration: InputDecoration( hintText: "请输入聊天室名称", filled: true, fillColor: isDark ? theme.colorScheme.surfaceVariant : Colors.grey[50], contentPadding: EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( color: theme.colorScheme.primary, width: 1.5, ), ), ), ), ), ], ), SizedBox(height: 20), // Engine selection Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "选择引擎", style: TextStyle( fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface.withOpacity(0.8), fontSize: 14, ), ), SizedBox(height: 8), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: isDark ? theme.colorScheme.surfaceVariant : Colors.grey[50], boxShadow: [ BoxShadow( color: theme.colorScheme.primary.withOpacity(0.1), blurRadius: 4, offset: Offset(0, 2), ), ], ), padding: EdgeInsets.symmetric(horizontal: 12), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedValue, isExpanded: true, icon: Icon( Icons.keyboard_arrow_down_rounded, color: theme.colorScheme.onSurface.withOpacity(0.6), ), borderRadius: BorderRadius.circular(16), dropdownColor: isDark ? theme.colorScheme.surfaceVariant : Colors.white, style: TextStyle( color: theme.colorScheme.onSurface, fontSize: 16, ), items: widget.options.map((option) { return DropdownMenuItem( value: option['value'], child: Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text(option['key'] ?? ''), ), ); }).toList(), onChanged: (val) { setState(() { _selectedValue = val; }); }, selectedItemBuilder: (BuildContext context) { return widget.options.map((item) { return Align( alignment: Alignment.centerLeft, child: Text( item['key'] ?? '', style: TextStyle( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), ); }).toList(); }, ), ), ), ], ), SizedBox(height: 32), // Confirm button SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, elevation: 0, shadowColor: Colors.transparent, ), onPressed: () { if (_roomController.text.isEmpty || _selectedValue == null) { showToast("请填写完整信息", position: ToastPosition.center); return; } widget.onConfirm(_roomController.text, _selectedValue!); Navigator.of(context).pop(); }, child: Text( "创建聊天室", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16), ), ), ), ], ), ); } }