export_page.dart 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:path_provider/path_provider.dart';
  5. import '../../services/api_service.dart';
  6. import '../../providers/app_provider.dart';
  7. const _channel = MethodChannel('com.tiku/file_picker');
  8. class ExportPage extends ConsumerStatefulWidget {
  9. final int? cartId;
  10. final String? cartName;
  11. const ExportPage({super.key, this.cartId, this.cartName});
  12. @override
  13. ConsumerState<ExportPage> createState() => _ExportPageState();
  14. }
  15. class _ExportPageState extends ConsumerState<ExportPage> {
  16. bool _exporting = false;
  17. Map<String, dynamic>? _result;
  18. Future<void> _doExport() async {
  19. setState(() => _exporting = true);
  20. final api = ref.read(apiServiceProvider);
  21. try {
  22. Map<String, dynamic> data;
  23. if (widget.cartId != null) {
  24. data = await api.exportCart(widget.cartId!, widget.cartName ?? '试题篮导出');
  25. } else {
  26. data = await api.exportChapter(null, null, '全部导出');
  27. }
  28. setState(() { _result = data; _exporting = false; });
  29. } catch (e) {
  30. setState(() => _exporting = false);
  31. if (mounted) {
  32. ScaffoldMessenger.of(context).showSnackBar(
  33. SnackBar(content: Text('导出失败: $e')),
  34. );
  35. }
  36. }
  37. }
  38. Future<void> _downloadFile(String filename, String label) async {
  39. final api = ref.read(apiServiceProvider);
  40. try {
  41. // Step 1: download to cache
  42. final dir = await getApplicationDocumentsDirectory();
  43. final tmpPath = '${dir.path}/$filename';
  44. await api.downloadExport(filename, tmpPath);
  45. // Step 2: let user choose save location
  46. final saved = await _channel.invokeMethod<bool>('saveFile', {
  47. 'filePath': tmpPath,
  48. 'fileName': filename,
  49. });
  50. if (mounted) {
  51. ScaffoldMessenger.of(context).showSnackBar(
  52. SnackBar(
  53. content: Text(saved == true ? '$label 已保存' : '已取消'),
  54. duration: const Duration(seconds: 2),
  55. ),
  56. );
  57. }
  58. } catch (e) {
  59. if (mounted) {
  60. ScaffoldMessenger.of(context).showSnackBar(
  61. SnackBar(content: Text('下载失败: $e')),
  62. );
  63. }
  64. }
  65. }
  66. @override
  67. Widget build(BuildContext context) {
  68. return Scaffold(
  69. appBar: AppBar(title: Text(widget.cartName != null ? '导出: ${widget.cartName}' : '导出')),
  70. body: Center(
  71. child: Padding(
  72. padding: const EdgeInsets.all(32),
  73. child: Column(
  74. mainAxisSize: MainAxisSize.min,
  75. children: [
  76. if (_result == null) ...[
  77. const Icon(Icons.picture_as_pdf, size: 80, color: Colors.blue),
  78. const SizedBox(height: 24),
  79. const Text('将生成学生版和教师版两份 docx 文件'),
  80. const SizedBox(height: 24),
  81. SizedBox(
  82. width: 200, height: 48,
  83. child: FilledButton.icon(
  84. onPressed: _exporting ? null : _doExport,
  85. icon: _exporting
  86. ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
  87. : const Icon(Icons.file_download),
  88. label: Text(_exporting ? '生成中...' : '导出 docx'),
  89. ),
  90. ),
  91. ] else ...[
  92. const Icon(Icons.check_circle, size: 80, color: Colors.green),
  93. const SizedBox(height: 24),
  94. Text('导出成功!共 ${_result!['count']} 题'),
  95. const SizedBox(height: 16),
  96. FilledButton.tonal(
  97. onPressed: () => _downloadFile(_result!['student'] as String, '学生版.docx'),
  98. child: const Row(mainAxisSize: MainAxisSize.min, children: [
  99. Icon(Icons.download, size: 18), SizedBox(width: 8), Text('学生版'),
  100. ]),
  101. ),
  102. const SizedBox(height: 8),
  103. FilledButton.tonal(
  104. onPressed: () => _downloadFile(_result!['teacher'] as String, '教师版.docx'),
  105. child: const Row(mainAxisSize: MainAxisSize.min, children: [
  106. Icon(Icons.download, size: 18), SizedBox(width: 8), Text('教师版'),
  107. ]),
  108. ),
  109. const SizedBox(height: 16),
  110. TextButton(
  111. onPressed: () => Navigator.pop(context),
  112. child: const Text('返回'),
  113. ),
  114. ],
  115. ],
  116. ),
  117. ),
  118. ),
  119. );
  120. }
  121. }