├── .bowerrc ├── .csscomb.json ├── .editorconfig ├── .gitignore ├── .npmignore ├── app ├── apis │ ├── default.coffee │ ├── point.coffee │ ├── section.coffee │ └── story.coffee ├── models │ ├── point.coffee │ ├── section.coffee │ ├── story.coffee │ └── user.coffee └── pages │ ├── account.coffee │ ├── default.coffee │ └── story.coffee ├── bin └── www ├── bower.json ├── coffeelint.json ├── config ├── default.yaml.example ├── development.yaml.example ├── production.yaml.example └── test.yaml ├── docs ├── Api 接口.md ├── 开发环境.md └── 约定模式.md ├── gulpfile.coffee ├── index.coffee ├── libs ├── config.coffee ├── express.coffee ├── mailer.coffee ├── mongoose.coffee ├── passport.coffee ├── router.coffee └── swig.coffee ├── package.json ├── public ├── crossdomain.xml ├── favicon.ico ├── humans.txt ├── images │ ├── cover-1024.jpg │ ├── cover-1600.jpg │ ├── cover-800.jpg │ └── iphone.png ├── robots.txt ├── scripts │ ├── components │ │ ├── chart.coffee │ │ ├── csrf.coffee │ │ ├── iconpicker.coffee │ │ └── upload-image.coffee │ ├── pages │ │ ├── account │ │ │ ├── signin.coffee │ │ │ └── signup.coffee │ │ ├── default │ │ │ └── show.coffee │ │ └── story │ │ │ ├── default.coffee │ │ │ └── share.coffee │ └── templates │ │ ├── components │ │ ├── iconpicker.hbs │ │ ├── logo.hbs │ │ ├── point-add.hbs │ │ ├── point-edit.hbs │ │ ├── point.hbs │ │ ├── profile.hbs │ │ ├── section-add.hbs │ │ ├── section-navigation.hbs │ │ ├── section-title.hbs │ │ └── section.hbs │ │ └── pages │ │ └── story │ │ ├── detail.hbs │ │ └── list.hbs └── styles │ ├── components │ ├── bootstrap.less │ ├── customized-variables.less │ ├── font-awesome.less │ └── section.less │ └── pages │ ├── account │ └── default.less │ ├── base.less │ ├── default │ └── default.less │ ├── error.less │ └── story │ ├── default.less │ └── share.less ├── readme.md ├── server.coffee ├── test ├── app │ └── apis │ │ ├── default.coffee │ │ ├── point.coffee │ │ ├── section.coffee │ │ └── story.coffee └── fixtures │ ├── points.json │ ├── sections.json │ ├── stories.json │ └── users.json ├── views ├── 404.html ├── 500.html ├── account │ ├── forgot.html │ ├── signin.html │ └── signup.html ├── default │ ├── default.html │ └── show.html ├── layout.html └── story │ ├── default.html │ └── share.html └── wercker.yml /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/components" 3 | } 4 | -------------------------------------------------------------------------------- /.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "always-semicolon": true, 3 | "block-indent": 2, 4 | "color-case": "lower", 5 | "color-shorthand": true, 6 | "element-case": "lower", 7 | "eof-newline": true, 8 | "leading-zero": false, 9 | "remove-empty-rulesets": true, 10 | "space-after-colon": 1, 11 | "space-after-combinator": 1, 12 | "space-before-selector-delimiter": 0, 13 | "space-between-declarations": "\n", 14 | "space-after-opening-brace": "\n", 15 | "space-before-closing-brace": "\n", 16 | "space-before-colon": 0, 17 | "space-before-combinator": 1, 18 | "space-before-opening-brace": 1, 19 | "strip-spaces": true, 20 | "unitless-zero": true, 21 | "vendor-prefix-align": true, 22 | "sort-order": [ 23 | [ 24 | "position", 25 | "top", 26 | "right", 27 | "bottom", 28 | "left", 29 | "z-index", 30 | "display", 31 | "float", 32 | "width", 33 | "min-width", 34 | "max-width", 35 | "height", 36 | "min-height", 37 | "max-height", 38 | "-webkit-box-sizing", 39 | "-moz-box-sizing", 40 | "box-sizing", 41 | "-webkit-appearance", 42 | "padding", 43 | "padding-top", 44 | "padding-right", 45 | "padding-bottom", 46 | "padding-left", 47 | "margin", 48 | "margin-top", 49 | "margin-right", 50 | "margin-bottom", 51 | "margin-left", 52 | "overflow", 53 | "overflow-x", 54 | "overflow-y", 55 | "-webkit-overflow-scrolling", 56 | "-ms-overflow-x", 57 | "-ms-overflow-y", 58 | "-ms-overflow-style", 59 | "clip", 60 | "clear", 61 | "font", 62 | "font-family", 63 | "font-size", 64 | "font-style", 65 | "font-weight", 66 | "font-variant", 67 | "font-size-adjust", 68 | "font-stretch", 69 | "font-effect", 70 | "font-emphasize", 71 | "font-emphasize-position", 72 | "font-emphasize-style", 73 | "font-smooth", 74 | "-webkit-hyphens", 75 | "-moz-hyphens", 76 | "hyphens", 77 | "line-height", 78 | "color", 79 | "text-align", 80 | "-webkit-text-align-last", 81 | "-moz-text-align-last", 82 | "-ms-text-align-last", 83 | "text-align-last", 84 | "text-emphasis", 85 | "text-emphasis-color", 86 | "text-emphasis-style", 87 | "text-emphasis-position", 88 | "text-decoration", 89 | "text-indent", 90 | "text-justify", 91 | "text-outline", 92 | "-ms-text-overflow", 93 | "text-overflow", 94 | "text-overflow-ellipsis", 95 | "text-overflow-mode", 96 | "text-shadow", 97 | "text-transform", 98 | "text-wrap", 99 | "-webkit-text-size-adjust", 100 | "-ms-text-size-adjust", 101 | "letter-spacing", 102 | "-ms-word-break", 103 | "word-break", 104 | "word-spacing", 105 | "-ms-word-wrap", 106 | "word-wrap", 107 | "-moz-tab-size", 108 | "-o-tab-size", 109 | "tab-size", 110 | "white-space", 111 | "vertical-align", 112 | "list-style", 113 | "list-style-position", 114 | "list-style-type", 115 | "list-style-image", 116 | "pointer-events", 117 | "-ms-touch-action", 118 | "touch-action", 119 | "cursor", 120 | "visibility", 121 | "zoom", 122 | "flex-direction", 123 | "flex-order", 124 | "flex-pack", 125 | "flex-align", 126 | "table-layout", 127 | "empty-cells", 128 | "caption-side", 129 | "border-spacing", 130 | "border-collapse", 131 | "content", 132 | "quotes", 133 | "counter-reset", 134 | "counter-increment", 135 | "resize", 136 | "-webkit-user-select", 137 | "-moz-user-select", 138 | "-ms-user-select", 139 | "-o-user-select", 140 | "user-select", 141 | "nav-index", 142 | "nav-up", 143 | "nav-right", 144 | "nav-down", 145 | "nav-left", 146 | "background", 147 | "background-color", 148 | "background-image", 149 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 150 | "filter:progid:DXImageTransform.Microsoft.gradient", 151 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 152 | "filter", 153 | "background-repeat", 154 | "background-attachment", 155 | "background-position", 156 | "background-position-x", 157 | "background-position-y", 158 | "-webkit-background-clip", 159 | "-moz-background-clip", 160 | "background-clip", 161 | "background-origin", 162 | "-webkit-background-size", 163 | "-moz-background-size", 164 | "-o-background-size", 165 | "background-size", 166 | "border", 167 | "border-color", 168 | "border-style", 169 | "border-width", 170 | "border-top", 171 | "border-top-color", 172 | "border-top-style", 173 | "border-top-width", 174 | "border-right", 175 | "border-right-color", 176 | "border-right-style", 177 | "border-right-width", 178 | "border-bottom", 179 | "border-bottom-color", 180 | "border-bottom-style", 181 | "border-bottom-width", 182 | "border-left", 183 | "border-left-color", 184 | "border-left-style", 185 | "border-left-width", 186 | "border-radius", 187 | "border-top-left-radius", 188 | "border-top-right-radius", 189 | "border-bottom-right-radius", 190 | "border-bottom-left-radius", 191 | "-webkit-border-image", 192 | "-moz-border-image", 193 | "-o-border-image", 194 | "border-image", 195 | "-webkit-border-image-source", 196 | "-moz-border-image-source", 197 | "-o-border-image-source", 198 | "border-image-source", 199 | "-webkit-border-image-slice", 200 | "-moz-border-image-slice", 201 | "-o-border-image-slice", 202 | "border-image-slice", 203 | "-webkit-border-image-width", 204 | "-moz-border-image-width", 205 | "-o-border-image-width", 206 | "border-image-width", 207 | "-webkit-border-image-outset", 208 | "-moz-border-image-outset", 209 | "-o-border-image-outset", 210 | "border-image-outset", 211 | "-webkit-border-image-repeat", 212 | "-moz-border-image-repeat", 213 | "-o-border-image-repeat", 214 | "border-image-repeat", 215 | "outline", 216 | "outline-width", 217 | "outline-style", 218 | "outline-color", 219 | "outline-offset", 220 | "-webkit-box-shadow", 221 | "-moz-box-shadow", 222 | "box-shadow", 223 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 224 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 225 | "opacity", 226 | "-ms-interpolation-mode", 227 | "-webkit-transition", 228 | "-moz-transition", 229 | "-ms-transition", 230 | "-o-transition", 231 | "transition", 232 | "-webkit-transition-delay", 233 | "-moz-transition-delay", 234 | "-ms-transition-delay", 235 | "-o-transition-delay", 236 | "transition-delay", 237 | "-webkit-transition-timing-function", 238 | "-moz-transition-timing-function", 239 | "-ms-transition-timing-function", 240 | "-o-transition-timing-function", 241 | "transition-timing-function", 242 | "-webkit-transition-duration", 243 | "-moz-transition-duration", 244 | "-ms-transition-duration", 245 | "-o-transition-duration", 246 | "transition-duration", 247 | "-webkit-transition-property", 248 | "-moz-transition-property", 249 | "-ms-transition-property", 250 | "-o-transition-property", 251 | "transition-property", 252 | "-webkit-transform", 253 | "-moz-transform", 254 | "-ms-transform", 255 | "-o-transform", 256 | "transform", 257 | "-webkit-transform-origin", 258 | "-moz-transform-origin", 259 | "-ms-transform-origin", 260 | "-o-transform-origin", 261 | "transform-origin", 262 | "-webkit-animation", 263 | "-moz-animation", 264 | "-ms-animation", 265 | "-o-animation", 266 | "animation", 267 | "-webkit-animation-name", 268 | "-moz-animation-name", 269 | "-ms-animation-name", 270 | "-o-animation-name", 271 | "animation-name", 272 | "-webkit-animation-duration", 273 | "-moz-animation-duration", 274 | "-ms-animation-duration", 275 | "-o-animation-duration", 276 | "animation-duration", 277 | "-webkit-animation-play-state", 278 | "-moz-animation-play-state", 279 | "-ms-animation-play-state", 280 | "-o-animation-play-state", 281 | "animation-play-state", 282 | "-webkit-animation-timing-function", 283 | "-moz-animation-timing-function", 284 | "-ms-animation-timing-function", 285 | "-o-animation-timing-function", 286 | "animation-timing-function", 287 | "-webkit-animation-delay", 288 | "-moz-animation-delay", 289 | "-ms-animation-delay", 290 | "-o-animation-delay", 291 | "animation-delay", 292 | "-webkit-animation-iteration-count", 293 | "-moz-animation-iteration-count", 294 | "-ms-animation-iteration-count", 295 | "-o-animation-iteration-count", 296 | "animation-iteration-count", 297 | "-webkit-animation-direction", 298 | "-moz-animation-direction", 299 | "-ms-animation-direction", 300 | "-o-animation-direction", 301 | "animation-direction" 302 | ] 303 | ] 304 | } 305 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node.js / npm 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | 16 | .tags 17 | npm-debug.log 18 | 19 | 20 | # misc / editors 21 | *~ 22 | *# 23 | .DS_STORE 24 | .netbeans 25 | nbproject 26 | .idea 27 | .nide 28 | 29 | # bower components 30 | public/components 31 | public/test/components 32 | 33 | 34 | # Test 35 | coverage.html 36 | 37 | # Grunt 38 | .tmp 39 | dist 40 | 41 | # Base 42 | coverage 43 | config/default.yaml 44 | config/development.yaml 45 | config/production.yaml 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # node.js / npm 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | 16 | .tags 17 | npm-debug.log 18 | 19 | 20 | # misc / editors 21 | *~ 22 | *# 23 | .DS_STORE 24 | .netbeans 25 | nbproject 26 | .idea 27 | .nide 28 | 29 | # bower components 30 | public/components 31 | public/test/components 32 | 33 | 34 | # Test 35 | coverage.html 36 | 37 | # Grunt 38 | .tmp 39 | dist 40 | 41 | # Base 42 | coverage 43 | config/development.yaml 44 | config/production.yaml 45 | -------------------------------------------------------------------------------- /app/apis/default.coffee: -------------------------------------------------------------------------------- 1 | # 默认 Api 2 | crypto = require 'crypto' 3 | async = require 'async' 4 | passport = require 'passport' 5 | router = require('express').Router() 6 | 7 | User = require '../models/user' 8 | 9 | # 又拍云生成 signature 和 policy 10 | router.route('/upyun_token').post (req, res) -> 11 | bucket = (b for b in adou.config.upyun.buckets when b.name is req.body.bucket.trim())[0] 12 | policy = new Buffer(JSON.stringify(req.body)).toString 'base64' 13 | signature = crypto.createHash('md5').update(policy + '&' + bucket.secret).digest 'hex' 14 | res.status(200).json policy: policy, signature: signature 15 | 16 | # 注册 17 | router.route('/signup').post (req, res, done) -> 18 | req.assert('name', '姓名不能为空').notEmpty() 19 | req.assert('email', '邮箱地址不能为空').notEmpty() 20 | req.assert('email', '邮箱地址格式不正确').isEmail() 21 | req.assert('password', '密码不能为空').notEmpty() 22 | 23 | errs = req.validationErrors() 24 | return done isValidation: true, errors: errs if errs 25 | 26 | done() 27 | .post (req, res, done) -> 28 | email = req.body.email.trim() 29 | 30 | User.count { email: email }, (err, count) -> 31 | if 0 < count 32 | return done isValidation: true, errors: [ 33 | param: 'email' 34 | msg: '该邮箱地址已经被使用' 35 | value: email 36 | ] 37 | done() 38 | .post (req, res, done) -> 39 | req.sanitize('name').escape() 40 | 41 | user = new User 42 | name: req.body.name.trim() 43 | email: req.body.email.trim() 44 | password: req.body.password 45 | 46 | user.save (err, user) -> 47 | return done err if err 48 | 49 | # TODO: 发送邮箱地址验证邮件 50 | 51 | # 并且登录 52 | req.logIn id: user.id, (err) -> 53 | return done err if err 54 | res.status(201).json id: user.id 55 | 56 | # 找回密码 57 | router.route('/forgot').post (req, res, done) -> 58 | req.assert('email', '邮箱地址不能为空').notEmpty() 59 | req.assert('email', '邮箱地址格式不正确').isEmail() 60 | 61 | errs = req.validationErrors() 62 | return done isValidation: true, errors: errs if errs 63 | 64 | async.waterfall [ 65 | (fn) -> 66 | User.findOne { email: req.body.email.trim() }, 'name email salt', fn 67 | (user, fn) -> 68 | if not user 69 | return fn 70 | isValidation: true 71 | errors: [ 72 | param: 'email' 73 | msg: '该邮箱地址不存在' 74 | value: email 75 | ] 76 | , null 77 | 78 | user.password = crypto.randomBytes(Math.ceil(6)).toString('hex').slice 0, 12 79 | user.save (err, user) -> fn err, user 80 | ], (err, user) -> 81 | return done err if err 82 | 83 | # TODO: 发送找回密码邮件 84 | res.status(202).json { id: user.id, name: user.name, email: user.name } 85 | 86 | # 登录和退出账号 87 | router.route('/signin').post (req, res, done) -> 88 | _logIn = (err, user) -> 89 | return done err if err 90 | return res.status(422).json error: '电子邮件地址或密码错误' if not user 91 | req.logIn user, (err) -> 92 | return done err if err 93 | res.status(201).json success: '登录成功' 94 | passport.authenticate('local', _logIn) req, res, done 95 | .delete (req, res) -> 96 | req.logout() 97 | res.status(202).json id: req.user.id 98 | 99 | # 获取个人信息 100 | router.route('/me').get (req, res) -> 101 | if not req.user 102 | return res.status(401).json 103 | error: '未登录,没有此权限' 104 | res.json req.user 105 | 106 | module.exports = router 107 | -------------------------------------------------------------------------------- /app/apis/point.coffee: -------------------------------------------------------------------------------- 1 | # 片段 Api 2 | async = require 'async' 3 | validator = require 'validator' 4 | router = require('express').Router() 5 | 6 | Point = require '../models/point' 7 | 8 | # 权限过滤 9 | router.route('/*').get (req, res, done) -> 10 | if not req.user 11 | return res.status(401).json 12 | error: '未登录,没有此权限' 13 | done() 14 | 15 | # 更新节点 16 | router.route(/^\/([0-9a-fA-F]{24})$/).post (req, res, done) -> 17 | { bubble, link, image } = req.body 18 | 19 | req.assert('title', '标题不能为空').notEmpty() 20 | req.assert('link', '链接格式不正确').isURL() if link && 0 isnt link.indexOf 'mailto:' 21 | req.assert('image', '图片地址格式不正确').isURL() if image 22 | 23 | errs = req.validationErrors() 24 | return done isValidation: true, errors: errs if errs 25 | 26 | async.waterfall [ 27 | (fn) -> 28 | Point.findById req.params[0], 'title description bubble link image', fn 29 | (point, fn) -> 30 | point.title = req.sanitize('title').escape() 31 | point.description = req.sanitize('description').escape() 32 | point.bubble = bubble 33 | point.link = link 34 | point.image = image 35 | point.save (err) -> fn err, point 36 | ], (err, point) -> 37 | return done err if err 38 | res.status(202).json point 39 | 40 | module.exports = router 41 | -------------------------------------------------------------------------------- /app/apis/section.coffee: -------------------------------------------------------------------------------- 1 | # 片段 Api 2 | async = require 'async' 3 | validator = require 'validator' 4 | router = require('express').Router() 5 | 6 | Section = require '../models/section' 7 | Point = require '../models/point' 8 | 9 | # 权限过滤 10 | router.route('/*').get (req, res, done) -> 11 | if not req.user 12 | return res.status(401).json 13 | error: '未登录,没有此权限' 14 | done() 15 | 16 | # 更新片段 17 | router.route(/^\/([0-9a-fA-F]{24})$/).patch (req, res, done) -> 18 | { title, points } = req.body 19 | 20 | req.assert('title', '标题不能为空').notEmpty() if title 21 | 22 | errs = req.validationErrors() 23 | return done isValidation: true, errors: errs if errs 24 | 25 | async.waterfall [ 26 | (fn) -> 27 | Section.findById req.params[0], 'title', fn 28 | (section, fn) -> 29 | section.title = req.sanitize('title').escape() if title 30 | section.points = points if points 31 | section.save (err) -> fn err, section 32 | ], (err, section) -> 33 | return done err if err 34 | res.status(202).json section 35 | 36 | # 新建故事节点 37 | router.route(/^\/([0-9a-fA-F]{24})\/points$/).post (req, res, done) -> 38 | { bubble, link, image } = req.body 39 | 40 | req.assert('title', '标题不能为空').notEmpty() 41 | req.assert('link', '链接格式不正确').isURL() if link && 0 isnt link.indexOf 'mailto:' 42 | req.assert('image', '图片地址格式不正确').isURL() if image 43 | 44 | errs = req.validationErrors() 45 | return done isValidation: true, errors: errs if errs 46 | 47 | async.waterfall [ 48 | (fn) -> 49 | point = new Point 50 | title: req.sanitize('title').escape() 51 | description: req.sanitize('description').escape() 52 | 53 | point.bubble = req.sanitize('bubble').escape() if bubble 54 | point.link = link if link 55 | point.image = image if image 56 | point.save (err, point) -> fn err, point 57 | (point, fn) -> 58 | Section.findById req.params[0], 'points', (err, section) -> fn err, section, point 59 | (section, point, fn) -> 60 | section.points.push point.id 61 | section.save (err) -> fn err, point 62 | ], (err, point) -> 63 | return done err if err 64 | res.status(201).json point 65 | 66 | # 删除故事节点 67 | router.route(/^\/([0-9a-fA-F]{24})\/points\/([0-9a-fA-F]{24})$/).delete (req, res, done) -> 68 | async.waterfall [ 69 | (fn) -> 70 | Section.findById req.params[0], 'points', fn 71 | (section, fn) -> 72 | id = req.params[1] 73 | index = section.points.indexOf id 74 | section.points.splice index, 1 75 | section.save (err) -> fn err, id: id 76 | ], (err, point) -> 77 | return done err if err 78 | res.status(202).json id: point.id 79 | 80 | 81 | module.exports = router 82 | -------------------------------------------------------------------------------- /app/apis/story.coffee: -------------------------------------------------------------------------------- 1 | # 故事 Api 2 | async = require 'async' 3 | validator = require 'validator' 4 | router = require('express').Router() 5 | 6 | Story = require '../models/story' 7 | Section = require '../models/section' 8 | Point = require '../models/point' 9 | 10 | # 权限过滤 11 | router.route('/*').get (req, res, done) -> 12 | if not req.user 13 | return res.status(401).json 14 | error: '未登录,没有此权限' 15 | done() 16 | 17 | # 新建 18 | router.route('/').post (req, res, done) -> 19 | async.waterfall [ 20 | (fn) -> 21 | section = new Section name: '' 22 | section.save (err, section) -> fn err, section 23 | (section, fn) -> 24 | story = new Story 25 | author: req.user.id 26 | sections: [ section.id ] 27 | 28 | story.save (err, story) -> fn err, story 29 | ], (err, story) -> 30 | return done err if err 31 | res.status(201).json id: story.id 32 | 33 | # 删除故事 34 | router.route(/^\/([0-9a-fA-F]{24})$/).delete (req, res, done) -> 35 | async.waterfall [ 36 | (fn) -> 37 | Story.findById req.params[0], 'author title', fn 38 | (story, fn) -> 39 | fn null, null if not story.author.equals req.user.id 40 | story.remove (err) -> fn err, story 41 | ], (err, story) -> 42 | return done err if err 43 | return done() if not story 44 | res.status(202).json id: story.id 45 | 46 | # 更新故事 47 | router.route(/^\/([0-9a-fA-F]{24})$/).patch (req, res, done) -> 48 | { background, cover, theme, sections } = req.body 49 | 50 | req.assert('background', '背景地址格式不正确').isURL() if background 51 | req.assert('cover', '封面地址格式不正确').isURL() if cover 52 | req.assert('theme', '主题格式不正确').isAlpha() if theme 53 | 54 | errs = req.validationErrors() 55 | return done isValidation: true, errors: errs if errs 56 | 57 | async.waterfall [ 58 | (fn) -> 59 | Story.findById req.params[0], 'author background cover theme sections', fn 60 | (story, fn) -> 61 | fn null, null if not story.author.equals req.user.id 62 | story.background = background if background 63 | story.cover = cover if cover 64 | story.theme = theme if theme 65 | story.sections = sections if sections 66 | story.save (err) -> fn err, story 67 | ], (err, story) -> 68 | return done err if err 69 | return done() if not story 70 | res.status(202).json story 71 | 72 | # 更新故事简介 73 | router.route(/^\/([0-9a-fA-F]{24})$/).post (req, res, done) -> 74 | req.assert('title', '标题不能为空').notEmpty() 75 | req.assert('mark', '标识不能为空').notEmpty() 76 | req.assert('mark', '标识格式不正确').matches /^[a-zA-Z0-9\-\.]+$/ 77 | 78 | errs = req.validationErrors() 79 | return done isValidation: true, errors: errs if errs 80 | done() 81 | .post (req, res, done) -> 82 | mark = req.body.mark.trim() 83 | Story.count { mark: mark, _id: $ne: req.params[0] }, (err, count) -> 84 | if 0 < count 85 | return done isValidation: true, errors: [ 86 | param: 'mark' 87 | msg: '该标识已经被使用' 88 | value: mark 89 | ] 90 | done() 91 | .post (req, res, done) -> 92 | async.waterfall [ 93 | (fn) -> 94 | Story.findById req.params[0], 'author title description mark', fn 95 | (story, fn) -> 96 | fn null, null if not story.author.equals req.user.id 97 | story.title = req.sanitize('title').escape() 98 | story.description = req.sanitize('description').escape() 99 | story.mark = req.body.mark.trim().toLowerCase() 100 | story.save (err) -> fn err, story 101 | ], (err, story) -> 102 | return done err if err 103 | return done() if not story 104 | res.status(202).json story 105 | 106 | # 新建故事片段 107 | router.route(/^\/([0-9a-fA-F]{24})\/sections$/).post (req, res, done) -> 108 | req.assert('title', '标题不能为空').notEmpty() 109 | 110 | errs = req.validationErrors() 111 | return done isValidation: true, errors: errs if errs 112 | 113 | async.waterfall [ 114 | (fn) -> 115 | section = new Section title: req.sanitize('title').escape() 116 | section.save (err, section) -> fn err, section 117 | (section, fn) -> 118 | Story.findById req.params[0], 'author sections', (err, story) -> fn err, story, section 119 | (story, section, fn) -> 120 | fn null, null if not story.author.equals req.user.id 121 | beforeIndex = if req.body.before then 1 + story.sections.indexOf req.body.before else 0 122 | story.sections.splice beforeIndex, 0, section.id 123 | story.save (err) -> fn err, section 124 | ], (err, section) -> 125 | return done err if err 126 | return done() if not section 127 | res.status(201).json section 128 | 129 | # 删除故事片段 130 | router.route(/^\/([0-9a-fA-F]{24})\/sections\/([0-9a-fA-F]{24})$/).delete (req, res, done) -> 131 | async.waterfall [ 132 | (fn) -> 133 | Story.findById req.params[0], 'author sections', fn 134 | (story, fn) -> 135 | fn null, null if not story.author.equals req.user.id 136 | id = req.params[1] 137 | index = story.sections.indexOf id 138 | story.sections.splice index, 1 139 | story.save (err) -> fn err, id: id 140 | ], (err, section) -> 141 | return done err if err 142 | return done() if not section 143 | res.status(202).json id: section.id 144 | 145 | # 列表 146 | router.route('/').get (req, res) -> 147 | Story.find { author: req.user.id }, 'title cover', { sort: _id: -1 }, (err, stories) -> 148 | return done err if err 149 | res.status(200).json stories 150 | 151 | # 详情 152 | router.route(/^\/([0-9a-fA-F]{24})$/).get (req, res) -> 153 | async.waterfall [ 154 | (fn) -> 155 | Story.findById req.params[0], 'title description mark background cover theme sections' 156 | .populate 'sections' 157 | .exec (err, story) -> fn err, story 158 | (story, fn) -> 159 | return fn null, null if not story 160 | Point.populate story.sections, { path: 'points' }, (err, points) -> 161 | story.sections.points = points 162 | fn err, story 163 | ], (err, story) -> 164 | return done err if err 165 | res.status(200).json story 166 | 167 | module.exports = router 168 | -------------------------------------------------------------------------------- /app/models/point.coffee: -------------------------------------------------------------------------------- 1 | # 节点 Models 2 | mongoose = require 'mongoose' 3 | ObjectId = mongoose.Schema.ObjectId 4 | 5 | # 结构 6 | PointSchema = new mongoose.Schema 7 | title: { type: String, default: '' } # 标题 8 | description: { type: String, default: '' } # 描述 9 | bubble: { type: String } # 气泡 10 | link: { type: String } # 链接 11 | image: { type: String } # 图片 12 | 13 | # 集合名称 14 | PointSchema.set 'collection', 'points' 15 | 16 | # 序列化结果 17 | PointSchema.set 'toJSON', 18 | virtuals: true 19 | transform: (doc, ret) -> 20 | delete ret._id 21 | delete ret.__v 22 | return ret 23 | 24 | module.exports = mongoose.model 'Point', PointSchema 25 | -------------------------------------------------------------------------------- /app/models/section.coffee: -------------------------------------------------------------------------------- 1 | # 片段 Models 2 | mongoose = require 'mongoose' 3 | ObjectId = mongoose.Schema.ObjectId 4 | 5 | # 结构 6 | SectionSchema = new mongoose.Schema 7 | title: { type: String, default: '' } # 标题 8 | points: [{ type: ObjectId, ref: 'Point' }] # 节点 9 | 10 | # 集合名称 11 | SectionSchema.set 'collection', 'sections' 12 | 13 | # 序列化结果 14 | SectionSchema.set 'toJSON', 15 | virtuals: true 16 | transform: (doc, ret) -> 17 | delete ret._id 18 | delete ret.__v 19 | return ret 20 | 21 | module.exports = mongoose.model 'Section', SectionSchema 22 | -------------------------------------------------------------------------------- /app/models/story.coffee: -------------------------------------------------------------------------------- 1 | # 故事 Models 2 | mongoose = require 'mongoose' 3 | ObjectId = mongoose.Schema.ObjectId 4 | 5 | # 结构 6 | StorySchema = new mongoose.Schema 7 | author: { type: ObjectId, ref: 'User'} # 作者 8 | title: { type: String, default: '' } # 标题 9 | description: { type: String, default: '' } # 描述 10 | mark: { type: String, index: true } # 标识 11 | background: { type: String } # 背景 12 | cover: { type: String } # 封面 13 | theme: { type: String, enum: [ 'black', 'gray', 'green', 'blue', 'pink', 'red' ], default: 'black' } # 主题 14 | sections: [{ type: ObjectId, ref: 'Section' }] # 片段 15 | 16 | # 集合名称 17 | StorySchema.set 'collection', 'stories' 18 | 19 | # 序列化结果 20 | StorySchema.set 'toJSON', 21 | virtuals: true 22 | transform: (doc, ret) -> 23 | delete ret._id 24 | delete ret.__v 25 | return ret 26 | 27 | module.exports = mongoose.model 'Story', StorySchema 28 | -------------------------------------------------------------------------------- /app/models/user.coffee: -------------------------------------------------------------------------------- 1 | # 用户 Models 2 | crypto = require 'crypto' 3 | mongoose = require 'mongoose' 4 | 5 | # 结构 6 | UserSchema = new mongoose.Schema 7 | name: { type: String, required: true } # 名称 8 | email: { type: String, required: true, index: true } # 电子邮件地址 9 | salt: { type: String, default: '' } # 盐 10 | hashed_password: { type: String } # 哈希密码 11 | active: { type: Boolean, default: false } # 是否已激活 12 | verify_code: { type: String } # 验证 code 13 | 14 | # 集合名称 15 | UserSchema.set 'collection', 'users' 16 | 17 | # 虚拟变量 18 | UserSchema.virtual('password').set (password) -> 19 | @_password = password 20 | @salt = @makeSalt() 21 | @hashed_password = @encryptPassword password 22 | .get -> 23 | return @_password 24 | 25 | # 方法 26 | UserSchema.methods = 27 | # 验证密码 28 | authenticate: (plainText) -> 29 | return @hashed_password is @encryptPassword plainText 30 | 31 | # 生成盐 32 | makeSalt: -> 33 | return '' + Math.round new Date().valueOf() * Math.random() 34 | 35 | # 密码加密 36 | encryptPassword: (password) -> 37 | return '' if not password 38 | 39 | try 40 | return crypto.createHmac('sha1', @salt).update(password).digest 'hex' 41 | catch err 42 | return '' 43 | 44 | # 序列化结果 45 | UserSchema.set 'toJSON', 46 | virtuals: true 47 | transform: (doc, ret) -> 48 | delete ret._id 49 | delete ret.__v 50 | return ret 51 | 52 | module.exports = mongoose.model 'User', UserSchema 53 | -------------------------------------------------------------------------------- /app/pages/account.coffee: -------------------------------------------------------------------------------- 1 | # 帐户 Page 2 | router = require('express').Router() 3 | 4 | # 登录 5 | router.route('/signin').get (req, res) -> 6 | return res.redirect '/' if req.user 7 | res.render 'account/signin' 8 | 9 | # 注册 10 | router.route('/signup').get (req, res) -> 11 | return res.redirect '/' if req.user 12 | res.render 'account/signup' 13 | 14 | # 忘记密码 15 | router.route('/forgot').get (req, res) -> 16 | return res.redirect '/' if req.user 17 | res.render 'account/forgot' 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /app/pages/default.coffee: -------------------------------------------------------------------------------- 1 | # 默认 Page 2 | async = require 'async' 3 | router = require('express').Router() 4 | 5 | Story = require '../models/story' 6 | Point = require '../models/point' 7 | 8 | # 首页 9 | router.route('/').get (req, res) -> 10 | return res.redirect '/stories' if req.user 11 | res.render 'default/default' 12 | 13 | # 展示 14 | router.route(/^\/\@(.+)$/).get (req, res, done) -> 15 | param = req.params[0] 16 | query = if /^[0-9a-fA-F]{24}$/.test param then {'$or': [{_id: param}, {mark: param}]} else mark: param 17 | async.waterfall [ 18 | (fn) -> 19 | Story.findOne query, 'title description mark background cover theme sections' 20 | .populate 'sections' 21 | .exec (err, story) -> fn err, story 22 | (story, fn) -> 23 | return fn null, null if not story 24 | Point.populate story.sections, {path: 'points'}, (err, points) -> 25 | story.sections.points = points 26 | fn err, story 27 | ], (err, story) -> 28 | return done err if err 29 | return done() if not story 30 | res.render 'default/show', {story: story} 31 | 32 | module.exports = router 33 | -------------------------------------------------------------------------------- /app/pages/story.coffee: -------------------------------------------------------------------------------- 1 | # 故事 Page 2 | async = require 'async' 3 | router = require('express').Router() 4 | 5 | Story = require '../models/story' 6 | Point = require '../models/point' 7 | 8 | # 权限过滤 9 | router.route('*').get (req, res, done) -> 10 | return res.redirect '/signin' if not req.user 11 | done() 12 | 13 | # 列表 14 | router.route('/').get (req, res, done) -> 15 | preloaded = {} 16 | Story.find {author: req.user.id}, 'title cover', {sort: _id: -1}, (err, stories) -> 17 | return done err if err 18 | preloaded.stories = stories 19 | res.render 'story/default', {preloaded: JSON.stringify preloaded} 20 | 21 | # 详情 22 | router.route(/^\/([0-9a-fA-F]{24})$/).get (req, res, done) -> 23 | preloaded = {} 24 | async.waterfall [ 25 | (fn) -> 26 | Story.findById req.params[0], 'author title description mark background cover theme sections' 27 | .populate 'sections' 28 | .exec (err, story) -> fn err, story 29 | (story, fn) -> 30 | return fn null, null if not story 31 | Point.populate story.sections, {path: 'points'}, (err, points) -> 32 | story.sections.points = points 33 | fn err, story 34 | ], (err, story) -> 35 | return done err if err 36 | return done() if not story || not story.author.equals req.user.id 37 | preloaded.story = story 38 | res.render 'story/default', {bige: true, preloaded: JSON.stringify preloaded} 39 | 40 | # 详情 41 | router.route(/^\/([0-9a-fA-F]{24})\/share$/).get (req, res, done) -> 42 | Story.findById req.params[0], 'mark', (err, story) -> 43 | return done err if err 44 | res.render 'story/share', {story: story} 45 | 46 | module.exports = router 47 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('coffee-script/register'); 3 | module.exports = require('../server.coffee'); 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Starry", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "font-awesome": "4.2.0", 6 | "modernizr": "2.8.3", 7 | "fastclick": "1.0.3", 8 | "markdown": "0.5.0", 9 | "handlebars": "1.3.0", 10 | "director": "1.2.2", 11 | "flow.js": "2.6.2", 12 | "jquery": "2.1.1", 13 | "toastr": "2.1.0", 14 | "jquery.cookie": "1.4.1", 15 | "jquery.easy-pie-chart": "2.1.4", 16 | "html.sortable": "0.1.8", 17 | "bootstrap": "3.3.1", 18 | "jquery-qrcode": "*", 19 | "zeroclipboard": "2.1.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "coffeescript_error": { 3 | "level": "error" 4 | }, 5 | "arrow_spacing": { 6 | "name": "arrow_spacing", 7 | "level": "error" 8 | }, 9 | "no_tabs": { 10 | "name": "no_tabs", 11 | "level": "error" 12 | }, 13 | "no_trailing_whitespace": { 14 | "name": "no_trailing_whitespace", 15 | "level": "error", 16 | "allowed_in_comments": false, 17 | "allowed_in_empty_lines": true 18 | }, 19 | "max_line_length": { 20 | "name": "max_line_length", 21 | "value": 200, 22 | "level": "error", 23 | "limitComments": true 24 | }, 25 | "line_endings": { 26 | "name": "line_endings", 27 | "level": "ignore", 28 | "value": "unix" 29 | }, 30 | "no_trailing_semicolons": { 31 | "name": "no_trailing_semicolons", 32 | "level": "error" 33 | }, 34 | "indentation": { 35 | "name": "indentation", 36 | "value": 2, 37 | "level": "error" 38 | }, 39 | "camel_case_classes": { 40 | "name": "camel_case_classes", 41 | "level": "error" 42 | }, 43 | "colon_assignment_spacing": { 44 | "name": "colon_assignment_spacing", 45 | "level": "ignore", 46 | "spacing": { 47 | "left": 0, 48 | "right": 0 49 | } 50 | }, 51 | "no_implicit_braces": { 52 | "name": "no_implicit_braces", 53 | "level": "ignore", 54 | "strict": true 55 | }, 56 | "no_plusplus": { 57 | "name": "no_plusplus", 58 | "level": "ignore" 59 | }, 60 | "no_throwing_strings": { 61 | "name": "no_throwing_strings", 62 | "level": "error" 63 | }, 64 | "no_backticks": { 65 | "name": "no_backticks", 66 | "level": "error" 67 | }, 68 | "no_implicit_parens": { 69 | "name": "no_implicit_parens", 70 | "strict": true, 71 | "level": "ignore" 72 | }, 73 | "no_empty_param_list": { 74 | "name": "no_empty_param_list", 75 | "level": "ignore" 76 | }, 77 | "no_stand_alone_at": { 78 | "name": "no_stand_alone_at", 79 | "level": "ignore" 80 | }, 81 | "space_operators": { 82 | "name": "space_operators", 83 | "level": "ignore" 84 | }, 85 | "duplicate_key": { 86 | "name": "duplicate_key", 87 | "level": "error" 88 | }, 89 | "empty_constructor_needs_parens": { 90 | "name": "empty_constructor_needs_parens", 91 | "level": "ignore" 92 | }, 93 | "cyclomatic_complexity": { 94 | "name": "cyclomatic_complexity", 95 | "value": 10, 96 | "level": "ignore" 97 | }, 98 | "newlines_after_classes": { 99 | "name": "newlines_after_classes", 100 | "value": 3, 101 | "level": "ignore" 102 | }, 103 | "no_unnecessary_fat_arrows": { 104 | "name": "no_unnecessary_fat_arrows", 105 | "level": "warn" 106 | }, 107 | "missing_fat_arrows": { 108 | "name": "missing_fat_arrows", 109 | "level": "ignore" 110 | }, 111 | "non_empty_constructor_needs_parens": { 112 | "name": "non_empty_constructor_needs_parens", 113 | "level": "ignore" 114 | }, 115 | "no_unnecessary_double_quotes": { 116 | "name": "no_unnecessary_double_quotes", 117 | "level": "ignore" 118 | }, 119 | "no_debugger": { 120 | "name": "no_debugger", 121 | "level": "warn" 122 | }, 123 | "no_interpolation_in_single_quotes": { 124 | "name": "no_interpolation_in_single_quotes", 125 | "level": "ignore" 126 | }, 127 | "no_empty_functions": { 128 | "name": "no_empty_functions", 129 | "level": "ignore" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /config/default.yaml.example: -------------------------------------------------------------------------------- 1 | # 系统信息 2 | setting: 3 | title: 'Starry' 4 | description: '完成一个故事' 5 | api_vnd: 'starry.v0.1' 6 | secret: 'gwb2vp6tk1inpspztkexdj2egxmegu' 7 | static_url: '' 8 | 9 | # 数据库配置 10 | db: 11 | redis: 12 | host: '127.0.0.1' 13 | mongodb: 14 | url: 'mongodb://127.0.0.1/starry' 15 | 16 | # 邮件配置 17 | mailer: 18 | smtp: 19 | host: 'smtp.exmail.qq.com' 20 | secure: true 21 | port: 465 22 | auth: 23 | user: 'Xxxxx@xxx.com' 24 | pass: 'Xxxxxx' 25 | from: 'Xxxx ' 26 | 27 | # 又拍云 28 | upyun: 29 | api: 'http://v0.api.upyun.com' 30 | buckets: 31 | - name: 'starry-images' 32 | secret: 'Xxxxxxxxx' 33 | url: 'http://starry-images.b0.upaiyun.com' 34 | -------------------------------------------------------------------------------- /config/development.yaml.example: -------------------------------------------------------------------------------- 1 | # 数据库配置 2 | db: 3 | mongodb: 4 | url: 'mongodb://127.0.0.1/starry-dev' 5 | -------------------------------------------------------------------------------- /config/production.yaml.example: -------------------------------------------------------------------------------- 1 | # 邮件配置 2 | mailer: 3 | smtp: 4 | host: 'smtp.exmail.qq.com' 5 | secure: true 6 | port: 465 7 | auth: 8 | user: 'xxxxx@xxx.com' 9 | pass: 'xxxxxx' 10 | from: 'xxxx ' 11 | -------------------------------------------------------------------------------- /config/test.yaml: -------------------------------------------------------------------------------- 1 | # 数据库配置 2 | db: 3 | mongodb: 4 | url: 'mongodb://127.0.0.1/starry-test' 5 | -------------------------------------------------------------------------------- /docs/Api 接口.md: -------------------------------------------------------------------------------- 1 | # Api 接口 2 | 3 | ### 默认 4 | - Post:/api/upyun_token -- 获取又拍云 Token 信息 5 | - Post:/api/signup -- 注册 6 | - Post:/api/forgot -- 找回密码 7 | - Post:/api/signin -- 登录 8 | - Delete:/api/signin -- 退出 9 | - Get:/api/me -- 获取个人信息 10 | 11 | 12 | ### 故事 13 | - Post:/api/stories -- 新建故事 14 | - Delete:/api/stories/:id -- 删除故事 15 | - Patch:/api/stories/:id -- 更新故事 16 | - Post:/api/stories/:id -- 更新故事简介 17 | - Post:/api/stories/:id/sections -- 新建故事片段 18 | - Delete:/api/stories/:id/sections/:id -- 删除故事片段 19 | - Get:/api/stories -- 获取故事列表 20 | - Get:/api/stories/:id -- 获取故事详情 21 | 22 | 23 | ### 片段 24 | - Patch:/api/sections/:id -- 更新片段 25 | - Post:/api/sections/:id/points -- 新建故事节点 26 | - Delete:/api/sections/:id/points/:id -- 删除故事节点 27 | 28 | 29 | ### 节点 30 | - Post:/api/points/:id -- 更新节点 -------------------------------------------------------------------------------- /docs/开发环境.md: -------------------------------------------------------------------------------- 1 | # 开发环境 2 | 3 | #### 后端 4 | 5 | `Node(>= 0.10) + MongoDB(>= 2.6.5) + Redis(>= 2.8)` [Redis 在这里只是用于存储 Session] 6 | 7 | ### 前端 8 | 9 | `Html + Css + Javascript` 10 | 11 | Javascript 统一使用 Coffeescript 来编写,Css 使用 Scss 编写,后端使用 Npm 做包的依赖管理,前端使用 Bower 做包的依赖管理,版本控制使用 Git 12 | 13 | ### 本地开发配置 14 | 15 | 服务器的所有配置都写在 `config/default.yaml` 中,可以重命名 `config/development.yaml.example` 改为 `config/development.yaml`,并将修改配置以适合自己本地开发。 16 | -------------------------------------------------------------------------------- /docs/约定模式.md: -------------------------------------------------------------------------------- 1 | # 约定模式 2 | 3 | #### 代码规范 4 | 5 | * 用两个空格来代替制表符(tab) — 这是唯一能保证在所有环境下获得一致展现的方法。 6 | * 嵌套元素应当缩进一次(即两个空格)。 7 | * 对于属性的定义,`html` 和 `css` 全部使用双引号,绝不要使用单引号, `coffee` 使用单引号,字符串例外 8 | * `html` 中的 `class` 属性变量统一使用 '-' 分割,其他普通变量统一使用驼峰形式,常量使用全部大写, 多个单词以下划线分隔。尽量避免潜在冲突; 精简短小, 见名知意。 9 | * `html` 和 `css` 除去变量,方法外其他全部小写。 10 | 11 | 这里只列出一些最基本的,其他的请参考下面文章: 12 | 13 | 前端开发规范:[http://sadne.me/2014/05/16/coding_standards/](http://sadne.me/2014/05/16/coding_standards/) 14 | 15 | RESTful API 设计:[http://sadne.me/2014/04/05/restful_api_design/](http://sadne.me/2014/04/05/restful_api_design/) 16 | 17 | 注: 这里的 RESTful 仅供参考,具体希望再讨论 18 | 19 | #### 模块分离 20 | 21 | ##### 目录结构 22 | 23 | ``` 24 | . 25 | ├── gulpfile.coffee 26 | ├── README.md 27 | ├── starry (项目 目录) 28 | │   ├── apis (API 控制器) 29 | │   ├── events (Events 事件) 30 | │   ├── models (Model 数据层) 31 | │   └── pages (Pages 页面) 32 | ├── app.coffee 33 | ├── bower.json 34 | ├── coffeelint.json 35 | ├── config (配置) 36 | ├── docs (文档) 37 | ├── libs (公共库) 38 | ├── node_modules 39 | ├── package.json 40 | ├── public (前端文件) 41 | │   ├── components (前端组件目录) 42 | │   ├── crossdomain.xml 43 | │   ├── scripts 44 | │   └── styles 45 | ├── test (测试) 46 | ├── views (模板) 47 | └── server.coffee 48 | ``` 49 | 50 | ##### 全局变量 51 | 52 | `global.adou` 为一个全局变量其中: 53 | 54 | * `adou.mail(subject, template, data)` 获取邮件对象 `subject` 标题, `template` 模板名称, `data` 模板参数 55 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | 3 | # 加载插件 4 | plugins = require('gulp-load-plugins')() 5 | process.env.NODE_ENV = 'development' if not process.env.NODE_ENV 6 | 7 | # error hander 8 | handler = (err) -> 9 | plugins.util.beep() 10 | plugins.util.log plugins.util.colors.red(err.name), err.message 11 | 12 | # Env development 13 | gulp.task 'development-env', (callback) -> 14 | process.env.NODE_ENV = 'development' 15 | callback() 16 | 17 | # Env test 18 | gulp.task 'test-env', (callback) -> 19 | process.env.NODE_ENV = 'test' 20 | callback() 21 | 22 | # Env production 23 | gulp.task 'production-env', (callback) -> 24 | process.env.NODE_ENV = 'production' 25 | callback() 26 | 27 | # Clean 28 | gulp.task 'clean', (callback) -> 29 | del = require 'del' 30 | del ['.tmp', 'dist'], callback 31 | 32 | # Create Setting 33 | gulp.task 'create-setting', (callback) -> 34 | buckets = {} 35 | config = require './libs/config' 36 | 37 | {setting, upyun} = config 38 | 39 | for bucket in upyun.buckets 40 | buckets[bucket.name] = bucket.url 41 | 42 | adou = JSON.stringify 43 | title: setting.title 44 | description: setting.description 45 | upyun: 46 | api: upyun.api 47 | buckets: buckets 48 | 49 | src = require('stream').Readable objectMode: true 50 | src._read = -> 51 | @push new plugins.util.File cwd: '', base: '', path: 'setting.js', contents: new Buffer "var adou = #{adou};" 52 | @push null 53 | return src.pipe gulp.dest '.tmp/public/scripts' 54 | 55 | # Lint 56 | gulp.task 'lint', -> 57 | return gulp.src ['libs/**/*.coffee', 'starry/**/*.coffee', 'public/scripts/**/*.coffee'] 58 | .pipe plugins.coffeelint() 59 | .pipe plugins.coffeelint.reporter() 60 | 61 | # Font 62 | gulp.task 'fonts', -> 63 | gulp.src 'public/components/font-awesome/**/*.{eot,svg,ttf,woff}' 64 | .pipe gulp.dest '.tmp/public' 65 | .pipe plugins.size() 66 | return 67 | 68 | # Flash 69 | gulp.task 'flashs', -> 70 | gulp.src 'public/components/zeroclipboard/dist/ZeroClipboard.swf' 71 | .pipe gulp.dest '.tmp/public' 72 | .pipe plugins.size() 73 | return 74 | 75 | # Style 76 | gulp.task 'styles', -> 77 | gulp.src 'public/styles/pages/**/*.less' 78 | .pipe plugins.plumber errorHandler: handler 79 | .pipe plugins.less 80 | dumpLineNumbers: 'comments' 81 | .pipe plugins.autoprefixer 'last 2 version', 'safari 5', 'ie 9', 'opera 12.1', 'ios 6', 'android 4' 82 | .pipe plugins.plumber.stop() 83 | .pipe gulp.dest '.tmp/public/styles' 84 | .pipe plugins.size() 85 | return 86 | 87 | # Script 88 | gulp.task 'scripts', ['create-setting'], -> 89 | gulp.src 'public/scripts/pages/**/*.coffee', { read: false } 90 | .pipe plugins.plumber errorHandler: handler 91 | .pipe plugins.browserify 92 | debug: true 93 | extensions: ['.hbs', '.coffee'] 94 | .pipe plugins.rename 95 | extname: '.js' 96 | .pipe plugins.plumber.stop() 97 | .pipe gulp.dest '.tmp/public/scripts' 98 | .pipe plugins.size() 99 | return 100 | 101 | # Watch 102 | gulp.task 'watch', -> 103 | plugins.watch 'public/styles/**/*.less', (files, ccallback) -> 104 | gulp.start 'styles', ccallback 105 | plugins.watch 'public/scripts/**/*.{hbs,coffee}', (files, ccallback) -> 106 | gulp.start 'scripts', ccallback 107 | return 108 | 109 | # Serve 110 | gulp.task 'serve', -> 111 | return plugins.nodemon 112 | script: 'server.coffee' 113 | ext: 'coffee' 114 | ignore: [ 115 | 'gulpfile.coffee' 116 | '.tmp/**' 117 | 'dist/**' 118 | 'node_modules/**' 119 | 'docs/**' 120 | 'public/**' 121 | 'tasks/**' 122 | 'test/**' 123 | 'commands/**' 124 | ] 125 | .on 'change', ['lint'] 126 | .on 'restart', -> console.log 'restarted!' 127 | 128 | # Develop 129 | gulp.task 'develop', ['development-env', 'clean', 'lint'], -> 130 | gulp.start 'fonts', 'flashs', 'styles', 'scripts', 'watch', 'serve' 131 | 132 | # Fixture 133 | gulp.task 'fixture', (callback) -> 134 | fixture = require 'easy-fixture' 135 | MongoFixture = require 'easy-mongo-fixture' 136 | 137 | mongoFixture = new MongoFixture 138 | database: 'starry-test' 139 | collections: ['users', 'stories', 'sections', 'points'] 140 | dir: 'test/fixtures' 141 | override: true 142 | 143 | fixture.use mongoFixture 144 | fixture.load().done -> callback() 145 | 146 | # Test 147 | gulp.task 'test', ['test-env', 'fixture'], -> 148 | return gulp.src 'test/**/*.coffee' 149 | .pipe plugins.mocha 150 | reporter: 'spec' 151 | timeout: 3000 152 | globals: 153 | should: require 'should' 154 | .once 'end', -> 155 | process.exit() 156 | 157 | # Build assets 158 | gulp.task 'build-assets', -> 159 | return gulp.src ['public/*.{ico,png,txt,xml}', 'public/components/font-awesome/**/*.{eot,svg,ttf,woff}', 'public/components/zeroclipboard/dist/ZeroClipboard.swf'] 160 | .pipe gulp.dest 'dist/public' 161 | .pipe plugins.size() 162 | 163 | # Build style 164 | gulp.task 'build-styles', -> 165 | return gulp.src 'public/styles/pages/**/*.less' 166 | .pipe plugins.less() 167 | .pipe plugins.autoprefixer 'last 2 version', 'safari 5', 'ie 9', 'opera 12.1', 'ios 6', 'android 4' 168 | .pipe gulp.dest '.tmp/public/styles' 169 | .pipe plugins.size() 170 | 171 | # Build script 172 | gulp.task 'build-scripts', ['create-setting'], -> 173 | return gulp.src 'public/scripts/pages/**/*.coffee', { read: false } 174 | .pipe plugins.browserify 175 | extensions: ['.hbs', '.coffee'] 176 | .pipe plugins.rename 177 | extname: '.js' 178 | .pipe gulp.dest '.tmp/public/scripts' 179 | .pipe plugins.size() 180 | 181 | # Build image 182 | gulp.task 'build-images', -> 183 | return gulp.src('public/images/**/*') 184 | .pipe plugins.imagemin 185 | optimizationLevel: 3 186 | progressive: true 187 | interlaced: true 188 | .pipe gulp.dest 'dist/public/images' 189 | .pipe plugins.size() 190 | 191 | # Html 192 | gulp.task 'html', ['build-assets', 'build-styles', 'build-scripts', 'build-images'], -> 193 | assets = plugins.useref.assets searchPath: '{.tmp/public,public}' 194 | 195 | return gulp.src 'views/**/*.html' 196 | .pipe assets 197 | .pipe plugins.if '*.css', plugins.csso() 198 | .pipe plugins.if '*.js', plugins.uglify() 199 | .pipe plugins.rev() 200 | .pipe assets.restore() 201 | .pipe plugins.useref() 202 | .pipe plugins.revReplace() 203 | .pipe plugins.if '*.html', plugins.minifyHtml() 204 | .pipe plugins.if '*.css', gulp.dest 'dist/public' 205 | .pipe plugins.if '*.js', gulp.dest 'dist/public' 206 | .pipe plugins.if '*.html', gulp.dest 'dist/views' 207 | .pipe plugins.size() 208 | 209 | # Templates 210 | gulp.task 'templates', ['html'], -> 211 | return gulp.src 'dist/views/**/*.html' 212 | .pipe plugins.replace /((href|src){1}=["']?)(\/(images|styles|scripts){1}[^'">]*["']?)/ig, '$1{{ app.static_url }}$3' 213 | .pipe gulp.dest 'dist/views/' 214 | 215 | # Build 216 | gulp.task 'build', ['production-env', 'clean', 'lint'], -> 217 | gulp.start 'templates' 218 | 219 | # Default 220 | gulp.task 'default', ['develop'] 221 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | # Application 2 | path = require 'path' 3 | express = require 'express' 4 | passport = require 'passport' 5 | 6 | config = require './libs/config' 7 | 8 | {setting, db, mailer} = config 9 | 10 | app = express() 11 | 12 | require('./libs/mongoose') db 13 | require('./libs/passport') passport 14 | require('./libs/express') app, passport, setting, db 15 | require('./libs/mailer') setting, db 16 | require('./libs/router') app, setting 17 | 18 | module.exports = app 19 | -------------------------------------------------------------------------------- /libs/config.coffee: -------------------------------------------------------------------------------- 1 | # config 配置 2 | path = require 'path' 3 | fs = require 'fs' 4 | _ = require 'lodash' 5 | yaml = require 'js-yaml' 6 | 7 | env = process.env.NODE_ENV or 'development' 8 | root = path.normalize path.join __dirname, '../' 9 | configPath = path.join root, 'config' 10 | 11 | _read = (file) -> 12 | return {} if not fs.existsSync file 13 | return yaml.safeLoad fs.readFileSync file, 'utf8' 14 | 15 | defaultOptions = _read "#{configPath}/default.yaml" 16 | 17 | options = _.merge defaultOptions, _read "#{configPath}/#{env}.yaml" 18 | options.setting.root = root 19 | 20 | global.adou = {} 21 | 22 | module.exports = adou.config = options 23 | -------------------------------------------------------------------------------- /libs/express.coffee: -------------------------------------------------------------------------------- 1 | # express 配置 2 | path = require 'path' 3 | express = require 'express' 4 | 5 | module.exports = (app, passport, setting, db) -> 6 | # 网络传输数据压缩 7 | app.use require('compression')() 8 | 9 | # 模板 10 | swig = require 'swig' 11 | require('./swig') swig 12 | app.engine 'html', swig.renderFile 13 | app.set 'view engine', 'html' 14 | 15 | # icon 16 | app.use require('serve-favicon') path.join setting.root, 'public', 'favicon.ico' 17 | 18 | # 开发环境 19 | if 'development' is app.get 'env' 20 | app.use express.static path.join setting.root, '.tmp', 'public' 21 | app.use express.static path.join setting.root, 'public' 22 | 23 | app.use require('morgan') 'short' 24 | 25 | app.set 'views', path.join setting.root, 'views' 26 | app.set 'view cache', false 27 | swig.setDefaults cache: false 28 | 29 | # 测试环境 30 | if 'test' is app.get 'env' 31 | app.set 'views', path.join setting.root, 'views' 32 | 33 | # 生产环境 34 | if 'production' is app.get 'env' 35 | app.use express.static path.join(setting.root, 'dist', 'public'), maxAge: 365 * 24 * 3600 36 | 37 | app.enable 'view cache' 38 | app.set 'views', path.join setting.root, 'dist', 'views' 39 | 40 | bodyParser = require 'body-parser' 41 | app.use bodyParser.urlencoded extended: true 42 | app.use bodyParser.json type: 'json' 43 | 44 | app.use require('method-override')() 45 | app.use require('express-validator')() 46 | 47 | # cookie 和 session 48 | app.use require('cookie-parser') setting.secret 49 | session = require 'express-session' 50 | app.use session 51 | name: 'starry.id' 52 | secret: setting.secret 53 | cookie: 54 | path: '/' 55 | httpOnly: true 56 | maxAge: 1000 * 60 * 60 * 24 * 30 * 12 57 | store: new (require('connect-redis')(session)) 58 | host: db.redis.host 59 | prefix: 'session:' 60 | resave: true 61 | saveUninitialized: true 62 | 63 | app.use passport.initialize() 64 | app.use passport.session() 65 | 66 | # 防 csrf 攻击 67 | app.use require('csurf')() if 'test' isnt app.get 'env' 68 | 69 | app.use (req, res, done) -> 70 | res.cookie '_csrf', req.csrfToken() if 'test' isnt app.get 'env' 71 | res.locals.app = setting 72 | res.locals.user = req.user 73 | done() 74 | -------------------------------------------------------------------------------- /libs/mailer.coffee: -------------------------------------------------------------------------------- 1 | # 邮件管理 2 | path = require 'path' 3 | nodemailer = require 'nodemailer' 4 | smtpTransport = require 'nodemailer-smtp-transport' 5 | 6 | module.exports = (setting, mailer) -> 7 | transporter = nodemailer.createTransport smtpTransport mailer.smtp 8 | 9 | # 邮件管理 10 | class Mail 11 | # 构造函数 12 | # subject 标题 13 | # template 模板名称 14 | # data 模板参数 15 | constructor: (@subject, @template, @data) -> 16 | 17 | # 发送邮件 18 | send: (fromTo, callback) -> 19 | require('swig-email-templates') 20 | root: if process.env.NODE_ENV is 'production' then path.join(setting.root, 'dist', 'views', 'email') else path.join(setting.root, 'views', 'email') 21 | , (err, render) => 22 | return callback err, null if err 23 | render @template + '.html', @data, (err, html, text) => 24 | return callback err, null if err 25 | transporter.sendMail 26 | from: mailer.from, 27 | to: fromTo, 28 | subject: @subject, 29 | text: text, 30 | html: html 31 | , (err, info) -> 32 | transporter.close() 33 | callback err, info if callback 34 | 35 | adou.mail = (subject, template, data) -> return new Mail subject, template, data 36 | -------------------------------------------------------------------------------- /libs/mongoose.coffee: -------------------------------------------------------------------------------- 1 | # MongoDB 管理 2 | mongoose = require 'mongoose' 3 | 4 | module.exports = (db) -> 5 | {url} = db.mongodb 6 | mongoose.connect url 7 | db = mongoose.connection 8 | db.on 'error', -> throw new Error "unable to connect to database at #{url}" 9 | -------------------------------------------------------------------------------- /libs/passport.coffee: -------------------------------------------------------------------------------- 1 | # 登录认证 2 | User = require '../app/models/user' 3 | 4 | module.exports = (passport) -> 5 | passport.serializeUser (user, done) -> 6 | done null, user.id 7 | 8 | passport.deserializeUser (id, done) -> 9 | User.findById id, 'name email', done 10 | 11 | # Local 12 | passport.use new (require('passport-local').Strategy) 13 | usernameField: 'email' 14 | passwordField: 'password' 15 | , (email, password, done) -> 16 | User.findOne { email: email }, 'name email salt hashed_password', (err, user) -> 17 | return done err if err 18 | return done null, false, message: '该用户不存在。' if not user 19 | return done null, false, message: '密码错误。' if not user.authenticate password 20 | return done null, user 21 | -------------------------------------------------------------------------------- /libs/router.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | express = require 'express' 3 | 4 | module.exports = (app, setting) -> 5 | # 页面 6 | app.use require '../app/pages/default' 7 | app.use require '../app/pages/account' 8 | app.use '/stories', require '../app/pages/story' 9 | 10 | # 给接口加上头信息 11 | app.use '/api/*', (req, res, done) -> 12 | res.contentType "application/vnd.#{setting.api_vnd}+json" 13 | done() 14 | 15 | # 接口 16 | app.use '/api/', require '../app/apis/default' 17 | app.use '/api/stories', require '../app/apis/story' 18 | app.use '/api/sections', require '../app/apis/section' 19 | app.use '/api/points', require '../app/apis/point' 20 | 21 | # 错误处理 22 | app.use (err, req, res, done) -> 23 | return done err if not err.isValidation 24 | errs = _.uniq err.errors, (err) -> 25 | return err.param 26 | return res.status(400).json error: errs 27 | 28 | # 生产环境 29 | if 'production' is app.get 'env' 30 | # 500 页面 31 | app.use (err, req, res, done) -> 32 | console.error err 33 | if req.xhr 34 | res.status(500).json error: '系统出错!' 35 | else 36 | res.status(500).render '500', 37 | title: 500 38 | 39 | # 404 页面 40 | app.use (req, res) -> 41 | console.warn "#{req.originalUrl} does not exist." 42 | if (req.xhr) 43 | res.status(404).json error: '请求地址不存在!' 44 | else 45 | res.status(404).render '404', 46 | title: 404 47 | -------------------------------------------------------------------------------- /libs/swig.coffee: -------------------------------------------------------------------------------- 1 | # swig 模板 2 | module.exports = (swig) -> 3 | swig.setFilter 'markdown', (text) -> 4 | return require('markdown').markdown.toHTML text 5 | 6 | swig.setFilter 'circle', (bubble) -> 7 | return '
' if not bubble 8 | progress = if /^([0-9]{1,3}\%)$/.test(bubble) then parseInt bubble.replace('%', ''), 10 else null 9 | return "
" if progress && progress >= 0 && progress <= 100 10 | return "
" if 0 is bubble.indexOf 'icon-' 11 | return "
#{bubble}
" 12 | 13 | swig.setFilter 'circle_type', (bubble) -> 14 | return '' if not bubble 15 | progress = if /^([0-9]{1,3}\%)$/.test(bubble) then parseInt bubble.replace('%', ''), 10 else null 16 | return 'progress' if progress && progress >= 0 && progress <= 100 17 | return 'icon' if 0 is bubble.indexOf 'icon-' 18 | return 'text' 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Starry", 3 | "version": "0.0.1", 4 | "private": true, 5 | "author": "不会飞的羊", 6 | "scripts": { 7 | "test": "gulp test" 8 | }, 9 | "engines": { 10 | "node": ">= 0.10" 11 | }, 12 | "browserify": { 13 | "transform": [ 14 | "hbsfy", 15 | "coffeeify", 16 | "browserify-shim" 17 | ] 18 | }, 19 | "browser": {}, 20 | "browserify-shim": { 21 | "adou": "global:adou", 22 | "hbsfy/runtime": "global:Handlebars", 23 | "markdown": "global:markdown", 24 | "fast-click": "global:FastClick", 25 | "router": "global:Router", 26 | "flow": "global:Flow", 27 | "jquery": "global:$", 28 | "toastr": "global:toastr", 29 | "zero-clipboard": "global:ZeroClipboard" 30 | }, 31 | "dependencies": { 32 | "async": "0.9.0", 33 | "body-parser": "1.9.2", 34 | "coffee-script": "1.8.0", 35 | "compression": "1.2.0", 36 | "connect-redis": "2.1.0", 37 | "cookie-parser": "1.3.3", 38 | "csurf": "1.6.2", 39 | "express": "4.10.1", 40 | "express-session": "1.9.2", 41 | "express-validator": "2.7.0", 42 | "js-yaml": "3.2.2", 43 | "lodash": "2.4.1", 44 | "markdown": "0.5.0", 45 | "method-override": "2.3.0", 46 | "mongoose": "3.8.19", 47 | "morgan": "1.5.0", 48 | "nodemailer": "1.3.0", 49 | "nodemailer-smtp-transport": "0.1.13", 50 | "passport": "0.2.1", 51 | "passport-local": "1.0.0", 52 | "serve-favicon": "2.1.6", 53 | "swig": "1.4.2", 54 | "swig-email-templates": "1.3.0", 55 | "validator": "3.22.1" 56 | }, 57 | "devDependencies": { 58 | "browserify": "^6.2.0", 59 | "browserify-shim": "^3.8.0", 60 | "coffeeify": "^0.7.0", 61 | "del": "^0.1.3", 62 | "easy-fixture": "^1.0.0", 63 | "easy-mongo-fixture": "^1.0.7", 64 | "glob": "^4.0.6", 65 | "gulp": "^3.8.10", 66 | "gulp-autoprefixer": "^1.0.1", 67 | "gulp-browserify": "^0.5.0", 68 | "gulp-coffeelint": "^0.4.0", 69 | "gulp-csso": "^0.2.9", 70 | "gulp-if": "^1.2.5", 71 | "gulp-imagemin": "^1.2.1", 72 | "gulp-less": "^1.3.6", 73 | "gulp-load-plugins": "^0.7.1", 74 | "gulp-minify-html": "^0.1.6", 75 | "gulp-mocha": "^1.1.1", 76 | "gulp-nodemon": "^1.0.4", 77 | "gulp-plumber": "^0.6.6", 78 | "gulp-rename": "^1.2.0", 79 | "gulp-replace": "^0.5.0", 80 | "gulp-rev": "^2.0.1", 81 | "gulp-rev-replace": "^0.3.1", 82 | "gulp-size": "^1.1.0", 83 | "gulp-uglify": "^1.0.1", 84 | "gulp-useref": "^1.0.2", 85 | "gulp-util": "^3.0.1", 86 | "gulp-watch": "^1.1.0", 87 | "handlebars": "^1.3.0", 88 | "hbsfy": "^2.2.0", 89 | "should": "^4.2.1", 90 | "supertest": "^0.14.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ysheep666/starry/1b1fcfdeb9425aa449fe7fe807eaa0296024b891/public/favicon.ico -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | /* TEAM */ 2 | Name: @不会飞的羊 3 | From: Beijing,China 4 | 5 | /* SITE */ 6 | Last update:2014/09/27 7 | Language: Chinese 8 | Standards: Html5, Css3, Javascript, Mongodb 9 | Components: jQuery, Express, Mongoose 10 | Doctype: HTML5 11 | IDE: Sublime Text, Sketch 12 | Software: etc. 13 | -------------------------------------------------------------------------------- /public/images/cover-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ysheep666/starry/1b1fcfdeb9425aa449fe7fe807eaa0296024b891/public/images/cover-1024.jpg -------------------------------------------------------------------------------- /public/images/cover-1600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ysheep666/starry/1b1fcfdeb9425aa449fe7fe807eaa0296024b891/public/images/cover-1600.jpg -------------------------------------------------------------------------------- /public/images/cover-800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ysheep666/starry/1b1fcfdeb9425aa449fe7fe807eaa0296024b891/public/images/cover-800.jpg -------------------------------------------------------------------------------- /public/images/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ysheep666/starry/1b1fcfdeb9425aa449fe7fe807eaa0296024b891/public/images/iphone.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /public/scripts/components/chart.coffee: -------------------------------------------------------------------------------- 1 | # 图表 2 | $ = require 'jquery' 3 | 4 | class Chart 5 | @DEFAULTS: 6 | container: $('body') 7 | 8 | constructor: (options) -> 9 | @options = $.extend {}, Chart.DEFAULTS, options 10 | @container = @options.container 11 | @container.on 'focusin', '[rel-chart="yes"]', (event) => 12 | @$el = $ event.currentTarget 13 | @$el.on 'input', => @_analyse @$el.val() 14 | @$el.one 'focusout', => @$el.unbind 'input' 15 | 16 | as: ($el) -> 17 | @$el = $el 18 | @_analyse @$el.val() 19 | 20 | # 分析 21 | _analyse: (val) -> 22 | percent = if /^([0-9]{1,3}\%)$/.test(val) then parseInt val.replace('%', ''), 10 else null 23 | if percent && percent >= 0 && percent <= 100 24 | $chart = @$el.siblings '.chart' 25 | if not $chart.data('easyPieChart') 26 | $chart.addClass('open').easyPieChart 27 | scaleColor: false 28 | size: 18 29 | lineWidth: 9 30 | barColor: $chart.css 'color' 31 | lineCap: 'butt' 32 | trackColor: 'transparent' 33 | 34 | $chart.data('easyPieChart').update percent 35 | else 36 | @$el.siblings('.chart').data('easyPieChart', null).html('').removeClass 'open' 37 | 38 | module.exports = Chart 39 | -------------------------------------------------------------------------------- /public/scripts/components/csrf.coffee: -------------------------------------------------------------------------------- 1 | # csrf 2 | $ = require 'jquery' 3 | $.ajaxSetup 4 | beforeSend: (xhr, settings) -> 5 | xhr.setRequestHeader 'x-csrf-token', $.cookie('_csrf') if settings.type is 'POST' or settings.type is 'PATCH' or settings.type is 'PUT' or settings.type == 'DELETE' 6 | -------------------------------------------------------------------------------- /public/scripts/components/iconpicker.coffee: -------------------------------------------------------------------------------- 1 | # 图标选择器 2 | $ = require 'jquery' 3 | template = require '../templates/components/iconpicker.hbs' 4 | 5 | class Iconpicker 6 | @DEFAULTS: 7 | container: $('body') 8 | icons: [ 9 | 'adjust', 'adn', 'align-center', 'align-justify', 'align-left', 'align-right', 'ambulance', 'anchor', 10 | 'android', 'angellist', 'angle-double-down', 'angle-double-left', 'angle-double-right', 'angle-double-up', 11 | 'angle-down', 'angle-left', 'angle-right', 'angle-up', 'apple', 'archive', 'area-chart', 'arrow-circle-down', 12 | 'arrow-circle-left', 'arrow-circle-o-down', 'arrow-circle-o-left', 'arrow-circle-o-right', 'arrow-circle-o-up', 13 | 'arrow-circle-right', 'arrow-circle-up', 'arrow-down', 'arrow-left', 'arrow-right', 'arrow-up', 'arrows', 14 | 'arrows-alt', 'arrows-h', 'arrows-v', 'asterisk', 'at', 'automobile', 'backward', 'ban', 'bank', 'bar-chart', 15 | 'bar-chart-o', 'barcode', 'bars', 'beer', 'behance', 'behance-square', 'bell', 'bell-o', 'bell-slash', 'bell-slash-o', 16 | 'bicycle', 'binoculars', 'birthday-cake', 'bitbucket', 'bitbucket-square', 'bitcoin', 'bold', 'bolt', 'bomb', 'book', 17 | 'bookmark', 'bookmark-o', 'briefcase', 'btc', 'bug', 'building', 'building-o', 'bullhorn', 'bullseye', 'bus', 'cab', 18 | 'calculator', 'calendar', 'calendar-o', 'camera', 'camera-retro', 'car', 'caret-down', 'caret-left', 'caret-right', 19 | 'caret-square-o-down', 'caret-square-o-left', 'caret-square-o-right', 'caret-square-o-up', 'caret-up', 'cc', 'cc-amex', 20 | 'cc-discover', 'cc-mastercard', 'cc-paypal', 'cc-stripe', 'cc-visa', 'certificate', 'chain', 'chain-broken', 'check', 21 | 'check-circle', 'check-circle-o', 'check-square', 'check-square-o', 'chevron-circle-down', 'chevron-circle-left', 'chevron-circle-right', 22 | 'chevron-circle-up', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'child', 'circle', 'circle-o', 'circle-o-notch', 23 | 'circle-thin', 'clipboard', 'clock-o', 'close', 'cloud', 'cloud-download', 'cloud-upload', 'cny', 'code', 'code-fork', 24 | 'codepen', 'coffee', 'cog', 'cogs', 'columns', 'comment', 'comment-o', 'comments', 'comments-o', 'compass', 'compress', 25 | 'copy', 'copyright', 'credit-card', 'crop', 'crosshairs', 'css3', 'cube', 'cubes', 'cut', 'cutlery', 'dashboard', 26 | 'database', 'dedent', 'delicious', 'desktop', 'deviantart', 'digg', 'dollar', 'dot-circle-o', 'download', 'dribbble', 27 | 'dropbox', 'drupal', 'edit', 'eject', 'ellipsis-h', 'ellipsis-v', 'empire', 'envelope', 'envelope-o', 'envelope-square', 28 | 'eraser', 'eur', 'euro', 'exchange', 'exclamation', 'exclamation-circle', 'exclamation-triangle', 'expand', 'external-link', 29 | 'external-link-square', 'eye', 'eye-slash', 'eyedropper', 'facebook', 'facebook-square', 'fast-backward', 'fast-forward', 30 | 'fax', 'female', 'fighter-jet', 'file', 'file-archive-o', 'file-audio-o', 'file-code-o', 'file-excel-o', 'file-image-o', 31 | 'file-movie-o', 'file-o', 'file-pdf-o', 'file-photo-o', 'file-picture-o', 'file-powerpoint-o', 'file-sound-o', 'file-text', 32 | 'file-text-o', 'file-video-o', 'file-word-o', 'file-zip-o', 'files-o', 'film', 'filter', 'fire', 'fire-extinguisher', 'flag', 33 | 'flag-checkered', 'flag-o', 'flash', 'flask', 'flickr', 'floppy-o', 'folder', 'folder-o', 'folder-open', 'folder-open-o', 34 | 'font', 'forward', 'foursquare', 'frown-o', 'futbol-o', 'gamepad', 'gavel', 'gbp', 'ge', 'gear', 'gears', 'gift', 'git', 35 | 'git-square', 'github', 'github-alt', 'github-square', 'gittip', 'glass', 'globe', 'google', 'google-plus', 36 | 'google-plus-square', 'google-wallet', 'graduation-cap', 'group', 'h-square', 'hacker-news', 'hand-o-down', 'hand-o-left', 37 | 'hand-o-right', 'hand-o-up', 'hdd-o', 'header', 'headphones', 'heart', 'heart-o', 'history', 'home', 'hospital-o', 'html5', 38 | 'ils', 'image', 'inbox', 'indent', 'info', 'info-circle', 'inr', 'instagram', 'institution', 'ioxhost', 'italic', 'joomla', 39 | 'jpy', 'jsfiddle', 'key', 'keyboard-o', 'krw', 'language', 'laptop', 'lastfm', 'lastfm-square', 'leaf', 'legal', 'lemon-o', 40 | 'level-down', 'level-up', 'life-bouy', 'life-buoy', 'life-ring', 'life-saver', 'lightbulb-o', 'line-chart', 'link', 'linkedin', 41 | 'linkedin-square', 'linux', 'list', 'list-alt', 'list-ol', 'list-ul', 'location-arrow', 'lock', 'long-arrow-down', 42 | 'long-arrow-left', 'long-arrow-right', 'long-arrow-up', 'magic', 'magnet', 'mail-forward', 'mail-reply', 'mail-reply-all', 43 | 'male', 'map-marker', 'maxcdn', 'meanpath', 'medkit', 'meh-o', 'microphone', 'microphone-slash', 'minus', 'minus-circle', 44 | 'minus-square', 'minus-square-o', 'mobile', 'mobile-phone', 'money', 'moon-o', 'mortar-board', 'music', 'navicon', 45 | 'newspaper-o', 'openid', 'outdent', 'pagelines', 'paint-brush', 'paper-plane', 'paper-plane-o', 'paperclip', 'paragraph', 46 | 'paste', 'pause', 'paw', 'paypal', 'pencil', 'pencil-square', 'pencil-square-o', 'phone', 'phone-square', 'photo', 47 | 'picture-o', 'pie-chart', 'pied-piper', 'pied-piper-alt', 'pinterest', 'pinterest-square', 'plane', 'play', 'play-circle', 48 | 'play-circle-o', 'plug', 'plus', 'plus-circle', 'plus-square', 'plus-square-o', 'power-off', 'print', 'puzzle-piece', 49 | 'qq', 'qrcode', 'question', 'question-circle', 'quote-left', 'quote-right', 'ra', 'random', 'rebel', 'recycle', 'reddit', 50 | 'reddit-square', 'refresh', 'remove', 'renren', 'reorder', 'repeat', 'reply', 'reply-all', 'retweet', 'rmb', 'road', 51 | 'rocket', 'rotate-left', 'rotate-right', 'rouble', 'rss', 'rss-square', 'rub', 'ruble', 'rupee', 'save', 'scissors', 52 | 'search', 'search-minus', 'search-plus', 'send', 'send-o', 'share', 'share-alt', 'share-alt-square', 'share-square', 53 | 'share-square-o', 'shekel', 'sheqel', 'shield', 'shopping-cart', 'sign-in', 'sign-out', 'signal', 'sitemap', 'skype', 54 | 'slack', 'sliders', 'slideshare', 'smile-o', 'soccer-ball-o', 'sort', 'sort-alpha-asc', 'sort-alpha-desc', 'sort-amount-asc', 55 | 'sort-amount-desc', 'sort-asc', 'sort-desc', 'sort-down', 'sort-numeric-asc', 'sort-numeric-desc', 'sort-up', 'soundcloud', 56 | 'space-shuttle', 'spinner', 'spoon', 'spotify', 'square', 'square-o', 'stack-exchange', 'stack-overflow', 'star', 57 | 'star-half', 'star-half-empty', 'star-half-full', 'star-half-o', 'star-o', 'steam', 'steam-square', 'step-backward', 58 | 'step-forward', 'stethoscope', 'stop', 'strikethrough', 'stumbleupon', 'stumbleupon-circle', 'subscript', 'suitcase', 59 | 'sun-o', 'superscript', 'support', 'table', 'tablet', 'tachometer', 'tag', 'tags', 'tasks', 'taxi', 'tencent-weibo', 60 | 'terminal', 'text-height', 'text-width', 'th', 'th-large', 'th-list', 'thumb-tack', 'thumbs-down', 'thumbs-o-down', 61 | 'thumbs-o-up', 'thumbs-up', 'ticket', 'times', 'times-circle', 'times-circle-o', 'tint', 'toggle-down', 'toggle-left', 62 | 'toggle-off', 'toggle-on', 'toggle-right', 'toggle-up', 'trash', 'trash-o', 'tree', 'trello', 'trophy', 'truck', 'try', 63 | 'tty', 'tumblr', 'tumblr-square', 'turkish-lira', 'twitch', 'twitter', 'twitter-square', 'umbrella', 'underline', 'undo', 64 | 'university', 'unlink', 'unlock', 'unlock-alt', 'unsorted', 'upload', 'usd', 'user', 'user-md', 'users', 'video-camera', 65 | 'vimeo-square', 'vine', 'vk', 'volume-down', 'volume-off', 'volume-up', 'warning', 'wechat', 'weibo', 'weixin', 'wheelchair', 66 | 'wifi', 'windows', 'won', 'wordpress', 'wrench', 'xing', 'xing-square', 'yahoo', 'yelp', 'yen', 'youtube', 'youtube-play', 67 | 'youtube-square' 68 | ] 69 | 70 | constructor: (options) -> 71 | @options = $.extend {}, Iconpicker.DEFAULTS, options 72 | @container = @options.container 73 | @container.on 'focusin', '[rel-iconpicker="yes"]', (event) => 74 | @$el = $ event.currentTarget 75 | @_analyse @$el.val() 76 | @$el.on 'input', => @_analyse @$el.val() 77 | @$el.one 'focusout', => @$el.unbind 'input' 78 | 79 | as: ($el) -> 80 | val = $el.val() 81 | icons = @_filter val 82 | if 1 is icons.length && val is "icon-#{icons[0]}" 83 | $el.siblings('.bubble-icon').html "" 84 | else 85 | $el.siblings('.bubble-icon').html '' 86 | 87 | # 分析 88 | _analyse: (val) -> 89 | icons = @_filter val 90 | $el = @$el 91 | if 0 < icons.length 92 | if not $el.data 'bs.popover' 93 | $el.popover placement: 'bottom', container: @container, trigger: 'focus', html: true, content: template icons: icons 94 | $el.popover 'show' 95 | 96 | popover = $el.data('bs.popover') 97 | popover.$tip.addClass 'iconpicker-popover' 98 | popover.$tip.on 'click', '.iconpicker a', (event) -> 99 | event.preventDefault() 100 | value = $(this).data 'value' 101 | $el.val("icon-#{value}").blur() 102 | $el.siblings('.bubble-icon').html "" 103 | else 104 | popover = $el.data('bs.popover') 105 | popover.options.content = template icons: icons 106 | popover.$tip.find('.popover-content').html popover.options.content 107 | else 108 | $el.popover 'destroy' if @$el.data 'bs.popover' 109 | 110 | if 1 is icons.length && val is "icon-#{icons[0]}" 111 | $el.siblings('.bubble-icon').html "" 112 | else 113 | $el.siblings('.bubble-icon').html '' 114 | 115 | # 筛选 116 | _filter: (val) -> 117 | return [] if 0 isnt val.indexOf 'icon-' 118 | return @options.icons if 'icon-' is val 119 | icons = [] 120 | for icon in @options.icons 121 | icons.push icon if 0 is icon.indexOf val.substr 5 122 | return icons 123 | 124 | 125 | module.exports = Iconpicker 126 | -------------------------------------------------------------------------------- /public/scripts/components/upload-image.coffee: -------------------------------------------------------------------------------- 1 | # 上传图片 2 | $ = require 'jquery' 3 | Flow = require 'flow' 4 | 5 | {upyun} = adou 6 | 7 | class Upload 8 | constructor: -> 9 | # Options 可以给之后的扩展使用 10 | options = 11 | target: upyun.api + '/starry-images' 12 | singleFile: true 13 | testChunks: false 14 | 15 | @flow = new Flow options 16 | 17 | assignBrowse: -> 18 | @flow.assignBrowse arguments 19 | 20 | assignDrop: -> 21 | @flow.assignDrop arguments 22 | 23 | upload: -> 24 | @flow.upload() 25 | 26 | on: (event, callback) -> 27 | if event is 'filesSubmitted' 28 | @flow.on event, (files) => 29 | return callback '图片大小不能超过 2M' if 2 * 1024 * 1024 < files[0].size 30 | return callback '图片格式错误' if not /^.*(\.jpg|\.jpeg|\.bmp|\.gif|\.png)$/.test files[0].name.toLowerCase() 31 | $.ajax 32 | url: '/api/upyun_token' 33 | type: 'POST' 34 | data: 35 | bucket: 'starry-images' 36 | expiration: parseInt (new Date().getTime() + 600000)/1000, 10 37 | 'save-key': '/{year}{mon}/{day}/{filemd5}-{random}{.suffix}' 38 | dataType: 'json' 39 | .done (data) => 40 | data['content-length-range'] = '0,2048000' 41 | @flow.opts.query = data 42 | callback() 43 | .fail (res) -> 44 | error = res.responseJSON.error 45 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 46 | callback errors.join('
') 47 | else 48 | @flow.on event, callback 49 | 50 | module.exports = Upload 51 | -------------------------------------------------------------------------------- /public/scripts/pages/account/signin.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | toastr = require 'toastr' 3 | FastClick = require 'fast-click' 4 | require '../../components/csrf' 5 | 6 | toastr.options.positionClass = 'toast-bottom-right' 7 | 8 | $ -> 9 | FastClick.attach document.body 10 | 11 | $form = $ '#formSignin' 12 | $inputs = $form.find '.inputs' 13 | $submit = $form.find 'button[type="submit"]' 14 | 15 | $form.on 'submit', (event) -> 16 | event.preventDefault() 17 | $submit.button 'loading' 18 | $.ajax 19 | url: '/api/signin' 20 | type: 'POST' 21 | data: $form.serialize() 22 | dataType: 'json' 23 | .done (res) -> 24 | $submit.button 'reset' 25 | window.location.href = '/' 26 | .fail (res) -> 27 | $submit.button 'reset' 28 | error = res.responseJSON.error 29 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 30 | toastr.error errors.join('
'), '登陆失败!' 31 | -------------------------------------------------------------------------------- /public/scripts/pages/account/signup.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | toastr = require 'toastr' 3 | FastClick = require 'fast-click' 4 | require '../../components/csrf' 5 | 6 | toastr.options.positionClass = 'toast-bottom-right' 7 | 8 | $ -> 9 | FastClick.attach document.body 10 | 11 | $form = $ '#formSignup' 12 | $inputs = $form.find '.inputs' 13 | $submit = $form.find 'button[type="submit"]' 14 | 15 | $form.on 'submit', (event) -> 16 | event.preventDefault() 17 | $submit.button 'loading' 18 | $.ajax 19 | url: '/api/signup' 20 | type: 'POST' 21 | data: $form.serialize() 22 | dataType: 'json' 23 | .done (res) -> 24 | $submit.button 'reset' 25 | window.location.href = '/' 26 | .fail (res) -> 27 | $submit.button 'reset' 28 | error = res.responseJSON.error 29 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 30 | toastr.error errors.join('
'), '注册失败!' 31 | -------------------------------------------------------------------------------- /public/scripts/pages/default/show.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | FastClick = require 'fast-click' 3 | 4 | $ -> 5 | FastClick.attach document.body 6 | 7 | $window = $ window 8 | width = $window.width() 9 | if width < 800 10 | adou.backgroundSuffix = 'background800' 11 | else if width > 600 and width < 1200 12 | adou.backgroundSuffix = 'background1024' 13 | else 14 | adou.backgroundSuffix = 'background1600' 15 | 16 | $background = $ '.section-background' 17 | $background.css 'backgroundImage', "url(#{$background.data('background')}!#{adou.backgroundSuffix})" if $background.data 'background' 18 | 19 | $('.section').each (index) -> 20 | $el = $ this 21 | if 0 is index%2 then $el.addClass 'section-black' else $el.removeClass 'section-black' 22 | 23 | $('.point').each (index) -> 24 | $el = $ this 25 | if 0 is index%2 then $el.removeClass 'point-right' else $el.addClass 'point-right' 26 | 27 | $('.circle .chart').each -> 28 | $el = $ this 29 | $el.easyPieChart 30 | scaleColor: false 31 | size: 31 32 | lineWidth: 15.5 33 | barColor: $el.css 'color' 34 | lineCap: 'butt' 35 | trackColor: 'transparent' 36 | 37 | $el.on 'mouseenter', -> 38 | pie = $el.data 'easyPieChart' 39 | pie.update 0 40 | pie.update $el.data 'progress' 41 | 42 | $el.data('easyPieChart').update $el.data 'progress' 43 | 44 | $('body').on 'click', 'a[href*=#]', (event) -> 45 | event.preventDefault() 46 | $target = $ '#' + @hash.slice 1 47 | $('html, body').animate { scrollTop: $target.position().top }, 600 if $target.length 48 | 49 | swingIn = -> 50 | $('.point').each (index) -> 51 | $el = $ this 52 | if (0 < $window.scrollTop() + $window.height() - $el.offset().top) && !$el.hasClass 'swing-in' 53 | $chart = $el.find '.circle .chart' 54 | window.setTimeout -> 55 | if $chart.length 56 | pie = $chart.data 'easyPieChart' 57 | pie.update 0 58 | pie.update $chart.data 'progress' 59 | $el.addClass 'swing-in' 60 | , 200 61 | 62 | swingIn() 63 | $window.on 'scroll', -> swingIn() 64 | -------------------------------------------------------------------------------- /public/scripts/pages/story/default.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | markdown = require 'markdown' 3 | toastr = require 'toastr' 4 | Router = require 'router' 5 | Handlebars = require 'hbsfy/runtime' 6 | FastClick = require 'fast-click' 7 | Upload = require '../../components/upload-image' 8 | Chart = require '../../components/chart' 9 | Iconpicker = require '../../components/iconpicker' 10 | require '../../components/csrf' 11 | 12 | adou.backgroundSuffix = 'background1600' 13 | toastr.options.positionClass = 'toast-bottom-right' 14 | 15 | # 组件 16 | components = 17 | logo: require '../../templates/components/logo.hbs' 18 | profile: require '../../templates/components/profile.hbs' 19 | section: require '../../templates/components/section.hbs' 20 | sectionAdd: require '../../templates/components/section-add.hbs' 21 | sectionTitle: require '../../templates/components/section-title.hbs' 22 | sectionNavigation: require '../../templates/components/section-navigation.hbs' 23 | point: require '../../templates/components/point.hbs' 24 | pointAdd: require '../../templates/components/point-add.hbs' 25 | pointEdit: require '../../templates/components/point-edit.hbs' 26 | 27 | # 页面 28 | pages = 29 | list: require '../../templates/pages/story/list.hbs' 30 | detail: require '../../templates/pages/story/detail.hbs' 31 | 32 | Handlebars.registerPartial 'logo', components.logo 33 | Handlebars.registerPartial 'profile', components.profile 34 | Handlebars.registerPartial 'section', components.section 35 | Handlebars.registerPartial 'section-add', components.sectionAdd 36 | Handlebars.registerPartial 'section-title', components.sectionTitle 37 | Handlebars.registerPartial 'section-navigation', components.sectionNavigation 38 | Handlebars.registerPartial 'point', components.point 39 | Handlebars.registerPartial 'point-add', components.pointAdd 40 | Handlebars.registerPartial 'point-edit', components.pointEdit 41 | 42 | Handlebars.registerHelper 'markdown', (text) -> 43 | return markdown.toHTML text 44 | 45 | Handlebars.registerHelper 'circle', (bubble) -> 46 | return '
' if not bubble 47 | progress = if /^([0-9]{1,3}\%)$/.test(bubble) then parseInt bubble.replace('%', ''), 10 else null 48 | return "
" if progress && progress >= 0 && progress <= 100 49 | return "
" if 0 is bubble.indexOf 'icon-' 50 | return "
#{bubble}
" 51 | 52 | Handlebars.registerHelper 'circle-type', (bubble) -> 53 | return '' if not bubble 54 | progress = if /^([0-9]{1,3}\%)$/.test(bubble) then parseInt bubble.replace('%', ''), 10 else null 55 | return 'progress' if progress && progress >= 0 && progress <= 100 56 | return 'icon' if 0 is bubble.indexOf 'icon-' 57 | return 'text' 58 | 59 | {upyun, preloaded} = adou 60 | 61 | $ -> 62 | FastClick.attach document.body 63 | 64 | $wrap = $ '#wrap' 65 | $list = $ '#list' 66 | $detail = $ '#detail' 67 | 68 | oChart = new Chart container: $detail 69 | oIconpicker = new Iconpicker container: $detail 70 | 71 | resize = -> 72 | $window = $ window 73 | width = $window.width() 74 | if width < 800 75 | adou.backgroundSuffix = 'background800' 76 | else if width > 600 and width < 1200 77 | adou.backgroundSuffix = 'background1024' 78 | else 79 | adou.backgroundSuffix = 'background1600' 80 | 81 | $wrap.height $window.height() 82 | 83 | resize() 84 | 85 | $(window).on 'resize', resize 86 | 87 | $('.feedback').tooltip() 88 | 89 | _list = (data) -> 90 | $list.html pages.list data 91 | 92 | # 新建故事 93 | $('#add').on 'click', (event) -> 94 | event.preventDefault() 95 | $.ajax 96 | url: '/api/stories' 97 | type: 'POST' 98 | dataType: 'json' 99 | .done (story) -> 100 | router.setRoute "stories/#{story.id}" 101 | .fail (res) -> 102 | error = res.responseJSON.error 103 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 104 | toastr.error errors.join('
'), '新建故事出错!' 105 | 106 | $items = $ '#items' 107 | $items.on 'click', 'a .trash', (event) -> 108 | event.preventDefault() 109 | event.stopPropagation() 110 | $(this).closest('.actions').addClass('open').one 'mouseleave', -> $(this).removeClass 'open' 111 | 112 | $items.on 'click', 'a .cancel', (event) -> 113 | event.preventDefault() 114 | event.stopPropagation() 115 | $(this).closest('.actions').removeClass('open').unbind 'mouseleave' 116 | 117 | $items.on 'click', 'a .remove', (event) -> 118 | event.preventDefault() 119 | event.stopPropagation() 120 | $item = $(this).closest 'a.item' 121 | $.ajax 122 | url: "/api/stories/#{$item.data('id')}" 123 | type: 'DELETE' 124 | dataType: 'json' 125 | .done -> 126 | $item.addClass('fadeOut').one $.support.transition.end, -> $item.remove() 127 | .fail (res) -> 128 | error = res.responseJSON.error 129 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 130 | toastr.error errors.join('
'), '删除故事出错!' 131 | 132 | $list.unbind($.support.transition.end).one $.support.transition.end, -> $detail.html '' 133 | $wrap.removeClass 'bige' 134 | 135 | _detail = (data) -> 136 | data.backgroundSuffix = adou.backgroundSuffix 137 | $detail.html pages.detail data 138 | 139 | $profile = $ '#profile' 140 | 141 | # 刷新 142 | refreshSection = -> 143 | sections = [] 144 | $detail.find('.section').each (index) -> 145 | $el = $ this 146 | if 0 is index%2 then $el.addClass 'section-black' else $el.removeClass 'section-black' 147 | if $el.data 'id' 148 | sections.push 149 | id: $el.data 'id' 150 | title: $el.find('.section-title .name').text() 151 | 152 | $profile.find('.nav').html components.sectionNavigation sections: sections 153 | 154 | refreshPoint = (noSortable) -> 155 | $detail.find('.point').each (index) -> 156 | $el = $ this 157 | if 0 is index%2 then $el.removeClass 'point-right' else $el.addClass 'point-right' 158 | 159 | if not noSortable 160 | $detail.find('.points').each -> 161 | $el = $ this 162 | $el.sortable 'destroy' 163 | $el.sortable 164 | forcePlaceholderSize: true 165 | handle: '.circle' 166 | items: '.point-data' 167 | placeholder: '
' 168 | .on 'dragenter.h5s', -> 169 | $detail.find('.point:not(.sortable-dragging)').each (index) -> 170 | $el = $ this 171 | if 0 is index%2 then $el.removeClass 'point-right' else $el.addClass 'point-right' 172 | .on 'sortupdate', (event) -> 173 | refreshPoint true 174 | $el = $ event.target 175 | points = [] 176 | $el.find('.point-data').each -> points.push $(this).data 'id' 177 | $.ajax 178 | url: "/api/sections/#{$el.closest('.section').data('id')}" 179 | type: 'PATCH' 180 | data: points: points 181 | dataType: 'json' 182 | .fail (res) -> 183 | error = res.responseJSON.error 184 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 185 | toastr.error errors.join('
'), '节点排序出错!' 186 | window.location.reload() 187 | 188 | refreshOnePieChart = ($el) -> 189 | $el.html('').data 'easyPieChart', null 190 | if not $el.data 'easyPieChart' 191 | $el.easyPieChart 192 | scaleColor: false 193 | size: 31 194 | lineWidth: 15.5 195 | barColor: $el.css 'color' 196 | lineCap: 'butt' 197 | trackColor: 'transparent' 198 | 199 | $el.on 'mouseenter', -> 200 | pie = $el.data 'easyPieChart' 201 | pie.update 0 202 | pie.update $el.data 'progress' 203 | 204 | $el.data('easyPieChart').update $el.data 'progress' 205 | 206 | refreshPieChart = -> 207 | $detail.find('.circle .chart').each -> refreshOnePieChart $ this 208 | 209 | refreshBtnPicture = -> 210 | $detail.find('.point .btn-picture').each -> 211 | $el = $ this 212 | if 0 is $el.find('[type="file"]').length 213 | pictureUpload = new Upload() 214 | pictureUpload.assignBrowse $el[0] 215 | pictureUpload.on 'filesAdded', -> 216 | $el.addClass 'loading' 217 | pictureUpload.on 'filesSubmitted', (err) -> 218 | if err 219 | pictureUpload.flow.cancel() 220 | toastr.error err, '上传图片出错!' 221 | return $el.removeClass 'loading' 222 | pictureUpload.upload() 223 | pictureUpload.on 'fileError', (file, message) -> 224 | data = JSON.parse message 225 | pictureUpload.flow.cancel() 226 | toastr.error data.message, '上传图片出错!' 227 | $el.removeClass 'loading' 228 | pictureUpload.on 'fileSuccess', (file, message) -> 229 | pictureUpload.flow.cancel() 230 | message = JSON.parse message 231 | image = upyun.buckets['starry-images'] + message.url 232 | window.setTimeout -> 233 | $el.removeClass 'loading' 234 | $pointPicture = $el.closest('.point').find '.point-picture img' 235 | if $pointPicture.length 236 | $pointPicture.attr 'src', image 237 | else 238 | $el.closest('.point').find('.point-body').after "
" 239 | 240 | $el.next('[name="image"]').val image 241 | , 800 242 | 243 | refresh = -> 244 | refreshSection() 245 | refreshPoint() 246 | refreshPieChart() 247 | refreshBtnPicture() 248 | 249 | story = data.story 250 | points = [] 251 | 252 | for section in story.sections 253 | for point in section.points 254 | points[point.id] = point 255 | 256 | # 替换背景图 257 | $replaceBackground = $ '#replaceBackground' 258 | replaceBackgroundUpload = new Upload() 259 | replaceBackgroundUpload.assignBrowse $replaceBackground[0] 260 | replaceBackgroundUpload.on 'filesAdded', -> 261 | $replaceBackground.addClass 'loading' 262 | replaceBackgroundUpload.on 'filesSubmitted', (err) -> 263 | if err 264 | replaceBackgroundUpload.flow.cancel() 265 | toastr.error err, '上传图片出错!' 266 | return $replaceBackground.removeClass 'loading' 267 | replaceBackgroundUpload.upload() 268 | replaceBackgroundUpload.on 'fileError', (file, message) -> 269 | data = JSON.parse message 270 | replaceBackgroundUpload.flow.cancel() 271 | toastr.error data.message, '上传图片出错!' 272 | $replaceBackground.removeClass 'loading' 273 | replaceBackgroundUpload.on 'fileSuccess', (file, message) -> 274 | replaceBackgroundUpload.flow.cancel() 275 | message = JSON.parse message 276 | image = upyun.buckets['starry-images'] + message.url 277 | $.ajax 278 | url: "/api/stories/#{story.id}" 279 | type: 'PATCH' 280 | data: background: image 281 | dataType: 'json' 282 | .done -> 283 | window.setTimeout -> 284 | $replaceBackground.removeClass 'loading' 285 | $replaceBackground.closest('.section-background').css 'backgroundImage', "url(#{image}!#{adou.backgroundSuffix})" 286 | , 800 287 | .fail (res) -> 288 | error = res.responseJSON.error 289 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 290 | toastr.error errors.join('
'), '上传图片出错!' 291 | $replaceBackground.removeClass 'loading' 292 | 293 | # 上传头像 294 | $profileImage = $ '#profileImage' 295 | profileImageUpload = new Upload() 296 | profileImageUpload.assignBrowse $profileImage[0] 297 | profileImageUpload.assignDrop $profileImage[0] 298 | profileImageUpload.on 'filesAdded', -> 299 | $profileImage.closest('.profile-image').addClass 'loading' 300 | profileImageUpload.on 'filesSubmitted', (err) -> 301 | if err 302 | profileImageUpload.flow.cancel() 303 | toastr.error err, '上传图片出错!' 304 | return $profileImage.removeClass('loading').addClass 'done' 305 | profileImageUpload.upload() 306 | profileImageUpload.on 'fileError', (file, message) -> 307 | data = JSON.parse message 308 | profileImageUpload.flow.cancel() 309 | toastr.error data.message, '上传图片出错!' 310 | $profileImage.removeClass('loading').addClass 'done' 311 | profileImageUpload.on 'fileSuccess', (file, message) -> 312 | profileImageUpload.flow.cancel() 313 | message = JSON.parse message 314 | image = upyun.buckets['starry-images'] + message.url 315 | $.ajax 316 | url: "/api/stories/#{story.id}" 317 | type: 'PATCH' 318 | data: cover: image 319 | dataType: 'json' 320 | .done -> 321 | window.setTimeout -> 322 | $profileImage.removeClass('loading').addClass 'done' 323 | $profileImage.css 'backgroundImage', "url(#{image}!avatar)" 324 | , 800 325 | .fail (res) -> 326 | error = res.responseJSON.error 327 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 328 | toastr.error errors.join('
'), '上传图片出错!' 329 | $profileImage.removeClass('loading').addClass 'done' 330 | 331 | # 主题 332 | $themes = $ '#themes' 333 | $('body').attr 'class', story.theme if story.theme 334 | 335 | $themes.on 'click', 'a', (event) -> 336 | event.preventDefault() 337 | theme = $(this).data 'color' 338 | $.ajax 339 | url: "/api/stories/#{story.id}" 340 | type: 'PATCH' 341 | data: theme: theme 342 | dataType: 'json' 343 | .done -> 344 | $('body').attr 'class', theme 345 | refresh() 346 | .fail (res) -> 347 | error = res.responseJSON.error 348 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 349 | toastr.error errors.join('
'), '切换主题出错!' 350 | 351 | # 简介 352 | $profile.on 'click', '.profile .edit', (event) -> 353 | event.preventDefault() 354 | $profile.addClass('edit').find('[name="title"]').focus() 355 | 356 | $profile.on 'click', '.profile-edit .cancel', (event) -> 357 | event.preventDefault() 358 | $profile.removeClass 'edit' 359 | 360 | $profile.on 'submit', '.profile-edit', (event) -> 361 | event.preventDefault() 362 | $form = $ this 363 | $submit = $form.find 'button[type="submit"]' 364 | $submit.button 'loading' 365 | $.ajax 366 | url: "/api/stories/#{story.id}" 367 | type: 'POST' 368 | data: $form.serialize() 369 | dataType: 'json' 370 | .done (story) -> 371 | $submit.button 'reset' 372 | $profile.html components.profile story 373 | $profile.removeClass 'edit' 374 | refreshSection() 375 | .fail (res) -> 376 | $submit.button 'reset' 377 | error = res.responseJSON.error 378 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 379 | toastr.error errors.join('
'), '更新简介出错!' 380 | 381 | # 片段 382 | sections = (section.id for section in story.sections) 383 | 384 | $detail.find('.container').on 'focusin', '.section-add input', (event) -> 385 | event.preventDefault() 386 | $(this).closest('.input-group').addClass 'open' 387 | 388 | $detail.find('.container').on 'focusout', '.section-add input', (event) -> 389 | event.preventDefault() 390 | $(this).closest('.input-group').removeClass 'open' 391 | 392 | $detail.find('.container').on 'submit', '.section-add', (event) -> 393 | event.preventDefault() 394 | $form = $ this 395 | $.ajax 396 | url: "/api/stories/#{story.id}/sections" 397 | type: 'POST' 398 | data: $form.serialize() 399 | dataType: 'json' 400 | .done (section) -> 401 | $form.find('input[name="title"]').val('').blur() 402 | $form.closest('.section').after components.section section 403 | refresh() 404 | .fail (res) -> 405 | error = res.responseJSON.error 406 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 407 | toastr.error errors.join('
'), '添加片段出错!' 408 | 409 | $detail.find('.container').on 'click', '.section-title .rename', (event) -> 410 | event.preventDefault() 411 | $(this).closest('.section-title').addClass('edit').find('[name="title"]').focus() 412 | 413 | $detail.find('.container').on 'submit', '.section-rename', (event) -> 414 | event.preventDefault() 415 | $form = $ this 416 | sectionId = $form.closest('.section').data 'id' 417 | $.ajax 418 | url: "/api/sections/#{sectionId}" 419 | type: 'PATCH' 420 | data: $form.serialize() 421 | dataType: 'json' 422 | .done (section) -> 423 | $form.closest('.section-title').removeClass('edit').html components.sectionTitle section 424 | refreshSection() 425 | .fail (res) -> 426 | error = res.responseJSON.error 427 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 428 | toastr.error errors.join('
'), '重命名出错!' 429 | 430 | $detail.find('.container').on 'click', '.section-title .down', (event) -> 431 | event.preventDefault() 432 | $section = $(this).closest '.section' 433 | id = $section.data 'id' 434 | index = sections.indexOf id 435 | sections.splice index, 1 436 | sections.splice index + 1, 0, id 437 | $.ajax 438 | url: "/api/stories/#{story.id}" 439 | type: 'PATCH' 440 | data: sections: sections 441 | dataType: 'json' 442 | .done -> 443 | $section.next().after $section 444 | refresh() 445 | $('html, body').animate { scrollTop: $section.position().top }, 600 446 | .fail (res) -> 447 | error = res.responseJSON.error 448 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 449 | toastr.error errors.join('
'), '移动片段出错!' 450 | 451 | $detail.find('.container').on 'click', '.section-title .trash', (event) -> 452 | event.preventDefault() 453 | $(this).closest('.confirm').addClass('open').one 'mouseleave', -> $(this).removeClass 'open' 454 | 455 | $detail.find('.container').on 'click', '.section-title .cancel', (event) -> 456 | event.preventDefault() 457 | $(this).closest('.confirm').removeClass('open').unbind 'mouseleave' 458 | 459 | $detail.find('.container').on 'click', '.section-title .remove', (event) -> 460 | event.preventDefault() 461 | $section = $(this).closest '.section' 462 | $.ajax 463 | url: "/api/stories/#{story.id}/sections/#{$section.data('id')}" 464 | type: 'DELETE' 465 | dataType: 'json' 466 | .done -> 467 | $section.remove() 468 | refresh() 469 | .fail (res) -> 470 | error = res.responseJSON.error 471 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 472 | toastr.error errors.join('
'), '删除片段出错!' 473 | 474 | # 节点 475 | $detail.find('.container').on 'submit', '.point-add', (event) -> 476 | event.preventDefault() 477 | $form = $ this 478 | sectionId = $form.closest('.section').data 'id' 479 | $.ajax 480 | url: "/api/sections/#{sectionId}/points" 481 | type: 'POST' 482 | data: $form.serialize() 483 | dataType: 'json' 484 | .done (point) -> 485 | points[point.id] = point 486 | $beforePoint = $ components.point point 487 | $point = $form.closest '.point' 488 | $point.before $beforePoint 489 | $point.find('.point-picture').remove() 490 | $form.replaceWith components.pointAdd() 491 | refreshPoint() 492 | refreshBtnPicture() 493 | $chart = $point.find '.chart' 494 | refreshOnePieChart $chart if $chart.length 495 | .fail (res) -> 496 | error = res.responseJSON.error 497 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 498 | toastr.error errors.join('
'), '添加节点出错!' 499 | 500 | $detail.find('.container').on 'click', '.point .actions .trash', (event) -> 501 | event.preventDefault() 502 | $(this).closest('.confirm').addClass('open').one 'mouseleave', -> $(this).removeClass 'open' 503 | 504 | $detail.find('.container').on 'click', '.point .actions .cancel', (event) -> 505 | event.preventDefault() 506 | $(this).closest('.confirm').removeClass('open').unbind 'mouseleave' 507 | 508 | $detail.find('.container').on 'click', '.point .actions .remove', (event) -> 509 | event.preventDefault() 510 | $section = $(this).closest '.section' 511 | $point = $(this).closest '.point' 512 | $.ajax 513 | url: "/api/sections/#{$section.data('id')}/points/#{$point.data('id')}" 514 | type: 'DELETE' 515 | dataType: 'json' 516 | .done (point) -> 517 | delete points[point.id] 518 | $point.remove() 519 | refreshPoint() 520 | .fail (res) -> 521 | error = res.responseJSON.error 522 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 523 | toastr.error errors.join('
'), '删除节点出错!' 524 | 525 | $detail.find('.container').on 'click', '.point .actions .edit', (event) -> 526 | event.preventDefault() 527 | $point = $(this).closest '.point' 528 | $pointEdit = $ components.pointEdit points[$point.data('id')] 529 | $point.replaceWith $pointEdit 530 | oChart.as $pointEdit.find '[rel-chart="yes"]' 531 | oIconpicker.as $pointEdit.find '[rel-iconpicker="yes"]' 532 | refreshPoint() 533 | refreshBtnPicture() 534 | 535 | $detail.find('.container').on 'click', '.point .point-edit .cancel', (event) -> 536 | event.preventDefault() 537 | $pointEdit = $(this).closest '.point' 538 | $point = $ components.point points[$pointEdit.data('id')] 539 | $pointEdit.replaceWith $point 540 | $chart = $point.find '.chart' 541 | refreshOnePieChart $chart if $chart.length 542 | 543 | $detail.find('.container').on 'submit', '.point-edit', (event) -> 544 | event.preventDefault() 545 | $form = $ this 546 | pointId = $form.closest('.point').data 'id' 547 | $.ajax 548 | url: "/api/points/#{pointId}" 549 | type: 'POST' 550 | data: $form.serialize() 551 | dataType: 'json' 552 | .done (point) -> 553 | points[point.id] = point 554 | $pointEdit = $form.closest '.point' 555 | $point = $ components.point points[$pointEdit.data('id')] 556 | $pointEdit.replaceWith $point 557 | refreshPoint() 558 | $chart = $point.find '.chart' 559 | refreshOnePieChart $chart if $chart.length 560 | .fail (res) -> 561 | error = res.responseJSON.error 562 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 563 | toastr.error errors.join('
'), '更新节点出错!' 564 | 565 | refresh() 566 | $list.unbind($.support.transition.end).one $.support.transition.end, -> 567 | $list.html '' 568 | $('html, body').scrollTop 0 569 | $detail.removeClass 'change' 570 | 571 | $wrap.addClass 'bige' 572 | 573 | router = new Router() 574 | 575 | # 列表 576 | router.on '/stories\/?/?', -> 577 | if preloaded 578 | _list { stories: preloaded.stories } 579 | return preloaded = null 580 | 581 | $.ajax 582 | url: '/api/stories' 583 | type: 'GET' 584 | dataType: 'json' 585 | .done (res) -> 586 | _list { stories: res } 587 | .fail (res) -> 588 | error = res.responseJSON.error 589 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 590 | toastr.error errors.join('
'), '获取列表数据出错!' 591 | 592 | # 详情 593 | router.on '/stories/:id', (id) -> 594 | if preloaded 595 | _detail { story: preloaded.story } 596 | return preloaded = null 597 | 598 | $.ajax 599 | url: "/api/stories/#{id}" 600 | type: 'GET' 601 | dataType: 'json' 602 | .done (res) -> 603 | $detail.addClass 'change' 604 | _detail { story: res } 605 | .fail (res) -> 606 | error = res.responseJSON.error 607 | if typeof error is 'string' then errors = [error] else errors = (err.msg for err in error) 608 | toastr.error errors.join('
'), '获取详情数据出错!' 609 | 610 | router.configure html5history: true 611 | router.init() 612 | 613 | # 描点平滑滚动 614 | $detail.on 'click', 'a[href*=#]', (event) -> 615 | event.preventDefault() 616 | $target = $ '#' + @hash.slice 1 617 | $('html, body').animate { scrollTop: $target.position().top }, 600 if $target.length 618 | 619 | # 跳转 620 | $('body').on 'click', 'a.go', (event) -> 621 | event.preventDefault() 622 | router.setRoute $(event.currentTarget).attr 'href' 623 | 624 | # 退出登录 625 | $('body').on 'click', 'a.signout', (event) -> 626 | event.preventDefault() 627 | $.ajax 628 | url: '/api/signin' 629 | type: 'DELETE' 630 | dataType: 'json' 631 | .always -> 632 | window.location.href = '/' 633 | -------------------------------------------------------------------------------- /public/scripts/pages/story/share.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | toastr = require 'toastr' 3 | ZeroClipboard = require 'zero-clipboard' 4 | 5 | toastr.options.positionClass = 'toast-bottom-right' 6 | ZeroClipboard.config swfPath: '../../ZeroClipboard.swf' 7 | 8 | $ -> 9 | $qrcode = $ '#qrcode' 10 | $qrcode.qrcode 11 | width: 160 12 | height: 160 13 | text: 'http://' + $qrcode.data 'url' 14 | 15 | $copy = $ '#copy' 16 | client = new ZeroClipboard $copy[0] 17 | client.on 'aftercopy', -> 18 | toastr.success '拷贝成功!' -------------------------------------------------------------------------------- /public/scripts/templates/components/iconpicker.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#each icons}} 3 | {{this}} 4 | {{/each}} 5 |
6 | -------------------------------------------------------------------------------- /public/scripts/templates/components/logo.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/scripts/templates/components/point-add.hbs: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /public/scripts/templates/components/point-edit.hbs: -------------------------------------------------------------------------------- 1 |
2 |
编辑
3 |

{{#if image}}
{{/if}}
4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 | 取消 29 |
30 |
31 |
-------------------------------------------------------------------------------- /public/scripts/templates/components/point.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{circle bubble}}} 3 |
4 |
5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 | {{#if link}} 15 |

{{title}}

16 | {{^}} 17 |

{{title}}

18 | {{/if}} 19 |
{{{markdown description}}}
20 | {{#if image}}
{{/if}} 21 |
22 |
-------------------------------------------------------------------------------- /public/scripts/templates/components/profile.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{#if title}}{{title}}{{^}}标题{{/if}}

3 | 6 | {{#if description}}

{{{markdown description}}}

{{/if}} 7 | 修改 8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 取消 22 | 23 |
24 |
25 | 28 | -------------------------------------------------------------------------------- /public/scripts/templates/components/section-add.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | {{#if id}}{{/if}} 7 | 8 |
-------------------------------------------------------------------------------- /public/scripts/templates/components/section-navigation.hbs: -------------------------------------------------------------------------------- 1 | {{#each sections}} 2 | {{#if title}}{{title}}{{^}}片段{{/if}} 3 | {{/each}} -------------------------------------------------------------------------------- /public/scripts/templates/components/section-title.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 删除 4 | 5 | 6 |
7 | 向下移动 8 | 重命名 9 |
10 | {{#if title}}{{title}}{{^}}片段{{/if}} 11 |
12 |
13 | 14 | 15 |
16 | 17 |
-------------------------------------------------------------------------------- /public/scripts/templates/components/section.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{> section-title this}}
3 |
4 | {{#each points}} 5 | {{> point this}} 6 | {{/each}} 7 |
8 |
添加
9 | {{> point-add}} 10 |
11 | {{> section-add this}} 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /public/scripts/templates/pages/story/detail.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 返回 4 |
5 | 预览 6 | 退出 7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |
{{> profile story}}
30 | {{> section-add}} 31 |
32 |
33 | {{#each story.sections}} 34 | {{> section this}} 35 | {{/each}} 36 |
37 | -------------------------------------------------------------------------------- /public/scripts/templates/pages/story/list.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 退出 6 |
7 |
8 | 22 |
23 | -------------------------------------------------------------------------------- /public/styles/components/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | // @import 'customized-variables.less'; 3 | 4 | // Mixins 5 | @import '../../components/bootstrap/less/mixins.less'; 6 | 7 | // Reset and dependencies 8 | @import '../../components/bootstrap/less/normalize.less'; 9 | @import '../../components/bootstrap/less/print.less'; 10 | // @import '../../components/bootstrap/less/glyphicons.less'; 11 | 12 | // Core CSS 13 | @import '../../components/bootstrap/less/scaffolding.less'; 14 | @import '../../components/bootstrap/less/type.less'; 15 | // @import '../../components/bootstrap/less/code.less'; 16 | // @import '../../components/bootstrap/less/grid.less'; 17 | // @import '../../components/bootstrap/less/tables.less'; 18 | @import '../../components/bootstrap/less/forms.less'; 19 | @import '../../components/bootstrap/less/buttons.less'; 20 | 21 | // Components 22 | @import '../../components/bootstrap/less/component-animations.less'; 23 | // @import '../../components/bootstrap/less/dropdowns.less'; 24 | @import '../../components/bootstrap/less/button-groups.less'; 25 | @import '../../components/bootstrap/less/input-groups.less'; 26 | // @import '../../components/bootstrap/less/navs.less'; 27 | // @import '../../components/bootstrap/less/navbar.less'; 28 | // @import '../../components/bootstrap/less/breadcrumbs.less'; 29 | // @import '../../components/bootstrap/less/pagination.less'; 30 | // @import '../../components/bootstrap/less/pager.less'; 31 | // @import '../../components/bootstrap/less/labels.less'; 32 | // @import '../../components/bootstrap/less/badges.less'; 33 | // @import '../../components/bootstrap/less/jumbotron.less'; 34 | // @import '../../components/bootstrap/less/thumbnails.less'; 35 | // @import '../../components/bootstrap/less/alerts.less'; 36 | // @import '../../components/bootstrap/less/progress-bars.less'; 37 | // @import '../../components/bootstrap/less/media.less'; 38 | // @import '../../components/bootstrap/less/list-group.less'; 39 | // @import '../../components/bootstrap/less/panels.less'; 40 | // @import '../../components/bootstrap/less/responsive-embed.less'; 41 | // @import '../../components/bootstrap/less/wells.less'; 42 | // @import '../../components/bootstrap/less/close.less'; 43 | 44 | // Components w/ JavaScript 45 | // @import '../../components/bootstrap/less/modals.less'; 46 | @import '../../components/bootstrap/less/tooltip.less'; 47 | @import '../../components/bootstrap/less/popovers.less'; 48 | // @import '../../components/bootstrap/less/carousel.less'; 49 | 50 | // Utility classes 51 | @import '../../components/bootstrap/less/utilities.less'; 52 | @import '../../components/bootstrap/less/responsive-utilities.less'; 53 | -------------------------------------------------------------------------------- /public/styles/components/customized-variables.less: -------------------------------------------------------------------------------- 1 | @import '../../components/font-awesome/less/variables.less'; 2 | @import '../../components/bootstrap/less/variables.less'; 3 | 4 | @font-family-base: 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'Hiragino Sans GB', 'Source Han Sans CN', 'Source Han Sans SC', 'Microsoft YaHei', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif; 5 | @link-hover-decoration: none; 6 | 7 | // 颜色 8 | @starry-color-white: #fff; 9 | @starry-color-black: #484848; 10 | @starry-color-gray: #6c6e7a; 11 | @starry-color-green: #2ecc71; 12 | @starry-color-blue: #44a0c8; 13 | @starry-color-pink: #eb836e; 14 | @starry-color-red: #e85a59; 15 | 16 | // 默认颜色 17 | @starry-color-default: lighten(@starry-color-green, 5%); 18 | 19 | // 层级 20 | @default-index: 0; 21 | @line-index: 1; 22 | @base-index: 9; 23 | @bige-index: 99; 24 | @profile-index: 100; 25 | -------------------------------------------------------------------------------- /public/styles/components/font-awesome.less: -------------------------------------------------------------------------------- 1 | // @import 'customized-variables.less'; 2 | @import '../../components/font-awesome/less/mixins.less'; 3 | @import '../../components/font-awesome/less/path.less'; 4 | @import '../../components/font-awesome/less/core.less'; 5 | @import '../../components/font-awesome/less/larger.less'; 6 | @import '../../components/font-awesome/less/fixed-width.less'; 7 | @import '../../components/font-awesome/less/list.less'; 8 | @import '../../components/font-awesome/less/bordered-pulled.less'; 9 | @import '../../components/font-awesome/less/spinning.less'; 10 | @import '../../components/font-awesome/less/rotated-flipped.less'; 11 | @import '../../components/font-awesome/less/stacked.less'; 12 | @import '../../components/font-awesome/less/icons.less'; 13 | -------------------------------------------------------------------------------- /public/styles/components/section.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | .section-base(@dark-color, @light-color) { 3 | background: @light-color; 4 | 5 | a { 6 | color: @dark-color; 7 | cursor: pointer; 8 | } 9 | 10 | .line { 11 | background: @dark-color; 12 | } 13 | 14 | .section-add, .section-title .section-rename { 15 | .input-group { 16 | border-color: @dark-color; 17 | 18 | label { 19 | background: @dark-color; 20 | color: @light-color; 21 | } 22 | 23 | input { 24 | color: @dark-color; 25 | background: @light-color; 26 | } 27 | } 28 | } 29 | 30 | .profile, .section-title, .point-container { 31 | color: @dark-color; 32 | } 33 | 34 | .profile, .section-rename, .profile-edit, .point-add, .point-edit { 35 | .form-control, .bubble-icon { 36 | border-color: @dark-color; 37 | color: @dark-color; 38 | } 39 | 40 | .form-control::placeholder { 41 | color: lighten(@dark-color, 15%); 42 | } 43 | 44 | .btn { 45 | border-color: @dark-color; 46 | color: @dark-color; 47 | } 48 | 49 | .btn-link { 50 | border-color: transparent !important; 51 | } 52 | 53 | .input-group-addon { 54 | border-color: @dark-color; 55 | background-color: @dark-color; 56 | color: @light-color; 57 | } 58 | } 59 | 60 | .circle { 61 | color: @light-color; 62 | background-color: @dark-color; 63 | 64 | .chart { 65 | border-color: @dark-color; 66 | background-color: @light-color; 67 | color: @dark-color; 68 | } 69 | } 70 | 71 | .chart { 72 | border-color: @dark-color; 73 | background-color: @light-color; 74 | color: @dark-color; 75 | } 76 | 77 | .point .actions a:hover { 78 | background-color: @dark-color; 79 | color: @light-color; 80 | } 81 | 82 | .point-placeholder .point-container { 83 | border-color: @dark-color; 84 | } 85 | 86 | .point-picture .picture-point-image { 87 | border-color: @dark-color; 88 | } 89 | 90 | .section-black { 91 | background-color: @dark-color; 92 | 93 | a { 94 | color: darken(@light-color, 10%); 95 | 96 | &:hover { 97 | color: @light-color; 98 | } 99 | } 100 | 101 | .line { 102 | background: @light-color; 103 | } 104 | 105 | .section-add, .section-title .section-rename { 106 | .input-group { 107 | border-color: @light-color; 108 | 109 | label { 110 | background: @light-color; 111 | color: @dark-color; 112 | } 113 | 114 | input { 115 | color: @light-color; 116 | background: @dark-color; 117 | } 118 | } 119 | } 120 | 121 | .section-title, .point-container { 122 | color: @light-color; 123 | } 124 | 125 | .section-rename, .point-add, .point-edit { 126 | .form-control, .bubble-icon { 127 | border-color: @light-color; 128 | color: @light-color; 129 | } 130 | 131 | .form-control::placeholder { 132 | color: darken(@light-color, 15%); 133 | } 134 | 135 | .btn { 136 | border-color: @light-color; 137 | color: @light-color; 138 | } 139 | 140 | .input-group-addon { 141 | border-color: @light-color; 142 | background-color: @light-color; 143 | color: @dark-color; 144 | } 145 | } 146 | 147 | .circle { 148 | background-color: @light-color; 149 | color: @dark-color; 150 | 151 | .chart { 152 | border-color: @light-color; 153 | background-color: @dark-color; 154 | color: @light-color; 155 | } 156 | } 157 | 158 | .chart { 159 | border-color: @light-color; 160 | background-color: @dark-color; 161 | color: @light-color; 162 | } 163 | 164 | .point .actions a:hover { 165 | background-color: @light-color; 166 | color: @dark-color; 167 | } 168 | 169 | .point-placeholder .point-container { 170 | border-color: @light-color; 171 | } 172 | 173 | .point-picture .picture-point-image { 174 | border-color: @light-color; 175 | } 176 | } 177 | 178 | .profile-image .loading i { 179 | color: @dark-color; 180 | } 181 | 182 | .profile-image.done .loading i { 183 | color: @light-color; 184 | } 185 | 186 | .section-header .content { 187 | background: @light-color; 188 | } 189 | 190 | .section:last-of-type .line:before { 191 | color: @light-color; 192 | background: @dark-color; 193 | } 194 | 195 | .section.section-black:last-of-type .line:before { 196 | color: @dark-color; 197 | background: @light-color; 198 | } 199 | 200 | .section:before { 201 | background: @dark-color; 202 | } 203 | 204 | .section.section-black:before { 205 | background: @light-color; 206 | } 207 | 208 | .section:last-of-type:before { 209 | display: none; 210 | } 211 | } 212 | 213 | @keyframes loading { 214 | from { 215 | transform: rotate(0deg); 216 | } 217 | 218 | to { 219 | transform: rotate(360deg); 220 | } 221 | } 222 | 223 | .form-control { 224 | -webkit-appearance: none; 225 | } 226 | 227 | .section { 228 | position: relative; 229 | z-index: @default-index; 230 | margin-top: 0; 231 | padding: 40px 0; 232 | width: 100%; 233 | 234 | .line { 235 | position: absolute; 236 | top: -20px; 237 | bottom: 0; 238 | left: 50%; 239 | z-index: @line-index; 240 | width: 1px; 241 | } 242 | 243 | &:last-of-type { 244 | padding-bottom: 200px; 245 | 246 | .line { 247 | bottom: 100px; 248 | margin-bottom: -50px; 249 | 250 | &:before { 251 | display: block; 252 | content: ''; 253 | position: absolute; 254 | bottom: 0; 255 | z-index: @default-index; 256 | margin-left: -25px; 257 | width: 51px; 258 | height: 51px; 259 | border-radius: 50%; 260 | } 261 | } 262 | } 263 | 264 | &:before { 265 | content: ''; 266 | position: absolute; 267 | bottom: -2px; 268 | left: 50%; 269 | z-index: @default-index; 270 | margin: -15px; 271 | width: 31px; 272 | height: 31px; 273 | transform: rotate(315deg); 274 | } 275 | 276 | .top-actions { 277 | position: absolute; 278 | top: 30px; 279 | right: 30px; 280 | 281 | a { 282 | margin-left: 8px; 283 | } 284 | } 285 | 286 | .profile { 287 | padding: 0 20px; 288 | } 289 | 290 | .profile-image { 291 | position: absolute; 292 | top: -100px; 293 | left: 50%; 294 | z-index: @profile-index; 295 | margin-left: -100px; 296 | width: 200px; 297 | height: 200px; 298 | border: 2px solid darken(@starry-color-white, 1%); 299 | background-color: darken(@starry-color-white, 1%); 300 | background-position: center; 301 | background-size: cover; 302 | background-repeat: no-repeat; 303 | border-radius: 50%; 304 | } 305 | 306 | .section-title { 307 | padding-right: 20px; 308 | margin-top: -30px; 309 | padding-bottom: 30px; 310 | text-align: right; 311 | 312 | span.name { 313 | font-size: 16px; 314 | } 315 | } 316 | 317 | .point { 318 | position: relative; 319 | width: 50%; 320 | min-height: 60px; 321 | padding: 0 20px; 322 | padding-right: 50px; 323 | backface-visibility: hidden; 324 | transition: opacity 0.4s ease; 325 | 326 | &.show-picture { 327 | min-height: 300px; 328 | margin-top: 10px; 329 | margin-bottom: 40px; 330 | } 331 | 332 | &.swing-out { 333 | .opacity(0); 334 | 335 | .circle { 336 | transform: scale(0.8); 337 | } 338 | 339 | .point-body { 340 | transform: translate(0, -5px); 341 | .opacity(0); 342 | } 343 | } 344 | 345 | &.swing-in { 346 | .opacity(1); 347 | 348 | .circle { 349 | transform: scale(1); 350 | } 351 | 352 | .point-body { 353 | transform: translate(0, 0); 354 | .opacity(1); 355 | } 356 | } 357 | } 358 | 359 | .point-container { 360 | margin-left: auto; 361 | margin-right: 0; 362 | padding-top: 5px; 363 | max-width: 400px; 364 | text-align: right; 365 | 366 | &.progress, &.icon { 367 | padding-top: 10px; 368 | 369 | .point-picture { 370 | top: 10px; 371 | } 372 | } 373 | 374 | &.text { 375 | padding-top: 15px; 376 | 377 | .point-picture { 378 | top: 15px; 379 | } 380 | } 381 | 382 | h3.title { 383 | margin-top: 0; 384 | font-size: 20px; 385 | font-weight: normal; 386 | 387 | a, a:hover { 388 | color: inherit !important; 389 | text-decoration: underline; 390 | } 391 | } 392 | 393 | .point-body { 394 | margin-bottom: 0; 395 | padding-bottom: 20px; 396 | font-size: 16px; 397 | transition: all 0.4s ease; 398 | 399 | h1, h2, h3, h4, h5 { 400 | font-size: 18px; 401 | font-weight: normal; 402 | } 403 | 404 | ul, ol { 405 | margin: 0; 406 | padding: 0; 407 | } 408 | 409 | li { 410 | list-style: none; 411 | } 412 | } 413 | } 414 | 415 | .circle { 416 | position: absolute; 417 | top: 0; 418 | z-index: @base-index; 419 | right: -11px; 420 | width: 21px; 421 | height: 21px; 422 | border-radius: 50%; 423 | transition: all 0.3s ease; 424 | 425 | .chart { 426 | position: relative; 427 | width: 31px; 428 | height: 31px; 429 | border-width: 1px; 430 | border-style: solid; 431 | border-radius: 50%; 432 | 433 | canvas { 434 | position: absolute; 435 | top: -1px; 436 | left: -1px; 437 | } 438 | } 439 | 440 | .fa { 441 | width: 31px; 442 | line-height: 31px; 443 | text-align: center; 444 | font-size: 18px; 445 | color: inherit; 446 | } 447 | } 448 | 449 | .circle-general { 450 | right: -16px; 451 | width: 31px; 452 | height: 31px; 453 | } 454 | 455 | .circle-large { 456 | right: -26px; 457 | width: 51px; 458 | height: 51px; 459 | 460 | .visible { 461 | width: 51px; 462 | line-height: 51px; 463 | text-align: center; 464 | } 465 | } 466 | 467 | .point-picture { 468 | position: absolute; 469 | top: 0; 470 | bottom: 0; 471 | left: 100%; 472 | width: 90%; 473 | padding: 10px 50px; 474 | max-width: 400px; 475 | 476 | .picture-point-image { 477 | margin: 0 auto; 478 | width: 100%; 479 | background-position: center; 480 | background-size: contain; 481 | border: 5px solid white; 482 | background-color: rgba(255, 255, 255, 0.7); 483 | } 484 | } 485 | 486 | .point-right { 487 | padding-left: 50px; 488 | margin-left: 50%; 489 | 490 | .point-container { 491 | margin-left: 0; 492 | margin-right: auto; 493 | text-align: left; 494 | } 495 | 496 | .circle { 497 | right: auto; 498 | left: -10px; 499 | } 500 | 501 | .circle-general { 502 | right: auto; 503 | left: -15px; 504 | } 505 | 506 | .circle-large { 507 | right: auto; 508 | left: -25px; 509 | } 510 | 511 | .point-picture { 512 | left: auto; 513 | right: 100%; 514 | text-align: right; 515 | } 516 | } 517 | } 518 | 519 | .section-background { 520 | background-position: center; 521 | background-size: cover; 522 | background-repeat: no-repeat; 523 | } 524 | 525 | .section-header { 526 | padding-top: 100px; 527 | padding-bottom: 30px; 528 | backface-visibility: hidden; 529 | text-align: center; 530 | 531 | .content { 532 | position: relative; 533 | z-index: @base-index; 534 | margin: 30px auto; 535 | margin-top: 40px; 536 | padding-bottom: 20px; 537 | max-width: 600px; 538 | text-align: center; 539 | 540 | h1 { 541 | font-size: 32px; 542 | padding-top: 20px; 543 | padding-bottom: 0; 544 | } 545 | 546 | .link { 547 | font-size: 15px; 548 | font-family: inherit; 549 | margin-top: 0; 550 | 551 | a { 552 | text-decoration: underline; 553 | } 554 | } 555 | 556 | .nav { 557 | display: inline-block; 558 | padding: 20px 0 10px; 559 | font-size: 16px; 560 | 561 | a { 562 | margin-right: 12px; 563 | 564 | &:last-of-type { 565 | margin-right: 0; 566 | } 567 | } 568 | } 569 | } 570 | } 571 | 572 | @media (max-width: 600px) { 573 | .section-background:before { 574 | display: none; 575 | } 576 | 577 | .section-header div.line { 578 | display: none; 579 | } 580 | 581 | .section:before, .section .line { 582 | left: 40px; 583 | } 584 | 585 | .section .point { 586 | width: 85%; 587 | margin-top: 0px; 588 | margin-left: 40px; 589 | padding-left: 40px; 590 | padding-right: 40px; 591 | 592 | .point-container { 593 | margin-left: 0; 594 | margin-right: auto; 595 | text-align: left; 596 | } 597 | 598 | .circle { 599 | right: auto; 600 | left: -10px; 601 | } 602 | 603 | .circle-general { 604 | right: auto; 605 | left: -15px; 606 | } 607 | 608 | .circle-large { 609 | right: auto; 610 | left: -25px; 611 | } 612 | 613 | .point-picture { 614 | left: auto; 615 | right: 100%; 616 | text-align: left; 617 | } 618 | } 619 | 620 | .section .point-picture { 621 | position: relative; 622 | display: block; 623 | min-height: 200px; 624 | top: 0 !important; 625 | left: 0 !important; 626 | right: 0 !important; 627 | width: auto; 628 | padding-left: 0; 629 | padding-right: 10px; 630 | } 631 | } 632 | 633 | 634 | body, body.black { 635 | .section-base(@starry-color-black, @starry-color-white); 636 | } 637 | 638 | body.gray { 639 | .section-base(@starry-color-gray, @starry-color-white); 640 | } 641 | 642 | body.green { 643 | .section-base(@starry-color-green, @starry-color-white); 644 | } 645 | 646 | body.blue { 647 | .section-base(@starry-color-blue, @starry-color-white); 648 | } 649 | 650 | body.pink { 651 | .section-base(@starry-color-pink, @starry-color-white); 652 | } 653 | 654 | body.red { 655 | .section-base(@starry-color-red, @starry-color-white); 656 | } 657 | -------------------------------------------------------------------------------- /public/styles/pages/account/default.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../../components/customized-variables.less'; 3 | 4 | body { 5 | padding-top: 40px; 6 | padding-bottom: 40px; 7 | } 8 | 9 | .account { 10 | max-width: 320px; 11 | padding: 15px; 12 | margin: 0 auto; 13 | 14 | .account-header { 15 | margin-bottom: 20px; 16 | text-align: center; 17 | 18 | svg path { 19 | fill: darken(@starry-color-default, 5%); 20 | } 21 | } 22 | 23 | .account-footer { 24 | margin-top: 30px; 25 | font-size: 14px; 26 | text-align: center; 27 | } 28 | 29 | a { 30 | color: darken(@starry-color-default, 5%); 31 | } 32 | 33 | .form-control { 34 | border-width: 2px; 35 | border-radius: 0; 36 | outline: none; 37 | box-shadow: none; 38 | 39 | &:focus { 40 | border-color: darken(@starry-color-default, 5%); 41 | } 42 | } 43 | 44 | .btn { 45 | width: 100%; 46 | background-color: @starry-color-default; 47 | outline: none; 48 | border: 2px solid darken(@starry-color-default, 5%); 49 | border-radius: 0; 50 | color: #fff; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/styles/pages/base.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../components/customized-variables.less'; 3 | 4 | @import '../components/font-awesome.less'; 5 | @import '../components/bootstrap.less'; 6 | @import '../components/section.less'; 7 | -------------------------------------------------------------------------------- /public/styles/pages/default/default.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../../components/customized-variables.less'; 3 | 4 | html, body { 5 | height: 100%; 6 | } 7 | 8 | .welcome { 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | width: 100%; 13 | overflow: hidden; 14 | background-image: url(../../images/cover-1600.jpg); 15 | background-position: center; 16 | background-repeat: no-repeat; 17 | background-size: cover; 18 | text-align: center; 19 | 20 | .container { 21 | display: block; 22 | position: absolute; 23 | top: 50%; 24 | width: 100%; 25 | transform: translate3d(0,-50%,0); 26 | padding: 40px 0 160px; 27 | } 28 | 29 | .welcome-title { 30 | svg path { 31 | fill: @starry-color-white; 32 | } 33 | } 34 | 35 | .welcome-subtitle { 36 | margin: 30px 0; 37 | font-size: 36px; 38 | letter-spacing: 3px; 39 | font-family: freight-text-pro,Georgia,Cambria,Times New Roman,Times,serif; 40 | color: @starry-color-white; 41 | } 42 | 43 | .btn { 44 | outline: none; 45 | background-color: rgba(0, 0, 0, 0.4); 46 | border: 2px solid @starry-color-white; 47 | border-radius: 0; 48 | letter-spacing: 1px; 49 | color: @starry-color-white; 50 | 51 | &:hover { 52 | background-color: @starry-color-white; 53 | border-color: transparent; 54 | color: @starry-color-black; 55 | } 56 | } 57 | } 58 | 59 | @media (max-width: 600px) { 60 | .welcome { 61 | .welcome-subtitle { 62 | font-size: 24px; 63 | } 64 | } 65 | } 66 | 67 | @media (max-width: 800px) { 68 | .welcome { 69 | background-image: url(../../images/cover-800.jpg); 70 | } 71 | } 72 | 73 | @media (min-width: 800px) and (max-width: 1200px) { 74 | .welcome { 75 | background-image: url(../../images/cover-1024.jpg); 76 | } 77 | } 78 | 79 | @media (min-width: 1200px) { 80 | .welcome { 81 | background-image: url(../../images/cover-1600.jpg); 82 | } 83 | } -------------------------------------------------------------------------------- /public/styles/pages/error.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../components/customized-variables.less'; 3 | 4 | body { 5 | background-color: @starry-color-gray; 6 | } 7 | 8 | .container { 9 | margin: 70px auto 0; 10 | padding: 40px; 11 | max-width: 620px; 12 | 13 | .circle { 14 | margin: 0 auto; 15 | width: 250px; 16 | height: 250px; 17 | line-height: 250px; 18 | font-weight: 600; 19 | font-size: 40px; 20 | background: @starry-color-white; 21 | text-align: center; 22 | color: inherit; 23 | border-radius: 300px; 24 | } 25 | 26 | h2 { 27 | margin-top: 40px; 28 | margin-bottom: 40px; 29 | line-height: 1.5em; 30 | font-size: 18px; 31 | text-align: center; 32 | color: @starry-color-white; 33 | } 34 | 35 | p { 36 | text-align: center; 37 | color: @starry-color-white; 38 | } 39 | 40 | a { 41 | color: @starry-color-white; 42 | } 43 | } 44 | 45 | @media (max-width: 600px) { 46 | .container { 47 | margin-top: 0; 48 | padding: 40px 20px; 49 | } 50 | } -------------------------------------------------------------------------------- /public/styles/pages/story/default.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../../components/customized-variables.less'; 3 | @import '../../../components/bootstrap/less/mixins.less'; 4 | 5 | .container { 6 | padding-right: 15px; 7 | padding-left: 15px; 8 | margin-right: auto; 9 | margin-left: auto; 10 | } 11 | 12 | .input-group { 13 | input { 14 | height: 40px; 15 | } 16 | 17 | textarea { 18 | padding: 10px; 19 | height: 100px; 20 | resize: none; 21 | } 22 | 23 | .form-control { 24 | background-color: transparent; 25 | border-radius: 0; 26 | box-shadow: none; 27 | } 28 | 29 | .input-group-addon { 30 | padding: 6px 20px; 31 | border-radius: 0; 32 | } 33 | } 34 | 35 | .iconpicker-popover { 36 | max-width: 360px; 37 | width: 360px; 38 | background: #fff; 39 | border-color: #eee; 40 | border-radius: 0; 41 | box-shadow: none; 42 | 43 | .arrow { 44 | border-bottom-color: #eee; 45 | } 46 | 47 | .popover-content { 48 | padding: 5px; 49 | } 50 | 51 | .iconpicker { 52 | padding: 4px 9px; 53 | max-height: 200px; 54 | overflow-y: auto; 55 | 56 | a { 57 | display: block; 58 | float: left; 59 | padding: 6px; 60 | margin: 4px; 61 | width: 72px; 62 | height: 60px; 63 | text-align: center; 64 | 65 | 66 | &:hover { 67 | background-color: #e6e6e6; 68 | } 69 | 70 | i { 71 | width: 60px; 72 | color: #333; 73 | font-size: 32px; 74 | } 75 | 76 | span { 77 | display: block; 78 | width: 60px; 79 | height: 16px; 80 | color: #666; 81 | font-size: 12px; 82 | overflow: hidden; 83 | } 84 | } 85 | } 86 | } 87 | 88 | .wrap { 89 | position: relative; 90 | } 91 | 92 | .feedback { 93 | position: fixed; 94 | left: 20px; 95 | bottom: 20px; 96 | width: 36px; 97 | height: 36px; 98 | text-align: center; 99 | line-height: 36px; 100 | font-size: 20px; 101 | z-index: 1000; 102 | background: rgba(0,0,0,0.2); 103 | border-radius: 50%; 104 | color: @starry-color-white !important; 105 | 106 | i { 107 | transform: scale(-1,1); 108 | } 109 | } 110 | 111 | // 列表 112 | .list { 113 | position: absolute; 114 | left: 0; 115 | top: 0; 116 | z-index: @bige-index; 117 | width: 100%; 118 | min-height: 100%; 119 | overflow: hidden; 120 | background: @starry-color-default; 121 | transition: transform 0.5s cubic-bezier(.6,0,.4,1); 122 | transform: translate(0,0); 123 | 124 | .container { 125 | @media (min-width: @screen-sm-min) { 126 | width: @container-sm; 127 | } 128 | } 129 | 130 | .header { 131 | margin-top: 30px; 132 | .clearfix(); 133 | 134 | a { 135 | color: darken(@starry-color-white, 10%); 136 | 137 | &:hover { 138 | color: @starry-color-white; 139 | } 140 | } 141 | } 142 | 143 | .logo, .header-actions { 144 | display: block; 145 | } 146 | 147 | .logo svg path { 148 | fill: @starry-color-white; 149 | } 150 | 151 | .items { 152 | margin-bottom: 60px; 153 | } 154 | 155 | .item { 156 | position: relative; 157 | display: block; 158 | margin: 8px auto; 159 | padding: 18px 20px; 160 | width: 100%; 161 | line-height: 40px; 162 | font-size: 24px; 163 | background: @starry-color-white; 164 | color: lighten(@starry-color-black, 10%); 165 | border-radius: 4px; 166 | transition: opacity .3s, background .3s, color .3s; 167 | 168 | &:hover { 169 | background: darken(@starry-color-white, 10%); 170 | } 171 | 172 | &.add { 173 | text-align: center; 174 | .opacity(0.95); 175 | } 176 | 177 | &.fadeOut { 178 | .opacity(0.01); 179 | } 180 | 181 | .cover { 182 | display: block; 183 | margin-right: 20px; 184 | width: 40px; 185 | height: 40px; 186 | background-color: #ccc; 187 | background-position: center; 188 | background-size: cover; 189 | background-repeat: no-repeat; 190 | } 191 | 192 | .actions { 193 | position: absolute; 194 | display: block; 195 | right: 30px; 196 | top: 18px; 197 | height: 40px; 198 | 199 | span { 200 | line-height: 40px; 201 | color: darken(@starry-color-white, 20%); 202 | 203 | &:hover { 204 | color: darken(@starry-color-white, 50%); 205 | } 206 | } 207 | 208 | .remove, .cancel { 209 | display: none; 210 | } 211 | 212 | &.open { 213 | .trash { 214 | display: none; 215 | } 216 | 217 | .remove, .cancel { 218 | display: inline-block; 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | // 详情 226 | .detail { 227 | position: absolute; 228 | top: 0; 229 | right: 0; 230 | width: 100%; 231 | min-height: 100%; 232 | overflow: hidden; 233 | 234 | &.change { 235 | position: fixed; 236 | } 237 | 238 | .container { 239 | padding: 0; 240 | } 241 | 242 | .navbar-brand { 243 | position: absolute; 244 | top: 20px; 245 | left: 30px; 246 | font-size: 16px; 247 | line-height: 36px; 248 | 249 | i { 250 | font-size: 18px; 251 | } 252 | } 253 | 254 | .replace-background-button { 255 | position: absolute; 256 | right: 30px; 257 | bottom: 20px; 258 | width: 36px; 259 | line-height: 36px; 260 | text-align: center; 261 | font-size: 24px; 262 | .opacity(0.5); 263 | 264 | &:hover { 265 | .opacity(1); 266 | } 267 | 268 | .loading { 269 | font-size: 16px; 270 | display: none; 271 | transform-origin: 50% 50%; 272 | animation: loading 1s infinite linear; 273 | } 274 | 275 | .instructions { 276 | display: block; 277 | } 278 | 279 | &.loading { 280 | .loading { 281 | display: block; 282 | } 283 | 284 | .instructions { 285 | display: none; 286 | } 287 | } 288 | } 289 | 290 | .profile-image { 291 | i { 292 | display: block; 293 | text-align: center; 294 | line-height: 200px; 295 | font-size: 28px; 296 | color: darken(@starry-color-white, 1%); 297 | } 298 | 299 | .loading { 300 | position: absolute; 301 | top: 0; 302 | left: 0; 303 | right: 0; 304 | bottom: 0; 305 | font-size: 18px; 306 | display: none; 307 | transform-origin: 50% 50%; 308 | animation: loading 1s infinite linear; 309 | } 310 | 311 | .instructions { 312 | position: absolute; 313 | top: 0; 314 | left: 0; 315 | right: 0; 316 | bottom: 0; 317 | border: none; 318 | overflow: hidden; 319 | background: transparent; 320 | backface-visibility: hidden; 321 | background-color: lighten(@starry-color-black, 25%); 322 | border-radius: 50%; 323 | } 324 | 325 | &.loading { 326 | .loading { 327 | display: block; 328 | } 329 | 330 | .instructions { 331 | display: none; 332 | } 333 | } 334 | 335 | &.done { 336 | .instructions { 337 | display: none; 338 | } 339 | 340 | &:not(.loading):hover .instructions { 341 | display: block; 342 | background-color: rgba(0,0,0,.5); 343 | } 344 | } 345 | } 346 | 347 | .theme-actions { 348 | position: absolute; 349 | top: 20px; 350 | left: 10px; 351 | 352 | a { 353 | display: inline-block; 354 | margin-right: 5px; 355 | width: 24px; 356 | height: 24px; 357 | border-radius: 12px; 358 | 359 | &.black { 360 | background: @starry-color-black; 361 | } 362 | 363 | &.gray { 364 | background: @starry-color-gray; 365 | } 366 | 367 | &.green { 368 | background: @starry-color-green; 369 | } 370 | 371 | &.blue { 372 | background: @starry-color-blue; 373 | } 374 | 375 | &.pink { 376 | background: @starry-color-pink; 377 | } 378 | 379 | &.red { 380 | background: @starry-color-red; 381 | } 382 | } 383 | } 384 | 385 | .method-actions { 386 | position: absolute; 387 | top: 10px; 388 | right: 10px; 389 | 390 | a { 391 | display: inline-block; 392 | position: relative; 393 | margin-right: 10px; 394 | font-size: 18px; 395 | 396 | span { 397 | display: none; 398 | position: absolute; 399 | top: 100%; 400 | left: 50%; 401 | transform: translate(-50%, 0); 402 | font-size: 14px; 403 | white-space:nowrap; 404 | } 405 | 406 | &:hover span { 407 | display: block; 408 | } 409 | } 410 | } 411 | 412 | .profile, .profile-edit { 413 | .btn { 414 | outline: none; 415 | background-color: transparent; 416 | border-radius: 0; 417 | } 418 | } 419 | 420 | .section-title { 421 | .actions { 422 | display: inline-block; 423 | visibility: hidden; 424 | 425 | a { 426 | display: inline-block; 427 | position: relative; 428 | margin-right: 10px; 429 | font-size: 18px; 430 | color: inherit; 431 | 432 | span { 433 | display: none; 434 | position: absolute; 435 | top: 100%; 436 | left: 50%; 437 | transform: translate(-50%, 10px); 438 | font-size: 15px; 439 | white-space:nowrap; 440 | } 441 | 442 | &:hover span { 443 | display: block; 444 | } 445 | } 446 | } 447 | 448 | .section-rename { 449 | display: none; 450 | width: 240px; 451 | 452 | .input-group { 453 | border: 1px solid #fff; 454 | 455 | label, input { 456 | border: none; 457 | } 458 | } 459 | 460 | button { 461 | display: none; 462 | } 463 | } 464 | 465 | .confirm { 466 | display: inline-block; 467 | position: relative; 468 | 469 | .remove, .cancel { 470 | display: none; 471 | } 472 | 473 | &.open { 474 | .trash { 475 | display: none; 476 | } 477 | 478 | .remove { 479 | position: absolute; 480 | top: 100%; 481 | } 482 | 483 | .remove, .cancel { 484 | display: block; 485 | } 486 | } 487 | } 488 | 489 | &.edit { 490 | .actions, .name { 491 | display: none; 492 | } 493 | 494 | .section-rename { 495 | display: inline-block; 496 | } 497 | } 498 | } 499 | 500 | .section:hover .section-title .actions { 501 | visibility: visible; 502 | } 503 | 504 | .section-add { 505 | padding: 5px 20px; 506 | margin-bottom: -20px; 507 | text-align: right; 508 | 509 | .input-group { 510 | width: 120px; 511 | display: inline-block; 512 | border: 1px solid #fff; 513 | overflow: hidden; 514 | transition: width .5s ease; 515 | 516 | label { 517 | position: absolute; 518 | left: 0; 519 | padding: 0; 520 | margin: 0; 521 | width: 118px; 522 | line-height: 40px; 523 | background: none; 524 | border: none; 525 | border-radius: 0; 526 | } 527 | 528 | input { 529 | display: block; 530 | padding: 10px; 531 | margin-left: 120px; 532 | height: 40px; 533 | width: ~'calc(100% - 120px)'; 534 | line-height: 1em; 535 | background: none; 536 | border: none; 537 | border-radius: 0; 538 | 539 | &::placeholder { 540 | color: inherit; 541 | } 542 | } 543 | } 544 | 545 | button { 546 | display: none; 547 | } 548 | } 549 | 550 | .section-add .input-group { 551 | &:hover, &.open { 552 | width: 300px; 553 | } 554 | } 555 | 556 | .section-header .content, .point-add, .point-edit { 557 | .profile-edit { 558 | display: none; 559 | padding: 20px; 560 | } 561 | 562 | .input-group { 563 | width: 100%; 564 | margin-bottom: 10px; 565 | } 566 | 567 | &.edit { 568 | .profile { 569 | display: none; 570 | } 571 | 572 | .profile-edit { 573 | display: block; 574 | } 575 | } 576 | } 577 | 578 | .point { 579 | &.show-picture { 580 | margin-bottom: 60px; 581 | } 582 | 583 | &.point-data .circle { 584 | cursor: move; 585 | } 586 | 587 | &:hover { 588 | .actions { 589 | display: block; 590 | } 591 | } 592 | 593 | .top { 594 | position: relative; 595 | margin-top: -10px; 596 | width: 100%; 597 | height: 10px; 598 | } 599 | 600 | .actions { 601 | position: absolute; 602 | display: none; 603 | bottom: 100%; 604 | right: 0; 605 | border-width: 1px; 606 | border-style: solid; 607 | font-size: 0; 608 | 609 | a { 610 | display: inline-block; 611 | width: 36px; 612 | height: 36px; 613 | line-height: 36px; 614 | text-align: center; 615 | font-size: 16px; 616 | } 617 | 618 | .confirm { 619 | display: inline-block; 620 | } 621 | 622 | .remove, .cancel { 623 | display: none; 624 | } 625 | 626 | .confirm.open { 627 | .trash { 628 | display: none; 629 | } 630 | 631 | .remove, .cancel { 632 | display: inline-block; 633 | } 634 | } 635 | } 636 | 637 | .btn-picture { 638 | .loading { 639 | width: 15px; 640 | display: none; 641 | transform-origin: 50% 50%; 642 | animation: loading 1s infinite linear; 643 | } 644 | 645 | .instructions { 646 | display: block; 647 | } 648 | 649 | &.loading { 650 | .loading { 651 | display: block; 652 | } 653 | 654 | .instructions { 655 | display: none; 656 | } 657 | } 658 | } 659 | } 660 | 661 | .point-placeholder .point-container { 662 | height: 100%; 663 | border: 1px dashed #000; 664 | } 665 | 666 | .point-right + .section-add { 667 | text-align: left; 668 | } 669 | 670 | .point-add, .point-edit { 671 | margin-left: auto; 672 | margin-right: 0; 673 | padding-top: 0; 674 | max-width: 400px; 675 | 676 | .btn { 677 | outline: none; 678 | background-color: transparent; 679 | border: 1px solid #fff; 680 | border-radius: 0; 681 | } 682 | 683 | .input-group { 684 | position: relative; 685 | } 686 | 687 | .bubble-icon { 688 | position: absolute; 689 | right: 0; 690 | width: 34px; 691 | line-height: 34px; 692 | font-size: 20px; 693 | text-align: center; 694 | } 695 | 696 | .chart.open { 697 | position: absolute; 698 | right: 7px; 699 | top: 7px; 700 | width: 20px; 701 | height: 20px; 702 | line-height: 18px; 703 | border-width: 1px; 704 | border-style: solid; 705 | border-radius: 50%; 706 | } 707 | } 708 | 709 | .point-right { 710 | .point-add, .point-edit { 711 | margin-left: 0; 712 | margin-right: auto; 713 | } 714 | 715 | .actions { 716 | left: 0; 717 | right: auto; 718 | } 719 | } 720 | } 721 | 722 | .bige .list { 723 | transform: translate(0,-100%); 724 | } 725 | 726 | @media (max-width: 600px) { 727 | .feedback { 728 | display: none; 729 | } 730 | 731 | .detail { 732 | .theme-actions { 733 | position: relative; 734 | display: inline-block; 735 | padding: 0; 736 | text-align: center; 737 | } 738 | 739 | .section-add .input-group { 740 | width: 100% !important; 741 | margin-top: 20px; 742 | } 743 | 744 | .section .point { 745 | margin-top: 50px; 746 | 747 | .point-add, .point-edit { 748 | margin-left: 0; 749 | margin-right: auto; 750 | } 751 | 752 | .actions { 753 | left: 0; 754 | right: auto; 755 | } 756 | } 757 | } 758 | } 759 | 760 | .touch .feedback { 761 | display: none; 762 | } 763 | 764 | .touch .detail { 765 | .method-actions { 766 | display: none; 767 | } 768 | 769 | .section-add .input-group { 770 | width: 100% !important; 771 | margin-top: 20px; 772 | } 773 | 774 | .section-title .actions { 775 | visibility: visible; 776 | 777 | a:hover span { 778 | display: none; 779 | } 780 | } 781 | 782 | .section .point { 783 | margin-top: 50px; 784 | 785 | .actions { 786 | display: block; 787 | } 788 | } 789 | } 790 | 791 | 792 | 793 | 794 | -------------------------------------------------------------------------------- /public/styles/pages/story/share.less: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | @import '../../components/customized-variables.less'; 3 | 4 | html { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | position: relative; 10 | min-width: 800px; 11 | height: 100%; 12 | min-height: 600px; 13 | background: @starry-color-default; 14 | } 15 | 16 | .share { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | text-align: center; 23 | 24 | &:before { 25 | content: ''; 26 | display: inline-block; 27 | height: 100%; 28 | vertical-align: middle; 29 | } 30 | } 31 | 32 | .container { 33 | display: inline-block; 34 | margin: 20px 0 60px; 35 | vertical-align: middle; 36 | 37 | .box { 38 | width: 700px; 39 | height: 600px; 40 | margin: 0 auto; 41 | } 42 | 43 | .demo { 44 | float: left; 45 | width: 396px; 46 | height: 782px; 47 | padding: 90px 24px 95px; 48 | background-image: url(../../images/iphone.png); 49 | background-position: center; 50 | background-size: cover; 51 | background-repeat: no-repeat; 52 | transform: scale(0.8); 53 | 54 | iframe { 55 | height: 100%; 56 | width: 100%; 57 | border: 1px solid #eee; 58 | } 59 | } 60 | 61 | .info { 62 | float: left; 63 | margin-left: 30px; 64 | padding-top: 100px; 65 | width: 250px; 66 | 67 | .qrcode { 68 | canvas { 69 | padding: 10px; 70 | background-color: @starry-color-white; 71 | border-radius: 2px; 72 | } 73 | } 74 | 75 | p { 76 | margin: 20px 0; 77 | font-size: 16px; 78 | color: @starry-color-white; 79 | } 80 | 81 | .btn { 82 | outline: none; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Starry 2 | 3 | 完成一个故事 4 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | app = require './index' 2 | port = process.env.APP_PORT || 3000 3 | 4 | app.listen port, -> 5 | console.log "Starry application listening on port #{port}" 6 | 7 | # 容错处理 8 | process.on 'uncaughtException', (exc) -> console.error exc if 'production' is process.env.NODE_ENV 9 | -------------------------------------------------------------------------------- /test/app/apis/default.coffee: -------------------------------------------------------------------------------- 1 | request = require 'supertest' 2 | 3 | # 默认 4 | # Post:/api/upyun_token -- 获取又拍云 Token 信息 5 | # Get:/api/me -- 获取个人信息 6 | # Post:/api/signin -- 登录 7 | # Delete:/api/signin -- 退出 8 | # Post:/api/signup -- 注册 9 | # Post:/api/forgot -- 找回密码 10 | 11 | describe 'Api --> default controller', -> 12 | agent = request.agent require '../../../index' 13 | cookies = '' 14 | 15 | # 获取 Cookie 16 | before (done) -> 17 | agent.post('/api/signin').send 18 | email: 'test@test.com' 19 | password: 'test' 20 | .end (err, res) -> 21 | return done err if err 22 | cookies = res.headers['set-cookie'].pop().split(';')[0] 23 | done() 24 | 25 | it 'Post:/api/signup -- 注册', (done) -> 26 | agent.post('/api/signup').send 27 | name: '测试2' 28 | email: 'test2@test.com' 29 | password: 'test' 30 | .expect(201).end (err, res) -> 31 | return done err if err 32 | done() 33 | 34 | it 'Get:/api/me -- 获取个人信息', (done) -> 35 | req = agent.get '/api/me' 36 | req.cookies = cookies 37 | 38 | req.expect(200).end (err, res) -> 39 | return done err if err 40 | done() 41 | 42 | it 'Post:/api/upyun_token -- 获取又拍云 Token 信息', (done) -> 43 | req = agent.post('/api/upyun_token').send 44 | bucket: 'starry-images' 45 | expiration: parseInt (new Date().getTime() + 600000)/1000, 10 46 | 'save-key': '/{year}{mon}/{day}/{filemd5}-{random}{.suffix}' 47 | req.cookies = cookies 48 | 49 | req.expect(200).end (err, res) -> 50 | return done err if err 51 | done() 52 | -------------------------------------------------------------------------------- /test/app/apis/point.coffee: -------------------------------------------------------------------------------- 1 | request = require 'supertest' 2 | 3 | # 节点 4 | # Post:/api/points/:id -- 更新节点 5 | 6 | describe 'Api --> point controller', -> 7 | agent = request.agent require '../../../index' 8 | cookies = '' 9 | 10 | # 获取 Cookie 11 | before (done) -> 12 | agent.post('/api/signin').send 13 | email: 'test@test.com' 14 | password: 'test' 15 | .end (err, res) -> 16 | return done err if err 17 | cookies = res.headers['set-cookie'].pop().split(';')[0] 18 | done() 19 | 20 | it 'Post:/api/points/:id -- 更新节点', (done) -> 21 | req = agent.post('/api/points/547ece8914a36a28576aa237').send 22 | title: '标题' 23 | req.cookies = cookies 24 | 25 | req.expect(202).end (err, res) -> 26 | return done err if err 27 | done() -------------------------------------------------------------------------------- /test/app/apis/section.coffee: -------------------------------------------------------------------------------- 1 | request = require 'supertest' 2 | 3 | # 片段 4 | # Patch:/api/sections/:id -- 更新片段 5 | # Post:/api/sections/:id/points -- 新建故事节点 6 | # Delete:/api/sections/:id/points/:id -- 删除故事节点 7 | 8 | describe 'Api --> section controller', -> 9 | agent = request.agent require '../../../index' 10 | cookies = '' 11 | 12 | # 获取 Cookie 13 | before (done) -> 14 | agent.post('/api/signin').send 15 | email: 'test@test.com' 16 | password: 'test' 17 | .end (err, res) -> 18 | return done err if err 19 | cookies = res.headers['set-cookie'].pop().split(';')[0] 20 | done() 21 | 22 | it 'Patch:/api/sections/:id -- 更新片段', (done) -> 23 | req = agent.patch('/api/sections/546ed951f517a1589e49748d').send 24 | title: '标题' 25 | req.cookies = cookies 26 | 27 | req.expect(202).end (err, res) -> 28 | return done err if err 29 | done() 30 | 31 | it 'Post:/api/sections/:id/points -- 新建故事节点', (done) -> 32 | req = agent.post('/api/sections/546ed951f517a1589e49748d/points').send 33 | title: '标题' 34 | req.cookies = cookies 35 | 36 | req.expect(201).end (err, res) -> 37 | return done err if err 38 | done() 39 | 40 | it 'Delete:/api/sections/:id/points/:id -- 删除故事节点', (done) -> 41 | req = agent.delete '/api/sections/546ed951f517a1589e49748d/points/547dda656f9f160000c53feb' 42 | req.cookies = cookies 43 | 44 | req.expect(202).end (err, res) -> 45 | return done err if err 46 | done() 47 | -------------------------------------------------------------------------------- /test/app/apis/story.coffee: -------------------------------------------------------------------------------- 1 | request = require 'supertest' 2 | 3 | # 故事 4 | # Post:/api/stories -- 新建故事 5 | # Delete:/api/stories/:id -- 删除故事 6 | # Patch:/api/stories/:id -- 更新故事 7 | # Post:/api/stories/:id -- 更新故事简介 8 | # Post:/api/stories/:id/sections -- 新建故事片段 9 | # Delete:/api/stories/:id/sections/:id -- 删除故事片段 10 | # Get:/api/stories -- 获取故事列表 11 | # Get:/api/stories/:id -- 获取故事详情 12 | 13 | describe 'Api --> story controller', -> 14 | agent = request.agent require '../../../index' 15 | cookies = '' 16 | 17 | # 获取 Cookie 18 | before (done) -> 19 | agent.post('/api/signin').send 20 | email: 'test@test.com' 21 | password: 'test' 22 | .end (err, res) -> 23 | return done err if err 24 | cookies = res.headers['set-cookie'].pop().split(';')[0] 25 | done() 26 | 27 | it 'Post:/api/stories -- 新建故事', (done) -> 28 | req = agent.post '/api/stories' 29 | req.cookies = cookies 30 | 31 | req.expect(201).end (err, res) -> 32 | return done err if err 33 | done() 34 | 35 | it 'Delete:/api/stories/:id -- 删除故事', (done) -> 36 | req = agent.delete '/api/stories/546d84668bcbbc00005bcd5d' 37 | req.cookies = cookies 38 | 39 | req.expect(202).end (err, res) -> 40 | return done err if err 41 | done() 42 | 43 | it 'Patch:/api/stories/:id -- 更新故事', (done) -> 44 | req = agent.patch('/api/stories/5468c9fa3faec100000e23a8').send 45 | background: 'http://starry-images.b0.upaiyun.com/201411/19/c1cc2b44e06ee12e783f60a81519bd86-41af1ae0f868005d.jpg' 46 | req.cookies = cookies 47 | 48 | req.expect(202).end (err, res) -> 49 | return done err if err 50 | done() 51 | 52 | it 'Post:/api/stories/:id -- 更新故事简介', (done) -> 53 | req = agent.post('/api/stories/5468c9fa3faec100000e23a8').send 54 | title: '故事标题' 55 | mark: 'url' 56 | req.cookies = cookies 57 | 58 | req.expect(202).end (err, res) -> 59 | return done err if err 60 | done() 61 | 62 | it 'Post:/api/stories/:id/sections -- 新建故事片段', (done) -> 63 | req = agent.post('/api/stories/5468c9fa3faec100000e23a8/sections').send 64 | title: '名称' 65 | req.cookies = cookies 66 | 67 | req.expect(201).end (err, res) -> 68 | return done err if err 69 | done() 70 | 71 | it 'Delete:/api/stories/:id/sections/:id -- 删除故事片段', (done) -> 72 | req = agent.delete '/api/stories/5468c9fa3faec100000e23a8/sections/546ed951f517a1589e49748d' 73 | req.cookies = cookies 74 | 75 | req.expect(202).end (err, res) -> 76 | return done err if err 77 | done() 78 | 79 | it 'Get:/api/stories -- 获取故事列表', (done) -> 80 | req = agent.get '/api/stories' 81 | req.cookies = cookies 82 | 83 | req.expect(200).end (err, res) -> 84 | return done err if err 85 | done() 86 | 87 | it 'Get:/api/stories/:id -- 获取故事详情', (done) -> 88 | req = agent.get '/api/stories/5468c9fa3faec100000e23a8' 89 | req.cookies = cookies 90 | 91 | req.expect(200).end (err, res) -> 92 | return done err if err 93 | done() 94 | -------------------------------------------------------------------------------- /test/fixtures/points.json: -------------------------------------------------------------------------------- 1 | { "_id": { "$oid" : "547dda656f9f160000c53feb" }, "title": "Xxxx" } 2 | { "_id": { "$oid" : "547ece8914a36a28576aa237" }, "title": "Xxxx" } -------------------------------------------------------------------------------- /test/fixtures/sections.json: -------------------------------------------------------------------------------- 1 | { "_id": { "$oid" : "546ed951f517a1589e49748d" }, "title": "", "points": [ { "$oid" : "547dda656f9f160000c53feb" }, { "$oid" : "547ece8914a36a28576aa237" } ] } 2 | { "_id": { "$oid" : "546ed9aa3d94b6029fe9f918" }, "title": "", "points": [] } -------------------------------------------------------------------------------- /test/fixtures/stories.json: -------------------------------------------------------------------------------- 1 | { "_id": { "$oid" : "5468c9fa3faec100000e23a8" }, "author": { "$oid" : "54509f4396fe3748940c586a" }, "title": "", "mark": "", "description": "", "background": "", "cover": "", "theme": "black", "sections": [ { "$oid" : "546ed951f517a1589e49748d" } ] } 2 | { "_id": { "$oid" : "546d84668bcbbc00005bcd5d" }, "author": { "$oid" : "54509f4396fe3748940c586a" }, "title": "", "mark": "", "description": "", "background": "", "cover": "", "theme": "black", "sections": [ { "$oid" : "546ed9aa3d94b6029fe9f918" } ] } 3 | -------------------------------------------------------------------------------- /test/fixtures/users.json: -------------------------------------------------------------------------------- 1 | { "_id" : { "$oid" : "54509f4396fe3748940c586a" }, "name" : "测试", "email" : "test@test.com", "salt" : "152731070325", "hashed_password" : "a2b5061f9fb6460c9a4b61d4a2e4302b15ca3157", "active" : true, "verify_code" : "" } 2 | { "_id" : { "$oid" : "54509f44ea2a7e4c94b7a1cb" }, "name" : "测试1", "email" : "test1@test.com", "salt" : "152731070325", "hashed_password" : "a2b5061f9fb6460c9a4b61d4a2e4302b15ca3157", "active" : true, "verify_code" : "" } 3 | -------------------------------------------------------------------------------- /views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 页面不存在 - {{app.title}} 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
404
15 |
16 |

抱歉,你访问的页面不存在。

17 |

返回首页

18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /views/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 500 服务器出错 - {{app.title}} 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
500
15 |
16 |

服务器出错了。我们会尽快修复,请您稍后再试。

17 |

返回首页

18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /views/account/forgot.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block sub_title %}找回密码{% endblock %} 5 | 6 | {# 样式 #} 7 | {% block sub_styles %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {# 内容 #} 14 | {% block content %} 15 |
16 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /views/account/signin.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block sub_title %}登录{% endblock %} 5 | 6 | {# 样式 #} 7 | {% block sub_styles %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {# 脚本 #} 14 | {% block sub_scripts %} 15 | 16 | 17 | 18 | {% endblock %} 19 | 20 | {# 内容 #} 21 | {% block content %} 22 |
23 | 36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /views/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block sub_title %}注册{% endblock %} 5 | 6 | {# 样式 #} 7 | {% block sub_styles %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {# 脚本 #} 14 | {% block sub_scripts %} 15 | 16 | 17 | 18 | {% endblock %} 19 | 20 | {# 内容 #} 21 | {% block content %} 22 |
23 | 39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /views/default/default.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block title %}{{app.title}} - {{app.description}}{% endblock %} 5 | 6 | {# 头 #} 7 | {% block heads %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {# 样式 #} 13 | {% block sub_styles %} 14 | 15 | 16 | 17 | {% endblock %} 18 | 19 | {# 内容 #} 20 | {% block content %} 21 |
22 |
23 |

24 |

Your job, Your life.

25 |

立即免费使用

26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /views/default/show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{story.title|default('标题')}} - {{app.title}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | {% if story.background %} 19 |
20 | {% else %} 21 |
22 | {% endif %} 23 |
24 | {% if story.cover %} 25 |
26 | {% else %} 27 |
28 | {% endif %} 29 |
30 |
31 |
32 |

{{story.title|default('标题')}}

33 | 36 | {% if story.description %}

{{story.description|markdown|safe}}

{% endif %} 37 | 42 |
43 |
44 |
45 |
46 | {% for section in story.sections %} 47 |
48 |
{{section.title|default('片段')}}
49 |
50 | {% for point in section.points %} 51 |
52 | {{point.bubble|circle|safe}} 53 |
54 | {% if point.link %} 55 |

{{point.title}}

56 | {% else %} 57 |

{{point.title}}

58 | {% endif %} 59 |
{{point.description|markdown|safe}}
60 | {% if point.image %}
{% endif %} 61 |
62 |
63 | {% endfor %} 64 |
65 |
66 |
67 | {% endfor %} 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% block sub_title %}{% endblock %} - {{app.title}}{% endblock %} 7 | {% block heads %}{% endblock %} 8 | 9 | {% block styles %} 10 | 11 | 12 | 13 | 14 | {% block sub_styles %}{% endblock %} 15 | {% endblock %} 16 | 17 | 18 | 19 | 20 | 21 | {% block content %}{% endblock %} 22 | {% block scripts %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% block sub_scripts %}{% endblock %} 36 | {% endblock %} 37 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /views/story/default.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block title %}{{app.title}} - {{app.description}}{% endblock %} 5 | 6 | {# 样式 #} 7 | {% block sub_styles %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {# 脚本 #} 14 | {% block sub_scripts %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% endblock %} 25 | 26 | {# 内容 #} 27 | {% block content %} 28 |
29 |
30 |
31 | 32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /views/story/share.html: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html' %} 2 | 3 | {# 标题 #} 4 | {% block sub_title %}分享故事{% endblock %} 5 | 6 | {# 样式 #} 7 | {% block sub_styles %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {# 脚本 #} 14 | {% block sub_scripts %} 15 | 16 | 17 | 18 | 19 | 20 | {% endblock %} 21 | 22 | {# 内容 #} 23 | {% block content %} 24 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: koding/base@0.0.14 2 | services: 3 | - wercker/redis@1.0.1 4 | - wercker/mongodb@1.0.1 5 | build: 6 | steps: 7 | - npm-install 8 | - npm-test 9 | --------------------------------------------------------------------------------