【cocos creator】滑动列表复用,减少drawcall(TS)

示例项目:https://download.csdn.net/download/K86338236/86860248
效果:999条数据,drawcall稳定在15
在这里插入图片描述

使用:ScrollViewCtrl挂载到滑动列表上,将滑动的预制体拖入节点itemPrefab属性上

		//传入的数据,需要是数组形式
        let arr = [{ name: "这是" + 1, id: 1 },{ name: "这是" + 2, id: 2 },{ name: "这是" + 3, id: 3 }]
        this.scrollView.getComponent(ScrollViewCtrl).init(arr);

预制体根节点上不能挂载组件
预制体上的脚本:


const { ccclass, property } = cc._decorator;

@ccclass
export default class ItemCtrl extends cc.Component {
    @property(cc.Label)
    desc: cc.Label = null;

    start() {

    }

    /**
     * 传入数据
     * @param data 数据
     * @param index 顺序
     * @param extData 额外数据
     */
    initData(data, index, extData) {
        this.desc.string = data.name + " " + data.id;
    }
}

代码预览:
ScrollViewCtrl:

import NodePool from "./NodePool";

const { ccclass, property, menu } = cc._decorator;

@ccclass
@menu('自定义组件/ScrollViewCtrl')
export default class ScrollViewCtrl extends cc.Component {

    @property(cc.Prefab)
    itemPrefab: cc.Prefab = null;

    bindIndexList: {};
    scrollView: cc.ScrollView = null;
    content: cc.Node = null;
    view: cc.Node = null;
    layout: cc.Layout = null;
    itemName: string = null;
    mat4: cc.Mat4 = null;
    isInit: boolean = null;
    data: any[] = null;
    callbackList: any[] = null;
    extData: any = null;
    firstX: number = null;
    firstY: number = null;
    itemCache: any[] = null;
    itemBuffer: any = null;
    _tmpV2: any = null;
    viewRect: any = null;

    start() {
        this.initOnce();
    }

    initOnce() {
        this.bindIndexList = {};    /**记录哪个index需要绑定Item */
        this.scrollView = this.node.getComponent(cc.ScrollView);
        this.content = this.scrollView.content
        this.view = this.content.parent;
        this.layout = this.content.getComponent(cc.Layout);
        this.itemName = this.itemPrefab.name;
        this.mat4 = cc.mat4();
        this.initOnce = function () { }
    }

    onDestroy() {
        this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
        this.recycle();
    }

    /**注册滚动事件 */
    registerScrollEvent(handler, target) {
        if (!handler.name) {
            return;
        }
        var scrollView = this.node.getComponent(cc.ScrollView);
        var eventHandler = new cc.Component.EventHandler();
        eventHandler.target = target.node;
        eventHandler.component = cc.js.getClassName(target);
        eventHandler.handler = handler.name;
        var index = scrollView.scrollEvents.length;
        scrollView.scrollEvents[index] = eventHandler;
    }

    /**
     * 初始化数据和表现
     * @param {Array} data 所有的列表数据组成的数组
     * @param {any} param 要传递到item的额外参数
     */
    init(data, param?) {
        this.initOnce();
        if (!Array.isArray(data)) {
            console.error("传进来的数据不为数组!");
            return;
        }
        if (!data.length) {
            this.recycle();
            this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
            return;
        }
        param = param || {};
        this.isInit = true;
        this.data = data;
        this.callbackList = [];
        this.extData = param.extData;
        if (param.onChanged) {
            this.onItemChanged(param.onChanged);
        }
        this.layout.enabled = false;
        this.scrollView.stopAutoScroll();
        if (!NodePool.Instance.hasPool(this.itemName)) {
            NodePool.Instance.initPool(this.itemPrefab);
        }

        /**模板 */
        var template = this.itemPrefab.data;

        var paddingLeft = this.layout.paddingLeft;      /**左边距 */
        var paddingRight = this.layout.paddingRight;    /**右边距 */
        var paddingTop = this.layout.paddingTop;        /**上边距 */
        var paddingBottom = this.layout.paddingBottom;  /**下边距 */
        var spacingX = this.layout.spacingX;            /**水平间隔距离 */
        var spacingY = this.layout.spacingY;            /**垂直间隔距离 */

        let firstX = template.x;
        let firstY = template.y;

        if (this.scrollView.horizontal) {
            firstX = -template.width / 2
            firstX -= paddingLeft;
        }
        if (this.scrollView.vertical) {
            firstY = -template.height / 2;
            firstY -= paddingTop;
        }
        this.firstX = firstX;
        this.firstY = firstY;

        this.itemCache = [];
        this.itemBuffer = this.itemBuffer || [];
        //初始化itemBuffer
        let i = 0
        this.itemBuffer.forEach(buffer => {
            buffer.index = -1;
            if (i >= data.length) {
                buffer.item.x = -9999999;
                buffer.item.y = -9999999;
                buffer.item.opacity = 0;
            }
            i++;
        });

        var initCache = (i) => {
            this.itemCache[i] = this.itemCache[i] || {};
            this.itemCache[i].x = firstX;
            this.itemCache[i].y = firstY;
            this.itemCache[i].width = template.width;
            this.itemCache[i].height = template.height;
            this.itemCache[i].scaleX = template.scaleX;
            this.itemCache[i].scaleY = template.scaleY;
            this.itemCache[i].visible = false;
        }
        initCache(0);
        for (let i = 1; i < this.data.length; i++) {
            initCache(i);
            if (this.scrollView.horizontal) {
                this.itemCache[i].x = this.itemCache[i - 1].x - (this.itemCache[i - 1].width / 2 + this.itemCache[i].width / 2 + spacingX);
            }
            if (this.scrollView.vertical) {
                this.itemCache[i].y = this.itemCache[i - 1].y - (this.itemCache[i - 1].height / 2 + this.itemCache[i].height / 2 + spacingY);
            }
        }
        let lastItem = this.itemCache[this.itemCache.length - 1];
        if (this.scrollView.horizontal) {
            this.content.width = Math.abs(lastItem.x + lastItem.width / 2 + paddingRight);
        }
        if (this.scrollView.vertical) {
            this.content.height = Math.abs(lastItem.y - lastItem.height / 2 - paddingBottom);
        }
        this.content.on(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
        this.scheduleOnce(this.updateListView);
    }

    updateItemView(item, index) {
        var js = this.getScript(item);
        if (js && js.initData) {
            js.initData(this.data[index], index, this.extData);
            js.updateView && js.updateView();
        }
    }
    //创建item预制体
    getItem() {
        let item = NodePool.Instance.getNode(this.itemName);
        item.x = this.firstX;
        item.y = this.firstY;
        let data = {
            item: item,
            index: -1,
        }
        //将创建的item预制体放入data数据结构,加入itemBuffer,方便在更新视图时操作
        this.itemBuffer.push(data);
        item.on(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, item), this);
        item.on(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, item), this);
        return data;
    }

    scrollEvent() {
        if (!this.content || !this.isInit) return;
        this.updateListView();
    }
    /**
     * 在显示区域的itemx x,y坐标相对于content的位置是固定的,这部分数据存于itemCache. 监听content位置改变时,
     * 遍历所有itemCache,根据itemCache中每个item相对于content的位置计算出每个item相对于世界坐标的rect区域,与当前mask相对世界坐标
     * 的rect区域判断是否相交,若相交则该item显示,根据itemCache里面存的坐标给item设置位置,层级和父节点,不显示的item给
     * 移到下方
     */
    updateListView() {
        if (!this.itemCache) {
            return;
        }
        var attach = (buffer, i) => {
            buffer.index = i;
            buffer.item.x = this.itemCache[i].x;
            buffer.item.y = this.itemCache[i].y;
            buffer.item.scaleX = this.itemCache[i].scaleX;
            buffer.item.scaleY = this.itemCache[i].scaleY;
            buffer.item.opacity = 255;
            if (this.scrollView.horizontal) {
                buffer.item.width = this.itemCache[i].width;
            }
            if (this.scrollView.vertical) {
                buffer.item.height = this.itemCache[i].height;
            }
            buffer.item.parent = this.content;
            this.updateItemView(buffer.item, i);
        }
        for (var i = 0; i < this.itemCache.length; i++) {
            var cache = this.itemCache[i];
            var visible = this.isItemInView(i);
            var buffer = this.itemBuffer.find((v) => {
                return v.index == i;
            });
            //创建当前滑动区域内可见的item预制体
            if (visible) {
                //当前下标有绑定的item,直接使用该item
                if (this.bindIndexList[i]) {
                    buffer = this.itemBuffer.find((v) => {
                        return v.bindIndex == i;
                    });
                }
                //创建item
                if (!buffer) {
                    //如果itemBuffer里面有初始化缓存的预制体,则使用之
                    buffer = this.itemBuffer.find((v) => {
                        return v.index == -1 && v.bindIndex == undefined;
                    });
                    //没有缓存的话,则使用getIItem创建后,并放入itemBuffer方便后面使用
                    buffer = buffer || this.getItem();
                }
                //根据item绑定的序列号重新设置层级以及位置
                if (buffer.index != i) {
                    attach(buffer, i);
                }
            } else if (buffer) { //将不可见item移到非常靠下的位置,使content大小足够大,能够向下滑动
                buffer.index = -1;
                buffer.item.x = -9999999;
                buffer.item.y = -9999999;
                buffer.item.opacity = 0;  //active会影响layout大小,所以不可见将透明度设置为0
            }
            //执行每个item可见变化的回调,可见->不可见 回调, 不可见->可见回调
            if (cache.visible != visible) {
                this.runItemChangedCallback(i, visible);
            }
            cache.visible = visible;
        }

        /**调整层级 */
        this.itemBuffer.sort((a, b) => {
            if (a.index < 0 || b.index < 0) {
                return 1;
            }
            return a.index - b.index;
        });
        //按照index对item进行排序
        for (var i = 0; i < this.itemBuffer.length; i++) {
            this.itemBuffer[i].item.setSiblingIndex(i);
        }
    }

    /**item是否在可视区域内 */
    isItemInView(index) {
        //当前滑动列表区域的坐标
        this._tmpV2 = this._tmpV2 || cc.v2(0, 0);
        //view为mask裁剪区域
        this.view.getWorldMatrix(this.mat4);
        let scale = this.mat4.m[0];
        let wposx = this.mat4.m[12];
        let wposy = this.mat4.m[13];

        let width1 = this.view.width * scale;
        let height1 = this.view.height * scale;

        let wpos = this.view.convertToWorldSpaceAR(cc.Vec2.ZERO, this._tmpV2);
        //rect比那辆没有初始化或则进行了缩放或则横坐标或纵坐标变动了,则将rect变量重赋值
        if (!this.viewRect || scale != 1 || (this.viewRect.x + width1 / 2) != wposx || (this.viewRect.y + height1 / 2) != wposy) {
            this.viewRect = new cc.Rect(wpos.x - width1 / 2, wpos.y - height1 / 2, width1, height1);
        }
        //获取当前item缓存的数据
        let data = this.itemCache[index];
        //转成世界坐标
        let wpos2 = this.content.convertToWorldSpaceAR(cc.v2(data.x, data.y));
        //获取item的宽高
        let width2 = data.width * data.scaleX;
        let height2 = data.height * data.scaleY;
        let rect = new cc.Rect(wpos2.x - width2 / 2, wpos2.y - height2 / 2, width2, height2);
        //判断两个矩形区域是否相交
        let ret = this.viewRect.intersects(rect);
        return ret;
    }

    onItemSizeChanged(item) {
        if (!this.itemCache) {
            return;
        }
        let data = this.itemBuffer.find((v) => {
            return v.item == item;
        });
        if (data && data.index >= 0) {
            var itemData = this.itemCache[data.index];
            if (this.scrollView.horizontal && itemData.width == item.width && itemData.scaleX == item.scaleX) {
                return;
            }
            if (this.scrollView.vertical && itemData.height == item.height && itemData.scaleY == item.scaleY) {
                return;
            }
            itemData.width = item.width;
            itemData.scaleX = item.scaleX;

            itemData.height = item.height;
            itemData.scaleY = item.scaleY;

            this.updateBuffer();
            this.scheduleOnce(this.updateListView);
        }
    }

    setItemProperty(index, property, value) {
        if (!this.itemCache || !this.itemBuffer) {
            return;
        }
        var buffer = this.itemBuffer.find(v => {
            return v.index == index;
        });
        if (buffer) {
            buffer.item[property] = value;
        } else {
            var itemData = this.itemCache[index];
            itemData[property] = value;
        }
        this.updateBuffer();
        this.scheduleOnce(this.updateListView);
    }
    /**刷新itemCache的缓存里面每个item缓存数据的y坐标,刷新itemBuffer缓存预制体的纵坐标,并更新content的高度
     * 每个显示的item相对于content的位置不变,只在content容器内的item大小变化时,刷新itemCache数据
     **/
    updateBuffer() {
        let lastItem = this.itemCache[this.itemCache.length - 1];
        if (this.scrollView.vertical) {

            this.itemCache[0].y = -this.itemCache[0].height / 2 - this.layout.paddingTop;
            if (this.itemCache[0].scaleY != 1) {
                this.itemCache[0].y = -Math.abs(this.itemCache[0].scaleY * this.itemCache[0].height) / 2 - this.layout.paddingTop;
            }
            this.itemBuffer.find((v) => {
                if (v.index == 0) {
                    v.item.y = this.itemCache[0].y;
                }
            });
            for (var i = 1; i < this.data.length; i++) {
                var data1 = this.itemCache[i - 1];
                var data2 = this.itemCache[i];
                var h1 = data1.height;
                var h2 = data2.height;

                if (data1.scaleY != 1) {
                    h1 = Math.abs(data1.scaleY * data1.height);
                }
                if (data2.scaleY != 1) {
                    h2 = Math.abs(data2.scaleY * data2.height);
                }

                data2.y = data1.y - (h1 / 2 + h2 / 2 + this.layout.spacingY);
                this.itemBuffer.find((v) => {
                    if (v.index == i) {
                        v.item.y = data2.y;
                    }
                });
            }
            var lastRealHeight = lastItem.height / 2;
            if (lastItem.scaleY != 1) {
                lastRealHeight = Math.abs(lastItem.scaleY * lastItem.height) / 2;
            }
            this.content.height = Math.abs(lastItem.y - lastRealHeight - this.layout.paddingBottom);
        }
    }

    /**
     * 滑动到指定Item位置
     * @param {*} index 
     */
    scrollToItem(index, t?, extParams?) {
        if (!this.itemCache || !this.itemCache.length) {
            return;
        }
        if (index < 0) index = 0;
        if (index >= this.itemCache.length) index = this.itemCache.length - 1;

        var cache = this.itemCache[index];
        if (!cache) {
            return;
        }
        extParams = extParams || {};
        t = t || 0;
        if (this.scrollView) {
            var toY;
            if (extParams.customTween) {
                // toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);
                // this.content.runAction(cc.moveTo(t, cc.v2(0, toY)));
            } else {
                toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);
                this.scrollView.scrollToOffset(cc.v2(0, toY), t)
            }
        }
    }

    /**
     * 获取Item
     */
    getItemByIndex(index) {
        if (!this.itemBuffer) {
            return;
        }
        var buffer = this.itemBuffer.find(v => {
            return v.index == index;
        }) || {};
        return buffer.item;
    }

    removeItemByIndex(index) {
        if (!this.itemBuffer) {
            return;
        }
        var idx = this.itemBuffer.findIndex(v => {
            return v.index == index;
        });
        if (idx >= 0) {
            var buffer = this.itemBuffer.splice(idx, 1);
            buffer.item.destroy();
        }
    }

    /**
     * 下标和Item绑定
     * item只能指定index, item将不可复用
     */
    bindItemWithIndex(item, bindIndex, isBind) {
        if (!this.itemBuffer) {
            return;
        }
        isBind = isBind || true;
        var buffer = this.itemBuffer.find(v => {
            return v.item == item;
        });
        if (buffer) {
            buffer.bindIndex = bindIndex;
            if (isBind) {
                this.bindIndexList[bindIndex] = isBind;
            } else {
                delete this.bindIndexList[bindIndex];
                delete buffer.bindIndex;
            }
        }
    }

    onItemChanged(callback) {
        if (typeof (callback) == "function") {
            this.callbackList.push(callback);
        }
    }

    runItemChangedCallback(index, visible) {
        try {
            for (let i = 0; i < this.callbackList.length; i++) {
                this.callbackList[i](index, visible);
            }
        } catch (e) {
            console.error(e);
        }
    }

    recycle() {
        if (this.itemBuffer) {
            this.itemBuffer.forEach(element => {
                if (element && cc.isValid(element.item)) {
                    element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
                    element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
                    NodePool.Instance.putNode(this.itemName, element.item);
                }
            });
        }
        this.itemCache = null;
        this.itemBuffer = null;
    }

    getScript(node: cc.Node) {
        if (!node) return null;
        //@ts-ignore
        let arr = node._components;
        for (let i = 0; i < arr.length; i++) {
            const element = arr[i];
            if (element && arr[i].hasOwnProperty("_super")) {
                return arr[i];
            }
        }
        return null;
    }
}



    /**
     * 获取Item
     */
    getItemByIndex(index) {
        if (!this.itemBuffer) {
            return;
        }
        var buffer = this.itemBuffer.find(v => {
            return v.index == index;
        }) || {};
        return buffer.item;
    }

    removeItemByIndex(index) {
        if (!this.itemBuffer) {
            return;
        }
        var idx = this.itemBuffer.findIndex(v => {
            return v.index == index;
        });
        if (idx >= 0) {
            var buffer = this.itemBuffer.splice(idx, 1);
            EngineUtil.destroyNode(buffer.item);
        }
    }

    /**
     * 下标和Item绑定
     * item只能指定index, item将不可复用
     */
    bindItemWithIndex(item, bindIndex, isBind) {
        if (!this.itemBuffer) {
            return;
        }
        isBind = isBind || true;
        var buffer = this.itemBuffer.find(v => {
            return v.item == item;
        });
        if (buffer) {
            buffer.bindIndex = bindIndex;
            if (isBind) {
                this.bindIndexList[bindIndex] = isBind;
            } else {
                delete this.bindIndexList[bindIndex];
                delete buffer.bindIndex;
            }
        }
    }

    onItemChanged(callback) {
        if (typeof (callback) == "function") {
            this.callbackList.push(callback);
        }
    }

    runItemChangedCallback(index, visible) {
        try {
            for (let i = 0; i < this.callbackList.length; i++) {
                this.callbackList[i](index, visible);
            }
        } catch (e) {
            console.error(e);
        }
    }

    recycle() {
        if (this.itemBuffer) {
            this.itemBuffer.forEach(element => {
                if (element && cc.isValid(element.item)) {
                    element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
                    element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
                    NodePool.Instance.putNode(this.itemName, element.item);
                }
            });
        }
        this.itemCache = null;
        this.itemBuffer = null;
    }

    getScript(node: cc.Node) {
        if (!node) return null;
        //@ts-ignore
        let arr = node._components;
        for (let i = 0; i < arr.length; i++) {
            const element = arr[i];
            if (element && arr[i].hasOwnProperty("_super")) {
                return arr[i];
            }
        }
        return null;
    }
}

