合并代码
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import 'package:new_learn/core/api/my_report/model.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【my_report 模块接口】我的报告 —— 对标 Vue3 端 api/myReport/index.ts
|
||||
///
|
||||
/// getReportPage —— 报告列表分页(页码分页)
|
||||
/// GET /api/v1/UserReport/GetUserReportList
|
||||
/// 参数:pageIndex / pageSize / reportType(可选)
|
||||
/// ============================================================
|
||||
class UserReportApi {
|
||||
UserReportApi._();
|
||||
|
||||
static final UserReportApi instance = UserReportApi._();
|
||||
|
||||
/// 报告列表分页(页码分页:pageIndex / pageSize)
|
||||
/// [reportType] 可选筛选条件,Vue 端默认不传
|
||||
Future<UserReportPage> getReportPage({
|
||||
int pageIndex = 1,
|
||||
int pageSize = 10,
|
||||
int? reportType,
|
||||
}) async {
|
||||
final query = <String, dynamic>{
|
||||
'pageIndex': pageIndex,
|
||||
'pageSize': pageSize,
|
||||
};
|
||||
if (reportType != null) query['reportType'] = reportType;
|
||||
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/UserReport/GetUserReportList',
|
||||
query: query,
|
||||
);
|
||||
return UserReportPage.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/// ============================================================
|
||||
/// 【my_report 模块模型】我的报告 —— 对标 Vue3 端 api/myReport/type.ts
|
||||
///
|
||||
/// UserReportDto(单条报告)/ UserReportPage(分页响应)
|
||||
///
|
||||
/// 字段说明:
|
||||
/// id 报告 ID(后端大整数,返回 string 类型,前端也用 String 存)
|
||||
/// reportName 报告名称(如"数学分析报告")
|
||||
/// reportType 报告类型
|
||||
/// createTime 创建时间字符串(如 "2026-08-28 15:24:39" 或 ISO "T" 分隔)
|
||||
/// reportUrl 报告详情 H5 页面 URL(点击列表项后 WebView 加载这个)
|
||||
///
|
||||
/// 序列化:手动 fromJson,做了类型兼容(后端 id 有时 string 有时 number,
|
||||
/// 统一转 String;reportUrl 清理首尾空白和反引号)。
|
||||
/// 与 my_point/model.dart 保持一致,不依赖 build_runner。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
/// 辅助:id 字段兼容 string / num 两种后端返回,统一转 String
|
||||
String _asStringId(dynamic value) {
|
||||
if (value == null) return '';
|
||||
if (value is String) return value.trim();
|
||||
if (value is num) return value.toInt().toString();
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/// 辅助:清理 reportUrl 首尾可能的反引号 / 空白(后端可能包了一层引号)
|
||||
String _cleanUrl(String raw) {
|
||||
final cleaned = raw.trim();
|
||||
// 去掉首尾反引号(用户粘贴时可能加的,或后端真返回了)
|
||||
if (cleaned.startsWith('`') && cleaned.endsWith('`')) {
|
||||
return cleaned.substring(1, cleaned.length - 1).trim();
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/// 单条报告
|
||||
class UserReportDto {
|
||||
const UserReportDto({
|
||||
this.id = '',
|
||||
this.reportName = '',
|
||||
this.reportType = 0,
|
||||
this.createTime = '',
|
||||
this.reportUrl = '',
|
||||
});
|
||||
|
||||
final String id; // 后端大整数 ID,用 String 存避免精度丢失
|
||||
final String reportName;
|
||||
final int reportType;
|
||||
final String createTime;
|
||||
final String reportUrl;
|
||||
|
||||
factory UserReportDto.fromJson(Map<String, dynamic> json) {
|
||||
return UserReportDto(
|
||||
id: _asStringId(json['id']),
|
||||
reportName: (json['reportName'] as String?) ?? '',
|
||||
reportType: (json['reportType'] as num?)?.toInt() ?? 0,
|
||||
createTime: (json['createTime'] as String?) ?? '',
|
||||
reportUrl: _cleanUrl((json['reportUrl'] as String?) ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 报告分页响应
|
||||
class UserReportPage {
|
||||
const UserReportPage({
|
||||
this.data = const [],
|
||||
this.total = 0,
|
||||
this.pageIndex = 1,
|
||||
this.pageSize = 10,
|
||||
});
|
||||
|
||||
final List<UserReportDto> data;
|
||||
final int total;
|
||||
final int pageIndex;
|
||||
final int pageSize;
|
||||
|
||||
/// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data
|
||||
factory UserReportPage.fromJson(Map<String, dynamic> json) {
|
||||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||||
return UserReportPage(
|
||||
data: (payload['data'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(UserReportDto.fromJson)
|
||||
.toList(),
|
||||
total: (payload['total'] as num?)?.toInt() ?? 0,
|
||||
pageIndex: (payload['pageIndex'] as num?)?.toInt() ?? 1,
|
||||
pageSize: (payload['pageSize'] as num?)?.toInt() ?? 10,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【CenterToast】居中轻量提示 —— 对标 uni.showToast(icon: 'none')
|
||||
///
|
||||
/// 一、适用场景:
|
||||
/// 表单校验失败("请填写账号"/"请填写密码")、轻量成功提示等
|
||||
/// 不需要用户确认、1.2s 自动消失、不阻塞交互(IgnorePointer)。
|
||||
///
|
||||
/// 二、与 SnackBar / ConfirmBox 的分工:
|
||||
/// CenterToast → 短暂轻量提示,自动消失,中间弹出
|
||||
/// SnackBar → 底部提示(信息可能较长),可手动关闭
|
||||
/// ConfirmBox → 需要用户决策的确认框,双按钮
|
||||
///
|
||||
/// 三、用法:
|
||||
/// CenterToast.show(context, '请填写账号');
|
||||
/// ============================================================
|
||||
class CenterToast {
|
||||
CenterToast._();
|
||||
|
||||
static OverlayEntry? _current;
|
||||
|
||||
static void show(
|
||||
BuildContext context,
|
||||
String msg, {
|
||||
Duration duration = const Duration(milliseconds: 1200),
|
||||
}) {
|
||||
// 连续调用时先移除上一个,避免叠加
|
||||
_current?.remove();
|
||||
|
||||
late final OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (_) => Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.75),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Text(
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
_current = entry;
|
||||
Overlay.of(context).insert(entry);
|
||||
Future<void>.delayed(duration, () {
|
||||
if (entry.mounted && _current == entry) {
|
||||
entry.remove();
|
||||
_current = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:new_learn/core/utils/img_url.dart';
|
||||
import 'package:new_learn/core/utils/screen_adapt_template.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【ResolveProblemBox】攻克任务卡(对标 Vue ResolveProblem.vue)
|
||||
///
|
||||
/// 用于展示"刚刚攻克了 「xxx」 的所有高难考点"类提示。
|
||||
/// 由 class_update_widgets.dart 抽离,详情页也可复用。
|
||||
/// 背景图 + 云朵装饰走 CDN,与 Vue 版 $img('/StaticResource/...') 同源。
|
||||
/// ============================================================
|
||||
class ResolveProblemBox extends StatelessWidget {
|
||||
const ResolveProblemBox({
|
||||
super.key,
|
||||
this.title = '刚刚攻克了',
|
||||
required this.desc,
|
||||
});
|
||||
|
||||
/// 标题(默认 "刚刚攻克了")
|
||||
final String title;
|
||||
|
||||
/// 攻克的知识点描述(如 "二次函数最值问题")
|
||||
final String desc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPad = MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint;
|
||||
|
||||
final bgUrl = ImgUrl.build(
|
||||
isPad
|
||||
? '/StaticResource/2026-06-02/resolve-bg2-pad1780388446390.png'
|
||||
: '/StaticResource/2026-05-07/resolve-bg2.png',
|
||||
);
|
||||
final cloudUrl = ImgUrl.build(
|
||||
isPad
|
||||
? '/StaticResource/2026-06-02/cloud-pad1780388443051.png'
|
||||
: '/StaticResource/2026-05-07/cloud.png',
|
||||
);
|
||||
|
||||
final cloudHeight = isPad ? 107.h : 74.h;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(bgUrl),
|
||||
fit: BoxFit.fill,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
// 裁剪所有子元素到圆角内(对标 Vue overflow:hidden)
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
// Stack 默认不裁剪 Positioned 子元素,必须显式设置
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
// 底部云朵装饰(贴底部,z-index 1)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: cloudUrl,
|
||||
height: cloudHeight,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
// 文本层(padding 在云朵之上,z-index 2)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(19.w, 25.h, 19.w, 11.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF1D2B47),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3.h),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '「$desc」',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2B47),
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' 的所有高难考点!',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF45597E),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user