Files
AutoTime-Tracker/lib/screens/export_data_screen.dart
2025-11-13 15:45:28 +08:00

404 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../theme/app_theme.dart';
import '../services/export_service.dart';
// Web 平台需要的导入
import 'dart:html' as html show Blob, Url, AnchorElement;
class ExportDataScreen extends StatefulWidget {
const ExportDataScreen({super.key});
@override
State<ExportDataScreen> createState() => _ExportDataScreenState();
}
class _ExportDataScreenState extends State<ExportDataScreen> {
final ExportService _exportService = ExportService();
DateTime _startDate = DateTime.now().subtract(const Duration(days: 7));
DateTime _endDate = DateTime.now();
bool _isExporting = false;
Future<void> _exportCSV() async {
setState(() {
_isExporting = true;
});
try {
final csvData = await _exportService.exportToCSV(
startDate: _startDate,
endDate: _endDate,
);
if (csvData.isEmpty || csvData.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('所选日期范围内没有数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([csvData], 'text/csv');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_export_${DateTime.now().millisecondsSinceEpoch}.csv')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: csvData));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '数据已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _exportReport() async {
setState(() {
_isExporting = true;
});
try {
final report = await _exportService.exportStatsReport(
startDate: _startDate,
endDate: _endDate,
);
if (report.isEmpty || report.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('所选日期范围内没有统计数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([report], 'text/plain');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_report_${DateTime.now().millisecondsSinceEpoch}.txt')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: report));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '报告已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _exportTodayReport() async {
setState(() {
_isExporting = true;
});
try {
final report = await _exportService.exportTodayReport();
if (report.isEmpty || report.trim().isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('今日暂无数据可导出'),
backgroundColor: AppTheme.warningColor,
),
);
}
return;
}
if (kIsWeb) {
// Web 平台:下载文件
final blob = html.Blob([report], 'text/plain');
final url = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: url)
..setAttribute('download', 'autotime_today_${DateTime.now().millisecondsSinceEpoch}.txt')
..click();
html.Url.revokeObjectUrl(url);
} else {
// 移动端:复制到剪贴板
await Clipboard.setData(ClipboardData(text: report));
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(kIsWeb ? '文件已下载' : '今日报告已复制到剪贴板'),
backgroundColor: AppTheme.successColor,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getExportErrorMessage(e)),
backgroundColor: AppTheme.errorColor,
action: SnackBarAction(
label: '重试',
textColor: Colors.white,
onPressed: _exportTodayReport,
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
Future<void> _selectDateRange() async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(start: _startDate, end: _endDate),
);
if (picked != null) {
setState(() {
_startDate = picked.start;
_endDate = picked.end;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('数据导出'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 日期范围选择
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择日期范围',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _selectDateRange,
icon: const Icon(Icons.calendar_today),
label: Text(
'${_startDate.toString().split(' ')[0]}${_endDate.toString().split(' ')[0]}',
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// 导出选项
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'导出选项',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// CSV 导出
_buildExportOption(
theme,
icon: Icons.table_chart,
title: '导出 CSV 数据',
subtitle: '导出原始应用使用数据CSV 格式)',
onTap: _exportCSV,
),
const SizedBox(height: 12),
// 统计报告
_buildExportOption(
theme,
icon: Icons.description,
title: '导出统计报告',
subtitle: '导出时间范围内的统计报告(文本格式)',
onTap: _exportReport,
),
const SizedBox(height: 12),
// 今日报告
_buildExportOption(
theme,
icon: Icons.today,
title: '导出今日报告',
subtitle: '导出今日的详细统计报告',
onTap: _exportTodayReport,
),
],
),
),
),
if (_isExporting)
const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
);
}
Widget _buildExportOption(
ThemeData theme, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return InkWell(
onTap: _isExporting ? null : onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: theme.colorScheme.onSurface.withOpacity(0.1),
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(icon, color: AppTheme.primaryColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
);
}
String _getExportErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败,请稍后重试';
} else if (errorString.contains('file') || errorString.contains('文件')) {
return '文件操作失败,请检查存储权限';
}
return '导出失败,请稍后重试';
}
}