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