feat:更新美食记录表单

This commit is contained in:
2025-11-23 00:02:41 +08:00
parent 1ac504f3d7
commit 6308169013
41 changed files with 465 additions and 1618 deletions

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/apis/session.dart';
import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
@@ -69,7 +70,7 @@ class _LoginPage extends State<LoginPage> {
if ((_formKey.currentState as FormState).validate()) {
(_formKey.currentState as FormState).save();
Session session = await loginApi(_username, _password);
showSuccessToast('登录成功');
ToastUtil.success('登录成功');
handleRememberState();
handleTokenState(session.saToken.tokenValue);

View File

@@ -1,8 +1,8 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/easy_refresh.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/moment/card.dart';
import 'package:provider/provider.dart';
@@ -108,7 +108,7 @@ class _MomentPageState extends State<MomentPage> {
body: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator(context)
buildLoadingIndicator()
else
_buildMomentList(provider),
],

View File

@@ -1,11 +1,14 @@
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/minio_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/common_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:food_hub_app/widgets/common/index.dart';
import 'package:provider/provider.dart';
class MomentFormPage extends StatefulWidget {
@@ -22,7 +25,9 @@ class _MomentFormPageState extends State<MomentFormPage> {
static const int maxContentLength = 200;
static const int maxImageCount = 9; // 最大图片数量
Widget _buildContentField() {
Widget _buildContentField(FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderTextField(
name: _contentField,
focusNode: _focusNode,
@@ -38,16 +43,13 @@ class _MomentFormPageState extends State<MomentFormPage> {
return Text(
'$currentLength/$maxContentLength',
style: TextStyle(
color:
currentLength > maxLength!
? Colors.red
: Theme.of(context).colorScheme.primary,
color: currentLength > maxLength! ? Colors.red : colors.primary,
),
);
},
onChanged: (value) {
setState(() {
// provider.momentFormItem.content = value!;
provider.momentFormItem.content = value!;
});
},
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
@@ -56,39 +58,41 @@ class _MomentFormPageState extends State<MomentFormPage> {
return '请输入朋友圈内容';
}
if (value.length > maxContentLength) {
// 修正为使用 maxContentLength
return '内容不能超过${maxContentLength}';
return '内容不能超过$maxContentLength';
}
return null;
},
);
}
Widget _buildImageGrid(FoodProvider provider) {
Widget _buildImageField(FoodProvider provider) {
final imageUrls = provider.momentFormItem.imageList;
final totalItems =
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
...imageUrls.asMap().entries.map((entry) {
final index = entry.key;
final imageUrl = entry.value;
return Stack(
children: [
buildImagePreviewItem(
context: context,
imageUrls: imageUrls,
index: index,
onRemoveImage: () => _removeImage(provider, index),
)
],
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: totalItems,
itemBuilder: (context, index) {
if (index < imageUrls.length) {
return ImagePreview(
imageUrls: imageUrls,
index: index,
onRemoveImage: () => _removeImage(provider, index),
);
}),
// 上传按钮(如果还有剩余位置
if (imageUrls.length < maxImageCount)
buildImageUploadButton(onPickImage: () => _pickImages(provider)),
],
}
// 上传按钮(最后一个
else {
return buildImageUploadButton(onTap: () => _pickImages(provider));
}
},
);
}
@@ -102,14 +106,14 @@ class _MomentFormPageState extends State<MomentFormPage> {
children: [
buildFormLabel('朋友圈内容', required: true),
const SizedBox(height: 8),
_buildContentField(),
const SizedBox(height: 20),
_buildContentField(provider),
const SizedBox(height: 8),
buildFormLabel('上传图片', required: true),
const SizedBox(height: 10),
_buildImageGrid(provider),
const SizedBox(height: 20),
const SizedBox(height: 8),
_buildImageField(provider),
const SizedBox(height: 8),
buildFormButtonGroup(
context: context,
onConfirm: () => _submitForm(provider),
@@ -125,7 +129,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
final remainingCount = maxImageCount - currentCount;
if (remainingCount <= 0) {
showErrorToast('最多只能上传$maxImageCount张图片');
ToastUtil.error('最多只能上传$maxImageCount张图片');
return;
}
@@ -147,7 +151,11 @@ class _MomentFormPageState extends State<MomentFormPage> {
// 逐个上传图片
for (final file in filesToUpload) {
final fileName = await MinIOHelper().uploadFile(file: file);
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
logger.i('图片名称:$fileName');
newImageUrls.add(fileName);
}
@@ -156,9 +164,9 @@ class _MomentFormPageState extends State<MomentFormPage> {
provider.momentFormItem.imageList.addAll(newImageUrls);
});
showSuccessToast('图片上传成功');
ToastUtil.success('图片上传成功');
} catch (e) {
showErrorToast('图片上传失败');
ToastUtil.error('图片上传失败');
}
}
}
@@ -199,7 +207,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10),
child: BuildCard(child: _buildFormBuilder()),
child: CommonCard(child: _buildFormBuilder()),
),
),
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/screenshot_util.dart';
@@ -226,7 +227,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
required String title,
required Widget content,
}) {
return BuildCard(
return CommonCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -24,7 +24,6 @@ class RecordPage extends StatefulWidget {
}
class _RecordPageState extends State<RecordPage> {
RecordTab _currentTab = RecordTab.recipe;
final List<Widget> _tabPages = [
RecipeList(),
RecipeCalendar(),
@@ -49,22 +48,8 @@ class _RecordPageState extends State<RecordPage> {
}
void _onTabChange(RecordTab tab, FoodProvider provider) {
setState(() {
_currentTab = tab;
});
switch (tab) {
case RecordTab.recipe:
provider.refreshRecipeList();
break;
case RecordTab.calendar:
provider.refreshRecordList(null, null);
break;
case RecordTab.timeline:
final year = DateTime.now().year;
provider.refreshRecordList("$year-01-01", "$year-12-31");
break;
}
provider.currentRecordTab = tab;
provider.onRecordTabChange();
}
@override
@@ -75,11 +60,14 @@ class _RecordPageState extends State<RecordPage> {
children: [
_buildTabs(
context: context,
currentTab: _currentTab,
currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider),
),
Expanded(
child: IndexedStack(index: _currentTab.index, children: _tabPages),
child: IndexedStack(
index: provider.currentRecordTab.index,
children: _tabPages,
),
),
],
);

View File

@@ -1,12 +1,14 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/minio_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/apis/recipe.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:food_hub_app/widgets/common/index.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
@@ -48,52 +50,19 @@ class _RecordFormPageState extends State<RecordFormPage> {
children: [
buildFormLabel('菜谱名称', required: true),
const SizedBox(height: 8),
_buildRecipeAutocomplete(provider),
_buildRecipeField(provider),
const SizedBox(height: 8),
const SizedBox(height: 10),
buildFormLabel('完成时间', required: true),
const SizedBox(height: 8),
FormBuilderTextField(
name: _dateField,
readOnly: true,
initialValue: provider.recordFormItem.date,
decoration: buildInputDecoration(
context: context,
hintText: '请选择完成时间',
prefixIcon: const Icon(
Icons.calendar_month,
size: 20,
color: Color(0xFF86909C),
),
),
onTap: () => _onSelectDate(provider),
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择完成时间';
}
return null;
},
),
const SizedBox(height: 10),
_buildDateField(provider),
const SizedBox(height: 8),
buildFormLabel('上传图片', required: true),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (provider.recordFormItem.imageUrl.isEmpty)
buildImageUploadButton(onPickImage: () => _pickImage(provider))
else
buildImagePreviewItem(
context: context,
imageUrls: [provider.recordFormItem.imageUrl],
index: 0,
onRemoveImage: () => _removeImage(provider),
),
],
),
const SizedBox(height: 8),
_buildImageField(provider),
const SizedBox(height: 8),
const SizedBox(height: 10),
buildFormButtonGroup(
context: context,
onConfirm: () => _submitForm(provider),
@@ -103,7 +72,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
);
}
Widget _buildRecipeAutocomplete(FoodProvider provider) {
Widget _buildRecipeField(FoodProvider provider) {
return Autocomplete<String>(
initialValue: TextEditingValue(text: provider.recordFormItem.name),
optionsBuilder: (TextEditingValue textEditingValue) {
@@ -118,9 +87,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
},
onSelected: (String value) {
setState(() {
provider.recordFormItem.name = value;
});
provider.recordFormItem.name = value;
},
optionsViewBuilder: (
@@ -149,7 +116,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
child: Container(
constraints: BoxConstraints(
maxHeight: 200,
maxWidth: MediaQuery.of(context).size.width - 32,
maxWidth: MediaQuery.of(context).size.width - 58,
),
child: ListView.builder(
padding: EdgeInsets.zero,
@@ -179,9 +146,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
void onClearRecipeName() {
controller.clear();
setState(() {
provider.recordFormItem.name = '';
});
provider.recordFormItem.name = '';
}
Widget? buildSuffixIcon() {
@@ -201,9 +166,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
focusNode: focusNode,
enabled: !provider.isEditing,
onChanged: (value) {
setState(() {
provider.recordFormItem.name = value ?? '';
});
provider.recordFormItem.name = value ?? '';
},
decoration: buildInputDecoration(
context: context,
@@ -225,6 +188,30 @@ class _RecordFormPageState extends State<RecordFormPage> {
);
}
Widget _buildDateField(FoodProvider provider) {
return FormBuilderTextField(
name: _dateField,
readOnly: true,
initialValue: provider.recordFormItem.date,
decoration: buildInputDecoration(
context: context,
hintText: '请选择完成时间',
prefixIcon: const Icon(
Icons.calendar_month,
size: 20,
color: Color(0xFF86909C),
),
),
onTap: () => _onSelectDate(provider),
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择完成时间';
}
return null;
},
);
}
/// 选择日期
Future<void> _onSelectDate(FoodProvider provider) async {
final DateTime? picked = await showDatePicker(
@@ -237,12 +224,26 @@ class _RecordFormPageState extends State<RecordFormPage> {
if (picked != null) {
final String date = DateFormat('yyyy-MM-dd').format(picked);
_formKey.currentState?.fields[_dateField]?.didChange(date);
setState(() {
provider.recordFormItem.date = date;
});
provider.recordFormItem.date = date;
}
}
Widget _buildImageField(FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (provider.recordFormItem.imageUrl.isEmpty)
buildImageUploadButton(onTap: () => _pickImage(provider))
else
ImagePreview(
imageUrls: [provider.recordFormItem.imageUrl],
index: 0,
onRemoveImage: () => _removeImage(provider),
),
],
);
}
/// 选择图片(限制单张)
Future<void> _pickImage(FoodProvider provider) async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
@@ -253,7 +254,11 @@ class _RecordFormPageState extends State<RecordFormPage> {
if (result != null) {
PlatformFile file = result.files.first;
final fileName = await MinIOHelper().uploadFile(file: file);
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
setState(() {
provider.recordFormItem.imageUrl = fileName;
});
@@ -268,16 +273,16 @@ class _RecordFormPageState extends State<RecordFormPage> {
}
/// 提交表单
void _submitForm(FoodProvider provider) {
void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
if (provider.recordFormItem.imageUrl.isEmpty) {
showErrorToast('请上传图片');
ToastUtil.error('请上传图片');
return;
}
print(provider.recordFormItem.name);
print(provider.recordFormItem.date);
print(provider.recordFormItem.imageUrl);
await provider.handleRecord();
await provider.onRecordTabChange();
Navigator.pop(context);
}
}
@@ -300,7 +305,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: BuildCard(child: _buildFormBuilder()),
child: CommonCard(child: _buildFormBuilder()),
),
),
);

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/chart.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/chart.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/stats/card.dart';
import 'package:provider/provider.dart';
@@ -21,7 +21,7 @@ class _StatsPage extends State<StatsPage> {
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().refreshBlogStats();
context.read<FoodProvider>().refreshFoodStats();
});
}
@@ -67,56 +67,6 @@ class _StatsPage extends State<StatsPage> {
);
}
Widget _buildRecordStats(FoodProvider provider) {
return Column(
children: [
buildChartTitle(context: context, title: '记录统计'),
const SizedBox(height: 3),
buildChartDivider(context: context),
const SizedBox(height: 3),
Expanded(
child: lineChart(
context: context,
xAxisName: '日期',
yAxisName: '次数',
unit: '',
data: provider.recordStats,
),
),
],
);
}
Widget _buildCategoryStats(FoodProvider provider) {
return Column(
children: [
buildChartTitle(context: context, title: '菜谱统计'),
const SizedBox(height: 3),
buildChartDivider(context: context),
const SizedBox(height: 3),
Expanded(
child: pieChart(
context: context,
unit: '',
data: provider.categoryStats,
),
),
],
);
}
Widget _buildRankStats(FoodProvider provider) {
return Column(
children: [
buildChartTitle(context: context, title: '排行榜'),
const SizedBox(height: 3),
buildChartDivider(context: context),
const SizedBox(height: 3),
Expanded(child: rankChart(data: provider.rankStats, unit: '')),
],
);
}
Widget _buildContent(FoodProvider provider) {
return Column(
children: [
@@ -124,17 +74,33 @@ class _StatsPage extends State<StatsPage> {
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildRecordStats(provider)),
child: CommonCard(
child: LineChart(
title: '记录统计',
xAxisName: '日期',
yAxisName: '次数',
unit: '',
data: provider.recordStats,
),
),
),
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildCategoryStats(provider)),
child: CommonCard(
child: PieChart(
title: '菜谱统计',
unit: '',
data: provider.categoryStats,
),
),
),
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildRankStats(provider)),
child: CommonCard(
child: RankChart(title: '排行榜', data: provider.rankStats, unit: ''),
),
),
],
);
@@ -147,7 +113,7 @@ class _StatsPage extends State<StatsPage> {
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator(context)
buildLoadingIndicator()
else
SingleChildScrollView(child: _buildContent(provider)),
],