refactor: 兼容后端返回数字类型波动,清理废弃代码
主要变更: 1. 全局修复类型转换问题,将多处`as int?`改为`(num?)?.toInt()`兼容浮点/字符串类型的数字字段 2. 移除废弃的nearby_p2p配对方式和对应的依赖包 3. 优化鸿蒙端快捷方式、引导页、路由导航的稳定性 4. 合并日志输出避免鸿蒙端IDE卡顿 5. 修复安卓端蓝牙权限冗余声明
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
|
||||
@@ -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>?;
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user