Merge remote-tracking branch 'origin/master'

# Conflicts:
#	lib/pages/home/home_page.dart
master
hs 2026-04-13 09:00:32 +08:00
commit 95c410bf1b
13 changed files with 3335 additions and 13 deletions

View File

@ -477,9 +477,10 @@ enum UploadFileType {
/// - : '609', : 'enclosed_area_personnel_applicant_signature'
enclosedAreaPersonnelApplicantSignature('609', 'enclosed_area_personnel_applicant_signature'),
/// - : '610', : 'enclosed_area_vehicle_applicant_signature'
enclosedAreaVehicleApplicantSignature('610', 'enclosed_area_vehicle_applicant_signature');
enclosedAreaVehicleApplicantSignature('610', 'enclosed_area_vehicle_applicant_signature'),
/// - : '168', : 'key_project_safety_management_agreement'
keyProjectSafetyManagementAgreement('168', 'key_project_safety_management_agreement');
const UploadFileType(this.type, this.path);

View File

@ -0,0 +1,211 @@
// lib/widgets/map_preview_widget.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/tools/asset_server.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// MapPreviewWidgetmarkers
/// points
/// [ { "longitude": 116.397428, "latitude": 39.90923, "iconPath": "/static/marker50.png" }, ... ]
class MapPreviewWidget extends StatefulWidget {
const MapPreviewWidget({
Key? key,
required this.points,
this.width = 160,
this.height = 160,
this.borderRadius = 6,
this.showBorder = true,
this.defaultLongitude = 116.397428,
this.defaultLatitude = 39.90923,
this.defaultZoom = 15,
}) : super(key: key);
final List<Map<String, dynamic>> points;
final double width;
final double height;
final double borderRadius;
final bool showBorder;
/// points 使
final double defaultLongitude;
final double defaultLatitude;
final int defaultZoom;
@override
State<MapPreviewWidget> createState() => _MapPreviewWidgetState();
}
class _MapPreviewWidgetState extends State<MapPreviewWidget> {
late final WebViewController _controller;
bool _loading = true;
Uri? _baseUri;
@override
void initState() {
super.initState();
// controller JS channel
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) async {
// +
await _injectCenterAndBlock();
setState(() {
_loading = false;
});
},
onWebResourceError: (err) {
debugPrint('[MapPreview] WebResourceError: $err');
},
));
// server page
_initServerAndLoad();
}
Future<void> _initServerAndLoad() async {
_baseUri = await AssetServer().start(); // asset server
// points JSON URLmap.html point
final jsonPoints = jsonEncode(widget.points);
final encoded = Uri.encodeComponent(jsonPoints);
final uri = Uri.parse('${_baseUri.toString()}/map.html?point=$encoded&t=${DateTime.now().millisecondsSinceEpoch}');
debugPrint('[MapPreview] load url: $uri');
try {
await _controller.loadRequest(uri);
} catch (e) {
debugPrint('[MapPreview] loadRequest failed: $e');
}
}
// null
Map<String, double>? _computeAverageCenter() {
double sumLon = 0.0, sumLat = 0.0;
int count = 0;
for (final p in widget.points) {
try {
final lon = _toDouble(p['longitude'] ?? p['longitue'] ?? p['long']);
final lat = _toDouble(p['latitude'] ?? p['lat']);
if (lon != null && lat != null) {
sumLon += lon;
sumLat += lat;
count++;
}
} catch (_) {}
}
if (count == 0) return null;
return {'longitude': sumLon / count, 'latitude': sumLat / count};
}
double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is double) return v;
if (v is int) return v.toDouble();
if (v is String) {
final cleaned = v.replaceAll(RegExp(r'[^\d\.\-]'), '');
return double.tryParse(cleaned);
}
return null;
}
// JS
Future<void> _injectCenterAndBlock() async {
final center = _computeAverageCenter();
final lon = center != null ? center['longitude']! : widget.defaultLongitude;
final lat = center != null ? center['latitude']! : widget.defaultLatitude;
final zoom = widget.defaultZoom;
// JS API
// div pointerEvents
final js = '''
(function(){
try {
var lon = ${lon.toString()};
var lat = ${lat.toString()};
var zoom = ${zoom.toString()};
// try custom setter
try {
if (typeof window.setMapCenter === 'function') {
window.setMapCenter(lon, lat, zoom);
}
} catch(e) {}
// Try T.Map style (used in your map.html)
try {
if (typeof map !== 'undefined' && map) {
if (typeof map.centerAndZoom === 'function' && typeof T !== 'undefined') {
try { map.centerAndZoom(new T.LngLat(lon, lat), zoom); } catch(e) {}
} else if (typeof map.setCenter === 'function') {
try { map.setCenter(new BMap.Point(lon, lat)); if (typeof map.setZoom === 'function') map.setZoom(zoom); } catch(e) {}
}
}
} catch(e) {}
// Add a full-page transparent blocker DIV to prevent interactions
try {
// avoid duplicate block
if (!document.getElementById('flutter_preview_block')) {
var block = document.createElement('div');
block.id = 'flutter_preview_block';
block.style.position = 'fixed';
block.style.left = '0';
block.style.top = '0';
block.style.width = '100%';
block.style.height = '100%';
block.style.zIndex = '2147483647'; // very high
block.style.background = 'transparent';
// pointerEvents 'auto' means this div captures events and prevents underlying map from receiving them
block.style.pointerEvents = 'auto';
// ensure it doesn't block CSS visuals (transparent)
document.documentElement.appendChild(block);
}
} catch (e) { console.log('preview block insert err', e); }
} catch(e) {
console.log('preview inject error', e);
}
})();
''';
try {
await _controller.runJavaScript(js);
debugPrint('[MapPreview] injected center + blocker');
} catch (e) {
debugPrint('[MapPreview] inject error: $e');
}
}
@override
void dispose() {
// AssetServer
super.dispose();
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(widget.borderRadius),
child: Container(
width: widget.width,
height: widget.height,
decoration: widget.showBorder
? BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(widget.borderRadius),
)
: null,
child: Stack(
children: [
// WebView使 AbsorbPointer JS
WebViewWidget(controller: _controller),
if (_loading)
const Positioned.fill(
child: Center(
child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2)),
),
),
],
),
),
);
}
}

View File

@ -9,11 +9,11 @@ class ApiService {
static final bool isProduct = true;
///
static final String basePath = "https://skqhdg.porthebei.com:9007";
// static final String basePath =
// isProduct
// ? "https://gbs-gateway.qhdsafety.com"
// : "http://192.168.20.100:30140";
// static final String basePath = "https://skqhdg.porthebei.com:9007";
static final String basePath =
isProduct
? "https://gbs-gateway.qhdsafety.com"
: "http://192.168.20.100:30140";
///

View File

@ -0,0 +1,59 @@
import 'package:dio/dio.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/http/HttpManager.dart';
import 'package:qhd_prevention/services/SessionService.dart';
class KeyTasksApi {
/// --
static Future<Map<String, dynamic>> getKeyTasksConfirmList(Map data) {
return HttpManager().request(
'${ApiService.basePath}/keyProject',
'/keyProject/pageConfirm',
method: Method.post,
data: {
...data
},
);
}
///
static Future<Map<String, dynamic>> getKeyTasksConfirmDetail(String id) {
return HttpManager().request(
'${ApiService.basePath}/keyProject',
'/keyProject/$id',
method: Method.get,
data: {
// ...data
},
);
}
///
static Future<Map<String, dynamic>> upKeyTasksData(Map data) {
return HttpManager().request(
'${ApiService.basePath}/keyProject',
'/keyProject/editStatus',
method: Method.post,
data: {
...data
},
);
}
/// -
static Future<Map<String, dynamic>> getKeyTasksListCameraAll(String type) {
return HttpManager().request(
'${ApiService.basePath}/keyProject',
'/keyProjectCamera/listCameraAll',
method: Method.post,
data: {
// ...data
'cameraType':type,
},
);
}
}

View File

@ -10,8 +10,8 @@ import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/pages/home/Study/study_tab_list_page.dart';
import 'package:qhd_prevention/pages/home/Tap/work_tab_list_page.dart';
import 'package:qhd_prevention/pages/home/doorAndCar/doorCar_tab_page.dart';
import 'package:qhd_prevention/pages/home/keyTasks/key_tasks_tab_page.dart';
import 'package:qhd_prevention/pages/home/scan_page.dart';
import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart';
import 'package:qhd_prevention/pages/main_tab.dart';
@ -120,7 +120,7 @@ class HomePageState extends RouteAwareState<HomePage>
"现场监管": "dashboard-Site-Supervision",
"危险作业": "dashboard-Hazardous-Work",
"隐患治理": "dashboard-Hazard-Management",
"重点作业": "", //
"重点作业": "dashboard-Hazard-Management", //
"口门门禁": "dashboard-Gate-Access-Control",
"入港培训": "dashboard-Study-Training",
};
@ -637,10 +637,9 @@ class HomePageState extends RouteAwareState<HomePage>
case "口门门禁":
pushPage(DoorcarTabPage(), context);
break;
case "危险作业":
pushPage(WorkTabListPage(), context);
break;
case "重点作业":
pushPage(KeyTasksTabPage(), context);
break;
default:
ToastUtil.showNormal(context, '功能开发中...');
break;

View File

