630 lines
18 KiB
Dart
630 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:yx_net_inspector/yx_net_inspector.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
class DemoPage extends StatefulWidget {
|
|
const DemoPage({super.key});
|
|
|
|
@override
|
|
State<DemoPage> createState() => _DemoPageState();
|
|
}
|
|
|
|
class _DemoPageState extends State<DemoPage> {
|
|
int _requestCounter = 0;
|
|
final Random _random = Random();
|
|
bool _isLoading = false;
|
|
|
|
// 真实的开放API端点
|
|
final List<Map<String, dynamic>> _endpoints = [
|
|
{
|
|
'name': '用户列表',
|
|
'method': 'GET',
|
|
'url': 'https://jsonplaceholder.typicode.com/users',
|
|
'description': '获取用户列表 (JSONPlaceholder)',
|
|
'icon': Icons.people,
|
|
},
|
|
{
|
|
'name': '文章列表',
|
|
'method': 'GET',
|
|
'url': 'https://jsonplaceholder.typicode.com/posts',
|
|
'description': '获取文章列表 (JSONPlaceholder)',
|
|
'icon': Icons.article,
|
|
},
|
|
{
|
|
'name': '随机猫咪事实',
|
|
'method': 'GET',
|
|
'url': 'https://catfact.ninja/fact',
|
|
'description': '获取随机猫咪事实 (Cat Facts API)',
|
|
'icon': Icons.pets,
|
|
},
|
|
{
|
|
'name': '创建文章',
|
|
'method': 'POST',
|
|
'url': 'https://jsonplaceholder.typicode.com/posts',
|
|
'description': '创建新文章 (JSONPlaceholder)',
|
|
'icon': Icons.post_add,
|
|
},
|
|
{
|
|
'name': 'HTTP测试',
|
|
'method': 'GET',
|
|
'url': 'https://httpbin.org/json',
|
|
'description': '测试HTTP响应 (HTTPBin)',
|
|
'icon': Icons.http,
|
|
},
|
|
{
|
|
'name': '用户信息',
|
|
'method': 'GET',
|
|
'url': 'https://reqres.in/api/users/2',
|
|
'description': '获取单个用户 (ReqRes)',
|
|
'icon': Icons.person,
|
|
},
|
|
{
|
|
'name': 'IP地址信息',
|
|
'method': 'GET',
|
|
'url': 'https://httpbin.org/ip',
|
|
'description': '获取IP地址信息 (HTTPBin)',
|
|
'icon': Icons.location_on,
|
|
},
|
|
{
|
|
'name': '用户代理',
|
|
'method': 'GET',
|
|
'url': 'https://httpbin.org/user-agent',
|
|
'description': '获取用户代理信息 (HTTPBin)',
|
|
'icon': Icons.info,
|
|
},
|
|
{
|
|
'name': '更新用户',
|
|
'method': 'PUT',
|
|
'url': 'https://reqres.in/api/users/2',
|
|
'description': '更新用户信息 (ReqRes)',
|
|
'icon': Icons.edit,
|
|
},
|
|
{
|
|
'name': '删除用户',
|
|
'method': 'DELETE',
|
|
'url': 'https://reqres.in/api/users/2',
|
|
'description': '删除用户 (ReqRes)',
|
|
'icon': Icons.delete,
|
|
},
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 状态卡片
|
|
_buildStatusCard(),
|
|
const SizedBox(height: 20),
|
|
|
|
// 快速操作区域
|
|
_buildQuickActions(),
|
|
const SizedBox(height: 20),
|
|
|
|
// API端点列表
|
|
Text(
|
|
'API 端点测试',
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
...(_endpoints.map((endpoint) => _buildEndpointCard(endpoint))),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// 批量测试区域
|
|
_buildBatchTestSection(),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// 使用提示
|
|
_buildHelpSection(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusCard() {
|
|
return Card(
|
|
elevation: 4,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Icon(
|
|
Icons.network_check,
|
|
size: 32,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'网络请求统计',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'已发送请求: $_requestCounter',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: Colors.blue,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (_isLoading)
|
|
const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActions() {
|
|
return Card(
|
|
elevation: 2,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'快速操作',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: _simulateRandomRequest,
|
|
icon: const Icon(Icons.shuffle, size: 18),
|
|
label: const Text('随机请求'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: _simulateMultipleRequests,
|
|
icon: const Icon(Icons.burst_mode, size: 18),
|
|
label: const Text('批量请求'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: _simulateErrorRequest,
|
|
icon: const Icon(Icons.error_outline, size: 18),
|
|
label: const Text('错误请求'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: _clearLogs,
|
|
icon: const Icon(Icons.clear_all, size: 18),
|
|
label: const Text('清空日志'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEndpointCard(Map<String, dynamic> endpoint) {
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: _getMethodColor(endpoint['method']).withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
endpoint['icon'],
|
|
color: _getMethodColor(endpoint['method']),
|
|
size: 20,
|
|
),
|
|
),
|
|
title: Text(
|
|
endpoint['name'],
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(endpoint['description']),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: _getMethodColor(endpoint['method']),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
endpoint['method'],
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
endpoint['url'],
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
fontFamily: 'monospace',
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.play_arrow),
|
|
onPressed: () => _simulateSpecificRequest(endpoint),
|
|
tooltip: '发送请求',
|
|
),
|
|
isThreeLine: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBatchTestSection() {
|
|
return Card(
|
|
elevation: 2,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'批量测试',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'测试大量网络请求的性能和日志管理功能',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _simulateBatchRequests(10),
|
|
icon: const Icon(Icons.speed),
|
|
label: const Text('10个请求'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _simulateBatchRequests(50),
|
|
icon: const Icon(Icons.flash_on),
|
|
label: const Text('50个请求'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.deepOrange,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHelpSection() {
|
|
return Card(
|
|
color: Colors.blue.shade50,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.lightbulb_outline, color: Colors.blue.shade700),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'使用提示',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildHelpItem('1. 点击屏幕上的蓝色悬浮球打开网络检查器'),
|
|
_buildHelpItem('2. 发送各种类型的网络请求查看详细信息'),
|
|
_buildHelpItem('3. 在检查器中搜索和过滤特定请求'),
|
|
_buildHelpItem('4. 点击日志条目查看完整的请求/响应详情'),
|
|
_buildHelpItem('5. 使用批量测试验证性能和内存管理'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHelpItem(String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
color: Colors.blue.shade700,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getMethodColor(String method) {
|
|
switch (method.toUpperCase()) {
|
|
case 'GET':
|
|
return Colors.green;
|
|
case 'POST':
|
|
return Colors.blue;
|
|
case 'PUT':
|
|
return Colors.orange;
|
|
case 'DELETE':
|
|
return Colors.red;
|
|
case 'PATCH':
|
|
return Colors.purple;
|
|
default:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
|
|
void _simulateRandomRequest() {
|
|
final endpoint = _endpoints[_random.nextInt(_endpoints.length)];
|
|
_simulateSpecificRequest(endpoint);
|
|
}
|
|
|
|
void _simulateSpecificRequest(Map<String, dynamic> endpoint) {
|
|
setState(() {
|
|
_requestCounter++;
|
|
_isLoading = true;
|
|
});
|
|
|
|
_sendRealHttpRequest(endpoint);
|
|
}
|
|
|
|
Future<void> _sendRealHttpRequest(Map<String, dynamic> endpoint) async {
|
|
final requestId = 'request_${DateTime.now().millisecondsSinceEpoch}';
|
|
final method = endpoint['method'] as String;
|
|
final url = endpoint['url'] as String;
|
|
final startTime = DateTime.now();
|
|
|
|
// 准备请求头
|
|
final headers = {
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'YX-Net-Inspector-Demo/1.0.0',
|
|
'Accept': 'application/json',
|
|
'X-Request-ID': requestId,
|
|
};
|
|
|
|
// 记录请求开始
|
|
YxNetInspectorController.instance.logRequest(
|
|
id: requestId,
|
|
method: method,
|
|
url: url,
|
|
headers: headers,
|
|
requestData: _getRequestData(method),
|
|
queryParameters: method == 'GET' ? _getQueryParameters() : null,
|
|
);
|
|
|
|
try {
|
|
http.Response? response;
|
|
|
|
switch (method.toUpperCase()) {
|
|
case 'GET':
|
|
response = await http.get(Uri.parse(url), headers: headers);
|
|
break;
|
|
case 'POST':
|
|
final requestData = _getRequestData(method);
|
|
response = await http.post(
|
|
Uri.parse(url),
|
|
headers: headers,
|
|
body: requestData != null ? json.encode(requestData) : null,
|
|
);
|
|
break;
|
|
case 'PUT':
|
|
final requestData = _getRequestData(method);
|
|
response = await http.put(
|
|
Uri.parse(url),
|
|
headers: headers,
|
|
body: requestData != null ? json.encode(requestData) : null,
|
|
);
|
|
break;
|
|
case 'DELETE':
|
|
response = await http.delete(Uri.parse(url), headers: headers);
|
|
break;
|
|
default:
|
|
response = await http.get(Uri.parse(url), headers: headers);
|
|
}
|
|
|
|
final duration = DateTime.now().difference(startTime);
|
|
|
|
// 解析响应数据
|
|
dynamic responseData;
|
|
try {
|
|
responseData = json.decode(response.body);
|
|
} catch (e) {
|
|
responseData = response.body;
|
|
}
|
|
|
|
// 记录响应
|
|
YxNetInspectorController.instance.logResponse(
|
|
id: requestId,
|
|
statusCode: response.statusCode,
|
|
responseData: responseData,
|
|
duration: duration,
|
|
);
|
|
} catch (error) {
|
|
final duration = DateTime.now().difference(startTime);
|
|
|
|
// 记录错误
|
|
YxNetInspectorController.instance.logError(
|
|
id: requestId,
|
|
error: error.toString(),
|
|
duration: duration,
|
|
);
|
|
} finally {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _simulateMultipleRequests() {
|
|
for (int i = 0; i < 5; i++) {
|
|
Future.delayed(Duration(milliseconds: i * 300), () {
|
|
_simulateRandomRequest();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _simulateErrorRequest() {
|
|
setState(() {
|
|
_requestCounter++;
|
|
_isLoading = true;
|
|
});
|
|
|
|
// 使用HTTPBin的状态码端点来模拟错误
|
|
final errorCodes = [400, 401, 403, 404, 500, 502, 503];
|
|
final errorCode = errorCodes[_random.nextInt(errorCodes.length)];
|
|
|
|
final errorEndpoint = {
|
|
'method': 'GET',
|
|
'url': 'https://httpbin.org/status/$errorCode',
|
|
'name': '错误请求测试',
|
|
'description': '模拟HTTP错误状态码',
|
|
};
|
|
|
|
_sendRealHttpRequest(errorEndpoint);
|
|
}
|
|
|
|
void _simulateBatchRequests(int count) {
|
|
for (int i = 0; i < count; i++) {
|
|
Future.delayed(Duration(milliseconds: i * 50), () {
|
|
_simulateRandomRequest();
|
|
});
|
|
}
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('正在发送 $count 个请求...'),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _clearLogs() {
|
|
YxNetInspectorController.instance.clearLogs();
|
|
setState(() {
|
|
_requestCounter = 0;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('日志已清空'),
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic>? _getRequestData(String method) {
|
|
if (method == 'GET') return null;
|
|
|
|
switch (method) {
|
|
case 'POST':
|
|
return {
|
|
'name': '张三',
|
|
'email': 'zhangsan@example.com',
|
|
'age': _random.nextInt(50) + 18,
|
|
'department': '技术部',
|
|
'skills': ['Flutter', 'Dart', 'Mobile Development'],
|
|
'metadata': {
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
'source': 'demo_app',
|
|
'version': '1.0.0',
|
|
}
|
|
};
|
|
case 'PUT':
|
|
return {
|
|
'profile': {
|
|
'avatar': 'https://example.com/avatar.jpg',
|
|
'bio': '这是一个示例用户简介',
|
|
'preferences': {
|
|
'theme': 'dark',
|
|
'language': 'zh-CN',
|
|
'notifications': true,
|
|
}
|
|
}
|
|
};
|
|
case 'PATCH':
|
|
return {
|
|
'status': 'active',
|
|
'last_login': DateTime.now().toIso8601String(),
|
|
};
|
|
default:
|
|
return {'action': method.toLowerCase()};
|
|
}
|
|
}
|
|
|
|
Map<String, String>? _getQueryParameters() {
|
|
return {
|
|
'page': (_random.nextInt(10) + 1).toString(),
|
|
'limit': '20',
|
|
'sort': ['name', 'created_at', 'updated_at'][_random.nextInt(3)],
|
|
'order': ['asc', 'desc'][_random.nextInt(2)],
|
|
'filter': 'active',
|
|
};
|
|
}
|
|
}
|