2026.4.10 重点工程
parent
86bee04f85
commit
7ccbcf8ead
|
|
@ -422,9 +422,10 @@ enum UploadFileType {
|
||||||
/// 封闭区域人员申请人签字 - 类型: '609', 路径: 'enclosed_area_personnel_applicant_signature'
|
/// 封闭区域人员申请人签字 - 类型: '609', 路径: 'enclosed_area_personnel_applicant_signature'
|
||||||
enclosedAreaPersonnelApplicantSignature('609', 'enclosed_area_personnel_applicant_signature'),
|
enclosedAreaPersonnelApplicantSignature('609', 'enclosed_area_personnel_applicant_signature'),
|
||||||
/// 封闭区域车辆申请人签字 - 类型: '610', 路径: 'enclosed_area_vehicle_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');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<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 编码到 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<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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,11 @@ class ApiService {
|
||||||
static final bool isProduct = true;
|
static final bool isProduct = true;
|
||||||
|
|
||||||
/// 登录及其他管理后台接口
|
/// 登录及其他管理后台接口
|
||||||
static final String basePath = "https://skqhdg.porthebei.com:9007";
|
// static final String basePath = "https://skqhdg.porthebei.com:9007";
|
||||||
// static final String basePath =
|
static final String basePath =
|
||||||
// isProduct
|
isProduct
|
||||||
// ? "https://gbs-gateway.qhdsafety.com"
|
? "https://gbs-gateway.qhdsafety.com"
|
||||||
// : "http://192.168.20.100:30140";
|
: "http://192.168.20.100:30140";
|
||||||
|
|
||||||
|
|
||||||
/// 图片文件服务
|
/// 图片文件服务
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||||
import 'package:qhd_prevention/http/ApiService.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/Study/study_tab_list_page.dart';
|
||||||
import 'package:qhd_prevention/pages/home/doorAndCar/doorCar_tab_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/scan_page.dart';
|
||||||
import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart';
|
import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart';
|
||||||
import 'package:qhd_prevention/pages/main_tab.dart';
|
import 'package:qhd_prevention/pages/main_tab.dart';
|
||||||
|
|
@ -119,7 +120,7 @@ class HomePageState extends RouteAwareState<HomePage>
|
||||||
"现场监管": "dashboard-Site-Supervision",
|
"现场监管": "dashboard-Site-Supervision",
|
||||||
"危险作业": "dashboard-Hazardous-Work",
|
"危险作业": "dashboard-Hazardous-Work",
|
||||||
"隐患治理": "dashboard-Hazard-Management",
|
"隐患治理": "dashboard-Hazard-Management",
|
||||||
"重点作业": "", // 无对应,暂时留空
|
"重点作业": "dashboard-Hazard-Management", // 无对应,暂时留空
|
||||||
"口门门禁": "dashboard-Gate-Access-Control",
|
"口门门禁": "dashboard-Gate-Access-Control",
|
||||||
"入港培训": "dashboard-Study-Training",
|
"入港培训": "dashboard-Study-Training",
|
||||||
};
|
};
|
||||||
|
|
@ -636,6 +637,9 @@ class HomePageState extends RouteAwareState<HomePage>
|
||||||
case "口门门禁":
|
case "口门门禁":
|
||||||
pushPage(DoorcarTabPage(), context);
|
pushPage(DoorcarTabPage(), context);
|
||||||
break;
|
break;
|
||||||
|
case "重点作业":
|
||||||
|
pushPage(KeyTasksTabPage(), context);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
ToastUtil.showNormal(context, '功能开发中...');
|
ToastUtil.showNormal(context, '功能开发中...');
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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": ""
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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": '',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
// 新建项:尽量包含 content、imgPath、imgPaths,其他字段为空(外部已有项的字段会被保留)
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue