浏览代码

Fix 修复导出excle无法看到的问题

Yue 1 天之前
父节点
当前提交
a3469b15e3

+ 2 - 1
UI/CF.APP/chicken_farm/android/app/src/main/AndroidManifest.xml

@@ -21,7 +21,8 @@
     <application
         android:label="广明养殖"
         android:name="${applicationName}"
-        android:icon="@mipmap/gm_logo">
+        android:icon="@mipmap/gm_logo"
+        android:requestLegacyExternalStorage="true">
         <activity
             android:name=".MainActivity"
             android:exported="true"

+ 29 - 19
UI/CF.APP/chicken_farm/android/app/src/main/java/com/vber/chicken_farm/MainActivity.java

@@ -7,6 +7,7 @@ import io.flutter.embedding.engine.FlutterEngine;
 import android.util.Log;
 import android.view.KeyEvent;
 
+import com.vber.chicken_farm.common.VberMethodCallHandler;
 import com.vber.chicken_farm.scan.ScanMethodCallHandler;
 import com.vber.chicken_farm.rfid.RfidMethodCallHandler;
 import com.rfid.InventoryTagMap;
@@ -14,32 +15,38 @@ import com.rfid.trans.TagCallback;
 
 import java.util.List;
 
-
 public class MainActivity extends FlutterActivity {
 
     private static final String TAG = "MainActivity";
 
     private ScanMethodCallHandler scanMethodCallHandler;
     private RfidMethodCallHandler rfidMethodCallHandler;
-    
+    private VberMethodCallHandler vberMethodCallHandler;
+
     // 双击检测相关变量
     private static final long DOUBLE_CLICK_TIME_DELTA = 300; // 双击时间间隔阈值,单位毫秒
     private long lastKeyDownTime = 0; // 上次按键按下的时间
     private int lastKeyCode = -1; // 上次按下的按键码
-    
+
     @Override
     public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
         super.configureFlutterEngine(flutterEngine);
         // 初始化并注册扫码MethodChannel处理器
         scanMethodCallHandler = new ScanMethodCallHandler(
-            getApplicationContext(), // 全局Context,避免Activity生命周期影响
-            flutterEngine.getDartExecutor().getBinaryMessenger() // Flutter通信信使
+                getApplicationContext(), // 全局Context,避免Activity生命周期影响
+                flutterEngine.getDartExecutor().getBinaryMessenger() // Flutter通信信使
         );
 
         // 初始化并注册RFID MethodChannel处理器
         rfidMethodCallHandler = new RfidMethodCallHandler(
-            getApplicationContext(), // 全局Context,避免Activity生命周期影响
-            flutterEngine.getDartExecutor().getBinaryMessenger() // Flutter通信信使
+                getApplicationContext(), // 全局Context,避免Activity生命周期影响
+                flutterEngine.getDartExecutor().getBinaryMessenger() // Flutter通信信使
+        );
+
+        // 初始化并注册RFID MethodChannel处理器
+        vberMethodCallHandler = new VberMethodCallHandler(
+                getApplicationContext(), // 全局Context,避免Activity生命周期影响
+                flutterEngine.getDartExecutor().getBinaryMessenger() // Flutter通信信使
         );
     }
 
@@ -57,29 +64,33 @@ public class MainActivity extends FlutterActivity {
             rfidMethodCallHandler.dispose();
             rfidMethodCallHandler = null;
         }
+
+        // 释放Vber资源
+        if (vberMethodCallHandler != null) {
+            vberMethodCallHandler.dispose();
+            vberMethodCallHandler = null;
+        }
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        
-        
+
         Log.d(TAG, "MainActivity创建完成");
     }
 
-    
     @Override
     protected void onPause() {
         super.onPause();
-        
+
         Log.d(TAG, "MainActivity暂停");
 
     }
-    
+
     @Override
     protected void onResume() {
         super.onResume();
-        
+
         Log.d(TAG, "MainActivity恢复");
 
     }
