feat(mobile/coupons): complete coupon holdings feature
- wallet_coupons_page: pass full CouponModel to detail route (was coupon.id) - wallet_coupons_page: fix status filters (in_circulation=可使用, listed=挂售中) - my_coupon_detail_page: full rewrite — StatefulWidget, accept CouponModel or String ID, real QR code via qr_flutter, barcode toggle, real data (faceValue/price/expiry/orderNo/resale), conditional action buttons per isTransferable + resaleCount, grey gradient for expired coupons - profile_page: convert to StatefulWidget, load holdingsSummary on init, show real hold count & totalSaved in quick stats (tappable → /wallet/coupons), add "我的持仓" menu entry - i18n: add myCoupon.switchQr + common.notFound to all 4 locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c00c48c8bd
commit
4065d586a9
|
|
@ -21,6 +21,7 @@ const Map<String, String> en = {
|
|||
'common.today': 'Today',
|
||||
'common.thisWeek': 'This Week',
|
||||
'common.thisMonth': 'This Month',
|
||||
'common.notFound': 'Not found',
|
||||
|
||||
// ============ Navigation ============
|
||||
'nav.home': 'Home',
|
||||
|
|
@ -272,6 +273,7 @@ const Map<String, String> en = {
|
|||
'myCoupon.active': 'Active',
|
||||
'myCoupon.showQrHint': 'Show this QR code to the merchant to redeem',
|
||||
'myCoupon.switchBarcode': 'Switch to Barcode',
|
||||
'myCoupon.switchQr': 'Switch to QR Code',
|
||||
'myCoupon.faceValue': 'Face Value',
|
||||
'myCoupon.purchasePrice': 'Purchase Price',
|
||||
'myCoupon.validUntil': 'Valid Until',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const Map<String, String> ja = {
|
|||
'common.today': '今日',
|
||||
'common.thisWeek': '今週',
|
||||
'common.thisMonth': '今月',
|
||||
'common.notFound': 'データが見つかりません',
|
||||
|
||||
// ============ Navigation ============
|
||||
'nav.home': 'ホーム',
|
||||
|
|
@ -273,6 +274,7 @@ const Map<String, String> ja = {
|
|||
'myCoupon.active': '利用可能',
|
||||
'myCoupon.showQrHint': 'このQRコードを店舗スタッフに提示してスキャンしてもらってください',
|
||||
'myCoupon.switchBarcode': 'バーコードに切替',
|
||||
'myCoupon.switchQr': 'QRコードに切替',
|
||||
'myCoupon.faceValue': '額面',
|
||||
'myCoupon.purchasePrice': '購入価格',
|
||||
'myCoupon.validUntil': '有効期限',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const Map<String, String> zhCN = {
|
|||
'common.today': '今日',
|
||||
'common.thisWeek': '本周',
|
||||
'common.thisMonth': '本月',
|
||||
'common.notFound': '数据不存在',
|
||||
|
||||
// ============ Navigation ============
|
||||
'nav.home': '首页',
|
||||
|
|
@ -273,6 +274,7 @@ const Map<String, String> zhCN = {
|
|||
'myCoupon.active': '可使用',
|
||||
'myCoupon.showQrHint': '出示此二维码给商户扫描核销',
|
||||
'myCoupon.switchBarcode': '切换条形码',
|
||||
'myCoupon.switchQr': '切换二维码',
|
||||
'myCoupon.faceValue': '面值',
|
||||
'myCoupon.purchasePrice': '购买价格',
|
||||
'myCoupon.validUntil': '有效期',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const Map<String, String> zhTW = {
|
|||
'common.today': '今日',
|
||||
'common.thisWeek': '本週',
|
||||
'common.thisMonth': '本月',
|
||||
'common.notFound': '資料不存在',
|
||||
|
||||
// ============ Navigation ============
|
||||
'nav.home': '首頁',
|
||||
|
|
@ -273,6 +274,7 @@ const Map<String, String> zhTW = {
|
|||
'myCoupon.active': '可使用',
|
||||
'myCoupon.showQrHint': '出示此二維碼給商戶掃描核銷',
|
||||
'myCoupon.switchBarcode': '切換條碼',
|
||||
'myCoupon.switchQr': '切換二維碼',
|
||||
'myCoupon.faceValue': '面值',
|
||||
'myCoupon.purchasePrice': '購買價格',
|
||||
'myCoupon.validUntil': '有效期',
|
||||
|
|
|
|||
|
|
@ -1,18 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
import '../../../../shared/widgets/status_tag.dart';
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../../../../core/services/coupon_service.dart';
|
||||
import '../../data/models/coupon_model.dart';
|
||||
|
||||
/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售/提取
|
||||
/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售
|
||||
///
|
||||
/// 券二维码/条形码(核销用)、券信息、使用说明、
|
||||
/// 「转赠」「出售」「使用说明」按钮
|
||||
class MyCouponDetailPage extends StatelessWidget {
|
||||
/// 接收路由参数:CouponModel(来自 wallet_coupons_page)或 String ID(兼容旧路由)
|
||||
class MyCouponDetailPage extends StatefulWidget {
|
||||
const MyCouponDetailPage({super.key});
|
||||
|
||||
@override
|
||||
State<MyCouponDetailPage> createState() => _MyCouponDetailPageState();
|
||||
}
|
||||
|
||||
class _MyCouponDetailPageState extends State<MyCouponDetailPage> {
|
||||
CouponModel? _coupon;
|
||||
bool _isLoading = false;
|
||||
bool _showBarcode = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_coupon != null) return;
|
||||
final args = ModalRoute.of(context)?.settings.arguments;
|
||||
if (args is CouponModel) {
|
||||
_coupon = args;
|
||||
} else if (args is String) {
|
||||
_loadById(args);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadById(String id) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final coupon = await CouponApiService().getCouponDetail(id);
|
||||
if (mounted) setState(() { _coupon = coupon; _isLoading = false; });
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -23,186 +55,275 @@ class MyCouponDetailPage extends StatelessWidget {
|
|||
),
|
||||
title: Text(context.t('myCoupon.title')),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
onPressed: () => _showMoreOptions(context),
|
||||
),
|
||||
if (_coupon != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_horiz_rounded),
|
||||
onPressed: () => _showMoreOptions(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: AppSpacing.pagePadding,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _coupon == null
|
||||
? Center(child: Text(context.t('common.notFound')))
|
||||
: _buildBody(context, _coupon!),
|
||||
);
|
||||
}
|
||||
|
||||
// QR Code Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.cardGradient,
|
||||
borderRadius: AppSpacing.borderRadiusLg,
|
||||
boxShadow: AppSpacing.shadowPrimary,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Brand + Status
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
Widget _buildBody(BuildContext context, CouponModel coupon) {
|
||||
final qrData = 'GNX:${coupon.id}';
|
||||
final isActive = coupon.status == 'in_circulation';
|
||||
final expiryStr = '${coupon.expiryDate.year}/${coupon.expiryDate.month.toString().padLeft(2, '0')}/${coupon.expiryDate.day.toString().padLeft(2, '0')}';
|
||||
final orderNo = 'GNX-${coupon.id.replaceAll('-', '').substring(0, 12).toUpperCase()}';
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: AppSpacing.pagePadding,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// QR / Barcode Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: coupon.isExpired
|
||||
? const LinearGradient(colors: [Color(0xFF9E9E9E), Color(0xFF757575)])
|
||||
: AppColors.cardGradient,
|
||||
borderRadius: AppSpacing.borderRadiusLg,
|
||||
boxShadow: AppSpacing.shadowPrimary,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Brand + Status
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Starbucks', style: AppTypography.bodySmall.copyWith(
|
||||
Text(coupon.brandName ?? '', style: AppTypography.bodySmall.copyWith(
|
||||
color: Colors.white70,
|
||||
)),
|
||||
Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith(
|
||||
Text(coupon.name, style: AppTypography.h2.copyWith(
|
||||
color: Colors.white,
|
||||
)),
|
||||
), overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: AppSpacing.borderRadiusFull,
|
||||
),
|
||||
child: Text(context.t('myCoupon.active'), style: AppTypography.caption.copyWith(
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: AppSpacing.borderRadiusFull,
|
||||
),
|
||||
child: Text(
|
||||
_statusLabel(context, coupon.status),
|
||||
style: AppTypography.caption.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// QR Code area
|
||||
Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.qr_code_rounded, size: 140,
|
||||
color: AppColors.textPrimary),
|
||||
const SizedBox(height: 8),
|
||||
Text('GNX-STB-A1B2C3D4',
|
||||
style: AppTypography.caption.copyWith(
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// QR Code / Barcode area
|
||||
Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
child: _showBarcode
|
||||
? _buildBarcode(coupon.id)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: QrImageView(
|
||||
data: qrData,
|
||||
version: QrVersions.auto,
|
||||
size: 176,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Instructions
|
||||
Text(
|
||||
context.t('myCoupon.showQrHint'),
|
||||
style: AppTypography.bodySmall.copyWith(color: Colors.white70),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Barcode toggle
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.view_headline_rounded, size: 18,
|
||||
color: Colors.white70),
|
||||
label: Text(context.t('myCoupon.switchBarcode'), style: AppTypography.labelSmall.copyWith(
|
||||
color: Colors.white70,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Face Value + Expiry
|
||||
Container(
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_infoRow(context.t('myCoupon.faceValue'), '\$25.00'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.purchasePrice'), '\$21.25'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.validUntil'), '2026/12/31'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.orderNo'), 'GNX-20260209-001234'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.resellCount'), '3次'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GenexButton(
|
||||
label: context.t('myCoupon.transfer'),
|
||||
icon: Icons.card_giftcard_rounded,
|
||||
variant: GenexButtonVariant.secondary,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/transfer');
|
||||
},
|
||||
Text(
|
||||
coupon.id.replaceAll('-', '').substring(0, 16).toUpperCase(),
|
||||
style: AppTypography.caption.copyWith(
|
||||
color: Colors.white70,
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GenexButton(
|
||||
label: context.t('myCoupon.sell'),
|
||||
icon: Icons.sell_rounded,
|
||||
variant: GenexButtonVariant.outline,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/sell');
|
||||
},
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Text(
|
||||
context.t('myCoupon.showQrHint'),
|
||||
style: AppTypography.bodySmall.copyWith(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(() => _showBarcode = !_showBarcode),
|
||||
icon: Icon(
|
||||
_showBarcode ? Icons.qr_code_rounded : Icons.view_headline_rounded,
|
||||
size: 18,
|
||||
color: Colors.white70,
|
||||
),
|
||||
label: Text(
|
||||
_showBarcode
|
||||
? context.t('myCoupon.switchQr')
|
||||
: context.t('myCoupon.switchBarcode'),
|
||||
style: AppTypography.labelSmall.copyWith(color: Colors.white70),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Usage Rules
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t('myCoupon.usageNote'), style: AppTypography.labelMedium),
|
||||
const SizedBox(height: 12),
|
||||
// Info Card
|
||||
Container(
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_infoRow(context.t('myCoupon.faceValue'),
|
||||
'\$${coupon.faceValue.toStringAsFixed(2)}'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.purchasePrice'),
|
||||
'\$${coupon.currentPrice.toStringAsFixed(2)}'),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.validUntil'), expiryStr),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.orderNo'), orderNo),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
|
||||
_infoRow(context.t('myCoupon.resellCount'),
|
||||
'${coupon.resaleCount}/${coupon.maxResaleCount}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Action Buttons (only for active coupons)
|
||||
if (isActive)
|
||||
Row(
|
||||
children: [
|
||||
if (coupon.isTransferable)
|
||||
Expanded(
|
||||
child: GenexButton(
|
||||
label: context.t('myCoupon.transfer'),
|
||||
icon: Icons.card_giftcard_rounded,
|
||||
variant: GenexButtonVariant.secondary,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/transfer', arguments: coupon.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (coupon.isTransferable) const SizedBox(width: 12),
|
||||
if (coupon.resaleCount < coupon.maxResaleCount)
|
||||
Expanded(
|
||||
child: GenexButton(
|
||||
label: context.t('myCoupon.sell'),
|
||||
icon: Icons.sell_rounded,
|
||||
variant: GenexButtonVariant.outline,
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/sell', arguments: coupon.id);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (isActive) const SizedBox(height: 16),
|
||||
|
||||
// Usage Rules
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: AppSpacing.borderRadiusMd,
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.t('myCoupon.usageNote'), style: AppTypography.labelMedium),
|
||||
const SizedBox(height: 12),
|
||||
if (coupon.description != null && coupon.description!.isNotEmpty)
|
||||
_ruleItem(coupon.description!)
|
||||
else ...[
|
||||
_ruleItem(context.t('myCoupon.useInStore')),
|
||||
_ruleItem(context.t('myCoupon.useInTime')),
|
||||
_ruleItem(context.t('myCoupon.onePerVisit')),
|
||||
_ruleItem(context.t('myCoupon.noCash')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Simple barcode visual using thin/thick bars
|
||||
Widget _buildBarcode(String id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: List.generate(40, (i) {
|
||||
final val = id.codeUnitAt(i % id.length);
|
||||
final width = (val % 3 == 0) ? 3.0 : (val % 2 == 0) ? 2.0 : 1.5;
|
||||
final isWhite = i % 7 == 0;
|
||||
return Container(
|
||||
width: width,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 0.5),
|
||||
color: isWhite ? Colors.white : Colors.black87,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
id.replaceAll('-', '').substring(0, 12).toUpperCase(),
|
||||
style: AppTypography.caption.copyWith(
|
||||
letterSpacing: 1.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusLabel(BuildContext context, String status) {
|
||||
switch (status) {
|
||||
case 'in_circulation': return context.t('myCoupon.active');
|
||||
case 'listed': return context.t('walletCoupons.pendingRedeem');
|
||||
case 'expired': return context.t('walletCoupons.expired');
|
||||
case 'redeemed': return context.t('status.used');
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _infoRow(String label, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
|
@ -219,16 +340,20 @@ class MyCouponDetailPage extends StatelessWidget {
|
|||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 4, height: 4,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.textTertiary,
|
||||
shape: BoxShape.circle,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Container(
|
||||
width: 4, height: 4,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.textTertiary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(text, style: AppTypography.bodySmall),
|
||||
Expanded(child: Text(text, style: AppTypography.bodySmall)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -250,7 +375,8 @@ class MyCouponDetailPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_optionTile(Icons.wallet_rounded, context.t('myCoupon.extractToWallet'), context.t('myCoupon.requireKycL2'), () {}),
|
||||
_optionTile(Icons.wallet_rounded, context.t('myCoupon.extractToWallet'),
|
||||
context.t('myCoupon.requireKycL2'), () {}),
|
||||
const Divider(),
|
||||
_optionTile(Icons.receipt_long_rounded, context.t('myCoupon.viewTrades'), '', () {}),
|
||||
const Divider(),
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ class _WalletCouponsPageState extends State<WalletCouponsPage>
|
|||
|
||||
// Tab index → backend status filter
|
||||
static const _tabStatusFilters = <int, String?>{
|
||||
0: null, // 全部
|
||||
1: 'listed', // 可使用 (listed / in_circulation)
|
||||
2: 'sold', // 待核销
|
||||
3: 'expired', // 已过期
|
||||
0: null, // 全部
|
||||
1: 'in_circulation', // 可使用(在用户钱包中)
|
||||
2: 'listed', // 挂售中(已挂出待成交)
|
||||
3: 'expired', // 已过期
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
@ -259,7 +259,7 @@ class _WalletCouponsPageState extends State<WalletCouponsPage>
|
|||
final displayStatus = _mapStatus(coupon.status);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon.id),
|
||||
onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon),
|
||||
child: Container(
|
||||
padding: AppSpacing.cardPadding,
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
|
|
@ -2,16 +2,39 @@ 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/kyc_badge.dart';
|
||||
import '../../../../app/i18n/app_localizations.dart';
|
||||
import '../../../../core/services/coupon_service.dart';
|
||||
import '../../data/models/holdings_summary_model.dart';
|
||||
|
||||
/// A7. 个人中心
|
||||
///
|
||||
/// 头像、昵称、KYC等级标识、信用积分
|
||||
/// KYC认证、支付管理、设置、Pro模式
|
||||
class ProfilePage extends StatelessWidget {
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
HoldingsSummaryModel? _holdingsSummary;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSummary();
|
||||
}
|
||||
|
||||
Future<void> _loadSummary() async {
|
||||
try {
|
||||
final summary = await CouponApiService().getHoldingsSummary();
|
||||
if (mounted) setState(() => _holdingsSummary = summary);
|
||||
} catch (e) {
|
||||
debugPrint('[ProfilePage] loadSummary error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -37,6 +60,9 @@ class ProfilePage extends StatelessWidget {
|
|||
])),
|
||||
|
||||
SliverToBoxAdapter(child: _buildMenuSection(context.t('profile.trade'), [
|
||||
_MenuItem(Icons.confirmation_number_outlined, context.t('profile.holdCoupons'),
|
||||
'${_holdingsSummary?.count ?? '--'}', true,
|
||||
onTap: () => Navigator.pushNamed(context, '/wallet/coupons')),
|
||||
_MenuItem(Icons.receipt_long_rounded, context.t('wallet.records'), '', true,
|
||||
onTap: () => Navigator.pushNamed(context, '/trading')),
|
||||
_MenuItem(Icons.storefront_rounded, context.t('tradingPage.pendingOrders'), context.t('status.onSale'), true,
|
||||
|
|
@ -144,11 +170,14 @@ class ProfilePage extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget _buildQuickStats(BuildContext context) {
|
||||
final holdCount = _holdingsSummary?.count ?? 0;
|
||||
final saved = _holdingsSummary?.totalSaved ?? 0;
|
||||
|
||||
final stats = [
|
||||
(context.t('profile.holdCoupons'), '12'),
|
||||
(context.t('profile.trade'), '28'),
|
||||
(context.t('profile.saved'), '\$156'),
|
||||
(context.t('profile.credit'), '750'),
|
||||
(context.t('profile.holdCoupons'), '$holdCount', () => Navigator.pushNamed(context, '/wallet/coupons')),
|
||||
(context.t('profile.trade'), '28', null),
|
||||
(context.t('profile.saved'), '\$${saved.toStringAsFixed(0)}', null),
|
||||
(context.t('profile.credit'), '750', null),
|
||||
];
|
||||
|
||||
return Container(
|
||||
|
|
@ -162,12 +191,15 @@ class ProfilePage extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: stats.map((stat) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)),
|
||||
const SizedBox(height: 4),
|
||||
Text(stat.$1, style: AppTypography.caption),
|
||||
],
|
||||
return GestureDetector(
|
||||
onTap: stat.$3,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)),
|
||||
const SizedBox(height: 4),
|
||||
Text(stat.$1, style: AppTypography.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue