Files
yanting/lib/features/settings/settings_page.dart
T
2026-06-07 11:38:08 +08:00

371 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../data/providers.dart';
import '../../data/state/app_interaction_state.dart';
import '../../routing/app_routes.dart';
import '../../theme/app_icons.dart';
import '../../theme/theme_controller.dart';
import '../../theme/yanting_text.dart';
import '../../theme/yanting_tokens.dart';
import '../../widgets/app_card.dart';
import '../../widgets/page_header.dart';
import '../../widgets/sheets.dart';
import '../../widgets/states.dart';
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final scheme = ShadTheme.of(context).colorScheme;
final auth = ref.watch(authControllerProvider);
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(AppIcons.arrowLeft),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go(AppRoutes.home);
}
},
),
title: const Text('设置 · Settings'),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(
YantingSpacing.screenX,
4,
YantingSpacing.screenX,
20,
),
children: [
const PageHeader(title: '设置', subtitle: '统一外观、主题和常用入口'),
Text('外观', style: YantingText.sectionTitle),
const SizedBox(height: 12),
AppCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_ThemeModeTile(
label: '跟随系统',
description: '按系统深浅色自动切换',
selected: themeMode == ThemeMode.system,
onTap: () => ref
.read(themeModeProvider.notifier)
.setMode(ThemeMode.system),
),
const Divider(height: 1, thickness: 1),
_ThemeModeTile(
label: '浅色',
description: '稳定的浅色展示模式',
selected: themeMode == ThemeMode.light,
onTap: () => ref
.read(themeModeProvider.notifier)
.setMode(ThemeMode.light),
),
const Divider(height: 1, thickness: 1),
_ThemeModeTile(
label: '深色',
description: '适合低光环境阅读',
selected: themeMode == ThemeMode.dark,
onTap: () => ref
.read(themeModeProvider.notifier)
.setMode(ThemeMode.dark),
),
],
),
),
const SizedBox(height: YantingSpacing.x3),
Text('入口', style: YantingText.sectionTitle),
const SizedBox(height: 12),
AppCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_LinkTile(
icon: Icons.description_outlined,
title: '用户协议',
onTap: () => _showOutbound(
context,
ref,
'settings_user_agreement',
'用户协议',
),
),
const Divider(height: 1, thickness: 1),
_LinkTile(
icon: Icons.privacy_tip_outlined,
title: '隐私政策',
onTap: () => _showOutbound(
context,
ref,
'settings_privacy_policy',
'隐私政策',
),
),
],
),
),
const SizedBox(height: YantingSpacing.x3),
Text('关于', style: YantingText.sectionTitle),
const SizedBox(height: 12),
AppCard(
color: scheme.secondary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('研听', style: YantingText.cardTitle),
const SizedBox(height: 6),
Text(
'全球机构研报中文解读',
style: YantingText.meta.copyWith(fontSize: 12.5),
),
const SizedBox(height: 12),
Text(
'主题、按钮、卡片和间距统一到 demo 的展示层基线。',
style: YantingText.body.copyWith(fontSize: 14),
),
const SizedBox(height: 14),
Text(
'当前版本以本地构建信息为准,发布时再注入正式版本号。',
style: YantingText.meta.copyWith(fontSize: 12),
),
],
),
),
if (auth.loggedIn) ...[
const SizedBox(height: YantingSpacing.x3),
Text('账户', style: YantingText.sectionTitle),
const SizedBox(height: 12),
AppCard(
padding: EdgeInsets.zero,
child: _ActionTile(
icon: Icons.logout,
title: '退出登录',
subtitle: '退出后本地登录态会清空',
destructive: true,
onTap: () => _confirmLogout(context, ref),
),
),
],
],
),
);
}
void _showOutbound(
BuildContext context,
WidgetRef ref,
String scene,
String title,
) {
showOutboundSheet(
context,
title: title,
onConfirm: () => ref
.read(outboundRepositoryProvider)
.recordOutbound(OutboundEvent(scene: scene)),
);
}
Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async {
final ok = await showShadDialog<bool>(
context: context,
variant: ShadDialogVariant.alert,
builder: (dialogContext) => ShadDialog.alert(
title: const Text('退出登录'),
description: const Text('退出后,本设备的登录态会清空,再次登录可继续使用。'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.of(dialogContext).pop(false),
),
ShadButton(
child: const Text('确定退出'),
onPressed: () => Navigator.of(dialogContext).pop(true),
),
],
),
);
if (ok != true) return;
await ref.read(authControllerProvider.notifier).logout();
await ref.read(profileControllerProvider.notifier).refresh();
if (!context.mounted) return;
showAppToast(context, '已退出登录');
context.go(AppRoutes.profile);
}
}
class _ThemeModeTile extends StatelessWidget {
const _ThemeModeTile({
required this.label,
required this.description,
required this.selected,
required this.onTap,
});
final String label;
final String description;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final scheme = ShadTheme.of(context).colorScheme;
final foreground = selected ? scheme.background : scheme.foreground;
final muted = scheme.mutedForeground;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: YantingText.body.copyWith(
color: scheme.foreground,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
description,
style: YantingText.meta.copyWith(
color: muted,
fontSize: 12.5,
),
),
],
),
),
const SizedBox(width: 12),
DecoratedBox(
decoration: BoxDecoration(
color: selected ? scheme.foreground : scheme.secondary,
borderRadius: BorderRadius.circular(YantingRadius.pill),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5),
child: Icon(
selected ? Icons.check : Icons.radio_button_unchecked,
size: 16,
color: foreground,
),
),
),
],
),
),
);
}
}
class _LinkTile extends StatelessWidget {
const _LinkTile({
required this.icon,
required this.title,
required this.onTap,
});
final IconData icon;
final String title;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
child: Row(
children: [
Icon(
icon,
size: 20,
color: ShadTheme.of(context).colorScheme.foreground,
),
const SizedBox(width: 13),
Expanded(
child: Text(
title,
style: YantingText.body.copyWith(
color: ShadTheme.of(context).colorScheme.foreground,
),
),
),
const Icon(AppIcons.arrowRight, size: 18),
],
),
),
);
}
}
class _ActionTile extends StatelessWidget {
const _ActionTile({
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
this.destructive = false,
});
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;
final bool destructive;
@override
Widget build(BuildContext context) {
final colors = ShadTheme.of(context).colorScheme;
final titleColor = destructive ? colors.destructive : colors.foreground;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
child: Row(
children: [
Icon(icon, size: 20, color: titleColor),
const SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: YantingText.body.copyWith(
color: titleColor,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: YantingText.meta.copyWith(
color: colors.mutedForeground,
fontSize: 12.5,
),
),
],
),
),
const Icon(AppIcons.arrowRight, size: 18),
],
),
),
);
}
}