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>
1158 lines
36 KiB
Dart
1158 lines
36 KiB
Dart
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,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|