593 lines
23 KiB
Dart
593 lines
23 KiB
Dart
// ============================================================
|
||
// 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. 检测 UA(iOS / 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 Sheet(AirDrop、微信、邮件、短信……)
|
||
/// 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" 大字(字间距 3,FontWeight.w800)
|
||
// ├── Container[白色背景] → QrImageView(180×180,紫色点阵)
|
||
// └── 推荐码胶囊(半透明白底 + 复制按钮)
|
||
//
|
||
// QR 码内容:_inviteLink(如 https://app.gogenex.com/download?ref=GNXAB2C3)
|
||
// QR 码颜色:eye=深紫 #4834D4,module=主紫 #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,
|
||
);
|
||
}
|
||
}
|