本次提交包含多项改进: 1. 新增Android启动页资源与配色配置,完善多版本主题适配 2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题 3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现 4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑 5. 移除冗余代码与默认参数,优化小部件性能 6. 新增Web端适配逻辑,处理不支持的原生功能 7. 更新鸿蒙兼容性工具,完善平台识别与路径处理 8. 优化设备注册错误捕获,避免非致命崩溃 9. 添加启动页图标与背景配置,优化首屏体验
222 lines
6.0 KiB
Dart
222 lines
6.0 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 崩溃日志服务
|
||
/// 创建时间: 2026-05-21
|
||
/// 更新时间: 2026-06-05
|
||
/// 作用: 持久化存储崩溃/异常日志,支持增删查清空,自动限制数量
|
||
/// 上次更新: 修复 Web 平台兼容性,kIsWeb 时跳过文件操作,使用内存存储
|
||
/// ============================================================
|
||
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
|
||
import '../utils/logger.dart';
|
||
|
||
class CrashLogEntry {
|
||
const CrashLogEntry({
|
||
required this.id,
|
||
required this.errorId,
|
||
required this.time,
|
||
required this.error,
|
||
required this.stackTrace,
|
||
this.deviceInfo,
|
||
this.appVersion,
|
||
});
|
||
|
||
final String id;
|
||
final String errorId;
|
||
final DateTime time;
|
||
final String error;
|
||
final String stackTrace;
|
||
final String? deviceInfo;
|
||
final String? appVersion;
|
||
|
||
Map<String, dynamic> toJson() => {
|
||
'id': id,
|
||
'errorId': errorId,
|
||
'time': time.toIso8601String(),
|
||
'error': error,
|
||
'stackTrace': stackTrace,
|
||
if (deviceInfo != null) 'deviceInfo': deviceInfo,
|
||
if (appVersion != null) 'appVersion': appVersion,
|
||
};
|
||
|
||
factory CrashLogEntry.fromJson(Map<String, dynamic> json) => CrashLogEntry(
|
||
id: json['id'] as String,
|
||
errorId: json['errorId'] as String,
|
||
time: DateTime.parse(json['time'] as String),
|
||
error: json['error'] as String,
|
||
stackTrace: json['stackTrace'] as String,
|
||
deviceInfo: json['deviceInfo'] as String?,
|
||
appVersion: json['appVersion'] as String?,
|
||
);
|
||
|
||
String toDisplayText() {
|
||
final buffer = StringBuffer();
|
||
buffer.writeln('[$errorId] ${time.toIso8601String()}');
|
||
if (appVersion != null) buffer.writeln('版本: $appVersion');
|
||
if (deviceInfo != null) buffer.writeln('设备: $deviceInfo');
|
||
buffer.writeln();
|
||
buffer.writeln('Error:');
|
||
buffer.writeln(error);
|
||
buffer.writeln();
|
||
buffer.writeln('Stack Trace:');
|
||
buffer.writeln(stackTrace);
|
||
return buffer.toString();
|
||
}
|
||
}
|
||
|
||
class CrashLogService {
|
||
CrashLogService._();
|
||
static final CrashLogService instance = CrashLogService._();
|
||
|
||
static const _maxLogs = 100;
|
||
static const _fileName = 'crash_logs.json';
|
||
|
||
List<CrashLogEntry> _logs = [];
|
||
List<CrashLogEntry> get logs => List.unmodifiable(_logs);
|
||
int get count => _logs.length;
|
||
|
||
bool _loaded = false;
|
||
|
||
/// 获取日志文件,Web 端返回 null
|
||
Future<File?> _getFile() async {
|
||
if (kIsWeb) return null;
|
||
final dir = await getApplicationSupportDirectory();
|
||
return File('${dir.path}/$_fileName');
|
||
}
|
||
|
||
/// 确保日志已加载
|
||
Future<void> ensureLoaded() async {
|
||
if (_loaded) return;
|
||
await load();
|
||
}
|
||
|
||
/// 加载日志,Web 端仅使用内存存储(无持久化)
|
||
Future<void> load() async {
|
||
if (kIsWeb) {
|
||
_logs = [];
|
||
_loaded = true;
|
||
Log.d('CrashLogService: Web 平台,跳过文件加载');
|
||
return;
|
||
}
|
||
try {
|
||
final file = await _getFile();
|
||
if (file == null || !await file.exists()) {
|
||
_logs = [];
|
||
_loaded = true;
|
||
return;
|
||
}
|
||
final content = await file.readAsString();
|
||
final list = jsonDecode(content) as List<dynamic>;
|
||
_logs = list
|
||
.map((e) => CrashLogEntry.fromJson(e as Map<String, dynamic>))
|
||
.toList();
|
||
_logs.sort((a, b) => b.time.compareTo(a.time));
|
||
_loaded = true;
|
||
Log.d('CrashLogService: 已加载 ${_logs.length} 条崩溃日志');
|
||
} catch (e) {
|
||
Log.e('CrashLogService: 加载失败', e);
|
||
_logs = [];
|
||
_loaded = true;
|
||
}
|
||
}
|
||
|
||
/// 保存日志到文件,Web 端跳过
|
||
Future<void> _save() async {
|
||
if (kIsWeb) return;
|
||
try {
|
||
final file = await _getFile();
|
||
if (file == null) return;
|
||
final json = jsonEncode(_logs.map((e) => e.toJson()).toList());
|
||
await file.writeAsString(json);
|
||
} catch (e) {
|
||
Log.e('CrashLogService: 保存失败', e);
|
||
}
|
||
}
|
||
|
||
Future<CrashLogEntry> addLog({
|
||
required String error,
|
||
required String stackTrace,
|
||
String? deviceInfo,
|
||
String? appVersion,
|
||
}) async {
|
||
await ensureLoaded();
|
||
final now = DateTime.now();
|
||
final id = now.millisecondsSinceEpoch.toString();
|
||
final errorId =
|
||
'ERR-${now.millisecondsSinceEpoch.toRadixString(36).toUpperCase()}';
|
||
|
||
final entry = CrashLogEntry(
|
||
id: id,
|
||
errorId: errorId,
|
||
time: now,
|
||
error: error,
|
||
stackTrace: stackTrace,
|
||
deviceInfo: deviceInfo,
|
||
appVersion: appVersion,
|
||
);
|
||
|
||
_logs.insert(0, entry);
|
||
|
||
if (_logs.length > _maxLogs) {
|
||
_logs.removeRange(_maxLogs, _logs.length);
|
||
}
|
||
|
||
await _save();
|
||
Log.d('CrashLogService: 新增崩溃日志 [$errorId]');
|
||
return entry;
|
||
}
|
||
|
||
Future<void> deleteLog(String id) async {
|
||
await ensureLoaded();
|
||
_logs.removeWhere((e) => e.id == id);
|
||
await _save();
|
||
}
|
||
|
||
Future<void> deleteLogs(List<String> ids) async {
|
||
await ensureLoaded();
|
||
final idSet = ids.toSet();
|
||
_logs.removeWhere((e) => idSet.contains(e.id));
|
||
await _save();
|
||
}
|
||
|
||
Future<void> clearAll() async {
|
||
_logs.clear();
|
||
await _save();
|
||
Log.i('CrashLogService: 已清空所有崩溃日志');
|
||
}
|
||
|
||
CrashLogEntry? getById(String id) {
|
||
for (final e in _logs) {
|
||
if (e.id == id) return e;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
Future<String> exportAllAsText() async {
|
||
await ensureLoaded();
|
||
final buffer = StringBuffer();
|
||
buffer.writeln('闲言APP 崩溃日志导出');
|
||
buffer.writeln('导出时间: ${DateTime.now().toIso8601String()}');
|
||
buffer.writeln('条目数量: ${_logs.length}');
|
||
buffer.writeln('=' * 60);
|
||
|
||
for (final entry in _logs) {
|
||
buffer.writeln(entry.toDisplayText());
|
||
buffer.writeln('-' * 60);
|
||
}
|
||
|
||
return buffer.toString();
|
||
}
|
||
}
|
||
|
||
final crashLogServiceProvider = Provider<CrashLogService>((ref) {
|
||
final service = CrashLogService.instance;
|
||
ref.onDispose(() {});
|
||
return service;
|
||
});
|