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();
    });
  }
}
相关推荐
wanfeng_091 分钟前
视频m3u8形式播放 -- python and html
python·html·video·hls·m3u8
阿俊仔(摸鱼版)25 分钟前
Python 常用运维模块之OS模块篇
运维·开发语言·python·云服务器
昆仑道长32 分钟前
ARM64平台Flutter环境搭建
flutter
sunly_32 分钟前
Flutter:自定义Tab切换,订单列表页tab,tab吸顶
开发语言·javascript·flutter
2401_8979078639 分钟前
10天学会flutter DAY2 玩转dart 类
android·flutter
前端没钱40 分钟前
flutter入门系列教程<一>:tab组件的灵活妙用
flutter
lly_csdn1231 小时前
【Image Captioning】DynRefer
python·深度学习·ai·图像分类·多模态·字幕生成·属性识别
西猫雷婶1 小时前
python学opencv|读取图像(四十一 )使用cv2.add()函数实现各个像素点BGR叠加
开发语言·python·opencv
金融OG1 小时前
99.11 金融难点通俗解释:净资产收益率(ROE)VS投资资本回报率(ROIC)VS总资产收益率(ROA)
大数据·python·算法·机器学习·金融
小唐C++3 小时前
C++小病毒-1.0勒索
开发语言·c++·vscode·python·算法·c#·编辑器