feat(client): pay v2 支付渠道 switch + 订单列表/详情端到端
购买/支付(Phase B): - 新 SegSwitch 段控 widget(镜像 .segswitch 原子) - 购买页重构:顶部支付渠道 switch(默认 USDT→加密货币 / 人民币→支付宝),套餐价随渠道分币种显示,删底部弹窗选方式;点购买直达支付页 - 支付页加订单信息卡(套餐/渠道/金额按币种) - models:PayChannel + PayCatalogItem.priceUsdtMicro/priceLabel(ch)/perMonthLabel(ch) 订单(P0,全新): - payment_api.listOrders() + orders_provider(列表 + 详情 family) - OrdersScreen 列表→详情(状态 pill / 可复制订单号 / 继续支付·重新购买) - 导航:NavView.orders + desktop_shell 分支 + account 入口 配套: - l10n +19 getter(支付渠道 4 + 订单 15)×6 语言,l10n 闸绿 - 图标单源迁移 lucide_icons_flutter + pangolin_icons.dart(codegen) - 联系渠道订正(Telegram/邮件 support@yanmeiai.com/官网) + 前端去广告&时长 - 测试:购买 switch 分币种 + 订单列表/空态/详情 + 假 API listOrders;golden 重录 - flutter analyze 0 error,flutter test 226 全绿 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// purchase_page.dart — 购买套餐(三档 pro:月/季/年,pay v2 通道)。
|
||||
// 档位数据来自 GET /v1/pay/catalog(server 单源);金额仅展示,扣款以 pay 为准。
|
||||
// 支付渠道即币种即方式(USDT→加密货币 默认 / 人民币→支付宝),顶部 SegSwitch 选定,
|
||||
// 套餐价随渠道币种刷新;点「立即购买」直接下单进支付页(不再底部弹窗选方式)。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -9,8 +11,9 @@ import '../pangolin_theme.dart';
|
||||
import '../state/payment_provider.dart';
|
||||
import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/plan_card.dart';
|
||||
import '../widgets/seg_switch.dart';
|
||||
|
||||
class PurchaseScreen extends ConsumerWidget {
|
||||
class PurchaseScreen extends ConsumerStatefulWidget {
|
||||
const PurchaseScreen({super.key, required this.t, this.onBack, this.onOrderCreated, this.embedded = false});
|
||||
|
||||
final AppText t;
|
||||
@@ -19,6 +22,16 @@ class PurchaseScreen extends ConsumerWidget {
|
||||
final VoidCallback? onOrderCreated;
|
||||
final bool embedded;
|
||||
|
||||
@override
|
||||
ConsumerState<PurchaseScreen> createState() => _PurchaseScreenState();
|
||||
}
|
||||
|
||||
class _PurchaseScreenState extends ConsumerState<PurchaseScreen> {
|
||||
AppText get t => widget.t;
|
||||
|
||||
// 支付渠道:默认 USDT·加密货币。切换即刷新套餐卡显价币种。
|
||||
PayChannel _channel = PayChannel.usdt;
|
||||
|
||||
String _name(String sku) => switch (sku) {
|
||||
'pro_month' => t.proMonthly,
|
||||
'pro_quarter' => t.proQuarterly,
|
||||
@@ -26,57 +39,16 @@ class PurchaseScreen extends ConsumerWidget {
|
||||
_ => sku,
|
||||
};
|
||||
|
||||
String _period(String sku) => switch (sku) {
|
||||
'pro_month' => t.perMonth,
|
||||
'pro_quarter' => t.perQuarter,
|
||||
'pro_year' => t.perYear,
|
||||
_ => '',
|
||||
};
|
||||
|
||||
Future<void> _choose(BuildContext context, WidgetRef ref, PayCatalogItem item) async {
|
||||
final c = context.pangolin;
|
||||
final method = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
backgroundColor: c.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(PangolinRadius.xl)),
|
||||
),
|
||||
builder: (sheetCtx) => SafeArea(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(t.choosePayMethod,
|
||||
style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(PangolinIcons.creditCard, color: c.fg2),
|
||||
title: Text(t.payMethodAlipay, style: PangolinText.body.copyWith(color: c.fg1)),
|
||||
trailing: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3),
|
||||
onTap: () => Navigator.of(sheetCtx).pop('alipay'),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(PangolinIcons.globe, color: c.fg2),
|
||||
title: Text(t.payMethodCrypto, style: PangolinText.body.copyWith(color: c.fg1)),
|
||||
trailing: Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3),
|
||||
onTap: () => Navigator.of(sheetCtx).pop('crypto'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
]),
|
||||
),
|
||||
);
|
||||
if (method == null || !context.mounted) return;
|
||||
await ref.read(paymentFlowProvider.notifier).start(item, method);
|
||||
if (!context.mounted) return;
|
||||
Future<void> _buy(PayCatalogItem item) async {
|
||||
await ref.read(paymentFlowProvider.notifier).start(item, _channel.method);
|
||||
if (!mounted) return;
|
||||
if (ref.read(paymentFlowProvider).phase == PaymentPhase.awaitingPayment) {
|
||||
onOrderCreated?.call();
|
||||
widget.onOrderCreated?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
final catalog = ref.watch(payCatalogProvider);
|
||||
|
||||
@@ -93,25 +65,42 @@ class PurchaseScreen extends ConsumerWidget {
|
||||
data: (items) => ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 20, 24),
|
||||
children: [
|
||||
// 支付渠道 SegSwitch(默认 USDT;人民币走支付宝)→ 套餐价随之刷新
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 2, bottom: 8),
|
||||
child: Text(t.payChannel.toUpperCase(),
|
||||
style: PangolinText.caption.copyWith(
|
||||
color: c.fg3, fontWeight: FontWeight.w700, letterSpacing: 0.6)),
|
||||
),
|
||||
SegSwitch(
|
||||
options: [
|
||||
(icon: PangolinIcons.globe, label: t.payChannelUsdt),
|
||||
(icon: PangolinIcons.creditCard, label: t.payChannelCny),
|
||||
],
|
||||
selectedIndex: _channel == PayChannel.usdt ? 0 : 1,
|
||||
onChanged: (i) => setState(
|
||||
() => _channel = i == 0 ? PayChannel.usdt : PayChannel.cny),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
for (final it in items)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 14, top: it.sku == 'pro_year' ? 12 : 0),
|
||||
child: PlanCard(
|
||||
name: _name(it.sku),
|
||||
price: it.priceLabel(),
|
||||
period: _period(it.sku),
|
||||
price: it.priceLabel(_channel),
|
||||
period: '${it.perMonthLabel(_channel)}${t.perMonthSuffix}',
|
||||
features: t.featsPro,
|
||||
ctaLabel: t.buyNow,
|
||||
featured: it.sku == 'pro_year',
|
||||
popularLabel: it.sku == 'pro_year' ? t.mostPopular : null,
|
||||
onPressed: () => _choose(context, ref, it),
|
||||
onPressed: () => _buy(it),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (embedded) return body;
|
||||
if (widget.embedded) return body;
|
||||
return Scaffold(
|
||||
backgroundColor: c.bg,
|
||||
body: SafeArea(
|
||||
@@ -120,7 +109,7 @@ class PurchaseScreen extends ConsumerWidget {
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 16, 10),
|
||||
child: Row(children: [
|
||||
IconButton(
|
||||
onPressed: onBack ?? () => Navigator.of(context).maybePop(),
|
||||
onPressed: widget.onBack ?? () => Navigator.of(context).maybePop(),
|
||||
icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1),
|
||||
),
|
||||
Text(t.purchaseTitle,
|
||||
|
||||
Reference in New Issue
Block a user