1183 lines
39 KiB
Dart
1183 lines
39 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 isDark = Theme.of(context).brightness == Brightness.dark;
|
||
final cardColor = isDark ? AppColors.surface : Colors.white;
|
||
|
||
return DefaultTabController(
|
||
length: 2,
|
||
child: 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);
|
||
ref.invalidate(userReferralInfoProvider);
|
||
ref.invalidate(myCircleProvider);
|
||
ref.invalidate(myPointsProvider);
|
||
},
|
||
),
|
||
],
|
||
bottom: TabBar(
|
||
indicatorColor: AppColors.primary,
|
||
labelColor: AppColors.primary,
|
||
unselectedLabelColor: Colors.grey,
|
||
tabs: [
|
||
Tab(text: l10n.referralTabTenant),
|
||
Tab(text: l10n.referralTabPersonal),
|
||
],
|
||
),
|
||
),
|
||
body: TabBarView(
|
||
children: [
|
||
_TenantReferralTab(cardColor: cardColor),
|
||
_PersonalCircleTab(cardColor: cardColor),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// Tab 1 — Tenant / B2B referral (existing)
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class _TenantReferralTab extends ConsumerWidget {
|
||
final Color cardColor;
|
||
const _TenantReferralTab({required this.cardColor});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final infoAsync = ref.watch(referralInfoProvider);
|
||
return infoAsync.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
|
||
data: (info) => ListView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
children: [
|
||
_ReferralCodeCard(info: info, cardColor: cardColor),
|
||
const SizedBox(height: 16),
|
||
_StatsRow(info: info, cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_RewardRulesCard(cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_SectionHeader(
|
||
title: l10n.referralRecordsSection,
|
||
onTap: () => Navigator.push(context,
|
||
MaterialPageRoute(builder: (_) => const _ReferralListPage())),
|
||
),
|
||
_ReferralPreviewList(cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_SectionHeader(
|
||
title: l10n.pendingRewardsSection,
|
||
onTap: () => Navigator.push(context,
|
||
MaterialPageRoute(builder: (_) => const _RewardListPage())),
|
||
),
|
||
_RewardPreviewList(cardColor: cardColor),
|
||
const SizedBox(height: 40),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// Tab 2 — Personal circle / C2C
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
class _PersonalCircleTab extends ConsumerWidget {
|
||
final Color cardColor;
|
||
const _PersonalCircleTab({required this.cardColor});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final infoAsync = ref.watch(userReferralInfoProvider);
|
||
return infoAsync.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
|
||
data: (info) => ListView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
children: [
|
||
_UserCodeCard(info: info, cardColor: cardColor),
|
||
const SizedBox(height: 16),
|
||
_PointsBalanceCard(info: info, cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_CircleRulesCard(cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_SectionHeader(
|
||
title: l10n.myCircleMembersSection,
|
||
onTap: () => Navigator.push(context,
|
||
MaterialPageRoute(builder: (_) => const _CircleListPage())),
|
||
),
|
||
_CirclePreviewList(cardColor: cardColor),
|
||
const SizedBox(height: 20),
|
||
_SectionHeader(
|
||
title: l10n.pointsHistorySection,
|
||
onTap: () => Navigator.push(context,
|
||
MaterialPageRoute(builder: (_) => const _PointsHistoryPage())),
|
||
),
|
||
_PointsPreviewList(cardColor: cardColor),
|
||
const SizedBox(height: 40),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── User Referral Code Card ───────────────────────────────────────────────────
|
||
|
||
class _UserCodeCard extends StatelessWidget {
|
||
final UserReferralInfo info;
|
||
final Color cardColor;
|
||
|
||
const _UserCodeCard({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.myPersonalInviteCode,
|
||
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
info.code,
|
||
style: const TextStyle(
|
||
fontSize: 26,
|
||
fontWeight: FontWeight.bold,
|
||
letterSpacing: 2,
|
||
color: Color(0xFF7C3AED),
|
||
),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.copy, color: Color(0xFF7C3AED)),
|
||
tooltip: l10n.copyReferralCodeTooltip,
|
||
onPressed: () => _copy(context, info.code),
|
||
),
|
||
],
|
||
),
|
||
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: const Color(0xFF7C3AED),
|
||
side: const BorderSide(color: Color(0xFF7C3AED)),
|
||
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: const Color(0xFF7C3AED),
|
||
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, UserReferralInfo info) {
|
||
final text =
|
||
'邀请你加入 IT0 智能体管理平台,用AI管理你的数字工作!加入即获 200 积分奖励!\n邀请码:${info.code}\n链接:${info.shareUrl}';
|
||
_copy(context, text);
|
||
}
|
||
}
|
||
|
||
// ── Points Balance Card ───────────────────────────────────────────────────────
|
||
|
||
class _PointsBalanceCard extends StatelessWidget {
|
||
final UserReferralInfo info;
|
||
final Color cardColor;
|
||
|
||
const _PointsBalanceCard({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: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.stars_rounded, color: Color(0xFFF59E0B), size: 20),
|
||
const SizedBox(width: 6),
|
||
Text(l10n.pointsBalanceTitle,
|
||
style:
|
||
const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
_PointsStatItem(
|
||
label: l10n.currentBalanceLabel,
|
||
value: '${info.pointsBalance}',
|
||
unit: 'pts',
|
||
color: const Color(0xFF7C3AED),
|
||
),
|
||
const SizedBox(width: 12),
|
||
_PointsStatItem(
|
||
label: l10n.circleMembersCountLabel,
|
||
value: '${info.circleSize}',
|
||
unit: '人',
|
||
color: const Color(0xFF10B981),
|
||
),
|
||
const SizedBox(width: 12),
|
||
_PointsStatItem(
|
||
label: l10n.totalEarnedLabel,
|
||
value: '${info.totalEarned}',
|
||
unit: 'pts',
|
||
color: const Color(0xFF6366F1),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PointsStatItem extends StatelessWidget {
|
||
final String label;
|
||
final String value;
|
||
final String unit;
|
||
final Color color;
|
||
|
||
const _PointsStatItem({
|
||
required this.label,
|
||
required this.value,
|
||
required this.unit,
|
||
required this.color,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Expanded(
|
||
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: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: color,
|
||
),
|
||
),
|
||
TextSpan(
|
||
text: ' $unit',
|
||
style: TextStyle(fontSize: 12, color: color.withAlpha(180)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Circle Rules Card ─────────────────────────────────────────────────────────
|
||
|
||
class _CircleRulesCard extends StatelessWidget {
|
||
final Color cardColor;
|
||
const _CircleRulesCard({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.people_alt_rounded,
|
||
color: Color(0xFF7C3AED), size: 20),
|
||
const SizedBox(width: 6),
|
||
Text(l10n.circleRewardRulesTitle,
|
||
style:
|
||
const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
_RuleItem(
|
||
icon: Icons.card_giftcard_rounded,
|
||
color: const Color(0xFF7C3AED),
|
||
text: l10n.circleRule1,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.star_rounded,
|
||
color: const Color(0xFF6366F1),
|
||
text: l10n.circleRule2,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.star_rounded,
|
||
color: const Color(0xFF7C3AED),
|
||
text: l10n.circleRule3,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.repeat_rounded,
|
||
color: const Color(0xFF10B981),
|
||
text: l10n.circleRule4,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.account_tree_rounded,
|
||
color: const Color(0xFFF59E0B),
|
||
text: l10n.circleRule5,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.redeem_rounded,
|
||
color: const Color(0xFF10B981),
|
||
text: l10n.circleRule6,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Circle Member Preview ─────────────────────────────────────────────────────
|
||
|
||
class _CirclePreviewList extends ConsumerWidget {
|
||
final Color cardColor;
|
||
const _CirclePreviewList({required this.cardColor});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final async = ref.watch(myCircleProvider);
|
||
return async.when(
|
||
loading: () => const SizedBox(
|
||
height: 60, child: Center(child: CircularProgressIndicator())),
|
||
error: (_, __) => const SizedBox.shrink(),
|
||
data: (result) {
|
||
if (result.items.isEmpty) {
|
||
return _EmptyCard(cardColor: cardColor, message: l10n.noCircleMembersMessage);
|
||
}
|
||
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((m) => _CircleMemberTile(member: m)).toList(),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CircleMemberTile extends StatelessWidget {
|
||
final CircleMember member;
|
||
const _CircleMemberTile({required this.member});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final statusColor = member.isActive
|
||
? const Color(0xFF10B981)
|
||
: member.status == 'EXPIRED'
|
||
? Colors.grey
|
||
: const Color(0xFFF59E0B);
|
||
final statusLabel = switch (member.status) {
|
||
'PENDING' => l10n.pendingPaymentStatus,
|
||
'ACTIVE' => l10n.activatedStatus,
|
||
'REWARDED' => l10n.rewardedStatus,
|
||
'EXPIRED' => l10n.expiredStatus,
|
||
_ => member.status,
|
||
};
|
||
final levelLabel = member.level == 1 ? 'L1' : 'L2';
|
||
|
||
return ListTile(
|
||
leading: CircleAvatar(
|
||
backgroundColor: const Color(0xFF7C3AED).withAlpha(20),
|
||
child: Text(
|
||
levelLabel,
|
||
style: const TextStyle(
|
||
color: Color(0xFF7C3AED),
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 12),
|
||
),
|
||
),
|
||
title: Text(
|
||
member.referredUserId.length > 8
|
||
? '${member.referredUserId.substring(0, 8)}...'
|
||
: member.referredUserId,
|
||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||
),
|
||
subtitle: Text(
|
||
'${l10n.joinedAtLabel} ${_formatDate(member.joinedAt)}',
|
||
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')}';
|
||
}
|
||
|
||
// ── Points Preview List ───────────────────────────────────────────────────────
|
||
|
||
class _PointsPreviewList extends ConsumerWidget {
|
||
final Color cardColor;
|
||
const _PointsPreviewList({required this.cardColor});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final async = ref.watch(myPointsProvider);
|
||
return async.when(
|
||
loading: () => const SizedBox(
|
||
height: 60, child: Center(child: CircularProgressIndicator())),
|
||
error: (_, __) => const SizedBox.shrink(),
|
||
data: (result) {
|
||
if (result.transactions.isEmpty) {
|
||
return _EmptyCard(cardColor: cardColor, message: l10n.noPointsHistoryMessage);
|
||
}
|
||
final preview = result.transactions.take(3).toList();
|
||
return Card(
|
||
color: cardColor,
|
||
elevation: 0,
|
||
shape:
|
||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
children:
|
||
preview.map((t) => _PointsTile(tx: t)).toList(),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PointsTile extends StatelessWidget {
|
||
final PointTransaction tx;
|
||
const _PointsTile({required this.tx});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final color =
|
||
tx.isEarned ? const Color(0xFF10B981) : const Color(0xFFEF4444);
|
||
final sign = tx.isEarned ? '+' : '';
|
||
|
||
return ListTile(
|
||
leading: CircleAvatar(
|
||
backgroundColor: color.withAlpha(20),
|
||
child: Icon(
|
||
tx.isEarned ? Icons.add_circle_outline : Icons.remove_circle_outline,
|
||
color: color,
|
||
size: 20,
|
||
),
|
||
),
|
||
title: Text(tx.typeLabel,
|
||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||
subtitle: Text(
|
||
_formatDate(tx.createdAt),
|
||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||
),
|
||
trailing: Text(
|
||
'$sign${tx.delta} pts',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.bold,
|
||
color: color),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _formatDate(DateTime dt) =>
|
||
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
||
}
|
||
|
||
// ── Full list pages (circle & points) ────────────────────────────────────────
|
||
|
||
class _CircleListPage extends ConsumerWidget {
|
||
const _CircleListPage();
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final async = ref.watch(myCircleProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text(l10n.referralTabPersonal)),
|
||
body: async.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
|
||
data: (result) => result.items.isEmpty
|
||
? Center(child: Text(l10n.noCircleMembersMessage))
|
||
: ListView.separated(
|
||
itemCount: result.items.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (_, i) =>
|
||
_CircleMemberTile(member: result.items[i]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PointsHistoryPage extends ConsumerWidget {
|
||
const _PointsHistoryPage();
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final l10n = AppLocalizations.of(context);
|
||
final async = ref.watch(myPointsProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text(l10n.pointsHistorySection)),
|
||
body: async.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
|
||
data: (result) => result.transactions.isEmpty
|
||
? Center(child: Text(l10n.noPointsHistoryMessage))
|
||
: ListView.separated(
|
||
itemCount: result.transactions.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (_, i) =>
|
||
_PointsTile(tx: result.transactions[i]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// Shared / Tenant tab widgets (unchanged logic, extracted)
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
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) {
|
||
final text =
|
||
'邀请你使用 IT0 智能体管理平台,注册即可获得积分奖励!\n推荐码:${info.referralCode}\n链接:${info.shareUrl}';
|
||
_copy(context, text);
|
||
}
|
||
}
|
||
|
||
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)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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: l10n.proReferralReward,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.star_rounded,
|
||
color: const Color(0xFF7C3AED),
|
||
text: l10n.enterpriseReferralReward,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.repeat_rounded,
|
||
color: const Color(0xFF10B981),
|
||
text: l10n.renewalBonusReward,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_RuleItem(
|
||
icon: Icons.account_balance_wallet_outlined,
|
||
color: const Color(0xFFF59E0B),
|
||
text: l10n.creditDeductionReward,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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)),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SectionHeader extends StatelessWidget {
|
||
final String title;
|
||
final VoidCallback onTap;
|
||
|
||
const _SectionHeader({required this.title, required this.onTap});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final l10n = AppLocalizations.of(context);
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(title,
|
||
style: const TextStyle(
|
||
fontWeight: FontWeight.w600, fontSize: 16)),
|
||
TextButton(onPressed: onTap, child: Text(l10n.viewAllButton)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _EmptyCard extends StatelessWidget {
|
||
final Color cardColor;
|
||
final String message;
|
||
|
||
const _EmptyCard({required this.cardColor, required this.message});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
color: cardColor,
|
||
elevation: 0,
|
||
shape:
|
||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Center(
|
||
child: Text(message,
|
||
style: const TextStyle(color: Colors.grey, fontSize: 13)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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 _EmptyCard(
|
||
cardColor: cardColor, message: l10n.noReferralsMessage);
|
||
}
|
||
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 _EmptyCard(
|
||
cardColor: cardColor, message: l10n.noPendingRewardsMessage);
|
||
}
|
||
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),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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('${l10n.loadFailed}: $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 l10n = AppLocalizations.of(context);
|
||
final async = ref.watch(allRewardsProvider);
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text(l10n.rewardHistoryPageTitle)),
|
||
body: async.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
|
||
data: (result) => result.items.isEmpty
|
||
? Center(child: Text(l10n.noRewardsHistoryMessage))
|
||
: ListView.separated(
|
||
itemCount: result.items.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (_, i) => _RewardTile(item: result.items[i]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|