652 lines
22 KiB
Dart
652 lines
22 KiB
Dart
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'];
|