Jquery插件之hover动画

前言

前一阵子网上看到一个写的挺好的动画插件,于是下载了下来。这两天阅读了一下代码,然后决定分享一下原作者的思路。原效果链接找了半天找不到了,如果有看客留意到了可以留言,我附上链接(混淆后叫xs.js,可以在后面的github链接上下载)。

第一个效果

我在这里为了加深印象重敲了一遍,摘取部分有代表性的效果来讲,首先我们看第一个效果图:

第一个效果

我们所期望的是通过插件注册一下容器即可产生效果,且插件拥有很多动画种类,首先是页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        a {
            color: #999;
            text-decoration: none;
        }
        * {
            margin: 0;
            padding: 0;
        }
        body {
            background-color: #222222;
        }
        .box {
            width: 1000px;
            display: flex;
            flex-wrap: wrap;
            margin: 20px auto;
        }
        .box li {
            width: 200px;
            height: 100px;
            background: #fff;
            line-height: 100px;
            margin: 20px;
            text-align: center;
            list-style: none;
        }
        .box1 {
            color: blue;
        }
    </style>
    <script type="text/javascript" src="jQuery-2.2.4.min.js"></script>
    <script type="text/javascript" src="btn-ani.js"></script>
    <script>
        $(document).ready(function () {
            $(".box1").btnHoverAni(1);
        });
    </script>
</head>
<body>
<div class="content">
    <ul class="box">
        <li class="box1"><a href="javascript:;">效果1</a></li>
    </ul>
</div>
</body>
</html>

然后是插件代码,即上面引用的btn-ani.js,注释我添加的应该算详细了

(function () {
    function ButtonHoverAnimation(btn, index, defaults) {
        //调用者
        this.btn = btn;
        //调用索引,哪一个效果
        this.index = index;
        this.color = defaults.color;
        this.init();
    }

    ButtonHoverAnimation.prototype = {
        init: function () {
            //后面的方法都使用这个obj对象的参数值
            var obj = this;
            obj.run = false;
            if (obj.btn.css('position') != 'fixed' && obj.btn.css('position') != 'absolute') {
                obj.btn.css('position', 'relative');
            }
            //宽高即调用者的宽高
            obj.width = obj.btn.width();
            obj.height = obj.btn.height();
            //将调用者的子内容层级提高,并且在最底层添加一个与调用者等宽高的画布canvas
            //这样,我们的在canvas上的绘制作为背景不会影响到内容的展示
            obj.btn.children().each(function () {
                if ($(this).css('position') != 'fixed' && $(this).css('position') != 'absolute') {
                    $(this).css({'position': 'relative', 'z-index': '2'});
                } else if (parseInt($(this).css('z-index')) < 2) {
                    $(this).css({'z-index': '2'});
                }
            });
            //背景色默认白色
            if (obj.btn.css('background-color') != "rgba(0, 0, 0, 0)") {
                obj.backgroundColor = obj.btn.css('background-color');
            } else {
                obj.backgroundColor = '#ffffff';
            }
            //添加画布,层级最低
            obj.btn.append('<canvas width="' + obj.width + '" height="' + obj.height + '" style="position:absolute; top:0; left:0; z-index:1;"></canvas>');
            obj.ctx = obj.btn.children('canvas')[0].getContext('2d');
            //画笔的颜色,如果调用者没有传入,每次鼠标移入初始化一个随机色
            if (obj.color === false) {
                obj.btn.mouseenter(function () {
                    obj.color = 'hsl(' + (Math.random() * 360) + ',60%,80%)';
                    obj.ctx.fillStyle = obj.color;
                });
            } else {
                obj.ctx.fillStyle = obj.color;
            }
            //鼠标移动时记录坐标x,y
            obj.btn.mousemove(function (e) {
                obj.x = e.pageX - obj.btn.offset().left - parseInt(obj.btn.css('border-left-width'));
                obj.y = e.pageY - obj.btn.offset().top - parseInt(obj.btn.css('border-top-width'));
            });
            obj.array = [];
            //鼠标移入移出时,我们记录一个mouseenter变量,并且移入时使用display方法去寻找对应的效果
            obj.btn.mouseenter(function (e) {
                obj.mouseenter = true;
                obj.x = e.pageX - obj.btn.offset().left - parseInt(obj.btn.css('border-left-width'));
                obj.y = e.pageY - obj.btn.offset().top - parseInt(obj.btn.css('border-top-width'));
                obj.display(obj);
            });
            obj.btn.mouseleave(function () {
                obj.mouseenter = false;
            });
            //清屏
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
        },
            //封装一个好画圆的方法
        drawArc: function (obj, x, y, radius, startAngle, endAngle, globalAlpha, fillStyle) {
            obj.ctx.beginPath();
            obj.ctx.arc(x, y, radius, startAngle, endAngle);
            obj.ctx.closePath();
            obj.ctx.globalAlpha = globalAlpha;
            obj.ctx.fillStyle = fillStyle;
            obj.ctx.fill();
        },
        display: function (obj) {
            if (obj.index == 1) {
                obj.ani1(obj);
            } else {
                console.warn('请注意,没有第' + obj.index + '个效果!!');
            }
        },
        ani1: function (obj) {
        },
    };
    var defaults = {
        color: false
    };
    $.fn.btnHoverAni = function (index, config) {
        defaults = {
            color: false
        };
        $.extend(defaults, config);
        $(this).each(function () {
            new ButtonHoverAnimation($(this), index, defaults);
        });
    }
})(jQuery);

