Files
slogan/lib/features/profile/photo_guide_page.dart
T
2026-08-03 10:29:31 +08:00

152 lines
4.9 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 'package:image_picker/image_picker.dart';
import '../../shared/widgets/loading_view.dart';
import 'photo_upload_provider.dart';
/// 拍照引导:3 视角全身照(正面/侧面/背面)逐一上传,用于 Tripo 构建 3D 化身
class PhotoGuidePage extends ConsumerStatefulWidget {
const PhotoGuidePage({super.key});
@override
ConsumerState<PhotoGuidePage> createState() => _PhotoGuidePageState();
}
class _PhotoGuidePageState extends ConsumerState<PhotoGuidePage> {
final _picker = ImagePicker();
int? _uploadingType;
Future<void> _pickAndUpload(int type) async {
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_camera_outlined),
title: const Text('拍照'),
onTap: () => Navigator.pop(ctx, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: const Text('从相册选择'),
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
),
],
),
),
);
if (source == null || !mounted) return;
final file = await _picker.pickImage(
source: source, maxWidth: 2048, imageQuality: 85);
if (file == null) return;
setState(() => _uploadingType = type);
try {
await ref.read(photoProvider.notifier).upload(type, file.path);
if (!mounted) return;
Fluttertoast.showToast(msg: '${PhotoType.labels[type]}上传成功');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: '上传失败:${e.toString().replaceFirst('Exception: ', '')}');
} finally {
if (mounted) setState(() => _uploadingType = null);
}
}
@override
Widget build(BuildContext context) {
final photos = ref.watch(photoProvider);
return Scaffold(
appBar: AppBar(title: const Text('拍摄形象照片')),
body: photos.when(
loading: () => const LoadingView(text: '加载照片状态...'),
error: (e, _) => Center(child: Text('加载失败:$e')),
data: (list) {
final allDone = PhotoType.all.every(
(t) => list.any((p) => p.type == t));
return ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('拍摄 3 张全身照(正面/侧面/背面),用于构建你的 3D 形象',
style: TextStyle(color: Colors.grey, fontSize: 13)),
const SizedBox(height: 12),
for (final type in PhotoType.all) ...[
_PhotoCard(
type: type,
uploaded: list.any((p) => p.type == type),
uploading: _uploadingType == type,
onTap: _uploadingType == null
? () => _pickAndUpload(type)
: null,
),
const SizedBox(height: 12),
],
const SizedBox(height: 8),
FilledButton.icon(
onPressed: allDone && _uploadingType == null
? () => context.go('/body-tune')
: null,
icon: const Icon(Icons.arrow_forward),
label: Text(allDone ? '照片已完成,设置身形' : '还需拍摄剩余照片'),
),
],
);
},
),
);
}
}
class _PhotoCard extends StatelessWidget {
final int type;
final bool uploaded;
final bool uploading;
final VoidCallback? onTap;
const _PhotoCard({
required this.type,
required this.uploaded,
required this.uploading,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
onTap: onTap,
leading: CircleAvatar(
backgroundColor:
uploaded ? Colors.green.shade100 : scheme.primaryContainer,
child: uploading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: Icon(uploaded
? Icons.check
: Icons.add_a_photo_outlined),
),
title: Text(PhotoType.labels[type]!),
subtitle: Text(PhotoType.descs[type]!),
trailing: uploaded
? const Chip(
label: Text('已上传'),
backgroundColor: Colors.green,
labelStyle: TextStyle(color: Colors.white, fontSize: 12),
)
: null,
),
);
}
}