diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart index 9f16a6f..a7edeea 100644 --- a/lib/features/home/home_page.dart +++ b/lib/features/home/home_page.dart @@ -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 { 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: '我的'), ], ), ); diff --git a/lib/features/mine/mine_page.dart b/lib/features/mine/mine_page.dart new file mode 100644 index 0000000..0dc2c0c --- /dev/null +++ b/lib/features/mine/mine_page.dart @@ -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), + ), + ], + ), + ); + } +} diff --git a/lib/features/outfit/outfit_page.dart b/lib/features/outfit/outfit_page.dart index c9de778..9df63ca 100644 --- a/lib/features/outfit/outfit_page.dart +++ b/lib/features/outfit/outfit_page.dart @@ -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 { 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() ?? {}; + 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 { 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 { 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 { } } +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; diff --git a/lib/features/profile/profile_page.dart b/lib/features/profile/profile_page.dart index e97addb..7c5e3a7 100644 --- a/lib/features/profile/profile_page.dart +++ b/lib/features/profile/profile_page.dart @@ -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), - ), ], ), );