当前位置:   article > 正文

HarmonyOS实战—时钟卡片

HarmonyOS实战—时钟卡片

​​​​​​​ 目录

一、时钟卡片简介:

 二、开发环境搭建:

第一步:创建一个空的工程文件:

使用JS开发卡片:

 标签”forms”中卡片的配置:

 卡片的定点/定时刷新:

时钟卡片配置文件:

卡片布局:

创建卡片:

创建卡片数据库:

卡片应用初始化:

更新卡片:

前台service

卡片组件更新


一、时钟卡片简介:

        服务卡片是FA的一种界面展示形式,将FA重要信息或操作前置到卡片,以此达到服务直达、减少层级体验的目的。

        这里将介绍如何在HarmonyOS上开发一个时钟类FA卡片应用,该卡片包含2*2、2*4两种布局样式,卡片应用在桌面上的显示效果如下:


 二、开发环境搭建:

安装和配置DevEco Studio,详情请参考我的入门篇博客:下载和安装软件

这次实战使用的是DevEco Studio版本为DevEco Studio 2.1 Beta4,使用的SDK版本为API Version 5

第一步:创建一个空的工程文件:

        创建一个HarmonyOS的工程,然后选择模板Empty Ability(JS)或Empty Ability(Java),点击按钮Next,进入到工程配置界面:(这里,使用的是SDK版本为API Version 5)

        其中,工程类型有两种:一种是”Service”,也就是原子化服务;另一种是”Application”,也就是传统的应用。此外,还可以选择”是否在服务中心进行展示”。我们将工程类型指定为”原子化服务”,并且选择”在服务中心进行展示”。按照上图进行配置之后,点击按钮Finish以创建一个工程。

连接远程设备: 

 开启远程设备:


使用JS开发卡片:

        打开DevEco Studio,创建一个HarmonyOS的工程,然后选择模板Empty Ability(JS),点击按钮Next,进入到工程配置界面:

        其中,将工程类型指定为传统应用,并且不选择”在服务中心进行展示”。按照上图进行配置之后,点击按钮Finish以创建一个工程。

        如何在一个传统应用的工程中创建卡片呢?

        在目录entry上点击右键,在弹出的菜单中选择New,然后在弹出的子菜单中点击Service Widget:

这里的Service Widget指的就是卡片。

        在模板选择界面,选择基本的模板Grid Pattern,点击按钮Next,进入到卡片配置界面: 

        首先配置卡片的名称和描述;然后配置卡片关联的Page Ability;然后配置卡片的编程语言类型是JS;接下来配置卡片的JS组件名称;最后配置卡片支持的规格,其中,2*2的小尺寸是必须要支持的,我们再勾选一个1*2的微尺寸。点击按钮Finish以创建一个卡片。

再创建2*4的中尺寸和4*4的大尺寸:

DevEco Studio会自动帮我们生成了一些目录和文件:

  •  widget01和widget02是创建卡片时配置的JS组件名称
  • index.hml中定义了卡片中包含哪些UI组件;index.css中定义了卡片中的UI组件样式
  • index.json中定义了卡片中动态绑定的数据,此外,还可以定义click触发事件

        widget01和widget01是创建卡片时配置的卡片名称;在MainAbility中添加了卡片的生命周期回调方法,如:onCreateForm()、onUpdateForm()、onTriggerFormEvent()、等。 还添加了FormControllerManager、FormController、以及两个以Impl结尾的实现类。

        在config.json中自动添加了很多配置:

  1. {
  2. "app": {
  3. "bundleName": "com.example.demo",
  4. "vendor": "example",
  5. "version": {
  6. "code": 1000000,
  7. "name": "1.0.0"
  8. }
  9. },
  10. "deviceConfig": {},
  11. "module": {
  12. "package": "com.example.demo",
  13. "name": ".MyApplication",
  14. "mainAbility": "com.example.demo.MainAbility",
  15. "deviceType": [
  16. "phone"
  17. ],
  18. "distro": {
  19. "deliveryWithInstall": true,
  20. "moduleName": "entry",
  21. "moduleType": "entry",
  22. "installationFree": true
  23. },
  24. "abilities": [
  25. {
  26. "skills": [
  27. {
  28. "entities": [
  29. "entity.system.home"
  30. ],
  31. "actions": [
  32. "action.system.home"
  33. ]
  34. }
  35. ],
  36. "name": "com.example.demo.MainAbility",
  37. "icon": "$media:icon",
  38. "description": "$string:mainability_description",
  39. "label": "$string:entry_MainAbility",
  40. "type": "page",
  41. "launchType": "standard",
  42. "formsEnabled": true,
  43. "forms": [
  44. {
  45. "jsComponentName": "widget01",
  46. "isDefault": true,
  47. "scheduledUpdateTime": "10:30",
  48. "defaultDimension": "2*2",
  49. "name": "widget01",
  50. "description": "This is a service widget",
  51. "colorMode": "auto",
  52. "type": "JS",
  53. "supportDimensions": [
  54. "1*2",
  55. "2*2"
  56. ],
  57. "updateEnabled": true,
  58. "updateDuration": 1
  59. },
  60. {
  61. "jsComponentName": "widget02",
  62. "isDefault": false,
  63. "scheduledUpdateTime": "10:30",
  64. "defaultDimension": "2*2",
  65. "name": "widget02",
  66. "description": "This is a service widget",
  67. "colorMode": "auto",
  68. "type": "JS",
  69. "supportDimensions": [
  70. "2*2",
  71. "2*4",
  72. "4*4"
  73. ],
  74. "updateEnabled": true,
  75. "updateDuration": 1
  76. }
  77. ]
  78. }
  79. ],
  80. "js": [
  81. {
  82. "pages": [
  83. "pages/index/index"
  84. ],
  85. "name": "default",
  86. "window": {
  87. "designWidth": 720,
  88. "autoDesignWidth": true
  89. }
  90. },
  91. {
  92. "pages": [
  93. "pages/index/index"
  94. ],
  95. "name": "widget01",
  96. "window": {
  97. "designWidth": 720,
  98. "autoDesignWidth": true
  99. },
  100. "type": "form"
  101. },
  102. {
  103. "pages": [
  104. "pages/index/index"
  105. ],
  106. "name": "widget02",
  107. "window": {
  108. "designWidth": 720,
  109. "autoDesignWidth": true
  110. },
  111. "type": "form"
  112. }
  113. ]
  114. }
  115. }

        MainAbility添加了标签”forms”,这里的form就是卡片的意思,和Service Widget是一回事儿。”forms”是一个数组,包含两个元素,分别表示我们创建的两个卡片。顺便提一下,在前面我们有讲到:“可以在config.json中为每个Page Ability配置0~16个卡片”,也就是说,数组”forms”中最多可以包含16个元素。上图的最下方还添加了一个标签”js”,”js”也是一个数组,包含三个元素,其中后两个元素就是两个卡片对应的js组件,”name”分别是”widget01”和”widget02”,这两个值就对应着上面的标签”forms”中”jsComponentName”的两个值。也就是说,上面的标签”forms”中卡片的js组件,是在下面的标签”js”中定义的。

 标签”forms”中卡片的配置:

  • “isDefault”表示该卡片是否为默认的上滑卡片,也就是用手指按下应用图标的同时往上滑时弹出的卡片。
  • “scheduledUpdateTime”表示卡片定点刷新的时刻,采用24小时制,精确到分钟。
  • “defaultDimension“表示卡片的默认尺寸规格,取值必须在下面的“supportDimensions“所配置的列表中。
  • “colorMode“表示卡片的主题样式,默认值是”auto”,表示自适应,还可以取值为”dark”或”light”,分别表示深色主题和浅色主题。
  • “supportDimensions“表示卡片支持的尺寸规格,也就是我们在创建卡片时配置的尺寸规格。
  • “updateEnabled”表示卡片是否支持定时刷新或定点刷新,优先选择定时刷新。
  • “updateDuration“表示卡片定时刷新的周期。当取值为0时,表示该参数不生效;当取值为正整数N时,表示刷新周期为30*N分钟。

         由于该工程的类型是”原子化服务”,所以在桌面上并没有相应的图标。由于在创建工程时选择了”在服务中心进行展示”,因此,打开服务中心,就看到了相应的入口卡片:

上下滑动所有卡片,总共有5个:

  • widget01的卡片有两个,尺寸分别是1*2和2*2
  • widget02的卡片有三个,尺寸分别是2*2、2*4和4*4

        widget1的2*2的卡片被设为了上滑卡片,这是因为:在config.json中,将”isDefault”设为了”true”,并且将”defaultDimension”设为了” 2*2”:

  1. "forms": [
  2. {
  3. "jsComponentName": "widget",
  4. "isDefault": true,
  5. "scheduledUpdateTime": "10:30",
  6. "defaultDimension": "2*2",
  7. "name": "widget",
  8. "description": "This is a service widget",
  9. "colorMode": "auto",
  10. "type": "JS",
  11. "supportDimensions": [
  12. "2*2"
  13. ],
  14. "updateEnabled": true,
  15. "updateDuration": 1
  16. },

        在桌面上长按应用的图标然后显示所有卡片的时候,MainAbility中卡片的生命周期方法onCreateForm()会被自动回调,方法onCreateForm()的实现:

  1. @Override
  2. protected ProviderFormInfo onCreateForm(Intent intent) {
  3. HiLog.info(TAG, "onCreateForm");
  4. long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
  5. String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
  6. int dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
  7. HiLog.info(TAG, "onCreateForm: formId=" + formId + ",formName=" + formName);
  8. FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
  9. FormController formController = formControllerManager.getController(formId);
  10. formController = (formController == null) ? formControllerManager.createFormController(formId,
  11. formName, dimension) : formController;
  12. if (formController == null) {
  13. HiLog.error(TAG, "Get null controller. formId: " + formId + ", formName: " + formName);
  14. return null;
  15. }
  16. return formController.bindFormData();
  17. }

因为总共有5个卡片,所以方法onCreateForm()会被回调5次!!!

方法bindFormData():

  1. @Override
  2. public ProviderFormInfo bindFormData() {
  3. HiLog.info(TAG, "bind form data");
  4. ZSONObject zsonObject = new ZSONObject();
  5. ProviderFormInfo providerFormInfo = new ProviderFormInfo();
  6. if (dimension == DIMENSION_1X2) {
  7. zsonObject.put("mini", true);
  8. }
  9. if (dimension == DIMENSION_2X4) {
  10. zsonObject.put("dim2X4", true);
  11. }
  12. providerFormInfo.setJsBindingData(new FormBindingData(zsonObject));
  13. return providerFormInfo;
  14. }

         程序根据卡片的尺寸,分别设置了两个变量”mini”和”dim2X4”的值,这两个变量是子目录widget01中的index.json中的两个变量。

  1. {
  2. "data": {
  3. "mini": false,
  4. "dim2X4": false,
  5. "miniTitle": "Title",
  6. "title": "Title",
  7. "content": "Introduction",
  8. "detailTitle": "Title"
  9. }
  10. }

        这里顺便说一下,在index.hml中,很多变量都使用两个花括号括了起来,这些变量的值是在程序的运行过程中动态确定的,这种技术称之为动态绑定。这些变量的初始值在index.json中的标签”data”中进行了定义。所以,在方法bindFormData()中,根据卡片的尺寸修改了两个动态绑定的变量的值。

 所有卡片的标题都被修改了(尺寸最小的卡片除外,因为他本来就不显示标题)。

 卡片的定点/定时刷新:

        打开config.json,先将widget02对应的标签”updateDuration”修改为0,以关闭定时刷新。

         对于标签“scheduledUpdateTime”设定的时刻,当到达之后,MainAbility中卡片的回调方法onUpdateForm()就会被自动调用:

        将要刷新的数据存放在一个ZSONObject实例中,然后,将其封装在一个FormBindingData的实例bindingData中,最后,调用MainAbility的方法updateForm(),并将bindingData作为第二个实参。

  1. @Override
  2. public void updateFormData(long formId, Object... vars) {
  3. HiLog.info(TAG, "update form data timing, default 30 minutes");
  4. //刷新数据:
  5. ZSONObject data = new ZSONObject();
  6. data.put("title","更新操作");
  7. FormBindingData bindingData = new FormBindingData(data);
  8. try {
  9. ((MainAbility)context).updateForm(formId, bindingData);
  10. } catch (FormException e) {
  11. HiLog.error(TAG,e.toString());
  12. }
  13. }

 打开config.json,将标签“scheduledUpdateTime”的值修改为当前时刻的一分钟之后:

        运行工程,将widget02对应的卡片都添加到桌面上,当到达设定的定点时刻之后,卡片标题都刷新了:


时钟卡片配置文件:

  1. "forms": [
  2. {
  3. "landscapeLayouts": [
  4. "$layout:form_image_with_info_date_card_2_2",
  5. "$layout:form_image_with_info_date_card_2_4"
  6. ],
  7. "isDefault": true,
  8. "scheduledUpdateTime": "10:30",
  9. "defaultDimension": "2*2",
  10. "name": "DateCard",
  11. "description": "This is a service widget",
  12. "colorMode": "auto",
  13. "type": "Java",
  14. "supportDimensions": [
  15. "2*2",
  16. "2*4"
  17. ],
  18. "portraitLayouts": [
  19. "$layout:form_image_with_info_date_card_2_2",
  20. "$layout:form_image_with_info_date_card_2_4"
  21. ],
  22. "updateEnabled": true,
  23. "updateDuration": 1,
  24. "formVisibleNotify": true
  25. }
  26. ]

卡片布局:

        以2*2布局为例,详细进行介绍。整个2*2卡片展示的内容从上到下分别为日期、时间、星期,整体由DependentLayout布局内嵌套四个DirectionalLayout构成,每个DirectionalLayout 内均使用Text组件进行展示,部分代码如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <DependentLayout
  3. xmlns:ohos="http://schemas.huawei.com/res/ohos"
  4. ohos:height="match_parent"
  5. ohos:width="match_parent"
  6. ohos:background_element="#6A9F99"
  7. ohos:remote="true">
  8. <DirectionalLayout
  9. ohos:height="match_content"
  10. ohos:width="match_parent"
  11. ohos:orientation="vertical"
  12. >
  13. <Text...>
  14. </DirectionalLayout>
  15. <DirectionalLayout
  16. ohos:id="$+id:title"
  17. ohos:height="match_content"
  18. ohos:width="match_parent"
  19. ohos:alignment="horizontal_center"
  20. ohos:orientation="horizontal"
  21. ohos:top_margin="35fp"
  22. >
  23. <Text...>
  24. <Text...>
  25. <Text...>
  26. </DirectionalLayout>
  27. <DirectionalLayout
  28. ohos:id="$+id:time"
  29. ohos:height="match_content"
  30. ohos:width="match_parent"
  31. ohos:alignment="horizontal_center"
  32. ohos:below="$id:title"
  33. ohos:orientation="horizontal"
  34. ohos:top_margin="0.5fp"
  35. >
  36. <Text...>
  37. <Text...>
  38. <Text...>
  39. <Text...>
  40. <Text...>
  41. </DirectionalLayout>
  42. <DirectionalLayout
  43. ohos:height="match_content"
  44. ohos:width="match_parent"
  45. ohos:alignment="center"
  46. ohos:below="$id:time"
  47. ohos:margin="20fp"
  48. ohos:orientation="horizontal"
  49. >
  50. <Text...>
  51. <Text...>
  52. <Text...>
  53. <Text...>
  54. <Text...>
  55. <Text...>
  56. <Text...>
  57. </DirectionalLayout>
  58. </DependentLayout>

创建卡片:

创建卡片数据库:

        使用对象关系映射数据库来对卡片ID,卡片名字等信息进行存储,我们创建了一个数据库(FormDatabase)和一张表(Form)。
首先定义了数据库类FormDatabase.java,数据库包含"Form"表,版本号为 "1",示例代码如下:

  1. @Database(
  2. entities = {Form.class},
  3. version = 1)
  4. public abstract class FormDatabase extends OrmDatabase {}

        定义实体类Form.java,对应数据库内的表名为"form",包含了卡片id"formId"(主键),卡片名称"formName"和卡片规格"dimension"三个字段,示例代码如下:

  1. @Entity(tableName = "form")
  2. public class Form extends OrmObject {
  3. @PrimaryKey()
  4. private Long formId;
  5. private String formName;
  6. private Integer dimension;
  7. public Form(Long formId, String formName, Integer dimension) {
  8. this.formId = formId;
  9. this.formName = formName;
  10. this.dimension = dimension;
  11. }
  12. // 自行添加字段的getter和setter方法或者参考完整代码
  13. }

卡片应用初始化:

卡片程序安装启动后,会进入MainAbility,在onStart时,会首先启动卡片定时器服务TimerAbility,以便刷新时钟卡片,部分示例代码如下:

  1. @Override
  2. public void onStart(Intent intent) {
  3. super.onStart(intent);
  4. connect = helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
  5. // 启动TimerAbility
  6. Intent intentService = new Intent();
  7. Operation operation =
  8. new Intent.OperationBuilder()
  9. .withDeviceId("")
  10. .withBundleName("com.huawei.cookbooks")
  11. .withAbilityName("com.huawei.cookbooks.TimerAbility")
  12. .build();
  13. intentService.setOperation(operation);
  14. startAbility(intentService);
  15. super.setMainRoute(ClockCardSlice.class.getName());
  16. }

        ​​​​​​​当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用onCreateForm回调函数,完成卡片信息的初始化,在MainAbility中有如下示例代码:

  1. @Override
  2. protected ProviderFormInfo onCreateForm(Intent intent) {
  3. if (intent == null) {
  4. return new ProviderFormInfo();
  5. }
  6. // 获取卡片id
  7. formId = INVALID_FORM_ID;
  8. if (intent.hasParameter(AbilitySlice.PARAM_FORM_IDENTITY_KEY)) {
  9. formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
  10. } else {
  11. return new ProviderFormInfo();
  12. }
  13. // 获取卡片名称
  14. String formName = EMPTY_STRING;
  15. if (intent.hasParameter(AbilitySlice.PARAM_FORM_NAME_KEY)) {
  16. formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
  17. }
  18. // 获取卡片规格
  19. int dimension = DEFAULT_DIMENSION_2X2;
  20. if (intent.hasParameter(AbilitySlice.PARAM_FORM_DIMENSION_KEY)) {
  21. dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
  22. }
  23. int layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_2;
  24. if (dimension == DEFAULT_DIMENSION_2X4) {
  25. layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_4;
  26. }
  27. formInfo = new ProviderFormInfo(layoutId, this);
  28. // 存储卡片信息
  29. Form form = new Form(formId, formName, dimension);
  30. ComponentProvider componentProvider = ComponentProviderUtils.getComponentProvider(form, this);
  31. formInfo.mergeActions(componentProvider);
  32. if (connect == null) {
  33. connect =
  34. helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
  35. }
  36. try {
  37. DatabaseUtils.insertForm(form, connect);
  38. } catch (Exception e) {
  39. DatabaseUtils.deleteFormData(form.getFormId(), connect);
  40. }
  41. return formInfo;
  42. }

当卡片被删除时,需要重写onDeleteForm方法,根据卡片id删除卡片实例数据:

  1. @Override
  2. protected void onDeleteForm(long formId) {
  3. super.onDeleteForm(formId);
  4. // 删除数据库中的卡片信息
  5. DatabaseUtils.deleteFormData(formId, connect);
  6. }

更新卡片:

为了方便处理时钟卡片刷新的定时任务,我们创建了一个Service Ability,定时去更新卡片信息,在TimerAbility.java中有如下部分参考代码:

  1. @Override
  2. public void onStart(Intent intent) {
  3. connect = helper.getOrmContext("FormDatabase", "FormDatabase.db", FormDatabase.class);
  4. startTimer();
  5. super.onStart(intent);
  6. }
  7. // 卡片更新定时器,每秒更新一次
  8. private void startTimer() {
  9. Timer timer = new Timer();
  10. timer.schedule(
  11. new TimerTask() {
  12. @Override
  13. public void run() {
  14. updateForms();
  15. notice();
  16. }
  17. },
  18. 0,SEND_PERIOD);
  19. }
  20. private void updateForms() {
  21. // 从数据库中获取卡片信息
  22. OrmPredicates ormPredicates = new OrmPredicates(Form.class);
  23. List<Form> formList = connect.query(ormPredicates);
  24. // 更新时分秒
  25. if (formList.size() > 0) {
  26. for (Form form : formList) {
  27. // 遍历卡片列表更新卡片
  28. ComponentProvider componentProvider = ComponentProviderUtils.getComponentProvider(form, this);
  29. try {
  30. Long updateFormId = form.getFormId();
  31. updateForm(updateFormId, componentProvider);
  32. } catch (FormException e) {
  33. // 删除不存在的卡片
  34. DatabaseUtils.deleteFormData(form.getFormId(), connect);
  35. HiLog.error(LABEL_LOG, "onUpdateForm updateForm error");
  36. }
  37. }
  38. }
  39. }

前台service

为了保持service不被系统销毁,需要使用前台service配合手机管家中的相关配置来达到目的。示例代码如下:

  1. private void notice() {
  2. // 创建通知
  3. NotificationRequest request = new NotificationRequest(NOTICE_ID);
  4. request.setAlertOneTime(true);
  5. NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
  6. content.setText(DateUtils.getCurrentDate("yyyy-MM-dd HH:mm:ss"));
  7. NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
  8. request.setContent(notificationContent);
  9. // 绑定通知
  10. keepBackgroundRunning(NOTICE_ID, request);
  11. }

卡片组件更新

        有关卡片组件的更新,我们封装了ComponentProviderUtils这个类,在卡片更新时候,通过调用updateForm方法,传入参数formId和componentProvider,以达到日期、时间和星期实时更新的效果,部分代码和效果如下:

  1. public static ComponentProvider getComponentProvider(Form form, Context context) {
  2. int layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_2;
  3. if (form.getDimension() == DIM_VERSION) {
  4. layoutId = ResourceTable.Layout_form_image_with_info_date_card_2_4;
  5. }
  6. ComponentProvider componentProvider = new ComponentProvider(layoutId, context);
  7. setComponentProviderValue(componentProvider);
  8. return componentProvider;
  9. }
  10. // 为时钟各个组件赋值
  11. private static void setComponentProviderValue(ComponentProvider componentProvider) {
  12. Calendar now = Calendar.getInstance();
  13. int hour = now.get(Calendar.HOUR_OF_DAY);
  14. int min = now.get(Calendar.MINUTE);
  15. int second = now.get(Calendar.SECOND);
  16. String hourString = int2String(hour);
  17. String minString = int2String(min);
  18. String secondString = int2String(second);
  19. componentProvider.setText(ResourceTable.Id_date, DateUtils.getCurrentDate("yyyy-MM-dd"));
  20. componentProvider.setText(ResourceTable.Id_hour, hourString);
  21. componentProvider.setText(ResourceTable.Id_min, minString);
  22. componentProvider.setText(ResourceTable.Id_sec, secondString);
  23. // 获取当前星期
  24. int weekDayId = getWeekDayId();
  25. componentProvider.setTextColor(weekDayId, nowWeekColor);
  26. // 将前一天的星期改回原色
  27. int lastWeekId = getLastWeekDayId();
  28. componentProvider.setTextColor(lastWeekId, primaryWeekColor);
  29. }

参考官网文档:JAVA时钟卡片


【本文正在参与"有奖征文|HarmoneyOS征文大赛"活动】

传送口:https://marketing.csdn.net/p/ad3879b53f4b8b31db27382b5fc65bbc

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/274185
推荐阅读
相关标签
  

闽ICP备14008679号