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 StatefulWidget { final ApiRequestInfo request; const SimpleApiCard({super.key, required this.request}); @override State createState() => _SimpleApiCardState(); } class _SimpleApiCardState extends State with SingleTickerProviderStateMixin { bool _isExpanded = false; late AnimationController _animationController; late Animation _expandAnimation; late Animation _iconRotation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _expandAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ); _iconRotation = Tween(begin: 0.0, end: 0.5).animate(_expandAnimation); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white, Colors.blue, ], ), border: Border.all(color: Colors.blue[200]!, width: 1), boxShadow: [ BoxShadow( color: Colors.blue[100]!.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Column( children: [ // 主要内容区域 InkWell( onTap: _toggleExpansion, borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // 状态指示器 Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( gradient: LinearGradient( colors: widget.request.statusText == 'SUCCESS' ? [Colors.green[500]!, Colors.teal[600]!] : [Colors.red[500]!, Colors.pink[600]!], ), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: widget.request.statusColor.withValues(alpha: 0.4), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Icon( widget.request.statusText == 'SUCCESS' ? Icons.check : Icons.close, color: Colors.white, size: 14, ), ), const SizedBox(width: 12), // HTTP方法 Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4), decoration: BoxDecoration( gradient: LinearGradient( colors: [ _getMethodColor(), _getMethodColor().withValues(alpha: 0.8) ], ), borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: _getMethodColor().withValues(alpha: 0.3), blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: Text( widget.request.method, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 8), // 状态标签 Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( gradient: LinearGradient( colors: widget.request.statusText == 'SUCCESS' ? [Colors.lightGreen[400]!, Colors.green[500]!] : [Colors.orange[400]!, Colors.red[500]!], ), borderRadius: BorderRadius.circular(8), ), child: Text( widget.request.statusText, style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), const Spacer(), // 状态码和响应时间 Row( mainAxisSize: MainAxisSize.min, children: [ if (widget.request.statusCode != null) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.indigo[600], borderRadius: BorderRadius.circular(6), ), child: Text( '${widget.request.statusCode}', style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), if (widget.request.duration != null) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.amber[500]!, Colors.orange[600]! ], ), borderRadius: BorderRadius.circular(6), ), child: Text( '${widget.request.duration!.inMilliseconds}ms', style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ], const SizedBox(width: 8), // 展开图标 AnimatedBuilder( animation: _iconRotation, builder: (context, child) { return Transform.rotate( angle: _iconRotation.value * 3.14159, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.blue[100], shape: BoxShape.circle, ), child: Icon( Icons.keyboard_arrow_down, color: Colors.blue[700], size: 16, ), ), ); }, ), ], ), ], ), const SizedBox(height: 8), // URL显示 Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!, width: 1), ), child: Text( widget.request.url, style: const TextStyle( fontSize: 12, color: Colors.black87, fontFamily: 'monospace', fontWeight: FontWeight.w500, ), ), ), ], ), ), ), // 展开的详细内容 SizeTransition( sizeFactor: _expandAnimation, child: Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.indigo[50]!, Colors.blue[50]!], ), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDataSection('Headers', widget.request.headers), if (widget.request.requestData != null) _buildDataSection('Request', widget.request.requestData), if (widget.request.responseData != null) _buildDataSection( 'Response', widget.request.responseData), if (widget.request.error != null) _buildDataSection('Error', widget.request.error), ], ), ), ), ), ], ), ); } Widget _buildDataSection(String title, dynamic data) { final jsonString = _formatAsJson(data); return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.deepPurple[500]!, Colors.purple[600]!], ), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.purple[200]!.withValues(alpha: 0.5), blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: Text( title, style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 12, ), ), ), const Spacer(), ElevatedButton.icon( icon: const Icon(Icons.copy, size: 14), label: const Text('复制'), style: ElevatedButton.styleFrom( backgroundColor: Colors.teal[600], foregroundColor: Colors.white, elevation: 3, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), onPressed: () => _copyToClipboard(jsonString), ), ], ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue[200]!, width: 1), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SelectableText( jsonString, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: Colors.black87, height: 1.4, ), ), ), ), ], ), ); } void _toggleExpansion() { setState(() { _isExpanded = !_isExpanded; if (_isExpanded) { _animationController.forward(); } else { _animationController.reverse(); } }); } String _formatAsJson(dynamic data) { try { if (data == null) return 'null'; // 如果已经是字符串,检查是否是JSON格式 if (data is String) { try { final decoded = jsonDecode(data); return const JsonEncoder.withIndent(' ').convert(decoded); } catch (e) { // 不是JSON格式,直接返回字符串 return data; } } // 对象转JSON,不限制大小 return const JsonEncoder.withIndent(' ').convert(data); } catch (e) { if (data.runtimeType.toString() == 'FormData') { if (data.fields is List>) { final fields = data.fields as List>; final jsonMap = { ...Map.fromEntries(fields.map((e) => MapEntry(e.key, e.value))), }; return const JsonEncoder.withIndent(' ').convert(jsonMap); } return data.fields.toString(); } // JSON序列化失败,返回字符串形式 return '${data.toString()}\n\n(JSON序列化失败: $e)'; } } void _copyToClipboard(String text) { Clipboard.setData(ClipboardData(text: text)); Get.snackbar( '🎉 复制成功', '内容已复制到剪贴板', duration: const Duration(seconds: 1), snackPosition: SnackPosition.bottom, backgroundColor: Colors.green[500], colorText: Colors.white, borderRadius: 10, margin: const EdgeInsets.all(16), boxShadows: [ BoxShadow( color: Colors.green[200]!.withValues(alpha: 0.5), blurRadius: 6, offset: const Offset(0, 3), ), ], ); } Color _getMethodColor() { switch (widget.request.method.toUpperCase()) { case 'GET': return Colors.blue[600]!; case 'POST': return Colors.green[600]!; case 'PUT': return Colors.orange[600]!; case 'DELETE': return Colors.red[600]!; case 'PATCH': return Colors.purple[600]!; default: return Colors.grey[600]!; } } }