- 主框架收敛为「穿搭」「我的」两个 tab:
- 穿搭 = 造型方案制作向导:个人信息完整性检查(4 类照片 +
身形参数,缺失引导去完善)+ 任务参数(日期范围/地点)+ 已有方案
- 我的 = 个人信息管理入口:用户卡片(昵称/会员状态)+ 我的形象 /
我的衣橱 / 会员中心入口 + 退出登录
- profile_page 移除退出登录(归「我的」页)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
200 lines
5.8 KiB
Dart
200 lines
5.8 KiB
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 'avatar_provider.dart';
|
||
import 'body_provider.dart';
|
||
import 'photo_upload_provider.dart';
|
||
|
||
/// 我的形象:化身 + 照片 + 身形(退出登录在「我的」页)
|
||
class ProfilePage extends ConsumerWidget {
|
||
const ProfilePage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final avatar = ref.watch(avatarProvider);
|
||
final photos = ref.watch(photoProvider);
|
||
final body = ref.watch(bodyProvider);
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('我的形象')),
|
||
body: ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
_buildAvatarCard(context, ref, avatar),
|
||
const SizedBox(height: 12),
|
||
_buildPhotoCard(context, ref, photos),
|
||
const SizedBox(height: 12),
|
||
_buildBodyCard(context, ref, body),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAvatarCard(BuildContext context, WidgetRef ref,
|
||
AsyncValue<AvatarState> avatar) {
|
||
return _SectionCard(
|
||
title: '3D 化身',
|
||
icon: Icons.face_retouching_natural,
|
||
onActionText: null,
|
||
child: avatar.when(
|
||
loading: () => const Padding(
|
||
padding: EdgeInsets.all(16),
|
||
child: Center(
|
||
child: SizedBox(
|
||
width: 24,
|
||
height: 24,
|
||
child: CircularProgressIndicator(strokeWidth: 2))),
|
||
),
|
||
error: (e, _) => _ActionRow(
|
||
text: '构建失败:${e.toString().replaceFirst('Exception: ', '')}',
|
||
actionText: '重试',
|
||
onAction: () => ref.read(avatarProvider.notifier).buildAvatar(),
|
||
),
|
||
data: (a) => _ActionRow(
|
||
text: a.built
|
||
? '已构建(模板 ${a.faceTemplateId}/${a.bodyTemplateId})'
|
||
: '尚未构建化身,需要先完成照片与身形',
|
||
actionText: a.built ? '查看' : '立即构建',
|
||
onAction: a.built
|
||
? () => context.go('/avatar-viewer')
|
||
: () async {
|
||
await ref.read(avatarProvider.notifier).buildAvatar();
|
||
if (context.mounted) {
|
||
Fluttertoast.showToast(msg: '化身构建完成');
|
||
}
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPhotoCard(
|
||
BuildContext context, WidgetRef ref, AsyncValue<List<UserPhotoInfo>> photos) {
|
||
return _SectionCard(
|
||
title: '形象照片',
|
||
icon: Icons.photo_camera_outlined,
|
||
onActionText: '去拍摄',
|
||
onAction: () => context.go('/photo-guide'),
|
||
child: photos.when(
|
||
loading: () => const _InfoText('加载中...'),
|
||
error: (e, _) => _InfoText('加载失败:$e'),
|
||
data: (list) => Column(
|
||
children: [
|
||
for (final type in PhotoType.all)
|
||
_InfoText(
|
||
'${PhotoType.labels[type]}:'
|
||
'${list.any((p) => p.type == type) ? '已上传' : '未上传'}'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildBodyCard(
|
||
BuildContext context, WidgetRef ref, AsyncValue<BodyState> body) {
|
||
return _SectionCard(
|
||
title: '身形参数',
|
||
icon: Icons.accessibility_new,
|
||
onActionText: '去设置',
|
||
onAction: () => context.go('/body-tune'),
|
||
child: body.when(
|
||
loading: () => const _InfoText('加载中...'),
|
||
error: (e, _) => _InfoText('加载失败:$e'),
|
||
data: (b) => _InfoText(
|
||
'身高 ${b.height}cm · 体重 ${b.weight}kg · 肤色 ${b.skinTone}'),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SectionCard extends StatelessWidget {
|
||
final String title;
|
||
final IconData icon;
|
||
final Widget child;
|
||
final String? onActionText;
|
||
final VoidCallback? onAction;
|
||
|
||
const _SectionCard({
|
||
required this.title,
|
||
required this.icon,
|
||
required this.child,
|
||
this.onActionText,
|
||
this.onAction,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(icon,
|
||
size: 20, color: Theme.of(context).colorScheme.primary),
|
||
const SizedBox(width: 8),
|
||
Text(title,
|
||
style: const TextStyle(
|
||
fontSize: 16, fontWeight: FontWeight.bold)),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
child,
|
||
if (onActionText != null) ...[
|
||
const SizedBox(height: 12),
|
||
Align(
|
||
alignment: Alignment.centerRight,
|
||
child: TextButton(
|
||
onPressed: onAction,
|
||
child: Text(onActionText!),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ActionRow extends StatelessWidget {
|
||
final String text;
|
||
final String actionText;
|
||
final VoidCallback? onAction;
|
||
|
||
const _ActionRow({
|
||
required this.text,
|
||
required this.actionText,
|
||
required this.onAction,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13))),
|
||
if (onAction != null)
|
||
TextButton(onPressed: onAction, child: Text(actionText)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _InfoText extends StatelessWidget {
|
||
final String text;
|
||
|
||
const _InfoText(this.text);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||
child: Text(text, style: const TextStyle(fontSize: 13)),
|
||
);
|
||
}
|
||
}
|