| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- 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;
- }
- }
- }
|