Flutter车载互联插件flutter_carplaybr的使用

Flutter车载互联插件flutter_carplaybr的使用

Flutter CarPlay

CarPlay with Flutter 🚗

License: MIT Pub Version (including pre-releases) Dart Pub Likes Dart Pub Multi-Platform DartPub Dart SDK

Flutter应用现在可以在Apple CarPlay上运行!flutter_carplay旨在通过与CarPlay集成来确保在车内安全地使用用Flutter开发的iPhone应用。CarPlay将你想要在驾驶时做的事情放到汽车内置显示屏上。

Apple在iOS 14中宣布了一些很棒的功能,其中之一是用户可以从App Store下载CarPlay应用,并像其他应用一样在iPhone上使用它们。当带有CarPlay应用的iPhone连接到CarPlay车辆时,应用图标会出现在CarPlay主屏幕上。CarPlay应用不是独立的应用——你需要为现有应用添加CarPlay支持。

你的应用使用CarPlay框架向用户提供界面元素。iOS管理界面元素的显示并处理与汽车的接口。你的应用不需要管理不同屏幕分辨率的布局,也不需要支持不同输入硬件,如触摸屏、旋钮或触控板。

它仅支持iOS 14.0及以上版本。有关一般设计指南,请参阅CarPlay应用的人机界面指南

概述

Flutter CarPlay Introduction

在开始CarPlay集成之前,你必须仔细阅读本部分。

来自Apple的官方编程指南 是理解CarPlay应用需求、限制和功能的最有价值资源。这份文档有49页,清楚地列出了所需的一些操作,强烈建议你阅读。如果你对CarPlay系统感兴趣,可以了解有关MFi项目的更多信息。

模板

CarPlay应用由一组固定的用户界面模板构建,这些模板由iOS在CarPlay屏幕上呈现。每个CarPlay应用类别只能使用有限数量的模板。你的应用权限决定了你可以访问哪些模板。 Flutter CarPlay

支持

