├── .gitignore ├── README.md ├── app.js ├── config.json ├── log.js ├── logs └── .gitignore ├── middlewares └── errorhandler.js ├── models ├── image.js ├── tug-of-war.js └── user.js ├── package.json ├── public ├── tug-of-war │ ├── script.js │ └── style.css ├── tuku │ ├── list.css │ └── list.js └── uploads │ ├── .gitignore │ └── temps │ └── .gitignore ├── routers ├── libs.js ├── login.js ├── tug-of-war.io.js ├── tug-of-war.js └── tuku.js └── views ├── login.jade ├── tug-of-war └── index.html └── tuku └── index.jade /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Runtime data 5 | pids 6 | *.pid 7 | *.seed 8 | 9 | # Directory for instrumented libs generated by jscoverage/JSCover 10 | lib-cov 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | 15 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 16 | .grunt 17 | 18 | # Compiled binary addons (http://nodejs.org/api/addons.html) 19 | build/Release 20 | 21 | # Dependency directory 22 | # Commenting this out is preferred by some people, see 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 24 | node_modules 25 | 26 | # Users Environment Variables 27 | .lock-wscript 28 | 29 | .DS_Store 30 | 31 | config.js 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | iro 2 | ======= 3 | colorful 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var app = express() 3 | var http = require('http').Server(app) 4 | var io = require('socket.io')(http) 5 | var mongoose = require('mongoose') 6 | var session = require('express-session') 7 | 8 | var config = require('./config') 9 | 10 | mongoose.connect(config.db) 11 | 12 | app.use(express.static(`${__dirname}/public`)) 13 | 14 | app.use('/libs', require('./routers/libs')) 15 | 16 | require('./routers/tug-of-war.io')(io.of('/tug-of-war')) 17 | app.use( 18 | '/tug', 19 | function (req, res, next) { 20 | res.locals.io = io.of('/tug-of-war') 21 | next() 22 | }, 23 | require('./routers/tug-of-war') 24 | ) 25 | 26 | app.engine('jade', require('jade').__express) 27 | app.set('view engine', 'jade') 28 | 29 | app.use(session({ 30 | secret: config.sessionSecret, 31 | resave: false, 32 | saveUninitialized: true 33 | })) 34 | 35 | app.use('/', require('./routers/tuku')) 36 | app.use('/tuku', require('./routers/tuku')) 37 | app.use('/login', require('./routers/login')) 38 | 39 | app.use(function (req, res, next) { 40 | res.sendStatus(404) 41 | }) 42 | 43 | app.use(require('./middlewares/errorhandler')) 44 | 45 | http.listen(config.port) 46 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "mongodb://localhost/tug-of-war", 3 | "sessionSecret": "secret", 4 | "port": 1338 5 | } 6 | -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var moment = require('moment') 3 | 4 | module.exports = function (err) { 5 | var showInConsole = true 6 | var callback 7 | 8 | if (arguments.length === 2) { 9 | if (typeof arguments[1] === 'function') callback = arguments[1] 10 | else showInConsole = arguments[1] 11 | } else if (arguments.length > 2) { 12 | showInConsole = arguments[1] 13 | callback = arguments[2] 14 | } 15 | 16 | var path = `${__dirname}/logs/${moment().format('YYYY-MM-DD')}.log` 17 | var content = `\n\n\n${moment().format('HH:mm:ss')}\n${err.stack || err}\n\n\n` 18 | if (showInConsole) console.log(content) 19 | fs.appendFile(path, content, callback) 20 | } 21 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /middlewares/errorhandler.js: -------------------------------------------------------------------------------- 1 | var log = require('../log') 2 | 3 | module.exports = function (err, req, res, next) { 4 | log(err, false) 5 | next(err) 6 | } 7 | -------------------------------------------------------------------------------- /models/image.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | 3 | var imageSchema = new mongoose.Schema({ 4 | _id: String, 5 | uploadTime: { type: Date, index: true }, 6 | fileName: String, 7 | width: Number, 8 | height: Number 9 | }, { autoIndex: false }) 10 | 11 | module.exports = mongoose.model('Image', imageSchema) 12 | -------------------------------------------------------------------------------- /models/tug-of-war.js: -------------------------------------------------------------------------------- 1 | var data = { 2 | onlines: 0, 3 | teams: { 4 | a: { score: 0, power: 0, image: null }, 5 | b: { score: 0, power: 0, image: null } 6 | } 7 | } 8 | 9 | module.exports = { 10 | get onlines() { 11 | return data.onlines 12 | }, 13 | set onlines(value) { 14 | if (typeof value !== 'number') return 15 | data.onlines = value 16 | }, 17 | get teams() { 18 | return { 19 | get a() { 20 | return { 21 | get score() { 22 | return data.teams.a.score 23 | }, 24 | set score(value) { 25 | if (typeof value !== 'number') return 26 | data.teams.a.score = value 27 | }, 28 | get power() { 29 | return data.teams.a.power 30 | }, 31 | set power(value) { 32 | if (typeof value !== 'number') return 33 | data.teams.a.power = value 34 | }, 35 | get image() { 36 | return data.teams.a.image 37 | }, 38 | set image(value) { 39 | if ( 40 | typeof value !== 'string' && 41 | value !== null 42 | ) { 43 | return 44 | } 45 | data.teams.a.image = value 46 | } 47 | } 48 | }, 49 | get b() { 50 | return { 51 | get score() { 52 | return data.teams.b.score 53 | }, 54 | set score(value) { 55 | if (typeof value !== 'number') return 56 | data.teams.b.score = value 57 | }, 58 | get power() { 59 | return data.teams.b.power 60 | }, 61 | set power(value) { 62 | if (typeof value !== 'number') return 63 | data.teams.b.power = value 64 | }, 65 | get image() { 66 | return data.teams.b.image 67 | }, 68 | set image(value) { 69 | if ( 70 | typeof value !== 'string' && 71 | value !== null 72 | ) { 73 | return 74 | } 75 | data.teams.b.image = value 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | 3 | var userSchema = new mongoose.Schema({ 4 | name: String, 5 | password: String, 6 | role: String 7 | }) 8 | 9 | module.exports = mongoose.model('User', userSchema) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iro", 3 | "version": "0.2.0", 4 | "main": "app.js", 5 | "author": "Kuro", 6 | "scripts": { 7 | "start": "node app" 8 | }, 9 | "dependencies": { 10 | "async": "^1.2.1", 11 | "bcrypt": "^0.8.3", 12 | "body-parser": "^1.13.1", 13 | "express": "^4.12.4", 14 | "express-session": "^1.11.3", 15 | "jade": "^1.11.0", 16 | "moment": "^2.10.3", 17 | "mongoose": "^4.0.6", 18 | "multer": "^0.1.8", 19 | "socket.io": "^1.3.5", 20 | "vue": "^0.12.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/tug-of-war/script.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | var socket = io.connect(location.host + '/tug-of-war') 3 | 4 | var app = new Vue({ 5 | el: document.documentElement, 6 | data: { 7 | loaded: false, 8 | myTeam: null, 9 | onlines: null, 10 | teams: { 11 | a: { score: null, power: null, image: null }, 12 | b: { score: null, power: null, image: null } 13 | } 14 | }, 15 | ready: function () { 16 | document.addEventListener('mousedown', function (e) { 17 | if (e.target.tagName === 'INPUT') 18 | return 19 | 20 | this.$$.textbox_a.blur() 21 | this.$$.textbox_b.blur() 22 | }.bind(this)) 23 | 24 | document.addEventListener('touchstart', function (e) { 25 | if (e.target.tagName === 'INPUT') 26 | return 27 | 28 | this.$$.textbox_a.blur() 29 | this.$$.textbox_b.blur() 30 | }.bind(this)) 31 | 32 | socket.on('update', this.update) 33 | socket.on('tucao', this.onTucao) 34 | }, 35 | methods: { 36 | preventDefault: function (e) { 37 | e.preventDefault() 38 | }, 39 | stopPropagation: function (e) { 40 | e.stopPropagation() 41 | }, 42 | oncontextmenu: function (e) { 43 | var image = this.teams[this.myTeam].image 44 | if (image) { 45 | e.preventDefault() 46 | window.open(image) 47 | } 48 | }, 49 | tug: function (team, event) { 50 | if (!this.teams[team]) 51 | return 52 | 53 | if ((event === 'click' || event === 'touchstart') && !this.teams[team].image) { 54 | return this.uploadImageFile(team) 55 | } 56 | 57 | if (event === 'click') 58 | return 59 | 60 | if (!this.teams[team].image) 61 | return 62 | 63 | this.myTeam = team 64 | socket.emit('tug', this.myTeam) 65 | 66 | var tip = document.createElement('span') 67 | tip.classList.add('tip') 68 | tip.innerHTML = '+1' 69 | 70 | this.$$['team_' + team].appendChild(tip) 71 | 72 | setTimeout(function () { 73 | this.$$['team_' + team].removeChild(tip) 74 | }.bind(this), 1000) 75 | }, 76 | uploadImageFile: function (team) { 77 | if (this.teams[team].image) 78 | return 79 | this.openFileDialog(function (file) { 80 | this.readImageFileSize(file, function (err, width, height) { 81 | if (err) return alert(err) 82 | 83 | var xhr = new XMLHttpRequest() 84 | xhr.open( 85 | 'PUT', 86 | location.pathname.replace(/\/$/, '') + 87 | '/teams/' + team + '/image' 88 | , 89 | true 90 | ) 91 | var formData = new FormData() 92 | formData.append('width', width) 93 | formData.append('height', height) 94 | formData.append('image', file) 95 | xhr.send(formData) 96 | }.bind(this)) 97 | }.bind(this)) 98 | }, 99 | openFileDialog: function (callback) { 100 | var input = document.createElement('input') 101 | input.type = 'file' 102 | input.accept = 'image/*' 103 | input.style.visibility = 'hidden' 104 | input.onchange = function (e) { 105 | clearTimeout(timeoutId) 106 | document.body.removeChild(input) 107 | callback(e.target.files[0]) 108 | } 109 | document.body.appendChild(input) 110 | input.click() 111 | var timeoutId = setTimeout(function () { 112 | document.body.removeChild(input) 113 | }, 60000) 114 | }, 115 | readImageFileSize: function (file, callback) { 116 | this.readFileAsURL(file, function (err, url) { 117 | if (err) return callback(err) 118 | 119 | var revoke = function (url) { 120 | if (url.indexOf('blob') === 0) { 121 | window.URL.revokeObjectURL(url) 122 | } 123 | } 124 | 125 | var img = document.createElement('img') 126 | img.onload = function (e) { 127 | revoke(url) 128 | callback(null, e.target.width, e.target.height) 129 | } 130 | img.onerror = function (e) { 131 | revoke(url) 132 | callback(new Error('图片加载失败')) 133 | } 134 | img.src = url 135 | }) 136 | }, 137 | readFileAsURL: function (file, callback) { 138 | var fr 139 | 140 | try { 141 | if (window.URL) { 142 | callback(null, window.URL.createObjectURL(file)) 143 | } else if (FileReader) { 144 | fr = new FileReader 145 | fr.onload = function (e) { 146 | callback(null, e.target.result) 147 | } 148 | fr.onerror = function (e) { 149 | throw e.target 150 | callback(e.target.error) 151 | } 152 | fr.readAsDataURL(file) 153 | } else { 154 | throw new Error('没有可用的 FileAPI') 155 | } 156 | } catch (err) { 157 | callback(err) 158 | } 159 | }, 160 | tucaoA: function (e) { 161 | e.preventDefault() 162 | 163 | var input = this.$$.textbox_a 164 | 165 | var value = input.value 166 | 167 | if (!value || value.length > 20) 168 | return 169 | 170 | socket.emit('tucao', 'a', value) 171 | 172 | input.value = '' 173 | }, 174 | tucaoB: function (e) { 175 | e.preventDefault() 176 | 177 | var input = this.$$.textbox_b 178 | 179 | var value = input.value 180 | 181 | if (!value || value.length > 20) 182 | return 183 | 184 | socket.emit('tucao', 'b', value) 185 | 186 | input.value = '' 187 | }, 188 | onTucao: function (team, content) { 189 | var tipTucao = document.createElement('div') 190 | tipTucao.classList.add('tip-tucao') 191 | tipTucao.classList.add('tip-tucao-' + team) 192 | tipTucao.textContent = content 193 | 194 | this.$$['tucao_area_' + team].appendChild(tipTucao) 195 | 196 | setTimeout(function () { 197 | tipTucao.classList.add('leave') 198 | setTimeout(function () { 199 | this.$$['tucao_area_' + team].removeChild(tipTucao) 200 | }.bind(this), 1000) 201 | }.bind(this), 6000) 202 | }, 203 | update: function (data) { 204 | if (!this.loaded) 205 | this.loaded = true 206 | 207 | this.teams.a.score = data.teams.a.score 208 | this.teams.a.power = data.teams.a.power 209 | this.teams.a.image = data.teams.a.image 210 | this.teams.b.score = data.teams.b.score 211 | this.teams.b.power = data.teams.b.power 212 | this.teams.b.image = data.teams.b.image 213 | 214 | this.onlines = data.onlines 215 | } 216 | } 217 | }) 218 | })() 219 | -------------------------------------------------------------------------------- /public/tug-of-war/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font-family: 7 | "PingFang SC", 8 | "Microsoft JhengHei", 9 | "Lantinghei SC", 10 | "Hiragino Sans GB", 11 | "WenQuanYi Micro Hei", 12 | "Microsoft YaHei", 13 | sans-serif; 14 | -ms-text-size-adjust: 100%; 15 | -webkit-text-size-adjust: 100%; 16 | -webkit-font-smoothing: antialiased; 17 | margin: 0; 18 | } 19 | 20 | input { 21 | font-family: inherit; 22 | text-rendering: inherit; 23 | } 24 | 25 | #app { 26 | position: fixed; 27 | left: 0; 28 | right: 0; 29 | top: 0; 30 | bottom: 0; 31 | } 32 | 33 | #line { 34 | position: absolute; 35 | left: 0; 36 | right: 0; 37 | top: 62%; 38 | bottom: 38%; 39 | margin: auto; 40 | width: 62%; 41 | height: 2px; 42 | background-color: black; 43 | } 44 | 45 | #flag { 46 | position: absolute; 47 | margin: 0 auto; 48 | width: 0; 49 | height: 0; 50 | -webkit-transform: translateX(-50%); 51 | transform: translateX(-50%); 52 | border-left: 10px solid transparent; 53 | border-right: 10px solid transparent; 54 | border-top: 30px solid black; 55 | border-bottom: 30px solid transparent; 56 | transition: all .2s; 57 | } 58 | 59 | #team_a, #team_b { 60 | position: absolute; 61 | width: 400px; 62 | height: 400px; 63 | border-radius: 16px; 64 | bottom: 38%; 65 | margin: auto 0; 66 | transition: all .6s; 67 | background-size: contain; 68 | background-repeat: no-repeat; 69 | background-position: center; 70 | box-shadow: 0 1px 4px #aaa; 71 | z-index: 2; 72 | font-size: 27px; 73 | } 74 | #team_a { 75 | left: 19%; 76 | } 77 | #team_b { 78 | right: 19%; 79 | } 80 | 81 | #scores { 82 | text-align: center; 83 | font-size: 60px; 84 | font-weight: inherit; 85 | margin: 0; 86 | position: absolute; 87 | left: 0; 88 | right: 0; 89 | top: 3%; 90 | } 91 | #scores_text { 92 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 93 | } 94 | 95 | @media screen and (max-width: 768px) { 96 | #scores { 97 | font-size: 30px; 98 | } 99 | } 100 | 101 | .tip { 102 | position: absolute; 103 | left: 0; 104 | right: 0; 105 | top: -1.3em; 106 | text-align: center; 107 | font-size: 70px; 108 | color: #e57373; 109 | -webkit-animation: leave .6s both; 110 | animation: leave .6s both; 111 | } 112 | 113 | #github { 114 | position: fixed; 115 | right: 10px; 116 | bottom: 10px; 117 | color: black; 118 | text-decoration: none; 119 | } 120 | 121 | #loading { 122 | text-align: center; 123 | position: fixed; 124 | left: 0; 125 | right: 0; 126 | top: 30%; 127 | font-size: 40px; 128 | } 129 | 130 | #form_tucao_a, #form_tucao_b { 131 | position: absolute; 132 | bottom: 3%; 133 | margin: auto; 134 | height: 30px; 135 | width: 200px; 136 | z-index: 1; 137 | } 138 | #form_tucao_a { 139 | left: 19%; 140 | -webkit-transform: translate(-50%); 141 | transform: translate(-50%); 142 | } 143 | #form_tucao_b { 144 | right: 19%; 145 | -webkit-transform: translate(50%); 146 | transform: translate(50%); 147 | } 148 | 149 | .textbox-tucao { 150 | text-align: center; 151 | font-size: 16px; 152 | width: 100%; 153 | border: none; 154 | border-bottom: 2px solid #ccc; 155 | outline: none; 156 | transition: all .2s; 157 | padding-bottom: 4px; 158 | } 159 | 160 | .textbox-tucao:focus { 161 | border-bottom-color: #39f; 162 | } 163 | 164 | .tucao-area { 165 | position: absolute; 166 | bottom: 413px; 167 | left: -500px; 168 | right: -500px; 169 | z-index: 1; 170 | -webkit-transform: translateZ(0); 171 | transform: translateZ(0); 172 | } 173 | 174 | .tip-tucao { 175 | -webkit-transform: translateZ(0); 176 | transform: translateZ(0); 177 | text-align: center; 178 | height: 1.2em; 179 | -webkit-animation: height-enter .2s both; 180 | animation: height-enter .2s both; 181 | } 182 | .tip-tucao.leave { 183 | -webkit-animation: leave .6s both; 184 | animation: leave .6s both; 185 | } 186 | 187 | @media screen and (max-width: 1500px) { 188 | #team_a, #team_b { 189 | width: 300px; 190 | height: 300px; 191 | font-size: 25px; 192 | } 193 | 194 | .tucao-area { 195 | bottom: 313px; 196 | } 197 | } 198 | 199 | @media screen and (max-width: 1000px) { 200 | #team_a, #team_b { 201 | width: 200px; 202 | height: 200px; 203 | font-size: 22px; 204 | } 205 | 206 | .tucao-area { 207 | bottom: 213px; 208 | } 209 | } 210 | 211 | @media screen and (max-width: 600px) { 212 | #team_a, #team_b { 213 | width: 100px; 214 | height: 100px; 215 | font-size: 18px; 216 | } 217 | 218 | .tucao-area { 219 | bottom: 113px; 220 | } 221 | } 222 | 223 | #onlines { 224 | position: fixed; 225 | right: 10px; 226 | top: 10px; 227 | } 228 | 229 | #link_tuku { 230 | position: fixed; 231 | left: 10px; 232 | top: 10px; 233 | color: black; 234 | text-decoration: none; 235 | } 236 | 237 | @-webkit-keyframes leave { 238 | from { 239 | opacity: 1; 240 | -webkit-transform: translateY(0); 241 | transform: translateY(0); 242 | } 243 | to { 244 | opacity: 0; 245 | -webkit-transform: translateY(-19.1%); 246 | transform: translateY(-19.1%); 247 | } 248 | } 249 | 250 | @keyframes leave { 251 | from { 252 | opacity: 1; 253 | -webkit-transform: translateY(0); 254 | transform: translateY(0); 255 | } 256 | to { 257 | opacity: 0; 258 | -webkit-transform: translateY(-19.1%); 259 | transform: translateY(-19.1%); 260 | } 261 | } 262 | 263 | @-webkit-keyframes height-enter { 264 | from { 265 | height: 0; 266 | opacity: 0; 267 | } 268 | to { 269 | height: 1.2em; 270 | opacity: 1; 271 | } 272 | } 273 | 274 | @keyframes height-enter { 275 | from { 276 | height: 0; 277 | opacity: 0; 278 | } 279 | to { 280 | height: 1.2em; 281 | opacity: 1; 282 | } 283 | } 284 | 285 | @-webkit-keyframes hide-leave { 286 | from { 287 | opacity: 1; 288 | } 289 | to { 290 | opacity: 0; 291 | } 292 | } 293 | 294 | @keyframes hide-leave { 295 | from { 296 | opacity: 1; 297 | } 298 | to { 299 | opacity: 0; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /public/tuku/list.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | "PingFang SC", 5 | "Microsoft JhengHei", 6 | "Lantinghei SC", 7 | "Hiragino Sans GB", 8 | "WenQuanYi Micro Hei", 9 | "Microsoft YaHei", 10 | sans-serif; 11 | -ms-text-size-adjust: 100%; 12 | -webkit-text-size-adjust: 100%; 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | #header { 17 | position: relative; 18 | border-bottom: 1px solid #eee; 19 | } 20 | 21 | #header .container { 22 | height: 48px; 23 | position: relative; 24 | margin: 0 20px; 25 | } 26 | 27 | #header .container h1 { 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | margin: 0; 32 | } 33 | 34 | #header .container h1 a { 35 | display: block; 36 | width: 72px; 37 | height: 48px; 38 | font-size: 48px; 39 | color: #000; 40 | font-family: Italiana,"Helvetica Neue",Helvetica,Arial,"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; 41 | background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODciIGhlaWdodD0iMjciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHJlY3QgeD0iMCIgeT0iMCIgcng9IjQiIHJ5PSI0IiB3aWR0aD0iMjciIGhlaWdodD0iMjciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LDk1LDk1KTsgc3Ryb2tlLXdpZHRoOiAxOyIgLz4KICA8cmVjdCB4PSIzMCIgeT0iMCIgcng9IjQiIHJ5PSI0IiB3aWR0aD0iMjciIGhlaWdodD0iMjciIHN0eWxlPSJmaWxsOiByZ2IoMTY4LDIxMSwzNik7IHN0cm9rZS13aWR0aDogMTsiIC8+CiAgPHJlY3QgeD0iNjAiIHk9IjAiIHJ4PSI0IiByeT0iNCIgd2lkdGg9IjI3IiBoZWlnaHQ9IjI3IiBzdHlsZT0iZmlsbDogcmdiKDgwLDE5MiwyMzMpOyBzdHJva2Utd2lkdGg6IDE7IiAvPgo8L3N2Zz4=); 42 | background-repeat: no-repeat; 43 | background-size: 48px; 44 | background-position: center center; 45 | } 46 | 47 | #nav { 48 | position: absolute; 49 | right: 0; 50 | top: 0; 51 | width: 224px; 52 | height: 48px; 53 | line-height: 48px; 54 | text-align: right; 55 | } 56 | 57 | #nav ul { 58 | margin: 0; 59 | } 60 | 61 | #nav li { 62 | display: inline-block; 63 | } 64 | 65 | #nav li a { 66 | display: block; 67 | padding: 0 19px; 68 | text-decoration: none; 69 | color: #258fb8; 70 | } 71 | 72 | #tug_link { 73 | position: absolute; 74 | left: 0; 75 | right: 0; 76 | margin: 0 auto; 77 | width: 40px; 78 | height: 48px; 79 | line-height: 48px; 80 | text-align: center; 81 | text-decoration: none; 82 | color: #258fb8; 83 | z-index: 1; 84 | } 85 | 86 | #image_list { 87 | position: relative; 88 | margin: 0 auto; 89 | text-align: center; 90 | } 91 | 92 | .image-item { 93 | display: inline-block; 94 | margin: 1%; 95 | background-color: white; 96 | box-sizing: border-box; 97 | max-width: 100%; 98 | } 99 | 100 | .image-item-img { 101 | width: 100%; 102 | vertical-align: middle; 103 | } 104 | 105 | #page_nav { 106 | padding: 30px 0; 107 | text-align: center; 108 | } 109 | 110 | .button-page { 111 | font-size: 18px; 112 | display: inline-block; 113 | padding: 6px 0; 114 | text-align: center; 115 | border: 1px solid #ddd; 116 | color: #777; 117 | cursor: pointer; 118 | -moz-user-select: -moz-none; 119 | -ms-user-select: none; 120 | -webkit-user-select: none; 121 | user-select: none; 122 | border-right: none; 123 | min-width: 40px; 124 | text-decoration: none; 125 | } 126 | 127 | .button-page:hover, .button-page:active { 128 | background-color: #eee; 129 | } 130 | 131 | .button-page:active { 132 | box-shadow: inset 0 1px 5px #ccc; 133 | } 134 | 135 | .button-page.current { 136 | color: #0b2; 137 | } 138 | 139 | .button-page:first-child { 140 | border-top-left-radius: 4px; 141 | border-bottom-left-radius: 4px; 142 | } 143 | 144 | .button-page:last-child { 145 | border-right: 1px solid #ddd; 146 | border-top-right-radius: 4px; 147 | border-bottom-right-radius: 4px; 148 | } 149 | -------------------------------------------------------------------------------- /public/tuku/list.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | Array.prototype.forEach.call( 3 | document.querySelectorAll('.image-item'), 4 | function (item) { 5 | item.addEventListener('contextmenu', onContextmenu) 6 | } 7 | ) 8 | 9 | function onContextmenu(e) { 10 | if (!user || user.role !== 'admin') return 11 | e.preventDefault() 12 | window.open('/tuku/delete?id=' + e.currentTarget.dataset.id) 13 | } 14 | })() 15 | -------------------------------------------------------------------------------- /public/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !temps 4 | -------------------------------------------------------------------------------- /public/uploads/temps/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /routers/libs.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var fs = require('fs') 4 | var path = require('path') 5 | 6 | router.get('/vue.js', function (req, res) { 7 | res.sendFile(path.normalize( 8 | `${__dirname}/../node_modules/vue/dist/vue.min.js` 9 | )) 10 | }) 11 | 12 | router.get('/async.js', function (req, res) { 13 | res.sendFile(path.normalize( 14 | `${__dirname}/../node_modules/async/lib/async.js` 15 | )) 16 | }) 17 | 18 | module.exports = router 19 | -------------------------------------------------------------------------------- /routers/login.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var bodyParser = require('body-parser') 4 | var bcrypt = require('bcrypt') 5 | 6 | var User = require('../models/user') 7 | 8 | router.use(bodyParser.json()) 9 | router.use(bodyParser.urlencoded({ extended: true })) 10 | 11 | router.get('/', function (req, res, next) { 12 | res.render('login') 13 | }) 14 | 15 | router.post('/', function (req, res, next) { 16 | var name = req.body.name 17 | var password = req.body.password 18 | 19 | User.findOne({ name: name }, function (err, user) { 20 | if (!user) return res.sendStatus(401) 21 | bcrypt.compare(password, user.password, function (err, equal) { 22 | if (err) return next(err) 23 | if (equal) { 24 | req.session.user = { 25 | id: user._id, 26 | name: user.name, 27 | role: user.role 28 | } 29 | res.sendStatus(201) 30 | } else { 31 | res.sendStatus(401) 32 | } 33 | }) 34 | }) 35 | }) 36 | 37 | module.exports = router 38 | -------------------------------------------------------------------------------- /routers/tug-of-war.io.js: -------------------------------------------------------------------------------- 1 | var tugOfWar = require('../models/tug-of-war') 2 | 3 | module.exports = function (io) { 4 | var updateTimeoutId 5 | 6 | io.on('connection', function (socket) { 7 | tugOfWar.onlines += 1 8 | 9 | socket.emit('update', tugOfWar) 10 | 11 | var tugTimeoutId 12 | 13 | socket.on('tug', function (team) { 14 | if (tugTimeoutId) 15 | return 16 | 17 | tugTimeoutId = setTimeout(function () { 18 | tugTimeoutId = null 19 | 20 | if (!tugOfWar.teams[team] || !tugOfWar.teams.a.image || !tugOfWar.teams.b.image) 21 | return 22 | 23 | if (tugOfWar.teams[team].image) 24 | tugOfWar.teams[team].score += 1 25 | 26 | if (Math.abs(tugOfWar.teams.a.score - tugOfWar.teams.b.score) > 100) { 27 | tugOfWar.teams.a.score = 0 28 | tugOfWar.teams.a.power = 0 29 | tugOfWar.teams.a.image = null 30 | tugOfWar.teams.b.score = 0 31 | tugOfWar.teams.b.power = 0 32 | tugOfWar.teams.b.image = null 33 | } 34 | 35 | if (updateTimeoutId) 36 | return 37 | 38 | updateTimeoutId = setTimeout(function () { 39 | updateTimeoutId = null 40 | io.emit('update', tugOfWar) 41 | }, 16) 42 | }, 100) 43 | }) 44 | 45 | var tucaoTimeoutId 46 | var tucaoCount = 0 47 | var clearTucaoCountTimeoutId 48 | 49 | socket.on('tucao', function (team, content) { 50 | tucaoCount += 1 51 | 52 | if (!clearTucaoCountTimeoutId) 53 | clearTucaoCountTimeoutId = setTimeout(function () { 54 | clearTucaoCountTimeoutId = null 55 | tucaoCount = 0 56 | }, 100000) 57 | 58 | if ( 59 | tucaoTimeoutId || 60 | !content || !content.trim() || 61 | content.length > 20 62 | ) 63 | return 64 | 65 | tucaoTimeoutId = setTimeout(function () { 66 | tucaoTimeoutId = null 67 | 68 | if (tucaoCount > 100) { 69 | clearTimeout(clearTucaoCountTimeoutId) 70 | socket.emit('tucao', team, content) 71 | return 72 | } 73 | 74 | io.emit('tucao', team, content) 75 | }, 16) 76 | }) 77 | 78 | socket.on('disconnect', function () { 79 | tugOfWar.onlines -= 1 80 | }) 81 | }) 82 | 83 | var lastTime = new Date().getTime() 84 | var lastScoreA = tugOfWar.teams.a.score 85 | var lastScoreB = tugOfWar.teams.b.score 86 | 87 | setInterval(function () { 88 | var now = new Date().getTime() 89 | var timespan = (now - lastTime) 90 | 91 | tugOfWar.teams.a.power = (tugOfWar.teams.a.score - lastScoreA) / timespan * 1000 92 | tugOfWar.teams.b.power = (tugOfWar.teams.b.score - lastScoreB) / timespan * 1000 93 | 94 | lastTime = now 95 | lastScoreA = tugOfWar.teams.a.score 96 | lastScoreB = tugOfWar.teams.b.score 97 | 98 | io.emit('update', tugOfWar) 99 | }, 1000) 100 | } 101 | -------------------------------------------------------------------------------- /routers/tug-of-war.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | var router = express.Router() 5 | var multer = require('multer') 6 | var crypto = require('crypto') 7 | var fs = require('fs') 8 | var path = require('path') 9 | var async = require('async') 10 | 11 | var log = require ('../log') 12 | var tugOfWar = require('../models/tug-of-war') 13 | var Image = require('../models/image') 14 | 15 | router.use(multer({ 16 | dest: './public/uploads/temps/', 17 | limits: { 18 | files: 1, 19 | fileSize: 10 * 1024 * 1024, 20 | fields: 2 21 | } 22 | })) 23 | 24 | router.get('/', function (req, res, next) { 25 | var viewsDirname = req.app.get('views') 26 | res.sendFile(path.normalize( 27 | `${viewsDirname}/tug-of-war/index.html` 28 | ), function (err) { 29 | if (err) return next(err) 30 | }) 31 | }) 32 | 33 | router.put('/teams/:name/image', function (req, res, next) { 34 | var name = req.params.name 35 | var width = req.body.width 36 | var height = req.body.height 37 | var imageFile = req.files && req.files.image 38 | 39 | if (!imageFile) 40 | return res.sendStatus(400) 41 | 42 | var team = tugOfWar.teams[name] 43 | 44 | if (!team || imageFile.mimetype.indexOf('image/') !== 0) { 45 | fs.unlink(imageFile.path) 46 | return res.sendStatus(400) 47 | } 48 | 49 | async.waterfall([ 50 | function (callback) { 51 | var md5 = crypto.createHash('md5') 52 | var s = fs.ReadStream(imageFile.path) 53 | s.on('data', function (d) { 54 | md5.update(d) 55 | }) 56 | s.on('end', function () { 57 | callback(null, md5.digest('hex')) 58 | }) 59 | }, 60 | function (imageId, callback) { 61 | Image.findById(imageId, 'fileName', function (err, image) { 62 | if (err) return callback(err) 63 | if (image) { 64 | if (team.image) return res.sendStatus(400) 65 | team.image = `/uploads/${image.fileName}` 66 | res.sendStatus(201) 67 | res.locals.io.emit('update', tugOfWar) 68 | return 69 | } 70 | callback(null, imageId) 71 | }) 72 | }, 73 | function (imageId, callback) { 74 | const FILE_NAME = `${imageId}${path.extname(imageFile.name)}` 75 | const IMAGE_SRC = `/uploads/${FILE_NAME}` 76 | 77 | fs.rename( 78 | imageFile.path, 79 | path.normalize(`${__dirname}/../public${IMAGE_SRC}`), 80 | function (err) { 81 | if (err) return callback(err) 82 | 83 | if (team.image) return res.sendStatus(400) 84 | 85 | team.image = IMAGE_SRC 86 | res.sendStatus(201) 87 | res.locals.io.emit('update', tugOfWar) 88 | 89 | callback(null, imageId, FILE_NAME) 90 | } 91 | ) 92 | }, 93 | function (imageId, fileName, callback) { 94 | Image.findByIdAndUpdate( 95 | imageId, 96 | { 97 | uploadTime: new Date(), 98 | fileName: fileName, 99 | width: width, 100 | height: height 101 | }, 102 | { upsert: true, select: '_id' }, 103 | function (err) { 104 | if (err) return log(err) 105 | callback() 106 | } 107 | ) 108 | } 109 | ], function (err) { 110 | if (err) return next(err) 111 | }) 112 | }) 113 | 114 | module.exports = router 115 | -------------------------------------------------------------------------------- /routers/tuku.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | var router = express.Router() 5 | var bodyParser = require('body-parser') 6 | var async = require('async') 7 | var fs = require('fs') 8 | var path = require('path') 9 | 10 | var Image = require('../models/image') 11 | 12 | router.use(bodyParser.json()) 13 | router.use(bodyParser.urlencoded({ extended: true })) 14 | 15 | const PAGE_SIZE = 20 16 | 17 | router.get('/', function (req, res, next) { 18 | var page = Math.floor(req.query.page) || 1 19 | 20 | renderList(req, res, next, null, '-uploadTime', page) 21 | }) 22 | 23 | router.get('/random', function (req, res, next) { 24 | renderList(req, res, next, 'random', null, 1, '随便看看') 25 | }) 26 | 27 | router.get('/today', function (req, res, next) { 28 | var page = Math.floor(req.query.page) || 1 29 | 30 | var start = new Date() 31 | start.setHours(0) 32 | start.setMinutes(0) 33 | start.setSeconds(0) 34 | start.setMilliseconds(0) 35 | 36 | var end = new Date() 37 | end.setHours(24) 38 | end.setMinutes(0) 39 | end.setSeconds(0) 40 | end.setMilliseconds(0) 41 | 42 | var selector = { 43 | uploadTime: { 44 | $gte: start, 45 | $lt: end 46 | } 47 | } 48 | 49 | renderList(req, res, next, selector, '-uploadTime', page, '今日最新') 50 | }) 51 | 52 | router.all('/delete', function (req, res, next) { 53 | var id = req.query.id 54 | 55 | if ( 56 | !req.session || 57 | !req.session.user || 58 | req.session.user.role !== 'admin' 59 | ) { 60 | return res.sendStatus(403) 61 | } 62 | 63 | Image.findByIdAndRemove( 64 | { _id: id }, 65 | { select: 'fileName' }, 66 | function (err, image) { 67 | if (err) return next(err) 68 | 69 | if (!image) return res.sendStatus(404) 70 | 71 | fs.unlink( 72 | path.normalize(`${__dirname}/../public/uploads/${image.fileName}`), 73 | function (err) { 74 | if (err) return next(err) 75 | } 76 | ) 77 | 78 | res.redirect('back') 79 | } 80 | ) 81 | }) 82 | 83 | function renderList(req, res, next, selector, sort, page, title) { 84 | async.waterfall([ 85 | function (callback) { 86 | var query 87 | 88 | if (selector === 'random') { 89 | res.locals.isRandom = true 90 | Image.find().select('_id').exec(function (err, images) { 91 | if (err) return next(err) 92 | 93 | var ids = [] 94 | 95 | var i = 0, index 96 | 97 | for (i = 0; i < PAGE_SIZE; i++) { 98 | index = Math.floor(Math.random() * images.length) 99 | 100 | var newId = images[index]._id 101 | 102 | if (ids.some(function (id) { 103 | return id === newId 104 | })) { 105 | i -= 1 106 | continue 107 | } 108 | 109 | ids.push(newId) 110 | } 111 | 112 | var query = Image.find({ _id: { $in: ids } }) 113 | 114 | callback(null, query, PAGE_SIZE) 115 | }) 116 | } else { 117 | res.locals.isRandom = false 118 | Image.find(selector).count(function (err, count) { 119 | if (err) return next(err) 120 | callback(null, Image.find(selector), count) 121 | }) 122 | } 123 | }, 124 | function (query, count, callback) { 125 | var pageCount = Math.max(Math.ceil(count / PAGE_SIZE), 1) 126 | 127 | if (page < 1) return res.redirect('?page=1') 128 | else if (page > pageCount) return res.redirect('?page=' + pageCount) 129 | 130 | var buttonCountHalf = 2 131 | 132 | var startPage 133 | 134 | if (page <= buttonCountHalf || pageCount < buttonCountHalf * 2 + 1) { 135 | startPage = 1 136 | } else if (page > pageCount - buttonCountHalf) { 137 | startPage = pageCount - buttonCountHalf * 2 138 | } else { 139 | startPage = page - buttonCountHalf 140 | } 141 | 142 | var pageButtons = [] 143 | 144 | if (startPage !== 1){ 145 | pageButtons.push({ 146 | text: '首', 147 | value: 1 148 | }) 149 | } 150 | 151 | var i 152 | 153 | for (i = startPage; i < startPage + 5 && i <= pageCount; i++) { 154 | pageButtons.push({ 155 | text: i.toString(), 156 | value: i 157 | }) 158 | } 159 | 160 | if (i - 1 !== pageCount) { 161 | pageButtons.push({ 162 | text: '尾', 163 | value: pageCount 164 | }) 165 | } 166 | 167 | query 168 | .sort(sort) 169 | .select('fileName width height') 170 | .skip((page - 1) * PAGE_SIZE) 171 | .limit(PAGE_SIZE) 172 | .exec(function (err, images) { 173 | if (err) return next(err) 174 | res.render('tuku/index', { 175 | title: title, 176 | user: req.session.user, 177 | page: page, 178 | pageCount: pageCount, 179 | pageButtons: pageButtons, 180 | images: images.map(function (image) { 181 | return { 182 | id: image._id, 183 | src: `/uploads/${image.fileName}`, 184 | width: image.width, 185 | height: image.height, 186 | uploadTime: image.uploadTime 187 | } 188 | }) 189 | }) 190 | }) 191 | } 192 | ]) 193 | } 194 | 195 | module.exports = router 196 | -------------------------------------------------------------------------------- /views/login.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | meta(content='IE=edge', http-equiv='X-UA-Compatible') 6 | meta(name='renderer', content='webkit') 7 | meta(name='viewport', content='width=device-width,initial-scale=1,user-scalable=no') 8 | meta(name='apple-mobile-web-app-capable', content='yes') 9 | title 登录 10 | body 11 | form(action='/login', method='post') 12 | input(type='text', name='name') 13 | input(type='password', name='password') 14 | input(type='submit') 15 | 16 | -------------------------------------------------------------------------------- /views/tug-of-war/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 拔河 - iro | colorful 10 | 11 | 12 | 13 | 14 |

载入中

15 | 66 | github 67 |
{{onlines}} 人在线
68 | 图库 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /views/tuku/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | meta(content='IE=edge', http-equiv='X-UA-Compatible') 6 | meta(name='renderer', content='webkit') 7 | meta(name='viewport', content='width=device-width,initial-scale=1,user-scalable=no') 8 | meta(name='apple-mobile-web-app-capable', content='yes') 9 | title= (title ? `${title} - ` : '') + 'iro | colorful' 10 | link(rel='icon', type='image/x-icon', href='http://kuro-iro.b0.upaiyun.com/images/favicon.ico') 11 | link(rel='stylesheet', href='/tuku/list.css') 12 | body 13 | header#header 14 | .container 15 | h1: a(href='/', title='iro | colorful') 16 | a#tug_link(href='/tug') 拔河 17 | nav#nav 18 | ul 19 | li: a(href='/random') 随便看看 20 | #image_list 21 | each image in images 22 | a.image-item( 23 | data-id=image.id, 24 | data-width=image.width, 25 | data-height=image.height, 26 | target='_blank', 27 | href=image.src 28 | ) 29 | img.image-item-img(src=image.src) 30 | #page_nav 31 | if isRandom 32 | a.button-page( 33 | href='', 34 | style='width: 100px;' 35 | ) 换一批 36 | else 37 | each pageButton in pageButtons 38 | a.button-page( 39 | href=`?page=${pageButton.value}`, 40 | class=pageButton.value === page ? 'current' : '' 41 | )= pageButton.text 42 | 43 | script. 44 | var user = !{JSON.stringify(user || {})} 45 | script(src='/tuku/list.js') 46 | --------------------------------------------------------------------------------