feat: add Flutter mobile clients and staff delivery API

This commit is contained in:
david
2026-07-30 21:47:41 +08:00
parent 550efb3812
commit 36a5ced1c0
228 changed files with 17159 additions and 22 deletions

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
class PreflightPage extends StatefulWidget {
const PreflightPage({required this.session, required this.repository, super.key});
final StaffSession session;
final ServiceRepository repository;
@override
State<PreflightPage> createState() => _PreflightPageState();
}
class _PreflightPageState extends State<PreflightPage> {
late Future<PreflightResult> _future;
bool _busy = false;
@override
void initState() {
super.initState();
_future = widget.repository.preflight();
}
Future<void> _attendance(String action) async {
setState(() => _busy = true);
try {
await widget.repository.attendance(action, widget.session.deviceIdentity);
setState(() => _future = widget.repository.preflight());
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString())));
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('作业前检查')),
body: FutureBuilder<PreflightResult>(
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.hasError) return Center(child: Text(snapshot.error.toString()));
return const Center(child: CircularProgressIndicator());
}
final result = snapshot.data!;
return ListView(
padding: const EdgeInsets.all(18),
children: [
Card(
color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(roleName(result.roleCode), style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 6),
Text(
result.canWork ? '可以开始今日作业' : '仍有前置条件未完成',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 24,
),
),
],
),
),
),
const SizedBox(height: 12),
...result.checks.entries.map((entry) {
final detail = entry.value is Map
? Map<Object?, Object?>.from(entry.value as Map)
: const <Object?, Object?>{};
final status = detail['status']?.toString() ?? 'blocked';
final passed = status == 'passed';
return Card(
child: ListTile(
leading: Icon(
passed
? Icons.check_circle
: status == 'not_configured'
? Icons.info_outline
: Icons.cancel,
color: passed
? Colors.green
: status == 'not_configured'
? Colors.orange
: Colors.red,
),
title: Text(_label(entry.key)),
subtitle: Text(
status == 'not_configured'
? '平台暂未启用,不冒充校验通过'
: passed
? '已通过服务端校验'
: '未通过',
),
),
);
}),
const SizedBox(height: 18),
if (result.workStatus != 'on_duty')
ElevatedButton.icon(
onPressed: _busy ? null : () => _attendance('clock_in'),
icon: const Icon(Icons.location_on),
label: const Text('定位并上班打卡'),
)
else ...[
ElevatedButton(
onPressed: result.canWork ? () => context.go('/work') : null,
child: const Text('进入工作台'),
),
TextButton(
onPressed: _busy ? null : () => _attendance('clock_out'),
child: const Text('下班打卡'),
),
],
],
);
},
),
);
String _label(String value) => switch (value) {
'account' => '账号状态',
'role' => '岗位',
'organization' => '所属组织',
'credential' => '人员资质',
'attendance' => '上班状态',
'daily_training' => '每日培训',
'service_area' => '服务区域',
'authorized_device' => '授权设备',
_ => value,
};
}