24 changed files with 1268 additions and 308 deletions
@ -0,0 +1,9 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/log_export_controller.dart'; |
||||
|
|
||||
|
class LogExportBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.lazyPut<LogExportController>(() => LogExportController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
import 'dart:io'; |
||||
|
|
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:share_plus/share_plus.dart'; |
||||
|
|
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
|
||||
|
class LogExportController extends GetxController { |
||||
|
final RxList<LogFileInfo> logFiles = <LogFileInfo>[].obs; |
||||
|
final RxBool isLoading = false.obs; |
||||
|
final RxString totalSize = ''.obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
loadLogFiles(); |
||||
|
} |
||||
|
|
||||
|
/// 加载日志文件列表 |
||||
|
Future<void> loadLogFiles() async { |
||||
|
isLoading.value = true; |
||||
|
try { |
||||
|
final files = await Logger.getLogFiles(); |
||||
|
int total = 0; |
||||
|
logFiles.clear(); |
||||
|
for (final file in files) { |
||||
|
final stat = await file.stat(); |
||||
|
total += stat.size; |
||||
|
logFiles.add(LogFileInfo( |
||||
|
file: file, |
||||
|
name: file.path.split('/').last, |
||||
|
size: _formatSize(stat.size), |
||||
|
modifiedTime: stat.modified, |
||||
|
)); |
||||
|
} |
||||
|
totalSize.value = _formatSize(total); |
||||
|
} catch (e) { |
||||
|
Logger.e('LogExport', '加载日志文件失败: $e'); |
||||
|
} finally { |
||||
|
isLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 导出所有日志 |
||||
|
Future<void> exportAllLogs() async { |
||||
|
if (logFiles.isEmpty) { |
||||
|
Get.snackbar('提示', '暂无日志文件'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await Logger.flush(); |
||||
|
|
||||
|
final xFiles = logFiles |
||||
|
.map((logFile) => XFile(logFile.file.path)) |
||||
|
.toList(); |
||||
|
|
||||
|
await SharePlus.instance.share(ShareParams( |
||||
|
files: xFiles, |
||||
|
text: '应用运行日志', |
||||
|
)); |
||||
|
} catch (e) { |
||||
|
Logger.e('LogExport', '导出日志失败: $e'); |
||||
|
Get.snackbar('错误', '导出日志失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 分享单个日志文件 |
||||
|
Future<void> shareSingleLog(LogFileInfo logFile) async { |
||||
|
try { |
||||
|
await Logger.flush(); |
||||
|
await SharePlus.instance.share(ShareParams( |
||||
|
files: [XFile(logFile.file.path)], |
||||
|
text: '应用日志: ${logFile.name}', |
||||
|
)); |
||||
|
} catch (e) { |
||||
|
Logger.e('LogExport', '分享日志失败: $e'); |
||||
|
Get.snackbar('错误', '分享日志失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 查看单个日志文件内容(最后200行) |
||||
|
Future<String> readLogContent(LogFileInfo logFile) async { |
||||
|
try { |
||||
|
await Logger.flush(); |
||||
|
final lines = await logFile.file.readAsLines(); |
||||
|
final start = lines.length > 200 ? lines.length - 200 : 0; |
||||
|
return lines.sublist(start).join('\n'); |
||||
|
} catch (e) { |
||||
|
return '读取日志失败: $e'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 清除所有日志 |
||||
|
Future<void> clearAllLogs() async { |
||||
|
try { |
||||
|
for (final logFile in logFiles) { |
||||
|
if (await logFile.file.exists()) { |
||||
|
await logFile.file.delete(); |
||||
|
} |
||||
|
} |
||||
|
logFiles.clear(); |
||||
|
totalSize.value = '0 B'; |
||||
|
Get.snackbar('提示', '日志已清除'); |
||||
|
} catch (e) { |
||||
|
Logger.e('LogExport', '清除日志失败: $e'); |
||||
|
Get.snackbar('错误', '清除日志失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
String _formatSize(int bytes) { |
||||
|
if (bytes < 1024) return '$bytes B'; |
||||
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; |
||||
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class LogFileInfo { |
||||
|
final File file; |
||||
|
final String name; |
||||
|
final String size; |
||||
|
final DateTime modifiedTime; |
||||
|
|
||||
|
LogFileInfo({ |
||||
|
required this.file, |
||||
|
required this.name, |
||||
|
required this.size, |
||||
|
required this.modifiedTime, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,271 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:intl/intl.dart'; |
||||
|
|
||||
|
import '../controllers/log_export_controller.dart'; |
||||
|
|
||||
|
class LogExportView extends GetView<LogExportController> { |
||||
|
const LogExportView({super.key}); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Scaffold( |
||||
|
backgroundColor: isDarkMode ? const Color(0xFF1A1A2E) : Colors.grey[50], |
||||
|
appBar: AppBar( |
||||
|
title: Text( |
||||
|
'运行日志', |
||||
|
style: TextStyle(fontSize: 18.sp), |
||||
|
), |
||||
|
backgroundColor: isDarkMode ? const Color(0xFF1A1A2E) : Colors.white, |
||||
|
actions: [ |
||||
|
Obx(() => controller.logFiles.isNotEmpty |
||||
|
? IconButton( |
||||
|
icon: Icon(Icons.delete_outline, size: 22.sp), |
||||
|
onPressed: () => _showClearConfirm(context, isDarkMode), |
||||
|
) |
||||
|
: const SizedBox.shrink()), |
||||
|
], |
||||
|
), |
||||
|
body: Obx(() { |
||||
|
if (controller.isLoading.value) { |
||||
|
return const Center(child: CircularProgressIndicator()); |
||||
|
} |
||||
|
|
||||
|
if (controller.logFiles.isEmpty) { |
||||
|
return Center( |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Icon(Icons.article_outlined, |
||||
|
size: 64.sp, |
||||
|
color: isDarkMode ? Colors.white30 : Colors.grey[300]), |
||||
|
SizedBox(height: 16.h), |
||||
|
Text( |
||||
|
'暂无日志文件', |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: isDarkMode ? Colors.white54 : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return Column( |
||||
|
children: [ |
||||
|
// 顶部信息和导出按钮 |
||||
|
Container( |
||||
|
margin: EdgeInsets.all(16.w), |
||||
|
padding: EdgeInsets.all(16.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? Colors.grey[850] : Colors.white, |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Icon(Icons.folder_outlined, |
||||
|
color: isDarkMode ? Colors.white70 : Colors.grey[600], |
||||
|
size: 24.sp), |
||||
|
SizedBox(width: 12.w), |
||||
|
Expanded( |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
'${controller.logFiles.length} 个日志文件', |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 2.h), |
||||
|
Obx(() => Text( |
||||
|
'总大小: ${controller.totalSize.value}', |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode |
||||
|
? Colors.white54 |
||||
|
: Colors.grey[600], |
||||
|
), |
||||
|
)), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
ElevatedButton.icon( |
||||
|
onPressed: controller.exportAllLogs, |
||||
|
icon: Icon(Icons.ios_share, size: 18.sp), |
||||
|
label: Text('导出全部', style: TextStyle(fontSize: 13.sp)), |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: |
||||
|
isDarkMode ? Colors.blue[700] : Colors.blue, |
||||
|
foregroundColor: Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(8.r), |
||||
|
), |
||||
|
padding: EdgeInsets.symmetric( |
||||
|
horizontal: 16.w, vertical: 10.h), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
|
||||
|
// 日志文件列表 |
||||
|
Expanded( |
||||
|
child: ListView.separated( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w), |
||||
|
itemCount: controller.logFiles.length, |
||||
|
separatorBuilder: (_, __) => SizedBox(height: 8.h), |
||||
|
itemBuilder: (context, index) { |
||||
|
final logFile = controller.logFiles[index]; |
||||
|
return _buildLogFileItem( |
||||
|
context, logFile, isDarkMode); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _buildLogFileItem( |
||||
|
BuildContext context, LogFileInfo logFile, bool isDarkMode) { |
||||
|
final timeStr = |
||||
|
DateFormat('yyyy-MM-dd HH:mm').format(logFile.modifiedTime); |
||||
|
|
||||
|
return Container( |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? Colors.grey[850] : Colors.white, |
||||
|
borderRadius: BorderRadius.circular(10.r), |
||||
|
), |
||||
|
child: ListTile( |
||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), |
||||
|
leading: Icon( |
||||
|
Icons.description_outlined, |
||||
|
color: isDarkMode ? Colors.amber[300] : Colors.amber[700], |
||||
|
size: 28.sp, |
||||
|
), |
||||
|
title: Text( |
||||
|
logFile.name, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
subtitle: Text( |
||||
|
'$timeStr | ${logFile.size}', |
||||
|
style: TextStyle( |
||||
|
fontSize: 11.sp, |
||||
|
color: isDarkMode ? Colors.white54 : Colors.grey[600], |
||||
|
), |
||||
|
), |
||||
|
trailing: Row( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
IconButton( |
||||
|
icon: Icon(Icons.visibility_outlined, |
||||
|
size: 20.sp, |
||||
|
color: isDarkMode ? Colors.white54 : Colors.grey[600]), |
||||
|
onPressed: () => _showLogContent(context, logFile, isDarkMode), |
||||
|
), |
||||
|
IconButton( |
||||
|
icon: Icon(Icons.ios_share, |
||||
|
size: 20.sp, |
||||
|
color: isDarkMode ? Colors.blue[300] : Colors.blue), |
||||
|
onPressed: () => controller.shareSingleLog(logFile), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _showLogContent( |
||||
|
BuildContext context, LogFileInfo logFile, bool isDarkMode) async { |
||||
|
final content = await controller.readLogContent(logFile); |
||||
|
Get.bottomSheet( |
||||
|
Container( |
||||
|
height: Get.height * 0.75, |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? const Color(0xFF2A2A3E) : Colors.white, |
||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), |
||||
|
), |
||||
|
child: Column( |
||||
|
children: [ |
||||
|
// 顶部栏 |
||||
|
Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), |
||||
|
decoration: BoxDecoration( |
||||
|
border: Border( |
||||
|
bottom: BorderSide( |
||||
|
color: isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.1) |
||||
|
: Colors.grey[200]!, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Text( |
||||
|
logFile.name, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
IconButton( |
||||
|
icon: Icon(Icons.close, |
||||
|
size: 22.sp, |
||||
|
color: isDarkMode ? Colors.white54 : Colors.grey[600]), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
// 日志内容 |
||||
|
Expanded( |
||||
|
child: SingleChildScrollView( |
||||
|
padding: EdgeInsets.all(12.w), |
||||
|
reverse: true, |
||||
|
child: SelectableText( |
||||
|
content, |
||||
|
style: TextStyle( |
||||
|
fontSize: 10.sp, |
||||
|
fontFamily: 'monospace', |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black87, |
||||
|
height: 1.5, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
isScrollControlled: true, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _showClearConfirm(BuildContext context, bool isDarkMode) { |
||||
|
Get.defaultDialog( |
||||
|
title: '确认清除', |
||||
|
middleText: '确定要清除所有日志文件吗?此操作不可恢复。', |
||||
|
textConfirm: '清除', |
||||
|
textCancel: '取消', |
||||
|
confirmTextColor: Colors.white, |
||||
|
buttonColor: Colors.red, |
||||
|
onConfirm: () { |
||||
|
Get.back(); |
||||
|
controller.clearAllLogs(); |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue