首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >navigatorState在使用pushNamed导航onGenerateRoutes of GetMaterialPage时为空

navigatorState在使用pushNamed导航onGenerateRoutes of GetMaterialPage时为空
EN

Stack Overflow用户
提问于 2022-07-03 09:58:16
回答 1查看 575关注 0票数 2

我使用传入包在我的应用程序的所有状态下通过FCM的有效负载获取应用程序中的callsNotification,即背景/FCM/终止状态。

在我的应用程序的基本状态下,单击“呼叫时接受”按钮后,导航就可以到达VideoCallingAgoraPage了。->使用来自NikahMatch类的listenerEvent

但是当这个listenerEvent用于在后台/终止状态下导航时会出现问题。 ->使用listenerEvent作为顶层函数,因为后台处理程序如下所示

当编译器在侦听器事件中读取这一行await NavigationService.instance.pushNamed(AppRoute.voiceCall);时,在我的应用程序的后台/终止状态下单击“接受来自flutter_callKit_incoming的通知”,我将在控制台中得到这个错误。

代码语言:javascript
运行
复制
E/flutter (11545): Receiver: null
E/flutter (11545): Tried calling: pushNamed<Object>("/videoCall_agora", arguments: null)
E/flutter (11545): #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:38:5)
E/flutter (11545): #1      NavigationService.pushNamed (package:nikah_match/helpers/navigationService.dart:38:39)
E/flutter (11545): #2      listenerEvent.<anonymous closure> (package:nikah_match/main.dart:311:46)
E/flutter (11545): <asynchronous suspension>
E/flutter (11545): 

在日志中,我发现在pushNamed函数中定义的pushNamed也是null。在地面导航的情况下,来自navigationKey.currentState函数的pushNamed从不为null。

当我收到终止状态的呼叫通知时,调用侦听器事件的接受大小写(顶级函数),而不创建小部件树并初始化导致导航器状态为null的GetMaterialPage。

我认为在启动/构建小部件树之前运行了listnerEvent接受情况,并且从未分配GetMaterialPage中的导航器键。

我怎么才能摆脱它?

这是我的backgroundHandler函数:

代码语言:javascript
运行
复制
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  bool videoCallEnabled = false;
  bool audioCallEnabled = false;

  if (message != null) {
    debugPrint("Handling background is called");
    print(
        "Handling a background message and background handler: ${message.messageId}");
    try {
      videoCallEnabled = message.data.containsKey('videoCall');
      audioCallEnabled = message.data.containsKey('voiceCall');

      if (videoCallEnabled || audioCallEnabled) {
        log("Video call is configured and is started");
        showCallkitIncoming(Uuid().v4(), message: message);
        //w8 for streaming
        debugPrint("Should listen to events in background/terminated state");
        listenerEvent(message);
      } else {
        log("No Video or audio call was initialized");
      }
    } catch (e) {
      debugPrint("Error occured:" + e.toString());
    }
  }

}

这是我的侦听器事件:

代码语言:javascript
运行
复制
 Future<void> listenerEvent(RemoteMessage message) async {


  log("Listner event background/terminated handler app has run");
  backgroundChatRoomId = message.data['chatRoomId'];
  backgroundCallsDocId = message.data['callsDocId'];
  backgroundRequesterName = message.data['callerName'];
  backgroundRequesterImageUrl = message.data['imageUrl'];
  // String imageUrl = message.data['imageUrl'];

  bool videoCallEnabled = false;


  if (message.data != null) {
    videoCallEnabled = message.data.containsKey('videoCall');
  } else {
    log("Data was null");
  }
  try {
    FlutterCallkitIncoming.onEvent.listen((event) async {
      print('HOME: $event');
      switch (event.name) {
        case CallEvent.ACTION_CALL_INCOMING:
        // TODO: received an incoming call
          log("Call is incoming");
          break;
        case CallEvent.ACTION_CALL_START:
        // TODO: started an outgoing call
        // TODO: show screen calling in Flutter
          log("Call is started");
          break;
        case CallEvent.ACTION_CALL_ACCEPT:
        // TODO: accepted an incoming call
        // TODO: show screen calling in Flutter
          log("......Call Accepted background/terminated state....");
          currentChannel = backgroundChatRoomId;
          log("currentChannel in accepted is: $currentChannel");
          debugPrint("Details of call"+backgroundChatRoomId+backgroundCallsDocId );
          await FirebaseFirestore.instance
              .collection("ChatRoom")
              .doc(backgroundChatRoomId)
              .collection("calls")
              .doc(backgroundCallsDocId)
              .update({
            'receiverCallResponse': 'Accepted',
            'callResponseDateTime': FieldValue.serverTimestamp()
          }).then((value) => log("Values updated at firebase firestore as Accepted"));

          if (videoCallEnabled) {
            log("in video call enabled in accept call of listener event");
            await NavigationService.instance.pushNamed(AppRoute.videoAgoraCall,);
           
          } 
          break;

      }
    });
  } on Exception {}
}

这是我的第一个有状态GetMaterial页面,它初始化了所有Firebase消息传递函数(为了提高可读性,代码中排除了本地FLutter本地通知):

代码语言:javascript
运行
复制
class NikkahMatch extends StatefulWidget {
  const NikkahMatch({Key key}) : super(key: key);

  @override
  State<NikkahMatch> createState() => _NikkahMatchState();
}

class _NikkahMatchState extends State<NikkahMatch> with WidgetsBindingObserver {


  


    @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);
//Function if from terminated state
    FirebaseMessaging.instance.getInitialMessage().then((message) async {

      log("get Initial Message function is used.. ");

      String screenName = 'No screen';
      bool screenEnabled = false;
      if (message != null) {
        if (message.data != null) {
          log("Remote message data is null for now");
          if (message.data.isNotEmpty) {
            screenEnabled = message.data.containsKey('screenName');
            if (screenEnabled) {
              if (screenName == 'chatScreen') {
                log("Screen is Chat");
                String type = 'Nothing';
                String chatRoomId = 'Nothing';
                if (message.data['type'] != null) {
                  type = message.data['type'];
                  if (type == 'profileMatched') {
                    String likerId = message.data['likerId'];
                    String likedId = message.data['likedId'];
                    chatRoomId = chatController.getChatRoomId(likerId, likedId);
                  }
                } else {
                  chatRoomId = message.data['chatRoomId'];
                }

                log("ChatRoom Id is: ${chatRoomId}");
                log("Navigating from onMessagePop to the ChatRoom 1");
                //We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
                await FirebaseFirestore.instance
                    .collection("ChatRoom")
                    .doc(chatRoomId)
                    .get()
                    .then((value) async {
                  if (value.exists) {
                    log("ChatRoom Doc " + value.toString());
                    log("Navigating from onMessagePop to the ChatRoom 2");
                    log("Last Message was : ${value.data()['lastMessage']}");
                    backGroundLevelChatRoomDoc = value.data();
                   
                    await NavigationService.instance.pushNamed(AppRoute.chatScreen);
                  } else {
                    log("no doc exist for chat");
                  }
                });
              }

          else if (screenName == 'videoScreen') {
                log("Screen is Video");
                initCall(message);
              } else if (screenName == 'voiceScreen') {
                log("Screen is Audio");
                initCall(message);
              } else {
                log("Screen is in Else method of getInitialMessage");
              }
            
            } else {
              debugPrint("Notification Pay load data is Empty");
            }
          } else {
            log("Screen isn't enabled");
          }
        } else {
          log("message data is null");
        }
      } else {
        log("...........message data is null in bahir wala else");
      }
    });

   
    //This function will constantly listen to the notification recieved from firebase

    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
      log("onMessageOpenedApp function is used.. ");
      String screenName = 'No screen';
      bool screenEnabled = false;
      if (message.data.isNotEmpty) {
        screenEnabled = message.data.containsKey('screenName');
        if (screenEnabled) {
          //Move to the screen which is needed to
          log("Screen is Enabled");
          screenName = message.data['screenName'];
          log("Screen name is: $screenName");

          if (screenName == 'chatScreen') {
            log("Screen is Chat");
            String type = 'Nothing';
            String chatRoomId = 'Nothing';
            if (message.data['type'] != null) {
              type = message.data['type'];
              if (type == 'profileMatched') {
                String likerId = message.data['likerId'];
                String likedId = message.data['likedId'];
                chatRoomId = chatController.getChatRoomId(likerId, likedId);
              }
            } else {
              chatRoomId = message.data['chatRoomId'];
            }

            log("ChatRoom Id is: ${chatRoomId}");
            log("Navigating from onMessagePop to the ChatRoom 1");
            //We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
            await FirebaseFirestore.instance
                .collection("ChatRoom")
                .doc(chatRoomId)
                .get()
                .then((value) async {
              if (value.exists) {
                log("ChatRoom Doc " + value.toString());
                log("Navigating from onMessagePop to the ChatRoom 2");
                log("Last Message was : ${value.data()['lastMessage']}");
                backGroundLevelChatRoomDoc = value.data();
                /*     await NavigationService.instance
                    .pushNamed(AppRoute.chatScreen, args:ChatArgs(value.data(), false));*/
                await NavigationService.instance.pushNamed(AppRoute.chatScreen);
              } else {
                log("no doc exist for chat");
              }
            });
          }

       else if (screenName == 'videoScreen') {
            log("Screen is Video");
            initCall(message);
          } else if (screenName == 'voiceScreen') {
            log("Screen is Audio");
            initCall(message);
          } else {
            log("Screen is in Else");
          }
        }
      } else {
        debugPrint("Notification Pay load data is Empty");
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    print("Main page build");
    return GetMaterialApp(
      onGenerateRoute: AppRoute.generateRoute,
      debugShowCheckedModeBanner: false,
      navigatorKey: NavigationService.instance.navigationKey,
      debugShowMaterialGrid: false,
      title: 'Nikah Match',
      initialRoute: '/splash_screen',
      theme: ThemeData(
        fontFamily: 'Poppins',
        scaffoldBackgroundColor: kScaffoldBgColor,
        appBarTheme: const AppBarTheme(
          elevation: 0,
          backgroundColor: kPrimaryColor,
        ),
        accentColor: kPrimaryColor.withOpacity(0.2),
      ),
      themeMode: ThemeMode.light,
      getPages: [
        GetPage(name: '/splash_screen', page: () => SplashScreen()),
        GetPage(name: '/get_started', page: () => GetStarted()),
        GetPage(
          name: '/videoCall_agora',
          page: () => VideoCallAgoraUIKit(
            anotherUserName: backgroundRequesterName,
            anotherUserImage: backgroundRequesterImageUrl,
            channelName: backgroundChatRoomId,
            token: "",
            anotherUserId: "",
            docId: backgroundCallsDocId,
            callDoc: backgroundPassableAbleCdm,
          ),
        ),
        // GetPage(name: '/after_log_in_screen', page: () => AfterLogin()),
      ],
    );
  }
}

这是我的NavigationService课程:

代码语言:javascript
运行
复制
class NavigationService {
  // Global navigation key for whole application
  GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>();

  /// Get app context
  BuildContext get appContext => navigationKey.currentContext;

  /// App route observer
  RouteObserver<Route<dynamic>> routeObserver = RouteObserver<Route<dynamic>>();

  static final NavigationService _instance = NavigationService._private();
  factory NavigationService() {
    return _instance;
  }
  NavigationService._private();

  static NavigationService get instance => _instance;

  /// Pushing new page into navigation stack
  ///
  /// `routeName` is page's route name defined in [AppRoute]
  /// `args` is optional data to be sent to new page
  Future<T> pushNamed<T extends Object>(String routeName,
      {Object args}) async {
    log(navigationKey.toString());
    log(navigationKey.currentState.toString());
    return navigationKey.currentState.pushNamed<T>(
      routeName,
      arguments: args,
    );
  }

  Future<T> pushNamedIfNotCurrent<T extends Object>(String routeName,
      {Object args}) async {
    if (!isCurrent(routeName)) {
      return pushNamed(routeName, args: args);
    }
    return null;
  }

  bool isCurrent(String routeName) {
    bool isCurrent = false;
    navigationKey.currentState.popUntil((route) {
      if (route.settings.name == routeName) {
        isCurrent = true;
      }
      return true;
    });
    return isCurrent;
  }

  /// Pushing new page into navigation stack
  ///
  /// `route` is route generator
  Future<T> push<T extends Object>(Route<T> route) async {
    return navigationKey.currentState.push<T>(route);
  }

  /// Replace the current route of the navigator by pushing the given route and
  /// then disposing the previous route once the new route has finished
  /// animating in.
  Future<T> pushReplacementNamed<T extends Object, TO extends Object>(
      String routeName,
      {Object args}) async {
    return navigationKey.currentState.pushReplacementNamed<T, TO>(
      routeName,
      arguments: args,
    );
  }

  /// Push the route with the given name onto the navigator, and then remove all
  /// the previous routes until the `predicate` returns true.
  Future<T> pushNamedAndRemoveUntil<T extends Object>(
      String routeName, {
        Object args,
        bool Function(Route<dynamic>) predicate,
      }) async {
    return navigationKey.currentState.pushNamedAndRemoveUntil<T>(
      routeName,
      predicate==null?  (_) => false: (_) => true,
      arguments: args,
    );
  }

  /// Push the given route onto the navigator, and then remove all the previous
  /// routes until the `predicate` returns true.
  Future<T> pushAndRemoveUntil<T extends Object>(
      Route<T> route, {
        bool Function(Route<dynamic>) predicate,
      }) async {
    return navigationKey.currentState.pushAndRemoveUntil<T>(
      route,
      predicate==null?  (_) => false: (_) => true,
    );
  }

  /// Consults the current route's [Route.willPop] method, and acts accordingly,
  /// potentially popping the route as a result; returns whether the pop request
  /// should be considered handled.
  Future<bool> maybePop<T extends Object>([Object args]) async {
    return navigationKey.currentState.maybePop<T>(args as T);
  }

  /// Whether the navigator can be popped.
  bool canPop() => navigationKey.currentState.canPop();

  /// Pop the top-most route off the navigator.
  void goBack<T extends Object>({T result}) {
    navigationKey.currentState.pop<T>(result);
  }

  /// Calls [pop] repeatedly until the predicate returns true.
  void popUntil(String route) {
    navigationKey.currentState.popUntil(ModalRoute.withName(route));
  }
}
class AppRoute {
  static const homePage = '/home_page';

  static const chatScreen ='/chat_screen';

  static const splash = '/splash_screen';
  static const voiceCall = '/voice_call';
  static const videoAgoraCall = '/videoCall_agora';

  static Route<Object> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case homePage:
        return MaterialPageRoute(
            builder: (_) => HomePage(), settings: settings);
      case chatScreen:

        return MaterialPageRoute(
            builder: (_) =>
                ChatScreen(docs: backGroundLevelChatRoomDoc, isArchived: false,), settings: settings);
        case splash:
        return MaterialPageRoute(
            builder: (_) =>  SplashScreen(), settings: settings);
      case voiceCall:
        return MaterialPageRoute(
            builder: (_) =>  VoiceCall(
              toCallName: backgroundRequesterName,
              toCallImageUrl: backgroundRequesterImageUrl,
              channelName: backgroundChatRoomId,
              token: voiceCallToken,
              docId: backgroundCallsDocId,
              callDoc: backgroundPassableAbleCdm,
            ), settings: settings);
      case videoAgoraCall:
        return MaterialPageRoute(
            builder: (_) =>  VideoCallAgoraUIKit(
              anotherUserName: backgroundRequesterName,
              anotherUserImage: backgroundRequesterImageUrl,
              channelName: backgroundChatRoomId,
              token: "",
              anotherUserId: "",
              docId: backgroundCallsDocId,
              callDoc: backgroundPassableAbleCdm,
            ), settings: settings);

      default:
        return null;
    }
  }
}
EN

回答 1

Stack Overflow用户

发布于 2022-07-17 11:09:39

实际上,当我在终止/背景状态下使用[医]愈伤组织导航数周时,我也被困住了,特别是,解决方案是如此简单。

造成此问题的原因是:

->用于接收处于终止状态或背景状态的通知,backgroundHandler函数是在它自己的隔离中工作的,而在这个孤立函数中,您实际上试图在pushNamed路由中导航,而对于这个隔离函数来说,NikkahMatch类中的和onGenerateRoutes是不知道的。

所以我的解决方案是:

->使用防火墙和云函数的组合进行导航。这将允许我们拥有上下文,并且它不会是空的,因为我们是从应用程序小部件树和中导航的,而不是从一个孤立的函数,即backgroundHandler。顶级listenerEvent函数仅用于在Firestore上更改调用文档中的值。

注意:顶层函数是一个不属于任何类的函数。

在接收方收到flutter_incoming_callkit通知时:

在“接受”按钮上,使用顶级listenerEvent函数将呼叫状态从传入更改为呼叫文档中的接受状态。这将从终止/背景状态打开应用程序。

我在我的第一类小部件树中使用了这个函数didChangeAppLifecycleState来处理/知道应用程序是否来自终止/背景状态:请查看以下代码:

代码语言:javascript
运行
复制
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    print(state);
    if (state == AppLifecycleState.resumed) {
      //Check call when open app from background
      print("in app life cycle resumed");
      checkAndNavigationCallingPage();
    }
  }

checkAndNavigationCallingPage() async {
    print("checkAndNavigationCallingPage CheckCalling page 1");
    if (auth.currentUser != null) {
      print("auth.currentUser.uid: ${auth.currentUser.uid}");
    }

    // NavigationService().navigationKey.currentState.pushNamed(AppRoute.videoAgoraCall);

    var currentCall = await getCurrentCall();
    print("inside the checkAndNavigationCallingPage and $currentCall");

    if (currentCall != null) {
      print("inside the currentCall != null");
      //Here we have to move to calling page
      final g = Get;
      if (!g.isDialogOpen) {
        g.defaultDialog(content: CircularProgressIndicator());
      }
      Future.delayed(Duration(milliseconds: 50));
      await FirebaseFirestore.instance
          .collection('Users')
          .doc(auth.currentUser.uid)
          .collection('calls')
          .doc(auth.currentUser.uid)
          .get()
          .then((userCallDoc) async {
        if (userCallDoc.exists) {
          if (userCallDoc['type'] == 'chapVoiceCall' ||
              userCallDoc['type'] == 'chapVideoCall') {
            bool isDeclined = false;
            String chapCallDocId = "";
            chapCallDocId = userCallDoc['chapCallDocId'];
            print(
                "............. Call was Accepted by receiver or sender.............");
            print("ChapCallDocId $chapCallDocId");
            ChapCallDocModel cdm = ChapCallDocModel();
            await FirebaseFirestore.instance
                .collection("ChatRoom")
                .doc(userCallDoc['chatRoomId'])
                .collection("chapCalls")
                .doc(chapCallDocId)
                .get()
                .then((value) {
              if ((value['requestedResponse'] == 'Declined' &&
                      value['requesterResponse'] == 'Declined') ||
                  (value['senderResponse'] == 'Declined') ||
                  (value['requestedResponse'] == 'TimeOut' &&
                      value['requesterResponse'] == 'TimeOut')) {
                isDeclined = true;
                print(
                    "in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
              } else {
                isDeclined = false;
                cdm = ChapCallDocModel.fromJson(value.data());
                print("CDM print is: ${cdm.toJson()}");
              }
            });
            currentChannel = userCallDoc['chatRoomId'];

            if (!isDeclined) {
              if (userCallDoc['type'] == 'chapVoiceCall') {
                print("in voice call enabled in accept call of listener event");
                var voiceCallToken = await GetToken().getTokenMethod(
                    userCallDoc['chatRoomId'], auth.currentUser.uid);
                print("token before if in splashscreen is: ${voiceCallToken}");
                if (voiceCallToken != null) {
                  if (g.isDialogOpen) {
                    g.back();
                  }
                  Get.to(
                    () => ChapVoiceCall(
                      toCallName: userCallDoc['requesterName'],
                      toCallImageUrl: userCallDoc['requesterImage'],
                      channelName: userCallDoc['chatRoomId'],
                      token: voiceCallToken,
                      docId: userCallDoc['chapCallDocId'],
                      callDoc: cdm,
                    ),
                  );
                } else {
                  print(
                      "......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
                }
              } else {
                if (g.isDialogOpen) {
                  g.back();
                }
                g.to(() => ChapVideoCallAgoraUIKit(
                      anotherUserName: userCallDoc['requesterName'],
                      anotherUserImage: userCallDoc['requesterImage'],
                      channelName: userCallDoc['chatRoomId'],
                      token: "",
                      anotherUserId: userCallDoc['requesterId'],
                      docId: userCallDoc['chapCallDocId'],
                      callDoc: cdm,
                    ));
              }
            } else {
              await FlutterCallkitIncoming.endAllCalls();
              print(
                  "the call was either declined by sender or receiver or was timed out.");
            }
          } else {
            bool isDeclined = false;
            print(
                "............. Call was Accepted by receiver or sender.............");
            CallDocModel cdm = CallDocModel();
            await FirebaseFirestore.instance
                .collection("ChatRoom")
                .doc(userCallDoc['chatRoomId'])
                .collection("calls")
                .doc(userCallDoc['callsDocId'])
                .get()
                .then((value) {
              if (value['receiverCallResponse'] == 'Declined' ||
                  value['senderCallResponse'] == 'Declined' ||
                  value['receiverCallResponse'] == 'TimeOut' ||
                  value['senderCallResponse'] == 'TimeOut') {
                isDeclined = true;
                print(
                    "in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
              } else {
                isDeclined = false;
                cdm = CallDocModel.fromJson(value.data());
                print("CDM print is: ${cdm.toJson()}");
              }
            });
            currentChannel = userCallDoc['chatRoomId'];

            if (!isDeclined) {
              if (userCallDoc['type'] == 'voiceCall') {
                print("in voice call enabled in accept call of listener event");
                var voiceCallToken = await GetToken().getTokenMethod(
                    userCallDoc['chatRoomId'], auth.currentUser.uid);
                print("token before if in splashscreen is: ${voiceCallToken}");
                if (voiceCallToken != null) {
                  if (g.isDialogOpen) {
                    g.back();
                  }
                  Get.to(
                    () => VoiceCall(
                      toCallName: userCallDoc['requesterName'],
                      toCallImageUrl: userCallDoc['requesterImage'],
                      channelName: userCallDoc['chatRoomId'],
                      token: voiceCallToken,
                      docId: userCallDoc['callsDocId'],
                      callDoc: cdm,
                    ),
                  );
                } else {
                  print(
                      "......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
                }
              } else {
                if (g.isDialogOpen) {
                  g.back();
                }
                g.to(() => VideoCallAgoraUIKit(
                      anotherUserName: userCallDoc['requesterName'],
                      anotherUserImage: userCallDoc['requesterImage'],
                      channelName: userCallDoc['chatRoomId'],
                      token: "",
                      anotherUserId: userCallDoc['requesterId'],
                      docId: userCallDoc['callsDocId'],
                      callDoc: cdm,
                    ));
              }
            } else {
              await FlutterCallkitIncoming.endAllCalls();
              print(
                  "the call was either declined by sender or receiver or was timed out.");
            }
          }
        } else {
          debugPrint("Document not found");
        }
      });
    }
  }

在上面的代码中,我给出了我的db场景,所以任何需要知道如何处理不同调用状态的人都可以深入研究它。或者,您可以在这里发表评论,我将很荣幸地回复.

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72845544

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档