feat(app): 家长端数据导出 — 添加 JSON 文件下载 + 预览
- 新建 file_download.dart 跨平台下载工具(conditional import) - download_impl_web.dart: Web 平台通过 html.AnchorElement + Blob 下载 - download_impl.dart: 非 Web 平台 stub(Phase 2 扩展 path_provider) - _ExportDataView: 添加下载按钮 + JSON 折叠预览 + PIPL 提示 - 移除 'Phase 1 预览' 占位文案,替换为完整下载功能
This commit is contained in:
9
app/lib/core/utils/download_impl.dart
Normal file
9
app/lib/core/utils/download_impl.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
// 文件下载 — 非 Web 平台 stub
|
||||
//
|
||||
// 非 Web 平台暂不支持文件下载,返回 false。
|
||||
// Phase 2 扩展:使用 path_provider + File 实现。
|
||||
|
||||
/// 下载文件(stub 实现)
|
||||
Future<bool> downloadFile(String content, String filename, String mimeType) async {
|
||||
return false;
|
||||
}
|
||||
21
app/lib/core/utils/download_impl_web.dart
Normal file
21
app/lib/core/utils/download_impl_web.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
// 文件下载 — Web 平台实现
|
||||
//
|
||||
// 使用 dart:html 的 AnchorElement + Blob 触发浏览器下载。
|
||||
// 通过 conditional import 自动选择此实现。
|
||||
|
||||
import 'dart:html' as html;
|
||||
|
||||
/// 下载文件(Web 实现)
|
||||
Future<bool> downloadFile(String content, String filename, String mimeType) async {
|
||||
try {
|
||||
final blob = html.Blob([content], mimeType);
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
html.AnchorElement(href: url)
|
||||
..setAttribute('download', filename)
|
||||
..click();
|
||||
html.Url.revokeObjectUrl(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
23
app/lib/core/utils/file_download.dart
Normal file
23
app/lib/core/utils/file_download.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// 文件下载工具 — 跨平台接口
|
||||
//
|
||||
// Web: 通过 html.AnchorElement + Blob 触发浏览器下载
|
||||
// 非 Web: 返回 false(Phase 2 扩展 path_provider)
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'download_impl.dart'
|
||||
if (dart.library.html) 'download_impl_web.dart';
|
||||
|
||||
/// 下载 JSON 数据为文件
|
||||
///
|
||||
/// [data] — 要导出的 JSON 数据
|
||||
/// [filename] — 下载文件名(如 "export_2026-06-02.json")
|
||||
///
|
||||
/// 返回 true 表示下载成功。
|
||||
Future<bool> downloadJsonFile(
|
||||
Map<String, dynamic> data,
|
||||
String filename,
|
||||
) async {
|
||||
final jsonStr = const JsonEncoder.withIndent(' ').convert(data);
|
||||
return downloadFile(jsonStr, filename, 'application/json');
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
// - 数据删除 → 确认对话框 → ParentDeleteData
|
||||
// 保留 PIPL 合规提示。
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -16,6 +18,7 @@ import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/utils/file_download.dart';
|
||||
import '../bloc/parent_bloc.dart';
|
||||
|
||||
/// 家长中心页面 — 家长查看孩子日记和统计数据
|
||||
@@ -929,7 +932,7 @@ class _JournalCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出数据视图 — 展示导出结果
|
||||
/// 导出数据视图 — 展示导出结果 + 下载按钮
|
||||
class _ExportDataView extends StatelessWidget {
|
||||
const _ExportDataView({
|
||||
required this.childId,
|
||||
@@ -1037,7 +1040,27 @@ class _ExportDataView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 提示
|
||||
// 下载按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => _handleDownload(context),
|
||||
icon: const Icon(Icons.download_rounded, size: 20),
|
||||
label: const Text('下载 JSON 文件'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.secondary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// JSON 预览(折叠面板)
|
||||
_JsonPreviewCard(data: data),
|
||||
const SizedBox(height: 16),
|
||||
// PIPL 提示
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -1048,14 +1071,15 @@ class _ExportDataView extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
Icons.shield_outlined,
|
||||
size: 18,
|
||||
color: AppColors.tertiary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'数据已生成。Phase 1 展示 JSON 预览,后续版本将支持文件下载。',
|
||||
'根据《个人信息保护法》,您有权导出孩子的全部个人数据。'
|
||||
'导出数据仅供个人查阅,请妥善保管。',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: AppColors.fg2Light,
|
||||
),
|
||||
@@ -1071,6 +1095,103 @@ class _ExportDataView extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 触发文件下载
|
||||
Future<void> _handleDownload(BuildContext context) async {
|
||||
final filename = '暖记_数据导出_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.json';
|
||||
final success = await downloadJsonFile(data, filename);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(success ? '文件已开始下载' : '下载失败,请重试'),
|
||||
backgroundColor: success ? AppColors.success : AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON 预览折叠卡片
|
||||
class _JsonPreviewCard extends StatefulWidget {
|
||||
const _JsonPreviewCard({required this.data});
|
||||
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
@override
|
||||
State<_JsonPreviewCard> createState() => _JsonPreviewCardState();
|
||||
}
|
||||
|
||||
class _JsonPreviewCardState extends State<_JsonPreviewCard> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(12),
|
||||
bottom: _expanded ? Radius.zero : const Radius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.data_object,
|
||||
size: 20,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'JSON 数据预览',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
_expanded ? Icons.expand_less : Icons.expand_more,
|
||||
size: 20,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_expanded) ...[
|
||||
const Divider(height: 1),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
const JsonEncoder.withIndent(' ').convert(widget.data),
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: 12,
|
||||
height: 1.5,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出信息行
|
||||
|
||||
Reference in New Issue
Block a user