@ -0,0 +1,716 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:qhd_prevention/common/route_service.dart';
import 'package:qhd_prevention/constants/app_enums.dart';
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
import 'package:qhd_prevention/customWidget/MultiDictValuesPicker.dart';
import 'package:qhd_prevention/customWidget/bottom_picker.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/department_person_picker.dart';
import 'package:qhd_prevention/customWidget/department_picker.dart';
import 'package:qhd_prevention/customWidget/dotted_border_box.dart';
import 'package:qhd_prevention/customWidget/item_list_widget.dart';
import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/http/modules/safety_check_api.dart';
import 'package:qhd_prevention/pages/mine/mine_sign_page.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/services/SessionService.dart';
import 'package:qhd_prevention/tools/HiddenListTable.dart';
import 'package:qhd_prevention/tools/MultiTextFieldWithTitle.dart';
import 'package:qhd_prevention/tools/tools.dart';
enum SafeCheckDetailMode {
add, //
edit, //
detail, //
}
/// SafecheckDetailWidget/// push pop null
class KeyTaskesDetailWidget extends StatefulWidget {
const KeyTaskesDetailWidget({
super.key,
required this.inspectionId,
required this.handleType,
this.autoFetch = true,
this.initialData,
this.initialForm,
this.initialInspectorList,
this.initialSituationList,
this.initialDangerList,
this.initialPersonUnderInspection,
this.initialInitiator,
});
final String inspectionId;
final SafeCheckDetailMode handleType;
/// true false SafetyCheckApi.safeCheckDetail
final bool autoFetch;
/// data result['data'] initialData
final Map<String, dynamic>? initialData;
// 使
final Map<String, dynamic>? initialForm;
final List<dynamic>? initialInspectorList;
final List<dynamic>? initialSituationList;
final List<dynamic>? initialDangerList;
final Map<String, dynamic>? initialPersonUnderInspection;
final Map<String, dynamic>? initialInitiator;
@override
State<KeyTaskesDetailWidget> createState() => KeyTaskesDetailWidgetState();
}
/// GlobalKey<SafecheckDetailWidgetState> 访 setDetailData(...)
class KeyTaskesDetailWidgetState extends State<KeyTaskesDetailWidget> {
//
bool _isEdit = false;
///
Map<String, dynamic> personUnderInspection = {
'departmentId': '',
'departmentName': '',
'userName': '',
'userId': '',
};
//
late List<dynamic> inspectorList = [];
// +
late Map<String, dynamic> initiator = {};
///
late List<dynamic> inspectorVerifyList = [];
///
final subjectList = [
{'bianma': "安全检查", 'name': "安全"},
{'bianma': "环保检查", 'name': "环保"},
{'bianma': "综合检查", 'name': "综合"},
];
///
late List<dynamic> toCheckUnitList = [];
//
late List<dynamic> situationList = [];
bool? chooseTitleType = null;
//
final Map<String, List<Map<String, dynamic>>> _personCache = {};
//
List<dynamic> dangerList = [];
List<String> signImages = [];
List<String> signTimes = []; //
Map<String, dynamic> form = {
'source': '5',
// 4- 5-
};
@override
void initState() {
super.initState();
_isEdit = false;
// initialData data
if (widget.initialData != null) {
setDetailData(widget.initialData!);
return;
}
// inspectionId autoFetch true
if (widget.inspectionId.isNotEmpty && widget.autoFetch) {
_getDetail();
} else {
// 使
form = widget.initialForm ?? form;
inspectorList = widget.initialInspectorList ?? [];
initiator = widget.initialInitiator ?? {};
inspectorVerifyList = [];
toCheckUnitList = [];
situationList = widget.initialSituationList ?? [];
personUnderInspection =
widget.initialPersonUnderInspection ?? personUnderInspection;
dangerList = widget.initialDangerList ?? [];
}
}
/// data
void setDetailData(Map<String, dynamic> data) {
setState(() {
form = data;
for (Map subject in subjectList) {
if (subject['bianma'] == form['subject']) {
form['subject'] = subject['name'];
}
}
personUnderInspection = data['inspectedPartyConfirmation'] ?? {};
inspectorList = data['inspectorVerificationList'] ?? [];
initiator = data['initiator'] ?? {};
inspectorVerifyList = data['inspectorVerifyList'] ?? [];
toCheckUnitList = data['toCheckUnitList'] ?? [];
situationList = data['content'] ?? [];
// /
_getAllFiles();
_getDangerList();
});
}
//
Future<void> _getAllFiles() async {
int idex = 0;
for (var item in situationList) {
final contentId = item['contentId'] ?? '';
if (contentId.length == 0) {
return;
}
await FileApi.getImagePathWithType(
contentId,
'',
UploadFileType.safetyEnvironmentalInspectionInspectionSituation,
)
.then((result) {
final data = result['data'];
final filePath =
(data is List && data.isNotEmpty)
? data.first['filePath'] ?? ''
: '';
item['imgPath'] = ApiService.baseImgPath + filePath;
situationList[idex] = item;
})
.catchError((_) {
//
});
idex++;
}
setState(() {});
}
///
Future<void> _getDetail() async {
try {
LoadingDialogHelper.show();
final result = await SafetyCheckApi.safeCheckDetail(widget.inspectionId);
LoadingDialogHelper.hide();
if (result != null) {
setState(() {
final data = result['data'];
form = data;
for (Map subject in subjectList) {
if (subject['bianma'] == form['subject']) {
form['subject'] = subject['name'];
}
}
personUnderInspection = data['inspectedPartyConfirmation'] ?? {};
inspectorList = data['inspectorVerificationList'] ?? [];
initiator = data['initiator'] ?? {};
inspectorVerifyList = data['inspectorVerifyList'] ?? [];
toCheckUnitList = data['toCheckUnitList'] ?? [];
situationList = data['content'] ?? [];
_getAllFiles();
_getDangerList();
_getAppealList(form['traceId'] ??'');
});
}
} catch (e) {
LoadingDialogHelper.hide();
ToastUtil.showNormal(context, '详情获取失败');
}
}
///
Future<void> _getAppealList(String traceId) async {
try {
// final parentPerm = 'dashboard:safety-env-inspection:plea-manage';
// final targetPerm = '';
// final menuPath = await RouteService.getMenuPath(parentPerm, targetPerm);
// final result = await SafetyCheckApi.safeCheckAppealList(
// traceId,
// {
// "pageIndex": 1,
// "pageSize": 100,
// "menuPath" : menuPath
// },
// );
// if (result != null && result['success']) {
// final dynamic data = result['data'];
// List fetched = [];
// if (data is List) {
// fetched = data;
// } else if (data is Map && data['list'] is List) {
// // { list: [...], total: x }
// fetched = data['list'];
// }
// personUnderInspection['content'] = fetched.last['content'];
//
// }
}catch(e){
}
}
///
Future<void> _getDangerList() async {
try {
LoadingDialogHelper.show();
final data = {
'foreignKey': form['traceId'],
'pageSize': 100,
'pageIndex': 1,
};
final result = await HiddenDangerApi.getHiddenDangerListByforeignKeyId(
data,
);
LoadingDialogHelper.hide();
if (result != null && result['success']) {
setState(() {
dangerList = result['data'];
});
} else {
ToastUtil.showNormal(context, '获取隐患失败');
}
} catch (e) {
LoadingDialogHelper.hide();
ToastUtil.showNormal(context, '获取隐患失败');
}
}
///
Future<void> _openDrawer(Map<String, dynamic> hiddenForm, int index) async {
// pushPage(HiddenRecordDetailPage(DangerType.ristRecord,7,hiddenForm['id'],hiddenForm['hiddenId'],false), context);
}
Future<T?> openCustomDrawer<T>(BuildContext context, Widget child) {
return Navigator.of(context).push<T>(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
barrierColor: Colors.black54,
pageBuilder: (_, __, ___) {
return Align(
alignment: Alignment.centerRight,
child: FractionallySizedBox(
widthFactor: 4 / 5,
child: Material(color: Colors.white, child: child),
),
);
},
transitionsBuilder: (_, anim, __, child) {
return SlideTransition(
position: Tween(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)),
child: child,
);
},
),
);
}
/// 使
void _removeCheckPerson(int index) async {
CustomAlertDialog.showConfirm(
context,
title: "提示",
content: '确定移除检查人员吗',
cancelText: '取消',
confirmText: '确定',
onConfirm: () {
setState(() {
inspectorList.removeAt(index);
form['inspectorList'] = inspectorList;
});
},
);
}
Widget _personUnitItem(Map item, int index) {
return Stack(
children: [
Container(
padding: EdgeInsets.all(5),
child: DottedBorderBox(
child: SizedBox(
child: Column(
children: [
ItemListWidget.selectableLineTitleTextRightButton(
isRequired: _isEdit,
label: '${index + 1}.检查部门:',
isEditable: _isEdit,
onTapClean: () {
ToastUtil.showNormal(context, '只读展示,无法清除');
},
text: item['departmentName'] ?? '',
onTap: () {
},
),
Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
isRequired: _isEdit,
label: '${index + 1}.检查人员:',
isEditable: _isEdit,
text: item['userName'] ?? '',
onTap: () {
},
),
],
),
),
),
),
// here _isEdit == false
],
);
}
Widget _personSignItem(Map item, int index) {
if (item['status'] == 0) {
return SizedBox(height: 0,);
}
return SizedBox(
child: Column(
children: [
if (FormUtils.hasValue(item, 'signature')) ...[
Column(
children: [
ItemListWidget.multiLineTitleTextField(
label: '审核意见:',
isEditable: false,
text: item['userRemarks'] ?? '',
),
if (FormUtils.hasValue(item, 'signature'))
ItemListWidget.twoRowTitleAndImages(
title: '签字图片',
onTapCallBack: (path) {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
imageUrls: [item['signature']],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const SizedBox(width: 10),
Text('签字时间: ${item['signatureTime'] ?? ''}'),
],
),
],
),
] else ...[
Center(child: Text('')),
],
],
),
);
}
//
Widget _unitSignItem(Map item) {
return SizedBox(
child: Column(
children: [
Column(
children: [
if (FormUtils.hasValue(item, 'content')) ...[
ItemListWidget.multiLineTitleTextField(
label: '申辩说明:',
isEditable: false,
text: item['content'] ?? '',
),
] else ...[
Center(child: Text('')),
],
if (FormUtils.hasValue(item, 'signature'))
ItemListWidget.twoRowTitleAndImages(
title: '签字图片',
onTapCallBack: (path) {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
imageUrls: [item['signature']],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const SizedBox(width: 10),
Text('签字时间: ${item['signatureTime'] ?? ''}'),
],
),
],
),
],
),
);
}
void _delHiddenForm(Map<String, dynamic> item, int index) async {
final ok = await CustomAlertDialog.showConfirm(
context,
title: '提示',
content: '确定移除发现问题吗?',
);
if (ok) {
setState(() {
dangerList.remove(item);
});
}
}
///
void _addInspector() {
setState(() {
inspectorList.add({
'departmentId': '',
'departmentName': '',
'userName': '',
'userId': '',
});
form['inspectorList'] = inspectorList;
});
}
Widget _mainWidget() {
if (!FormUtils.hasValue(form, 'inspectionId')) {
return SizedBox(height: 500, child: NoDataWidget.show(),);
}
String subjectShow = form['subject'] ?? '';
if (widget.handleType == SafeCheckDetailMode.add) {
for (var item in subjectList) {
if (item['bianma'] == form['subject']) {
form['subject'] = item['id'];
subjectShow = item['name'] ?? '';
break;
}
}
}
bool isPlan = form['planType'] == 1;
return (widget.handleType == SafeCheckDetailMode.add) ||
(widget.handleType == SafeCheckDetailMode.edit &&
FormUtils.hasValue(form, 'inspectionId') ||
widget.handleType == SafeCheckDetailMode.detail)
? Column(
children: [
// ItemListWidget.selectableLineTitleTextRightButton(
// isRequired: _isEdit,
// label: '检查题目',
// isEditable: _isEdit,
// text: '${form['subject'] ?? ''}',
// onTap: () {},
// ),
// const Divider(),
// ItemListWidget.selectableLineTitleTextRightButton(
// isRequired: _isEdit,
// label: '计划属性',
// isEditable: _isEdit,
// text: isPlan ? '计划内' : '计划外',
// onTap: () {},
// ),
// if (isPlan) ...[
// const Divider(),
// ItemListWidget.selectableLineTitleTextRightButton(
// isRequired: _isEdit,
// label: '计划名称',
// isEditable: _isEdit,
// text: form['planName'] ?? '',
// onTap: () {},
// ),
// ],
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
isRequired: _isEdit,
label: '辖区单位',
isEditable: _isEdit,
text: personUnderInspection['departmentName'] ?? '',
onTap: () {},
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
isRequired: _isEdit,
label: '被检查单位',
isEditable: _isEdit,
text: personUnderInspection['departmentName'] ?? '',
onTap: () {},
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
isRequired: _isEdit,
label: '被检查单位现场负责人',
isEditable: _isEdit,
text: personUnderInspection['userName'] ?? '',
onTap: () {},
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '检查类型:',
onTap: () {},
isEditable: _isEdit,
text: form['typeName'] ?? '',
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '检查开始时间:',
isEditable: _isEdit,
text: form['timeStart'] ?? '',
onTap: () async {},
),
const Divider(),
ItemListWidget.selectableLineTitleTextRightButton(
label: '检查结束时间:',
isEditable: _isEdit,
text: form['timeEnd'] ?? '',
onTap: () async {},
),
// if (!_isEdit)
// Column(
// children: [
// const Divider(),
// ItemListWidget.selectableLineTitleTextRightButton(
// label: '记录填写时间:',
// isEditable: _isEdit,
// text: form['createTime'] ?? '',
// ),
// ],
// ),
const Divider(),
ItemListWidget.singleLineTitleText(
label: '检查场所:',
isEditable: _isEdit,
text: form['place'] ?? '',
hintText: '请输入检查场所',
onChanged: (val) {
//
},
),
const Divider(),
ItemListWidget.itemContainer(
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListItemFactory.headerTitle('检查人员'),
// _isEdit == false
],
),
SizedBox(height: 10),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: inspectorList.length,
itemBuilder: (context, index) {
return _personUnitItem(inspectorList[index], index);
},
),
],
),
),
const Divider(),
MultiTextFieldWithTitle(
label: "检查情况:",
isEditable: _isEdit,
items: situationList,
hintText: "请输入检查情况...",
onItemsChanged: (List<Map<String, dynamic>> value) {
situationList = value;
},
),
const Divider(),
ItemListWidget.itemContainer(
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListItemFactory.headerTitle('发现问题'),
//
],
),
],
),
),
HiddenListTable(
hiddenList: dangerList,
forbidEdit: _isEdit,
baseImgPath: ApiService.baseImgPath,
personSignImg: '',
personSignTime: '',
showHidden: (item, idx) {
// / push
_openDrawer(item, idx);
},
removeHidden: (item, idx) {
_delHiddenForm(item, idx);
},
context: context,
),
if (widget.handleType == SafeCheckDetailMode.detail)
_detailWidget(),
],
)
: SizedBox();
}
Widget _detailWidget() {
return Column(
children: [
if (inspectorList.isNotEmpty)
Column(
children: [
const Divider(),
ListItemFactory.createBuildSimpleSection('检查人员审核情况'),
SizedBox(height: 5),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: inspectorList.length,
itemBuilder: (context, index) {
return _personSignItem(inspectorList[index], index);
},
),
],
),
if (personUnderInspection.isNotEmpty && FormUtils.hasValue(personUnderInspection, 'signature'))
Column(
children: [
const Divider(),
ListItemFactory.createBuildSimpleSection('被检查单位现场负责人确认情况'),
SizedBox(height: 5),
_unitSignItem(personUnderInspection)
],
),
],
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: form.isNotEmpty ? _mainWidget() : SizedBox(),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:qhd_prevention/pages/home/keyTasks/keyTasksDetail/key_taskes_detail_widget.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
class KeyTaskesOnlylookDetailPage extends StatefulWidget {
const KeyTaskesOnlylookDetailPage({
super.key,
required this.inspectionId,
});
final String inspectionId;
@override
State<KeyTaskesOnlylookDetailPage> createState() => _KeyTaskesOnlylookDetailPageState();
}
class _KeyTaskesOnlylookDetailPageState extends State<KeyTaskesOnlylookDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: '详情'),
body: SafeArea(
child: ListView(
children: [
Container(
color: Colors.white,
child: KeyTaskesDetailWidget(
inspectionId: widget.inspectionId,
handleType: SafeCheckDetailMode.detail,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,740 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qhd_prevention/constants/app_enums.dart';
import 'package:qhd_prevention/customWidget/BaiDuMap/map_preview_widget.dart';
import 'package:qhd_prevention/customWidget/BaiDuMap/map_webview_page.dart';
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
import 'package:qhd_prevention/customWidget/center_multi_picker.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/item_list_widget.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/http/modules/auth_api.dart';
import 'package:qhd_prevention/http/modules/key_tasks_api.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/tools/HiddenListTable.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:url_launcher/url_launcher.dart';
class KeyTasksConfirmDetailPage extends StatefulWidget {
const KeyTasksConfirmDetailPage(this.id,this.type, {Key? key, }) : super(key: key);
final String id;
final int type;//1 2
@override
_KeyTasksConfirmDetailPageState createState() => _KeyTasksConfirmDetailPageState();
}
class _KeyTasksConfirmDetailPageState extends State<KeyTasksConfirmDetailPage> {
final _standardController = TextEditingController();
double centerLat = 39.8883;
double centerLng = 119.519;
late Map<String, dynamic> pd = {};
// List<dynamic> hiddenList = [];//
List<dynamic> monitorList = [];//
List<dynamic> fileList = [];//
String buMenId = "";//
String buMenName = "";
String responsibleId = "";//
String responsibleName = "";
@override
void initState() {
super.initState();
_getKeyTasksConfirmDetail();
_getUserData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: widget.type==1?'查看':'处置'),
body:
pd.isEmpty
? const Center(child: CircularProgressIndicator())
: _listWidget(),
);
}
Widget _listWidget() {
return LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: Row(
children: [
Container(
width: 3,
height: 15,
color: Colors.blue,
),
const SizedBox(width: 8),
Text(
"基本信息",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
_buildInfoItem('辖区单位', pd['jurisdictionCorpinfoName'] ?? ''),
Divider(height: 1),
_buildInfoItem('辖区单位负责人', pd['jurisdictionUserName'] ?? ''),
Divider(height: 1),
_buildInfoItem('主管部门', pd['jurisdictionDepartmentName'] ?? ''),
Divider(height: 1),
_buildInfoItem('是否已有项目内作业', _getHouseAssignment(pd)),
Divider(height: 1),
_buildInfoItem('重点作业名称', pd['projectName'] ?? ''),
Divider(height: 1),
_buildInfoItem('重点作业属性', pd['projectTypeName'] ?? ''),
Divider(height: 1),
_buildInfoItem('计划工期开始', pd['planWorkStartDate'] ?? ''),
Divider(height: 1),
_buildInfoItem('计划工期结束', pd['planWorkEndDate'] ?? ''),
Divider(height: 1),
_buildInfoItem('相关方单位', pd['xgfCorpinfoName'] ?? ''),
Divider(height: 1),
_buildInfoItem('相关方单位负责人', pd['xgfUserName'] ?? ''),
Divider(height: 1),
_buildInfoItem('相关方单位负责人手机号', pd['xgfMasterPhone'] ?? ''),
Divider(height: 1),
_buildInfoItem('是否设置监理单位', _getSupervisionUnitFlag(pd)),
Divider(height: 1),
_buildInfoItem('监理单位', pd['supervisionUnitCorpName'] ?? ''),
Divider(height: 1),
_buildInfoItem('监理单位工程负责人', pd['supervisionUnitUserName'] ?? ''),
Divider(height: 1),
_buildInfoItem('监理单位工程负责人电话', pd['supervisionUnitUserPhone'] ?? ''),
Divider(height: 1),
_buildInfoItem('安全管理协议',''),
//
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: fileList.length,
itemBuilder: (context, index) {
final file = fileList[index];
final fileName = file['fileName'] ?? '协议${index + 1}';
final fileUrl = file['filePath'] ?? '';
return ItemListWidget.OneRowButtonTitleText(
horizontalnum: 10,
label: fileName,
buttonText: '查看',
text: '',
onTap: () {
if (fileUrl.isEmpty) {
ToastUtil.showNormal(context, '文件不存在');
return;
}
//
_downloadAndLoad(ApiService.baseImgPath +fileUrl);
// pushPage(ReadFilePage(fileUrl: ApiService.baseImgPath + fileUrl), context);
},
);
},
),
Divider(height: 1),
_buildInfoItem('状态', _getState(pd)),
Divider(height: 1),
if(widget.type==1)
_buildInfoItem('重点作业定位', ''),
if(widget.type==2)
ItemListWidget.OneRowButtonTitleText(
horizontalnum: 10,
label: '重点作业定位',
buttonText: '定位',
text: '',
onTap: () async {
final result = await pushPage(MapWebViewPage(), context);
print(result);
if (result != null) {
setState(() {
upKeyTasksData["longitude"] = result['longitude'].toString();
upKeyTasksData["latitude"] = result['latitude'].toString();
centerLng=double.parse(upKeyTasksData['longitude']);
centerLat=double.parse(upKeyTasksData['latitude']);
});
}
},
),
Container(
height: 200,
margin: EdgeInsetsGeometry.symmetric(horizontal: 15),
child:
MapPreviewWidget(
width: MediaQuery.of(context).size.width - 30,
height: 200,
points: [
{
"longitude": centerLng,
"latitude": centerLat,
// iconPath assets/map/static/marker50.png
"iconPath": "map/50.png"
},
//
],
),
),
const SizedBox(height: 10,),
],
),
),
const SizedBox(height: 6,),
Card(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: Row(
children: [
Container(
width: 3,
height: 15,
color: Colors.blue,
),
const SizedBox(width: 8),
Expanded(
child: Text(
"绑定视频监控",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
CustomButton(
text: '添加',
height: 30,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
textStyle: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
backgroundColor: Colors.blue,
onPressed: () {
_getVisitPortArea();
},
),
],
),
),
if(widget.type==1)
_buildOtherHiddenTable(),
if(widget.type==2)
_buildOtherHiddenTableTwo(),
],
),
),
if(widget.type==2)...[
const SizedBox(height: 10,),
CustomButton(
height: 40,
onPressed: () {
_upKeyTasksData();
},
textStyle: const TextStyle(color: Colors.white),
buttonStyle: ButtonStyleType.primary,
text: '提交',
),
],
//
SizedBox(height: MediaQuery.of(context).padding.bottom + 20),
],
),
),
),
);
},
);
}
Widget _buildInfoItem(String title, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// SizedBox(
// width: 120,
// child:
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
// ),
Expanded(child: Text(value, textAlign: TextAlign.right)),
],
),
);
}
Widget _buildOtherHiddenTable() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
decoration: BoxDecoration(border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(4)),
child: Table(
border: TableBorder.all(color: Colors.grey[300]!),
columnWidths: const {0: FlexColumnWidth(1), 1: FlexColumnWidth(2),2: FlexColumnWidth(2)},
children: [
TableRow(decoration: BoxDecoration(color: Colors.grey[100]), children: [
_buildTableHeaderCell("序号"),
_buildTableHeaderCell("视频名称"),
_buildTableHeaderCell("视频类型"),
]),
if (monitorList.isEmpty)
TableRow(children: [Padding(padding: EdgeInsets.all(12), child: Text("暂无数据")), SizedBox()])
else
...monitorList.asMap().entries.map((entry) {
final index = entry.key + 1; // 1
final item = entry.value;
final mi = (item is Map) ? item as Map<String, dynamic> : Map<String, dynamic>.from(item);
return TableRow(children: [
Padding(
padding: EdgeInsets.all(8),
child: Text(index.toString(),textAlign: TextAlign.center, ), //
),
Padding(
padding: EdgeInsets.all(8),
child: Text(mi["cameraName"]?.toString() ?? "",textAlign: TextAlign.center, ),
),
Padding(
padding: EdgeInsets.all(8),
child: Text(_getMonitorType(mi),textAlign: TextAlign.center, ),
),
// GestureDetector(
// onTap: () => _goToDetail(
// mi["hiddenId"]?.toString() ?? '',
// mi["hiddenUserId"]?.toString() ?? ''
// ),
// child: Padding(
// padding: EdgeInsets.all(8),
// child: Center(
// child: Text("查看", style: TextStyle(color: Colors.blue)),
// ),
// ),
// ),
]);
}).toList(),
],
),
);
}
Widget _buildOtherHiddenTableTwo() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
decoration: BoxDecoration(border: Border.all(color: Colors.grey[300]!), borderRadius: BorderRadius.circular(4)),
child: Table(
border: TableBorder.all(color: Colors.grey[300]!),
columnWidths: const {0: FlexColumnWidth(1), 1: FlexColumnWidth(2),2: FlexColumnWidth(2),3: FlexColumnWidth(1)},
children: [
TableRow(decoration: BoxDecoration(color: Colors.grey[100]), children: [
_buildTableHeaderCell("序号"),
_buildTableHeaderCell("视频名称"),
_buildTableHeaderCell("视频类型"),
_buildTableHeaderCell("操作"),
]),
if (monitorList.isEmpty)
TableRow(children: [Padding(padding: EdgeInsets.all(12), child: Text("暂无数据")), SizedBox()])
else
...monitorList.asMap().entries.map((entry) {
final index = entry.key + 1; // 1
final item = entry.value;
final mi = (item is Map) ? item as Map<String, dynamic> : Map<String, dynamic>.from(item);
return TableRow(children: [
Padding(
padding: EdgeInsets.all(8),
child: Text(index.toString(),textAlign: TextAlign.center, ), //
),
Padding(
padding: EdgeInsets.all(8),
child: Text(mi["cameraName"]?.toString() ?? "",textAlign: TextAlign.center, ),
),
Padding(
padding: EdgeInsets.all(8),
child: Text(_getMonitorType(mi),textAlign: TextAlign.center, ),
),
if(item["cameraType"]==2)
GestureDetector(
onTap: () {
setState(() {
monitorList.removeAt(entry.key);
});
},
child: Padding(
padding: EdgeInsets.all(8),
child: Center(
child: Text("删除", style: TextStyle(color: Colors.red)),
),
),
)
else
SizedBox(),
]);
}).toList(),
],
),
);
}
Future<void> _getVisitPortArea() async {
List<String> result = [];
//
final List<int> initialIndices = [];
final resultList = await KeyTasksApi.getKeyTasksListCameraAll('2');
List<dynamic> raw = resultList["data"] as List<dynamic>;
if(raw.isEmpty){
ToastUtil.showNormal(context, '未获取到视频监控');
}
result = raw.map((item) => item['cameraName'].toString()).toList();
if(monitorList.isNotEmpty){
// value
List<String> targetValues = monitorList.map((item) => item['cameraId'].toString()).toList();
// raw
for (String targetValue in targetValues) {
for (int i = 0; i < raw.length; i++) {
var item = raw[i];
if (item != null && item is Map && item.containsKey('cameraId')) {
if (item['cameraId'].toString() == targetValue) {
initialIndices.add(i);// 0
break;
}
}
}
}
}
//
final selectedItems = await CenterMultiPicker.show<String>(
context,
items: result,
itemBuilder: (item) => Text(
item,
style: const TextStyle(fontSize: 16),
),
initialSelectedIndices: initialIndices, //
maxSelection: null, //
allowEmpty: true,
title: '视频监控',
);
//
if (selectedItems != null) {
setState(() {
// JSON
List<Map<String, dynamic>> areaList = [];
// raw
for (String selectedItem in selectedItems) {
for (var item in raw) {
if (item != null && item is Map && item.containsKey('cameraId')) {
if (item['cameraName'].toString() == selectedItem) {
// value(dictLabel) bianma(dictValue)
areaList.add({
'cameraId': item['cameraId'].toString(),
'cameraName': item['cameraName'].toString(),
'cameraType': item['cameraType'], // 使 toString()
});
break;
}
}
}
}
for(int i=0;i<monitorList.length;i++){
if(monitorList[i]['cameraType']==2){
monitorList.removeAt(i);
}
}
monitorList.addAll(areaList);
upKeyTasksData['keyProjectCameraAddCmdList']=monitorList;
});
}
}
Widget _buildTableHeaderCell(String text) {
return Padding(padding: EdgeInsets.all(8), child: Center(child: Text(text, style: TextStyle(fontWeight: FontWeight.bold))));
}
String _getHouseAssignment(final item) {
//,1:,0:
int type = item["projectWorkFlag"]??'';
String typeText='';
switch(type){
case 0:
typeText='';
break;
case 1:
typeText='';
break;
}
return typeText;
}
String _getMonitorType(final item) {
//,1:,2:
int type = item["cameraType"]??'';
String typeText='';
switch(type){
case 1:
typeText='固定摄像头';
break;
case 2:
typeText='移动摄像头';
break;
}
return typeText;
}
String _getSupervisionUnitFlag(final item) {
//,1:,0:
int type = item["supervisionUnitFlag"]??'';
String typeText='';
switch(type){
case 0:
typeText='';
break;
case 1:
typeText='';
break;
}
return typeText;
}
String _getState(final item) {
//,1:,2:,3:,4:,5:,6:
int type = item["applyStatus"]??'';
String typeText='';
switch(type){
case 1:
typeText='未开工';
break;
case 2:
typeText='开工申请中';
break;
case 3:
typeText='已超期';
break;
case 4:
typeText='进行中';
break;
case 5:
typeText='完工申请中';
break;
case 6:
typeText='已完工';
break;
}
return typeText;
}
Future<void> _downloadAndLoad(String url) async {
try {
final filename = url.split('/').last;
final dir = await getTemporaryDirectory();
final filePath = '${dir.path}/$filename';
final dio = Dio();
final response = await dio.get<List<int>>(
url,
options: Options(responseType: ResponseType.bytes),
);
final file = File(filePath);
await file.writeAsBytes(response.data!);
final result = await OpenFile.open(filePath);
if (result.type != ResultType.done) {
ToastUtil.showNormal(context, "文件加载失败");
}
} catch (e) {
//
ToastUtil.showNormal(context, "文件加载失败");
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('文件加载失败: \$e')),
// );
}
}
Future<void> _getUserData() async {
try {
final raw = await AuthApi.getUserData();
if (raw['success']) {
setState(() {
responsibleId = raw['data']['id'];
responsibleName = raw['data']['name'];
buMenId = raw['data']['departmentId'];
buMenName = raw['data']['departmentName'];
});
} else {
ToastUtil.showNormal(context, "获取个人信息失败");
}
} catch (e) {
// Toast
print('加载首页数据失败:$e');
}
}
Future<void> _getKeyTasksConfirmDetail() async {
try {
final raw = await KeyTasksApi.getKeyTasksConfirmDetail(widget.id);
if (raw['success']) {
setState(() async {
pd = raw['data'];
monitorList=pd['keyProjectCameraList']??[];
if((pd['latitude']??'').isNotEmpty){
centerLat=double.parse(pd['latitude']);
centerLng=double.parse(pd['longitude']);
upKeyTasksData["latitude"]=pd['latitude']??'';
upKeyTasksData["longitude"]=pd['longitude']??'';
}
final fileData = await FileApi.getImagePath(pd['keyProjectId'], UploadFileType.keyProjectSafetyManagementAgreement);
if (fileData['success']) {
setState(() {
fileList=fileData['data']??[];
});
}
});
} else {
ToastUtil.showNormal(context, "获取个人信息失败");
}
} catch (e) {
// Toast
print('加载首页数据失败:$e');
}
}
Future<void> _upKeyTasksData( ) async {
try {
if((upKeyTasksData["latitude"]??'').isEmpty||(upKeyTasksData["longitude"]??'').isEmpty){
ToastUtil.showNormal(context, '请选择作业定位');
return;
}
upKeyTasksData["id"]=widget.id;
final Map<String, dynamic> result;
result = await KeyTasksApi.upKeyTasksData(upKeyTasksData);
if (result['success'] ) {
ToastUtil.showNormal(context, '提交成功');
Navigator.pop(context);
}else{
ToastUtil.showNormal(context, '提交失败');
// _showMessage('加载数据失败');
}
} catch (e) {
// Toast
print('加载数据失败:$e');
}
}
Map<String, dynamic> upKeyTasksData={
"applyStatus": '2',
"id": '',
"keyProjectCameraAddCmdList": [
// {
// "cameraId": "",
// "cameraType": 0,
// "keyProjectId": ""
// }
],
"latitude": "",
"longitude": ""
};
}

