/* * @message: 日志拦截器 * @Author: Jack * @Email: Jack@163.com * @Date: 2020-06-18 19:47:32 */ import 'package:dio/dio.dart'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; // 必须引入此库处理 Emoji 截断问题 import 'package:characters/characters.dart'; /// description: Dio 日志拦截器 class SCDioLogger extends Interceptor { final bool request; final bool requestHeader; final bool requestBody; final bool responseBody; final bool responseHeader; final bool error; static const int initialTab = 1; static const String tabStep = ' '; final bool compact; final int maxWidth; /// Log printer; defaults debugPrint to console. void Function(Object object) logPrint; final bool enableLog; SCDioLogger({ this.enableLog = kDebugMode, this.request = true, this.requestHeader = true, this.requestBody = true, this.responseHeader = false, this.responseBody = true, this.error = true, this.maxWidth = 90, this.compact = true, this.logPrint = print, // 修复:在 iOS 上 debugPrint 比 print 更稳健 }); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (!enableLog) return handler.next(options); if (request) _pReqHdr(options); if (requestHeader) { _pTable(options.queryParameters, header: 'Query Parameters'); final requestHeaders = {}; requestHeaders.addAll(options.headers); requestHeaders['contentType'] = options.contentType?.toString(); requestHeaders['responseType'] = options.responseType?.toString(); requestHeaders['followRedirects'] = options.followRedirects; requestHeaders['connectTimeout'] = options.connectTimeout; requestHeaders['receiveTimeout'] = options.receiveTimeout; _pTable(requestHeaders, header: 'Headers'); _pTable(options.extra, header: 'Extras'); } if (requestBody && options.method != 'GET') { final data = options.data; if (data != null) { if (data is Map) { _pTable(data, header: 'Body'); } else if (data is FormData) { final formDataMap = Map() ..addEntries(data.fields) ..addEntries(data.files); _pTable(formDataMap, header: 'Form data | ${data.boundary}'); } else { _pBlock(data.toString()); } } } handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) { if (!enableLog) return handler.next(err); if (error) { if (err.type == DioExceptionType.badResponse) { final uri = err.response?.requestOptions.uri; _pBox( header: 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', text: uri.toString()); if (err.response?.data != null) { logPrint('╔ ${err.type.toString()}'); _pResp(err.response!); } _pLine('╚'); } else { final uri = err.requestOptions.uri; _pBox(header: 'DioError ║ ${err.type} ║ ${uri.toString()}', text: err.message); } } handler.next(err); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { if (!enableLog) return handler.next(response); if (responseHeader) { final responseHeaders = {}; response.headers.forEach((k, list) => responseHeaders[k] = list.toString()); _pTable(responseHeaders, header: 'Headers'); } if (responseBody) { _pRespHdr(response); logPrint('╔ Body'); logPrint('║'); _pResp(response); logPrint('║'); _pLine('╚'); } handler.next(response); } void _pBox({String? header, String? text}) { logPrint(''); logPrint('╔╣ $header'); logPrint('║ $text'); _pLine('╚'); } void _pResp(Response response) { if (response.data != null) { if (response.data is Map) { _pMap(response.data); } else if (response.data is List) { logPrint('║${_ind()}['); _pList(response.data); logPrint('║${_ind()}]'); } else { _pBlock(response.data.toString()); } } } void _pRespHdr(Response response) { final uri = response.requestOptions.uri; final method = response.requestOptions.method; String header = 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}'; logPrint('╔╣ $header ${'═' * 20}'); logPrint('║ ${uri.toString()}'); logPrint('║ '); } void _pReqHdr(RequestOptions options) { final uri = options.uri; final method = options.method; _pBox(header: 'Request ║ $method ', text: uri.toString()); } void _pLine([String pre = '', String suf = '╝']) => logPrint('$pre${'═' * maxWidth}'); void _pKV(String key, Object v) { final pre = '╟ $key: '; final msg = v.toString(); if (pre.length + msg.length > maxWidth) { logPrint(pre); _pBlock(msg); } else { logPrint('$pre$msg'); } } // 修复:使用 characters 处理截断,防止 Emoji 损坏 void _pBlock(String msg) { final charData = msg.characters; int lines = (charData.length / maxWidth).ceil(); for (int i = 0; i < lines; ++i) { final start = i * maxWidth; final end = math.min(start + maxWidth, charData.length); logPrint('║ ' + charData.getRange(start, end).toString()); } } String _ind([int tabCount = initialTab]) => tabStep * tabCount; void _pMap(Map data, {int tabs = initialTab, bool isListItem = false, bool isLast = false}) { final bool isRoot = tabs == initialTab; final initialIndent = _ind(tabs); tabs++; if (isRoot || isListItem) logPrint('║$initialIndent{'); data.keys.toList().asMap().forEach((index, key) { final isLast = index == data.length - 1; var value = data[key]; if (value is String) value = '\"$value\"'; if (value is Map) { if (compact && _flatMap(value)) { logPrint('║${_ind(tabs)} $key: $value${!isLast ? ',' : ''}'); } else { logPrint('║${_ind(tabs)} $key: {'); _pMap(value, tabs: tabs); } } else if (value is List) { if (compact && _flatList(value)) { logPrint('║${_ind(tabs)} $key: ${value.toString()}'); } else { logPrint('║${_ind(tabs)} $key: ['); _pList(value, tabs: tabs); logPrint('║${_ind(tabs)} ]${isLast ? '' : ','}'); } } else { final msg = value.toString().replaceAll('\n', ''); final indent = _ind(tabs); final charMsg = msg.characters; final linWidth = maxWidth - indent.length; // 修复:字符串分段时处理 Emoji if (charMsg.length + indent.length > maxWidth) { int lines = (charMsg.length / linWidth).ceil(); for (int i = 0; i < lines; ++i) { final start = i * linWidth; final end = math.min(start + linWidth, charMsg.length); logPrint('║${_ind(tabs)} ${charMsg.getRange(start, end).toString()}'); } } else { logPrint('║${_ind(tabs)} $key: $msg${!isLast ? ',' : ''}'); } } }); logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}'); } void _pList(List list, {int tabs = initialTab}) { list.asMap().forEach((i, e) { final isLast = i == list.length - 1; if (e is Map) { if (compact && _flatMap(e)) { logPrint('║${_ind(tabs)} $e${!isLast ? ',' : ''}'); } else { _pMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast); } } else { logPrint('║${_ind(tabs + 2)} $e${isLast ? '' : ','}'); } }); } bool _flatMap(Map map) { return map.values.where((val) => val is Map || val is List).isEmpty && map.toString().length < maxWidth; } bool _flatList(List list) { return (list.length < 10 && list.toString().length < maxWidth); } void _pTable(Map? map, {String? header}) { if (map == null || map.isEmpty) return; logPrint('╔ $header '); map.forEach((key, value) { _pKV(key.toString(), (value ?? 'null').toString()); }); _pLine('╚'); } }