一次大的提交
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'] ?? '未知错误'}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/features/v2/workorder/presentation/cubit/transfer_cubit.dart
Normal file
133
lib/features/v2/workorder/presentation/cubit/transfer_cubit.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user