From 06c0e8ba317f83f774a28eafb4ad0025a09879c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 31 Jul 2026 13:55:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E4=BB=98=E9=A1=B5=EF=BC=88?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=B5=8F=E8=A7=88=E5=99=A8=E6=94=B6=E9=93=B6?= =?UTF-8?q?=E5=8F=B0=20+=202s=20=E8=BD=AE=E8=AF=A2=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/member/pay_page.dart | 152 ++++++++++++++++++++++++++++++ lib/main.dart | 5 + 2 files changed, 157 insertions(+) create mode 100644 lib/features/member/pay_page.dart 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(),