一次大的提交

This commit is contained in:
2026-08-07 08:49:29 +08:00
parent bf2fc49c19
commit db70665ced
181 changed files with 23060 additions and 2804 deletions

View File

@@ -1,12 +1,35 @@
import '../models/workorder_model.dart';
/// 工单远程数据源抽象类
abstract class WorkOrderRemoteDataSource {
/// 获取工单列表
Future<List<WorkOrderModel>> getWorkOrderList({
String? status,
required int page,
required int pageSize,
int? siteId,
int? orgId,
});
/// 获取工单统计
Future<WorkOrderCountModel> getWorkOrderCount();
Future<WorkOrderModel> getWorkOrderDetail(
String orderId, {
int? siteId,
int? orgId,
});
Future<List<Map<String, dynamic>>> fetchUsers({
required int orgId,
required int siteId,
});
Future<void> dispatchWorkOrder({
required String orderId,
required String assigneeId,
required String assigneeName,
String? dispatchRemark,
String? collaboratorIds,
String? collaboratorNames,
String? planStartTime,
String? planEndTime,
String? deadlineTime,
});
}

View File

@@ -1,87 +1,145 @@
import 'package:dio/dio.dart';
import '../../../../../core/consts/http_api_consts.dart';
import '../../../../../core/consts/workorder_consts.dart';
import 'workorder_remote_datasource.dart';
import '../models/workorder_model.dart';
/// 工单远程数据源实现(模拟数据)
class WorkOrderRemoteDataSourceImpl implements WorkOrderRemoteDataSource {
WorkOrderRemoteDataSourceImpl(this._dio);
final Dio _dio;
@override
Future<List<WorkOrderModel>> getWorkOrderList({String? status}) async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 1));
Future<List<WorkOrderModel>> getWorkOrderList({
required int page,
required int pageSize,
int? siteId,
int? orgId,
}) async {
final queryParameters = {'page': page, 'pageSize': pageSize};
if (siteId != null) {
queryParameters['siteId'] = siteId;
}
if (orgId != null) {
queryParameters['orgId'] = orgId;
}
final response = await _dio.get(
HttpApiConsts.workOrderList,
queryParameters: queryParameters,
);
// 模拟数据
final allOrders = [
WorkOrderModel(
id: '1',
title: '逆变器通讯故障处理',
orderNo: 'WO-20250521001',
priority: WorkOrderPriority.high,
executor: null,
createTime: DateTime(2025, 5, 21, 10, 12),
completeTime: null,
location: 'A区 / INV-001',
status: WorkOrderStatus.pending,
progress: null,
),
WorkOrderModel(
id: '2',
title: '组件清洗作业',
orderNo: 'WO-20250521002',
priority: WorkOrderPriority.medium,
executor: '张工',
createTime: DateTime(2025, 5, 21, 9, 30),
completeTime: null,
location: 'B区',
status: WorkOrderStatus.executing,
progress: 60.0,
),
WorkOrderModel(
id: '3',
title: '汇流箱巡检',
orderNo: 'WO-20250520098',
priority: WorkOrderPriority.low,
executor: '李工',
createTime: DateTime(2025, 5, 20, 14, 20),
completeTime: DateTime(2025, 5, 20, 16, 32),
location: 'C区',
status: WorkOrderStatus.completed,
progress: 100.0,
),
];
// 根据状态筛选
if (status != null && status != 'all') {
return allOrders
.where((order) => _statusToString(order.status) == status)
.toList();
final responseData = response.data;
final int code = responseData['code'] ?? -1;
if (code != 0 && code != 200) {
throw Exception('获取工单列表失败: code=$code');
}
return allOrders;
final List<dynamic> rows = responseData['rows'] ?? [];
return rows
.map((item) => WorkOrderModel.fromJson(item as Map<String, dynamic>))
.toList();
}
@override
Future<WorkOrderCountModel> getWorkOrderCount() async {
// 模拟网络延迟
await Future.delayed(const Duration(milliseconds: 500));
// 模拟统计数据
return WorkOrderCountModel(
pendingCount: 12,
executingCount: 8,
todayCompletedCount: 18,
final response = await _dio.get(HttpApiConsts.workOrderCount);
final responseData = response.data;
final int code = responseData['code'] ?? -1;
if (code != 0 && code != 200) {
throw Exception('获取工单统计失败: code=$code');
}
return WorkOrderCountModel.fromJson(
responseData['data'] as Map<String, dynamic>,
);
}
String _statusToString(WorkOrderStatus status) {
switch (status) {
case WorkOrderStatus.pending:
return 'pending';
case WorkOrderStatus.executing:
return 'executing';
case WorkOrderStatus.completed:
return 'completed';
case WorkOrderStatus.all:
return 'all';
@override
Future<WorkOrderModel> getWorkOrderDetail(
String orderId, {
int? siteId,
int? orgId,
}) async {
final queryParameters = <String, dynamic>{};
if (siteId != null) {
queryParameters['siteId'] = siteId;
}
if (orgId != null) {
queryParameters['orgId'] = orgId;
}
final response = await _dio.get(
'${HttpApiConsts.workOrderDetail}/$orderId',
queryParameters: queryParameters,
);
final responseData = response.data;
final int code = responseData['code'] ?? -1;
if (code != 0 && code != 200) {
throw Exception('获取工单详情失败: code=$code');
}
final data = responseData['data'];
if (data == null) {
throw Exception('工单详情数据为空');
}
return WorkOrderModel.fromJson(data as Map<String, dynamic>);
}
@override
Future<List<Map<String, dynamic>>> fetchUsers({
required int orgId,
required int siteId,
}) async {
final response = await _dio.get(
HttpApiConsts.systemUserList,
queryParameters: {
'pageNum': 1,
'pageSize': 99999,
'orgId': orgId,
'siteId': siteId,
},
);
final responseData = response.data;
final int code = responseData['code'] ?? -1;
if (code != 0 && code != 200) {
throw Exception('获取人员列表失败: code=$code');
}
final List<dynamic> rows =
responseData['rows'] ?? responseData['data'] ?? [];
return rows.cast<Map<String, dynamic>>();
}
@override
Future<void> dispatchWorkOrder({
required String orderId,
required String assigneeId,
required String assigneeName,
String? dispatchRemark,
String? collaboratorIds,
String? collaboratorNames,
String? planStartTime,
String? planEndTime,
String? deadlineTime,
}) async {
final data = <String, dynamic>{
'orderId': orderId,
'assigneeId': assigneeId,
'assigneeName': assigneeName,
'collaboratorIds': collaboratorIds ?? '',
'collaboratorNames': collaboratorNames ?? '',
'planStartTime': planStartTime ?? '',
'planEndTime': planEndTime ?? '',
'deadlineTime': deadlineTime ?? '',
};
final response = await _dio.post(HttpApiConsts.workOrderDispat, data: data);
final responseData = response.data;
final int code = responseData['code'] ?? -1;
if (code != 0 && code != 200) {
throw Exception('转派工单失败: ${responseData['msg'] ?? '未知错误'}');
}
}
}

View File

@@ -1,5 +1,140 @@
import '../../../../../core/consts/workorder_consts.dart';
import '../../domain/entities/workorder_entity.dart';
import 'dart:developer' as developer;
/// 设备对象数据模型
class DeviceObjectModel {
final String deviceName;
final String deviceType;
final String assetCode;
DeviceObjectModel({
required this.deviceName,
required this.deviceType,
required this.assetCode,
});
factory DeviceObjectModel.fromJson(Map<String, dynamic> json) {
return DeviceObjectModel(
deviceName: json['deviceName'] as String? ?? '',
deviceType: json['deviceType'] as String? ?? '',
assetCode: json['assetCode'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'deviceName': deviceName,
'deviceType': deviceType,
'assetCode': assetCode,
};
}
DeviceObjectEntity toEntity() {
return DeviceObjectEntity(
deviceName: deviceName,
deviceType: deviceType,
assetCode: assetCode,
);
}
}
/// 地点数据模型
class LocationModel {
final String stationName;
final String area;
final String region;
final String detailAddress;
LocationModel({
required this.stationName,
required this.area,
required this.region,
required this.detailAddress,
});
factory LocationModel.fromJson(Map<String, dynamic> json) {
return LocationModel(
stationName: json['stationName'] as String? ?? '',
area: json['area'] as String? ?? '',
region: json['region'] as String? ?? '',
detailAddress: json['detailAddress'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'stationName': stationName,
'area': area,
'region': region,
'detailAddress': detailAddress,
};
}
LocationEntity toEntity() {
return LocationEntity(
stationName: stationName,
area: area,
region: region,
detailAddress: detailAddress,
);
}
}
/// 时限要求数据模型
class TimeLimitModel {
final String expectedCompleteTime;
final String remainingTime;
TimeLimitModel({
required this.expectedCompleteTime,
required this.remainingTime,
});
factory TimeLimitModel.fromJson(Map<String, dynamic> json) {
return TimeLimitModel(
expectedCompleteTime: json['expectedCompleteTime'] as String? ?? '',
remainingTime: json['remainingTime'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'expectedCompleteTime': expectedCompleteTime,
'remainingTime': remainingTime,
};
}
TimeLimitEntity toEntity() {
return TimeLimitEntity(
expectedCompleteTime: expectedCompleteTime,
remainingTime: remainingTime,
);
}
}
/// 附件数据模型
class AttachmentModel {
final String fileName;
final String url;
AttachmentModel({required this.fileName, required this.url});
factory AttachmentModel.fromJson(Map<String, dynamic> json) {
return AttachmentModel(
fileName: json['fileName'] as String? ?? '',
url: json['url'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {'fileName': fileName, 'url': url};
}
AttachmentEntity toEntity() {
return AttachmentEntity(fileName: fileName, url: url);
}
}
/// 工单数据模型
class WorkOrderModel {
@@ -13,6 +148,34 @@ class WorkOrderModel {
final String location;
final WorkOrderStatus status;
final double? progress;
final String? source;
final DeviceObjectModel? deviceObject;
final LocationModel? locationDetail;
final TimeLimitModel? timeLimit;
final String? description;
final List<AttachmentModel>? attachments;
final String? orderType;
final String? alarmId;
final String? alarmNo;
final String? handleResult;
final String? handleRemark;
final String? planStartTime;
final String? planEndTime;
final String? siteName;
final String? deviceId;
final String? deviceName;
final String? deviceType;
final String? assigneeName;
final String? aiSuggestion;
final String? requiredEquipment;
final String? estimatedImpact;
final String? taskDescription;
final String? actualStartTime;
final String? actualEndTime;
final String? deadlineTime;
final int? sourceType;
final List<String>? videoUrls;
final String? updateTime;
WorkOrderModel({
required this.id,
@@ -25,27 +188,118 @@ class WorkOrderModel {
required this.location,
required this.status,
this.progress,
this.source,
this.deviceObject,
this.locationDetail,
this.timeLimit,
this.description,
this.attachments,
this.orderType,
this.alarmId,
this.alarmNo,
this.handleResult,
this.handleRemark,
this.planStartTime,
this.planEndTime,
this.siteName,
this.deviceId,
this.deviceName,
this.deviceType,
this.assigneeName,
this.aiSuggestion,
this.requiredEquipment,
this.estimatedImpact,
this.taskDescription,
this.actualStartTime,
this.actualEndTime,
this.deadlineTime,
this.sourceType,
this.videoUrls,
this.updateTime,
});
/// 从 JSON 创建
factory WorkOrderModel.fromJson(Map<String, dynamic> json) {
final attachments = _parseAttachments(json);
final videoUrls = _parseVideoUrls(json);
developer.log(
'WorkOrderModel.fromJson keys: ${json.keys.toList()}',
name: 'WorkOrder',
);
developer.log(
'attachments: ${attachments?.length ?? 0}',
name: 'WorkOrder',
);
developer.log('videoUrls: ${videoUrls?.length ?? 0}', name: 'WorkOrder');
return WorkOrderModel(
id: json['id'] as String,
title: json['title'] as String,
orderNo: json['orderNo'] as String,
priority: _parsePriority(json['priority'] as String),
executor: json['executor'] as String?,
createTime: DateTime.parse(json['createTime'] as String),
completeTime: json['completeTime'] != null
? DateTime.parse(json['completeTime'] as String)
id: (json['id'] as dynamic)?.toString() ?? '',
title: json['orderTitle'] as String? ?? '',
orderNo: json['orderNo'] as String? ?? '',
priority: _parsePriority(json['priorityLevel'] as String? ?? 'medium'),
executor: json['assigneeName'] as String?,
createTime: DateTime.parse(
json['createTime'] as String? ?? '2026-01-01 00:00:00',
),
completeTime: json['actualEndTime'] != null
? DateTime.parse(json['actualEndTime'] as String)
: null,
location: json['location'] as String,
status: _parseStatus(json['status'] as String),
progress: json['progress'] as double?,
location: json['siteName'] as String? ?? '',
status: _parseStatusFromInt(json['orderStatus'] as int? ?? 1),
progress: (json['progress'] as dynamic)?.toDouble(),
source: _parseSourceType(json['sourceType'] as int? ?? 0),
deviceObject: json['deviceName'] != null
? DeviceObjectModel(
deviceName: json['deviceName'] as String? ?? '',
deviceType: json['deviceType'] as String? ?? '',
assetCode: json['deviceId'] as String? ?? '',
)
: null,
locationDetail: json['siteName'] != null
? LocationModel(
stationName: json['siteName'] as String? ?? '',
area: json['area'] as String? ?? '',
region: json['region'] as String? ?? '',
detailAddress: json['detailAddress'] as String? ?? '',
)
: null,
timeLimit: json['planEndTime'] != null
? TimeLimitModel(
expectedCompleteTime: json['planEndTime'] as String? ?? '',
remainingTime: '',
)
: null,
description: json['taskDescription'] as String?,
attachments: attachments,
orderType: json['orderType'] as String?,
alarmId: (json['alarmId'] as dynamic)?.toString(),
alarmNo: json['alarmNo'] as String?,
handleResult: json['handleResult'] as String?,
handleRemark: json['handleRemark'] as String?,
planStartTime: json['planStartTime'] as String?,
planEndTime: json['planEndTime'] as String?,
siteName: json['siteName'] as String?,
deviceId: json['deviceId'] as String?,
deviceName: json['deviceName'] as String?,
deviceType: json['deviceType'] as String?,
assigneeName:
json['assigneeName'] as String? ??
json['executorName'] as String? ??
json['executor'] as String? ??
json['assignee'] as String?,
aiSuggestion: json['aiSuggestion'] as String?,
requiredEquipment: json['requiredEquipment'] as String?,
estimatedImpact: json['estimatedImpact'] as String?,
taskDescription: json['taskDescription'] as String?,
actualStartTime: json['actualStartTime'] as String?,
actualEndTime: json['actualEndTime'] as String?,
deadlineTime: json['deadlineTime'] as String?,
sourceType: json['sourceType'] as int?,
videoUrls: videoUrls,
updateTime: json['updateTime'] as String?,
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'id': id,
@@ -58,10 +312,15 @@ class WorkOrderModel {
'location': location,
'status': _statusToString(status),
'progress': progress,
'source': source,
'deviceObject': deviceObject?.toJson(),
'locationDetail': locationDetail?.toJson(),
'timeLimit': timeLimit?.toJson(),
'description': description,
'attachments': attachments?.map((e) => e.toJson()).toList(),
};
}
/// 转换为实体
WorkOrderEntity toEntity() {
return WorkOrderEntity(
id: id,
@@ -74,10 +333,37 @@ class WorkOrderModel {
location: location,
status: status,
progress: progress,
source: source,
deviceObject: deviceObject?.toEntity(),
locationDetail: locationDetail?.toEntity(),
timeLimit: timeLimit?.toEntity(),
description: description,
attachments: attachments?.map((e) => e.toEntity()).toList(),
orderType: orderType,
alarmId: alarmId,
alarmNo: alarmNo,
handleResult: handleResult,
handleRemark: handleRemark,
planStartTime: planStartTime,
planEndTime: planEndTime,
siteName: siteName,
deviceId: deviceId,
deviceName: deviceName,
deviceType: deviceType,
assigneeName: assigneeName,
aiSuggestion: aiSuggestion,
requiredEquipment: requiredEquipment,
estimatedImpact: estimatedImpact,
taskDescription: taskDescription,
actualStartTime: actualStartTime,
actualEndTime: actualEndTime,
deadlineTime: deadlineTime,
sourceType: sourceType,
videoUrls: videoUrls,
updateTime: updateTime,
);
}
/// 从实体创建
factory WorkOrderModel.fromEntity(WorkOrderEntity entity) {
return WorkOrderModel(
id: entity.id,
@@ -90,17 +376,217 @@ class WorkOrderModel {
location: entity.location,
status: entity.status,
progress: entity.progress,
source: entity.source,
deviceObject: entity.deviceObject != null
? DeviceObjectModel(
deviceName: entity.deviceObject!.deviceName,
deviceType: entity.deviceObject!.deviceType,
assetCode: entity.deviceObject!.assetCode,
)
: null,
locationDetail: entity.locationDetail != null
? LocationModel(
stationName: entity.locationDetail!.stationName,
area: entity.locationDetail!.area,
region: entity.locationDetail!.region,
detailAddress: entity.locationDetail!.detailAddress,
)
: null,
timeLimit: entity.timeLimit != null
? TimeLimitModel(
expectedCompleteTime: entity.timeLimit!.expectedCompleteTime,
remainingTime: entity.timeLimit!.remainingTime,
)
: null,
description: entity.description,
attachments: entity.attachments
?.map((e) => AttachmentModel(fileName: e.fileName, url: e.url))
.toList(),
orderType: entity.orderType,
alarmId: entity.alarmId,
alarmNo: entity.alarmNo,
handleResult: entity.handleResult,
handleRemark: entity.handleRemark,
planStartTime: entity.planStartTime,
planEndTime: entity.planEndTime,
siteName: entity.siteName,
deviceId: entity.deviceId,
deviceName: entity.deviceName,
deviceType: entity.deviceType,
assigneeName: entity.assigneeName,
aiSuggestion: entity.aiSuggestion,
requiredEquipment: entity.requiredEquipment,
estimatedImpact: entity.estimatedImpact,
taskDescription: entity.taskDescription,
actualStartTime: entity.actualStartTime,
actualEndTime: entity.actualEndTime,
deadlineTime: entity.deadlineTime,
sourceType: entity.sourceType,
videoUrls: entity.videoUrls,
updateTime: entity.updateTime,
);
}
static List<AttachmentModel>? _parseAttachments(Map<String, dynamic> json) {
const fieldNames = [
'imgUrl',
'imageUrl',
'imageUrls',
'photos',
'images',
'attachments',
'files',
'media',
];
for (final field in fieldNames) {
final dynamic value = json[field];
if (value == null) continue;
if (value is String && value.isNotEmpty) {
return [AttachmentModel(fileName: '', url: value)];
}
if (value is List) {
final list = <AttachmentModel>[];
for (final item in value) {
if (item is String && item.isNotEmpty) {
list.add(AttachmentModel(fileName: '', url: item));
} else if (item is Map) {
final type =
(item['type']?.toString() ??
item['mediaType']?.toString() ??
'')
.toLowerCase();
if (type == 'video' || type == 'video/mp4') continue;
final url =
item['url']?.toString() ?? item['fileUrl']?.toString() ?? '';
final fileName =
item['fileName']?.toString() ?? item['name']?.toString() ?? '';
if (url.isNotEmpty) {
list.add(AttachmentModel(fileName: fileName, url: url));
}
}
}
if (list.isNotEmpty) return list;
}
if (value is Map) {
final dynamic nestedImages =
value['images'] ??
value['photos'] ??
value['imgUrl'] ??
value['imageUrls'];
if (nestedImages != null) {
if (nestedImages is List) {
final list = <AttachmentModel>[];
for (final item in nestedImages) {
if (item is String && item.isNotEmpty) {
list.add(AttachmentModel(fileName: '', url: item));
} else if (item is Map) {
final url =
item['url']?.toString() ??
item['fileUrl']?.toString() ??
'';
final fileName =
item['fileName']?.toString() ??
item['name']?.toString() ??
'';
if (url.isNotEmpty)
list.add(AttachmentModel(fileName: fileName, url: url));
}
}
if (list.isNotEmpty) return list;
} else if (nestedImages is String && nestedImages.isNotEmpty) {
return [AttachmentModel(fileName: '', url: nestedImages)];
}
}
}
}
return null;
}
static List<String>? _parseVideoUrls(Map<String, dynamic> json) {
const fieldNames = ['videoUrl', 'videoUrls', 'videos', 'videoList'];
for (final field in fieldNames) {
final dynamic value = json[field];
if (value == null) continue;
if (value is String && value.isNotEmpty) {
return [value];
}
if (value is List) {
final list = <String>[];
for (final item in value) {
if (item is String && item.isNotEmpty) {
list.add(item);
} else if (item is Map) {
final url =
item['url']?.toString() ?? item['fileUrl']?.toString() ?? '';
if (url.isNotEmpty) list.add(url);
}
}
if (list.isNotEmpty) return list;
}
}
final dynamic attachments = json['attachments'];
if (attachments is List) {
final list = <String>[];
for (final item in attachments) {
if (item is Map) {
final type =
(item['type']?.toString() ?? item['mediaType']?.toString() ?? '')
.toLowerCase();
if (type == 'video' || type == 'video/mp4') {
final url =
item['url']?.toString() ?? item['fileUrl']?.toString() ?? '';
if (url.isNotEmpty) list.add(url);
}
}
}
if (list.isNotEmpty) return list;
}
final dynamic media = json['media'];
if (media is Map) {
final dynamic nestedVideos =
media['videos'] ?? media['videoUrls'] ?? media['videoUrl'];
if (nestedVideos != null) {
if (nestedVideos is List) {
final list = <String>[];
for (final item in nestedVideos) {
if (item is String && item.isNotEmpty) list.add(item);
if (item is Map) {
final url =
item['url']?.toString() ?? item['fileUrl']?.toString() ?? '';
if (url.isNotEmpty) list.add(url);
}
}
if (list.isNotEmpty) return list;
} else if (nestedVideos is String && nestedVideos.isNotEmpty) {
return [nestedVideos];
}
}
}
return null;
}
static WorkOrderPriority _parsePriority(String value) {
switch (value) {
switch (value.toUpperCase()) {
case 'HIGH':
case 'high':
return WorkOrderPriority.high;
case 'MEDIUM':
case 'medium':
return WorkOrderPriority.medium;
case 'LOW':
case 'low':
return WorkOrderPriority.low;
case 'ERROR':
return WorkOrderPriority.high;
case 'WARNING':
return WorkOrderPriority.medium;
case 'INFO':
return WorkOrderPriority.low;
default:
return WorkOrderPriority.medium;
}
@@ -130,6 +616,40 @@ class WorkOrderModel {
}
}
static WorkOrderStatus _parseStatusFromInt(int value) {
switch (value) {
case 1:
return WorkOrderStatus.pending;
case 2:
return WorkOrderStatus.executing;
case 3:
return WorkOrderStatus.completed;
default:
return WorkOrderStatus.pending;
}
}
static String _parseSourceType(int value) {
switch (value) {
case 1:
return '手动创建';
case 2:
return '计划任务';
case 3:
return '定期维护';
case 4:
return '巡检发现';
case 5:
return '设备上报';
case 6:
return '客户反馈';
case 7:
return '告警联动';
default:
return '未知来源';
}
}
static String _statusToString(WorkOrderStatus status) {
switch (status) {
case WorkOrderStatus.pending:
@@ -156,7 +676,6 @@ class WorkOrderCountModel {
required this.todayCompletedCount,
});
/// 从 JSON 创建
factory WorkOrderCountModel.fromJson(Map<String, dynamic> json) {
return WorkOrderCountModel(
pendingCount: json['pendingCount'] as int,
@@ -165,7 +684,6 @@ class WorkOrderCountModel {
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'pendingCount': pendingCount,
@@ -174,7 +692,6 @@ class WorkOrderCountModel {
};
}
/// 转换为实体
WorkOrderCountEntity toEntity() {
return WorkOrderCountEntity(
pendingCount: pendingCount,

View File

@@ -1,4 +1,3 @@
import '../../../../../core/consts/workorder_consts.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/error/workorder_failure.dart';
import '../../domain/repositories/workorder_repository.dart';
@@ -6,7 +5,6 @@ import '../../domain/entities/workorder_entity.dart';
import '../datasources/workorder_remote_datasource.dart';
import 'package:fpdart/fpdart.dart';
class WorkOrderRepositoryImpl implements WorkOrderRepository {
final WorkOrderRemoteDataSource remoteDataSource;
@@ -14,11 +12,17 @@ class WorkOrderRepositoryImpl implements WorkOrderRepository {
@override
Future<Either<Failure, List<WorkOrderEntity>>> getWorkOrderList({
WorkOrderStatus? status,
required int page,
required int pageSize,
int? siteId,
int? orgId,
}) async {
try {
final models = await remoteDataSource.getWorkOrderList(
status: status != null ? _statusToString(status) : null,
page: page,
pageSize: pageSize,
siteId: siteId,
orgId: orgId,
);
final entities = models.map((model) => model.toEntity()).toList();
return Right(entities);
@@ -26,7 +30,7 @@ class WorkOrderRepositoryImpl implements WorkOrderRepository {
return Left(UnknownFailure(message: '获取工单列表失败: $e'));
}
}
@override
Future<Either<Failure, WorkOrderCountEntity>> getWorkOrderCount() async {
try {
@@ -38,22 +42,66 @@ class WorkOrderRepositoryImpl implements WorkOrderRepository {
}
@override
Future<Either<Failure, List<WorkOrderEntity>>> filterWorkOrders({
required WorkOrderStatus status,
Future<Either<Failure, WorkOrderEntity>> getWorkOrderDetail(
String orderId, {
int? siteId,
int? orgId,
}) async {
return getWorkOrderList(status: status);
try {
final model = await remoteDataSource.getWorkOrderDetail(
orderId,
siteId: siteId,
orgId: orgId,
);
return Right(model.toEntity());
} catch (e) {
return Left(UnknownFailure(message: '获取工单详情失败: $e'));
}
}
String _statusToString(WorkOrderStatus status) {
switch (status) {
case WorkOrderStatus.pending:
return 'pending';
case WorkOrderStatus.executing:
return 'executing';
case WorkOrderStatus.completed:
return 'completed';
case WorkOrderStatus.all:
return 'all';
@override
Future<Either<Failure, List<Map<String, dynamic>>>> fetchUsers({
required int orgId,
required int siteId,
}) async {
try {
final result = await remoteDataSource.fetchUsers(
orgId: orgId,
siteId: siteId,
);
return Right(result);
} catch (e) {
return Left(UnknownFailure(message: '获取人员列表失败: $e'));
}
}
@override
Future<Either<Failure, void>> dispatchWorkOrder({
required String orderId,
required String assigneeId,
required String assigneeName,
String? dispatchRemark,
String? collaboratorIds,
String? collaboratorNames,
String? planStartTime,
String? planEndTime,
String? deadlineTime,
}) async {
try {
await remoteDataSource.dispatchWorkOrder(
orderId: orderId,
assigneeId: assigneeId,
assigneeName: assigneeName,
dispatchRemark: dispatchRemark,
collaboratorIds: collaboratorIds,
collaboratorNames: collaboratorNames,
planStartTime: planStartTime,
planEndTime: planEndTime,
deadlineTime: deadlineTime,
);
return const Right(null);
} catch (e) {
return Left(UnknownFailure(message: '转派工单失败: $e'));
}
}
}

View File

@@ -1,5 +1,64 @@
import 'package:equatable/equatable.dart';
import '../../../../../core/consts/workorder_consts.dart';
/// 设备对象实体
class DeviceObjectEntity extends Equatable {
final String deviceName;
final String deviceType;
final String assetCode;
const DeviceObjectEntity({
required this.deviceName,
required this.deviceType,
required this.assetCode,
});
@override
List<Object?> get props => [deviceName, deviceType, assetCode];
}
/// 地点实体
class LocationEntity extends Equatable {
final String stationName;
final String area;
final String region;
final String detailAddress;
const LocationEntity({
required this.stationName,
required this.area,
required this.region,
required this.detailAddress,
});
@override
List<Object?> get props => [stationName, area, region, detailAddress];
}
/// 时限要求实体
class TimeLimitEntity extends Equatable {
final String expectedCompleteTime;
final String remainingTime;
const TimeLimitEntity({
required this.expectedCompleteTime,
required this.remainingTime,
});
@override
List<Object?> get props => [expectedCompleteTime, remainingTime];
}
/// 附件实体
class AttachmentEntity extends Equatable {
final String fileName;
final String url;
const AttachmentEntity({required this.fileName, required this.url});
@override
List<Object?> get props => [fileName, url];
}
/// 工单详情实体
class WorkOrderEntity {
@@ -13,6 +72,34 @@ class WorkOrderEntity {
final String location;
final WorkOrderStatus status;
final double? progress;
final String? source;
final DeviceObjectEntity? deviceObject;
final LocationEntity? locationDetail;
final TimeLimitEntity? timeLimit;
final String? description;
final List<AttachmentEntity>? attachments;
final String? orderType;
final String? alarmId;
final String? alarmNo;
final String? handleResult;
final String? handleRemark;
final String? planStartTime;
final String? planEndTime;
final String? siteName;
final String? deviceId;
final String? deviceName;
final String? deviceType;
final String? assigneeName;
final String? aiSuggestion;
final String? requiredEquipment;
final String? estimatedImpact;
final String? taskDescription;
final String? actualStartTime;
final String? actualEndTime;
final String? deadlineTime;
final int? sourceType;
final List<String>? videoUrls;
final String? updateTime;
WorkOrderEntity({
required this.id,
@@ -25,6 +112,34 @@ class WorkOrderEntity {
required this.location,
required this.status,
this.progress,
this.source,
this.deviceObject,
this.locationDetail,
this.timeLimit,
this.description,
this.attachments,
this.orderType,
this.alarmId,
this.alarmNo,
this.handleResult,
this.handleRemark,
this.planStartTime,
this.planEndTime,
this.siteName,
this.deviceId,
this.deviceName,
this.deviceType,
this.assigneeName,
this.aiSuggestion,
this.requiredEquipment,
this.estimatedImpact,
this.taskDescription,
this.actualStartTime,
this.actualEndTime,
this.deadlineTime,
this.sourceType,
this.videoUrls,
this.updateTime,
});
}

View File

@@ -1,20 +1,37 @@
import '../../../../../core/consts/workorder_consts.dart';
import '../../../../../core/error/failure.dart';
import '../entities/workorder_entity.dart';
import 'package:fpdart/fpdart.dart';
/// 工单仓储接口
abstract class WorkOrderRepository {
/// 获取工单列表
Future<Either<Failure, List<WorkOrderEntity>>> getWorkOrderList({
WorkOrderStatus? status,
required int page,
required int pageSize,
int? siteId,
int? orgId,
});
/// 获取工单统计
Future<Either<Failure, WorkOrderCountEntity>> getWorkOrderCount();
/// 筛选工单
Future<Either<Failure, List<WorkOrderEntity>>> filterWorkOrders({
required WorkOrderStatus status,
Future<Either<Failure, WorkOrderEntity>> getWorkOrderDetail(
String orderId, {
int? siteId,
int? orgId,
});
Future<Either<Failure, List<Map<String, dynamic>>>> fetchUsers({
required int orgId,
required int siteId,
});
Future<Either<Failure, void>> dispatchWorkOrder({
required String orderId,
required String assigneeId,
required String assigneeName,
String? dispatchRemark,
String? collaboratorIds,
String? collaboratorNames,
String? planStartTime,
String? planEndTime,
String? deadlineTime,
});
}

View File

@@ -0,0 +1,14 @@
import '../repositories/workorder_repository.dart';
import '../entities/workorder_entity.dart';
import '../../../../../core/error/failure.dart';
import 'package:fpdart/fpdart.dart';
class GetWorkOrderDetailUseCase {
final WorkOrderRepository repository;
GetWorkOrderDetailUseCase({required this.repository});
Future<Either<Failure, WorkOrderEntity>> call(String orderId, {int? siteId, int? orgId}) {
return repository.getWorkOrderDetail(orderId, siteId: siteId, orgId: orgId);
}
}

View File

@@ -11,9 +11,17 @@ class GetWorkOrderListUseCase {
GetWorkOrderListUseCase({required this.repository});
Future<Either<Failure, List<WorkOrderEntity>>> execute({
WorkOrderStatus? status,
required int page,
required int pageSize,
int? siteId,
int? orgId,
}) async {
return await repository.getWorkOrderList(status: status);
return await repository.getWorkOrderList(
page: page,
pageSize: pageSize,
siteId: siteId,
orgId: orgId,
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:fpdart/fpdart.dart';
import '../../../../../core/error/failure.dart';
import '../repositories/workorder_repository.dart';
class FetchUsersUseCase {
final WorkOrderRepository repository;
FetchUsersUseCase(this.repository);
Future<Either<Failure, List<Map<String, dynamic>>>> execute({
required int orgId,
required int siteId,
}) async {
return await repository.fetchUsers(orgId: orgId, siteId: siteId);
}
}
class DispatchWorkOrderUseCase {
final WorkOrderRepository repository;
DispatchWorkOrderUseCase(this.repository);
Future<Either<Failure, void>> execute({
required String orderId,
required String assigneeId,
required String assigneeName,
String? dispatchRemark,
String? collaboratorIds,
String? collaboratorNames,
String? planStartTime,
String? planEndTime,
String? deadlineTime,
}) async {
return await repository.dispatchWorkOrder(
orderId: orderId,
assigneeId: assigneeId,
assigneeName: assigneeName,
dispatchRemark: dispatchRemark,
collaboratorIds: collaboratorIds,
collaboratorNames: collaboratorNames,
planStartTime: planStartTime,
planEndTime: planEndTime,
deadlineTime: deadlineTime,
);
}
}

View File

@@ -0,0 +1,133 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/transfer_workorder_usecase.dart';
import '../../../../../core/error/failure.dart';
class TransferState extends Equatable {
final bool isLoading;
final bool isSubmitting;
final List<Map<String, dynamic>> users;
final String? selectedUserId;
final String? selectedUserName;
final String? remark;
final String? errorMessage;
final bool? isSuccess;
const TransferState({
this.isLoading = false,
this.isSubmitting = false,
this.users = const [],
this.selectedUserId,
this.selectedUserName,
this.remark,
this.errorMessage,
this.isSuccess,
});
TransferState copyWith({
bool? isLoading,
bool? isSubmitting,
List<Map<String, dynamic>>? users,
String? selectedUserId,
String? selectedUserName,
String? remark,
String? errorMessage,
bool? isSuccess,
}) {
return TransferState(
isLoading: isLoading ?? this.isLoading,
isSubmitting: isSubmitting ?? this.isSubmitting,
users: users ?? this.users,
selectedUserId: selectedUserId ?? this.selectedUserId,
selectedUserName: selectedUserName ?? this.selectedUserName,
remark: remark ?? this.remark,
errorMessage: errorMessage,
isSuccess: isSuccess,
);
}
@override
List<Object?> get props => [
isLoading,
isSubmitting,
users,
selectedUserId,
selectedUserName,
remark,
errorMessage,
isSuccess,
];
}
class TransferCubit extends Cubit<TransferState> {
final FetchUsersUseCase fetchUsersUseCase;
final DispatchWorkOrderUseCase dispatchUseCase;
TransferCubit({
required this.fetchUsersUseCase,
required this.dispatchUseCase,
}) : super(const TransferState());
Future<void> loadUsers({required int orgId, required int siteId}) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
final result = await fetchUsersUseCase.execute(
orgId: orgId,
siteId: siteId,
);
result.fold(
(failure) {
emit(state.copyWith(isLoading: false, errorMessage: failure.message));
},
(users) {
emit(state.copyWith(isLoading: false, users: users));
},
);
}
void selectUser(String userId, String userName) {
emit(state.copyWith(selectedUserId: userId, selectedUserName: userName));
}
void updateRemark(String remark) {
emit(state.copyWith(remark: remark));
}
Future<bool> submitTransfer({required String workOrderId}) async {
if (state.selectedUserId == null || state.selectedUserId!.isEmpty) {
emit(state.copyWith(errorMessage: '请选择接收人', isSuccess: false));
return false;
}
emit(state.copyWith(isSubmitting: true, errorMessage: null));
final result = await dispatchUseCase.execute(
orderId: workOrderId,
assigneeId: state.selectedUserId!,
assigneeName: state.selectedUserName ?? '',
dispatchRemark: state.remark,
);
return result.fold(
(failure) {
emit(
state.copyWith(
isSubmitting: false,
errorMessage: failure.message,
isSuccess: false,
),
);
return false;
},
(_) {
emit(state.copyWith(isSubmitting: false, isSuccess: true));
return true;
},
);
}
void reset() {
emit(const TransferState());
}
}

View File

@@ -1,5 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
import '../../../../../core/app/app_user_cubit.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/error/workorder_failure.dart';
@@ -22,17 +26,58 @@ class WorkOrderLoading extends WorkOrderState {}
class WorkOrderLoaded extends WorkOrderState {
final List<WorkOrderEntity> workOrders;
final List<WorkOrderEntity> allWorkOrders;
final WorkOrderCountEntity? count;
final WorkOrderStatus currentStatus;
final int currentPage;
final bool hasMore;
final bool isLoadingMore;
final String? errorMessage;
const WorkOrderLoaded({
required this.workOrders,
required this.allWorkOrders,
this.count,
this.currentStatus = WorkOrderStatus.pending,
this.currentPage = 1,
this.hasMore = true,
this.isLoadingMore = false,
this.errorMessage,
});
@override
List<Object?> get props => [workOrders, count, currentStatus];
List<Object?> get props => [
workOrders,
allWorkOrders,
count,
currentStatus,
currentPage,
hasMore,
isLoadingMore,
errorMessage,
];
WorkOrderLoaded copyWith({
List<WorkOrderEntity>? workOrders,
List<WorkOrderEntity>? allWorkOrders,
WorkOrderCountEntity? count,
WorkOrderStatus? currentStatus,
int? currentPage,
bool? hasMore,
bool? isLoadingMore,
String? errorMessage,
}) {
return WorkOrderLoaded(
workOrders: workOrders ?? this.workOrders,
allWorkOrders: allWorkOrders ?? this.allWorkOrders,
count: count ?? this.count,
currentStatus: currentStatus ?? this.currentStatus,
currentPage: currentPage ?? this.currentPage,
hasMore: hasMore ?? this.hasMore,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
class WorkOrderError extends WorkOrderState {
@@ -48,42 +93,114 @@ class WorkOrderError extends WorkOrderState {
class WorkOrderCubit extends Cubit<WorkOrderState> {
final GetWorkOrderListUseCase getWorkOrderListUseCase;
final GetWorkOrderCountUseCase getWorkOrderCountUseCase;
WorkOrderCubit({
required this.getWorkOrderListUseCase,
required this.getWorkOrderCountUseCase,
}) : super(WorkOrderInitial());
WorkOrderCubit({required this.getWorkOrderListUseCase})
: super(WorkOrderInitial());
static const int _pageSize = 100;
List<WorkOrderEntity> _filterWorkOrders(
List<WorkOrderEntity> allWorkOrders,
WorkOrderStatus status,
) {
if (status == WorkOrderStatus.all) {
return allWorkOrders;
}
return allWorkOrders.where((order) => order.status == status).toList();
}
/// 初始加载
Future<void> loadInitialData() async {
emit(WorkOrderLoading());
try {
// 并行加载工单列表和统计数据
final results = await Future.wait([
getWorkOrderListUseCase.execute(status: WorkOrderStatus.pending),
getWorkOrderCountUseCase.execute(),
]);
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId;
final listResult = results[0] as Either<Failure, List<WorkOrderEntity>>;
final countResult = results[1] as Either<Failure, WorkOrderCountEntity>;
List<WorkOrderEntity> allWorkOrders = [];
String? errorMessage;
listResult.fold(
(Failure failure) => emit(WorkOrderError(failure: failure)),
(workOrders) {
countResult.fold(
(Failure failure) => emit(WorkOrderError(failure: failure)),
(count) => emit(WorkOrderLoaded(
workOrders: workOrders,
count: count,
currentStatus: WorkOrderStatus.pending,
)),
);
},
final listResult = await getWorkOrderListUseCase.execute(
page: 1,
pageSize: _pageSize,
siteId: siteId,
orgId: orgId,
);
listResult.fold((Failure failure) {
debugPrint('❌ [WorkOrder] 获取工单列表失败: ${failure.message}');
errorMessage = '暂无数据';
}, (List<WorkOrderEntity> orders) => allWorkOrders = orders);
emit(
WorkOrderLoaded(
workOrders: _filterWorkOrders(allWorkOrders, WorkOrderStatus.pending),
allWorkOrders: allWorkOrders,
count: null,
currentStatus: WorkOrderStatus.pending,
currentPage: 1,
hasMore: allWorkOrders.length >= _pageSize,
errorMessage: errorMessage,
),
);
} catch (e) {
emit(WorkOrderError(failure: UnknownFailure(message: '加载失败: $e')));
debugPrint('❌ [WorkOrder] 加载工单异常: $e');
emit(
WorkOrderLoaded(
workOrders: [],
allWorkOrders: [],
count: null,
currentStatus: WorkOrderStatus.pending,
currentPage: 1,
hasMore: false,
errorMessage: '暂无数据',
),
);
}
}
/// 加载更多
Future<void> loadMore() async {
final currentState = state;
if (currentState is WorkOrderLoaded) {
if (!currentState.hasMore || currentState.isLoadingMore) return;
emit(currentState.copyWith(isLoadingMore: true));
try {
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId;
final result = await getWorkOrderListUseCase.execute(
page: currentState.currentPage + 1,
pageSize: _pageSize,
siteId: siteId,
orgId: orgId,
);
result.fold(
(Failure failure) => emit(WorkOrderError(failure: failure)),
(newWorkOrders) {
final updatedAllWorkOrders = [
...currentState.allWorkOrders,
...newWorkOrders,
];
emit(
currentState.copyWith(
allWorkOrders: updatedAllWorkOrders,
workOrders: _filterWorkOrders(
updatedAllWorkOrders,
currentState.currentStatus,
),
currentPage: currentState.currentPage + 1,
hasMore: newWorkOrders.length >= _pageSize,
isLoadingMore: false,
),
);
},
);
} catch (e) {
emit(currentState.copyWith(isLoadingMore: false));
}
}
}
@@ -93,22 +210,12 @@ class WorkOrderCubit extends Cubit<WorkOrderState> {
final currentState = state as WorkOrderLoaded;
if (currentState.currentStatus == status) return;
emit(WorkOrderLoading());
try {
final result = await getWorkOrderListUseCase.execute(status: status);
result.fold(
(Failure failure) => emit(WorkOrderError(failure: failure)),
(workOrders) => emit(WorkOrderLoaded(
workOrders: workOrders,
count: currentState.count,
currentStatus: status,
)),
);
} catch (e) {
emit(WorkOrderError(failure: UnknownFailure(message: '切换失败: $e')));
}
emit(
currentState.copyWith(
workOrders: _filterWorkOrders(currentState.allWorkOrders, status),
currentStatus: status,
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
import '../../../../../core/app/app_user_cubit.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/error/workorder_failure.dart';
import '../../domain/usecases/get_workorder_detail_usecase.dart';
import '../../domain/entities/workorder_entity.dart';
import 'package:fpdart/fpdart.dart';
abstract class WorkOrderDetailState extends Equatable {
const WorkOrderDetailState();
@override
List<Object?> get props => [];
}
class WorkOrderDetailInitial extends WorkOrderDetailState {}
class WorkOrderDetailLoading extends WorkOrderDetailState {}
class WorkOrderDetailLoaded extends WorkOrderDetailState {
final WorkOrderEntity workOrder;
const WorkOrderDetailLoaded({required this.workOrder});
@override
List<Object?> get props => [workOrder];
}
class WorkOrderDetailError extends WorkOrderDetailState {
final Failure failure;
const WorkOrderDetailError({required this.failure});
@override
List<Object?> get props => [failure];
}
class WorkOrderDetailCubit extends Cubit<WorkOrderDetailState> {
final GetWorkOrderDetailUseCase getWorkOrderDetailUseCase;
WorkOrderDetailCubit({required this.getWorkOrderDetailUseCase})
: super(WorkOrderDetailInitial());
Future<void> loadWorkOrderDetail(String orderId) async {
emit(WorkOrderDetailLoading());
try {
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId;
final result = await getWorkOrderDetailUseCase(
orderId,
siteId: siteId,
orgId: orgId,
);
result.fold(
(Failure failure) => emit(WorkOrderDetailError(failure: failure)),
(workOrder) => emit(WorkOrderDetailLoaded(workOrder: workOrder)),
);
} catch (e) {
emit(WorkOrderDetailError(failure: UnknownFailure(message: '加载失败: $e')));
}
}
}

View File

@@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../../../../core/app/app_user_cubit.dart';
import '../../../../../core/network/dio_client.dart';
import '../../../site/presentation/cubit/site_cubit.dart';
import '../../domain/usecases/transfer_workorder_usecase.dart';
import '../../data/repositories/workorder_repository_impl.dart';
import '../../data/datasources/workorder_remote_datasource_impl.dart';
import '../cubit/transfer_cubit.dart';
class TransferDialog extends StatelessWidget {
final String workOrderId;
const TransferDialog({super.key, required this.workOrderId});
@override
Widget build(BuildContext context) {
final appUserCubit = GetIt.I<AppUserCubit>();
final user = appUserCubit.state.user;
final orgId = user?.orgId ?? 0;
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id ?? 0;
final dio = DioClient.create();
final remoteDataSource = WorkOrderRemoteDataSourceImpl(dio);
final repository = WorkOrderRepositoryImpl(
remoteDataSource: remoteDataSource,
);
final fetchUsersUseCase = FetchUsersUseCase(repository);
final dispatchUseCase = DispatchWorkOrderUseCase(repository);
return BlocProvider(
create: (_) => TransferCubit(
fetchUsersUseCase: fetchUsersUseCase,
dispatchUseCase: dispatchUseCase,
)..loadUsers(orgId: orgId, siteId: siteId),
child: _TransferDialogContent(workOrderId: workOrderId),
);
}
}
class _TransferDialogContent extends StatefulWidget {
final String workOrderId;
const _TransferDialogContent({required this.workOrderId});
@override
State<_TransferDialogContent> createState() => _TransferDialogContentState();
}
class _TransferDialogContentState extends State<_TransferDialogContent> {
late TextEditingController _remarkController;
@override
void initState() {
super.initState();
_remarkController = TextEditingController();
}
@override
void dispose() {
_remarkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<TransferCubit, TransferState>(
listener: (context, state) {
if (state.isSuccess == true) {
Navigator.of(context).pop(true);
}
if (state.errorMessage != null && state.isSuccess == false) {
_showErrorDialog(context, state.errorMessage!);
}
},
builder: (context, state) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 16),
_buildUserList(state),
const SizedBox(height: 12),
_buildRemarkInput(context),
const SizedBox(height: 16),
_buildSubmitButton(context, state),
],
),
);
},
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
const Expanded(
child: Text(
'转派工单',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1D2129),
),
),
),
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const Icon(Icons.close, size: 24, color: Color(0xFF86909C)),
),
],
);
}
Widget _buildUserList(TransferState state) {
if (state.isLoading) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: CircularProgressIndicator(),
),
);
}
if (state.users.isEmpty) {
return const Padding(
padding: EdgeInsets.all(24),
child: Center(
child: Text(
'暂无可转派人员',
style: TextStyle(color: Color(0xFF86909C), fontSize: 14),
),
),
);
}
return Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ListView.builder(
shrinkWrap: true,
itemCount: state.users.length,
itemBuilder: (context, index) {
final user = state.users[index];
final userId = (user['userId'] ?? user['id'] ?? '').toString();
final userName = (user['nickName'] ?? user['username'] ?? '')
.toString();
final isSelected = state.selectedUserId == userId;
return GestureDetector(
onTap: () =>
context.read<TransferCubit>().selectUser(userId, userName),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFE8F3FF)
: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? const Color(0xFF165DFF)
: Colors.transparent,
),
),
child: Row(
children: [
CircleAvatar(
radius: 18,
backgroundColor: isSelected
? const Color(0xFF165DFF)
: const Color(0xFFC9CDD4),
child: Text(
userName.isNotEmpty ? userName[0] : '?',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
userName.isNotEmpty ? userName : '未知用户',
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1D2129),
),
),
),
if (isSelected)
const Icon(
Icons.check_circle,
color: Color(0xFF165DFF),
size: 22,
),
],
),
),
);
},
),
);
}
Widget _buildRemarkInput(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'派发备注(选填)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
controller: _remarkController,
maxLines: 3,
maxLength: 200,
onChanged: (value) =>
context.read<TransferCubit>().updateRemark(value),
decoration: const InputDecoration(
hintText: '请输入派发备注',
border: InputBorder.none,
contentPadding: EdgeInsets.all(12),
isDense: true,
),
),
),
],
);
}
Widget _buildSubmitButton(BuildContext context, TransferState state) {
return SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: state.isSubmitting
? null
: () {
context.read<TransferCubit>().submitTransfer(
workOrderId: widget.workOrderId,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
elevation: 0,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: state.isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'确认转派',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
);
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFFF53F3F)),
const SizedBox(height: 12),
const Text(
'转派失败',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
Text(
message,
style: const TextStyle(fontSize: 14, color: Color(0xFF86909C)),
textAlign: TextAlign.center,
),
],
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('我知道了', style: TextStyle(fontSize: 15)),
),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,992 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:video_player/video_player.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../domain/entities/workorder_entity.dart';
import '../../../work_order/domain/usecases/work_order_usecases.dart';
class WorkOrderOnSitePage extends StatefulWidget {
final WorkOrderEntity workOrder;
final String orderId;
const WorkOrderOnSitePage({
super.key,
required this.workOrder,
required this.orderId,
});
@override
State<WorkOrderOnSitePage> createState() => _WorkOrderOnSitePageState();
}
class _WorkOrderOnSitePageState extends State<WorkOrderOnSitePage> {
bool _isOperating = false;
WorkOrderStatus? _currentStatus;
bool _dataChanged = false;
Future<void> _handleStartWork() async {
if (_isOperating) return;
setState(() => _isOperating = true);
try {
final useCase = GetIt.I<StartWorkOrderUseCase>();
final workOrder = widget.workOrder;
final ids = <int>[int.parse(workOrder.id)];
final result = await useCase.execute(
ids: ids,
deviceId: workOrder.deviceId,
startTime: DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
);
if (!mounted) return;
result.fold(
(failure) => _showSnackBar('开工失败: ${failure.message}', isError: true),
(success) {
setState(() {
_currentStatus = WorkOrderStatus.executing;
_dataChanged = true;
});
_showSnackBar('开工成功');
},
);
} catch (e) {
if (mounted) _showSnackBar('开工异常: $e', isError: true);
} finally {
if (mounted) setState(() => _isOperating = false);
}
}
Future<void> _handleSuspendWork() async {
if (_isOperating) return;
setState(() => _isOperating = true);
try {
final useCase = GetIt.I<SuspendWorkOrderUseCase>();
final id = int.parse(widget.workOrder.id);
final result = await useCase.execute(id);
if (!mounted) return;
result.fold(
(failure) => _showSnackBar('暂停失败: ${failure.message}', isError: true),
(success) {
setState(() {
_currentStatus = WorkOrderStatus.pending;
_dataChanged = true;
});
_showSnackBar('已暂停');
},
);
} catch (e) {
if (mounted) _showSnackBar('暂停异常: $e', isError: true);
} finally {
if (mounted) setState(() => _isOperating = false);
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
duration: const Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1F2329)),
onPressed: () => context.pop(_dataChanged),
),
title: const Text(
'现场执行',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2329),
),
),
centerTitle: true,
actions: [
TextButton(
onPressed: () {},
child: const Text(
'更多',
style: TextStyle(fontSize: 15, color: Color(0xFF1890FF)),
),
),
],
),
body: _buildContent(widget.workOrder),
bottomNavigationBar: _buildBottomButtons(context),
),
);
}
Widget _buildContent(WorkOrderEntity workOrder) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildWorkOrderInfoCard(workOrder),
const SizedBox(height: 16),
_buildChecklistCard(),
const SizedBox(height: 16),
_buildOnSiteRecordCard(context, workOrder),
const SizedBox(height: 80),
],
),
);
}
Widget _buildWorkOrderInfoCard(WorkOrderEntity workOrder) {
final deviceName =
workOrder.deviceName ?? workOrder.deviceObject?.deviceName ?? '--';
final location =
workOrder.locationDetail?.detailAddress ?? workOrder.location ?? '--';
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 工单编号 + 紧急标签
Row(
children: [
Expanded(
child: Text(
workOrder.orderNo,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2329),
),
),
),
if (workOrder.priority == WorkOrderPriority.high)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFF4D4F),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'紧急',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 16),
_buildInfoRowWithValueColor(
'状态',
_getStatusLabel(_currentStatus ?? workOrder.status),
valueColor: _getStatusColor(_currentStatus ?? workOrder.status),
),
_buildInfoRowWithValueColor(
'优先级',
_getPriorityLabel(workOrder.priority),
valueColor: _getPriorityColor(workOrder.priority),
),
_buildInfoRow('来源', workOrder.source ?? '--'),
_buildInfoRow('工单类型', _getOrderTypeLabel(workOrder.orderType)),
_buildInfoRow('设备', deviceName, showArrow: true, multiline: true),
_buildInfoRow('当前位置', location, showLocation: true, isLast: true),
],
),
);
}
Widget _buildInfoRow(
String label,
String value, {
bool showArrow = false,
bool showLocation = false,
bool isLast = false,
bool multiline = false,
}) {
if (multiline) {
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF1F2329),
),
),
),
if (showArrow)
const Padding(
padding: EdgeInsets.only(left: 8),
child: Icon(
Icons.chevron_right,
size: 18,
color: Color(0xFFBBBBBB),
),
),
],
),
],
),
);
}
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
Row(
children: [
Text(
value,
style: const TextStyle(fontSize: 13, color: Color(0xFF1F2329)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (showLocation)
const Padding(
padding: EdgeInsets.only(left: 8),
child: Icon(
Icons.location_on,
size: 16,
color: Color(0xFF1890FF),
),
),
if (showArrow)
const Padding(
padding: EdgeInsets.only(left: 8),
child: Icon(
Icons.chevron_right,
size: 18,
color: Color(0xFFBBBBBB),
),
),
],
),
],
),
);
}
Widget _buildInfoRowWithValueColor(
String label,
String value, {
Color? valueColor,
bool isLast = false,
}) {
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
Text(
value,
style: TextStyle(
fontSize: 13,
color: valueColor ?? const Color(0xFF1F2329),
fontWeight: FontWeight.w500,
),
),
],
),
);
}
String _getStatusLabel(WorkOrderStatus status) {
switch (status) {
case WorkOrderStatus.pending:
return '待处理';
case WorkOrderStatus.executing:
return '处理中';
case WorkOrderStatus.completed:
return '已完成';
case WorkOrderStatus.all:
return '全部';
}
}
Color _getStatusColor(WorkOrderStatus status) {
switch (status) {
case WorkOrderStatus.pending:
return const Color(0xFFFF7D00);
case WorkOrderStatus.executing:
return const Color(0xFF1890FF);
case WorkOrderStatus.completed:
return const Color(0xFF00B42A);
case WorkOrderStatus.all:
return const Color(0xFF86909C);
}
}
String _getPriorityLabel(WorkOrderPriority priority) {
switch (priority) {
case WorkOrderPriority.high:
return '紧急';
case WorkOrderPriority.medium:
return '中等';
case WorkOrderPriority.low:
return '低';
}
}
Color _getPriorityColor(WorkOrderPriority priority) {
switch (priority) {
case WorkOrderPriority.high:
return const Color(0xFFFF4D4F);
case WorkOrderPriority.medium:
return const Color(0xFFFF7D00);
case WorkOrderPriority.low:
return const Color(0xFF86909C);
}
}
String _getOrderTypeLabel(String? orderType) {
if (orderType == null || orderType.isEmpty) return '--';
switch (orderType.toUpperCase()) {
case 'MOWER_ERROR':
return '割草机故障';
case 'INVERTER_ERROR':
return '逆变器故障';
case 'PANEL_ERROR':
return '组件故障';
case 'CLEANING':
return '清洗作业';
case 'MAINTENANCE':
return '定期维护';
case 'INSPECTION':
return '巡检任务';
default:
return orderType;
}
}
Widget _buildChecklistCard() {
final checklist = [
_CheckItem(id: 1, title: '现场安全确认', checked: true),
_CheckItem(id: 2, title: '设备外观检查', checked: true),
_CheckItem(id: 3, title: '通讯线路检查', checked: true),
_CheckItem(id: 4, title: '通讯模块更换', checked: false, current: true),
_CheckItem(id: 5, title: '参数配置与调试', checked: false),
_CheckItem(id: 6, title: '功能测试', checked: false),
_CheckItem(id: 7, title: '现场清理与恢复', checked: false),
];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'检查项清单 (5/7)',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2329),
),
),
const SizedBox(height: 16),
...checklist.map((item) => _buildCheckItem(item)),
],
),
);
}
Widget _buildCheckItem(_CheckItem item) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${item.id}. ${item.title}',
style: TextStyle(
fontSize: 14,
color: item.checked
? const Color(0xFF52C41A)
: item.current
? const Color(0xFF1890FF)
: const Color(0xFF1F2329),
),
),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: item.checked
? const Color(0xFF52C41A)
: item.current
? const Color(0xFF1890FF)
: const Color(0xFFD9D9D9),
width: 2,
),
color: item.checked
? const Color(0xFF52C41A)
: item.current
? const Color(0xFFE6F7FF)
: Colors.transparent,
),
child: item.checked
? const Icon(Icons.check, size: 12, color: Colors.white)
: item.current
? Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF1890FF),
),
)
: Container(),
),
],
),
);
}
Widget _buildOnSiteRecordCard(
BuildContext context,
WorkOrderEntity workOrder,
) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'现场记录',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2329),
),
),
const SizedBox(height: 16),
_buildAudioRecorder(),
const SizedBox(height: 20),
_buildPhotoGallery(context, workOrder),
],
),
);
}
Widget _buildAudioRecorder() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFFF5F7FA),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.mic_none, size: 24, color: Color(0xFFBBBBBB)),
const SizedBox(width: 12),
const Text(
'录音备注',
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
],
),
);
}
Widget _buildPhotoGallery(BuildContext context, WorkOrderEntity workOrder) {
final hasImages =
workOrder.attachments != null && workOrder.attachments!.isNotEmpty;
final hasVideos =
workOrder.videoUrls != null && workOrder.videoUrls!.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'现场照片',
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const SizedBox(width: 8),
Text(
'(${hasImages ? workOrder.attachments!.length : 0}张图片${hasVideos ? ' ${workOrder.videoUrls!.length}个视频' : ''})',
style: const TextStyle(fontSize: 13, color: Color(0xFF1890FF)),
),
],
),
const SizedBox(height: 12),
if (!hasImages && !hasVideos)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
'暂无照片或视频',
style: TextStyle(fontSize: 13, color: Color(0xFFBBBBBB)),
),
)
else
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 4,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: [
if (hasImages)
...workOrder.attachments!.map((attachment) {
return _buildMediaItem(attachment.url, true, context);
}).toList(),
if (hasVideos)
...workOrder.videoUrls!.map((videoUrl) {
return _buildMediaItem(videoUrl, false, context);
}).toList(),
_buildCameraButton(),
],
),
],
);
}
Widget _buildMediaItem(String url, bool isImage, BuildContext context) {
return GestureDetector(
onTap: () => _previewMedia(url, isImage),
child: Container(
width: double.infinity,
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: const Color(0xFFF7F8FA),
border: Border.all(color: const Color(0xFFE5E6EB)),
),
child: isImage
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
url,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.image_outlined,
size: 24,
color: Color(0xFFBBBBBB),
);
},
),
)
: const Center(
child: Icon(
Icons.video_library_outlined,
size: 24,
color: Color(0xFF1890FF),
),
),
),
);
}
void _previewMedia(String url, bool isImage) {
Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) {
return isImage
? _buildImagePreviewPage(url)
: _buildVideoPreviewPage(url);
},
),
);
}
Widget _buildImagePreviewPage(String url) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: Center(
child: InteractiveViewer(
child: Image.network(
url,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.broken_image,
color: Colors.white,
size: 64,
);
},
),
),
),
);
}
Widget _buildVideoPreviewPage(String url) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: VideoPreviewWidget(url: url),
);
}
Widget _buildCameraButton() {
return Container(
width: double.infinity,
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFD9D9D9), width: 1),
),
child: const Icon(Icons.camera_alt, size: 24, color: Color(0xFFBBBBBB)),
);
}
Widget _buildBottomButtons(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isOperating ? null : _handleStartWork,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF52C41A),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isOperating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: const Text(
'开工',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isOperating ? null : _handleSuspendWork,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFAAD14),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isOperating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: const Text(
'暂停',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: () async {
final receiptChanged = await context.push<bool>(
'/workorder/detail/${widget.orderId}/receipt',
extra: widget.workOrder,
);
if (receiptChanged == true) {
setState(() => _dataChanged = true);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1890FF),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'完成',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
),
),
],
),
);
}
}
class VideoPreviewWidget extends StatefulWidget {
final String url;
const VideoPreviewWidget({super.key, required this.url});
@override
State<VideoPreviewWidget> createState() => _VideoPreviewWidgetState();
}
class _VideoPreviewWidgetState extends State<VideoPreviewWidget> {
VideoPlayerController? _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_initVideo();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> _initVideo() async {
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.url));
try {
await _controller!.initialize();
setState(() {
_isLoading = false;
});
_controller!.play();
} catch (e) {
debugPrint('视频加载失败: $e');
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (_controller == null || !_controller!.value.isInitialized) {
return const Center(
child: Icon(
Icons.video_library_outlined,
color: Colors.white,
size: 64,
),
);
}
return Center(
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: Stack(
alignment: Alignment.center,
children: [
VideoPlayer(_controller!),
VideoProgressIndicator(
_controller!,
allowScrubbing: true,
colors: const VideoProgressColors(
playedColor: Colors.white,
bufferedColor: Colors.white38,
backgroundColor: Colors.white12,
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: VideoControlsWidget(controller: _controller!),
),
],
),
),
);
}
}
class VideoControlsWidget extends StatefulWidget {
final VideoPlayerController controller;
const VideoControlsWidget({super.key, required this.controller});
@override
State<VideoControlsWidget> createState() => _VideoControlsWidgetState();
}
class _VideoControlsWidgetState extends State<VideoControlsWidget> {
bool _isPlaying = true;
@override
void initState() {
super.initState();
widget.controller.addListener(_onVideoChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onVideoChanged);
super.dispose();
}
void _onVideoChanged() {
setState(() {
_isPlaying = widget.controller.value.isPlaying;
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 32,
),
onPressed: () {
if (_isPlaying) {
widget.controller.pause();
} else {
widget.controller.play();
}
},
),
],
);
}
}
class _CheckItem {
final int id;
final String title;
final bool checked;
final bool current;
_CheckItem({
required this.id,
required this.title,
required this.checked,
this.current = false,
});
}

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/core/network/dio_client.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../../site/presentation/widgets/site_selector_widget.dart';
import '../cubit/workorder_cubit.dart';
import '../widgets/workorder_tabbar.dart';
import '../widgets/workorder_count_card.dart';
@@ -12,44 +15,74 @@ import '../../data/repositories/workorder_repository_impl.dart';
import '../../data/datasources/workorder_remote_datasource_impl.dart';
/// 工单任务主页面
class WorkOrderPage extends StatelessWidget {
class WorkOrderPage extends StatefulWidget {
const WorkOrderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 创建 Repository 和 UseCases
final remoteDataSource = WorkOrderRemoteDataSourceImpl();
State<WorkOrderPage> createState() => _WorkOrderPageState();
}
class _WorkOrderPageState extends State<WorkOrderPage> {
final ScrollController _scrollController = ScrollController();
late final WorkOrderCubit _cubit;
@override
void initState() {
super.initState();
final dio = DioClient.create();
final remoteDataSource = WorkOrderRemoteDataSourceImpl(dio);
final repository = WorkOrderRepositoryImpl(
remoteDataSource: remoteDataSource,
);
final getWorkOrderListUseCase = GetWorkOrderListUseCase(
repository: repository,
);
final getWorkOrderCountUseCase = GetWorkOrderCountUseCase(
repository: repository,
_cubit = WorkOrderCubit(
getWorkOrderListUseCase: GetWorkOrderListUseCase(repository: repository),
);
return BlocProvider(
create: (context) {
final cubit = WorkOrderCubit(
getWorkOrderListUseCase: getWorkOrderListUseCase,
getWorkOrderCountUseCase: getWorkOrderCountUseCase,
);
cubit.loadInitialData();
return cubit;
},
_scrollController.addListener(_onScroll);
_cubit.loadInitialData();
}
@override
void dispose() {
_scrollController.dispose();
_cubit.close();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_cubit.loadMore();
}
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cubit,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
body: BlocBuilder<WorkOrderCubit, WorkOrderState>(
body: BlocConsumer<WorkOrderCubit, WorkOrderState>(
listener: (context, state) {
if (state is WorkOrderLoaded && state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage!),
duration: const Duration(seconds: 3),
),
);
}
},
builder: (context, state) {
if (state is WorkOrderLoading) {
return _buildLoadingView();
} else if (state is WorkOrderLoaded) {
return _buildContentView(context, state);
} else if (state is WorkOrderError) {
return _buildErrorView(context, state);
return _buildEmptyView(context);
}
return _buildLoadingView();
},
@@ -67,37 +100,24 @@ class WorkOrderPage extends StatelessWidget {
);
}
Widget _buildErrorView(BuildContext context, WorkOrderError state) {
Widget _buildEmptyView(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
Icons.inbox_outlined,
size: 48,
color: AppConstants.auxiliaryTextColor,
),
const SizedBox(height: 16),
Text(
state.failure.message,
'无数据',
style: TextStyle(
fontSize: 14,
color: AppConstants.auxiliaryTextColor,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<WorkOrderCubit>().loadInitialData();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
),
child: Text(
AppLocalizations.of(context).translate('work_order_v2.retry'),
),
),
],
),
);
@@ -107,34 +127,33 @@ class WorkOrderPage extends StatelessWidget {
return SafeArea(
child: Column(
children: [
// 导航栏
_buildAppBar(context),
// 标签栏
_buildTabBar(context, state),
// 内容区域
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await context.read<WorkOrderCubit>().refreshData();
},
child: ListView(
controller: _scrollController,
children: [
// 统计卡片
if (state.count != null)
WorkOrderCountCard(count: state.count!),
// 工单列表
...state.workOrders.map(
(workOrder) => WorkOrderItemCard(
workOrder: workOrder,
onTap: () {
// TODO: 跳转到工单详情页
debugPrint('点击工单: ${workOrder.title}');
},
if (state.workOrders.isEmpty) _buildEmptyView(context),
if (state.workOrders.isNotEmpty)
...state.workOrders.map(
(workOrder) => WorkOrderItemCard(
workOrder: workOrder,
onTap: () async {
await context.push(
'/workorder/detail/${workOrder.id}',
);
context.read<WorkOrderCubit>().loadInitialData();
},
),
),
),
if (state.workOrders.isNotEmpty)
_buildLoadMoreIndicator(state),
const SizedBox(height: 20),
],
),
@@ -151,16 +170,32 @@ class WorkOrderPage extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Text(
AppLocalizations.of(context).translate('work_order_v2.title'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
Expanded(
child: Row(
children: [
const Flexible(child: SiteSelectorWidget(compact: true)),
const SizedBox(width: 8),
Container(width: 1, height: 20, color: const Color(0xFFE5E6EB)),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context).translate('work_order_v2.title'),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF86909C),
),
),
],
),
),
GestureDetector(
onTap: () => context.push('/workorder/report'),
child: const Icon(
Icons.bar_chart,
size: 24,
color: Color(0xFF1D2129),
),
),
const Spacer(),
Icon(Icons.add_circle_outline, size: 24, color: Color(0xFF1D2129)),
],
),
);
@@ -174,4 +209,27 @@ class WorkOrderPage extends StatelessWidget {
},
);
}
Widget _buildLoadMoreIndicator(WorkOrderLoaded state) {
if (state.isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
);
} else if (state.hasMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('上滑加载更多', style: TextStyle(color: Color(0xFF86909C))),
),
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text('已加载全部数据', style: TextStyle(color: Color(0xFF86909C))),
),
);
}
}
}

View File

@@ -0,0 +1,353 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class WorkOrderProcessPage extends StatelessWidget {
final String orderId;
const WorkOrderProcessPage({super.key, required this.orderId});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1F2329)),
onPressed: () => context.pop(),
),
title: const Text(
'工单流程',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2329),
),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.share, color: Color(0xFF86909C)),
onPressed: () {},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildProcessTimeline(),
const SizedBox(height: 24),
_buildExecutionRecords(),
const SizedBox(height: 32),
],
),
),
bottomNavigationBar: _buildBottomIndicator(),
);
}
Widget _buildProcessTimeline() {
final processes = [
_ProcessStep(
status: ProcessStatus.completed,
title: '创建',
time: '2025-05-21 09:12',
details: ['系统创建工单(告警联动)', '创建人:系统'],
),
_ProcessStep(
status: ProcessStatus.completed,
title: '派发',
time: '2025-05-21 09:15',
details: ['派发给:张工(运维班组)', '派发人:调度员-李明'],
),
_ProcessStep(
status: ProcessStatus.completed,
title: '接单',
time: '2025-05-21 09:17',
details: ['张工已接单'],
),
_ProcessStep(
status: ProcessStatus.completed,
title: '处理中',
time: '2025-05-21 09:35',
details: ['现场处理进行中'],
),
_ProcessStep(
status: ProcessStatus.pending,
title: '验收',
time: '',
details: ['待验收'],
),
_ProcessStep(
status: ProcessStatus.pending,
title: '关闭',
time: '',
details: ['待关闭'],
),
];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: processes.asMap().entries.map((entry) {
final index = entry.key;
final step = entry.value;
final isLast = index == processes.length - 1;
final nextStatus = isLast ? null : processes[index + 1].status;
return _buildProcessItem(step, nextStatus, isLast);
}).toList(),
),
);
}
Widget _buildProcessItem(
_ProcessStep step,
ProcessStatus? nextStatus,
bool isLast,
) {
final showLine = !isLast;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
_buildStatusIcon(step.status, 0),
if (showLine) _buildProcessLine(step.status, nextStatus!),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
step.title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: step.status == ProcessStatus.completed
? const Color(0xFF1F2329)
: const Color(0xFFBBBBBB),
),
),
if (step.time.isNotEmpty) ...[
const SizedBox(width: 12),
Text(
step.time,
style: TextStyle(
fontSize: 12,
color: step.status == ProcessStatus.completed
? const Color(0xFF86909C)
: const Color(0xFFBBBBBB),
),
),
],
],
),
const SizedBox(height: 4),
...step.details.map(
(detail) => Text(
detail,
style: TextStyle(
fontSize: 12,
color: step.status == ProcessStatus.completed
? const Color(0xFF86909C)
: const Color(0xFFBBBBBB),
),
),
),
const SizedBox(height: 8),
],
),
),
],
);
}
Widget _buildStatusIcon(ProcessStatus status, int index) {
Widget icon;
Color iconColor;
Color backgroundColor;
switch (status) {
case ProcessStatus.completed:
icon = const Icon(Icons.check, size: 16);
iconColor = const Color(0xFF52C41A);
backgroundColor = const Color(0xFFF6FFED);
break;
case ProcessStatus.current:
icon = const Icon(Icons.check, size: 16);
iconColor = const Color(0xFF1890FF);
backgroundColor = const Color(0xFFE6F7FF);
break;
case ProcessStatus.pending:
icon = Container();
iconColor = const Color(0xFFBBBBBB);
backgroundColor = const Color(0xFFFAFAFA);
break;
}
return Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: iconColor, width: 2),
),
child: icon,
);
}
Widget _buildProcessLine(
ProcessStatus currentStatus,
ProcessStatus nextStatus,
) {
Color lineColor;
if (currentStatus == ProcessStatus.completed &&
nextStatus == ProcessStatus.completed) {
lineColor = const Color(0xFF52C41A);
} else if (currentStatus == ProcessStatus.completed &&
nextStatus == ProcessStatus.current) {
lineColor = const Color(0xFF1890FF);
} else {
lineColor = const Color(0xFFE8E8E8);
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
height: 28,
width: 2,
color: lineColor,
);
}
Widget _buildExecutionRecords() {
final records = [
_ExecutionRecord(time: '2025-05-21 09:20', action: '到达现场'),
_ExecutionRecord(time: '2025-05-21 09:35', action: '现场定位打卡'),
_ExecutionRecord(time: '2025-05-21 09:35', action: '开始处理'),
_ExecutionRecord(time: '2025-05-21 10:05', action: '更换通讯模块'),
_ExecutionRecord(time: '2025-05-21 10:05', action: '暂停处理'),
_ExecutionRecord(time: '2025-05-21 10:05', action: '等待备件到货'),
_ExecutionRecord(time: '2025-05-21 11:10', action: '继续处理'),
_ExecutionRecord(time: '2025-05-21 11:10', action: '更换完成,调试中'),
];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'执行记录',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2329),
),
),
const SizedBox(height: 16),
...records.map((record) => _buildRecordItem(record)),
],
),
);
}
Widget _buildRecordItem(_ExecutionRecord record) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Text(
record.time,
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
),
const SizedBox(width: 32),
Expanded(
child: Text(
record.action,
style: const TextStyle(fontSize: 13, color: Color(0xFF1F2329)),
),
),
],
),
);
}
Widget _buildBottomIndicator() {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: SizedBox(
height: 48,
child: Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0xFF1890FF),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: const Center(
child: const Text(
'工单流程',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
);
}
}
enum ProcessStatus { completed, current, pending }
class _ProcessStep {
final ProcessStatus status;
final String title;
final String time;
final List<String> details;
_ProcessStep({
required this.status,
required this.title,
required this.time,
required this.details,
});
}
class _ExecutionRecord {
final String time;
final String action;
_ExecutionRecord({required this.time, required this.action});
}