我们通过传入index来确定动画效果,在插件内通过display方法来寻找对应的效果并执行,这里我们预留了一个ani1来实现我们的动画。
我们可以看到第一个效果很简单,就是鼠标移入,圆圈放大,鼠标移出,圆圈缩小,圆圈的最大半径是中心到顶点的距离。(填充的随机色在上面已经写好了,使用obj.color即可)。
ani1是我们执行每一帧的方法,那么一些不需要一直计算的值(比如最大半径)在display中计算好即可:

 if (obj.index == 1) {
                obj.radius = 0;
                obj.radiusMax = Math.sqrt(Math.pow(obj.width / 2, 2) + Math.pow(obj.height / 2, 2));
                if (!obj.run) {
                    obj.run = true;
                    obj.ani1(obj);
                }
            }

然后实现动画效果

ani1: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            if (obj.mouseenter) {
                obj.radius += 5;
            } else {
                obj.radius -= 5;
            }

            if (obj.radius >= obj.radiusMax) {
                obj.radius = obj.radiusMax;
            } else if (obj.radius <= 0) {
                obj.radius = 0;
            }
            obj.drawArc(obj, obj.width / 2, obj.height / 2, obj.radius, 0, Math.PI * 2, 1, obj.color);
            if (obj.mouseenter || obj.radius > 0) {
                requestAnimationFrame(function () {
                    obj.ani1(obj);
                })
            } else {
                obj.radius = 0;
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
                obj.run = false;
            }
        }

这样第一个简单的动画就做完了,后面添加动画,只需要在display方法中添加分支即可。

第二个效果

第二个效果

第二个效果就是一个转圈了,这里我们以长方形中心为圆心,圆心到顶点的距离为半径,每帧都自正上方顺时针画圆弧即可(超过canvas的内容是看不到的)。鼠标移入时,每一帧扫过弧度自增,鼠标移出时自减。
首先是添加一个动画分支:

else if (obj.index == 2) {
                if (obj.array.length == 0) {
                    obj.radius = Math.sqrt(Math.pow(obj.width / 2, 2) + Math.pow(obj.height / 2, 2));
                    obj.array = [obj.width / 2, obj.height / 2, obj.radius, -90];
                }
                if (!obj.run) {
                    obj.run = true;
                    obj.ani2(obj);
                }
            }

