it0/it0_app/lib/features/servers/presentation/pages/servers_page.dart

481 lines
16 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/widgets/empty_state.dart';
import '../../../../core/widgets/status_badge.dart';
// ---------------------------------------------------------------------------
// Provider fetch servers from inventory API
// ---------------------------------------------------------------------------
/// Fetches the full list of servers from the backend.
final serversProvider =
FutureProvider<List<Map<String, dynamic>>>((ref) async {
final dio = ref.watch(dioClientProvider);
final response = await dio.get(ApiEndpoints.servers);
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 [];
});
// ---------------------------------------------------------------------------
// Servers page ConsumerStatefulWidget (needs local filter state)
// ---------------------------------------------------------------------------
class ServersPage extends ConsumerStatefulWidget {
const ServersPage({super.key});
@override
ConsumerState<ServersPage> createState() => _ServersPageState();
}
class _ServersPageState extends ConsumerState<ServersPage> {
String _envFilter = 'all';
static const _environments = ['all', 'dev', 'staging', 'prod'];
@override
Widget build(BuildContext context) {
final serversAsync = ref.watch(serversProvider);
return Scaffold(
appBar: AppBar(
title: const Text('服务器'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.invalidate(serversProvider),
),
],
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(serversProvider),
child: Column(
children: [
// Environment filter chips
_buildFilterBar(),
// Server list
Expanded(
child: serversAsync.when(
data: (servers) {
final filtered = _filterServers(servers);
if (filtered.isEmpty) {
return const EmptyState(
icon: Icons.dns_outlined,
title: '未找到服务器',
subtitle: '没有匹配当前筛选条件的服务器',
);
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: filtered.length,
itemBuilder: (context, index) {
final server = filtered[index];
return _ServerCard(
server: server,
onTap: () => _showServerDetails(context, server),
);
},
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline,
color: AppColors.error, size: 48),
const SizedBox(height: 12),
const Text(
'加载服务器失败',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Text(
e.toString(),
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () => ref.invalidate(serversProvider),
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
),
),
),
),
),
],
),
),
);
}
// ---- Environment filter bar -----------------------------------------------
Widget _buildFilterBar() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: _environments.map((env) {
final selected = _envFilter == env;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(env == 'all' ? '全部' : env),
selected: selected,
onSelected: (_) => setState(() => _envFilter = env),
selectedColor: _envColor(env).withOpacity(0.25),
checkmarkColor: _envColor(env),
labelStyle: TextStyle(
color: selected ? _envColor(env) : AppColors.textSecondary,
fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
),
side: BorderSide(
color: selected
? _envColor(env).withOpacity(0.5)
: AppColors.surfaceLight,
),
backgroundColor: AppColors.surface,
),
);
}).toList(),
),
);
}
List<Map<String, dynamic>> _filterServers(
List<Map<String, dynamic>> servers) {
if (_envFilter == 'all') return servers;
return servers.where((s) {
final env = (s['environment'] as String? ?? '').toLowerCase();
return env == _envFilter;
}).toList();
}
// ---- Server details bottom sheet ------------------------------------------
void _showServerDetails(
BuildContext context, Map<String, dynamic> server) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??
'N/A';
final env = server['environment'] as String? ?? 'unknown';
final status = server['status'] as String? ?? 'unknown';
final role = server['role'] as String? ?? '';
final os = server['os'] as String? ??
server['operating_system'] as String? ??
'';
final cpu = server['cpu'] as String? ?? server['cpu_cores']?.toString() ?? '';
final memory =
server['memory'] as String? ?? server['memory_gb']?.toString() ?? '';
final region = server['region'] as String? ?? '';
final provider = server['provider'] as String? ??
server['cloud_provider'] as String? ??
'';
final createdAt =
server['created_at'] as String? ?? server['createdAt'] as String? ?? '';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.55,
minChildSize: 0.3,
maxChildSize: 0.85,
expand: false,
builder: (context, scrollController) => SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.textMuted,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
// Hostname + status
Row(
children: [
Expanded(
child: Text(
hostname,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
StatusBadge(status: status),
],
),
const SizedBox(height: 16),
// Environment + Role row
Row(
children: [
_EnvironmentTag(environment: env),
if (role.isNotEmpty) ...[
const SizedBox(width: 8),
StatusBadge(status: role, color: AppColors.info),
],
],
),
const SizedBox(height: 20),
// Detail rows
_DetailRow(label: 'IP 地址', value: ip),
if (os.isNotEmpty) _DetailRow(label: '操作系统', value: os),
if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu),
if (memory.isNotEmpty)
_DetailRow(label: '内存', value: memory),
if (region.isNotEmpty)
_DetailRow(label: '区域', value: region),
if (provider.isNotEmpty)
_DetailRow(label: '云厂商', value: provider),
if (createdAt.isNotEmpty)
_DetailRow(label: '创建时间', value: createdAt),
],
),
),
),
);
}
// ---- Helpers --------------------------------------------------------------
static Color _envColor(String env) {
switch (env.toLowerCase()) {
case 'dev':
return AppColors.info;
case 'staging':
return AppColors.warning;
case 'prod':
return AppColors.error;
default:
return AppColors.primary;
}
}
}
// ---------------------------------------------------------------------------
// Server card widget
// ---------------------------------------------------------------------------
class _ServerCard extends StatelessWidget {
final Map<String, dynamic> server;
final VoidCallback onTap;
const _ServerCard({required this.server, required this.onTap});
@override
Widget build(BuildContext context) {
final hostname =
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
final ip = server['ip_address'] as String? ??
server['ipAddress'] as String? ??
server['ip'] as String? ??
'N/A';
final env = server['environment'] as String? ?? 'unknown';
final status = server['status'] as String? ?? 'unknown';
final role = server['role'] as String? ?? '';
final isOnline = _isOnline(status);
return Card(
color: AppColors.surface,
margin: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hostname + status dot
Row(
children: [
// Status indicator dot
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? AppColors.success : AppColors.error,
boxShadow: [
BoxShadow(
color: (isOnline ? AppColors.success : AppColors.error)
.withOpacity(0.4),
blurRadius: 6,
spreadRadius: 1,
),
],
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
hostname,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_EnvironmentTag(environment: env),
],
),
const SizedBox(height: 8),
// IP + role row
Row(
children: [
const Icon(Icons.lan_outlined,
size: 14, color: AppColors.textMuted),
const SizedBox(width: 4),
Text(
ip,
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 13),
),
if (role.isNotEmpty) ...[
const Spacer(),
Icon(Icons.workspaces_outline,
size: 14, color: AppColors.textMuted),
const SizedBox(width: 4),
Text(
role,
style: const TextStyle(
color: AppColors.textSecondary, fontSize: 13),
),
],
],
),
],
),
),
),
);
}
bool _isOnline(String status) {
final lower = status.toLowerCase();
return lower == 'online' ||
lower == 'running' ||
lower == 'active' ||
lower == 'healthy';
}
}
// ---------------------------------------------------------------------------
// Environment tag chip
// ---------------------------------------------------------------------------
class _EnvironmentTag extends StatelessWidget {
final String environment;
const _EnvironmentTag({required this.environment});
Color get _color {
switch (environment.toLowerCase()) {
case 'dev':
return AppColors.info;
case 'staging':
return AppColors.warning;
case 'prod':
return AppColors.error;
default:
return AppColors.textMuted;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _color.withOpacity(0.3)),
),
child: Text(
environment.toUpperCase(),
style: TextStyle(
color: _color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
);
}
}
// ---------------------------------------------------------------------------
// Detail row for bottom sheet
// ---------------------------------------------------------------------------
class _DetailRow extends StatelessWidget {
final String label;
final String value;
const _DetailRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 13,
fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
color: AppColors.textPrimary, fontSize: 13),
),
),
],
),
);
}
}