refactor: 会员中心并入我的 tab,删除独立会员中心页

- 我的 tab 直接展示:用户卡片 + 会员卡(开通/会员码/权益)+ 免费获取
  权益(广告激励)+ 最近优惠 + 形象/衣橱入口 + 退出登录
- 删除 member_center_page.dart 与 /commercial 路由(内容全部迁移)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:38:17 +08:00
co-authored by Claude Opus 4.7
parent 01d8c72366
commit 8e38370b78
3 changed files with 413 additions and 435 deletions
-419
View File
@@ -1,419 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../core/ads/ads_service.dart';
import '../../core/config/app_config.dart';
import '../../features/cps/cps_provider.dart';
import '../../shared/widgets/loading_view.dart';
import 'member_provider.dart';
import 'pay_page.dart';
/// 会员中心:会员状态/套餐充值/广告激励 + 最近优惠
class MemberCenterPage extends ConsumerStatefulWidget {
const MemberCenterPage({super.key});
@override
ConsumerState<MemberCenterPage> createState() => _MemberCenterPageState();
}
class _MemberCenterPageState extends ConsumerState<MemberCenterPage> {
bool _rewarding = false;
// web 上无 Platformdart:io 不可用),且 web 端默认走系统浏览器收银台
bool get _isIOS =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
Future<void> _openPlans() async {
final plans = await showModalBottomSheet<MemberPlan>(
context: context,
builder: (ctx) => const _PlanSheet(),
);
if (plans == null || !mounted) return;
try {
final result = await createMemberOrder(ref, plans.id);
if (!mounted || result.payUrl.isEmpty) return;
await context.push('/pay', extra: PayArgs(orderNo: result.orderNo, payUrl: result.payUrl));
ref.read(memberProvider.notifier).refresh();
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
}
}
Future<void> _claimReward(String adType, String successMsg) async {
if (_rewarding) return;
final ads = ref.read(adsServiceProvider);
if (!ads.enabled) {
Fluttertoast.showToast(msg: '广告功能暂未开通');
return;
}
setState(() => _rewarding = true);
try {
final watched = await ads.showRewarded();
if (!watched) {
Fluttertoast.showToast(msg: '未完整观看,无法领取');
return;
}
final remaining =
await ref.read(memberProvider.notifier).claimReward(adType);
if (!mounted) return;
Fluttertoast.showToast(msg: '$successMsg(今日剩余 $remaining 次)');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
} finally {
if (mounted) setState(() => _rewarding = false);
}
}
@override
Widget build(BuildContext context) {
final member = ref.watch(memberProvider);
return Scaffold(
appBar: AppBar(title: const Text('会员中心')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_MemberCard(
member: member,
isIOS: _isIOS,
onOpenPlans: _openPlans,
),
const SizedBox(height: 12),
const _RecentDealsCard(),
if (member.value?.isVip == false) ...[
const SizedBox(height: 12),
_AdsRewardCard(
rewarding: _rewarding,
onClaim: (adType, msg) => _claimReward(adType, msg),
),
],
],
),
);
}
}
class _MemberCard extends ConsumerWidget {
final AsyncValue<MemberInfo> member;
final bool isIOS;
final VoidCallback onOpenPlans;
const _MemberCard(
{required this.member, required this.isIOS, required this.onOpenPlans});
@override
Widget build(BuildContext context, WidgetRef ref) {
final scheme = Theme.of(context).colorScheme;
return Card(
color: scheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: member.when(
loading: () => const Text('加载会员状态...',
style: TextStyle(fontSize: 13)),
error: (e, _) => Text('会员状态加载失败:$e',
style: const TextStyle(fontSize: 12)),
data: (m) {
if (m.isVip) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(Icons.workspace_premium, size: 30, color: scheme.primary),
const SizedBox(width: 10),
const Text('形象会员',
style:
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const Spacer(),
Chip(
label: Text(m.planName),
labelStyle:
TextStyle(color: scheme.primary, fontSize: 12),
visualDensity: VisualDensity.compact,
),
]),
const SizedBox(height: 6),
Text('有效期至 ${m.expireAt}',
style: TextStyle(fontSize: 12, color: scheme.primary)),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: FilledButton.tonalIcon(
onPressed: () => _showMemberCode(context, m),
icon: const Icon(Icons.qr_code_2, size: 18),
label: const Text('会员码'),
),
),
if (benefitTexts(m).isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final b in benefitTexts(m))
Chip(
label: Text(b),
labelStyle: const TextStyle(fontSize: 11),
visualDensity: VisualDensity.compact,
),
],
),
],
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(children: [
Icon(Icons.workspace_premium, size: 30),
SizedBox(width: 10),
Text('形象会员',
style:
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
]),
const SizedBox(height: 6),
const Text('会员专享:无限次效果图生成 · 优先 AI 方案 · 门店专属折扣',
style: TextStyle(fontSize: 12)),
const SizedBox(height: 10),
if (isIOS)
const Text('iOS 端暂不支持充值(App Store 政策),可观看广告获得体验会员',
style: TextStyle(fontSize: 11, color: Colors.grey))
else
FilledButton.icon(
onPressed: onOpenPlans,
icon: const Icon(Icons.payment, size: 18),
label: const Text('开通会员'),
),
],
);
},
),
),
);
}
}
/// 会员码(纯客户端展示,门店出示核销)
void _showMemberCode(BuildContext context, MemberInfo m) {
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('会员码'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Theme.of(ctx).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.qr_code_2, size: 96),
),
const SizedBox(height: 12),
Text('${m.planName} · 有效期至 ${m.expireAt}',
style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
const Text('到店出示即可享受会员价与专属折扣',
style: TextStyle(fontSize: 12)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭'),
),
],
),
);
}
/// 最近优惠卡:接口错误或空列表时整卡隐藏
class _RecentDealsCard extends ConsumerWidget {
const _RecentDealsCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final deals = ref.watch(cpsRecentProvider).value;
if (deals == null || deals.isEmpty) return const SizedBox.shrink();
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('最近优惠',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
for (final p in deals.take(5))
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: p.coverUrl.isEmpty
? const SizedBox(
width: 40,
height: 40,
child: Icon(Icons.shopping_bag_outlined,
color: Colors.grey),
)
: Image.network(
AppConfig.resolveUrl(p.coverUrl),
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => const SizedBox(
width: 40,
height: 40,
child: Icon(Icons.broken_image_outlined,
color: Colors.grey),
),
),
),
title: Text(p.name,
maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
'¥${p.priceText}${p.shopName.isNotEmpty ? ' · ${p.shopName}' : ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis),
trailing:
const Icon(Icons.chevron_right, size: 18, color: Colors.grey),
onTap: () => context.push('/cps-product-list', extra: CpsListArgs(
title: '最近优惠',
source: p.source,
categoryCode: p.categoryCode,
city: p.city,
scene: 'member_benefit',
)),
),
],
),
),
);
}
}
class _AdsRewardCard extends ConsumerWidget {
final bool rewarding;
final void Function(String adType, String msg) onClaim;
const _AdsRewardCard({required this.rewarding, required this.onClaim});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('免费获取权益',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.ondemand_video, color: Colors.deepPurple),
title: const Text('看视频 · 效果图 +1'),
subtitle: const Text('每日最多 2 次,次日重置'),
trailing: OutlinedButton(
onPressed: rewarding
? null
: () => onClaim('effect_extra', '已获得 1 次效果图'),
child: const Text('看视频'),
),
),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.ondemand_video, color: Colors.teal),
title: const Text('看视频 · 体验会员 1 天'),
subtitle: const Text('每日最多 1 次,含无限效果图'),
trailing: OutlinedButton(
onPressed: rewarding
? null
: () => onClaim('vip_trial', '已获得 1 天体验会员'),
child: const Text('看视频'),
),
),
],
),
),
);
}
}
class _PlanSheet extends ConsumerWidget {
const _PlanSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final plans = ref.watch(memberPlanProvider);
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('选择会员套餐',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
plans.when(
loading: () => const Padding(
padding: EdgeInsets.all(24), child: LoadingView()),
error: (e, _) => Text('套餐加载失败:$e',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey)),
data: (list) => list.isEmpty
? const Padding(
padding: EdgeInsets.all(24),
child: Text('暂未开放套餐', textAlign: TextAlign.center),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final p in list)
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
tileColor: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.5),
title: Text(p.name,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
subtitle: Text(
'${p.durationDays} 天 · ${p.features.map((f) => benefitLabels[f] ?? f).join(' · ')}',
style: const TextStyle(fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text(p.priceText,
style: TextStyle(
color:
Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.bold)),
onTap: () => Navigator.pop(context, p),
),
],
),
),
],
),
),
);
}
}
+413 -11
View File
@@ -1,22 +1,87 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../../core/ads/ads_service.dart';
import '../../core/auth/auth_provider.dart';
import '../../features/member/member_provider.dart';
import '../../core/config/app_config.dart';
import '../../features/cps/cps_provider.dart';
import '../../shared/widgets/loading_view.dart';
import '../member/member_provider.dart';
import '../member/pay_page.dart';
/// 我的:个人信息管理入口(形象 / 衣橱 / 会员)+ 退出登录
class MinePage extends ConsumerWidget {
/// 我的:个人信息管理(会员卡/免费权益/最近优惠 + 形象/衣橱入口 + 退出登录
class MinePage extends ConsumerStatefulWidget {
const MinePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<MinePage> createState() => _MinePageState();
}
class _MinePageState extends ConsumerState<MinePage> {
bool _rewarding = false;
// web 上无 Platformdart:io 不可用),且 web 端默认走系统浏览器收银台
bool get _isIOS =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
Future<void> _openPlans() async {
final plans = await showModalBottomSheet<MemberPlan>(
context: context,
builder: (ctx) => const _PlanSheet(),
);
if (plans == null || !mounted) return;
try {
final result = await createMemberOrder(ref, plans.id);
if (!mounted || result.payUrl.isEmpty) return;
await context.push(
'/pay', extra: PayArgs(orderNo: result.orderNo, payUrl: result.payUrl));
ref.read(memberProvider.notifier).refresh();
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
}
}
Future<void> _claimReward(String adType, String successMsg) async {
if (_rewarding) return;
final ads = ref.read(adsServiceProvider);
if (!ads.enabled) {
Fluttertoast.showToast(msg: '广告功能暂未开通');
return;
}
setState(() => _rewarding = true);
try {
final watched = await ads.showRewarded();
if (!watched) {
Fluttertoast.showToast(msg: '未完整观看,无法领取');
return;
}
final remaining =
await ref.read(memberProvider.notifier).claimReward(adType);
if (!mounted) return;
Fluttertoast.showToast(msg: '$successMsg(今日剩余 $remaining 次)');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: e.toString().replaceFirst('Exception: ', ''));
} finally {
if (mounted) setState(() => _rewarding = false);
}
}
@override
Widget build(BuildContext context) {
final member = ref.watch(memberProvider);
final auth = ref.watch(authProvider).value;
final member = ref.watch(memberProvider).value;
final scheme = Theme.of(context).colorScheme;
final rawName = auth?.name?.trim() ?? '';
final name = rawName.isEmpty ? '我的' : rawName;
return Scaffold(
appBar: AppBar(title: const Text('我的')),
body: ListView(
@@ -36,15 +101,28 @@ class MinePage extends ConsumerWidget {
style: const TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
subtitle: Text(
member?.isVip == true
? '形象会员 · ${member!.planName} · 有效期至 ${member.expireAt}'
member.value?.isVip == true
? '形象会员 · ${member.value!.planName} · 有效期至 ${member.value!.expireAt}'
: '普通用户',
style: const TextStyle(fontSize: 12),
),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () => context.push('/commercial'),
),
),
const SizedBox(height: 12),
_MemberCard(
member: member,
isIOS: _isIOS,
onOpenPlans: _openPlans,
),
const SizedBox(height: 12),
const _RecentDealsCard(),
if (member.value?.isVip == false) ...[
const SizedBox(height: 12),
_AdsRewardCard(
rewarding: _rewarding,
onClaim: (adType, msg) => _claimReward(adType, msg),
),
],
const SizedBox(height: 16),
Card(
child: Column(
@@ -53,7 +131,8 @@ class MinePage extends ConsumerWidget {
leading: const Icon(Icons.face_retouching_natural),
title: const Text('我的形象'),
subtitle: const Text('3D 化身 · 形象照片 · 身形参数'),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
trailing:
const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () => context.push('/profile'),
),
const Divider(height: 1, indent: 56),
@@ -61,7 +140,8 @@ class MinePage extends ConsumerWidget {
leading: const Icon(Icons.checkroom),
title: const Text('我的衣橱'),
subtitle: const Text('管理服装单品,AI 生成时自动搭配'),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
trailing:
const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () => context.push('/wardrobe'),
),
],
@@ -82,3 +162,325 @@ class MinePage extends ConsumerWidget {
);
}
}
class _MemberCard extends ConsumerWidget {
final AsyncValue<MemberInfo> member;
final bool isIOS;
final VoidCallback onOpenPlans;
const _MemberCard(
{required this.member, required this.isIOS, required this.onOpenPlans});
@override
Widget build(BuildContext context, WidgetRef ref) {
final scheme = Theme.of(context).colorScheme;
return Card(
color: scheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: member.when(
loading: () => const Text('加载会员状态...',
style: TextStyle(fontSize: 13)),
error: (e, _) => Text('会员状态加载失败:$e',
style: const TextStyle(fontSize: 12)),
data: (m) {
if (m.isVip) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(Icons.workspace_premium,
size: 30, color: scheme.primary),
const SizedBox(width: 10),
const Text('形象会员',
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
const Spacer(),
Chip(
label: Text(m.planName),
labelStyle:
TextStyle(color: scheme.primary, fontSize: 12),
visualDensity: VisualDensity.compact,
),
]),
const SizedBox(height: 6),
Text('有效期至 ${m.expireAt}',
style:
TextStyle(fontSize: 12, color: scheme.primary)),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: FilledButton.tonalIcon(
onPressed: () => _showMemberCode(context, m),
icon: const Icon(Icons.qr_code_2, size: 18),
label: const Text('会员码'),
),
),
if (benefitTexts(m).isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final b in benefitTexts(m))
Chip(
label: Text(b),
labelStyle: const TextStyle(fontSize: 11),
visualDensity: VisualDensity.compact,
),
],
),
],
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(children: [
Icon(Icons.workspace_premium, size: 30),
SizedBox(width: 10),
Text('形象会员',
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
]),
const SizedBox(height: 6),
const Text('会员专享:无限次效果图生成 · 优先 AI 方案 · 门店专属折扣',
style: TextStyle(fontSize: 12)),
const SizedBox(height: 10),
if (isIOS)
const Text('iOS 端暂不支持充值(App Store 政策),可观看广告获得体验会员',
style: TextStyle(fontSize: 11, color: Colors.grey))
else
FilledButton.icon(
onPressed: onOpenPlans,
icon: const Icon(Icons.payment, size: 18),
label: const Text('开通会员'),
),
],
);
},
),
),
);
}
}
/// 会员码(纯客户端展示,门店出示核销)
void _showMemberCode(BuildContext context, MemberInfo m) {
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('会员码'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Theme.of(ctx).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.qr_code_2, size: 96),
),
const SizedBox(height: 12),
Text('${m.planName} · 有效期至 ${m.expireAt}',
style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
const Text('到店出示即可享受会员价与专属折扣',
style: TextStyle(fontSize: 12)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭'),
),
],
),
);
}
/// 最近优惠卡:接口错误或空列表时整卡隐藏
class _RecentDealsCard extends ConsumerWidget {
const _RecentDealsCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final deals = ref.watch(cpsRecentProvider).value;
if (deals == null || deals.isEmpty) return const SizedBox.shrink();
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('最近优惠',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
for (final p in deals.take(5))
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: p.coverUrl.isEmpty
? const SizedBox(
width: 40,
height: 40,
child: Icon(Icons.shopping_bag_outlined,
color: Colors.grey),
)
: Image.network(
AppConfig.resolveUrl(p.coverUrl),
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => const SizedBox(
width: 40,
height: 40,
child: Icon(Icons.broken_image_outlined,
color: Colors.grey),
),
),
),
title: Text(p.name,
maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
'¥${p.priceText}${p.shopName.isNotEmpty ? ' · ${p.shopName}' : ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis),
trailing:
const Icon(Icons.chevron_right, size: 18, color: Colors.grey),
onTap: () => context.push('/cps-product-list',
extra: CpsListArgs(
title: '最近优惠',
source: p.source,
categoryCode: p.categoryCode,
city: p.city,
scene: 'member_benefit',
)),
),
],
),
),
);
}
}
class _AdsRewardCard extends ConsumerWidget {
final bool rewarding;
final void Function(String adType, String msg) onClaim;
const _AdsRewardCard({required this.rewarding, required this.onClaim});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('免费获取权益',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.ondemand_video, color: Colors.deepPurple),
title: const Text('看视频 · 效果图 +1'),
subtitle: const Text('每日最多 2 次,次日重置'),
trailing: OutlinedButton(
onPressed: rewarding
? null
: () => onClaim('effect_extra', '已获得 1 次效果图'),
child: const Text('看视频'),
),
),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.ondemand_video, color: Colors.teal),
title: const Text('看视频 · 体验会员 1 天'),
subtitle: const Text('每日最多 1 次,含无限效果图'),
trailing: OutlinedButton(
onPressed: rewarding
? null
: () => onClaim('vip_trial', '已获得 1 天体验会员'),
child: const Text('看视频'),
),
),
],
),
),
);
}
}
class _PlanSheet extends ConsumerWidget {
const _PlanSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final plans = ref.watch(memberPlanProvider);
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('选择会员套餐',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
plans.when(
loading: () => const Padding(
padding: EdgeInsets.all(24), child: LoadingView()),
error: (e, _) => Text('套餐加载失败:$e',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey)),
data: (list) => list.isEmpty
? const Padding(
padding: EdgeInsets.all(24),
child: Text('暂未开放套餐', textAlign: TextAlign.center),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final p in list)
ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
tileColor: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.5),
title: Text(p.name,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
subtitle: Text(
'${p.durationDays} 天 · ${p.features.map((f) => benefitLabels[f] ?? f).join(' · ')}',
style: const TextStyle(fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text(p.priceText,
style: TextStyle(
color:
Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.bold)),
onTap: () => Navigator.pop(context, p),
),
],
),
),
],
),
),
);
}
}
-5
View File
@@ -6,7 +6,6 @@ import 'features/auth/login_page.dart';
import 'features/cps/cps_product_list_page.dart';
import 'features/cps/cps_provider.dart';
import 'features/home/home_page.dart';
import 'features/member/member_center_page.dart';
import 'features/member/pay_page.dart';
import 'features/outfit/outfit_page.dart';
import 'features/outfit/outfit_provider.dart';
@@ -78,10 +77,6 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/outfit',
builder: (ctx, state) => const OutfitPage(),
),
GoRoute(
path: '/commercial',
builder: (ctx, state) => const MemberCenterPage(),
),
GoRoute(
path: '/pay',
builder: (context, state) => PayPage(args: state.extra! as PayArgs),