同样是不需要重复计算值优先计算下来,我们记下了一个数组来确定圆弧的圆心和扫过的结束角度。然后是具体实现:

  ani2: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            if (obj.mouseenter) {
                obj.array[3] += 15;
            } else {
                obj.array[3] -= 15;
            }
            if (obj.array[3] >= 270) {
                //扫完一圈
                obj.array[3] = 270;
            } else if (obj.array[3] <= -90) {
                //回到原位
                obj.array[3] = -90;
            }

            obj.ctx.globalAlpha = 1;
            obj.ctx.fillStyle = obj.color;
            obj.ctx.beginPath();
            obj.ctx.moveTo(obj.array[0], obj.array[1]);
             //开始角度为-90,即圆心正上方开始
            obj.ctx.arc(obj.array[0], obj.array[1], obj.array[2], -90 * Math.PI / 180, obj.array[3] * Math.PI / 180);
            obj.ctx.closePath();
            obj.ctx.fill();
            if (obj.mouseenter || obj.array[3] > -90) {
                requestAnimationFrame(function () {
                    obj.ani2(obj);
                });
            } else {
                obj.array = [];
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
                obj.run = false;
            }
        }

第三个效果

第三个效果

gif压缩后好像有点掉帧,实际上的效果就是首先上下各填充至三分之一处,然后中间的三分之一左右填充。
为了描述形象一点我决定秀一波画作:

图片.png

那么事实上就是一开始在长方块四周画四个长方块,上下的高度各为总高的三分之一,左右的宽度各为总宽的二分之一,我们能看到的是宽w高h的区域。鼠标移入时,首先移动上下方块,当各自移动三分之一时,开始移动左右方块,直至填满。

else if (obj.index == 3) {
                if (obj.array.length == 0) {
                    obj.rectHeight = Math.ceil(obj.height / 3);
                    obj.array[0] = {x: 0, y: -obj.rectHeight, w: obj.width, h: obj.rectHeight};
                    obj.array[1] = {x: 0, y: obj.height, w: obj.width, h: obj.rectHeight};
                    obj.array[2] = {x: -obj.width / 2, y: obj.rectHeight, w: obj.width / 2, h: obj.rectHeight};
                    obj.array[3] = {x: obj.width, y: obj.rectHeight, w: obj.width / 2, h: obj.rectHeight};
                    obj.ySpeed = obj.rectHeight / 10;
                    obj.xSpeed = obj.width / 20;
                }
                if (!obj.run) {
                    obj.run = true;
                    obj.ani3(obj);
                }
            } 

我们记下方块的高度三分之一,四个方块的坐标,以及上下移动速度ySpeed和左右移动速度xSpeed,接着具体实现如下:

ani3: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            if (obj.mouseenter) {
                obj.array[0].y += obj.ySpeed;
                obj.array[1].y -= obj.ySpeed;
                if (obj.array[0].y >= 0) {
                    obj.array[0].y = 0;
                    obj.array[1].y = obj.height - obj.rectHeight;
                    obj.array[2].x += obj.xSpeed;
                    obj.array[3].x -= obj.xSpeed;
                    if (obj.array[2].x >= 0) {
                        obj.array[2].x = 0;
                        obj.array[3].x = obj.width / 2;
                    }
                }
            } else {
                obj.array[2].x -= obj.xSpeed;
                obj.array[3].x += obj.xSpeed;
                if (obj.array[2].x <= -obj.width / 2) {
                    obj.array[2].x = -obj.width / 2;
                    obj.array[3].x = obj.width;
                    obj.array[0].y -= obj.ySpeed;
                    obj.array[1].y += obj.ySpeed;
                    if (obj.array[0].y <= -obj.rectHeight) {
                        obj.array[0].y = -obj.rectHeight;
                        obj.array[1].y = obj.height;
                    }
                }
            }
            obj.ctx.globalAlpha = 1;
            obj.ctx.fillStyle = obj.color;
            for (var i = 0; i < obj.array.length; i++) {
                obj.ctx.fillRect(obj.array[i].x, obj.array[i].y, obj.array[i].w, obj.array[i].h);
            }

            if (obj.mouseenter || obj.array[0].y > -obj.rectHeight) {
                requestAnimationFrame(function () {
                    obj.ani3(obj);
                });
            } else {
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
                obj.array = [];
                obj.run = false;
            }
        }

鼠标移入时,当第一个方块的左上角坐标y大于等于0时,左右方块开始移动直至填满,鼠标移出反之即可。

第四个效果

基于第三个效果,我就又想了一个效果,就是四边四个三角形同时移入移出,先看展示效果:

第四个效果

原理是一样的,所以直接上代码了:

else if (obj.index == 4) {
                if (obj.array.length == 0) {
                    obj.array[0] = {x: obj.width / 2, y: 0};
                    obj.array[1] = {x: obj.width / 2, y: obj.height};
                    obj.array[2] = {x: 0, y: obj.height / 2};
                    obj.array[3] = {x: obj.width, y: obj.height / 2};
                    obj.ySpeed = obj.height / 40;
                    obj.xSpeed = obj.width / 40;
                }
                if (!obj.run) {
                    obj.run = true;
                    obj.ani4(obj);
                }

            }

我这里只记下四个三角形顶点的左边,因为其他两点的坐标可以通过顶点坐标来计算。为了同时移入,ySpeed和xSpeed根据宽高等分各自定义。

 ani4: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            if (obj.mouseenter) {
                obj.array[0].y += obj.ySpeed;
                obj.array[1].y -= obj.ySpeed;
                obj.array[2].x += obj.xSpeed;
                obj.array[3].x -= obj.xSpeed;
                if (obj.array[0].y >= obj.height / 2) {
                    obj.array[0].y = obj.array[1].y = obj.height / 2;
                    obj.array[2].x = obj.array[3].x = obj.width / 2;
                }
            } else {
                obj.array[0].y -= obj.ySpeed;
                obj.array[1].y += obj.ySpeed;
                obj.array[2].x -= obj.xSpeed;
                obj.array[3].x += obj.xSpeed;
                if (obj.array[0].y <= 0) {
                    obj.array[0].y = 0;
                    obj.array[1].y = obj.height;
                    obj.array[2].x = 0;
                    obj.array[3].x = obj.width;
                }
            }

            for (var i = 0; i < obj.array.length; i++) {
                obj.ctx.globalAlpha = 1;
                obj.ctx.fillStyle = obj.color;
                if (obj.array[0].y >= obj.height / 2) {
                    obj.ctx.fillRect(0, 0, obj.width, obj.height);
                } else {
                    obj.ctx.beginPath();
                    obj.ctx.moveTo(obj.array[i].x, obj.array[i].y);
                    obj.ctx.lineTo(i == 3 ? obj.array[i].x + obj.width / 2 : obj.array[i].x - obj.width / 2,
                        i == 1 ? obj.array[i].y + obj.height / 2 : obj.array[i].y - obj.height / 2);
                    obj.ctx.lineTo(i == 2 ? obj.array[i].x - obj.width / 2 : obj.array[i].x + obj.width / 2,
                        i == 0 ? obj.array[i].y - obj.height / 2 : obj.array[i].y + obj.height / 2);
                    obj.ctx.fill();
                }
            }
            if (obj.mouseenter || obj.array[0].x > 0) {
                requestAnimationFrame(function () {
                    obj.ani4(obj);
                });
            } else {
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
                obj.array = [];
                obj.run = false;
            }
        }

lineTo的计算其实四个三角形分别写清晰一点,我这里为了缩短代码长度所以这么写了,看着有点乱= 。= 。

第五个效果

第五个效果

这个效果就是随机画圆,我们用一个数组去存储这些圆,每次绘制数组中所有的圆,然后每个圆的透明度不断变小,半径不断变大,当透明度减到0以下时,从数组中移除这个圆。圆心的位置通过随机数来确定,即Math.random() 乘以obj.width和Math.random() 乘以obj.height
下面看代码:

