
本文详解如何基于 laravel 提供的 json api 响应,在 flutter 中构建清晰、响应式的“出入校打卡图”界面:按日期分组,左右并排显示签到/签退照片,支持缺省状态(如仅签到无签退时右侧留空)。
本文详解如何基于 laravel 提供的 json api 响应,在 flutter 中构建清晰、响应式的“出入校打卡图”界面:按日期分组,左右并排显示签到/签退照片,支持缺省状态(如仅签到无签退时右侧留空)。
要实现如图所示的“按日期分组、左右并排展示签到/签退照片”的 Flutter 界面(参考示意图:左侧为 check_in 图片,右侧为 check_out 图片),关键不在于后端逻辑重构,而在于前端数据建模 + 智能分组渲染。Laravel 当前返回的扁平化 JSON 已足够支撑该设计,无需修改 API 结构,只需在 Flutter 层完成数据聚合与 UI 映射。
✅ 第一步:定义 Dart 数据模型(推荐自动生成)
使用 quicktype.io 将你提供的 JSON 示例粘贴进去,选择 Dart 语言,可一键生成类型安全的模型类。以下是精简优化后的 CheckInOut 模型(含反序列化支持):
class CheckInOutResponse {
final List<checkinout> data;
CheckInOutResponse({required this.data});
factory CheckInOutResponse.fromJson(Map<string dynamic> json) {
return CheckInOutResponse(
data: List<checkinout>.from(
(json['data'] as List).map((e) => CheckInOut.fromJson(e)),
),
);
}
}
class CheckInOut {
final int id;
final int childId;
final String date; // "18-08-2022"
final String time;
final String status; // "check_in" | "check_out"
final String image;
CheckInOut({
required this.id,
required this.childId,
required this.date,
required this.time,
required this.status,
required this.image,
});
factory CheckInOut.fromJson(Map<string dynamic> json) {
return CheckInOut(
id: json['id'],
childId: json['child_id'],
date: json['date'],
time: json['time'],
status: json['status'],
image: json['image'],
);
}
}</string></checkinout></string></checkinout>
✅ 第二步:按日期分组,合并出入校记录
核心逻辑:将原始列表按 date 分组,每组最多包含 1 条 check_in 和 1 条 check_out 记录。推荐使用 package:collection 的 groupBy:
import 'package:collection/collection.dart';
Map<string map checkinout>> groupByDate(List<checkinout> list) {
return groupBy(list, (item) => item.date)
.map((date, items) {
final checkIn = items.firstWhere(
(e) => e.status == 'check_in',
orElse: () => CheckInOut.empty(), // 自定义空占位对象
);
final checkOut = items.firstWhere(
(e) => e.status == 'check_out',
orElse: () => CheckInOut.empty(),
);
return MapEntry(date, {'check_in': checkIn, 'check_out': checkOut});
});
}</checkinout></string>
补充空对象支持(避免空值异常):
extension CheckInOutExtension on CheckInOut {
static CheckInOut empty() => CheckInOut(
id: 0,
childId: 0,
date: '',
time: '',
status: '',
image: '', // 空字符串表示无图
);
}
✅ 第三步:构建双栏卡片 UI(Material Design 风格)
使用 ListView.builder 渲染分组数据,并为每个日期创建横向卡片:
Widget buildAttendanceCard(String date, Map<string checkinout> pair) {
final checkIn = pair['check_in']!;
final checkOut = pair['check_out']!;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 日期标题
Text(
date,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 12),
// 双图并排(Row + Expanded)
Row(
children: [
// 左侧:签到
Expanded(
flex: 1,
child: _buildImageSection(checkIn, '签到'),
),
const SizedBox(width: 12),
// 右侧:签退
Expanded(
flex: 1,
child: _buildImageSection(checkOut, '签退'),
),
],
),
],
),
),
);
}
Widget _buildImageSection(CheckInOut record, String label) {
final hasImage = record.image.isNotEmpty;
return Column(
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 8),
Container(
height: 120,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1),
borderRadius: BorderRadius.circular(8),
),
child: hasImage
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
record.image,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, color: Colors.grey),
),
)
: const Center(child: Icon(Icons.photo_camera_front_outlined, color: Colors.grey)),
),
const SizedBox(height: 4),
if (hasImage) Text(record.time, style: const TextStyle(fontSize: 12, color: Colors.blueGrey)),
],
);
}</string>
✅ 第四步:发起 API 请求(使用 http 包)
在 pubspec.yaml 中添加依赖:
dependencies: http: ^1.1.0 collection: ^1.17.0
请求示例(带简单错误处理):
Future<list>> fetchAttendance(int childId) async {
final url = Uri.parse('https://your-api.com/api/v1/check-in-out?child_id=$childId');
final response = await http.get(url);
if (response.statusCode == 200) {
final data = CheckInOutResponse.fromJson(jsonDecode(response.body));
return data.data;
} else {
throw Exception('Failed to load attendance: ${response.statusCode}');
}
}</list>
⚠️ 注意事项与最佳实践
-
空状态处理:务必对
image字段做空值/空字符串判断,避免Image.network报错;建议统一用record.image?.isNotEmpty ?? false。 -
图片加载体验:生产环境建议搭配
cached_network_image包实现缓存与占位图,提升滚动性能。 -
日期格式一致性:Laravel 返回
d-m-Y格式已适配展示需求;若需排序或本地化,可在 Dart 层用intl包解析为DateTime。 -
网络错误兜底:在 UI 层增加
FutureBuilder的ConnectionState.waiting/snapshot.hasError状态反馈(如加载指示器、重试按钮)。 -
安全性提醒:Laravel 接口必须启用认证(如 Sanctum 或 Passport),禁止未授权访问
child_id相关数据。
通过以上四步,你即可在 Flutter 中稳健实现目标 UI —— 不依赖后端结构调整,专注前端数据整合与声明式渲染,兼顾可读性、可维护性与用户体验。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











