// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/product.dart'; import '../../core/data/repositories/store_repository.dart'; import '../../core/data/services/store_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../../l10n/app_localizations.dart'; /// 商品货架:金币包与会员档位共用一套真实接口渲染。 class StorePage extends StatefulWidget { const StorePage({ super.key, required this.type, this.repository, this.onPurchased, }); final String type; final StoreRepository? repository; final VoidCallback? onPurchased; @override State createState() => _StorePageState(); } class _StorePageState extends State { late final StoreController controller; @override void initState() { super.initState(); controller = StoreController( widget.repository ?? context.read(), type: widget.type, ); unawaited(controller.load()); } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final isVip = widget.type == StoreController.vipType; return Theme( data: AppTheme.dark(), child: PlainPageScaffold( pageKey: const Key('store-page'), backKey: const Key('store-back'), title: isVip ? l10n.vipTitle : l10n.rechargeTitle, child: ListenableBuilder( listenable: controller, builder: (context, _) => _body(l10n, isVip), ), ), ); } Widget _body(AppLocalizations l10n, bool isVip) { if (controller.loading && controller.products.isEmpty) { return const Center( child: CircularProgressIndicator(key: Key('store-loading')), ); } if (controller.error != null && controller.products.isEmpty) { return EmptyState( key: const Key('store-error'), icon: Icons.cloud_off_rounded, title: errorText(l10n, controller.error, l10n.productsLoadFailed), actionLabel: l10n.retry, actionKey: const Key('store-retry'), onAction: () => unawaited(controller.load()), ); } if (controller.products.isEmpty) { return EmptyState( key: const Key('store-empty'), icon: isVip ? Icons.workspace_premium_outlined : Icons.savings_outlined, title: isVip ? l10n.noVipPlans : l10n.noCoinPacks, ); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), itemCount: controller.products.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, index) => _ProductTile( product: controller.products[index], purchasing: controller.purchasingProductId == controller.products[index].id, onBuy: () => _buy(controller.products[index]), ), ); } Future _buy(AppProduct product) async { final order = await controller.buy(product); if (!mounted) return; final l10n = AppLocalizations.of(context)!; if (order == null) { final message = errorText( l10n, controller.purchaseError, l10n.orderCreateFailed, ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); return; } widget.onPurchased?.call(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${l10n.orderCreated(order.orderNo)} ${l10n.paymentPending}', ), ), ); } } class _ProductTile extends StatelessWidget { const _ProductTile({ required this.product, required this.purchasing, required this.onBuy, }); final AppProduct product; final bool purchasing; final VoidCallback onBuy; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Container( key: Key('store-product-${product.id}'), padding: const EdgeInsets.fromLTRB(14, 14, 12, 14), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.border), ), child: Row( children: [ _ProductBadge(vip: product.isVip), const SizedBox(width: 12), Expanded(child: _info(l10n)), const SizedBox(width: 10), _price(l10n), ], ), ); } Widget _info(AppLocalizations l10n) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, key: Key('store-product-name-${product.id}'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 4), Text( _detail(l10n), style: const TextStyle(color: AppColors.muted, fontSize: 11), ), ], ); } Widget _price(AppLocalizations l10n) { return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( product.displayPrice, key: Key('store-product-price-${product.id}'), style: const TextStyle( color: AppColors.gold, fontSize: 13, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 8), SizedBox( width: 88, height: 32, child: FilledButton( key: Key('store-buy-${product.id}'), onPressed: purchasing ? null : onBuy, style: FilledButton.styleFrom( backgroundColor: AppColors.blue, foregroundColor: AppColors.onPrimary, padding: EdgeInsets.zero, textStyle: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, ), ), child: purchasing ? _buyProgress() : Text(l10n.buy), ), ), ], ); } Widget _buyProgress() { return const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.onPrimary, ), ); } String _detail(AppLocalizations l10n) { if (product.isVip) return l10n.vipDuration(product.vipDays); final base = l10n.walletBalanceShort(product.coinAmount); if (product.bonusCoin <= 0) return base; return '$base · ${l10n.bonusCoin(product.bonusCoin)}'; } } class _ProductBadge extends StatelessWidget { const _ProductBadge({required this.vip}); final bool vip; @override Widget build(BuildContext context) { return Container( width: 46, height: 46, decoration: BoxDecoration( color: AppColors.goldCard, borderRadius: BorderRadius.circular(13), ), child: Icon( vip ? Icons.workspace_premium_rounded : Icons.monetization_on_rounded, color: AppColors.gold, size: AppDimens.sectionTitle, ), ); } }