feat(apps): establish shared mobile design system
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user