475 lines
17 KiB
Dart
475 lines
17 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';
|
||
import '../../../../app/i18n/app_localizations.dart';
|
||
import '../../../../core/services/coupon_service.dart';
|
||
import '../../data/models/coupon_model.dart';
|
||
import '../../data/models/holdings_summary_model.dart';
|
||
|
||
/// 首页 - 轻量持仓卡 + 分类网格 + AI推荐 + 精选券
|
||
///
|
||
/// Tab导航:首页/交易/消息/我的
|
||
class HomePage extends StatefulWidget {
|
||
const HomePage({super.key});
|
||
|
||
@override
|
||
State<HomePage> createState() => _HomePageState();
|
||
}
|
||
|
||
class _HomePageState extends State<HomePage> {
|
||
final CouponApiService _couponService = CouponApiService();
|
||
|
||
List<CouponModel> _featuredCoupons = [];
|
||
HoldingsSummaryModel? _holdingsSummary;
|
||
bool _isLoading = true;
|
||
String? _error;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadData();
|
||
}
|
||
|
||
Future<void> _loadData() async {
|
||
setState(() {
|
||
_isLoading = true;
|
||
_error = null;
|
||
});
|
||
|
||
try {
|
||
final results = await Future.wait([
|
||
_couponService.getFeaturedCoupons(limit: 10),
|
||
_couponService.getHoldingsSummary(),
|
||
]);
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_featuredCoupons = results[0] as List<CouponModel>;
|
||
_holdingsSummary = results[1] as HoldingsSummaryModel;
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_error = e.toString();
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: Stack(
|
||
children: [
|
||
RefreshIndicator(
|
||
onRefresh: _loadData,
|
||
child: CustomScrollView(
|
||
slivers: [
|
||
// Floating App Bar
|
||
SliverAppBar(
|
||
floating: true,
|
||
pinned: false,
|
||
automaticallyImplyLeading: 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,
|
||
),
|
||
],
|
||
),
|
||
|
||
// Lightweight Position Card (持仓)
|
||
SliverToBoxAdapter(child: _buildWalletCard(context)),
|
||
|
||
// Category Grid (8 items, 4x2)
|
||
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
|
||
if (_isLoading)
|
||
SliverPadding(
|
||
padding: AppSpacing.pagePadding,
|
||
sliver: SliverList(
|
||
delegate: SliverChildBuilderDelegate(
|
||
(context, index) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: _buildSkeletonCard(),
|
||
),
|
||
childCount: 3,
|
||
),
|
||
),
|
||
)
|
||
else if (_error != null)
|
||
SliverToBoxAdapter(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
children: [
|
||
Icon(Icons.cloud_off_rounded, size: 48, color: AppColors.textTertiary),
|
||
const SizedBox(height: 12),
|
||
Text(
|
||
context.t('home.loadFailed'),
|
||
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary),
|
||
),
|
||
const SizedBox(height: 12),
|
||
TextButton(
|
||
onPressed: _loadData,
|
||
child: Text(context.t('home.retry')),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
)
|
||
else if (_featuredCoupons.isEmpty)
|
||
SliverToBoxAdapter(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(40),
|
||
child: Center(
|
||
child: Text(
|
||
context.t('home.noCoupons'),
|
||
style: AppTypography.bodyMedium.copyWith(color: AppColors.textTertiary),
|
||
),
|
||
),
|
||
),
|
||
)
|
||
else
|
||
SliverPadding(
|
||
padding: AppSpacing.pagePadding,
|
||
sliver: SliverList(
|
||
delegate: SliverChildBuilderDelegate(
|
||
(context, index) {
|
||
final coupon = _featuredCoupons[index];
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: CouponCard(
|
||
brandName: coupon.brandName ?? '',
|
||
couponName: coupon.name,
|
||
faceValue: coupon.faceValue,
|
||
currentPrice: coupon.currentPrice,
|
||
creditRating: coupon.creditRating ?? '',
|
||
expiryDate: coupon.expiryDate,
|
||
onTap: () {
|
||
Navigator.pushNamed(context, '/coupon/detail', arguments: coupon.id);
|
||
},
|
||
),
|
||
);
|
||
},
|
||
childCount: _featuredCoupons.length,
|
||
),
|
||
),
|
||
),
|
||
|
||
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 _buildSkeletonCard() {
|
||
return Container(
|
||
height: 100,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.gray50,
|
||
borderRadius: AppSpacing.borderRadiusMd,
|
||
),
|
||
child: const Center(
|
||
child: SizedBox(
|
||
width: 24,
|
||
height: 24,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// Lightweight Position Card (持仓卡片)
|
||
// 点击非快捷入口区域打开完整持仓页面
|
||
// ============================================================
|
||
Widget _buildWalletCard(BuildContext context) {
|
||
final count = _holdingsSummary?.count ?? 0;
|
||
final totalValue = _holdingsSummary?.totalFaceValue ?? 0;
|
||
|
||
return GestureDetector(
|
||
onTap: () => Navigator.pushNamed(context, '/wallet/coupons'),
|
||
child: Container(
|
||
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
gradient: AppColors.cardGradient,
|
||
borderRadius: AppSpacing.borderRadiusLg,
|
||
boxShadow: AppSpacing.shadowPrimary,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// Top row: wallet info + receive button
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.inventory_2_rounded,
|
||
size: 20, color: Colors.white),
|
||
const SizedBox(width: 8),
|
||
Text(context.t('home.position'),
|
||
style: AppTypography.labelMedium.copyWith(color: Colors.white)),
|
||
const Spacer(),
|
||
// Summary
|
||
Text('${context.t('home.hold')} ',
|
||
style: AppTypography.bodySmall.copyWith(
|
||
color: Colors.white.withValues(alpha: 0.7),
|
||
)),
|
||
Text('$count',
|
||
style: AppTypography.h3.copyWith(
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.w700,
|
||
)),
|
||
Text(' ${context.t('home.couponUnit')} ${context.t('home.totalValue')} ',
|
||
style: AppTypography.bodySmall.copyWith(
|
||
color: Colors.white.withValues(alpha: 0.7),
|
||
)),
|
||
Text('\$${totalValue.toStringAsFixed(0)}',
|
||
style: AppTypography.h3.copyWith(
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.w700,
|
||
)),
|
||
const SizedBox(width: 4),
|
||
Icon(Icons.chevron_right_rounded,
|
||
size: 18, color: Colors.white.withValues(alpha: 0.7)),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 14),
|
||
|
||
// Quick action entries
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: [
|
||
_buildQuickAction(context, Icons.qr_code_rounded, context.t('home.receive'), () {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
builder: (_) => const ReceiveCouponSheet(),
|
||
);
|
||
}),
|
||
_buildQuickAction(context, Icons.card_giftcard_rounded, context.t('home.transfer'), () {
|
||
Navigator.pushNamed(context, '/transfer');
|
||
}),
|
||
_buildQuickAction(context, Icons.sell_rounded, context.t('home.sell'), () {
|
||
Navigator.pushNamed(context, '/sell');
|
||
}),
|
||
_buildQuickAction(context, Icons.check_circle_outline_rounded, context.t('home.redeem'), () {
|
||
Navigator.pushNamed(context, '/redeem');
|
||
}),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildQuickAction(
|
||
BuildContext context, IconData icon, String label, VoidCallback onTap) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.15),
|
||
borderRadius: AppSpacing.borderRadiusMd,
|
||
),
|
||
child: Icon(icon, size: 20, color: Colors.white),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
label,
|
||
style: AppTypography.caption.copyWith(
|
||
color: Colors.white.withValues(alpha: 0.9),
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// Category Grid (8 items, 4x2, A+C savings-focused)
|
||
// ============================================================
|
||
Widget _buildCategoryGrid(BuildContext context) {
|
||
final categories = [
|
||
(context.t('home.flashSale'), Icons.flash_on_rounded, AppColors.error),
|
||
(context.t('home.newRelease'), Icons.fiber_new_rounded, AppColors.primary),
|
||
(context.t('home.discountRank'), Icons.trending_up_rounded, AppColors.couponEntertainment),
|
||
(context.t('home.expiringSoon'), Icons.timer_rounded, AppColors.warning),
|
||
(context.t('home.priceCompare'), Icons.compare_arrows_rounded, AppColors.couponShopping),
|
||
(context.t('home.resaleMarket'), Icons.swap_horiz_rounded, AppColors.info),
|
||
(context.t('home.hotTrades'), Icons.local_fire_department_rounded, AppColors.couponDining),
|
||
(context.t('home.viewAll'), 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: 4,
|
||
crossAxisSpacing: 4,
|
||
childAspectRatio: 0.9,
|
||
),
|
||
itemCount: categories.length,
|
||
itemBuilder: (context, index) {
|
||
final (name, icon, color) = categories[index];
|
||
return GestureDetector(
|
||
onTap: () {},
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.1),
|
||
borderRadius: AppSpacing.borderRadiusMd,
|
||
),
|
||
child: Icon(icon, color: color, size: 22),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(name, 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),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|