Files
slogan/lib/features/wardrobe/wardrobe_upload_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

164 lines
5.4 KiB
Dart

import 'dart:io';
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 'wardrobe_provider.dart';
/// 服装上传:选图 + 分类/季节/风格/颜色信息
class WardrobeUploadPage extends ConsumerStatefulWidget {
const WardrobeUploadPage({super.key});
@override
ConsumerState<WardrobeUploadPage> createState() => _WardrobeUploadPageState();
}
class _WardrobeUploadPageState extends ConsumerState<WardrobeUploadPage> {
final _picker = ImagePicker();
final _styleCtrl = TextEditingController();
final _colorCtrl = TextEditingController();
String? _imagePath;
String? _category;
String? _season;
bool _uploading = false;
@override
void dispose() {
_styleCtrl.dispose();
_colorCtrl.dispose();
super.dispose();
}
Future<void> _pickImage() async {
final file = await _picker.pickImage(
source: ImageSource.gallery, maxWidth: 2048, imageQuality: 85);
if (file == null) return;
setState(() => _imagePath = file.path);
}
Future<void> _submit() async {
if (_imagePath == null) {
Fluttertoast.showToast(msg: '请先选择服装照片');
return;
}
if (_category == null) {
Fluttertoast.showToast(msg: '请选择分类');
return;
}
setState(() => _uploading = true);
try {
await ref.read(wardrobeProvider.notifier).upload(
_imagePath!,
category: _category!,
season: _season ?? '',
styleTags: _styleCtrl.text.trim(),
colorInfo: _colorCtrl.text.trim(),
);
if (!mounted) return;
Fluttertoast.showToast(msg: '上传成功');
context.go('/wardrobe');
} catch (e) {
if (!mounted) return;
Fluttertoast.showToast(
msg: '上传失败:${e.toString().replaceFirst('Exception: ', '')}');
} finally {
if (mounted) setState(() => _uploading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('上传服装')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
GestureDetector(
onTap: _uploading ? null : _pickImage,
child: Container(
height: 220,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
clipBehavior: Clip.antiAlias,
child: _imagePath == null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_photo_alternate_outlined,
size: 48, color: Colors.grey.shade500),
const SizedBox(height: 8),
const Text('点击选择服装照片',
style: TextStyle(color: Colors.grey)),
],
)
: Image.file(File(_imagePath!), fit: BoxFit.cover),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
initialValue: _category,
decoration: const InputDecoration(
labelText: '分类', border: OutlineInputBorder()),
items: [
for (final c in wardrobeCategories)
DropdownMenuItem(value: c, child: Text(c)),
],
onChanged: _uploading
? null
: (v) => setState(() => _category = v),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _season,
decoration: const InputDecoration(
labelText: '适用季节(选填)', border: OutlineInputBorder()),
items: [
for (final s in wardrobeSeasons)
DropdownMenuItem(value: s, child: Text(s)),
],
onChanged:
_uploading ? null : (v) => setState(() => _season = v),
),
const SizedBox(height: 12),
TextField(
controller: _styleCtrl,
enabled: !_uploading,
decoration: const InputDecoration(
labelText: '风格标签(选填,如:通勤、休闲)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _colorCtrl,
enabled: !_uploading,
decoration: const InputDecoration(
labelText: '颜色描述(选填,如:黑色)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _uploading ? null : _submit,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14)),
child: _uploading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('上传到衣橱'),
),
],
),
);
}
}