simple_api_card.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:get/get.dart';
  5. import 'ix_developer_tools.dart';
  6. /// 轻量级API请求卡片
  7. class SimpleApiCard extends StatefulWidget {
  8. final ApiRequestInfo request;
  9. const SimpleApiCard({super.key, required this.request});
  10. @override
  11. State<SimpleApiCard> createState() => _SimpleApiCardState();
  12. }
  13. class _SimpleApiCardState extends State<SimpleApiCard>
  14. with SingleTickerProviderStateMixin {
  15. bool _isExpanded = false;
  16. late AnimationController _animationController;
  17. late Animation<double> _expandAnimation;
  18. late Animation<double> _iconRotation;
  19. @override
  20. void initState() {
  21. super.initState();
  22. _animationController = AnimationController(
  23. duration: const Duration(milliseconds: 300),
  24. vsync: this,
  25. );
  26. _expandAnimation = CurvedAnimation(
  27. parent: _animationController,
  28. curve: Curves.easeInOut,
  29. );
  30. _iconRotation =
  31. Tween<double>(begin: 0.0, end: 0.5).animate(_expandAnimation);
  32. }
  33. @override
  34. void dispose() {
  35. _animationController.dispose();
  36. super.dispose();
  37. }
  38. @override
  39. Widget build(BuildContext context) {
  40. return Container(
  41. margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
  42. decoration: BoxDecoration(
  43. borderRadius: BorderRadius.circular(12),
  44. gradient: const LinearGradient(
  45. begin: Alignment.topLeft,
  46. end: Alignment.bottomRight,
  47. colors: [
  48. Colors.white,
  49. Colors.blue,
  50. ],
  51. ),
  52. border: Border.all(color: Colors.blue[200]!, width: 1),
  53. boxShadow: [
  54. BoxShadow(
  55. color: Colors.blue[100]!.withValues(alpha: 0.3),
  56. blurRadius: 8,
  57. offset: const Offset(0, 3),
  58. ),
  59. ],
  60. ),
  61. child: Column(
  62. children: [
  63. // 主要内容区域
  64. InkWell(
  65. onTap: _toggleExpansion,
  66. borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
  67. child: Padding(
  68. padding: const EdgeInsets.all(16),
  69. child: Column(
  70. crossAxisAlignment: CrossAxisAlignment.start,
  71. children: [
  72. Row(
  73. children: [
  74. // 状态指示器
  75. Container(
  76. padding: const EdgeInsets.all(6),
  77. decoration: BoxDecoration(
  78. gradient: LinearGradient(
  79. colors: widget.request.statusText == 'SUCCESS'
  80. ? [Colors.green[500]!, Colors.teal[600]!]
  81. : [Colors.red[500]!, Colors.pink[600]!],
  82. ),
  83. shape: BoxShape.circle,
  84. boxShadow: [
  85. BoxShadow(
  86. color:
  87. widget.request.statusColor.withValues(alpha: 0.4),
  88. blurRadius: 4,
  89. offset: const Offset(0, 2),
  90. ),
  91. ],
  92. ),
  93. child: Icon(
  94. widget.request.statusText == 'SUCCESS'
  95. ? Icons.check
  96. : Icons.close,
  97. color: Colors.white,
  98. size: 14,
  99. ),
  100. ),
  101. const SizedBox(width: 12),
  102. // HTTP方法
  103. Container(
  104. padding: const EdgeInsets.symmetric(
  105. horizontal: 10, vertical: 4),
  106. decoration: BoxDecoration(
  107. gradient: LinearGradient(
  108. colors: [
  109. _getMethodColor(),
  110. _getMethodColor().withValues(alpha: 0.8)
  111. ],
  112. ),
  113. borderRadius: BorderRadius.circular(10),
  114. boxShadow: [
  115. BoxShadow(
  116. color: _getMethodColor().withValues(alpha: 0.3),
  117. blurRadius: 3,
  118. offset: const Offset(0, 1),
  119. ),
  120. ],
  121. ),
  122. child: Text(
  123. widget.request.method,
  124. style: const TextStyle(
  125. color: Colors.white,
  126. fontSize: 11,
  127. fontWeight: FontWeight.bold,
  128. ),
  129. ),
  130. ),
  131. const SizedBox(width: 8),
  132. // 状态标签
  133. Container(
  134. padding: const EdgeInsets.symmetric(
  135. horizontal: 8, vertical: 3),
  136. decoration: BoxDecoration(
  137. gradient: LinearGradient(
  138. colors: widget.request.statusText == 'SUCCESS'
  139. ? [Colors.lightGreen[400]!, Colors.green[500]!]
  140. : [Colors.orange[400]!, Colors.red[500]!],
  141. ),
  142. borderRadius: BorderRadius.circular(8),
  143. ),
  144. child: Text(
  145. widget.request.statusText,
  146. style: const TextStyle(
  147. color: Colors.white,
  148. fontSize: 10,
  149. fontWeight: FontWeight.bold,
  150. ),
  151. ),
  152. ),
  153. const Spacer(),
  154. // 状态码和响应时间
  155. Row(
  156. mainAxisSize: MainAxisSize.min,
  157. children: [
  158. if (widget.request.statusCode != null)
  159. Container(
  160. padding: const EdgeInsets.symmetric(
  161. horizontal: 6, vertical: 2),
  162. decoration: BoxDecoration(
  163. color: Colors.indigo[600],
  164. borderRadius: BorderRadius.circular(6),
  165. ),
  166. child: Text(
  167. '${widget.request.statusCode}',
  168. style: const TextStyle(
  169. fontSize: 10,
  170. color: Colors.white,
  171. fontWeight: FontWeight.bold,
  172. ),
  173. ),
  174. ),
  175. if (widget.request.duration != null) ...[
  176. const SizedBox(width: 6),
  177. Container(
  178. padding: const EdgeInsets.symmetric(
  179. horizontal: 6, vertical: 2),
  180. decoration: BoxDecoration(
  181. gradient: LinearGradient(
  182. colors: [
  183. Colors.amber[500]!,
  184. Colors.orange[600]!
  185. ],
  186. ),
  187. borderRadius: BorderRadius.circular(6),
  188. ),
  189. child: Text(
  190. '${widget.request.duration!.inMilliseconds}ms',
  191. style: const TextStyle(
  192. fontSize: 10,
  193. color: Colors.white,
  194. fontWeight: FontWeight.bold,
  195. ),
  196. ),
  197. ),
  198. ],
  199. const SizedBox(width: 8),
  200. // 展开图标
  201. AnimatedBuilder(
  202. animation: _iconRotation,
  203. builder: (context, child) {
  204. return Transform.rotate(
  205. angle: _iconRotation.value * 3.14159,
  206. child: Container(
  207. padding: const EdgeInsets.all(4),
  208. decoration: BoxDecoration(
  209. color: Colors.blue[100],
  210. shape: BoxShape.circle,
  211. ),
  212. child: Icon(
  213. Icons.keyboard_arrow_down,
  214. color: Colors.blue[700],
  215. size: 16,
  216. ),
  217. ),
  218. );
  219. },
  220. ),
  221. ],
  222. ),
  223. ],
  224. ),
  225. const SizedBox(height: 8),
  226. // URL显示
  227. Container(
  228. width: double.infinity,
  229. padding: const EdgeInsets.all(12),
  230. decoration: BoxDecoration(
  231. color: Colors.grey[100],
  232. borderRadius: BorderRadius.circular(8),
  233. border: Border.all(color: Colors.grey[300]!, width: 1),
  234. ),
  235. child: Text(
  236. widget.request.url,
  237. style: const TextStyle(
  238. fontSize: 12,
  239. color: Colors.black87,
  240. fontFamily: 'monospace',
  241. fontWeight: FontWeight.w500,
  242. ),
  243. ),
  244. ),
  245. ],
  246. ),
  247. ),
  248. ),
  249. // 展开的详细内容
  250. SizeTransition(
  251. sizeFactor: _expandAnimation,
  252. child: Container(
  253. width: double.infinity,
  254. decoration: BoxDecoration(
  255. gradient: LinearGradient(
  256. begin: Alignment.topCenter,
  257. end: Alignment.bottomCenter,
  258. colors: [Colors.indigo[50]!, Colors.blue[50]!],
  259. ),
  260. borderRadius: const BorderRadius.only(
  261. bottomLeft: Radius.circular(12),
  262. bottomRight: Radius.circular(12),
  263. ),
  264. ),
  265. child: Padding(
  266. padding: const EdgeInsets.all(16),
  267. child: Column(
  268. crossAxisAlignment: CrossAxisAlignment.start,
  269. children: [
  270. _buildDataSection('Headers', widget.request.headers),
  271. if (widget.request.requestData != null)
  272. _buildDataSection('Request', widget.request.requestData),
  273. if (widget.request.responseData != null)
  274. _buildDataSection(
  275. 'Response', widget.request.responseData),
  276. if (widget.request.error != null)
  277. _buildDataSection('Error', widget.request.error),
  278. ],
  279. ),
  280. ),
  281. ),
  282. ),
  283. ],
  284. ),
  285. );
  286. }
  287. Widget _buildDataSection(String title, dynamic data) {
  288. final jsonString = _formatAsJson(data);
  289. return Padding(
  290. padding: const EdgeInsets.only(bottom: 16),
  291. child: Column(
  292. crossAxisAlignment: CrossAxisAlignment.start,
  293. children: [
  294. Row(
  295. children: [
  296. Container(
  297. padding:
  298. const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
  299. decoration: BoxDecoration(
  300. gradient: LinearGradient(
  301. colors: [Colors.deepPurple[500]!, Colors.purple[600]!],
  302. ),
  303. borderRadius: BorderRadius.circular(8),
  304. boxShadow: [
  305. BoxShadow(
  306. color: Colors.purple[200]!.withValues(alpha: 0.5),
  307. blurRadius: 3,
  308. offset: const Offset(0, 1),
  309. ),
  310. ],
  311. ),
  312. child: Text(
  313. title,
  314. style: const TextStyle(
  315. fontWeight: FontWeight.bold,
  316. color: Colors.white,
  317. fontSize: 12,
  318. ),
  319. ),
  320. ),
  321. const Spacer(),
  322. ElevatedButton.icon(
  323. icon: const Icon(Icons.copy, size: 14),
  324. label: const Text('复制'),
  325. style: ElevatedButton.styleFrom(
  326. backgroundColor: Colors.teal[600],
  327. foregroundColor: Colors.white,
  328. elevation: 3,
  329. padding:
  330. const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
  331. shape: RoundedRectangleBorder(
  332. borderRadius: BorderRadius.circular(8),
  333. ),
  334. ),
  335. onPressed: () => _copyToClipboard(jsonString),
  336. ),
  337. ],
  338. ),
  339. const SizedBox(height: 8),
  340. Container(
  341. width: double.infinity,
  342. padding: const EdgeInsets.all(16),
  343. decoration: BoxDecoration(
  344. color: Colors.grey[50],
  345. borderRadius: BorderRadius.circular(8),
  346. border: Border.all(color: Colors.blue[200]!, width: 1),
  347. ),
  348. child: SingleChildScrollView(
  349. scrollDirection: Axis.horizontal,
  350. child: SelectableText(
  351. jsonString,
  352. style: const TextStyle(
  353. fontFamily: 'monospace',
  354. fontSize: 12,
  355. color: Colors.black87,
  356. height: 1.4,
  357. ),
  358. ),
  359. ),
  360. ),
  361. ],
  362. ),
  363. );
  364. }
  365. void _toggleExpansion() {
  366. setState(() {
  367. _isExpanded = !_isExpanded;
  368. if (_isExpanded) {
  369. _animationController.forward();
  370. } else {
  371. _animationController.reverse();
  372. }
  373. });
  374. }
  375. String _formatAsJson(dynamic data) {
  376. try {
  377. if (data == null) return 'null';
  378. // 如果已经是字符串,检查是否是JSON格式
  379. if (data is String) {
  380. try {
  381. final decoded = jsonDecode(data);
  382. return const JsonEncoder.withIndent(' ').convert(decoded);
  383. } catch (e) {
  384. // 不是JSON格式,直接返回字符串
  385. return data;
  386. }
  387. }
  388. // 对象转JSON,不限制大小
  389. return const JsonEncoder.withIndent(' ').convert(data);
  390. } catch (e) {
  391. if (data.runtimeType.toString() == 'FormData') {
  392. if (data.fields is List<MapEntry<String, String>>) {
  393. final fields = data.fields as List<MapEntry<String, String>>;
  394. final jsonMap = {
  395. ...Map.fromEntries(fields.map((e) => MapEntry(e.key, e.value))),
  396. };
  397. return const JsonEncoder.withIndent(' ').convert(jsonMap);
  398. }
  399. return data.fields.toString();
  400. }
  401. // JSON序列化失败,返回字符串形式
  402. return '${data.toString()}\n\n(JSON序列化失败: $e)';
  403. }
  404. }
  405. void _copyToClipboard(String text) {
  406. Clipboard.setData(ClipboardData(text: text));
  407. Get.snackbar(
  408. '🎉 复制成功',
  409. '内容已复制到剪贴板',
  410. duration: const Duration(seconds: 1),
  411. snackPosition: SnackPosition.bottom,
  412. backgroundColor: Colors.green[500],
  413. colorText: Colors.white,
  414. borderRadius: 10,
  415. margin: const EdgeInsets.all(16),
  416. boxShadows: [
  417. BoxShadow(
  418. color: Colors.green[200]!.withValues(alpha: 0.5),
  419. blurRadius: 6,
  420. offset: const Offset(0, 3),
  421. ),
  422. ],
  423. );
  424. }
  425. Color _getMethodColor() {
  426. switch (widget.request.method.toUpperCase()) {
  427. case 'GET':
  428. return Colors.blue[600]!;
  429. case 'POST':
  430. return Colors.green[600]!;
  431. case 'PUT':
  432. return Colors.orange[600]!;
  433. case 'DELETE':
  434. return Colors.red[600]!;
  435. case 'PATCH':
  436. return Colors.purple[600]!;
  437. default:
  438. return Colors.grey[600]!;
  439. }
  440. }
  441. }