- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
1151 lines
36 KiB
Dart
1151 lines
36 KiB
Dart
// 家长中心页面 — 只读查看孩子日记 + 心情统计 + 数据导出/删除
|
|
//
|
|
// 通过 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 StatefulWidget {
|
|
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;
|
|
|
|
@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<String>(
|
|
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<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 (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<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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|