feat(client/purchase): 全员优惠月付特殊卡片(¥6/$1,随渠道显价)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,14 @@ import '../widgets/pangolin_icons.dart';
|
||||
import '../widgets/plan_card.dart';
|
||||
import '../widgets/seg_switch.dart';
|
||||
|
||||
/// 手写 firstWhere-or-null(避免仅为一次查找引入 package:collection 依赖)。
|
||||
T? _firstWhereOrNull<T>(List<T> items, bool Function(T) test) {
|
||||
for (final it in items) {
|
||||
if (test(it)) return it;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class PurchaseScreen extends ConsumerStatefulWidget {
|
||||
const PurchaseScreen({super.key, required this.t, this.onBack, this.onOrderCreated, this.embedded = false});
|
||||
|
||||
@@ -62,42 +70,58 @@ class _PurchaseScreenState extends ConsumerState<PurchaseScreen> {
|
||||
style: PangolinText.body.copyWith(color: c.fg3)),
|
||||
),
|
||||
),
|
||||
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)
|
||||
data: (items) {
|
||||
final promoItem = _firstWhereOrNull(items, (i) => i.promo);
|
||||
final planItems = items.where((i) => !i.promo).toList();
|
||||
final originalItem = _firstWhereOrNull(items, (i) => i.sku == 'pro_month');
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 20, 24),
|
||||
children: [
|
||||
// 支付渠道 SegSwitch(默认 USDT;人民币走支付宝)→ 套餐价随之刷新
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 14, top: it.sku == 'pro_year' ? 12 : 0),
|
||||
child: PlanCard(
|
||||
name: _name(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: () => _buy(it),
|
||||
),
|
||||
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),
|
||||
if (promoItem != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: _PromoCard(
|
||||
t: t,
|
||||
channel: _channel,
|
||||
item: promoItem,
|
||||
originalItem: originalItem,
|
||||
onPressed: () => _buy(promoItem),
|
||||
),
|
||||
),
|
||||
for (final it in planItems)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 14, top: it.sku == 'pro_year' ? 12 : 0),
|
||||
child: PlanCard(
|
||||
name: _name(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: () => _buy(it),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.embedded) return body;
|
||||
@@ -122,3 +146,104 @@ class _PurchaseScreenState extends ConsumerState<PurchaseScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// _PromoCard — 全员优惠月付特殊卡片(横幅式,视觉上与普通 PlanCard 拉开层次)。
|
||||
/// 徽章 + 大号现价(随渠道 ¥/$ 联动)+ 可选原价划线(以 pro_month 为参照)。
|
||||
class _PromoCard extends StatelessWidget {
|
||||
const _PromoCard({
|
||||
required this.t,
|
||||
required this.channel,
|
||||
required this.item,
|
||||
required this.originalItem,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final AppText t;
|
||||
final PayChannel channel;
|
||||
final PayCatalogItem item;
|
||||
final PayCatalogItem? originalItem;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final originalLabel = originalItem?.priceLabel(channel);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [PangolinColors.clay500, PangolinColors.clay700],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(PangolinRadius.xl),
|
||||
boxShadow: PangolinShadow.md,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: PangolinColors.white.withValues(alpha: 0.22),
|
||||
borderRadius: BorderRadius.circular(PangolinRadius.full),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(PangolinIcons.zap, size: 13, color: PangolinColors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(t.promoBadge,
|
||||
style: PangolinText.caption.copyWith(
|
||||
color: PangolinColors.white, fontWeight: FontWeight.w700, fontSize: 11)),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(t.proMonthly,
|
||||
style: PangolinText.sm.copyWith(
|
||||
color: PangolinColors.white.withValues(alpha: 0.85), fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(item.priceLabel(channel),
|
||||
style: PangolinText.display.copyWith(color: PangolinColors.white, fontSize: 30)),
|
||||
Text(t.perMonthSuffix,
|
||||
style: PangolinText.caption.copyWith(
|
||||
color: PangolinColors.white.withValues(alpha: 0.85), fontSize: 13)),
|
||||
if (originalLabel != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(originalLabel,
|
||||
style: PangolinText.sm.copyWith(
|
||||
color: PangolinColors.white.withValues(alpha: 0.62),
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Material(
|
||||
color: PangolinColors.white,
|
||||
shape: const StadiumBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Center(
|
||||
child: Text(t.buyNow,
|
||||
style: PangolinText.sm.copyWith(
|
||||
fontWeight: FontWeight.w700, fontSize: 14, color: PangolinColors.clay700)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user