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();
});
}
}