add
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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<UsdtPayScreen> createState() => _UsdtPayScreenState();
|
||||
}
|
||||
|
||||
class _UsdtPayScreenState extends ConsumerState<UsdtPayScreen> {
|
||||
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<void> _cancel(BuildContext context) async {
|
||||
final ok = await showDialog<bool>(
|
||||
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<void>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user