feat(app): 完成 P0 钱包解锁、下载落盘与手机号登录
This commit is contained in:
1 parent
de941c841c
commit
136ee74618
47 files changed
+3264
-70
No files matched your search
@@ -0,0 +1,400 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/models/wallet.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_date_time.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/empty_state.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// 金币钱包:余额、累计收支与最近流水。
|
||||
class WalletPage extends StatefulWidget {
|
||||
const WalletPage({super.key});
|
||||
|
||||
@override
|
||||
State<WalletPage> createState() => _WalletPageState();
|
||||
}
|
||||
|
||||
class _WalletPageState extends State<WalletPage> {
|
||||
bool _requested = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_requested) return;
|
||||
_requested = true;
|
||||
final wallet = context.read<WalletController>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) unawaited(wallet.load());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final wallet = context.watch<WalletController>();
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
key: const Key('wallet-page'),
|
||||
backgroundColor: AppColors.ink,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.ink,
|
||||
foregroundColor: AppColors.onSurface,
|
||||
title: Text(l10n.wallet),
|
||||
),
|
||||
body: SafeArea(top: false, child: _body(l10n, wallet)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _body(AppLocalizations l10n, WalletController wallet) {
|
||||
if (wallet.loading && wallet.snapshot == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final snapshot = wallet.snapshot;
|
||||
if (snapshot == null) {
|
||||
return EmptyState(
|
||||
key: const Key('wallet-error'),
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
title: errorText(l10n, wallet.error, l10n.walletLoadFailed),
|
||||
actionLabel: l10n.retry,
|
||||
actionKey: const Key('wallet-retry'),
|
||||
onAction: () => unawaited(wallet.load()),
|
||||
);
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: wallet.load,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 28),
|
||||
children: [
|
||||
_BalanceHero(wallet: snapshot.wallet),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _SummaryTile(
|
||||
icon: Icons.add_card_rounded,
|
||||
label: l10n.totalRecharged,
|
||||
value: snapshot.wallet.totalRechargedCoin,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _SummaryTile(
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
label: l10n.totalConsumed,
|
||||
value: snapshot.wallet.totalConsumedCoin,
|
||||
color: AppColors.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Text(
|
||||
l10n.coinLedger,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: AppDimens.sectionTitle,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (snapshot.ledger.isEmpty)
|
||||
_LedgerEmpty(message: l10n.noCoinLedger)
|
||||
else
|
||||
for (final entry in snapshot.ledger) ...[
|
||||
_LedgerTile(entry: entry),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BalanceHero extends StatelessWidget {
|
||||
const _BalanceHero({required this.wallet});
|
||||
|
||||
final CoinWallet wallet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
key: const Key('wallet-balance-card'),
|
||||
padding: const EdgeInsets.fromLTRB(20, 22, 20, 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF2B2116), Color(0xFF17171D)],
|
||||
),
|
||||
border: Border.all(color: AppColors.gold.withValues(alpha: 0.35)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.gold.withValues(alpha: 0.08),
|
||||
blurRadius: 28,
|
||||
offset: const Offset(0, 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gold.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.monetization_on_rounded,
|
||||
color: AppColors.gold,
|
||||
size: 21,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n.coinBalance,
|
||||
style: const TextStyle(
|
||||
color: AppColors.muted,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${wallet.coinBalance}',
|
||||
key: const Key('wallet-balance'),
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 42,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: Text(
|
||||
l10n.coins,
|
||||
style: const TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryTile extends StatelessWidget {
|
||||
const _SummaryTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final int value;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 18),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$value',
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: AppColors.subtle, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LedgerTile extends StatelessWidget {
|
||||
const _LedgerTile({required this.entry});
|
||||
|
||||
final CoinLedgerEntry entry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final positive = entry.changeAmount >= 0;
|
||||
final color = positive ? AppColors.success : AppColors.accent;
|
||||
final amount = positive ? '+${entry.changeAmount}' : '${entry.changeAmount}';
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
positive ? Icons.south_west_rounded : Icons.north_east_rounded,
|
||||
color: color,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_ledgerLabel(context, entry.bizType),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (entry.remark.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
entry.remark,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.subtle,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (entry.createdAt != null) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
formatLocalizedDateTime(context, entry.createdAt!),
|
||||
style: const TextStyle(
|
||||
color: AppColors.subtle,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
key: Key('wallet-ledger-${entry.id}'),
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${entry.balanceAfter}',
|
||||
style: const TextStyle(color: AppColors.subtle, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _ledgerLabel(BuildContext context, String bizType) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return switch (bizType) {
|
||||
'UNLOCK' => l10n.ledgerUnlock,
|
||||
'RECHARGE' || 'ORDER' => l10n.ledgerRecharge,
|
||||
'REFUND' => l10n.ledgerRefund,
|
||||
_ => l10n.ledgerAdjustment,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _LedgerEmpty extends StatelessWidget {
|
||||
const _LedgerEmpty({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 22),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.receipt_long_outlined, color: AppColors.subtle),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(color: AppColors.subtle, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user