From bd50a97f8f5da7ff67348a8ffca18c19169adba8 Mon Sep 17 00:00:00 2001 From: YuanXuan Date: Thu, 28 Aug 2025 17:26:47 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/web/manifest.json | 2 + .../yx_net_inspector_controller.dart | 10 +++ lib/src/models/network_log_entry.dart | 42 ++++++++-- lib/src/widgets/inspector_panel.dart | 84 +++++++++++++++++-- lib/src/widgets/log_detail_page.dart | 56 +------------ 5 files changed, 123 insertions(+), 71 deletions(-) diff --git a/example/web/manifest.json b/example/web/manifest.json index a9a420a..188cfb0 100644 --- a/example/web/manifest.json +++ b/example/web/manifest.json @@ -33,3 +33,5 @@ } ] } + + diff --git a/lib/src/controller/yx_net_inspector_controller.dart b/lib/src/controller/yx_net_inspector_controller.dart index 92025e9..b16d103 100644 --- a/lib/src/controller/yx_net_inspector_controller.dart +++ b/lib/src/controller/yx_net_inspector_controller.dart @@ -65,6 +65,7 @@ class YxNetInspectorController extends ChangeNotifier { queryParameters: queryParameters, timestamp: DateTime.now(), isSuccess: false, // 响应到达时会更新 + status: NetworkRequestStatus.pending, // 初始状态为进行中 ); _logs.insert(0, entry); @@ -98,6 +99,9 @@ class YxNetInspectorController extends ChangeNotifier { responseData: responseData, duration: duration, isSuccess: isSuccess, + status: isSuccess + ? NetworkRequestStatus.success + : NetworkRequestStatus.failed, ); _logs[index] = updatedLog; @@ -139,6 +143,7 @@ class YxNetInspectorController extends ChangeNotifier { errorMessage: error, duration: duration, isSuccess: false, + status: NetworkRequestStatus.failed, ); _logs[index] = updatedLog; @@ -216,6 +221,11 @@ class YxNetInspectorController extends ChangeNotifier { return _logs.where((log) => log.isSuccess == isSuccess).toList(); } + /// 根据请求状态筛选日志 + List getLogsByRequestStatus(NetworkRequestStatus status) { + return _logs.where((log) => log.status == status).toList(); + } + /// 根据关键词搜索日志 List searchLogs(String keyword) { if (keyword.isEmpty) return _logs.toList(); diff --git a/lib/src/models/network_log_entry.dart b/lib/src/models/network_log_entry.dart index b5182cd..17d8466 100644 --- a/lib/src/models/network_log_entry.dart +++ b/lib/src/models/network_log_entry.dart @@ -1,5 +1,17 @@ import 'package:flutter/material.dart'; +/// 网络请求状态枚举 +enum NetworkRequestStatus { + /// 请求进行中 + pending, + + /// 请求成功 + success, + + /// 请求失败 + failed, +} + /// 网络请求日志条目 class NetworkLogEntry { final String id; @@ -14,6 +26,7 @@ class NetworkLogEntry { final DateTime timestamp; final Duration? duration; final bool isSuccess; + final NetworkRequestStatus status; NetworkLogEntry({ required this.id, @@ -28,22 +41,31 @@ class NetworkLogEntry { required this.timestamp, this.duration, required this.isSuccess, + this.status = NetworkRequestStatus.pending, }); - /// 根据成功状态和状态码获取状态颜色 + /// 根据请求状态获取状态颜色 Color get statusColor { - if (!isSuccess) return Colors.red; - if (statusCode == null) return Colors.orange; - if (statusCode! >= 200 && statusCode! < 300) return Colors.green; - if (statusCode! >= 400 && statusCode! < 500) return Colors.orange; - return Colors.red; + switch (status) { + case NetworkRequestStatus.pending: + return Colors.blue; // 进行中显示为蓝色 + case NetworkRequestStatus.success: + return Colors.green; // 成功显示为绿色 + case NetworkRequestStatus.failed: + return Colors.red; // 失败显示为红色 + } } /// 获取状态文本 String get statusText { - if (statusCode != null) return '$statusCode'; - if (!isSuccess) return '错误'; - return '未知'; + switch (status) { + case NetworkRequestStatus.pending: + return '进行中'; + case NetworkRequestStatus.success: + return statusCode != null ? '$statusCode' : '成功'; + case NetworkRequestStatus.failed: + return statusCode != null ? '$statusCode' : '错误'; + } } /// 获取预估请求大小 @@ -108,6 +130,7 @@ class NetworkLogEntry { DateTime? timestamp, Duration? duration, bool? isSuccess, + NetworkRequestStatus? status, }) { return NetworkLogEntry( id: id ?? this.id, @@ -122,6 +145,7 @@ class NetworkLogEntry { timestamp: timestamp ?? this.timestamp, duration: duration ?? this.duration, isSuccess: isSuccess ?? this.isSuccess, + status: status ?? this.status, ); } diff --git a/lib/src/widgets/inspector_panel.dart b/lib/src/widgets/inspector_panel.dart index 2a1ad6a..3d7d3c7 100644 --- a/lib/src/widgets/inspector_panel.dart +++ b/lib/src/widgets/inspector_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../controller/yx_net_inspector_controller.dart'; import '../models/inspector_theme.dart'; import '../models/network_log_entry.dart'; @@ -61,7 +62,9 @@ class _YxInspectorPanelState extends State { // 错误过滤 if (_showOnlyErrors) { - logs = logs.where((log) => !log.isSuccess).toList(); + logs = logs + .where((log) => log.status == NetworkRequestStatus.failed) + .toList(); } return logs; @@ -167,6 +170,14 @@ class _YxInspectorPanelState extends State { ), ), const Spacer(), + IconButton( + onPressed: () => _copyLogDetails(_selectedLog!), + icon: const Icon( + Icons.copy, + color: Colors.white, + ), + tooltip: '复制详情', + ), IconButton( onPressed: () { _isFullScreen = !_isFullScreen; @@ -403,10 +414,10 @@ class _YxInspectorPanelState extends State { color: widget.theme.cardColor, borderRadius: BorderRadius.circular(6), border: Border.all( - color: log.isSuccess - ? Colors.transparent - : widget.theme.errorColor.withValues(alpha: 0.3), - width: log.isSuccess ? 0 : 1, + color: log.status == NetworkRequestStatus.failed + ? widget.theme.errorColor.withValues(alpha: 0.3) + : Colors.transparent, + width: log.status == NetworkRequestStatus.failed ? 1 : 0, ), ), child: InkWell( @@ -428,7 +439,7 @@ class _YxInspectorPanelState extends State { ), ), const SizedBox(width: 8), - + // 方法标签 Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), @@ -446,7 +457,7 @@ class _YxInspectorPanelState extends State { ), ), const SizedBox(width: 8), - + // URL和状态 Expanded( child: Column( @@ -486,10 +497,12 @@ class _YxInspectorPanelState extends State { ], ), ), - + // 时间 Text( - log.formattedTime.split(' ')[1], // 只显示时间部分 + log.formattedTime.contains(' ') + ? log.formattedTime.split(' ')[1] + : log.formattedTime, // 只显示时间部分 style: TextStyle( fontSize: 10, color: widget.theme.secondaryTextColor, @@ -524,4 +537,57 @@ class _YxInspectorPanelState extends State { return widget.theme.secondaryTextColor; } } + + void _copyLogDetails(NetworkLogEntry log) { + final details = ''' +Network Request Details +======================= + +Basic Information: +- Method: ${log.method} +- Status Code: ${log.statusCode ?? 'N/A'} +- Status: ${log.statusText} +- Duration: ${log.formattedDuration} +- Request Size: ${log.requestSize} B +- Response Size: ${log.responseSize} B + +Full URL: +${log.url} + +${log.requestData != null ? 'Request Body:\n${_formatRequestBody(log.requestData)}\n\n' : ''}${log.responseData != null ? 'Response Data:\n${log.responseData}\n\n' : ''}${log.errorMessage != null ? 'Error Message:\n${log.errorMessage}\n\n' : ''}Time Information: +- Request Time: ${log.formattedTime} +- Start Time: ${_formatDateTime(log.timestamp)} +${log.duration != null ? '- End Time: ${_formatDateTime(log.timestamp.add(log.duration!))}' : ''} +'''; + + Clipboard.setData(ClipboardData(text: details)); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('详情已复制到剪贴板'), + duration: Duration(seconds: 2), + ), + ); + } + + String _formatRequestBody(dynamic requestData) { + if (requestData == null) return 'No request data'; + + if (requestData is String) { + return requestData; + } else if (requestData is Map) { + try { + return requestData.toString(); + } catch (e) { + return requestData.toString(); + } + } else { + return requestData.toString(); + } + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ' + '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}'; + } } diff --git a/lib/src/widgets/log_detail_page.dart b/lib/src/widgets/log_detail_page.dart index 09718fe..e442d45 100644 --- a/lib/src/widgets/log_detail_page.dart +++ b/lib/src/widgets/log_detail_page.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import '../models/network_log_entry.dart'; import '../models/inspector_theme.dart'; @@ -13,23 +12,9 @@ class YxLogDetailPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: theme.backgroundColor, - appBar: AppBar( - title: const Text('请求详情'), - backgroundColor: theme.primaryColor, - foregroundColor: Colors.white, - actions: [ - IconButton( - onPressed: () { - _copyToClipboard(context); - }, - icon: const Icon(Icons.copy), - tooltip: '复制详情', - ), - ], - ), - body: SingleChildScrollView( + return Container( + color: theme.backgroundColor, + child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -402,39 +387,4 @@ class YxLogDetailPage extends StatelessWidget { return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ' '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}'; } - - void _copyToClipboard(BuildContext context) { - final details = ''' -Network Request Details -======================= - -Basic Information: -- Method: ${log.method} -- Status Code: ${log.statusCode ?? 'N/A'} -- Status: ${log.statusText} -- Duration: ${log.formattedDuration} -- Request Size: ${log.requestSize} B -- Response Size: ${log.responseSize} B - -Full URL: -${log.url} - -${log.requestData != null ? 'Request Body:\n${_formatRequestBody(log.requestData)}\n\n' : ''} -${log.responseData != null ? 'Response Data:\n${log.responseData}\n\n' : ''} -${log.errorMessage != null ? 'Error Message:\n${log.errorMessage}\n\n' : ''} -Time Information: -- Request Time: ${log.formattedTime} -- Start Time: ${_formatDateTime(log.timestamp)} -${log.duration != null ? '- End Time: ${_formatDateTime(log.timestamp.add(log.duration!))}' : ''} -'''; - - Clipboard.setData(ClipboardData(text: details)); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('详情已复制到剪贴板'), - duration: Duration(seconds: 2), - ), - ); - } }