it0/it0_app/lib/features/dashboard/presentation/pages/dashboard_page.dart

442 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../../../core/widgets/empty_state.dart';
import '../../../../core/widgets/status_badge.dart';
// ---------------------------------------------------------------------------
// TODO 38 Dashboard providers & page
// ---------------------------------------------------------------------------
/// Summary model for server fleet status.
class _ServerSummary {
final int total;
final int online;
final int warning;
final int offline;
const _ServerSummary({
required this.total,
required this.online,
required this.warning,
required this.offline,
});
}
/// Summary model for alert severity counts.
class _AlertSummary {
final int critical;
final int warning;
final int info;
const _AlertSummary({
required this.critical,
required this.warning,
required this.info,
});
int get total => critical + warning + info;
}
/// Fetches server list and computes online/warning/offline counts.
final serverSummaryProvider = FutureProvider<_ServerSummary>((ref) async {
final dio = ref.watch(dioClientProvider);
final response = await dio.get(ApiEndpoints.servers);
final servers = (response.data as List?) ?? [];
int online = 0;
int warning = 0;
int offline = 0;
for (final s in servers) {
final status = (s['status'] as String? ?? '').toLowerCase();
switch (status) {
case 'online':
case 'healthy':
case 'running':
case 'active':
online++;
break;
case 'warning':
case 'degraded':
warning++;
break;
default:
offline++;
}
}
return _ServerSummary(
total: servers.length,
online: online,
warning: warning,
offline: offline,
);
});
/// Fetches alert events and counts by severity.
final alertSummaryProvider = FutureProvider<_AlertSummary>((ref) async {
final dio = ref.watch(dioClientProvider);
final response = await dio.get(ApiEndpoints.alertEvents);
final events = (response.data as List?) ?? [];
int critical = 0;
int warning = 0;
int info = 0;
for (final e in events) {
final severity = (e['severity'] as String? ?? '').toLowerCase();
switch (severity) {
case 'critical':
case 'error':
critical++;
break;
case 'warning':
warning++;
break;
default:
info++;
}
}
return _AlertSummary(critical: critical, warning: warning, info: info);
});
/// Fetches the 5 most recent ops tasks.
final recentOpsProvider = FutureProvider<List<Map<String, dynamic>>>((ref) async {
final dio = ref.watch(dioClientProvider);
final response = await dio.get(
ApiEndpoints.opsTasks,
queryParameters: {'limit': 5},
);
final data = response.data;
if (data is List) {
return data.cast<Map<String, dynamic>>();
}
if (data is Map && data.containsKey('items')) {
return (data['items'] as List).cast<Map<String, dynamic>>();
}
return [];
});
// ---------------------------------------------------------------------------
// Dashboard page ConsumerWidget
// ---------------------------------------------------------------------------
class DashboardPage extends ConsumerWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverAsync = ref.watch(serverSummaryProvider);
final alertAsync = ref.watch(alertSummaryProvider);
final recentOpsAsync = ref.watch(recentOpsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('仪表盘'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.invalidate(serverSummaryProvider);
ref.invalidate(alertSummaryProvider);
ref.invalidate(recentOpsProvider);
},
),
],
),
body: RefreshIndicator(
onRefresh: () async {
ref.invalidate(serverSummaryProvider);
ref.invalidate(alertSummaryProvider);
ref.invalidate(recentOpsProvider);
},
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// ------------- Summary cards row ---------------------------------
const Text(
'概览',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
// Server card
Expanded(child: _buildServerCard(serverAsync)),
const SizedBox(width: 12),
// Alert card
Expanded(child: _buildAlertCard(alertAsync)),
const SizedBox(width: 12),
// Tasks card
Expanded(child: _buildTasksCard(recentOpsAsync)),
],
),
const SizedBox(height: 24),
// ------------- Recent operations --------------------------------
const Text(
'最近操作',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
_buildRecentOps(recentOpsAsync),
],
),
),
);
}
// ---- Card builders -------------------------------------------------------
Widget _buildServerCard(AsyncValue<_ServerSummary> value) {
return value.when(
data: (summary) => _SummaryCard(
icon: Icons.dns,
iconColor: AppColors.success,
title: '服务器',
mainValue: '${summary.online}/${summary.total}',
subtitle: '在线',
details: [
if (summary.warning > 0)
_DetailRow(label: '告警', count: summary.warning, color: AppColors.warning),
if (summary.offline > 0)
_DetailRow(label: '离线', count: summary.offline, color: AppColors.error),
],
),
loading: () => const _SummaryCardLoading(),
error: (e, _) => _SummaryCardError(message: e.toString()),
);
}
Widget _buildAlertCard(AsyncValue<_AlertSummary> value) {
return value.when(
data: (summary) => _SummaryCard(
icon: Icons.warning_amber_rounded,
iconColor: summary.critical > 0 ? AppColors.error : AppColors.warning,
title: '告警',
mainValue: '${summary.total}',
subtitle: '活跃',
details: [
if (summary.critical > 0)
_DetailRow(label: '严重', count: summary.critical, color: AppColors.error),
if (summary.warning > 0)
_DetailRow(label: '警告', count: summary.warning, color: AppColors.warning),
if (summary.info > 0)
_DetailRow(label: '信息', count: summary.info, color: AppColors.info),
],
),
loading: () => const _SummaryCardLoading(),
error: (e, _) => _SummaryCardError(message: e.toString()),
);
}
Widget _buildTasksCard(AsyncValue<List<Map<String, dynamic>>> value) {
return value.when(
data: (tasks) {
final running =
tasks.where((t) => (t['status'] as String?)?.toLowerCase() == 'running').length;
return _SummaryCard(
icon: Icons.assignment,
iconColor: AppColors.info,
title: '任务',
mainValue: '$running',
subtitle: '运行中',
details: [
_DetailRow(label: '最近', count: tasks.length, color: AppColors.textSecondary),
],
);
},
loading: () => const _SummaryCardLoading(),
error: (e, _) => _SummaryCardError(message: e.toString()),
);
}
// ---- Recent ops list ------------------------------------------------------
Widget _buildRecentOps(AsyncValue<List<Map<String, dynamic>>> value) {
return value.when(
data: (tasks) {
if (tasks.isEmpty) {
return const EmptyState(
icon: Icons.assignment_outlined,
title: '暂无操作记录',
subtitle: '操作任务将显示在此处',
);
}
return Column(
children: tasks.map((task) {
final title = task['title'] as String? ?? task['name'] as String? ?? '未命名';
final status = task['status'] as String? ?? 'unknown';
final createdAt = task['created_at'] as String? ?? task['createdAt'] as String?;
final timeLabel = createdAt != null
? DateFormatter.timeAgo(DateTime.parse(createdAt))
: '';
return Card(
color: AppColors.surface,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(title),
subtitle: Text(timeLabel, style: const TextStyle(color: AppColors.textSecondary)),
trailing: StatusBadge(status: status),
),
);
}).toList(),
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
),
),
error: (e, _) => Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text('加载失败: $e', style: const TextStyle(color: AppColors.error)),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Private helper widgets
// ---------------------------------------------------------------------------
class _DetailRow {
final String label;
final int count;
final Color color;
const _DetailRow({required this.label, required this.count, required this.color});
}
class _SummaryCard extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final String mainValue;
final String subtitle;
final List<_DetailRow> details;
const _SummaryCard({
required this.icon,
required this.iconColor,
required this.title,
required this.mainValue,
required this.subtitle,
this.details = const [],
});
@override
Widget build(BuildContext context) {
return Card(
color: AppColors.surface,
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 6),
Expanded(
child: Text(
title,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 10),
Text(
mainValue,
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
Text(subtitle, style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
if (details.isNotEmpty) ...[
const SizedBox(height: 8),
...details.map((d) => Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: d.color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
'${d.count} ${d.label}',
style: TextStyle(color: d.color, fontSize: 11),
),
],
),
)),
],
],
),
),
);
}
}
class _SummaryCardLoading extends StatelessWidget {
const _SummaryCardLoading();
@override
Widget build(BuildContext context) {
return const Card(
color: AppColors.surface,
child: SizedBox(
height: 140,
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
),
);
}
}
class _SummaryCardError extends StatelessWidget {
final String message;
const _SummaryCardError({required this.message});
@override
Widget build(BuildContext context) {
return Card(
color: AppColors.surface,
child: SizedBox(
height: 140,
child: Center(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: AppColors.error, size: 28),
const SizedBox(height: 6),
Text(
message,
style: const TextStyle(color: AppColors.error, fontSize: 11),
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
);
}
}