287 lines
10 KiB
Dart
287 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../../../../app/i18n/app_localizations.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';
|
||
|
||
/// A2. 首页 - 搜索栏 + Banner + 热门分类 + 精选券 + AI Agent
|
||
///
|
||
/// Tab导航:首页/市场/我的券/消息/我的
|
||
class HomePage extends StatelessWidget {
|
||
const HomePage({super.key});
|
||
|
||
@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,
|
||
),
|
||
],
|
||
),
|
||
|
||
// Banner Carousel
|
||
SliverToBoxAdapter(child: _buildBanner(context)),
|
||
|
||
// Category Grid
|
||
SliverToBoxAdapter(child: _buildCategoryGrid(context)),
|
||
|
||
// AI Smart Suggestions
|
||
SliverToBoxAdapter(child: _buildAiSuggestions(context)),
|
||
|
||
// Section: Featured Coupons
|
||
SliverToBoxAdapter(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 12),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(context.t('home.featuredCoupons'), style: AppTypography.h2),
|
||
GestureDetector(
|
||
onTap: () {},
|
||
child: Text(context.t('home.viewAllCoupons'), 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(
|
||
context.t('home.searchHint'),
|
||
style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildBanner(BuildContext context) {
|
||
return Container(
|
||
height: 160,
|
||
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
||
child: PageView.builder(
|
||
itemCount: 3,
|
||
itemBuilder: (context, index) {
|
||
final colors = [
|
||
AppColors.primaryGradient,
|
||
AppColors.successGradient,
|
||
AppColors.cardGradient,
|
||
];
|
||
final titleKeys = ['home.bannerNewUser', 'home.bannerDiscount', 'home.bannerHot'];
|
||
final subtitleKeys = ['home.bannerNewUserDesc', 'home.bannerDiscountDesc', 'home.bannerHotDesc'];
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||
decoration: BoxDecoration(
|
||
gradient: colors[index],
|
||
borderRadius: AppSpacing.borderRadiusLg,
|
||
),
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
context.t(titleKeys[index]),
|
||
style: AppTypography.h1.copyWith(color: Colors.white),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
context.t(subtitleKeys[index]),
|
||
style: AppTypography.bodyMedium.copyWith(
|
||
color: Colors.white.withValues(alpha: 0.8),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCategoryGrid(BuildContext context) {
|
||
final categoryKeys = [
|
||
('home.dining', Icons.restaurant_rounded, AppColors.couponDining),
|
||
('home.shopping', Icons.shopping_bag_rounded, AppColors.couponShopping),
|
||
('home.entertainment', Icons.sports_esports_rounded, AppColors.couponEntertainment),
|
||
('home.travel', Icons.directions_car_rounded, AppColors.couponTravel),
|
||
('home.lifestyle', Icons.home_rounded, AppColors.couponOther),
|
||
('home.brand', Icons.storefront_rounded, AppColors.primary),
|
||
('home.discount', Icons.local_offer_rounded, AppColors.error),
|
||
('home.allCategories', 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: 4,
|
||
mainAxisSpacing: 8,
|
||
crossAxisSpacing: 8,
|
||
childAspectRatio: 0.85,
|
||
),
|
||
itemCount: categoryKeys.length,
|
||
itemBuilder: (context, index) {
|
||
final (key, icon, color) = categoryKeys[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(context.t(key), style: AppTypography.caption.copyWith(
|
||
color: AppColors.textPrimary,
|
||
fontWeight: FontWeight.w500,
|
||
)),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAiSuggestions(BuildContext context) {
|
||
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(context.t('home.aiRecommend'), style: AppTypography.labelSmall.copyWith(
|
||
color: AppColors.primary,
|
||
)),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
context.t('home.aiRecommendDesc'),
|
||
style: AppTypography.bodySmall,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Icon(Icons.chevron_right_rounded, color: AppColors.primary, size: 20),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// Mock data
|
||
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'];
|