refactor: 前端改 2 Tab(穿搭向导 + 我的入口)
- 主框架收敛为「穿搭」「我的」两个 tab:
- 穿搭 = 造型方案制作向导:个人信息完整性检查(4 类照片 +
身形参数,缺失引导去完善)+ 任务参数(日期范围/地点)+ 已有方案
- 我的 = 个人信息管理入口:用户卡片(昵称/会员状态)+ 我的形象 /
我的衣橱 / 会员中心入口 + 退出登录
- profile_page 移除退出登录(归「我的」页)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/ads/ads_service.dart';
|
||||
import '../member/member_center_page.dart';
|
||||
import '../mine/mine_page.dart';
|
||||
import '../outfit/outfit_page.dart';
|
||||
import '../profile/profile_page.dart';
|
||||
import '../wardrobe/wardrobe_page.dart';
|
||||
|
||||
/// 主框架:4 Tab(我的形象 / 我的衣橱 / 穿搭方案 / 门店电商)
|
||||
/// 主框架:2 Tab(穿搭 = 造型方案制作向导 / 我的 = 个人信息管理入口)
|
||||
class HomePage extends ConsumerStatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@@ -45,32 +43,22 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
body: IndexedStack(
|
||||
index: _index,
|
||||
children: const [
|
||||
ProfilePage(),
|
||||
WardrobePage(),
|
||||
OutfitPage(),
|
||||
MemberCenterPage(),
|
||||
MinePage(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _index,
|
||||
onDestinationSelected: (i) => setState(() => _index = i),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.face_outlined),
|
||||
selectedIcon: Icon(Icons.face),
|
||||
label: '形象'),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.checkroom_outlined),
|
||||
selectedIcon: Icon(Icons.checkroom),
|
||||
label: '衣橱'),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.auto_awesome_outlined),
|
||||
selectedIcon: Icon(Icons.auto_awesome),
|
||||
label: '穿搭'),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.store_outlined),
|
||||
selectedIcon: Icon(Icons.store),
|
||||
label: '会员'),
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: '我的'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/auth/auth_provider.dart';
|
||||
import '../../features/member/member_provider.dart';
|
||||
|
||||
/// 我的:个人信息管理入口(形象 / 衣橱 / 会员)+ 退出登录
|
||||
class MinePage extends ConsumerWidget {
|
||||
const MinePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
color: scheme.primaryContainer.withValues(alpha: 0.4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: scheme.primary,
|
||||
child: Text(
|
||||
name.characters.first,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
title: Text(name,
|
||||
style: const TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(
|
||||
member?.isVip == true
|
||||
? '形象会员 · ${member!.planName} · 有效期至 ${member.expireAt}'
|
||||
: '普通用户',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
onTap: () => context.push('/commercial'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.face_retouching_natural),
|
||||
title: const Text('我的形象'),
|
||||
subtitle: const Text('3D 化身 · 形象照片 · 身形参数'),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
onTap: () => context.push('/profile'),
|
||||
),
|
||||
const Divider(height: 1, indent: 56),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.checkroom),
|
||||
title: const Text('我的衣橱'),
|
||||
subtitle: const Text('管理服装单品,AI 生成时自动搭配'),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
onTap: () => context.push('/wardrobe'),
|
||||
),
|
||||
const Divider(height: 1, indent: 56),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.workspace_premium),
|
||||
title: const Text('会员中心'),
|
||||
subtitle: const Text('会员权益 · 套餐充值 · 合作门店 · 最近优惠'),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||
onTap: () => context.push('/commercial'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
if (context.mounted) context.go('/login');
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('退出登录'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../shared/widgets/empty_view.dart';
|
||||
import '../../shared/widgets/error_view.dart';
|
||||
import '../../shared/widgets/loading_view.dart';
|
||||
import '../profile/body_provider.dart';
|
||||
import '../profile/photo_upload_provider.dart';
|
||||
import 'outfit_provider.dart';
|
||||
|
||||
/// 穿搭:生成表单 + 方案列表
|
||||
/// 穿搭:造型方案制作向导
|
||||
/// 输入 = 个人信息(照片/身形)+ 任务参数(日期范围/地点)→ AI 生成造型方案
|
||||
class OutfitPage extends ConsumerStatefulWidget {
|
||||
const OutfitPage({super.key});
|
||||
|
||||
@@ -76,10 +79,17 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final plans = ref.watch(outfitPlanProvider);
|
||||
final gen = ref.watch(generateProvider);
|
||||
final photos = ref.watch(photoProvider);
|
||||
final body = ref.watch(bodyProvider);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
final uploadedTypes = photos.value?.map((p) => p.type).toSet() ?? <int>{};
|
||||
final bodyReady = body.value != null && body.value!.height > 0;
|
||||
final photoCount = PhotoType.all.where(uploadedTypes.contains).length;
|
||||
final doneCount = photoCount + (bodyReady ? 1 : 0);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('穿搭方案')),
|
||||
appBar: AppBar(title: const Text('造型穿搭')),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => ref.read(outfitPlanProvider.notifier).refresh(),
|
||||
child: ListView(
|
||||
@@ -91,7 +101,77 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('生成穿搭方案',
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.face_retouching_natural,
|
||||
color: scheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text('个人信息(AI 生成依据)',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: scheme.primary)),
|
||||
),
|
||||
Chip(
|
||||
label: Text('$doneCount/5'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (final type in PhotoType.all)
|
||||
_StatusRow(
|
||||
ok: uploadedTypes.contains(type),
|
||||
text: '${PhotoType.labels[type]}照片',
|
||||
trailingText:
|
||||
uploadedTypes.contains(type) ? '已上传' : '未上传',
|
||||
actionText:
|
||||
uploadedTypes.contains(type) ? null : '去完善',
|
||||
onAction: () => context.push('/photo-guide'),
|
||||
),
|
||||
_StatusRow(
|
||||
ok: bodyReady,
|
||||
text: '身形参数',
|
||||
trailingText: bodyReady
|
||||
? '${body.value!.height}cm · ${body.value!.weight}kg · 肤色${body.value!.skinTone}'
|
||||
: '未设置',
|
||||
actionText: bodyReady ? '调整' : '去设置',
|
||||
onAction: () => context.push('/body-tune'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (doneCount == 5)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.check_circle,
|
||||
size: 16, color: Colors.green),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'个人信息齐全,生成的方案将更贴合你的身材与气质',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'完善个人信息后,AI 生成的造型方案将更贴合你的身材与气质',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('任务参数',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -137,7 +217,7 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
|
||||
onPressed: _submitting ? null : _generate,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14)),
|
||||
child: const Text('生成'),
|
||||
child: const Text('生成穿搭方案'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -183,6 +263,52 @@ class _OutfitPageState extends ConsumerState<OutfitPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusRow extends StatelessWidget {
|
||||
final bool ok;
|
||||
final String text;
|
||||
final String trailingText;
|
||||
final String? actionText;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const _StatusRow({
|
||||
required this.ok,
|
||||
required this.text,
|
||||
required this.trailingText,
|
||||
this.actionText,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
ok ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||
size: 16,
|
||||
color: ok ? Colors.green : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text, style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
Text(trailingText,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
if (actionText != null)
|
||||
TextButton(
|
||||
onPressed: onAction,
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
child: Text(actionText!),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanCard extends ConsumerWidget {
|
||||
final OutfitPlanInfo plan;
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/auth/auth_provider.dart';
|
||||
import 'avatar_provider.dart';
|
||||
import 'body_provider.dart';
|
||||
import 'photo_upload_provider.dart';
|
||||
|
||||
/// 我的形象:化身 + 照片 + 身形 + 登出
|
||||
/// 我的形象:化身 + 照片 + 身形(退出登录在「我的」页)
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@@ -28,16 +27,6 @@ class ProfilePage extends ConsumerWidget {
|
||||
_buildPhotoCard(context, ref, photos),
|
||||
const SizedBox(height: 12),
|
||||
_buildBodyCard(context, ref, body),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
if (context.mounted) context.go('/login');
|
||||
},
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('退出登录'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user