From 7ccbcf8ead3ff3ed3b52247e6afbf3548a3214c1 Mon Sep 17 00:00:00 2001 From: xufei <727302827@qq.com> Date: Fri, 10 Apr 2026 17:25:59 +0800 Subject: [PATCH] =?UTF-8?q?2026.4.10=20=E9=87=8D=E7=82=B9=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/constants/app_enums.dart | 5 +- .../BaiDuMap/map_preview_widget.dart | 211 +++++ lib/http/ApiService.dart | 10 +- lib/http/modules/key_tasks_api.dart | 59 ++ lib/pages/home/home_page.dart | 6 +- .../key_taskes_detail_widget.dart | 716 +++++++++++++++++ .../key_taskes_onlylook_detail_page.dart | 39 + .../key_tasks_confirm_detail_page.dart | 740 ++++++++++++++++++ .../keyTasks/key_tasks_confirm_list_page.dart | 395 ++++++++++ .../home/keyTasks/key_tasks_tab_page.dart | 395 ++++++++++ lib/tools/HiddenListTable.dart | 184 +++++ lib/tools/MultiTextFieldWithTitle.dart | 485 ++++++++++++ lib/tools/asset_server.dart | 98 +++ 13 files changed, 3335 insertions(+), 8 deletions(-) create mode 100644 lib/customWidget/BaiDuMap/map_preview_widget.dart create mode 100644 lib/http/modules/key_tasks_api.dart create mode 100644 lib/pages/home/keyTasks/keyTasksDetail/key_taskes_detail_widget.dart create mode 100644 lib/pages/home/keyTasks/keyTasksDetail/key_taskes_onlylook_detail_page.dart create mode 100644 lib/pages/home/keyTasks/keyTasksDetail/key_tasks_confirm_detail_page.dart create mode 100644 lib/pages/home/keyTasks/key_tasks_confirm_list_page.dart create mode 100644 lib/pages/home/keyTasks/key_tasks_tab_page.dart create mode 100644 lib/tools/HiddenListTable.dart create mode 100644 lib/tools/MultiTextFieldWithTitle.dart create mode 100644 lib/tools/asset_server.dart diff --git a/lib/constants/app_enums.dart b/lib/constants/app_enums.dart index fd25d85..ea832c7 100644 --- a/lib/constants/app_enums.dart +++ b/lib/constants/app_enums.dart @@ -422,9 +422,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'); diff --git a/lib/customWidget/BaiDuMap/map_preview_widget.dart b/lib/customWidget/BaiDuMap/map_preview_widget.dart new file mode 100644 index 0000000..d7f5cdf --- /dev/null +++ b/lib/customWidget/BaiDuMap/map_preview_widget.dart @@ -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'; + +/// MapPreviewWidget:只读缩略地图(不能交互),显示传入的点位(markers)。 +/// 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> 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 createState() => _MapPreviewWidgetState(); +} + +class _MapPreviewWidgetState extends State { + 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 _initServerAndLoad() async { + _baseUri = await AssetServer().start(); // 单例 asset server + // 把 points JSON 编码到 URL(map.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? _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 _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)), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index ff71459..37759fc 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -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"; /// 图片文件服务 diff --git a/lib/http/modules/key_tasks_api.dart b/lib/http/modules/key_tasks_api.dart new file mode 100644 index 0000000..9921a2d --- /dev/null +++ b/lib/http/modules/key_tasks_api.dart @@ -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> getKeyTasksConfirmList(Map data) { + return HttpManager().request( + '${ApiService.basePath}/keyProject', + '/keyProject/pageConfirm', + method: Method.post, + data: { + ...data + }, + ); + } + + /// 重点工程详情 + static Future> getKeyTasksConfirmDetail(String id) { + return HttpManager().request( + '${ApiService.basePath}/keyProject', + '/keyProject/$id', + method: Method.get, + data: { + // ...data + }, + ); + } + + /// 修改状态 + static Future> upKeyTasksData(Map data) { + return HttpManager().request( + '${ApiService.basePath}/keyProject', + '/keyProject/editStatus', + method: Method.post, + data: { + ...data + }, + ); + } + + /// 摄像头系统-获取所有的摄像头数据 + static Future> getKeyTasksListCameraAll(String type) { + return HttpManager().request( + '${ApiService.basePath}/keyProject', + '/keyProjectCamera/listCameraAll', + method: Method.post, + data: { + // ...data + 'cameraType':type, + }, + ); + } + + +} \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 1d4a459..e617e1b 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -11,6 +11,7 @@ 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/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'; @@ -119,7 +120,7 @@ class HomePageState extends RouteAwareState "现场监管": "dashboard-Site-Supervision", "危险作业": "dashboard-Hazardous-Work", "隐患治理": "dashboard-Hazard-Management", - "重点作业": "", // 无对应,暂时留空 + "重点作业": "dashboard-Hazard-Management", // 无对应,暂时留空 "口门门禁": "dashboard-Gate-Access-Control", "入港培训": "dashboard-Study-Training", }; @@ -636,6 +637,9 @@ class HomePageState extends RouteAwareState case "口门门禁": pushPage(DoorcarTabPage(), context); break; + case "重点作业": + pushPage(KeyTasksTabPage(), context); + break; default: ToastUtil.showNormal(context, '功能开发中...'); break; diff --git a/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_detail_widget.dart b/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_detail_widget.dart new file mode 100644 index 0000000..945246a --- /dev/null +++ b/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_detail_widget.dart @@ -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? initialData; + + // 兼容原有可选初始字段(不再必须使用) + final Map? initialForm; + final List? initialInspectorList; + final List? initialSituationList; + final List? initialDangerList; + final Map? initialPersonUnderInspection; + final Map? initialInitiator; + + @override + State createState() => KeyTaskesDetailWidgetState(); +} + +/// 注意:状态类为公有,外部可以通过 GlobalKey 访问 setDetailData(...) +class KeyTaskesDetailWidgetState extends State { + // 固定只读 + bool _isEdit = false; + + /// 被检查单位相关信息 + Map personUnderInspection = { + 'departmentId': '', + 'departmentName': '', + 'userName': '', + 'userId': '', + }; + + // 检查人员 + late List inspectorList = []; + + // 检查发起人信息(意见+签字) + late Map initiator = {}; + + /// 打回信息 + late List inspectorVerifyList = []; + + /// 检查题目 + final subjectList = [ + {'bianma': "安全检查", 'name': "安全"}, + {'bianma': "环保检查", 'name': "环保"}, + {'bianma': "综合检查", 'name': "综合"}, + ]; + + /// 被检查单位 + late List toCheckUnitList = []; + + // 检查情况 + late List situationList = []; + bool? chooseTitleType = null; + + // 存储各单位的人员列表 + final Map>> _personCache = {}; + + // 隐患列表 + List dangerList = []; + + List signImages = []; + List signTimes = []; // 签字时间列表 + + Map 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 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 _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 _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 _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 _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 _openDrawer(Map hiddenForm, int index) async { + // pushPage(HiddenRecordDetailPage(DangerType.ristRecord,7,hiddenForm['id'],hiddenForm['hiddenId'],false), context); + } + + Future openCustomDrawer(BuildContext context, Widget child) { + return Navigator.of(context).push( + 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 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> 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(), + ); + } +} diff --git a/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_onlylook_detail_page.dart b/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_onlylook_detail_page.dart new file mode 100644 index 0000000..67256c3 --- /dev/null +++ b/lib/pages/home/keyTasks/keyTasksDetail/key_taskes_onlylook_detail_page.dart @@ -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 createState() => _KeyTaskesOnlylookDetailPageState(); +} + +class _KeyTaskesOnlylookDetailPageState extends State { + @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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/keyTasks/keyTasksDetail/key_tasks_confirm_detail_page.dart b/lib/pages/home/keyTasks/keyTasksDetail/key_tasks_confirm_detail_page.dart new file mode 100644 index 0000000..5453922 --- /dev/null +++ b/lib/pages/home/keyTasks/keyTasksDetail/key_tasks_confirm_detail_page.dart @@ -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 { + + final _standardController = TextEditingController(); + + double centerLat = 39.8883; + double centerLng = 119.519; + + late Map pd = {}; + + // List hiddenList = [];//其他隐患信息 + List monitorList = [];//监控列表 + List 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 : Map.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 : Map.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 _getVisitPortArea() async { + + List result = []; + // 计算预选索引 + final List initialIndices = []; + + final resultList = await KeyTasksApi.getKeyTasksListCameraAll('2'); + + List raw = resultList["data"] as List; + if(raw.isEmpty){ + ToastUtil.showNormal(context, '未获取到视频监控'); + } + + result = raw.map((item) => item['cameraName'].toString()).toList(); + + if(monitorList.isNotEmpty){ + + // 提取 value 值列表 + List 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( + 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> 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 _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>( + 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 _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 _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 _upKeyTasksData( ) async { + try { + + if((upKeyTasksData["latitude"]??'').isEmpty||(upKeyTasksData["longitude"]??'').isEmpty){ + ToastUtil.showNormal(context, '请选择作业定位'); + return; + } + + upKeyTasksData["id"]=widget.id; + + final Map 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 upKeyTasksData={ + "applyStatus": '2', + "id": '', + "keyProjectCameraAddCmdList": [ + // { + // "cameraId": "", + // "cameraType": 0, + // "keyProjectId": "" + // } + ], + "latitude": "", + "longitude": "" + }; + + + +} diff --git a/lib/pages/home/keyTasks/key_tasks_confirm_list_page.dart b/lib/pages/home/keyTasks/key_tasks_confirm_list_page.dart new file mode 100644 index 0000000..d9d5525 --- /dev/null +++ b/lib/pages/home/keyTasks/key_tasks_confirm_list_page.dart @@ -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 createState() => _KeyTasksConfirmListPageState(); +} + +class _KeyTasksConfirmListPageState extends State { + final TextEditingController _searchController = TextEditingController(); + + String appBarTitle="重点作业管理"; + String searchHintText='请输入重点作业名称'; + + int _page = 1; + String searchKey=""; + int _totalPage=1; + + bool _isLoading = false; + bool _hasMore = true; + + List _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( + 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 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 _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 _getKeyTasksConfirmList( bool loadMore) async { + try { + if (_isLoading) return; + _isLoading = true; + + keyTasksConfirmListData['projectName']=searchKey.isNotEmpty?searchKey:null; + keyTasksConfirmListData['pageIndex']=_page; + + final Map result; + + result = await KeyTasksApi.getKeyTasksConfirmList(keyTasksConfirmListData); + + if (result['success'] ) { + + _totalPage =result['totalPages'] ?? 1; + final List 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 keyTasksConfirmListData={ + "projectName": "", + "applyStatusList": [1,4],//状态,0:暂存。1:未开工,2:开工申请中,3:已超期,4:进行中,5:完工申请中,6:已完工 + "pageSize": 20, + "pageIndex": 1, + "needTotalCount": true, + }; + + + Future _upKeyTasksData( item) async { + try { + upKeyTasksData["id"]=item["id"]; + final Map 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 upKeyTasksData={ + "applyStatus": '5', + "id": '', + }; + + + + +} diff --git a/lib/pages/home/keyTasks/key_tasks_tab_page.dart b/lib/pages/home/keyTasks/key_tasks_tab_page.dart new file mode 100644 index 0000000..07c9659 --- /dev/null +++ b/lib/pages/home/keyTasks/key_tasks_tab_page.dart @@ -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 createState() => _DoorcarTabPageState(); +} + +class _DoorcarTabPageState extends State { + + final String bannerAsset = 'assets/images/door_banner.png'; + late List 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 _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 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 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((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, + ), + ], + ), + ), + ); + } +} diff --git a/lib/tools/HiddenListTable.dart b/lib/tools/HiddenListTable.dart new file mode 100644 index 0000000..86d75a7 --- /dev/null +++ b/lib/tools/HiddenListTable.dart @@ -0,0 +1,184 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +Widget HiddenListTable({ + required List hiddenList, + required bool forbidEdit, + required String baseImgPath, + required String personSignImg, + required String personSignTime, + required void Function(Map item, int index) showHidden, + required void Function(Map 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 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 更明确、避免惰性展开可能的问题 + children: [ + _buildHeader(), + ...hiddenList.asMap().entries.map((e) => _buildRow(e.value, e.key)).toList(), + ], + ), + ), + ); + }); +} \ No newline at end of file diff --git a/lib/tools/MultiTextFieldWithTitle.dart b/lib/tools/MultiTextFieldWithTitle.dart new file mode 100644 index 0000000..51edd94 --- /dev/null +++ b/lib/tools/MultiTextFieldWithTitle.dart @@ -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 items; // 改为包含文本和图片的列表(外部结构任意字段都会保留) + final bool isEditable; + final bool isAddImage; + final String hintText; + final double fontSize; + final bool isRequired; + final bool isDeletFirst; + final OperationType operationType; + + final ValueChanged>> 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 createState() => + _MultiTextFieldWithTitleState(); +} + +class _MultiTextFieldWithTitleState extends State { + final List _controllers = []; + final List _focusNodes = []; + + /// 保存每一项的完整 map(来自 widget.items 的深复制),这样可以保留额外字段 + final List> _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 item; + if (rawItem is Map) { + item = Map.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'] = []; + } else if (v is String && v.isNotEmpty) { + item['imgPaths'] = [v]; + } else if (v is List) { + item['imgPaths'] = List.from(v.map((e) => e.toString())); + } else { + item['imgPaths'] = []; + } + } else if (!item.containsKey('imgPaths')) { + item['imgPaths'] = []; + } else { + // 确保 imgPaths 是 List + final v = item['imgPaths']; + if (v == null) { + item['imgPaths'] = []; + } else if (v is List) { + item['imgPaths'] = List.from(v.map((e) => e.toString())); + } else if (v is String && v.isNotEmpty) { + item['imgPaths'] = [v]; + } else { + item['imgPaths'] = []; + } + } + + 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) + List _getImageListForIndex(int index) { + if (index < 0 || index >= _itemsData.length) return []; + final v = _itemsData[index]['imgPaths']; + if (v == null) return []; + if (v is List) return List.from(v); + if (v is List) return List.from(v.map((e) => e.toString())); + if (v is String && v.isNotEmpty) return [v]; + return []; + } + + // 将新的图片列表写回 itemsData,同时也同步 imgPath 单字符串字段以兼容外部期待 + void _setImageListForIndex(int index, List list) { + if (index < 0 || index >= _itemsData.length) return; + _itemsData[index]['imgPaths'] = List.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); + + // 新建项:尽量包含 content、imgPath、imgPaths,其他字段为空(外部已有项的字段会被保留) + final Map newItem = { + 'content': '', + 'imgPath': '', + 'imgPaths': [], + }; + + _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> _getAllItems() { + final List> items = []; + for (int i = 0; i < _itemsData.length; i++) { + final Map copy = Map.from( + _itemsData[i], + ); + // 确保 content 与图片字段同步最新值 + copy['content'] = _controllers[i].text; + // 保证 imgPaths 是 List + 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 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 current = _getImageListForIndex(index); + current.add(localPath); + setState(() { + _setImageListForIndex(index, current); + }); + _onDataChanged(); + }, + onMediaRemoved: (localPath) async { + final List 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)); + } + } +} diff --git a/lib/tools/asset_server.dart b/lib/tools/asset_server.dart new file mode 100644 index 0000000..2454a2d --- /dev/null +++ b/lib/tools/asset_server.dart @@ -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 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/ + 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 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'); + } +}