NodePool:


var _nodePool = {};
var _prefabList = {};

export default class NodePool {

    protected static _instance: NodePool = null;
    public static get Instance(): NodePool {
        if (NodePool._instance == null) {
            NodePool._instance = new NodePool();
        }
        return NodePool._instance;
    }

    private _pathList: {};
    /**
     * 添加路径,调用loadPool的时候就不需要传完整路径了
     * @param {*} path 路径数组或者字符串
     */
    addPath(path) {
        this._pathList = this._pathList || {};
        if (!Array.isArray(path)) {
            path = [path];
        }
        for (let i = 0; i < path.length; i++) {
            let name = cc.path.basename(path[i]);
            this._pathList[name] = this._pathList[name] || path[i];
        }
        cc.log(this._pathList);
    }

    /**
     * 加载预制,保存到节点池中
     * @param {*} name 预制名字,不需要带路径
     * @param {*} callback 
     */
    loadPool(name, callback) {
        if (!Array.isArray(name)) {
            name = [name];
        }
        var paths = [];
        for (let i = 0; i < name.length; i++) {
            paths.push(this._pathList[name[i]]);
        }
        cc.resources.load(paths, cc.Prefab, (res: any) => {
            if (!Array.isArray(res)) {
                res = [res];
            }
            for (let i = 0; i < res.length; i++) {
                let poolName = res[i].name;
                if (!this.hasPool(poolName)) {
                    this.initPool(poolName, res[i]);
                }
            }
            if (callback) callback();
        });
    }

    /**
     * 初始化节点池
     * @param {*} name 缓存名字,一般和预制体名字一致 
     * @param {*} object 预制对象
     * @param {*} count 缓存个数,默认为1
     */
    initPool(object, count = 1, name = "") {
        if (!name) name = object.name;
        if (!this.hasPool(name)) {
            if (_nodePool[name]) {
                for (const n of _nodePool[name]) {
                    this.destroyNode(n);
                }
            }
            _nodePool[name] = [];
            _prefabList[name] = object;
            for (var i = 0; i < count; i++) {
                var node = cc.instantiate(object);
                node.active = false;
                _nodePool[name].push(node);
            }
        }
    }

    /**
     * 是否已经缓存在节点池
     * @param {*} name 预制名字
     */
    hasPool(name) {
        return _prefabList[name] && _prefabList[name].isValid;
    }

    /**
     * 回收节点
     * @param {*} name 预制名字 
     * @param {*} node 预制节点实例
     */
    putNode(name, node) {
        if (!cc.isValid(node)) {
            console.error("putNode: node param is invalid");
            return;
        }
        var pool = _nodePool[name];
        if (!pool) {
            console.error("putNode: pool %s not found", name);
            return;
        }

        if (pool.findIndex((item) => {
            return item == node
        }) >= 0) {
            return;
        }
        node.stopAllActions();
        node.removeFromParent(true);
        node.x = 0;
        node.y = 0;
        node.scale = 1;
        node.opacity = 255;
        node.active = false;
        pool.push(node);
    }

    /**
     * 获取缓存的节点实例
     * @param {*} name 预制名字
     */
    getNode(name) {
        var pool = _nodePool[name];
        if (!pool) {
            console.error("getNode: pool %s not found", name);
            return null;
        }
        var node = pool.length > 0 ? pool.pop() : cc.instantiate(_prefabList[name]);
        node = cc.isValid(node) ? node : cc.instantiate(_prefabList[name]);
        node.active = true;
        node.x = 0;
        node.y = 0;
        // ant.FontTools.updateFont(node);
        return node;
    }

    /**重置节点池 */
    reset() {
        if (!_nodePool) {
            return;
        }
        var list = _nodePool;
        for (var key in list) {
            var pool = list[key];
            while (pool.length > 0) {
                this.destroyNode(pool.pop());
            }
        }
    }

    getPool() {
        return _nodePool;
    }

    destroyNode(node) {
        if (!cc.isValid(node)) {
            console.error("Tools: destroyNode error, param is invalid");
            return;
        }
        node.removeFromParent(false);
        node.destroy();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烧仙草奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值