Files
nj/app/lib/features/parent/views/parent_page.dart
iven 749ef55b89
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat: Week 4 收尾 + 架构治理 — 搜索/家长中心/Feature Flag/Docker/环境配置
架构治理:
- Feature Flag 落地: Cargo.toml [features] default=["diary"] + main.rs cfg 条件编译
- 环境配置统一: AppConfig 类 + --dart-define 注入 + SSE 端口 8080→3000 修复

搜索替代方案 (无 FTS):
- SearchBloc + 标签/心情筛选接入后端 API
- JournalRepository 扩展 mood/tag 筛选参数
- 搜索页 UI 接入实际数据(替换占位文本)

家长中心最小集 (PIPL 合规):
- 后端: parent_service (绑定/查看/导出/删除/解绑) + parent_handler (6 个 API 端点)
- 前端: ParentBloc + ParentPage 功能完整实现
- 绑定孩子、只读查看日记、导出数据、删除数据、解绑

Docker 部署:
- verify.sh 健康检查脚本 (Axum/PG/Redis/OpenAPI 四项检查)

测试修复:
- home_bloc_test / calendar_bloc_test 适配 JournalRepository 新参数

验证: flutter test 84/84 pass, cargo test 76/76 pass, cargo check pass
2026-06-01 23:53:34 +08:00

1082 lines
34 KiB
Dart
Raw 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.
// 家长中心页面 — 只读查看孩子日记 + 心情统计 + 数据导出/删除
//
// 通过 ParentBloc 驱动家长中心状态:
// - 进入页面 → ParentLoadChildren 加载孩子列表
// - 无孩子 → 显示绑定入口
// - 有孩子 → 显示孩子卡片 + 4 个功能按钮
// - 日记查看 → ParentViewJournals → 日记列表
// - 数据导出 → ParentExportData → 展示导出结果
// - 数据删除 → 确认对话框 → ParentDeleteData
// 保留 PIPL 合规提示。
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 '../bloc/parent_bloc.dart';
/// 家长中心页面 — 家长查看孩子日记和统计数据
class ParentPage extends StatefulWidget {
const ParentPage({super.key});
@override
State<ParentPage> createState() => _ParentPageState();
}
class _ParentPageState extends State<ParentPage> {
final _childIdController = TextEditingController();
@override
void initState() {
super.initState();
// 进入页面自动加载孩子列表
context.read<ParentBloc>().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<ParentBloc, ParentState>(
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<ParentBloc>().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<ParentBloc>().add(const ParentLoadChildren()),
);
}
// 导出数据视图
if (state is ParentDataExported) {
return _ExportDataView(
childId: state.childId,
data: state.data,
onBack: () =>
context.read<ParentBloc>().add(const ParentLoadChildren()),
);
}
// 孩子列表或绑定入口
final children = state is ParentChildrenLoaded ? state.children : <ChildBinding>[];
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<ParentBloc>().add(ParentBindChild(childId));
_childIdController.clear();
},
),
] else ...[
_ChildListSection(
children: children,
theme: theme,
colorScheme: colorScheme,
),
const SizedBox(height: 20),
_ActionGrid(
children: children,
onViewJournals: (childId) => context
.read<ParentBloc>()
.add(ParentViewJournals(childId)),
onExport: (childId) => context
.read<ParentBloc>()
.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<ParentBloc>().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<ChildBinding> 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<ParentBloc>().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 StatelessWidget {
const _ActionGrid({
required this.children,
required this.onViewJournals,
required this.onExport,
required this.onDelete,
required this.onMoodStats,
});
final List<ChildBinding> 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;
/// 取第一个绑定的孩子 IDPhase 1 简化逻辑)
String get _firstChildId =>
children.isNotEmpty ? children.first.childId : '';
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'功能',
style: Theme.of(context).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: () => onViewJournals(_firstChildId),
),
const SizedBox(height: 12),
_ActionCard(
icon: Icons.bar_chart_outlined,
iconColor: AppColors.secondary,
iconBgColor: AppColors.secondary.withValues(alpha: 0.12),
title: '心情统计',
subtitle: '查看孩子的写作频率和心情趋势',
onTap: () => onMoodStats(_firstChildId),
),
const SizedBox(height: 12),
_ActionCard(
icon: Icons.download_outlined,
iconColor: AppColors.tertiary,
iconBgColor: AppColors.tertiary.withValues(alpha: 0.12),
title: '数据导出',
subtitle: '导出孩子的所有日记数据',
onTap: () => onExport(_firstChildId),
),
const SizedBox(height: 12),
_ActionCard(
icon: Icons.delete_outline,
iconColor: AppColors.error,
iconBgColor: AppColors.error.withValues(alpha: 0.12),
title: '数据删除',
subtitle: '永久删除孩子的日记数据',
onTap: () => onDelete(_firstChildId),
),
],
);
}
}
/// 功能操作卡片
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<Map<String, dynamic>> 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<String, dynamic> 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 (_) {
return '';
}
}
}
/// 导出数据视图 — 展示导出结果
class _ExportDataView extends StatelessWidget {
const _ExportDataView({
required this.childId,
required this.data,
required this.onBack,
});
final String childId;
final Map<String, dynamic> 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),
// 提示
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.tertiarySoftLight,
borderRadius: AppRadius.smBorder,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
size: 18,
color: AppColors.tertiary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'数据已生成。Phase 1 展示 JSON 预览,后续版本将支持文件下载。',
style: theme.textTheme.bodySmall?.copyWith(
color: AppColors.fg2Light,
),
),
),
],
),
),
],
),
),
),
],
);
}
}
/// 导出信息行
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),
),
),
),
],
),
);
}
}