Files
App.K12.Student.Miniapp_Flu…/lib/features/my/my_point/my_point_page.dart
T
2026-09-04 15:22:20 +08:00

231 lines
8.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:new_learn/core/api/my_point/api.dart';
import 'package:new_learn/core/api/my_point/model.dart';
import 'package:new_learn/core/logger/app_talker.dart';
import 'package:new_learn/core/providers/user_provider.dart';
import 'package:new_learn/core/components/common_paged_list.dart';
/// ============================================================
/// 【MyPointPage】我的积分页(点「我的」tab 的"我的积分"进入)
///
/// 一、数据来源(对齐 Vue3 端 api/userPoint/index.ts):
/// 1. 顶部积分卡:当前总积分来自 Riverpod 全局用户
/// currentUser.userPoints(登录时已拉取,无需再请求接口)
/// 2. 积分明细列表:GetUserPointRecordPagedList(游标分页,见下方)
///
/// 二、列表为什么用 CommonPagedList:
/// 积分记录可能几千条,必须懒加载(只渲染屏幕可见行)+ 上拉分页。
/// 组件基于 infinite_scroll_pagination 封装(Flutter Favorite 认证),
/// 已内置:下拉刷新 / 触底加载 / 防重入 / 失败重试 / 空状态 / 没有更多。
/// 页面只做两件事:load(调接口)+ buildItem(画每一行)。
///
/// 三、页码分页:
/// CommonPagedList 的 load 只给 pageKey(首页 1,之后 +1),业务把
/// pageKey 当 pageIndex 用。hasNext 用 total 判断:总条数 > 已请求条数。
/// 对应 Vue3 端 type.ts 的 UserPointRecordRequest{pageIndex,pageSize}。
///
/// 四、高频坑:
/// 1. 列表要包在 Expanded 里(Column 里无限高度会报 unbounded height);
/// 2. 积分正负号决定颜色:points>0 绿色加分,<0 橙色扣分;
/// 3. bizTime 后端直接给字符串,不需要自己格式化;
/// 4. go_router ShellRoute push 出来的页面不自动显示返回箭头
/// (issue #144687),必须手动加 leading,见 build 里 AppBar。
/// ============================================================
class MyPointPage extends ConsumerStatefulWidget {
const MyPointPage({super.key});
@override
ConsumerState<MyPointPage> createState() => _MyPointPageState();
}
class _MyPointPageState extends ConsumerState<MyPointPage> {
/// 每页条数(CommonPagedList 无 pageSize 概念,由业务在 load 里自定)
static const int _pageSize = 10;
@override
void initState() {
super.initState();
talker.debug('打开页面:我的积分');
}
@override
Widget build(BuildContext context) {
// 全局用户:当前总积分(登录后才有值;未登录 0)
final user = ref.watch(currentUserProvider).value;
return Scaffold(
appBar: AppBar(
title: const Text('我的积分'),
// go_router 已知 issue #144687:从 ShellRoute(4 tab)内 push 出来的
// 页面,AppBar 不自动显示返回箭头。这里手动加:
// 能 pop(正常从"我的"进入)→ pop 回上一页;
// 不能 pop(如 Web 深链/刷新直达)→ 兜底回"我的"tab。
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () =>
context.canPop() ? context.pop() : context.go('/my'),
),
),
body: Column(
children: [
// ---------- 顶部积分卡 ----------
_PointHeaderCard(totalPoints: user?.userPoints ?? 0),
// ---------- 积分明细列表(分页) ----------
Expanded(
child: CommonPagedList<UserPointRecord>(
load: (page) async {
talker.debug('加载积分记录:pageIndex=$page pageSize=$_pageSize');
// pageKey 当 pageIndex;CommonPagedList 从 1 开始递增
final res = await UserPointApi.instance.getRecordPage(
pageIndex: page,
pageSize: _pageSize,
);
return PagingResult<UserPointRecord>(
data: res.data,
hasNext: res.total > page * _pageSize,
);
},
buildItem: (context, item, index) => _RecordRow(record: item),
),
),
],
),
);
}
}
/// 顶部积分卡:当前总积分
class _PointHeaderCard extends StatelessWidget {
const _PointHeaderCard({required this.totalPoints});
final double totalPoints;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 12.h),
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 18.h),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFF9800), Color(0xFFFFB74D)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前积分',
style: TextStyle(fontSize: 13.sp, color: Colors.white.withValues(alpha: 0.9)),
),
SizedBox(height: 6.h),
Text(
// 积分可能是小数(接口返回 105.5)
totalPoints.toString(),
style: TextStyle(
fontSize: 34.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Text(
'积分明细 · 变动随时记录',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white70,
),
),
],
),
);
}
}
/// 单条积分记录行
class _RecordRow extends StatelessWidget {
const _RecordRow({required this.record});
final UserPointRecord record;
@override
Widget build(BuildContext context) {
// 正负号决定颜色:加分绿 / 扣分橙 / 0 灰
final isAdd = record.points > 0;
final isSub = record.points < 0;
final Color signColor = isAdd
? const Color(0xFF2E7D32)
: (isSub ? const Color(0xFFE65100) : const Color(0xFF9E9E9E));
final Color iconBg = isAdd
? const Color(0xFFE8F5E9)
: (isSub ? const Color(0xFFFFF3E0) : const Color(0xFFF5F5F5));
final String sign = isAdd ? '+' : (isSub ? '-' : '');
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: Row(
children: [
// 左侧圆形图标(加减符号)
Container(
width: 40.w,
height: 40.h,
decoration: BoxDecoration(
color: iconBg,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
sign,
style: TextStyle(
fontSize: 22.sp,
color: signColor,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 12.w),
// 中间:说明 + 时间
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record.pointMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF333333),
),
),
SizedBox(height: 4.h),
Text(
record.bizTime,
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF999999)),
),
],
),
),
SizedBox(width: 8.w),
// 右侧:积分变动值
Text(
'$sign${record.points.abs()}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: signColor,
),
),
],
),
);
}
}