546 lines
16 KiB
Dart
546 lines
16 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:qhd_prevention/constants/app_enums.dart';
|
|
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
|
import 'package:qhd_prevention/http/ApiService.dart';
|
|
import 'package:qhd_prevention/http/modules/file_api.dart';
|
|
import 'package:qhd_prevention/pages/my_appbar.dart';
|
|
|
|
// 单个隐患数据模型
|
|
class HiddenItem {
|
|
String? rectificationSuggestions;
|
|
String? hiddenDescr;
|
|
String? legalBasis;
|
|
bool isSelect=false;
|
|
|
|
HiddenItem({
|
|
this.rectificationSuggestions,
|
|
this.hiddenDescr,
|
|
this.legalBasis,
|
|
|
|
});
|
|
|
|
factory HiddenItem.fromJson(Map<String, dynamic> json) {
|
|
return HiddenItem(
|
|
rectificationSuggestions: json['rectificationSuggestions'] as String?,
|
|
hiddenDescr: json['hiddenDescr'] as String?,
|
|
legalBasis: json['legalBasis'] as String?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'rectificationSuggestions': rectificationSuggestions,
|
|
'hiddenDescr': hiddenDescr,
|
|
'legalBasis': legalBasis,
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
class AiPage extends StatefulWidget {
|
|
const AiPage(this.imagePath, {super.key});
|
|
|
|
final String imagePath;
|
|
|
|
@override
|
|
State<AiPage> createState() => _AiPageState();
|
|
}
|
|
|
|
class _AiPageState extends State<AiPage> with SingleTickerProviderStateMixin {
|
|
Image? _selectedImage;
|
|
bool _isScanning = false;
|
|
bool _showResult = false;
|
|
late AnimationController _animationController;
|
|
late Animation<double> _animation;
|
|
|
|
List<HiddenItem> hiddenItems = [];
|
|
|
|
// List<String> serialNumbers = [
|
|
// 'SN-001-2024',
|
|
// 'SN-002-2024',
|
|
// 'SN-003-2024',
|
|
// 'SN-004-2024',
|
|
// 'SN-005-2024',
|
|
// 'SN-006-2024',
|
|
// 'SN-007-2024',
|
|
// ];
|
|
|
|
// Map<String, bool> selectedItems = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: Duration(seconds: 2), // 单次扫描时间改为2秒
|
|
vsync: this,
|
|
);
|
|
|
|
// 使用往复动画,让扫描线来回移动
|
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
// // 初始化选择状态
|
|
// for (var serial in serialNumbers) {
|
|
// selectedItems[serial] = false;
|
|
// }
|
|
|
|
_pickImage();
|
|
_getAiRecognize();
|
|
}
|
|
|
|
|
|
/// 手动拍照并上传
|
|
Future<void> _getAiRecognize() async {
|
|
|
|
try {
|
|
|
|
final raw = await FileApi.uploadFile( widget.imagePath, UploadFileType.aiRecognitionImages,'');
|
|
if (raw['success'] ) {
|
|
// String imagePath= ApiService.baseImgPath+raw['data']['filePath'];
|
|
String imagePath= 'https://jpfz.qhdsafety.com/gbsFileTest/20251201171655.jpg';
|
|
final res = await HiddenDangerApi.aiRecognitionImages(imagePath);
|
|
|
|
if (res['success'] ) {
|
|
List<dynamic> data=res['data']['aiHiddens']??[];
|
|
if(data.isNotEmpty){
|
|
|
|
for(int i=0;i<data.length;i++){
|
|
if (data[i] is String) {
|
|
String item=data[i];
|
|
try {
|
|
// 尝试解析字符串格式的 JSON
|
|
final cleanedJson = item.replaceAll('\\"', '"').replaceAll('\\\\', '\\');
|
|
final itemJson = jsonDecode(cleanedJson) as Map<String, dynamic>;
|
|
hiddenItems.add(HiddenItem.fromJson(itemJson));
|
|
|
|
} catch (e) {
|
|
print('解析失败: $e, 原始数据: $item');
|
|
}
|
|
} else if (data[i] is Map<String, dynamic>) {
|
|
hiddenItems.add(HiddenItem.fromJson(data[i]));
|
|
}
|
|
}
|
|
|
|
_animationController.stop();
|
|
setState(() {
|
|
_isScanning = false;
|
|
_showResult = true;
|
|
});
|
|
|
|
}else {
|
|
ToastUtil.showError(context, '未获取到隐患,请重新拍照');
|
|
}
|
|
|
|
} else {
|
|
ToastUtil.showError(context, '识别失败,请重试');
|
|
}
|
|
|
|
}else{
|
|
// _showMessage('反馈提交失败');
|
|
ToastUtil.showError(context, '识别失败,请重试');
|
|
}
|
|
|
|
} catch (e, st) {
|
|
debugPrint('[FaceRecognition] manual capture error: $e\n$st');
|
|
ToastUtil.showError(context, '识别失败,请重试');
|
|
}
|
|
|
|
}
|
|
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
// 这里使用 image_picker 包来选择图片
|
|
// final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
|
|
|
// 为了演示,我们使用一个网络图片
|
|
setState(() {
|
|
_selectedImage = Image.file(
|
|
File(widget.imagePath),//'https://picsum.photos/400/600',
|
|
fit: BoxFit.cover,
|
|
);
|
|
_isScanning = true;
|
|
});
|
|
|
|
// 重置动画到开始位置(顶部)
|
|
_animationController.value = 0.0;
|
|
|
|
// 开始往复扫描动画
|
|
_animationController.repeat(reverse: true);
|
|
|
|
// 5秒后结束扫描
|
|
// Future.delayed(Duration(seconds: 5), () {
|
|
// _animationController.stop();
|
|
// setState(() {
|
|
// _isScanning = false;
|
|
// _showResult = true;
|
|
// });
|
|
// });
|
|
}
|
|
|
|
Widget _buildImagePickerBox() {
|
|
return GestureDetector(
|
|
onTap: _pickImage,
|
|
child: Container(
|
|
width: 200,
|
|
height: 150,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey, width: 2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child:
|
|
_selectedImage == null
|
|
? Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.photo_library, size: 40, color: Colors.grey),
|
|
SizedBox(height: 8),
|
|
Text('选择图片', style: TextStyle(color: Colors.grey)),
|
|
],
|
|
)
|
|
: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: _selectedImage,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildScanLine() {
|
|
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
|
|
// 计算扫描线位置,从顶部开始,确保不超出屏幕底部
|
|
double screenHeight = (MediaQuery.of(context).size.height)-100;
|
|
double scanLineHeight = 4.0; // 扫描线高度
|
|
|
|
// 计算扫描线顶部位置,确保扫描线底部不超出屏幕
|
|
double maxTopPosition = screenHeight - scanLineHeight;
|
|
double scanTopPosition = screenHeight * _animation.value;
|
|
|
|
// 限制扫描线位置在屏幕范围内
|
|
scanTopPosition = scanTopPosition.clamp(0.0, maxTopPosition);
|
|
|
|
return Positioned(
|
|
top: scanTopPosition,
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
height: 3,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.blue,
|
|
Colors.blue,
|
|
Colors.blue,
|
|
Colors.transparent,
|
|
],
|
|
stops: [0.0, 0.2, 0.5, 0.8, 1.0],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.blue.withOpacity(0.5),
|
|
blurRadius: 8,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildScanOverlay() {
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
height: MediaQuery.of(context).size.height,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.black.withOpacity(0.7),
|
|
Colors.black.withOpacity(0.3),
|
|
Colors.transparent,
|
|
Colors.black.withOpacity(0.3),
|
|
Colors.black.withOpacity(0.7),
|
|
],
|
|
stops: [0.0, 0.2, 0.5, 0.8, 1.0],
|
|
transform: GradientRotation(_animation.value * 3.14159), // 旋转渐变方向
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildResultPanel() {
|
|
return Positioned(
|
|
top: 20,
|
|
left: 20,
|
|
child: AnimatedContainer(
|
|
duration: Duration(milliseconds: 500),
|
|
width: _showResult ? 80 : 200,
|
|
height: _showResult ? 60 : 150,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black26,
|
|
blurRadius: 8,
|
|
offset: Offset(2, 2),
|
|
),
|
|
],
|
|
),
|
|
child:
|
|
_selectedImage != null
|
|
? ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: _selectedImage,
|
|
)
|
|
: SizedBox(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSerialNumberList() {
|
|
return AnimatedOpacity(
|
|
opacity: _showResult ? 1.0 : 0.0,
|
|
duration: Duration(milliseconds: 500),
|
|
child: Container(
|
|
margin: EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 15),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'检测到的隐患:',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
SizedBox(height: 10),
|
|
Expanded(
|
|
child: Container(
|
|
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: ListView.builder(
|
|
itemCount: hiddenItems.length,
|
|
itemBuilder: (context, index) {
|
|
final serial = hiddenItems[index].hiddenDescr;
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom:
|
|
index < hiddenItems.length - 1
|
|
? BorderSide(color: Colors.grey.shade300)
|
|
: BorderSide.none,
|
|
),
|
|
),
|
|
child: CheckboxListTile(
|
|
title: Text(serial??'', style: TextStyle(fontSize: 14)),
|
|
value: hiddenItems[index].isSelect ,
|
|
onChanged: (bool? value) {
|
|
setState(() {
|
|
hiddenItems[index].isSelect=value ?? false;
|
|
// selectedItems[serial??''] = value ?? false;
|
|
});
|
|
},
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
dense: true,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 20),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
// 取消操作
|
|
_resetPage();
|
|
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey.shade400,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: Size(0, 50),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text('合并', style: TextStyle(fontSize: 16)),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
// 确认操作
|
|
_showConfirmationDialog();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: Size(0, 50),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text('处理', style: TextStyle(fontSize: 16)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showConfirmationDialog() {
|
|
// final selectedSerials =
|
|
// selectedItems.entries
|
|
// .where((entry) => entry.value)
|
|
// .map((entry) => entry.key)
|
|
// .toList();
|
|
//
|
|
// showDialog(
|
|
// context: context,
|
|
// builder:
|
|
// (context) => AlertDialog(
|
|
// title: Text('确认选择'),
|
|
// content: Column(
|
|
// mainAxisSize: MainAxisSize.min,
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
// children: [
|
|
// Text('已选择 ${selectedSerials.length} 个序列号:'),
|
|
// SizedBox(height: 10),
|
|
// if (selectedSerials.isNotEmpty)
|
|
// ...selectedSerials
|
|
// .map((serial) => Text('• $serial'))
|
|
// .toList(),
|
|
// ],
|
|
// ),
|
|
// actions: [
|
|
// TextButton(
|
|
// onPressed: () => Navigator.of(context).pop(),
|
|
// child: Text('确定'),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
}
|
|
|
|
void _resetPage() {
|
|
// setState(() {
|
|
// _selectedImage = null;
|
|
// _isScanning = false;
|
|
// _showResult = false;
|
|
// _animationController.reset();
|
|
//
|
|
// // // 重置选择状态
|
|
// // for (var key in selectedItems.keys) {
|
|
// // selectedItems[key] = false;
|
|
// // }
|
|
// });
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: MyAppbar(title: 'Ai识别'),
|
|
body: Stack(
|
|
children: [
|
|
// 主内容
|
|
Column(
|
|
children: [
|
|
SizedBox(height: 50),
|
|
// if (!_showResult)
|
|
// Center(child: _buildImagePickerBox()),
|
|
if (_showResult) Expanded(child: _buildSerialNumberList()),
|
|
],
|
|
),
|
|
|
|
// 全屏扫描效果
|
|
if (_isScanning && _selectedImage != null)
|
|
Container(
|
|
color: Colors.black,
|
|
child: Stack(
|
|
children: [
|
|
// 全屏图片
|
|
SizedBox.expand(child: _selectedImage),
|
|
|
|
// 扫描遮罩效果
|
|
_buildScanOverlay(),
|
|
|
|
// 扫描线
|
|
_buildScanLine(),
|
|
|
|
// 扫描提示
|
|
Positioned(
|
|
top: 50,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'扫描中...',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 10),
|
|
Container(
|
|
width: 30,
|
|
height: 30,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.blue,
|
|
strokeWidth: 3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 结果面板
|
|
if (_showResult) _buildResultPanel(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|