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 '../widgets/receive_coupon_sheet.dart';
|
||||
|
||||
/// 首页 - 券钱包 + 分类网格 + AI推荐 + 精选券
|
||||
/// 首页 - 轻量钱包卡 + 分类网格 + AI推荐 + 精选券
|
||||
///
|
||||
/// Tab导航:首页/交易/消息/我的
|
||||
class HomePage extends StatefulWidget {
|
||||
class HomePage extends StatelessWidget {
|
||||
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(
|
||||
|
|
@ -43,10 +36,10 @@ class _HomePageState extends State<HomePage> {
|
|||
],
|
||||
),
|
||||
|
||||
// Coupon Wallet (replaces Banner)
|
||||
SliverToBoxAdapter(child: _buildCouponWallet(context)),
|
||||
// Lightweight Wallet Card (replaces heavy wallet)
|
||||
SliverToBoxAdapter(child: _buildWalletCard(context)),
|
||||
|
||||
// Category Grid (6 new categories)
|
||||
// Category Grid (8 items, 4x2)
|
||||
SliverToBoxAdapter(child: _buildCategoryGrid()),
|
||||
|
||||
// AI Smart Suggestions
|
||||
|
|
@ -143,277 +136,79 @@ class _HomePageState extends State<HomePage> {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Coupon Wallet Section (replaces Banner)
|
||||
// Lightweight Wallet Card
|
||||
// 点击非快捷入口区域打开完整钱包页面
|
||||
// ============================================================
|
||||
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;
|
||||
Widget _buildWalletCard(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _walletFilter = index),
|
||||
onTap: () => Navigator.pushNamed(context, '/wallet/coupons'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
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,
|
||||
),
|
||||
gradient: AppColors.cardGradient,
|
||||
borderRadius: AppSpacing.borderRadiusLg,
|
||||
boxShadow: AppSpacing.shadowPrimary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Top row: wallet info + receive button
|
||||
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(
|
||||
const Icon(Icons.account_balance_wallet_rounded,
|
||||
size: 20, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text('我的钱包',
|
||||
style: AppTypography.labelMedium.copyWith(color: Colors.white)),
|
||||
const Spacer(),
|
||||
// Summary
|
||||
Text('持有 ',
|
||||
style: AppTypography.bodySmall.copyWith(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)),
|
||||
Text('4',
|
||||
style: AppTypography.h3.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
)),
|
||||
Text(' 张券 总值 ',
|
||||
style: AppTypography.bodySmall.copyWith(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
)),
|
||||
Text('\$235',
|
||||
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)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
coupon.name,
|
||||
style: AppTypography.labelSmall.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Quick action entries
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildQuickAction(context, Icons.qr_code_rounded, '接收', () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
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');
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -422,18 +217,28 @@ class _HomePageState extends State<HomePage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletAction(IconData icon, String label, VoidCallback onTap) {
|
||||
Widget _buildQuickAction(
|
||||
BuildContext context, IconData icon, String label, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: Colors.white.withValues(alpha: 0.9)),
|
||||
const SizedBox(height: 4),
|
||||
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.8),
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
@ -442,43 +247,19 @@ class _HomePageState extends State<HomePage> {
|
|||
);
|
||||
}
|
||||
|
||||
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)
|
||||
// Category Grid (8 items, 4x2, A+C savings-focused)
|
||||
// ============================================================
|
||||
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.timer_rounded, AppColors.warning),
|
||||
('比价', 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(
|
||||
|
|
@ -487,10 +268,10 @@ class _HomePageState extends State<HomePage> {
|
|||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 1.1,
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: 0.9,
|
||||
),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
|
@ -501,13 +282,13 @@ class _HomePageState extends State<HomePage> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
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
|
||||
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];
|
||||
|
|
|
|||
|
|
@ -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/merchant/presentation/pages/merchant_home_page.dart';
|
||||
import 'features/trading/presentation/pages/trading_detail_page.dart';
|
||||
import 'features/coupons/presentation/pages/wallet_coupons_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const GenexConsumerApp());
|
||||
|
|
@ -109,6 +110,8 @@ class GenexConsumerApp extends StatelessWidget {
|
|||
return MaterialPageRoute(builder: (_) => const MerchantHomePage());
|
||||
case '/trading/detail':
|
||||
return MaterialPageRoute(builder: (_) => const TradingDetailPage());
|
||||
case '/wallet/coupons':
|
||||
return MaterialPageRoute(builder: (_) => const WalletCouponsPage());
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
|
|
|
|||
Loading…
Reference in New Issue