feat: 支付页(系统浏览器收银台 + 2s 轮询确认)
This commit is contained in:
@@ -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<PayPage> createState() => _PayPageState();
|
||||
}
|
||||
|
||||
class _PayPageState extends ConsumerState<PayPage> {
|
||||
PayPhase _phase = PayPhase.launching;
|
||||
Timer? _timer;
|
||||
int _elapsed = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_start();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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('返回'),
|
||||
),
|
||||
]),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<GoRouter>((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(),
|
||||
|
||||
Reference in New Issue
Block a user