Files
nj/app/lib/features/auth/views/parental_consent_page.dart
iven 99db8e5cb0
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 家长同意验证流程 — PIPL 第28条合规
- 新增 ParentalConsentPage: 显示隐私政策要点 + 双重确认复选框
- 角色选择流程: 学生 → 家长同意确认 → 班级码输入
- Authenticated 状态: 添加 needsParentalConsent/parentalConsentAt/selectedRole
- ParentalConsentAccepted 事件: 记录同意时间戳
- 路由守卫: 注册 /parental-consent 路径和重定向逻辑
- 非学生角色(老师/家长/独立用户)不需要经过同意流程

审计 ID: S-03
2026-06-03 10:25:23 +08:00

255 lines
8.5 KiB
Dart
Raw Permalink 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.
// 家长同意确认页面 — PIPL 第28条合规
//
// 未满 14 岁用户选择"学生"角色后,必须经过家长/监护人确认。
// 页面展示隐私政策要点,要求家长勾选同意并确认。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../../core/constants/design_tokens.dart';
import '../../../core/theme/app_radius.dart';
import '../bloc/auth_bloc.dart';
/// 家长同意确认页面
class ParentalConsentPage extends StatefulWidget {
const ParentalConsentPage({super.key});
@override
State<ParentalConsentPage> createState() => _ParentalConsentPageState();
}
class _ParentalConsentPageState extends State<ParentalConsentPage> {
bool _consentGiven = false;
bool _privacyPolicyAccepted = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final canProceed = _consentGiven && _privacyPolicyAccepted;
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar(
title: const Text('家长/监护人确认'),
backgroundColor: theme.colorScheme.surface,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(DesignTokens.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Icon(
Icons.shield_rounded,
size: 48,
color: theme.colorScheme.primary,
),
const SizedBox(height: DesignTokens.spacing12),
Text(
'儿童个人信息保护',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: DesignTokens.spacing8),
Text(
'根据《中华人民共和国个人信息保护法》第28条'
'未满14周岁未成年人的个人信息处理需要取得父母或监护人的同意。',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: DesignTokens.spacing24),
// 信息收集说明卡片
_buildInfoCard(
context,
icon: Icons.info_outline_rounded,
title: '我们会收集哪些信息',
items: const [
'昵称和年级(不收集真实姓名和身份证号)',
'日记内容和手写笔画',
'心情标签和照片',
],
),
const SizedBox(height: DesignTokens.spacing12),
// 用途说明卡片
_buildInfoCard(
context,
icon: Icons.security_rounded,
title: '信息如何保护',
items: const [
'所有数据加密存储和传输',
'仅用于日记记录和班级互动',
'不会用于商业广告或分享给第三方',
'您可以随时查阅、更正或删除孩子数据',
],
),
const SizedBox(height: DesignTokens.spacing24),
// 同意复选框
_buildCheckbox(
value: _privacyPolicyAccepted,
onChanged: (v) =>
setState(() => _privacyPolicyAccepted = v ?? false),
text: '我已阅读并同意《暖记隐私政策》和《儿童个人信息保护规则》',
),
const SizedBox(height: DesignTokens.spacing4),
_buildCheckbox(
value: _consentGiven,
onChanged: (v) => setState(() => _consentGiven = v ?? false),
text: '我是该用户的家长/监护人,同意暖记收集和处理上述信息',
),
const SizedBox(height: DesignTokens.spacing32),
// 确认按钮
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: canProceed ? _onConfirm : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: DesignTokens.spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.pill),
),
),
child: const Text('确认同意,继续'),
),
),
const SizedBox(height: DesignTokens.spacing8),
// 拒绝按钮
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => context.pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: DesignTokens.spacing12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.pill),
),
),
child: const Text('不同意,返回'),
),
),
],
),
),
),
);
}
Widget _buildInfoCard(
BuildContext context, {
required IconData icon,
required String title,
required List<String> items,
}) {
final theme = Theme.of(context);
return Card(
elevation: 0,
color: theme.colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Padding(
padding: const EdgeInsets.all(DesignTokens.spacing12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 20, color: theme.colorScheme.primary),
const SizedBox(width: DesignTokens.spacing8),
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: DesignTokens.spacing8),
...items.map(
(item) => Padding(
padding: const EdgeInsets.only(
bottom: DesignTokens.spacing4,
left: DesignTokens.spacing12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
),
),
Expanded(
child: Text(
item,
style: theme.textTheme.bodySmall,
),
),
],
),
),
),
],
),
),
);
}
Widget _buildCheckbox({
required bool value,
required ValueChanged<bool?> onChanged,
required String text,
}) {
final theme = Theme.of(context);
return InkWell(
onTap: () => onChanged(!value),
borderRadius: BorderRadius.circular(AppRadius.md),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: DesignTokens.spacing4,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: value,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
const SizedBox(width: DesignTokens.spacing4),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
text,
style: theme.textTheme.bodySmall,
),
),
),
],
),
),
);
}
/// 确认同意 — 发出事件继续注册流程
void _onConfirm() {
final consentAt = DateTime.now();
context.read<AuthBloc>().add(ParentalConsentAccepted(consentAt));
}
}