egret 使用frame转载

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xiaoyang0611/article/details/49128077
egret的WebView实现(基于egret2.5)
标签(空格分隔): egret webview

客户端开发中有一种很常见的场景就是展现web页面,也就是 WebView 。例如 IOS 、 Android ,都为开发者提供了 WebView 组件,而在 egret 中暂时并没有提供 WebView 组件,所以我们只能自己动手实现一个能在 egret 引擎中可用的 WebView 组件。

由于egret运行在浏览器中,所以想要实现一个能展现web页面的组件,第一想法应该是使用iframe。
那么这些问题需要解决:

与 egret 集成,并提供与 egret 组件一致的调用方式
融入 egret 的展现,即 WebView 的坐标、大小需要与 egret 一致
查看 egret 源码,我们不难发现 egret 的 HtmlInput 组件也是使用上面我们所考虑的方式实现在游戏中进行文本输入的。所以,我们可以采用 egret HtmlInput 类似的方式实现 WebView 。因此,我们首先从 egret HtmlInuput 源码入手:

  1. Egret中HtmlInput的实现
    1.1 HtmlInput实现方式:
    在 egret.web.js 中可以很找到 egret.web.HTMLInput 的初始化相关的源码:

HtmlInput.prototype._initStageDelegateDiv = function (container, canvas) {
        this.canvas = canvas;
        var self = this;
        var stageDelegateDiv;
        if (!stageDelegateDiv) {
            stageDelegateDiv = document.createElement("div");
            this.StageDelegateDiv = stageDelegateDiv;
            stageDelegateDiv.id = "StageDelegateDiv";
            container.appendChild(stageDelegateDiv);
            self.initValue(stageDelegateDiv);
            self._inputDIV = document.createElement("div");
            self.initValue(self._inputDIV);
            self._inputDIV.style.width = "0px";
            self._inputDIV.style.height = "0px";
            self._inputDIV.style.left = 0 + "px";
            self._inputDIV.style.top = "-100px";
            self._inputDIV.style[egret.web.getPrefixStyleName("transformOrigin")] = "0% 0% 0px";
            stageDelegateDiv.appendChild(self._inputDIV);
            this.canvas.addEventListener("click", function (e) {
                if (self._needShow) {
                    self._needShow = false;
                    self._stageText._onClickHandler(e);
                    self.show();
                }
                else {
                    if (self._inputElement) {
                        self.clearInputElement();
                        self._inputElement.blur();
                        self._inputElement = null;
                    }
                }
            });
            self.initInputElement(true);
            self.initInputElement(false);
        }
    };

HtmlInput.prototype.initInputElement = function (multiline) {
                var self = this;
                //增加1个空的textarea
                var inputElement;
                if (multiline) {
                    inputElement = document.createElement("textarea");
                    inputElement.style["resize"] = "none";
                    self._multiElement = inputElement;
                    inputElement.id = "egretTextarea";
                }
                else {
                    inputElement = document.createElement("input");
                    self._simpleElement = inputElement;
                    inputElement.id = "egretInput";
                }
                inputElement.type = "text";
                self._inputDIV.appendChild(inputElement);
                inputElement.setAttribute("tabindex", "-1");
                inputElement.style.width = "1px";
                inputElement.style.height = "12px";
                self.initValue(inputElement);
                inputElement.style.outline = "thin";
                inputElement.style.background = "none";
                inputElement.style.overflow = "hidden";
                inputElement.style.wordBreak = "break-all";
                //隐藏输入框
                inputElement.style.opacity = 0;
                inputElement.oninput = function () {
                    if (self._stageText) {
                        self._stageText._onInput();
                    }
                };
            };

很明显,是在id为 StageDelegateDiv 的 dom 中嵌入 和 ,并在这两个 html 元素上做文章。

1.2 HtmlInput 渲染展现:
那么 text 输入框和 textarea 文本输入域是如何正确展现到 egret 的 canvas 上的呢?
在 egret.web.js 的 egret.web.HTML5StageText 中我们看到如下代码:

         p.$setTextField = function (textfield) {
            this.$textfield = textfield;
            return true;
        };
        /**
         * @private
         *
         */
        p.$addToStage = function () {
            this.htmlInput = egret.web.$getTextAdapter(this.$textfield);
        };
        /**
         * @private
         *
         */
        p._initElement = function () {
            var point = this.$textfield.localToGlobal(0, 0);
            var x = point.x;
            var y = point.y;
            var cX = this.$textfield.$renderMatrix.a;
            var cY = this.$textfield.$renderMatrix.d;
            var scaleX = this.htmlInput.$scaleX;
            var scaleY = this.htmlInput.$scaleY;
            this.inputDiv.style.left = x * scaleX + "px";
            this.inputDiv.style.top = y * scaleY + "px";
            if (this.$textfield.multiline) {
                this.inputDiv.style.top = (y) * scaleY + "px";
                this.inputElement.style.top = (-this.$textfield.lineSpacing / 2) + "px";
            }
            else {
                this.inputDiv.style.top = y * scaleY + "px";
                this.inputElement.style.top = 0 + "px";
            }
            this._gscaleX = scaleX * cX;
            this._gscaleY = scaleY * cY;
        };

从这几段关键代码中我们可以看到, HtmlInput 的文本使用 egret.TextFiled 组件进行渲染的,并且 HtmlInput 对应的 Dom 位置、宽高直接使用 TextFiled 的位置、宽高,这样 HtmlInputDom 就能与 egret 的坐标宽高一致了。

1.3 HtmlInput文本输入:
那么是怎么实现输入的呢?
在 egret.web.js 的 egret.web.HTML5StageText 的源码中能找到如下代码:

HTML5StageText.prototype.executeShow = function () {
var self = this;
//打开
this.inputElement.value = this. g e t T e x t ( ) ; i f ( t h i s . i n p u t E l e m e n t . o n b l u r = = n u l l ) t h i s . i n p u t E l e m e n t . o n b l u r = t h i s . o n B l u r H a n d l e r . b i n d ( t h i s ) ; t h i s . getText(); if (this.inputElement.onblur == null) { this.inputElement.onblur = this.onBlurHandler.bind(this); } this. getText();if(this.inputElement.onblur==null)this.inputElement.onblur=this.onBlurHandler.bind(this);this.resetStageText();
if (this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …xlength", this.textfield.maxChars);
}
else {
this.inputElement.removeAttribute(“maxlength”);
}
this.inputElement.selectionStart = this.inputElement.value.length;
this.inputElement.selectionEnd = this.inputElement.value.length;
this.inputElement.focus();
};


从最后一行代码可以看到,直接调用 HtmlInput 对应的 dom 的 onfocus() ,从而弹出输入框,即可进行文本输入操作。

  1. WebView的实现
    看完 HtmlInput 的实现,可见 WebView 的实现有所不同:
    HtmlInput 依赖 egret.TextField 来进行渲染展现,egret.TextField 是 egret 中内建的组件,所以 HtmlInput 坐标宽高直接使用 textfield 的坐标、宽高就可以了。但是 WebView 依赖的是 iframe ,是 html 的标准组件,并不是 egret 的内建组件,而且在 egret 的 canvas 之外,所以必然会涉及到 webview 组件的坐标宽高的换算。结合上文提到的 WebView 应该达到的效果,我们可以考虑如下实 现WebView :

WebView 继承degret.displayObjectContailner,override相关方法(getX,getY,setX,setY,getWidht,getHeight,setWidth,setHeight等)以提供与egret引擎中内建组件一致的调用方式:

class WebView extends egret.DisplayObjectContainer {

private _x:number=0;
private _y:number=0;
private _width:number=0;
private _height:number=0;
private _src:string="";
//getter  setter ...

}


与 HtmlInput 一样,在 StageDelegatDiv 中 加入 iframe, WebView 对象持有此 iframe dom 的引用,但是必须根据其 x,y,width,height 进行相应的坐标、大小的转化并进行显示:
要实现以上想法,以下需要处理:

(1) 了解egret的scalemode( 缩放模式)
(2) 根据各种scalemode的实现方式换算webview的x,y,width,height
(3) 控制WebView中iframe的样式及显示时机

2.1 egret 的 scalemode (缩放模式)
egret的六种 scalemode :

StageScaleMode.NO_SCALE = "noScale";
StageScaleMode.SHOW_ALL = "showAll";
StageScaleMode.NO_BORDER = "noBorder";
StageScaleMode.EXACT_FIT = "exactFit";
StageScaleMode.FIXED_WIDTH = "fixedWidth";
StageScaleMode.FIXED_HEIGHT = "fixedHeight";

StageScaleMode.NO_SCALE:
不缩放应用程序内容。即使在更改播放器视口大小时,它仍然保持不变。如果播放器视口比内容小,则可能进行一些裁切。 在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终跟播放器视口大小保持一致。

StageScaleMode.SHOW_ALL:
保持原始宽高比缩放应用程序内容,缩放后应用程序内容的较宽方向填满播放器视口,另一个方向的两侧可能会不够宽而留有黑边。在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。

StageScaleMode.NO_BORDER:
保持原始宽高比缩放应用程序内容,缩放后应用程序内容的较窄方向填满播放器视口,另一个方向的两侧可能会超出播放器视口而被裁切在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。

StageScaleMode.EXACT_FIT:
不保持原始宽高比缩放应用程序内容,缩放后应用程序内容正好填满播放器视口。在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。

StageScaleMode.FIXED_WIDTH :
保持原始宽高比缩放应用程序内容,缩放后应用程序内容在水平和垂直方向都填满播放器视口,但只保持应用程序内容的原始高度不变,宽度可能会改变。在此模式下,舞台高度(Stage.stageHeight)始终等于初始化时外部传入的应用程序内容高度。舞台宽度(Stage.stageWidth)由当前的缩放比例与播放器视口宽度决定。

StageScaleMode.FIXED_HEIGHT :
保持原始宽高比缩放应用程序内容,缩放后应用程序内容在水平和垂直方向都填满播放器视口,但只保持应用程序内容的原始高度不变,宽度可能会改变。在此模式下,舞台高度(Stage.stageHeight)始终等于初始化时外部传入的应用程序内容高度。舞台宽度(Stage.stageWidth)由当前的缩放比例与播放器视口宽度决定。

引用官方的一张图:

egret官方文章的介绍: egret scalemode ,有很详细的讲解。

可见,不同的缩放模式对于舞台大小、显示宽高的大小有决定性的影响。我们仅考虑SHOW_ALL,NO_BORDER,FIXED_WIDTH,FIXED_HEIGHT这几种在实际生产中用得比较多的缩放模式。
各种缩放模式下是如何计算舞台宽高、显示宽高的呢?
在egret.js的源码中,我们可以找到如下代码片段:

/**
             * @private
             * 计算舞台显示尺寸
             * @param scaleMode 当前的缩放模式
             * @param screenWidth 播放器视口宽度
             * @param screenHeight 播放器视口高度
             * @param contentWidth 初始化内容宽度
             * @param contentHeight 初始化内容高度
             */
            ScreenAdapter.prototype.calculateStageSize = function (scaleMode, screenWidth, screenHeight, contentWidth, contentHeight) {
                var displayWidth = screenWidth;
                var displayHeight = screenHeight;
                var stageWidth = contentWidth;
                var stageHeight = contentHeight;
                var scaleX = (screenWidth / stageWidth) || 0;
                var scaleY = (screenHeight / stageHeight) || 0;
                switch (scaleMode) {
                    case egret.StageScaleMode.EXACT_FIT:
                        break;
                    case egret.StageScaleMode.FIXED_HEIGHT:
                        stageWidth = Math.round(screenWidth / scaleY);
                        break;
                    case egret.StageScaleMode.FIXED_WIDTH:
                        stageHeight = Math.round(screenHeight / scaleX);
                        break;
                    case egret.StageScaleMode.NO_BORDER:
                        if (scaleX > scaleY) {
                            displayHeight = Math.round(stageHeight * scaleX);
                        }
                        else {
                            displayWidth = Math.round(stageWidth * scaleY);
                        }
                        break;
                    case egret.StageScaleMode.SHOW_ALL:
                        if (scaleX > scaleY) {
                            displayWidth = Math.round(stageWidth * scaleY);
                        }
                        else {
                            displayHeight = Math.round(stageHeight * scaleX);
                        }
                        break;
                    default:
                        stageWidth = screenWidth;
                        stageHeight = screenHeight;
                        break;
                }
                return {
                    stageWidth: stageWidth,
                    stageHeight: stageHeight,
                    displayWidth: displayWidth,
                    displayHeight: displayHeight
                };

2.2 根据各种 scalemode 的实现方式换算 webview 的 x,y,width,height
我们可以清楚看到各种缩放模式下,舞台宽高、显示宽高的计算,因此我们可以根据相应的缩放模式来换算WebView的x,y,widht,height对应在web窗口中的具体数值,这里以WebView.setwidth(w:number)为例:

public set width(value:number) {
        this._width = value;
        if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ){
            this._iframe.width=this._width/this._stageW*this._windowW+"px";
            this._iframeWrapper.style.width=this._width/this._stageW*this._windowW+"px";
        }
        if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
            if(this._windowW==this._displayW){
                this._iframe.style.width = this._width / this._stageW * this._windowW + "px";
                this._iframeWrapper.style.width = this._width / this._stageW * this._windowW + "px";
            }else{
                this._iframe.style.width = this._width / this._stageW * this._displayW + "px";
                this._iframeWrapper.style.width = this._width / this._stageW * this._displayW + "px";
            }
        }
    }
    

setX,setY,setHeight等方法坐标换算与上述方式相同。
通过坐标换算,我们就能以egret组件一样的方式调用WebView并且展现在正确的位置。

2.3 控制 WebView 中 iframe 的样式及显示时机
坐标宽高都确定了,就可以设置 iframe 的样式了,这个比较简单,通过js即可设置样式。另外,iframe在网页完全加载完再显示会比较友好。

  1. IOS 系统中 iframe 宽高撑大、无法滑动的解决方案
    测试如上方法开发的 WebView 时,发现在 ios 中, iframe 宽高会被里面的内容撑大、并且无法滑动:

通过google,找到了解决方案:
http://stackoverflow.com/questions/23083462/how-to-get-an-iframe-to-be-responsive-in-ios-safari
或者
http://davidwalsh.name/scroll-iframes-ios

即:
给iframe一个父级div,设置如下css属性:

#iframe-wrapper {
    -webkit-overflow-scrolling: touch;
    overflow-y: scroll;

    /* important:  dimensions or positioning here! */
}

#iframe-wrapper iframe {
    /* nada! */
}

