Files
slogan/lib/features/profile/profile_page.dart
T
admin c4fd49bd7e feat(app): 穿搭生成 + 方案流 + 效果图 + 3D 化身查看
- outfit_provider:方案列表/生成任务轮询(pending→planning→scoring→rendering→done)/详情/选定主方案/收藏
- outfit_page:日期范围 + 地点生成表单,生成中显示任务进度,方案列表卡片
- plan_viewer_page:PageView 手指滑动切换方案,穿衣清单(衣橱/新品标记)、发型发色、设为主方案/收藏
- plan_effect_page:效果图 3 视角(正面/侧面/背面)查看
- avatar_viewer:three_dart + flutter_gl 3D 渲染实现(自动旋转),GLB 资产未接入时占位展示,kEnable3D 开关
- avatar_viewer_page:化身查看页 + 重新构建
- 新增 /plan-viewer /plan-effect /avatar-viewer 路由
2026-07-31 12:50:02 +08:00

211 lines
6.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/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});
@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),
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),
),
],
),
);
}
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)),
);
}
}