Compare commits

...

3 Commits

Author SHA1 Message Date
Misaka
99ed3bb9ec feat: add temp storage shelf (B prefix) UI support
Recognize B-prefix location codes as temp storage shelves. Add purple
color scheme, conflict detection for temp_stored status, and updated
submit/dialog logic for temp storage target handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-25 22:48:29 +08:00
Misaka_Company
f6bfd8cb10 style: add curly braces to if/else statements in settings_page
Fix curly_braces_in_flow_control_structures lint warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 09:58:42 +08:00
Misaka_Company
9db50af1e6 chore: document Flutter pre-commit checks 2026-05-22 09:56:03 +08:00
11 changed files with 167 additions and 28 deletions

View File

@@ -6,6 +6,28 @@
- All new feature development must be done on a `dev` branch, created from `master` - All new feature development must be done on a `dev` branch, created from `master`
- After verification, merge `dev` back into `master` - After verification, merge `dev` back into `master`
## Pre-Commit Checks
Before committing Flutter app changes, run these checks:
```bash
dart format --set-exit-if-changed .
flutter analyze
flutter test
```
If `flutter test` fails with a localhost WebSocket/proxy error, follow the proxy cleanup steps in the Flutter Test Rules section below and run it again.
### Handling Check Failures
- If a check fails because of the current change, fix the issue before committing.
- If `flutter analyze` reports any issue in files changed by the current task, fix it before committing.
- If `flutter analyze` reports only unrelated pre-existing issues, do not fix them in the current commit. Report the file, line, and lint/error name, then handle them in a separate cleanup commit or task.
- If a check fails because of unrelated pre-existing issues, do not include unrelated fixes in the same commit. Report the failing command and the existing issues, then handle them in a separate cleanup commit or task.
- If formatting fails, format only files changed by the current task. Do not run a broad formatting cleanup unless that is the explicit task.
- Treat `flutter test` failures as blocking unless the failure is clearly caused by the proxy issue described below and passes after rerunning with proxy variables cleared.
- When committing or reporting completion, mention which checks were run and whether any remaining failures are unrelated pre-existing issues.
## App Installation Rules ## App Installation Rules
**Always use `adb install -r` to install the app. Never use `flutter install`.** **Always use `adb install -r` to install the app. Never use `flutter install`.**

View File

@@ -97,6 +97,7 @@ extension _RegistrationDialogsPart on _RegistrationPageState {
void _showLocationConflictDialog({ void _showLocationConflictDialog({
required List<PaichaOverviewItem> onShelfItems, required List<PaichaOverviewItem> onShelfItems,
required List<PaichaOverviewItem> transferredItems, required List<PaichaOverviewItem> transferredItems,
List<PaichaOverviewItem> tempStoredItems = const [],
}) { }) {
final contentParts = <Widget>[]; final contentParts = <Widget>[];
@@ -142,6 +143,29 @@ extension _RegistrationDialogsPart on _RegistrationPageState {
} }
} }
if (tempStoredItems.isNotEmpty) {
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
contentParts.add(const Divider());
contentParts.add(const SizedBox(height: 4));
}
contentParts.add(
Text(
'暂存中',
style: TextStyle(
fontWeight: FontWeight.bold,
color: const Color(0xFF7B1FA2),
),
),
);
for (final item in tempStoredItems) {
contentParts.addAll([
Text('总排号:${item.zongpaiNo}'),
Text('暂存货位:${item.locationCode ?? "未知"}'),
const SizedBox(height: 6),
]);
}
}
contentParts.add(const SizedBox(height: 4)); contentParts.add(const SizedBox(height: 4));
contentParts.add( contentParts.add(
const Text( const Text(

View File

@@ -19,9 +19,9 @@ extension _RegistrationOverviewPart on _RegistrationPageState {
); );
} }
/// Whether any scanned item already has a location binding (on_shelf or transferred). /// Find all scanned items that are in temp storage.
bool _hasAnyLocatedInScanned() { List<PaichaOverviewItem> _findTempStoredItemsInScanned() {
return registration_calculations.hasAnyLocatedInScanned( return registration_calculations.findTempStoredItemsInScanned(
overview: _overview, overview: _overview,
zongpaiNos: _zongpaiNos, zongpaiNos: _zongpaiNos,
); );

View File

@@ -48,6 +48,7 @@ extension _RegistrationScanPart on _RegistrationPageState {
_handleZongpaiScan(parsed.value); _handleZongpaiScan(parsed.value);
case CodeType.locationNormal: case CodeType.locationNormal:
case CodeType.locationTransit: case CodeType.locationTransit:
case CodeType.locationTempStorage:
_handleLocationScan(parsed.value, parsed.type); _handleLocationScan(parsed.value, parsed.type);
case CodeType.invalid: case CodeType.invalid:
_feedbackService.trigger(FeedbackEvent.scanInvalid); _feedbackService.trigger(FeedbackEvent.scanInvalid);

View File

@@ -52,4 +52,7 @@ extension _RegistrationStatusPart on _RegistrationPageState {
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting; _zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode); bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
bool get _isTempStorageTarget =>
isTempStorageTarget(_locationType, _locationCode);
} }

View File

@@ -34,6 +34,23 @@ extension _RegistrationSubmitPart on _RegistrationPageState {
} }
} }
// For temp storage target: block if any scanned item is transferred or temp_stored
// (on_shelf items are allowed — temp storage acts as shelf change)
if (_isTempStorageTarget) {
final transferredItems = _findTransferredItemsInScanned();
final tempStoredItems = _findTempStoredItemsInScanned();
if (transferredItems.isNotEmpty || tempStoredItems.isNotEmpty) {
setState(() => _isSubmitting = false);
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showLocationConflictDialog(
onShelfItems: [],
transferredItems: transferredItems,
tempStoredItems: tempStoredItems,
);
return;
}
}
if (_mode == RegistrationMode.multiCode && _zongpaiNos.length > 1) { if (_mode == RegistrationMode.multiCode && _zongpaiNos.length > 1) {
if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) { if (_isTransitTarget && !await _ensureBatchSamePaicha(baseUrl)) {
if (!mounted) return; if (!mounted) return;
@@ -47,7 +64,7 @@ extension _RegistrationSubmitPart on _RegistrationPageState {
return; return;
} }
// Pre-check: block entire batch if any item already has a location binding // Pre-check: block entire batch if any item already has a location binding
if (!_isTransitTarget) { if (!_isTransitTarget && !_isTempStorageTarget) {
final onShelfItems = _findOnShelfItemsInScanned(); final onShelfItems = _findOnShelfItemsInScanned();
final transferredItems = _findTransferredItemsInScanned(); final transferredItems = _findTransferredItemsInScanned();
if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) { if (onShelfItems.isNotEmpty || transferredItems.isNotEmpty) {
@@ -117,13 +134,15 @@ extension _RegistrationSubmitPart on _RegistrationPageState {
} }
setState(() { setState(() {
_zongpaiNos.remove(zongpaiNo); _zongpaiNos.remove(zongpaiNo);
if (_mode == RegistrationMode.singleCode) { if (_mode == RegistrationMode.singleCode && !_isTempStorageTarget) {
_locationCode = null; _locationCode = null;
_locationType = null; _locationType = null;
} }
}); });
final msg = result.isOffShelfSuccess final msg = result.isOffShelfSuccess
? '下架成功' ? '下架成功'
: _isTempStorageTarget
? '变更货架成功'
: (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功'); : (_mode == RegistrationMode.multiCode ? '多码上架模式,请扫描下一张执行卡' : '上架成功');
_showStatusOverride( _showStatusOverride(
msg, msg,

View File

@@ -5,11 +5,13 @@ class RegistrationOverviewStats {
final int totalCount; final int totalCount;
final int shelvedCount; final int shelvedCount;
final int transferredCount; final int transferredCount;
final int tempStoredCount;
const RegistrationOverviewStats({ const RegistrationOverviewStats({
required this.totalCount, required this.totalCount,
required this.shelvedCount, required this.shelvedCount,
required this.transferredCount, required this.transferredCount,
required this.tempStoredCount,
}); });
} }
@@ -31,17 +33,21 @@ RegistrationOverviewStats overviewStats(PaichaOverviewResult? overview) {
totalCount: 0, totalCount: 0,
shelvedCount: 0, shelvedCount: 0,
transferredCount: 0, transferredCount: 0,
tempStoredCount: 0,
); );
} }
var shelvedCount = 0; var shelvedCount = 0;
var transferredCount = 0; var transferredCount = 0;
var tempStoredCount = 0;
for (final item in overview!.items) { for (final item in overview!.items) {
switch (item.status) { switch (item.status) {
case 'on_shelf': case 'on_shelf':
shelvedCount++; shelvedCount++;
case 'transferred': case 'transferred':
transferredCount++; transferredCount++;
case 'temp_stored':
tempStoredCount++;
} }
} }
@@ -49,6 +55,7 @@ RegistrationOverviewStats overviewStats(PaichaOverviewResult? overview) {
totalCount: overview.totalCount, totalCount: overview.totalCount,
shelvedCount: shelvedCount, shelvedCount: shelvedCount,
transferredCount: transferredCount, transferredCount: transferredCount,
tempStoredCount: tempStoredCount,
); );
} }
@@ -91,6 +98,17 @@ List<PaichaOverviewItem> findTransferredItemsInScanned({
); );
} }
List<PaichaOverviewItem> findTempStoredItemsInScanned({
required PaichaOverviewResult? overview,
required Iterable<String> zongpaiNos,
}) {
return _findScannedItemsByStatus(
overview: overview,
zongpaiNos: zongpaiNos,
status: 'temp_stored',
);
}
bool hasAnyLocatedInScanned({ bool hasAnyLocatedInScanned({
required PaichaOverviewResult? overview, required PaichaOverviewResult? overview,
required Iterable<String> zongpaiNos, required Iterable<String> zongpaiNos,
@@ -100,7 +118,9 @@ bool hasAnyLocatedInScanned({
return overview!.items.any( return overview!.items.any(
(item) => (item) =>
scannedSet.contains(item.zongpaiNo) && scannedSet.contains(item.zongpaiNo) &&
(item.status == 'on_shelf' || item.status == 'transferred'), (item.status == 'on_shelf' ||
item.status == 'transferred' ||
item.status == 'temp_stored'),
); );
} }
@@ -108,10 +128,13 @@ Color registrationBarColor({
required String status, required String status,
required bool isScanned, required bool isScanned,
required bool isTransitTarget, required bool isTransitTarget,
bool isTempStorageTarget = false,
}) { }) {
if (isScanned) { if (isScanned) {
final isConflict = isTransitTarget final isConflict = isTransitTarget
? status == 'transferred' ? status == 'transferred'
: isTempStorageTarget
? (status == 'transferred' || status == 'temp_stored')
: (status == 'on_shelf' || status == 'transferred'); : (status == 'on_shelf' || status == 'transferred');
return isConflict ? Colors.red : const Color(0xFF43A047); return isConflict ? Colors.red : const Color(0xFF43A047);
} }
@@ -119,6 +142,8 @@ Color registrationBarColor({
switch (status) { switch (status) {
case 'on_shelf': case 'on_shelf':
return const Color(0xFF2196F3); return const Color(0xFF2196F3);
case 'temp_stored':
return const Color(0xFF7B1FA2);
case 'transferred': case 'transferred':
return const Color(0xFFFF9800); return const Color(0xFFFF9800);
default: default:
@@ -130,10 +155,13 @@ Color registrationRowBgColor({
required String status, required String status,
required bool isScanned, required bool isScanned,
required bool isTransitTarget, required bool isTransitTarget,
bool isTempStorageTarget = false,
}) { }) {
if (isScanned) { if (isScanned) {
final isConflict = isTransitTarget final isConflict = isTransitTarget
? status == 'transferred' ? status == 'transferred'
: isTempStorageTarget
? (status == 'transferred' || status == 'temp_stored')
: (status == 'on_shelf' || status == 'transferred'); : (status == 'on_shelf' || status == 'transferred');
return isConflict ? Colors.red.shade50 : const Color(0xFFF1F8E9); return isConflict ? Colors.red.shade50 : const Color(0xFFF1F8E9);
} }
@@ -141,6 +169,8 @@ Color registrationRowBgColor({
switch (status) { switch (status) {
case 'on_shelf': case 'on_shelf':
return const Color(0xFFE3F2FD); return const Color(0xFFE3F2FD);
case 'temp_stored':
return const Color(0xFFF3E5F5);
case 'transferred': case 'transferred':
return const Color(0xFFFFF3E0); return const Color(0xFFFFF3E0);
default: default:

View File

@@ -5,11 +5,13 @@ part of '../../registration_page.dart';
extension _RegistrationWidgetsPart on _RegistrationPageState { extension _RegistrationWidgetsPart on _RegistrationPageState {
String _locationLabel(CodeType? type) { String _locationLabel(CodeType? type) {
if (type == CodeType.locationTransit) return '转运区域'; if (type == CodeType.locationTransit) return '转运区域';
if (type == CodeType.locationTempStorage) return '暂存货架';
return '普通货架'; return '普通货架';
} }
Color _locationLabelColor(CodeType? type) { Color _locationLabelColor(CodeType? type) {
if (type == CodeType.locationTransit) return Colors.orange; if (type == CodeType.locationTransit) return Colors.orange;
if (type == CodeType.locationTempStorage) return const Color(0xFF7B1FA2);
return Colors.blue; return Colors.blue;
} }
@@ -108,6 +110,12 @@ extension _RegistrationWidgetsPart on _RegistrationPageState {
const Color(0xFFE65100), const Color(0xFFE65100),
hasData, hasData,
), ),
_buildStatTag(
'暂存 ${stats.tempStoredCount}',
const Color(0xFFF3E5F5),
const Color(0xFF7B1FA2),
hasData,
),
], ],
), ),
], ],
@@ -293,11 +301,13 @@ extension _RegistrationWidgetsPart on _RegistrationPageState {
status: item.status, status: item.status,
isScanned: isScanned, isScanned: isScanned,
isTransitTarget: _isTransitTarget, isTransitTarget: _isTransitTarget,
isTempStorageTarget: _isTempStorageTarget,
); );
final bgColor = registration_calculations.registrationRowBgColor( final bgColor = registration_calculations.registrationRowBgColor(
status: item.status, status: item.status,
isScanned: isScanned, isScanned: isScanned,
isTransitTarget: _isTransitTarget, isTransitTarget: _isTransitTarget,
isTempStorageTarget: _isTempStorageTarget,
); );
return Container( return Container(

View File

@@ -67,16 +67,31 @@ class _SettingsPageState extends State<SettingsPage> {
final configService = AppConfigService(); final configService = AppConfigService();
final config = await configService.loadConfig(); final config = await configService.loadConfig();
config['api_url'] = url; config['api_url'] = url;
if (_successPath != null) config['sound_success'] = _successPath; if (_successPath != null) {
else config.remove('sound_success'); config['sound_success'] = _successPath;
if (_failurePath != null) config['sound_failure'] = _failurePath; } else {
else config.remove('sound_failure'); config.remove('sound_success');
if (_beepPath != null) config['sound_beep'] = _beepPath; }
else config.remove('sound_beep'); if (_failurePath != null) {
if (_errorPath != null) config['sound_error'] = _errorPath; config['sound_failure'] = _failurePath;
else config.remove('sound_error'); } else {
if (_alertPath != null) config['sound_alert'] = _alertPath; config.remove('sound_failure');
else config.remove('sound_alert'); }
if (_beepPath != null) {
config['sound_beep'] = _beepPath;
} else {
config.remove('sound_beep');
}
if (_errorPath != null) {
config['sound_error'] = _errorPath;
} else {
config.remove('sound_error');
}
if (_alertPath != null) {
config['sound_alert'] = _alertPath;
} else {
config.remove('sound_alert');
}
await configService.saveConfig(config); await configService.saveConfig(config);
setState(() => _saving = false); setState(() => _saving = false);
if (mounted) { if (mounted) {

View File

@@ -1,14 +1,8 @@
// lib/services/code_parser.dart // lib/services/code_parser.dart
class CodeParser { class CodeParser {
static final _zongpaiRegex = RegExp( static final _zongpaiRegex = RegExp(r'^\d{2}(B|C|T)\d+$|^\d{2}(BW|CW)\d{4}$');
r'^\d{2}(B|C|T)\d+$|^\d{2}(BW|CW)\d{4}$', static final _locationRegex = RegExp(r'^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$');
); static final _transitRegex = RegExp(r'^TRANS-');
static final _locationRegex = RegExp(
r'^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$',
);
static final _transitRegex = RegExp(
r'^TRANS-',
);
static ParseResult parse(String code) { static ParseResult parse(String code) {
final trimmed = code.trim(); final trimmed = code.trim();
@@ -23,16 +17,30 @@ class CodeParser {
return ParseResult(type: CodeType.zongpaiNo, value: normalized); return ParseResult(type: CodeType.zongpaiNo, value: normalized);
} }
if (_locationRegex.hasMatch(normalized)) { if (_locationRegex.hasMatch(normalized)) {
if (normalized.startsWith('B')) {
return ParseResult(
type: CodeType.locationTempStorage,
value: normalized,
);
}
return ParseResult(type: CodeType.locationNormal, value: normalized); return ParseResult(type: CodeType.locationNormal, value: normalized);
} }
return ParseResult(type: CodeType.invalid, value: code); return ParseResult(type: CodeType.invalid, value: code);
} }
static bool isLocation(CodeType type) => static bool isLocation(CodeType type) =>
type == CodeType.locationNormal || type == CodeType.locationTransit; type == CodeType.locationNormal ||
type == CodeType.locationTransit ||
type == CodeType.locationTempStorage;
} }
enum CodeType { zongpaiNo, locationNormal, locationTransit, invalid } enum CodeType {
zongpaiNo,
locationNormal,
locationTransit,
locationTempStorage,
invalid,
}
class ParseResult { class ParseResult {
final CodeType type; final CodeType type;

View File

@@ -8,13 +8,20 @@ bool isTransitTarget(CodeType? locationType, String? locationCode) {
(locationCode?.startsWith('TRANS-') ?? false); (locationCode?.startsWith('TRANS-') ?? false);
} }
bool isTempStorageTarget(CodeType? locationType, String? locationCode) {
return locationType == CodeType.locationTempStorage ||
(locationCode?.startsWith('B') ?? false);
}
String registrationSubmitLabel({ String registrationSubmitLabel({
required bool isTransitTarget, required bool isTransitTarget,
required bool isMultiCode, required bool isMultiCode,
required int zongpaiCount, required int zongpaiCount,
}) { }) {
if (!isTransitTarget) { if (!isTransitTarget) {
return isMultiCode && zongpaiCount > 1 ? '批量上架($zongpaiCount 条)' : '确 认 上 架'; return isMultiCode && zongpaiCount > 1
? '批量上架($zongpaiCount 条)'
: '确 认 上 架';
} }
return isMultiCode && zongpaiCount > 1 ? '批量转运并凑箱($zongpaiCount 条)' : '转运并装箱'; return isMultiCode && zongpaiCount > 1 ? '批量转运并凑箱($zongpaiCount 条)' : '转运并装箱';
} }