Files
2026-06-18 15:02:28 +08:00

1232 lines
38 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.
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,
),
),
),
);
}
}