flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user