simple_api_card.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 StatelessWidget {
  8. final ApiRequestInfo request;
  9. const SimpleApiCard({super.key, required this.request});
  10. /// 格式化请求时间
  11. String get _formattedTime {
  12. final t = request.timestamp;
  13. return '${t.hour.toString().padLeft(2, '0')}:'
  14. '${t.minute.toString().padLeft(2, '0')}:'
  15. '${t.second.toString().padLeft(2, '0')}';
  16. }
  17. /// 获取完整URL
  18. String get _fullUrl {
  19. return request.url;
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. return InkWell(
  24. onTap: () => _showDetailDialog(context),
  25. borderRadius: BorderRadius.circular(10),
  26. child: Container(
  27. margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
  28. padding: const EdgeInsets.all(12),
  29. decoration: BoxDecoration(
  30. color: Colors.white,
  31. borderRadius: BorderRadius.circular(10),
  32. border: Border.all(color: Colors.grey[200]!, width: 1),
  33. boxShadow: [
  34. BoxShadow(
  35. color: Colors.black.withValues(alpha: 0.03),
  36. blurRadius: 4,
  37. offset: const Offset(0, 1),
  38. ),
  39. ],
  40. ),
  41. child: Column(
  42. crossAxisAlignment: CrossAxisAlignment.start,
  43. children: [
  44. // 第一行:方法、状态码、耗时、时间
  45. Row(
  46. children: [
  47. // 状态指示点
  48. Container(
  49. width: 8,
  50. height: 8,
  51. decoration: BoxDecoration(
  52. color: _getStatusColor(),
  53. shape: BoxShape.circle,
  54. ),
  55. ),
  56. const SizedBox(width: 8),
  57. // 方法标签
  58. Container(
  59. padding: const EdgeInsets.symmetric(
  60. horizontal: 8,
  61. vertical: 3,
  62. ),
  63. decoration: BoxDecoration(
  64. color: _getMethodColor().withValues(alpha: 0.1),
  65. borderRadius: BorderRadius.circular(4),
  66. ),
  67. child: Text(
  68. request.method,
  69. style: TextStyle(
  70. fontSize: 11,
  71. fontWeight: FontWeight.w600,
  72. color: _getMethodColor(),
  73. ),
  74. ),
  75. ),
  76. const SizedBox(width: 8),
  77. // 状态码
  78. if (request.statusCode != null)
  79. Container(
  80. padding: const EdgeInsets.symmetric(
  81. horizontal: 6,
  82. vertical: 3,
  83. ),
  84. decoration: BoxDecoration(
  85. color: _getStatusColor().withValues(alpha: 0.1),
  86. borderRadius: BorderRadius.circular(4),
  87. ),
  88. child: Text(
  89. '${request.statusCode}',
  90. style: TextStyle(
  91. fontSize: 11,
  92. fontWeight: FontWeight.w600,
  93. color: _getStatusColor(),
  94. ),
  95. ),
  96. ),
  97. const Spacer(),
  98. // 耗时
  99. if (request.duration != null)
  100. Text(
  101. '${request.duration!.inMilliseconds}ms',
  102. style: TextStyle(fontSize: 10, color: Colors.grey[500]),
  103. ),
  104. const SizedBox(width: 8),
  105. // 时间
  106. Text(
  107. _formattedTime,
  108. style: TextStyle(
  109. fontSize: 10,
  110. color: Colors.grey[400],
  111. fontFamily: 'monospace',
  112. ),
  113. ),
  114. ],
  115. ),
  116. const SizedBox(height: 8),
  117. // 第二行:完整URL
  118. Text(
  119. _fullUrl,
  120. style: TextStyle(fontSize: 12, color: Colors.grey[700]),
  121. maxLines: 2,
  122. overflow: TextOverflow.ellipsis,
  123. ),
  124. ],
  125. ),
  126. ),
  127. );
  128. }
  129. void _showDetailDialog(BuildContext context) {
  130. showDialog(
  131. context: context,
  132. builder: (context) => _ApiDetailDialog(request: request),
  133. );
  134. }
  135. Color _getStatusColor() {
  136. if (request.error != null) return Colors.red;
  137. if (request.statusCode == null) return Colors.orange;
  138. if (request.statusCode! >= 200 && request.statusCode! < 300) {
  139. return Colors.green;
  140. }
  141. return Colors.red;
  142. }
  143. Color _getMethodColor() {
  144. switch (request.method.toUpperCase()) {
  145. case 'GET':
  146. return Colors.blue;
  147. case 'POST':
  148. return Colors.teal;
  149. case 'PUT':
  150. return Colors.orange;
  151. case 'DELETE':
  152. return Colors.red;
  153. case 'PATCH':
  154. return Colors.purple;
  155. default:
  156. return Colors.grey;
  157. }
  158. }
  159. }
  160. /// API详情弹窗
  161. class _ApiDetailDialog extends StatelessWidget {
  162. final ApiRequestInfo request;
  163. const _ApiDetailDialog({required this.request});
  164. @override
  165. Widget build(BuildContext context) {
  166. return Dialog(
  167. backgroundColor: Colors.grey[50],
  168. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  169. insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
  170. child: Container(
  171. width: Get.width * 0.92,
  172. constraints: BoxConstraints(maxHeight: Get.height * 0.85),
  173. child: Column(
  174. mainAxisSize: MainAxisSize.min,
  175. crossAxisAlignment: CrossAxisAlignment.start,
  176. children: [
  177. // 头部
  178. Container(
  179. padding: const EdgeInsets.all(16),
  180. decoration: BoxDecoration(
  181. color: Colors.white,
  182. borderRadius: const BorderRadius.vertical(
  183. top: Radius.circular(16),
  184. ),
  185. boxShadow: [
  186. BoxShadow(
  187. color: Colors.black.withValues(alpha: 0.05),
  188. blurRadius: 4,
  189. offset: const Offset(0, 2),
  190. ),
  191. ],
  192. ),
  193. child: Column(
  194. crossAxisAlignment: CrossAxisAlignment.start,
  195. children: [
  196. // 第一行:方法、状态码、关闭按钮
  197. Row(
  198. children: [
  199. // 方法标签
  200. Container(
  201. padding: const EdgeInsets.symmetric(
  202. horizontal: 10,
  203. vertical: 5,
  204. ),
  205. decoration: BoxDecoration(
  206. color: _getMethodColor().withValues(alpha: 0.1),
  207. borderRadius: BorderRadius.circular(6),
  208. border: Border.all(
  209. color: _getMethodColor().withValues(alpha: 0.3),
  210. ),
  211. ),
  212. child: Text(
  213. request.method,
  214. style: TextStyle(
  215. fontSize: 13,
  216. fontWeight: FontWeight.w700,
  217. color: _getMethodColor(),
  218. ),
  219. ),
  220. ),
  221. const SizedBox(width: 10),
  222. // 状态码
  223. if (request.statusCode != null)
  224. Container(
  225. padding: const EdgeInsets.symmetric(
  226. horizontal: 8,
  227. vertical: 4,
  228. ),
  229. decoration: BoxDecoration(
  230. color: _getStatusColor().withValues(alpha: 0.1),
  231. borderRadius: BorderRadius.circular(6),
  232. ),
  233. child: Text(
  234. '${request.statusCode}',
  235. style: TextStyle(
  236. fontSize: 13,
  237. fontWeight: FontWeight.w600,
  238. color: _getStatusColor(),
  239. ),
  240. ),
  241. ),
  242. const SizedBox(width: 10),
  243. // 耗时
  244. if (request.duration != null)
  245. Container(
  246. padding: const EdgeInsets.symmetric(
  247. horizontal: 8,
  248. vertical: 4,
  249. ),
  250. decoration: BoxDecoration(
  251. color: Colors.grey[100],
  252. borderRadius: BorderRadius.circular(6),
  253. ),
  254. child: Text(
  255. '${request.duration!.inMilliseconds}ms',
  256. style: TextStyle(
  257. fontSize: 12,
  258. fontWeight: FontWeight.w500,
  259. color: Colors.grey[700],
  260. ),
  261. ),
  262. ),
  263. const Spacer(),
  264. // 关闭按钮
  265. InkWell(
  266. onTap: () => Navigator.pop(context),
  267. borderRadius: BorderRadius.circular(8),
  268. child: Container(
  269. padding: const EdgeInsets.all(6),
  270. decoration: BoxDecoration(
  271. color: Colors.grey[100],
  272. borderRadius: BorderRadius.circular(8),
  273. ),
  274. child: Icon(
  275. Icons.close,
  276. size: 18,
  277. color: Colors.grey[600],
  278. ),
  279. ),
  280. ),
  281. ],
  282. ),
  283. ],
  284. ),
  285. ),
  286. // 内容
  287. Flexible(
  288. child: SingleChildScrollView(
  289. padding: const EdgeInsets.all(16),
  290. child: Column(
  291. crossAxisAlignment: CrossAxisAlignment.start,
  292. children: [
  293. _buildSection('URL', request.url, icon: Icons.link),
  294. if (request.headers != null)
  295. _buildSection(
  296. 'Headers',
  297. _formatJson(request.headers),
  298. icon: Icons.description_outlined,
  299. ),
  300. if (request.requestData != null)
  301. _buildSection(
  302. 'Request',
  303. _formatJson(request.requestData),
  304. icon: Icons.upload_outlined,
  305. color: Colors.blue,
  306. ),
  307. if (request.responseData != null)
  308. _buildSection(
  309. 'Response',
  310. _formatJson(request.responseData),
  311. icon: Icons.download_outlined,
  312. color: Colors.green,
  313. ),
  314. if (request.error != null)
  315. _buildSection(
  316. 'Error',
  317. request.error!,
  318. icon: Icons.error_outline,
  319. isError: true,
  320. ),
  321. ],
  322. ),
  323. ),
  324. ),
  325. ],
  326. ),
  327. ),
  328. );
  329. }
  330. Widget _buildSection(
  331. String title,
  332. dynamic content, {
  333. IconData? icon,
  334. Color? color,
  335. bool isError = false,
  336. }) {
  337. final text = content is String ? content : content.toString();
  338. final sectionColor = isError ? Colors.red : (color ?? Colors.grey[700]!);
  339. return Container(
  340. margin: const EdgeInsets.only(bottom: 16),
  341. decoration: BoxDecoration(
  342. color: Colors.white,
  343. borderRadius: BorderRadius.circular(12),
  344. border: Border.all(
  345. color: isError
  346. ? Colors.red.withValues(alpha: 0.3)
  347. : Colors.grey[200]!,
  348. ),
  349. boxShadow: [
  350. BoxShadow(
  351. color: Colors.black.withValues(alpha: 0.02),
  352. blurRadius: 4,
  353. offset: const Offset(0, 1),
  354. ),
  355. ],
  356. ),
  357. child: Column(
  358. crossAxisAlignment: CrossAxisAlignment.start,
  359. children: [
  360. // 标题栏
  361. Container(
  362. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
  363. decoration: BoxDecoration(
  364. color: isError
  365. ? Colors.red.withValues(alpha: 0.05)
  366. : Colors.grey[50],
  367. borderRadius: const BorderRadius.vertical(
  368. top: Radius.circular(11),
  369. ),
  370. ),
  371. child: Row(
  372. children: [
  373. if (icon != null) ...[
  374. Icon(icon, size: 16, color: sectionColor),
  375. const SizedBox(width: 6),
  376. ],
  377. Text(
  378. title,
  379. style: TextStyle(
  380. fontSize: 13,
  381. fontWeight: FontWeight.w600,
  382. color: sectionColor,
  383. ),
  384. ),
  385. const Spacer(),
  386. InkWell(
  387. onTap: () => _copyToClipboard(text, title),
  388. borderRadius: BorderRadius.circular(6),
  389. child: Container(
  390. padding: const EdgeInsets.symmetric(
  391. horizontal: 10,
  392. vertical: 5,
  393. ),
  394. decoration: BoxDecoration(
  395. color: Colors.grey[100],
  396. borderRadius: BorderRadius.circular(6),
  397. ),
  398. child: Row(
  399. mainAxisSize: MainAxisSize.min,
  400. children: [
  401. Icon(Icons.copy, size: 12, color: Colors.grey[600]),
  402. const SizedBox(width: 4),
  403. Text(
  404. '复制',
  405. style: TextStyle(
  406. fontSize: 11,
  407. fontWeight: FontWeight.w500,
  408. color: Colors.grey[600],
  409. ),
  410. ),
  411. ],
  412. ),
  413. ),
  414. ),
  415. ],
  416. ),
  417. ),
  418. // 分隔线
  419. Divider(height: 1, color: Colors.grey[200]),
  420. // 内容区域 - 不限制高度
  421. Padding(
  422. padding: const EdgeInsets.all(14),
  423. child: SelectableText(
  424. text,
  425. style: TextStyle(
  426. fontFamily: 'monospace',
  427. fontSize: 12,
  428. color: isError ? Colors.red[700] : Colors.black87,
  429. height: 1.5,
  430. ),
  431. ),
  432. ),
  433. ],
  434. ),
  435. );
  436. }
  437. String _formatJson(dynamic data) {
  438. try {
  439. if (data == null) return 'null';
  440. if (data is String) {
  441. try {
  442. final decoded = jsonDecode(data);
  443. return const JsonEncoder.withIndent(' ').convert(decoded);
  444. } catch (_) {
  445. return data;
  446. }
  447. }
  448. return const JsonEncoder.withIndent(' ').convert(data);
  449. } catch (e) {
  450. return data.toString();
  451. }
  452. }
  453. void _copyToClipboard(String text, String type) {
  454. Clipboard.setData(ClipboardData(text: text));
  455. Get.snackbar(
  456. '已复制',
  457. '$type 内容已复制到剪贴板',
  458. duration: const Duration(seconds: 1),
  459. snackPosition: SnackPosition.bottom,
  460. backgroundColor: Colors.grey[800],
  461. colorText: Colors.white,
  462. borderRadius: 10,
  463. margin: const EdgeInsets.all(16),
  464. icon: const Padding(
  465. padding: EdgeInsets.only(left: 12),
  466. child: Icon(Icons.check_circle, color: Colors.white, size: 20),
  467. ),
  468. );
  469. }
  470. Color _getStatusColor() {
  471. if (request.error != null) return Colors.red;
  472. if (request.statusCode == null) return Colors.orange;
  473. if (request.statusCode! >= 200 && request.statusCode! < 300) {
  474. return Colors.green;
  475. }
  476. return Colors.red;
  477. }
  478. Color _getMethodColor() {
  479. switch (request.method.toUpperCase()) {
  480. case 'GET':
  481. return Colors.blue;
  482. case 'POST':
  483. return Colors.teal;
  484. case 'PUT':
  485. return Colors.orange;
  486. case 'DELETE':
  487. return Colors.red;
  488. case 'PATCH':
  489. return Colors.purple;
  490. default:
  491. return Colors.grey;
  492. }
  493. }
  494. }