feat: add Flutter web build support

This commit is contained in:
2026-08-02 01:30:07 +08:00
parent 98bf7ed6a9
commit f0b8af0bc8
24 changed files with 708 additions and 329 deletions

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
@@ -42,14 +40,19 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
? widget.repository.deliveryDetail(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);
try {
await action(item);
setState(() => _future = _load());
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString())));
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
}
} finally {
if (mounted) setState(() => _busy = false);
@@ -68,7 +71,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
decoration: const InputDecoration(labelText: '原因'),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.pop(context, controller.text.trim()),
child: const Text('确认'),
@@ -81,7 +87,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
}
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;
String? sealedName;
if (existing?['kind'] == 'delivery_receipt' && existing?['sealed_name'] is String) {
@@ -91,25 +100,34 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
title: const Text('发现未提交签收草稿'),
content: const Text('是否继续提交上次加密保存的签收凭证?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('重新拍摄')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('继续提交')),
TextButton(
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 (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;
sealedName = await widget.drafts.sealAttachment(
accountIdentity: widget.session.identity,
taskIdentity: item.identity,
stage: 'receipt',
sourcePath: image.path,
source: image,
);
}
setState(() => _busy = true);
String? temporaryPath;
XFile? temporaryFile;
try {
await widget.drafts.saveDraft(
accountIdentity: widget.session.identity,
@@ -122,7 +140,7 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
'captured_at': DateTime.now().toUtc().toIso8601String(),
},
);
temporaryPath = await widget.drafts.materializeAttachment(
temporaryFile = await widget.drafts.materializeAttachment(
accountIdentity: widget.session.identity,
sealedName: sealedName,
);
@@ -130,7 +148,7 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
identity: item.identity,
recipientName: item.raw['contact_name'] as String? ?? '收货人',
recipientPhone: item.raw['contact_phone'] as String? ?? '',
proofFile: temporaryPath,
proofFile: temporaryFile,
);
await widget.drafts.deleteDraft(widget.session.identity, item.identity);
if (mounted) setState(() => _future = _load());
@@ -141,9 +159,8 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
).showSnackBar(SnackBar(content: Text('签收提交失败,加密草稿已保留:$error')));
}
} finally {
if (temporaryPath != null) {
final file = File(temporaryPath);
if (file.existsSync()) await file.delete();
if (temporaryFile != null) {
await widget.drafts.discardMaterializedAttachment(temporaryFile);
}
if (mounted) setState(() => _busy = false);
}
@@ -170,7 +187,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.number, style: const TextStyle(color: Colors.white70)),
Text(
item.number,
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 8),
Text(
item.title,
@@ -206,7 +226,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
onPressed: _busy
? null
: () => _run(
(value) => widget.repository.start(value, widget.session.roleCode),
(value) => widget.repository.start(
value,
widget.session.roleCode,
),
item,
),
child: const Text('开始处理'),
@@ -216,7 +239,9 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
onPressed: _busy
? null
: () => _run(
(value) => widget.repository.appendCurrentTrack(value.identity),
(value) => widget.repository.appendCurrentTrack(
value.identity,
),
item,
),
child: const Text('上报当前位置'),
@@ -225,7 +250,10 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
ElevatedButton(
onPressed: _busy
? null
: () => _run((value) => widget.repository.arrive(value.identity), item),
: () => _run(
(value) => widget.repository.arrive(value.identity),
item,
),
child: const Text('到达并校验围栏'),
),
if (item.allowedActions.contains('submit_receipt'))
@@ -253,8 +281,11 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
final reason = await _reason();
if (reason != null && reason.isNotEmpty) {
await _run(
(value) =>
widget.repository.exception(value, widget.session.roleCode, reason),
(value) => widget.repository.exception(
value,
widget.session.roleCode,
reason,
),
item,
);
}
@@ -269,8 +300,11 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
final reason = await _reason();
if (reason != null && reason.isNotEmpty) {
await _run(
(value) =>
widget.repository.recover(value, widget.session.roleCode, reason),
(value) => widget.repository.recover(
value,
widget.session.roleCode,
reason,
),
item,
);
}