@@ -87,15 +98,14 @@ public class MainActivity extends FlutterActivity {
     @Override
     protected void onDestroy() {
         super.onDestroy();
-      
-       
+
         Log.d(TAG, "MainActivity销毁,资源已释放");
     }
 
-     @Override
+    @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         Log.d(TAG, "按键按下: " + keyCode);
-        
+
         // 处理622和623按键事件
         if (keyCode == 622 || keyCode == 623) {
             if (scanMethodCallHandler != null) {
@@ -109,7 +119,7 @@ public class MainActivity extends FlutterActivity {
             // 检查是否为双击(相同按键且在时间阈值内)
             if (lastKeyCode == keyCode && (currentTime - lastKeyDownTime) < DOUBLE_CLICK_TIME_DELTA) {
                 // 双击事件
-                lastKeyCode = -1; 
+                lastKeyCode = -1;
                 Log.d(TAG, "按键双击: " + keyCode);
                 if (scanMethodCallHandler != null) {
                     scanMethodCallHandler.onDoubleKeyEvent(keyCode);
@@ -132,7 +142,7 @@ public class MainActivity extends FlutterActivity {
                 }, DOUBLE_CLICK_TIME_DELTA);
                 return true;
             }
-        }        
+        }
         // 其他按键处理
         return super.onKeyDown(keyCode, event);
     }

+ 138 - 0
UI/CF.APP/chicken_farm/android/app/src/main/java/com/vber/chicken_farm/common/VberMethodCallHandler.java

@@ -0,0 +1,138 @@
+package com.vber.chicken_farm.common;
+
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class VberMethodCallHandler implements MethodCallHandler, EventChannel.StreamHandler {
+  private static final String TAG = "VberMethodCallHandler";
+  // Channel名称(需与Flutter端保持一致)
+  public static final String METHOD_CHANNEL_NAME = "com.vber.chicken_farm/vber";
+  public static final String EVENT_CHANNEL_NAME = "com.vber.chicken_farm/vber_events";
+
+  private final Context context;
+  private MethodChannel methodChannel;
+  private EventChannel.EventSink eventSink;
+  private final Handler mainHandler;
+
+  /**
+   * 构造函数
+   */
+  public VberMethodCallHandler(Context context, BinaryMessenger messenger) {
+    this.context = context.getApplicationContext();
+    this.mainHandler = new Handler(Looper.getMainLooper());
+    methodChannel = new MethodChannel(messenger, METHOD_CHANNEL_NAME);
+    methodChannel.setMethodCallHandler(this);
+    EventChannel eventChannel = new EventChannel(messenger, EVENT_CHANNEL_NAME);
+    eventChannel.setStreamHandler(this);
+    Log.d(TAG, "VBER Flutter MethodChannel初始化完成");
+  }
+
+  // -------------------------- MethodChannel实现 --------------------------
+  @Override
+  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+    Log.d(TAG, "收到Flutter方法调用: " + call.method);
+    switch (call.method) {
+      case "getPublicDownloadDir":
+        handleGetPublicDownloadDir(result);
+        break;
+      case "scanFile":
+        String filePath = call.argument("filePath");
+        handleScanFile(filePath, result);
+        break;
+      default:
+        result.notImplemented();
+        break;
+
+    }
+
+  }
+
+  public void handleGetPublicDownloadDir(Result result) {
+    // 获取Android系统公共下载目录
+    String downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+        .getAbsolutePath();
+    Log.i(TAG, "公共下载目录路径:" + downloadPath);
+    result.success(downloadPath);
+  }
+
+  public void handleScanFile(String filePath, Result result) {
+    if (filePath == null || filePath.isEmpty()) {
+      result.error("INVALID_PATH", "文件路径为空", null);
+      return;
+    }
+    // 触发媒体扫描,让文件在文件管理器显示
+    MediaScannerConnection.scanFile(
+        context,
+        new String[] { filePath },
+        null,
+        (path, uri) -> {
+          Log.i(TAG, "媒体扫描完成:" + path);
+          result.success(true);
+        });
+  }
+
+  // -------------------------- EventChannel实现 --------------------------
+  @Override
+  public void onListen(Object arguments, EventChannel.EventSink events) {
+    this.eventSink = events;
+    Log.d(TAG, "VBER事件监听已注册");
+  }
+
+  @Override
+  public void onCancel(Object arguments) {
+    this.eventSink = null;
+    Log.d(TAG, "VBER事件监听已取消");
+  }
+
+  /**
+   * 发送事件到Flutter(确保在主线程执行)
+   */
+  private void sendEvent(int type, Object data, String msg, String error) {
+    if (eventSink == null) {
+      Log.w(TAG, "事件接收器未初始化,忽略事件发送");
+      return;
+    }
+
+    mainHandler.post(() -> {
+      Map<String, Object> event = new HashMap<>();
+      event.put("type", type);
+      if (data != null) {
+        event.put("data", data);
+      }
+      event.put("msg", msg);
+      event.put("error", error);
+      eventSink.success(event);
+    });
+  }
+
+  /**
+   * 释放资源
+   */
+  public void dispose() {
+    if (methodChannel != null) {
+      methodChannel.setMethodCallHandler(null);
+    }
+    eventSink = null;
+    Log.d(TAG, "VBER桥接处理器资源已释放");
+  }
+
+}

+ 34 - 0
UI/CF.APP/chicken_farm/lib/core/services/common_channel.dart

@@ -0,0 +1,34 @@
+import 'dart:io';
+
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:flutter/services.dart';
+
+class CommonChannel {
+  static const MethodChannel _channel = MethodChannel(
+    'com.vber.chicken_farm/vber',
+  );
+
+  /// 通过Android原生Environment获取公共下载目录(需MethodChannel)
+  static Future<Directory?> getAndroidPublicDownloadDir() async {
+    try {
+      final path = await _channel.invokeMethod<String>('getPublicDownloadDir');
+      if (path != null && path.isNotEmpty) {
+        return Directory(path);
+      }
+    } catch (e) {
+      logger.e("获取Android公共下载目录失败:$e");
+    }
+    return null;
+  }
+
+  /// 触发Android媒体扫描,让文件显示在文件管理器
+  static Future<void> scanFile(String filePath) async {
+    if (!Platform.isAndroid) return;
+    try {
+      await _channel.invokeMethod('scanFile', {'filePath': filePath});
+      logger.d("媒体扫描触发成功:$filePath");
+    } catch (e) {
+      logger.e("媒体扫描失败:$e");
+    }
+  }
+}

+ 68 - 40
UI/CF.APP/chicken_farm/lib/core/utils/excel_export_util.dart

@@ -7,6 +7,8 @@ import 'package:excel/excel.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:permission_handler/permission_handler.dart';
 import 'package:open_filex/open_filex.dart';
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:media_scanner/media_scanner.dart';
 
 /// Excel 导出工具类(仅支持 excel: ^4.0.0)
 class ExcelExportUtil {
@@ -60,20 +62,10 @@ class ExcelExportUtil {
       }
       if (fileName.isEmpty) throw Exception('文件名不能为空');
 
-      // 2. Android权限申请
-      if (Platform.isAndroid) {
-        final storagePermission = Permission.manageExternalStorage;
-
-        // 检查是否有管理外部存储权限
-        if (!await storagePermission.isGranted) {
-          // 尝试请求权限
-          var status = await storagePermission.request();
-          if (!status.isGranted) {
-            // 如果管理外部存储权限被拒绝,引导用户去设置页面
-            await openAppSettings();
-            throw Exception('存储权限被拒绝,请在设置中开启存储权限');
-          }
-        }
+      // 2. 检查权限
+      final hasPermission = await _checkAndRequestStoragePermission();
+      if (!hasPermission) {
+        throw Exception('存储权限被拒绝');
       }
 
       // 3. 获取表头和字段映射
@@ -131,7 +123,7 @@ class ExcelExportUtil {
         sheet.setColumnWidth(col, 22);
       }
 
-      // 8. 处理保存路径(核心修改:创建自定义文件夹)
+      // 8. 处理保存路径
       // 8.1 获取下载目录
       final downloadDir = await _getSafeDownloadDirectory();
 
@@ -139,7 +131,7 @@ class ExcelExportUtil {
       final customDir = Directory('${downloadDir.path}/$filePath');
       if (!await customDir.exists()) {
         await customDir.create(recursive: true);
-        logger.d('自定义文件夹创建成功:${customDir.path}');
+        // logger.d('自定义文件夹创建成功:${customDir.path}');
       }
 
       // 8.3 处理文件名(过滤特殊字符)
@@ -149,6 +141,10 @@ class ExcelExportUtil {
       // 8.4 保存文件
       await File(fullFilePath).writeAsBytes(excel.save()!);
 
+      // 8.5. 触发媒体扫描(让文件管理器显示)
+      // await CommonChannel.scanFile(fullFilePath);
+      await MediaScanner.loadMedia(path: fullFilePath);
+
       // 9. 打开文件
       await OpenFilex.open(fullFilePath);
       logger.d('导出成功:$fullFilePath');
@@ -159,30 +155,33 @@ class ExcelExportUtil {
     }
   }
 
-  /// 安全地获取下载目录路径
+  static Future<bool> _checkAndRequestStoragePermission() async {
+    if (!Platform.isAndroid) return true;
+
+    final androidInfo = await DeviceInfoPlugin().androidInfo;
+    final sdkInt = androidInfo.version.sdkInt;
+
+    // Android 11+(API 30+):写入Download公共目录无需权限,无需请求manageExternalStorage
+    if (sdkInt >= 30) {
+      return true;
+    }
+    // Android 10(API 29):依赖requestLegacyExternalStorage,请求普通存储权限
+    else if (sdkInt == 29) {
+      final status = await Permission.storage.request();
+      return status.isGranted;
+    }
+    // Android 9及以下(API ≤28):请求普通存储权限
+    else {
+      final status = await Permission.storage.request();
+      return status.isGranted;
+    }
+  }
+
+  /// 安全获取系统公共下载目录(修复Android路径问题)
   static Future<Directory> _getSafeDownloadDirectory() async {
     // 获取下载目录
     Directory? downloadDir = await getDownloadsDirectory();
 
-    // if (downloadDir == null) {
-    //   logger.e("获取下载目录失败,换种方法重新获取...");
-    //   if (Platform.isAndroid) {
-    //     // Android 下载目录
-    //     downloadDir = await getExternalStorageDirectory();
-    //     logger.i("Android 根目录: ${downloadDir?.path}");
-    //     if (downloadDir != null) {
-    //       downloadDir = Directory(
-    //         '${downloadDir.parent.parent.parent.parent}/Download',
-    //       );
-    //       logger.i("Android 下载目录: ${downloadDir.path}");
-    //     }
-    //   } else if (Platform.isIOS) {
-    //     // iOS 下载目录
-    //     downloadDir = await getApplicationSupportDirectory();
-    //     downloadDir = Directory('${downloadDir.path}/Download');
-    //   }
-    // }
-
     // 安全地处理 Android 下载目录路径
     if (downloadDir != null) {
       try {
@@ -205,23 +204,52 @@ class ExcelExportUtil {
         // 如果成功向上访问了足够的层级,则构造新的下载目录路径
         if (levelsUp >= 4) {
           downloadDir = Directory('${parentDir!.path}/Download');
-          logger.i("重构后的 Android 下载目录: ${downloadDir.path}");
+          // logger.i("重构后的 Android 下载目录: ${downloadDir.path}");
         } else {
           // 向上访问层级不够,使用原始下载目录
           logger.w("无法安全重构下载目录路径,使用原始下载目录: ${downloadDir.path}");
         }
       } catch (e) {
-        // 出现任何异常都回退到原始下载目录
+        // 处理异常
+        logger.e("获取下载目录异常:$e");
+        downloadDir = await getExternalStorageDirectory();
+        downloadDir = Directory('${downloadDir?.path}/Download');
       }
     }
 
-    logger.i("下载目录: ${downloadDir?.path}");
+    // 确保目录存在
     downloadDir ??= await getApplicationDocumentsDirectory();
-    logger.i("下载目录2: ${downloadDir.path}");
+    if (!await downloadDir.exists()) {
+      await downloadDir.create(recursive: true);
+    }
 
+    // logger.i("最终下载目录:${downloadDir.path}");
     return downloadDir;
   }
 
+  // /// 安全地获取下载目录路径
+  // static Future<Directory> _getSafeDownloadDirectory() async {
+  //   Directory? downloadDir;
+
+  //   if (Platform.isAndroid) {
+  //     // Android 下载目录(/storage/emulated/0/Download)
+  //     downloadDir = await getExternalStorageDirectory();
+  //     if (downloadDir != null) {
+  //       downloadDir = Directory(
+  //         '${downloadDir.parent.parent.parent.path}/Download',
+  //       );
+  //     }
+  //   } else if (Platform.isIOS) {
+  //     // iOS 下载目录(App 沙盒内的 Downloads,用户可通过文件 App 访问)
+  //     downloadDir = await getApplicationDocumentsDirectory();
+  //     downloadDir = Directory('${downloadDir.path}/Download');
+  //   }
+  //   downloadDir ??= await getApplicationDocumentsDirectory();
+  //   logger.i("下载目录2: ${downloadDir.path}");
+
+  //   return downloadDir;
+  // }
+
   /// 获取已注册的模板列表
   static List<String> getRegisteredTemplates() =>
       _headerTemplates.keys.toList();

+ 2 - 0
UI/CF.APP/chicken_farm/macos/Flutter/GeneratedPluginRegistrant.swift

@@ -7,6 +7,7 @@ import Foundation
 
 import audioplayers_darwin
 import connectivity_plus
+import device_info_plus
 import file_selector_macos
 import flutter_image_compress_macos
 import mobile_scanner
@@ -17,6 +18,7 @@ import sqflite_darwin
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
   ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
+  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
   FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
   MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))

+ 2 - 0
UI/CF.APP/chicken_farm/pubspec.yaml

@@ -64,6 +64,8 @@ dependencies:
   open_filex: ^4.3.2
    # 权限处理(Android/iOS)
   permission_handler: ^11.3.1
+  device_info_plus: ^12.3.0
+  media_scanner: ^2.2.0
   
 
 dev_dependencies: