Files
pad_scanner/lib/pages/accessory_page.dart
Misaka 12cab16888 feat: add accessory registration page
Create accessory_page.dart with step-by-step form for registering
product accessories: paichan_no query, work order selection, zongpai_no
auto-fill, accessory type selector (preset + custom), quantity input,
location code (scanner + manual), and submit. Includes registered
accessories list with edit/delete actions and boxed-item indicators.
Integrates with ScannerService, FeedbackService, and StatusBar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-24 17:51:03 +08:00

1158 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AccessoryPage> createState() => _AccessoryPageState();
}
class _AccessoryPageState extends State<AccessoryPage> {
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<WorkOrderGroup> _workOrders = [];
String? _selectedWorkOrder;
String _zongpaiNo = '';
List<String> _availableZongpaiNos = [];
List<AccessoryTypeInfo> _accessoryTypes = [];
String _selectedAccessoryType = ''; // empty = not selected
bool _isCustomType = false;
int _quantity = 0;
String _locationCode = '';
// Registered items list
List<AccessoryRecord> _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<ScanResult>? _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<String?> _baseUrl() async {
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) return null;
return baseUrl;
}
Future<void> _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<void> _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<void> _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<void> _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<void> _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<void> _deleteRecord(AccessoryRecord record) async {
final confirmed = await showDialog<bool>(
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<String>(
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<String>(
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 = <DropdownMenuItem<String>>[
..._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<String>(
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 = <DropdownMenuItem<String>>[
..._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<String>(
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,
),
),
),
);
}
}