feat: add Flutter web build support
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 瓶安芯服务工作台
|
# 瓶安芯服务工作台
|
||||||
|
|
||||||
配送员、安装维修员和安检员共用的 Android/iOS Flutter 客户端。首期一账号一角色,统一访问 `/heqi/client/v1/staff`。
|
配送员、安装维修员和安检员共用的 Android/iOS/Web Flutter 客户端。首期一账号一角色,统一访问 `/heqi/client/v1/staff`。
|
||||||
|
|
||||||
## 本地运行
|
## 本地运行
|
||||||
|
|
||||||
@@ -20,4 +20,7 @@ flutter analyze
|
|||||||
flutter test
|
flutter test
|
||||||
flutter build apk --debug
|
flutter build apk --debug
|
||||||
flutter build ios --simulator --no-codesign
|
flutter build ios --simulator --no-codesign
|
||||||
|
flutter build web --release --dart-define=API_BASE_URL=https://api.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仓库根目录可使用 `API_BASE_URL=https://api.example.com ./scripts/build-apps.sh service_app all` 同时生成 Android 和 Web 产物。Web 端支持登录、任务处理、按浏览器授权采集位置和相机取证,但不支持 Android/iOS 的后台定位保障;生产环境必须使用 HTTPS API 地址。
|
||||||
|
|||||||
@@ -1,146 +1,2 @@
|
|||||||
import 'dart:convert';
|
export 'encrypted_draft_store_io.dart'
|
||||||
import 'dart:io';
|
if (dart.library.html) 'encrypted_draft_store_web.dart';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
class EncryptedDraftStore {
|
|
||||||
EncryptedDraftStore({
|
|
||||||
FlutterSecureStorage? storage,
|
|
||||||
AesGcm? algorithm,
|
|
||||||
}) : _storage = storage ?? const FlutterSecureStorage(),
|
|
||||||
_algorithm = algorithm ?? AesGcm.with256bits();
|
|
||||||
|
|
||||||
static const _keyPrefix = 'service_draft_key_';
|
|
||||||
final FlutterSecureStorage _storage;
|
|
||||||
final AesGcm _algorithm;
|
|
||||||
|
|
||||||
Future<void> saveDraft({
|
|
||||||
required String accountIdentity,
|
|
||||||
required String taskIdentity,
|
|
||||||
required Map<String, Object?> value,
|
|
||||||
}) async {
|
|
||||||
final directory = await _accountDirectory(accountIdentity);
|
|
||||||
final key = await _key(accountIdentity);
|
|
||||||
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
|
|
||||||
final box = await _algorithm.encrypt(
|
|
||||||
utf8.encode(jsonEncode(value)),
|
|
||||||
secretKey: key,
|
|
||||||
nonce: nonce,
|
|
||||||
);
|
|
||||||
final payload = jsonEncode({
|
|
||||||
'nonce': base64Encode(box.nonce),
|
|
||||||
'cipherText': base64Encode(box.cipherText),
|
|
||||||
'mac': base64Encode(box.mac.bytes),
|
|
||||||
});
|
|
||||||
await File('${directory.path}/$taskIdentity.draft').writeAsString(payload, flush: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> sealAttachment({
|
|
||||||
required String accountIdentity,
|
|
||||||
required String taskIdentity,
|
|
||||||
required String stage,
|
|
||||||
required String sourcePath,
|
|
||||||
}) async {
|
|
||||||
final source = File(sourcePath);
|
|
||||||
final bytes = await source.readAsBytes();
|
|
||||||
final box = await _encrypt(accountIdentity, bytes);
|
|
||||||
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
|
||||||
final file = File('${(await _accountDirectory(accountIdentity)).path}/$name');
|
|
||||||
await file.writeAsString(_boxJson(box), flush: true);
|
|
||||||
if (source.existsSync()) await source.delete();
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> materializeAttachment({
|
|
||||||
required String accountIdentity,
|
|
||||||
required String sealedName,
|
|
||||||
}) async {
|
|
||||||
final source = File('${(await _accountDirectory(accountIdentity)).path}/$sealedName');
|
|
||||||
final clear = await _decrypt(accountIdentity, await source.readAsString());
|
|
||||||
final temporary = await getTemporaryDirectory();
|
|
||||||
final file = File('${temporary.path}/${sealedName.replaceAll('.evidence', '.jpg')}');
|
|
||||||
await file.writeAsBytes(clear, flush: true);
|
|
||||||
return file.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, Object?>?> readDraft(String accountIdentity, String taskIdentity) async {
|
|
||||||
final file = File('${(await _accountDirectory(accountIdentity)).path}/$taskIdentity.draft');
|
|
||||||
if (!file.existsSync()) return null;
|
|
||||||
final payload = jsonDecode(await file.readAsString());
|
|
||||||
if (payload is! Map) return null;
|
|
||||||
final clear = await _decrypt(accountIdentity, await file.readAsString());
|
|
||||||
final decoded = jsonDecode(utf8.decode(clear));
|
|
||||||
if (decoded is! Map) return null;
|
|
||||||
return decoded.map<String, Object?>((key, value) => MapEntry(key.toString(), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> count(String accountIdentity) async {
|
|
||||||
final directory = await _accountDirectory(accountIdentity);
|
|
||||||
return directory
|
|
||||||
.listSync()
|
|
||||||
.whereType<File>()
|
|
||||||
.where((file) => file.path.endsWith('.draft'))
|
|
||||||
.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
|
||||||
final directory = await _accountDirectory(accountIdentity);
|
|
||||||
final file = File('${directory.path}/$taskIdentity.draft');
|
|
||||||
if (file.existsSync()) await file.delete();
|
|
||||||
for (final evidence in directory.listSync().whereType<File>().where(
|
|
||||||
(item) => item.uri.pathSegments.last.startsWith('$taskIdentity-'),
|
|
||||||
)) {
|
|
||||||
await evidence.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> discardAccount(String accountIdentity) async {
|
|
||||||
final directory = await _accountDirectory(accountIdentity);
|
|
||||||
if (directory.existsSync()) await directory.delete(recursive: true);
|
|
||||||
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Directory> _accountDirectory(String accountIdentity) async {
|
|
||||||
final root = await getApplicationSupportDirectory();
|
|
||||||
final directory = Directory('${root.path}/drafts/$accountIdentity');
|
|
||||||
if (!directory.existsSync()) await directory.create(recursive: true);
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SecretKey> _key(String accountIdentity) async {
|
|
||||||
final storageKey = '$_keyPrefix$accountIdentity';
|
|
||||||
var encoded = await _storage.read(key: storageKey);
|
|
||||||
if (encoded == null) {
|
|
||||||
encoded = base64Encode(await (await _algorithm.newSecretKey()).extractBytes());
|
|
||||||
await _storage.write(key: storageKey, value: encoded);
|
|
||||||
}
|
|
||||||
return SecretKey(base64Decode(encoded));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async {
|
|
||||||
final nonce = List<int>.generate(12, (_) => Random.secure().nextInt(256));
|
|
||||||
return _algorithm.encrypt(clear, secretKey: await _key(accountIdentity), nonce: nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<int>> _decrypt(String accountIdentity, String encodedPayload) async {
|
|
||||||
final payload = jsonDecode(encodedPayload);
|
|
||||||
if (payload is! Map) throw const FormatException('Invalid encrypted draft');
|
|
||||||
return _algorithm.decrypt(
|
|
||||||
SecretBox(
|
|
||||||
base64Decode(payload['cipherText'] as String),
|
|
||||||
nonce: base64Decode(payload['nonce'] as String),
|
|
||||||
mac: Mac(base64Decode(payload['mac'] as String)),
|
|
||||||
),
|
|
||||||
secretKey: await _key(accountIdentity),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _boxJson(SecretBox box) => jsonEncode({
|
|
||||||
'nonce': base64Encode(box.nonce),
|
|
||||||
'cipherText': base64Encode(box.cipherText),
|
|
||||||
'mac': base64Encode(box.mac.bytes),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
154
apps/service_app/lib/data/offline/encrypted_draft_store_io.dart
Normal file
154
apps/service_app/lib/data/offline/encrypted_draft_store_io.dart
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Android/iOS 端按账号加密保存现场草稿和附件。
|
||||||
|
class EncryptedDraftStore {
|
||||||
|
EncryptedDraftStore({FlutterSecureStorage? storage, AesGcm? algorithm})
|
||||||
|
: _storage = storage ?? const FlutterSecureStorage(),
|
||||||
|
_algorithm = algorithm ?? AesGcm.with256bits();
|
||||||
|
|
||||||
|
static const _keyPrefix = 'service_draft_key_';
|
||||||
|
final FlutterSecureStorage _storage;
|
||||||
|
final AesGcm _algorithm;
|
||||||
|
|
||||||
|
Future<void> saveDraft({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String taskIdentity,
|
||||||
|
required Map<String, Object?> value,
|
||||||
|
}) async {
|
||||||
|
final directory = await _accountDirectory(accountIdentity);
|
||||||
|
final box = await _encrypt(accountIdentity, utf8.encode(jsonEncode(value)));
|
||||||
|
await File(
|
||||||
|
'${directory.path}/$taskIdentity.draft',
|
||||||
|
).writeAsString(_boxJson(box), flush: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> sealAttachment({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String taskIdentity,
|
||||||
|
required String stage,
|
||||||
|
required XFile source,
|
||||||
|
}) async {
|
||||||
|
final box = await _encrypt(accountIdentity, await source.readAsBytes());
|
||||||
|
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
||||||
|
final file = File(
|
||||||
|
'${(await _accountDirectory(accountIdentity)).path}/$name',
|
||||||
|
);
|
||||||
|
await file.writeAsString(_boxJson(box), flush: true);
|
||||||
|
final sourceFile = File(source.path);
|
||||||
|
if (sourceFile.existsSync()) await sourceFile.delete();
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<XFile> materializeAttachment({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String sealedName,
|
||||||
|
}) async {
|
||||||
|
final source = File(
|
||||||
|
'${(await _accountDirectory(accountIdentity)).path}/$sealedName',
|
||||||
|
);
|
||||||
|
final clear = await _decrypt(accountIdentity, await source.readAsString());
|
||||||
|
final temporary = await getTemporaryDirectory();
|
||||||
|
final file = File(
|
||||||
|
'${temporary.path}/${sealedName.replaceAll('.evidence', '.jpg')}',
|
||||||
|
);
|
||||||
|
await file.writeAsBytes(clear, flush: true);
|
||||||
|
return XFile(file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> discardMaterializedAttachment(XFile file) async {
|
||||||
|
final temporary = File(file.path);
|
||||||
|
if (temporary.existsSync()) await temporary.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>?> readDraft(
|
||||||
|
String accountIdentity,
|
||||||
|
String taskIdentity,
|
||||||
|
) async {
|
||||||
|
final file = File(
|
||||||
|
'${(await _accountDirectory(accountIdentity)).path}/$taskIdentity.draft',
|
||||||
|
);
|
||||||
|
if (!file.existsSync()) return null;
|
||||||
|
final decoded = jsonDecode(
|
||||||
|
utf8.decode(await _decrypt(accountIdentity, await file.readAsString())),
|
||||||
|
);
|
||||||
|
if (decoded is! Map) return null;
|
||||||
|
return decoded.map<String, Object?>(
|
||||||
|
(key, value) => MapEntry(key.toString(), value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> count(String accountIdentity) async => (await _accountDirectory(
|
||||||
|
accountIdentity,
|
||||||
|
)).listSync().whereType<File>().where((file) => file.path.endsWith('.draft')).length;
|
||||||
|
|
||||||
|
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
||||||
|
final directory = await _accountDirectory(accountIdentity);
|
||||||
|
final file = File('${directory.path}/$taskIdentity.draft');
|
||||||
|
if (file.existsSync()) await file.delete();
|
||||||
|
for (final evidence in directory.listSync().whereType<File>().where(
|
||||||
|
(item) => item.uri.pathSegments.last.startsWith('$taskIdentity-'),
|
||||||
|
)) {
|
||||||
|
await evidence.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> discardAccount(String accountIdentity) async {
|
||||||
|
final directory = await _accountDirectory(accountIdentity);
|
||||||
|
if (directory.existsSync()) await directory.delete(recursive: true);
|
||||||
|
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Directory> _accountDirectory(String accountIdentity) async {
|
||||||
|
final root = await getApplicationSupportDirectory();
|
||||||
|
final directory = Directory('${root.path}/drafts/$accountIdentity');
|
||||||
|
if (!directory.existsSync()) await directory.create(recursive: true);
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretKey> _key(String accountIdentity) async {
|
||||||
|
final storageKey = '$_keyPrefix$accountIdentity';
|
||||||
|
var encoded = await _storage.read(key: storageKey);
|
||||||
|
if (encoded == null) {
|
||||||
|
encoded = base64Encode(
|
||||||
|
await (await _algorithm.newSecretKey()).extractBytes(),
|
||||||
|
);
|
||||||
|
await _storage.write(key: storageKey, value: encoded);
|
||||||
|
}
|
||||||
|
return SecretKey(base64Decode(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async => _algorithm.encrypt(
|
||||||
|
clear,
|
||||||
|
secretKey: await _key(accountIdentity),
|
||||||
|
nonce: List<int>.generate(12, (_) => Random.secure().nextInt(256)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<int>> _decrypt(
|
||||||
|
String accountIdentity,
|
||||||
|
String encodedPayload,
|
||||||
|
) async {
|
||||||
|
final payload = jsonDecode(encodedPayload);
|
||||||
|
if (payload is! Map) throw const FormatException('Invalid encrypted draft');
|
||||||
|
return _algorithm.decrypt(
|
||||||
|
SecretBox(
|
||||||
|
base64Decode(payload['cipherText'] as String),
|
||||||
|
nonce: base64Decode(payload['nonce'] as String),
|
||||||
|
mac: Mac(base64Decode(payload['mac'] as String)),
|
||||||
|
),
|
||||||
|
secretKey: await _key(accountIdentity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _boxJson(SecretBox box) => jsonEncode({
|
||||||
|
'nonce': base64Encode(box.nonce),
|
||||||
|
'cipherText': base64Encode(box.cipherText),
|
||||||
|
'mac': base64Encode(box.mac.bytes),
|
||||||
|
});
|
||||||
|
}
|
||||||
150
apps/service_app/lib/data/offline/encrypted_draft_store_web.dart
Normal file
150
apps/service_app/lib/data/offline/encrypted_draft_store_web.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
/// Web 端仅保存 AES-GCM 密文;密钥继续由浏览器安全存储插件管理。
|
||||||
|
class EncryptedDraftStore {
|
||||||
|
EncryptedDraftStore({FlutterSecureStorage? storage, AesGcm? algorithm})
|
||||||
|
: _storage = storage ?? const FlutterSecureStorage(),
|
||||||
|
_algorithm = algorithm ?? AesGcm.with256bits();
|
||||||
|
|
||||||
|
static const _keyPrefix = 'service_draft_key_';
|
||||||
|
static const _entryPrefix = 'service_draft_entry_';
|
||||||
|
final FlutterSecureStorage _storage;
|
||||||
|
final AesGcm _algorithm;
|
||||||
|
|
||||||
|
Future<void> saveDraft({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String taskIdentity,
|
||||||
|
required Map<String, Object?> value,
|
||||||
|
}) async {
|
||||||
|
final box = await _encrypt(accountIdentity, utf8.encode(jsonEncode(value)));
|
||||||
|
_entries.setItem(_draftKey(accountIdentity, taskIdentity), _boxJson(box));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> sealAttachment({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String taskIdentity,
|
||||||
|
required String stage,
|
||||||
|
required XFile source,
|
||||||
|
}) async {
|
||||||
|
final box = await _encrypt(accountIdentity, await source.readAsBytes());
|
||||||
|
final name = '$taskIdentity-$stage-${DateTime.now().microsecondsSinceEpoch}.evidence';
|
||||||
|
_entries.setItem(
|
||||||
|
_attachmentKey(accountIdentity, name),
|
||||||
|
jsonEncode({'box': _boxJson(box), 'file_name': source.name}),
|
||||||
|
);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<XFile> materializeAttachment({
|
||||||
|
required String accountIdentity,
|
||||||
|
required String sealedName,
|
||||||
|
}) async {
|
||||||
|
final encoded = _entries.getItem(_attachmentKey(accountIdentity, sealedName));
|
||||||
|
if (encoded == null) {
|
||||||
|
throw StateError('未找到加密附件');
|
||||||
|
}
|
||||||
|
final payload = jsonDecode(encoded);
|
||||||
|
if (payload is! Map) {
|
||||||
|
throw const FormatException('Invalid encrypted attachment');
|
||||||
|
}
|
||||||
|
final clear = await _decrypt(accountIdentity, payload['box'] as String);
|
||||||
|
return XFile.fromData(
|
||||||
|
Uint8List.fromList(clear),
|
||||||
|
name: payload['file_name'] as String? ?? '$sealedName.jpg',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> discardMaterializedAttachment(XFile file) async {}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>?> readDraft(String accountIdentity, String taskIdentity) async {
|
||||||
|
final encoded = _entries.getItem(_draftKey(accountIdentity, taskIdentity));
|
||||||
|
if (encoded == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final decoded = jsonDecode(utf8.decode(await _decrypt(accountIdentity, encoded)));
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decoded.map<String, Object?>((key, value) => MapEntry(key.toString(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> count(String accountIdentity) async =>
|
||||||
|
_entryKeys.where((key) => key.startsWith('$_entryPrefix${accountIdentity}_draft_')).length;
|
||||||
|
|
||||||
|
Future<void> deleteDraft(String accountIdentity, String taskIdentity) async {
|
||||||
|
final prefix = '$_entryPrefix${accountIdentity}_';
|
||||||
|
_entries.removeItem(
|
||||||
|
'$prefix'
|
||||||
|
'draft_$taskIdentity',
|
||||||
|
);
|
||||||
|
final attachmentPrefix =
|
||||||
|
'$prefix'
|
||||||
|
'attachment_$taskIdentity-';
|
||||||
|
for (final key in _entryKeys.where((key) => key.startsWith(attachmentPrefix)).toList()) {
|
||||||
|
_entries.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> discardAccount(String accountIdentity) async {
|
||||||
|
final prefix = '$_entryPrefix${accountIdentity}_';
|
||||||
|
for (final key in _entryKeys.where((key) => key.startsWith(prefix)).toList()) {
|
||||||
|
_entries.removeItem(key);
|
||||||
|
}
|
||||||
|
await _storage.delete(key: '$_keyPrefix$accountIdentity');
|
||||||
|
}
|
||||||
|
|
||||||
|
web.Storage get _entries => web.window.localStorage;
|
||||||
|
|
||||||
|
Iterable<String> get _entryKeys =>
|
||||||
|
List<String?>.generate(_entries.length, (index) => _entries.key(index)).whereType<String>();
|
||||||
|
|
||||||
|
String _draftKey(String accountIdentity, String taskIdentity) =>
|
||||||
|
'$_entryPrefix${accountIdentity}_draft_$taskIdentity';
|
||||||
|
|
||||||
|
String _attachmentKey(String accountIdentity, String sealedName) =>
|
||||||
|
'$_entryPrefix${accountIdentity}_attachment_$sealedName';
|
||||||
|
|
||||||
|
Future<SecretKey> _key(String accountIdentity) async {
|
||||||
|
final storageKey = '$_keyPrefix$accountIdentity';
|
||||||
|
var encoded = await _storage.read(key: storageKey);
|
||||||
|
if (encoded == null) {
|
||||||
|
encoded = base64Encode(await (await _algorithm.newSecretKey()).extractBytes());
|
||||||
|
await _storage.write(key: storageKey, value: encoded);
|
||||||
|
}
|
||||||
|
return SecretKey(base64Decode(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretBox> _encrypt(String accountIdentity, List<int> clear) async => _algorithm.encrypt(
|
||||||
|
clear,
|
||||||
|
secretKey: await _key(accountIdentity),
|
||||||
|
nonce: List<int>.generate(12, (_) => Random.secure().nextInt(256)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<int>> _decrypt(String accountIdentity, String encodedPayload) async {
|
||||||
|
final payload = jsonDecode(encodedPayload);
|
||||||
|
if (payload is! Map) {
|
||||||
|
throw const FormatException('Invalid encrypted draft');
|
||||||
|
}
|
||||||
|
return _algorithm.decrypt(
|
||||||
|
SecretBox(
|
||||||
|
base64Decode(payload['cipherText'] as String),
|
||||||
|
nonce: base64Decode(payload['nonce'] as String),
|
||||||
|
mac: Mac(base64Decode(payload['mac'] as String)),
|
||||||
|
),
|
||||||
|
secretKey: await _key(accountIdentity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _boxJson(SecretBox box) => jsonEncode({
|
||||||
|
'nonce': base64Encode(box.nonce),
|
||||||
|
'cipherText': base64Encode(box.cipherText),
|
||||||
|
'mac': base64Encode(box.mac.bytes),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../../domain/models/service_models.dart';
|
import '../../domain/models/service_models.dart';
|
||||||
import '../services/api_client.dart';
|
import '../services/api_client.dart';
|
||||||
@@ -86,14 +87,17 @@ class ServiceRepository {
|
|||||||
|
|
||||||
Future<void> arrive(String identity) async {
|
Future<void> arrive(String identity) async {
|
||||||
final point = await _location.current();
|
final point = await _location.current();
|
||||||
await _api.post('$root/delivery/orders/$identity/arrive', body: _pointJson(point));
|
await _api.post(
|
||||||
|
'$root/delivery/orders/$identity/arrive',
|
||||||
|
body: _pointJson(point),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> submitDeliveryReceipt({
|
Future<void> submitDeliveryReceipt({
|
||||||
required String identity,
|
required String identity,
|
||||||
required String recipientName,
|
required String recipientName,
|
||||||
required String recipientPhone,
|
required String recipientPhone,
|
||||||
required String proofFile,
|
required XFile proofFile,
|
||||||
}) async {
|
}) async {
|
||||||
final proofUri = await _api.upload(proofFile);
|
final proofUri = await _api.upload(proofFile);
|
||||||
await _api.post(
|
await _api.post(
|
||||||
@@ -118,7 +122,7 @@ class ServiceRepository {
|
|||||||
final uploaded = <Map<String, Object?>>[];
|
final uploaded = <Map<String, Object?>>[];
|
||||||
for (final item in evidence) {
|
for (final item in evidence) {
|
||||||
final uri = await _api.upload(
|
final uri = await _api.upload(
|
||||||
item.filePath,
|
item.file,
|
||||||
contentType: item.mediaType == 'video' ? 'video/mp4' : 'image/jpeg',
|
contentType: item.mediaType == 'video' ? 'video/mp4' : 'image/jpeg',
|
||||||
);
|
);
|
||||||
uploaded.add({
|
uploaded.add({
|
||||||
@@ -153,14 +157,14 @@ class EvidenceInput {
|
|||||||
const EvidenceInput({
|
const EvidenceInput({
|
||||||
required this.evidenceType,
|
required this.evidenceType,
|
||||||
required this.mediaType,
|
required this.mediaType,
|
||||||
required this.filePath,
|
required this.file,
|
||||||
required this.capturedAt,
|
required this.capturedAt,
|
||||||
required this.requestNo,
|
required this.requestNo,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String evidenceType;
|
final String evidenceType;
|
||||||
final String mediaType;
|
final String mediaType;
|
||||||
final String filePath;
|
final XFile file;
|
||||||
final DateTime capturedAt;
|
final DateTime capturedAt;
|
||||||
final String requestNo;
|
final String requestNo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
class ApiException implements Exception {
|
class ApiException implements Exception {
|
||||||
const ApiException(this.code, this.message);
|
const ApiException(this.code, this.message);
|
||||||
@@ -38,11 +38,21 @@ class ApiClient {
|
|||||||
|
|
||||||
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
|
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
|
||||||
|
|
||||||
Future<String> upload(String filePath, {String contentType = 'image/jpeg'}) async {
|
/// 上传跨平台文件对象;浏览器端不能依赖本地文件路径。
|
||||||
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/upload/file'));
|
Future<String> upload(XFile file, {String contentType = 'image/jpeg'}) async {
|
||||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
final request = http.MultipartRequest(
|
||||||
|
'POST',
|
||||||
|
Uri.parse('$baseUrl/upload/file'),
|
||||||
|
);
|
||||||
|
request.headers['authorization'] = _tokenProvider();
|
||||||
request.fields['declared_content_type'] = contentType;
|
request.fields['declared_content_type'] = contentType;
|
||||||
request.files.add(await http.MultipartFile.fromPath('file', filePath));
|
request.files.add(
|
||||||
|
http.MultipartFile.fromBytes(
|
||||||
|
'file',
|
||||||
|
await file.readAsBytes(),
|
||||||
|
filename: file.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
final response = await http.Response.fromStream(await request.send());
|
final response = await http.Response.fromStream(await request.send());
|
||||||
final details = _decode(response);
|
final details = _decode(response);
|
||||||
return jsonMap(details)['uri'] as String? ?? '';
|
return jsonMap(details)['uri'] as String? ?? '';
|
||||||
@@ -55,15 +65,17 @@ class ApiClient {
|
|||||||
bool authenticated = true,
|
bool authenticated = true,
|
||||||
}) async {
|
}) async {
|
||||||
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
||||||
request.headers[HttpHeaders.acceptHeader] = 'application/json';
|
request.headers['accept'] = 'application/json';
|
||||||
if (authenticated && _tokenProvider().isNotEmpty) {
|
if (authenticated && _tokenProvider().isNotEmpty) {
|
||||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
request.headers['authorization'] = _tokenProvider();
|
||||||
}
|
}
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
|
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
||||||
request.body = jsonEncode(body);
|
request.body = jsonEncode(body);
|
||||||
}
|
}
|
||||||
final response = await http.Response.fromStream(await _client.send(request));
|
final response = await http.Response.fromStream(
|
||||||
|
await _client.send(request),
|
||||||
|
);
|
||||||
return _decode(response);
|
return _decode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -44,12 +42,17 @@ class _EvidencePageState extends State<EvidencePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _restore() async {
|
Future<void> _restore() async {
|
||||||
final draft = await widget.drafts.readDraft(widget.session.identity, widget.taskIdentity);
|
final draft = await widget.drafts.readDraft(
|
||||||
|
widget.session.identity,
|
||||||
|
widget.taskIdentity,
|
||||||
|
);
|
||||||
if (draft == null || !mounted) return;
|
if (draft == null || !mounted) return;
|
||||||
final values = draft['evidence'];
|
final values = draft['evidence'];
|
||||||
if (values is List) {
|
if (values is List) {
|
||||||
for (final value in values.whereType<Map<Object?, Object?>>()) {
|
for (final value in values.whereType<Map<Object?, Object?>>()) {
|
||||||
final mapped = value.map<String, Object?>((key, item) => MapEntry(key.toString(), item));
|
final mapped = value.map<String, Object?>(
|
||||||
|
(key, item) => MapEntry(key.toString(), item),
|
||||||
|
);
|
||||||
final stage = mapped['stage'] as String? ?? '';
|
final stage = mapped['stage'] as String? ?? '';
|
||||||
if (stage.isNotEmpty) _evidence[stage] = mapped;
|
if (stage.isNotEmpty) _evidence[stage] = mapped;
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,7 @@ class _EvidencePageState extends State<EvidencePage> {
|
|||||||
accountIdentity: widget.session.identity,
|
accountIdentity: widget.session.identity,
|
||||||
taskIdentity: widget.taskIdentity,
|
taskIdentity: widget.taskIdentity,
|
||||||
stage: stage,
|
stage: stage,
|
||||||
sourcePath: image.path,
|
source: image,
|
||||||
);
|
);
|
||||||
_evidence[stage] = {
|
_evidence[stage] = {
|
||||||
'stage': stage,
|
'stage': stage,
|
||||||
@@ -97,25 +100,27 @@ class _EvidencePageState extends State<EvidencePage> {
|
|||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_requiredStages.every(_evidence.containsKey) || _result.text.trim().isEmpty) {
|
if (!_requiredStages.every(_evidence.containsKey) || _result.text.trim().isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请完成结果说明和全部必需取证项')));
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('请完成结果说明和全部必需取证项')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
final temporaryFiles = <String>[];
|
final temporaryFiles = <XFile>[];
|
||||||
try {
|
try {
|
||||||
await _save();
|
await _save();
|
||||||
final inputs = <EvidenceInput>[];
|
final inputs = <EvidenceInput>[];
|
||||||
for (final item in _evidence.values) {
|
for (final item in _evidence.values) {
|
||||||
final path = await widget.drafts.materializeAttachment(
|
final file = await widget.drafts.materializeAttachment(
|
||||||
accountIdentity: widget.session.identity,
|
accountIdentity: widget.session.identity,
|
||||||
sealedName: item['sealed_name'] as String,
|
sealedName: item['sealed_name'] as String,
|
||||||
);
|
);
|
||||||
temporaryFiles.add(path);
|
temporaryFiles.add(file);
|
||||||
inputs.add(
|
inputs.add(
|
||||||
EvidenceInput(
|
EvidenceInput(
|
||||||
evidenceType: item['stage'] as String,
|
evidenceType: item['stage'] as String,
|
||||||
mediaType: item['media_type'] as String,
|
mediaType: item['media_type'] as String,
|
||||||
filePath: path,
|
file: file,
|
||||||
capturedAt: DateTime.parse(item['captured_at'] as String),
|
capturedAt: DateTime.parse(item['captured_at'] as String),
|
||||||
requestNo: item['request_no'] as String,
|
requestNo: item['request_no'] as String,
|
||||||
),
|
),
|
||||||
@@ -127,16 +132,20 @@ class _EvidencePageState extends State<EvidencePage> {
|
|||||||
conclusion: _conclusion,
|
conclusion: _conclusion,
|
||||||
evidence: inputs,
|
evidence: inputs,
|
||||||
);
|
);
|
||||||
await widget.drafts.deleteDraft(widget.session.identity, widget.taskIdentity);
|
await widget.drafts.deleteDraft(
|
||||||
|
widget.session.identity,
|
||||||
|
widget.taskIdentity,
|
||||||
|
);
|
||||||
if (mounted) Navigator.pop(context, true);
|
if (mounted) Navigator.pop(context, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('提交失败,草稿仍安全保留:$error')));
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('提交失败,草稿仍安全保留:$error')));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
for (final path in temporaryFiles) {
|
for (final file in temporaryFiles) {
|
||||||
final file = File(path);
|
await widget.drafts.discardMaterializedAttachment(file);
|
||||||
if (file.existsSync()) await file.delete();
|
|
||||||
}
|
}
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -42,14 +40,19 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
? widget.repository.deliveryDetail(widget.identity)
|
? widget.repository.deliveryDetail(widget.identity)
|
||||||
: widget.repository.ticketDetail(widget.identity);
|
: widget.repository.ticketDetail(widget.identity);
|
||||||
|
|
||||||
Future<void> _run(Future<void> Function(WorkItem) action, WorkItem item) async {
|
Future<void> _run(
|
||||||
|
Future<void> Function(WorkItem) action,
|
||||||
|
WorkItem item,
|
||||||
|
) async {
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
try {
|
try {
|
||||||
await action(item);
|
await action(item);
|
||||||
setState(() => _future = _load());
|
setState(() => _future = _load());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString())));
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(error.toString())));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
@@ -68,7 +71,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
decoration: const InputDecoration(labelText: '原因'),
|
decoration: const InputDecoration(labelText: '原因'),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
onPressed: () => Navigator.pop(context, controller.text.trim()),
|
||||||
child: const Text('确认'),
|
child: const Text('确认'),
|
||||||
@@ -81,7 +87,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _receipt(WorkItem item) async {
|
Future<void> _receipt(WorkItem item) async {
|
||||||
final existing = await widget.drafts.readDraft(widget.session.identity, item.identity);
|
final existing = await widget.drafts.readDraft(
|
||||||
|
widget.session.identity,
|
||||||
|
item.identity,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
String? sealedName;
|
String? sealedName;
|
||||||
if (existing?['kind'] == 'delivery_receipt' && existing?['sealed_name'] is String) {
|
if (existing?['kind'] == 'delivery_receipt' && existing?['sealed_name'] is String) {
|
||||||
@@ -91,25 +100,34 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
title: const Text('发现未提交签收草稿'),
|
title: const Text('发现未提交签收草稿'),
|
||||||
content: const Text('是否继续提交上次加密保存的签收凭证?'),
|
content: const Text('是否继续提交上次加密保存的签收凭证?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('重新拍摄')),
|
TextButton(
|
||||||
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('继续提交')),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('重新拍摄'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('继续提交'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (reuse == true) sealedName = existing!['sealed_name'] as String;
|
if (reuse == true) sealedName = existing!['sealed_name'] as String;
|
||||||
}
|
}
|
||||||
if (sealedName == null) {
|
if (sealedName == null) {
|
||||||
final image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 82);
|
final image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 82,
|
||||||
|
);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
sealedName = await widget.drafts.sealAttachment(
|
sealedName = await widget.drafts.sealAttachment(
|
||||||
accountIdentity: widget.session.identity,
|
accountIdentity: widget.session.identity,
|
||||||
taskIdentity: item.identity,
|
taskIdentity: item.identity,
|
||||||
stage: 'receipt',
|
stage: 'receipt',
|
||||||
sourcePath: image.path,
|
source: image,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
String? temporaryPath;
|
XFile? temporaryFile;
|
||||||
try {
|
try {
|
||||||
await widget.drafts.saveDraft(
|
await widget.drafts.saveDraft(
|
||||||
accountIdentity: widget.session.identity,
|
accountIdentity: widget.session.identity,
|
||||||
@@ -122,7 +140,7 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
'captured_at': DateTime.now().toUtc().toIso8601String(),
|
'captured_at': DateTime.now().toUtc().toIso8601String(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
temporaryPath = await widget.drafts.materializeAttachment(
|
temporaryFile = await widget.drafts.materializeAttachment(
|
||||||
accountIdentity: widget.session.identity,
|
accountIdentity: widget.session.identity,
|
||||||
sealedName: sealedName,
|
sealedName: sealedName,
|
||||||
);
|
);
|
||||||
@@ -130,7 +148,7 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
identity: item.identity,
|
identity: item.identity,
|
||||||
recipientName: item.raw['contact_name'] as String? ?? '收货人',
|
recipientName: item.raw['contact_name'] as String? ?? '收货人',
|
||||||
recipientPhone: item.raw['contact_phone'] as String? ?? '',
|
recipientPhone: item.raw['contact_phone'] as String? ?? '',
|
||||||
proofFile: temporaryPath,
|
proofFile: temporaryFile,
|
||||||
);
|
);
|
||||||
await widget.drafts.deleteDraft(widget.session.identity, item.identity);
|
await widget.drafts.deleteDraft(widget.session.identity, item.identity);
|
||||||
if (mounted) setState(() => _future = _load());
|
if (mounted) setState(() => _future = _load());
|
||||||
@@ -141,9 +159,8 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
).showSnackBar(SnackBar(content: Text('签收提交失败,加密草稿已保留:$error')));
|
).showSnackBar(SnackBar(content: Text('签收提交失败,加密草稿已保留:$error')));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (temporaryPath != null) {
|
if (temporaryFile != null) {
|
||||||
final file = File(temporaryPath);
|
await widget.drafts.discardMaterializedAttachment(temporaryFile);
|
||||||
if (file.existsSync()) await file.delete();
|
|
||||||
}
|
}
|
||||||
if (mounted) setState(() => _busy = false);
|
if (mounted) setState(() => _busy = false);
|
||||||
}
|
}
|
||||||
@@ -170,7 +187,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.number, style: const TextStyle(color: Colors.white70)),
|
Text(
|
||||||
|
item.number,
|
||||||
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
item.title,
|
||||||
@@ -206,7 +226,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
onPressed: _busy
|
onPressed: _busy
|
||||||
? null
|
? null
|
||||||
: () => _run(
|
: () => _run(
|
||||||
(value) => widget.repository.start(value, widget.session.roleCode),
|
(value) => widget.repository.start(
|
||||||
|
value,
|
||||||
|
widget.session.roleCode,
|
||||||
|
),
|
||||||
item,
|
item,
|
||||||
),
|
),
|
||||||
child: const Text('开始处理'),
|
child: const Text('开始处理'),
|
||||||
@@ -216,7 +239,9 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
onPressed: _busy
|
onPressed: _busy
|
||||||
? null
|
? null
|
||||||
: () => _run(
|
: () => _run(
|
||||||
(value) => widget.repository.appendCurrentTrack(value.identity),
|
(value) => widget.repository.appendCurrentTrack(
|
||||||
|
value.identity,
|
||||||
|
),
|
||||||
item,
|
item,
|
||||||
),
|
),
|
||||||
child: const Text('上报当前位置'),
|
child: const Text('上报当前位置'),
|
||||||
@@ -225,7 +250,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _busy
|
onPressed: _busy
|
||||||
? null
|
? null
|
||||||
: () => _run((value) => widget.repository.arrive(value.identity), item),
|
: () => _run(
|
||||||
|
(value) => widget.repository.arrive(value.identity),
|
||||||
|
item,
|
||||||
|
),
|
||||||
child: const Text('到达并校验围栏'),
|
child: const Text('到达并校验围栏'),
|
||||||
),
|
),
|
||||||
if (item.allowedActions.contains('submit_receipt'))
|
if (item.allowedActions.contains('submit_receipt'))
|
||||||
@@ -253,8 +281,11 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
final reason = await _reason();
|
final reason = await _reason();
|
||||||
if (reason != null && reason.isNotEmpty) {
|
if (reason != null && reason.isNotEmpty) {
|
||||||
await _run(
|
await _run(
|
||||||
(value) =>
|
(value) => widget.repository.exception(
|
||||||
widget.repository.exception(value, widget.session.roleCode, reason),
|
value,
|
||||||
|
widget.session.roleCode,
|
||||||
|
reason,
|
||||||
|
),
|
||||||
item,
|
item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -269,8 +300,11 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
|||||||
final reason = await _reason();
|
final reason = await _reason();
|
||||||
if (reason != null && reason.isNotEmpty) {
|
if (reason != null && reason.isNotEmpty) {
|
||||||
await _run(
|
await _run(
|
||||||
(value) =>
|
(value) => widget.repository.recover(
|
||||||
widget.repository.recover(value, widget.session.roleCode, reason),
|
value,
|
||||||
|
widget.session.roleCode,
|
||||||
|
reason,
|
||||||
|
),
|
||||||
item,
|
item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
@@ -14,7 +14,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
version: "2.13.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
@@ -22,7 +22,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
characters:
|
characters:
|
||||||
@@ -30,7 +30,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
clock:
|
clock:
|
||||||
@@ -38,7 +38,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
code_assets:
|
code_assets:
|
||||||
@@ -46,7 +46,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: code_assets
|
name: code_assets
|
||||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
collection:
|
collection:
|
||||||
@@ -54,23 +54,23 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus
|
name: connectivity_plus
|
||||||
sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0"
|
sha256: "25cebb79dfe304022550e0c3c893ae051ded07183d2607f7b88473cb3ade33cb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.1"
|
version: "7.3.0"
|
||||||
connectivity_plus_platform_interface:
|
connectivity_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus_platform_interface
|
name: connectivity_plus_platform_interface
|
||||||
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
|
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
cross_file:
|
cross_file:
|
||||||
@@ -78,7 +78,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: cross_file
|
name: cross_file
|
||||||
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5+4"
|
version: "0.3.5+4"
|
||||||
crypto:
|
crypto:
|
||||||
@@ -86,7 +86,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
cryptography:
|
cryptography:
|
||||||
@@ -94,7 +94,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: cryptography
|
name: cryptography
|
||||||
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
|
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.0"
|
version: "2.9.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
@@ -102,7 +102,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "1.0.9"
|
||||||
dbus:
|
dbus:
|
||||||
@@ -110,7 +110,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: dbus
|
name: dbus
|
||||||
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
|
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.14"
|
version: "0.7.14"
|
||||||
fake_async:
|
fake_async:
|
||||||
@@ -118,7 +118,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
@@ -126,7 +126,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
ffi_leak_tracker:
|
ffi_leak_tracker:
|
||||||
@@ -134,7 +134,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: ffi_leak_tracker
|
name: ffi_leak_tracker
|
||||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
@@ -142,7 +142,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: file_selector_linux
|
name: file_selector_linux
|
||||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4"
|
version: "0.9.4"
|
||||||
file_selector_macos:
|
file_selector_macos:
|
||||||
@@ -150,7 +150,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: file_selector_macos
|
name: file_selector_macos
|
||||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.5"
|
version: "0.9.5"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
@@ -158,7 +158,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: file_selector_platform_interface
|
name: file_selector_platform_interface
|
||||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
file_selector_windows:
|
file_selector_windows:
|
||||||
@@ -166,7 +166,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: file_selector_windows
|
name: file_selector_windows
|
||||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
@@ -174,7 +174,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: fixnum
|
name: fixnum
|
||||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
@@ -187,7 +187,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
@@ -195,7 +195,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.35"
|
version: "2.0.35"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
@@ -203,7 +203,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.3.1"
|
version: "10.3.1"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
@@ -211,7 +211,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_darwin
|
name: flutter_secure_storage_darwin
|
||||||
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
@@ -219,23 +219,23 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_linux
|
name: flutter_secure_storage_linux
|
||||||
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_platform_interface
|
name: flutter_secure_storage_platform_interface
|
||||||
sha256: "06686df417f34fe9f963ed217b7fdce250e2f637ddd6c374d7eb054549770633"
|
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.1"
|
||||||
flutter_secure_storage_web:
|
flutter_secure_storage_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_web
|
name: flutter_secure_storage_web
|
||||||
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
flutter_secure_storage_windows:
|
flutter_secure_storage_windows:
|
||||||
@@ -243,7 +243,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_windows
|
name: flutter_secure_storage_windows
|
||||||
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.2"
|
version: "4.2.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -261,7 +261,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geoclue
|
name: geoclue
|
||||||
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
geolocator:
|
geolocator:
|
||||||
@@ -269,7 +269,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator
|
name: geolocator
|
||||||
sha256: e146a6d63776582651e97a79cbe459f8e1211b100101fadcd84db83361fa599f
|
sha256: e146a6d63776582651e97a79cbe459f8e1211b100101fadcd84db83361fa599f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.0.3"
|
version: "14.0.3"
|
||||||
geolocator_android:
|
geolocator_android:
|
||||||
@@ -277,7 +277,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_android
|
name: geolocator_android
|
||||||
sha256: "86ea1654e4f61ff51466848e91c116b422d6010ea269fda0fbe1af7e9e742ce1"
|
sha256: "86ea1654e4f61ff51466848e91c116b422d6010ea269fda0fbe1af7e9e742ce1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.3"
|
version: "5.0.3"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
@@ -285,7 +285,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_apple
|
name: geolocator_apple
|
||||||
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
|
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.14"
|
version: "2.3.14"
|
||||||
geolocator_linux:
|
geolocator_linux:
|
||||||
@@ -293,7 +293,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_linux
|
name: geolocator_linux
|
||||||
sha256: "3da7420f11c3496511a5bd3c18fd67b88e5659f12e46b7ce00a788f6996e850a"
|
sha256: "3da7420f11c3496511a5bd3c18fd67b88e5659f12e46b7ce00a788f6996e850a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.6"
|
version: "0.2.6"
|
||||||
geolocator_platform_interface:
|
geolocator_platform_interface:
|
||||||
@@ -301,7 +301,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_platform_interface
|
name: geolocator_platform_interface
|
||||||
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
|
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.8"
|
version: "4.2.8"
|
||||||
geolocator_web:
|
geolocator_web:
|
||||||
@@ -309,7 +309,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_web
|
name: geolocator_web
|
||||||
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
|
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.4"
|
version: "4.1.4"
|
||||||
geolocator_windows:
|
geolocator_windows:
|
||||||
@@ -317,7 +317,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: geolocator_windows
|
name: geolocator_windows
|
||||||
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.5"
|
version: "0.2.5"
|
||||||
go_router:
|
go_router:
|
||||||
@@ -325,7 +325,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
|
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.3.0"
|
version: "17.3.0"
|
||||||
gsettings:
|
gsettings:
|
||||||
@@ -333,7 +333,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: gsettings
|
name: gsettings
|
||||||
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.8"
|
version: "0.2.8"
|
||||||
hooks:
|
hooks:
|
||||||
@@ -341,7 +341,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
http:
|
http:
|
||||||
@@ -349,7 +349,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
version: "1.6.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
@@ -357,7 +357,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
image_picker:
|
image_picker:
|
||||||
@@ -365,7 +365,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker
|
name: image_picker
|
||||||
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.3"
|
version: "1.2.3"
|
||||||
image_picker_android:
|
image_picker_android:
|
||||||
@@ -373,7 +373,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+19"
|
version: "0.8.13+19"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
@@ -381,7 +381,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_for_web
|
name: image_picker_for_web
|
||||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
@@ -389,7 +389,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+6"
|
version: "0.8.13+6"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
@@ -397,7 +397,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_linux
|
name: image_picker_linux
|
||||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
image_picker_macos:
|
image_picker_macos:
|
||||||
@@ -405,7 +405,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_macos
|
name: image_picker_macos
|
||||||
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2+1"
|
version: "0.2.2+1"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
@@ -413,7 +413,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.1"
|
version: "2.11.1"
|
||||||
image_picker_windows:
|
image_picker_windows:
|
||||||
@@ -421,39 +421,31 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: image_picker_windows
|
name: image_picker_windows
|
||||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
jni:
|
jni:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: jni
|
name: jni
|
||||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.0"
|
||||||
jni_flutter:
|
jni_flutter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: jni_flutter
|
name: jni_flutter
|
||||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.1"
|
||||||
jni_util:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: jni_util
|
|
||||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.0.2"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
@@ -461,7 +453,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.10"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
@@ -469,7 +461,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
@@ -477,7 +469,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
logging:
|
logging:
|
||||||
@@ -485,7 +477,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
@@ -493,7 +485,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
@@ -501,7 +493,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
@@ -509,7 +501,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
@@ -517,7 +509,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
nm:
|
nm:
|
||||||
@@ -525,31 +517,31 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: nm
|
name: nm
|
||||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: objective_c
|
name: objective_c
|
||||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.5.0"
|
version: "9.4.1"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_config
|
name: package_config
|
||||||
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "2.2.0"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
|
sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.1"
|
version: "10.2.1"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
@@ -557,7 +549,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: package_info_plus_platform_interface
|
name: package_info_plus_platform_interface
|
||||||
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
path:
|
path:
|
||||||
@@ -565,7 +557,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
@@ -573,7 +565,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.6"
|
version: "2.1.6"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
@@ -581,7 +573,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.1"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
@@ -589,7 +581,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.6.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
@@ -597,7 +589,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.2"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
@@ -605,7 +597,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
@@ -613,7 +605,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
petitparser:
|
petitparser:
|
||||||
@@ -621,7 +613,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.2"
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
@@ -629,7 +621,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.6"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
@@ -637,7 +629,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: plugin_platform_interface
|
name: plugin_platform_interface
|
||||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
@@ -645,7 +637,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: pub_semver
|
name: pub_semver
|
||||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
record_use:
|
record_use:
|
||||||
@@ -653,7 +645,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: record_use
|
name: record_use
|
||||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
@@ -666,7 +658,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
version: "1.10.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
@@ -674,7 +666,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.12.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
@@ -682,7 +674,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
@@ -690,7 +682,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
@@ -698,7 +690,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
test_api:
|
test_api:
|
||||||
@@ -706,7 +698,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
@@ -714,7 +706,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
uuid:
|
uuid:
|
||||||
@@ -722,7 +714,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.6.0"
|
version: "4.6.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
@@ -730,7 +722,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
@@ -738,15 +730,15 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.2.0"
|
version: "15.2.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
win32:
|
win32:
|
||||||
@@ -754,7 +746,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.0"
|
version: "6.3.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
@@ -762,7 +754,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
xml:
|
xml:
|
||||||
@@ -770,7 +762,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
yaml:
|
yaml:
|
||||||
@@ -778,7 +770,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ dependencies:
|
|||||||
cryptography: ^2.9.0
|
cryptography: ^2.9.0
|
||||||
image_picker: ^1.2.3
|
image_picker: ^1.2.3
|
||||||
geolocator: ^14.0.3
|
geolocator: ^14.0.3
|
||||||
connectivity_plus: ^7.3.1
|
connectivity_plus: ^7.3.0
|
||||||
|
web: ^1.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
1
apps/service_app/web/favicon.svg
Normal file
1
apps/service_app/web/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#bf360c"/><path d="M32 11c10 8 16 16 16 25a16 16 0 1 1-32 0c0-9 6-17 16-25Z" fill="#fff"/><path d="M25 38h14" stroke="#bf360c" stroke-width="4" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
1
apps/service_app/web/icons/app-icon.svg
Normal file
1
apps/service_app/web/icons/app-icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="112" fill="#bf360c"/><path d="M256 88c80 65 128 130 128 204a128 128 0 1 1-256 0c0-74 48-139 128-204Z" fill="#fff"/><path d="M200 310h112" stroke="#bf360c" stroke-width="34" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 301 B |
16
apps/service_app/web/index.html
Normal file
16
apps/service_app/web/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="和气瓶安服务工作台 Web 应用。">
|
||||||
|
<meta name="theme-color" content="#bf360c">
|
||||||
|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<title>和气瓶安服务工作台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
apps/service_app/web/manifest.json
Normal file
17
apps/service_app/web/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "和气瓶安服务工作台",
|
||||||
|
"short_name": "服务工作台",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#bf360c",
|
||||||
|
"description": "和气瓶安服务工作台 Web 应用。",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/app-icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 瓶安芯用户端
|
# 瓶安芯用户端
|
||||||
|
|
||||||
Android/iOS Flutter 客户端。首期只接入 `/heqi/client/v1/user` 的真实能力:首页安全内容与服务归属、商城、订单、合同、工单、钱包、地址和个人资料。
|
Android/iOS/Web Flutter 客户端。首期只接入 `/heqi/client/v1/user` 的真实能力:首页安全内容与服务归属、商城、订单、合同、工单、钱包、地址和个人资料。
|
||||||
|
|
||||||
## 本地运行
|
## 本地运行
|
||||||
|
|
||||||
@@ -18,6 +18,9 @@ flutter analyze
|
|||||||
flutter test
|
flutter test
|
||||||
flutter build apk --debug
|
flutter build apk --debug
|
||||||
flutter build ios --simulator --no-codesign
|
flutter build ios --simulator --no-codesign
|
||||||
|
flutter build web --release --dart-define=API_BASE_URL=https://api.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仓库根目录可使用 `API_BASE_URL=https://api.example.com ./scripts/build-apps.sh user_app all` 同时生成 Android 和 Web 产物。Web 本地调试使用 `flutter run -d chrome --dart-define=API_BASE_URL=http://127.0.0.1:12426`;生产环境必须使用 HTTPS API 地址。
|
||||||
|
|
||||||
充值 Mock 确认、设备控制、收藏、押金、消息和发票均不在 Release UI 暴露。
|
充值 Mock 确认、设备控制、收藏、押金、消息和发票均不在 Release UI 暴露。
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
@@ -51,13 +50,13 @@ class ApiClient {
|
|||||||
bool authenticated = true,
|
bool authenticated = true,
|
||||||
}) async {
|
}) async {
|
||||||
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
||||||
request.headers[HttpHeaders.acceptHeader] = 'application/json';
|
request.headers['accept'] = 'application/json';
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
final token = _tokenProvider();
|
final token = _tokenProvider();
|
||||||
if (token.isNotEmpty) request.headers[HttpHeaders.authorizationHeader] = token;
|
if (token.isNotEmpty) request.headers['authorization'] = token;
|
||||||
}
|
}
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
|
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
||||||
request.body = jsonEncode(body);
|
request.body = jsonEncode(body);
|
||||||
}
|
}
|
||||||
final streamed = await _client.send(request);
|
final streamed = await _client.send(request);
|
||||||
|
|||||||
1
apps/user_app/web/favicon.svg
Normal file
1
apps/user_app/web/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#00695c"/><path d="M32 11c10 8 16 16 16 25a16 16 0 1 1-32 0c0-9 6-17 16-25Z" fill="#fff"/><path d="M25 38h14" stroke="#00695c" stroke-width="4" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
1
apps/user_app/web/icons/app-icon.svg
Normal file
1
apps/user_app/web/icons/app-icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="112" fill="#00695c"/><path d="M256 88c80 65 128 130 128 204a128 128 0 1 1-256 0c0-74 48-139 128-204Z" fill="#fff"/><path d="M200 310h112" stroke="#00695c" stroke-width="34" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 301 B |
16
apps/user_app/web/index.html
Normal file
16
apps/user_app/web/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="和气瓶安用户端 Web 应用。">
|
||||||
|
<meta name="theme-color" content="#00695c">
|
||||||
|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<title>和气瓶安</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
apps/user_app/web/manifest.json
Normal file
17
apps/user_app/web/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "和气瓶安",
|
||||||
|
"short_name": "和气瓶安",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#00695c",
|
||||||
|
"description": "和气瓶安用户端 Web 应用。",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/app-icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -103,9 +103,9 @@
|
|||||||
|
|
||||||
### 7.1 平台、工程与原型边界
|
### 7.1 平台、工程与原型边界
|
||||||
|
|
||||||
- 用户端只交付 Android、iOS,不建设 Flutter Web、桌面端或小程序兼容层。平台差异通过适配器隔离,不在业务页面散落 `Platform.isAndroid`、`Platform.isIOS` 判断。
|
- 用户端交付 Android、iOS、Web;Web 作为浏览器入口,不延伸为桌面端或小程序兼容层。平台差异通过适配器隔离,不在业务页面散落 `Platform.isAndroid`、`Platform.isIOS` 判断。Web 生产部署必须使用 HTTPS,并通过 `--dart-define=API_BASE_URL=...` 注入 API 地址。
|
||||||
- 生产工程按规划放在 `apps/user_app`;当前 `ui` 目录是基于产品设计图制作的交互原型,仅用于视觉、信息架构和流程确认,不得把其中的演示数据或模拟成功状态当作业务实现。
|
- 生产工程按规划放在 `apps/user_app`;当前 `ui` 目录是基于产品设计图制作的交互原型,仅用于视觉、信息架构和流程确认,不得把其中的演示数据或模拟成功状态当作业务实现。
|
||||||
- Flutter 与 Dart 版本由工程根目录的版本管理文件和 CI 固定;升级 SDK、Gradle、Kotlin、Xcode、CocoaPods 或插件时必须单独验证 Android/iOS 构建、权限和深链。
|
- Flutter 与 Dart 版本由工程根目录的版本管理文件和 CI 固定;升级 SDK、Gradle、Kotlin、Xcode、CocoaPods 或插件时必须单独验证 Android/iOS/Web 构建、权限和深链。
|
||||||
- 应用令牌的 client claim 固定为 `user_app`,API 根路径固定为 `/heqi/client/v1/user`;不得复用 `service_app` 或任何管理后台会话。
|
- 应用令牌的 client claim 固定为 `user_app`,API 根路径固定为 `/heqi/client/v1/user`;不得复用 `service_app` 或任何管理后台会话。
|
||||||
|
|
||||||
### 7.2 分层结构与依赖方向
|
### 7.2 分层结构与依赖方向
|
||||||
@@ -163,7 +163,7 @@ apps/user_app/lib/
|
|||||||
|
|
||||||
### 7.5 本地数据、安全与平台能力
|
### 7.5 本地数据、安全与平台能力
|
||||||
|
|
||||||
- 访问令牌、刷新令牌和支付相关临时凭据只进入 Android Keystore / iOS Keychain 支持的安全存储;日志、崩溃报告、埋点和剪贴板不得记录令牌、支付密码、完整手机号、地址或定位。
|
- 访问令牌、刷新令牌和支付相关临时凭据只进入 Android Keystore / iOS Keychain 支持的安全存储,Web 仅可在 HTTPS 域名使用浏览器安全存储;日志、崩溃报告、埋点和剪贴板不得记录令牌、支付密码、完整手机号、地址或定位。
|
||||||
- 普通缓存只保存可恢复数据并设置版本与过期时间。安全事件、资金、订单和设备命令的服务端事实不能由本地缓存覆盖;退出登录时按数据分类清理。
|
- 普通缓存只保存可恢复数据并设置版本与过期时间。安全事件、资金、订单和设备命令的服务端事实不能由本地缓存覆盖;退出登录时按数据分类清理。
|
||||||
- Android/iOS 的相机、相册、蓝牙、定位、通知权限均采用使用时申请和拒绝后降级。定位失败提供手动地址入口;蓝牙失败提供扫码或手动设备码入口。
|
- Android/iOS 的相机、相册、蓝牙、定位、通知权限均采用使用时申请和拒绝后降级。定位失败提供手动地址入口;蓝牙失败提供扫码或手动设备码入口。
|
||||||
- 推送点击必须先恢复会话并重新向服务端读取对象状态;通知载荷不得包含完整地址、手机号、支付信息或可直接执行设备控制的凭证。
|
- 推送点击必须先恢复会话并重新向服务端读取对象状态;通知载荷不得包含完整地址、手机号、支付信息或可直接执行设备控制的凭证。
|
||||||
@@ -174,4 +174,4 @@ apps/user_app/lib/
|
|||||||
- 设计基线以 `doc/用户端APP-产品设计` 和 `ui/?app=user` 为准:安全蓝为主色,瓶阀状态、告警与命令回执优先于营销内容。危险、警告、成功不能只靠颜色表达。
|
- 设计基线以 `doc/用户端APP-产品设计` 和 `ui/?app=user` 为准:安全蓝为主色,瓶阀状态、告警与命令回执优先于营销内容。危险、警告、成功不能只靠颜色表达。
|
||||||
- 使用统一 ThemeExtension 管理颜色、圆角、间距、阴影和状态色;正文最小字号、动态字体缩放、44×44 logical pixels 触控目标、屏幕阅读器语义和对比度必须在 Android/iOS 真机验证。
|
- 使用统一 ThemeExtension 管理颜色、圆角、间距、阴影和状态色;正文最小字号、动态字体缩放、44×44 logical pixels 触控目标、屏幕阅读器语义和对比度必须在 Android/iOS 真机验证。
|
||||||
- ViewModel、Use Case、Repository 覆盖单元测试;瓶阀状态、支付、登录守卫和错误恢复覆盖 Widget 测试;扫码绑定、开关阀回执、下单支付、订单轨迹覆盖集成测试。
|
- ViewModel、Use Case、Repository 覆盖单元测试;瓶阀状态、支付、登录守卫和错误恢复覆盖 Widget 测试;扫码绑定、开关阀回执、下单支付、订单轨迹覆盖集成测试。
|
||||||
- 每次合并至少执行 `flutter analyze`、`flutter test`、Android debug 构建和 iOS Simulator 构建;涉及相机、蓝牙、推送、支付、Universal Links/App Links 的改动还须执行对应真机回归。
|
- 每次合并至少执行 `flutter analyze`、`flutter test`、Android debug 构建、iOS Simulator 构建和 Web release 构建;涉及相机、蓝牙、推送、支付、Universal Links/App Links 的改动还须执行对应真机或浏览器回归。
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
|
|
||||||
### 9.1 平台、角色与工程边界
|
### 9.1 平台、角色与工程边界
|
||||||
|
|
||||||
- 服务人员端只交付 Android、iOS。生产工程按规划放在 `apps/service_app`;当前 `ui` 目录是配送、安装维修、安检三类原型的交互实现,不连接真实定位、相机、蓝牙、支付或业务 API。
|
- 服务人员端交付 Android、iOS、Web。生产工程按规划放在 `apps/service_app`;Web 只支持浏览器前台授权的位置与相机能力,不作为后台定位或后台任务的替代方案。当前 `ui` 目录是配送、安装维修、安检三类原型的交互实现,不连接真实定位、相机、蓝牙、支付或业务 API。
|
||||||
- 三类岗位共用一个 Flutter 应用和登录体系,每个账号仅有一个服务岗位,令牌 client claim 固定为 `service_app` 并携带唯一 `role_code`。岗位变更必须由后台完成并重新登录换取令牌;客户端不提供角色切换。
|
- 三类岗位共用一个 Flutter 应用和登录体系,每个账号仅有一个服务岗位,令牌 client claim 固定为 `service_app` 并携带唯一 `role_code`。岗位变更必须由后台完成并重新登录换取令牌;客户端不提供角色切换。
|
||||||
- 工作人员 API 根路径固定为 `/heqi/client/v1/staff`。客户端不得访问用户端、平台后台、气站后台或配送点后台的令牌与接口。
|
- 工作人员 API 根路径固定为 `/heqi/client/v1/staff`。客户端不得访问用户端、平台后台、气站后台或配送点后台的令牌与接口。
|
||||||
- 首期后端不提供工作人员自助注册时,Flutter 注册页面不得伪造成功;若未来开放申请,只能创建待审核账户,不能直接授予岗位能力。
|
- 首期后端不提供工作人员自助注册时,Flutter 注册页面不得伪造成功;若未来开放申请,只能创建待审核账户,不能直接授予岗位能力。
|
||||||
@@ -203,6 +203,8 @@ apps/service_app/lib/
|
|||||||
|
|
||||||
### 9.6 Android/iOS 平台适配
|
### 9.6 Android/iOS 平台适配
|
||||||
|
|
||||||
|
Web 生产部署必须使用 HTTPS,并通过 `--dart-define=API_BASE_URL=...` 注入 API 地址。浏览器草稿和附件只保存 AES-GCM 密文;不支持 Android/iOS 的持续后台定位与后台任务保障。
|
||||||
|
|
||||||
| 能力 | Android | iOS |
|
| 能力 | Android | iOS |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 相机/相册 | 运行时按用途申请 Camera/Photo Picker,使用系统选择器优先 | 使用相机和 Photos 限定访问,解释用途并处理 Limited 状态 |
|
| 相机/相册 | 运行时按用途申请 Camera/Photo Picker,使用系统选择器优先 | 使用相机和 Photos 限定访问,解释用途并处理 Limited 状态 |
|
||||||
@@ -221,4 +223,4 @@ apps/service_app/lib/
|
|||||||
- 任务详情优先展示任务号、类型、预约/SLA、地址、脱敏联系人、风险与允许动作。固定底部操作按钮不得遮挡检查项、签名或系统安全区。
|
- 任务详情优先展示任务号、类型、预约/SLA、地址、脱敏联系人、风险与允许动作。固定底部操作按钮不得遮挡检查项、签名或系统安全区。
|
||||||
- 长清单使用惰性列表和分段保存;照片视频缩略图解码、压缩和上传移出 UI isolate。后台轨迹、上传和补传必须受电量、网络与系统调度约束,不用常驻无限循环。
|
- 长清单使用惰性列表和分段保存;照片视频缩略图解码、压缩和上传移出 UI isolate。后台轨迹、上传和补传必须受电量、网络与系统调度约束,不用常驻无限循环。
|
||||||
- ViewModel、Use Case、Repository、离线队列和冲突处理覆盖单元测试;角色/组织守卫、步骤前置、风险结论和弱网状态覆盖 Widget 测试;三角色主闭环覆盖 Android/iOS 集成测试。
|
- ViewModel、Use Case、Repository、离线队列和冲突处理覆盖单元测试;角色/组织守卫、步骤前置、风险结论和弱网状态覆盖 Widget 测试;三角色主闭环覆盖 Android/iOS 集成测试。
|
||||||
- 每次合并至少执行 `flutter analyze`、`flutter test`、Android debug 构建和 iOS Simulator 构建;定位、相机、蓝牙、推送、后台任务、App Links/Universal Links 和安全存储改动必须真机回归。
|
- 每次合并至少执行 `flutter analyze`、`flutter test`、Android debug 构建、iOS Simulator 构建和 Web release 构建;定位、相机、蓝牙、推送、后台任务、App Links/Universal Links 和安全存储改动必须真机或浏览器回归。
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ flowchart LR
|
|||||||
|
|
||||||
### 4.2 Flutter 工程落地基线
|
### 4.2 Flutter 工程落地基线
|
||||||
|
|
||||||
- 生产工程已落在 `apps/user_app` 与 `apps/service_app`,只生成 Android、iOS 平台目录;`ui` 继续作为视觉和交互原型,不作为运行时依赖。
|
- 生产工程已落在 `apps/user_app` 与 `apps/service_app`,生成 Android、iOS、Web 平台目录;`ui` 继续作为视觉和交互原型,不作为运行时依赖。Web 构建通过仓库根目录 `scripts/build-apps.sh` 输出,生产环境必须注入 HTTPS `API_BASE_URL`。
|
||||||
- 两个 App 使用 `MaterialApp.router`、`go_router`、MVVM + Repository 与注入的平台 Service。HTTP 根地址通过 `--dart-define=API_BASE_URL=...` 注入;用户端和工作人员端分别固定访问 `/heqi/client/v1/user` 与 `/heqi/client/v1/staff`,JWT 请求头沿用当前服务端原始令牌契约。
|
- 两个 App 使用 `MaterialApp.router`、`go_router`、MVVM + Repository 与注入的平台 Service。HTTP 根地址通过 `--dart-define=API_BASE_URL=...` 注入;用户端和工作人员端分别固定访问 `/heqi/client/v1/user` 与 `/heqi/client/v1/staff`,JWT 请求头沿用当前服务端原始令牌契约。
|
||||||
- 访问令牌保存在 Android Keystore / iOS Keychain 支持的安全存储。服务端 App 的现场草稿和附件按账号使用 AES-GCM 加密;恢复网络后才上传并执行最终业务提交。
|
- 访问令牌保存在 Android Keystore / iOS Keychain 支持的安全存储;Web 使用浏览器安全存储,且只能部署在 HTTPS 域名。服务端 App 的现场草稿和附件按账号使用 AES-GCM 加密;Web 仅保存密文,恢复网络后才上传并执行最终业务提交。浏览器端不承担 Android/iOS 的后台定位保障。
|
||||||
- 充值 Mock 确认只允许 Debug/开发联调,Release UI 不注册该入口;未落地的设备控制、收藏、押金、消息和发票能力不得以静态成功状态替代。
|
- 充值 Mock 确认只允许 Debug/开发联调,Release UI 不注册该入口;未落地的设备控制、收藏、押金、消息和发票能力不得以静态成功状态替代。
|
||||||
|
|
||||||
## 5. 研发目录规划(建议)
|
## 5. 研发目录规划(建议)
|
||||||
|
|||||||
90
scripts/build-apps.sh
Normal file
90
scripts/build-apps.sh
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
OUTPUT_ROOT="${PROJECT_ROOT}/output/apps"
|
||||||
|
TARGET="${1:-all}"
|
||||||
|
PLATFORM="${2:-all}"
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
local command_name="$1"
|
||||||
|
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||||
|
echo "缺少命令:${command_name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_value() {
|
||||||
|
local value_name="$1"
|
||||||
|
local value="${!value_name:-}"
|
||||||
|
if [[ -z "${value}" ]]; then
|
||||||
|
echo "缺少环境变量:${value_name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_output() {
|
||||||
|
local source="$1"
|
||||||
|
local target="$2"
|
||||||
|
if [[ "${target}" != "${OUTPUT_ROOT}/"* ]]; then
|
||||||
|
echo "拒绝清理非预期输出目录:${target}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf "${target}"
|
||||||
|
mkdir -p "${target}"
|
||||||
|
cp -R "${source}/." "${target}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_app() {
|
||||||
|
local app_name="$1"
|
||||||
|
local app_dir="${PROJECT_ROOT}/apps/${app_name}"
|
||||||
|
local app_output="${OUTPUT_ROOT}/${app_name}"
|
||||||
|
|
||||||
|
echo "开始构建:${app_name}"
|
||||||
|
(
|
||||||
|
cd "${app_dir}"
|
||||||
|
flutter pub get
|
||||||
|
flutter analyze
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
if [[ "${PLATFORM}" == "all" || "${PLATFORM}" == "android" ]]; then
|
||||||
|
flutter build apk --release --no-pub --dart-define="API_BASE_URL=${API_BASE_URL}"
|
||||||
|
mkdir -p "${app_output}"
|
||||||
|
cp "build/app/outputs/flutter-apk/app-release.apk" "${app_output}/${app_name}-release.apk"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${PLATFORM}" == "all" || "${PLATFORM}" == "web" ]]; then
|
||||||
|
flutter build web --release --no-pub --dart-define="API_BASE_URL=${API_BASE_URL}"
|
||||||
|
copy_output "${app_dir}/build/web" "${app_output}/web"
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
echo "构建完成:${app_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${TARGET}" != "all" && "${TARGET}" != "user_app" && "${TARGET}" != "service_app" ]]; then
|
||||||
|
echo "应用参数只能是 all、user_app 或 service_app。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "${PLATFORM}" != "all" && "${PLATFORM}" != "android" && "${PLATFORM}" != "web" ]]; then
|
||||||
|
echo "平台参数只能是 all、android 或 web。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_command flutter
|
||||||
|
require_value API_BASE_URL
|
||||||
|
|
||||||
|
if [[ "${PLATFORM}" == "all" || "${PLATFORM}" == "web" ]]; then
|
||||||
|
flutter config --enable-web
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${OUTPUT_ROOT}"
|
||||||
|
if [[ "${TARGET}" == "all" || "${TARGET}" == "user_app" ]]; then
|
||||||
|
build_app user_app
|
||||||
|
fi
|
||||||
|
if [[ "${TARGET}" == "all" || "${TARGET}" == "service_app" ]]; then
|
||||||
|
build_app service_app
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "产物目录:${OUTPUT_ROOT}"
|
||||||
Reference in New Issue
Block a user