flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/// ============================================================
|
||||
/// 【PostModel】数据模型 —— JSON ↔ 强类型对象的桥梁
|
||||
/// TS:接口返回就是 JS 对象,interface 只做类型提示,不用转换;
|
||||
// Flutter:接口返回是通用 Map 字典,**必须通过 fromJson 把 Map 转成 Dart 类实例,才能拥有强类型、点访问、编译检查**。
|
||||
///
|
||||
/// 一、为什么必须有 fromJson?
|
||||
/// Dio 只会把响应 JSON 解码成 Map/List,如果页面里到处写
|
||||
/// data['title'],容易拼错、无类型提示、改字段要全局搜。
|
||||
/// 模型类把 JSON 映射成强类型对象,编译期就能发现错误。
|
||||
///
|
||||
/// 二、高频坑:
|
||||
/// 1. 服务端数字类型可能不稳定(int 变 double),用
|
||||
/// (json['id'] as num?)?.toInt() 做防御性转换;
|
||||
/// 2. 字段缺失会直接抛异常,生产环境给 ?? 兜底值;
|
||||
/// 3. fromJson(响应→对象)和 toJson(对象→提交)必须成对维护。
|
||||
/// ============================================================
|
||||
class PostModel {
|
||||
const PostModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.title,
|
||||
required this.body,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int userId;
|
||||
final String title;
|
||||
final String body;
|
||||
|
||||
/// JSON → 对象(Dio 已把 JSON 字符串解码成 Map)
|
||||
factory PostModel.fromJson(Map<String, dynamic> json) {
|
||||
return PostModel(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
userId: (json['userId'] as num?)?.toInt() ?? 0,
|
||||
title: json['title'] as String? ?? '',
|
||||
body: json['body'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 对象 → JSON(用于 POST 提交给服务器)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'userId': userId, 'title': title, 'body': body};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/// ============================================================
|
||||
/// 【UserModel】用户相关模型
|
||||
///
|
||||
/// CurrentUserExtension:GET /api/v1/User/GetCurrentUserExtension 的响应体,
|
||||
/// 对齐 Vue 端 api/user/type.ts 的 CurrentUserExtension 全部字段(17 个)。
|
||||
///
|
||||
/// 一、为什么字段全都要:
|
||||
/// 登录后要缓存这套信息(user_storage.dart 里存 JSON),
|
||||
/// "我的"页 / 任务页都可能用到,缺一个就得多调一次接口。
|
||||
///
|
||||
/// 二、高频坑:
|
||||
/// 1. fromJson 全部 ?? 兜底,服务端缺字段/类型漂移不会闪崩;
|
||||
/// 2. 数字统一 (json['x'] as num?)?.toInt(),防 int/double 不一致;
|
||||
/// 3. toJson 与 fromJson 必须成对,否则缓存/回填会丢字段。
|
||||
/// ============================================================
|
||||
class CurrentUserExtension {
|
||||
const CurrentUserExtension({
|
||||
this.userId = 0,
|
||||
this.userName = '',
|
||||
this.userPoints = 0.0,
|
||||
this.levelName = '',
|
||||
this.avatar = '',
|
||||
this.className = '',
|
||||
this.classId = 0,
|
||||
this.gradeLevel = '',
|
||||
this.graduationYear = 0,
|
||||
this.targetConfigId = 0,
|
||||
this.targetConfigName = '',
|
||||
this.userAvatarStatus = 0,
|
||||
this.userAvatarReason = '',
|
||||
this.isShowGuide = false,
|
||||
this.isShowReport = false,
|
||||
this.id = 0,
|
||||
});
|
||||
|
||||
final int userId;
|
||||
final String userName;
|
||||
final double userPoints;
|
||||
final String levelName;
|
||||
final String avatar;
|
||||
final String className;
|
||||
final int classId;
|
||||
final String gradeLevel;
|
||||
final int graduationYear;
|
||||
final int targetConfigId;
|
||||
final String targetConfigName;
|
||||
final int userAvatarStatus;
|
||||
final String userAvatarReason;
|
||||
final bool isShowGuide;
|
||||
final bool isShowReport;
|
||||
final int id;
|
||||
|
||||
/// JSON → 对象(服务器响应)
|
||||
///
|
||||
/// 两个兼容点(对齐接口真实返回 {code, message, data: {...}}):
|
||||
/// 1. data 包装:data 存在就取 data,直接返回 {userId,...} 也支持;
|
||||
/// 2. 字符串数字:后端 userId/classId 返回的是字符串
|
||||
/// ("820466383020136"),不能直接 as num,用 _toInt 统一转换。
|
||||
factory CurrentUserExtension.fromJson(Map<String, dynamic> json) {
|
||||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||||
return CurrentUserExtension(
|
||||
userId: _toInt(payload['userId']),
|
||||
userName: payload['userName'] as String? ?? '',
|
||||
userPoints: _toDouble(payload['userPoints']),
|
||||
levelName: payload['levelName'] as String? ?? '',
|
||||
avatar: payload['avatar'] as String? ?? '',
|
||||
className: payload['className'] as String? ?? '',
|
||||
classId: _toInt(payload['classId']),
|
||||
gradeLevel: payload['gradeLevel'] as String? ?? '',
|
||||
graduationYear: _toInt(payload['graduationYear']),
|
||||
targetConfigId: _toInt(payload['targetConfigId']),
|
||||
targetConfigName: payload['targetConfigName'] as String? ?? '',
|
||||
userAvatarStatus: _toInt(payload['userAvatarStatus']),
|
||||
userAvatarReason: payload['userAvatarReason'] as String? ?? '',
|
||||
isShowGuide: payload['isShowGuide'] as bool? ?? false,
|
||||
isShowReport: payload['isShowReport'] as bool? ?? false,
|
||||
id: _toInt(payload['id']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 对象 → JSON(写入本地缓存)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'userName': userName,
|
||||
'userPoints': userPoints,
|
||||
'levelName': levelName,
|
||||
'avatar': avatar,
|
||||
'className': className,
|
||||
'classId': classId,
|
||||
'gradeLevel': gradeLevel,
|
||||
'graduationYear': graduationYear,
|
||||
'targetConfigId': targetConfigId,
|
||||
'targetConfigName': targetConfigName,
|
||||
'userAvatarStatus': userAvatarStatus,
|
||||
'userAvatarReason': userAvatarReason,
|
||||
'isShowGuide': isShowGuide,
|
||||
'isShowReport': isShowReport,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
/// 局部更新(目前只用得上 avatar:头像上传成功后替换 URL)
|
||||
CurrentUserExtension copyWith({String? avatar}) {
|
||||
return CurrentUserExtension(
|
||||
userId: userId,
|
||||
userName: userName,
|
||||
userPoints: userPoints,
|
||||
levelName: levelName,
|
||||
avatar: avatar ?? this.avatar,
|
||||
className: className,
|
||||
classId: classId,
|
||||
gradeLevel: gradeLevel,
|
||||
graduationYear: graduationYear,
|
||||
targetConfigId: targetConfigId,
|
||||
targetConfigName: targetConfigName,
|
||||
userAvatarStatus: userAvatarStatus,
|
||||
userAvatarReason: userAvatarReason,
|
||||
isShowGuide: isShowGuide,
|
||||
isShowReport: isShowReport,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户积分等级(GET /api/v1/User/GetUserPointLevel,备用)
|
||||
class UserPointLevel {
|
||||
const UserPointLevel({
|
||||
this.userId = 0,
|
||||
this.totalPoints = 0,
|
||||
this.level = 0,
|
||||
this.levelPoints = 0,
|
||||
this.levelMaxPoints = 0,
|
||||
this.levelUpPoints = 0,
|
||||
});
|
||||
|
||||
final int userId;
|
||||
final int totalPoints;
|
||||
final int level;
|
||||
final int levelPoints;
|
||||
final int levelMaxPoints;
|
||||
final int levelUpPoints;
|
||||
|
||||
factory UserPointLevel.fromJson(Map<String, dynamic> json) {
|
||||
return UserPointLevel(
|
||||
userId: (json['userId'] as num?)?.toInt() ?? 0,
|
||||
totalPoints: (json['totalPoints'] as num?)?.toInt() ?? 0,
|
||||
level: (json['level'] as num?)?.toInt() ?? 0,
|
||||
levelPoints: (json['levelPoints'] as num?)?.toInt() ?? 0,
|
||||
levelMaxPoints: (json['levelMaxPoints'] as num?)?.toInt() ?? 0,
|
||||
levelUpPoints: (json['levelUpPoints'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容"字符串数字"(后端 userId/classId 返回 "820466383020136")
|
||||
int _toInt(dynamic v) {
|
||||
if (v is num) return v.toInt();
|
||||
if (v is String) return int.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 兼容小数积分(后端 userPoints 返回 105.5)
|
||||
double _toDouble(dynamic v) {
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user