feat: 新增勘察派单功能及拜访详情页操作

新增客户空间拓扑API、勘察明细模型,重构勘察接口,新增拜访详情页操作逻辑及新建勘察派单页面
This commit is contained in:
andyesfly committed 2026-10-17 10:10:02 +08:00
1 parent 7ad604ff8b
commit 83df770eaa
10 files changed
+2788 -529

No files matched your search

+19 -25
View File
@@ -9,23 +9,6 @@ final surveyApiProvider = Provider<SurveyApi>(
(ref) => SurveyApi(ref.watch(apiClientProvider)),
);
/// SOP 回执增量保存结果。
class SopSaveResult {
const SopSaveResult({required this.updated, required this.skipped, required this.sopResults});
final int updated;
final List<int> skipped;
final List<SurveySopResult> sopResults;
factory SopSaveResult.fromJson(Map<String, dynamic> json) => SopSaveResult(
updated: (json['updated'] as num?)?.toInt() ?? 0,
skipped: (json['skipped'] as List? ?? []).map((e) => (e as num).toInt()).toList(),
sopResults: (json['sopResults'] as List? ?? [])
.map((e) => SurveySopResult.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
);
}
/// 勘察工单接口,对应 uni-app 版 `api/survey.ts`。
class SurveyApi {
SurveyApi(this._client);
@@ -77,6 +60,14 @@ class SurveyApi {
parse: (d) => SurveyItem.fromJson((d as Map).cast<String, dynamic>()),
);
/// 新建勘察派单:workerId 可省略,后端默认落「待指派(0)」。
Future<ApiResponse<SurveyItem>> create(Map<String, dynamic> body) =>
_client.post<SurveyItem>(
'/survey-orders',
body: body,
parse: (d) => SurveyItem.fromJson((d as Map).cast<String, dynamic>()),
);
Future<ApiResponse<SurveyItem>> update(int id, Map<String, dynamic> body) =>
_client.put<SurveyItem>(
'/survey-orders/$id',
@@ -84,12 +75,15 @@ class SurveyApi {
parse: (d) => SurveyItem.fromJson((d as Map).cast<String, dynamic>()),
);
/// 按 SOP 检查项增量保存现场回执;
/// 仅现场勘察中(status=1)且工单对当前用户可见时后端才放行。
Future<ApiResponse<SopSaveResult>> saveSopResults(int id, List<SopResultPayload> results) =>
_client.put<SopSaveResult>(
'/survey-orders/$id/sop-results',
body: {'results': results.map((e) => e.toJson()).toList()},
parse: (d) => SopSaveResult.fromJson((d as Map).cast<String, dynamic>()),
);
/// 全量替换工单勘察明细(新增/编辑/删除统一走此接口,返回最新明细)。
Future<ApiResponse<List<SurveyOrderDetail>>> saveDetails(
int id,
List<SurveyOrderDetail> details,
) => _client.put<List<SurveyOrderDetail>>(
'/survey-orders/$id/details',
body: {'details': details.map((e) => e.toJson()).toList()},
parse: (d) => (d as List? ?? [])
.map((e) => SurveyOrderDetail.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
);
}
+51 -58
View File
@@ -94,69 +94,62 @@ class SurveyWorker {
);
}
class SurveySopResult {
const SurveySopResult({
required this.id,
required this.surveyOrderId,
required this.sopItemId,
required this.checked,
required this.itemName,
this.itemDesc = '',
this.receiptType = '',
this.receiptRequire = '',
this.required = false,
this.receiptValue = const [],
this.receiptUrls = const [],
/// 勘察明细:一条 = 客户空间拓扑中的 建筑 + 楼层 + 房间(选填) + 现场照片 + 备注。
class SurveyOrderDetail {
const SurveyOrderDetail({
this.id,
this.surveyOrderId,
this.buildingId,
this.buildingName = '',
this.floorNo = '',
this.roomId,
this.roomName = '',
this.photos = const [],
this.photoUrls = const [],
this.remark = '',
});
final int id;
final int surveyOrderId;
final int sopItemId;
final bool checked;
final String itemName;
final String itemDesc;
final String receiptType;
final String receiptRequire;
final bool required;
final int? id;
final int? surveyOrderId;
final int? buildingId;
/// 回执内容:photo/meter 类为照片对象 key 数组,form 类为单元素文本值数组。
final List<String> receiptValue;
/// 建筑名称(冗余存储,客户拓扑变更后仍可读)。
final String buildingName;
/// 详情接口附带:照片类回执的预签名访问 URL(与 receiptValue 顺序一致)。
final List<String> receiptUrls;
/// 楼层:正数为地上、负数为地下,如 `"1"` / `"-1"`。
final String floorNo;
final int? roomId;
final String roomName;
/// 现场照片对象 key。
final List<String> photos;
/// 详情接口附带:照片预签名访问 URL(与 photos 顺序一致)。
final List<String> photoUrls;
final String remark;
factory SurveySopResult.fromJson(Map<String, dynamic> json) => SurveySopResult(
id: toIntOr(json['id'], 0),
surveyOrderId: toIntOr(json['surveyOrderId'], 0),
sopItemId: toIntOr(json['sopItemId'], 0),
checked: json['checked'] == true,
itemName: json['itemName'] as String? ?? '',
itemDesc: json['itemDesc'] as String? ?? '',
receiptType: json['receiptType'] as String? ?? '',
receiptRequire: json['receiptRequire'] as String? ?? '',
required: json['required'] == true,
receiptValue: (json['receiptValue'] as List? ?? []).map((e) => e.toString()).toList(),
receiptUrls: (json['receiptUrls'] as List? ?? []).map((e) => e.toString()).toList(),
factory SurveyOrderDetail.fromJson(Map<String, dynamic> json) => SurveyOrderDetail(
id: toIntOrNull(json['id']),
surveyOrderId: toIntOrNull(json['surveyOrderId']),
buildingId: toIntOrNull(json['buildingId']),
buildingName: json['buildingName'] as String? ?? '',
floorNo: json['floorNo'] == null ? '' : json['floorNo'].toString(),
roomId: toIntOrNull(json['roomId']),
roomName: json['roomName'] as String? ?? '',
photos: (json['photos'] as List? ?? []).map((e) => e.toString()).toList(),
photoUrls: (json['photoUrls'] as List? ?? []).map((e) => e.toString()).toList(),
remark: json['remark'] as String? ?? '',
);
}
/// 逐项回执的增量提交体:只传需要变更的 SOP 项,未传项保持原值。
class SopResultPayload {
const SopResultPayload({required this.sopItemId, this.checked, this.receiptValue, this.remark});
final int sopItemId;
final bool? checked;
final List<String>? receiptValue;
final String? remark;
/// 提交体:只带定位要素与照片/备注,行 id 由后端全量重建。
Map<String, dynamic> toJson() => {
'sopItemId': sopItemId,
if (checked != null) 'checked': checked,
if (receiptValue != null) 'receiptValue': receiptValue,
if (remark != null) 'remark': remark,
'buildingId': buildingId,
'buildingName': buildingName,
'floorNo': floorNo,
'roomId': roomId,
'roomName': roomName,
'photos': photos,
'remark': remark,
};
}
@@ -204,7 +197,7 @@ class SurveyItem {
this.reviewedAt,
this.customer,
this.worker,
this.sopResults,
this.details,
});
final int id;
@@ -251,8 +244,8 @@ class SurveyItem {
final String salesmanName;
final String salesmanPhone;
// 仅详情接口返回
final List<SurveySopResult>? sopResults;
// 仅详情接口返回:勘察明细(建筑/楼层/房间 + 现场照片)
final List<SurveyOrderDetail>? details;
factory SurveyItem.fromJson(Map<String, dynamic> json) => SurveyItem(
id: toIntOr(json['id'], 0),
@@ -300,10 +293,10 @@ class SurveyItem {
workerName: json['workerName'] as String? ?? '',
salesmanName: json['salesmanName'] as String? ?? '',
salesmanPhone: json['salesmanPhone'] as String? ?? '',
sopResults: json['sopResults'] == null
details: json['details'] == null
? null
: (json['sopResults'] as List)
.map((e) => SurveySopResult.fromJson((e as Map).cast<String, dynamic>()))
: (json['details'] as List)
.map((e) => SurveyOrderDetail.fromJson((e as Map).cast<String, dynamic>()))
.toList(),
);