it0/it0_app/lib/features/referral/presentation/screens/referral_screen.dart

643 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../providers/referral_providers.dart';
import '../../domain/models/referral_info.dart';
class ReferralScreen extends ConsumerWidget {
const ReferralScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final infoAsync = ref.watch(referralInfoProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
final cardColor = isDark ? AppColors.surface : Colors.white;
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: Text(l10n.referralScreenTitle),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.invalidate(referralInfoProvider);
ref.invalidate(referralListProvider);
ref.invalidate(pendingRewardsProvider);
},
),
],
),
body: infoAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
data: (info) => _ReferralBody(info: info, cardColor: cardColor),
),
);
}
}
class _ReferralBody extends ConsumerWidget {
final ReferralInfo info;
final Color cardColor;
const _ReferralBody({required this.info, required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
// ── Referral Code Card ─────────────────────────────────────
_ReferralCodeCard(info: info, cardColor: cardColor),
const SizedBox(height: 16),
// ── Stats Row ─────────────────────────────────────────────
_StatsRow(info: info, cardColor: cardColor),
const SizedBox(height: 20),
// ── Reward Rules ──────────────────────────────────────────
_RewardRulesCard(cardColor: cardColor),
const SizedBox(height: 20),
// ── Referral List ─────────────────────────────────────────
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.referralRecordsSection,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
TextButton(
onPressed: () => _showReferralList(context),
child: Text(l10n.viewAllReferralsLink),
),
],
),
_ReferralPreviewList(cardColor: cardColor),
const SizedBox(height: 20),
// ── Reward List ───────────────────────────────────────────
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.pendingRewardsSection,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
TextButton(
onPressed: () => _showRewardList(context),
child: Text(l10n.viewAllRewardsLink),
),
],
),
_RewardPreviewList(cardColor: cardColor),
const SizedBox(height: 40),
],
);
}
void _showReferralList(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _ReferralListPage()),
);
}
void _showRewardList(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const _RewardListPage()),
);
}
}
// ── Referral Code Card ────────────────────────────────────────────────────────
class _ReferralCodeCard extends StatelessWidget {
final ReferralInfo info;
final Color cardColor;
const _ReferralCodeCard({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.yourReferralCodeLabel,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
info.referralCode,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 2,
color: AppColors.primary,
),
),
),
IconButton(
icon: const Icon(Icons.copy, color: AppColors.primary),
tooltip: l10n.copyReferralCodeTooltip,
onPressed: () => _copy(context, info.referralCode),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.link, size: 18),
label: Text(l10n.copyInviteLinkButton),
onPressed: () => _copy(context, info.shareUrl),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.share, size: 18),
label: Text(l10n.shareButton),
onPressed: () => _share(context, info),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
],
),
],
),
),
);
}
void _copy(BuildContext context, String text) {
final l10n = AppLocalizations.of(context);
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.copiedToClipboard), duration: const Duration(seconds: 2)),
);
}
void _share(BuildContext context, ReferralInfo info) {
// For a full implementation, use share_plus package
// For now, copy the share text
final text = '邀请你使用 IT0 智能运维平台,注册即可获得积分奖励!\n推荐码:${info.referralCode}\n链接:${info.shareUrl}';
_copy(context, text);
}
}
// ── Stats Row ─────────────────────────────────────────────────────────────────
class _StatsRow extends StatelessWidget {
final ReferralInfo info;
final Color cardColor;
const _StatsRow({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
children: [
_StatCard(
cardColor: cardColor,
label: l10n.referredLabel,
value: '${info.directCount}',
unit: l10n.peopleUnit,
color: const Color(0xFF6366F1),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: l10n.activatedLabel,
value: '${info.activeCount}',
unit: l10n.peopleUnit,
color: const Color(0xFF10B981),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: l10n.pendingCreditsLabel,
value: info.pendingCreditFormatted,
unit: '',
color: const Color(0xFFF59E0B),
),
],
);
}
}
class _StatCard extends StatelessWidget {
final Color cardColor;
final String label;
final String value;
final String unit;
final Color color;
const _StatCard({
required this.cardColor,
required this.label,
required this.value,
required this.unit,
required this.color,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
RichText(
text: TextSpan(
children: [
TextSpan(
text: value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: color,
),
),
if (unit.isNotEmpty)
TextSpan(
text: unit,
style: TextStyle(
fontSize: 13, color: color.withAlpha(180)),
),
],
),
),
],
),
),
),
);
}
}
// ── Reward Rules Card ─────────────────────────────────────────────────────────
class _RewardRulesCard extends StatelessWidget {
final Color cardColor;
const _RewardRulesCard({required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.card_giftcard, color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 6),
Text(
l10n.rewardRulesTitle,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
],
),
const SizedBox(height: 12),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF6366F1),
text: '推荐 Pro 套餐:你获得 \$15 积分,对方获得 \$5 积分',
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF7C3AED),
text: '推荐 Enterprise 套餐:你获得 \$50 积分,对方获得 \$20 积分',
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.repeat_rounded,
color: const Color(0xFF10B981),
text: '对方续订后,你持续获得每月付款额 10% 的积分,最长 12 个月',
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.account_balance_wallet_outlined,
color: const Color(0xFFF59E0B),
text: '积分自动抵扣你的下期账单',
),
],
),
),
);
}
}
class _RuleItem extends StatelessWidget {
final IconData icon;
final Color color;
final String text;
const _RuleItem({required this.icon, required this.color, required this.text});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 13, height: 1.4)),
),
],
);
}
}
// ── Preview Lists ─────────────────────────────────────────────────────────────
class _ReferralPreviewList extends ConsumerWidget {
final Color cardColor;
const _ReferralPreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return async.when(
loading: () => const SizedBox(
height: 60,
child: Center(child: CircularProgressIndicator()),
),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.items.isEmpty) {
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
l10n.noReferralsMessage,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
),
),
);
}
final preview = result.items.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children: preview.map((item) => _ReferralTile(item: item)).toList(),
),
);
},
);
}
}
class _ReferralTile extends StatelessWidget {
final ReferralItem item;
const _ReferralTile({required this.item});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final statusColor = item.isActive
? const Color(0xFF10B981)
: item.status == 'EXPIRED'
? Colors.grey
: const Color(0xFFF59E0B);
final statusLabel = switch (item.status) {
'PENDING' => l10n.pendingPaymentStatus,
'ACTIVE' => l10n.activeStatus,
'REWARDED' => l10n.rewardedStatus,
'EXPIRED' => l10n.expiredStatus,
_ => item.status,
};
return ListTile(
leading: CircleAvatar(
backgroundColor: statusColor.withAlpha(30),
child: Icon(
item.isActive ? Icons.check_circle : Icons.pending,
color: statusColor,
size: 20,
),
),
title: Text(
item.referredTenantId.length > 8
? '${item.referredTenantId.substring(0, 8)}...'
: item.referredTenantId,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
subtitle: Text(
'${l10n.registeredAt} ${_formatDate(item.registeredAt)}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusLabel,
style: TextStyle(
fontSize: 12, color: statusColor, fontWeight: FontWeight.w500),
),
),
);
}
String _formatDate(DateTime dt) =>
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
class _RewardPreviewList extends ConsumerWidget {
final Color cardColor;
const _RewardPreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(pendingRewardsProvider);
return async.when(
loading: () => const SizedBox(
height: 60, child: Center(child: CircularProgressIndicator())),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.items.isEmpty) {
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
l10n.noPendingRewardsMessage,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
),
),
);
}
final preview = result.items.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children: preview
.map((item) => _RewardTile(item: item))
.toList(),
),
);
},
);
}
}
class _RewardTile extends StatelessWidget {
final RewardItem item;
const _RewardTile({required this.item});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFF59E0B).withAlpha(30),
child: const Icon(Icons.attach_money,
color: Color(0xFFF59E0B), size: 20),
),
title: Text(
item.amountFormatted,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF10B981)),
),
subtitle: Text(item.triggerLabel,
style: const TextStyle(fontSize: 12)),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: Text(
l10n.pendingDeductionStatus,
style: const TextStyle(
fontSize: 12,
color: Color(0xFFF59E0B),
fontWeight: FontWeight.w500),
),
),
);
}
}
// ── Full list pages ───────────────────────────────────────────────────────────
class _ReferralListPage extends ConsumerWidget {
const _ReferralListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.referralRecordsSection)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
data: (result) => result.items.isEmpty
? Center(child: Text(l10n.noReferralsMessage))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _ReferralTile(item: result.items[i]),
),
),
);
}
}
class _RewardListPage extends ConsumerWidget {
const _RewardListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final async = ref.watch(allRewardsProvider);
return Scaffold(
appBar: AppBar(title: const Text('奖励历史')),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败: $e')),
data: (result) => result.items.isEmpty
? const Center(child: Text('暂无奖励记录'))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _RewardTile(item: result.items[i]),
),
),
);
}
}