改bug
parent
e8bb886e93
commit
a4072fd761
|
@ -24,9 +24,9 @@ class ApiService {
|
|||
/// 登录及其他管理后台接口
|
||||
// static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb";
|
||||
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";//测试服务器
|
||||
// static const String basePath = "http://192.168.20.240:8500/integrated_whb";
|
||||
static const String basePath = "http://192.168.20.240:8500/integrated_whb";
|
||||
// static const String basePath = "http://192.168.0.25:28199";//王轩服务器
|
||||
static const String basePath = "http://192.168.0.45:28199";//长久服务器
|
||||
// static const String basePath = "http://192.168.0.45:28199";//长久服务器
|
||||
|
||||
/// 图片文件服务
|
||||
static const String baseImgPath = "https://file.zcloudchina.com/YTHFile";
|
||||
|
|
|
@ -137,70 +137,48 @@ class _CheckRecordDetailPageState extends State<CheckRecordDetailPage> {
|
|||
List<Map<String, dynamic>> buildCoversFromRes(Map res) {
|
||||
final list = <Map<String, dynamic>>[];
|
||||
const String defaultIconAsset = 'assets/map/50.png';
|
||||
final seen = <String>{}; // "lat|lng|type" 去重
|
||||
|
||||
void tryAdd(double? lat, double? lng, String type, [dynamic data]) {
|
||||
if (lat == null || lng == null) return;
|
||||
final key = '${lat.toStringAsFixed(6)}|${lng.toStringAsFixed(6)}|$type';
|
||||
if (seen.contains(key)) return;
|
||||
seen.add(key);
|
||||
list.add({
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'icon': defaultIconAsset,
|
||||
'data': {'type': type, 'item': data}
|
||||
});
|
||||
}
|
||||
|
||||
final cinfo = res['cinfo'];
|
||||
if (cinfo != null &&
|
||||
cinfo['LONGITUDE'] != null &&
|
||||
cinfo['LATITUDE'] != null &&
|
||||
cinfo['LONGITUDE'].toString().isNotEmpty &&
|
||||
cinfo['LATITUDE'].toString().isNotEmpty) {
|
||||
final lat = double.tryParse(cinfo['LATITUDE'].toString());
|
||||
final lng = double.tryParse(cinfo['LONGITUDE'].toString());
|
||||
if (lat != null && lng != null) {
|
||||
list.add({
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'icon': defaultIconAsset,
|
||||
'data': {'type': 'cinfo'}
|
||||
});
|
||||
}
|
||||
if (cinfo != null) {
|
||||
final lat = double.tryParse(cinfo['LATITUDE']?.toString() ?? '');
|
||||
final lng = double.tryParse(cinfo['LONGITUDE']?.toString() ?? '');
|
||||
tryAdd(lat, lng, 'cinfo', cinfo);
|
||||
}
|
||||
|
||||
final check = res['checkrecord'];
|
||||
if (check != null &&
|
||||
check['LONGITUDE'] != null &&
|
||||
check['LATITUDE'] != null &&
|
||||
check['LONGITUDE'].toString().isNotEmpty &&
|
||||
check['LATITUDE'].toString().isNotEmpty) {
|
||||
final lat = double.tryParse(check['LATITUDE'].toString());
|
||||
final lng = double.tryParse(check['LONGITUDE'].toString());
|
||||
if (lat != null && lng != null) {
|
||||
list.add({
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'icon': defaultIconAsset,
|
||||
'data': {'type': 'checkrecord'}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final vlist = (res['varList'] as List?) ?? [];
|
||||
for (final item in vlist) {
|
||||
try {
|
||||
if (item != null &&
|
||||
item['LONGITUDE'] != null &&
|
||||
item['LATITUDE'] != null &&
|
||||
item['LONGITUDE'].toString().isNotEmpty &&
|
||||
item['LATITUDE'].toString().isNotEmpty) {
|
||||
final lat = double.tryParse(item['LATITUDE'].toString());
|
||||
final lng = double.tryParse(item['LONGITUDE'].toString());
|
||||
if (lat != null && lng != null) {
|
||||
list.add({
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'icon': defaultIconAsset,
|
||||
'data': {'type': 'varList', 'item': item}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 忽略单条解析错误
|
||||
}
|
||||
}
|
||||
// final check = res['checkrecord'];
|
||||
// if (check != null) {
|
||||
// final lat = double.tryParse(check['LATITUDE']?.toString() ?? '');
|
||||
// final lng = double.tryParse(check['LONGITUDE']?.toString() ?? '');
|
||||
// tryAdd(lat, lng, 'checkrecord', check);
|
||||
// }
|
||||
//
|
||||
// final vlist = (res['varList'] as List?) ?? [];
|
||||
// for (final item in vlist) {
|
||||
// try {
|
||||
// final lat = double.tryParse(item['LATITUDE']?.toString() ?? '');
|
||||
// final lng = double.tryParse(item['LONGITUDE']?.toString() ?? '');
|
||||
// tryAdd(lat, lng, 'varList', item);
|
||||
// } catch (_) {}
|
||||
// }
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
// 当 map 创建完成后会回调这里
|
||||
void _onBmfMapCreated(BMFMapController controller) async {
|
||||
_mapController = controller;
|
||||
|
@ -255,6 +233,18 @@ class _CheckRecordDetailPageState extends State<CheckRecordDetailPage> {
|
|||
}
|
||||
|
||||
Future<void> _updateMarkers() async {
|
||||
bool cleared = false;
|
||||
try {
|
||||
final res = await _mapController!.cleanAllMarkers();
|
||||
debugPrint('_updateMarkers: cleanAllMarkers returned: $res');
|
||||
cleared = (res == true);
|
||||
} catch (e) {
|
||||
debugPrint('_updateMarkers: cleanAllMarkers exception: $e');
|
||||
}
|
||||
|
||||
if (!cleared) {
|
||||
debugPrint('_updateMarkers: warning - markers not cleared. proceeding but will guard duplicates.');
|
||||
}
|
||||
if (_mapController == null) {
|
||||
debugPrint('_updateMarkers: mapController is null');
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
||||
|
@ -49,6 +50,7 @@ class SignImageData {
|
|||
@override
|
||||
String toString() => 'SignImageData(key:$key, filePath:$filePath, SIGNER_TIME:$SIGNER_TIME)';
|
||||
}
|
||||
|
||||
class DangerousOptionsPage extends StatefulWidget {
|
||||
final int index;
|
||||
final int status;
|
||||
|
@ -86,17 +88,21 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
status = widget.status;
|
||||
measures = widget.measures;
|
||||
imgList = List<ImageData>.from(widget.imgList);
|
||||
signImgList =
|
||||
widget.signImgList.map((map) => SignImageData.fromJson(map)).toList();
|
||||
signImgList = widget.signImgList.map((map) => SignImageData.fromJson(map)).toList();
|
||||
}
|
||||
|
||||
/// 拍照或选图后的回调
|
||||
/// 拍照或选图后的回调(上传)
|
||||
Future<void> _onImageAdded(String localPath) async {
|
||||
// 上传到服务器
|
||||
if (!mounted) return;
|
||||
LoadingDialogHelper.show();
|
||||
final res = await ApiService.uploadSaveFile(localPath);
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
try {
|
||||
// 给网络请求设置超时,避免长时间卡住
|
||||
final res = await ApiService.uploadSaveFile(localPath).timeout(const Duration(seconds: 30));
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (res['result'] == 'success') {
|
||||
final url = res['FILE_PATH'] as String;
|
||||
setState(() {
|
||||
|
@ -104,25 +110,41 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
});
|
||||
} else {
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
// 保持与原逻辑一致:清空并通知
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
}
|
||||
} catch (_) {
|
||||
} on TimeoutException {
|
||||
LoadingDialogHelper.hide();
|
||||
if (!mounted) return;
|
||||
ToastUtil.showError(context, '上传超时,请检查网络后重试');
|
||||
} catch (e, st) {
|
||||
LoadingDialogHelper.hide();
|
||||
debugPrint('_onImageAdded error: $e\n$st');
|
||||
if (!mounted) return;
|
||||
ToastUtil.showError(context, '上传失败,资源过大请重新选择');
|
||||
setState(() {
|
||||
imgList = [];
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
});
|
||||
MediaBus().emit(MediaEvent.clear(kAcceptVideoSectionKey));
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除图片处理:调用删除接口并更新列表
|
||||
Future<void> _onImageRemoved(ImageData item) async {
|
||||
if (item.serverPath != null) {
|
||||
await ApiService.deleteSaveFile(item.serverPath!);
|
||||
try {
|
||||
if (item.serverPath != null && item.serverPath.isNotEmpty) {
|
||||
// 不等待过久:加超时保护
|
||||
await ApiService.deleteSaveFile(item.serverPath!).timeout(const Duration(seconds: 15));
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('_onImageRemoved: delete api error: $e\n$st');
|
||||
// 不阻塞删除流程:即使删除接口失败,也从本地 UI 中移除
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
imgList.remove(item);
|
||||
});
|
||||
|
@ -134,53 +156,70 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
return;
|
||||
}
|
||||
LoadingDialogHelper.show();
|
||||
List<String> filePaths =
|
||||
signImgList.map((img) => img.filePath ?? '').toList();
|
||||
final result = await ApiService.saveDangerousOptionsFile(filePaths);
|
||||
final List<dynamic> signList = result['FILE_PATH_LIST'];
|
||||
List<Map<String, dynamic>> sineImageList = [];
|
||||
for (SignImageData data in signImgList) {
|
||||
for (Map<String, dynamic> img in signList) {
|
||||
String imgName = 'file${data.key}';
|
||||
if (data.filePath!.contains('uploadFiles')) {
|
||||
final idata = {
|
||||
'filePath': data.filePath,
|
||||
'SIGNER_TIME': data.SIGNER_TIME,
|
||||
'key': data.key,
|
||||
};
|
||||
sineImageList.add(idata);
|
||||
}
|
||||
if (imgName == img['key']) {
|
||||
final idata = {
|
||||
'filePath': img['filePath'] ?? '',
|
||||
'SIGNER_TIME': data.SIGNER_TIME,
|
||||
'key': data.key,
|
||||
};
|
||||
sineImageList.add(idata);
|
||||
try {
|
||||
// 组织要上传的路径(以原逻辑为准)
|
||||
List<String> filePaths = signImgList.map((img) => img.filePath ?? '').toList();
|
||||
|
||||
// 保存文件 API,增加超时保护
|
||||
final result = await ApiService.saveDangerousOptionsFile(filePaths).timeout(const Duration(seconds: 30));
|
||||
|
||||
final List<dynamic> signList = result['FILE_PATH_LIST'] ?? [];
|
||||
List<Map<String, dynamic>> sineImageList = [];
|
||||
for (SignImageData data in signImgList) {
|
||||
for (Map<String, dynamic> img in signList) {
|
||||
String imgName = 'file${data.key}';
|
||||
if (data.filePath != null && data.filePath!.contains('uploadFiles')) {
|
||||
final idata = {
|
||||
'filePath': data.filePath,
|
||||
'SIGNER_TIME': data.SIGNER_TIME,
|
||||
'key': data.key,
|
||||
};
|
||||
sineImageList.add(idata);
|
||||
}
|
||||
if (imgName == (img['key'] ?? '')) {
|
||||
final idata = {
|
||||
'filePath': img['filePath'] ?? '',
|
||||
'SIGNER_TIME': data.SIGNER_TIME,
|
||||
'key': data.key,
|
||||
};
|
||||
sineImageList.add(idata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => buttonLoading = true); // 保持原行为
|
||||
LoadingDialogHelper.hide();
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, {
|
||||
'imgList': imgList.map((e) => {'local': e.localPath, 'remote': e.serverPath}).toList(),
|
||||
'signImgList': sineImageList,
|
||||
'index': index,
|
||||
'status': status,
|
||||
});
|
||||
} on TimeoutException {
|
||||
LoadingDialogHelper.hide();
|
||||
if (!mounted) return;
|
||||
ToastUtil.showError(context, '保存超时,请稍后重试');
|
||||
} catch (e, st) {
|
||||
LoadingDialogHelper.hide();
|
||||
debugPrint('_submit error: $e\n$st');
|
||||
if (!mounted) return;
|
||||
ToastUtil.showError(context, '提交失败,请重试');
|
||||
}
|
||||
setState(() => buttonLoading = true);
|
||||
LoadingDialogHelper.hide();
|
||||
Navigator.pop(context, {
|
||||
'imgList':
|
||||
imgList
|
||||
.map((e) => {'local': e.localPath, 'remote': e.serverPath})
|
||||
.toList(),
|
||||
'signImgList': sineImageList,
|
||||
'index': index,
|
||||
'status': status,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sign() async {
|
||||
await NativeOrientation.setLandscape();
|
||||
final String path = await Navigator.push(
|
||||
final String? path = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (c) => MineSignPage()),
|
||||
);
|
||||
await NativeOrientation.setPortrait();
|
||||
if (path != null) {
|
||||
|
||||
if (!mounted) return;
|
||||
if (path != null && path.isNotEmpty) {
|
||||
final now = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
|
||||
setState(() {
|
||||
final imageData = SignImageData(
|
||||
|
@ -191,63 +230,121 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
signImgList.add(imageData);
|
||||
signTimes.add(now);
|
||||
});
|
||||
//FocusHelper.clearFocus(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _signListWidget() {
|
||||
// 使用 ListView 或 Column 的时候,注意图片的解码在主线程可能造成阻塞。这里限制尺寸并使用 ResizeImage 去解码。
|
||||
return Column(
|
||||
children:
|
||||
signImgList.map((imgData) {
|
||||
final idx = signImgList.indexOf(imgData);
|
||||
return Column(
|
||||
children: signImgList.map((imgData) {
|
||||
final idx = signImgList.indexOf(imgData);
|
||||
final rawPath = (imgData.filePath ?? '').toString();
|
||||
final isNetwork = rawPath.startsWith('http://') || rawPath.startsWith('https://');
|
||||
|
||||
// 目标尺寸(尽量与 UI 展示一致,避免解码过大)
|
||||
const targetWidth = 460; // 约为显示宽度 * devicePixelRatio 的一个合理值
|
||||
const targetHeight = 300;
|
||||
|
||||
Widget imageWidget;
|
||||
if (rawPath.isEmpty) {
|
||||
imageWidget = Container(
|
||||
width: 230,
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image, size: 28, color: Colors.grey)),
|
||||
);
|
||||
} else if (isNetwork) {
|
||||
// 网络图片:加上 width/height 与 loadingBuilder,避免阻塞
|
||||
imageWidget = Image.network(
|
||||
rawPath.startsWith('http') ? rawPath : ApiService.baseImgPath + rawPath,
|
||||
width: 230,
|
||||
height: 150,
|
||||
fit: BoxFit.cover,
|
||||
// 显示 loading 占位,避免白屏卡顿
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
width: 230,
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
},
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
width: 230,
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 本地文件:使用 ResizeImage 包装 FileImage,按目标像素解码,避免解码超大图片堵主线程
|
||||
final file = File(rawPath);
|
||||
if (file.existsSync()) {
|
||||
imageWidget = Image(
|
||||
image: ResizeImage(
|
||||
FileImage(file),
|
||||
// 这里给出一个合理的解码尺寸(可根据实际设备 pixel ratio 适当调整)
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
),
|
||||
width: 230,
|
||||
height: 150,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
width: 230,
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
imageWidget = Container(
|
||||
width: 230,
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(child: Icon(Icons.broken_image)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
GestureDetector(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 230,
|
||||
maxHeight: 150,
|
||||
),
|
||||
child: imageWidget,
|
||||
),
|
||||
onTap: () => presentOpaque(SingleImageViewer(imageUrl: rawPath), context),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 230,
|
||||
maxHeight: 150,
|
||||
),
|
||||
child:
|
||||
(imgData.filePath ?? '').contains('uploadFiles')
|
||||
? Image.network(
|
||||
'${ApiService.baseImgPath}${imgData.filePath}',
|
||||
)
|
||||
: Image.file(
|
||||
File(imgData.filePath ?? ''),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
onTap:
|
||||
() => presentOpaque(
|
||||
SingleImageViewer(imageUrl: imgData.filePath ?? ''),
|
||||
context,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CustomButton(
|
||||
text: '删除',
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
setState(() => signImgList.removeAt(idx));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
CustomButton(
|
||||
text: '删除',
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
if (!mounted) return;
|
||||
setState(() => signImgList.removeAt(idx));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -258,19 +355,19 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
body: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
color: Colors.white,
|
||||
child: ListView(
|
||||
children: [
|
||||
Table(
|
||||
border: TableBorder.all(color: Colors.grey.shade300),
|
||||
columnWidths: {0: FlexColumnWidth(3), 1: FlexColumnWidth(2)},
|
||||
columnWidths: const {0: FlexColumnWidth(3), 1: FlexColumnWidth(2)},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: BoxDecoration(color: Colors.grey.shade200),
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'主要安全措施',
|
||||
|
@ -279,7 +376,7 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'操作',
|
||||
|
@ -305,12 +402,12 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
RadioListTile<int>(
|
||||
value: -1,
|
||||
groupValue: status,
|
||||
title: Text('不涉及'),
|
||||
title: const Text('不涉及'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
visualDensity: VisualDensity(
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: -4,
|
||||
horizontal: 0,
|
||||
),
|
||||
|
@ -319,12 +416,12 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
RadioListTile<int>(
|
||||
value: 1,
|
||||
groupValue: status,
|
||||
title: Text('涉及'),
|
||||
title: const Text('涉及'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
visualDensity: VisualDensity(
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: -4,
|
||||
horizontal: 0,
|
||||
),
|
||||
|
@ -343,15 +440,17 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
maxCount: 2,
|
||||
mediaType: MediaType.image,
|
||||
initialMediaPaths:
|
||||
imgList
|
||||
.map((e) => '${ApiService.baseImgPath}${e.serverPath}')
|
||||
.toList(),
|
||||
imgList.map((e) => '${ApiService.baseImgPath}${e.serverPath}').toList(),
|
||||
onChanged: (paths) {},
|
||||
onMediaAdded: _onImageAdded,
|
||||
onMediaRemoved: (path) {
|
||||
print(path);
|
||||
final item = imgList.firstWhere((e) => path.contains(e.localPath) );
|
||||
_onImageRemoved(item);
|
||||
// 原逻辑保持:通过包含 localPath 的方式匹配
|
||||
try {
|
||||
final item = imgList.firstWhere((e) => path.contains(e.localPath));
|
||||
_onImageRemoved(item);
|
||||
} catch (e) {
|
||||
debugPrint('onMediaRemoved: find item error: $e');
|
||||
}
|
||||
},
|
||||
onAiIdentify: () {},
|
||||
),
|
||||
|
@ -359,7 +458,7 @@ class _DangerousOptionsPageState extends State<DangerousOptionsPage> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('签字:', style: TextStyle(fontSize: 16)),
|
||||
const Text('签字:', style: TextStyle(fontSize: 16)),
|
||||
CustomButton(
|
||||
text: '新增手写签字',
|
||||
height: 36,
|
||||
|
|
|
@ -114,7 +114,7 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
|
|||
_getHotWorkNameList();
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
|
@ -333,15 +333,6 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
|
||||
Widget _card(Widget child) {
|
||||
|
|
|
@ -103,7 +103,7 @@ class _CutroadApplyDetailState extends State<CutroadApplyDetail> {
|
|||
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
pd['WORK_REASON'] = _contentController.text.trim();
|
||||
|
@ -504,14 +504,7 @@ class _CutroadApplyDetailState extends State<CutroadApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ class _BreakgroundApplyDetailState extends State<BreakgroundApplyDetail> {
|
|||
}
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
pd['JOB_CONTENT'] = _contentController.text.trim();
|
||||
|
@ -275,14 +275,7 @@ class _BreakgroundApplyDetailState extends State<BreakgroundApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
Future<void> _chooseLevel() async {
|
||||
|
|
|
@ -105,7 +105,7 @@ class _HoistworkApplyDetailState extends State<HoistworkApplyDetail> {
|
|||
}
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
pd['WORK_CONTENT'] = _contentController.text.trim();
|
||||
|
@ -248,14 +248,7 @@ class _HoistworkApplyDetailState extends State<HoistworkApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
void set_pd_DEPARTMENT_ID(EditUserType type, String id) {
|
||||
|
|
|
@ -104,7 +104,7 @@ class _HighworkApplyDetailState extends State<HighworkApplyDetail> {
|
|||
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
pd['WORK_CONTENT'] = _contentController.text.trim();
|
||||
|
@ -245,14 +245,7 @@ class _HighworkApplyDetailState extends State<HighworkApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
void set_pd_DEPARTMENT_ID(EditUserType type, String id) {
|
||||
|
|
|
@ -111,7 +111,7 @@ class _ElectricityApplyDetailState extends State<ElectricityApplyDetail> {
|
|||
}
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
pd['WORK_CONTENT'] = _contentController.text.trim();
|
||||
|
||||
|
@ -244,14 +244,7 @@ class _ElectricityApplyDetailState extends State<ElectricityApplyDetail> {
|
|||
unitAllList = result['varList'] ?? [];
|
||||
});
|
||||
}
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
void set_pd_DEPARTMENT_ID(EditUserType type, String id) {
|
||||
pd['${type.name}_DEPARTMENT_ID'] = id;
|
||||
|
|
|
@ -180,7 +180,7 @@ class _BlindboardCjryDetailState extends State<BlindboardCjryDetail> {
|
|||
// formData['WORK_CONTENT'] = _contentController.text.trim();
|
||||
formData['CONIMG_PATH'] = serverPathString;
|
||||
|
||||
formData['DESCR'] = FormUtils.hasValue(pd, 'DESCR') ? pd['DESCR'] : '无';
|
||||
formData['DESCR'] = '';
|
||||
formData['BLINDBOARD_ID'] = pd['BLINDBOARD_ID'] ?? widget.BLINDBOARD_ID;
|
||||
formData['SIGNTIME'] = signTimes.join(',');
|
||||
formData['USER_ID'] = SessionService.instance.loginUserId;
|
||||
|
|
|
@ -110,7 +110,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> {
|
|||
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_nameController.addListener(() {
|
||||
setState(() {
|
||||
pd['NAME'] = _nameController.text.trim();
|
||||
|
@ -234,14 +234,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> {
|
|||
unitAllList = result['varList'] ?? [];
|
||||
});
|
||||
}
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
void set_pd_DEPARTMENT_ID(EditUserType type, String id) {
|
||||
pd['${type.name}_DEPARTMENT_ID'] = id;
|
||||
|
|
|
@ -112,7 +112,7 @@ class _SpaceworkApplyDetailState extends State<SpaceworkApplyDetail> {
|
|||
_getSpaceWorkNameList();
|
||||
_getVideoList();
|
||||
_getUnitListAll();
|
||||
_getPlsList();
|
||||
|
||||
_contentController.addListener(() {
|
||||
setState(() {
|
||||
pd['WORK_CONTENT'] = _contentController.text.trim();
|
||||
|
@ -245,14 +245,7 @@ class _SpaceworkApplyDetailState extends State<SpaceworkApplyDetail> {
|
|||
});
|
||||
}
|
||||
|
||||
/// 作业区域列表
|
||||
Future<void> _getPlsList() async {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
setState(() {
|
||||
final String zTreeNodes = result['zTreeNodes'] ?? '';
|
||||
workAreaList = jsonDecode(zTreeNodes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
void set_pd_DEPARTMENT_ID(EditUserType type, String id) {
|
||||
|
|
|
@ -24,20 +24,18 @@ class Category {
|
|||
final name = (json['name'] ?? '').toString();
|
||||
final positions = (json['POSITIONS'] ?? '').toString();
|
||||
|
||||
// children 可能不存在、为 null、或已经是 List
|
||||
// children 仍然尝试解析(兼容旧结构),但在扁平模式下不会使用
|
||||
final rawChildren = json['children'];
|
||||
List<Category> childrenList = [];
|
||||
if (rawChildren is List) {
|
||||
childrenList = rawChildren
|
||||
.where((e) => e != null)
|
||||
.map((e) {
|
||||
// e 可能已经是 Map<String,dynamic> 或 dynamic
|
||||
if (e is Map<String, dynamic>) {
|
||||
return Category.fromJson(e);
|
||||
} else if (e is Map) {
|
||||
return Category.fromJson(Map<String, dynamic>.from(e));
|
||||
} else {
|
||||
// 无法解析的项跳过,或返回空占位
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
@ -54,9 +52,7 @@ class Category {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
typedef DeptSelectCallback =
|
||||
void Function(String id, String POSITIONS, String name);
|
||||
typedef DeptSelectCallback = void Function(String id, String POSITIONS, String name);
|
||||
|
||||
class WorkAreaPicker extends StatefulWidget {
|
||||
final DeptSelectCallback onSelected;
|
||||
|
@ -72,8 +68,6 @@ class _WorkAreaPickerState extends State<WorkAreaPicker> {
|
|||
String selectedName = '';
|
||||
String selected_POSITIONS = '';
|
||||
|
||||
Set<String> expandedSet = {};
|
||||
|
||||
List<Category> original = [];
|
||||
List<Category> filtered = [];
|
||||
bool loading = true;
|
||||
|
@ -83,11 +77,9 @@ class _WorkAreaPickerState extends State<WorkAreaPicker> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始均为空
|
||||
selectedId = '';
|
||||
selectedName = '';
|
||||
selected_POSITIONS = '';
|
||||
expandedSet = {};
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
_loadData();
|
||||
}
|
||||
|
@ -102,36 +94,60 @@ class _WorkAreaPickerState extends State<WorkAreaPicker> {
|
|||
Future<void> _loadData() async {
|
||||
try {
|
||||
final result = await ApiService.getWorkAreaList();
|
||||
final dynamic nodesField = result['zTreeNodes'];
|
||||
List<dynamic> raw = result['varList'] ?? [];
|
||||
|
||||
// nodesField 可能是已经解析过的 List 或者是 String(JSON 字符串)
|
||||
List<dynamic> raw;
|
||||
if (nodesField is String) {
|
||||
raw = json.decode(nodesField) as List<dynamic>;
|
||||
} else if (nodesField is List) {
|
||||
raw = nodesField;
|
||||
} else {
|
||||
raw = [];
|
||||
// 解析为 Category 列表(可能包含 children)
|
||||
final parsed = raw
|
||||
.map((e) {
|
||||
if (e is Map<String, dynamic>) return Category.fromJson(e);
|
||||
if (e is Map) return Category.fromJson(Map<String, dynamic>.from(e));
|
||||
return null;
|
||||
})
|
||||
.whereType<Category>()
|
||||
.toList();
|
||||
|
||||
// 扁平化:父与子都放到同一列表中(以兼容后端可能还是两层结构)
|
||||
final List<Category> flat = [];
|
||||
for (final c in parsed) {
|
||||
flat.add(c);
|
||||
if (c.children.isNotEmpty) {
|
||||
flat.addAll(c.children);
|
||||
}
|
||||
}
|
||||
|
||||
// debug 打印(运行中可查看控制台)
|
||||
// print('raw length = ${raw.length}');
|
||||
// 规范化 id:如果后端 ID 为空,则为该项赋予一个独一无二的占位 id,
|
||||
// 避免与默认 selectedId = '' 冲突导致“全部被选中”的问题。
|
||||
final List<Category> normalized = [];
|
||||
for (var i = 0; i < flat.length; i++) {
|
||||
final c = flat[i];
|
||||
final rawId = (c.ELECTRONIC_FENCE_AREA_ID ?? '').toString().trim();
|
||||
if (rawId.isEmpty) {
|
||||
// 生成占位 id(基于索引和 name hash,足够唯一且不会是空字符串)
|
||||
final generatedId = '__generated_${i}_${c.name.hashCode}';
|
||||
normalized.add(Category(
|
||||
ELECTRONIC_FENCE_AREA_ID: generatedId,
|
||||
name: c.name,
|
||||
POSITIONS: c.POSITIONS,
|
||||
children: const [],
|
||||
));
|
||||
} else {
|
||||
// 保持原 id
|
||||
normalized.add(Category(
|
||||
ELECTRONIC_FENCE_AREA_ID: rawId,
|
||||
name: c.name,
|
||||
POSITIONS: c.POSITIONS,
|
||||
children: const [],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
original = raw
|
||||
.map((e) {
|
||||
if (e is Map<String, dynamic>) return Category.fromJson(e);
|
||||
if (e is Map) return Category.fromJson(Map<String, dynamic>.from(e));
|
||||
return null;
|
||||
})
|
||||
.whereType<Category>()
|
||||
.toList();
|
||||
original = normalized;
|
||||
filtered = original;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e, st) {
|
||||
// 打印错误以便调试
|
||||
// print('loadData error: $e\n$st');
|
||||
debugPrint('WorkAreaPicker._loadData error: $e\n$st');
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
@ -143,83 +159,40 @@ class _WorkAreaPickerState extends State<WorkAreaPicker> {
|
|||
});
|
||||
}
|
||||
|
||||
// 扁平过滤:在 name 或 POSITIONS 中搜索
|
||||
List<Category> _filterCategories(List<Category> list, String query) {
|
||||
List<Category> result = [];
|
||||
for (var cat in list) {
|
||||
final children = _filterCategories(cat.children, query);
|
||||
if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) {
|
||||
result.add(
|
||||
Category(
|
||||
ELECTRONIC_FENCE_AREA_ID: cat.ELECTRONIC_FENCE_AREA_ID,
|
||||
name: cat.name,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
if (query.isEmpty) return list;
|
||||
final q = query.toLowerCase();
|
||||
return list.where((c) {
|
||||
final name = (c.name ?? '').toLowerCase();
|
||||
final pos = (c.POSITIONS ?? '').toLowerCase();
|
||||
return name.contains(q) || pos.contains(q);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _buildRow(Category cat, int indent) {
|
||||
final hasChildren = cat.children.isNotEmpty;
|
||||
final isExpanded = expandedSet.contains(cat.ELECTRONIC_FENCE_AREA_ID);
|
||||
final isSelected = cat.ELECTRONIC_FENCE_AREA_ID == selectedId;
|
||||
return Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (hasChildren) {
|
||||
isExpanded
|
||||
? expandedSet.remove(cat.ELECTRONIC_FENCE_AREA_ID)
|
||||
: expandedSet.add(cat.ELECTRONIC_FENCE_AREA_ID);
|
||||
}
|
||||
selectedId = cat.ELECTRONIC_FENCE_AREA_ID;
|
||||
selectedName = cat.name;
|
||||
selected_POSITIONS = cat.POSITIONS;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 16.0 * indent),
|
||||
SizedBox(
|
||||
width: 24,
|
||||
child:
|
||||
hasChildren
|
||||
? Icon(
|
||||
isExpanded
|
||||
? Icons.arrow_drop_down_rounded
|
||||
: Icons.arrow_right_rounded,
|
||||
size: 35,
|
||||
color: Colors.grey[600],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(cat.name),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Icon(
|
||||
isSelected
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_unchecked,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget _buildRow(Category cat) {
|
||||
final isSelected = selectedId == cat.ELECTRONIC_FENCE_AREA_ID;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedId = cat.ELECTRONIC_FENCE_AREA_ID;
|
||||
selectedName = cat.name;
|
||||
selected_POSITIONS = cat.POSITIONS;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(cat.name)),
|
||||
Icon(
|
||||
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
||||
color: isSelected ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasChildren && isExpanded)
|
||||
...cat.children.map((c) => _buildRow(c, indent + 1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -267,18 +240,19 @@ class _WorkAreaPickerState extends State<WorkAreaPicker> {
|
|||
],
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child:
|
||||
loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Container(
|
||||
color: Colors.white,
|
||||
child: ListView.builder(
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0),
|
||||
),
|
||||
),
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: (filtered.isEmpty
|
||||
? const Center(child: Text('没有找到匹配的工作区域'))
|
||||
: Container(
|
||||
color: Colors.white,
|
||||
child: ListView.builder(
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (ctx, idx) => _buildRow(filtered[idx]),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.1.2+8
|
||||
version: 2.1.2+9
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
|
|
Loading…
Reference in New Issue