Files
slogan/lib/features/profile/profile_page.dart
T
admin ceef7bc64e feat(app): 形象照片上传 + 身形设置 + 化身构建 + 衣橱管理
- photo_upload_provider/photo_guide_page:4 类照片(大头照/正面/侧面/背面)拍照或相册上传
- body_provider/body_tune_page:身高 145-200cm / 体重 / 肤色 1-5 滑杆设置
- avatar_provider:化身构建(同步 + 异步轮询兜底)
- profile_page:形象主页聚合化身/照片/身形状态 + 登出
- wardrobe_provider/wardrobe_page/wardrobe_upload_page:网格展示 + 分类筛选 + 长按删除 + 表单上传
- AppConfig.resolveUrl 相对路径拼接;新增 /photo-guide /body-tune /wardrobe-upload 路由
2026-07-31 12:44:10 +08:00

209 lines
6.1 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: () 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)),
);
}
}