├── .gitignore ├── README.md ├── composer.json ├── configs ├── cache.php ├── db.php ├── log.php ├── login.php ├── redis.php ├── upload.php └── webim.php ├── resources ├── static │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap1.css │ │ ├── chat.css │ │ └── main.css │ ├── img │ │ ├── avatar1.jpg │ │ ├── button.gif │ │ ├── button.jpg │ │ ├── button.png │ │ ├── button1.jpg │ │ ├── button2.jpg │ │ ├── default.jpg │ │ ├── f1.png │ │ └── face │ │ │ ├── 1.gif │ │ │ ├── 10.gif │ │ │ ├── 11.gif │ │ │ ├── 12.gif │ │ │ ├── 13.gif │ │ │ ├── 14.gif │ │ │ ├── 15.gif │ │ │ ├── 16.gif │ │ │ ├── 17.gif │ │ │ ├── 18.gif │ │ │ ├── 19.gif │ │ │ ├── 2.gif │ │ │ ├── 3.gif │ │ │ ├── 4.gif │ │ │ ├── 5.gif │ │ │ ├── 6.gif │ │ │ ├── 7.gif │ │ │ ├── 8.gif │ │ │ └── 9.gif │ └── js │ │ ├── bootstrap.js │ │ ├── chat.js │ │ ├── comet.js │ │ ├── console.js │ │ ├── jquery.js │ │ └── jquery.json.js └── templates │ └── chatroom.php ├── server.php └── src ├── MySQLPool.php ├── Pool.php ├── RedisPool.php └── Server.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | /.project 4 | /.settings 5 | .buildpath 6 | *.lock 7 | /logs 8 | /resources/static/uploads/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebIM 2 | ======== 3 | 4 | 使用`PHP`+`Swoole4`实现的网页即时聊天工具,在线体验地址:[http://webim.swoole.com/](http://webim.swoole.com/) 5 | 6 | * 基于`Swoole4`协程实现,可以同时支持数百万`TCP`连接在线 7 | * 基于`WebSocket`+`Http Comet`支持所有浏览器/客户端/移动端 8 | * 支持单聊/群聊/组聊等功能 9 | * 聊天记录使用`MySQL`存储 10 | * 用户列表和在线信息使用`Redis`存储 11 | * 基于`Server PUSH`的即时内容更新,登录/登出/状态变更/消息等会内容即时更新 12 | * 支持发送链接/图片/语音/视频/文件(开发中) 13 | * 支持`Web`端直接管理所有在线用户和群组(开发中) 14 | 15 | 依赖 16 | ---- 17 | 需要`Swoole-4.4.7`或更高版本 18 | ```shell 19 | pecl install swoole 20 | ``` 21 | 22 | 部署说明 23 | ---- 24 | 25 | ### 安装依赖的 Composer 包 26 | 27 | ```shell 28 | composer install 29 | ``` 30 | 31 | ### 修改配置 32 | 33 | * 配置`configs/redis.php`中的`Redis`服务器信息,用户列表和信息会存到`Redis`中 34 | * 配置`configs/db.php`中数据库信息,聊天记录会存储到`MySQL`中 35 | * 导入`MySQL`表接口到对应的数据库中 36 | 37 | 表结构 38 | ```sql 39 | CREATE TABLE `webim_history` ( 40 | `id` int(11) NOT NULL AUTO_INCREMENT, 41 | `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 42 | `name` varchar(64) COLLATE utf8mb4_bin NOT NULL, 43 | `avatar` varchar(255) COLLATE utf8mb4_bin NOT NULL, 44 | `type` varchar(12) COLLATE utf8mb4_bin NOT NULL, 45 | `msg` text COLLATE utf8mb4_bin NOT NULL, 46 | `send_ip` varchar(20) COLLATE utf8mb4_bin NOT NULL, 47 | PRIMARY KEY (`id`) 48 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin 49 | ``` 50 | 51 | * 修改`configs/webim.php`中的选项,设置服务器的URL和端口 52 | ```php 53 | $config['server'] = array( 54 | //监听的HOST 55 | 'host' => '0.0.0.0', 56 | //监听的端口 57 | 'port' => '9503', 58 | //配置域名 [可选] 59 | 'name' => 'im.swoole.com', 60 | ); 61 | ``` 62 | 63 | * `server.host`,`server.port` 项为`WebIM`服务器即`WebSocket`服务器的地址与端口 64 | * `server.name`配置使用的域名(可选),如果未设置将直接使用`IP:PORT`进行访问 65 | * 监听`80`和`443`等`1024`以内端口需要`root`权限 66 | 67 | 68 | ### 启动服务器 69 | 70 | ```shell 71 | php server.php 72 | ``` 73 | 74 | ### 配置域名解析或者本地 Host [可选] 75 | 76 | * 直接使用`IP:PORT`,这里不需要设置。直接打开 `http://IP:PORT/` 即可 77 | * 外网域名需要配置`DNS`解析 78 | * 本机域名需要修改`/etc/hosts`,增加`127.0.0.1 im.swoole.com`本机域名绑定 79 | 80 | 配置成功后,可以使用浏览器打开,如:`http://im.swoole.com:9503/` 81 | 82 | > 以上仅为示例,实际项目需要修改为对应的域名 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "matyhtf/framework": ">=3.0.0", 4 | "symfony/console": "^4.3", 5 | "swoole/ide-helper": "~4.4.7" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /configs/cache.php: -------------------------------------------------------------------------------- 1 | 'Redis', 4 | ); 5 | return $cache; -------------------------------------------------------------------------------- /configs/db.php: -------------------------------------------------------------------------------- 1 | SPF\Database::TYPE_MYSQLi, 4 | 'host' => "127.0.0.1", 5 | 'port' => 3306, 6 | 'dbms' => 'mysql', 7 | 'engine' => 'MyISAM', 8 | 'user' => "root", 9 | 'password' => "root", 10 | 'database' => "webim", 11 | 'charset' => "utf8", 12 | 'setname' => true, 13 | 'persistent' => false, //MySQL长连接 14 | ); 15 | return $db; -------------------------------------------------------------------------------- /configs/log.php: -------------------------------------------------------------------------------- 1 | 'FileLog', 4 | 'file' => dirname(__DIR__) . '/logs/webim.log', 5 | ); 6 | return $log; -------------------------------------------------------------------------------- /configs/login.php: -------------------------------------------------------------------------------- 1 | BASE_URL . '/api/get_user_info/', 5 | 'passport' => BASE_URL.'/page/login/', 6 | ); -------------------------------------------------------------------------------- /configs/redis.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 4 | ); 5 | return $redis; -------------------------------------------------------------------------------- /configs/upload.php: -------------------------------------------------------------------------------- 1 | ROOT_PATH . '/resources/static/uploads/', 4 | 'base_url' => '/static/uploads/', 5 | ); -------------------------------------------------------------------------------- /configs/webim.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 5 | //监听的端口 6 | 'port' => '9503', 7 | //WebSocket的URL地址,供浏览器使用的 8 | 'url' => 'ws://127.0.0.1:9503', 9 | //用于Comet跨域,必须设置为html所在的URL 10 | 'origin' => 'http://im.swoole.com:8888', 11 | ); 12 | 13 | $config['swoole'] = array( 14 | 'log_file' => ROOT_PATH . '/log/swoole.log', 15 | 'worker_num' => 1, 16 | //不要修改这里 17 | 'max_request' => 0, 18 | 'task_worker_num' => 1, 19 | //是否要作为守护进程 20 | 'daemonize' => 0, 21 | ); 22 | 23 | $config['webim'] = array( 24 | //聊天记录存储的目录 25 | 'log_file' => ROOT_PATH . '/log/webim.log', 26 | 'send_interval_limit' => 2, //只允许1秒发送一次 27 | ); 28 | 29 | $config['storage'] = array( 30 | 'history_num' => 100, 31 | ); 32 | 33 | return $config; -------------------------------------------------------------------------------- /resources/static/css/chat.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | 5 | ul.dropdown-menu { 6 | min-width: 100px; 7 | } 8 | 9 | .login-box { 10 | padding: 50px 0; 11 | background-color: white; 12 | } 13 | 14 | div.container { 15 | width: 98%; 16 | } 17 | 18 | .c-sidebar-nav { 19 | padding: 9px 0; 20 | height: 620px; 21 | } 22 | 23 | #left-column { 24 | position:fixed; 25 | left: -10px; 26 | } 27 | 28 | #chat-column { 29 | position: fixed; 30 | background-color: whiteSmoke; 31 | left: 220px; 32 | overflow:auto; 33 | height:600px; 34 | } 35 | 36 | #right-column { 37 | position:fixed; 38 | right: 10px; 39 | } 40 | 41 | #input-box { 42 | position:fixed; 43 | bottom: 0px; 44 | left: 220px; 45 | height: 65px; 46 | background-color: #DDD; 47 | } 48 | 49 | .post-form{ 50 | margin: 10px 10px; 51 | } 52 | 53 | .separator { 54 | border-bottom: 1px solid #222; 55 | } 56 | 57 | #chat-column ul{ 58 | list-style-type: none; 59 | margin-left: 0px; 60 | } 61 | 62 | div.message-container{ 63 | clear: both; 64 | position:relative; 65 | line-height: 20px; 66 | padding: 6px 10px; 67 | } 68 | 69 | div#chat-messages { 70 | overflow: auto; 71 | height:450px; 72 | } 73 | 74 | div.msg-time{ 75 | position:absolute; 76 | top:7px; 77 | right:10px; 78 | font-size: 11px; 79 | color: #999; 80 | } 81 | 82 | div.message{ 83 | margin-right:100px; 84 | } 85 | 86 | #chat-column .userpic{ 87 | float:left; 88 | margin-right:5px; 89 | } 90 | 91 | li.user-online { 92 | margin-bottom: 5px; 93 | } 94 | 95 | li.user-online img { 96 | margin-right: 5px; 97 | } 98 | 99 | #group-column { 100 | left: 250px; 101 | position: fixed; 102 | } 103 | 104 | .chat-room{ 105 | margin-bottom: 20px; 106 | } 107 | #left-userlist { 108 | margin-left: 10px; 109 | } 110 | 111 | #left-userlist li { 112 | float: left; 113 | height: 60px; 114 | list-style: none outside none; 115 | margin-top: 10px; 116 | width: 60px; 117 | margin-left: 0px; 118 | } 119 | #left-userlist li img{ 120 | border-radius: 5px; 121 | } 122 | 123 | .chat_face { 124 | text-align:center; 125 | height:28px; 126 | line-height:28px; 127 | color:#333; 128 | cursor:pointer; 129 | overflow:hidden; 130 | display:inline-block; 131 | } 132 | 133 | .chat_face_hover { 134 | color:#f25000; 135 | height:28px; 136 | line-height:28px; 137 | overflow:hidden; 138 | } 139 | 140 | .show_face { 141 | background:#FFFFFF; 142 | border:#ccc 1px solid; 143 | width:600px; 144 | display:none; 145 | z-index:9999; 146 | } 147 | .show_face_hovers { 148 | display:block; 149 | position:absolute; 150 | } 151 | 152 | .face { 153 | width:30px; 154 | margin-right:1px; 155 | height:30px; 156 | line-height:30px; 157 | font-size:13px; 158 | text-align:center; 159 | background:#f6f6f6; 160 | vertical-align:top; 161 | margin-bottom:1px; 162 | display:inline-block; 163 | } 164 | -------------------------------------------------------------------------------- /resources/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | 5 | ul.dropdown-menu { 6 | min-width: 100px; 7 | } 8 | 9 | .login-box { 10 | padding: 50px 0; 11 | background-color: white; 12 | } 13 | 14 | div.container { 15 | width: 98%; 16 | } 17 | 18 | .c-sidebar-nav { 19 | padding: 9px 0; 20 | } 21 | 22 | #left-column { 23 | position:fixed; 24 | left: -10px; 25 | } 26 | 27 | #chat-column { 28 | position: fixed; 29 | background-color: whiteSmoke; 30 | left: 220px; 31 | overflow:auto; 32 | height:600px; 33 | } 34 | 35 | #right-column { 36 | position:fixed; 37 | right: 10px; 38 | } 39 | 40 | #input-box { 41 | position:fixed; 42 | bottom: 0px; 43 | left: 220px; 44 | height: 65px; 45 | background-color: #DDD; 46 | } 47 | 48 | .post-form{ 49 | margin: 10px 10px; 50 | } 51 | 52 | .separator { 53 | border-bottom: 1px solid #222; 54 | } 55 | 56 | #chat-column ul{ 57 | list-style-type: none; 58 | margin-left: 0px; 59 | } 60 | 61 | div.message-container{ 62 | clear: both; 63 | position:relative; 64 | line-height: 20px; 65 | padding: 6px 10px; 66 | } 67 | 68 | div#chat-messages { 69 | overflow: auto; 70 | height:600px; 71 | } 72 | 73 | div.msg-time{ 74 | position:absolute; 75 | top:7px; 76 | right:10px; 77 | font-size: 11px; 78 | color: #999; 79 | } 80 | 81 | div.message{ 82 | margin-right:100px; 83 | } 84 | 85 | #chat-column .userpic{ 86 | float:left; 87 | margin-right:5px; 88 | } 89 | 90 | li.user-online { 91 | margin-bottom: 5px; 92 | } 93 | 94 | li.user-online img { 95 | margin-right: 5px; 96 | } 97 | 98 | #group-column { 99 | left: 250px; 100 | position: fixed; 101 | } 102 | 103 | .chat-room{ 104 | margin-bottom: 20px; 105 | } 106 | #left-userlist { 107 | margin-left: 10px; 108 | } 109 | 110 | #left-userlist li { 111 | float: left; 112 | height: 60px; 113 | list-style: none outside none; 114 | margin-top: 10px; 115 | width: 60px; 116 | margin-left: 0px; 117 | } 118 | #left-userlist li img{ 119 | border-radius: 5px; 120 | } -------------------------------------------------------------------------------- /resources/static/img/avatar1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/avatar1.jpg -------------------------------------------------------------------------------- /resources/static/img/button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/button.gif -------------------------------------------------------------------------------- /resources/static/img/button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/button.jpg -------------------------------------------------------------------------------- /resources/static/img/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/button.png -------------------------------------------------------------------------------- /resources/static/img/button1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/button1.jpg -------------------------------------------------------------------------------- /resources/static/img/button2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/button2.jpg -------------------------------------------------------------------------------- /resources/static/img/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/default.jpg -------------------------------------------------------------------------------- /resources/static/img/f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/f1.png -------------------------------------------------------------------------------- /resources/static/img/face/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/1.gif -------------------------------------------------------------------------------- /resources/static/img/face/10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/10.gif -------------------------------------------------------------------------------- /resources/static/img/face/11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/11.gif -------------------------------------------------------------------------------- /resources/static/img/face/12.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/12.gif -------------------------------------------------------------------------------- /resources/static/img/face/13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/13.gif -------------------------------------------------------------------------------- /resources/static/img/face/14.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/14.gif -------------------------------------------------------------------------------- /resources/static/img/face/15.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/15.gif -------------------------------------------------------------------------------- /resources/static/img/face/16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/16.gif -------------------------------------------------------------------------------- /resources/static/img/face/17.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/17.gif -------------------------------------------------------------------------------- /resources/static/img/face/18.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/18.gif -------------------------------------------------------------------------------- /resources/static/img/face/19.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/19.gif -------------------------------------------------------------------------------- /resources/static/img/face/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/2.gif -------------------------------------------------------------------------------- /resources/static/img/face/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/3.gif -------------------------------------------------------------------------------- /resources/static/img/face/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/4.gif -------------------------------------------------------------------------------- /resources/static/img/face/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/5.gif -------------------------------------------------------------------------------- /resources/static/img/face/6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/6.gif -------------------------------------------------------------------------------- /resources/static/img/face/7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/7.gif -------------------------------------------------------------------------------- /resources/static/img/face/8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/8.gif -------------------------------------------------------------------------------- /resources/static/img/face/9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matyhtf/webim/62b3f879eacfe6edf8e72a20970b01247de4ecb1/resources/static/img/face/9.gif -------------------------------------------------------------------------------- /resources/static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); -------------------------------------------------------------------------------- /resources/static/js/chat.js: -------------------------------------------------------------------------------- 1 | var ws = {}; 2 | var client_id = 0; 3 | var userlist = {}; 4 | var GET = getRequest(); 5 | var face_count = 19; 6 | 7 | $(document).ready(function() { 8 | //使用原生WebSocket 9 | //if (false) 10 | if (window.WebSocket || window.MozWebSocket) { 11 | ws = new WebSocket(webim.server); 12 | } 13 | //使用http xhr长轮循 14 | else { 15 | ws = new Comet(webim.server); 16 | } 17 | listenEvent(); 18 | }); 19 | 20 | function listenEvent() { 21 | /** 22 | * 连接建立时触发 23 | */ 24 | ws.onopen = function(e) { 25 | //连接成功 26 | console.log("connect webim server success."); 27 | //发送登录信息 28 | msg = new Object(); 29 | msg.cmd = 'login'; 30 | msg.name = user.nickname; 31 | msg.avatar = user.avatar; 32 | ws.send($.toJSON(msg)); 33 | }; 34 | 35 | //有消息到来时触发 36 | ws.onmessage = function(e) { 37 | var message = $.evalJSON(e.data); 38 | var cmd = message.cmd; 39 | if (cmd == 'login') { 40 | client_id = $.evalJSON(e.data).fd; 41 | //获取在线列表 42 | ws.send($.toJSON({ 43 | cmd : 'getOnline' 44 | })); 45 | //获取历史记录 46 | ws.send($.toJSON({ 47 | cmd : 'getHistory' 48 | })); 49 | //alert( "收到消息了:"+e.data ); 50 | } else if (cmd == 'getOnline') { 51 | showOnlineList(message); 52 | } else if (cmd == 'getHistory') { 53 | showHistory(message); 54 | } else if (cmd == 'newUser') { 55 | showNewUser(message); 56 | } else if (cmd == 'fromMsg') { 57 | showNewMsg(message); 58 | } else if (cmd == 'offline') { 59 | var cid = message.fd; 60 | delUser(cid); 61 | showNewMsg(message); 62 | } 63 | }; 64 | 65 | /** 66 | * 连接关闭事件 67 | */ 68 | ws.onclose = function(e) { 69 | $(document.body).html( 70 | "

    连接已断开,请刷新页面重新登录。

    "); 71 | }; 72 | 73 | /** 74 | * 异常事件 75 | */ 76 | ws.onerror = function(e) { 77 | $(document.body).html( 78 | "

    服务器[" + webim.server 79 | + "]: 拒绝了连接. 请检查服务器是否启动.

    "); 80 | console.log("onerror: " + e.data); 81 | }; 82 | } 83 | 84 | document.onkeydown = function(e) { 85 | var ev = document.all ? window.event : e; 86 | if (ev.keyCode == 13) { 87 | sendMsg($('#msg_content').val(), 'text'); 88 | return false; 89 | } else { 90 | return true; 91 | } 92 | }; 93 | 94 | function selectUser(userid) { 95 | $('#userlist').val(userid); 96 | } 97 | 98 | /** 99 | * 显示所有在线列表 100 | * @param dataObj 101 | */ 102 | function showOnlineList(dataObj) { 103 | var li = ''; 104 | var option = ""; 105 | 106 | for (var i = 0; i < dataObj.list.length; i++) { 107 | li = li + "
  • " 108 | + "" + "
  • "; 111 | 112 | userlist[dataObj.list[i].fd] = dataObj.list[i].name; 113 | 114 | if (dataObj.list[i].fd != client_id) { 115 | option = option + "" 118 | } 119 | } 120 | $('#left-userlist').html(li); 121 | $('#userlist').html(option); 122 | } 123 | 124 | /** 125 | * 显示所有在线列表 126 | * @param dataObj 127 | */ 128 | function showHistory(dataObj) { 129 | var msg; 130 | if (debug) { 131 | console.dir(dataObj); 132 | } 133 | for (var i = 0; i < dataObj.history.length; i++) { 134 | msg = dataObj.history[i]['msg']; 135 | if (!msg) 136 | continue; 137 | msg['time'] = dataObj.history[i]['time']; 138 | msg['user'] = dataObj.history[i]['user']; 139 | if (dataObj.history[i]['type']) { 140 | msg['type'] = dataObj.history[i]['type']; 141 | } 142 | msg['channal'] = 3; 143 | showNewMsg(msg); 144 | } 145 | } 146 | 147 | /** 148 | * 当有一个新用户连接上来时 149 | * @param dataObj 150 | */ 151 | function showNewUser(dataObj) { 152 | if (!userlist[dataObj.fd]) { 153 | userlist[dataObj.fd] = dataObj.name; 154 | if (dataObj.fd != client_id) { 155 | $('#userlist').append( 156 | ""); 158 | 159 | } 160 | $('#left-userlist').append( 161 | "
  • " 162 | + '' + "
  • "); 165 | } 166 | } 167 | 168 | /** 169 | * 显示新消息 170 | */ 171 | function showNewMsg(dataObj) { 172 | 173 | var content; 174 | if (!dataObj.type || dataObj.type == 'text') { 175 | content = xssFilter(dataObj.data); 176 | } else if (dataObj.type == 'image') { 177 | var image = eval('(' + dataObj.data + ')'); 178 | content = '
    '; 180 | } 181 | 182 | var fromId = dataObj.from; 183 | var channal = dataObj.channal; 184 | 185 | content = parseXss(content); 186 | var said = ''; 187 | var time_str; 188 | 189 | if (dataObj.time) { 190 | time_str = GetDateT(dataObj.time) 191 | } else { 192 | time_str = GetDateT() 193 | } 194 | 195 | $("#msg-template .msg-time").html(time_str); 196 | if (fromId == 0) { 197 | $("#msg-template .userpic").html(""); 198 | $("#msg-template .content").html( 199 | "【系统消息】 " + content); 200 | } else { 201 | var html = ''; 202 | var to = dataObj.to; 203 | 204 | //历史记录 205 | if (channal == 3) { 206 | said = '对大家说: '; 207 | html += '【历史记录】' 208 | + dataObj.user.name + said; 209 | html += ''; 210 | } 211 | //如果说话的是我自己 212 | else { 213 | if (client_id == fromId) { 214 | if (channal == 0) { 215 | said = '我对大家说:'; 216 | } else if (channal == 1) { 217 | said = "我悄悄的对" + userlist[to] + "说:"; 218 | } 219 | html += '' + said + ' '; 220 | } else { 221 | if (channal == 0) { 222 | said = '对大家说:'; 223 | } else if (channal == 1) { 224 | said = "悄悄的对我说:"; 225 | } 226 | 227 | html += '' + userlist[fromId] + said; 229 | html += ' ' 230 | } 231 | } 232 | html += content + ''; 233 | $("#msg-template .content").html(html); 234 | } 235 | $("#chat-messages").append($("#msg-template").html()); 236 | $('#chat-messages')[0].scrollTop = 1000000; 237 | } 238 | 239 | function xssFilter(val) { 240 | val = val.replace(/&/g, '&').replace(//g, '>').replace(/\x22/g, '"').replace(/\x27/g, 242 | '''); 243 | return val; 244 | } 245 | 246 | function parseXss(val) { 247 | for (var i = 1; i < 20; i++) { 248 | val = val.replace('#' + i + '#', ''); 250 | } 251 | val = val.replace('&', '&'); 252 | return val; 253 | } 254 | 255 | function GetDateT(time_stamp) { 256 | var d; 257 | d = new Date(); 258 | 259 | if (time_stamp) { 260 | d.setTime(time_stamp * 1000); 261 | } 262 | var h, i, s; 263 | h = d.getHours(); 264 | i = d.getMinutes(); 265 | s = d.getSeconds(); 266 | 267 | h = (h < 10) ? '0' + h : h; 268 | i = (i < 10) ? '0' + i : i; 269 | s = (s < 10) ? '0' + s : s; 270 | return h + ":" + i + ":" + s; 271 | } 272 | 273 | function getRequest() { 274 | var url = location.search; // 获取url中"?"符后的字串 275 | var theRequest = new Object(); 276 | if (url.indexOf("?") != -1) { 277 | var str = url.substr(1); 278 | 279 | strs = str.split("&"); 280 | for (var i = 0; i < strs.length; i++) { 281 | var decodeParam = decodeURIComponent(strs[i]); 282 | var param = decodeParam.split("="); 283 | theRequest[param[0]] = param[1]; 284 | } 285 | 286 | } 287 | return theRequest; 288 | } 289 | 290 | function selectUser(userid) { 291 | $('#userlist').val(userid); 292 | } 293 | 294 | function delUser(userid) { 295 | $('#user_' + userid).remove(); 296 | $('#inroom_' + userid).remove(); 297 | delete (userlist[userid]); 298 | } 299 | 300 | function sendMsg(content, type) { 301 | var msg = {}; 302 | 303 | if (typeof content == "string") { 304 | content = content.replace(" ", " "); 305 | } 306 | 307 | if (!content) { 308 | return false; 309 | } 310 | 311 | if ($('#userlist').val() == 0) { 312 | msg.cmd = 'message'; 313 | msg.from = client_id; 314 | msg.channal = 0; 315 | msg.data = content; 316 | msg.type = type; 317 | ws.send($.toJSON(msg)); 318 | } else { 319 | msg.cmd = 'message'; 320 | msg.from = client_id; 321 | msg.to = $('#userlist').val(); 322 | msg.channal = 1; 323 | msg.data = content; 324 | msg.type = type; 325 | ws.send($.toJSON(msg)); 326 | } 327 | showNewMsg(msg); 328 | $('#msg_content').val('') 329 | } 330 | 331 | $(document).ready( 332 | function() { 333 | var a = ''; 334 | for (var i = 1; i < 20; i++) { 335 | a = a + ''; 338 | } 339 | $("#show_face").html(a); 340 | }); 341 | 342 | (function($) { 343 | $.fn.extend({ 344 | insertAtCaret : function(myValue) { 345 | var $t = $(this)[0]; 346 | if (document.selection) { 347 | this.focus(); 348 | sel = document.selection.createRange(); 349 | sel.text = myValue; 350 | this.focus(); 351 | } else if ($t.selectionStart || $t.selectionStart == '0') { 352 | 353 | var startPos = $t.selectionStart; 354 | var endPos = $t.selectionEnd; 355 | var scrollTop = $t.scrollTop; 356 | $t.value = $t.value.substring(0, startPos) + myValue 357 | + $t.value.substring(endPos, $t.value.length); 358 | this.focus(); 359 | $t.selectionStart = startPos + myValue.length; 360 | $t.selectionEnd = startPos + myValue.length; 361 | $t.scrollTop = scrollTop; 362 | } else { 363 | 364 | this.value += myValue; 365 | this.focus(); 366 | } 367 | } 368 | }) 369 | })(jQuery); 370 | 371 | function selectFace(id) { 372 | var img = ''; 373 | $("#msg_content").insertAtCaret('#' + id + '#'); 374 | closeChatFace(); 375 | } 376 | 377 | function showChatFace() { 378 | $("#chat_face").attr("class", "chat_face chat_face_hover"); 379 | $("#show_face").attr("class", "show_face show_face_hovers"); 380 | } 381 | 382 | function closeChatFace() { 383 | $("#chat_face").attr("class", "chat_face"); 384 | $("#show_face").attr("class", "show_face"); 385 | } 386 | 387 | function toggleFace() { 388 | $("#chat_face").toggleClass("chat_face_hover"); 389 | $("#show_face").toggleClass("show_face_hovers"); 390 | } 391 | -------------------------------------------------------------------------------- /resources/static/js/comet.js: -------------------------------------------------------------------------------- 1 | function Comet(url) { 2 | this.url = url.replace('ws://', 'http://'); 3 | this.connected = false; 4 | this.send_queue = []; 5 | this.sending = false; 6 | jQuery.support.cors = true; 7 | 8 | this.send = function(msg) { 9 | this.send_queue.push(msg); 10 | //当前状态是否可以发送数据 11 | if (this.connected && !this.sending) { 12 | this.sendMessage(); 13 | } 14 | }; 15 | 16 | this.sendMessage = function() { 17 | if (this.send_queue.length == 0) { 18 | this.sending = false; 19 | return; 20 | } 21 | 22 | var websocket = this; 23 | var msg = this.send_queue.pop(); 24 | this.sending = true; 25 | 26 | $.ajax({ 27 | type : "POST", 28 | dataType : "json", 29 | url : this.url + '/pub', 30 | data : { 31 | message : msg 32 | }, 33 | success : function(data, textStatus) { 34 | //发送数据成功 35 | if (data.success == "1") { 36 | //继续发送 37 | websocket.sendMessage(); 38 | } else { 39 | console.log("ErrorMessage: " + data); 40 | } 41 | }, 42 | error : function(XMLHttpRequest, textStatus, errorThrown) { 43 | var e = {}; 44 | e.data = textStatus; 45 | websocket.onerror(e); 46 | } 47 | }); 48 | }; 49 | 50 | //连接到服务器 51 | this.connect = function() { 52 | var websocket = this; 53 | $.ajax({ 54 | type : "POST", 55 | dataType : "json", 56 | url : this.url + '/connect', 57 | data : { 58 | 'type' : 'connect' 59 | }, 60 | success : function(data, textStatus) { 61 | //发送数据成功 62 | if (data.success == "1") { 63 | websocket.connected = true; 64 | websocket.loop(); 65 | websocket.onopen({}); 66 | } else { 67 | console.log("ErrorMessage: " + data); 68 | } 69 | }, 70 | error : function(XMLHttpRequest, textStatus, errorThrown) { 71 | var e = {}; 72 | e.data = textStatus; 73 | alert("connect to server [" + websocket.url 74 | + "] failed. Error: " + errorThrown); 75 | } 76 | }); 77 | }; 78 | 79 | this.loop = function() { 80 | var websocket = this; 81 | $.ajax({ 82 | type : "GET", 83 | dataType : "json", 84 | url : websocket.url + '/sub', 85 | timeout : 80000, //ajax请求超时时间80秒 86 | data : { 87 | time : "80" 88 | }, //80秒后无论结果服务器都返回数据 89 | success : function(data, textStatus) { 90 | var e = { 91 | 'data' : data.data 92 | }; 93 | //从服务器得到数据,显示数据并继续查询 94 | if (data.success == "1") { 95 | websocket.onmessage(e); 96 | } 97 | //未从服务器得到数据,继续查询 98 | else if (data.success == "0") { 99 | //$("#msg").append("
    [无数据]"); 100 | } else { 101 | console.log("ErrorMessage: " + data); 102 | } 103 | websocket.loop(); 104 | }, 105 | //Ajax请求超时,继续查询 106 | error : function(XMLHttpRequest, textStatus, errorThrown) { 107 | if (textStatus == "timeout") { 108 | websocket.loop(); 109 | } else { 110 | console.log("Server Error: " + textStatus); 111 | var e = {}; 112 | e.data = textStatus; 113 | websocket.onclose(e); 114 | } 115 | } 116 | }); 117 | }; 118 | this.connect(); 119 | } 120 | -------------------------------------------------------------------------------- /resources/static/js/console.js: -------------------------------------------------------------------------------- 1 | if (!console) { 2 | var console = {}; 3 | console.log = function (str) {}; 4 | console.dir = function (str) {}; 5 | } -------------------------------------------------------------------------------- /resources/static/js/jquery.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("