feat: 完成 Flutter 版智能电网 IoT 管理应用初始化搭建
This commit is contained in:
commit
f61b7bf90b
120 files changed
+15229
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/network/api_response.dart';
|
||||
import '../../../core/providers.dart';
|
||||
import 'visit_models.dart';
|
||||
|
||||
final visitApiProvider = Provider<VisitApi>(
|
||||
(ref) => VisitApi(ref.watch(apiClientProvider)),
|
||||
);
|
||||
|
||||
/// 拜访台账接口,对应 uni-app 版 `api/customerVisit.ts`。
|
||||
class VisitApi {
|
||||
VisitApi(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<ApiResponse<VisitPage>> list({
|
||||
int page = 1,
|
||||
int pageSize = 100,
|
||||
String? keyword,
|
||||
int? customerId,
|
||||
int? salesmanId,
|
||||
int? visitType,
|
||||
int? status,
|
||||
int? priority,
|
||||
String? planStartDate,
|
||||
String? planEndDate,
|
||||
}) => _client.get<VisitPage>(
|
||||
'/customer-visits',
|
||||
query: {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'keyword': keyword,
|
||||
'customerId': customerId,
|
||||
'salesmanId': salesmanId,
|
||||
'visitType': visitType,
|
||||
'status': status,
|
||||
'priority': priority,
|
||||
'planStartDate': planStartDate,
|
||||
'planEndDate': planEndDate,
|
||||
},
|
||||
parse: (d) {
|
||||
final m = (d as Map).cast<String, dynamic>();
|
||||
return VisitPage(
|
||||
list: (m['list'] as List? ?? [])
|
||||
.map((e) => VisitInfo.fromJson((e as Map).cast<String, dynamic>()))
|
||||
.toList(),
|
||||
total: (m['total'] as num?)?.toInt() ?? 0,
|
||||
page: (m['page'] as num?)?.toInt() ?? 1,
|
||||
pageSize: (m['pageSize'] as num?)?.toInt() ?? pageSize,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Future<ApiResponse<VisitStats>> stats() => _client.get<VisitStats>(
|
||||
'/customer-visits/stats',
|
||||
parse: (d) => VisitStats.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
Future<ApiResponse<VisitInfo>> detail(int id) => _client.get<VisitInfo>(
|
||||
'/customer-visits/$id',
|
||||
parse: (d) => VisitInfo.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
Future<ApiResponse<VisitInfo>> create(Map<String, dynamic> body) =>
|
||||
_client.post<VisitInfo>(
|
||||
'/customer-visits',
|
||||
body: body,
|
||||
parse: (d) => VisitInfo.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
/// 批量安排拜访:多选客户生成统一的待执行计划。
|
||||
Future<ApiResponse<VisitBatchResult>> batchCreate(Map<String, dynamic> body) =>
|
||||
_client.post<VisitBatchResult>(
|
||||
'/customer-visits/batch',
|
||||
body: body,
|
||||
parse: (d) => VisitBatchResult.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
Future<ApiResponse<VisitInfo>> update(int id, Map<String, dynamic> body) =>
|
||||
_client.put<VisitInfo>(
|
||||
'/customer-visits/$id',
|
||||
body: body,
|
||||
parse: (d) => VisitInfo.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
/// 状态流转:待执行 → 已完成 / 取消 等。
|
||||
Future<ApiResponse<VisitInfo>> updateStatus(int id, Map<String, dynamic> body) =>
|
||||
_client.put<VisitInfo>(
|
||||
'/customer-visits/$id/status',
|
||||
body: body,
|
||||
parse: (d) => VisitInfo.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
/// 改派:更换跟进业务员(仅 ADMIN)。
|
||||
Future<ApiResponse<VisitInfo>> reassign(int id, Map<String, dynamic> body) =>
|
||||
_client.put<VisitInfo>(
|
||||
'/customer-visits/$id/reassign',
|
||||
body: body,
|
||||
parse: (d) => VisitInfo.fromJson((d as Map).cast<String, dynamic>()),
|
||||
);
|
||||
|
||||
Future<ApiResponse<void>> delete(int id) => _client.delete<void>('/customer-visits/$id');
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/utils/id_util.dart';
|
||||
|
||||
/// 数值下拉选项。
|
||||
typedef ValueOption = ({int value, String label});
|
||||
|
||||
/// 拜访类型:1 首访摸排 2 方案沟通 3 商务二访 4 维保回访 5 应急巡检
|
||||
const visitTypeOptions = <ValueOption>[
|
||||
(value: 1, label: '首访摸排'),
|
||||
(value: 2, label: '方案沟通'),
|
||||
(value: 3, label: '商务二访'),
|
||||
(value: 4, label: '维保回访'),
|
||||
(value: 5, label: '应急巡检'),
|
||||
];
|
||||
|
||||
/// 拜访方式:1 现场拜访 2 商务拜访 3 电话沟通 4 线上会议
|
||||
const visitModeOptions = <ValueOption>[
|
||||
(value: 1, label: '现场拜访'),
|
||||
(value: 2, label: '商务拜访'),
|
||||
(value: 3, label: '电话沟通'),
|
||||
(value: 4, label: '线上会议'),
|
||||
];
|
||||
|
||||
/// 优先级:1 特急 P1 2 加急 P2 3 常规 P3
|
||||
const visitPriorityOptions = <ValueOption>[
|
||||
(value: 1, label: '特急 · P1'),
|
||||
(value: 2, label: '加急 · P2'),
|
||||
(value: 3, label: '常规 · P3'),
|
||||
];
|
||||
|
||||
/// 意向评级:1=A 高意向 2=B 中意向 3=C 低意向
|
||||
const visitIntentOptions = <ValueOption>[
|
||||
(value: 1, label: 'A级 · 高意向'),
|
||||
(value: 2, label: 'B级 · 中意向'),
|
||||
(value: 3, label: 'C级 · 低意向'),
|
||||
];
|
||||
|
||||
/// 状态:0 待执行 1 已完成待复核 2 取证待技术复核 3 已立项转勘察 4 已归档 5 已取消
|
||||
const visitStatusOptions = <ValueOption>[
|
||||
(value: 0, label: '待执行'),
|
||||
(value: 1, label: '已完成待复核'),
|
||||
(value: 2, label: '取证待技术复核'),
|
||||
(value: 3, label: '已立项转勘察'),
|
||||
(value: 4, label: '已归档'),
|
||||
(value: 5, label: '已取消'),
|
||||
];
|
||||
|
||||
abstract final class VisitStatus {
|
||||
static const pending = 0;
|
||||
static const done = 1;
|
||||
static const evidence = 2;
|
||||
static const survey = 3;
|
||||
static const archived = 4;
|
||||
static const canceled = 5;
|
||||
}
|
||||
|
||||
const visitStatusMap = <int, ({String text, ChipTone tone})>{
|
||||
0: (text: '待执行', tone: ChipTone.warn),
|
||||
1: (text: '已完成待复核', tone: ChipTone.primary),
|
||||
2: (text: '取证待技术复核', tone: ChipTone.secondary),
|
||||
3: (text: '已立项转勘察', tone: ChipTone.secondary),
|
||||
4: (text: '已归档', tone: ChipTone.tertiary),
|
||||
5: (text: '已取消', tone: ChipTone.error),
|
||||
};
|
||||
|
||||
const visitPriorityMap = <int, ({String text, ChipTone tone})>{
|
||||
1: (text: '特急 P1', tone: ChipTone.error),
|
||||
2: (text: '加急 P2', tone: ChipTone.warn),
|
||||
3: (text: '常规 P3', tone: ChipTone.muted),
|
||||
};
|
||||
|
||||
String visitStatusText(int? s) => visitStatusMap[s]?.text ?? '未知';
|
||||
|
||||
ChipTone visitStatusTone(int? s) => visitStatusMap[s]?.tone ?? ChipTone.muted;
|
||||
|
||||
String visitPriorityText(int? p) => visitPriorityMap[p]?.text ?? '—';
|
||||
|
||||
ChipTone visitPriorityTone(int? p) => visitPriorityMap[p]?.tone ?? ChipTone.muted;
|
||||
|
||||
String optionLabel(List<ValueOption> options, int? value) =>
|
||||
options.where((o) => o.value == value).map((o) => o.label).firstOrNull ?? '—';
|
||||
|
||||
/// 列表卡片左侧色条颜色。
|
||||
Color visitStatusBarColor(int status) => switch (status) {
|
||||
0 => AppColors.statusWarn,
|
||||
1 => AppColors.primary,
|
||||
2 || 3 => AppColors.secondary,
|
||||
4 => AppColors.tertiaryContainer,
|
||||
5 => AppColors.statusError,
|
||||
_ => AppColors.borderRule,
|
||||
};
|
||||
|
||||
/// 关联客户(列表/详情 include 的精简字段)。
|
||||
class VisitCustomer {
|
||||
const VisitCustomer({
|
||||
required this.id,
|
||||
this.customerNo,
|
||||
this.customerName,
|
||||
this.customerLevel,
|
||||
this.contactPerson,
|
||||
this.contactPhone,
|
||||
this.address,
|
||||
this.salesmanId,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String? customerNo;
|
||||
final String? customerName;
|
||||
final String? customerLevel;
|
||||
final String? contactPerson;
|
||||
final String? contactPhone;
|
||||
final String? address;
|
||||
final int? salesmanId;
|
||||
|
||||
factory VisitCustomer.fromJson(Map<String, dynamic> json) => VisitCustomer(
|
||||
id: toIntOr(json['id'], 0),
|
||||
customerNo: json['customerNo'] as String?,
|
||||
customerName: json['customerName'] as String?,
|
||||
customerLevel: json['customerLevel'] as String?,
|
||||
contactPerson: json['contactPerson'] as String?,
|
||||
contactPhone: json['contactPhone'] as String?,
|
||||
address: json['address'] as String?,
|
||||
salesmanId: toIntOrNull(json['salesmanId']),
|
||||
);
|
||||
}
|
||||
|
||||
/// 拜访台账,与后端 `/api/customer-visits` 契约对齐。
|
||||
class VisitInfo {
|
||||
const VisitInfo({
|
||||
required this.id,
|
||||
required this.visitNo,
|
||||
required this.customerId,
|
||||
required this.visitPerson,
|
||||
required this.status,
|
||||
this.visitPhone,
|
||||
this.visitContent,
|
||||
this.visitTime,
|
||||
this.salesmanId,
|
||||
this.willPlan,
|
||||
this.visitType,
|
||||
this.visitMode,
|
||||
this.priority,
|
||||
this.subject,
|
||||
this.planTime,
|
||||
this.planEndTime,
|
||||
this.checkinTime,
|
||||
this.checkinStatus,
|
||||
this.checkinLng,
|
||||
this.checkinLat,
|
||||
this.spaceDesc,
|
||||
this.intentLevel,
|
||||
this.intentDesc,
|
||||
this.demands,
|
||||
this.companion,
|
||||
this.receptionPerson,
|
||||
this.receptionTitle,
|
||||
this.receptionPhone,
|
||||
this.resultRating,
|
||||
this.nextVisitTime,
|
||||
this.nextAction,
|
||||
this.photos = const [],
|
||||
this.photoUrls = const [],
|
||||
this.surveyOrderId,
|
||||
this.projectNo,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.customer,
|
||||
this.salesmanName,
|
||||
this.salesmanPhone,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String visitNo;
|
||||
final int customerId;
|
||||
final String visitPerson;
|
||||
final int status;
|
||||
final String? visitPhone;
|
||||
final String? visitContent;
|
||||
final String? visitTime;
|
||||
final int? salesmanId;
|
||||
final int? willPlan;
|
||||
final int? visitType;
|
||||
final int? visitMode;
|
||||
final int? priority;
|
||||
final String? subject;
|
||||
final String? planTime;
|
||||
final String? planEndTime;
|
||||
final String? checkinTime;
|
||||
final int? checkinStatus;
|
||||
final double? checkinLng;
|
||||
final double? checkinLat;
|
||||
final String? spaceDesc;
|
||||
final int? intentLevel;
|
||||
final String? intentDesc;
|
||||
final String? demands;
|
||||
final String? companion;
|
||||
final String? receptionPerson;
|
||||
final String? receptionTitle;
|
||||
final String? receptionPhone;
|
||||
final String? resultRating;
|
||||
final String? nextVisitTime;
|
||||
final String? nextAction;
|
||||
|
||||
/// 现场照片对象 key 列表(持久化字段)
|
||||
final List<String> photos;
|
||||
|
||||
/// 现场照片预签名访问 URL 列表(后端动态生成)
|
||||
final List<String> photoUrls;
|
||||
final int? surveyOrderId;
|
||||
final String? projectNo;
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final VisitCustomer? customer;
|
||||
final String? salesmanName;
|
||||
final String? salesmanPhone;
|
||||
|
||||
factory VisitInfo.fromJson(Map<String, dynamic> json) => VisitInfo(
|
||||
id: toIntOr(json['id'], 0),
|
||||
visitNo: json['visitNo'] as String? ?? '',
|
||||
customerId: toIntOr(json['customerId'], 0),
|
||||
visitPerson: json['visitPerson'] as String? ?? '',
|
||||
status: toIntOr(json['status'], 0),
|
||||
visitPhone: json['visitPhone'] as String?,
|
||||
visitContent: json['visitContent'] as String?,
|
||||
visitTime: json['visitTime'] as String?,
|
||||
salesmanId: toIntOrNull(json['salesmanId']),
|
||||
willPlan: toIntOrNull(json['willPlan']),
|
||||
visitType: toIntOrNull(json['visitType']),
|
||||
visitMode: toIntOrNull(json['visitMode']),
|
||||
priority: toIntOrNull(json['priority']),
|
||||
subject: json['subject'] as String?,
|
||||
planTime: json['planTime'] as String?,
|
||||
planEndTime: json['planEndTime'] as String?,
|
||||
checkinTime: json['checkinTime'] as String?,
|
||||
checkinStatus: toIntOrNull(json['checkinStatus']),
|
||||
checkinLng: (json['checkinLng'] as num?)?.toDouble(),
|
||||
checkinLat: (json['checkinLat'] as num?)?.toDouble(),
|
||||
spaceDesc: json['spaceDesc'] as String?,
|
||||
intentLevel: toIntOrNull(json['intentLevel']),
|
||||
intentDesc: json['intentDesc'] as String?,
|
||||
demands: json['demands'] as String?,
|
||||
companion: json['companion'] as String?,
|
||||
receptionPerson: json['receptionPerson'] as String?,
|
||||
receptionTitle: json['receptionTitle'] as String?,
|
||||
receptionPhone: json['receptionPhone'] as String?,
|
||||
resultRating: json['resultRating'] as String?,
|
||||
nextVisitTime: json['nextVisitTime'] as String?,
|
||||
nextAction: json['nextAction'] as String?,
|
||||
photos: (json['photos'] as List? ?? []).map((e) => e.toString()).toList(),
|
||||
photoUrls: (json['photoUrls'] as List? ?? []).map((e) => e.toString()).toList(),
|
||||
surveyOrderId: toIntOrNull(json['surveyOrderId']),
|
||||
projectNo: json['projectNo'] as String?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
updatedAt: json['updated_at'] as String?,
|
||||
customer: json['customer'] == null
|
||||
? null
|
||||
: VisitCustomer.fromJson((json['customer'] as Map).cast<String, dynamic>()),
|
||||
salesmanName: json['salesmanName'] as String?,
|
||||
salesmanPhone: json['salesmanPhone'] as String?,
|
||||
);
|
||||
|
||||
String get customerName =>
|
||||
customer?.customerName ?? '客户 #$customerId';
|
||||
}
|
||||
|
||||
class VisitPage {
|
||||
const VisitPage({
|
||||
required this.list,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
final List<VisitInfo> list;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
}
|
||||
|
||||
/// KPI 统计卡数据。
|
||||
class VisitStats {
|
||||
const VisitStats({
|
||||
required this.total,
|
||||
required this.pending,
|
||||
required this.todayPlan,
|
||||
required this.urgentPending,
|
||||
required this.conversionRate,
|
||||
required this.pendingReview,
|
||||
required this.completed,
|
||||
required this.converted,
|
||||
required this.intentTotal,
|
||||
});
|
||||
|
||||
final int total;
|
||||
final int pending;
|
||||
final int todayPlan;
|
||||
final int urgentPending;
|
||||
final num conversionRate;
|
||||
final int pendingReview;
|
||||
final int completed;
|
||||
final int converted;
|
||||
final int intentTotal;
|
||||
|
||||
factory VisitStats.fromJson(Map<String, dynamic> json) => VisitStats(
|
||||
total: toIntOr(json['total'], 0),
|
||||
pending: toIntOr(json['pending'], 0),
|
||||
todayPlan: toIntOr(json['todayPlan'], 0),
|
||||
urgentPending: toIntOr(json['urgentPending'], 0),
|
||||
conversionRate: (json['conversionRate'] as num?) ?? 0,
|
||||
pendingReview: toIntOr(json['pendingReview'], 0),
|
||||
completed: toIntOr(json['completed'], 0),
|
||||
converted: toIntOr(json['converted'], 0),
|
||||
intentTotal: toIntOr(json['intentTotal'], 0),
|
||||
);
|
||||
|
||||
static const empty = VisitStats(
|
||||
total: 0,
|
||||
pending: 0,
|
||||
todayPlan: 0,
|
||||
urgentPending: 0,
|
||||
conversionRate: 0,
|
||||
pendingReview: 0,
|
||||
completed: 0,
|
||||
converted: 0,
|
||||
intentTotal: 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 批量安排拜访结果。
|
||||
class VisitBatchResult {
|
||||
const VisitBatchResult({required this.total, required this.ids});
|
||||
|
||||
final int total;
|
||||
final List<int> ids;
|
||||
|
||||
factory VisitBatchResult.fromJson(Map<String, dynamic> json) => VisitBatchResult(
|
||||
total: toIntOr(json['total'], 0),
|
||||
ids: (json['ids'] as List? ?? []).map((e) => toIntOr(e, 0)).toList(),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user