gcx/frontend/genex-mobile/lib/features/coupons/presentation/pages/home_page.dart

652 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/coupon_card.dart';
import '../../../ai_agent/presentation/widgets/ai_fab.dart';
import '../widgets/receive_coupon_sheet.dart';
/// 首页 - 券钱包 + 分类网格 + AI推荐 + 精选券
///
/// Tab导航首页/交易/消息/我的
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _walletFilter = 0; // 0=全部, 1=可使用, 2=待核销, 3=已过期
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomScrollView(
slivers: [
// Floating App Bar
SliverAppBar(
floating: true,
pinned: false,
backgroundColor: AppColors.background,
elevation: 0,
toolbarHeight: 60,
title: _buildSearchBar(context),
actions: [
IconButton(
icon: const Icon(Icons.qr_code_scanner_rounded, size: 24),
onPressed: () {},
color: AppColors.textPrimary,
),
],
),
// Coupon Wallet (replaces Banner)
SliverToBoxAdapter(child: _buildCouponWallet(context)),
// Category Grid (6 new categories)
SliverToBoxAdapter(child: _buildCategoryGrid()),
// AI Smart Suggestions
SliverToBoxAdapter(child: _buildAiSuggestions()),
// Section: Featured Coupons
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('精选好券', style: AppTypography.h2),
GestureDetector(
onTap: () {},
child: Text('查看全部', style: AppTypography.labelSmall.copyWith(
color: AppColors.primary,
)),
),
],
),
),
),
// Coupon List
SliverPadding(
padding: AppSpacing.pagePadding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: CouponCard(
brandName: _mockBrands[index % _mockBrands.length],
couponName: _mockNames[index % _mockNames.length],
faceValue: _mockFaceValues[index % _mockFaceValues.length],
currentPrice: _mockPrices[index % _mockPrices.length],
creditRating: _mockRatings[index % _mockRatings.length],
expiryDate: DateTime.now().add(Duration(days: (index + 1) * 5)),
onTap: () {
Navigator.pushNamed(context, '/coupon/detail');
},
),
),
childCount: 10,
),
),
),
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
],
),
// AI FAB
Positioned(
right: 20,
bottom: 100,
child: AiFab(
unreadCount: 3,
onTap: () {
Navigator.pushNamed(context, '/ai-chat');
},
),
),
],
),
);
}
Widget _buildSearchBar(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/search');
},
child: Container(
height: 40,
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusFull,
border: Border.all(color: AppColors.borderLight),
),
child: Row(
children: [
const SizedBox(width: 14),
const Icon(Icons.search_rounded, size: 20, color: AppColors.textTertiary),
const SizedBox(width: 8),
Text(
'搜索券、品牌、分类...',
style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
),
],
),
),
);
}
// ============================================================
// Coupon Wallet Section (replaces Banner)
// ============================================================
Widget _buildCouponWallet(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
decoration: BoxDecoration(
gradient: AppColors.cardGradient,
borderRadius: AppSpacing.borderRadiusLg,
boxShadow: AppSpacing.shadowPrimary,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: 我的钱包 + 接收 button
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.account_balance_wallet_rounded,
size: 20, color: Colors.white),
const SizedBox(width: 8),
Text('我的钱包',
style: AppTypography.h3.copyWith(color: Colors.white)),
],
),
GestureDetector(
onTap: () => _showReceiveSheet(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: AppSpacing.borderRadiusFull,
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.qr_code_rounded,
size: 14, color: Colors.white),
const SizedBox(width: 4),
Text('接收',
style: AppTypography.labelSmall.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
)),
],
),
),
),
],
),
),
// Stats row
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
child: Row(
children: [
_buildWalletStat('可使用', '3', true),
const SizedBox(width: 20),
_buildWalletStat('待核销', '1', false),
const SizedBox(width: 20),
_buildWalletStat('已过期', '0', false),
],
),
),
// Filter tabs
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
_buildWalletTab('全部', 0),
const SizedBox(width: 6),
_buildWalletTab('可使用', 1),
const SizedBox(width: 6),
_buildWalletTab('待核销', 2),
const SizedBox(width: 6),
_buildWalletTab('已过期', 3),
],
),
),
// Coupon mini-cards (horizontal scroll)
SizedBox(
height: 88,
child: _filteredWalletCoupons.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'暂无券,去交易市场看看吧',
style: AppTypography.bodySmall.copyWith(
color: Colors.white.withValues(alpha: 0.6),
),
),
),
)
: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(16, 10, 16, 14),
itemCount: _filteredWalletCoupons.length,
separatorBuilder: (_, __) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final coupon = _filteredWalletCoupons[index];
return _buildWalletCouponCard(context, coupon);
},
),
),
// Quick actions bar
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.1),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildWalletAction(Icons.qr_code_rounded, '接收', () {
_showReceiveSheet(context);
}),
_buildWalletAction(Icons.card_giftcard_rounded, '转赠', () {
Navigator.pushNamed(context, '/transfer');
}),
_buildWalletAction(Icons.sell_rounded, '出售', () {
Navigator.pushNamed(context, '/sell');
}),
_buildWalletAction(Icons.check_circle_outline_rounded, '核销', () {
Navigator.pushNamed(context, '/redeem');
}),
],
),
),
],
),
);
}
Widget _buildWalletStat(String label, String count, bool highlight) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
count,
style: AppTypography.h2.copyWith(
color: highlight ? Colors.white : Colors.white.withValues(alpha: 0.7),
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
label,
style: AppTypography.caption.copyWith(
color: Colors.white.withValues(alpha: 0.6),
),
),
],
);
}
Widget _buildWalletTab(String label, int index) {
final isSelected = _walletFilter == index;
return GestureDetector(
onTap: () => setState(() => _walletFilter = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withValues(alpha: 0.25)
: Colors.transparent,
borderRadius: AppSpacing.borderRadiusFull,
border: Border.all(
color: isSelected
? Colors.white.withValues(alpha: 0.4)
: Colors.transparent,
width: 0.5,
),
),
child: Text(
label,
style: AppTypography.caption.copyWith(
color: isSelected
? Colors.white
: Colors.white.withValues(alpha: 0.5),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
),
);
}
Widget _buildWalletCouponCard(BuildContext context, _WalletCoupon coupon) {
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail'),
child: Container(
width: 140,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.confirmation_number_outlined,
size: 14, color: Colors.white.withValues(alpha: 0.7)),
const SizedBox(width: 4),
Expanded(
child: Text(
coupon.brandName,
style: AppTypography.caption.copyWith(
color: Colors.white.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
Text(
coupon.name,
style: AppTypography.labelSmall.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'\$${coupon.faceValue.toStringAsFixed(0)}',
style: AppTypography.priceSmall.copyWith(
color: Colors.white,
fontSize: 13,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: coupon.statusColor.withValues(alpha: 0.3),
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(
coupon.statusLabel,
style: AppTypography.caption.copyWith(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
),
);
}
Widget _buildWalletAction(IconData icon, String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.9)),
const SizedBox(height: 4),
Text(
label,
style: AppTypography.caption.copyWith(
color: Colors.white.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
],
),
);
}
void _showReceiveSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const ReceiveCouponSheet(),
);
}
List<_WalletCoupon> get _filteredWalletCoupons {
if (_walletFilter == 0) return _mockWalletCoupons;
if (_walletFilter == 1) {
return _mockWalletCoupons
.where((c) => c.status == CouponStatus.active)
.toList();
}
if (_walletFilter == 2) {
return _mockWalletCoupons
.where((c) => c.status == CouponStatus.pending)
.toList();
}
return _mockWalletCoupons
.where((c) => c.status == CouponStatus.expired)
.toList();
}
// ============================================================
// Category Grid (6 new savings-focused categories)
// ============================================================
Widget _buildCategoryGrid() {
final categories = [
('限时抢购', Icons.flash_on_rounded, AppColors.error),
('新券首发', Icons.fiber_new_rounded, AppColors.primary),
('折扣排行', Icons.trending_up_rounded, AppColors.couponEntertainment),
('即将到期', Icons.timer_rounded, AppColors.info),
('比价', Icons.compare_arrows_rounded, AppColors.couponShopping),
('全部分类', Icons.grid_view_rounded, AppColors.textSecondary),
];
return Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.1,
),
itemCount: categories.length,
itemBuilder: (context, index) {
final (name, icon, color) = categories[index];
return GestureDetector(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: AppSpacing.borderRadiusMd,
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 6),
Text(name, style: AppTypography.caption.copyWith(
color: AppColors.textPrimary,
fontWeight: FontWeight.w500,
)),
],
),
);
},
),
);
}
Widget _buildAiSuggestions() {
return Container(
margin: const EdgeInsets.fromLTRB(20, 16, 20, 0),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: AppSpacing.borderRadiusSm,
),
child: const Icon(Icons.auto_awesome_rounded, color: Colors.white, size: 16),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('AI 推荐', style: AppTypography.labelSmall.copyWith(
color: AppColors.primary,
)),
const SizedBox(height: 2),
Text(
'根据你的偏好发现了3张高性价比券',
style: AppTypography.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 20),
],
),
);
}
}
// ============================================================
// Wallet Coupon Model
// ============================================================
class _WalletCoupon {
final String brandName;
final String name;
final double faceValue;
final CouponStatus status;
final DateTime expiryDate;
const _WalletCoupon({
required this.brandName,
required this.name,
required this.faceValue,
required this.status,
required this.expiryDate,
});
String get statusLabel {
switch (status) {
case CouponStatus.active:
return '可使用';
case CouponStatus.pending:
return '待核销';
case CouponStatus.expired:
return '已过期';
case CouponStatus.used:
return '已使用';
}
}
Color get statusColor {
switch (status) {
case CouponStatus.active:
return AppColors.success;
case CouponStatus.pending:
return AppColors.warning;
case CouponStatus.expired:
return AppColors.textTertiary;
case CouponStatus.used:
return AppColors.textDisabled;
}
}
}
// Mock data
final _mockWalletCoupons = [
_WalletCoupon(
brandName: 'Starbucks',
name: '星巴克 \$25 礼品卡',
faceValue: 25.0,
status: CouponStatus.active,
expiryDate: DateTime.now().add(const Duration(days: 30)),
),
_WalletCoupon(
brandName: 'Amazon',
name: 'Amazon \$100 购物券',
faceValue: 100.0,
status: CouponStatus.active,
expiryDate: DateTime.now().add(const Duration(days: 45)),
),
_WalletCoupon(
brandName: 'Nike',
name: 'Nike \$80 运动券',
faceValue: 80.0,
status: CouponStatus.pending,
expiryDate: DateTime.now().add(const Duration(days: 15)),
),
_WalletCoupon(
brandName: 'Target',
name: 'Target \$30 折扣券',
faceValue: 30.0,
status: CouponStatus.active,
expiryDate: DateTime.now().add(const Duration(days: 60)),
),
];
const _mockBrands = ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike'];
const _mockNames = ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'];
const _mockFaceValues = [25.0, 100.0, 50.0, 30.0, 80.0];
const _mockPrices = [21.25, 85.0, 42.5, 24.0, 68.0];
const _mockRatings = ['AAA', 'AA', 'AAA', 'A', 'AA'];