View File

@ -0,0 +1,395 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/danner_repain_item.dart';
import 'package:qhd_prevention/customWidget/search_bar_widget.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/http/modules/doorAndCar_api.dart';
import 'package:qhd_prevention/http/modules/key_tasks_api.dart';
import 'package:qhd_prevention/pages/home/keyTasks/keyTasksDetail/key_taskes_onlylook_detail_page.dart';
import 'package:qhd_prevention/pages/home/keyTasks/keyTasksDetail/key_tasks_confirm_detail_page.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/tools/h_colors.dart';
import 'package:qhd_prevention/tools/tools.dart';
class KeyTasksConfirmListPage extends StatefulWidget {
const KeyTasksConfirmListPage( {super.key});
@override
State<KeyTasksConfirmListPage> createState() => _KeyTasksConfirmListPageState();
}
class _KeyTasksConfirmListPageState extends State<KeyTasksConfirmListPage> {
final TextEditingController _searchController = TextEditingController();
String appBarTitle="重点作业管理";
String searchHintText='请输入重点作业名称';
int _page = 1;
String searchKey="";
int _totalPage=1;
bool _isLoading = false;
bool _hasMore = true;
List<dynamic> _list = [];
@override
void initState() {
// TODO: implement initState
super.initState();
getListData(false);
// SessionService.instance.setCustomRecordDangerJson("");
}
void getListData(bool addList){
_getKeyTasksConfirmList(addList);
}
@override
Widget build(BuildContext context) {
//
final double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: MyAppbar(
title: appBarTitle,
actions: [],
),
body: SafeArea(
child: NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child:_vcDetailWidget()
)
),
// backgroundColor: Colors.white,
);
}
Widget _vcDetailWidget() {
return Column(
children: [
Container(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(10),
child: SearchBarWidget(
controller: _searchController,
hintText: searchHintText,
showResetButton:true,
onSearch: (keyboard) {
//
_page=1;
searchKey=keyboard;
getListData(false);
},
),
),
),
Container(
height: 5,
color: h_backGroundColor(),
),
Expanded(
child:_list.isEmpty
? NoDataWidget.show()
: ListView.separated(
padding: EdgeInsets.only(top: 5),
itemCount: _list.length,
separatorBuilder: (_, __) => const SizedBox(),
itemBuilder: (context, index){
if (index >= _list.length) {
//
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: CircularProgressIndicator()),
);
}
return _buildListItem(_list[index],index);
},
),
)
],
);
}
Widget _buildListItem(Map<String, dynamic> item,index) {
return InkWell(
onTap: () {
},
child: Container(
padding: const EdgeInsets.all(15),
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${item['projectName']??''}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildItemChild('重点作业属性:',item['projectTypeName']??""),
const SizedBox(height: 8),
_buildItemChild('辖区单位:',item['jurisdictionCorpinfoName']??""),
const SizedBox(height: 8),
_buildItemChild('主管部门:',item['masterDepartmentName']??""),
const SizedBox(height: 8),
_buildItemChild('相关方单位:',item['xgfCorpinfoName']??""),
const SizedBox(height: 8),
_buildItemChild('计划工期时间:',_changeTime(item)),
const SizedBox(height: 8),
_buildItemChild('固定视频数:', ((item['fixedCameraidList'] ?? []).length).toString()),
const SizedBox(height: 8),
_buildItemChild('移动视频数:',((item['mobileCameraIdList']??[]).length).toString()),
const SizedBox(height: 8),
_buildItemChild('申请时间:',item['createTime']??""),
const SizedBox(height: 8),
_buildItemChild('状态:',_getState(item)),
const SizedBox(height: 12),
Row(children: [
Expanded(child: CustomButton(
height: 35,
onPressed: () {
_goToDetail(item,index,1);
},
backgroundColor: h_AppBarColor(),
textStyle: const TextStyle(color: Colors.white),
buttonStyle:ButtonStyleType.primary,
text: '查看'),),
const SizedBox(width: 8),
Expanded(child: CustomButton(
height: 35,
onPressed: () async {
if( item["applyStatus"]==1){
_goToDetail(item,index,2);
}else{
final ok = await CustomAlertDialog.showConfirm(
context,
title: '完工申请',
content: '确定要提交吗?',
cancelText: '取消',
);
if (ok) {
_upKeyTasksData(item);
}
}
},
backgroundColor: h_AppBarColor(),
textStyle: const TextStyle(color: Colors.white),
buttonStyle:ButtonStyleType.primary,
text: item["applyStatus"]==1?'开工申请':'完工申请'),),
],),
],
),
),
);
}
Future<void> _goToDetail(item,index,int type) async {
//item
await pushPage(KeyTasksConfirmDetailPage( item['id']??'',type), context);
getListData(false);
}
bool _onScroll(ScrollNotification n) {
if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 &&
_hasMore && !_isLoading) {
_page++;
getListData(true);
}
return false;
}
Widget _buildItemChild(String title,String text) {
return Row(
children: [
// Expanded(
// child:
Text(
title,
style: const TextStyle(
color: Colors.black87,
fontSize: 14,
),
),
// ),
const SizedBox(width: 5,),
Expanded(
child: Text(
text,
style: const TextStyle(
color: Colors.black87,
fontSize: 14,
),
textAlign: TextAlign.right, //
),
),
],
);
}
String _changeTime(final item) {
String timeStart = item["planWorkStartDate"]??'';
String timeEnd = item["planWorkEndDate"]??'';
if(timeStart.isNotEmpty&&timeEnd.isNotEmpty){
return '$timeStart-$timeEnd';
}else{
return '$timeStart$timeEnd';
}
}
String _getState(final item) {
//,1:,2:,3:,4:,5:,6:
int type = item["applyStatus"]??'';
String typeText='';
switch(type){
case 1:
typeText='未开工';
break;
case 2:
typeText='开工申请中';
break;
case 3:
typeText='已超期';
break;
case 4:
typeText='进行中';
break;
case 5:
typeText='完工申请中';
break;
case 6:
typeText='已完工';
break;
}
return typeText;
}
Future<void> _getKeyTasksConfirmList( bool loadMore) async {
try {
if (_isLoading) return;
_isLoading = true;
keyTasksConfirmListData['projectName']=searchKey.isNotEmpty?searchKey:null;
keyTasksConfirmListData['pageIndex']=_page;
final Map<String, dynamic> result;
result = await KeyTasksApi.getKeyTasksConfirmList(keyTasksConfirmListData);
if (result['success'] ) {
_totalPage =result['totalPages'] ?? 1;
final List<dynamic> newList = result['data'] ?? [];
// setState(() {
// _list.addAll(newList);
// });
setState(() {
if (loadMore) {
_list.addAll(newList);
} else {
_list = newList;
}
_hasMore = _page < _totalPage;
// if (_hasMore) _page++;
});
}else{
ToastUtil.showNormal(context, '加载数据失败');
// _showMessage('加载数据失败');
}
} catch (e) {
// Toast
print('加载数据失败:$e');
} finally {
// if (!loadMore) LoadingDialogHelper.hide();
_isLoading = false;
}
}
Map<String, dynamic> keyTasksConfirmListData={
"projectName": "",
"applyStatusList": [1,4],//,0:1:,2:,3:,4:,5:,6:
"pageSize": 20,
"pageIndex": 1,
"needTotalCount": true,
};
Future<void> _upKeyTasksData( item) async {
try {
upKeyTasksData["id"]=item["id"];
final Map<String, dynamic> result;
result = await KeyTasksApi.upKeyTasksData(upKeyTasksData);
if (result['success'] ) {
ToastUtil.showNormal(context, '提交成功');
getListData(false);
}else{
ToastUtil.showNormal(context, '提交失败');
// _showMessage('加载数据失败');
}
} catch (e) {
// Toast
print('加载数据失败:$e');
}
}
Map<String, dynamic> upKeyTasksData={
"applyStatus": '5',
"id": '',
};
}

View File

@ -0,0 +1,395 @@
// lib/pages/application_template.dart
import 'dart:ffi';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/pages/home/doorAndCar/doorCar_tab_page.dart';
import 'package:qhd_prevention/pages/home/keyTasks/key_tasks_confirm_list_page.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/tools/tools.dart';
class KeyTasksTabPage extends StatefulWidget {
const KeyTasksTabPage({super.key});
@override
State<KeyTasksTabPage> createState() => _DoorcarTabPageState();
}
class _DoorcarTabPageState extends State<KeyTasksTabPage> {
final String bannerAsset = 'assets/images/door_banner.png';
late List<AppSection> defaultSections;
@override
void initState() {
// TODO: implement initState
super.initState();
_initSections(); // sections
_getDoorCarCount();
}
// sections
void _initSections() {
defaultSections = [
AppSection(title: '重点作业管理', items: [
AppSectionItem(
title: '重点作业申请',
icon: 'assets/images/door_ico9.png',
badge: 0,
onTap: () async {
await pushPage(KeyTasksConfirmListPage(), context);
_getDoorCarCount();
},
),
AppSectionItem(
title: '被检查确认',
icon: 'assets/images/door_ico9.png',
badge: 0,
onTap: () async {
// await pushPage(KeyTasksCheckListPage(1, widget.isLoginJGD), context);
_getDoorCarCount();
},
),
AppSectionItem(
title: '隐患整改',
icon: 'assets/images/door_ico9.png',
badge: 0,
onTap: () async {
// await pushPage(KeyTasksHiddenList(1), context);
_getDoorCarCount();
},
),
AppSectionItem(
title: '隐患记录',
icon: 'assets/images/door_ico9.png',
badge: 0,
onTap: () async {
// await pushPage(KeyTasksHiddenList(2), context);
_getDoorCarCount();
},
),
]),
];
}
Future<void> _getDoorCarCount() async {
// try {
// final result = await DoorAndCarApi.getDoorCarCount();
// if (result['success'] ) {
// List< dynamic> data = result['data']??[] ;
//
// int stakeholderPersonCount =0;
// int stakeholderCarCount =0;
// int temporaryPersonCount =0;
// int temporaryCarCount =0;
// int companyCarCount =0;
// int closureLongPersonCount =0;
// int closureLongCarCount =0;
// int closureTemporaryPersonCount =0;
// int closureTemporaryCarCount =0;
// for(int i=0;i<data.length;i++){
// if(data[i]['type']=='one_level_person'){
// if(data[i]['belongType']=='3'){
// stakeholderPersonCount=data[i]['waitAuditCount']??0;
// }
// if(data[i]['belongType']=='4'){
// temporaryPersonCount=data[i]['waitAuditCount']??0;
// }
// }
//
// if(data[i]['type']=='one_level_car'){
// if(widget.isLoginJGD&&data[i]['belongType']=='2'){
// stakeholderCarCount=data[i]['waitAuditCount']??0;
// }
// if(!widget.isLoginJGD&&data[i]['belongType']=='4'){
// temporaryCarCount=data[i]['waitAuditCount']??0;
// }
// }
//
//
// if(data[i]['type']=='one_level_car'){//
// if(data[i]['belongType']=='2'){
// companyCarCount=data[i]['waitAuditCount']??0;
// }
// if(data[i]['belongType']=='4'){
// companyCarCount=data[i]['waitAuditCount']??0;
// }
// }
//
// if(data[i]['type']=='two_level_person'){
// if(data[i]['belongType']=='1'||data[i]['belongType']=='2'||data[i]['belongType']=='3'){
// closureLongPersonCount=closureLongPersonCount+((data[i]['waitAuditCount']??0)as int);
// }
// if(data[i]['belongType']=='4'){
// closureTemporaryPersonCount=data[i]['waitAuditCount']??0;
// }
// }
//
// if(data[i]['type']=='two_level_car'){
// if(data[i]['belongType']=='1'||data[i]['belongType']=='2'||data[i]['belongType']=='3'){
// closureLongCarCount=closureLongCarCount+((data[i]['waitAuditCount']??0)as int);
// }
// if(data[i]['belongType']=='4'){
// closureTemporaryCarCount=data[i]['waitAuditCount']??0;
// }
// }
//
// }
//
// setState(() {
// // badge
// final gateSection = defaultSections[0];
//
// //
// gateSection.items[0].badge = stakeholderPersonCount;
// //
// gateSection.items[1].badge = stakeholderCarCount;
// // 访
// gateSection.items[3].badge = temporaryPersonCount;
// //
// gateSection.items[4].badge = temporaryCarCount;
//
// //
// gateSection.items[7].badge = companyCarCount;
//
// // JGD
// if (!widget.isLoginJGD && defaultSections.length > 2) {
// final closureSection = defaultSections[2];
// //
// closureSection.items[0].badge = closureLongPersonCount;
// //
// closureSection.items[1].badge = closureLongCarCount;
// // 访
// closureSection.items[2].badge = closureTemporaryPersonCount;
// //
// closureSection.items[3].badge = closureTemporaryCarCount;
// }
// });
// }
// // else {
// // ToastUtil.showNormal(context, result['errMessage'] ?? "加载数据失败");
// // }
// } catch (e) {
// LoadingDialogHelper.hide();
// print('加载数据失败:$e');
// }
}
@override
Widget build(BuildContext context) {
final double bannerHeight = (730.0 / 1125.0) * MediaQuery.of(context).size.width;
final double iconSectionHeight =
MediaQuery.of(context).size.height - bannerHeight - 30.0;
const double iconOverlapBanner = 30.0;
// items
final visibleSections = defaultSections
.map((s) => AppSection(
title: s.title,
items: s.items.where((it) => it.visible).toList()))
.where((s) => s.items.isNotEmpty)
.toList();
return Scaffold(
extendBodyBehindAppBar: true,
appBar: MyAppbar(title: '重点作业', backgroundColor: Colors.transparent,),
body: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.zero,
children: [
SizedBox(
height: bannerHeight + iconSectionHeight,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: 0,
left: 0,
right: 0,
height: bannerHeight,
child: _buildBannerSection(bannerHeight, context),
),
Positioned(
left: 10,
right: 10,
top: bannerHeight - iconOverlapBanner,
height: iconSectionHeight,
child: _buildIconSection(context, visibleSections),
),
],
),
),
],
),
);
}
Widget _buildBannerSection(double bannerHeight, BuildContext context) {
return Stack(
children: [
Image.asset(
bannerAsset,
width: MediaQuery.of(context).size.width,
height: bannerHeight,
fit: BoxFit.fitWidth,
errorBuilder: (c, e, s) {
return Container(
color: Colors.blueGrey,
height: bannerHeight,
alignment: Alignment.center,
child: const Text('Banner', style: TextStyle(color: Colors.white)),
);
},
),
],
);
}
Widget _buildIconSection(BuildContext context, List<AppSection> buttonInfos) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)),
],
),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(0),
itemCount: buttonInfos.length,
itemBuilder: (context, index) {
final section = buttonInfos[index];
final items = section.items;
if (items.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 10, left: 10, right: 10),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 5),
child: Row(
children: [
Container(width: 2, height: 10, color: Colors.blue),
const SizedBox(width: 5),
Text(section.title,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold)),
],
),
),
// icons
Padding(
padding: const EdgeInsets.all(10),
child: LayoutBuilder(
builder: (context, constraints) {
const spacing = 20.0;
final totalWidth = constraints.maxWidth;
final itemWidth = (totalWidth - spacing * 2) / 3;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: items.map<Widget>((item) {
return SizedBox(
width: itemWidth,
child: _buildItem(
item,
onTap: item.onTap ??
() => debugPrint('Tapped ${item.title}'),
),
);
}).toList(),
);
},
),
),
],
),
),
);
},
),
);
}
Widget _buildItem(AppSectionItem item, {VoidCallback? onTap}) {
const double iconSize = 30;
final int badgeNum = item.badge;
final String title = item.title;
final String iconPath = item.icon;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [
SizedBox(
width: iconSize,
height: iconSize,
child: Image.asset(
iconPath,
fit: BoxFit.contain,
errorBuilder: (c, e, s) {
return Container(
color: Colors.grey.shade200,
child: const Center(child: Icon(Icons.image, size: 18)),
);
},
),
),
if (badgeNum > 0)
Positioned(
top: -6,
right: -6,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration:
const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
child: Center(
child: Text(
badgeNum > 99 ? '99+' : badgeNum.toString(),
style: const TextStyle(color: Colors.white, fontSize: 10, height: 1),
textAlign: TextAlign.center,
),
),
),
),
],
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View File

@ -0,0 +1,184 @@
import 'dart:io';
import 'package:flutter/material.dart';
Widget HiddenListTable({
required List<dynamic> hiddenList,
required bool forbidEdit,
required String baseImgPath,
required String personSignImg,
required String personSignTime,
required void Function(Map<String, dynamic> item, int index) showHidden,
required void Function(Map<String, dynamic> item, int index) removeHidden,
required BuildContext context,
}) {
// textAlign alignment
Widget _buildCell(String text, {bool isHeader = false, Alignment alignment = Alignment.center, double minWidth = 30, TextAlign? textAlign}) {
// textAlign alignment
textAlign ??= (alignment == Alignment.center || alignment == Alignment.centerRight) ? TextAlign.center : TextAlign.left;
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5),
alignment: alignment,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: Text(
text,
textAlign: textAlign,
style: TextStyle(
fontWeight: isHeader ? FontWeight.bold : FontWeight.normal,
fontSize: 14,
color: isHeader ? Colors.black87 : Colors.black54,
),
),
),
);
}
TableRow _buildHeader() {
return TableRow(
decoration: BoxDecoration(color: Colors.grey.shade200),
children: [
_buildCell('序号', isHeader: true, alignment: Alignment.center),
// _buildCell('隐患部位', isHeader: true),
_buildCell('隐患描述', isHeader: true),
_buildCell('操作', isHeader: true, alignment: Alignment.center),
],
);
}
TableRow _buildRow(Map<String, dynamic> item, int index) {
final partName = (item['hiddenPartName'] ?? '').toString();
final part = (item['hiddenPart'] ?? '').toString();
final descr = (item['hiddenDesc'] ?? '').toString();
// 使 styleFrom使 MaterialStateProperty
final ButtonStyle tinyButtonStyle = TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: const VisualDensity(horizontal: -1, vertical: -1),
// foregroundColor, textStyle
);
return TableRow(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300, width: 0.5),
),
),
children: [
_buildCell('${index + 1}', alignment: Alignment.center),
// _buildCell(partName.isNotEmpty ? partName : part),
_buildCell(descr),
Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
child: Align(
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
style: tinyButtonStyle,
onPressed: () => showHidden(item, index),
child: Text(
forbidEdit ? '编辑' : '查看',
style: const TextStyle(color: Colors.blue, fontSize: 14),
),
),
if (forbidEdit)
...[
const SizedBox(width: 15), //
TextButton(
style: tinyButtonStyle,
onPressed: () => removeHidden(item, index),
child: const Text(
'删除',
style: TextStyle(color: Colors.red, fontSize: 14),
),
),
]
],
),
),
),
],
);
}
Widget _buildHeaderRowWidget() {
return Container(
decoration: BoxDecoration(color: Colors.grey.shade200),
child: Row(
children: [
Expanded(flex: 1, child: _buildCell('序号', isHeader: true, alignment: Alignment.center)),
// Expanded(flex: 3, child: _buildCell('隐患部位', isHeader: true)),
Expanded(flex: 6, child: _buildCell('隐患描述', isHeader: true)),
Expanded(flex: 3, child: _buildCell('操作', isHeader: true, alignment: Alignment.center)),
],
),
);
}
// LayoutBuilder MediaQuery.of(context) null
return LayoutBuilder(builder: (ctx, constraints) {
// 使退 MediaQuery fallback
final double availableWidth = (constraints.maxWidth != double.infinity && constraints.maxWidth > 0)
? constraints.maxWidth
: ((MediaQuery.maybeOf(ctx)?.size.width ?? 400.0));
final tableWidth = (availableWidth - 20).clamp(200.0, double.infinity);
if (hiddenList.isEmpty) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeaderRowWidget(),
Container(
height: 56,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300, width: 0.5),
),
),
alignment: Alignment.center,
child: Text(
'暂无数据',
style: TextStyle(fontSize: 14, color: Colors.black54),
),
),
],
),
),
);
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
columnWidths: const {
0: FlexColumnWidth(1),
// 1: FlexColumnWidth(3),
1: FlexColumnWidth(6),
2: FlexColumnWidth(3),
},
border: TableBorder.symmetric(
inside: BorderSide(color: Colors.grey.shade300, width: 0.5),
),
// map List<TableRow>
children: [
_buildHeader(),
...hiddenList.asMap().entries.map((e) => _buildRow(e.value, e.key)).toList(),
],
),
),
);
});
}