else if (obj.index == 5) {
                if (obj.array.length == 0) {
                    obj.ani5(obj);
                }
            }

 ani5: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            //第二个条件是为了控制产生速率
            if (obj.mouseenter && Math.random() < .5) {
                obj.array.push({
                    x: Math.random() * obj.width,
                    y: Math.random() * obj.height,
                    radius: 1,
                    alpha: 1,
                    color: obj.color
                })
            }

            for (var i = 0; i < obj.array.length; i++) {
                obj.drawArc(obj, obj.array[i].x, obj.array[i].y, obj.array[i].radius, 0, 2 * Math.PI, obj.array[i].alpha, obj.array[i].color);
                obj.array[i].alpha -= .02;
                obj.array[i].radius += .4;
                if (obj.array[i].alpha <= 0) {
                    obj.array.splice(i, 1);
                    i--;
                }
            }
            obj.ctx.globalAlpha = 1;
            if (obj.mouseenter || obj.array.length > 0) {
                requestAnimationFrame(function () {
                    obj.ani5(obj);
                })
            } else {
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
            }

        }

第六个效果

第六个效果

这个效果即在鼠标的位置产生随机向四周移动的圆圈,主要的技术点就在于圆圈随机移动的方向了,所以只能由我灵魂画师来解释了:

图片.png

假设圆圈在p1点,下一帧移动到p2点,移动距离为1,假设角度为a,那么p2点的坐标即为x1+cosa,y1+sina。这样下来,只要角度取一个随机值,即可实现向四面八方扩散的效果。需要注意是的移除圆圈的条件,因为圆圈完全移动出方块,是需要考虑圆圈的半径的。
代码如下:

else if (obj.index == 6) {
                if (obj.array.length == 0) {
                    obj.ani6(obj);
                }
            }

  ani6: function (obj) {
            obj.ctx.clearRect(0, 0, obj.width, obj.height);
            if (obj.mouseenter) {
                obj.radius = Math.random() * 3 + 1;
                obj.direction = Math.random() * Math.PI * 2;
                obj.array.push({x: obj.x, y: obj.y, radius: obj.radius, direction: obj.direction})
            }

            for (var i = 0; i < obj.array.length; i++) {
                obj.drawArc(obj, obj.array[i].x, obj.array[i].y, obj.array[i].radius, 0, 2 * Math.PI, 0.6, obj.color);
                obj.array[i].x += Math.cos(obj.array[i].direction);
                obj.array[i].y += Math.sin(obj.array[i].direction);
                //注意移除圆圈的条件需要考虑半径
                if (obj.array[i].y > obj.array[i].radius + obj.height || obj.array[i].y < -obj.array[i].radius || obj.array[i].x < -obj.array[i].radius || obj.array[i].x > obj.array[i].radius + obj.width) {
                    obj.array.splice(i, 1);
                    i--;
                }
            }

            obj.ctx.globalAlpha = 1;
            if (obj.mouseenter || obj.array.length > 0) {
                requestAnimationFrame(function () {
                    obj.ani6(obj);
                })
            } else {
                obj.ctx.clearRect(0, 0, obj.width, obj.height);
            }
        }

最后我们再来看这样一个效果:

第七个效果

可以看到,这个效果实际上也就是上两个效果加在一起而已,就如何实现抖动而言,上个效果方向是固定的,我们这里移动的xy都取随机,如下:

    obj.array[i].x += Math.cos(Math.random() * 2 * Math.PI);
    obj.array[i].y += Math.sin(Math.random() * 2 * Math.PI);

或者两个取相同的随机角度也行,都可以实现抖动的效果。

结语

效果还有很多就不多说了,有兴趣的同学可以再去研究研究。但是只要明白原理,再复杂的动画拆解了我们都可以明了。
博主对于前端的动画比较感兴趣,一些比较酷炫的动画以后也会持续更新,这里先激励自己一波。

附上演示地址:
http://jsrun.net/pPYKp
github地址:
https://github.com/bestneville/JqueryBtnHoverAni

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • Core Animation Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,...
    45b645c5912e阅读 3,016评论 0 21
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,471评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,104评论 5 13
  • 第三天,结束。
    米修a米修阅读 100评论 0 0
  • 创融商学院,系出名门,前身为清华大学深圳研究生院培训学院直属部门——金融研修中心,依托于清华大学深圳研究生院校友会...
    创融商学院阅读 433评论 0 1