feat:增加Minio工具类

This commit is contained in:
2025-11-18 20:03:25 +08:00
parent 9a8e1a7419
commit b6995d8dd1
15 changed files with 332 additions and 194 deletions

View File

@@ -51,11 +51,11 @@ Future<List<String>> queryFoodNameListApi() {
); );
} }
Future<bool> addRecordApi(Record record) { Future<bool> addRecordApi(FoodRecord record) {
return HttpUtil().post<bool>("/food/record", data: record); return HttpUtil().post<bool>("/food/record", data: record);
} }
Future<bool> updateRecordApi(int id, Record record) { Future<bool> updateRecordApi(int id, FoodRecord record) {
return HttpUtil().put<bool>("/food/record/$id", data: record); return HttpUtil().put<bool>("/food/record/$id", data: record);
} }
@@ -83,11 +83,11 @@ Future<bool> deleteRecipeFavouriteApi(int id) {
return HttpUtil().delete<bool>("/food/recipe/$id/like"); return HttpUtil().delete<bool>("/food/recipe/$id/like");
} }
Future<List<Record>> queryRecordApi(String startDate, String endDate) { Future<List<FoodRecord>> queryRecordApi(String startDate, String endDate) {
return HttpUtil().get<List<Record>>( return HttpUtil().get<List<FoodRecord>>(
"/food/record", "/food/record",
queryParameters: {"startDate": startDate, "endDate": endDate}, queryParameters: {"startDate": startDate, "endDate": endDate},
converter: (data) => convertListResponse<Record>(data, Record.fromJson), converter: (data) => convertListResponse<FoodRecord>(data, FoodRecord.fromJson),
); );
} }

View File

@@ -1,18 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/views/home.dart'; import 'package:food_hub_app/views/home.dart';
import 'package:food_hub_app/views/login.dart'; import 'package:food_hub_app/views/login.dart';
import 'package:food_hub_app/views/record_form.dart'; import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/views/recipe_detail.dart'; import 'package:food_hub_app/views/recipe_detail.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:provider/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
await SPUtil.init(); await SPUtil.init();
runApp(const MyApp()); runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => FoodProvider()),
],
child: MyApp(),
),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {

View File

@@ -9,7 +9,11 @@ class RecipeMaterial {
String name; String name;
String amount; String amount;
RecipeMaterial({required this.type, required this.name, required this.amount}); RecipeMaterial({
required this.type,
required this.name,
required this.amount,
});
factory RecipeMaterial.fromJson(Map<String, dynamic> json) => factory RecipeMaterial.fromJson(Map<String, dynamic> json) =>
_$RecipeMaterialFromJson(json); _$RecipeMaterialFromJson(json);
@@ -24,9 +28,14 @@ class RecipeStep {
String content; String content;
String imageUrl; String imageUrl;
RecipeStep({required this.sort, required this.content, required this.imageUrl}); RecipeStep({
required this.sort,
required this.content,
required this.imageUrl,
});
factory RecipeStep.fromJson(Map<String, dynamic> json) => _$RecipeStepFromJson(json); factory RecipeStep.fromJson(Map<String, dynamic> json) =>
_$RecipeStepFromJson(json);
Map<String, dynamic> toJson() => _$RecipeStepToJson(this); Map<String, dynamic> toJson() => _$RecipeStepToJson(this);
} }
@@ -56,7 +65,7 @@ class RecipeComment {
/// 成果信息 /// 成果信息
@JsonSerializable() @JsonSerializable()
class Record { class FoodRecord {
int? id; int? id;
String name; String name;
String category; String category;
@@ -64,7 +73,7 @@ class Record {
String date; String date;
String imageUrl; String imageUrl;
Record({ FoodRecord({
this.id, this.id,
required this.name, required this.name,
required this.category, required this.category,
@@ -73,20 +82,31 @@ class Record {
required this.imageUrl, required this.imageUrl,
}); });
factory Record.fromJson(Map<String, dynamic> json) => _$RecordFromJson(json); factory FoodRecord.fromJson(Map<String, dynamic> json) =>
_$RecordFromJson(json);
Map<String, dynamic> toJson() => _$RecordToJson(this); Map<String, dynamic> toJson() => _$RecordToJson(this);
static FoodRecord getEmpty() {
return FoodRecord(
id: 0,
name: '',
category: '',
person: 0,
date: '',
imageUrl: '',
);
}
} }
@JsonSerializable() @JsonSerializable()
class RecipeQuery { class RecipeQuery {
String category; String category;
RecipeQuery({ RecipeQuery({required this.category});
required this.category
});
factory RecipeQuery.fromJson(Map<String, dynamic> json) => _$RecipeQueryFromJson(json); factory RecipeQuery.fromJson(Map<String, dynamic> json) =>
_$RecipeQueryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeQueryToJson(this); Map<String, dynamic> toJson() => _$RecipeQueryToJson(this);
} }
@@ -105,7 +125,7 @@ class Recipe {
String avatar; String avatar;
List<RecipeMaterial> materialList; List<RecipeMaterial> materialList;
List<RecipeStep> stepList; List<RecipeStep> stepList;
List<Record> recordList; List<FoodRecord> recordList;
List<int> likeList; List<int> likeList;
int likeCount; int likeCount;
List<int> favouriteList; List<int> favouriteList;
@@ -146,7 +166,7 @@ class RecipeSummary {
String category; String category;
double recommendRate; double recommendRate;
bool isShare; bool isShare;
List<Record> recordList; List<FoodRecord> recordList;
int likeCount; int likeCount;
int favouriteCount; int favouriteCount;
int commentCount; int commentCount;
@@ -169,7 +189,8 @@ class RecipeSummary {
required this.commentCount, required this.commentCount,
}); });
factory RecipeSummary.fromJson(Map<String, dynamic> json) => _$RecipeSummaryFromJson(json); factory RecipeSummary.fromJson(Map<String, dynamic> json) =>
_$RecipeSummaryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this); Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this);
} }
@@ -187,7 +208,7 @@ class RecipeDetail {
String avatar; String avatar;
List<RecipeMaterial> materialList; List<RecipeMaterial> materialList;
List<RecipeStep> stepList; List<RecipeStep> stepList;
List<Record> recordList; List<FoodRecord> recordList;
List<int> likeList; List<int> likeList;
List<int> favouriteList; List<int> favouriteList;
List<RecipeComment> commentList; List<RecipeComment> commentList;
@@ -210,7 +231,8 @@ class RecipeDetail {
required this.commentList, required this.commentList,
}); });
factory RecipeDetail.fromJson(Map<String, dynamic> json) => _$RecipeDetailFromJson(json); factory RecipeDetail.fromJson(Map<String, dynamic> json) =>
_$RecipeDetailFromJson(json);
Map<String, dynamic> toJson() => _$RecipeDetailToJson(this); Map<String, dynamic> toJson() => _$RecipeDetailToJson(this);
} }

View File

@@ -51,7 +51,7 @@ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
'date': instance.date, 'date': instance.date,
}; };
Record _$RecordFromJson(Map<String, dynamic> json) => Record( FoodRecord _$RecordFromJson(Map<String, dynamic> json) => FoodRecord(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
name: json['name'] as String, name: json['name'] as String,
category: json['category'] as String, category: json['category'] as String,
@@ -60,7 +60,7 @@ Record _$RecordFromJson(Map<String, dynamic> json) => Record(
imageUrl: json['imageUrl'] as String, imageUrl: json['imageUrl'] as String,
); );
Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{ Map<String, dynamic> _$RecordToJson(FoodRecord instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'category': instance.category, 'category': instance.category,
@@ -95,7 +95,7 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
.toList(), .toList(),
recordList: recordList:
(json['recordList'] as List<dynamic>) (json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>)) .map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
likeList: likeList:
(json['likeList'] as List<dynamic>) (json['likeList'] as List<dynamic>)
@@ -147,7 +147,7 @@ RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
avatar: json['avatar'] as String, avatar: json['avatar'] as String,
recordList: recordList:
(json['recordList'] as List<dynamic>) (json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>)) .map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
likeCount: (json['likeCount'] as num).toInt(), likeCount: (json['likeCount'] as num).toInt(),
favouriteCount: (json['favouriteCount'] as num).toInt(), favouriteCount: (json['favouriteCount'] as num).toInt(),
@@ -190,7 +190,7 @@ RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
.toList(), .toList(),
recordList: recordList:
(json['recordList'] as List<dynamic>) (json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>)) .map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
likeList: likeList:
(json['likeList'] as List<dynamic>) (json['likeList'] as List<dynamic>)

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/recipe.dart';
class FoodProvider with ChangeNotifier {
late FoodRecord _recordFormItem;
FoodRecord get recordFormItem => _recordFormItem;
void resetRecordForm() {
_recordFormItem = FoodRecord.getEmpty();
notifyListeners();
}
void initRecordForm(FoodRecord record) {
_recordFormItem = record;
notifyListeners();
}
}

View File

@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
final String rustfsIp = '14.103.235.151';
final String rustfsFileUrl = 'http://14.103.235.151:9100';
final String bucketName = 'flisp';
MinIOHelper._internal() {
_minio = Minio(
endPoint: rustfsIp,
port: 9100,
accessKey: "tHSFfcDW8qpCzKa2Xg6Y",
secretKey: "oq79EeYJ4jdczRp2IHUMCnbKtSw58NgDlG3sOkvX",
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
Function(double)? onProgress,
}) async {
try {
String hashName = await _generateMD5HashName(file.path!);
String fileName = '$hashName${_getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, file.path!);
return fileName;
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
String _getFileExtension(String fileName) {
if (fileName.contains('.')) {
return '.${fileName.split('.').last.toLowerCase()}';
}
return '';
}
Future<String> _generateMD5HashName(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
final hash = md5.convert(bytes);
return hash.toString();
}
}

View File

@@ -71,7 +71,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('菜谱信息')), appBar: AppBar(title: Text('菜谱信息')),
backgroundColor: Color(0xFFF4F4F4), backgroundColor: Color(0xFFF5F5F5),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()), child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
), ),

View File

