refactor: 兼容后端返回数字类型波动,清理废弃代码

主要变更:
1.  全局修复类型转换问题,将多处`as int?`改为`(num?)?.toInt()`兼容浮点/字符串类型的数字字段
2.  移除废弃的nearby_p2p配对方式和对应的依赖包
3.  优化鸿蒙端快捷方式、引导页、路由导航的稳定性
4.  合并日志输出避免鸿蒙端IDE卡顿
5.  修复安卓端蓝牙权限冗余声明
This commit is contained in:
Developer
2026-06-07 08:04:38 +08:00
parent 2db07728a4
commit ae6804e8bd
122 changed files with 2727 additions and 1908 deletions

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 鸿蒙端专用布局壳
/// 创建时间: 2026-05-18
/// 更新时间: 2026-05-22
/// 更新时间: 2026-06-06
/// 作用: 鸿蒙端使用 Scaffold+GlassBottomBar 替代 GoRouter+StatefulShellRoute
/// 上次更新: 修复引导页在鸿蒙端不显示的问题initState检查onboarding状态
/// 上次更新: 修复鸿蒙端杀后台重启时引导页重复弹出的问题添加KvStorage.isReady检查
/// ============================================================
///
/// 根因: 鸿蒙端 Flutter 引擎中 MaterialApp.router + 额外包导入 = 白屏
@@ -66,6 +66,17 @@ class _OhosAppShellState extends ConsumerState<OhosAppShell> {
void _checkOnboarding() {
if (!mounted) return;
// 等待KvStorage初始化完成再检查引导页状态
// 避免Hive未初始化时默认值导致引导页重复弹出
if (!KvStorage.isReady) {
Log.i('🟢 [OHOS] 引导页检查: KvStorage未就绪延迟检查');
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) _checkOnboarding();
});
return;
}
final shouldShow =
KvStorage.isFirstLaunch || KvStorage.shouldShowOnboarding;
if (shouldShow) {

View File

@@ -46,7 +46,7 @@ class ApiInterceptor extends Interceptor {
final data = response.data;
if (data is Map<String, dynamic> && data.containsKey('code')) {
final code = data['code'] as int?;
final code = (data['code'] as num?)?.toInt();
if (code == -1 || code == 401) {
_logger.w('业务层Token过期: code=$code');
_handleAuthExpired();

View File

@@ -350,7 +350,8 @@ final List<RouteDef> routeRegistry = [
module: RouteModule.user,
page: () => const SourcePage(),
),
// 注意:onboarding 已在 app_router.dart 中直接定义(位于 ShellRoute 之前,需要 rootNavigatorKey此处不再重复注册
// onboarding 路由已在 app_router.dart 中手动定义(需 parentNavigatorKey + iOS 转场
// 此处不再重复注册,避免 GoRouter duplicate name 断言崩溃
// ============================================================
// Tool module

View File

@@ -3,12 +3,14 @@
/// 创建时间: 2026-04-23
/// 更新时间: 2026-06-06
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板/分享权限 + iOS ATT授权
/// 上次更新: 新增requestTrackingPermission()方法支持iOS App Tracking Transparency授权
/// 上次更新: 鸿蒙端permission_handler不支持时引导用户去系统设置+MissingPluginException捕获
/// ============================================================
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -247,6 +249,7 @@ enum AppPermission {
/// Android 13+ 不需要 storage 权限(由 photos 替代)
/// tracking 权限仅 iOS 展示
/// 鸿蒙端:过滤 permission_handler 不支持或不需要的权限
bool get isPlatformRelevant {
if (this == AppPermission.storage) {
if (!pu.isAndroid) return false;
@@ -256,6 +259,26 @@ enum AppPermission {
if (this == AppPermission.tracking) {
return Platform.isIOS;
}
// 鸿蒙端:过滤不支持的权限,防止 permission_handler 挂起或卡死
if (pu.isOhos) {
return switch (this) {
// 鸿蒙端支持的权限
AppPermission.camera => true,
AppPermission.photos => true,
AppPermission.notification => true,
AppPermission.microphone => true,
AppPermission.nearbyDevices => true,
// 鸿蒙端不需要/不支持的权限
AppPermission.location => false, // 闲言APP鸿蒙端不需要定位权限
AppPermission.storage => false, // 鸿蒙端使用 READ_MEDIA/WRITE_MEDIA 替代
AppPermission.tracking => false, // 鸿蒙端不支持tracking权限
// 虚拟权限
AppPermission.network => true,
AppPermission.clipboard => true,
AppPermission.share => true,
AppPermission.shake => true,
};
}
return true;
}
@@ -407,6 +430,7 @@ class PermissionService {
}
/// 检查单个权限状态
/// 鸿蒙端添加超时保护,防止 permission_handler 挂起
static Future<AppPermissionStatus> checkStatus(AppPermission perm) async {
if (perm.isVirtual) {
return _checkVirtualStatus(perm);
@@ -416,6 +440,14 @@ class PermissionService {
return _checkTrackingStatus();
}
try {
// 鸿蒙端添加超时保护,防止 permission_handler 挂起
if (pu.isOhos) {
final status = await perm.permission.status.timeout(
const Duration(seconds: 3),
onTimeout: () => PermissionStatus.denied,
);
return AppPermissionStatus.fromPermissionStatus(status);
}
final status = await perm.permission.status;
return AppPermissionStatus.fromPermissionStatus(status);
} catch (e) {
@@ -485,7 +517,13 @@ class PermissionService {
return _requestTrackingPermission(context);
}
try {
final status = await perm.permission.status;
// 鸿蒙端添加超时保护,防止 permission_handler 挂起
final status = pu.isOhos
? await perm.permission.status.timeout(
const Duration(seconds: 3),
onTimeout: () => PermissionStatus.denied,
)
: await perm.permission.status;
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
@@ -504,6 +542,33 @@ class PermissionService {
if (!userConfirmed) return false;
}
// 鸿蒙端如果permission_handler不支持引导用户去系统设置
if (pu.isOhos) {
try {
final result = await perm.permission.request().timeout(
const Duration(seconds: 5),
onTimeout: () => PermissionStatus.denied,
);
if (result.isGranted || result.isLimited) {
_log.i('${perm.name} 权限已授予');
return true;
}
// 鸿蒙端请求后仍是denied/notDetermined引导用户去系统设置
_log.w('⚠️ 鸿蒙端 ${perm.name} 权限未授予(${result.name}),引导用户前往系统设置');
if (context.mounted) {
_showSettingsDialog(context, perm);
}
return false;
} on MissingPluginException {
// permission_handler 未实现该权限,引导去系统设置
_log.w('⚠️ 鸿蒙端 ${perm.name} permission_handler通道未实现引导去系统设置');
if (context.mounted) {
_showSettingsDialog(context, perm);
}
return false;
}
}
final result = await perm.permission.request();
if (result.isGranted) {
_log.i('${perm.name} 权限已授予');
@@ -618,7 +683,8 @@ class PermissionService {
if (!Platform.isIOS) return true;
try {
// 先检查当前状态
final currentStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
final currentStatus =
await AppTrackingTransparency.trackingAuthorizationStatus;
if (currentStatus == TrackingStatus.authorized) {
_log.i('ATT已授权无需重复请求');
return true;
@@ -842,7 +908,7 @@ class PermissionUsageStat {
factory PermissionUsageStat.fromJson(Map<String, dynamic> json) {
return PermissionUsageStat(
count: json['count'] as int? ?? 0,
count: (json['count'] as num?)?.toInt() ?? 0,
lastUsed: json['lastUsed'] as String? ?? '',
firstUsed: json['firstUsed'] as String?,
);

View File

@@ -48,11 +48,11 @@ class TokenService {
'/api/token/check',
);
final data = response.data as Map<String, dynamic>;
final code = data['code'] as int? ?? 0;
final code = (data['code'] as num?)?.toInt() ?? 0;
if (code == 1) {
final tokenData = data['data'] as Map<String, dynamic>? ?? {};
final expiresIn = tokenData['expires_in'] as int? ?? 0;
final expiresIn = (tokenData['expires_in'] as num?)?.toInt() ?? 0;
return TokenCheckResult(
valid: true,
expiresIn: expiresIn,
@@ -76,7 +76,7 @@ class TokenService {
'/api/token/refresh',
);
final data = response.data as Map<String, dynamic>;
final code = data['code'] as int? ?? 0;
final code = (data['code'] as num?)?.toInt() ?? 0;
if (code == 1) {
final tokenData = data['data'] as Map<String, dynamic>?;

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — Catcher2 配置服务
/// 创建时间: 2026-05-21
/// 更新时间: 2026-06-05
/// 更新时间: 2026-06-06
/// 作用: 统一管理 Catcher2 异常捕获开关与动态配置更新
/// 上次更新: 修复Zone mismatch不使用runAppFunction手动调用runApp
/// 上次更新: 错误弹窗中文化改英文、复制显示字数上限提升至2000
/// ============================================================
import 'package:catcher_2/catcher_2.dart';
@@ -101,6 +101,13 @@ class CopyableDialogReportMode extends ReportMode {
}
Future<void> _showDialog(Report report, BuildContext? context) async {
// 布局溢出错误不弹窗
final errorStr = report.error.toString();
if (errorStr.contains('overflowed') || errorStr.contains('RenderFlex')) {
onActionConfirmed(report); // 自动确认,不弹窗
return;
}
await Future<void>.delayed(Duration.zero);
if (context != null && context.mounted) {
showCupertinoDialog<void>(
@@ -157,7 +164,12 @@ class _CopyableErrorDialog extends StatelessWidget {
}
void _copyToClipboard(BuildContext context) {
Clipboard.setData(ClipboardData(text: _errorText));
// 限制剪贴板内容不超过400字避免超长文本占用剪贴板
final text = _errorText;
final clipboardText = text.length > 400
? '${text.substring(0, 400)}...'
: text;
Clipboard.setData(ClipboardData(text: clipboardText));
HapticFeedback.lightImpact();
showCupertinoDialog<void>(
context: context,
@@ -165,15 +177,12 @@ class _CopyableErrorDialog extends StatelessWidget {
builder: (ctx) => CupertinoAlertDialog(
content: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
'✅ 已复制到剪贴板\n标识: $_errorId',
style: const TextStyle(fontSize: 14),
),
child: Text('已复制到剪贴板', style: const TextStyle(fontSize: 14)),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('好的'),
child: const Text('OK'),
),
],
),
@@ -183,14 +192,14 @@ class _CopyableErrorDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final errorStr = report.error.toString();
final displayError = errorStr.length > 300
? '${errorStr.substring(0, 300)}...'
final displayError = errorStr.length > 2000
? '${errorStr.substring(0, 2000)}...'
: errorStr;
return CupertinoAlertDialog(
title: Column(
children: [
const Text('⚠️ 应用异常'),
const Text('App Error'),
const SizedBox(height: 4),
Text(
_errorId,
@@ -220,7 +229,7 @@ class _CopyableErrorDialog extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'时间: ${report.dateTime.toIso8601String()}',
'Time: ${report.dateTime.toIso8601String()}',
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
@@ -231,17 +240,17 @@ class _CopyableErrorDialog extends StatelessWidget {
actions: [
CupertinoDialogAction(
onPressed: () => _copyToClipboard(context),
child: const Text('📋 复制详情'),
child: const Text('Copy Details'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: onReject,
child: const Text('忽略'),
child: const Text('Dismiss'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: onAccept,
child: const Text('确认'),
child: const Text('Confirm'),
),
],
);

View File

@@ -377,7 +377,7 @@ class ImageCacheMetadataService {
static int getCacheSizeLimit() {
final box = _safeBox();
if (box == null) return 100;
return box.get('_cache_size_limit') as int? ?? 100;
return (box.get('_cache_size_limit') as num?)?.toInt() ?? 100;
}
static Future<void> setCacheSizeLimit(int limitMB) async {

View File

@@ -85,7 +85,7 @@ class SettingsExportService {
return false;
}
final version = decoded['export_version'] as int?;
final version = (decoded['export_version'] as num?)?.toInt();
if (version == null) {
Log.w('设置导入: 缺少版本号');
return false;

View File

@@ -1,19 +1,22 @@
/// ============================================================
/// 闲言APP — 日历同步服务
/// 创建时间: 2026-05-29
/// 更新时间: 2026-05-29
/// 更新时间: 2026-06-06
/// 作用: 跨平台日历事件同步Android/iOS/HarmonyOS/macOS/Windows
/// 上次更新: 修复analyze错误(unnecessary_import/prefer_final_locals/errorMessages→errors)
/// 上次更新: 鸿蒙端MethodChannel添加超时保护+MissingPluginException捕获+平台判断早期返回
/// ============================================================
import 'dart:io';
import 'dart:async';
import 'package:device_calendar/device_calendar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
/// 日历事件数据模型
class CalendarEvent {
@@ -65,8 +68,10 @@ class CalendarService {
// ============================================================
/// 请求日历权限,成功后自动创建/获取日历
/// 鸿蒙端日历通道可能未实现通过_requestPermissionOhos()处理
Future<bool> requestPermission() async {
try {
// 鸿蒙端直接走鸿蒙通道通道未实现时返回false
if (_isOhos()) {
return await _requestPermissionOhos();
}
@@ -92,14 +97,20 @@ class CalendarService {
}
}
/// HarmonyOS权限请求
/// HarmonyOS权限请求 — 添加超时保护+MissingPluginException捕获
Future<bool> _requestPermissionOhos() async {
try {
final result = await _ohosChannel.invokeMethod<bool>('requestPermission');
final result = await _ohosChannel
.invokeMethod<bool>('requestPermission')
.timeout(const Duration(seconds: 5), onTimeout: () => false);
_isAvailable = result ?? false;
return _isAvailable;
} on MissingPluginException {
Log.w('CalendarService: 鸿蒙端日历权限通道未实现');
_isAvailable = false;
return false;
} catch (e) {
Log.e('CalendarService: OHOS日历权限请求失败', e);
Log.e('CalendarService: 鸿蒙端请求日历权限失败', e);
_isAvailable = false;
return false;
}
@@ -136,7 +147,9 @@ class CalendarService {
// ============================================================
/// 添加日历事件
/// 鸿蒙端通过_addEventOhos()桥接通道未实现时返回false
Future<bool> addEvent(CalendarEvent event) async {
// 鸿蒙端:直接走鸿蒙通道
if (_isOhos()) {
return _addEventOhos(event);
}
@@ -178,20 +191,25 @@ class CalendarService {
}
}
/// HarmonyOS添加事件
/// HarmonyOS添加事件 — 添加超时保护+MissingPluginException捕获
Future<bool> _addEventOhos(CalendarEvent event) async {
try {
final result = await _ohosChannel.invokeMethod<bool>('addEvent', {
'title': event.title,
'description': event.description,
'startTime': event.start.millisecondsSinceEpoch,
'endTime': event.end.millisecondsSinceEpoch,
'reminderMinutes': event.reminderMinutesBefore,
'location': event.location,
});
final result = await _ohosChannel
.invokeMethod<bool>('addEvent', {
'title': event.title,
'description': event.description,
'startTime': event.start.millisecondsSinceEpoch,
'endTime': event.end.millisecondsSinceEpoch,
'reminderMinutes': event.reminderMinutesBefore,
'location': event.location,
})
.timeout(const Duration(seconds: 5), onTimeout: () => false);
return result ?? false;
} on MissingPluginException {
Log.w('CalendarService: 鸿蒙端日历事件通道未实现');
return false;
} catch (e) {
Log.e('CalendarService: OHOS日历桥接失败', e);
Log.e('CalendarService: 鸿蒙端添加日历事件失败', e);
return false;
}
}
@@ -213,11 +231,5 @@ class CalendarService {
// ============================================================
/// 是否为HarmonyOS平台
bool _isOhos() {
try {
return Platform.operatingSystem == 'ohos';
} catch (_) {
return false;
}
}
bool _isOhos() => pu.isOhos;
}

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 快捷操作服务
// 创建时间: 2026-05-31
/// 更新时间: 2026-06-05
/// 更新时间: 2026-06-06
/// 作用: 管理主屏幕快捷操作(Quick Actions / App Shortcuts)
/// 上次更新: 新增PlatformCapabilities能力查询补充quickActions判断
/// 上次更新: 修复安卓端冷启动快捷方式闪退,添加延迟和异常捕获
// 跨平台: iOS(UIApplicationShortcutItems) + Android(App Shortcuts)
// + 鸿蒙(module.json5 shortcuts + MethodChannel)
// ============================================================
@@ -119,7 +119,14 @@ class QuickActionsService {
if (route == null) return;
if (onAction != null) {
onAction!(route);
// 延迟执行确保路由系统已完全初始化冷启动时回调可能早于GoRouter就绪
Future.delayed(const Duration(milliseconds: 500), () {
try {
onAction!(route);
} catch (e) {
Log.e('🚀 [QuickActions] 快捷方式导航失败', e);
}
});
_pendingAction = null;
return;
}

View File

@@ -398,7 +398,7 @@ class RemoteFeatureFlagService {
.map((f) => FeatureFlagItem.fromJson(f as Map<String, dynamic>))
.toList();
final timestamp = box.get(_cacheTimestampKey) as int?;
final timestamp = (box.get(_cacheTimestampKey) as num?)?.toInt();
if (timestamp != null) {
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
if (DateTime.now().difference(cacheTime) > _cacheExpiry) {

View File

@@ -125,7 +125,7 @@ class IpLocationService {
final body = response.data;
if (body == null) return null;
final code = body['code'] as int? ?? 0;
final code = (body['code'] as num?)?.toInt() ?? 0;
if (code != 1) {
Log.w('IpLocation: API返回错误 ${body['msg']}');
return null;
@@ -154,7 +154,7 @@ class IpLocationService {
if (raw == null || raw.isEmpty) return null;
final map = jsonDecode(raw) as Map<String, dynamic>;
final cachedTime = map['_cache_time'] as int? ?? 0;
final cachedTime = (map['_cache_time'] as num?)?.toInt() ?? 0;
final now = DateTime.now().millisecondsSinceEpoch;
if (now - cachedTime > _cacheExpiry.inMilliseconds) {

View File

@@ -52,7 +52,7 @@ class SharedReadlaterList {
?.map((e) => e.toString())
.toList() ??
[],
messageCount: json['message_count'] as int? ?? 0,
messageCount: (json['message_count'] as num?)?.toInt() ?? 0,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: DateTime.now(),

View File

@@ -234,8 +234,8 @@ class ReadlaterDeviceSyncService {
source: source,
feedType: meta['feedType'] as String?,
feedName: meta['feedName'] as String?,
likeCount: meta['likeCount'] as int?,
views: meta['views'] as int?,
likeCount: (meta['likeCount'] as num?)?.toInt(),
views: (meta['views'] as num?)?.toInt(),
sentenceId: meta['sentenceId'] as String?,
);
} else if (msgType == 'link') {
@@ -253,7 +253,7 @@ class ReadlaterDeviceSyncService {
fileName: meta['fileName'] as String? ?? '未知文档',
filePath: meta['filePath'] as String? ?? '',
fileType: meta['mimeType'] as String? ?? 'application/octet-stream',
fileSize: meta['fileSize'] as int? ?? 0,
fileSize: (meta['fileSize'] as num?)?.toInt() ?? 0,
);
} else {
await ChatMessageService.sendText(

View File

@@ -155,7 +155,7 @@ class ReadlaterSyncService {
DateTime.now().toIso8601String(),
),
isRead: metaMap['isRead'] as bool? ?? false,
readCount: metaMap['readCount'] as int? ?? 0,
readCount: (metaMap['readCount'] as num?)?.toInt() ?? 0,
meta: metaMap['meta'] as Map<String, dynamic>?,
ext: metaMap['ext'] as Map<String, dynamic>?,
attachments:

View File

@@ -64,7 +64,7 @@ class SafeSharedMediaFile {
path: rawPath as String,
type: mediaType,
thumbnail: json['thumbnail'] as String?,
duration: json['duration'] as int?,
duration: (json['duration'] as num?)?.toInt(),
mimeType: json['mimeType'] as String?,
message: json['message'] as String?,
uri: json['uri'] as String?,

View File

@@ -534,8 +534,12 @@ class KvStorage {
static Future<bool> markOnboardingCompleted() =>
setBool(StorageKeys.onboardingCompleted, true);
static bool get shouldShowOnboarding =>
getBool(StorageKeys.showOnboarding) ?? true;
/// 是否应显示引导页
/// KvStorage未就绪时返回false安全默认值不显示引导避免引导页重复弹出
static bool get shouldShowOnboarding {
if (!isReady) return false;
return getBool(StorageKeys.showOnboarding) ?? true;
}
static Future<bool> setShowOnboarding(bool value) =>
setBool(StorageKeys.showOnboarding, value);

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 日志工具
/// 创建时间: 2026-04-20
/// 更新时间: 2026-06-05
/// 更新时间: 2026-06-06
/// 作用: 统一日志封装,支持分级与格式化 + 日志查看器 + 日志导出 + 按模块分类控制
/// 上次更新: 添加 LogCategory 结构化日志,按模块控制日志级别
/// 上次更新: 优化日志输出简洁Printer模式 + 提高高频分类默认日志级别
/// ============================================================
import 'dart:convert';
@@ -15,8 +15,16 @@ import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// 全局日志器release模式仅输出error级别避免敏感信息泄露
/// 简洁模式减少调用栈、关闭颜色和emoji、缩短行宽
final appLogger = Logger(
printer: PrettyPrinter(),
printer: PrettyPrinter(
methodCount: 0, // 不打印调用栈
errorMethodCount: 5, // 错误时只打印5层调用栈
lineLength: 80, // 缩短行宽
// colors: true, // 颜色减少ANSI转义序列
printEmojis: false, // 关闭emoji减少输出
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, // 只显示时间
),
level: _isDebugMode ? Level.debug : Level.error,
);
@@ -43,6 +51,7 @@ enum LogLevel {
/// 级别中文名
final String label;
/// 级别数值(越大越严格)
final int value;
}
@@ -53,28 +62,29 @@ enum LogLevel {
/// 作用: 支持按模块设置日志级别,调试时可只关注特定模块
/// ============================================================
enum LogCategory {
ui('UI', LogLevel.debug), // UI渲染、布局
network('网络', LogLevel.info), // API请求、响应
router('路由', LogLevel.info), // 页面导航
storage('存储', LogLevel.info), // 数据库、KV存储
device('设备', LogLevel.info), // 设备信息、传感器
auth('认证', LogLevel.info), // 登录、鉴权
transfer('传输', LogLevel.info), // 文件传输
search('搜索', LogLevel.info), // 搜索功能
chart('图表', LogLevel.warning), // Syncfusion图表减少日志噪音
haptic('触觉', LogLevel.warning), // 震动反馈(高频调用)
provider('状态', LogLevel.info), // Riverpod Provider
service('服务', LogLevel.info), // 后台服务
sync('同步', LogLevel.info), // 数据同步
offline('离线', LogLevel.info), // 离线模式
onboarding('引导', LogLevel.info), // 引导页
push('推送', LogLevel.warning), // 推送通知
general('通用', LogLevel.debug); // 其他
ui('UI', LogLevel.warning), // UI渲染、布局减少高频UI日志
network('网络', LogLevel.warning), // API请求、响应(减少网络日志噪音)
router('路由', LogLevel.warning), // 页面导航(减少路由日志)
storage('存储', LogLevel.warning), // 数据库、KV存储(减少存储日志)
device('设备', LogLevel.warning), // 设备信息、传感器(减少设备日志)
auth('认证', LogLevel.info), // 登录、鉴权保留info级别关注认证流程
transfer('传输', LogLevel.info), // 文件传输保留info级别
search('搜索', LogLevel.warning), // 搜索功能(减少搜索日志)
chart('图表', LogLevel.error), // Syncfusion图表仅输出错误
haptic('触觉', LogLevel.error), // 震动反馈(仅输出错误,极高频调用)
provider('状态', LogLevel.warning), // Riverpod Provider(减少状态日志)
service('服务', LogLevel.info), // 后台服务保留info级别
sync('同步', LogLevel.info), // 数据同步保留info级别
offline('离线', LogLevel.info), // 离线模式保留info级别
onboarding('引导', LogLevel.warning), // 引导页(减少引导日志)
push('推送', LogLevel.error), // 推送通知(仅输出错误)
general('通用', LogLevel.warning); // 其他(减少通用日志)
const LogCategory(this.label, this.defaultLevel);
/// 分类中文名
final String label;
/// 默认日志级别
final LogLevel defaultLevel;
@@ -111,17 +121,19 @@ enum LogCategory {
/// 获取所有自定义级别(用于持久化)
static Map<String, String> exportCustomLevels() {
return _customLevels.map(
(k, v) => MapEntry(k.name, v.name),
);
return _customLevels.map((k, v) => MapEntry(k.name, v.name));
}
/// 从持久化数据恢复自定义级别
static void importCustomLevels(Map<String, String> data) {
_customLevels.clear();
for (final entry in data.entries) {
final cat = LogCategory.values.where((c) => c.name == entry.key).firstOrNull;
final level = LogLevel.values.where((l) => l.name == entry.value).firstOrNull;
final cat = LogCategory.values
.where((c) => c.name == entry.key)
.firstOrNull;
final level = LogLevel.values
.where((l) => l.name == entry.value)
.firstOrNull;
if (cat != null && level != null) {
_customLevels[cat] = level;
}
@@ -143,6 +155,7 @@ class LogEntry {
final LogLevel level;
final String message;
final DateTime time;
/// 日志分类
final LogCategory category;
final dynamic error;
@@ -212,18 +225,32 @@ class Log {
/// 调试日志release模式下不输出
/// [category] 日志分类,默认为 LogCategory.general
static void d(dynamic message, [dynamic error, StackTrace? stackTrace, LogCategory? category]) {
static void d(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.debug, cat)) return;
appLogger.d(_formatMessage(message, cat), error: error, stackTrace: stackTrace);
appLogger.d(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.debug, message, error, stackTrace, cat);
}
/// 信息日志release模式下不输出
/// 添加节流机制同一消息5秒内不重复输出到控制台
/// [category] 日志分类,默认为 LogCategory.general
static void i(dynamic message, [dynamic error, StackTrace? stackTrace, LogCategory? category]) {
static void i(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.info, cat)) {
@@ -236,7 +263,11 @@ class Log {
final now = DateTime.now();
final lastTime = _infoThrottleMap[msgStr];
if (lastTime == null || now.difference(lastTime) > _infoThrottleDuration) {
appLogger.i(_formatMessage(message, cat), error: error, stackTrace: stackTrace);
appLogger.i(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_infoThrottleMap[msgStr] = now;
// 清理过期的节流记录,避免内存泄漏
_infoThrottleMap.removeWhere(
@@ -248,30 +279,57 @@ class Log {
/// 警告日志release模式下不输出
/// [category] 日志分类,默认为 LogCategory.general
static void w(dynamic message, [dynamic error, StackTrace? stackTrace, LogCategory? category]) {
static void w(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
if (!_isDebugMode) return;
final cat = category ?? LogCategory.general;
if (!_shouldLog(LogLevel.warning, cat)) {
_addEntry(LogLevel.warning, message, error, stackTrace, cat);
return;
}
appLogger.w(_formatMessage(message, cat), error: error, stackTrace: stackTrace);
appLogger.w(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.warning, message, error, stackTrace, cat);
}
/// 错误日志(始终输出,错误必须记录)
/// [category] 日志分类,默认为 LogCategory.general
static void e(dynamic message, [dynamic error, StackTrace? stackTrace, LogCategory? category]) {
static void e(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
final cat = category ?? LogCategory.general;
appLogger.e(_formatMessage(message, cat), error: error, stackTrace: stackTrace);
appLogger.e(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.error, message, error, stackTrace, cat);
}
/// 致命错误日志(始终输出,错误必须记录)
/// [category] 日志分类,默认为 LogCategory.general
static void f(dynamic message, [dynamic error, StackTrace? stackTrace, LogCategory? category]) {
static void f(
dynamic message, [
dynamic error,
StackTrace? stackTrace,
LogCategory? category,
]) {
final cat = category ?? LogCategory.general;
appLogger.f(_formatMessage(message, cat), error: error, stackTrace: stackTrace);
appLogger.f(
_formatMessage(message, cat),
error: error,
stackTrace: stackTrace,
);
_addEntry(LogLevel.error, message, error, stackTrace, cat);
}