merge: integrate boxing module from remote with sound/vibration feedback
Merge remote boxing module (one2one, one2many, many2one modes) with local sound and vibration feedback feature. Migrate SharedPreferences to AppConfigService across all services including SoundService. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,120 @@ class RegistrationResult {
|
||||
RegistrationResult(success: false, errorMessage: message);
|
||||
}
|
||||
|
||||
// === 装箱模块数据类 ===
|
||||
|
||||
/// 箱号内单个总排号明细
|
||||
class BoxItemData {
|
||||
final String zongpaiNo;
|
||||
final int quantity;
|
||||
|
||||
BoxItemData({required this.zongpaiNo, required this.quantity});
|
||||
|
||||
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
||||
return BoxItemData(
|
||||
zongpaiNo: json['zongpai_no'] as String,
|
||||
quantity: json['quantity'] as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 箱号明细
|
||||
class BoxDetailData {
|
||||
final int boxNo;
|
||||
final List<BoxItemData> items;
|
||||
|
||||
BoxDetailData({required this.boxNo, required this.items});
|
||||
|
||||
factory BoxDetailData.fromJson(Map<String, dynamic> json) {
|
||||
final items = (json['items'] as List<dynamic>?)
|
||||
?.map((i) => BoxItemData.fromJson(i as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
return BoxDetailData(boxNo: json['box_no'] as int, items: items);
|
||||
}
|
||||
}
|
||||
|
||||
/// 装箱信息查询结果
|
||||
class BoxInfoResult {
|
||||
final bool success;
|
||||
final String? errorMessage;
|
||||
final String? zongpaiNo;
|
||||
final String? paichanNo;
|
||||
final int? quantity;
|
||||
final List<BoxDetailData> existingBoxes;
|
||||
final int maxBoxNo;
|
||||
final int suggestedBoxNo;
|
||||
|
||||
BoxInfoResult({
|
||||
required this.success,
|
||||
this.errorMessage,
|
||||
this.zongpaiNo,
|
||||
this.paichanNo,
|
||||
this.quantity,
|
||||
this.existingBoxes = const [],
|
||||
this.maxBoxNo = 0,
|
||||
this.suggestedBoxNo = 1,
|
||||
});
|
||||
|
||||
factory BoxInfoResult.ok(Map<String, dynamic> json) {
|
||||
final boxes = (json['existing_boxes'] as List<dynamic>?)
|
||||
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
return BoxInfoResult(
|
||||
success: true,
|
||||
zongpaiNo: json['zongpai_no'] as String?,
|
||||
paichanNo: json['paichan_no'] as String?,
|
||||
quantity: json['quantity'] as int?,
|
||||
existingBoxes: boxes,
|
||||
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
||||
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
factory BoxInfoResult.error(String message) {
|
||||
return BoxInfoResult(success: false, errorMessage: message);
|
||||
}
|
||||
}
|
||||
|
||||
/// 装箱保存结果
|
||||
class BoxSaveResult {
|
||||
final bool success;
|
||||
final bool isDuplicate;
|
||||
final String? errorMessage;
|
||||
final String? paichanNo;
|
||||
final int? boxNo;
|
||||
|
||||
BoxSaveResult({
|
||||
required this.success,
|
||||
this.isDuplicate = false,
|
||||
this.errorMessage,
|
||||
this.paichanNo,
|
||||
this.boxNo,
|
||||
});
|
||||
|
||||
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
||||
return BoxSaveResult(
|
||||
success: true,
|
||||
paichanNo: json['paichan_no'] as String?,
|
||||
boxNo: json['box_no'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
factory BoxSaveResult.duplicate(Map<String, dynamic> json) {
|
||||
return BoxSaveResult(
|
||||
success: false,
|
||||
isDuplicate: true,
|
||||
paichanNo: json['paichan_no'] as String?,
|
||||
boxNo: json['box_no'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
factory BoxSaveResult.error(String message) {
|
||||
return BoxSaveResult(success: false, errorMessage: message);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
final http.Client _client;
|
||||
final Duration timeout;
|
||||
@@ -54,12 +168,16 @@ class ApiService {
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
return RegistrationResult.ok();
|
||||
case 400:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final msg = body['message']?.toString() ?? '请求参数错误';
|
||||
return RegistrationResult.error(msg);
|
||||
case 409:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return RegistrationResult.duplicate(body);
|
||||
default:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final msg = body['error'] ?? body['message'] ?? 'Unknown error (${response.statusCode})';
|
||||
final msg = body['message'] ?? body['error'] ?? 'Unknown error (${response.statusCode})';
|
||||
return RegistrationResult.error(msg.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -67,6 +185,77 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询装箱信息 — GET /CargoTrace/box/info
|
||||
Future<BoxInfoResult> fetchBoxInfo({
|
||||
required String baseUrl,
|
||||
required String zongpaiNo,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/CargoTrace/box/info').replace(
|
||||
queryParameters: {'zongpai_no': zongpaiNo},
|
||||
);
|
||||
try {
|
||||
final response = await _client
|
||||
.get(uri, headers: {'Content-Type': 'application/json'})
|
||||
.timeout(timeout);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return BoxInfoResult.ok(body);
|
||||
case 400:
|
||||
return BoxInfoResult.error('无效的总排号格式');
|
||||
case 404:
|
||||
return BoxInfoResult.error('未找到该总排号对应的排产号信息');
|
||||
default:
|
||||
return BoxInfoResult.error('查询失败 (${response.statusCode})');
|
||||
}
|
||||
} catch (e) {
|
||||
return BoxInfoResult.error('网络异常,请检查网络连接');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存装箱记录 — POST /CargoTrace/box
|
||||
Future<BoxSaveResult> saveBoxRecord({
|
||||
required String baseUrl,
|
||||
required String zongpaiNo,
|
||||
required int boxNo,
|
||||
required int quantity,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/CargoTrace/box');
|
||||
try {
|
||||
final response = await _client
|
||||
.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'zongpai_no': zongpaiNo,
|
||||
'box_no': boxNo,
|
||||
'quantity': quantity,
|
||||
}),
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return BoxSaveResult.ok(body);
|
||||
case 400:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final msg = body['message']?.toString() ?? '请求参数错误';
|
||||
return BoxSaveResult.error(msg);
|
||||
case 409:
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return BoxSaveResult.duplicate(body);
|
||||
case 404:
|
||||
return BoxSaveResult.error('未找到该总排号对应的排产号信息');
|
||||
default:
|
||||
return BoxSaveResult.error('提交失败 (${response.statusCode})');
|
||||
}
|
||||
} catch (e) {
|
||||
return BoxSaveResult.error('网络异常,请检查网络连接');
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connectivity by making a HEAD request to the base URL.
|
||||
Future<bool> testConnection(String baseUrl) async {
|
||||
try {
|
||||
|
||||
53
lib/services/app_config_service.dart
Normal file
53
lib/services/app_config_service.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
/// 应用全局配置服务 (替代 SharedPreferences)
|
||||
/// 配置数据持久化为本地 JSON 文件,覆盖安装应用时数据不丢失。
|
||||
class AppConfigService {
|
||||
static const String _kConfigFileName = 'app_config.json';
|
||||
static final Map<String, dynamic> _defaults = {'api_url': ''};
|
||||
|
||||
Future<String> get _filePath async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return '${directory.path}/$_kConfigFileName';
|
||||
}
|
||||
|
||||
/// 读取完整配置字典
|
||||
Future<Map<String, dynamic>> loadConfig() async {
|
||||
try {
|
||||
final file = File(await _filePath);
|
||||
if (await file.exists()) {
|
||||
final jsonString = await file.readAsString();
|
||||
return jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[AppConfig] 读取配置失败: $e');
|
||||
}
|
||||
return Map<String, dynamic>.from(_defaults);
|
||||
}
|
||||
|
||||
/// 读取字符串配置项
|
||||
Future<String?> getString(String key) async {
|
||||
final config = await loadConfig();
|
||||
return config[key] as String?;
|
||||
}
|
||||
|
||||
/// 保存字符串配置项
|
||||
Future<void> setString(String key, String value) async {
|
||||
final config = await loadConfig();
|
||||
config[key.toString()] = value.toString();
|
||||
await _saveConfig(config);
|
||||
}
|
||||
|
||||
/// 完整写入 JSON 文件
|
||||
Future<void> _saveConfig(Map<String, dynamic> config) async {
|
||||
try {
|
||||
final file = File(await _filePath);
|
||||
await file.writeAsString(jsonEncode(config));
|
||||
} catch (e) {
|
||||
debugPrint('[AppConfig] 保存配置失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,36 @@
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:pad_scanner/services/app_config_service.dart';
|
||||
|
||||
class SoundService {
|
||||
static const _keySuccess = 'sound_success';
|
||||
static const _keyFailure = 'sound_failure';
|
||||
|
||||
final _player = AudioPlayer();
|
||||
final _configService = AppConfigService();
|
||||
|
||||
Future<String?> getSuccessPath() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_keySuccess);
|
||||
return _configService.getString(_keySuccess);
|
||||
}
|
||||
|
||||
Future<String?> getFailurePath() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_keyFailure);
|
||||
return _configService.getString(_keyFailure);
|
||||
}
|
||||
|
||||
Future<void> setSuccessPath(String? path) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (path != null) {
|
||||
await prefs.setString(_keySuccess, path);
|
||||
await _configService.setString(_keySuccess, path);
|
||||
} else {
|
||||
await prefs.remove(_keySuccess);
|
||||
final config = await _configService.loadConfig();
|
||||
config.remove(_keySuccess);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setFailurePath(String? path) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (path != null) {
|
||||
await prefs.setString(_keyFailure, path);
|
||||
await _configService.setString(_keyFailure, path);
|
||||
} else {
|
||||
await prefs.remove(_keyFailure);
|
||||
final config = await _configService.loadConfig();
|
||||
config.remove(_keyFailure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user