gcx/frontend/genex-mobile/lib/features/trading/presentation/pages/transfer_page.dart

868 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
/// 转赠页面 - 混合方案
///
/// 顶部:扫码转赠 / 输入ID 两个入口卡片
/// 中部:最近转赠人列表(联系方式有有效期,过期需重新确认)
/// 流程:选方式 → 填/扫地址 → 选券 → 确认
class TransferPage extends StatefulWidget {
const TransferPage({super.key});
@override
State<TransferPage> createState() => _TransferPageState();
}
class _TransferPageState extends State<TransferPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('转赠'),
actions: [
TextButton(
onPressed: () {},
child: Text('转赠记录',
style: AppTypography.labelSmall
.copyWith(color: AppColors.primary)),
),
],
),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 40),
children: [
// 两个入口卡片
_buildEntryCards(context),
const SizedBox(height: 28),
// 最近转赠
_buildRecentSection(),
const SizedBox(height: 28),
// 转赠记录摘要
_buildTransferHistory(),
],
),
);
}
// ============================================================
// 入口卡片:扫码 / 输入ID
// ============================================================
Widget _buildEntryCards(BuildContext context) {
return Row(
children: [
// 扫码转赠
Expanded(
child: GestureDetector(
onTap: () {
// TODO: 打开扫码器,扫描对方接收二维码
},
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: AppSpacing.borderRadiusMd,
boxShadow: AppSpacing.shadowPrimary,
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: AppSpacing.borderRadiusMd,
),
child: const Icon(Icons.qr_code_scanner_rounded,
size: 26, color: Colors.white),
),
const SizedBox(height: 10),
Text('扫码转赠',
style: AppTypography.labelMedium
.copyWith(color: Colors.white)),
const SizedBox(height: 4),
Text('扫描对方接收码',
style: AppTypography.caption.copyWith(
color: Colors.white.withValues(alpha: 0.7),
)),
],
),
),
),
),
const SizedBox(width: 12),
// 输入ID/邮箱/手机
Expanded(
child: GestureDetector(
onTap: () => _showInputRecipient(context),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusMd,
),
child: const Icon(Icons.edit_rounded,
size: 26, color: AppColors.primary),
),
const SizedBox(height: 10),
Text('输入ID', style: AppTypography.labelMedium),
const SizedBox(height: 4),
Text('邮箱 / 手机 / 接收ID',
style: AppTypography.caption.copyWith(
color: AppColors.textTertiary,
)),
],
),
),
),
),
],
);
}
// ============================================================
// 最近转赠人列表(联系方式有有效期)
// ============================================================
Widget _buildRecentSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('最近转赠', style: AppTypography.h3),
GestureDetector(
onTap: () {},
child: Text('管理',
style: AppTypography.caption
.copyWith(color: AppColors.primary)),
),
],
),
const SizedBox(height: 12),
if (_mockRecents.isEmpty)
_buildEmptyRecent()
else
..._mockRecents.map((r) => _buildRecentItem(r)),
],
);
}
Widget _buildEmptyRecent() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 32),
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusMd,
),
child: Column(
children: [
Icon(Icons.people_outline_rounded,
size: 36, color: AppColors.textDisabled),
const SizedBox(height: 8),
Text('暂无转赠记录',
style: AppTypography.bodySmall
.copyWith(color: AppColors.textTertiary)),
const SizedBox(height: 4),
Text('通过扫码或输入ID开始第一次转赠',
style: AppTypography.caption
.copyWith(color: AppColors.textDisabled)),
],
),
);
}
Widget _buildRecentItem(_RecentRecipient recipient) {
final isExpired = recipient.isExpired;
return GestureDetector(
onTap: isExpired
? () => _showExpiredDialog(context, recipient)
: () => _navigateToSelectCoupon(context, recipient),
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(
color: isExpired ? AppColors.borderLight : AppColors.borderLight,
),
),
child: Row(
children: [
// 头像/首字母
CircleAvatar(
radius: 20,
backgroundColor:
isExpired ? AppColors.gray100 : AppColors.primaryContainer,
child: Text(
recipient.avatarLetter,
style: TextStyle(
color: isExpired ? AppColors.textDisabled : AppColors.primary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 12),
// 信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
recipient.displayName,
style: AppTypography.labelMedium.copyWith(
color: isExpired
? AppColors.textDisabled
: AppColors.textPrimary,
),
),
const SizedBox(width: 6),
// 联系方式类型标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: isExpired
? AppColors.gray100
: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(
recipient.contactTypeLabel,
style: AppTypography.caption.copyWith(
fontSize: 10,
color: isExpired
? AppColors.textDisabled
: AppColors.primary,
),
),
),
],
),
const SizedBox(height: 2),
Row(
children: [
Text(
recipient.maskedContact,
style: AppTypography.caption.copyWith(
color: AppColors.textTertiary,
),
),
const SizedBox(width: 8),
Text(
'上次: ${recipient.lastTransferText}',
style: AppTypography.caption.copyWith(
color: AppColors.textDisabled,
fontSize: 10,
),
),
],
),
],
),
),
// 状态
if (isExpired)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.gray100,
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text('已过期',
style: AppTypography.caption.copyWith(
color: AppColors.textDisabled,
fontSize: 10,
)),
)
else
const Icon(Icons.chevron_right_rounded,
color: AppColors.textTertiary, size: 20),
],
),
),
);
}
// ============================================================
// 转赠记录摘要
// ============================================================
Widget _buildTransferHistory() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('最近转赠记录', style: AppTypography.h3),
const SizedBox(height: 12),
..._mockHistory.map((h) => Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusSm,
),
child: Row(
children: [
Icon(
h.isOutgoing
? Icons.arrow_upward_rounded
: Icons.arrow_downward_rounded,
size: 16,
color:
h.isOutgoing ? AppColors.error : AppColors.success,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(h.couponName, style: AppTypography.labelSmall),
Text(
'${h.isOutgoing ? "转赠给" : "收到来自"} ${h.personName}',
style: AppTypography.caption
.copyWith(color: AppColors.textTertiary),
),
],
),
),
Text(h.dateText,
style: AppTypography.caption
.copyWith(color: AppColors.textDisabled)),
],
),
)),
],
);
}
// ============================================================
// 输入收款人弹窗
// ============================================================
void _showInputRecipient(BuildContext context) {
final controller = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: EdgeInsets.fromLTRB(
20, 24, 20, MediaQuery.of(ctx).viewInsets.bottom + 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('输入收款人', style: AppTypography.h2),
const SizedBox(height: 4),
Text('支持接收ID、邮箱或手机号',
style: AppTypography.bodySmall
.copyWith(color: AppColors.textTertiary)),
const SizedBox(height: 20),
TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
hintText: 'GNX-USR-XXXX / email / phone',
prefixIcon:
const Icon(Icons.person_search_rounded, size: 22),
suffixIcon: IconButton(
icon: const Icon(Icons.content_paste_rounded, size: 20),
onPressed: () async {
final data = await Clipboard.getData('text/plain');
if (data?.text != null) {
controller.text = data!.text!;
}
},
tooltip: '粘贴',
),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: AppSpacing.buttonHeight,
child: ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
// 进入选券页面,带上收款人信息
_navigateToSelectCouponWithInput(
context, controller.text);
},
child: const Text('下一步:选择券'),
),
),
],
),
),
);
}
// ============================================================
// 联系方式过期提示
// ============================================================
void _showExpiredDialog(BuildContext context, _RecentRecipient recipient) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('联系方式已过期'),
content: Text(
'${recipient.displayName}${recipient.contactTypeLabel}'
'已超过90天未验证请重新输入确认。',
style: AppTypography.bodyMedium
.copyWith(color: AppColors.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
_showInputRecipient(context);
},
child: const Text('重新输入'),
),
],
),
);
}
// ============================================================
// 导航到选券(带收款人信息)
// ============================================================
void _navigateToSelectCoupon(
BuildContext context, _RecentRecipient recipient) {
_showSelectCouponSheet(context, recipient.displayName);
}
void _navigateToSelectCouponWithInput(
BuildContext context, String input) {
if (input.trim().isEmpty) return;
_showSelectCouponSheet(context, input);
}
void _showSelectCouponSheet(BuildContext context, String recipientName) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => DraggableScrollableSheet(
initialChildSize: 0.7,
maxChildSize: 0.9,
minChildSize: 0.5,
expand: false,
builder: (_, scrollController) => Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('选择要转赠的券', style: AppTypography.h2),
const SizedBox(height: 2),
Text('转赠给 $recipientName',
style: AppTypography.caption
.copyWith(color: AppColors.primary)),
],
),
IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => Navigator.pop(ctx),
),
],
),
),
const Divider(),
// Coupon list
Expanded(
child: ListView.separated(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
itemCount: _mockCouponsForTransfer.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final c = _mockCouponsForTransfer[index];
return GestureDetector(
onTap: () {
Navigator.pop(ctx);
_showConfirm(context, recipientName, c);
},
child: Container(
padding: AppSpacing.cardPadding,
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusSm,
),
child: Icon(
Icons.confirmation_number_outlined,
color: AppColors.primary
.withValues(alpha: 0.4),
size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(c.brandName,
style: AppTypography.caption),
Text(c.name,
style: AppTypography.labelMedium),
],
),
),
Text(
'\$${c.faceValue.toStringAsFixed(0)}',
style: AppTypography.labelMedium.copyWith(
color: AppColors.primary),
),
],
),
),
);
},
),
),
],
),
),
);
}
// ============================================================
// 确认转赠
// ============================================================
void _showConfirm(
BuildContext context, String recipientName, _TransferCoupon coupon) {
showModalBottomSheet(
context: context,
backgroundColor: AppColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.card_giftcard_rounded,
color: AppColors.primary, size: 48),
const SizedBox(height: 16),
Text('确认转赠', style: AppTypography.h2),
const SizedBox(height: 12),
// 券信息
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.gray50,
borderRadius: AppSpacing.borderRadiusSm,
),
child: Column(
children: [
Text(coupon.name, style: AppTypography.labelMedium),
const SizedBox(height: 4),
Text('面值 \$${coupon.faceValue.toStringAsFixed(2)}',
style: AppTypography.bodySmall),
],
),
),
const SizedBox(height: 12),
// 收款人
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.arrow_downward_rounded,
size: 16, color: AppColors.primary),
const SizedBox(width: 6),
Text('转赠给 ',
style: AppTypography.bodyMedium
.copyWith(color: AppColors.textSecondary)),
Text(recipientName,
style: AppTypography.labelMedium
.copyWith(color: AppColors.primary)),
],
),
const SizedBox(height: 8),
Text('转赠后您将不再持有此券',
style: AppTypography.caption
.copyWith(color: AppColors.warning)),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
_showSuccess(context, recipientName, coupon);
},
child: const Text('确认转赠'),
),
),
],
),
],
),
),
);
}
void _showSuccess(
BuildContext context, String recipientName, _TransferCoupon coupon) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_rounded,
color: AppColors.success, size: 56),
const SizedBox(height: 16),
Text('转赠成功', style: AppTypography.h2),
const SizedBox(height: 8),
Text('${coupon.name} 已转赠给 $recipientName',
style: AppTypography.bodyMedium
.copyWith(color: AppColors.textSecondary),
textAlign: TextAlign.center),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(ctx);
Navigator.pop(context);
},
child: const Text('完成'),
),
],
),
);
}
}
// ============================================================
// Data Models
// ============================================================
enum _ContactType { email, phone, receiveId }
class _RecentRecipient {
final String displayName;
final String contact;
final _ContactType contactType;
final DateTime lastTransfer;
final DateTime contactExpiry;
const _RecentRecipient({
required this.displayName,
required this.contact,
required this.contactType,
required this.lastTransfer,
required this.contactExpiry,
});
bool get isExpired => DateTime.now().isAfter(contactExpiry);
String get avatarLetter => displayName.isNotEmpty ? displayName[0] : '?';
String get contactTypeLabel {
switch (contactType) {
case _ContactType.email:
return '邮箱';
case _ContactType.phone:
return '手机';
case _ContactType.receiveId:
return 'ID';
}
}
String get maskedContact {
switch (contactType) {
case _ContactType.email:
final parts = contact.split('@');
if (parts.length == 2) {
return '${parts[0].substring(0, 2)}***@${parts[1]}';
}
return contact;
case _ContactType.phone:
if (contact.length >= 7) {
return '${contact.substring(0, 3)}****${contact.substring(contact.length - 4)}';
}
return contact;
case _ContactType.receiveId:
return contact;
}
}
String get lastTransferText {
final days = DateTime.now().difference(lastTransfer).inDays;
if (days == 0) return '今天';
if (days == 1) return '昨天';
if (days < 7) return '$days天前';
if (days < 30) return '${days ~/ 7}周前';
return '${days ~/ 30}月前';
}
}
class _TransferCoupon {
final String brandName;
final String name;
final double faceValue;
const _TransferCoupon({
required this.brandName,
required this.name,
required this.faceValue,
});
}
class _TransferRecord {
final String couponName;
final String personName;
final bool isOutgoing;
final DateTime date;
const _TransferRecord({
required this.couponName,
required this.personName,
required this.isOutgoing,
required this.date,
});
String get dateText {
final days = DateTime.now().difference(date).inDays;
if (days == 0) return '今天';
if (days == 1) return '昨天';
if (days < 7) return '$days天前';
return '${date.month}/${date.day}';
}
}
// ============================================================
// Mock Data
// ============================================================
final _mockRecents = [
_RecentRecipient(
displayName: 'Alice',
contact: 'alice@gmail.com',
contactType: _ContactType.email,
lastTransfer: DateTime.now().subtract(const Duration(days: 3)),
contactExpiry: DateTime.now().add(const Duration(days: 87)),
),
_RecentRecipient(
displayName: 'Bob',
contact: '1385551234',
contactType: _ContactType.phone,
lastTransfer: DateTime.now().subtract(const Duration(days: 12)),
contactExpiry: DateTime.now().add(const Duration(days: 78)),
),
_RecentRecipient(
displayName: 'Charlie',
contact: 'GNX-USR-7B2E-M4F1',
contactType: _ContactType.receiveId,
lastTransfer: DateTime.now().subtract(const Duration(days: 45)),
contactExpiry: DateTime.now().subtract(const Duration(days: 5)), // 已过期
),
];
const _mockCouponsForTransfer = [
_TransferCoupon(
brandName: 'Starbucks',
name: '星巴克 \$25 礼品卡',
faceValue: 25.0,
),
_TransferCoupon(
brandName: 'Amazon',
name: 'Amazon \$100 购物券',
faceValue: 100.0,
),
_TransferCoupon(
brandName: 'Target',
name: 'Target \$30 折扣券',
faceValue: 30.0,
),
];
final _mockHistory = [
_TransferRecord(
couponName: '星巴克 \$25 礼品卡',
personName: 'Alice',
isOutgoing: true,
date: DateTime.now().subtract(const Duration(days: 3)),
),
_TransferRecord(
couponName: 'Nike \$80 运动券',
personName: 'Bob',
isOutgoing: false,
date: DateTime.now().subtract(const Duration(days: 7)),
),
_TransferRecord(
couponName: 'Walmart \$50 生活券',
personName: 'Diana',
isOutgoing: true,
date: DateTime.now().subtract(const Duration(days: 15)),
),
];