feat: 新增勘察明细管理与修改密码功能

This commit is contained in:
andyesfly committed 2026-10-17 10:10:09 +08:00
1 parent 56830e94ec
commit 774403785c
12 files changed
+1553 -18

No files matched your search

+13
View File
@@ -33,6 +33,19 @@ class OrderApi {
Future<ApiResponse<void>> grab(String source, int id) =>
_client.post<void>('/worker-app/orders/$source/$id/grab');
/// 保存勘察明细(全量替换)。仅本人已接单且状态为「现场勘察中」的勘察单可写,
/// 成功返回服务端最新明细(含预签名 `photoUrls`)。
Future<ApiResponse<List<SurveyDetail>>> saveSurveyDetails(
int id,
List<SurveyDetail> details,
) => _client.put<List<SurveyDetail>>(
'/worker-app/orders/survey/$id/details',
body: {'details': details.map((d) => d.toPayload()).toList()},
parse: (d) => (d as List? ?? const [])
.map((e) => SurveyDetail.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
);
/// 完成订单。非本人订单后端返回 403。
Future<ApiResponse<void>> complete(String source, int id) =>
_client.put<void>('/worker-app/orders/$source/$id/complete');
+173
View File
@@ -22,6 +22,8 @@ class Order {
required this.laborFee,
required this.materialFee,
required this.estimatedDuration,
this.details = const [],
this.topology,
});
/// `repair-12`
@@ -52,6 +54,18 @@ class Order {
final double materialFee;
final String estimatedDuration;
/// 勘察明细(仅 `source == "survey"` 的详情接口返回)。
final List<SurveyDetail> details;
/// 客户空间拓扑(仅 `source == "survey"` 的详情接口返回)。
final CustomerTopology? topology;
/// 是否为现场勘察单。
bool get isSurvey => source == 'survey';
/// 是否处于「现场勘察中(1)」,仅该状态可维护勘察明细。
bool get surveyEditable => source == 'survey' && status == 1;
/// 来源中文名,用于收益明细副标题等场景。
String get sourceName => switch (source) {
'repair' => '家庭电路维修',
@@ -109,5 +123,164 @@ class Order {
laborFee: toDoubleOr(json['laborFee'], 0),
materialFee: toDoubleOr(json['materialFee'], 0),
estimatedDuration: json['estimatedDuration'] as String? ?? '',
details: (json['details'] as List? ?? const [])
.map((e) => SurveyDetail.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
topology: json['topology'] is Map
? CustomerTopology.fromJson(
(json['topology'] as Map).cast<String, dynamic>(),
)
: null,
);
}
/// 勘察明细:客户空间拓扑中的「建筑 + 楼层 + 房间(选填) + 现场照片 + 备注」。
class SurveyDetail {
const SurveyDetail({
this.id = 0,
required this.buildingId,
required this.buildingName,
required this.floorNo,
this.roomId,
this.roomName = '',
this.photos = const [],
this.photoUrls = const [],
this.remark = '',
});
/// 后端主键,新增行传 0(服务端忽略)。
final int id;
final int buildingId;
final String buildingName;
/// 楼层:地上 `"1"`、地下 `"-1"`。
final String floorNo;
final int? roomId;
final String roomName;
/// 对象存储 key,提交时传数组。
final List<String> photos;
/// 预签名访问 URL,仅用于预览。
final List<String> photoUrls;
final String remark;
/// 楼层展示文案:正数 `N层`、负数 `负N层`,无值返回空串。
String get floorLabel {
final n = int.tryParse(floorNo);
if (n == null || n == 0) return '';
return n > 0 ? '$n层' : '负${-n}层';
}
factory SurveyDetail.fromJson(Map<String, dynamic> json) => SurveyDetail(
id: toIntOr(json['id'], 0),
buildingId: toIntOr(json['buildingId'], 0),
buildingName: json['buildingName'] as String? ?? '',
floorNo: json['floorNo']?.toString() ?? '',
roomId: toIntOrNull(json['roomId']),
roomName: json['roomName'] as String? ?? '',
photos:
(json['photos'] as List?)?.map((e) => e.toString()).toList() ??
const [],
photoUrls:
(json['photoUrls'] as List?)?.map((e) => e.toString()).toList() ??
const [],
remark: json['remark'] as String? ?? '',
);
/// 提交给后端的字段(全量替换数组的元素)。
Map<String, dynamic> toPayload() => {
'buildingId': buildingId,
'buildingName': buildingName,
'floorNo': floorNo,
'roomId': roomId,
'roomName': roomName,
'photos': photos,
'remark': remark,
};
}
/// 拓扑建筑:`floorCount` 地上层数、`floorDownCount` 地下层数。
class TopologyBuilding {
const TopologyBuilding({
required this.id,
required this.buildingName,
required this.floorCount,
required this.floorDownCount,
});
final int id;
final String buildingName;
final int floorCount;
final int floorDownCount;
factory TopologyBuilding.fromJson(Map<String, dynamic> json) =>
TopologyBuilding(
id: toIntOr(json['id'], 0),
buildingName: json['buildingName'] as String? ?? '',
floorCount: toIntOr(json['floorCount'], 0),
floorDownCount: toIntOr(json['floorDownCount'], 0),
);
/// 生成楼层选项:value 地上 `"1"`/地下 `"-1"`,label `"1层"`/`"负1层"`。
List<({String value, String label})> get floorOptions => [
for (var i = 1; i <= floorCount; i++) (value: '$i', label: '$i层'),
for (var i = 1; i <= floorDownCount; i++) (value: '-$i', label: '负$i层'),
];
}
/// 拓扑房间:按 `buildingId` + `floorNo` 归属。
class TopologyRoom {
const TopologyRoom({
required this.id,
required this.roomName,
required this.buildingId,
required this.floorNo,
});
final int id;
final String roomName;
final int buildingId;
final String floorNo;
factory TopologyRoom.fromJson(Map<String, dynamic> json) => TopologyRoom(
id: toIntOr(json['id'], 0),
roomName: json['roomName'] as String? ?? '',
buildingId: toIntOr(json['buildingId'], 0),
floorNo: json['floorNo']?.toString() ?? '',
);
}
/// 客户空间拓扑:建筑 + 房间。
class CustomerTopology {
const CustomerTopology({this.buildings = const [], this.rooms = const []});
final List<TopologyBuilding> buildings;
final List<TopologyRoom> rooms;
factory CustomerTopology.fromJson(Map<String, dynamic> json) =>
CustomerTopology(
buildings: (json['buildings'] as List? ?? const [])
.map(
(e) => TopologyBuilding.fromJson((e as Map).cast<String, dynamic>()),
)
.toList(),
rooms: (json['rooms'] as List? ?? const [])
.map((e) => TopologyRoom.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
);
/// 按建筑 + 楼层过滤房间,用于明细编辑时的房间下拉。
List<TopologyRoom> roomsOf(int buildingId, String floorNo) => rooms
.where((r) => r.buildingId == buildingId && r.floorNo == floorNo)
.toList();
/// 按 id 查建筑。
TopologyBuilding? buildingOf(int id) {
for (final b in buildings) {
if (b.id == id) return b;
}
return null;
}
}