feat(apps): establish shared mobile design system
This commit is contained in:
@@ -7,6 +7,7 @@ import '../../../app/dependencies.dart';
|
||||
import '../../../data/offline/encrypted_draft_store.dart';
|
||||
import '../../../data/repositories/service_repository.dart';
|
||||
import '../../../domain/models/service_models.dart';
|
||||
import '../../core/widgets.dart';
|
||||
|
||||
class WorkDetailPage extends StatefulWidget {
|
||||
const WorkDetailPage({
|
||||
@@ -173,150 +174,183 @@ class _WorkDetailPageState extends State<WorkDetailPage> {
|
||||
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 = _load()),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final item = snapshot.data!;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
children: [
|
||||
Card(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(22),
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.number,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w900,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Chip(label: Text(statusName(item.status))),
|
||||
const SizedBox(height: 16),
|
||||
StatusPill(label: statusName(item.status), tone: StatusTone.info),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.location_on_outlined),
|
||||
title: const Text('服务地址'),
|
||||
subtitle: Text(item.address.isEmpty ? '未提供地址' : item.address),
|
||||
const SizedBox(height: 28),
|
||||
const SectionHeader(title: '服务信息'),
|
||||
const SizedBox(height: 12),
|
||||
SurfaceSection(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on_outlined),
|
||||
title: const Text('服务地址'),
|
||||
subtitle: Text(item.address.isEmpty ? '未提供地址' : item.address),
|
||||
),
|
||||
const Divider(indent: 56),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(item.raw['contact_name'] as String? ?? '服务用户'),
|
||||
subtitle: Text(item.raw['contact_phone'] as String? ?? '未提供联系电话'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(item.raw['contact_name'] as String? ?? '服务用户'),
|
||||
subtitle: Text(item.raw['contact_phone'] as String? ?? ''),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
if (item.allowedActions.contains('start'))
|
||||
ElevatedButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
(value) => widget.repository.start(
|
||||
value,
|
||||
widget.session.roleCode,
|
||||
const SizedBox(height: 28),
|
||||
const SectionHeader(title: '任务操作', description: '操作结果以服务端状态与现场凭证为准'),
|
||||
const SizedBox(height: 12),
|
||||
if (item.allowedActions.isEmpty) const SurfaceSection(child: Text('当前状态没有可执行操作')),
|
||||
if (item.allowedActions.contains('start'))
|
||||
FilledButton.icon(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
(value) => widget.repository.start(value, widget.session.roleCode),
|
||||
item,
|
||||
),
|
||||
item,
|
||||
),
|
||||
child: const Text('开始处理'),
|
||||
),
|
||||
if (item.allowedActions.contains('append_tracks'))
|
||||
OutlinedButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
(value) => widget.repository.appendCurrentTrack(
|
||||
value.identity,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text('开始处理'),
|
||||
),
|
||||
if (item.allowedActions.contains('append_tracks')) ...[
|
||||
if (item.allowedActions.contains('start')) const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
(value) => widget.repository.appendCurrentTrack(value.identity),
|
||||
item,
|
||||
),
|
||||
item,
|
||||
),
|
||||
child: const Text('上报当前位置'),
|
||||
),
|
||||
if (item.allowedActions.contains('arrive'))
|
||||
ElevatedButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run(
|
||||
(value) => widget.repository.arrive(value.identity),
|
||||
item,
|
||||
),
|
||||
child: const Text('到达并校验围栏'),
|
||||
),
|
||||
if (item.allowedActions.contains('submit_receipt'))
|
||||
ElevatedButton(
|
||||
onPressed: _busy ? null : () => _receipt(item),
|
||||
child: const Text('拍摄签收凭证并提交'),
|
||||
),
|
||||
if (item.allowedActions.contains('submit_result'))
|
||||
ElevatedButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final changed = await context.push<bool>(
|
||||
'/tasks/${item.identity}/evidence',
|
||||
);
|
||||
if (changed == true) setState(() => _future = _load());
|
||||
},
|
||||
child: const Text('现场取证与提交'),
|
||||
),
|
||||
if (item.allowedActions.contains('exception'))
|
||||
TextButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final reason = await _reason();
|
||||
if (reason != null && reason.isNotEmpty) {
|
||||
await _run(
|
||||
(value) => widget.repository.exception(
|
||||
value,
|
||||
widget.session.roleCode,
|
||||
reason,
|
||||
),
|
||||
item,
|
||||
icon: const Icon(Icons.my_location_outlined),
|
||||
label: const Text('上报当前位置'),
|
||||
),
|
||||
],
|
||||
if (item.allowedActions.contains('arrive')) ...[
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () => _run((value) => widget.repository.arrive(value.identity), item),
|
||||
icon: const Icon(Icons.location_on_outlined),
|
||||
label: const Text('到达并校验围栏'),
|
||||
),
|
||||
],
|
||||
if (item.allowedActions.contains('submit_receipt')) ...[
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: _busy ? null : () => _receipt(item),
|
||||
icon: const Icon(Icons.photo_camera_outlined),
|
||||
label: const Text('拍摄签收凭证并提交'),
|
||||
),
|
||||
],
|
||||
if (item.allowedActions.contains('submit_result')) ...[
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final changed = await context.push<bool>(
|
||||
'/tasks/${item.identity}/evidence',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('标记异常'),
|
||||
),
|
||||
if (item.allowedActions.contains('recover'))
|
||||
ElevatedButton(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final reason = await _reason();
|
||||
if (reason != null && reason.isNotEmpty) {
|
||||
await _run(
|
||||
(value) => widget.repository.recover(
|
||||
value,
|
||||
widget.session.roleCode,
|
||||
reason,
|
||||
),
|
||||
item,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('恢复任务'),
|
||||
),
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
if (changed == true) setState(() => _future = _load());
|
||||
},
|
||||
icon: const Icon(Icons.fact_check_outlined),
|
||||
label: const Text('现场取证与提交'),
|
||||
),
|
||||
],
|
||||
if (item.allowedActions.contains('recover')) ...[
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final reason = await _reason();
|
||||
if (reason != null && reason.isNotEmpty) {
|
||||
await _run(
|
||||
(value) =>
|
||||
widget.repository.recover(value, widget.session.roleCode, reason),
|
||||
item,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.restart_alt_rounded),
|
||||
label: const Text('恢复任务'),
|
||||
),
|
||||
],
|
||||
if (item.allowedActions.contains('exception')) ...[
|
||||
const SizedBox(height: 20),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
|
||||
onPressed: _busy
|
||||
? null
|
||||
: () async {
|
||||
final reason = await _reason();
|
||||
if (reason != null && reason.isNotEmpty) {
|
||||
await _run(
|
||||
(value) => widget.repository.exception(
|
||||
value,
|
||||
widget.session.roleCode,
|
||||
reason,
|
||||
),
|
||||
item,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.report_problem_outlined),
|
||||
label: const Text('标记异常'),
|
||||
),
|
||||
],
|
||||
if (_busy)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Semantics(
|
||||
liveRegion: true,
|
||||
label: '正在处理任务操作',
|
||||
child: const LinearProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
import 'work_list_view_model.dart';
|
||||
|
||||
class WorkListPage extends StatefulWidget {
|
||||
@@ -53,59 +54,88 @@ class _WorkListPageState extends State<WorkListPage> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_viewModel.error != null && _viewModel.items.isEmpty) {
|
||||
return Center(child: Text(_viewModel.error.toString()));
|
||||
return MessageState(
|
||||
title: '任务加载失败',
|
||||
description: '请检查网络后重新加载',
|
||||
onRetry: _viewModel.load,
|
||||
);
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: _viewModel.load,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 28),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||||
children: [
|
||||
Card(
|
||||
color: widget.session.roleCode == 'operations'
|
||||
? const Color(0xFFE9F8F0)
|
||||
: const Color(0xFFEEEAFE),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(
|
||||
widget.session.roleCode == 'delivery'
|
||||
? Icons.local_shipping
|
||||
? Icons.local_shipping_outlined
|
||||
: widget.session.roleCode == 'installer'
|
||||
? Icons.handyman
|
||||
: Icons.health_and_safety,
|
||||
size: 38,
|
||||
? Icons.handyman_outlined
|
||||
: Icons.health_and_safety_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.completed ? '服务端确认完成的历史记录' : '仅展示服务端分派给本账号的任务',
|
||||
style: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.completed ? '作业历史' : '${roleName(widget.session.roleCode)}任务',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.completed ? '服务端确认完成的历史记录' : '仅展示服务端分派给本账号的任务',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
SectionHeader(
|
||||
title: widget.completed ? '全部记录' : '待处理任务',
|
||||
description: _viewModel.items.isEmpty
|
||||
? '当前没有需要展示的任务'
|
||||
: '共 ${_viewModel.items.length} 项',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_viewModel.items.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(42),
|
||||
child: Center(child: Text('暂无任务')),
|
||||
)
|
||||
const MessageState(title: '暂无任务', description: '新的服务端分派任务会显示在这里')
|
||||
else
|
||||
..._viewModel.items.map(
|
||||
(item) => Card(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
title: Text(item.title, style: const TextStyle(fontWeight: FontWeight.w800)),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('${item.number}\n${item.address}'),
|
||||
),
|
||||
trailing: Chip(label: Text(statusName(item.status))),
|
||||
onTap: () => context.push('/tasks/${item.identity}'),
|
||||
),
|
||||
SurfaceSection(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < _viewModel.items.length; index++) ...[
|
||||
_TaskRow(
|
||||
item: _viewModel.items[index],
|
||||
onTap: () => context.push('/tasks/${_viewModel.items[index].identity}'),
|
||||
),
|
||||
if (index < _viewModel.items.length - 1)
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -115,3 +145,28 @@ class _WorkListPageState extends State<WorkListPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _TaskRow extends StatelessWidget {
|
||||
const _TaskRow({required this.item, required this.onTap});
|
||||
final WorkItem item;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
minTileHeight: 88,
|
||||
title: Text(item.title, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
titleTextStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
'${item.number}${item.address.isEmpty ? '' : ' · ${item.address}'}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
trailing: StatusPill(label: statusName(item.status), tone: StatusTone.info),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user