此提交包含多项变更: 1. 新增鸿蒙平台支持,完善设备检测与数据库适配 2. 替换旧版分享插件API为SharePlus 3. 批量迁移StateNotifier到Notifier以适配新版Riverpod 4. 修复zip编码判断、图表API参数等bug 5. 更新应用图标、启动页资源与多尺寸适配图标 6. 调整Android最小SDK版本与应用名称 7. 优化日志打印与正则表达式使用 8. 修正编辑器画布样式初始化与配置逻辑 9. 更新依赖与CI插件配置
199 lines
5.7 KiB
Dart
199 lines
5.7 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 日志工具
|
|
/// 创建时间: 2026-04-20
|
|
/// 更新时间: 2026-05-07
|
|
/// 作用: 统一日志封装,支持分级与格式化 + 日志查看器 + 日志导出
|
|
/// 上次更新: 新增日志导出文件功能 + 按级别过滤
|
|
/// ============================================================
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:logger/logger.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
/// 全局日志器
|
|
final appLogger = Logger(printer: PrettyPrinter(), level: Level.debug);
|
|
|
|
/// 日志级别
|
|
enum LogLevel {
|
|
all('全部', 0),
|
|
verbose('详细', 1),
|
|
debug('调试', 2),
|
|
info('信息', 3),
|
|
warning('警告', 4),
|
|
error('错误', 5);
|
|
|
|
const LogLevel(this.label, this.value);
|
|
|
|
final String label;
|
|
final int value;
|
|
}
|
|
|
|
/// 日志条目
|
|
class LogEntry {
|
|
const LogEntry({
|
|
required this.level,
|
|
required this.message,
|
|
required this.time,
|
|
this.error,
|
|
this.stackTrace,
|
|
});
|
|
|
|
final LogLevel level;
|
|
final String message;
|
|
final DateTime time;
|
|
final dynamic error;
|
|
final StackTrace? stackTrace;
|
|
}
|
|
|
|
/// 日志工具 — 静态便捷方法 + 内存缓冲
|
|
class Log {
|
|
Log._();
|
|
|
|
static const _maxEntries = 500;
|
|
static final List<LogEntry> _entries = [];
|
|
|
|
/// 获取所有日志条目
|
|
static List<LogEntry> get entries => List.unmodifiable(_entries);
|
|
|
|
/// 日志条目数量
|
|
static int get entryCount => _entries.length;
|
|
|
|
/// 清空日志
|
|
static void clearEntries() => _entries.clear();
|
|
|
|
static void _addEntry(
|
|
LogLevel level,
|
|
dynamic message, [
|
|
dynamic error,
|
|
StackTrace? stackTrace,
|
|
]) {
|
|
_entries.add(
|
|
LogEntry(
|
|
level: level,
|
|
message: message?.toString() ?? '',
|
|
time: DateTime.now(),
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
),
|
|
);
|
|
if (_entries.length > _maxEntries) {
|
|
_entries.removeRange(0, _entries.length - _maxEntries);
|
|
}
|
|
}
|
|
|
|
/// 调试日志
|
|
static void d(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
|
appLogger.d(message, error: error, stackTrace: stackTrace);
|
|
_addEntry(LogLevel.debug, message, error, stackTrace);
|
|
}
|
|
|
|
/// 信息日志
|
|
static void i(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
|
appLogger.i(message, error: error, stackTrace: stackTrace);
|
|
_addEntry(LogLevel.info, message, error, stackTrace);
|
|
}
|
|
|
|
/// 警告日志
|
|
static void w(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
|
appLogger.w(message, error: error, stackTrace: stackTrace);
|
|
_addEntry(LogLevel.warning, message, error, stackTrace);
|
|
}
|
|
|
|
/// 错误日志
|
|
static void e(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
|
appLogger.e(message, error: error, stackTrace: stackTrace);
|
|
_addEntry(LogLevel.error, message, error, stackTrace);
|
|
}
|
|
|
|
/// 致命错误日志
|
|
static void f(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
|
appLogger.f(message, error: error, stackTrace: stackTrace);
|
|
_addEntry(LogLevel.error, message, error, stackTrace);
|
|
}
|
|
|
|
/// 按级别过滤日志
|
|
static List<LogEntry> filterByLevel(LogLevel level) {
|
|
if (level == LogLevel.all) return entries;
|
|
return _entries.where((e) => e.level.value >= level.value).toList();
|
|
}
|
|
|
|
/// 导出日志为 JSON 字符串
|
|
static String exportToJson({LogLevel level = LogLevel.all}) {
|
|
final filtered = filterByLevel(level);
|
|
final data = filtered
|
|
.map(
|
|
(e) => {
|
|
'time': e.time.toIso8601String(),
|
|
'level': e.level.label,
|
|
'message': e.message,
|
|
if (e.error != null) 'error': e.error.toString(),
|
|
if (e.stackTrace != null) 'stackTrace': e.stackTrace.toString(),
|
|
},
|
|
)
|
|
.toList();
|
|
|
|
return const JsonEncoder.withIndent(' ').convert({
|
|
'export_time': DateTime.now().toIso8601String(),
|
|
'app_name': '闲言',
|
|
'log_count': data.length,
|
|
'filter_level': level.label,
|
|
'entries': data,
|
|
});
|
|
}
|
|
|
|
/// 导出日志为纯文本
|
|
static String exportToText({LogLevel level = LogLevel.all}) {
|
|
final filtered = filterByLevel(level);
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('闲言APP 日志导出');
|
|
buffer.writeln('导出时间: ${DateTime.now().toIso8601String()}');
|
|
buffer.writeln('过滤级别: ${level.label}');
|
|
buffer.writeln('条目数量: ${filtered.length}');
|
|
buffer.writeln('=' * 60);
|
|
|
|
for (final e in filtered) {
|
|
buffer.writeln(
|
|
'[${e.time.toString().substring(0, 19)}] [${e.level.label}] ${e.message}',
|
|
);
|
|
if (e.error != null) buffer.writeln(' Error: ${e.error}');
|
|
if (e.stackTrace != null) buffer.writeln(' Stack: ${e.stackTrace}');
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// 导出日志到文件
|
|
static Future<String> exportToFile({
|
|
LogLevel level = LogLevel.all,
|
|
bool asJson = true,
|
|
}) async {
|
|
try {
|
|
final content = asJson
|
|
? exportToJson(level: level)
|
|
: exportToText(level: level);
|
|
final ext = asJson ? 'json' : 'txt';
|
|
final dir = await getTemporaryDirectory();
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final file = File('${dir.path}/xianyan_logs_$timestamp.$ext');
|
|
await file.writeAsString(content);
|
|
return file.path;
|
|
} catch (e) {
|
|
appLogger.e('日志导出失败', error: e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// 分享日志文件
|
|
static Future<void> shareLogs({LogLevel level = LogLevel.all}) async {
|
|
try {
|
|
final path = await exportToFile(level: level);
|
|
await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '闲言APP 日志文件'));
|
|
} catch (e) {
|
|
appLogger.e('日志分享失败', error: e);
|
|
}
|
|
}
|
|
}
|