Merge remote-tracking branch 'origin/master'
# Conflicts: # lib/pages/home/home_page.dartmaster
commit
95c410bf1b
|
|
@ -477,9 +477,10 @@ enum UploadFileType {
|
|||
/// 封闭区域人员申请人签字 - 类型: '609', 路径: 'enclosed_area_personnel_applicant_signature'
|
||||
enclosedAreaPersonnelApplicantSignature('609', 'enclosed_area_personnel_applicant_signature'),
|
||||
/// 封闭区域车辆申请人签字 - 类型: '610', 路径: 'enclosed_area_vehicle_applicant_signature'
|
||||
enclosedAreaVehicleApplicantSignature('610', 'enclosed_area_vehicle_applicant_signature');
|
||||
|
||||
enclosedAreaVehicleApplicantSignature('610', 'enclosed_area_vehicle_applicant_signature'),
|
||||
|
||||
/// 重点工程安全管理协议 - 类型: '168', 路径: 'key_project_safety_management_agreement'
|
||||
keyProjectSafetyManagementAgreement('168', 'key_project_safety_management_agreement');
|
||||
|
||||
const UploadFileType(this.type, this.path);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 String basePath = "https://skqhdg.porthebei.com:9007";
|
||||
// static final String basePath =
|
||||
// isProduct
|
||||
// ? "https://gbs-gateway.qhdsafety.com"
|
||||
// : "http://192.168.20.100:30140";
|
||||
// static final String basePath = "https://skqhdg.porthebei.com:9007";
|
||||
static final String basePath =
|
||||
isProduct
|
||||
? "https://gbs-gateway.qhdsafety.com"
|
||||
: "http://192.168.20.100:30140";
|
||||
|
||||
|
||||
/// 图片文件服务
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import 'package:qhd_prevention/customWidget/custom_button.dart';
|
|||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
import 'package:qhd_prevention/http/ApiService.dart';
|
||||
import 'package:qhd_prevention/pages/home/Study/study_tab_list_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/Tap/work_tab_list_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/doorAndCar/doorCar_tab_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/keyTasks/key_tasks_tab_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/scan_page.dart';
|
||||
import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart';
|
||||
import 'package:qhd_prevention/pages/main_tab.dart';
|
||||
|
|
@ -120,7 +120,7 @@ class HomePageState extends RouteAwareState<HomePage>
|
|||
"现场监管": "dashboard-Site-Supervision",
|
||||
"危险作业": "dashboard-Hazardous-Work",
|
||||
"隐患治理": "dashboard-Hazard-Management",
|
||||
"重点作业": "", // 无对应,暂时留空
|
||||
"重点作业": "dashboard-Hazard-Management", // 无对应,暂时留空
|
||||
"口门门禁": "dashboard-Gate-Access-Control",
|
||||
"入港培训": "dashboard-Study-Training",
|
||||
};
|
||||
|
|
@ -637,10 +637,9 @@ class HomePageState extends RouteAwareState<HomePage>
|
|||
case "口门门禁":
|
||||
pushPage(DoorcarTabPage(), context);
|
||||
break;
|
||||
case "危险作业":
|
||||
pushPage(WorkTabListPage(), context);
|
||||
case "重点作业":
|
||||
pushPage(KeyTasksTabPage(), context);
|
||||
break;
|
||||
|
||||
default:
|
||||
ToastUtil.showNormal(context, '功能开发中...');
|
||||
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