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, ), ), ), ); } }