View File

@@ -0,0 +1,541 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../domain/entities/workorder_entity.dart';
import '../../../work_order/domain/usecases/work_order_usecases.dart';
class WorkOrderReceiptPage extends StatefulWidget {
final WorkOrderEntity workOrder;
final String orderId;
WorkOrderReceiptPage({
super.key,
required this.workOrder,
required this.orderId,
});
@override
State<WorkOrderReceiptPage> createState() => _WorkOrderReceiptPageState();
}
class _WorkOrderReceiptPageState extends State<WorkOrderReceiptPage> {
bool _isSubmitting = false;
// ============ 表单状态 ============
int _selectedResultIndex = 0; // 0=已解决, 1=部分解决, 2=未解决
final _faultCauseController = TextEditingController(text: '通讯模块故障');
final _measureController = TextEditingController(text: '更换通讯模块并重启设备,恢复通讯。');
final _sparePartController = TextEditingController(text: '通讯模块(型号:COM-485)');
final _remarkController = TextEditingController();
// 下拉选项
final _faultCauseOptions = ['通讯模块故障', '电源故障', '传感器异常', '机械故障', '网络连接异常'];
final _sparePartOptions = ['通讯模块(型号:COM-485)', '电源模块(型号:PWR-220)', '传感器模块(型号:SEN-001)'];
@override
void dispose() {
_faultCauseController.dispose();
_measureController.dispose();
_sparePartController.dispose();
_remarkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => context.pop(),
),
title: const Text(
'工单回执',
style: TextStyle(
color: Color(0xFF1D2129),
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
actions: [
TextButton(
onPressed: _isSubmitting ? null : _handleSubmit,
child: const Text(
'提交',
style: TextStyle(
color: Color(0xFF165DFF),
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
],
),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 8),
_buildOrderNumberCard(),
const SizedBox(height: 8),
_buildResultCard(),
const SizedBox(height: 8),
_buildDropdownInputCard(
label: '故障原因',
controller: _faultCauseController,
options: _faultCauseOptions,
required: true,
),
const SizedBox(height: 8),
_buildTextInputCard(
label: '处理措施',
controller: _measureController,
maxLines: 3,
),
const SizedBox(height: 8),
_buildDropdownInputCard(
label: '备件使用',
controller: _sparePartController,
options: _sparePartOptions,
),
const SizedBox(height: 8),
_buildPhotoCard(),
const SizedBox(height: 8),
_buildSignatureCard(),
const SizedBox(height: 8),
_buildTextInputCard(
label: '备注说明',
controller: _remarkController,
hintText: '请填写备注信息(选填)',
maxLines: 3,
),
const SizedBox(height: 100),
],
),
),
),
bottomNavigationBar: _buildBottomBar(context),
);
}
// ============ 提交 ============
Future<void> _handleSubmit() async {
if (_isSubmitting) return;
setState(() => _isSubmitting = true);
try {
final useCase = GetIt.I<CompleteWorkOrderUseCase>();
const results = ['已解决', '部分解决', '未解决'];
final id = int.parse(widget.workOrder.id);
final result = await useCase.execute(
id: id,
handleResult: results[_selectedResultIndex],
failureCause: _faultCauseController.text.isNotEmpty
? _faultCauseController.text
: null,
handleMeasures: _measureController.text.isNotEmpty
? _measureController.text
: null,
completeRemark: _remarkController.text.isNotEmpty
? _remarkController.text
: null,
completeTime: DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
);
if (!mounted) return;
result.fold(
(failure) {
setState(() => _isSubmitting = false);
_showSnackBar('提交失败: ${failure.message}', isError: true);
},
(success) {
setState(() => _isSubmitting = false);
_showSnackBar('提交成功');
context.pop(true);
},
);
} catch (e) {
if (mounted) {
setState(() => _isSubmitting = false);
_showSnackBar('提交异常: $e', isError: true);
}
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
duration: const Duration(seconds: 2),
),
);
}
// ============ 工单编号 ============
Widget _buildOrderNumberCard() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
color: Colors.white,
child: Row(
children: [
const Text(
'工单编号',
style: TextStyle(color: Color(0xFF4E5969), fontSize: 14),
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.workOrder.orderNo,
style: const TextStyle(
color: Color(0xFF1D2129),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
// ============ 处理结果 ============
Widget _buildResultCard() {
const results = ['已解决', '部分解决', '未解决'];
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel('处理结果', required: true),
const SizedBox(height: 12),
Row(
children: List.generate(results.length, (index) {
final selected = _selectedResultIndex == index;
return Padding(
padding: EdgeInsets.only(left: index == 0 ? 0 : 10),
child: GestureDetector(
onTap: () => setState(() => _selectedResultIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: selected ? const Color(0xFF165DFF) : Colors.white,
border: Border.all(
color: selected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB),
),
),
child: Text(
results[index],
style: TextStyle(
fontSize: 13,
color: selected ? Colors.white : const Color(0xFF4E5969),
fontWeight: selected ? FontWeight.w500 : FontWeight.normal,
),
),
),
),
);
}),
),
],
),
);
}
// ============ 输入+下拉卡片 ============
Widget _buildDropdownInputCard({
required String label,
required TextEditingController controller,
required List<String> options,
bool required = false,
}) {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel(label, required: required),
const SizedBox(height: 10),
TextField(
controller: controller,
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
filled: true,
fillColor: const Color(0xFFF7F8FA),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFF165DFF)),
),
isDense: true,
suffixIcon: PopupMenuButton<String>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.keyboard_arrow_down, color: Color(0xFF86909C), size: 20),
onSelected: (value) {
controller.text = value;
},
itemBuilder: (_) => options.map((o) => PopupMenuItem(value: o, child: Text(o, style: const TextStyle(fontSize: 14)))).toList(),
),
),
),
],
),
);
}
// ============ 纯文本输入卡片 ============
Widget _buildTextInputCard({
required String label,
required TextEditingController controller,
String? hintText,
int maxLines = 1,
}) {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel(label),
const SizedBox(height: 10),
TextField(
controller: controller,
maxLines: maxLines,
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129), height: 1.5),
decoration: InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(color: Color(0xFFC9CDD4), fontSize: 14),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
filled: true,
fillColor: const Color(0xFFF7F8FA),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Color(0xFF165DFF)),
),
isDense: true,
),
),
],
),
);
}
// ============ 现场照片 ============
Widget _buildPhotoCard() {
final hasImages = widget.workOrder.attachments != null &&
widget.workOrder.attachments!.isNotEmpty;
final hasVideos = widget.workOrder.videoUrls != null &&
widget.workOrder.videoUrls!.isNotEmpty;
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel('现场照片', required: true),
const SizedBox(height: 12),
if (!hasImages && !hasVideos)
const Text(
'暂无照片或视频',
style: TextStyle(fontSize: 13, color: Color(0xFFBBBBBB)),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (hasImages)
...widget.workOrder.attachments!.map((attachment) {
return _buildPhotoThumbNet(attachment.url);
}),
if (hasVideos)
...widget.workOrder.videoUrls!.map((url) {
return _buildVideoThumb(url);
}),
],
),
],
),
);
}
Widget _buildPhotoThumbNet(String url) {
return ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(url, width: 72, height: 72, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 72,
height: 72,
color: const Color(0xFFF7F8FA),
child: const Icon(Icons.broken_image, color: Color(0xFFBBBBBB)),
);
},
),
);
}
Widget _buildVideoThumb(String url) {
return Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE5E6EB)),
),
child: const Icon(Icons.video_library_outlined, size: 24, color: Color(0xFF1890FF)),
);
}
Widget _buildAddPhotoBtn() {
return GestureDetector(
onTap: () {
// TODO: 选择/拍照上传
},
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE5E6EB)),
),
child: const Icon(Icons.add, color: Color(0xFFC9CDD4), size: 28),
),
);
}
// ============ 客户/验收签名 ============
Widget _buildSignatureCard() {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildLabel('客户/验收签名', required: true),
GestureDetector(
onTap: () {
// TODO: 清除签名
},
child: const Text(
'清空',
style: TextStyle(color: Color(0xFF165DFF), fontSize: 13),
),
),
],
),
const SizedBox(height: 10),
Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE5E6EB)),
),
child: const Center(
child: Text(
'李建国',
style: TextStyle(fontSize: 24, color: Color(0xFF1D2129), fontFamily: 'KaiTi'),
),
),
),
],
),
);
}
// ============ 标签 ============
Widget _buildLabel(String text, {bool required = false}) {
return Row(
children: [
if (required)
const Text(
'* ',
style: TextStyle(color: Color(0xFFF53F3F), fontSize: 14),
),
Text(
text,
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
);
}
// ============ 底部按钮 ============
Widget _buildBottomBar(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom,
),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Color(0xFFE5E6EB), width: 0.5)),
),
child: SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _handleSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: _isSubmitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'提交并完成',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
);
}
}

View File

@@ -0,0 +1,440 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:go_router/go_router.dart';
class WorkOrderReportPage extends StatefulWidget {
const WorkOrderReportPage({super.key});
@override
State<WorkOrderReportPage> createState() => _WorkOrderReportPageState();
}
class _WorkOrderReportPageState extends State<WorkOrderReportPage> {
// ============ 模拟数据 ============
static const _totalOrders = 58;
static const _completedOrders = 46;
static const _completionRate = 79.3;
static const _typeData = [
(label: '故障处理', value: 48.0, color: Color(0xFF4A90D9)),
(label: '运维维护', value: 28.0, color: Color(0xFF00B42A)),
(label: '设备更换', value: 14.0, color: Color(0xFFFF7D00)),
(label: '用户报修', value: 10.0, color: Color(0xFFF53F3F)),
];
static const _trendDays = ['05-19', '05-20', '05-21', '05-22', '05-23', '05-24', '05-25'];
static const _createdData = [8, 14, 10, 17, 12, 10, 5];
static const _completedData = [5, 12, 9, 15, 10, 8, 4];
static const _topPendingList = [
(title: '逆变器通讯异常', area: '逆变器', remainHours: 2),
(title: '稽查温度偏高', area: '开压站区', remainHours: 8),
(title: '汇流箱熔断器更换', area: '2k 方阵', remainHours: 18),
(title: '变压柜电流异常', area: '1k 方阵', remainHours: 24),
(title: '数据采集器离线', area: '中心站区', remainHours: 36),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => context.pop(),
),
title: const Text(
'移动报表概览',
style: TextStyle(
color: Color(0xFF1D2129),
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
actions: [
TextButton(
onPressed: () {},
child: const Text(
'更多',
style: TextStyle(
color: Color(0xFF165DFF),
fontSize: 14,
),
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildTimeRange(),
const SizedBox(height: 16),
_buildMetricCards(),
const SizedBox(height: 16),
_buildTypeDistribution(),
const SizedBox(height: 16),
_buildTrendChart(),
const SizedBox(height: 16),
_buildPendingTop5(),
const SizedBox(height: 24),
_buildBottomActions(),
const SizedBox(height: 100),
],
),
),
);
}
// ============ 时间范围 ============
Widget _buildTimeRange() {
return Row(
children: [
const Icon(Icons.calendar_today, size: 16, color: Color(0xFF4E5969)),
const SizedBox(width: 6),
const Text(
'本周 05-19 ~ 05-25',
style: TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
),
const Spacer(),
GestureDetector(
onTap: () {},
child: const Icon(Icons.keyboard_arrow_down, size: 20, color: Color(0xFF86909C)),
),
],
);
}
// ============ 三个指标卡片 ============
Widget _buildMetricCards() {
return Row(
children: [
Expanded(child: _metricCard('个人工单总数', '$_totalOrders', '单', const Color(0xFFE8F3FF), const Color(0xFF165DFF))),
const SizedBox(width: 10),
Expanded(child: _metricCard('已完成', '$_completedOrders', '单', const Color(0xFFE8FFEA), const Color(0xFF00B42A))),
const SizedBox(width: 10),
Expanded(child: _metricCard('完成率', '$_completionRate', '%', const Color(0xFFFFF3E0), const Color(0xFFFF7D00))),
],
);
}
Widget _metricCard(String title, String value, String unit, Color bgColor, Color valueColor) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
const SizedBox(height: 8),
RichText(
text: TextSpan(
children: [
TextSpan(
text: value,
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: valueColor),
),
TextSpan(
text: ' $unit',
style: TextStyle(fontSize: 13, color: valueColor),
),
],
),
),
],
),
);
}
// ============ 工单类型分布(环形图)============
Widget _buildTypeDistribution() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('工单类型分布', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1D2129))),
const SizedBox(height: 16),
Row(
children: [
// 环形图
SizedBox(
width: 140,
height: 140,
child: PieChart(
PieChartData(
centerSpaceRadius: 32,
sectionsSpace: 3,
sections: _typeData.map((d) {
return PieChartSectionData(
value: d.value,
color: d.color,
radius: 18,
showTitle: false,
);
}).toList(),
),
),
),
const SizedBox(width: 20),
// 图例
Expanded(
child: Column(
children: _typeData.map((d) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: d.color,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Expanded(child: Text(d.label, style: const TextStyle(fontSize: 13, color: Color(0xFF4E5969)))),
Text(
'${d.value.toStringAsFixed(0)}%',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF1D2129)),
),
],
),
);
}).toList(),
),
),
],
),
],
),
);
}
// ============ 本周趋势(柱状图)============
Widget _buildTrendChart() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('本周趋势', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1D2129))),
const Spacer(),
_legendDot(const Color(0xFF165DFF), '创建数'),
const SizedBox(width: 12),
_legendDot(const Color(0xFF00B42A), '完成数'),
],
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 24,
barGroups: List.generate(_trendDays.length, (i) {
return BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: _createdData[i].toDouble(),
color: const Color(0xFF165DFF),
width: 10,
borderRadius: const BorderRadius.vertical(top: Radius.circular(3)),
),
BarChartRodData(
toY: _completedData[i].toDouble(),
color: const Color(0xFF00B42A),
width: 10,
borderRadius: const BorderRadius.vertical(top: Radius.circular(3)),
),
],
);
}),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value % 6 == 0) {
return Text('${value.toInt()}', style: const TextStyle(fontSize: 11, color: Color(0xFF86909C)));
}
return const SizedBox.shrink();
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final i = value.toInt();
if (i >= 0 && i < _trendDays.length) {
return Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(_trendDays[i], style: const TextStyle(fontSize: 10, color: Color(0xFF86909C))),
);
}
return const SizedBox.shrink();
},
),
),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 6,
getDrawingHorizontalLine: (value) => FlLine(
color: const Color(0xFFF0F0F0),
strokeWidth: 1,
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(enabled: false),
),
),
),
],
),
);
}
Widget _legendDot(Color color, String label) {
return Row(
children: [
Container(width: 8, height: 8, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))),
const SizedBox(width: 4),
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
],
);
}
// ============ 待处理工单 TOP5 ============
Widget _buildPendingTop5() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('待处理工单 TOP5', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1D2129))),
const SizedBox(height: 12),
...List.generate(_topPendingList.length, (i) {
final item = _topPendingList[i];
final isUrgent = item.remainHours <= 8;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: i < 3 ? const Color(0xFF165DFF) : const Color(0xFFC9CDD4),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
'${i + 1}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: i < 3 ? Colors.white : const Color(0xFF86909C),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.title, style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129))),
const SizedBox(height: 2),
Text(item.area, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isUrgent ? const Color(0xFFFFECE8) : const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'剩余${item.remainHours}h',
style: TextStyle(
fontSize: 12,
color: isUrgent ? const Color(0xFFF53F3F) : const Color(0xFF86909C),
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}),
],
),
);
}
// ============ 底部操作按钮 ============
Widget _buildBottomActions() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.download, size: 18),
label: const Text('导出报表', style: TextStyle(fontSize: 14)),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF165DFF),
side: const BorderSide(color: Color(0xFF165DFF)),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.share, size: 18),
label: const Text('分享', style: TextStyle(fontSize: 14)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/network/dio_client.dart';
import '../../domain/entities/workorder_entity.dart';
import '../../domain/usecases/get_workorder_detail_usecase.dart';
import '../../data/repositories/workorder_repository_impl.dart';
import '../../data/datasources/workorder_remote_datasource_impl.dart';
import '../cubit/workorder_detail_cubit.dart';
import '../pages/workorder_page.dart';
import '../pages/workorder_detail_page.dart';
import '../pages/workorder_process_page.dart';
import '../pages/workorder_on_site_page.dart';
import '../pages/workorder_receipt_page.dart';
import '../pages/workorder_report_page.dart';
class WorkOrderRoutes {
static List<RouteBase> get routes => [
GoRoute(
path: '/workorder',
name: 'workOrder',
builder: (context, state) => const WorkOrderPage(),
routes: [
GoRoute(
path: 'report',
name: 'workOrderReport',
builder: (context, state) => const WorkOrderReportPage(),
),
GoRoute(
path: 'detail/:orderId',
name: 'workOrderDetail',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
final dio = DioClient.create();
final remoteDataSource = WorkOrderRemoteDataSourceImpl(dio);
final repository = WorkOrderRepositoryImpl(
remoteDataSource: remoteDataSource,
);
final getWorkOrderDetailUseCase = GetWorkOrderDetailUseCase(
repository: repository,
);
return BlocProvider(
create: (_) => WorkOrderDetailCubit(
getWorkOrderDetailUseCase: getWorkOrderDetailUseCase,
),
child: WorkOrderDetailPage(orderId: orderId),
);
},
routes: [
GoRoute(
path: 'process',
name: 'workOrderProcess',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
return WorkOrderProcessPage(orderId: orderId);
},
),
GoRoute(
path: 'onsite',
name: 'workOrderOnSite',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
final workOrder = state.extra as WorkOrderEntity;
return WorkOrderOnSitePage(
workOrder: workOrder,
orderId: orderId,
);
},
),
GoRoute(
path: 'receipt',
name: 'workOrderReceipt',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
final workOrder = state.extra as WorkOrderEntity;
return WorkOrderReceiptPage(
workOrder: workOrder,
orderId: orderId,
);
},
),
],
),
],
),
];
}

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../domain/entities/workorder_entity.dart';
/// 工单标签栏组件
/// 展示待处理/执行中/已完成/全部四个静态标签
class WorkOrderTabBar extends StatelessWidget {
final WorkOrderStatus currentStatus;
final Function(WorkOrderStatus) onTabChanged;
@@ -15,25 +17,40 @@ class WorkOrderTabBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tabs = [
return Container(
height: 44,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 4,
itemBuilder: (context, index) {
final tabs = _getStaticTabs(context);
final tab = tabs[index];
return _buildTabItem(
context,
label: tab['label'] as String,
isSelected: currentStatus == tab['status'],
showBadge: tab['badge'] as bool,
onTap: () => onTabChanged(tab['status'] as WorkOrderStatus),
);
},
),
);
}
List<Map<String, dynamic>> _getStaticTabs(BuildContext context) {
return [
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.pending'),
'label': AppLocalizations.of(context).translate('work_order_v2.pending'),
'status': WorkOrderStatus.pending,
'badge': false,
},
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.executing'),
'label': AppLocalizations.of(context).translate('work_order_v2.executing'),
'status': WorkOrderStatus.executing,
'badge': true,
},
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.completed'),
'label': AppLocalizations.of(context).translate('work_order_v2.completed'),
'status': WorkOrderStatus.completed,
'badge': false,
},
@@ -43,69 +60,64 @@ class WorkOrderTabBar extends StatelessWidget {
'badge': true,
},
];
}
return Container(
height: 44,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: tabs.length,
itemBuilder: (context, index) {
final tab = tabs[index];
final isSelected = currentStatus == tab['status'];
return GestureDetector(
onTap: () => onTabChanged(tab['status'] as WorkOrderStatus),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Text(
tab['label'] as String,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? AppConstants.primaryColor
: AppConstants.secondaryTextColor,
),
),
if (tab['badge'] as bool)
Positioned(
right: -8,
top: -4,
child: Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
Widget _buildTabItem(
BuildContext context, {
required String label,
required bool isSelected,
required VoidCallback onTap,
bool showBadge = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected
? AppConstants.primaryColor
: AppConstants.secondaryTextColor,
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 4),
width: 24,
height: 3,
decoration: BoxDecoration(
color: AppConstants.primaryColor,
borderRadius: BorderRadius.circular(1.5),
),
if (showBadge)
Positioned(
right: -8,
top: -4,
child: Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
],
),
),
],
),
);
},
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 4),
width: 24,
height: 3,
decoration: BoxDecoration(
color: AppConstants.primaryColor,
borderRadius: BorderRadius.circular(1.5),
),
),
],
),
),
);
}
}
}

View File

@@ -1,7 +1,4 @@
/// WorkOrder 模块导出文件
///
/// 使用方式:
/// import 'package:your_app/features/v2/workorder/workorder.dart';
// Constants
export '../../../core/consts/workorder_consts.dart';
@@ -15,6 +12,7 @@ export 'domain/repositories/workorder_repository.dart';
// Domain - UseCases
export 'domain/usecases/get_workorder_list_usecase.dart';
export 'domain/usecases/get_workorder_detail_usecase.dart';
// Data - Models
export 'data/models/workorder_model.dart';
@@ -28,6 +26,7 @@ export 'data/repositories/workorder_repository_impl.dart';
// Presentation - Cubit
export 'presentation/cubit/workorder_cubit.dart';
export 'presentation/cubit/workorder_detail_cubit.dart';
// Presentation - Widgets
export 'presentation/widgets/workorder_tabbar.dart';
@@ -36,3 +35,4 @@ export 'presentation/widgets/workorder_item_card.dart';
// Presentation - Pages
export 'presentation/pages/workorder_page.dart';
export 'presentation/pages/workorder_detail_page.dart';