1232 lines
38 KiB
Dart
1232 lines
38 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||
import 'package:flutter_remix/flutter_remix.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||
import 'package:url_launcher/url_launcher_string.dart';
|
||
|
||
import '../../../common/config/app_h5_urls.dart';
|
||
import '../../../common/router/app_router.dart';
|
||
import '../../../common/theme/app_text.dart';
|
||
import '../../../common/theme/spacing.dart';
|
||
import '../../../common/theme/theme_controller.dart';
|
||
import '../../../common/widgets/adaptive.dart';
|
||
import '../../../common/widgets/app_toast.dart';
|
||
import '../../../common/widgets/circle_back_button.dart';
|
||
import '../../auth/application/auth_controller.dart';
|
||
import '../../auth/data/auth_models.dart';
|
||
import '../../profile/application/profile_settings_controller.dart';
|
||
import '../../profile/data/profile_models.dart';
|
||
|
||
class SettingsPage extends HookConsumerWidget {
|
||
const SettingsPage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final themeMode = ref.watch(themeModeControllerProvider);
|
||
final profileSettings = ref.watch(profileSettingsControllerProvider);
|
||
final authAsync = ref.watch(authProvider);
|
||
final loggedIn = authAsync.valueOrNull is LoggedInAuthState;
|
||
final contactDebugTapCount = useState(0);
|
||
final contactLastTapAt = useRef<DateTime?>(null);
|
||
|
||
return Scaffold(
|
||
backgroundColor: cs.background,
|
||
appBar: AppBar(
|
||
backgroundColor: cs.background,
|
||
surfaceTintColor: Colors.transparent,
|
||
elevation: 0,
|
||
scrolledUnderElevation: 0,
|
||
centerTitle: true,
|
||
title: Text(
|
||
'设置',
|
||
style: TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
leading: const CircleBackButton(),
|
||
),
|
||
body: ListView(
|
||
padding: Adaptive.screenPadding(context, top: 8, bottom: 32),
|
||
children: [
|
||
const _Section('外观'),
|
||
_ThemeSegmentCard(
|
||
themeMode: themeMode,
|
||
onChanged: (mode) =>
|
||
ref.read(themeModeControllerProvider.notifier).set(mode),
|
||
),
|
||
|
||
const SizedBox(height: 22),
|
||
const _Section('资产参数'),
|
||
_Card(
|
||
children: [
|
||
_SwitchRow(
|
||
title: '隐藏金额',
|
||
subtitle: '在资产页和持仓列表中隐藏金额信息',
|
||
value: profileSettings.amountHidden,
|
||
onChanged: (_) => ref
|
||
.read(profileSettingsControllerProvider.notifier)
|
||
.toggleAmountHidden(),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.line_chart_line,
|
||
label: '涨跌颜色',
|
||
trailing: profileSettings.priceColorMode.label,
|
||
onTap: () => _showPriceColorSheet(context, ref),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.coins_line,
|
||
label: '纯度系数',
|
||
trailing: '${profileSettings.purityPresets.length} 项',
|
||
onTap: () => _showPuritySheet(context, profileSettings),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.exchange_box_line,
|
||
label: '回收折扣',
|
||
trailing: '已设置',
|
||
onTap: () => _showRecycleSheet(context, profileSettings),
|
||
),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 22),
|
||
const _Section('备份'),
|
||
_Card(
|
||
children: [
|
||
_NavRow(
|
||
icon: FlutterRemix.download_2_line,
|
||
label: '本地导出备份',
|
||
trailing: '导出',
|
||
onTap: () => _showLocalBackupSheet(context),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.cloud_line,
|
||
label: '云备份',
|
||
trailing: '占位',
|
||
onTap: () => _showCloudBackupSheet(context),
|
||
),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 22),
|
||
const _Section('工具'),
|
||
_Card(
|
||
children: [
|
||
_NavRow(
|
||
icon: FlutterRemix.layout_grid_line,
|
||
label: '桌面小组件',
|
||
trailing: '占位',
|
||
onTap: () => _showWidgetSheet(context),
|
||
),
|
||
if (kDebugMode) ...[
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: Icons.bug_report_outlined,
|
||
label: '调试工具',
|
||
onTap: () => context.push(AppRoutes.debug),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 22),
|
||
const _Section('法律'),
|
||
_Card(
|
||
children: [
|
||
_NavRow(
|
||
icon: FlutterRemix.file_list_3_line,
|
||
label: '用户协议',
|
||
onTap: () => launchUrlString(
|
||
AppH5UrlsHelper.withCacheBuster(AppH5Urls.userProtocolUrl),
|
||
mode: LaunchMode.inAppBrowserView,
|
||
),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.shield_check_line,
|
||
label: '隐私政策',
|
||
onTap: () => launchUrlString(
|
||
AppH5UrlsHelper.withCacheBuster(AppH5Urls.privacyUrl),
|
||
mode: LaunchMode.inAppBrowserView,
|
||
),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.alert_line,
|
||
label: '免责声明',
|
||
onTap: () => _showDisclaimerSheet(context),
|
||
),
|
||
],
|
||
),
|
||
|
||
if (loggedIn) ...[
|
||
const SizedBox(height: 22),
|
||
const _Section('账户'),
|
||
_Card(
|
||
children: [
|
||
_NavRow(
|
||
icon: FlutterRemix.logout_box_r_line,
|
||
label: '退出登录',
|
||
onTap: () => _confirmLogout(context, ref),
|
||
),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.delete_bin_line,
|
||
label: '注销账号',
|
||
destructive: true,
|
||
onTap: () => context.push(AppRoutes.deleteAccount),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
|
||
const SizedBox(height: 22),
|
||
const _Section('关于'),
|
||
_Card(
|
||
children: [
|
||
const _AboutRow(),
|
||
Divider(height: 1, thickness: 1, color: cs.border),
|
||
_NavRow(
|
||
icon: FlutterRemix.mail_line,
|
||
label: '联系我们',
|
||
trailing: 'business@hnsiderui.cn',
|
||
onTap: () {
|
||
final now = DateTime.now();
|
||
final lastTapAt = contactLastTapAt.value;
|
||
if (lastTapAt == null ||
|
||
now.difference(lastTapAt) >
|
||
const Duration(milliseconds: 500)) {
|
||
contactDebugTapCount.value = 0;
|
||
}
|
||
contactLastTapAt.value = now;
|
||
contactDebugTapCount.value += 1;
|
||
if (contactDebugTapCount.value >= 10) {
|
||
contactDebugTapCount.value = 0;
|
||
contactLastTapAt.value = null;
|
||
context.push(AppRoutes.debug);
|
||
}
|
||
},
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
_RiskFooter(cs: cs),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _confirmLogout(BuildContext context, WidgetRef ref) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final ok = await showModalBottomSheet<bool>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'退出登录',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'确定要退出登录吗?',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 22),
|
||
_LogoutSheetButton(
|
||
label: '退出',
|
||
primary: true,
|
||
onTap: () => Navigator.of(ctx).pop(true),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_LogoutSheetButton(
|
||
label: '取消',
|
||
primary: false,
|
||
onTap: () => Navigator.of(ctx).pop(false),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
if (ok == true && context.mounted) {
|
||
await ref.read(authProvider.notifier).logout();
|
||
}
|
||
}
|
||
|
||
Future<void> _showPriceColorSheet(BuildContext context, WidgetRef ref) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final current = ref.read(profileSettingsControllerProvider).priceColorMode;
|
||
final selected = await showModalBottomSheet<PriceColorMode>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'涨跌颜色',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'选择行情上涨和下跌的配色方案。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_ChoiceButton(
|
||
label: PriceColorMode.redUpGreenDown.label,
|
||
selected: current == PriceColorMode.redUpGreenDown,
|
||
onTap: () => Navigator.of(ctx).pop(PriceColorMode.redUpGreenDown),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_ChoiceButton(
|
||
label: PriceColorMode.greenUpRedDown.label,
|
||
selected: current == PriceColorMode.greenUpRedDown,
|
||
onTap: () => Navigator.of(ctx).pop(PriceColorMode.greenUpRedDown),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
if (selected != null && context.mounted) {
|
||
ref.read(profileSettingsControllerProvider.notifier).setPriceColorMode(
|
||
selected,
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _showPuritySheet(
|
||
BuildContext context,
|
||
ProfileSettings settings,
|
||
) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'纯度系数',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'用于估值换算的本地纯度预设。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 16),
|
||
for (final entry in settings.purityPresets.entries) ...[
|
||
_ValueRow(
|
||
label: entry.key,
|
||
value: '${(entry.value * 100).toStringAsFixed(2)}%',
|
||
),
|
||
if (entry.key != settings.purityPresets.keys.last) ...[
|
||
const SizedBox(height: 10),
|
||
],
|
||
],
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '知道了',
|
||
onTap: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showRecycleSheet(
|
||
BuildContext context,
|
||
ProfileSettings settings,
|
||
) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'回收折扣',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'用于计算金、铂、银的回收参考值。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 16),
|
||
for (final entry in settings.recycleDiscounts.entries) ...[
|
||
_ValueRow(
|
||
label: entry.key.label,
|
||
value: '${(entry.value * 100).toStringAsFixed(0)}%',
|
||
),
|
||
if (entry.key != settings.recycleDiscounts.keys.last) ...[
|
||
const SizedBox(height: 10),
|
||
],
|
||
],
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '知道了',
|
||
onTap: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showLocalBackupSheet(BuildContext context) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final ok = await showModalBottomSheet<bool>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'本地导出备份',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'将当前持仓、偏好设置和 mock 账号状态导出到本地文件。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '导出备份',
|
||
onTap: () => Navigator.of(ctx).pop(true),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_SheetSecondaryButton(
|
||
label: '取消',
|
||
onTap: () => Navigator.of(ctx).pop(false),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
if (ok == true && context.mounted) {
|
||
toastInfo(msg: '已导出本地备份(mock)');
|
||
}
|
||
}
|
||
|
||
Future<void> _showCloudBackupSheet(BuildContext context) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'云备份',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'后续会接入云同步与多端恢复,这里先保留入口。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '知道了',
|
||
onTap: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showWidgetSheet(BuildContext context) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'桌面小组件',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'桌面行情小组件正在整理中,先保留入口。',
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '知道了',
|
||
onTap: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showDisclaimerSheet(BuildContext context) async {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
backgroundColor: cs.card,
|
||
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: Radius.circular(Spacing.radiusCard),
|
||
),
|
||
),
|
||
builder: (ctx) => SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 38,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
decoration: BoxDecoration(
|
||
color: cs.border,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'免责声明',
|
||
style: TextStyle(
|
||
fontSize: 19,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'金值提供的价格、资讯和估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
height: 1.55,
|
||
color: cs.mutedForeground,
|
||
),
|
||
),
|
||
const SizedBox(height: 18),
|
||
_SheetPrimaryButton(
|
||
label: '知道了',
|
||
onTap: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _Section extends StatelessWidget {
|
||
const _Section(this.title);
|
||
|
||
final String title;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w700,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _Card extends StatelessWidget {
|
||
const _Card({required this.children});
|
||
|
||
final List<Widget> children;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: cs.card,
|
||
border: Border.all(color: cs.border),
|
||
borderRadius: BorderRadius.circular(Spacing.radiusCard),
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
children: [
|
||
for (var i = 0; i < children.length; i++) ...[
|
||
if (i > 0) Divider(height: 1, thickness: 1, color: cs.border),
|
||
children[i],
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ThemeSegmentCard extends StatelessWidget {
|
||
const _ThemeSegmentCard({required this.themeMode, required this.onChanged});
|
||
|
||
final ThemeMode themeMode;
|
||
final ValueChanged<ThemeMode> onChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final options = <(ThemeMode, String, IconData)>[
|
||
(ThemeMode.system, '跟随系统', Icons.brightness_auto_outlined),
|
||
(ThemeMode.light, '浅色', Icons.light_mode_outlined),
|
||
(ThemeMode.dark, '深色', Icons.dark_mode_outlined),
|
||
];
|
||
return Container(
|
||
padding: const EdgeInsets.all(4),
|
||
decoration: BoxDecoration(
|
||
color: cs.secondary,
|
||
border: Border.all(color: cs.border),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
for (final opt in options)
|
||
Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(
|
||
left: opt == options.first ? 0 : 2,
|
||
right: opt == options.last ? 0 : 2,
|
||
),
|
||
child: _ThemeSegmentButton(
|
||
label: opt.$2,
|
||
icon: opt.$3,
|
||
selected: themeMode == opt.$1,
|
||
onTap: () => onChanged(opt.$1),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ThemeSegmentButton extends StatelessWidget {
|
||
const _ThemeSegmentButton({
|
||
required this.label,
|
||
required this.icon,
|
||
required this.selected,
|
||
required this.onTap,
|
||
});
|
||
|
||
final String label;
|
||
final IconData icon;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final fg = selected ? cs.foreground : cs.mutedForeground;
|
||
return Material(
|
||
color: selected ? cs.background : Colors.transparent,
|
||
borderRadius: BorderRadius.circular(10),
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(10),
|
||
child: Container(
|
||
height: 44,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(10),
|
||
boxShadow: selected
|
||
? [
|
||
const BoxShadow(
|
||
color: Color(0x0A000000),
|
||
blurRadius: 10,
|
||
offset: Offset(0, 2),
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 17, color: fg),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
color: fg,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SwitchRow extends StatelessWidget {
|
||
const _SwitchRow({
|
||
required this.title,
|
||
required this.subtitle,
|
||
required this.value,
|
||
required this.onChanged,
|
||
});
|
||
|
||
final String title;
|
||
final String subtitle;
|
||
final bool value;
|
||
final ValueChanged<bool> onChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
subtitle,
|
||
style: AppText.meta.copyWith(
|
||
fontSize: 13,
|
||
color: cs.mutedForeground,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
ShadSwitch(
|
||
value: value,
|
||
onChanged: onChanged,
|
||
checkedTrackColor: cs.primary,
|
||
thumbColor: cs.background,
|
||
width: 44,
|
||
height: 26,
|
||
margin: 2,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NavRow extends StatelessWidget {
|
||
const _NavRow({
|
||
required this.icon,
|
||
required this.label,
|
||
this.trailing,
|
||
this.onTap,
|
||
this.destructive = false,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String label;
|
||
final String? trailing;
|
||
final VoidCallback? onTap;
|
||
final bool destructive;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
final color = destructive ? cs.destructive : cs.foreground;
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||
child: Row(
|
||
children: [
|
||
SizedBox(width: 24, child: Icon(icon, size: 20, color: color)),
|
||
const SizedBox(width: 13),
|
||
Expanded(
|
||
child: Text(label, style: TextStyle(fontSize: 15, color: color)),
|
||
),
|
||
if (trailing != null)
|
||
Text(
|
||
trailing!,
|
||
style: AppText.meta.copyWith(
|
||
fontSize: 13,
|
||
color: cs.mutedForeground,
|
||
),
|
||
),
|
||
if (onTap != null && trailing == null)
|
||
Icon(
|
||
FlutterRemix.arrow_right_s_line,
|
||
size: 20,
|
||
color: cs.mutedForeground,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _AboutRow extends StatelessWidget {
|
||
const _AboutRow();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return FutureBuilder<PackageInfo>(
|
||
future: PackageInfo.fromPlatform(),
|
||
builder: (context, snap) {
|
||
final v = snap.hasData
|
||
? 'v${snap.data!.version}+${snap.data!.buildNumber}'
|
||
: '';
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||
child: Row(
|
||
children: [
|
||
SizedBox(
|
||
width: 24,
|
||
child: Icon(
|
||
FlutterRemix.information_line,
|
||
size: 20,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
const SizedBox(width: 13),
|
||
Expanded(
|
||
child: Text(
|
||
'金值',
|
||
style: TextStyle(fontSize: 15, color: cs.foreground),
|
||
),
|
||
),
|
||
Text(
|
||
v,
|
||
style: AppText.meta.copyWith(
|
||
fontSize: 13,
|
||
color: cs.mutedForeground,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RiskFooter extends StatelessWidget {
|
||
const _RiskFooter({required this.cs});
|
||
|
||
final ShadColorScheme cs;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||
decoration: BoxDecoration(
|
||
color: cs.card,
|
||
border: Border.all(color: cs.border),
|
||
borderRadius: BorderRadius.circular(Spacing.radiusCard),
|
||
),
|
||
child: Text(
|
||
'风险提示 · 数据来源 · 免责\n价格、资讯与估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。',
|
||
style: AppText.meta.copyWith(
|
||
fontSize: 12,
|
||
color: cs.mutedForeground,
|
||
height: 1.65,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ChoiceButton extends StatelessWidget {
|
||
const _ChoiceButton({
|
||
required this.label,
|
||
required this.selected,
|
||
required this.onTap,
|
||
});
|
||
|
||
final String label;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 52,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: selected ? cs.primary : cs.secondary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: selected ? cs.primary : cs.border,
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: selected ? cs.primaryForeground : cs.foreground,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SheetPrimaryButton extends StatelessWidget {
|
||
const _SheetPrimaryButton({required this.label, required this.onTap});
|
||
|
||
final String label;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 52,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: cs.primary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: cs.primaryForeground,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SheetSecondaryButton extends StatelessWidget {
|
||
const _SheetSecondaryButton({required this.label, required this.onTap});
|
||
|
||
final String label;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 52,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: cs.secondary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: cs.border),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: cs.foreground,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ValueRow extends StatelessWidget {
|
||
const _ValueRow({required this.label, required this.value});
|
||
|
||
final String label;
|
||
final String value;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(fontSize: 15, color: cs.foreground),
|
||
),
|
||
),
|
||
Text(
|
||
value,
|
||
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LogoutSheetButton extends StatelessWidget {
|
||
const _LogoutSheetButton({
|
||
required this.label,
|
||
required this.primary,
|
||
required this.onTap,
|
||
});
|
||
|
||
final String label;
|
||
final bool primary;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cs = ShadTheme.of(context).colorScheme;
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Container(
|
||
width: double.infinity,
|
||
height: 52,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: primary ? cs.primary : cs.secondary,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: primary ? null : Border.all(color: cs.border),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: primary ? cs.primaryForeground : cs.foreground,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|