Skeleton loading

This commit is contained in:
2026-02-28 21:47:24 +08:00
parent c5e859ad88
commit d3239d8c76
9 changed files with 2755 additions and 2285 deletions
+141 -86
View File
@@ -10,6 +10,8 @@ import '../../providers/profile_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../providers/realtime_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/status_pill.dart';
@@ -294,97 +296,150 @@ class DashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveBody(
child: LayoutBuilder(
builder: (context, constraints) {
final sections = <Widget>[
const SizedBox(height: 16),
_sectionTitle(context, 'IT Staff Pulse'),
const _StaffTable(),
const SizedBox(height: 20),
_sectionTitle(context, 'Core Daily KPIs'),
_cardGrid(context, [
_MetricCard(
title: 'New tickets today',
valueBuilder: (metrics) => metrics.newTicketsToday.toString(),
),
_MetricCard(
title: 'Closed today',
valueBuilder: (metrics) => metrics.closedToday.toString(),
),
_MetricCard(
title: 'Open tickets',
valueBuilder: (metrics) => metrics.openTickets.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'Task Flow'),
_cardGrid(context, [
_MetricCard(
title: 'Tasks created',
valueBuilder: (metrics) => metrics.tasksCreatedToday.toString(),
),
_MetricCard(
title: 'Tasks completed',
valueBuilder: (metrics) =>
metrics.tasksCompletedToday.toString(),
),
_MetricCard(
title: 'Open tasks',
valueBuilder: (metrics) => metrics.openTasks.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'TAT / Response'),
_cardGrid(context, [
_MetricCard(
title: 'Avg response',
valueBuilder: (metrics) => _formatDuration(metrics.avgResponse),
),
_MetricCard(
title: 'Avg triage',
valueBuilder: (metrics) => _formatDuration(metrics.avgTriage),
),
_MetricCard(
title: 'Longest response',
valueBuilder: (metrics) =>
_formatDuration(metrics.longestResponse),
),
]),
];
final realtime = ProviderScope.containerOf(
context,
).read(realtimeControllerProvider);
final content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Align(
alignment: Alignment.center,
child: Text(
'Dashboard',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
return ResponsiveBody(
child: Skeletonizer(
enabled: realtime.isConnecting,
child: LayoutBuilder(
builder: (context, constraints) {
final sections = <Widget>[
const SizedBox(height: 16),
_sectionTitle(context, 'IT Staff Pulse'),
const _StaffTable(),
const SizedBox(height: 20),
_sectionTitle(context, 'Core Daily KPIs'),
_cardGrid(context, [
_MetricCard(
title: 'New tickets today',
valueBuilder: (metrics) => metrics.newTicketsToday.toString(),
),
_MetricCard(
title: 'Closed today',
valueBuilder: (metrics) => metrics.closedToday.toString(),
),
_MetricCard(
title: 'Open tickets',
valueBuilder: (metrics) => metrics.openTickets.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'Task Flow'),
_cardGrid(context, [
_MetricCard(
title: 'Tasks created',
valueBuilder: (metrics) =>
metrics.tasksCreatedToday.toString(),
),
_MetricCard(
title: 'Tasks completed',
valueBuilder: (metrics) =>
metrics.tasksCompletedToday.toString(),
),
_MetricCard(
title: 'Open tasks',
valueBuilder: (metrics) => metrics.openTasks.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'TAT / Response'),
_cardGrid(context, [
_MetricCard(
title: 'Avg response',
valueBuilder: (metrics) =>
_formatDuration(metrics.avgResponse),
),
_MetricCard(
title: 'Avg triage',
valueBuilder: (metrics) => _formatDuration(metrics.avgTriage),
),
_MetricCard(
title: 'Longest response',
valueBuilder: (metrics) =>
_formatDuration(metrics.longestResponse),
),
]),
];
final content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Align(
alignment: Alignment.center,
child: Text(
'Dashboard',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
),
),
const _DashboardStatusBanner(),
...sections,
],
);
const _DashboardStatusBanner(),
...sections,
],
);
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 24),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: content,
),
),
);
},
return Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 24),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: content,
),
),
),
if (realtime.isConnecting)
Positioned.fill(
child: AbsorbPointer(
absorbing: true,
child: Container(
color: Theme.of(
context,
).colorScheme.surface.withAlpha((0.35 * 255).round()),
alignment: Alignment.topCenter,
padding: const EdgeInsets.only(top: 36),
child: SizedBox(
width: 280,
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 12),
Expanded(
child: Text('Reconnecting realtime…'),
),
],
),
),
),
),
),
),
),
],
);
},
),
),
);
}