Flutter接django后台文件通道

flutter接django后台


python 复制代码
import 'package:flutter/material.dart';  // Material设计库库
import 'package:flutter/cupertino.dart'; // iOS风格组件库

import 'package:webview_flutter/webview_flutter.dart';// WebView核心库
// import './web_channel_controller.dart';// webview文件上传控制器
// import 'package:flutter/gestures.dart';  // 导入手势库,用于处理WebView的手势
// import 'package:webview_flutter_android/webview_flutter_android.dart';  // Android平台WebView支持
// import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; // iOS平台WebView支持
import 'package:flutter/foundation.dart'; // Flutter基础库
import 'package:file_picker/file_picker.dart';// 文件选择器
// import 'dart:io'; // IO操作
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';  // 图片选择器
import 'dart:convert';  // 用于Base64编码
import 'package:flutter/foundation.dart'; // Flutter基础库
import 'dart:io';// 需要导入这个包来使用 HttpException 和 SocketException// IO操作
import 'package:flutter/material.dart';//弹窗颜色要用到颜色组件
import 'package:flutter/cupertino.dart';//iOS风格组件库
import 'package:flutter/gestures.dart';  // 导入手势库,用于处理WebView的手势
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';// WebView核心库
import 'package:webview_flutter_android/webview_flutter_android.dart';// Android平台WebView支持
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';// iOS平台WebView支持
import 'web_page_load_error_prompt.dart';//页面加载错误提示组件【IOS】风格
import 'package:file_picker/file_picker.dart';// 文件选择器
import 'package:image_picker/image_picker.dart';  // 图片选择器
import 'dart:convert';// 用于Base64编码
import 'dart:async';





class OneWebPage extends GetView<WebHistoryController> {
  final String initialUrl;
  final String pageTitle;
  OneWebPage({Key? key, required this.initialUrl, required this.pageTitle}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final web_history_Controller = Get.find<WebHistoryController>();

    // 初始化时加载URL
    WidgetsBinding.instance.addPostFrameCallback((_) {web_history_Controller.loadUrl(initialUrl);});
    
    return PopScope(
      canPop: true,// 指示页面是否可以被弹出
      onPopInvokedWithResult: (didPop, result) async {
        if (web_history_Controller.canGoBack()) {
          await web_history_Controller.goBack();// 在网页历史中后退
        } else {Get.back(); }
      },
      child: SafeArea(
        child: CupertinoPageScaffold(
          backgroundColor: CupertinoColors.systemBackground,
          //导航栏
          navigationBar: CupertinoNavigationBar(
            // 用于调整高度的自定义填充
            padding: const EdgeInsetsDirectional.only(top: 0, start: 2, end: 0,bottom: 3),
            backgroundColor: CupertinoColors.systemBackground.withOpacity(0.8),
            border: null,
            middle: Text(pageTitle,style: TextStyle(fontSize: 14,fontWeight: FontWeight.w600,letterSpacing: -0.41,color: CupertinoColors.label,),),
            leading: Transform.scale(
              scale: 0.85, // Adjust the scale factor as needed
              child: CupertinoNavigationBarBackButton(
                color: CupertinoColors.activeBlue,
                onPressed: () => Get.back(),// 处理返回按钮的点击事件
              ),
            ),

            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                // 后退按钮
                Obx(() => CupertinoButton(
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  onPressed: web_history_Controller.canGoBack() ? () async {await web_history_Controller.goBack();} : null,
                  child: Icon(CupertinoIcons.chevron_back,color: web_history_Controller.canGoBack()? CupertinoColors.activeBlue: CupertinoColors.inactiveGray,),
                )),
                // 前进按钮
                Obx(() => CupertinoButton(
                  padding: const EdgeInsets.symmetric(horizontal: 8),
                  onPressed: web_history_Controller.canGoForward() ? () async {await web_history_Controller.goForward();} : null,
                  child: Icon(CupertinoIcons.chevron_forward,color: web_history_Controller.canGoForward()? CupertinoColors.activeBlue: CupertinoColors.inactiveGray,),
                )),
                // 刷新按钮
                CupertinoButton(padding: const EdgeInsets.symmetric(horizontal: 8),onPressed: web_history_Controller.reload,child: const Icon(CupertinoIcons.refresh,color: CupertinoColors.activeBlue,),),
              ],
            ),
          ),
          child: Stack(
            children: [
              Obx(() {
                final controller = web_history_Controller.webViewController.value;
                return controller != null
                    ? WebViewWidget(controller: controller)
                    : const Center(child: CupertinoActivityIndicator());
              }),
              Obx(() {
                final isLoading = web_history_Controller.isLoading.value;
                final hasController = web_history_Controller.webViewController.value != null;
                return isLoading && hasController
                    ? const Positioned.fill(
                        child: Center(
                          child: CupertinoActivityIndicator(),
                        ),
                      )
                    : const SizedBox.shrink();
              }),
            ],
          ),
        ),
      ),
    );
  }
}


class OneWebBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => WebHistoryController());

  }
}




// 文件上传状态值
enum FileUploadState {
  idle,
  picking,
  uploading,
  success,
  error
}


class WebHistoryController extends GetxController {
  // 添加 ImagePicker 实例
  final _imagePicker = ImagePicker();

  //文件上传状态值
    // 1[状态管理]--->
  // 1.1 页面导航状态
  final RxBool isLoading = true.obs;
  final RxBool hasError = false.obs;
  final RxList<String> history = <String>[].obs;
  final RxInt currentIndex = (-1).obs;
  // ----------------------------------------------------------------
  // 1.2 文件上传状态
  final Rx<FileUploadState> uploadState = FileUploadState.idle.obs;
  final RxDouble uploadProgress = 0.0.obs;
  final RxString uploadError = ''.obs;
  // ----------------------------------------------------------------
    // 1.3 WebView控制器
  final webViewController = Rxn<WebViewController>();
  // ----------------------------------------------------------------

  // [导航功能]--->
  // 1.4 判断是否可以后退
  // 1.5 判断是否可以前进
  bool canGoBack() => currentIndex.value > 0;
  bool canGoForward() => currentIndex.value < history.length - 1;

  @override
  void onInit() {
    super.onInit();
    initializeWebView();
  }

  void initializeWebView() {
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }
    
    final controller = WebViewController.fromPlatformCreationParams(params);
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      AndroidWebViewController androidController = controller.platform as AndroidWebViewController;
      androidController.setMediaPlaybackRequiresUserGesture(false);
    }

    // 配置控制器(只调用一次)
    configureController(controller);
    webViewController.value = controller;
    
    // 等待下一帧再注入脚本,确保 WebView 已经准备好
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _debouncedInject();
      _setupFileUploadChannel();
    });
  }

  void configureController(WebViewController controller) {
    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (String url) {
            isLoading.value = true;
            handlePageStarted(url);
          },
          onPageFinished: (String url) {
            isLoading.value = false;
            // 页面加载完成后注入文件上传脚本
            _debouncedInject();
          },
          onWebResourceError: (WebResourceError error) {
            _handleWebResourceError(error);
          },
          onNavigationRequest: (NavigationRequest request) {
            return NavigationDecision.navigate;
          },
        ),
      );
  }

  // 2[添加资源加载检查]--->
  Future<void> _checkPageLoadComplete() async {
    try {
      final isComplete = await webViewController.value?.runJavaScriptReturningResult(
        'document.readyState === "complete"'
      );
      if (isComplete == true) {
        isLoading.value = false;
      }
    } catch (e) {
      // 忽略JavaScript执行错误
    }
  }

    // 注入文件上传脚本
  void _injectFileUploadScript() {
    webViewController.value?.runJavaScript('''
      (function() {
        // 防止重复初始化
        if (window._fileUploadInitialized) return;
        window._fileUploadInitialized = true;

        // 1[初始化]--->仅处理文件输入框的点击事件
        function initFileInputs() {
          document.querySelectorAll('input[type="file"]').forEach(function(input) {
            if (!input.dataset.initialized) {
              input.dataset.initialized = 'true';
              input.dataset.inputId = 'file_input_' + Math.random().toString(36).substr(2, 9);
              
              // 只处理点击事件,让 Flutter 处理文件选择
              input.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                
                // 发送消息到 Flutter
                window.FileUpload.postMessage(JSON.stringify({
                  'action': 'pick',
                  'type': input.accept.includes('image/') ? 'image' : 'file',
                  'accept': input.accept || '',
                  'inputId': input.dataset.inputId,
                  'multiple': input.multiple
                }));
              });
            }
          });
        }

        // 2[文件处理]--->只负责设置文件到输入框
        window.handleFileSelection = function(inputId, fileData) {
          const input = document.querySelector('input[data-input-id="' + inputId + '"]');
          if (!input) return;
          
          try {
            // 创建文件对象
            const byteCharacters = atob(fileData.data);
            const byteArrays = [];
            
            for (let offset = 0; offset < byteCharacters.length; offset += 512) {
              const slice = byteCharacters.slice(offset, offset + 512);
              const byteNumbers = new Array(slice.length);
              for (let i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
              }
              byteArrays.push(new Uint8Array(byteNumbers));
            }
            
            // 确保使用完整的文件名创建文件对象
            const blob = new Blob(byteArrays, { type: fileData.type });
            const file = new File([blob], fileData.name, { 
              type: fileData.type,
              lastModified: new Date().getTime()
            });
            
            console.log('文件名:', file.name); // 调试日志
            console.log('文件类型:', file.type); // 调试日志
            
            // 设置文件到 input
            const dt = new DataTransfer();
            dt.items.add(file);
            input.files = dt.files;
            
            // 触发 change 事件,让 Django admin 处理预览和其他逻辑
            const event = new Event('change', { bubbles: true });
            input.dispatchEvent(event);
            
          } catch (error) {
            console.error('File processing error:', error);
            window.FileUpload.postMessage(JSON.stringify({
              'action': 'error',
              'message': error.message
            }));
          }
        };

        // 3[监听变化]--->监听新添加的文件输入框
        const observer = new MutationObserver(function(mutations) {
          mutations.forEach(function(mutation) {
            if (mutation.addedNodes.length) {
              initFileInputs();
            }
          });
        });

        observer.observe(document.body, {
          childList: true,
          subtree: true
        });

        // 4[初始化]--->初始化现有的文件输入框
        initFileInputs();
      })();
    ''');
  }

  // 设置文件上传通道
  void _setupFileUploadChannel() {
    webViewController.value?.addJavaScriptChannel(
      'FileUpload',
      onMessageReceived: (JavaScriptMessage message) async {
        try {
          final data = jsonDecode(message.message);
          
          if (data['action'] == 'pick') {
            Map<String, dynamic>? fileData;
            
            if (data['type'] == 'image') {
              // 显示图片来源选择对话框
              final source = await Get.dialog<ImageSource>(
                CupertinoAlertDialog(
                  title: Text('选择图片来源'),
                  actions: [
                    CupertinoDialogAction(
                      child: Text('相机'),
                      onPressed: () => Get.back(result: ImageSource.camera),
                    ),
                    CupertinoDialogAction(
                      child: Text('相册'),
                      onPressed: () => Get.back(result: ImageSource.gallery),
                    ),
                  ],
                ),
              );
              
              if (source != null) {
                fileData = await _pickImage(source);
              }
            } else {
              fileData = await _pickFile(data['accept'] ?? '');
            }
            
            if (fileData != null) {
              final js = '''
                window.handleFileSelection('${data['inputId']}', ${jsonEncode(fileData)});
              ''';
              await webViewController.value?.runJavaScript(js);
            }
          }
        } catch (e) {
          print('处理文件选择错误: $e');
          Get.snackbar(
            '错误',
            '文件处理失败: ${e.toString()}',
            snackPosition: SnackPosition.BOTTOM,
            backgroundColor: Colors.red,
            colorText: Colors.white,
          );
        }
      },
    );
  }

  // 添加图片选择方法
  Future<Map<String, dynamic>?> _pickImage(ImageSource source) async {
    try {
      final XFile? image = await _imagePicker.pickImage(
        source: source,
        imageQuality: 85,
      );

      if (image != null) {
        final bytes = await image.readAsBytes();
        final fileName = image.name;
        
        return {
          'name': fileName,
          'data': base64Encode(bytes),
          'type': 'image/${fileName.split('.').last}',
          'size': bytes.length,
          'extension': fileName.split('.').last
        };
      }
    } catch (e) {
      print('选择图片错误: $e');
      Get.snackbar(
        '错误',
        '选择图片失败: ${e.toString()}',
        snackPosition: SnackPosition.BOTTOM,
        backgroundColor: Colors.red,
        colorText: Colors.white,
      );
    }
    return null;
  }

  // 处理文件选择
  Future<Map<String, dynamic>?> _pickFile(String accept) async {
    try {
      final result = await FilePicker.platform.pickFiles(
        type: FileType.any,
        allowMultiple: false,
        withData: true,
        // 不限制扩展名,让系统处理
        allowedExtensions: null,
      );

      if (result != null && result.files.isNotEmpty) {
        final file = result.files.first;
        if (file.bytes != null) {
          // 添加文件大小限制
          const maxSize = 2048 * 1024 * 1024;
          if (file.size > maxSize) {
            Get.snackbar('错误', '文件大小不能超过2048MB');
            return null;
          }

          // 确保使用完整的文件名(包括扩展名)
          final fileName = file.name;
          final fileExtension = fileName.contains('.') ? fileName.split('.').last : '';
          final mimeType = _getMimeType(fileName);

          print('选择的文件名: $fileName'); // 调试日志
          print('文件扩展名: $fileExtension'); // 调试日志
          print('MIME类型: $mimeType'); // 调试日志

          return {
            'name': fileName,                    // 使用完整文件名
            'data': base64Encode(file.bytes!),   // 文件数据
            'type': mimeType,
            'size': file.size,
            'extension': fileExtension          // 显式包含扩展名
          };
        }
      }
    } catch (e) {
      print('选择文件错误: $e');
      Get.snackbar(
        '错误',
        '选择文件失败: ${e.toString()}',
        snackPosition: SnackPosition.BOTTOM,
        backgroundColor: Colors.red,
        colorText: Colors.white,
      );
    }
    return null;
  }

  // 添加 MIME 类型判断方法
  String _getMimeType(String fileName) {
    final ext = fileName.split('.').last.toLowerCase();
    switch (ext) {
      case 'mp4':
        return 'video/mp4';
      case 'mp3':
        return 'audio/mpeg';
      case 'wav':
        return 'audio/wav';
      case 'pdf':
        return 'application/pdf';
      case 'doc':
        return 'application/msword';
      case 'docx':
        return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
      case 'xls':
        return 'application/vnd.ms-excel';
      case 'xlsx':
        return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
      case 'zip':
        return 'application/zip';
      case 'rar':
        return 'application/x-rar-compressed';
      default:
        return 'application/octet-stream';
    }
  }

  void handlePageStarted(String url) {
    if (history.isEmpty || history[currentIndex.value] != url) {
        if (history.isNotEmpty && currentIndex.value != history.length - 1) {
            history.removeRange(currentIndex.value + 1, history.length);
        }
        history.add(url);
        currentIndex.value = history.length - 1;
    }
    printDebugInfo();
  }




  // 打印调试信息【查看首次加载时是否正常】
  void printDebugInfo() {
    print('History: ${history.toString()}');
    print('Current Index: ${currentIndex.value}');
    print('Can Go Back: ${canGoBack()}');
    print('Can Go Forward: ${canGoForward()}');
  }
 



  ///-------------------------------------------------------------------------------------------
  



  ///-------------------------------------------------------------------------------------------
  
  // 2. 文件上传进度监控
  Future<void> _handleFileResult(Map<String, dynamic> result) async {
    final progressController = RxDouble(0.0);
    try {
      // 显示进度对话框
      Get.dialog(
        Obx(() => CupertinoAlertDialog(
          title: Text('上传中...'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              CupertinoActivityIndicator(),
              SizedBox(height: 10),
              Text('${(progressController.value * 100).toStringAsFixed(1)}%'),
            ],
          ),
        )),
        barrierDismissible: false,
      );

      // 监听上传进度
      if (result['status'] == 'progress') {
        progressController.value = result['progress'] as double;
      } else if (result['status'] == 'complete') {
        await Future.delayed(Duration(milliseconds: 500)); // 稍微延迟以显示100%
        Get.back(); // 关闭进度对话框
        Get.snackbar('成功', '文件上传完成');
      } else if (result['status'] == 'error') {
        Get.back(); // 关闭进度对话框
        Get.snackbar('错误', '上传失败: ${result['error']}');
      }
    } catch (e) {
      Get.back(); // 关闭进度对话框
      print('处理文件结果错误: $e');
      Get.snackbar('错误', '处理文件失败: $e');
    }
  }



  ///-------------------------------------------------------------------------------------------
  /// 加载URL
  Future<void> loadUrl(String url) async {
    final controller = webViewController.value;
    if (controller == null) {Get.snackbar('错误', 'WebView未初始化');return;}
    try {
      isLoading.value = true; // 开始加载时设置
      await controller.loadRequest(Uri.parse(url));
    } catch (e) {
      isLoading.value = false; // 发生错误时设置为 false
      print("加载url失败: ${e.toString()}");
      print("加载url失败");

      // Get.snackbar('错误', '页面加载失败: ${e.toString()}');
      Get.snackbar(
        '错误',
        '页面加载失败: ${e.toString()}',
        snackPosition: SnackPosition.BOTTOM,
        backgroundColor: Colors.red,
        colorText: Colors.white,
        duration: Duration(seconds: 3),
      );
      rethrow;
    }
  }

  Future<void> goBack() async {
    if (!canGoBack()) {Get.back();return;}
    try {
        currentIndex.value--;
        final prevUrl = history[currentIndex.value];
        print('Going back to: $prevUrl');// 添加调试信息
        print('History: ${history.toString()}');
        print('New Index: ${currentIndex.value}');
        printDebugInfo();
        await loadUrl(prevUrl);
    } catch (e) {
        currentIndex.value++;
        _showErrorSnackbar('页面加载失败', _getErrorMessage(e));
        rethrow;
    }
  }

  Future<void> goForward() async {
    if (!canGoForward()) {Get.snackbar('提示', '已经是最后一个页面');return;}
    try {
        // 更新当前索引到下一个位置
        currentIndex.value++;
        final nextUrl = history[currentIndex.value];
        print('Going forward to: $nextUrl'); // 添加调试信息
        print('History: ${history.toString()}');
        print('New Index: ${currentIndex.value}');
        printDebugInfo();
        await loadUrl(nextUrl);
    } catch (e) {
        currentIndex.value--;
        _showErrorSnackbar('页面加载失败', _getErrorMessage(e));
        rethrow;
    }
  }

  String _getErrorMessage(Object e) {
    if (e is SocketException) {
        return '网络连接问题: 请检查您的网络连接。';
    } else if (e is FormatException) {
        return '无效的URL: ${e.message}';
    } else if (e is HttpException) {
        return 'HTTP错误: ${e.message}';
    } else {
        return '发生未知错误: ${e.toString()}';
    }
  }

  /// 刷新当前页面
  /// 检查 WebView 控制器是否已初始化,如果已初始化则刷新页面。
  ///错误处理:在刷新过程中捕获并处理可能的错误。
  Future<void> reload() async {
    final controller = webViewController.value;
    if (controller == null) {Get.snackbar('错误', 'WebView未初始化');return;}

    try {
      isLoading.value = true; // 开始刷新时设置
      await controller.reload();
    } catch (e) {
      isLoading.value = false; // 出错时设置
      // 定义具体的错误消息
      String errorMessage;
      if (e is SocketException) {
        errorMessage = '网络连接问题: 请检查您的网络连接。';
      } else if (e is FormatException) {
        errorMessage = '无效的URL: ${e.message}';
      } else if (e is HttpException) {
        errorMessage = 'HTTP错误: ${e.message}';
      } else {
        errorMessage = '发生未知错误: ${e.toString()}';
      }
      // 显示错误消息
      Get.snackbar(
        '刷新失败',
        errorMessage,
        snackPosition: SnackPosition.BOTTOM,
        backgroundColor: Colors.red,
        colorText: Colors.white,
        duration: Duration(seconds: 3),
      );
      rethrow;
    }
  }


  /// 清理缓存:在 onClose() 方法中调用 webViewController.value?.clearCache();
  /// 是为了在控制器被销毁时清理 WebView 的缓存数据。
  /// 这有助于释放内存资源,减少应用程序的内存占用,从而提高性能和稳定性。
  /// 清理缓存还可以确保在下次使用 WebView 时,加载的内容是最新的。
  ///
  /// 确保父类的 onClose() 被调用:通过调用 super.onClose();,
  /// 确保在自定义的 onClose() 方法执行完之后,
  /// 父类(GetX 的 GetxController)的 onClose() 方法也被调用。
  /// 这样做是为了确保父类的清理工作或其他重要操作不会被忽略。

  @override
  void onClose() {
    webViewController.value?.clearCache();
    // 不要将 webViewController.value 设置为 null,除非有必要
    // webViewController.value = null;
    super.onClose();
  }

  // 添加缺失的错误处理方法
  void _handleWebResourceError(WebResourceError error) {
    isLoading.value = false;
    hasError.value = true;
    
    String errorMessage = '加载失败: ${error.description}';
    if (error.errorCode == -2) {
      errorMessage = '网络连接失败,请检查网络设置';
    } else if (error.errorCode == -6) {
      errorMessage = '无法连接到服务器';
    }
    
    Get.snackbar(
      '页面错误',
      errorMessage,
      snackPosition: SnackPosition.BOTTOM,
      backgroundColor: Colors.red,
      colorText: Colors.white,
      duration: Duration(seconds: 3),
    );
  }

  // 添加缺失的错误提示方法
  void _showErrorSnackbar(String title, String message) {
    Get.snackbar(
      title,
      message,
      snackPosition: SnackPosition.BOTTOM,
      backgroundColor: Colors.red,
      colorText: Colors.white,
      duration: Duration(seconds: 3),
    );
  }

  Timer? _debounceTimer;

  void _debouncedInject() {
    _debounceTimer?.cancel();
    _debounceTimer = Timer(Duration(milliseconds: 300), () {
      _injectFileUploadScript();
    });
  }
}
相关推荐
databook7 小时前
Manim实现闪光轨迹特效
后端·python·动效
Juchecar8 小时前
解惑:NumPy 中 ndarray.ndim 到底是什么?
python
用户8356290780518 小时前
Python 删除 Excel 工作表中的空白行列
后端·python
Json_8 小时前
使用python-fastApi框架开发一个学校宿舍管理系统-前后端分离项目
后端·python·fastapi
孤鸿玉10 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
数据智能老司机15 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机16 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机16 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机16 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i16 小时前
drf初步梳理
python·django