refactor: 轻量化首页钱包卡片,新增完整钱包页面
- 首页钱包区域从重量级(stats+filter+coupon cards+actions) 精简为轻量卡片(汇总信息+4个快捷入口),点击进入完整钱包页 - 新增 wallet_coupons_page.dart:融合"我的券"全部功能 (汇总面板+4-Tab筛选+券列表+转赠/出售快捷操作+接收券) - 分类网格从6项(3列)扩展为8项(4列x2行): 限时抢购/新券首发/折扣排行/即将到期/比价/转让市场/热门交易/全部 - HomePage 从 StatefulWidget 简化为 StatelessWidget - main.dart 新增 /wallet/coupons 路由 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b63414542b
commit
b1a0f29f06
|
|
@ -6,19 +6,12 @@ import '../../../../shared/widgets/coupon_card.dart';
|
||||||
import '../../../ai_agent/presentation/widgets/ai_fab.dart';
|
import '../../../ai_agent/presentation/widgets/ai_fab.dart';
|
||||||
import '../widgets/receive_coupon_sheet.dart';
|
import '../widgets/receive_coupon_sheet.dart';
|
||||||
|
|
||||||
/// 首页 - 券钱包 + 分类网格 + AI推荐 + 精选券
|
/// 首页 - 轻量钱包卡 + 分类网格 + AI推荐 + 精选券
|
||||||
///
|
///
|
||||||
/// Tab导航:首页/交易/消息/我的
|
/// Tab导航:首页/交易/消息/我的
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatelessWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<HomePage> createState() => _HomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
|
||||||
int _walletFilter = 0; // 0=全部, 1=可使用, 2=待核销, 3=已过期
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -43,10 +36,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Coupon Wallet (replaces Banner)
|
// Lightweight Wallet Card (replaces heavy wallet)
|
||||||
SliverToBoxAdapter(child: _buildCouponWallet(context)),
|
SliverToBoxAdapter(child: _buildWalletCard(context)),
|
||||||
|
|
||||||
// Category Grid (6 new categories)
|
// Category Grid (8 items, 4x2)
|
||||||
SliverToBoxAdapter(child: _buildCategoryGrid()),
|
SliverToBoxAdapter(child: _buildCategoryGrid()),
|
||||||
|
|
||||||
// AI Smart Suggestions
|
// AI Smart Suggestions
|
||||||
|
|
@ -143,342 +136,130 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Coupon Wallet Section (replaces Banner)
|
// Lightweight Wallet Card
|
||||||
|
// 点击非快捷入口区域打开完整钱包页面
|
||||||
// ============================================================
|
// ============================================================
|
||||||
Widget _buildCouponWallet(BuildContext context) {
|
Widget _buildWalletCard(BuildContext context) {
|
||||||
return Container(
|
return GestureDetector(
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/wallet/coupons'),
|
||||||
|
child: Container(
|
||||||
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: AppColors.cardGradient,
|
gradient: AppColors.cardGradient,
|
||||||
borderRadius: AppSpacing.borderRadiusLg,
|
borderRadius: AppSpacing.borderRadiusLg,
|
||||||
boxShadow: AppSpacing.shadowPrimary,
|
boxShadow: AppSpacing.shadowPrimary,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Header: 我的钱包 + 接收 button
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Top row: wallet info + receive button
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.account_balance_wallet_rounded,
|
const Icon(Icons.account_balance_wallet_rounded,
|
||||||
size: 20, color: Colors.white),
|
size: 20, color: Colors.white),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('我的钱包',
|
Text('我的钱包',
|
||||||
style: AppTypography.h3.copyWith(color: Colors.white)),
|
style: AppTypography.labelMedium.copyWith(color: Colors.white)),
|
||||||
],
|
const Spacer(),
|
||||||
),
|
// Summary
|
||||||
GestureDetector(
|
Text('持有 ',
|
||||||
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(
|
style: AppTypography.bodySmall.copyWith(
|
||||||
color: Colors.white.withValues(alpha: 0.6),
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
),
|
)),
|
||||||
),
|
Text('4',
|
||||||
),
|
style: AppTypography.h3.copyWith(
|
||||||
)
|
color: Colors.white,
|
||||||
: ListView.separated(
|
fontWeight: FontWeight.w700,
|
||||||
scrollDirection: Axis.horizontal,
|
)),
|
||||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 14),
|
Text(' 张券 总值 ',
|
||||||
itemCount: _filteredWalletCoupons.length,
|
style: AppTypography.bodySmall.copyWith(
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 10),
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
itemBuilder: (context, index) {
|
)),
|
||||||
final coupon = _filteredWalletCoupons[index];
|
Text('\$235',
|
||||||
return _buildWalletCouponCard(context, coupon);
|
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)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Quick actions bar
|
const SizedBox(height: 14),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
// Quick action entries
|
||||||
decoration: BoxDecoration(
|
Row(
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(16),
|
|
||||||
bottomRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildWalletAction(Icons.qr_code_rounded, '接收', () {
|
_buildQuickAction(context, 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(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (_) => const ReceiveCouponSheet(),
|
builder: (_) => const ReceiveCouponSheet(),
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
_buildQuickAction(context, Icons.card_giftcard_rounded, '转赠', () {
|
||||||
|
Navigator.pushNamed(context, '/transfer');
|
||||||
|
}),
|
||||||
|
_buildQuickAction(context, Icons.sell_rounded, '出售', () {
|
||||||
|
Navigator.pushNamed(context, '/sell');
|
||||||
|
}),
|
||||||
|
_buildQuickAction(context, Icons.check_circle_outline_rounded, '核销', () {
|
||||||
|
Navigator.pushNamed(context, '/redeem');
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_WalletCoupon> get _filteredWalletCoupons {
|
Widget _buildQuickAction(
|
||||||
if (_walletFilter == 0) return _mockWalletCoupons;
|
BuildContext context, IconData icon, String label, VoidCallback onTap) {
|
||||||
if (_walletFilter == 1) {
|
return GestureDetector(
|
||||||
return _mockWalletCoupons
|
onTap: onTap,
|
||||||
.where((c) => c.status == CouponStatus.active)
|
behavior: HitTestBehavior.opaque,
|
||||||
.toList();
|
child: Column(
|
||||||
}
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (_walletFilter == 2) {
|
children: [
|
||||||
return _mockWalletCoupons
|
Container(
|
||||||
.where((c) => c.status == CouponStatus.pending)
|
width: 40,
|
||||||
.toList();
|
height: 40,
|
||||||
}
|
decoration: BoxDecoration(
|
||||||
return _mockWalletCoupons
|
color: Colors.white.withValues(alpha: 0.15),
|
||||||
.where((c) => c.status == CouponStatus.expired)
|
borderRadius: AppSpacing.borderRadiusMd,
|
||||||
.toList();
|
),
|
||||||
|
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 (6 new savings-focused categories)
|
// Category Grid (8 items, 4x2, A+C savings-focused)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
Widget _buildCategoryGrid() {
|
Widget _buildCategoryGrid() {
|
||||||
final categories = [
|
final categories = [
|
||||||
('限时抢购', Icons.flash_on_rounded, AppColors.error),
|
('限时抢购', Icons.flash_on_rounded, AppColors.error),
|
||||||
('新券首发', Icons.fiber_new_rounded, AppColors.primary),
|
('新券首发', Icons.fiber_new_rounded, AppColors.primary),
|
||||||
('折扣排行', Icons.trending_up_rounded, AppColors.couponEntertainment),
|
('折扣排行', Icons.trending_up_rounded, AppColors.couponEntertainment),
|
||||||
('即将到期', Icons.timer_rounded, AppColors.info),
|
('即将到期', Icons.timer_rounded, AppColors.warning),
|
||||||
('比价', Icons.compare_arrows_rounded, AppColors.couponShopping),
|
('比价', Icons.compare_arrows_rounded, AppColors.couponShopping),
|
||||||
('全部分类', Icons.grid_view_rounded, AppColors.textSecondary),
|
('转让市场', Icons.swap_horiz_rounded, AppColors.info),
|
||||||
|
('热门交易', Icons.local_fire_department_rounded, AppColors.couponFood),
|
||||||
|
('全部', Icons.grid_view_rounded, AppColors.textSecondary),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -487,10 +268,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 4,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 4,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 4,
|
||||||
childAspectRatio: 1.1,
|
childAspectRatio: 0.9,
|
||||||
),
|
),
|
||||||
itemCount: categories.length,
|
itemCount: categories.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|
@ -501,13 +282,13 @@ class _HomePageState extends State<HomePage> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 48,
|
width: 44,
|
||||||
height: 48,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withValues(alpha: 0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: AppSpacing.borderRadiusMd,
|
borderRadius: AppSpacing.borderRadiusMd,
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: color, size: 24),
|
child: Icon(icon, color: color, size: 22),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(name, style: AppTypography.caption.copyWith(
|
Text(name, style: AppTypography.caption.copyWith(
|
||||||
|
|
@ -567,83 +348,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 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
|
// 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 _mockBrands = ['Starbucks', 'Amazon', 'Walmart', 'Target', 'Nike'];
|
||||||
const _mockNames = ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'];
|
const _mockNames = ['星巴克 \$25 礼品卡', 'Amazon \$100 购物券', 'Walmart \$50 生活券', 'Target \$30 折扣券', 'Nike \$80 运动券'];
|
||||||
const _mockFaceValues = [25.0, 100.0, 50.0, 30.0, 80.0];
|
const _mockFaceValues = [25.0, 100.0, 50.0, 30.0, 80.0];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
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/status_tag.dart';
|
||||||
|
import '../../../../shared/widgets/empty_state.dart';
|
||||||
|
import '../widgets/receive_coupon_sheet.dart';
|
||||||
|
|
||||||
|
/// 完整钱包页面 - 融合"我的券"所有功能
|
||||||
|
///
|
||||||
|
/// 顶部汇总卡片 + 4-Tab筛选(全部/可使用/待核销/已过期)
|
||||||
|
/// 券列表(品牌+面值+状态+到期+快捷操作)
|
||||||
|
/// AppBar含接收按钮
|
||||||
|
class WalletCouponsPage extends StatefulWidget {
|
||||||
|
const WalletCouponsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WalletCouponsPage> createState() => _WalletCouponsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletCouponsPageState extends State<WalletCouponsPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 4, vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('我的钱包'),
|
||||||
|
actions: [
|
||||||
|
// 接收券
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_rounded, size: 22),
|
||||||
|
onPressed: () => _showReceiveSheet(context),
|
||||||
|
tooltip: '接收券',
|
||||||
|
),
|
||||||
|
// 排序
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.sort_rounded, size: 22),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: '全部'),
|
||||||
|
Tab(text: '可使用'),
|
||||||
|
Tab(text: '待核销'),
|
||||||
|
Tab(text: '已过期'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// 汇总卡片
|
||||||
|
_buildSummaryCard(),
|
||||||
|
|
||||||
|
// 券列表
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
_buildCouponList(null),
|
||||||
|
_buildCouponList(CouponStatus.active),
|
||||||
|
_buildCouponList(CouponStatus.pending),
|
||||||
|
_buildCouponList(CouponStatus.expired),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 顶部汇总卡片:持有数量 + 总面值 + 快捷操作
|
||||||
|
Widget _buildSummaryCard() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: AppColors.cardGradient,
|
||||||
|
borderRadius: AppSpacing.borderRadiusMd,
|
||||||
|
boxShadow: AppSpacing.shadowPrimary,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 持有券数
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('4',
|
||||||
|
style: AppTypography.display.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('持有券数',
|
||||||
|
style: AppTypography.caption.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 36,
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
// 总面值
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('\$235',
|
||||||
|
style: AppTypography.display.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('总面值',
|
||||||
|
style: AppTypography.caption.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 36,
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
// 节省金额
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('\$38',
|
||||||
|
style: AppTypography.display.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('已节省',
|
||||||
|
style: AppTypography.caption.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCouponList(CouponStatus? filter) {
|
||||||
|
final coupons = _filterCoupons(filter);
|
||||||
|
|
||||||
|
if (coupons.isEmpty) {
|
||||||
|
return EmptyState.noCoupons(
|
||||||
|
onBrowse: () => Navigator.pop(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
|
||||||
|
itemCount: coupons.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final coupon = coupons[index];
|
||||||
|
return _buildCouponCard(context, coupon);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCouponCard(BuildContext context, _WalletCouponItem coupon) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail'),
|
||||||
|
child: Container(
|
||||||
|
padding: AppSpacing.cardPadding,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surface,
|
||||||
|
borderRadius: AppSpacing.borderRadiusMd,
|
||||||
|
boxShadow: AppSpacing.shadowSm,
|
||||||
|
border: Border.all(color: AppColors.borderLight),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// 券图片占位
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.primarySurface,
|
||||||
|
borderRadius: AppSpacing.borderRadiusSm,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.confirmation_number_outlined,
|
||||||
|
color: AppColors.primary.withValues(alpha: 0.4),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(coupon.brandName, style: AppTypography.caption),
|
||||||
|
Text(coupon.name, style: AppTypography.labelMedium),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('面值 \$${coupon.faceValue.toStringAsFixed(0)}',
|
||||||
|
style: AppTypography.bodySmall),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_statusWidget(coupon.status),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.chevron_right_rounded,
|
||||||
|
color: AppColors.textTertiary, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// 过期时间 + 快捷操作
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.gray50,
|
||||||
|
borderRadius: AppSpacing.borderRadiusSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time_rounded,
|
||||||
|
size: 14, color: _expiryColor(coupon.expiryDate)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_expiryText(coupon.expiryDate),
|
||||||
|
style: AppTypography.caption.copyWith(
|
||||||
|
color: _expiryColor(coupon.expiryDate)),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (coupon.status == CouponStatus.active) ...[
|
||||||
|
_quickAction('转赠', Icons.card_giftcard_rounded, () {
|
||||||
|
Navigator.pushNamed(context, '/transfer');
|
||||||
|
}),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_quickAction('出售', Icons.sell_rounded, () {
|
||||||
|
Navigator.pushNamed(context, '/sell');
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statusWidget(CouponStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case CouponStatus.active:
|
||||||
|
return StatusTags.active();
|
||||||
|
case CouponStatus.pending:
|
||||||
|
return StatusTags.pending();
|
||||||
|
case CouponStatus.expired:
|
||||||
|
return StatusTags.expired();
|
||||||
|
case CouponStatus.used:
|
||||||
|
return StatusTags.used();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _quickAction(String label, IconData icon, VoidCallback onTap) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: AppColors.primary),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(label,
|
||||||
|
style: AppTypography.caption.copyWith(
|
||||||
|
color: AppColors.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _expiryText(DateTime expiryDate) {
|
||||||
|
final days = expiryDate.difference(DateTime.now()).inDays;
|
||||||
|
if (days < 0) return '已过期';
|
||||||
|
if (days == 0) return '今天到期';
|
||||||
|
if (days <= 7) return '$days天后到期';
|
||||||
|
return '${expiryDate.year}/${expiryDate.month}/${expiryDate.day}到期';
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _expiryColor(DateTime expiryDate) {
|
||||||
|
final days = expiryDate.difference(DateTime.now()).inDays;
|
||||||
|
if (days <= 3) return AppColors.error;
|
||||||
|
if (days <= 7) return AppColors.warning;
|
||||||
|
return AppColors.textTertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showReceiveSheet(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => const ReceiveCouponSheet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_WalletCouponItem> _filterCoupons(CouponStatus? filter) {
|
||||||
|
if (filter == null) return _mockCoupons;
|
||||||
|
return _mockCoupons.where((c) => c.status == filter).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Data Model
|
||||||
|
// ============================================================
|
||||||
|
class _WalletCouponItem {
|
||||||
|
final String brandName;
|
||||||
|
final String name;
|
||||||
|
final double faceValue;
|
||||||
|
final CouponStatus status;
|
||||||
|
final DateTime expiryDate;
|
||||||
|
|
||||||
|
const _WalletCouponItem({
|
||||||
|
required this.brandName,
|
||||||
|
required this.name,
|
||||||
|
required this.faceValue,
|
||||||
|
required this.status,
|
||||||
|
required this.expiryDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
final _mockCoupons = [
|
||||||
|
_WalletCouponItem(
|
||||||
|
brandName: 'Starbucks',
|
||||||
|
name: '星巴克 \$25 礼品卡',
|
||||||
|
faceValue: 25.0,
|
||||||
|
status: CouponStatus.active,
|
||||||
|
expiryDate: DateTime.now().add(const Duration(days: 30)),
|
||||||
|
),
|
||||||
|
_WalletCouponItem(
|
||||||
|
brandName: 'Amazon',
|
||||||
|
name: 'Amazon \$100 购物券',
|
||||||
|
faceValue: 100.0,
|
||||||
|
status: CouponStatus.active,
|
||||||
|
expiryDate: DateTime.now().add(const Duration(days: 45)),
|
||||||
|
),
|
||||||
|
_WalletCouponItem(
|
||||||
|
brandName: 'Nike',
|
||||||
|
name: 'Nike \$80 运动券',
|
||||||
|
faceValue: 80.0,
|
||||||
|
status: CouponStatus.pending,
|
||||||
|
expiryDate: DateTime.now().add(const Duration(days: 15)),
|
||||||
|
),
|
||||||
|
_WalletCouponItem(
|
||||||
|
brandName: 'Target',
|
||||||
|
name: 'Target \$30 折扣券',
|
||||||
|
faceValue: 30.0,
|
||||||
|
status: CouponStatus.active,
|
||||||
|
expiryDate: DateTime.now().add(const Duration(days: 60)),
|
||||||
|
),
|
||||||
|
_WalletCouponItem(
|
||||||
|
brandName: 'Walmart',
|
||||||
|
name: 'Walmart \$50 生活券',
|
||||||
|
faceValue: 50.0,
|
||||||
|
status: CouponStatus.expired,
|
||||||
|
expiryDate: DateTime.now().subtract(const Duration(days: 5)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
@ -28,6 +28,7 @@ import 'features/message/presentation/pages/message_detail_page.dart';
|
||||||
import 'features/issuer/presentation/pages/issuer_main_page.dart';
|
import 'features/issuer/presentation/pages/issuer_main_page.dart';
|
||||||
import 'features/merchant/presentation/pages/merchant_home_page.dart';
|
import 'features/merchant/presentation/pages/merchant_home_page.dart';
|
||||||
import 'features/trading/presentation/pages/trading_detail_page.dart';
|
import 'features/trading/presentation/pages/trading_detail_page.dart';
|
||||||
|
import 'features/coupons/presentation/pages/wallet_coupons_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const GenexConsumerApp());
|
runApp(const GenexConsumerApp());
|
||||||
|
|
@ -109,6 +110,8 @@ class GenexConsumerApp extends StatelessWidget {
|
||||||
return MaterialPageRoute(builder: (_) => const MerchantHomePage());
|
return MaterialPageRoute(builder: (_) => const MerchantHomePage());
|
||||||
case '/trading/detail':
|
case '/trading/detail':
|
||||||
return MaterialPageRoute(builder: (_) => const TradingDetailPage());
|
return MaterialPageRoute(builder: (_) => const TradingDetailPage());
|
||||||
|
case '/wallet/coupons':
|
||||||
|
return MaterialPageRoute(builder: (_) => const WalletCouponsPage());
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) => Scaffold(
|
builder: (_) => Scaffold(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue