feat: 接入起卦后端流程并完善积分扣减链路

This commit is contained in:
qzl
2026-04-03 19:04:46 +08:00
parent a136e42290
commit d87b2e1e3a
56 changed files with 3310 additions and 809 deletions
@@ -4,7 +4,6 @@ import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../data/models/profile_settings.dart';
import 'language_settings_screen.dart';
import 'settings_placeholder_screen.dart';
import '../widgets/settings_section_widgets.dart';
class GeneralSettingsScreen extends StatefulWidget {
@@ -70,50 +69,8 @@ class _GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
),
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: _openLanguageSettings,
),
SettingsMenuTile(
icon: Icons.auto_awesome_rounded,
title: l10n.settingsAiLanguage,
subtitle: displayLanguageLabel(
l10n,
_settings.preferences.aiLanguage,
),
tint: colors.secondary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
title: l10n.settingsAiLanguage,
value: displayLanguageLabel(
l10n,
_settings.preferences.aiLanguage,
),
description: l10n.settingsAiLanguageHint,
),
),
SettingsMenuTile(
icon: Icons.public_rounded,
title: l10n.settingsTimezone,
subtitle: _settings.preferences.timezone,
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
title: l10n.settingsTimezone,
value: _settings.preferences.timezone,
description: l10n.settingsTimezoneHint,
),
),
SettingsMenuTile(
icon: Icons.flag_outlined,
title: l10n.settingsCountry,
subtitle: _settings.preferences.country,
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: false,
onTap: () => _openPlaceholder(
title: l10n.settingsCountry,
value: _settings.preferences.country,
description: l10n.settingsCountryHint,
),
onTap: _openLanguageSettings,
),
],
),
@@ -144,20 +101,4 @@ class _GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
);
});
}
Future<void> _openPlaceholder({
required String title,
required String value,
required String description,
}) async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => SettingsPlaceholderScreen(
title: title,
value: value,
description: description,
),
),
);
}
}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
@@ -15,9 +16,23 @@ class LegalDocumentScreen extends StatelessWidget {
final String title;
final String assetPath;
Future<String> _loadAsset() async {
try {
final data = await rootBundle.loadString(assetPath);
if (data.isEmpty) {
throw Exception('Asset file is empty: $assetPath');
}
return data;
} catch (e) {
debugPrint('Failed to load asset: $assetPath, error: $e');
rethrow;
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
@@ -27,8 +42,43 @@ class LegalDocumentScreen extends StatelessWidget {
surfaceTintColor: colors.surfaceContainerLow,
),
body: FutureBuilder<String>(
future: rootBundle.loadString(assetPath),
future: _loadAsset(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: colors.error),
const SizedBox(height: AppSpacing.md),
Text(
l10n.legalDocumentLoadFailedTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
Text(
'${l10n.legalDocumentLoadFailedPathPrefix}: $assetPath',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: colors.error),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.xs),
Text(
'${l10n.legalDocumentLoadFailedErrorPrefix}: ${snapshot.error}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: colors.error),
textAlign: TextAlign.center,
),
],
),
),
);
}
if (!snapshot.hasData) {
return const Center(
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
@@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../data/models/profile_settings.dart';
import '../widgets/settings_section_widgets.dart';
import 'coin_center_screen.dart';
import 'general_settings_screen.dart';
import 'legal_center_screen.dart';
import 'privacy_notification_settings_screen.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({
@@ -44,7 +42,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
@@ -62,10 +59,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
AppSpacing.xl,
),
children: [
ProfileHeaderCard(
account: widget.account,
version: _settings.version,
),
ProfileHeaderCard(account: widget.account),
const SizedBox(height: AppSpacing.lg),
WalletHeroCard(
balance: widget.coinBalance,
@@ -76,39 +70,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
SectionLabel(text: l10n.settingsSectionQuickAccess),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.toll_rounded,
title: l10n.settingsCoinCenterTitle,
subtitle: l10n.settingsCoinCenterSubtitle(widget.coinBalance),
tint: palette.historyGoldText,
background: palette.historyGoldBg,
onTap: _openCoinCenter,
),
SettingsMenuTile(
icon: Icons.tune_rounded,
title: l10n.settingsGeneralTitle,
subtitle: l10n.settingsGeneralSubtitle(
displayLanguageLabel(
l10n,
_settings.preferences.interfaceLanguage,
),
),
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: _openGeneralSettings,
),
SettingsMenuTile(
icon: Icons.privacy_tip_outlined,
title: l10n.settingsPrivacyAndNotificationTitle,
subtitle: l10n.settingsPrivacyAndNotificationSubtitle,
tint: palette.warning,
background: palette.warningContainer,
onTap: _openPrivacyAndNotification,
),
SettingsMenuTile(
icon: Icons.description_outlined,
title: l10n.settingsLegalCenterTitle,
subtitle: l10n.settingsLegalCenterSubtitle,
tint: colors.secondary,
background: colors.surfaceContainerHighest,
showDivider: false,
@@ -117,21 +88,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
const SizedBox(height: AppSpacing.xl),
SectionLabel(text: l10n.settingsSectionAccount),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.logout_rounded,
title: l10n.logout,
subtitle: l10n.settingsLogoutSubtitle,
tint: colors.error,
background: colors.error.withValues(alpha: 0.08),
showDivider: false,
onTap: _confirmLogout,
),
],
),
const SizedBox(height: AppSpacing.xl),
FilledButton(
onPressed: _isLoggingOut ? null : _confirmLogout,
style: FilledButton.styleFrom(
@@ -175,14 +131,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
}
Future<void> _openPrivacyAndNotification() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => PrivacyNotificationSettingsScreen(settings: _settings),
),
);
}
Future<void> _openLegalCenter() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(builder: (_) => const LegalCenterScreen()),
@@ -55,18 +55,18 @@ class SettingsMenuTile extends StatelessWidget {
super.key,
required this.icon,
required this.title,
required this.subtitle,
required this.tint,
required this.background,
required this.onTap,
this.showDivider = true,
this.showChevron = true,
this.trailing,
this.subtitle,
});
final IconData icon;
final String title;
final String subtitle;
final String? subtitle;
final Color tint;
final Color background;
final VoidCallback onTap;
@@ -95,10 +95,12 @@ class SettingsMenuTile extends StatelessWidget {
child: Icon(icon, color: tint),
),
title: Text(title),
subtitle: Padding(
padding: const EdgeInsets.only(top: AppSpacing.xs),
child: Text(subtitle),
),
subtitle: subtitle == null
? null
: Padding(
padding: const EdgeInsets.only(top: AppSpacing.xs),
child: Text(subtitle!),
),
trailing:
trailing ??
(showChevron
@@ -118,18 +120,12 @@ class SettingsMenuTile extends StatelessWidget {
}
class ProfileHeaderCard extends StatelessWidget {
const ProfileHeaderCard({
super.key,
required this.account,
required this.version,
});
const ProfileHeaderCard({super.key, required this.account});
final String account;
final int version;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Card(
margin: EdgeInsets.zero,
@@ -153,14 +149,10 @@ class ProfileHeaderCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(account, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: AppSpacing.xs),
Text(
'${l10n.settingsVersionLabel}: v$version',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Icon(Icons.edit_outlined, color: colors.outline, size: 20),
],
),
),
@@ -185,17 +177,18 @@ class WalletHeroCard extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.xl),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
colors: [colors.primary, palette.accentPurple],
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.xl),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
colors: [colors.primary, palette.accentPurple],
),
),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -213,6 +206,13 @@ class WalletHeroCard extends StatelessWidget {
color: colors.onPrimary,
),
),
const SizedBox(width: AppSpacing.lg),
Text(
l10n.settingsCoinBalanceValue(balance),
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: colors.onPrimary,
),
),
const Spacer(),
Icon(
Icons.chevron_right_rounded,
@@ -220,18 +220,14 @@ class WalletHeroCard extends StatelessWidget {
),
],
),
const SizedBox(height: AppSpacing.lg),
Text(
l10n.settingsCoinBalanceValue(balance),
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(color: colors.onPrimary),
),
const SizedBox(height: AppSpacing.xs),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.92),
const SizedBox(height: AppSpacing.md),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.92),
),
),
),
],