当前位置:   article > 正文

跨平台应用开发进阶(五十四)cordova自定义插件

跨平台应用开发进阶(五十四)cordova自定义插件

一、前言

在前期博文《ReactNative进阶(一):ReactNative 学习资料汇总》中,提到目前主流的移动跨平台技术方案大体可以分为三类,

  • 使用原生内置浏览器加载HTML5的Hybrid技术方案,采用此种方案的主要有CordovaIonic微信小程序
  • 使用JavaScript语言进行开发,然后使用原生组件进行渲染,采用此方案的主要有React NativeWeex轻快应用
  • 使用自带渲染引擎自带原生组件来实现跨平台,采用此种方案的主要是Flutter

二、cordova 自定义插件

注意⚠️:存放自定义cordova插件目录不能有空格可能会报错!

2.1 cordova 安装

前提条件:已安装node.js
安装cordova使用node.jsnpm工具。打开控制台输入

npm install -g cordova
  • 1

-g是全局安装cordova。安装完成后就可以在命令行使用cordova命令查看版本信息。

cordova -v
  • 1

安装plugman
cordova需要用plugman来创建自定义插件。

npm install -g plugman
  • 1

2.2 cordova 创建 android 工程

  1. 创建cordova工程
    首先在电脑中创建一个cordova工程的目录,然后进入到创建的目录里。之后就可以开始创建cordova工程,下面是创建工程的代码和实例图。
cordova create hello com.example.hello HelloWorld
  • 1

参数说明:

名称必填描述
hello工程的文件夹名
com.example.hello应用程序的id, 名如果不指定的话默认为io.cordova.hellocordova
HelloWorldApp的名称

创建成功后的目录:

在这里插入图片描述
2. 添加 Android 平台
先进入到刚刚创建的cordova目录中,然后开始创建android工程。

cordova platform add android
  • 1

android --> 创建的平台名也可以是iOS等。

下图是创建成功的情况,失败的原因有可能是 android 开发环境没有配好。

在这里插入图片描述

生成之后开始导入到android studio中:

第一步:选择打开本地已有的android工程。

第二步:找到刚刚创建好的android工程导入。

导入之后目录如下:

在这里插入图片描述
至此就已经把cordova插件开头的部分创建好了。

2.3 使用 cordova 官方提供的插件

首先进入到创建好的android工程目录下,然后再进行添加插件操作:

cordova plugin add cordova-plugin-camera
  • 1

应用 cordova plugin list 命令查看cordova插件列表。

若误添加插件,可通过 cordova plugin remove cordova-plugin-camera 删除已安装插件。

添加成功后的Android目录变化如下:

在这里插入图片描述

使用插件的位置,在Android工程下找到index.htmlindex.js这两个文件。

使用位置
index.html文件中添加一个测试按钮。

<button id="test">test</button>
  • 1

在index.js文件中添加:

//前面的test是之前那个button的按钮,后面的test是方法名,给按钮注册点击事件
document.getElementById("test").addEventListener("click",test);
//点击事件绑定的方法
function test(){
    //调用添加的camera插件
    // onSuccess:是调用成功的返回事件;onFail:是调用失败的返回事件
    navigator.camera.getPicture(onSuccess,onFail);
}
//成功的返回事件
function onSuccess() {
    console.log("Camera cleanup success.")
}
//失败的返回事件
function onFail(message) {
    alert('Failed because: ' + message);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

然后运行起来项目。

2.4 创建自定义插件

注意⚠️:创建插件是先cd到待创建目录,存放自定义cordova插件目录不能有中文和空格!

然后,在电脑上创建一个cordova插件目录,跳转到该目录,开始创建cordova插件。

创建自定义插件

plugman create --name toast-plugin --plugin_id toast-plugin --plugin_version 1.0.0
  • 1
  • --name toast-plugin : 插件名

  • --plugin_id toast-plugin : 插件ID

  • --plugin_version 1.0.0 : 插件版本号

编写ToastDemo.java用于被调用
可以在刚刚创建的Android工程下面编写这个代码,具体代码如下:

execute方法是插件被调用时,会把操作和参数以及回调传递过来。

package com.demo.android;

import android.widget.Toast;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONException;

/**
 * ToastDemo
 *
 * @author shq1874009
 * @date 2028-08-19
 */
public class ToastDemo extends CordovaPlugin {

    @Override
    public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
        if ("show".equals(action)){
            // 获取activity和context --> cordova.getActivity()和cordova.getContext()
            Toast.makeText(cordova.getContext(),args.getString(0),Toast.LENGTH_SHORT).show();
            return true;
        }
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

把这个文件复制到刚刚生成的插件目录下。

修改plugin.xml,如下:

<?xml version='1.0' encoding='utf-8'?>
<plugin id="toast-demo-plugin" version="1.0.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
    <name>toast-demo-plugin</name>
    <js-module name="ToastShow" src="www/toast-demo-plugin.js">
        <clobbers target="ToastShow" />
    </js-module>

    <!--添加Android平台  -->
    <platform name="android">
        <config-file target="res/xml/config.xml" parent="/*">  
            <!-- JS调用时的名字 -->
            <feature name="ToastShow">  
                <!-- value:的值是对应的插件中ToastDemo.java存放的路径 --> 
                <param name="android-package" value="com.shq1874009.android.ToastDemo"/>
            </feature>  
        </config-file>  
        <!-- src:是插件里存放对应.java的路径, target-dir:安装插件时把.java文件存放的位置,要和上面的value路径对应 -->
        <source-file src="src/android/ToastDemo.java" target-dir="src/com/shq1874009/android" />
    </platform> 
</plugin>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

编辑www目录下的toast-demo-plugin.js文件
打开后是这样的:

var exec = require('cordova/exec');

exports.coolMethod = function (arg0, success, error) {
    exec(success, error, 'toast-plugin', 'coolMethod', [arg0]);
};
  • 1
  • 2
  • 3
  • 4
  • 5

修改为

var exec = require('cordova/exec');

// ToastShow: 是plugin.xml文件中的feature标签 name属性
// show:是js中调用的方法名
// [arg0]: 表示一个参数,[arg0,arg1]:表示两个参数
exports.show = function (arg0, success, error) {
    exec(success, error, 'ToastShow', 'show', [arg0]);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

初始化插件
进入到插件目录,初始化插件

npm init
  • 1

上面基本上就完成了一个简单的自定义插件的制作,接下来把插件添加到之前创建的Android工程中测试,验证是否成功。

添加的方法和上面一样,先进入到platforms目录下

然后输入

// 插件的本地目录
cordova plugin add D:\CordovaPlugin\plugins\toast-plugin
  • 1
  • 2

添加插件
然后在Android studio中调用测试。

ToastShow.show("123456");
  • 1

三、cordova 插件实战

在Vue项目中集成引用cordova插件步骤如下:

3.1 cordova.js 自定义生成

鉴于Cordova 是 OSS 的,所以可以通过源代码来生成 cordova.js

a) 下载 cordova 源码
cordova目前稳定版是6.1.x。
https://github.com/shq5785/cordova-js/tree/6.1.x) 下载 cordova-js-6.1.x.zip 并解压。

b) 下载 cordova-android、cordova-ios 源码

Cordova Android is an Android application library that allows for Cordova-based projects to be built for the Android Platform. Cordova based applications are, at the core, applications written with web technology: HTML, CSS and JavaScript.

Cordova iOS is an iOS application library that allows for Cordova-based projects to be built for the iOS Platform. Cordova based applications are, at the core, applications written with web technology: HTML, CSS and JavaScript.

npm i cordova-ios
npm i cordova-android
  • 1
  • 2

cordova源码构建依赖平台包library,依据Gruntfile.js文件配置信息可知,构建前需提前检出至cordova同级目录。点击下载平台编译包源码

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/

const path = require('path');
const { build, collectModules } = require('./build-tools');

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        compile: {
            android: {},
            ios: {},
            osx: {},
            windows: { useWindowsLineEndings: true },
            browser: {},
            electron: {}
        },
        clean: ['pkg']
    });

    // custom tasks
    grunt.registerMultiTask('compile', 'Packages cordova.js', function () {
        const done = this.async();

        const platformPath = path.resolve(`../cordova-${this.target}`);
        const platformPkgPath = path.join(platformPath, 'package');
        const platformModulesPath = path.join(platformPath, 'cordova-js-src');

        build({
            platformName: this.target,
            platformVersion: grunt.option('platformVersion') ||
                             require(platformPkgPath).version,
            extraModules: collectModules(platformModulesPath)
        })
            .then(cordovaJs => {
                // if we are using windows line endings, we will also add the BOM
                if (this.data.useWindowsLineEndings) {
                    cordovaJs = '\ufeff' + cordovaJs.split(/\r?\n/).join('\r\n');
                }

                // Write out the bundle
                const baseName = `cordova.${this.target}.js`;
                const fileName = path.join('pkg', baseName);
                grunt.file.write(fileName, cordovaJs);

                console.log(`Generated ${fileName}`);
            })
            .then(done, done);
    });

    // external tasks
    grunt.loadNpmTasks('grunt-contrib-clean');

    // defaults
    grunt.registerTask('default', ['compile']);
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

在这里插入图片描述

c) 构建工具使用的是 Grunt,所以要先安装 grunt-cli

npm install -g grunt-cli
  • 1

d) 手动生成 cordova.js

cd cordova-js-6.1.x 
npm install  -- 安装package.json中 devDependencies 定义的依赖包 
grunt        -- 运行 grunt
  • 1
  • 2
  • 3

在这里插入图片描述

d) 执行后新生成2个文件夹:xnode_modules、pkg
pkg文件夹下包含如下cordova.js文件:

在这里插入图片描述

在这里插入图片描述

其中, cordova.android.jscordova.ios.js就是我们 Android、iOS原生工程要用到的 cordova.js

但是,生成的cordova.ios.js文件为67kb,cordova.android.js文件为64kb,相较于项目中目前使用的文件体积(cordova.ios.js文件为20kb,cordova.android.js文件为24kb)较大,由此可知,现有项目中使用的cordova.js文件进行了压缩。

When it comes to distributing your work, you want to offer a version as small in size as possible. To create a minified version you need a Grunt plugin like grunt-contrib-uglify. Moreover, unless the project you’re developing is very small, chances are that you’ve split the code in multiple files. While this is a good practice for the developer, you want users to include only one file. So, before minifying the code, you should concatenate the source files to create a single one. To achieve this goal you need a Grunt plugin like grunt-contrib-concat.

官方文档指出,可应用 grunt-contrib-uglify 插件实现cordova.js文件压缩。

首先,安装 grunt-contrib-uglify

npm install grunt-contrib-uglify --save-dev
  • 1

然后,配置Gruntfile.js文件,新增 grunt-contrib-uglify 配置信息。

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/

const path = require('path');
const { build, collectModules } = require('./build-tools');

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
		uglify: {
		  dist: {
			files: {
			  'pkg/cordova.ios.min.js': ['pkg/cordova.ios.js'],
			  'pkg/cordova.android.min.js': ['pkg/cordova.android.js']
			}
		  }
		},
        compile: {
            android: {},
            ios: {},
            osx: {},
            windows: { useWindowsLineEndings: true },
            browser: {},
            electron: {}
        },
        clean: ['pkg']
    });

    // custom tasks
    grunt.registerMultiTask('compile', 'Packages cordova.js', function () {
        const done = this.async();

        const platformPath = path.resolve(`../cordova-${this.target}`);
        const platformPkgPath = path.join(platformPath, 'package');
        const platformModulesPath = path.join(platformPath, 'cordova-js-src');

        build({
            platformName: this.target,
            platformVersion: grunt.option('platformVersion') ||
                             require(platformPkgPath).version,
            extraModules: collectModules(platformModulesPath)
        })
            .then(cordovaJs => {
                // if we are using windows line endings, we will also add the BOM
                if (this.data.useWindowsLineEndings) {
                    cordovaJs = '\ufeff' + cordovaJs.split(/\r?\n/).join('\r\n');
                }

                // Write out the bundle
                const baseName = `cordova.${this.target}.js`;
                const fileName = path.join('pkg', baseName);
                grunt.file.write(fileName, cordovaJs);

                console.log(`Generated ${fileName}`);
            })
            .then(done, done);
    });

    // external tasks
	grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-clean');

    // defaults
    grunt.registerTask('default', ['compile']);
	grunt.registerTask('default', ['uglify']);
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

执行后压缩文件大小参见下图,可知cordova.android.js文件压缩到21kb,cordova.ios.js文件压缩到20kb,压缩率客观。

在这里插入图片描述

3.2 vue 项目引入cordova

3.2.1 配置文件维护

将之前生成的cordova文件,存放至public文件加下,并新增配置桥接文件public/plugins/cordova-plugin-gdBridge/gdBridge.js

cordova.define("com.framework.plugin.gdBridge", function(require, exports, module) {
    var exec = require('cordova/exec');
    //调起原生的getUserInfo方法获取登录返回的报文
    exports.getUserInfo  = function (arg0, success, error) {
        exec(success, error, 'gdBridge', 'getUserInfo', [arg0]);
    };
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

继续新增public/cordova_plugins.js配置文件

cordova.define('cordova/plugin_list', function(require, exports, module) {
  module.exports = [
    {
      "id": "com.framework.plugin.gdBridge", // 插件中某个模块或具体功能的id
      "file": "plugins/cordova-plugin-gdBridge/gdBridge.js", // 这个id对应的js文件(实现这个模块或功能的文件)
      "pluginId": "com.framework.plugin", // 插件的ID,通过这个ID可以进行插件的安装和卸载
      "clobbers": [ // H5或js通过它去调用js中间件(插件)中定义的方法
        "cordova.plugins.gdBridge"
      ]
    }
  ];
  module.exports.metadata = {
    "ocom.framework.plugin": "0.0.1"
  };
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

维护后的目录结构如下:

在这里插入图片描述

3.2.2 cordova 引入

index.html文件中,通过动态添加标签方式引入cordova

<script type="text/javascript">
  // 识别设备终端,获取app相关信息
   window.terminal = (function () {
    var u = navigator.userAgent;//app = navigator.appVersion;
    var appSequence = u.match(/appSequence:([^;]*);/) //虚拟版本号
    return {//浏览器版本信息
      trident: u.indexOf('Trident') > -1,
      presto: u.indexOf('Presto') > -1,
      webKit: u.indexOf('AppleWebKit') > -1,
      gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') > -1,
      mobile: !!u.match(/AppleWebKit.*Mobile.*/),
      ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
      android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1,
      iPhone: u.indexOf('iPhone') > -1,
      iPad: u.indexOf('iPad') > -1,
      webApp: u.indexOf('Safari') > -1,
      weixin: u.indexOf('MicroMessenger') > -1 && u.replace(/.*(MicroMessenger\/[^\s]*).*/, "$1").replace("MicroMessenger/", "") || false,
      appVersion: u.indexOf('bocapp(') > -1 ? u.match(/bocapp\(\S+\)/)[0].slice(7, -1) : '3.0.0',
      appSequence: (appSequence && appSequence[1]) || ''
    };
  })()
  // 引入cordova
  var script = document.createElement('script')
  if (window.terminal.iPhone) {
    // script.src = pth + 'cordova.ios.js'
    script.src = 'cordova.ios.min.js'
  } else if (window.terminal.android) {
    // script.src = pth + 'cordova.android.js'
    script.src = 'cordova.android.min.js'
  } else {
    script.src = "cordova.android.min.js"
  }
  script.setAttribute("id","cordova");
  document.getElementsByTagName('head')[0].appendChild(script)

  let cordovaAlready = "pending"
  
  script.onload = function () {
    cordovaAlready= true;
  }
  script.onerror = function () {
    cordovaAlready= false;
  }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

3.3 cordova 应用(H5侧)

经过前面的一系列配置,项目中应用cordova环境已维护完毕。

应用cordova实现H5调用原生功能,项目结构方面,可定义一个js文件用于专门处理H5通过桥接调用原生方法,例如新建src/api/service/jsBrigde.js文件,

//获取登录信息
export const getNativeUserInfo = () => {
  return new Promise((resolve, reject) =>{
    cordova.plugins.gdBridge.getUserInfo(
      {}
      , function(responseData) {
        resolve(JSON.parse(responseData));
      },function(error){
         reject('');
      });
    });
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

应用逻辑层,可进行接口调用,

import {getNativeUserInfo} from "@/api/service/jsBrigde";

const userInfo = await getNativeUserInfo();
  • 1
  • 2
  • 3

3.4 cordova 应用(iOS 原生侧)

Model层新建GDBridge.h、GDBridge.m文件

/// 全局单例
+ (GDBridge *)sharedInstance;

// 获取用户信息
- (void)getUserInfo:(BPGCTCDVInvokedUrlCommand *)command;

@end

NS_ASSUME_NONNULL_END

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

GDBridge.m文件如下:

#import "GDBridge.h"
#import "AppDelegate.h"

@implementation GDBridge

+ (GDBridge *)sharedInstance {
    static GDBridge *bridge = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        bridge = [[GDBridge alloc] init];
    });
    return bridge;
}

// 获取用户信息
- (void)getUserInfo:(BPGCTCDVInvokedUrlCommand *)command {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.4 cordova 应用(Android 原生侧)

MainActivity.java文件中添加监听H5调用原生逻辑,

@Override
protected <S extends EventMessage> void onEventListener(S eventMessage) {
    super.onEventListener(eventMessage);
    if (eventMessage == null) return;
    if (eventMessage instanceof GDBridgeEvent) {
        GDBridgeEvent e = (GDBridgeEvent) eventMessage;
        LogUtil.e(Thread.currentThread().getName());
        LogUtil.e(e.toString());
        callbackContext = e.getCallbackContext();
        runOnUiThread(() -> executeH5Callback(e));
    }
}

protected void executeH5Callback(GDBridgeEvent event) {
    switch (event.getAction()) {
        // H5调用用户登录信息
        case "getUserInfo":
            String userInfoMsg = gson.toJson(userInfoBean);
            LogUtil.e(userInfoMsg);
            callbackContext.success(userInfoMsg);
            break;
        default:
            break;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

四、拓展阅读

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

闽ICP备14008679号