flutter接django背景


- 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();
- });
- }
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |