From 4f033a4d0a1fae2d228d9e39b79d442abc4208a5 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Thu, 20 Nov 2025 18:43:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/screenshot_util.dart | 61 ++++++++++++++ lib/views/home.dart | 9 ++- lib/views/recipe_detail.dart | 55 ++++++++++++- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 80 +++++++++++++++++++ pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 6 ++ windows/flutter/generated_plugins.cmake | 2 + 10 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 lib/utils/screenshot_util.dart diff --git a/lib/utils/screenshot_util.dart b/lib/utils/screenshot_util.dart new file mode 100644 index 0000000..414b3af --- /dev/null +++ b/lib/utils/screenshot_util.dart @@ -0,0 +1,61 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +/// 截图工具类 +class ScreenshotUtil { + // 截图功能 + static Future captureRecipeImage({ + required GlobalKey globalKey, + double pixelRatio = 3.0, + Duration delay = const Duration(milliseconds: 50), + }) async { + // 等待一帧确保UI已渲染 + await Future.delayed(delay); + final RenderObject? render = globalKey.currentContext!.findRenderObject(); + final RenderRepaintBoundary boundary = render as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + return byteData?.buffer.asUint8List(); + } + + // 保存图片到临时文件 + static Future saveImageToFile(Uint8List bytes, String fileName) async { + final directory = await getTemporaryDirectory(); + final File imageFile = File('${directory.path}/$fileName'); + await imageFile.writeAsBytes(bytes); + return imageFile; + } + + static Future captureAndShare({ + required GlobalKey globalKey, + required String fileName, + required String shareText, + }) async { + try { + final imageBytes = await captureRecipeImage(globalKey: globalKey); + + if (imageBytes != null) { + final imageFile = await saveImageToFile(imageBytes, fileName); + await SharePlus.instance.share( + ShareParams(files: [XFile(imageFile.path)], text: shareText), + ); + + // 分享后删除临时文件 + await imageFile.delete(); + } else { + showErrorToast('生成图片失败,请重试'); + } + } catch (e) { + showErrorToast('分享失败: $e'); + } + } +} diff --git a/lib/views/home.dart b/lib/views/home.dart index 410292b..1129838 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -44,14 +44,17 @@ class _HomePage extends State { onPressed: () { buildBottomSheet(context); }, - ) + ), ], - ) + ), ], ), // backgroundColor: Color(0xFFF5F5F5), drawer: SettingsDrawer(), - body: Padding(padding: EdgeInsets.all(10), child: tabPages[_currentIndex]), + body: Padding( + padding: EdgeInsets.all(10), + child: tabPages[_currentIndex], + ), bottomNavigationBar: buildBottomNavBar( currentIndex: _currentIndex, onTap: (index) { diff --git a/lib/views/recipe_detail.dart b/lib/views/recipe_detail.dart index 1c17968..58b039a 100644 --- a/lib/views/recipe_detail.dart +++ b/lib/views/recipe_detail.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.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'; import 'package:food_hub_app/widgets/common/index.dart'; class RecipeDetailPage extends StatefulWidget { @@ -12,6 +13,8 @@ class RecipeDetailPage extends StatefulWidget { class _RecipeDetailState extends State { RecipeDetail recipe = RecipeDetail.getEmpty(); + final GlobalKey _recipeDetailKey = GlobalKey(); + bool _isSharing = false; // 定义三个分类列表 List mainMaterialList = []; // 主料 @@ -36,6 +39,10 @@ class _RecipeDetailState extends State { } void getRecipeMaterial() { + mainMaterialList.clear(); + auxiliaryMaterialList.clear(); + accessoryMaterialList.clear(); + for (var material in recipe.materialList) { switch (material.type) { case '主料': @@ -51,12 +58,45 @@ class _RecipeDetailState extends State { } } + // 分享图片 + Future _shareRecipeAsImage() async { + if (_isSharing) return; + + setState(() { + _isSharing = true; + }); + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final imageName = '${recipe.name}_$timestamp.png'; + await ScreenshotUtil.captureAndShare( + globalKey: _recipeDetailKey, + fileName: imageName, + shareText: '分享菜谱: ${recipe.name}', + ); + + setState(() { + _isSharing = false; + }); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('菜谱信息')), body: SingleChildScrollView( - child: Padding(padding: EdgeInsets.all(10), child: _buildRecipeDetail()), + child: Column( + children: [ + // 用RepaintBoundary包裹要截图的部分 + RepaintBoundary( + key: _recipeDetailKey, + child: Padding( + padding: EdgeInsets.all(10), + child: _buildRecipeDetail(), + ), + ), + _buildButtons(), + ], + ), ), ); } @@ -206,4 +246,17 @@ class _RecipeDetailState extends State { ), ); } + + Widget _buildButtons() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + circleIconButton( + context: context, + icon: _isSharing ? Icons.hourglass_top : Icons.share, + onPressed: _shareRecipeAsImage, + ), + ], + ); + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c0be2ee..47f1218 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,14 @@ import Foundation import file_picker import file_selector_macos +import path_provider_foundation +import share_plus import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index b15fcb4..a6b153a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -661,6 +661,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -749,6 +773,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: @@ -978,6 +1018,46 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 843950f..c7a5d08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,8 @@ dependencies: minio: ^3.5.8 crypto: ^3.0.7 file_picker: ^10.3.3 + share_plus: ^11.0.0 + path_provider: ^2.1.5 dependency_overrides: tdesign_flutter_adaptation: 3.16.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 77ab7a0..5e62361 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a423a02..281df51 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST