前端修复: - calendar_page: 移除不存在的 JournalEntry.content getter - responsive_scaffold: 移除不存在的 notchThickness 参数 - splash_page: SingleTickerProvider → TickerProvider (多 AnimationController) - profile_page: UserRoleType.name → .code (修复运行时崩溃) - 导入缺失的 user.dart 后端修复: - class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞 - diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT 基础设施: - config/default.toml: CORS 改为通配符(开发模式) - scripts/dev.sh: 统一启动脚本(自动清理端口) - docs/opendesign/: Open Design 设计规格 HTML 原型稿 验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
501 lines
15 KiB
Dart
501 lines
15 KiB
Dart
// 设置页面 — 主题切换 + 关于 + 隐私政策
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||
import 'package:nuanji_app/core/theme/app_radius.dart';
|
||
import 'package:nuanji_app/features/profile/bloc/settings_bloc.dart';
|
||
|
||
/// 设置页面
|
||
class SettingsPage extends StatelessWidget {
|
||
const SettingsPage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final colorScheme = theme.colorScheme;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('设置')),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// ===== 外观设置 =====
|
||
_SectionHeader(title: '外观', theme: theme),
|
||
const SizedBox(height: 8),
|
||
const _ThemeSelectorCard(),
|
||
const SizedBox(height: 24),
|
||
|
||
// ===== 关于 =====
|
||
_SectionHeader(title: '关于', theme: theme),
|
||
const SizedBox(height: 8),
|
||
_AboutCard(colorScheme: colorScheme),
|
||
const SizedBox(height: 24),
|
||
|
||
// ===== 法律信息 =====
|
||
_SectionHeader(title: '法律信息', theme: theme),
|
||
const SizedBox(height: 8),
|
||
_LegalCard(colorScheme: colorScheme),
|
||
const SizedBox(height: 32),
|
||
|
||
// ===== 底部版本号 =====
|
||
Center(
|
||
child: Text(
|
||
'暖记 v1.0.0 (Phase 1)',
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 分区标题 =====
|
||
|
||
class _SectionHeader extends StatelessWidget {
|
||
const _SectionHeader({required this.title, required this.theme});
|
||
|
||
final String title;
|
||
final ThemeData theme;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(left: 4),
|
||
child: Text(
|
||
title,
|
||
style: theme.textTheme.titleSmall?.copyWith(
|
||
color: theme.colorScheme.primary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 主题选择器卡片 =====
|
||
|
||
class _ThemeSelectorCard extends StatelessWidget {
|
||
const _ThemeSelectorCard();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final settingsBloc = context.read<SettingsBloc>();
|
||
|
||
return Card(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: AppRadius.lgBorder,
|
||
side: BorderSide(color: colorScheme.outlineVariant),
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
children: [
|
||
// 标题行
|
||
Row(
|
||
children: [
|
||
Icon(Icons.palette_outlined, color: colorScheme.primary),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
'主题模式',
|
||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 三选一 SegmentedButton(实时响应主题切换)
|
||
ListenableBuilder(
|
||
listenable: settingsBloc,
|
||
builder: (context, _) {
|
||
final current = settingsBloc.state.themeMode;
|
||
return SegmentedButton<ThemeMode>(
|
||
segments: const [
|
||
ButtonSegment(
|
||
value: ThemeMode.system,
|
||
icon: Icon(Icons.brightness_auto),
|
||
label: Text('跟随系统'),
|
||
),
|
||
ButtonSegment(
|
||
value: ThemeMode.light,
|
||
icon: Icon(Icons.light_mode),
|
||
label: Text('浅色'),
|
||
),
|
||
ButtonSegment(
|
||
value: ThemeMode.dark,
|
||
icon: Icon(Icons.dark_mode),
|
||
label: Text('深色'),
|
||
),
|
||
],
|
||
selected: {current},
|
||
onSelectionChanged: (modes) {
|
||
settingsBloc.changeTheme(modes.first);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 关于卡片 =====
|
||
|
||
class _AboutCard extends StatelessWidget {
|
||
const _AboutCard({required this.colorScheme});
|
||
|
||
final ColorScheme colorScheme;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
return Card(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: AppRadius.lgBorder,
|
||
side: BorderSide(color: colorScheme.outlineVariant),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 应用 Logo 信息
|
||
Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 56,
|
||
height: 56,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.accent.withValues(alpha: 0.15),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
alignment: Alignment.center,
|
||
child: const Text('📝', style: TextStyle(fontSize: 28)),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'暖记',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'温暖治愈的手账日记',
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1, indent: 20, endIndent: 20),
|
||
_SettingsTile(
|
||
icon: Icons.info_outline,
|
||
iconColor: colorScheme.primary,
|
||
title: '版本信息',
|
||
subtitle: 'v1.0.0 · Phase 1',
|
||
onTap: () => _showAboutDialog(context),
|
||
),
|
||
_SettingsTile(
|
||
icon: Icons.favorite_outline,
|
||
iconColor: AppColors.accent,
|
||
title: '给暖记评分',
|
||
subtitle: '你的鼓励是我们前进的动力',
|
||
onTap: () {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('应用商店评分功能即将上线')),
|
||
);
|
||
},
|
||
),
|
||
_SettingsTile(
|
||
icon: Icons.feedback_outlined,
|
||
iconColor: AppColors.secondary,
|
||
title: '意见反馈',
|
||
subtitle: '告诉我们你的想法',
|
||
onTap: () {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('反馈功能即将上线')),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showAboutDialog(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Row(
|
||
children: [
|
||
Text('📝', style: TextStyle(fontSize: 24)),
|
||
SizedBox(width: 8),
|
||
Text('暖记'),
|
||
],
|
||
),
|
||
content: const Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('温暖治愈的手账日记 App'),
|
||
SizedBox(height: 8),
|
||
Text('版本: v1.0.0 (Phase 1)'),
|
||
SizedBox(height: 8),
|
||
Text('面向小学生首发,以手写/涂鸦为核心输入方式。'),
|
||
SizedBox(height: 16),
|
||
Text(
|
||
'© 2026 暖记团队',
|
||
style: TextStyle(fontSize: 12),
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(ctx),
|
||
child: const Text('确定'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 法律信息卡片 =====
|
||
|
||
class _LegalCard extends StatelessWidget {
|
||
const _LegalCard({required this.colorScheme});
|
||
|
||
final ColorScheme colorScheme;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: AppRadius.lgBorder,
|
||
side: BorderSide(color: colorScheme.outlineVariant),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
_SettingsTile(
|
||
icon: Icons.shield_outlined,
|
||
iconColor: colorScheme.primary,
|
||
title: '隐私政策',
|
||
subtitle: '了解我们如何保护你的数据',
|
||
onTap: () => _showLegalDialog(context, page: _LegalPage.privacy),
|
||
),
|
||
_SettingsTile(
|
||
icon: Icons.description_outlined,
|
||
iconColor: AppColors.tertiary,
|
||
title: '用户协议',
|
||
subtitle: '使用条款和服务说明',
|
||
onTap: () => _showLegalDialog(context, page: _LegalPage.terms),
|
||
),
|
||
_SettingsTile(
|
||
icon: Icons.child_care_outlined,
|
||
iconColor: AppColors.secondary,
|
||
title: '儿童隐私保护',
|
||
subtitle: '我们如何保护未成年人的权益',
|
||
subtitleColor: AppColors.secondary,
|
||
onTap: () => _showLegalDialog(context, page: _LegalPage.child),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showLegalDialog(BuildContext context, {required _LegalPage page}) {
|
||
final (title, icon, content) = switch (page) {
|
||
_LegalPage.privacy => (
|
||
'隐私政策',
|
||
'🔒',
|
||
'''
|
||
暖记隐私政策(最后更新:2026年6月)
|
||
|
||
一、我们收集的信息
|
||
• 昵称和年级(不收集真实姓名和身份证号)
|
||
• 日记内容(仅存储在你的设备上,云端加密存储)
|
||
• 心情记录(仅用于统计分析)
|
||
|
||
二、信息使用
|
||
• 提供日记手账服务
|
||
• 班级互动功能
|
||
• 个性化体验
|
||
|
||
三、信息保护
|
||
• 所有数据传输采用 TLS 加密
|
||
• 云端数据采用 AES-256-GCM 加密
|
||
• 本地数据采用设备级加密
|
||
|
||
四、你的权利
|
||
• 查阅、更正你的个人数据
|
||
• 要求删除所有数据
|
||
• 导出你的数据
|
||
|
||
五、儿童保护
|
||
• 未满 14 岁需家长授权
|
||
• 最小必要数据原则
|
||
• 家长可管理孩子的数据
|
||
|
||
如有疑问,请联系:privacy@nuanji.app''',
|
||
),
|
||
_LegalPage.terms => (
|
||
'用户协议',
|
||
'📋',
|
||
'''
|
||
暖记用户协议(最后更新:2026年6月)
|
||
|
||
一、服务说明
|
||
暖记是一款面向小学生的手账日记应用,提供日记书写、心情记录、班级互动等功能。
|
||
|
||
二、用户行为规范
|
||
• 尊重他人,友善交流
|
||
• 不发布不当内容
|
||
• 遵守学校规章制度
|
||
|
||
三、内容归属
|
||
• 用户创作的日记内容归用户所有
|
||
• 暖记不会在未经授权的情况下使用用户内容
|
||
|
||
四、免责声明
|
||
• 因不可抗力导致的服务中断不承担责任
|
||
• 用户因不当使用导致的损失自行承担
|
||
|
||
五、协议修改
|
||
• 修改协议将提前通知用户
|
||
• 继续使用视为同意修改后的协议''',
|
||
),
|
||
_LegalPage.child => (
|
||
'儿童隐私保护',
|
||
'👶',
|
||
'''
|
||
暖记特别重视儿童个人信息保护,严格遵守《儿童个人信息网络保护规定》。
|
||
|
||
一、家长授权
|
||
• 未满 14 周岁的用户需家长同意后方可使用
|
||
• 注册时需完成家长授权验证
|
||
• 家长可随时撤回授权
|
||
|
||
二、最小必要原则
|
||
• 仅收集提供服务必需的最少数据
|
||
• 不收集真实姓名、身份证号等敏感信息
|
||
• 不进行用户画像和个性化广告推送
|
||
|
||
三、数据安全
|
||
• 采用多重加密保护儿童数据
|
||
• 严格限制数据访问权限
|
||
• 定期进行安全审计
|
||
|
||
四、家长权利
|
||
• 查阅孩子的所有数据
|
||
• 要求更正错误数据
|
||
• 要求删除孩子数据(30天内完成)
|
||
• 导出孩子数据
|
||
|
||
五、账号注销
|
||
• 注销后30天内删除所有关联数据
|
||
• 包括日记、贴纸、成就等
|
||
• 删除后不可恢复
|
||
|
||
六、内容安全
|
||
• 自动过滤敏感内容
|
||
• 老师可审核班级内容
|
||
• 举报机制保护儿童安全
|
||
|
||
如需联系:child-safety@nuanji.app''',
|
||
),
|
||
};
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: Row(
|
||
children: [
|
||
Text(icon, style: const TextStyle(fontSize: 24)),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: Text(title)),
|
||
],
|
||
),
|
||
content: SingleChildScrollView(
|
||
child: Text(
|
||
content.trim(),
|
||
style: const TextStyle(fontSize: 13, height: 1.6),
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(ctx),
|
||
child: const Text('我知道了'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
enum _LegalPage { privacy, terms, child }
|
||
|
||
// ===== 通用设置列表项 =====
|
||
|
||
class _SettingsTile extends StatelessWidget {
|
||
const _SettingsTile({
|
||
required this.icon,
|
||
required this.iconColor,
|
||
required this.title,
|
||
required this.onTap,
|
||
this.subtitle,
|
||
this.subtitleColor,
|
||
});
|
||
|
||
final IconData icon;
|
||
final Color iconColor;
|
||
final String title;
|
||
final String? subtitle;
|
||
final Color? subtitleColor;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
return ListTile(
|
||
leading: Icon(icon, color: iconColor),
|
||
title: Text(title, style: theme.textTheme.bodyMedium),
|
||
subtitle: subtitle != null
|
||
? Text(
|
||
subtitle!,
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: subtitleColor ??
|
||
theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||
),
|
||
)
|
||
: null,
|
||
trailing: const Icon(Icons.chevron_right, size: 20),
|
||
onTap: onTap,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||
);
|
||
}
|
||
}
|