flutter_carplaybr 目前支持:

  • ✅ 动作表模板
  • ✅ 警告模板
  • ✅ 网格模板
  • ✅ 列表模板
  • ✅ 标签栏模板
  • ✅ 信息模板(贡献自OSch11
  • ✅ POI模板(贡献自OSch11

通过评估这些信息,你可以向Apple申请相关的权限。

未来路线图

未来的版本中,flutter_carplay 将支持更多模板。

  • ❌ 地图
  • ❌ 搜索
  • ❌ 语音控制 & “Hey Siri” 手势语音激活
  • ❌ 联系人
  • ❌ 正在播放

贡献

  • 始终欢迎拉取请求。
  • 更欢迎拉取请求审核!我需要帮助进行测试。
  • 如果你想更积极地贡献,请联系我 info@oguzhanatalay.com。感谢!
  • 如果你想参与编码,请加入Discord服务器,我们可以在那里聊天。

请求CarPlay权限

所有CarPlay应用都需要CarPlay应用权限。

如果你想在Apple上构建、运行和发布带有CarPlay兼容性的应用,或者通过TestfFlight或AdHoc测试或分享应用,你必须首先请求Apple批准你的开发者账户以获得CarPlay访问权限。这个过程可能需要几天到几周甚至几个月的时间,具体取决于你请求的权限类型。

要从Apple请求CarPlay应用权限,请访问https://developer.apple.com/contact/carplay 并提供有关你的应用的信息,包括CarPlay应用类别。你还必须同意CarPlay权限附加条款。

通过这个项目,你可以开始通过Apple的CarPlay模拟器进行开发和测试,无需等待CarPlay权限。Apple将审查你的请求。如果您的应用符合CarPlay应用的标准,Apple将为你的Apple开发者帐户分配一个CarPlay应用权限,并通知你。

无论你是通过模拟器运行应用还是为其分发进行开发,都必须确保相关的权限键已添加到Entitlements.plist文件中。如果你还没有一个Entitlements.plist文件,则需要创建一个。

在收到CarPlay权限后

收到权限后,你需要配置你的Xcode项目以使用它,这涉及多个步骤。你需要创建并导入一个配置文件,并添加一个Entitlements.plist文件。你的项目的代码签名设置也需要进行一些更改。

有关如何创建和导入CarPlay配置文件以及如何将Entitlements文件添加到Xcode项目的详细说明,请访问配置具有所需权限的CarPlay启用的应用。

安装前免责声明

安装此软件包后,你将对你的Xcode项目进行一些小的修改。这是因为它需要位码编译,而位码编译在Flutter中缺失。你将执行一个重新定位(我们不会删除或编辑)一些Flutter及其包引擎的过程。如果你计划将此软件包添加到对你至关重要的项目中,你应该谨慎行事。

请在开始安装之前检查示例项目

安装步骤可能很难或可能无法正确处理当前项目中与Flutter引擎通信的一些包。如果你完全不确定你在做什么,请创建一个问题,以便我可以帮助你解决问题或解释你需要做什么。

在安装过程中,如果你尝试更改任何东西(例如,任何与Flutter引擎工作的东西,任何生成的插件注册人中的特定位置,任何文件名,任何类名,或任何在AppDelegate类、模板或窗口应用程序委托场景名称中使用的功能,包括但不限于这些),你很可能会遇到不可逆的错误,并且可能会损坏你的项目。我强烈建议你在安装前备份现有项目。

开始使用

安装后的必要操作

  1. iOS平台版本必须设置为14.0。全局设置,请导航到ios/Podfile并复制前两行:
# Uncomment this line to define a global platform for your project
+ platform :ios, '14.0'
- # platform :ios, '9.0'

更改平台版本后,在终端中执行以下命令以更新你的pod文件:

// For Apple Silicon M1 chips:
$ cd ios && arch -x86_64 pod install --repo-update

// For Intel chips:
$ cd ios && pod install --repo-update
  1. 在Xcode中打开ios/Runner.xcworkspace。在项目导航器中,打开AppDelegate.swift
Flutter CarPlay

AppDelegate.swift中的application函数中删除指定代码,并将其替换为以下代码:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application( _ application: UIApplication,
                        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-   GeneratedPluginRegistrant.register(with: self)
-   return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+   return true
}
}
  1. 在Runner文件夹中创建一个名为SceneDelegate.swift的Swift文件(不在Xcode主项目文件中)并添加以下代码:
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }

        window = UIWindow(windowScene: windowScene)

        let flutterEngine = FlutterEngine(name: "SceneDelegateEngine")
        flutterEngine.run()
        GeneratedPluginRegistrant.register(with: flutterEngine)
        let controller = FlutterViewController.init(engine: flutterEngine, nibName: nil, bundle: nil)
        window?.rootViewController = controller
        window?.makeKeyAndVisible()
    }
}
Flutter CarPlay
  1. 最后一步,打开Info.plist文件,无论是用你喜欢的代码编辑器还是在Xcode中。我将分享基础代码,所以如果你在Xcode中打开,你可以用原始键值填充。
<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true />
  <key>UISceneConfigurations</key>
  <dict>
    <key>CPTemplateApplicationSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>CarPlay Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>flutter_carplay.FlutterCarPlaySceneDelegate</string>
      </dict>
    </array>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
        <key>UISceneStoryboardFile</key>
        <string>Main</string>
      </dict>
    </array>
  </dict>
</dict>

那就是了,你准备好构建你的第一个CarPlay应用了!🚀 😎

解决配置项目中的问题

如果你遇到任何错误,请查看这个详细的回复

使用与功能

查看完整示例

基本使用

  1. 导入所有需要的类:
import 'package:flutter_carplaybr/flutter_carplaybr.dart';
  1. 初始化CarPlay控制器并设置CarPlay视图层次结构的根模板:
final FlutterCarplay _flutterCarplay = FlutterCarplay();

FlutterCarplay.setRootTemplate(
  rootTemplate: CPTabBarTemplate(
    templates: [
      CPListTemplate(
        sections: [
          CPListSection(
            items: [
              CPListItem(
                text: "Item 1",
                detailText: "Detail Text",
                onPress: (complete, self) {
                  self.setDetailText("You can change the detail text.. 🚀");
                  Future.delayed(const Duration(seconds: 1), () {
                    self.setDetailText("Customizable Detail Text");
                    complete();
                  });
                },
              ),
            ],
            header: "First Section",
          ),
        ],
        title: "Home",
        showsTabBadge: false,
        systemIcon: "house.fill",
      ),
    ],
  ),
  animated: true,
);

你可以在不初始化CarPlay控制器的情况下设置根模板,但某些回调函数可能无法正常工作或可能会导致错误。

建议你在app的`initState`中设置根模板。

监听连接变化

你可以检测连接变化,比如当CarPlay连接到iPhone时,处于后台状态,或完全断开连接时。

/// 添加监听器
_flutterCarplay.addListenerOnConnectionChange(onCarplayConnectionChange);

void onCarplayConnectionChange(CPConnectionStatusTypes status) {
  // 当CarPlay连接状态为:
  // - CPConnectionStatusTypes.connected
  // - CPConnectionStatusTypes.background
  // - CPConnectionStatusTypes.disconnected
  // - CPConnectionStatusTypes.unknown
}

/// 移除监听器
_flutterCarplay.removeListenerOnConnectionChange();

CarPlay API 方法

CarPlay.setRootTemplate

设置导航层次结构的根模板。如果存在导航层次结构,CarPlay将替换整个层次结构。

  • rootTemplate 是要作为新导航层次结构根使用的模板。如果存在,它将替换当前的rootTemplate必须是以下类型之一:CPTabBarTemplateCPGridTemplateCPListTemplate。如果不是,将抛出TypeError
  • 如果animatedtrue,CarPlay将以动画形式呈现模板,但如果不存在需要替换的现有导航层次结构,此标志将被忽略。

CarPlay屏幕上不能超过5个模板。

FlutterCarplay.setRootTemplate(
  rootTemplate: /* CPTabBarTemplate, CPGridTemplate or CPListTemplate */,
  animated: true,
);
CarPlay.push

将模板添加到导航层次结构并显示它。

  • template 是要添加到导航层次结构的模板。必须是以下类型之一:CPGridTemplateCPListTemplate。如果不是,将抛出TypeError
  • 如果animatedtrue,CarPlay将以动画形式切换模板。

屏幕上的模板数量有限制。所有应用最多可以推送到5个模板,包括根模板。

FlutterCarplay.push(
  template: /* CPGridTemplate or CPListTemplate */,
  animated: true,
);
CarPlay.pop

从导航层次结构中移除最上面的模板。

  • 如果animatedtrue,CarPlay将以动画形式切换模板。
  • count 表示此函数将发生多少次。
FlutterCarplay.pop();
// OR
FlutterCarplay.pop(animated: true, count: 1);
CarPlay.popToRoot

从导航层次结构中移除所有模板,除了根模板。

  • 如果animatedtrue,CarPlay将以动画形式呈现模板。
FlutterCarplay.popToRoot(animated: true);
CarPlay.popModal

移除一个模态模板。由于CPAlertTemplateCPActionSheetTemplate都是模态的,它们都可以被移除。

  • 如果animatedtrue,CarPlay将以动画形式切换模板。
FlutterCarplay.popModal(animated: true);
CarPlay.connectionStatus

获取当前CarPlay连接状态。它将返回一个CPConnectionStatusTypes类型的字符串。

FlutterCarplay.connectionStatus

模板

CarPlay支持通用模板,如警告、列表和标签栏。它们用于从应用在CarPlay屏幕上显示内容。请参阅开发者指南以获取更多有关Apple支持的模板的信息。

如果你尝试使用不受你权限支持的模板,运行时将发生异常。

标签栏模板

Flutter CarPlay

标签栏是一个多功能容器,用于其他模板,每个模板占据标签栏的一个标签。

final CPTabBarTemplate tabBarTemplate = CPTabBarTemplate(
  templates: [
    CPListTemplate(
      sections: [
        CPListSection(
          items: [
            CPListItem(
              text: "Item 1",
              detailText: "Detail Text",
              onPress: (complete, self) {
                self.setDetailText("You can change the detail text.. 🚀");
                complete();
              },
              image: 'images/logo_flutter_1080px_clr.png',
            ),
            CPListItem(
              text: "Item 2",
              detailText: "Start progress bar",
              isPlaying: false,
              playbackProgress: 0,
              image: 'images/logo_flutter_1080px_clr.png',
              onPress: (complete, self) {
                complete();
              },
            ),
          ],
          header: "First Section",
        ),
      ],
      title: "Home",
      showsTabBadge: false,
      systemIcon: "house.fill",
    ),
    CPListTemplate(
      sections: [],
      title: "Settings",
      emptyViewTitleVariants: ["Settings"],
      emptyViewSubtitleVariants: [
        "No settings have been added here yet. You can start adding right away"
      ],
      showsTabBadge: false,
      systemIcon: "gear",
    ),
  ],
);

FlutterCarplay.setRootTemplate(rootTemplate: tabBarTemplate, animated: true);

网格模板

Flutter CarPlay

网格模板是一种特定样式的菜单,它呈现最多8个由图像和标题表示的项目。使用网格模板让用户从固定列表中选择类别。

final CPGridTemplate gridTemplate = CPGridTemplate(
  title: "Grid Template",
  buttons: [
    for (var i = 1; i < 9; i++)
      CPGridButton(
        titleVariants: ["Item $i"],
        image: 'images/logo_flutter_1080px_clr.png',
        onPress: () {
          print("Grid Button $i pressed");
        },
      ),
  ],
);

FlutterCarplay.push(template: gridTemplate, animated: true);
// OR
FlutterCarplay.setRootTemplate(rootTemplate: gridTemplate, animated: true);

警告模板

Flutter CarPlay

警告提供了关于应用状态的重要信息。警告由一个标题和一个或多个按钮组成,具体取决于类型。

如果底层条件允许,警告可以通过程序化方式取消。

final CPAlertTemplate alertTemplate = CPAlertTemplate(
  titleVariants: ["Alert Title"],
  actions: [
    CPAlertAction(
      title: "Okay",
      style: CPAlertActionStyles.normal,
      onPress: () {
        print("Okay pressed");
        FlutterCarplay.popModal(animated: true);
      },
    ),
    CPAlertAction(
      title: "Cancel",
      style: CPAlertActionStyles.cancel,
      onPress: () {
        print("Cancel pressed");
        FlutterCarplay.popModal(animated: true);
      },
    ),
    CPAlertAction(
      title: "Remove",
      style: CPAlertActionStyles.destructive,
      onPress: () {
        print("Remove pressed");
        FlutterCarplay.popModal(animated: true);
      },
    ),
  ],
),

FlutterCarplay.showAlert(template: alertTemplate, animated: true);

动作表模板

Flutter CarPlay

动作表模板是一种警告类型,当采取控制或行动时出现,根据当前上下文提供一系列选项。

使用动作表模板让用户发起任务,或在执行潜在破坏性操作之前请求确认。

final CPActionSheetTemplate actionSheetTemplate = CPActionSheetTemplate(
  title: "Action Sheet Template",
  message: "This is an example message.",
  actions: [
    CPAlertAction(
      title: "Cancel",
      style: CPAlertActionStyles.cancel,
      onPress: () {
        print("Cancel pressed in action sheet");
        FlutterCarplay.popModal(animated: true);
      },
    ),
    CPAlertAction(
      title: "Dismiss",
      style: CPAlertActionStyles.destructive,
      onPress: () {
        print("Dismiss pressed in action sheet");
        FlutterCarplay.popModal(animated: true);
      },
    ),
    CPAlertAction(
      title: "Ok",
      style: CPAlertActionStyles.normal,
      onPress: () {
        print("Ok pressed in action sheet");
        FlutterCarplay.popModal(animated: true);
      },
    ),
  ],
);

FlutterCarplay.showActionSheet(template: actionSheetTemplate, animated: true);

列表模板

Flutter CarPlay

列表以滚动的单列表格形式呈现数据,每行可以分为不同的部分。列表适用于文本内容,并可用作分层信息的导航手段。列表中的每个项目可以包括图标、标题、副标题、披露指示器、进度指示器、播放状态或读取状态等属性。

某些汽车动态限制列表最多只能显示12个项目。你始终需要准备处理只能显示12个项目的案例。超过最大值的项目将不会显示。

final CPListTemplate listTemplate = CPListTemplate(
  sections: [
    CPListSection(
      items: [
        CPListItem(
          text: "Item 1",
          detailText: "Detail Text",
          onPress: (complete, self) {
            self.setDetailText("You can change the detail text.. 🚀");
            complete();
          },
          image: 'images/logo_flutter_1080px_clr.png',
        ),
        CPListItem(
          text: "Item 2",
          detailText: "Start progress bar",
          isPlaying: false,
          playbackProgress: 0,
          image: 'images/logo_flutter_1080px_clr.png',
          onPress: (complete, self) {
            complete();
          },
        ),
      ],
      header: "First Section",
    ),
  ],
  title: "Home",
  showsTabBadge: false,
  systemIcon: "house.fill",
  emptyViewTitleVariants: ["Home"],
  emptyViewSubtitleVariants: [
    "Nothing has added here yet. You can start adding right away"
  ],
);

FlutterCarplay.push(template: listTemplate, animated: true);
// OR
FlutterCarplay.setRootTemplate(rootTemplate: listTemplate, animated: true);

信息模板

Flutter CarPlay

信息模板显示一个项目列表和最多三个操作(以文本按钮的形式)。

列表最多限于10个项目。超过最大值的项目将不会显示。最多支持三个操作。

final CPInformationTemplate informationTemplate = CPInformationTemplate(
  title: "Title",
  layout: CPInformationTemplateLayout.twoColumn,
  actions: [
    CPTextButton(
      title: "Button Title 1",
      onPress: () {
        print("Button 1");
      }
    ),
    CPTextButton(
      title: "Button Title 2",
      onPress: () {
        print("Button 2");
       }
    ),
  ],
  informationItems: [
    CPInformationItem(title: "Title", detail: "Detail"),
  ]
);

FlutterCarplay.push(template: informationTemplate, animated: true);
// OR
FlutterCarplay.setRootTemplate(rootTemplate: informationTemplate, animated: true);

POI模板

Flutter CarPlay

POI模板显示地图上的多个兴趣点。 地图部分由兴趣点决定。

该模板最多限于12个兴趣点。

 final CPPointOfInterestTemplate pointOfInterestTemplate =
   CPPointOfInterestTemplate(title: "Title", poi: [
     CPPointOfInterest(
       latitude: 51.5052,
       longitude: 7.4938,
       title: "Title",
       subtitle: "Subtitle",
       summary: "Summary",
       detailTitle: "DetailTitle",
       detailSubtitle: "detailSubtitle",
       detailSummary: "detailSummary",
       image: "images/logo_flutter_1080px_clr.png",
       primaryButton: CPTextButton(
         title: "Primary",
         onPress: () {
           print("Primary button pressed");
         }
       ),
       secondaryButton: CPTextButton(
         title: "Secondary",
         onPress: () {
           print("Secondary button pressed");
         })
    ]);

    FlutterCarplay.push(template: pointOfInterestTemplate, animated: true);
    // OR
    FlutterCarplay.setRootTemplate(rootTemplate: pointOfInterestTemplate, animated: true);

许可证

MIT许可证

版权所有 © 2021 Oğuzhan Atalay

特此授予任何获得该软件及其相关文档文件副本的人免费许可,以不受限制地处理软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许他人这样做,但须遵守以下条件:

上述版权声明和本许可声明应包含在所有副本或实质性部分中。

软件按“原样”提供,不附带任何形式的明示或暗示保证,包括但不限于适销性、特定用途适用性和非侵权的保证。在任何情况下,作者或版权持有人均不对因软件或软件的使用或其他方式引起的任何索赔、损害或其他责任负责。

Flutter CarPlay

发布

fvm flutter pub publish --dry-run
fvm flutter pub publish

更多关于Flutter车载互联插件flutter_carplaybr的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter车载互联插件flutter_carplaybr的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中集成和使用flutter_carplaybr插件进行车载互联(特别是针对CarPlay)的示例代码。这个示例将展示如何初始化插件、设置界面,并处理一些基本事件。

首先,确保你已经在pubspec.yaml文件中添加了flutter_carplaybr依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_carplaybr: ^最新版本号  # 请替换为实际发布的最新版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤使用flutter_carplaybr插件:

  1. 导入插件并初始化

在你的主Dart文件(通常是main.dart)中,导入flutter_carplaybr插件:

import 'package:flutter/material.dart';
import 'package:flutter_carplaybr/flutter_carplaybr.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter CarPlay Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late FlutterCarplayBr _carplay;

  @override
  void initState() {
    super.initState();
    _carplay = FlutterCarplayBr();

    // 监听CarPlay连接状态变化
    _carplay.onConnected.listen((_) {
      print("CarPlay Connected");
      // 在这里设置CarPlay界面
      setUpCarPlayInterface();
    });

    _carplay.onDisconnected.listen((_) {
      print("CarPlay Disconnected");
      // 清理或重置界面
    });
  }

  @override
  void dispose() {
    _carplay.dispose();
    super.dispose();
  }

  void setUpCarPlayInterface() {
    // 设置CarPlay的根视图
    _carplay.setRootViewController(
      CarplayViewController(
        template: CarplayTemplate.application,
        viewControllers: [
          // 添加你的视图控制器,例如仪表盘、媒体播放器等
          CarplayViewController(
            template: CarplayTemplate.nowPlaying,
            nowPlayingInfo: NowPlayingInfo(
              title: "Song Title",
              artist: "Artist Name",
              album: "Album Name",
              artwork: Image.asset('assets/artwork.png').image, // 确保你有这张图片
            ),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter CarPlay Demo'),
      ),
      body: Center(
        child: Text('Check CarPlay to see the interface.'),
      ),
    );
  }
}
  1. 定义NowPlayingInfo(如果插件未自带,你可能需要自己定义或根据插件文档调整):
class NowPlayingInfo {
  final String title;
  final String artist;
  final String album;
  final ui.Image artwork;

  NowPlayingInfo({
    required this.title,
    required this.artist,
    required this.album,
    required this.artwork,
  });
}

注意:上述代码是一个简化的示例,实际使用时,flutter_carplaybr插件的API可能会有所不同,具体请参考该插件的官方文档和示例。特别是CarplayViewControllerCarplayTemplateNowPlayingInfo等类的具体实现和属性可能会根据插件版本有所不同。

此外,由于CarPlay开发涉及到与原生iOS代码的交互,因此你可能还需要在iOS原生代码中进行一些配置和设置。这通常包括在Info.plist中添加必要的权限声明,以及在AppDelegateSceneDelegate中处理CarPlay相关的生命周期事件。这些步骤通常会在插件的官方文档中有详细说明。

回到顶部