import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/i18n/app_localizations.dart'; import '../../../../core/services/redemption_service.dart'; /// 核销管理页面 /// /// 扫码核销 + 手动输入券码 + 批量核销 + 核销记录 class RedemptionPage extends StatelessWidget { const RedemptionPage({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: Text(context.t('redemption_title')), bottom: TabBar( tabs: [ Tab(text: context.t('redemption_tab_scan')), Tab(text: context.t('redemption_tab_history')), ], ), ), body: const TabBarView( children: [ _ScanRedeemTab(), _RedeemHistoryTab(), ], ), ), ); } } class _ScanRedeemTab extends StatefulWidget { const _ScanRedeemTab(); @override State<_ScanRedeemTab> createState() => _ScanRedeemTabState(); } class _ScanRedeemTabState extends State<_ScanRedeemTab> { final _manualCodeController = TextEditingController(); final _redemptionService = RedemptionService(); bool _isRedeeming = false; bool _isLoadingStats = true; TodayRedemptionStats? _todayStats; @override void initState() { super.initState(); _loadTodayStats(); } @override void dispose() { _manualCodeController.dispose(); super.dispose(); } Future _loadTodayStats() async { try { final stats = await _redemptionService.getTodayStats(); if (!mounted) return; setState(() { _todayStats = stats; _isLoadingStats = false; }); } catch (e) { debugPrint('[RedemptionPage] loadTodayStats error: $e'); if (!mounted) return; setState(() => _isLoadingStats = false); } } Future _manualRedeem() async { final code = _manualCodeController.text.trim(); if (code.isEmpty) return; setState(() => _isRedeeming = true); try { final result = await _redemptionService.manual(code); if (!mounted) return; setState(() => _isRedeeming = false); _manualCodeController.clear(); _loadTodayStats(); _showRedeemResult(context, result); } catch (e) { debugPrint('[RedemptionPage] manualRedeem error: $e'); if (!mounted) return; setState(() => _isRedeeming = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('核销失败: $e'), backgroundColor: AppColors.error), ); } } void _showRedeemResult(BuildContext context, RedemptionResult result) { showModalBottomSheet( context: context, builder: (ctx) => Container( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( result.status == 'completed' ? Icons.check_circle_rounded : Icons.error_rounded, color: result.status == 'completed' ? AppColors.success : AppColors.error, size: 56, ), const SizedBox(height: 16), Text(context.t('redemption_confirm_title'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), const SizedBox(height: 8), Text(result.couponName, style: const TextStyle(color: AppColors.textSecondary)), if (result.amount > 0) ...[ const SizedBox(height: 4), Text('\$${result.amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ], const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(ctx), child: Text(context.t('redemption_confirm_button')), ), ), const SizedBox(height: 12), ], ), ), ); } Future _showBatchRedeem(BuildContext context) async { final batchController = TextEditingController(); showModalBottomSheet( context: context, isScrollControlled: true, builder: (ctx) => DraggableScrollableSheet( expand: false, initialChildSize: 0.6, builder: (_, controller) => StatefulBuilder( builder: (ctx, setLocalState) { bool isBatching = false; return Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('redemption_batch'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), const SizedBox(height: 8), Text(context.t('redemption_batch_desc'), style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), Expanded( child: TextField( controller: batchController, maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( hintText: context.t('redemption_batch_hint'), border: const OutlineInputBorder(), ), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: isBatching ? null : () async { final text = batchController.text.trim(); if (text.isEmpty) return; final codes = text .split(RegExp(r'[\n,;]+')) .map((c) => c.trim()) .where((c) => c.isNotEmpty) .toList(); if (codes.isEmpty) return; setLocalState(() => isBatching = true); try { final result = await _redemptionService.batch(codes); if (!mounted) return; Navigator.pop(ctx); _loadTodayStats(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('成功: ${result.successCount}, 失败: ${result.failCount}'), backgroundColor: result.failCount == 0 ? AppColors.success : AppColors.warning, ), ); } catch (e) { debugPrint('[RedemptionPage] batch error: $e'); setLocalState(() => isBatching = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('批量核销失败: $e'), backgroundColor: AppColors.error), ); } } }, child: isBatching ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text(context.t('redemption_batch')), ), ), ], ), ); }, ), ), ); } @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( children: [ // Scan Area Container( height: 260, decoration: BoxDecoration( color: AppColors.gray900, borderRadius: BorderRadius.circular(16), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 160, height: 160, decoration: BoxDecoration( border: Border.all(color: AppColors.primary, width: 2), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.qr_code_scanner_rounded, color: AppColors.primary, size: 64), ), const SizedBox(height: 16), Text(context.t('redemption_scan_hint'), style: const TextStyle(color: Colors.white70, fontSize: 14)), ], ), ), ), const SizedBox(height: 20), // Manual Input Row( children: [ Expanded( child: TextField( controller: _manualCodeController, enabled: !_isRedeeming, decoration: InputDecoration( hintText: context.t('redemption_manual_hint'), prefixIcon: const Icon(Icons.keyboard_rounded), ), ), ), const SizedBox(width: 12), SizedBox( height: 52, child: ElevatedButton( onPressed: _isRedeeming ? null : _manualRedeem, child: _isRedeeming ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text(context.t('redemption_redeem')), ), ), ], ), const SizedBox(height: 20), // Batch Redeem OutlinedButton.icon( onPressed: () => _showBatchRedeem(context), icon: const Icon(Icons.list_alt_rounded), label: Text(context.t('redemption_batch')), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ), const SizedBox(height: 24), // Today Stats Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(context.t('redemption_today_title'), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(height: 12), _isLoadingStats ? const Center(child: CircularProgressIndicator()) : Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _StatItem( label: context.t('redemption_today_count'), value: '${_todayStats?.count ?? 0}', ), _StatItem( label: context.t('redemption_today_amount'), value: '\$${_todayStats?.totalAmount.toStringAsFixed(0) ?? '0'}', ), _StatItem( label: context.t('redemption_today_stores'), value: '--', ), ], ), ], ), ), ], ), ); } } class _RedeemHistoryTab extends StatefulWidget { const _RedeemHistoryTab(); @override State<_RedeemHistoryTab> createState() => _RedeemHistoryTabState(); } class _RedeemHistoryTabState extends State<_RedeemHistoryTab> { final _redemptionService = RedemptionService(); bool _isLoading = true; String? _error; List _records = []; @override void initState() { super.initState(); _loadHistory(); } Future _loadHistory() async { setState(() { _isLoading = true; _error = null; }); try { final result = await _redemptionService.getHistory(); if (!mounted) return; setState(() { _records = result.items; _isLoading = false; }); } catch (e) { debugPrint('[RedemptionPage] loadHistory error: $e'); if (!mounted) return; setState(() { _isLoading = false; _error = e.toString(); }); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.textTertiary), const SizedBox(height: 16), Text(_error!, style: const TextStyle(color: AppColors.textSecondary)), const SizedBox(height: 16), ElevatedButton( onPressed: _loadHistory, child: Text(context.t('retry')), ), ], ), ); } if (_records.isEmpty) { return const Center( child: Text('暂无核销记录', style: TextStyle(color: AppColors.textSecondary)), ); } return RefreshIndicator( onRefresh: _loadHistory, child: ListView.separated( padding: const EdgeInsets.all(20), itemCount: _records.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final record = _records[index]; final success = record.status == 'completed'; return ListTile( contentPadding: const EdgeInsets.symmetric(vertical: 8), leading: Container( width: 40, height: 40, decoration: BoxDecoration( color: success ? AppColors.successLight : AppColors.errorLight, borderRadius: BorderRadius.circular(10), ), child: Icon( success ? Icons.check_rounded : Icons.close_rounded, color: success ? AppColors.success : AppColors.error, size: 20, ), ), title: Text(record.couponName, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text( '${record.method} · \$${record.amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), ), trailing: Text( _formatTime(record.createdAt), style: const TextStyle(fontSize: 11, color: AppColors.textTertiary), ), ); }, ), ); } String _formatTime(DateTime dt) { final now = DateTime.now(); final diff = now.difference(dt); if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前'; if (diff.inHours < 24) return '${diff.inHours}小时前'; if (diff.inDays < 7) return '${diff.inDays}天前'; return '${dt.month}/${dt.day}'; } } class _StatItem extends StatelessWidget { final String label; final String value; const _StatItem({required this.label, required this.value}); @override Widget build(BuildContext context) { return Column( children: [ Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primary)), const SizedBox(height: 4), Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textTertiary)), ], ); } }