cage_change_page.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import 'package:chicken_farm/apis/index.dart';
  2. import 'package:chicken_farm/core/utils/datetime_util.dart';
  3. import 'package:chicken_farm/core/utils/logger.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:chicken_farm/components/vb_app_bar.dart';
  6. import 'package:chicken_farm/core/utils/toast.dart';
  7. import 'package:chicken_farm/core/services/pda/scan_channel.dart';
  8. import 'package:chicken_farm/components/vb_rfid_field.dart';
  9. import 'package:go_router/go_router.dart';
  10. class CageChangePage extends StatefulWidget {
  11. const CageChangePage({super.key});
  12. @override
  13. State<CageChangePage> createState() => _CageChangePageState();
  14. }
  15. class _CageChangePageState extends State<CageChangePage> {
  16. final List<String> _rfids = [];
  17. // 添加TextEditingControllers用于控制输入框
  18. final TextEditingController _sourceCageController = TextEditingController();
  19. final TextEditingController _targetCageController = TextEditingController();
  20. // 添加FocusNode用于控制焦点
  21. final FocusNode _sourceCageFocusNode = FocusNode();
  22. final FocusNode _targetCageFocusNode = FocusNode();
  23. // 扫描状态
  24. bool _isScanningSource = false;
  25. bool _isScanningTarget = false;
  26. int _scanTag = 1;
  27. @override
  28. void initState() {
  29. super.initState();
  30. _sourceCageController.clear();
  31. _targetCageController.clear();
  32. // 监听焦点变化以更新_scanTag值
  33. _sourceCageFocusNode.addListener(() {
  34. if (_sourceCageFocusNode.hasFocus) {
  35. _scanTag = 1;
  36. }
  37. });
  38. _targetCageFocusNode.addListener(() {
  39. if (_targetCageFocusNode.hasFocus) {
  40. _scanTag = 2;
  41. }
  42. });
  43. // 延迟到下一帧执行,确保页面构建完成后再打开扫描头
  44. WidgetsBinding.instance.addPostFrameCallback((_) {
  45. ScanChannel.openScanHead().then((success) {
  46. if (success) {
  47. logger.d("扫描头已打开");
  48. } else {
  49. ToastUtil.errorAlert("扫描头打开失败");
  50. if (mounted) {
  51. context.pop();
  52. }
  53. }
  54. });
  55. // 初始化监听
  56. ScanChannel.initScanListener(
  57. onScanResult: (result) {
  58. if (_scanTag == 1) {
  59. // 源笼号
  60. setState(() {
  61. _sourceCageController.text = result;
  62. _isScanningSource = false;
  63. });
  64. if (_targetCageController.text.isEmpty) {
  65. // 源笼号扫描成功后自动延时扫描目标笼号
  66. ScanChannel.stopSingleScan();
  67. logger.d("自动扫描目标笼号");
  68. _targetCageFocusNode.requestFocus();
  69. _dealyScanCode(2);
  70. }
  71. } else if (_scanTag == 2) {
  72. // 目标笼号
  73. setState(() {
  74. _targetCageController.text = result;
  75. _isScanningTarget = false;
  76. });
  77. }
  78. // 扫码成功后停止解码(保留扫描头)
  79. ScanChannel.stopSingleScan();
  80. },
  81. onScanError: (error) {
  82. logger.d("扫码错误:$error");
  83. setState(() {
  84. _isScanningSource = false;
  85. });
  86. },
  87. onKeyPress: (bool isDouble, String keyCode) {
  88. if (isDouble && keyCode == "KEY_619") {
  89. _sourceCageController.clear();
  90. _targetCageController.clear();
  91. _scanTag = 1;
  92. ScanChannel.startScan();
  93. } else {
  94. logger.d("按键:$isDouble $keyCode");
  95. }
  96. },
  97. );
  98. });
  99. _scanTag = 1;
  100. }
  101. @override
  102. void dispose() {
  103. // 释放TextEditingControllers
  104. _sourceCageController.dispose();
  105. _targetCageController.dispose();
  106. // 释放FocusNode
  107. _sourceCageFocusNode.dispose();
  108. _targetCageFocusNode.dispose();
  109. // 手动关闭扫描头,立即释放资源
  110. ScanChannel.closeScanHead();
  111. // 释放通道
  112. ScanChannel.dispose();
  113. super.dispose();
  114. }
  115. @override
  116. Widget build(BuildContext context) {
  117. return Scaffold(
  118. appBar: const VberAppBar(title: '换笼管理', showLeftButton: true),
  119. body: SingleChildScrollView(
  120. child: Padding(
  121. padding: const EdgeInsets.all(16.0),
  122. child: Column(
  123. crossAxisAlignment: CrossAxisAlignment.start,
  124. children: [
  125. // 源笼号区域
  126. _buildCageSection(1),
  127. const SizedBox(height: 15),
  128. // 目标笼号区域
  129. _buildCageSection(2),
  130. const SizedBox(height: 15),
  131. // 电子编号区域
  132. _buildRfidSection(),
  133. const SizedBox(height: 15),
  134. // 提交按钮
  135. SizedBox(
  136. width: double.infinity,
  137. child: ElevatedButton(
  138. onPressed:
  139. _sourceCageController.text.isNotEmpty &&
  140. _targetCageController.text.isNotEmpty &&
  141. _rfids.isNotEmpty
  142. ? _handleSubmit
  143. : null,
  144. style: ElevatedButton.styleFrom(
  145. backgroundColor:
  146. (_sourceCageController.text.isNotEmpty &&
  147. _targetCageController.text.isNotEmpty &&
  148. _rfids.isNotEmpty)
  149. ? Colors.blue
  150. : Colors.grey,
  151. foregroundColor: Colors.white,
  152. ),
  153. child: const Text('提交'),
  154. ),
  155. ),
  156. const SizedBox(height: 20),
  157. // 已识别的电子编号列表
  158. if (_rfids.isNotEmpty) ...[
  159. Row(
  160. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  161. children: [
  162. const Text(
  163. '已识别的电子编号',
  164. style: TextStyle(fontWeight: FontWeight.bold),
  165. ),
  166. IconButton(
  167. icon: const Icon(Icons.clear, size: 18),
  168. onPressed: _clearRfids,
  169. tooltip: '清空编号',
  170. ),
  171. ],
  172. ),
  173. const SizedBox(height: 10),
  174. Container(
  175. padding: const EdgeInsets.all(10),
  176. decoration: BoxDecoration(
  177. border: Border.all(color: Colors.grey),
  178. borderRadius: BorderRadius.circular(8),
  179. ),
  180. child: ListView.builder(
  181. shrinkWrap: true,
  182. physics: const NeverScrollableScrollPhysics(),
  183. padding: EdgeInsets.zero,
  184. itemCount: _rfids.length,
  185. itemBuilder: (context, index) {
  186. return ListTile(
  187. visualDensity: VisualDensity.compact,
  188. contentPadding: const EdgeInsets.symmetric(
  189. horizontal: 2,
  190. vertical: 0,
  191. ),
  192. title: Text(_rfids[index]),
  193. trailing: IconButton(
  194. icon: const Icon(
  195. Icons.delete,
  196. size: 18,
  197. color: Colors.red,
  198. ),
  199. onPressed: () => _removeRfid(index),
  200. ),
  201. );
  202. },
  203. ),
  204. ),
  205. ],
  206. const SizedBox(height: 20),
  207. ],
  208. ),
  209. ),
  210. ),
  211. );
  212. }
  213. Widget _buildCageSection(int tag) {
  214. String title = '源笼号';
  215. TextEditingController controller = _sourceCageController;
  216. FocusNode focusNode = _sourceCageFocusNode;
  217. bool isScanning = _isScanningSource;
  218. if (tag != 1) {
  219. tag = 2;
  220. title = '目标笼号';
  221. controller = _targetCageController;
  222. focusNode = _targetCageFocusNode;
  223. isScanning = _isScanningTarget;
  224. }
  225. // 确保光标在文本末尾
  226. if (controller.text.isNotEmpty) {
  227. controller.selection = TextSelection.fromPosition(
  228. TextPosition(offset: controller.text.length),
  229. );
  230. }
  231. return Column(
  232. crossAxisAlignment: CrossAxisAlignment.start,
  233. children: [
  234. Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
  235. const SizedBox(height: 10),
  236. Container(
  237. padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
  238. decoration: BoxDecoration(
  239. border: Border.all(color: Colors.grey),
  240. borderRadius: BorderRadius.circular(8),
  241. ),
  242. child: Row(
  243. children: [
  244. Expanded(
  245. // 将Text替换为TextField
  246. child: TextField(
  247. focusNode: focusNode,
  248. controller: controller,
  249. enabled: controller.text.isEmpty,
  250. decoration: InputDecoration(
  251. border: InputBorder.none,
  252. hintText: controller.text.isNotEmpty ? null : '未扫描',
  253. hintStyle: TextStyle(
  254. color: controller.text.isNotEmpty
  255. ? Colors.black
  256. : Colors.grey,
  257. fontSize: 16,
  258. ),
  259. ),
  260. style: TextStyle(
  261. color: controller.text.isNotEmpty
  262. ? Colors.black
  263. : Colors.grey,
  264. fontSize: 16,
  265. ),
  266. onChanged: (text) {
  267. setState(() {
  268. // 文本更改由controller自动处理
  269. });
  270. },
  271. ),
  272. ),
  273. if (controller.text.isNotEmpty) ...[
  274. IconButton(
  275. icon: const Icon(Icons.refresh, size: 20),
  276. onPressed: () {
  277. _handleChangeCageCode(tag);
  278. },
  279. ),
  280. ] else ...[
  281. IconButton(
  282. icon: isScanning
  283. ? const SizedBox(
  284. width: 20,
  285. height: 20,
  286. child: CircularProgressIndicator(strokeWidth: 2),
  287. )
  288. : const Icon(Icons.qr_code_scanner, size: 20),
  289. onPressed: () {
  290. _handleScanCageCode(tag);
  291. },
  292. ),
  293. ],
  294. ],
  295. ),
  296. ),
  297. ],
  298. );
  299. }
  300. Widget _buildRfidSection() {
  301. return VberRfidField(
  302. rfids: _rfids,
  303. onRfidScanned: (rfid) {
  304. // 检查是否已存在相同的RFID标签
  305. bool isDuplicate = _rfids.any((existingRfid) => existingRfid == rfid);
  306. if (isDuplicate) {
  307. ToastUtil.info("电子编号已存在");
  308. } else {
  309. // 如果不重复,添加到列表中
  310. setState(() {
  311. _rfids.insert(0, rfid);
  312. });
  313. ToastUtil.success("成功添加新的电子编号");
  314. }
  315. },
  316. multiple: true,
  317. multipleScan: false,
  318. label: '电子编号',
  319. multiplePlaceholder: '未识别',
  320. multipleFormat: '已识别 %d 枚电子编号',
  321. );
  322. }
  323. void _dealyScanCode(int scanTag, {int? dealy}) async {
  324. dealy ??= 800;
  325. _scanTag = scanTag;
  326. logger.d("开始扫码: $_scanTag");
  327. await Future.delayed(Duration(milliseconds: dealy));
  328. bool success = await ScanChannel.startScan();
  329. if (!success) {
  330. ToastUtil.error("扫码失败");
  331. }
  332. }
  333. void _handleScanCageCode(int tag) async {
  334. if (_isScanningTarget || _isScanningSource) return;
  335. setState(() {
  336. if (tag == 1) {
  337. _isScanningSource = true;
  338. } else if (tag == 2) {
  339. _isScanningTarget = true;
  340. }
  341. });
  342. _scanTag = tag;
  343. logger.d("开始扫码: $_scanTag");
  344. bool success = await ScanChannel.startScan();
  345. if (!success) {
  346. ToastUtil.error("扫码失败");
  347. }
  348. }
  349. void _handleChangeCageCode(int tag) {
  350. setState(() {
  351. if (tag == 1) {
  352. _sourceCageController.clear();
  353. } else {
  354. _targetCageController.clear();
  355. }
  356. });
  357. _dealyScanCode(tag);
  358. }
  359. // 移除指定索引的电子编号
  360. void _removeRfid(int index) {
  361. setState(() {
  362. _rfids.removeAt(index);
  363. });
  364. }
  365. // 清空所有已识别的电子编号
  366. void _clearRfids() {
  367. setState(() {
  368. _rfids.clear();
  369. });
  370. ToastUtil.info('已清空所有电子编号');
  371. }
  372. // 提交数据
  373. void _handleSubmit() {
  374. final data = {
  375. 'sourceCage': _sourceCageController.text,
  376. 'targetCage': _targetCageController.text,
  377. 'rfids': _rfids,
  378. 'date': DateTimeUtil.format(DateTime.now()),
  379. };
  380. apis.breeding.submitApi
  381. .cageChange(data)
  382. .then((_) {
  383. if (mounted) {
  384. ScaffoldMessenger.of(context).showSnackBar(
  385. const SnackBar(
  386. content: Text('换笼提交成功'),
  387. backgroundColor: Colors.green,
  388. ),
  389. );
  390. // 提交后重置表单
  391. setState(() {
  392. _sourceCageController.clear();
  393. _targetCageController.clear();
  394. _rfids.clear();
  395. });
  396. }
  397. })
  398. .catchError((err) {
  399. ToastUtil.error('换笼提交失败');
  400. if (mounted && err != null) {
  401. String errorMessage = err.toString();
  402. if (err is Exception) {
  403. errorMessage = err.toString();
  404. }
  405. ScaffoldMessenger.of(context).showSnackBar(
  406. SnackBar(
  407. content: Text(errorMessage),
  408. backgroundColor: Colors.red,
  409. ),
  410. );
  411. }
  412. });
  413. }
  414. }