feat(my): add 我的考试 page and related modules
1. 新增我的考试页面,包含学科筛选Tab和分页考试列表 2. 新增学科图标工具类,替换我的报告页重复的学科图标逻辑 3. 在我的页面菜单中添加我的考试入口 4. 新增exam模块的API接口和数据模型 5. 优化我的报告页面卡片样式,统一与考试卡片的布局
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:new_learn/core/api/my_report/model.dart';
|
||||
import 'package:new_learn/core/components/common_paged_list.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/utils/img_url.dart';
|
||||
import 'package:new_learn/core/utils/subject_icons.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【MyReportPage】我的报告列表 —— 对标 Vue pages/myPackage/myReport/myReport.vue
|
||||
@@ -25,19 +26,6 @@ import 'package:new_learn/core/utils/img_url.dart';
|
||||
/// 四、先只做 phone 布局,Pad 后续再说
|
||||
/// ============================================================
|
||||
|
||||
/// 学科 icon 映射表(对标 Vue utils/constant.js SUBJECT_ICON_MAP)
|
||||
const Map<String, String> _subjectIconMap = {
|
||||
'语文': '/StaticResource/2026-06-11/shuxue1781163770612.png',
|
||||
'化学': '/StaticResource/2026-06-11/huaxue1781163828443.png',
|
||||
'英语': '/StaticResource/2026-06-11/yingyu1781163725441.png',
|
||||
'物理': '/StaticResource/2026-06-11/wuli1781148438346.png',
|
||||
'数学': '/StaticResource/2026-06-11/shuxue1781163770612.png',
|
||||
'生物': '/StaticResource/2026-06-11/shengwu1781163791768.png',
|
||||
'历史': '/StaticResource/2026-06-11/lishi1781163811822.png',
|
||||
'政治': '/StaticResource/2026-06-11/zhengzhi1781163750422.png',
|
||||
'地理': '/StaticResource/2026-06-11/dili1781163844311.png',
|
||||
};
|
||||
|
||||
/// 取学科名(报告名前 2 个字,对标 Vue getSubjectName)
|
||||
String _extractSubjectName(UserReportDto item) {
|
||||
final name = item.reportName.trim();
|
||||
@@ -45,12 +33,6 @@ String _extractSubjectName(UserReportDto item) {
|
||||
return name.substring(0, 2);
|
||||
}
|
||||
|
||||
/// 取学科 icon URL(无匹配则用数学兜底,对标 Vue getSubjectIcon)
|
||||
String _getSubjectIconUrl(String subjectName) {
|
||||
final path = _subjectIconMap[subjectName] ?? _subjectIconMap['数学']!;
|
||||
return ImgUrl.build(path);
|
||||
}
|
||||
|
||||
/// 格式化创建时间(对标 Vue getReportTime)
|
||||
String _formatCreateTime(String raw) {
|
||||
if (raw.isEmpty) return '';
|
||||
@@ -120,8 +102,8 @@ class _MyReportPageState extends ConsumerState<MyReportPage> {
|
||||
color: const Color(0xFFF5F8FA),
|
||||
child: CommonPagedList<UserReportDto>(
|
||||
load: _loadPage,
|
||||
padding: EdgeInsets.fromLTRB(12.w, 16.h, 12.w, 0),
|
||||
separatorBuilder: (_, _) => SizedBox(height: 14.h),
|
||||
padding: EdgeInsets.fromLTRB(12.w, 4.h, 12.w, 0),
|
||||
// separatorBuilder: (_, _) => SizedBox(height: 14.h),
|
||||
buildItem: (context, item, index) {
|
||||
return _ReportCard(item: item, onTap: () => _onTapCard(item));
|
||||
},
|
||||
@@ -140,11 +122,11 @@ class _MyReportPageState extends ConsumerState<MyReportPage> {
|
||||
/// ============================================================
|
||||
/// 【_ReportCard】报告卡片
|
||||
///
|
||||
/// 布局(Row 从左到右):
|
||||
/// [icon + 学科标签] → [报告名 + 时间] → [箭头]
|
||||
/// 左侧装饰与我的考试(exam)卡片一致:
|
||||
/// 白色圆角卡片 + 左侧溢出书图(book_icon)+ 学科图标叠在书图上 +
|
||||
/// 底部学科名胶囊。书图/卡片尺寸统一用 .w 锁定宽高比,适配 pad。
|
||||
///
|
||||
/// 背景图叠底,InkWell 提供点击水波纹。
|
||||
/// 图标/标签自然靠左,不做偏移 hack。
|
||||
/// 内容区:中间 [报告名 + 时间],右侧 [箭头]。
|
||||
/// ============================================================
|
||||
class _ReportCard extends StatelessWidget {
|
||||
const _ReportCard({required this.item, required this.onTap});
|
||||
@@ -152,115 +134,177 @@ class _ReportCard extends StatelessWidget {
|
||||
final UserReportDto item;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static const String _cardBgBook =
|
||||
'/StaticResource/2026-09-10/book_icon_final1789026573675.png';
|
||||
|
||||
// 书图标 / 卡片尺寸(与 exam 卡片一致,统一 .w 锁比例)
|
||||
static const double _bookWidth = 124;
|
||||
static const double _bookHeight =
|
||||
124; // 卡片间距 = _bookHeight - _cardHeight(书图向上探出的高度)
|
||||
static const double _cardHeight = 92;
|
||||
static const double _cardRadius = 24;
|
||||
// ⚠ book_icon.png 自带透明 padding,偏移用于补偿留白
|
||||
static const double _bookOffsetLeft = -14;
|
||||
static const double _bookOffsetBottom = -10;
|
||||
static const double _bookExtraHeight = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subjectName = _extractSubjectName(item);
|
||||
final subjectIconUrl = _getSubjectIconUrl(subjectName);
|
||||
final iconUrl = subjectIconUrl(subjectName);
|
||||
final timeText = _formatCreateTime(item.createTime);
|
||||
|
||||
const bgPath =
|
||||
'/StaticResource/2026-06-10/examListItem_bg1781074465963.png';
|
||||
|
||||
return InkWell(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: Container(
|
||||
height: 109.h,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12.r)),
|
||||
child: SizedBox(
|
||||
height: (_bookHeight + _bookExtraHeight).w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 底层:卡片背景图
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build(bgPath),
|
||||
fit: BoxFit.fill,
|
||||
placeholder: (_, _) => Container(color: Colors.white),
|
||||
errorWidget: (_, _, _) => Container(color: Colors.white),
|
||||
// 底层:白色圆角卡片(底部对齐,书图只向上溢出)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: _cardHeight.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(_cardRadius.r),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0A1D2B47),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 上层内容:Row 左对齐,padding 控制整体边距
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
// 中层:左侧书形装饰图
|
||||
Positioned(
|
||||
left: _bookOffsetLeft.w,
|
||||
bottom: _bookOffsetBottom.w,
|
||||
width: _bookWidth.w,
|
||||
height: (_bookHeight + _bookExtraHeight).w,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build(_cardBgBook),
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.bottomLeft,
|
||||
errorWidget: (_, __, ___) =>
|
||||
SizedBox(width: _bookWidth.w, height: _bookHeight.w),
|
||||
),
|
||||
),
|
||||
// 学科小图标(叠在书图上半部分)
|
||||
Positioned(
|
||||
left: 22.w,
|
||||
top: 14.w,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: iconUrl,
|
||||
width: 56.w,
|
||||
height: 56.w,
|
||||
errorWidget: (_, _, _) => Icon(
|
||||
Icons.menu_book,
|
||||
size: 56.w,
|
||||
color: const Color(0xFF3D7DF5),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 上层:内容(与卡片同高,底部对齐)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: _cardHeight.w,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// ── 左侧:学科 icon + 标签(垂直排列,自然靠左) ──
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: subjectIconUrl,
|
||||
width: 56.w,
|
||||
height: 56.w,
|
||||
placeholder: (_, _) => const SizedBox.shrink(),
|
||||
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w),
|
||||
height: 25.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAF5FE),
|
||||
borderRadius: BorderRadius.circular(7.r),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
subjectName,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: const Color(0xFF3D7DF5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 40.w),
|
||||
// ── 中间:报告名 + 时间(Expanded 占剩余) ──
|
||||
// 左侧书图区域占位
|
||||
SizedBox(width: 90.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
item.reportName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.sp,
|
||||
color: const Color(0xFF1D2B47),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Text(
|
||||
timeText,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF687FAD),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ── 右侧:箭头 ──
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10.h),
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
color: const Color(0xFF4B6B99),
|
||||
size: 22.sp,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildMid(timeText)),
|
||||
_buildRight(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 学科标签(覆盖在书图底部胶囊上,卡片贴底)
|
||||
Positioned(
|
||||
left: -10.w,
|
||||
bottom: 10.w,
|
||||
width: _bookWidth.w,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14.w,
|
||||
vertical: 6.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F2FF),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Text(
|
||||
subjectName,
|
||||
style: TextStyle(
|
||||
fontSize: 16.spMin,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF3D7DF5),
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 中间:报告名 + 时间
|
||||
Widget _buildMid(String timeText) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.reportName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.spMin,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF1D2B47),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
timeText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.spMin,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF687FAD),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 右侧:箭头
|
||||
Widget _buildRight() {
|
||||
return Icon(
|
||||
Icons.chevron_right,
|
||||
color: const Color(0xFF4B6B99),
|
||||
size: 22.sp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user