Files
nj/app/lib/features/settings/views/settings_page.dart
iven b320641d9c
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 全链路验证修复 — 编译错误/CORS/迁移/启动脚本
前端修复:
- 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个页面全部渲染正常
2026-06-02 01:03:58 +08:00

501 lines
15 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.
// 设置页面 — 主题切换 + 关于 + 隐私政策
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),
);
}
}