private _iframeWrapper:HTMLDivElement=null;
    private _iframe:HTMLIFrameElement=null;

    /**
     * @param src
     */
    public constructor(src:string){
        super();

        var stageDelegateDom:HTMLElement=document.getElementById("StageDelegateDiv"),playerContainer:HTMLElement=stageDelegateDom.parentElement;
        var iframeWrapperDom=document.getElementById("iframe-wrapper");
        if(!iframeWrapperDom){
            iframeWrapperDom=document.createElement("div");
            iframeWrapperDom.style.display="none";
            iframeWrapperDom.attributes['style'].value+='position:absolute;-webkit-overflow-scrolling: touch;overflow-y: scroll;';//解决iframe在ios下的显示问题
            iframeWrapperDom.id="iframe-wrapper";
            stageDelegateDom.appendChild(iframeWrapperDom);
        }
        this._iframeWrapper=<HTMLDivElement>iframeWrapperDom;
        this._iframeWrapper.style.display="none";
        this._iframeWrapper.style.opacity="0";

        var iframe = document.createElement("iframe"),t=new Date().getTime();
        iframe.src=src;
        iframe.id="webview-iframe-"+t;
        iframe.name="webview-iframe-"+t;
        iframe.style.position="absolute";
        iframe.style.top="0";
        iframe.style.left="0";
        iframe.style.opacity="0";
        iframe.style.display='none';
        iframe.frameBorder='0';
        iframe.border="0";
        this._iframeWrapper.appendChild(iframe);

        this._iframe=<HTMLIFrameElement>document.getElementById("webview-iframe-"+t);
        var self=this;
        this._iframe.οnlοad=function(){
            self._iframeWrapper.style.opacity="1";
            self._iframe.style.opacity="1";
        }

    }    
  1. WebView.ts完整代码及使用方式:
    WebView.ts:
/**
 * WebView
 * 适配FIXED_WIDTH、FIXED_HEIGHT、NO_BORDER、SHOW_ALL四种缩放模式
 * 暂未考虑屏幕大小改变、屏幕旋转以及单页面多Webplay实例的情形
 * Created by yxiao on 2015/9/30.
 */
class WebView extends egret.DisplayObjectContainer {

    private _x:number=0;
    private _y:number=0;
    private _width:number=0;
    private _height:number=0;
    private _src:string="";

    private _scaleMode:string=egret.MainContext.instance.stage.scaleMode;
    private _stageW:number;
    private _stageH:number;
    private _windowW:number;
    private _windowH:number;
    private _displayH:number;
    private _displayW:number;
    private _designH:number;
    private _designW:number;

    private _iframeWrapper:HTMLDivElement=null;
    private _iframe:HTMLIFrameElement=null;

    /**
     * @param src
     */
    public constructor(src:string){
        super();

        var stageDelegateDom:HTMLElement=document.getElementById("StageDelegateDiv"),playerContainer:HTMLElement=stageDelegateDom.parentElement;
        var iframeWrapperDom=document.getElementById("iframe-wrapper");
        if(!iframeWrapperDom){
            iframeWrapperDom=document.createElement("div");
            iframeWrapperDom.style.display="none";
            iframeWrapperDom.attributes['style'].value+='position:absolute;-webkit-overflow-scrolling: touch;overflow-y: scroll;';//解决iframe在ios下的显示问题
            iframeWrapperDom.id="iframe-wrapper";
            stageDelegateDom.appendChild(iframeWrapperDom);
        }
        this._iframeWrapper=<HTMLDivElement>iframeWrapperDom;
        this._iframeWrapper.style.display="none";
        this._iframeWrapper.style.opacity="0";

        var iframe = document.createElement("iframe"),t=new Date().getTime();
        iframe.src=src;
        iframe.id="webview-iframe-"+t;
        iframe.name="webview-iframe-"+t;
        iframe.style.position="absolute";
        iframe.style.top="0";
        iframe.style.left="0";
        iframe.style.opacity="0";
        iframe.style.display='none';
        iframe.frameBorder='0';
        iframe.border="0";
        this._iframeWrapper.appendChild(iframe);

        this._iframe=<HTMLIFrameElement>document.getElementById("webview-iframe-"+t);
        var self=this;
        this._iframe.οnlοad=function(){
            self._iframeWrapper.style.opacity="1";
            self._iframe.style.opacity="1";
        }

        this._stageW=egret.MainContext.instance.stage.stageWidth;
        this._stageH=egret.MainContext.instance.stage.stageHeight;
        this._windowW=window.innerWidth;
        this._windowH=window.innerHeight;
        this._designH=parseInt(playerContainer.attributes['data-content-height'].value);
        this._designW=parseInt(playerContainer.attributes['data-content-width'].value);

        var stageSize = egret.sys.screenAdapter.calculateStageSize(egret.MainContext.instance.stage.scaleMode, this._windowW, this._windowH, this._designW, this._designH);
        this._displayH=stageSize.displayHeight;
        this._displayW=stageSize.displayWidth;

        console.log("windowW:"+this._windowW);
        console.log("stageW:"+this._stageW);
        console.log("disPlayW:"+this._displayW);
        console.log("windowH:"+this._windowH);
        console.log("stageH:"+this._stageH);
        console.log("displayH:"+this._displayH);
    }

