diff --git a/lib/features/member/pay_page.dart b/lib/features/member/pay_page.dart new file mode 100644 index 0000000..5df5061 --- /dev/null +++ b/lib/features/member/pay_page.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'member_provider.dart'; + +class PayArgs { + final String orderNo; + final String payUrl; + + const PayArgs({required this.orderNo, required this.payUrl}); +} + +enum PayPhase { launching, paying, paid, timeout, failed } + +/// 支付页:打开系统浏览器收银台,2s 轮询订单状态(上限 60s) +class PayPage extends ConsumerStatefulWidget { + final PayArgs args; + + const PayPage({super.key, required this.args}); + + @override + ConsumerState createState() => _PayPageState(); +} + +class _PayPageState extends ConsumerState { + PayPhase _phase = PayPhase.launching; + Timer? _timer; + int _elapsed = 0; + + @override + void initState() { + super.initState(); + _start(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _start() async { + try { + final ok = await launchUrl(Uri.parse(widget.args.payUrl), + mode: LaunchMode.externalApplication); + if (!ok) { + setState(() => _phase = PayPhase.failed); + return; + } + setState(() => _phase = PayPhase.paying); + } catch (e) { + setState(() => _phase = PayPhase.failed); + return; + } + _timer = Timer.periodic(const Duration(seconds: 2), (_) => _check()); + } + + Future _check() async { + _elapsed += 2; + try { + final status = await fetchOrderStatus(ref, widget.args.orderNo); + if (status == 'paid') { + _timer?.cancel(); + if (!mounted) return; + setState(() => _phase = PayPhase.paid); + Fluttertoast.showToast(msg: '会员开通成功'); + return; + } + if (_elapsed >= 60) { + _timer?.cancel(); + if (!mounted) return; + setState(() => _phase = PayPhase.timeout); + } + } catch (_) { + // 轮询失败不中断,下次再试 + } + } + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Scaffold( + appBar: AppBar(title: const Text('会员支付')), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + switch (_phase) { + PayPhase.launching || + PayPhase.paying => Column(children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + const Text('请在浏览器中完成支付,正在确认结果…'), + const SizedBox(height: 8), + Text('订单号 ${widget.args.orderNo}', + style: const TextStyle(color: Colors.grey, fontSize: 12)), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => _check(), + child: const Text('我已完成支付'), + ), + ]), + PayPhase.paid => Column(children: [ + Icon(Icons.check_circle, size: 64, color: scheme.primary), + const SizedBox(height: 12), + const Text('支付成功,会员已开通!', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + FilledButton( + onPressed: () => context.pop(), + child: const Text('返回会员中心'), + ), + ]), + PayPhase.timeout => Column(children: [ + Icon(Icons.hourglass_empty, size: 64, color: Colors.orange), + const SizedBox(height: 12), + const Text('支付结果确认中'), + const SizedBox(height: 8), + const Text('可稍后到会员中心查看开通状态,以支付结果为准', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey, fontSize: 12)), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => context.pop(), + child: const Text('返回'), + ), + ]), + PayPhase.failed => Column(children: [ + Icon(Icons.error_outline, size: 64, color: scheme.error), + const SizedBox(height: 12), + const Text('无法打开支付页面'), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => context.pop(), + child: const Text('返回'), + ), + ]), + }, + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 14f37a0..375861c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'features/auth/login_page.dart'; import 'features/home/home_page.dart'; import 'features/member/member_center_page.dart'; +import 'features/member/pay_page.dart'; import 'features/outfit/outfit_page.dart'; import 'features/outfit/outfit_provider.dart'; import 'features/outfit/plan_effect_page.dart'; @@ -79,6 +80,10 @@ final routerProvider = Provider((ref) { path: '/commercial', builder: (ctx, state) => const MemberCenterPage(), ), + GoRoute( + path: '/pay', + builder: (context, state) => PayPage(args: state.extra! as PayArgs), + ), GoRoute( path: '/plan-viewer', builder: (ctx, state) => const PlanViewerPage(),