228 lines
6.7 KiB
Dart
228 lines
6.7 KiB
Dart
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_common/utils/log_utils.dart';
|
||
import 'package:flutter_common/utils/toast_util.dart';
|
||
import 'package:flutter_common/widget/dialog_widget.dart';
|
||
import 'package:flutter_common/widget/loading_widget.dart';
|
||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||
import 'package:food_hub_app/config/app_config.dart';
|
||
import 'package:food_hub_app/provider/food_provider.dart';
|
||
import 'package:food_hub_app/utils/minio_utils.dart';
|
||
import 'package:food_hub_app/widgets/common/form.dart';
|
||
import 'package:food_hub_app/widgets/common/image.dart';
|
||
import 'package:provider/provider.dart';
|
||
|
||
class MomentForm extends StatefulWidget {
|
||
const MomentForm({super.key});
|
||
|
||
@override
|
||
State<MomentForm> createState() => _MomentFormState();
|
||
}
|
||
|
||
class _MomentFormState extends State<MomentForm> {
|
||
final _formKey = GlobalKey<FormBuilderState>();
|
||
final _focusNode = FocusNode();
|
||
static const String _contentField = 'content';
|
||
static const int maxContentLength = 200;
|
||
static const int maxImageCount = 9; // 最大图片数量
|
||
|
||
Widget _buildContentField(FoodProvider provider) {
|
||
final colors = Theme.of(context).colorScheme;
|
||
|
||
return FormBuilderTextField(
|
||
name: _contentField,
|
||
initialValue: provider.momentFormItem.content,
|
||
focusNode: _focusNode,
|
||
maxLines: 8,
|
||
minLines: 5,
|
||
maxLength: maxContentLength,
|
||
buildCounter: (
|
||
context, {
|
||
required currentLength,
|
||
required isFocused,
|
||
maxLength,
|
||
}) {
|
||
return Text(
|
||
'$currentLength/$maxContentLength',
|
||
style: TextStyle(
|
||
color: currentLength > maxLength! ? Colors.red : colors.primary,
|
||
),
|
||
);
|
||
},
|
||
onChanged: (value) {
|
||
provider.updateMomentFormItem(content: value);
|
||
},
|
||
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
|
||
validator: (value) {
|
||
if (value == null || value.isEmpty) {
|
||
return '请输入朋友圈内容';
|
||
}
|
||
if (value.length > maxContentLength) {
|
||
return '内容不能超过$maxContentLength字';
|
||
}
|
||
return null;
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildImageField(FoodProvider provider) {
|
||
final imageUrls = provider.momentFormItem.imageList;
|
||
final totalItems =
|
||
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
|
||
|
||
return GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
crossAxisSpacing: 8,
|
||
mainAxisSpacing: 8,
|
||
childAspectRatio: 4 / 3,
|
||
),
|
||
itemCount: totalItems,
|
||
itemBuilder: (context, index) {
|
||
if (index < imageUrls.length) {
|
||
return ImagePreview(
|
||
imageUrls: imageUrls,
|
||
index: index,
|
||
onRemoveImage: () => _removeImage(provider, index),
|
||
);
|
||
}
|
||
// 上传按钮(最后一个)
|
||
else {
|
||
return buildImageUploadButton(
|
||
context: context,
|
||
onTap: () => _pickImages(provider),
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildFormBuilder(FoodProvider provider) {
|
||
return FormBuilder(
|
||
key: _formKey,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
buildFormLabel(context: context, text: '朋友圈内容', isRequired: true),
|
||
const SizedBox(height: 8),
|
||
_buildContentField(provider),
|
||
const SizedBox(height: 8),
|
||
|
||
buildFormLabel(
|
||
context: context,
|
||
text: '上传图片(最多9张)',
|
||
isRequired: false,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_buildImageField(provider),
|
||
const SizedBox(height: 8),
|
||
|
||
buildFormButtonGroup(
|
||
context: context,
|
||
isShowDelete: provider.isEditing,
|
||
onConfirm: () => _submitForm(provider),
|
||
onDelete: () {},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 选择多张图片
|
||
Future<void> _pickImages(FoodProvider provider) async {
|
||
final currentCount = provider.momentFormItem.imageList.length;
|
||
final remainingCount = maxImageCount - currentCount;
|
||
|
||
if (remainingCount <= 0) {
|
||
ToastUtil.error('最多只能上传$maxImageCount张图片');
|
||
return;
|
||
}
|
||
|
||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||
type: FileType.custom,
|
||
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||
allowMultiple: true,
|
||
);
|
||
|
||
if (result != null) {
|
||
// 限制选择数量
|
||
final filesToUpload = result.files.take(remainingCount).toList();
|
||
LoadingDialog.show(context, message: '上传中');
|
||
|
||
try {
|
||
final newImageUrls = <String>[];
|
||
// 逐个上传图片
|
||
for (final file in filesToUpload) {
|
||
final fileName = await MinIOHelper().uploadFile(
|
||
bucketName: AppConfig.bucketName,
|
||
file: file,
|
||
);
|
||
logger.i('图片名称:$fileName');
|
||
newImageUrls.add(fileName);
|
||
}
|
||
|
||
// 更新图片列表
|
||
provider.addMomentFormImage(newImageUrls);
|
||
} catch (e) {
|
||
ToastUtil.error(e.toString());
|
||
} finally {
|
||
LoadingDialog.hide(context);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 移除单张图片
|
||
void _removeImage(FoodProvider provider, int index) {
|
||
provider.removeMomentFormImage(index);
|
||
}
|
||
|
||
/// 提交表单
|
||
void _submitForm(FoodProvider provider) async {
|
||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||
LoadingDialog.show(context, message: '提交中');
|
||
final result = await provider.handleMoment();
|
||
if (result == true) {
|
||
await provider.refreshMomentList();
|
||
LoadingDialog.hide(context);
|
||
|
||
if (provider.isEditing) {
|
||
showSuccessTip(context, '更新朋友圈成功');
|
||
} else {
|
||
showSuccessTip(context, '新增朋友圈成功');
|
||
}
|
||
|
||
Future.delayed(Duration(milliseconds: 1500), () {
|
||
if (mounted) {
|
||
Navigator.pop(context);
|
||
}
|
||
});
|
||
} else {
|
||
LoadingDialog.hide(context);
|
||
if (provider.isEditing) {
|
||
showErrorTip(context, '更新朋友圈失败');
|
||
} else {
|
||
showErrorTip(context, '新增朋友圈失败');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final provider = context.watch<FoodProvider>();
|
||
|
||
return AnimatedPadding(
|
||
duration: const Duration(milliseconds: 200),
|
||
padding: MediaQuery.of(context).viewInsets,
|
||
child: SingleChildScrollView(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(16),
|
||
child: _buildFormBuilder(provider),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|