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>>((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>(); } if (data is Map && data.containsKey('items')) { return (data['items'] as List).cast>(); } 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>> 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>> 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, ), ], ), ), ), ), ); } }