gcx/frontend/genex-mobile/lib/features/profile/presentation/pages/share_page.dart

593 lines
23 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.

// ============================================================
// SharePage — 邀请好友 / 推广分享页
//
// 路由:/share从 ProfilePage「邀请好友」横幅入口跳转
//
// 功能概述:
// 1. Hero Card — 紫色渐变卡片,包含:
// · 专属二维码qr_flutter 生成,编码邀请落地页 URL
// · 推荐码胶囊(可点击一键复制)
// 2. 邀请进度 — 显示直接推荐人数 + 团队总人数
// 3. 分享操作列表 — 三种快捷方式:复制推荐码 / 复制链接 / 原生分享
// 4. 主操作按钮 — 「分享给好友」触发系统原生分享弹层
//
// 支持的分享场景(取决于用户设备安装的 App
// 微信 / QQ / WhatsApp / Telegram / Line / Twitter/X
// 短信 / 邮件 / AirDrop / 复制到剪贴板 / 保存至文件……
//
// 数据来源:
// GET /api/v1/referral/my → ReferralService.getMyInfo()
// 推荐码通过 Kafka 事件在用户注册时自动生成,首次打开此页若
// 数据尚未就绪,将显示 Loading → 加载失败 → 重试 流程。
//
// 邀请链接格式:
// https://app.gogenex.com/download?ref={referralCode}
// 落地页检测设备后分别跳转 App Store / Google Play / 浏览器下载。
// ref 参数由注册流程自动读取并预填推荐码输入框。
//
// 依赖包:
// qr_flutter ^4.1.0 — 本地生成二维码,无网络依赖
// share_plus ^10.0.2 — 调用系统原生分享弹层iOS Share Sheet / Android Sharesheet
// ============================================================
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart';
import '../../../../core/services/referral_service.dart';
import '../../../../shared/widgets/genex_button.dart';
import '../../../../app/i18n/app_localizations.dart';
/// A9. 邀请好友 / 推广分享页
///
/// 页面生命周期:
/// initState → _loadInfo() → [Loading] → [Content | Error]
///
/// Widget 树概览:
/// ```
/// Scaffold
/// └── SingleChildScrollView
/// ├── _buildHeroCard() 渐变卡片QR 码 + 推荐码
/// ├── _buildStatsCard() 邀请进度:直接推荐 | 团队人数
/// ├── _buildShareActions() 操作列表:复制码 / 复制链 / 系统分享
/// └── GenexButton 主按钮:「分享给好友」
/// ```
class SharePage extends StatefulWidget {
const SharePage({super.key});
@override
State<SharePage> createState() => _SharePageState();
}
class _SharePageState extends State<SharePage> {
// ── 状态 ────────────────────────────────────────────────────────────────
/// 从 referral-service 加载的推荐信息;加载完成前为 null
ReferralInfo? _info;
/// 正在请求后端数据
bool _loading = true;
/// 请求失败时的错误信息(用于展示重试按钮)
String? _error;
// ── 常量 ────────────────────────────────────────────────────────────────
/// App 下载落地页基础地址
///
/// 落地页职责:
/// 1. 检测 UAiOS / Android / 其他)
/// 2. iOS → 跳转 App Store
/// 3. Android → 跳转 Google Play 或直接下载 APK
/// 4. 读取 `ref` 参数并存入 localStorage注册页自动填充推荐码
static const String _baseInviteUrl = 'https://app.gogenex.com/download';
// ── 计算属性 ─────────────────────────────────────────────────────────────
/// 完整邀请链接(二维码内容 + 分享文案使用)
///
/// 已加载https://app.gogenex.com/download?ref=GNXAB2C3
/// 未加载https://app.gogenex.com/download
String get _inviteLink => _info != null
? '$_baseInviteUrl?ref=${_info!.referralCode}'
: _baseInviteUrl;
// ── 生命周期 ─────────────────────────────────────────────────────────────
@override
void initState() {
super.initState();
_loadInfo();
}
// ── 数据加载 ─────────────────────────────────────────────────────────────
/// 从 referral-service 拉取当前用户的推荐信息
///
/// 成功:更新 [_info],隐藏 loading
/// 失败:记录 [_error],显示重试界面
Future<void> _loadInfo() async {
setState(() {
_loading = true;
_error = null;
});
try {
final info = await ReferralService.instance.getMyInfo();
if (mounted) setState(() { _info = info; _loading = false; });
} catch (e) {
if (mounted) setState(() { _error = e.toString(); _loading = false; });
}
}
// ── 用户操作 ─────────────────────────────────────────────────────────────
/// 复制成功后展示浮动 SnackBar紫色背景 + 勾号图标)
void _showCopied(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_rounded, color: Colors.white, size: 18),
const SizedBox(width: 8),
Text(message),
],
),
backgroundColor: AppColors.primary,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
),
);
}
/// 复制推荐码到剪贴板
///
/// 示例复制内容GNXAB2C3
Future<void> _copyCode() async {
if (_info == null) return;
await Clipboard.setData(ClipboardData(text: _info!.referralCode));
if (mounted) _showCopied(context.t('share.codeCopied'));
}
/// 复制完整邀请链接到剪贴板
///
/// 示例https://app.gogenex.com/download?ref=GNXAB2C3
Future<void> _copyLink() async {
await Clipboard.setData(ClipboardData(text: _inviteLink));
if (mounted) _showCopied(context.t('share.linkCopied'));
}
/// 调用系统原生分享弹层
///
/// 分享内容:多行文案(包含推荐码 + 邀请链接)
///
/// iOS → Share SheetAirDrop、微信、邮件、短信……
/// Android → Sharesheet微信、WhatsApp、Telegram……
///
/// 分享文案模板zh-CN
/// 我在用 Genex 玩数字券金融!使用我的推荐码 {code} 注册,立享专属福利。
/// 下载链接https://app.gogenex.com/download?ref={code}
Future<void> _shareNative() async {
final code = _info?.referralCode ?? '';
final text = context
.t('share.shareText')
.replaceAll('{code}', code)
.replaceAll('{link}', _inviteLink);
await Share.share(text, subject: context.t('share.nativeShareTitle'));
}
// ── Build ────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(context.t('share.title')),
backgroundColor: Colors.transparent,
elevation: 0,
surfaceTintColor: Colors.transparent,
),
body: _loading
? _buildLoading()
: _error != null
? _buildError()
: _buildContent(),
);
}
// ── 加载态 ───────────────────────────────────────────────────────────────
Widget _buildLoading() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
context.t('share.loading'),
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary),
),
],
),
);
}
// ── 错误态 ───────────────────────────────────────────────────────────────
/// 加载失败界面:图标 + 描述文本 + 重试按钮
Widget _buildError() {
return Center(
child: Padding(
padding: AppSpacing.pagePadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline_rounded, size: 56, color: AppColors.textTertiary),
const SizedBox(height: 16),
Text(
context.t('share.loadFailed'),
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
TextButton.icon(
onPressed: _loadInfo,
icon: const Icon(Icons.refresh_rounded),
label: Text(context.t('share.retry')),
style: TextButton.styleFrom(foregroundColor: AppColors.primary),
),
],
),
),
);
}
// ── 主内容 ───────────────────────────────────────────────────────────────
Widget _buildContent() {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeroCard(), // QR 码 + 推荐码
const SizedBox(height: 16),
_buildStatsCard(), // 邀请进度统计
const SizedBox(height: 16),
_buildShareActions(), // 快捷操作列表
const SizedBox(height: 24),
GenexButton( // 主操作按钮
label: context.t('share.shareToFriend'),
onPressed: _shareNative,
),
const SizedBox(height: 40),
],
),
);
}
// ── Hero Card二维码 + 推荐码 ────────────────────────────────────────────
//
// 视觉层次(由外到内):
// Container[渐变背景 + 圆角20 + 阴影]
// └── Column
// ├── 标题行(星形图标 + "扫码下载 Genex"
// ├── "Genex" 大字(字间距 3FontWeight.w800
// ├── Container[白色背景] → QrImageView180×180紫色点阵
// └── 推荐码胶囊(半透明白底 + 复制按钮)
//
// QR 码内容_inviteLink如 https://app.gogenex.com/download?ref=GNXAB2C3
// QR 码颜色eye=深紫 #4834D4module=主紫 #6C5CE7背景=白色
Widget _buildHeroCard() {
return Container(
decoration: BoxDecoration(
gradient: AppColors.cardGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.35),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
child: Column(
children: [
// ── 顶部标题 ────────────────────────────────────────────────────
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.auto_awesome_rounded, color: Colors.white60, size: 16),
const SizedBox(width: 6),
Text(
context.t('share.scanToJoin'),
style: AppTypography.bodyMedium.copyWith(color: Colors.white70),
),
],
),
const SizedBox(height: 6),
Text(
'Genex',
style: AppTypography.displayMedium.copyWith(
color: Colors.white,
letterSpacing: 3,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 24),
// ── 二维码 ──────────────────────────────────────────────────────
// 白色衬底卡片包裹 QrImageView确保二维码在任何背景下均可扫描
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: QrImageView(
data: _inviteLink, // 编码完整邀请 URL
version: QrVersions.auto, // 自动选择最小版本
size: 180,
backgroundColor: Colors.white,
// 定位图案(三个角的方框)用深紫色
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Color(0xFF4834D4),
),
// 数据模块用主紫色
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Color(0xFF6C5CE7),
),
),
),
const SizedBox(height: 24),
// ── 推荐码胶囊 ─────────────────────────────────────────────────
// 点击整个胶囊即可复制,内嵌「复制推荐码」小按钮提示可交互
Text(
context.t('share.myReferralCode'),
style: AppTypography.caption.copyWith(color: Colors.white60),
),
const SizedBox(height: 8),
GestureDetector(
onTap: _copyCode,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15), // 半透明白底
borderRadius: BorderRadius.circular(40),
border: Border.all(color: Colors.white30),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 推荐码文字:等宽字体 + 字间距 4视觉更清晰
Text(
_info?.referralCode ?? '------',
style: AppTypography.h2.copyWith(
color: Colors.white,
letterSpacing: 4,
fontWeight: FontWeight.w700,
fontFamily: 'monospace',
),
),
const SizedBox(width: 12),
// 内嵌复制提示按钮
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.copy_rounded, size: 12, color: Colors.white),
const SizedBox(width: 4),
Text(
context.t('share.copyCode'),
style: AppTypography.caption.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
],
),
),
);
}
// ── 邀请进度统计卡片 ─────────────────────────────────────────────────────
//
// 布局:白色圆角卡片,水平等分两格,中间细竖线分隔
// 左直接推荐人数Icons.people
// 右团队总人数Icons.groups
//
// 数据来源ReferralInfo.directReferralCount / totalTeamCount
// 加载前显示 0刷新后实时更新
Widget _buildStatsCard() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
boxShadow: AppSpacing.shadowSm,
),
child: Row(
children: [
Expanded(
child: _buildStatItem(
icon: Icons.people_rounded,
value: '${_info?.directReferralCount ?? 0}',
label: context.t('share.directReferrals'),
),
),
// 分隔线
Container(width: 1, height: 44, color: AppColors.borderLight),
Expanded(
child: _buildStatItem(
icon: Icons.groups_rounded,
value: '${_info?.totalTeamCount ?? 0}',
label: context.t('share.teamSize'),
),
),
],
),
);
}
/// 单个统计格:图标 + 数值 + 标签
Widget _buildStatItem({
required IconData icon,
required String value,
required String label,
}) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: AppColors.primary),
const SizedBox(width: 4),
Text(value, style: AppTypography.h2.copyWith(color: AppColors.primary)),
],
),
const SizedBox(height: 2),
Text(label, style: AppTypography.caption.copyWith(color: AppColors.textSecondary)),
],
);
}
// ── 分享操作列表 ─────────────────────────────────────────────────────────
//
// 白色圆角卡片ListTile 列表Divider 分隔):
//
// [紫色图标] 复制推荐码 副文本GNXAB2C3 >
// [蓝色图标] 复制链接 副文本https://... >
// [绿色图标] 分享给好友 副文本:通过微信/WhatsApp >
//
// 每项点击行为:
// 复制推荐码 → _copyCode() → 剪贴板 + SnackBar
// 复制链接 → _copyLink() → 剪贴板 + SnackBar
// 分享给好友 → _shareNative() → 系统原生分享弹层
Widget _buildShareActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 10),
child: Text(
context.t('share.quickShare'),
style: AppTypography.labelSmall.copyWith(color: AppColors.textTertiary),
),
),
Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.borderLight),
boxShadow: AppSpacing.shadowSm,
),
child: Column(
children: [
// 复制推荐码
_buildActionTile(
icon: Icons.qr_code_rounded,
iconBg: AppColors.primaryContainer,
iconColor: AppColors.primary,
title: context.t('share.copyCode'),
subtitle: _info?.referralCode ?? '',
onTap: _copyCode,
),
const Divider(indent: 60, height: 1),
// 复制链接
_buildActionTile(
icon: Icons.link_rounded,
iconBg: AppColors.infoLight,
iconColor: AppColors.info,
title: context.t('share.copyLink'),
subtitle: _inviteLink,
onTap: _copyLink,
),
const Divider(indent: 60, height: 1),
// 系统原生分享
_buildActionTile(
icon: Icons.share_rounded,
iconBg: AppColors.successLight,
iconColor: AppColors.success,
title: context.t('share.shareToFriend'),
subtitle: context.t('share.shareSubtitle'),
onTap: _shareNative,
),
],
),
),
],
);
}
/// 操作列表项
///
/// - [iconBg] 图标容器背景色(使用语义色的浅色版本)
/// - [subtitle] 单行截断,防止链接/代码过长破坏布局
Widget _buildActionTile({
required IconData icon,
required Color iconBg,
required Color iconColor,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
leading: Container(
width: 38,
height: 38,
decoration: BoxDecoration(color: iconBg, borderRadius: BorderRadius.circular(10)),
child: Icon(icon, size: 20, color: iconColor),
),
title: Text(title, style: AppTypography.bodyMedium),
subtitle: Text(
subtitle,
style: AppTypography.caption.copyWith(color: AppColors.textTertiary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right_rounded, color: AppColors.textTertiary, size: 20),
onTap: onTap,
);
}
}