View File

@ -0,0 +1,485 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/dotted_border_box.dart';
import 'package:qhd_prevention/customWidget/photo_picker_row.dart';
import 'package:qhd_prevention/customWidget/single_image_viewer.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/tools/tools.dart';
//
enum OperationType {
//
check,
//
measure,
}
class MultiTextFieldWithTitle extends StatefulWidget {
final String label;
final List<dynamic> items; //
final bool isEditable;
final bool isAddImage;
final String hintText;
final double fontSize;
final bool isRequired;
final bool isDeletFirst;
final OperationType operationType;
final ValueChanged<List<Map<String, dynamic>>> onItemsChanged;
const MultiTextFieldWithTitle({
super.key,
required this.label,
required this.isEditable,
required this.hintText,
required this.onItemsChanged,
this.fontSize = 15,
this.items = const [],
this.isRequired = true,
this.isAddImage = true,
this.isDeletFirst = false,
this.operationType = OperationType.check,
});
@override
State<MultiTextFieldWithTitle> createState() =>
_MultiTextFieldWithTitleState();
}
class _MultiTextFieldWithTitleState extends State<MultiTextFieldWithTitle> {
final List<TextEditingController> _controllers = [];
final List<FocusNode> _focusNodes = [];
/// map widget.items
final List<Map<String, dynamic>> _itemsData = [];
@override
void initState() {
super.initState();
_initializeFromItems();
}
void _initializeFromItems() {
//
for (var c in _controllers) {
c.dispose();
}
for (var n in _focusNodes) {
n.dispose();
}
_controllers.clear();
_focusNodes.clear();
_itemsData.clear();
if (widget.items.isNotEmpty) {
for (final rawItem in widget.items) {
// item Map
Map<String, dynamic> item;
if (rawItem is Map<String, dynamic>) {
item = Map<String, dynamic>.from(rawItem);
} else {
// map map
item = {'content': rawItem?.toString() ?? ''};
}
// content
final text = item['content'] ?? '';
// imgPath imgPaths list
if (item.containsKey('imgPath') && !item.containsKey('imgPaths')) {
final v = item['imgPath'];
if (v == null) {
item['imgPaths'] = <String>[];
} else if (v is String && v.isNotEmpty) {
item['imgPaths'] = [v];
} else if (v is List) {
item['imgPaths'] = List<String>.from(v.map((e) => e.toString()));
} else {
item['imgPaths'] = <String>[];
}
} else if (!item.containsKey('imgPaths')) {
item['imgPaths'] = <String>[];
} else {
// imgPaths List<String>
final v = item['imgPaths'];
if (v == null) {
item['imgPaths'] = <String>[];
} else if (v is List) {
item['imgPaths'] = List<String>.from(v.map((e) => e.toString()));
} else if (v is String && v.isNotEmpty) {
item['imgPaths'] = [v];
} else {
item['imgPaths'] = <String>[];
}
}
final controller = TextEditingController(text: text);
final node = FocusNode();
controller.addListener(_onDataChanged);
_controllers.add(controller);
_focusNodes.add(node);
_itemsData.add(item);
}
} else {
//
if (widget.operationType == OperationType.check)
_addNewItem(initialize: true);
}
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _onDataChanged();
});
}
@override
void didUpdateWidget(covariant MultiTextFieldWithTitle oldWidget) {
super.didUpdateWidget(oldWidget);
// items
if (oldWidget.items != widget.items) {
_initializeFromItems();
}
}
@override
void dispose() {
for (var c in _controllers) {
c.dispose();
}
for (var n in _focusNodes) {
n.dispose();
}
super.dispose();
}
void _onDataChanged() {
widget.onItemsChanged(_getAllItems());
}
// List<String>
List<String> _getImageListForIndex(int index) {
if (index < 0 || index >= _itemsData.length) return <String>[];
final v = _itemsData[index]['imgPaths'];
if (v == null) return <String>[];
if (v is List<String>) return List<String>.from(v);
if (v is List) return List<String>.from(v.map((e) => e.toString()));
if (v is String && v.isNotEmpty) return [v];
return <String>[];
}
// itemsData imgPath
void _setImageListForIndex(int index, List<String> list) {
if (index < 0 || index >= _itemsData.length) return;
_itemsData[index]['imgPaths'] = List<String>.from(list);
//
_itemsData[index]['imgPath'] = list.isNotEmpty ? list.first : '';
}
//
void _addNewItem({bool initialize = false}) {
// initialize onDataChanged
setState(() {
final newController = TextEditingController();
final newFocusNode = FocusNode();
newController.addListener(_onDataChanged);
// contentimgPathimgPaths
final Map<String, dynamic> newItem = {
'content': '',
'imgPath': '',
'imgPaths': <String>[],
};
_controllers.add(newController);
_focusNodes.add(newFocusNode);
_itemsData.add(newItem);
// initialize
if (!initialize) _onDataChanged();
});
}
//
void _removeItem(int index) async {
if (_controllers.length <= 1) return;
final confirmed = await CustomAlertDialog.showConfirm(
context,
title: '提示',
content: '确定删除此项吗?',
cancelText: '取消',
confirmText: '确定',
);
if (!confirmed) return;
setState(() {
_controllers[index].dispose();
_focusNodes[index].dispose();
_controllers.removeAt(index);
_focusNodes.removeAt(index);
_itemsData.removeAt(index);
_onDataChanged();
});
}
// Map content/imgPaths/imgPath
List<Map<String, dynamic>> _getAllItems() {
final List<Map<String, dynamic>> items = [];
for (int i = 0; i < _itemsData.length; i++) {
final Map<String, dynamic> copy = Map<String, dynamic>.from(
_itemsData[i],
);
// content
copy['content'] = _controllers[i].text;
// imgPaths List<String>
final imgs = _getImageListForIndex(i);
copy['imgPaths'] = imgs;
copy['imgPath'] = imgs.isNotEmpty ? imgs.first : '';
items.add(copy);
}
return items;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
InkWell(
child: Row(
children: [
Flexible(
fit: FlexFit.loose,
child: Row(
children: [
if (widget.isRequired && widget.isEditable)
const Text('* ', style: TextStyle(color: Colors.red)),
Flexible(
child: Text(
widget.label,
style: TextStyle(
fontSize: widget.fontSize,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
if (widget.isEditable)
CustomButton(
text: " 添加 ",
height: 30,
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 5,
),
backgroundColor: Colors.blue,
onPressed: _addNewItem,
),
],
),
),
const SizedBox(height: 8),
//
Column(
children: [
..._controllers.asMap().entries.map((entry) {
final index = entry.key;
final itemMap = _itemsData[index];
final item = {
'content': _controllers[index].text,
'imgPaths': _getImageListForIndex(index),
// 便 _buildItem 使 map _itemsData
};
return _buildItem(index, itemMap);
}).toList(),
],
),
],
),
);
}
Widget _buildItem(int index, Map<String, dynamic> fullItemMap) {
final text = _controllers[index].text;
final imageList = _getImageListForIndex(index);
final itemTitle =
widget.operationType == OperationType.check
? '检查情况${index + 1}'
: '其他安全措施${index + 1}';
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// "检查情况1、2、3..."
if (_controllers.length > 1)
Padding(
padding: const EdgeInsets.only(top: 0, left: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (index > 0)
Text(
itemTitle,
style: TextStyle(
fontSize: widget.fontSize,
fontWeight: FontWeight.bold,
),
),
if (widget.isEditable && index > 0)
IconButton(
onPressed: () => _removeItem(index),
icon: const Icon(
Icons.close,
color: Colors.red,
size: 30,
),
),
],
),
),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 7),
child: SizedBox(
width: double.maxFinite,
child: DottedBorderBox(
child:
widget.isEditable
? TextField(
controller: _controllers[index],
decoration: InputDecoration(
hintText: widget.hintText,
),
focusNode: _focusNodes[index],
keyboardType: TextInputType.multiline,
maxLines: 3,
style: TextStyle(fontSize: widget.fontSize),
)
: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text.isEmpty ? '暂无内容' : text,
style: TextStyle(
fontSize: widget.fontSize,
color: text.isEmpty ? Colors.grey : Colors.black,
),
),
),
),
),
),
//
if (widget.isEditable && widget.isAddImage)
RepairedPhotoSection(
title: '图片',
maxCount: 1,
isEdit: widget.isEditable,
followInitialUpdates: true,
//
initialMediaPaths: imageList,
// / _itemsData
onChanged: (files) {
// setState(() {
// if (files.isNotEmpty) {
// imageList[index] = files.first.path;
// } else {
// imageList[index] = '';
// }
// });
// _onDataChanged();
},
onMediaAdded: (localPath) async {
// 使
final List<String> current = _getImageListForIndex(index);
current.add(localPath);
setState(() {
_setImageListForIndex(index, current);
});
_onDataChanged();
},
onMediaRemoved: (localPath) async {
final List<String> current = _getImageListForIndex(index);
current.removeWhere((p) => p == localPath);
setState(() {
_setImageListForIndex(index, current);
});
_onDataChanged();
},
onMediaTapped: (path) async {
presentOpaque(SingleImageViewer(imageUrl: path), context);
},
onAiIdentify: () {},
),
//
if (!widget.isEditable && imageList.isNotEmpty)
_buildReadOnlyImage(imageList.first),
],
),
);
}
Widget _buildReadOnlyImage(String imagePath) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
'图片:',
style: TextStyle(
fontSize: widget.fontSize - 1,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
GestureDetector(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: _getImageProvider(imagePath),
fit: BoxFit.cover,
),
),
),
onTap: () {
presentOpaque(SingleImageViewer(imageUrl: imagePath), context);
},
),
],
),
);
}
ImageProvider _getImageProvider(String imagePath) {
if (imagePath.startsWith('http')) {
return NetworkImage(imagePath);
} else {
return FileImage(File(imagePath));
}
}
}

View File

@ -0,0 +1,98 @@
// lib/utils/asset_server.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class AssetServer {
AssetServer._internal();
static final AssetServer _instance = AssetServer._internal();
factory AssetServer() => _instance;
HttpServer? _server;
int? _port;
bool get running => _server != null;
Future<Uri> start() async {
if (_server != null && _port != null) {
return Uri.parse('http://127.0.0.1:$_port');
}
// Bind to ephemeral port
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_port = _server!.port;
_server!.listen((HttpRequest request) async {
try {
final path = request.uri.path.isEmpty ? '/map.html' : request.uri.path;
debugPrint('[AssetServer] -> Request: $path');
// map to assets/map/<path>
final assetPath = 'assets/map$path';
// simple content-type detection
final contentType = _contentTypeFromPath(path);
if (assetPath.endsWith('.html') || assetPath.endsWith('.htm')) {
// For HTML, we prefer to read as text
final html = await rootBundle.loadString('assets/map/map.html');
request.response.headers.contentType = ContentType('text', 'html', charset: 'utf-8');
request.response.add(utf8.encode(html));
await request.response.close();
return;
}
// Other assets as bytes
try {
final bd = await rootBundle.load(assetPath);
final bytes = bd.buffer.asUint8List();
if (contentType != null) request.response.headers.contentType = contentType;
request.response.add(bytes);
await request.response.close();
} catch (e) {
debugPrint('[AssetServer] asset not found: $assetPath -> $e');
request.response.statusCode = HttpStatus.notFound;
request.response.write('Not Found');
await request.response.close();
}
} catch (e) {
debugPrint('[AssetServer] internal error: $e');
try {
request.response.statusCode = HttpStatus.internalServerError;
request.response.write('Internal Server Error');
await request.response.close();
} catch (_) {}
}
});
debugPrint('[AssetServer] started at http://127.0.0.1:$_port');
return Uri.parse('http://127.0.0.1:$_port');
}
Future<void> stop() async {
try {
await _server?.close(force: true);
_server = null;
_port = null;
debugPrint('[AssetServer] stopped');
} catch (e) {
debugPrint('[AssetServer] stop error: $e');
}
}
ContentType? _contentTypeFromPath(String path) {
final lower = path.toLowerCase();
if (lower.endsWith('.js')) return ContentType('application', 'javascript', charset: 'utf-8');
if (lower.endsWith('.css')) return ContentType('text', 'css', charset: 'utf-8');
if (lower.endsWith('.html') || lower.endsWith('.htm')) return ContentType('text', 'html', charset: 'utf-8');
if (lower.endsWith('.json')) return ContentType('application', 'json', charset: 'utf-8');
if (lower.endsWith('.png')) return ContentType('image', 'png');
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return ContentType('image', 'jpeg');
if (lower.endsWith('.svg')) return ContentType('image', 'svg+xml');
if (lower.endsWith('.gif')) return ContentType('image', 'gif');
if (lower.endsWith('.webp')) return ContentType('image', 'webp');
return ContentType('application', 'octet-stream');
}
}