feat(apps): establish shared mobile design system

This commit is contained in:
2026-08-10 23:31:44 +08:00
parent 7242048abf
commit a2383fc8a1
34 changed files with 2007 additions and 751 deletions

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
import '../../../data/repositories/service_repository.dart';
import '../../../domain/models/service_models.dart';
import '../../core/widgets.dart';
class PreflightPage extends StatefulWidget {
const PreflightPage({required this.session, required this.repository, super.key});
@@ -46,84 +47,60 @@ class _PreflightPageState extends State<PreflightPage> {
future: _future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.hasError) return Center(child: Text(snapshot.error.toString()));
if (snapshot.hasError) {
return MessageState(
title: '作业检查加载失败',
description: '请检查网络后重新加载',
onRetry: () => setState(() => _future = widget.repository.preflight()),
);
}
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),
final entries = result.checks.entries.toList();
return SafeArea(
top: false,
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
children: [
_PreflightBanner(result: result),
const SizedBox(height: 28),
SectionHeader(
title: '准入检查',
description: result.canWork ? '所有必需条件均已通过服务端校验' : '完成阻断项后才能进入工作台',
),
const SizedBox(height: 12),
SurfaceSection(
padding: EdgeInsets.zero,
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,
),
),
for (var index = 0; index < entries.length; index++) ...[
_CheckRow(entry: entries[index], label: _label(entries[index].key)),
if (index < entries.length - 1) const Divider(indent: 56),
],
],
),
),
),
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: 28),
if (result.workStatus != 'on_duty')
FilledButton.icon(
onPressed: _busy ? null : () => _attendance('clock_in'),
icon: const Icon(Icons.location_on_outlined),
label: Text(_busy ? '正在定位并打卡…' : '定位并上班打卡'),
)
else ...[
FilledButton(
onPressed: result.canWork ? () => context.go('/work') : null,
child: const Text('进入工作台'),
),
);
}),
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('下班打卡'),
),
const SizedBox(height: 8),
TextButton(
onPressed: _busy ? null : () => _attendance('clock_out'),
child: const Text('下班打卡'),
),
],
],
],
),
);
},
),
@@ -141,3 +118,83 @@ class _PreflightPageState extends State<PreflightPage> {
_ => value,
};
}
class _PreflightBanner extends StatelessWidget {
const _PreflightBanner({required this.result});
final PreflightResult result;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final background = result.canWork ? scheme.secondaryContainer : scheme.tertiaryContainer;
final foreground = result.canWork ? scheme.onSecondaryContainer : scheme.onTertiaryContainer;
return Semantics(
liveRegion: true,
label: result.canWork ? '作业检查通过,可以开始今日作业' : '作业检查未通过,仍有前置条件未完成',
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(20),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
result.canWork ? Icons.verified_rounded : Icons.warning_amber_rounded,
color: foreground,
size: 32,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
roleName(result.roleCode),
style: Theme.of(context).textTheme.labelLarge?.copyWith(color: foreground),
),
const SizedBox(height: 6),
Text(
result.canWork ? '可以开始今日作业' : '仍有前置条件未完成',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: foreground),
),
],
),
),
],
),
),
);
}
}
class _CheckRow extends StatelessWidget {
const _CheckRow({required this.entry, required this.label});
final MapEntry<String, Object?> entry;
final String label;
@override
Widget build(BuildContext context) {
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';
final tone = passed
? StatusTone.success
: status == 'not_configured'
? StatusTone.warning
: StatusTone.danger;
final text = status == 'not_configured'
? '平台暂未启用'
: passed
? '已通过'
: '未通过';
return ListTile(
leading: Icon(passed ? Icons.check_circle_outline : Icons.info_outline_rounded),
title: Text(label),
trailing: StatusPill(label: text, tone: tone),
);
}
}