Files
video/app/lib/screens/assets/withdraw_screen.dart
T
2026-10-15 20:36:59 +08:00

287 lines
9.1 KiB
Dart
Raw 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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/network/api_exception.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/asset_provider.dart';
import '../../services/asset_service.dart';
import '../feed/widgets/feed_overlays.dart';
import 'assets_screen.dart';
/// 提现页(《03-接口文档》9.5/9.6):提现表单 + 提现记录
class WithdrawScreen extends ConsumerStatefulWidget {
const WithdrawScreen({super.key});
@override
ConsumerState<WithdrawScreen> createState() => _WithdrawScreenState();
}
class _WithdrawScreenState extends ConsumerState<WithdrawScreen> {
final _amountCtrl = TextEditingController();
final _accountCtrl = TextEditingController();
int _channel = 1;
bool _submitting = false;
static const _channels = [
{'value': 1, 'label': '支付宝', 'hint': '支付宝账号(手机号/邮箱)'},
{'value': 2, 'label': '微信', 'hint': '微信收款账号'},
{'value': 3, 'label': 'USDT', 'hint': 'USDT 收款地址(TRC20)'},
];
@override
void dispose() {
_amountCtrl.dispose();
_accountCtrl.dispose();
super.dispose();
}
Map<String, Object> get _channelCfg =>
_channels.firstWhere((c) => c['value'] == _channel);
@override
Widget build(BuildContext context) {
final asset = ref.watch(assetProvider);
final records = ref.watch(withdrawRecordsProvider);
final wallet = asset.wallet;
return Scaffold(
appBar: AppBar(title: const Text('申请提现')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildForm(wallet),
const SizedBox(height: 24),
const Text(
'提现记录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
if (records.isLoading && records.items.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: CircularProgressIndicator()),
)
else if (records.items.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(
child: Text('暂无提现记录',
style: TextStyle(color: AppColors.textWeak)),
),
)
else ...[
for (final r in records.items) _WithdrawTile(record: r),
if (records.hasMore)
TextButton(
onPressed: () =>
ref.read(withdrawRecordsProvider.notifier).loadMore(),
child: const Text('加载更多'),
),
],
],
),
);
}
Widget _buildForm(AssetWallet? wallet) {
final hasPending = wallet?.hasPendingWithdraw ?? false;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'可提资产 ${fmtAssets(wallet?.assets ?? 0)}',
style: const TextStyle(color: AppColors.textSecondary),
),
const Spacer(),
Text(
'最低 ${wallet?.minWithdrawAssets ?? 100} 资产',
style:
const TextStyle(color: AppColors.textWeak, fontSize: 12),
),
],
),
const SizedBox(height: 12),
// 金额
TextField(
controller: _amountCtrl,
enabled: !hasPending,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
],
decoration: InputDecoration(
labelText: '提现金额(资产)',
suffix: TextButton(
onPressed: hasPending || wallet == null
? null
: () => _amountCtrl.text = fmtAssets(wallet.assets),
child: const Text('全部提现'),
),
),
),
const SizedBox(height: 12),
// 渠道
Row(
children: _channels.map((c) {
final active = _channel == c['value'];
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(c['label'] as String),
selected: active,
onSelected: hasPending
? null
: (_) => setState(() => _channel = c['value'] as int),
selectedColor: AppColors.primary,
backgroundColor: AppColors.surface,
labelStyle: TextStyle(
color: active ? Colors.white : AppColors.textSecondary,
),
),
);
}).toList(),
),
const SizedBox(height: 12),
// 账号
TextField(
controller: _accountCtrl,
enabled: !hasPending,
decoration: InputDecoration(
labelText: '收款账号',
hintText: _channelCfg['hint'] as String,
),
),
if (hasPending) ...[
const SizedBox(height: 12),
const Text(
'有一笔提现审核中,审核完成后才能再次申请',
style: TextStyle(color: AppColors.gold, fontSize: 12),
),
],
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_submitting || hasPending ? null : () => _submit(wallet),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 44),
),
child: Text(_submitting ? '提交中…' : '提交申请'),
),
),
],
),
);
}
Future<void> _submit(AssetWallet? wallet) async {
final amount = double.tryParse(_amountCtrl.text.trim());
final account = _accountCtrl.text.trim();
final minAssets = wallet?.minWithdrawAssets ?? 100;
if (amount == null || amount <= 0) {
showTopToast(context, '请输入提现金额');
return;
}
if (amount < minAssets) {
showTopToast(context, '最低提现 $minAssets 资产');
return;
}
if (wallet != null && amount > wallet.assets) {
showTopToast(context, '超出可提资产余额');
return;
}
if (account.isEmpty) {
showTopToast(context, '请输入收款账号');
return;
}
setState(() => _submitting = true);
try {
final result = await AssetService.applyWithdraw(
assets: amount,
channel: _channel,
account: account,
);
if (!mounted) return;
showTopToast(
context,
'提现申请已提交:¥${result.amountYuan.toStringAsFixed(2)},等待审核',
);
_amountCtrl.clear();
// 刷新资产与记录
ref.read(assetProvider.notifier).load();
ref.read(withdrawRecordsProvider.notifier).load();
} on ApiException catch (e) {
if (mounted) showTopToast(context, e.message);
} catch (_) {
if (mounted) showTopToast(context, '提交失败,请稍后重试');
} finally {
if (mounted) setState(() => _submitting = false);
}
}
}
class _WithdrawTile extends StatelessWidget {
const _WithdrawTile({required this.record});
final WithdrawRecord record;
Color get _statusColor => switch (record.status) {
1 => Colors.greenAccent,
2 => AppColors.primary,
_ => AppColors.gold,
};
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'¥${record.amountYuan.toStringAsFixed(2)}(${fmtAssets(record.assets)} 资产)',
style: const TextStyle(fontSize: 15),
),
Text(
'${record.channelLabel} · ${record.withdrawNo}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textWeak,
fontSize: 12,
),
),
Text(
record.createdAt,
style: const TextStyle(
color: AppColors.textWeak,
fontSize: 11,
),
),
],
),
),
Text(
record.statusText,
style: TextStyle(color: _statusColor, fontSize: 13),
),
],
),
);
}
}