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:
wangjia
2026-07-12 00:07:15 +08:00
parent f6ce5267d5
commit 272eca04ce
66 changed files with 1398 additions and 262 deletions
+9 -19
View File
@@ -41,7 +41,6 @@ class ConnectionState {
required this.phase,
this.elapsed = Duration.zero,
this.error,
this.freeCountdown,
});
final VpnPhase phase;
@@ -50,15 +49,10 @@ class ConnectionState {
/// 连接失败/中断原因(已本地化);null = 无错误。供 UI 提示,不再静默吞掉。
final String? error;
/// 免费版连接期剩余额度(倒计时);null = 不适用(会员/未连接/额度不限)。
/// 由状态机按「连接时锁定的剩余额度 − 已用时长」本地推算,归零即自动切断。
final Duration? freeCountdown;
ConnectionState copyWith({VpnPhase? phase, Duration? elapsed, Duration? freeCountdown}) =>
ConnectionState copyWith({VpnPhase? phase, Duration? elapsed}) =>
ConnectionState(
phase: phase ?? this.phase,
elapsed: elapsed ?? this.elapsed,
freeCountdown: freeCountdown ?? this.freeCountdown,
);
@override
@@ -66,11 +60,10 @@ class ConnectionState {
other is ConnectionState &&
other.phase == phase &&
other.elapsed == elapsed &&
other.error == error &&
other.freeCountdown == freeCountdown;
other.error == error;
@override
int get hashCode => Object.hash(phase, elapsed, error, freeCountdown);
int get hashCode => Object.hash(phase, elapsed, error);
}
// ── 连通看门狗 ─────────────────────────────────────────────────────
@@ -503,17 +496,14 @@ class ConnectionController extends StateNotifier<ConnectionState> {
if (!mounted || state.phase != VpnPhase.on || at == null) return;
final elapsed = _now().difference(at);
Duration? countdown;
// 免费版:到点(锁定额度 − 已用秒 ≤ 0)即自动切断(enforcement,非展示;
// 额度 UI 已移除,不再推算展示用倒计时)。null = 会员/额度不限,不切。
final capSec = _freeRemainingSec;
if (capSec != null) {
final leftSec = capSec - elapsed.inSeconds;
if (leftSec <= 0) {
unawaited(_onFreeQuotaExhausted());
return;
}
countdown = Duration(seconds: leftSec);
if (capSec != null && capSec - elapsed.inSeconds <= 0) {
unawaited(_onFreeQuotaExhausted());
return;
}
state = ConnectionState(phase: VpnPhase.on, elapsed: elapsed, freeCountdown: countdown);
state = ConnectionState(phase: VpnPhase.on, elapsed: elapsed);
}
/// 免费额度耗尽:主动切断隧道(不报节点异常),本地置耗尽让按钮灰化,并拉 me 校准。
+2 -2
View File
@@ -4,10 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 一级视图 + 账户子页。
/// desktop 侧栏暴露 6 个一级项(connect..settings);mobile/tablet 取前 4 项,
/// contact/settings 在 mobile 走账户子页。plans/redeem 是 account 的下钻页。
enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices, purchase, payment }
enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices, purchase, payment, orders }
/// 账户下钻子页(desktop 在内容区切换、顶栏带返回;mobile/tablet 走全屏 push)。
const Set<NavView> kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices, NavView.purchase, NavView.payment};
const Set<NavView> kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices, NavView.purchase, NavView.payment, NavView.orders};
/// 当前视图。
final navViewProvider = StateProvider<NavView>((ref) => NavView.connect);
+19
View File
@@ -0,0 +1,19 @@
// orders_provider.dart — 订单台账数据(列表 + 单条详情)。
// 纯本地台账读取(server 不逐单回查 pay 上游);autoDispose 让每次进入订单页
// 拿到新鲜数据,刷新 = ref.invalidate(...)。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/payment.dart';
import 'payment_provider.dart';
/// 订单列表(server 倒序返回)。刷新:`ref.invalidate(ordersProvider)`。
final ordersProvider = FutureProvider.autoDispose<List<PayOrderSummary>>(
(ref) => ref.watch(paymentApiProvider).listOrders(),
);
/// 单条订单富详情(含 summary 台账字段 + activated/expiresAt)。
/// 刷新:`ref.invalidate(orderDetailProvider(orderNo))`。
final orderDetailProvider =
FutureProvider.autoDispose.family<PayOrderStatus, String>(
(ref, orderNo) => ref.watch(paymentApiProvider).orderStatus(orderNo),
);