import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../providers/payment_provider.dart'; import '../../providers/auth_provider.dart'; import '../../services/payment_service.dart'; import '../../services/user_service.dart'; import '../feed/widgets/feed_overlays.dart'; /// USDT 支付页:金额(含尾差)/ 收款地址 / 网络 / 倒计时 / 轮询到账 / 取消订单 class UsdtPayScreen extends ConsumerStatefulWidget { const UsdtPayScreen({super.key, required this.order}); final RechargeOrderInfo order; @override ConsumerState createState() => _UsdtPayScreenState(); } class _UsdtPayScreenState extends ConsumerState { Timer? _ticker; Duration _remain = Duration.zero; bool _resultHandled = false; @override void initState() { super.initState(); _remain = _calcRemain(); _ticker = Timer.periodic(const Duration(seconds: 1), (_) { final r = _calcRemain(); if (!mounted) return; setState(() => _remain = r); }); } Duration _calcRemain() { final expire = DateTime.tryParse(widget.order.expireAt); if (expire == null) return Duration.zero; final diff = expire.difference(DateTime.now()); return diff.isNegative ? Duration.zero : diff; } @override void dispose() { _ticker?.cancel(); ref.read(orderPollProvider(widget.order).notifier).stop(); super.dispose(); } @override Widget build(BuildContext context) { final poll = ref.watch(orderPollProvider(widget.order)); final order = widget.order; // 轮询结果处理(仅一次) if (!_resultHandled && poll.status != OrderPollStatus.polling) { _resultHandled = true; WidgetsBinding.instance.addPostFrameCallback((_) => _onResult(poll)); } final mm = _remain.inMinutes.remainder(60).toString().padLeft(2, '0'); final ss = _remain.inSeconds.remainder(60).toString().padLeft(2, '0'); return Scaffold( appBar: AppBar(title: const Text('USDT 支付')), body: ListView( padding: const EdgeInsets.all(16), children: [ // 金额卡 Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ const Text('请精确转账以下金额(含尾差用于订单匹配)', style: TextStyle( color: AppColors.textSecondary, fontSize: 12)), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( order.usdtAmount ?? '-', style: const TextStyle( fontSize: 34, fontWeight: FontWeight.w800, color: AppColors.gold), ), const SizedBox(width: 6), const Padding( padding: EdgeInsets.only(bottom: 6), child: Text('USDT', style: TextStyle( color: AppColors.textSecondary, fontSize: 14)), ), ], ), const SizedBox(height: 6), Text( '≈ ¥${order.amountYuan}(汇率 ${order.usdtRate ?? '-'})· 到账 ${order.points} 积分', style: const TextStyle( color: AppColors.textSecondary, fontSize: 12), ), ], ), ), const SizedBox(height: 12), // 收款信息 _InfoRow( label: '收款地址', value: order.receiveAddress ?? '-', onCopy: () => _copy(order.receiveAddress, '地址已复制'), ), _InfoRow(label: '网络', value: order.network ?? 'TRC20'), _InfoRow(label: '订单号', value: order.orderNo), const SizedBox(height: 12), // 倒计时 + 轮询状态 Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary), ), const SizedBox(width: 10), Expanded( child: Text( _remain == Duration.zero ? '订单已过期,请重新下单' : '等待链上确认(19 个区块)… 剩余 $mm:$ss', style: const TextStyle(fontSize: 13), ), ), ], ), ), const SizedBox(height: 16), const Text( '注意事项:\n' '· 请务必按显示金额精确转账,金额不符将无法自动到账\n' '· 仅支持 TRC20 网络,其他网络转账无法找回\n' '· 满 19 个区块确认后积分自动到账', style: TextStyle(color: AppColors.textWeak, fontSize: 12, height: 1.6), ), const SizedBox(height: 20), TextButton( onPressed: () => _cancel(context), child: const Text('取消订单', style: TextStyle(color: AppColors.textSecondary)), ), ], ), ); } void _copy(String? text, String tip) { if (text == null || text.isEmpty) return; Clipboard.setData(ClipboardData(text: text)); showTopToast(context, tip); } void _refreshUser() async { // 支付成功后刷新全局用户信息(积分余额) try { final user = await UserService.fetchMe(); await ref.read(authProvider.notifier).onLoginSuccess(user); } catch (_) {} } Future _cancel(BuildContext context) async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppColors.surface, title: const Text('取消订单', style: TextStyle(fontSize: 16)), content: const Text('取消后该笔订单将关闭,是否继续?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('继续支付'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('取消订单', style: TextStyle(color: AppColors.primary)), ), ], ), ); if (ok != true || !context.mounted) return; try { await ref.read(orderPollProvider(widget.order).notifier).cancel(); } catch (_) { if (context.mounted) showTopToast(context, '取消失败,请稍后重试'); } } void _onResult(OrderPollState poll) { if (!mounted) return; if (poll.status == OrderPollStatus.paid) { _refreshUser(); showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( backgroundColor: AppColors.surface, title: const Text('充值成功', style: TextStyle(fontSize: 16)), content: Text( '已到账 ${widget.order.points} 积分,当前余额 ${poll.walletPoints ?? '-'}'), actions: [ TextButton( onPressed: () { Navigator.pop(ctx); context.pop(true); // 返回充值页并标记已支付 }, child: const Text('确定'), ), ], ), ); } else { showTopToast( context, poll.status == OrderPollStatus.timeout ? '查询超时,请稍后在钱包查看' : '订单已关闭'); context.pop(); } } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value, this.onCopy}); final String label; final String value; final VoidCallback? onCopy; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 1), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), color: AppColors.surface, child: Row( children: [ SizedBox( width: 64, child: Text(label, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13)), ), Expanded( child: Text( value, style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis, ), ), if (onCopy != null) GestureDetector( onTap: onCopy, child: const Padding( padding: EdgeInsets.only(left: 8), child: Icon(Icons.copy, size: 16, color: AppColors.textSecondary), ), ), ], ), ); } }