    public show():void {
        this._iframe.style.display='block';
        this._iframeWrapper.style.display='block';
    }

    public destroy():void {
        if(this._iframe){
            this._iframeWrapper.style.display="none";
            this._iframeWrapper.removeChild(this._iframe);
        }
    }

    public get width():number {
        return this._width;
    }

    public set width(value:number) {
        this._width = value;
        if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ){
            this._iframe.width=this._width/this._stageW*this._windowW+"px";
            this._iframeWrapper.style.width=this._width/this._stageW*this._windowW+"px";
        }
        if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
            if(this._windowW==this._displayW){
                this._iframe.style.width = this._width / this._stageW * this._windowW + "px";
                this._iframeWrapper.style.width = this._width / this._stageW * this._windowW + "px";
            }else{
                this._iframe.style.width = this._width / this._stageW * this._displayW + "px";
                this._iframeWrapper.style.width = this._width / this._stageW * this._displayW + "px";
            }
        }
    }

    public get height():number {
        return this._height;
    }

    public set height(value:number) {
        this._height = value;
        if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ) {
            this._iframe.height=this._height/this._stageH*this._windowH+"px";
            this._iframeWrapper.style.height=this._height/this._stageH*this._windowH+"px";
        }
        if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
            if(this._windowH==this._displayH){
                this._iframe.style.height = this._height / this._stageH * this._windowH + "px";
                this._iframeWrapper.style.height = this._height / this._stageH * this._windowH + "px";
            }else{
                this._iframe.style.height = this._height / this._stageH * this._displayH + "px";
                this._iframeWrapper.style.height = this._height / this._stageH * this._displayH + "px";
            }
        }
    }

    public set x(value:number) {
        this._x = value;
        if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH   || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT) {
            this._iframeWrapper.style.left = this._x / this._stageW * this._windowW + "px";
        }
        if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
            if(this._windowW==this._displayW){
                this._iframeWrapper.style.left = this._x / this._stageW * this._windowW + "px";
            }else{
                this._iframeWrapper.style.left = this._x / this._stageW * this._displayW + "px";
            }
        }
    }

    public set y(value:number) {
        this._y = value;
        if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH  || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ) {
            this._iframeWrapper.style.top = this._y / this._stageH * this._windowH + "px";
        }
        if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER){
            if(this._windowH==this._displayH){
                this._iframeWrapper.style.top = this._y / this._stageH * this._windowH + "px";
            }else{
                this._iframeWrapper.style.top =this._y / this._stageH * this._displayH + "px";
            }
        }
    }

    public get x():number {
        return this._x;
    }

    public get y():number {
        return this._y;
    }

    public get src():string {
        return this._src;
    }

    public set src(value:string) {
        this._src = value;
    }
}

使用方式:

var webview=new WebView("http://www.sina.com");
webview.x=100;
webview.y=100;
webview.width = 500;
webview.height = 800;
webview.show();

效果:

  1. 后续
    考虑屏幕大小改变、屏幕旋转以及单页面多Webplay实例的情形
    ————————————————
    版权声明:本文为CSDN博主「小样儿0611」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/xiaoyang0611/article/details/49128077
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值