@@ -1,10 +1,17 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.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/form.dart';
import 'package:image_picker/image_picker.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'dart:io'; import 'dart:io';
import 'package:provider/provider.dart';
class RecordFormPage extends StatefulWidget { class RecordFormPage extends StatefulWidget {
const RecordFormPage({super.key}); const RecordFormPage({super.key});
@@ -17,9 +24,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
final _focusNode = FocusNode(); final _focusNode = FocusNode();
static const String _nameField = 'name'; static const String _nameField = 'name';
static const String _dateField = 'date'; static const String _dateField = 'date';
File? imageFile;
final ImagePicker _picker = ImagePicker();
File? imageUrl; // 改为单张图片变量
@override @override
void dispose() { void dispose() {
@@ -29,12 +34,12 @@ class _RecordFormPageState extends State<RecordFormPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final colors = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('新增记录', style: TextStyle(color: Colors.white)), title: const Text('新增记录', style: TextStyle(color: Colors.white)),
backgroundColor: theme.primaryColor, backgroundColor: colors.primary,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@@ -43,24 +48,16 @@ class _RecordFormPageState extends State<RecordFormPage> {
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(5), padding: EdgeInsets.all(5),
child: Card( child: buildCard(context: context, child: _buildFormBuilder()),
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildFormBuilder(),
),
),
), ),
), ),
); );
} }
Widget _buildFormBuilder() { Widget _buildFormBuilder() {
final foodProvider = context.watch<FoodProvider>();
return FormBuilder( return FormBuilder(
key: _formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -82,7 +79,10 @@ class _RecordFormPageState extends State<RecordFormPage> {
_buildImageUploadArea(), _buildImageUploadArea(),
const SizedBox(height: 10), const SizedBox(height: 10),
buildFormButtonGroup(context: context, onConfirm: () => _submitForm), buildFormButtonGroup(
context: context,
onConfirm: () => _submitForm(),
),
], ],
), ),
); );
@@ -93,7 +93,11 @@ class _RecordFormPageState extends State<RecordFormPage> {
return FormBuilderTextField( return FormBuilderTextField(
name: _nameField, name: _nameField,
focusNode: _focusNode, focusNode: _focusNode,
decoration: buildInputDecoration(context: context, hintText: '请输入菜谱名称'), decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱名称',
prefixIcon: const Icon(Icons.title, size: 20, color: Color(0xFF86909C)),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return '请输入菜谱名称'; return '请输入菜谱名称';
@@ -132,10 +136,10 @@ class _RecordFormPageState extends State<RecordFormPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (imageUrl != null) if (imageFile != null)
_buildImagePreviewItem() _buildImagePreviewItem()
else else
_buildImageUploadButton(), buildImageUploadButton(onPickImage: _pickImage),
], ],
); );
} }
@@ -147,109 +151,8 @@ class _RecordFormPageState extends State<RecordFormPage> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Stack( child: Stack(
children: [ children: [
GestureDetector( buildFileImage(context, imageFile!),
onTap: () => _showImagePreview(), buildDeleteImage(onRemoveImage: _removeImage),
child: Image(
image: FileImage(imageUrl!),
width: 120,
height: 120,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 120,
height: 120,
color: Colors.grey[100],
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
);
},
),
),
Positioned(
top: 6,
right: 6,
child: GestureDetector(
onTap: _removeImage,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 2,
spreadRadius: 0,
),
],
),
child: const Icon(Icons.close, color: Colors.white, size: 16),
),
),
),
],
),
),
);
}
void _showImagePreview() {
showDialog(
context: context,
barrierColor: Colors.black87, // 半透明黑色背景
builder:
(context) => Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
insetPadding: const EdgeInsets.all(16),
child: GestureDetector(
onTap: () => Navigator.pop(context), // 点击空白处关闭
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
// 预览图添加轻微阴影
boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 10)],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
imageUrl!,
fit: BoxFit.contain, // 保持图片比例
height: MediaQuery.of(context).size.height * 0.7, // 限制最大高度
),
),
),
),
),
);
}
/// 图片上传按钮
Widget _buildImageUploadButton() {
return InkWell(
onTap: _pickImage,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFDCDFE6), width: 1),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add, color: Color(0xFF86909C), size: 24),
SizedBox(height: 6),
Text(
'添加图片',
style: TextStyle(color: Color(0xFF86909C), fontSize: 13),
),
], ],
), ),
), ),
@@ -263,19 +166,6 @@ class _RecordFormPageState extends State<RecordFormPage> {
initialDate: DateTime.now(), initialDate: DateTime.now(),
firstDate: DateTime(2000), firstDate: DateTime(2000),
lastDate: DateTime(2100), lastDate: DateTime(2100),
builder:
(context, child) => Theme(
data: ThemeData.light().copyWith(
primaryColor: Theme.of(context).primaryColor,
colorScheme: ColorScheme.light(
primary: Theme.of(context).primaryColor,
),
buttonTheme: const ButtonThemeData(
textTheme: ButtonTextTheme.primary,
),
),
child: child!,
),
); );
if (picked != null) { if (picked != null) {
@@ -287,10 +177,17 @@ class _RecordFormPageState extends State<RecordFormPage> {
/// 选择图片(限制单张) /// 选择图片(限制单张)
Future<void> _pickImage() async { Future<void> _pickImage() async {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery); FilePickerResult? result = await FilePicker.platform.pickFiles(
if (image != null) { type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png'],
allowMultiple: false,
);
if (result != null) {
PlatformFile file = result.files.first;
final fileName = await MinIOHelper().uploadFile(file: file);
setState(() { setState(() {
imageUrl = File(image.path); // 直接覆盖现有图片 // imageFile = File(image.path);
}); });
} }
} }
@@ -298,14 +195,14 @@ class _RecordFormPageState extends State<RecordFormPage> {
/// 移除图片 /// 移除图片
void _removeImage() { void _removeImage() {
setState(() { setState(() {
imageUrl = null; imageFile = null;
}); });
} }
/// 提交表单 /// 提交表单
void _submitForm() { void _submitForm() {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
if (imageUrl == null) { if (imageFile == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('请上传图片'), content: Text('请上传图片'),
@@ -318,7 +215,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
final formData = { final formData = {
..._formKey.currentState!.value, ..._formKey.currentState!.value,
'imageUrl': imageUrl?.path, // 单张图片路径 'imageUrl': imageFile?.path,
}; };
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -79,7 +79,6 @@ Widget _buildCancelButton(BuildContext context) {
backgroundColor: Colors.white, backgroundColor: Colors.white,
foregroundColor: const Color(0xFF4E5969), foregroundColor: const Color(0xFF4E5969),
side: const BorderSide(color: Color(0xFFDCDFE6)), side: const BorderSide(color: Color(0xFFDCDFE6)),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0, elevation: 0,
), ),
@@ -90,10 +89,9 @@ Widget _buildCancelButton(BuildContext context) {
/// 提交按钮 /// 提交按钮
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) { Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
return ElevatedButton( return ElevatedButton(
onPressed: onConfirm, onPressed: () => onConfirm(),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor, // 使用传入的context获取主题 backgroundColor: Theme.of(context).colorScheme.primary,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0, elevation: 0,
), ),

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
@@ -88,19 +90,35 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
} }
} }
Widget buildFileImage(BuildContext context, File imageFile) {
return GestureDetector(
onTap: () => showFullScreenImage(context, FileImage(imageFile)),
child: Image(
image: FileImage(imageFile),
width: 120,
height: 120,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return buildImageLoadingIndicator(loadingProgress);
},
),
);
}
Widget buildNetworkImage(BuildContext context, String url) { Widget buildNetworkImage(BuildContext context, String url) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
_showFullScreenImage(context, url); showFullScreenImage(context, NetworkImage(url));
}, },
child: Image.network( child: Image.network(
url, url,
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return _buildLoadingIndicator(loadingProgress); return buildImageLoadingIndicator(loadingProgress);
}, },
errorBuilder: (context, error, stackTrace) => _buildErrorImage(), errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
), ),
@@ -108,7 +126,7 @@ Widget buildNetworkImage(BuildContext context, String url) {
); );
} }
Widget _buildLoadingIndicator(ImageChunkEvent? loadingProgress) { Widget buildImageLoadingIndicator(ImageChunkEvent? loadingProgress) {
return Center( return Center(
child: SizedBox( child: SizedBox(
width: 30, width: 30,
@@ -131,15 +149,14 @@ Widget _buildErrorImage() {
); );
} }
Widget _buildPhotoView(String url) { Widget _buildPhotoView(ImageProvider imageProvider) {
return PhotoView( return PhotoView(
imageProvider: NetworkImage(url), imageProvider: imageProvider,
backgroundDecoration: const BoxDecoration(color: Colors.transparent), backgroundDecoration: const BoxDecoration(color: Colors.transparent),
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2, maxScale: PhotoViewComputedScale.covered * 2,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(tag: url), loadingBuilder: (context, event) => buildImageLoadingIndicator(event),
loadingBuilder: (context, event) => _buildLoadingIndicator(event),
errorBuilder: (context, error, stackTrace) => _buildErrorImage(), errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
); );
} }
@@ -155,7 +172,7 @@ Widget _buildCloseImage(BuildContext context) {
); );
} }
void _showFullScreenImage(BuildContext context, String url) { void showFullScreenImage(BuildContext context, ImageProvider imageProvider) {
Navigator.of(context).push( Navigator.of(context).push(
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
@@ -169,7 +186,7 @@ void _showFullScreenImage(BuildContext context, String url) {
body: Stack( body: Stack(
children: [ children: [
// 可缩放图片 // 可缩放图片
Positioned.fill(child: _buildPhotoView(url)), Positioned.fill(child: _buildPhotoView(imageProvider)),
// 关闭按钮 // 关闭按钮
_buildCloseImage(context), _buildCloseImage(context),
], ],
@@ -179,3 +196,46 @@ void _showFullScreenImage(BuildContext context, String url) {
), ),
); );
} }
Widget buildImageUploadButton({required VoidCallback onPickImage}) {
return InkWell(
onTap: onPickImage,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.black),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add, color: Color(0xFF86909C)),
SizedBox(height: 6),
Text('添加图片', style: TextStyle(color: Color(0xFF86909C))),
],
),
),
);
}
Widget buildDeleteImage({required VoidCallback onRemoveImage}) {
return Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: onRemoveImage,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 16),
),
),
);
}

View File

@@ -17,8 +17,8 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
DateTime _selectedDay = DateTime.now(); DateTime _selectedDay = DateTime.now();
DateTime _focusedDay = DateTime.now(); DateTime _focusedDay = DateTime.now();
List<Record> recordList = []; List<FoodRecord> recordList = [];
List<Record> selectRecordList = []; List<FoodRecord> selectRecordList = [];
@override @override
void initState() { void initState() {
@@ -109,7 +109,7 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
); );
} }
Widget recipeRecordItem(BuildContext context, Record record) { Widget recipeRecordItem(BuildContext context, FoodRecord record) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Card( return Card(

View File

@@ -15,7 +15,7 @@ class RecipeTimeline extends StatefulWidget {
} }
class _RecipeTimeline extends State<RecipeTimeline> { class _RecipeTimeline extends State<RecipeTimeline> {
List<Record> recordList = []; List<FoodRecord> recordList = [];
@override @override
void initState() { void initState() {
@@ -30,7 +30,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
}); });
} }
Widget buildTimelineCard(BuildContext context, Record record) { Widget buildTimelineCard(BuildContext context, FoodRecord record) {
final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}']; final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}'];
void imageTapClick() { void imageTapClick() {
@@ -79,7 +79,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
); );
} }
Widget buildTimeline(BuildContext context, List<Record> recordList) { Widget buildTimeline(BuildContext context, List<FoodRecord> recordList) {
if (recordList.isEmpty) { if (recordList.isEmpty) {
return buildEmptyData(); return buildEmptyData();
} else { } else {

View File

@@ -5,10 +5,12 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_picker
import file_selector_macos import file_selector_macos
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@@ -41,6 +41,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
buffer:
dependency: transitive
description:
name: buffer
sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.3"
build: build:
dependency: transitive dependency: transitive
description: description:
@@ -162,13 +170,13 @@ packages:
source: hosted source: hosted
version: "0.3.4+2" version: "0.3.4+2"
crypto: crypto:
dependency: transitive dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -185,6 +193,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -233,6 +249,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.3.6"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -589,6 +613,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
minio:
dependency: "direct main"
description:
name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.5.8"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -645,6 +685,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -677,6 +725,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -970,6 +1026,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.13.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -978,6 +1042,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@@ -36,6 +36,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
dio: ^5.7.0 dio: ^5.7.0
provider: ^6.1.1
timelines_plus: ^1.0.7 timelines_plus: ^1.0.7
table_calendar: ^3.1.3 table_calendar: ^3.1.3
toggle_switch: ^2.3.0 toggle_switch: ^2.3.0
@@ -51,6 +52,9 @@ dependencies:
flutter_carousel_widget: ^3.1.0 flutter_carousel_widget: ^3.1.0
easy_refresh: ^3.4.0 easy_refresh: ^3.4.0
syncfusion_flutter_charts: ^30.1.41 syncfusion_flutter_charts: ^30.1.41
minio: ^3.5.8
crypto: ^3.0.7
file_picker: ^10.3.3
dependency_overrides: dependency_overrides:
tdesign_flutter_adaptation: 3.16.0 tdesign_flutter_adaptation: 3.16.0