道家人 发表于 2025-1-24 15:23:55

Flutter接django背景文件通道

flutter接django背景
https://i-blog.csdnimg.cn/direct/a78f15cffad64d8d93429629380bfc01.png
https://i-blog.csdnimg.cn/direct/0f14b28890b2497790e300312afe4d69.png
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').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');
          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 = slice.charCodeAt(i);
            }
            byteArrays.push(new Uint8Array(byteNumbers));
            }
            
            // 确保使用完整的文件名创建文件对象
            const blob = new Blob(byteArrays, { type: fileData.type });
            const file = new File(, 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 != 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;
      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;
      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();
    });
}
}

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Flutter接django背景文件通道