feat:增加Minio工具类
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Future<bool> updateRecordApi(int id, Record record) {
|
||||
Future<bool> updateRecordApi(int id, FoodRecord 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");
|
||||
}
|
||||
|
||||
Future<List<Record>> queryRecordApi(String startDate, String endDate) {
|
||||
return HttpUtil().get<List<Record>>(
|
||||
Future<List<FoodRecord>> queryRecordApi(String startDate, String endDate) {
|
||||
return HttpUtil().get<List<FoodRecord>>(
|
||||
"/food/record",
|
||||
queryParameters: {"startDate": startDate, "endDate": endDate},
|
||||
converter: (data) => convertListResponse<Record>(data, Record.fromJson),
|
||||
converter: (data) => convertListResponse<FoodRecord>(data, FoodRecord.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import 'package:flutter/material.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/views/home.dart';
|
||||
import 'package:food_hub_app/views/login.dart';
|
||||
import 'package:food_hub_app/views/record_form.dart';
|
||||
import 'package:food_hub_app/views/recipe_detail.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 {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
|
||||
await SPUtil.init();
|
||||
runApp(const MyApp());
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => FoodProvider()),
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
|
||||
@@ -9,7 +9,11 @@ class RecipeMaterial {
|
||||
String name;
|
||||
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) =>
|
||||
_$RecipeMaterialFromJson(json);
|
||||
@@ -24,9 +28,14 @@ class RecipeStep {
|
||||
String content;
|
||||
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);
|
||||
}
|
||||
@@ -56,7 +65,7 @@ class RecipeComment {
|
||||
|
||||
/// 成果信息
|
||||
@JsonSerializable()
|
||||
class Record {
|
||||
class FoodRecord {
|
||||
int? id;
|
||||
String name;
|
||||
String category;
|
||||
@@ -64,7 +73,7 @@ class Record {
|
||||
String date;
|
||||
String imageUrl;
|
||||
|
||||
Record({
|
||||
FoodRecord({
|
||||
this.id,
|
||||
required this.name,
|
||||
required this.category,
|
||||
@@ -73,20 +82,31 @@ class Record {
|
||||
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);
|
||||
|
||||
static FoodRecord getEmpty() {
|
||||
return FoodRecord(
|
||||
id: 0,
|
||||
name: '',
|
||||
category: '',
|
||||
person: 0,
|
||||
date: '',
|
||||
imageUrl: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class RecipeQuery {
|
||||
String category;
|
||||
|
||||
RecipeQuery({
|
||||
required this.category
|
||||
});
|
||||
RecipeQuery({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);
|
||||
}
|
||||
@@ -105,7 +125,7 @@ class Recipe {
|
||||
String avatar;
|
||||
List<RecipeMaterial> materialList;
|
||||
List<RecipeStep> stepList;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
List<int> likeList;
|
||||
int likeCount;
|
||||
List<int> favouriteList;
|
||||
@@ -146,7 +166,7 @@ class RecipeSummary {
|
||||
String category;
|
||||
double recommendRate;
|
||||
bool isShare;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
int likeCount;
|
||||
int favouriteCount;
|
||||
int commentCount;
|
||||
@@ -169,7 +189,8 @@ class RecipeSummary {
|
||||
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);
|
||||
}
|
||||
@@ -187,7 +208,7 @@ class RecipeDetail {
|
||||
String avatar;
|
||||
List<RecipeMaterial> materialList;
|
||||
List<RecipeStep> stepList;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
List<int> likeList;
|
||||
List<int> favouriteList;
|
||||
List<RecipeComment> commentList;
|
||||
@@ -210,7 +231,8 @@ class RecipeDetail {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
|
||||
'date': instance.date,
|
||||
};
|
||||
|
||||
Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
||||
FoodRecord _$RecordFromJson(Map<String, dynamic> json) => FoodRecord(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String,
|
||||
category: json['category'] as String,
|
||||
@@ -60,7 +60,7 @@ Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$RecordToJson(FoodRecord instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'category': instance.category,
|
||||
@@ -95,7 +95,7 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||
.toList(),
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeList:
|
||||
(json['likeList'] as List<dynamic>)
|
||||
@@ -147,7 +147,7 @@ RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
|
||||
avatar: json['avatar'] as String,
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeCount: (json['likeCount'] as num).toInt(),
|
||||
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||
@@ -190,7 +190,7 @@ RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
|
||||
.toList(),
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeList:
|
||||
(json['likeList'] as List<dynamic>)
|
||||
|
||||
18
lib/provider/food_provider.dart
Normal file
18
lib/provider/food_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
57
lib/utils/minio_utils.dart
Normal file
57
lib/utils/minio_utils.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('菜谱信息')),
|
||||
backgroundColor: Color(0xFFF4F4F4),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
|
||||
),
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.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: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 'dart:io';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecordFormPage extends StatefulWidget {
|
||||
const RecordFormPage({super.key});
|
||||
|
||||
@@ -17,9 +24,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
final _focusNode = FocusNode();
|
||||
static const String _nameField = 'name';
|
||||
static const String _dateField = 'date';
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
File? imageUrl; // 改为单张图片变量
|
||||
File? imageFile;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -29,12 +34,12 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('新增记录', style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: theme.primaryColor,
|
||||
backgroundColor: colors.primary,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
@@ -43,24 +48,16 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildFormBuilder(),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(5),
|
||||
child: buildCard(context: context, child: _buildFormBuilder()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormBuilder() {
|
||||
final foodProvider = context.watch<FoodProvider>();
|
||||
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
@@ -82,7 +79,10 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
_buildImageUploadArea(),
|
||||
|
||||
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(
|
||||
name: _nameField,
|
||||
focusNode: _focusNode,
|
||||
decoration: buildInputDecoration(context: context, hintText: '请输入菜谱名称'),
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入菜谱名称',
|
||||
prefixIcon: const Icon(Icons.title, size: 20, color: Color(0xFF86909C)),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入菜谱名称';
|
||||
@@ -132,10 +136,10 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (imageUrl != null)
|
||||
if (imageFile != null)
|
||||
_buildImagePreviewItem()
|
||||
else
|
||||
_buildImageUploadButton(),
|
||||
buildImageUploadButton(onPickImage: _pickImage),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -147,109 +151,8 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _showImagePreview(),
|
||||
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),
|
||||
),
|
||||
buildFileImage(context, imageFile!),
|
||||
buildDeleteImage(onRemoveImage: _removeImage),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -263,19 +166,6 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
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) {
|
||||
@@ -287,10 +177,17 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
|
||||
/// 选择图片(限制单张)
|
||||
Future<void> _pickImage() async {
|
||||
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
|
||||
if (image != null) {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
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(() {
|
||||
imageUrl = File(image.path); // 直接覆盖现有图片
|
||||
// imageFile = File(image.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -298,14 +195,14 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
/// 移除图片
|
||||
void _removeImage() {
|
||||
setState(() {
|
||||
imageUrl = null;
|
||||
imageFile = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 提交表单
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
if (imageUrl == null) {
|
||||
if (imageFile == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请上传图片'),
|
||||
@@ -318,7 +215,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
|
||||
final formData = {
|
||||
..._formKey.currentState!.value,
|
||||
'imageUrl': imageUrl?.path, // 单张图片路径
|
||||
'imageUrl': imageFile?.path,
|
||||
};
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -79,7 +79,6 @@ Widget _buildCancelButton(BuildContext context) {
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: const Color(0xFF4E5969),
|
||||
side: const BorderSide(color: Color(0xFFDCDFE6)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -90,10 +89,9 @@ Widget _buildCancelButton(BuildContext context) {
|
||||
/// 提交按钮
|
||||
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
||||
return ElevatedButton(
|
||||
onPressed: onConfirm,
|
||||
onPressed: () => onConfirm(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor, // 使用传入的context获取主题
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.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) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showFullScreenImage(context, url);
|
||||
showFullScreenImage(context, NetworkImage(url));
|
||||
},
|
||||
child: Image.network(
|
||||
url,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return _buildLoadingIndicator(loadingProgress);
|
||||
return buildImageLoadingIndicator(loadingProgress);
|
||||
},
|
||||
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(
|
||||
child: SizedBox(
|
||||
width: 30,
|
||||
@@ -131,15 +149,14 @@ Widget _buildErrorImage() {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhotoView(String url) {
|
||||
Widget _buildPhotoView(ImageProvider imageProvider) {
|
||||
return PhotoView(
|
||||
imageProvider: NetworkImage(url),
|
||||
imageProvider: imageProvider,
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 2,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: url),
|
||||
loadingBuilder: (context, event) => _buildLoadingIndicator(event),
|
||||
loadingBuilder: (context, event) => buildImageLoadingIndicator(event),
|
||||
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(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
@@ -169,7 +186,7 @@ void _showFullScreenImage(BuildContext context, String url) {
|
||||
body: Stack(
|
||||
children: [
|
||||
// 可缩放图片
|
||||
Positioned.fill(child: _buildPhotoView(url)),
|
||||
Positioned.fill(child: _buildPhotoView(imageProvider)),
|
||||
// 关闭按钮
|
||||
_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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||
DateTime _selectedDay = DateTime.now();
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
|
||||
List<Record> recordList = [];
|
||||
List<Record> selectRecordList = [];
|
||||
List<FoodRecord> recordList = [];
|
||||
List<FoodRecord> selectRecordList = [];
|
||||
|
||||
@override
|
||||
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;
|
||||
|
||||
return Card(
|
||||
|
||||
@@ -15,7 +15,7 @@ class RecipeTimeline extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecipeTimeline extends State<RecipeTimeline> {
|
||||
List<Record> recordList = [];
|
||||
List<FoodRecord> recordList = [];
|
||||
|
||||
@override
|
||||
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}'];
|
||||
|
||||
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) {
|
||||
return buildEmptyData();
|
||||
} else {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
||||
78
pubspec.lock
78
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -162,13 +170,13 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -185,6 +193,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -233,6 +249,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -589,6 +613,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -645,6 +685,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -677,6 +725,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -970,6 +1026,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -978,6 +1042,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -36,6 +36,7 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
dio: ^5.7.0
|
||||
provider: ^6.1.1
|
||||
timelines_plus: ^1.0.7
|
||||
table_calendar: ^3.1.3
|
||||
toggle_switch: ^2.3.0
|
||||
@@ -51,6 +52,9 @@ dependencies:
|
||||
flutter_carousel_widget: ^3.1.0
|
||||
easy_refresh: ^3.4.0
|
||||
syncfusion_flutter_charts: ^30.1.41
|
||||
minio: ^3.5.8
|
||||
crypto: ^3.0.7
|
||||
file_picker: ^10.3.3
|
||||
|
||||
dependency_overrides:
|
||||
tdesign_flutter_adaptation: 3.16.0
|
||||
|
||||
Reference in New Issue
Block a user