├── logs
└── .gitignore
├── README.md
├── public
├── uploads
│ ├── temps
│ │ └── .gitignore
│ └── .gitignore
├── tuku
│ ├── list.js
│ └── list.css
└── tug-of-war
│ ├── style.css
│ └── script.js
├── config.json
├── middlewares
└── errorhandler.js
├── models
├── user.js
├── image.js
└── tug-of-war.js
├── routers
├── libs.js
├── login.js
├── tug-of-war.io.js
├── tug-of-war.js
└── tuku.js
├── package.json
├── views
├── login.jade
├── tuku
│ └── index.jade
└── tug-of-war
│ └── index.html
├── log.js
├── .gitignore
└── app.js
/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | iro
2 | =======
3 | colorful
4 |
--------------------------------------------------------------------------------
/public/uploads/temps/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/uploads/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !temps
4 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "db": "mongodb://localhost/tug-of-war",
3 | "sessionSecret": "secret",
4 | "port": 1338
5 | }
6 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/views/tug-of-war/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 拔河 - iro | colorful
10 |
11 |
12 |
13 |
14 | 载入中
15 |
16 |
17 | {{teams.a.score}} : {{teams.b.score}}
18 | 上传图片时间
19 |
20 |
23 |
41 |
59 |
62 |
65 |
66 | github
67 | {{onlines}} 人在线
68 | 图库
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------