Files
崔宝新 586c75f841 feat: 完成班级模块全量功能迭代
本次提交完成了班级模块的多项核心功能开发与优化:
1. 新增DELETE请求封装,支持业务删除接口且不自动重试
2. 完善UserPostCommonDto模型,新增头像字段适配详情接口
3. 重构路由架构,使用StatefulShellRoute实现Tab页面保活
4. 新增我的互动页面,包含我的动态/点赞/留言三个子页
5. 新增全部同学页面,展示班级全员信息与在线状态
6. 新增动态详情页面,支持查看完整评论与点赞头像
7. 抽离通用PostListBody组件,复用列表分页与交互逻辑
8. 优化班级列表页加载逻辑与图片预览交互
9. 新增评论删除、多图预览等细节交互功能
2026-09-07 09:20:06 +08:00

190 lines
5.8 KiB
Dart

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<AllStudentPage> createState() => _AllStudentPageState();
}
class _AllStudentPageState extends State<AllStudentPage> {
List<ClassUser> _students = [];
bool _loading = true;
bool _failed = false;
/// 手机端一行固定 4 个(对标 Vue 小屏下的 4 列网格布局)
static const int _columnCount = 4;
@override
void initState() {
super.initState();
_fetchStudents();
}
Future<void> _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;
}
}