import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/api/class/api.dart'; import 'package:new_learn/core/api/class/model.dart'; import 'package:new_learn/core/components/user_circle_avatar.dart'; import 'package:new_learn/core/utils/img_url.dart'; /// ============================================================ /// 【AllStudentPage】「全部同学」页 —— 对标 Vue pages/classPackage/allStudent/index.vue /// /// 一、入口:班级动态页右上角「全部」按钮。 /// 二、数据:getClassUsers 一次性拉全员(含在线状态),非分页。 /// 三、布局:按屏宽自适应列数铺满(对标 Vue updateStudentLayout), /// 每格正方形,内部 头像 + 名字,头像右上角显示在线绿点。 /// 四、主题:整页渐变背景 + 透明导航栏(NavBar 叠在渐变上)。 /// ============================================================ class AllStudentPage extends StatefulWidget { const AllStudentPage({super.key}); @override State createState() => _AllStudentPageState(); } class _AllStudentPageState extends State { List _students = []; bool _loading = true; bool _failed = false; /// 手机端一行固定 4 个(对标 Vue 小屏下的 4 列网格布局) static const int _columnCount = 4; @override void initState() { super.initState(); _fetchStudents(); } Future _fetchStudents() async { setState(() { _loading = true; _failed = false; }); try { final users = await ClassApi.instance.getClassUsers(); if (!mounted) return; setState(() { _students = users; _loading = false; }); } catch (_) { if (!mounted) return; setState(() { _loading = false; _failed = true; }); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFEDF4FF), // 背景渐变与 AppBar 同色(EDF4FF),网格体置于 AppBar 之下, // 不再 extendBodyBehindAppBar,避免内容被标题/返回键遮挡。 appBar: AppBar( backgroundColor: const Color(0xFFEDF4FF), elevation: 0, title: Text( '全部同学 (${_students.length})', style: const TextStyle(color: Color(0xFF1D2B47)), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2B47)), onPressed: () => context.canPop() ? context.pop() : context.go('/class'), ), ), body: Container( width: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFEDF4FF), Color(0xFFD1E3FE)], ), ), child: _buildBody(), ), ); } Widget _buildBody() { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_failed) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '加载失败,请检查网络', style: TextStyle(fontSize: 14.sp, color: const Color(0xFF888888)), ), SizedBox(height: 12.h), FilledButton(onPressed: _fetchStudents, child: const Text('重新加载')), ], ), ); } if (_students.isEmpty) { return const Center( child: Text('暂无同学', style: TextStyle(color: Color(0xFF888888))), ); } return GridView.builder( padding: EdgeInsets.fromLTRB(14.w, 20.h, 14.w, 20.h), physics: const AlwaysScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: _columnCount, crossAxisSpacing: 9.w, mainAxisSpacing: 10.h, childAspectRatio: 0.78, ), itemCount: _students.length, itemBuilder: (context, index) => _StudentItem(user: _students[index]), ); } } /// 单个同学格:在线点 + 头像 + 名字 class _StudentItem extends StatelessWidget { const _StudentItem({required this.user}); final ClassUser user; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(8.r), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 头像 + 右上角在线点 Stack( clipBehavior: Clip.none, children: [ UserCircleAvatar(url: _avatarUrl(), radius: 17.5.w), if (user.isOnline) Positioned( right: -2.w, top: -2.w, child: Container( width: 7.w, height: 7.w, decoration: const BoxDecoration( color: Color(0x8000CC96), shape: BoxShape.circle, ), ), ), ], ), SizedBox(height: 7.h), Padding( padding: EdgeInsets.symmetric(horizontal: 2.w), child: Text( user.userName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2B47)), ), ), ], ), ); } String _avatarUrl() { final url = ImgUrl.build(user.avatar); return url.isEmpty ? ImgUrl.build('/StaticResource/2026-04-03/fake-avatar.png') : url; } }