Files
ReelShort/lib/pages/wallet/wallet_page.dart
T

450 lines
13 KiB
Dart

// 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 '../../core/widgets/plain_page_scaffold.dart';
import '../store/recharge_page.dart';
import '../store/vip_page.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.light(),
child: PlainPageScaffold(
pageKey: const Key('wallet-page'),
backKey: const Key('wallet-back'),
title: l10n.wallet,
child: _body(l10n, wallet),
),
);
}
Widget _storeActions(AppLocalizations l10n, WalletController wallet) {
return Row(
children: [
Expanded(
child: FilledButton.icon(
key: const Key('wallet-top-up'),
onPressed: () => _openRecharge(wallet),
icon: const Icon(Icons.add_card_rounded, size: 16),
label: Text(l10n.topUpWallet),
style: FilledButton.styleFrom(
backgroundColor: AppColors.blue,
foregroundColor: AppColors.onPrimary,
),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton.icon(
key: const Key('wallet-vip'),
onPressed: () => _openVip(wallet),
icon: const Icon(Icons.workspace_premium_outlined, size: 16),
label: Text(l10n.vipMembership),
style: OutlinedButton.styleFrom(foregroundColor: AppColors.gold),
),
),
],
);
}
void _openRecharge(WalletController wallet) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) =>
RechargePage(onPurchased: () => unawaited(wallet.load())),
),
);
}
void _openVip(WalletController wallet) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => VipPage(onPurchased: () => unawaited(wallet.load())),
),
);
}
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: 12),
_storeActions(l10n, 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: [AppColors.goldCard, AppColors.card],
),
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),
),
),
],
),
);
}
}