cage_change_page.dart 14 KB

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