import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'ix_developer_tools.dart'; /// 轻量级API请求卡片 - 纵向排列,点击弹窗显示详情 class SimpleApiCard extends StatelessWidget { final ApiRequestInfo request; const SimpleApiCard({super.key, required this.request}); /// 格式化请求时间 String get _formattedTime { final t = request.timestamp; return '${t.hour.toString().padLeft(2, '0')}:' '${t.minute.toString().padLeft(2, '0')}:' '${t.second.toString().padLeft(2, '0')}'; } /// 获取完整URL String get _fullUrl { return request.url; } @override Widget build(BuildContext context) { return InkWell( onTap: () => _showDetailDialog(context), borderRadius: BorderRadius.circular(10), child: Container( margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey[200]!, width: 1), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 第一行:方法、状态码、耗时、时间 Row( children: [ // 状态指示点 Container( width: 8, height: 8, decoration: BoxDecoration( color: _getStatusColor(), shape: BoxShape.circle, ), ), const SizedBox(width: 8), // 方法标签 Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: _getMethodColor().withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( request.method, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _getMethodColor(), ), ), ), const SizedBox(width: 8), // 状态码 if (request.statusCode != null) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 3, ), decoration: BoxDecoration( color: _getStatusColor().withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '${request.statusCode}', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _getStatusColor(), ), ), ), const Spacer(), // 耗时 if (request.duration != null) Text( '${request.duration!.inMilliseconds}ms', style: TextStyle(fontSize: 10, color: Colors.grey[500]), ), const SizedBox(width: 8), // 时间 Text( _formattedTime, style: TextStyle( fontSize: 10, color: Colors.grey[400], fontFamily: 'monospace', ), ), ], ), const SizedBox(height: 8), // 第二行:完整URL Text( _fullUrl, style: TextStyle(fontSize: 12, color: Colors.grey[700]), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ); } void _showDetailDialog(BuildContext context) { showDialog( context: context, builder: (context) => _ApiDetailDialog(request: request), ); } Color _getStatusColor() { if (request.error != null) return Colors.red; if (request.statusCode == null) return Colors.orange; if (request.statusCode! >= 200 && request.statusCode! < 300) { return Colors.green; } return Colors.red; } Color _getMethodColor() { switch (request.method.toUpperCase()) { case 'GET': return Colors.blue; case 'POST': return Colors.teal; case 'PUT': return Colors.orange; case 'DELETE': return Colors.red; case 'PATCH': return Colors.purple; default: return Colors.grey; } } } /// API详情弹窗 class _ApiDetailDialog extends StatelessWidget { final ApiRequestInfo request; const _ApiDetailDialog({required this.request}); @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.grey[50], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: Container( width: Get.width * 0.92, constraints: BoxConstraints(maxHeight: Get.height * 0.85), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头部 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical( top: Radius.circular(16), ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 第一行:方法、状态码、关闭按钮 Row( children: [ // 方法标签 Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: _getMethodColor().withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: _getMethodColor().withValues(alpha: 0.3), ), ), child: Text( request.method, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: _getMethodColor(), ), ), ), const SizedBox(width: 10), // 状态码 if (request.statusCode != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _getStatusColor().withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( '${request.statusCode}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _getStatusColor(), ), ), ), const SizedBox(width: 10), // 耗时 if (request.duration != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(6), ), child: Text( '${request.duration!.inMilliseconds}ms', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.grey[700], ), ), ), const Spacer(), // 关闭按钮 InkWell( onTap: () => Navigator.pop(context), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.close, size: 18, color: Colors.grey[600], ), ), ), ], ), ], ), ), // 内容 Flexible( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSection('URL', request.url, icon: Icons.link), if (request.headers != null) _buildSection( 'Headers', _formatJson(request.headers), icon: Icons.description_outlined, ), if (request.requestData != null) _buildSection( 'Request', _formatJson(request.requestData), icon: Icons.upload_outlined, color: Colors.blue, ), if (request.responseData != null) _buildSection( 'Response', _formatJson(request.responseData), icon: Icons.download_outlined, color: Colors.green, ), if (request.error != null) _buildSection( 'Error', request.error!, icon: Icons.error_outline, isError: true, ), ], ), ), ), ], ), ), ); } Widget _buildSection( String title, dynamic content, { IconData? icon, Color? color, bool isError = false, }) { final text = content is String ? content : content.toString(); final sectionColor = isError ? Colors.red : (color ?? Colors.grey[700]!); return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: isError ? Colors.red.withValues(alpha: 0.3) : Colors.grey[200]!, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.02), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题栏 Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: isError ? Colors.red.withValues(alpha: 0.05) : Colors.grey[50], borderRadius: const BorderRadius.vertical( top: Radius.circular(11), ), ), child: Row( children: [ if (icon != null) ...[ Icon(icon, size: 16, color: sectionColor), const SizedBox(width: 6), ], Text( title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: sectionColor, ), ), const Spacer(), InkWell( onTap: () => _copyToClipboard(text, title), borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.copy, size: 12, color: Colors.grey[600]), const SizedBox(width: 4), Text( '复制', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.grey[600], ), ), ], ), ), ), ], ), ), // 分隔线 Divider(height: 1, color: Colors.grey[200]), // 内容区域 - 不限制高度 Padding( padding: const EdgeInsets.all(14), child: SelectableText( text, style: TextStyle( fontFamily: 'monospace', fontSize: 12, color: isError ? Colors.red[700] : Colors.black87, height: 1.5, ), ), ), ], ), ); } String _formatJson(dynamic data) { try { if (data == null) return 'null'; if (data is String) { try { final decoded = jsonDecode(data); return const JsonEncoder.withIndent(' ').convert(decoded); } catch (_) { return data; } } return const JsonEncoder.withIndent(' ').convert(data); } catch (e) { return data.toString(); } } void _copyToClipboard(String text, String type) { Clipboard.setData(ClipboardData(text: text)); Get.snackbar( '已复制', '$type 内容已复制到剪贴板', duration: const Duration(seconds: 1), snackPosition: SnackPosition.bottom, backgroundColor: Colors.grey[800], colorText: Colors.white, borderRadius: 10, margin: const EdgeInsets.all(16), icon: const Padding( padding: EdgeInsets.only(left: 12), child: Icon(Icons.check_circle, color: Colors.white, size: 20), ), ); } Color _getStatusColor() { if (request.error != null) return Colors.red; if (request.statusCode == null) return Colors.orange; if (request.statusCode! >= 200 && request.statusCode! < 300) { return Colors.green; } return Colors.red; } Color _getMethodColor() { switch (request.method.toUpperCase()) { case 'GET': return Colors.blue; case 'POST': return Colors.teal; case 'PUT': return Colors.orange; case 'DELETE': return Colors.red; case 'PATCH': return Colors.purple; default: return Colors.grey; } } }