diff --git a/lib/pages/accessory_page.dart b/lib/pages/accessory_page.dart new file mode 100644 index 0000000..a54f34a --- /dev/null +++ b/lib/pages/accessory_page.dart @@ -0,0 +1,1157 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pad_scanner/services/app_config_service.dart'; +import 'package:pad_scanner/services/scanner_service.dart'; +import 'package:pad_scanner/services/code_parser.dart'; +import 'package:pad_scanner/services/api_service.dart'; +import 'package:pad_scanner/services/feedback_service.dart'; +import 'package:pad_scanner/widgets/status_bar.dart'; + +/// Custom option sentinel for the accessory type dropdown. +const _kCustomTypeOption = '__custom__'; + +class AccessoryPage extends StatefulWidget { + const AccessoryPage({super.key}); + + @override + State createState() => _AccessoryPageState(); +} + +class _AccessoryPageState extends State { + final _scannerService = ScannerService(); + final _apiService = ApiService(); + final _feedbackService = FeedbackService(); + final _focusNode = FocusNode(); + + // Form state + final _paichanNoController = TextEditingController(); + final _quantityController = TextEditingController(); + final _locationCodeController = TextEditingController(); + final _customTypeController = TextEditingController(); + + bool _paichanQueried = false; + bool _isQuerying = false; + + List _workOrders = []; + String? _selectedWorkOrder; + + String _zongpaiNo = ''; + List _availableZongpaiNos = []; + + List _accessoryTypes = []; + String _selectedAccessoryType = ''; // empty = not selected + bool _isCustomType = false; + + int _quantity = 0; + String _locationCode = ''; + + // Registered items list + List _registeredItems = []; + bool _isSubmitting = false; + + // Edit state + int? _editingId; + final _editQuantityController = TextEditingController(); + final _editLocationController = TextEditingController(); + String _editAccessoryType = ''; + bool _editIsCustomType = false; + final _editCustomTypeController = TextEditingController(); + + // Status bar state + StatusDotColor _statusDot = StatusDotColor.blue; + String _statusText = '等待输入排产号…'; + String? _statusOverrideText; + StatusDotColor? _statusOverrideDot; + + StreamSubscription? _scanSubscription; + + @override + void initState() { + super.initState(); + _startScanListening(); + _loadAccessoryTypes(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _scanSubscription?.cancel(); + _focusNode.dispose(); + _paichanNoController.dispose(); + _quantityController.dispose(); + _locationCodeController.dispose(); + _customTypeController.dispose(); + _editQuantityController.dispose(); + _editLocationController.dispose(); + _editCustomTypeController.dispose(); + _feedbackService.dispose(); + super.dispose(); + } + + // ─── Scanner ──────────────────────────────────────────────────────── + + void _startScanListening() { + _scanSubscription ??= _scannerService.scanResults.listen(_onScan); + } + + void _onScan(ScanResult result) { + final parsed = CodeParser.parse(result.barcode); + switch (parsed.type) { + case CodeType.locationNormal: + case CodeType.locationTransit: + _feedbackService.trigger(FeedbackEvent.scanValid); + setState(() { + _locationCode = parsed.value; + _locationCodeController.text = parsed.value; + _clearStatusOverride(); + }); + case CodeType.zongpaiNo: + case CodeType.invalid: + _feedbackService.trigger(FeedbackEvent.scanInvalid); + _showStatusOverride( + '无效码:${result.barcode}', + StatusDotColor.red, + const Duration(seconds: 2), + ); + } + } + + // ─── Status bar helpers ───────────────────────────────────────────── + + void _clearStatusOverride() { + _statusOverrideText = null; + _statusOverrideDot = null; + } + + void _showStatusOverride(String text, StatusDotColor dot, Duration duration) { + setState(() { + _statusOverrideText = text; + _statusOverrideDot = dot; + }); + Future.delayed(duration, () { + if (mounted) setState(() => _clearStatusOverride()); + }); + } + + void _updateBaseStatus() { + if (_isSubmitting) { + _statusDot = StatusDotColor.orange; + _statusText = '正在提交…'; + return; + } + if (!_paichanQueried) { + _statusDot = StatusDotColor.blue; + _statusText = '等待输入排产号…'; + return; + } + if (_selectedWorkOrder == null) { + _statusDot = StatusDotColor.blue; + _statusText = '请选择工令号'; + return; + } + if (_zongpaiNo.isEmpty) { + _statusDot = StatusDotColor.blue; + _statusText = '请选择总排号'; + return; + } + if (_selectedAccessoryType.isEmpty) { + _statusDot = StatusDotColor.blue; + _statusText = '请选择附件类型'; + return; + } + if (_quantity <= 0) { + _statusDot = StatusDotColor.blue; + _statusText = '请输入数量'; + return; + } + if (_locationCode.isEmpty) { + _statusDot = StatusDotColor.blue; + _statusText = '请扫描或输入货位号'; + return; + } + _statusDot = StatusDotColor.blue; + _statusText = '请确认并提交'; + } + + // ─── API helpers ──────────────────────────────────────────────────── + + Future _baseUrl() async { + final configService = AppConfigService(); + final baseUrl = await configService.getString('api_url') ?? ''; + if (baseUrl.isEmpty) return null; + return baseUrl; + } + + Future _loadAccessoryTypes() async { + final baseUrl = await _baseUrl(); + if (baseUrl == null) return; + final result = await _apiService.listAccessoryTypes(baseUrl: baseUrl); + if (result.success && mounted) { + setState(() { + _accessoryTypes = result.types; + }); + } + } + + // ─── Query paichan_no ─────────────────────────────────────────────── + + Future _queryPaichanNo() async { + final paichanNo = _paichanNoController.text.trim().toUpperCase(); + if (paichanNo.isEmpty) return; + + setState(() => _isQuerying = true); + + final baseUrl = await _baseUrl(); + if (!mounted) return; + if (baseUrl == null) { + setState(() => _isQuerying = false); + _feedbackService.trigger(FeedbackEvent.submitFailure); + _showStatusOverride( + '未配置 API 地址,请前往设置', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + // Query work orders + final woResult = await _apiService.queryWorkOrders( + baseUrl: baseUrl, + paichanNo: paichanNo, + ); + + if (!mounted) return; + + if (!woResult.success) { + setState(() => _isQuerying = false); + final isNetwork = woResult.errorMessage == '网络异常,请检查网络连接'; + _feedbackService.trigger( + isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure, + ); + _showStatusOverride( + woResult.errorMessage ?? '查询失败', + isNetwork ? StatusDotColor.yellow : StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + // Query registered accessories list + final listResult = await _apiService.listAccessories( + baseUrl: baseUrl, + paichanNo: paichanNo, + ); + + if (!mounted) return; + + setState(() { + _isQuerying = false; + _paichanQueried = true; + _workOrders = woResult.workOrders; + _selectedWorkOrder = null; + _zongpaiNo = ''; + _availableZongpaiNos = []; + _selectedAccessoryType = ''; + _isCustomType = false; + _customTypeController.clear(); + _quantity = 0; + _quantityController.clear(); + _locationCode = ''; + _locationCodeController.clear(); + _editingId = null; + + if (listResult.success) { + _registeredItems = listResult.items; + } else { + _registeredItems = []; + } + + // Auto-select if only one work order + if (_workOrders.length == 1) { + _selectedWorkOrder = _workOrders.first.workOrderNo; + _onWorkOrderSelected(_selectedWorkOrder); + } + }); + + _feedbackService.trigger(FeedbackEvent.scanValid); + } + + void _onWorkOrderSelected(String? workOrderNo) { + if (workOrderNo == null) return; + final group = _workOrders.where((g) => g.workOrderNo == workOrderNo).first; + setState(() { + _availableZongpaiNos = group.zongpaiNos; + if (group.zongpaiNos.length == 1) { + _zongpaiNo = group.zongpaiNos.first; + } else { + _zongpaiNo = ''; + } + }); + } + + // ─── Submit ───────────────────────────────────────────────────────── + + bool get _canSubmit => + _paichanQueried && + _selectedWorkOrder != null && + _zongpaiNo.isNotEmpty && + _selectedAccessoryType.isNotEmpty && + _quantity > 0 && + !_isSubmitting; + + Future _submit() async { + if (!_canSubmit) return; + + final baseUrl = await _baseUrl(); + if (baseUrl == null) { + _feedbackService.trigger(FeedbackEvent.submitFailure); + _showStatusOverride( + '未配置 API 地址,请前往设置', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + final paichanNo = _paichanNoController.text.trim().toUpperCase(); + final accessoryType = _isCustomType + ? _customTypeController.text.trim() + : _selectedAccessoryType; + + if (accessoryType.isEmpty) { + _feedbackService.trigger(FeedbackEvent.scanInvalid); + _showStatusOverride( + '请输入附件类型名称', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + setState(() => _isSubmitting = true); + + final result = await _apiService.createAccessory( + baseUrl: baseUrl, + paichanNo: paichanNo, + zongpaiNo: _zongpaiNo, + accessoryType: accessoryType, + quantity: _quantity, + locationCode: _locationCode.isNotEmpty ? _locationCode : null, + ); + + if (!mounted) return; + + setState(() => _isSubmitting = false); + + if (result.success) { + _feedbackService.trigger(FeedbackEvent.submitSuccess); + _showStatusOverride( + '登记成功', + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); + // Reset form partially (keep paichan_no and work order selection) + setState(() { + _selectedAccessoryType = ''; + _isCustomType = false; + _customTypeController.clear(); + _quantity = 0; + _quantityController.clear(); + _locationCode = ''; + _locationCodeController.clear(); + }); + // Refresh registered list + _refreshList(); + } else { + final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; + _feedbackService.trigger( + isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure, + ); + _showStatusOverride( + result.errorMessage ?? '提交失败', + isNetwork ? StatusDotColor.yellow : StatusDotColor.red, + const Duration(seconds: 2), + ); + } + } + + Future _refreshList() async { + final baseUrl = await _baseUrl(); + if (baseUrl == null) return; + final paichanNo = _paichanNoController.text.trim().toUpperCase(); + if (paichanNo.isEmpty) return; + final result = await _apiService.listAccessories( + baseUrl: baseUrl, + paichanNo: paichanNo, + ); + if (result.success && mounted) { + setState(() { + _registeredItems = result.items; + }); + } + } + + // ─── Edit / Delete ────────────────────────────────────────────────── + + void _startEdit(AccessoryRecord record) { + setState(() { + _editingId = record.id; + _editQuantityController.text = record.quantity.toString(); + _editLocationController.text = record.locationCode ?? ''; + _editAccessoryType = record.accessoryType; + // Check if the type matches a preset + final isPreset = _accessoryTypes.any( + (t) => t.name == record.accessoryType, + ); + _editIsCustomType = !isPreset; + _editCustomTypeController.text = isPreset ? '' : record.accessoryType; + }); + } + + Future _saveEdit(AccessoryRecord record) async { + final baseUrl = await _baseUrl(); + if (baseUrl == null) { + _showStatusOverride( + '未配置 API 地址', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + final newQuantity = int.tryParse(_editQuantityController.text.trim()) ?? 0; + if (newQuantity <= 0) { + _showStatusOverride( + '数量必须为正整数', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + final newType = _editIsCustomType + ? _editCustomTypeController.text.trim() + : _editAccessoryType; + if (newType.isEmpty) { + _showStatusOverride( + '附件类型不能为空', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + final newLocation = _editLocationController.text.trim(); + + setState(() => _isSubmitting = true); + final result = await _apiService.updateAccessory( + baseUrl: baseUrl, + id: record.id, + accessoryType: newType != record.accessoryType ? newType : null, + quantity: newQuantity != record.quantity ? newQuantity : null, + locationCode: newLocation.isNotEmpty ? newLocation : null, + ); + if (!mounted) return; + setState(() { + _isSubmitting = false; + _editingId = null; + }); + + if (result.success) { + _feedbackService.trigger(FeedbackEvent.submitSuccess); + _showStatusOverride( + '修改成功', + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); + _refreshList(); + } else { + final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; + _feedbackService.trigger( + isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure, + ); + _showStatusOverride( + result.errorMessage ?? '修改失败', + isNetwork ? StatusDotColor.yellow : StatusDotColor.red, + const Duration(seconds: 2), + ); + } + } + + Future _deleteRecord(AccessoryRecord record) async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('确认删除'), + content: Text( + '确定删除该附件记录(${record.accessoryType} x${record.quantity})?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('删除', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + if (confirmed != true) return; + + final baseUrl = await _baseUrl(); + if (baseUrl == null) { + _showStatusOverride( + '未配置 API 地址', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } + + setState(() => _isSubmitting = true); + final result = await _apiService.deleteAccessory( + baseUrl: baseUrl, + id: record.id, + ); + if (!mounted) return; + setState(() => _isSubmitting = false); + + if (result.success) { + _feedbackService.trigger(FeedbackEvent.submitSuccess); + _showStatusOverride( + '删除成功', + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); + _refreshList(); + } else { + final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; + _feedbackService.trigger( + isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure, + ); + _showStatusOverride( + result.errorMessage ?? '删除失败', + isNetwork ? StatusDotColor.yellow : StatusDotColor.red, + const Duration(seconds: 2), + ); + } + } + + // ─── Build ────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + _updateBaseStatus(); + final effectiveDot = _statusOverrideDot ?? _statusDot; + final effectiveText = _statusOverrideText ?? _statusText; + + return KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _onKeyEvent, + child: Scaffold( + appBar: AppBar(title: const Text('附件登记'), titleSpacing: 12), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildPaichanInput(), + if (_paichanQueried) ...[ + const SizedBox(height: 8), + _buildWorkOrderDropdown(), + ], + if (_selectedWorkOrder != null) ...[ + const SizedBox(height: 8), + _buildZongpaiSelector(), + ], + if (_zongpaiNo.isNotEmpty) ...[ + const SizedBox(height: 8), + _buildAccessoryTypeSelector(), + ], + if (_selectedAccessoryType.isNotEmpty) ...[ + const SizedBox(height: 8), + _buildQuantityInput(), + ], + if (_quantity > 0) ...[ + const SizedBox(height: 8), + _buildLocationCodeInput(), + ], + if (_canSubmit) ...[ + const SizedBox(height: 12), + _buildSubmitButton(), + ], + if (_registeredItems.isNotEmpty) ...[ + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 8), + _buildRegisteredSectionTitle(), + const SizedBox(height: 6), + _buildRegisteredList(), + ], + ], + ), + ), + ), + StatusBar(dotColor: effectiveDot, text: effectiveText), + ], + ), + ), + ); + } + + void _onKeyEvent(KeyEvent event) { + if (event is! KeyDownEvent) return; + if (event.logicalKey == LogicalKeyboardKey.enter) { + if (!_paichanQueried && _paichanNoController.text.trim().isNotEmpty) { + _queryPaichanNo(); + } else if (_canSubmit) { + _submit(); + } + } + } + + // ─── Form widgets ─────────────────────────────────────────────────── + + Widget _buildPaichanInput() { + return Row( + children: [ + Expanded( + child: TextField( + controller: _paichanNoController, + enabled: !_isQuerying && !_paichanQueried, + decoration: const InputDecoration( + labelText: '排产号', + hintText: '如 W00009', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + ), + style: const TextStyle(fontSize: 14), + onSubmitted: (_) => _queryPaichanNo(), + ), + ), + const SizedBox(width: 8), + SizedBox( + height: 40, + child: FilledButton( + onPressed: (!_paichanQueried && !_isQuerying) + ? _queryPaichanNo + : null, + child: _isQuerying + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('查询'), + ), + ), + if (_paichanQueried) ...[ + const SizedBox(width: 6), + SizedBox( + height: 40, + child: OutlinedButton( + onPressed: _resetPaichan, + child: const Text('重置'), + ), + ), + ], + ], + ); + } + + void _resetPaichan() { + setState(() { + _paichanQueried = false; + _paichanNoController.clear(); + _workOrders = []; + _selectedWorkOrder = null; + _zongpaiNo = ''; + _availableZongpaiNos = []; + _selectedAccessoryType = ''; + _isCustomType = false; + _customTypeController.clear(); + _quantity = 0; + _quantityController.clear(); + _locationCode = ''; + _locationCodeController.clear(); + _registeredItems = []; + _editingId = null; + }); + } + + Widget _buildWorkOrderDropdown() { + return InputDecorator( + decoration: const InputDecoration( + labelText: '工令号', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedWorkOrder, + hint: const Text('请选择工令号', style: TextStyle(fontSize: 14)), + isDense: true, + style: const TextStyle(fontSize: 14, color: Colors.black87), + items: _workOrders.map((g) { + return DropdownMenuItem( + value: g.workOrderNo, + child: Text(g.workOrderNo), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedWorkOrder = value; + _selectedAccessoryType = ''; + _isCustomType = false; + _customTypeController.clear(); + _quantity = 0; + _quantityController.clear(); + _locationCode = ''; + _locationCodeController.clear(); + }); + _onWorkOrderSelected(value); + }, + ), + ), + ); + } + + Widget _buildZongpaiSelector() { + if (_availableZongpaiNos.length == 1) { + return InputDecorator( + decoration: const InputDecoration( + labelText: '总排号', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + child: Text(_zongpaiNo, style: const TextStyle(fontSize: 14)), + ); + } + return InputDecorator( + decoration: const InputDecoration( + labelText: '总排号', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _zongpaiNo.isEmpty ? null : _zongpaiNo, + hint: const Text('请选择总排号', style: TextStyle(fontSize: 14)), + isDense: true, + style: const TextStyle(fontSize: 14, color: Colors.black87), + items: _availableZongpaiNos.map((z) { + return DropdownMenuItem(value: z, child: Text(z)); + }).toList(), + onChanged: (value) { + setState(() { + _zongpaiNo = value ?? ''; + }); + }, + ), + ), + ); + } + + Widget _buildAccessoryTypeSelector() { + final dropdownItems = >[ + ..._accessoryTypes.map( + (t) => DropdownMenuItem(value: t.name, child: Text(t.name)), + ), + const DropdownMenuItem( + value: _kCustomTypeOption, + child: Text('自定义…', style: TextStyle(color: Colors.grey)), + ), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + InputDecorator( + decoration: const InputDecoration( + labelText: '附件类型', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedAccessoryType.isEmpty + ? null + : _selectedAccessoryType, + hint: const Text('请选择附件类型', style: TextStyle(fontSize: 14)), + isDense: true, + style: const TextStyle(fontSize: 14, color: Colors.black87), + items: dropdownItems, + onChanged: (value) { + if (value == null) return; + setState(() { + if (value == _kCustomTypeOption) { + _isCustomType = true; + _selectedAccessoryType = _kCustomTypeOption; + _customTypeController.clear(); + } else { + _isCustomType = false; + _selectedAccessoryType = value; + } + }); + }, + ), + ), + ), + if (_isCustomType) ...[ + const SizedBox(height: 6), + TextField( + controller: _customTypeController, + decoration: const InputDecoration( + labelText: '自定义附件类型', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + ), + style: const TextStyle(fontSize: 14), + onChanged: (value) { + // Mark as "complete" when non-empty + setState(() { + _selectedAccessoryType = value.trim().isNotEmpty + ? _kCustomTypeOption + : ''; + }); + }, + ), + ], + ], + ); + } + + Widget _buildQuantityInput() { + return TextField( + controller: _quantityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '数量', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + style: const TextStyle(fontSize: 14), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (value) { + setState(() { + _quantity = int.tryParse(value) ?? 0; + }); + }, + ); + } + + Widget _buildLocationCodeInput() { + return TextField( + controller: _locationCodeController, + decoration: const InputDecoration( + labelText: '货位号', + hintText: '扫描或手动输入', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + style: const TextStyle(fontSize: 14), + onChanged: (value) { + setState(() { + _locationCode = value.trim(); + }); + }, + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + height: 42, + child: FilledButton( + onPressed: _isSubmitting ? null : _submit, + child: _isSubmitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('确认登记', style: TextStyle(fontSize: 16)), + ), + ); + } + + // ─── Registered list widgets ──────────────────────────────────────── + + Widget _buildRegisteredSectionTitle() { + return Text( + '已登记列表(${_registeredItems.length})', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ); + } + + Widget _buildRegisteredList() { + return Column( + children: _registeredItems + .map((item) => _buildRegisteredItem(item)) + .toList(), + ); + } + + Widget _buildRegisteredItem(AccessoryRecord record) { + final isEditing = _editingId == record.id; + final isBoxed = record.isBoxed; + + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isBoxed ? Colors.grey.shade100 : Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: isEditing ? _buildEditRow(record) : _buildDisplayRow(record), + ); + } + + Widget _buildDisplayRow(AccessoryRecord record) { + final isBoxed = record.isBoxed; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + record.zongpaiNo, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + Text(record.accessoryType, style: const TextStyle(fontSize: 12)), + const SizedBox(width: 12), + Text( + 'x${record.quantity}', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), + const SizedBox(width: 8), + if (record.locationCode != null) + Text( + record.locationCode!, + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + ), + if (isBoxed) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(3), + border: Border.all(color: Colors.green.shade200), + ), + child: const Text( + '已装箱', + style: TextStyle(fontSize: 10, color: Colors.green), + ), + ), + ] else ...[ + const SizedBox(width: 6), + _actionButton('改', Colors.blue, () => _startEdit(record)), + const SizedBox(width: 4), + _actionButton('删', Colors.red, () => _deleteRecord(record)), + ], + ], + ), + ], + ); + } + + Widget _buildEditRow(AccessoryRecord record) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + record.zongpaiNo, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), + const Spacer(), + Text( + record.accessoryType, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 6), + // Edit accessory type dropdown + _buildEditTypeDropdown(), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: TextField( + controller: _editQuantityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '数量', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + ), + style: const TextStyle(fontSize: 13), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), + ), + const SizedBox(width: 6), + Expanded( + child: TextField( + controller: _editLocationController, + decoration: const InputDecoration( + labelText: '货位号', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + ), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _actionButton('保存', Colors.green, () => _saveEdit(record)), + const SizedBox(width: 6), + _actionButton('取消', Colors.grey, () { + setState(() => _editingId = null); + }), + ], + ), + ], + ); + } + + Widget _buildEditTypeDropdown() { + final dropdownItems = >[ + ..._accessoryTypes.map( + (t) => DropdownMenuItem( + value: t.name, + child: Text(t.name, style: const TextStyle(fontSize: 13)), + ), + ), + const DropdownMenuItem( + value: _kCustomTypeOption, + child: Text('自定义…', style: TextStyle(fontSize: 13, color: Colors.grey)), + ), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + InputDecorator( + decoration: const InputDecoration( + labelText: '附件类型', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _editIsCustomType + ? _kCustomTypeOption + : (_editAccessoryType.isEmpty ? null : _editAccessoryType), + hint: const Text('选择类型', style: TextStyle(fontSize: 13)), + isDense: true, + style: const TextStyle(fontSize: 13, color: Colors.black87), + items: dropdownItems, + onChanged: (value) { + if (value == null) return; + setState(() { + if (value == _kCustomTypeOption) { + _editIsCustomType = true; + _editCustomTypeController.clear(); + } else { + _editIsCustomType = false; + _editAccessoryType = value; + } + }); + }, + ), + ), + ), + if (_editIsCustomType) ...[ + const SizedBox(width: 4), + TextField( + controller: _editCustomTypeController, + decoration: const InputDecoration( + labelText: '自定义类型', + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + ), + style: const TextStyle(fontSize: 13), + onChanged: (value) { + setState(() { + _editAccessoryType = value.trim(); + }); + }, + ), + ], + ], + ); + } + + Widget _actionButton(String label, Color color, VoidCallback onPressed) { + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + border: Border.all(color: color), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +}