11 changed files with 659 additions and 593 deletions
@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart'; |
||||
|
||||
enum ProblemBindStatus { |
||||
bound('绑定', Colors.green), |
||||
unbound('未绑定', Colors.red); |
||||
|
||||
final String displayName; |
||||
final Color displayColor; |
||||
|
||||
const ProblemBindStatus(this.displayName, this.displayColor); |
||||
} |
||||
@ -1,15 +1,20 @@
|
||||
import 'package:problem_check_system/app/features/problem/domain/entities/problem_bind_status.dart'; |
||||
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; |
||||
|
||||
class ProblemListItemEntity { |
||||
final String id; |
||||
final String description; |
||||
final String location; |
||||
final DateTime creationTime; |
||||
final ProblemEntity problemEntity; |
||||
final String enterpriseName; |
||||
|
||||
ProblemListItemEntity({ |
||||
required this.id, |
||||
required this.description, |
||||
required this.location, |
||||
required this.creationTime, |
||||
required this.problemEntity, |
||||
required this.enterpriseName, |
||||
}); |
||||
|
||||
ProblemBindStatus get boundStatus { |
||||
if (problemEntity.bindData == null || problemEntity.bindData!.isEmpty) { |
||||
return ProblemBindStatus.unbound; |
||||
} else { |
||||
return ProblemBindStatus.bound; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -1,8 +1,30 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/bindings/base_bindings.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; |
||||
|
||||
class ProblemUploadBinding extends Bindings { |
||||
class ProblemUploadBinding extends BaseBindings { |
||||
@override |
||||
void dependencies() { |
||||
// TODO: implement dependencies |
||||
void register1Services() { |
||||
// TODO: implement register1Services |
||||
} |
||||
|
||||
@override |
||||
void register2DataSource() { |
||||
// TODO: implement register2DataSource |
||||
} |
||||
|
||||
@override |
||||
void register3Repositories() { |
||||
// TODO: implement register3Repositories |
||||
} |
||||
|
||||
@override |
||||
void register4Usecases() { |
||||
// TODO: implement register4Usecases |
||||
} |
||||
|
||||
@override |
||||
void register5Controllers() { |
||||
Get.lazyPut(() => ProblemUploadController()); |
||||
} |
||||
} |
||||
|
||||
@ -1,97 +0,0 @@
|
||||
// presentation/states/problem_card_state.dart |
||||
import 'package:equatable/equatable.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; |
||||
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; |
||||
|
||||
class ProblemCardState extends Equatable { |
||||
final String problemId; // 用于点击事件导航 |
||||
final String? previewImageUrl; // [图片] 可空,因为问题可能没有图片 |
||||
final String description; // [描述] |
||||
final String enterpriseName; // [企业名称] |
||||
final String location; // [地点] |
||||
final String creationTime; // [创建时间] 已格式化好的字符串 |
||||
final StatusTagState uploadStatus; // [上传状态] 包含文本和颜色 |
||||
final StatusTagState bindingStatus; // [绑定状态] 包含文本和颜色 |
||||
|
||||
const ProblemCardState({ |
||||
required this.problemId, |
||||
this.previewImageUrl, |
||||
required this.description, |
||||
required this.enterpriseName, |
||||
required this.location, |
||||
required this.creationTime, |
||||
required this.uploadStatus, |
||||
required this.bindingStatus, |
||||
}); |
||||
|
||||
// [核心逻辑]:从领域实体转换的工厂构造函数 |
||||
factory ProblemCardState.fromEntities({ |
||||
required ProblemEntity problem, |
||||
required Enterprise enterprise, |
||||
}) { |
||||
// 1. 处理图片:取列表的第一张图作为预览,如果列表为空则为 null |
||||
final String? previewImageUrl = problem.imageUrls.isNotEmpty |
||||
? problem.imageUrls.first |
||||
: null; |
||||
|
||||
// 2. 格式化时间:将 DateTime 转换为用户友好的字符串 |
||||
final String formattedTime = |
||||
'${problem.creationTime.year}-${problem.creationTime.month.toString().padLeft(2, '0')}-${problem.creationTime.day.toString().padLeft(2, '0')}'; |
||||
|
||||
// 3. 转换上传状态:将领域的 SyncStatus 映射为UI的 StatusTagState |
||||
StatusTagState uploadStatusState; |
||||
switch (problem.syncStatus) { |
||||
case SyncStatus.synced: |
||||
uploadStatusState = StatusTagState(text: '已上传', color: Colors.green); |
||||
break; |
||||
case SyncStatus.pendingCreate: |
||||
case SyncStatus.pendingUpdate: |
||||
case SyncStatus.pendingDelete: |
||||
uploadStatusState = StatusTagState(text: '待上传', color: Colors.orange); |
||||
break; |
||||
case SyncStatus.untracked: |
||||
default: |
||||
uploadStatusState = StatusTagState(text: '未上传', color: Colors.grey); |
||||
break; |
||||
} |
||||
|
||||
// 4. 转换绑定状态:根据 bindData 是否有值来判断 |
||||
final StatusTagState bindingStatusState = |
||||
(problem.bindData != null && problem.bindData!.isNotEmpty) |
||||
? StatusTagState(text: '已绑定', color: Colors.blue) |
||||
: StatusTagState(text: '未绑定', color: Colors.red); |
||||
|
||||
return ProblemCardState( |
||||
problemId: problem.id, |
||||
previewImageUrl: previewImageUrl, |
||||
description: problem.description, |
||||
enterpriseName: enterprise.name, |
||||
location: problem.location, |
||||
creationTime: formattedTime, |
||||
uploadStatus: uploadStatusState, |
||||
bindingStatus: bindingStatusState, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
List<Object?> get props => [ |
||||
problemId, |
||||
previewImageUrl, |
||||
description, |
||||
enterpriseName, |
||||
location, |
||||
creationTime, |
||||
uploadStatus, |
||||
bindingStatus, |
||||
]; |
||||
} |
||||
|
||||
// 一个通用的状态标签模型,包含文本和颜色 |
||||
class StatusTagState { |
||||
final String text; |
||||
final Color color; |
||||
|
||||
StatusTagState({required this.text, required this.color}); |
||||
} |
||||
@ -1,13 +0,0 @@
|
||||
class ProblemFormModel { |
||||
final String enterpriseName; |
||||
final String description; |
||||
final String location; |
||||
final List<String> imageUrls; |
||||
|
||||
ProblemFormModel({ |
||||
required this.enterpriseName, |
||||
required this.description, |
||||
required this.location, |
||||
required this.imageUrls, |
||||
}); |
||||
} |
||||
@ -1,248 +1,240 @@
|
||||
import 'dart:io'; |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:intl/intl.dart'; |
||||
import 'package:problem_check_system/app/core/routes/app_routes.dart'; |
||||
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/custom_button.dart'; |
||||
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; |
||||
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart'; |
||||
import 'package:tdesign_flutter/tdesign_flutter.dart'; |
||||
import 'dart:io'; // 添加文件操作支持 |
||||
|
||||
// 定义枚举类型 |
||||
enum ProblemCardViewType { buttons, checkbox } |
||||
|
||||
class ProblemCard extends StatelessWidget { |
||||
final Problem problem; |
||||
final ProblemCardViewType viewType; |
||||
final Function(Problem, bool)? onChanged; |
||||
final Function()? onTap; |
||||
final bool isSelected; // 改为必需参数 |
||||
final ProblemListItemEntity problemListItem; |
||||
final bool isSelected; |
||||
final VoidCallback? onTap; |
||||
final Widget? actions; |
||||
|
||||
const ProblemCard({ |
||||
super.key, |
||||
required this.problem, |
||||
this.viewType = ProblemCardViewType.buttons, |
||||
this.onChanged, |
||||
required this.problemListItem, |
||||
this.isSelected = false, |
||||
this.onTap, |
||||
required this.isSelected, // 改为必需参数 |
||||
this.actions, |
||||
}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
// 根据是否已删除决定卡片的颜色 |
||||
final bool isDeleted = |
||||
problem.syncStatus == ProblemSyncStatus.pendingDelete; |
||||
problemListItem.problemEntity.syncStatus == SyncStatus.pendingDelete; |
||||
final Color cardColor = isDeleted |
||||
? Colors.grey[300]! |
||||
? Colors.grey[200]! // 使用更浅的灰色以保证内容可读 |
||||
: Theme.of(context).cardColor; |
||||
final Color contentColor = isDeleted |
||||
? Colors.grey[600]! |
||||
: Theme.of(context).textTheme.bodyMedium!.color!; |
||||
|
||||
return Card( |
||||
elevation: isSelected ? 4.0 : 0.2, |
||||
shape: RoundedRectangleBorder( |
||||
borderRadius: BorderRadius.circular(12.r), |
||||
side: isSelected |
||||
? BorderSide(color: Theme.of(context).primaryColor, width: 1.5.w) |
||||
: BorderSide(color: Colors.grey.shade300, width: 1.w), |
||||
), |
||||
clipBehavior: Clip.antiAlias, |
||||
margin: EdgeInsets.zero, |
||||
color: cardColor, |
||||
child: InkWell( |
||||
onTap: viewType == ProblemCardViewType.checkbox |
||||
? () { |
||||
onChanged?.call(problem, !isSelected); |
||||
} |
||||
: null, |
||||
onTap: onTap, |
||||
// FIX 5: 添加了整体的内边距 |
||||
child: Column( |
||||
children: [ |
||||
Padding( |
||||
padding: EdgeInsets.all(12.r), |
||||
child: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: <Widget>[ |
||||
ListTile( |
||||
leading: _buildImageWidget(isDeleted), // 使用新的图片构建方法 |
||||
title: Text( |
||||
'问题描述', |
||||
style: TextStyle(fontSize: 16.sp, color: contentColor), |
||||
), |
||||
subtitle: LayoutBuilder( |
||||
builder: (context, constraints) { |
||||
return Text( |
||||
problem.description, |
||||
maxLines: 2, |
||||
overflow: TextOverflow.ellipsis, |
||||
style: TextStyle(fontSize: 14.sp, color: contentColor), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
SizedBox(height: 8.h), |
||||
Row( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
SizedBox(width: 16.w), |
||||
Icon(Icons.location_on, color: contentColor, size: 16.h), |
||||
SizedBox(width: 8.w), |
||||
SizedBox( |
||||
width: 100.w, |
||||
child: Text( |
||||
problem.location, |
||||
style: TextStyle(fontSize: 12.sp, color: contentColor), |
||||
overflow: TextOverflow.ellipsis, |
||||
maxLines: 1, |
||||
), |
||||
), |
||||
SizedBox(width: 16.w), |
||||
Icon(Icons.access_time, color: contentColor, size: 16.h), |
||||
SizedBox(width: 8.w), |
||||
_buildImageWidget(isDeleted), |
||||
SizedBox(width: 12.w), // FIX 7: 统一单位 |
||||
// 使用 Expanded 确保文字部分能自适应宽度 |
||||
Expanded( |
||||
child: Text( |
||||
DateFormat( |
||||
'yyyy-MM-dd HH:mm:ss', |
||||
).format(problem.creationTime), |
||||
style: TextStyle(fontSize: 12.sp, color: contentColor), |
||||
child: _buildDescriptionAndCompany(contentColor), |
||||
), |
||||
], |
||||
), |
||||
SizedBox(height: 12.h), |
||||
// const Divider(height: 1, color: Color(0xFFEEEEEE)), |
||||
// SizedBox(height: 10.h), |
||||
_buildLocationAndTime(contentColor), |
||||
], |
||||
), |
||||
SizedBox(height: 8.h), |
||||
Row( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: <Widget>[ |
||||
SizedBox(width: 16.w), |
||||
Wrap( |
||||
spacing: 8, |
||||
children: [ |
||||
...problem.syncStatus == ProblemSyncStatus.pendingDelete |
||||
? [ |
||||
TDTag( |
||||
'服务器未删除', |
||||
isLight: true, |
||||
theme: TDTagTheme.defaultTheme, |
||||
textColor: isDeleted ? Colors.grey[700] : null, |
||||
// backgroundColor: isDeleted |
||||
// ? Colors.grey[400] |
||||
// : null, |
||||
), |
||||
] |
||||
: [ |
||||
problem.syncStatus == ProblemSyncStatus.synced |
||||
? TDTag( |
||||
'已上传', |
||||
isLight: true, |
||||
theme: TDTagTheme.success, |
||||
) |
||||
: TDTag( |
||||
'未上传', |
||||
isLight: true, |
||||
theme: TDTagTheme.danger, |
||||
), |
||||
problem.bindData != null && |
||||
problem.bindData!.isNotEmpty |
||||
? TDTag( |
||||
'已绑定', |
||||
isLight: true, |
||||
theme: TDTagTheme.primary, |
||||
) |
||||
: TDTag( |
||||
'未绑定', |
||||
isLight: true, |
||||
theme: TDTagTheme.warning, |
||||
), |
||||
// 如果有 actions,才显示分割线和底部行 |
||||
if (actions != null) ...[ |
||||
// const Divider(height: 1, color: Color(0xFFEEEEEE)), |
||||
// SizedBox(height: 10.h), |
||||
_buildBottomActionRow(), |
||||
], |
||||
], |
||||
), |
||||
const Spacer(), |
||||
_buildBottomActions(isDeleted), |
||||
], |
||||
), |
||||
SizedBox(height: 8.h), |
||||
], |
||||
); |
||||
} |
||||
|
||||
Widget _buildDescriptionAndCompany(Color contentColor) { |
||||
return Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
Text( |
||||
'问题描述', |
||||
style: TextStyle(color: Colors.grey, fontSize: 9.sp), |
||||
), |
||||
SizedBox(height: 2.h), |
||||
// FIX 3: 使用动态数据 |
||||
Text( |
||||
problemListItem.problemEntity.description, |
||||
style: TextStyle( |
||||
fontSize: 12.5.sp, |
||||
color: contentColor, |
||||
fontWeight: FontWeight.w500, |
||||
), |
||||
maxLines: 1, |
||||
overflow: TextOverflow.ellipsis, |
||||
), |
||||
Text( |
||||
'企业名称', |
||||
style: TextStyle(color: Colors.grey, fontSize: 9.sp), |
||||
), |
||||
SizedBox(height: 2.h), |
||||
// FIX 3: 使用动态数据 |
||||
Text( |
||||
problemListItem.enterpriseName, |
||||
style: TextStyle( |
||||
fontSize: 12.5.sp, |
||||
color: contentColor, |
||||
fontWeight: FontWeight.bold, |
||||
), |
||||
maxLines: 1, |
||||
overflow: TextOverflow.ellipsis, |
||||
), |
||||
], |
||||
); |
||||
} |
||||
|
||||
Widget _buildImageWidget(bool isDeleted) { |
||||
return AspectRatio( |
||||
aspectRatio: 1, // 强制正方形 |
||||
return SizedBox( |
||||
width: 64.w, |
||||
height: 64.w, |
||||
child: ClipRRect( |
||||
borderRadius: BorderRadius.circular(8.r), |
||||
child: _buildImageContent(isDeleted), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildImageContent(bool isDeleted) { |
||||
// 检查是否有图片路径 |
||||
if (problem.imageUrls.isEmpty || problem.imageUrls[0].localPath.isEmpty) { |
||||
// 如果没有图片,显示默认占位图 |
||||
return Image.asset( |
||||
'assets/images/problem_preview.png', |
||||
fit: BoxFit.cover, // 使用 cover 来填充正方形区域 |
||||
color: isDeleted ? Colors.grey[500] : null, |
||||
colorBlendMode: isDeleted ? BlendMode.saturation : null, |
||||
); |
||||
} |
||||
// FIX 1: 安全地访问图片列表 |
||||
final String? imagePath = problemListItem.problemEntity.imageUrls.isNotEmpty |
||||
? problemListItem.problemEntity.imageUrls.first |
||||
: null; |
||||
|
||||
final String imagePath = problem.imageUrls[0].localPath; |
||||
|
||||
// 检查文件是否存在 |
||||
final File imageFile = File(imagePath); |
||||
if (!imageFile.existsSync()) { |
||||
// 如果文件不存在,显示默认占位图 |
||||
return Image.asset( |
||||
final placeholder = Image.asset( |
||||
'assets/images/problem_preview.png', |
||||
fit: BoxFit.cover, |
||||
color: isDeleted ? Colors.grey[500] : null, |
||||
colorBlendMode: isDeleted ? BlendMode.saturation : null, |
||||
); |
||||
|
||||
if (imagePath == null || imagePath.isEmpty) { |
||||
return placeholder; |
||||
} |
||||
|
||||
// 如果文件存在,使用 Image.file 加载 |
||||
// BEST PRACTICE 1: 移除 existsSync() |
||||
return Image.file( |
||||
imageFile, |
||||
fit: BoxFit.cover, // 使用 cover 来填充正方形区域 |
||||
color: isDeleted ? Colors.grey[500] : null, |
||||
colorBlendMode: isDeleted ? BlendMode.saturation : null, |
||||
errorBuilder: (context, error, stackTrace) { |
||||
// 如果加载失败,显示默认图片 |
||||
return Image.asset( |
||||
'assets/images/problem_preview.png', |
||||
File(imagePath), |
||||
fit: BoxFit.cover, |
||||
color: isDeleted ? Colors.grey[500] : null, |
||||
colorBlendMode: isDeleted ? BlendMode.saturation : null, |
||||
); |
||||
errorBuilder: (context, error, stackTrace) { |
||||
// 如果加载失败(包括文件不存在),显示默认图片 |
||||
return placeholder; |
||||
}, |
||||
); |
||||
} |
||||
|
||||
Widget _buildBottomActions(bool isDeleted) { |
||||
switch (viewType) { |
||||
case ProblemCardViewType.buttons: |
||||
Widget _buildIconText(IconData icon, String text, Color color) { |
||||
return Row( |
||||
crossAxisAlignment: CrossAxisAlignment.center, |
||||
mainAxisSize: MainAxisSize.min, // 让 Row 只占据必要的宽度 |
||||
children: [ |
||||
if (!isDeleted) |
||||
CustomButton( |
||||
text: '修改', |
||||
onTap: () { |
||||
onTap; |
||||
// controller.toProblemFormPageAndRefresh(problem: problem); |
||||
}, |
||||
Icon(icon, color: color, size: 16.r), |
||||
SizedBox(width: 4.w), |
||||
Text( |
||||
text, |
||||
style: TextStyle(fontSize: 12.sp, color: color), |
||||
overflow: TextOverflow.ellipsis, |
||||
maxLines: 1, |
||||
), |
||||
if (!isDeleted) SizedBox(width: 8.w), |
||||
CustomButton( |
||||
text: '查看', |
||||
onTap: () { |
||||
Get.toNamed( |
||||
AppRoutes.problemForm, |
||||
arguments: problem, |
||||
parameters: {'isReadOnly': 'true'}, |
||||
], |
||||
); |
||||
}, |
||||
} |
||||
|
||||
Widget _buildLocationAndTime(Color contentColor) { |
||||
return Row( |
||||
crossAxisAlignment: CrossAxisAlignment.center, |
||||
children: [ |
||||
Flexible( |
||||
child: _buildIconText( |
||||
Icons.location_on_outlined, |
||||
problemListItem.problemEntity.location, |
||||
contentColor, |
||||
), |
||||
), |
||||
SizedBox(width: 12.w), |
||||
_buildIconText( |
||||
Icons.access_time_outlined, |
||||
problemListItem.problemEntity.creationTime.toDateTimeString(), |
||||
contentColor, |
||||
), |
||||
SizedBox(width: 16.w), |
||||
], |
||||
); |
||||
case ProblemCardViewType.checkbox: |
||||
return Padding( |
||||
padding: EdgeInsets.only(right: 16.w), |
||||
child: Checkbox( |
||||
value: isSelected, |
||||
onChanged: (bool? value) { |
||||
if (value != null) { |
||||
onChanged?.call(problem, value); |
||||
} |
||||
}, |
||||
|
||||
Widget _buildBottomActionRow() { |
||||
return Row( |
||||
crossAxisAlignment: CrossAxisAlignment.end, |
||||
children: [ |
||||
Padding( |
||||
padding: EdgeInsets.only(left: 12.r, bottom: 12.r), |
||||
child: Row( |
||||
children: [ |
||||
TDTag( |
||||
problemListItem.problemEntity.syncStatus.displayName, |
||||
textColor: |
||||
problemListItem.problemEntity.syncStatus.displayColor, |
||||
backgroundColor: problemListItem |
||||
.problemEntity |
||||
.syncStatus |
||||
.displayColor |
||||
.withAlpha(20), |
||||
), |
||||
SizedBox(width: 8.w), |
||||
TDTag( |
||||
problemListItem.boundStatus.displayName, |
||||
textColor: problemListItem.boundStatus.displayColor, |
||||
backgroundColor: problemListItem.boundStatus.displayColor |
||||
.withAlpha(20), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
|
||||
const Spacer(), |
||||
// FIX 2: 安全地处理 actions |
||||
if (actions != null) actions!, |
||||
], |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue