// 家长中心页面 — 只读查看孩子日记 + 心情统计 + 数据导出/删除 // // 通过 ParentBloc 驱动家长中心状态: // - 进入页面 → ParentLoadChildren 加载孩子列表 // - 无孩子 → 显示绑定入口 // - 有孩子 → 显示孩子卡片 + 4 个功能按钮 // - 日记查看 → ParentViewJournals → 日记列表 // - 数据导出 → ParentExportData → 展示导出结果 // - 数据删除 → 确认对话框 → ParentDeleteData // 保留 PIPL 合规提示。 import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; 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'; /// 家长中心页面 — 家长查看孩子日记和统计数据 class ParentPage extends StatefulWidget { const ParentPage({super.key}); @override State createState() => _ParentPageState(); } class _ParentPageState extends State { final _childIdController = TextEditingController(); @override void initState() { super.initState(); // 进入页面自动加载孩子列表 context.read().add(const ParentLoadChildren()); } @override void dispose() { _childIdController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Scaffold( appBar: AppBar(title: const Text('家长中心')), body: BlocConsumer( listener: (context, state) { if (state is ParentError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: AppColors.error, ), ); } if (state is ParentDataDeleted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('孩子数据已删除'), backgroundColor: AppColors.success, ), ); // 删除后重新加载列表 context.read().add(const ParentLoadChildren()); } }, builder: (context, state) { if (state is ParentLoading) { return const Center(child: CircularProgressIndicator()); } // 日记列表视图 if (state is ParentJournalsLoaded) { return _JournalListView( childId: state.childId, journals: state.journals, onBack: () => context.read().add(const ParentLoadChildren()), ); } // 导出数据视图 if (state is ParentDataExported) { return _ExportDataView( childId: state.childId, data: state.data, onBack: () => context.read().add(const ParentLoadChildren()), ); } // 孩子列表或绑定入口 final children = state is ParentChildrenLoaded ? state.children : []; return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 页面标题区 _HeaderCard(theme: theme, colorScheme: colorScheme), const SizedBox(height: 20), // 孩子列表或绑定入口 if (children.isEmpty) ...[ _BindChildSection( controller: _childIdController, onBind: () { final childId = _childIdController.text.trim(); if (childId.isEmpty) return; context.read().add(ParentBindChild(childId)); _childIdController.clear(); }, ), ] else ...[ _ChildListSection( children: children, theme: theme, colorScheme: colorScheme, ), const SizedBox(height: 20), _ActionGrid( children: children, onViewJournals: (childId) => context .read() .add(ParentViewJournals(childId)), onExport: (childId) => context .read() .add(ParentExportData(childId)), onDelete: (childId) => _showDeleteConfirmDialog( context, childId, ), onMoodStats: (childId) { // 复用已有心情统计页面 — 带孩子 ID 参数 context.push('/mood?child_id=$childId'); }, ), ], const SizedBox(height: 24), // PIPL 提示 _PiplNotice(theme: theme, colorScheme: colorScheme), ], ), ); }, ), ); } /// 删除确认对话框 void _showDeleteConfirmDialog(BuildContext context, String childId) { final theme = Theme.of(context); showDialog( context: context, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), title: Row( children: [ Icon(Icons.warning_amber_rounded, color: AppColors.warning, size: 28), const SizedBox(width: 12), const Text('确认删除'), ], ), content: Text( '此操作将永久删除该孩子的所有日记数据,包括文字、图片和贴纸。' '\n\n根据《个人信息保护法》,此操作不可撤销。', style: theme.textTheme.bodyMedium, ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('取消'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: AppColors.error, shape: RoundedRectangleBorder( borderRadius: AppRadius.smBorder, ), ), onPressed: () { Navigator.of(dialogContext).pop(); context.read().add(ParentDeleteData(childId)); }, child: const Text('确认删除'), ), ], ), ); } } // ===== 子组件 ===== /// 标题卡片 — 孩子信息概览 class _HeaderCard extends StatelessWidget { const _HeaderCard({ required this.theme, required this.colorScheme, }); final ThemeData theme; final ColorScheme colorScheme; @override Widget build(BuildContext context) { return Card( elevation: 0, shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), color: colorScheme.primaryContainer, child: Padding( padding: const EdgeInsets.all(20), child: Row( children: [ CircleAvatar( radius: 28, backgroundColor: AppColors.rose.withValues(alpha: 0.2), child: const Text('👶', style: TextStyle(fontSize: 24)), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '孩子的日记', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '查看和管理孩子的日记数据', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ), ], ), ), ); } } /// 绑定孩子入口 — 输入框 + 绑定按钮 class _BindChildSection extends StatelessWidget { const _BindChildSection({ required this.controller, required this.onBind, }); final TextEditingController controller; final VoidCallback onBind; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.lgBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: AppColors.accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.link, color: AppColors.accent, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '绑定孩子账号', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( '输入孩子的账号 ID 建立绑定关系', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: controller, decoration: InputDecoration( hintText: '输入孩子 ID', prefixIcon: const Icon(Icons.person_outline, size: 20), border: OutlineInputBorder( borderRadius: AppRadius.smBorder, borderSide: BorderSide(color: colorScheme.outlineVariant), ), enabledBorder: OutlineInputBorder( borderRadius: AppRadius.smBorder, borderSide: BorderSide(color: colorScheme.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: AppRadius.smBorder, borderSide: BorderSide(color: AppColors.accent, width: 2), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 12, ), ), onSubmitted: (_) => onBind(), ), ), const SizedBox(width: 12), FilledButton( onPressed: onBind, style: FilledButton.styleFrom( backgroundColor: AppColors.accent, shape: RoundedRectangleBorder( borderRadius: AppRadius.smBorder, ), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 14, ), ), child: const Text('绑定'), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.tertiarySoftLight, borderRadius: AppRadius.smBorder, ), child: Row( children: [ Icon( Icons.info_outline, size: 16, color: AppColors.tertiary, ), const SizedBox(width: 8), Expanded( child: Text( '孩子的 ID 可在孩子的"我的"页面中查看', style: theme.textTheme.bodySmall?.copyWith( color: AppColors.fg2Light, ), ), ), ], ), ), ], ), ), ); } } /// 孩子列表 — 显示已绑定的孩子 class _ChildListSection extends StatelessWidget { const _ChildListSection({ required this.children, required this.theme, required this.colorScheme, }); final List children; final ThemeData theme; final ColorScheme colorScheme; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '已绑定的孩子 (${children.length})', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...children.map((child) => _ChildCard( binding: child, theme: theme, colorScheme: colorScheme, onUnbind: () { _showUnbindConfirmDialog(context, child.childId); }, )), ], ); } void _showUnbindConfirmDialog(BuildContext context, String childId) { showDialog( context: context, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), title: const Text('确认解绑'), content: Text( '解绑后将无法查看该孩子的日记数据。', style: theme.textTheme.bodyMedium, ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('取消'), ), FilledButton( onPressed: () { Navigator.of(dialogContext).pop(); context.read().add(ParentUnbindChild(childId)); }, child: const Text('确认解绑'), ), ], ), ); } } /// 单个孩子卡片 class _ChildCard extends StatelessWidget { const _ChildCard({ required this.binding, required this.theme, required this.colorScheme, required this.onUnbind, }); final ChildBinding binding; final ThemeData theme; final ColorScheme colorScheme; final VoidCallback onUnbind; @override Widget build(BuildContext context) { final verifiedStr = binding.verifiedAt != null ? DateFormat('yyyy-MM-dd').format(binding.verifiedAt!) : '未验证'; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ CircleAvatar( radius: 24, backgroundColor: AppColors.secondary.withValues(alpha: 0.15), child: const Text('👧', style: TextStyle(fontSize: 20)), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '孩子 ${binding.childId.substring(0, binding.childId.length > 8 ? 8 : binding.childId.length)}', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( '绑定时间: $verifiedStr', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ), IconButton( icon: Icon( Icons.link_off, size: 20, color: colorScheme.onSurface.withValues(alpha: 0.4), ), onPressed: onUnbind, tooltip: '解绑', ), ], ), ), ); } } /// 功能操作网格 — 4 个功能按钮 class _ActionGrid extends StatefulWidget { const _ActionGrid({ required this.children, required this.onViewJournals, required this.onExport, required this.onDelete, required this.onMoodStats, }); final List children; final void Function(String childId) onViewJournals; final void Function(String childId) onExport; final void Function(String childId) onDelete; final void Function(String childId) onMoodStats; @override State<_ActionGrid> createState() => _ActionGridState(); } class _ActionGridState extends State<_ActionGrid> { late String _selectedChildId; @override void initState() { super.initState(); _selectedChildId = widget.children.isNotEmpty ? widget.children.first.childId : ''; } @override void didUpdateWidget(covariant _ActionGrid oldWidget) { super.didUpdateWidget(oldWidget); // 当孩子列表变化时更新选中 ID if (oldWidget.children != widget.children) { if (!widget.children.any((c) => c.childId == _selectedChildId)) { _selectedChildId = widget.children.isNotEmpty ? widget.children.first.childId : ''; } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 孩子选择器(多孩子时显示) if (widget.children.length > 1) ...[ Text( '选择孩子', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: AppRadius.mdBorder, border: Border.all(color: colorScheme.outlineVariant), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedChildId.isEmpty ? null : _selectedChildId, isExpanded: true, icon: const Icon(Icons.expand_more, size: 20), items: widget.children.map((child) { final shortId = child.childId.length > 8 ? child.childId.substring(0, 8) : child.childId; return DropdownMenuItem( value: child.childId, child: Row( children: [ const Text('👧', style: TextStyle(fontSize: 18)), const SizedBox(width: 8), Text('孩子 $shortId'), ], ), ); }).toList(), onChanged: (v) { if (v != null) setState(() => _selectedChildId = v); }, ), ), ), const SizedBox(height: 16), ], Text( '功能', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), _ActionCard( icon: Icons.auto_stories_outlined, iconColor: AppColors.accent, iconBgColor: AppColors.accent.withValues(alpha: 0.12), title: '日记查看', subtitle: '只读查看孩子的日记和评语', onTap: () => widget.onViewJournals(_selectedChildId), ), const SizedBox(height: 12), _ActionCard( icon: Icons.bar_chart_outlined, iconColor: AppColors.secondary, iconBgColor: AppColors.secondary.withValues(alpha: 0.12), title: '心情统计', subtitle: '查看孩子的写作频率和心情趋势', onTap: () => widget.onMoodStats(_selectedChildId), ), const SizedBox(height: 12), _ActionCard( icon: Icons.download_outlined, iconColor: AppColors.tertiary, iconBgColor: AppColors.tertiary.withValues(alpha: 0.12), title: '数据导出', subtitle: '导出孩子的所有日记数据', onTap: () => widget.onExport(_selectedChildId), ), const SizedBox(height: 12), _ActionCard( icon: Icons.delete_outline, iconColor: AppColors.error, iconBgColor: AppColors.error.withValues(alpha: 0.12), title: '数据删除', subtitle: '永久删除孩子的日记数据', onTap: () => widget.onDelete(_selectedChildId), ), ], ); } } /// 功能操作卡片 class _ActionCard extends StatelessWidget { const _ActionCard({ required this.icon, required this.iconColor, required this.iconBgColor, required this.title, required this.subtitle, required this.onTap, }); final IconData icon; final Color iconColor; final Color iconBgColor; final String title; final String subtitle; final VoidCallback onTap; @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: InkWell( onTap: onTap, borderRadius: AppRadius.mdBorder, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: iconBgColor, borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: iconColor, size: 22), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( subtitle, style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ), Icon( Icons.chevron_right, color: colorScheme.onSurface.withValues(alpha: 0.3), ), ], ), ), ), ); } } /// 日记列表视图 — 只读查看孩子日记 class _JournalListView extends StatelessWidget { const _JournalListView({ required this.childId, required this.journals, required this.onBack, }); final String childId; final List> journals; final VoidCallback onBack; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Column( children: [ // 返回按钮 + 标题 SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: onBack, ), Expanded( child: Text( '孩子的日记', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), Icon( Icons.visibility_outlined, size: 20, color: colorScheme.onSurface.withValues(alpha: 0.4), ), const SizedBox(width: 4), Text( '只读', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.4), ), ), ], ), ), ), // 日记列表 Expanded( child: journals.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('📝', style: TextStyle(fontSize: 48)), const SizedBox(height: 16), Text( '暂无日记', style: theme.textTheme.titleMedium?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ) : ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: journals.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, index) { final journal = journals[index]; return _JournalCard( journal: journal, theme: theme, colorScheme: colorScheme, ); }, ), ), ], ); } } /// 单条日记卡片(只读) class _JournalCard extends StatelessWidget { const _JournalCard({ required this.journal, required this.theme, required this.colorScheme, }); final Map journal; final ThemeData theme; final ColorScheme colorScheme; @override Widget build(BuildContext context) { final title = journal['title'] as String? ?? '无标题'; final createdAt = journal['created_at'] as String? ?? ''; final preview = journal['preview'] as String? ?? journal['content'] as String? ?? ''; final moodEmoji = journal['mood_emoji'] as String? ?? ''; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (moodEmoji.isNotEmpty) ...[ Text(moodEmoji, style: const TextStyle(fontSize: 18)), const SizedBox(width: 8), ], Expanded( child: Text( title, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (createdAt.isNotEmpty) Text( _formatDate(createdAt), style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.4), fontSize: 12, ), ), ], ), if (preview.isNotEmpty) ...[ const SizedBox(height: 8), Text( preview, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.7), height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ], ], ), ), ); } String _formatDate(String isoStr) { try { final dt = DateTime.parse(isoStr); return DateFormat('MM-dd').format(dt); } catch (e) { debugPrint('ParentPage._formatDate 失败: $e'); return ''; } } } /// 导出数据视图 — 展示导出结果 + 下载按钮 class _ExportDataView extends StatelessWidget { const _ExportDataView({ required this.childId, required this.data, required this.onBack, }); final String childId; final Map data; final VoidCallback onBack; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final journalCount = (data['journal_count'] as num?)?.toInt() ?? 0; final exportDate = data['export_date'] as String? ?? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); return Column( children: [ // 返回按钮 + 标题 SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: onBack, ), Expanded( child: Text( '数据导出', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), ], ), ), ), // 导出结果 Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ // 成功图标 Container( width: 80, height: 80, decoration: BoxDecoration( color: AppColors.success.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: Icon( Icons.check_circle_outline, color: AppColors.success, size: 40, ), ), const SizedBox(height: 20), Text( '导出成功', style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 24), // 数据概要卡片 Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ _ExportInfoRow( label: '导出时间', value: exportDate, theme: theme, colorScheme: colorScheme, ), const SizedBox(height: 12), _ExportInfoRow( label: '日记数量', value: '$journalCount 篇', theme: theme, colorScheme: colorScheme, ), const SizedBox(height: 12), _ExportInfoRow( label: '数据格式', value: 'JSON', theme: theme, colorScheme: colorScheme, ), ], ), ), ), 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( color: AppColors.tertiarySoftLight, borderRadius: AppRadius.smBorder, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.shield_outlined, size: 18, color: AppColors.tertiary, ), const SizedBox(width: 8), Expanded( child: Text( '根据《个人信息保护法》,您有权导出孩子的全部个人数据。' '导出数据仅供个人查阅,请妥善保管。', style: theme.textTheme.bodySmall?.copyWith( color: AppColors.fg2Light, ), ), ), ], ), ), ], ), ), ), ], ); } /// 触发文件下载 Future _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 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), ), ), ), ), ], ], ), ); } } /// 导出信息行 class _ExportInfoRow extends StatelessWidget { const _ExportInfoRow({ required this.label, required this.value, required this.theme, required this.colorScheme, }); final String label; final String value; final ThemeData theme; final ColorScheme colorScheme; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), Text( value, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ], ); } } /// PIPL 合规提示 class _PiplNotice extends StatelessWidget { const _PiplNotice({ required this.theme, required this.colorScheme, }); final ThemeData theme; final ColorScheme colorScheme; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( Icons.shield_outlined, size: 18, color: colorScheme.onSurface.withValues(alpha: 0.5), ), const SizedBox(width: 8), Expanded( child: Text( '根据《个人信息保护法》,您有权查阅、更正、删除和导出孩子的数据。', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ), ], ), ); } }