home_page.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /// 首页:商品展示 + 7 种支付方式入口
  2. ///
  3. /// 渠道与流程(详见 docs/):
  4. /// - 微信支付:移动端 App 支付 / Web Native 扫码
  5. /// - 支付宝支付:移动端 App 支付 / Web WAP 跳转
  6. /// - 云闪付:移动端 App 支付(拉起云闪付 App)/ Web 银联收银台跳转
  7. /// - Stripe 支付:PaymentSheet 卡片支付
  8. /// - Apple Pay / Google Pay:经 Stripe 确认
  9. /// - 网页收银台:聚合支付(微信扫码 / 支付宝 / Stripe 跳转)
  10. library;
  11. import 'package:flutter/material.dart';
  12. import '../models/payment.dart';
  13. import '../service/api_service.dart';
  14. import '../service/pay_service.dart';
  15. import '../utils/toast.dart';
  16. class HomePage extends StatefulWidget {
  17. const HomePage({super.key});
  18. @override
  19. State<HomePage> createState() => _HomePageState();
  20. }
  21. class _HomePageState extends State<HomePage> {
  22. final PayService _pay = PayService.instance;
  23. final APIServer _api = APIServer();
  24. PaymentProduct? _product;
  25. bool _loading = true;
  26. @override
  27. void initState() {
  28. super.initState();
  29. _loadProduct();
  30. }
  31. /// 拉取商品(失败时使用本地兜底商品,保证页面可展示)
  32. Future<void> _loadProduct() async {
  33. try {
  34. final products = await _api.fetchProducts();
  35. if (!mounted) return;
  36. setState(() {
  37. _product = products.consume.isNotEmpty ? products.consume.first : null;
  38. _loading = false;
  39. });
  40. } on Exception catch (e) {
  41. if (!mounted) return;
  42. setState(() {
  43. _product = PaymentProduct(
  44. id: 'p001',
  45. name: '橘子',
  46. quota: 1,
  47. retailPrice: 1100,
  48. retailPriceUSD: 150,
  49. expirePolicy: 'once',
  50. expirePolicyText: '一次性',
  51. );
  52. _loading = false;
  53. });
  54. showErrorMessage('商品加载失败,已使用演示数据:${_api.resolveError(e)}');
  55. }
  56. }
  57. /// 打开网页收银台渠道选择
  58. Future<void> _pickCheckoutChannel() async {
  59. final product = _product;
  60. if (product == null) return;
  61. if (!mounted) return;
  62. final channel = await showDialog<String>(
  63. context: context,
  64. builder: (ctx) => AlertDialog(
  65. title: const Text('选择收银台支付渠道'),
  66. content: const Column(
  67. mainAxisSize: MainAxisSize.min,
  68. children: [
  69. ListTile(
  70. leading: Icon(Icons.qr_code, color: Colors.green),
  71. title: Text('微信支付(扫码)'),
  72. subtitle: Text('Native 扫码'),
  73. ),
  74. ListTile(
  75. leading: Icon(Icons.payment, color: Colors.blue),
  76. title: Text('支付宝'),
  77. subtitle: Text('电脑/手机网站跳转'),
  78. ),
  79. ListTile(
  80. leading: Icon(Icons.credit_card, color: Colors.indigo),
  81. title: Text('Stripe'),
  82. subtitle: Text('托管收银台跳转'),
  83. ),
  84. ],
  85. ),
  86. actions: [
  87. TextButton(
  88. onPressed: () => Navigator.of(ctx).pop('wechat'),
  89. child: const Text('微信'),
  90. ),
  91. TextButton(
  92. onPressed: () => Navigator.of(ctx).pop('alipay'),
  93. child: const Text('支付宝'),
  94. ),
  95. TextButton(
  96. onPressed: () => Navigator.of(ctx).pop('stripe'),
  97. child: const Text('Stripe'),
  98. ),
  99. TextButton(
  100. onPressed: () => Navigator.of(ctx).pop(),
  101. child: const Text('取消'),
  102. ),
  103. ],
  104. ),
  105. );
  106. if (channel == null) return;
  107. if (!mounted) return;
  108. await _pay.payWithCheckout(context, channel: channel, productId: product.id);
  109. }
  110. @override
  111. Widget build(BuildContext context) {
  112. final product = _product;
  113. return Scaffold(
  114. appBar: AppBar(
  115. title: const Text('支付演示'),
  116. centerTitle: true,
  117. ),
  118. body: _loading || product == null
  119. ? const Center(child: CircularProgressIndicator())
  120. : SingleChildScrollView(
  121. padding: const EdgeInsets.all(16),
  122. child: Column(
  123. crossAxisAlignment: CrossAxisAlignment.stretch,
  124. children: [
  125. _buildProductCard(product),
  126. const SizedBox(height: 24),
  127. const Text(
  128. '选择支付方式',
  129. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  130. ),
  131. const SizedBox(height: 12),
  132. _buildPayButtons(product),
  133. ],
  134. ),
  135. ),
  136. );
  137. }
  138. /// 商品卡片
  139. Widget _buildProductCard(PaymentProduct product) {
  140. return Card(
  141. elevation: 2,
  142. child: Padding(
  143. padding: const EdgeInsets.all(20),
  144. child: Row(
  145. children: [
  146. Container(
  147. width: 72,
  148. height: 72,
  149. decoration: BoxDecoration(
  150. color: Colors.orange.shade100,
  151. borderRadius: BorderRadius.circular(12),
  152. ),
  153. child: const Icon(Icons.agriculture, size: 40, color: Colors.orange),
  154. ),
  155. const SizedBox(width: 16),
  156. Expanded(
  157. child: Column(
  158. crossAxisAlignment: CrossAxisAlignment.start,
  159. children: [
  160. Text(product.name,
  161. style: const TextStyle(
  162. fontSize: 18, fontWeight: FontWeight.bold)),
  163. const SizedBox(height: 4),
  164. Text('数量:${product.quota}${product.expirePolicyText}',
  165. style: const TextStyle(color: Colors.grey)),
  166. const SizedBox(height: 4),
  167. Row(
  168. children: [
  169. Text(product.retailPriceText,
  170. style: TextStyle(
  171. fontSize: 18,
  172. fontWeight: FontWeight.bold,
  173. color: Colors.red.shade600,
  174. )),
  175. if (product.retailPriceUSD > 0) ...[
  176. const SizedBox(width: 8),
  177. Text(product.retailPriceUSDText,
  178. style: const TextStyle(color: Colors.grey)),
  179. ],
  180. ],
  181. ),
  182. ],
  183. ),
  184. ),
  185. ],
  186. ),
  187. ),
  188. );
  189. }
  190. /// 7 个支付按钮
  191. Widget _buildPayButtons(PaymentProduct product) {
  192. final entries = <_PayEntry>[
  193. _PayEntry('微信支付', Icons.wechat, Colors.green,
  194. onTap: () => _pay.payWithWechat(context, productId: product.id)),
  195. _PayEntry('支付宝支付', Icons.account_balance_wallet, Colors.blue,
  196. onTap: () => _pay.payWithAlipay(context, productId: product.id)),
  197. _PayEntry('云闪付', Icons.account_balance, Colors.red,
  198. onTap: () => _pay.payWithUnionPay(context, productId: product.id)),
  199. _PayEntry('Stripe 支付', Icons.credit_card, Colors.indigo,
  200. onTap: () => _pay.payWithStripe(context, productId: product.id)),
  201. _PayEntry('Apple Pay', Icons.apple, Colors.black,
  202. onTap: () => _pay.payWithApple(context, product: product)),
  203. _PayEntry('Google Pay', Icons.g_mobiledata, Colors.deepOrange,
  204. onTap: () => _pay.payWithGoogle(context, product: product)),
  205. _PayEntry('网页收银台', Icons.storefront, Colors.teal,
  206. onTap: _pickCheckoutChannel),
  207. ];
  208. return GridView.count(
  209. crossAxisCount: 2,
  210. shrinkWrap: true,
  211. physics: const NeverScrollableScrollPhysics(),
  212. mainAxisSpacing: 12,
  213. crossAxisSpacing: 12,
  214. childAspectRatio: 1.6,
  215. children: entries.map((e) => e.build()).toList(),
  216. );
  217. }
  218. }
  219. /// 支付入口项
  220. class _PayEntry {
  221. final String label;
  222. final IconData icon;
  223. final Color color;
  224. final VoidCallback onTap;
  225. _PayEntry(this.label, this.icon, this.color, {required this.onTap});
  226. Widget build() {
  227. return Material(
  228. color: color.withValues(alpha: 0.06),
  229. borderRadius: BorderRadius.circular(12),
  230. child: InkWell(
  231. borderRadius: BorderRadius.circular(12),
  232. onTap: onTap,
  233. child: Column(
  234. mainAxisAlignment: MainAxisAlignment.center,
  235. children: [
  236. Icon(icon, color: color, size: 32),
  237. const SizedBox(height: 8),
  238. Text(label, style: const TextStyle(fontSize: 14)),
  239. ],
  240. ),
  241. ),
  242. );
  243. }
  244. }