├── .bowerrc ├── .gitignore ├── public ├── upload │ ├── 1511779737585.png │ ├── 1511779755748.png │ ├── 1511780403733.png │ ├── 1511780414412.png │ ├── 1511781273516.png │ └── 1511781284958.png └── js │ ├── detail.js │ └── admin.js ├── .vscode └── settings.json ├── app ├── models │ ├── user.js │ ├── movie.js │ ├── comment.js │ └── category.js ├── views │ ├── includes │ │ ├── head.pug │ │ └── header.pug │ ├── layout.pug │ └── pages │ │ ├── user │ │ ├── signin.pug │ │ └── signup.pug │ │ ├── admin │ │ ├── category_add.pug │ │ ├── category_list.pug │ │ ├── user_list.pug │ │ ├── movie_list.pug │ │ └── movie_add.pug │ │ ├── index.pug │ │ └── movie │ │ ├── results.pug │ │ └── detail.pug ├── routes │ ├── movie.js │ ├── user.js │ ├── index.js │ └── admin.js ├── middleware │ └── permission.js ├── controllers │ ├── comment.js │ ├── user.js │ ├── index.js │ ├── category.js │ └── movie.js └── schemas │ ├── category.js │ ├── comment.js │ ├── movie.js │ └── user.js ├── config ├── index.js └── routes.js ├── .editorconfig ├── .eslintrc.js ├── bower.json ├── package.json ├── gruntfile.js ├── app.js └── README.md /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/libs" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | public/libs/ -------------------------------------------------------------------------------- /public/upload/1511779737585.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511779737585.png -------------------------------------------------------------------------------- /public/upload/1511779755748.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511779755748.png -------------------------------------------------------------------------------- /public/upload/1511780403733.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511780403733.png -------------------------------------------------------------------------------- /public/upload/1511780414412.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511780414412.png -------------------------------------------------------------------------------- /public/upload/1511781273516.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511781273516.png -------------------------------------------------------------------------------- /public/upload/1511781284958.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savoygu/koajs-movie/HEAD/public/upload/1511781284958.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 3 | "typescript.validate.enable": false, 4 | "javascript.validate.enable": false 5 | } 6 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const UserSchema = require('../schemas/user') 3 | const User = mongoose.model('User', UserSchema) 4 | 5 | module.exports = User 6 | -------------------------------------------------------------------------------- /app/models/movie.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const MovieSchema = require('../schemas/movie') 3 | const Movie = mongoose.model('Movie', MovieSchema) 4 | 5 | module.exports = Movie 6 | -------------------------------------------------------------------------------- /app/models/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const CommentSchema = require('../schemas/comment') 3 | const Comment = mongoose.model('Comment', CommentSchema) 4 | 5 | module.exports = Comment 6 | -------------------------------------------------------------------------------- /app/views/includes/head.pug: -------------------------------------------------------------------------------- 1 | link(href="/libs/bootstrap/dist/css/bootstrap.css", rel="stylesheet") 2 | script(src="/libs/jquery/dist/jquery.min.js") 3 | script(src="/libs/bootstrap/dist/js/bootstrap.min.js") 4 | -------------------------------------------------------------------------------- /app/routes/movie.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | const Movie = require('../controllers/movie') 3 | 4 | // 电影 5 | router.get('/:id', Movie.detail) // 电影详情 6 | 7 | module.exports = router.routes() 8 | -------------------------------------------------------------------------------- /app/models/category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const CategorySchema = require('../schemas/category') 3 | const Category = mongoose.model('Category', CategorySchema) 4 | 5 | module.exports = Category 6 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | appName: 'movie-koa2', 3 | host: 'localhost', 4 | port: 3000, 5 | database: { 6 | host: 'localhost', 7 | db: 'movie' 8 | } 9 | } 10 | 11 | module.exports = config 12 | -------------------------------------------------------------------------------- /app/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | head 4 | meta(charset="utf-8") 5 | title #{title} 6 | include includes/head 7 | body 8 | include includes/header 9 | block content 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 🎨 editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | globals: { 8 | $: 'readonly' 9 | }, 10 | ignorePatterns: ['node_modules/', 'public/libs/'], 11 | extends: [ 12 | 'standard' 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 13 16 | }, 17 | rules: { 18 | 'n/handle-callback-err': 'off' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie", 3 | "authors": [ 4 | "savoygu " 5 | ], 6 | "description": "", 7 | "main": "", 8 | "license": "MIT", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "public/libs", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "bootstrap": "^3.3.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/middleware/permission.js: -------------------------------------------------------------------------------- 1 | // 管理员 2 | exports.signinRequired = async function (ctx, next) { 3 | const user = ctx.session.user 4 | 5 | if (!user) { 6 | return ctx.redirect('/signin') 7 | } 8 | 9 | await next() 10 | } 11 | 12 | // 管理员 13 | exports.adminRequired = async function (ctx, next) { 14 | const user = ctx.session.user 15 | 16 | if (user.role <= 10) { 17 | return ctx.redirect('/signin') 18 | } 19 | 20 | await next() 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/user.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | const Permission = require('../middleware/permission') 3 | const User = require('../controllers/user') 4 | const Comment = require('../controllers/comment') 5 | 6 | // 用户 7 | router.post('/signup', User.signup) // 用户注册 8 | router.post('/signin', User.signin) // 用户登录 9 | 10 | // 评论 11 | router.post('/comment', Permission.signinRequired, Comment.save) // 新增评论 12 | 13 | module.exports = router.routes() 14 | -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | 3 | const Index = require('../controllers/index') 4 | const User = require('../controllers/user') 5 | 6 | // 首页 7 | router.get('/', Index.index) // 电影首页 8 | 9 | // 用户 10 | router.get('signin', User.showSignin) // 登录页面 11 | router.get('signup', User.showSignup) // 注册页面 12 | router.get('logout', User.logout) // 登出 13 | 14 | router.get('results', Index.search) // 电影分类列表 15 | 16 | module.exports = router.routes() 17 | -------------------------------------------------------------------------------- /app/views/pages/user/signin.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | .col-md-5 7 | form(method='post', action='/user/signin') 8 | .form-group 9 | label(for='signinName') 用户名 10 | input#signinName.form-control(name='user[name]', type='text') 11 | .form-group 12 | label(for='signinPassword') 密码 13 | input#signinPassword.form-control(name='user[password]', type='text') 14 | button.btn.btn-default(type='button', data-dismiss='modal') 关闭 15 | button.btn.btn-success(type='submit') 提交 -------------------------------------------------------------------------------- /app/views/pages/user/signup.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | .col-md-5 7 | form(method='post', action='/user/signup') 8 | .form-group 9 | label(for='signupName') 用户名 10 | input#signupName.form-control(name='user[name]', type='text') 11 | .form-group 12 | label(for='signupPassword') 密码 13 | input#signupPassword.form-control(name='user[password]', type='text') 14 | button.btn.btn-default(type='button', data-dismiss='modal') 关闭 15 | button.btn.btn-success(type='submit') 提交 -------------------------------------------------------------------------------- /app/views/pages/admin/category_add.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | form.form-horizontal(method="post", action="/admin/category") 7 | if category._id 8 | input(type="hidden", name="category[_id]", value=category._id) 9 | .form-group 10 | label.col-sm-3.control-label(for="inputCategory") 电影分类 11 | .col-sm-9 12 | input#inputCategory.form-control(type="text", name="category[name]", value=category.name) 13 | .form-group 14 | .col-sm-offset-3.col-sm-9 15 | button.btn.btn-default(type="submit") 录入 -------------------------------------------------------------------------------- /app/controllers/comment.js: -------------------------------------------------------------------------------- 1 | const Comment = require('../models/comment') 2 | 3 | // 新增电影 / 更新电影 4 | exports.save = async function (ctx) { 5 | const _comment = ctx.request.body.comment 6 | const movieId = _comment.movie 7 | let comment 8 | 9 | try { 10 | if (_comment.cid) { 11 | comment = await Comment.findById(_comment.cid) 12 | const reply = { 13 | from: _comment.from, 14 | to: _comment.tid, 15 | content: _comment.content 16 | } 17 | 18 | comment.reply.push(reply) 19 | 20 | await comment.save() 21 | ctx.redirect('/movie/' + movieId) 22 | } else { 23 | comment = new Comment(_comment) 24 | 25 | await comment.save() 26 | ctx.redirect('/movie/' + movieId) 27 | } 28 | } catch (e) { 29 | console.log(e) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/js/detail.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $('.comment').click(function (e) { 3 | const target = $(this) 4 | const toId = target.data('tid') 5 | const commentId = target.data('cid') 6 | console.log(target) 7 | console.log(commentId) 8 | 9 | if ($('#toId').length > 0) { 10 | $('#toId').val(toId) 11 | } else { 12 | $('').attr({ 13 | type: 'hidden', 14 | id: 'toId', 15 | name: 'comment[tid]', 16 | value: toId 17 | }).appendTo('#commentForm') 18 | } 19 | 20 | if ($('#commentId').length > 0) { 21 | $('#commentId').val(commentId) 22 | } else { 23 | $('').attr({ 24 | type: 'hidden', 25 | id: 'commentId', 26 | name: 'comment[cid]', 27 | value: commentId 28 | }).appendTo('#commentForm') 29 | } 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /app/schemas/category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = mongoose.Schema 3 | const ObjectId = Schema.Types.ObjectId 4 | 5 | const CategorySchema = new Schema({ 6 | name: String, 7 | movies: [{ type: ObjectId, ref: 'Movie' }], 8 | meta: { 9 | createAt: { 10 | type: Date, 11 | default: Date.now() 12 | }, 13 | updateAt: { 14 | type: Date, 15 | default: Date.now() 16 | } 17 | } 18 | }) 19 | 20 | CategorySchema.pre('save', function (next) { 21 | if (this.isNew) { 22 | this.meta.createAt = this.meta.updateAt = Date.now() 23 | } else { 24 | this.meta.updateAt = Date.now() 25 | } 26 | 27 | next() 28 | }) 29 | 30 | CategorySchema.statics = { 31 | fetch: function () { 32 | return this 33 | .find({}) 34 | .sort('meta.updateAt') 35 | .exec() 36 | }, 37 | findById: function (id) { 38 | return this 39 | .findOne({ _id: id }) 40 | .exec() 41 | } 42 | } 43 | 44 | module.exports = CategorySchema 45 | -------------------------------------------------------------------------------- /app/views/pages/admin/category_list.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | table.table.table-hover.table-bordered 7 | thead 8 | tr 9 | th 电影分类 10 | th 录入时间 11 | th 查看 12 | th 更新 13 | th 删除 14 | tbody 15 | each item in categories 16 | tr(class='item-id-' + item._id) 17 | td #{item.name} 18 | td #{moment(item.meta.createAt).format('MM/DD/YYYY')} 19 | td: a(target='_blank', href='/admin/category/' + item._id) 查看 20 | td: a(target='_blank', href='/admin/category/update/' + item._id) 修改 21 | td 22 | button.btn.btn-danger.js-category-del(type='button', data-id=item._id) 删除 23 | script(src='/js/admin.js') -------------------------------------------------------------------------------- /app/views/pages/admin/user_list.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | table.table.table-hover.table-bordered 7 | thead 8 | tr 9 | th 用户名 10 | th 密码 11 | //- th 录入时间 12 | th 查看 13 | th 更新 14 | th 删除 15 | tbody 16 | each item in users 17 | tr(class='item-id-' + item._id) 18 | td #{item.name} 19 | //- td #{item.password} 20 | td #{moment(item.meta.createAt).format('MM/DD/YYYY')} 21 | td: a(target='_blank', href='/admin/user/' + item._id) 查看 22 | td: a(target='_blank', href='/admin/user/update/' + item._id) 修改 23 | td 24 | button.btn.btn-danger.del(type='button', data-id=item._id) 删除 25 | script(src='/js/admin.js') -------------------------------------------------------------------------------- /app/routes/admin.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | 3 | const Permission = require('../middleware/permission') 4 | const User = require('../controllers/user') 5 | const Movie = require('../controllers/movie') 6 | const Category = require('../controllers/category') 7 | const { koaBody } = require('koa-body') 8 | 9 | router.use(Permission.signinRequired) 10 | router.use(Permission.adminRequired) 11 | 12 | // 用户 13 | router.get('/user/list', User.list)// 用户列表 14 | 15 | // 电影 16 | router.get('/movie/new', Movie.new) // 新增电影(回显数据) 17 | router.get('/movie/update/:id', Movie.update) // 更新电影(回显数据) 18 | router.post('/movie', koaBody({ multipart: true }), Movie.savePoster, Movie.save) // 新增电影 / 更新电影 19 | router.get('/movie/list', Movie.list) // 电影列表 20 | router.delete('/movie', Movie.del) // 删除电影 21 | 22 | // 电影分类 23 | router.get('/category/new', Category.new) // 新增电影分类(回显数据) 24 | router.get('/category/update/:id', Category.update) 25 | router.post('/category', Category.save) // 新增电影分类 / 更新电影 26 | router.get('/category/list', Category.list) // 电影分类列表 27 | router.delete('/category', Category.del) // 电影分类列表 28 | 29 | module.exports = router.routes() 30 | -------------------------------------------------------------------------------- /app/schemas/comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = mongoose.Schema 3 | const ObjectId = Schema.Types.ObjectId 4 | 5 | const CommentSchema = new Schema({ 6 | movie: { type: ObjectId, ref: 'Movie' }, 7 | from: { type: ObjectId, ref: 'User' }, 8 | reply: [{ 9 | from: { type: ObjectId, ref: 'User' }, 10 | to: { type: ObjectId, ref: 'User' }, 11 | content: String 12 | }], 13 | content: String, 14 | meta: { 15 | createAt: { 16 | type: Date, 17 | default: Date.now() 18 | }, 19 | updateAt: { 20 | type: Date, 21 | default: Date.now() 22 | } 23 | } 24 | }) 25 | 26 | CommentSchema.pre('save', function (next) { 27 | if (this.isNew) { 28 | this.meta.createAt = this.meta.updateAt = Date.now() 29 | } else { 30 | this.meta.updateAt = Date.now() 31 | } 32 | 33 | next() 34 | }) 35 | 36 | CommentSchema.statics = { 37 | fetch: function () { 38 | return this 39 | .find({}) 40 | .sort('meta.updateAt') 41 | .exec() 42 | }, 43 | findById: function (id) { 44 | return this 45 | .findOne({ _id: id }) 46 | .exec() 47 | } 48 | } 49 | 50 | module.exports = CommentSchema 51 | -------------------------------------------------------------------------------- /app/views/pages/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block content 4 | .container 5 | .row 6 | each cat in categories 7 | .panel.panel-default 8 | .panel-heading 9 | h3 10 | a(href='/results?cat='+cat._id+'&p=0') #{cat.name} 11 | .panel-body 12 | if cat.movies && cat.movies.length > 0 13 | each item in cat.movies 14 | .col-md-2 15 | .thumbnail 16 | a(href='/movie/' + item._id) 17 | if item.poster.indexOf('http:') > -1 18 | img(src=item.poster, alt=item.title) 19 | else 20 | img(src='/upload/'+item.poster, alt=item.title) 21 | .caption 22 | h3 #{item.title} 23 | p: a.btn.btn-primary(href='/movie/' + item._id, role='button') 欢迎观看预告片 24 | -------------------------------------------------------------------------------- /app/schemas/movie.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = mongoose.Schema 3 | const ObjectId = Schema.Types.ObjectId 4 | 5 | const MovieSchema = new Schema({ 6 | doctor: String, 7 | title: String, 8 | language: String, 9 | country: String, 10 | summary: String, 11 | flash: String, 12 | poster: String, 13 | year: Number, 14 | pv: { 15 | type: Number, 16 | default: 0 17 | }, 18 | category: { 19 | type: ObjectId, 20 | ref: 'Category' 21 | }, 22 | meta: { 23 | createAt: { 24 | type: Date, 25 | default: Date.now() 26 | }, 27 | updateAt: { 28 | type: Date, 29 | default: Date.now() 30 | } 31 | } 32 | }) 33 | 34 | MovieSchema.pre('save', function (next) { 35 | if (this.isNew) { 36 | this.meta.createAt = this.meta.updateAt = Date.now() 37 | } else { 38 | this.meta.updateAt = Date.now() 39 | } 40 | 41 | next() 42 | }) 43 | 44 | MovieSchema.statics = { 45 | fetch: function () { 46 | return this 47 | .find({}) 48 | .sort('meta.updateAt') 49 | .exec() 50 | }, 51 | findById: function (id) { 52 | return this 53 | .findOne({ _id: id }) 54 | .exec() 55 | } 56 | } 57 | 58 | module.exports = MovieSchema 59 | -------------------------------------------------------------------------------- /app/views/pages/admin/movie_list.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | table.table.table-hover.table-bordered 7 | thead 8 | tr 9 | th 电影名字 10 | th 导演 11 | th 国家 12 | th 上映年份 13 | th 录入时间 14 | th pv 15 | th 查看 16 | th 更新 17 | th 删除 18 | tbody 19 | each item in movies 20 | tr(class='item-id-' + item._id) 21 | td #{item.title} 22 | td #{item.doctor} 23 | td #{item.country} 24 | td #{item.year} 25 | td #{moment(item.meta.createdAt).format('MM/DD/YYYY')} 26 | td #{item.pv} 27 | td: a(target='_blank', href='/movie/' + item._id) 查看 28 | td: a(target='_blank', href='/admin/movie/update/' + item._id) 修改 29 | td 30 | button.btn.btn-danger.js-movie-del(type='button', data-id=item._id) 删除 31 | script(src='/js/admin.js') -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "dependencies": { 7 | "bcryptjs": "^2.4.3", 8 | "koa": "^2.14.1", 9 | "koa-body": "^6.0.1", 10 | "koa-bodyparser": "^4.4.0", 11 | "koa-compress": "^2.0.0", 12 | "koa-convert": "^1.2.0", 13 | "koa-cors": "^0.0.16", 14 | "koa-mount": "^3.0.0", 15 | "koa-pug": "^5.1.0", 16 | "koa-router": "^12.0.0", 17 | "koa-session": "^6.4.0", 18 | "koa-static": "^4.0.2", 19 | "moment": "^2.29.4", 20 | "mongoose": "^6.10.4", 21 | "pug": "^2.0.0-rc.4", 22 | "underscore": "^1.13.6" 23 | }, 24 | "devDependencies": { 25 | "cross-env": "^5.1.1", 26 | "eslint": "^8.36.0", 27 | "eslint-config-standard": "^17.0.0", 28 | "eslint-plugin-import": "^2.27.5", 29 | "eslint-plugin-n": "^15.6.1", 30 | "eslint-plugin-promise": "^6.1.1", 31 | "grunt": "^1.6.1", 32 | "grunt-concurrent": "^2.3.1", 33 | "grunt-contrib-watch": "^1.0.0", 34 | "grunt-nodemon": "^0.4.2" 35 | }, 36 | "scripts": { 37 | "test": "echo \"Error: no test specified\" && exit 1", 38 | "dev": "cross-env NODE_ENV=development grunt", 39 | "filemap": "filemap -i node_modules libs .git .idea upload", 40 | "lint": "eslint . --fix" 41 | }, 42 | "author": "", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /app/views/pages/movie/results.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | .panel.panel-default 7 | .panel-heading 8 | h3 #{keyword} 9 | .panel-body 10 | if movies && movies.length > 0 11 | each item in movies 12 | .col-md-2 13 | .thumbnail 14 | a(href='/movie/' + item._id) 15 | if item.poster.indexOf('http:') > -1 16 | img(src=item.poster, alt=item.title) 17 | else 18 | img(src='/upload/' + item.poster, alt=item.title) 19 | .caption 20 | h3 #{item.title} 21 | p: a.btn.btn-primary(href='/movie/' + item._id, role='button') 欢迎观看预告片 22 | ul.pagination 23 | - for (var i = 0; i < totalPage; i++) { 24 | - if (currentPage == i+1) { 25 | li.active 26 | span #{currentPage} 27 | - } 28 | - else { 29 | li 30 | a(href='/results?'+query+'&p='+i) #{i+1} 31 | - } 32 | - } -------------------------------------------------------------------------------- /public/js/admin.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | function remove (element, url) { 3 | $(element).click(function (e) { 4 | const target = $(e.target) 5 | const id = target.data('id') 6 | const tr = $('.item-id-' + id) 7 | 8 | $.ajax({ 9 | type: 'DELETE', 10 | url: url + '?id=' + id 11 | }) 12 | .done(function (results) { 13 | if (results.success === 1) { 14 | if (tr.length > 0) { 15 | tr.remove() 16 | } 17 | } 18 | }) 19 | }) 20 | } 21 | 22 | remove('.js-movie-del', '/admin/movie') 23 | remove('.js-category-del', '/admin/category') 24 | 25 | $('#douban').blur(function () { 26 | const douban = $(this) 27 | const id = douban.val() 28 | 29 | if (id) { 30 | $.ajax({ 31 | url: 'http://api.douban.com/v2/movie/subject/' + id, 32 | cache: true, 33 | type: 'get', 34 | dataType: 'jsonp', 35 | crossDomain: true, 36 | jsonp: 'callback', 37 | success: function (data) { 38 | $('#inputTitle').val(data.title) 39 | $('#inputDoctor').val(data.directors[0].name) 40 | $('#inputCountry').val(data.countries[0]) 41 | // $('inputLanguage').val(data) 42 | $('#inputPoster').val(data.images.large) 43 | $('#inputYear').val(data.year) 44 | $('#inputSummary').val(data.summary) 45 | } 46 | }) 47 | } 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | watch: { 4 | pug: { 5 | files: ['app/views/**'], 6 | options: { 7 | nospawn: true, 8 | interrupt: false, 9 | debounceDelay: 250 10 | // livereload: true 11 | } 12 | }, 13 | js: { 14 | files: ['public/js/**', 'app/models/**/*.js', 'app/schemas/**/*.js'], 15 | // tasks: ['jshint'], 16 | options: { 17 | nospawn: true, 18 | interrupt: false, 19 | debounceDelay: 250 20 | // livereload: true 21 | } 22 | } 23 | }, 24 | 25 | nodemon: { 26 | dev: { 27 | options: { 28 | file: 'app.js', 29 | args: [], 30 | ignoredFiles: ['README.md', 'node_modules/**', '.DS_Store'], 31 | watchedExtensions: ['js'], 32 | watchFolders: ['./'], 33 | debug: true, 34 | delayTime: 1, 35 | env: { 36 | PORT: 3000 37 | }, 38 | cwd: __dirname 39 | } 40 | } 41 | }, 42 | 43 | concurrent: { 44 | tasks: ['nodemon', 'watch'], 45 | options: { 46 | logConcurrentOutput: true 47 | } 48 | } 49 | }) 50 | 51 | grunt.loadNpmTasks('grunt-contrib-watch') 52 | grunt.loadNpmTasks('grunt-nodemon') 53 | grunt.loadNpmTasks('grunt-concurrent') 54 | 55 | grunt.option('force', true) 56 | grunt.registerTask('default', ['concurrent']) 57 | } 58 | -------------------------------------------------------------------------------- /app/schemas/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const bcrypt = require('bcryptjs') 3 | const SALT_WORK_FACTOR = 10 4 | 5 | const UserSchema = new mongoose.Schema({ 6 | name: { 7 | unique: true, 8 | type: String 9 | }, 10 | password: String, 11 | role: { 12 | type: Number, 13 | default: 0 14 | }, 15 | meta: { 16 | createAt: { 17 | type: Date, 18 | default: Date.now() 19 | }, 20 | updateAt: { 21 | type: Date, 22 | default: Date.now() 23 | } 24 | } 25 | }) 26 | 27 | UserSchema.pre('save', function (next) { 28 | const user = this 29 | if (this.isNew) { 30 | this.meta.createAt = this.meta.updateAt = Date.now() 31 | } else { 32 | this.meta.updateAt = Date.now() 33 | } 34 | 35 | bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) { 36 | if (err) return next(err) 37 | bcrypt.hash(user.password, salt, function (err, hash) { 38 | if (err) return next(err) 39 | 40 | user.password = hash 41 | next() 42 | }) 43 | }) 44 | }) 45 | 46 | UserSchema.methods = { 47 | comparePassword: async function (password) { 48 | try { 49 | return await bcrypt.compare(password, this.password) 50 | } catch (e) { 51 | console.log(e) 52 | } 53 | } 54 | } 55 | 56 | UserSchema.statics = { 57 | fetch: function () { 58 | return this 59 | .find({}) 60 | .sort('meta.updateAt') 61 | .exec() 62 | }, 63 | findById: function (id) { 64 | return this 65 | .findOne({ _id: id }) 66 | .exec() 67 | } 68 | } 69 | 70 | module.exports = UserSchema 71 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const router = new Router() 3 | 4 | router.use(function (ctx, next) { 5 | const _user = ctx.session.user 6 | 7 | ctx.state.user = _user 8 | 9 | return next() 10 | }) 11 | 12 | router.use('/', require('../app/routes/index')) 13 | router.use('/admin', require('../app/routes/admin')) 14 | router.use('/movie', require('../app/routes/movie')) 15 | router.use('/user', require('../app/routes/user')) 16 | 17 | module.exports = router.routes() 18 | 19 | // module.exports = function (app) { 20 | // // app.use(function (req, res, next) { 21 | // // var _user = req.session.user 22 | // // 23 | // // app.locals.user = _user 24 | // // 25 | // // return next() 26 | // // }) 27 | // 28 | // // 路由分发 29 | // // app.use('/', require('../app/routes/index')) 30 | // // app.use('/user', require('../app/routes/user')) 31 | // // app.use('/movie', require('../app/routes/movie')) 32 | // // app.use('/admin', require('../app/routes/admin')) 33 | // 34 | // /* 35 | // { 36 | // _id: 1, 37 | // doctor: '何塞·帕迪里亚', 38 | // country: '美国', 39 | // title: '机械战警', 40 | // year: 2014, 41 | // poster: 'http://g4.ykimg.com/05160000530EEB63675839160D0B79D5', 42 | // language: '英语', 43 | // flash: 'http://player.youku.com/embed/XNzEwNDg4OTY4', 44 | // summary: '2028年,专事军火开发的机器人公司Omni Corp.生产了大量装备精良的机械战警,他们被投入到惩治犯罪等行动中,取得显著的效果。罪犯横行的底特律市,嫉恶如仇、正义感十足的警察亚历克斯·墨菲(乔尔·金纳曼 饰)遭到仇家暗算,身体受到毁灭性破坏。借助于Omni公司天才博士丹尼特·诺顿(加里·奥德曼 饰)最前沿的技术,墨菲以机械战警的形态复活。数轮严格的测试表明,墨菲足以承担起维护社会治安的重任,他的口碑在民众中直线飙升,而墨菲的妻子克拉拉(艾比·考尼什 饰)和儿子大卫却再难从他身上感觉亲人的温暖。 感知到妻儿的痛苦,墨菲决心向策划杀害自己的犯罪头子展开反击……' 45 | // } 46 | // */ 47 | // } 48 | -------------------------------------------------------------------------------- /app/controllers/user.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user') 2 | 3 | // 用户注册 4 | exports.showSignup = async function (ctx) { 5 | await ctx.render('user/signup', { 6 | title: '注册页面' 7 | }) 8 | } 9 | 10 | exports.showSignin = async function (ctx) { 11 | await ctx.render('user/signin', { 12 | title: '登录页面' 13 | }) 14 | } 15 | 16 | // 用户注册 17 | exports.signup = async function (ctx) { 18 | const _user = ctx.request.body.user 19 | 20 | try { 21 | const oldUser = await User.findOne({ name: _user.name }) 22 | if (oldUser) { 23 | return ctx.redirect('/signin') 24 | } else { 25 | const user = new User(_user) 26 | 27 | await user.save() 28 | return ctx.redirect('/') 29 | } 30 | } catch (e) { 31 | console.log(e) 32 | } 33 | } 34 | 35 | // 用户登录 36 | exports.signin = async function (ctx) { 37 | const _user = ctx.request.body.user 38 | const name = _user.name 39 | const password = _user.password 40 | 41 | try { 42 | const user = await User.findOne({ name }) 43 | if (!user) { 44 | return ctx.redirect('/signup') 45 | } 46 | 47 | const isMatch = await user.comparePassword(password) 48 | if (isMatch) { 49 | ctx.session.user = user 50 | 51 | return ctx.redirect('/') 52 | } else { 53 | return ctx.redirect('/signin') 54 | } 55 | } catch (e) { 56 | console.log(e) 57 | } 58 | } 59 | 60 | exports.logout = function (ctx) { 61 | delete ctx.session.user 62 | // delete app.locals.user 63 | 64 | ctx.redirect('/') 65 | } 66 | 67 | // 用户列表 68 | exports.list = async function (ctx) { 69 | try { 70 | const users = await User.fetch() 71 | await ctx.render('admin/user_list', { 72 | title: '用户列表页', 73 | users 74 | }) 75 | } catch (e) { 76 | console.log(e) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | const Movie = require('../models/movie') 2 | const Category = require('../models/category') 3 | 4 | exports.index = async function (ctx) { 5 | try { 6 | const categories = await Category 7 | .find({}) 8 | .populate({ path: 'movies', options: { limit: 20 } }) 9 | .exec() 10 | await ctx.render('index', { 11 | title: '电影首页', 12 | categories 13 | }) 14 | } catch (e) { 15 | console.log(e) 16 | } 17 | } 18 | 19 | exports.search = async function (ctx) { 20 | try { 21 | const catId = ctx.request.query.cat 22 | const q = ctx.request.query.q 23 | const page = parseInt(ctx.request.query.p, 10) || 0 24 | const count = 2 25 | const index = page * count 26 | 27 | if (catId) { 28 | const categories = await Category 29 | .find({ _id: catId }) 30 | .populate({ 31 | path: 'movies', 32 | select: 'title poster' 33 | }) 34 | .exec() 35 | const category = categories[0] || {} 36 | const movies = category.movies || [] 37 | const results = movies.slice(index, index + count) 38 | 39 | await ctx.render('movie/results', { 40 | title: '电影结果列表页面', 41 | keyword: category.name, 42 | currentPage: page + 1, 43 | query: 'cat=' + catId, 44 | totalPage: Math.ceil(movies.length / count), 45 | movies: results 46 | }) 47 | } else { 48 | const movies = await Movie 49 | .find({ title: new RegExp(q + '.*', 'i') }) 50 | .exec() 51 | const results = movies.slice(index, index + count) 52 | 53 | await ctx.render('movie/results', { 54 | title: '电影结果列表页面', 55 | keyword: q, 56 | currentPage: page + 1, 57 | query: 'q=' + q, 58 | totalPage: Math.ceil(movies.length / count), 59 | movies: results 60 | }) 61 | } 62 | } catch (e) { 63 | console.log(e) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/controllers/category.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore') 2 | const Category = require('../models/category') 3 | 4 | // 新增电影分类(回显数据) 5 | exports.new = async function (ctx) { 6 | await ctx.render('admin/category_add', { 7 | title: '电影后台分类录入页', 8 | category: { 9 | name: '' 10 | } 11 | }) 12 | } 13 | 14 | // 更新电影分类(回显数据) 15 | exports.update = async function (ctx) { 16 | const id = ctx.params.id 17 | try { 18 | if (id) { 19 | const category = await Category.findById(id) 20 | await ctx.render('admin/category_add', { 21 | title: '电影后台分类更新页', 22 | category 23 | }) 24 | } 25 | } catch (e) { 26 | console.log(e) 27 | } 28 | } 29 | 30 | // 新增电影分类 / 更新电影分类 31 | exports.save = async function (ctx) { 32 | const id = ctx.request.body.category._id 33 | const category = ctx.request.body.category 34 | let _category 35 | 36 | try { 37 | if (id) { 38 | const oldCategory = await Category.findById(id) 39 | _category = _.extend(oldCategory, category) 40 | await _category.save() 41 | 42 | ctx.redirect('/admin/category/list') 43 | } else { 44 | _category = new Category(category) 45 | await _category.save() 46 | 47 | ctx.redirect('/admin/category/list') 48 | } 49 | } catch (e) { 50 | console.log(e) 51 | } 52 | } 53 | 54 | // 电影分类列表 55 | exports.list = async function (ctx) { 56 | try { 57 | const categories = await Category.fetch() 58 | await ctx.render('admin/category_list', { 59 | title: '电影分类列表页', 60 | categories 61 | }) 62 | } catch (e) { 63 | console.log(e) 64 | } 65 | } 66 | 67 | // 删除分类 68 | exports.del = async function (ctx) { 69 | const id = ctx.request.query.id 70 | let body = { success: 1 } 71 | 72 | try { 73 | if (id) { 74 | await Category.remove({ _id: id }) 75 | } 76 | } catch (e) { 77 | console.log(e) 78 | body = { success: 0 } 79 | } finally { 80 | ctx.body = body 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/views/includes/header.pug: -------------------------------------------------------------------------------- 1 | .container 2 | .row 3 | .page-header.clearfix 4 | h1= title 5 | .col-md-4 6 | small 重度科幻迷 7 | .col-md-8 8 | form(method='get', action='/results') 9 | .input-group.col-sm-4.pull-right 10 | input.form-control(type='text', name='q') 11 | span.input-group-btn 12 | button.btn.btn-default(type='submit') 搜索 13 | .navbar.navbar-default.navbar-fixed-bottom 14 | .container 15 | .navbar-header 16 | a.navbar-brand(href='/') 重度科幻迷 17 | if user 18 | p.navbar-text.navbar-right 19 | span 欢迎您,#{user.name} 20 | span  |  21 | a.navbar-link(href='/logout') 登出 22 | else 23 | p.navbar-text.navbar-right 24 | a.navbar-link(href='#', data-toggle='modal', data-target='#signupModal') 注册 25 | span  |  26 | a.navbar-link(href='#', data-toggle='modal', data-target='#signinModal') 登录 27 | #signupModal.modal.fade 28 | .modal-dialog 29 | .modal-content 30 | form(method='post', action='/user/signup') 31 | .modal-header 注册 32 | .modal-body 33 | .form-group 34 | label(for='signupName') 用户名 35 | input#signupName.form-control(name='user[name]', type='text') 36 | .form-group 37 | label(for='signupPassword') 密码 38 | input#signupPassword.form-control(name='user[password]', type='text') 39 | .modal-footer 40 | button.btn.btn-default(type='button', data-dismiss='modal') 关闭 41 | button.btn.btn-success(type='submit') 提交 42 | #signinModal.modal.fade 43 | .modal-dialog 44 | .modal-content 45 | form(method='post', action='/user/signin') 46 | .modal-header 登录 47 | .modal-body 48 | .form-group 49 | label(for='signinName') 用户名 50 | input#signinName.form-control(name='user[name]', type='text') 51 | .form-group 52 | label(for='signinPassword') 密码 53 | input#signinPassword.form-control(name='user[password]', type='text') 54 | .modal-footer 55 | button.btn.btn-default(type='button', data-dismiss='modal') 关闭 56 | button.btn.btn-success(type='submit') 提交 -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Koa = require('koa') 3 | const cors = require('koa-cors') // 跨域处理 4 | const bodyparser = require('koa-bodyparser') // 传参获取 5 | const compress = require('koa-compress') // 传输压缩 6 | const mount = require('koa-mount') // 路由挂载 7 | const convert = require('koa-convert') // 封装中间件 8 | const mongoose = require('mongoose') 9 | const Pug = require('koa-pug') // 模板引擎 10 | const serve = require('koa-static') // 静态资源 11 | const session = require('koa-session') // session 12 | const moment = require('moment') // 时间库 13 | 14 | const config = require('./config') // 项目配置 15 | const router = require('./config/routes') // 路由 16 | 17 | const app = new Koa() 18 | // eslint-disable-next-line no-unused-vars 19 | const pug = new Pug({ 20 | app, 21 | viewPath: path.resolve(__dirname, './app/views/pages'), // 模板路径 22 | // pretty: true, // 格式化 23 | locals: { // 全局变量 24 | moment 25 | } 26 | }) 27 | 28 | app.use(serve(path.join(__dirname, '/public'))) 29 | app.use(convert(bodyparser())) 30 | app.use(convert(compress())) 31 | app.use(convert(cors())) 32 | 33 | app.keys = ['secret is only a secret'] 34 | app.use(session({ 35 | key: 'movie-koa2', /** (string) cookie key (default is koa:sess) */ 36 | /** (number || 'session') maxAge in ms (default is 1 days) */ 37 | /** 'session' will result in a cookie that expires when session/browser is closed */ 38 | /** Warning: If a session cookie is stolen, this cookie will never expire */ 39 | maxAge: 86400000, 40 | overwrite: true, /** (boolean) can overwrite or not (default true) */ 41 | httpOnly: true, /** (boolean) httpOnly or not (default true) */ 42 | signed: true, /** (boolean) signed or not (default true) */ 43 | rolling: false /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/ 44 | }, app)) 45 | 46 | app.use(async (ctx, next) => { 47 | const start = new Date() 48 | await next() 49 | const ms = new Date() - start 50 | console.log(`${ctx.method}, ${ctx.url}, ${ms}`) 51 | }) 52 | 53 | // 错误监听 54 | app.on('error', (err, ctx) => { 55 | console.log('error: ', err) 56 | }) 57 | 58 | // 开发配置 59 | if (process.env.NODE_ENV === 'development') { 60 | // 开发配置 61 | mongoose.set('debug', true) 62 | } 63 | 64 | // 挂在路由 65 | // app.use(mount('/api/v1', router)) 66 | app.use(mount('/', router)) 67 | 68 | connect() 69 | 70 | function listen () { 71 | app.listen(config.port, () => { 72 | console.log('%s BackEnd Server is running on: http://%s:%s', config.appName, config.host, config.port) 73 | }) 74 | } 75 | 76 | // 数据库连接 77 | function connect () { 78 | mongoose.connection.on('error', console.log) 79 | .on('disconnected', connect) 80 | .once('open', listen) 81 | 82 | mongoose.set('strictQuery', true) 83 | mongoose.connect('mongodb://' + config.database.host + '/' + config.database.db) 84 | .catch(console.error.bind(console, 'connect error:')) 85 | 86 | // mongoose.Promise = global.Promise // resolve a bug that mpromise is deprecated,plug in your own promise library instead 87 | // return mongoose.connect('mongodb://' + config.database.host + '/' + config.database.db, { 88 | // useMongoClient: true 89 | // }) 90 | } 91 | -------------------------------------------------------------------------------- /app/views/pages/movie/detail.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | .col-md-7 7 | embed(src=movie.flash, allowFullScreen='true', quality='high', width='720', height='600', align='middle', type='application/x-shockwave-flash') 8 | .panel.panel-default(style='margin-bottom: 60px;') 9 | .panel-heading 10 | h3 评论区 11 | .panel-body 12 | ul.media-list 13 | each item in comments 14 | li.media 15 | .pull-left 16 | a.comment(href='#comments', data-cid=item._id, data-tid=item.from._id) 17 | img.media-object(style='width: 64px; height: 64px;') 18 | .media-body 19 | h4.media-heading #{item.from.name} 20 | p #{item.content} 21 | if item.reply && item.reply.length > 0 22 | each reply in item.reply 23 | .media 24 | .pull-left 25 | a.comment(href='#comments', data-cid=item._id, data-tid=reply.from._id) 26 | img.media-object(style='width: 64px; height: 64px;') 27 | .media-body 28 | h4.media-heading 29 | | #{reply.from.name} 30 | span.text-info  回复  31 | | #{reply.to.name}: 32 | p #{reply.content} 33 | hr 34 | #comments 35 | form#commentForm(method='post', action='/user/comment') 36 | input(type='hidden', name='comment[movie]', value=movie._id) 37 | if user 38 | input(type='hidden', name='comment[from]', value=user._id) 39 | .form-group 40 | textarea.form-control(name='comment[content]', row='3') 41 | if user 42 | button.btn.btn-primary(type='submit') 提交 43 | else 44 | a.navbar-link(href='#', data-toggle='modal', data-target='#signinModal') 登录后评论 45 | .col-md-5 46 | dl.dl-horizontal 47 | dt 电影名字 48 | dd= movie.title 49 | dt 导演 50 | dd= movie.doctor 51 | dt 国家 52 | dd= movie.country 53 | dt 语言 54 | dd= movie.language 55 | dt 上映年份 56 | dd= movie.year 57 | dt 简介 58 | dd= movie.summary 59 | script(src='/js/detail.js') -------------------------------------------------------------------------------- /app/views/pages/admin/movie_add.pug: -------------------------------------------------------------------------------- 1 | extends ../../layout 2 | 3 | block content 4 | .container 5 | .row 6 | form.form-horizontal(method="post", action="/admin/movie", enctype="multipart/form-data", style="padding-bottom: 120px;") 7 | if movie._id 8 | input(type="hidden", name="_id", value=movie._id) 9 | input(type="hidden", name="oldCategory", value=movie.category) 10 | .form-group 11 | label.col-sm-3.control-label(for="douban") 豆瓣同步 12 | .col-sm-9 13 | input#douban.form-control(type="text") 14 | .form-group 15 | label.col-sm-3.control-label(for="inputCategory") 电影分类 16 | .col-sm-9 17 | input#inputCategory.form-control(type="text", name="categoryName", value=movie.categoryName) 18 | .form-group 19 | label.col-sm-3.control-label 分类选择 20 | .col-sm-9 21 | if categories && categories.length > 0 22 | each cat in categories 23 | label.radio-inline 24 | if movie._id 25 | input(type='radio', name='category', value=cat._id, checked=cat._id.toString() === movie.category.toString()) 26 | else 27 | input(type='radio', name='category', value=cat._id) 28 | | #{cat.name} 29 | .form-group 30 | label.col-sm-3.control-label(for="inputTitle") 电影名字 31 | .col-sm-9 32 | input#inputTitle.form-control(type="text", name="title", value=movie.title) 33 | .form-group 34 | label.col-sm-3.control-label(for="inputDoctor") 导演 35 | .col-sm-9 36 | input#inputDoctor.form-control(type="text", name="doctor", value=movie.doctor) 37 | .form-group 38 | label.col-sm-3.control-label(for="inputCountry") 国家 39 | .col-sm-9 40 | input#inputCountry.form-control(type="text", name="country", value=movie.country) 41 | .form-group 42 | label.col-sm-3.control-label(for="inputLanguage") 语种 43 | .col-sm-9 44 | input#inputLanguage.form-control(type="text", name="language", value=movie.language) 45 | .form-group 46 | label.col-sm-3.control-label(for="inputPoster") 海报地址 47 | .col-sm-9 48 | input#inputPoster.form-control(type="text", name="poster", value=movie.poster) 49 | .form-group 50 | label.col-sm-3.control-label(for="uploadPoster") 海报上传 51 | .col-sm-9 52 | input#uploadPoster(type="file", name="uploadPoster") 53 | .form-group 54 | label.col-sm-3.control-label(for="inputFlash") 片源地址 55 | .col-sm-9 56 | input#inputFlash.form-control(type="text", name="flash", value=movie.flash) 57 | .form-group 58 | label.col-sm-3.control-label(for="inputYear") 上映年代 59 | .col-sm-9 60 | input#inputYear.form-control(type="text", name="year", value=movie.year) 61 | .form-group 62 | label.col-sm-3.control-label(for="inputSummary") 电影简介 63 | .col-sm-9 64 | input#inputSummary.form-control(type="text", name="summary", value=movie.summary) 65 | .form-group 66 | .col-sm-offset-3.col-sm-9 67 | button.btn.btn-default(type="submit") 录入 68 | script(src="/js/admin.js") -------------------------------------------------------------------------------- /app/controllers/movie.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore') 2 | const Movie = require('../models/movie') 3 | const Comment = require('../models/comment') 4 | const Category = require('../models/category') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | // 电影详情 9 | exports.detail = async function (ctx) { 10 | const id = ctx.params.id 11 | try { 12 | const movie = await Movie.findById(id) 13 | await Movie.update({ _id: id }, { $inc: { pv: 1 } }) 14 | 15 | const comments = await Comment 16 | .find({ movie: id }) 17 | .populate('from', 'name') 18 | .populate('reply.from reply.to', 'name') 19 | .exec() 20 | await ctx.render('movie/detail', { 21 | title: '电影详情页——' + movie.title, 22 | movie, 23 | comments 24 | }) 25 | } catch (e) { 26 | console.log(e) 27 | } 28 | } 29 | 30 | // 新增电影(回显数据) 31 | exports.new = async function (ctx) { 32 | try { 33 | const categories = await Category.find({}) 34 | await ctx.render('admin/movie_add', { 35 | title: '电影后台录入页', 36 | categories, 37 | movie: {} 38 | }) 39 | } catch (e) { 40 | console.log(e) 41 | } 42 | } 43 | 44 | // 更新电影(回显数据) 45 | exports.update = async function (ctx) { 46 | const id = ctx.params.id 47 | try { 48 | if (id) { 49 | const movie = await Movie.findById(id) 50 | const categories = await Category.find({}) 51 | await ctx.render('admin/movie_add', { 52 | title: '电影后台更新页', 53 | movie, 54 | categories 55 | }) 56 | } 57 | } catch (e) { 58 | console.log(e) 59 | } 60 | } 61 | 62 | exports.savePoster = async function (ctx, next) { 63 | try { 64 | const posterData = ctx.request.body.files.uploadPoster 65 | const filePath = posterData.path 66 | const name = posterData.name 67 | 68 | if (name) { 69 | const reader = fs.createReadStream(filePath) 70 | 71 | const timestamp = Date.now() 72 | const type = posterData.type.split('/')[1] 73 | const poster = timestamp + '.' + type 74 | const newPath = path.join(__dirname, '../../', '/public/upload/' + poster) 75 | const stream = fs.createWriteStream(newPath) 76 | reader.pipe(stream) 77 | 78 | ctx.poster = poster 79 | } 80 | await next() 81 | } catch (e) { 82 | console.log(e) 83 | } 84 | } 85 | 86 | // 新增电影 / 更新电影 87 | exports.save = async function (ctx) { 88 | const id = ctx.request.body.fields._id 89 | const movie = ctx.request.body.fields 90 | let _movie 91 | 92 | try { 93 | if (ctx.poster) { 94 | movie.poster = ctx.poster 95 | } 96 | 97 | if (id) { 98 | const oldMovie = await Movie.findById(id) 99 | _movie = _.extend(oldMovie, movie) 100 | const newMovie = await _movie.save() 101 | 102 | const oldCategoryId = movie.oldCategory 103 | const categoryId = movie.category 104 | if (categoryId !== oldCategoryId) { // 更改类别,需要针对两个类别对里面的电影进行操作(增加,删除) 105 | await Category.findByIdAndUpdate({ _id: categoryId }, { $addToSet: { movies: newMovie._id } }) 106 | await Category.findByIdAndUpdate({ _id: oldCategoryId }, { $pull: { movies: newMovie._id } }) 107 | } 108 | ctx.redirect('/movie/' + movie._id) 109 | } else { 110 | _movie = new Movie(movie) 111 | const newMovie = await _movie.save() 112 | 113 | const categoryId = _movie.category 114 | const categoryName = movie.categoryName 115 | if (categoryId) { 116 | const category = await Category.findById(categoryId) 117 | category.movies.push(newMovie._id) 118 | 119 | await category.save() 120 | } else if (categoryName) { 121 | const category = new Category({ 122 | name: categoryName, 123 | movies: [newMovie._id] 124 | }) 125 | 126 | const newCategory = await category.save() 127 | newMovie.category = newCategory._id 128 | await newMovie.save() 129 | } 130 | ctx.redirect('/movie/' + newMovie._id) 131 | } 132 | } catch (e) { 133 | console.log(e) 134 | } 135 | } 136 | 137 | // 电影列表 138 | exports.list = async function (ctx) { 139 | try { 140 | const movies = await Movie.fetch() 141 | await ctx.render('admin/movie_list', { 142 | title: '电影列表页', 143 | movies 144 | }) 145 | } catch (e) { 146 | console.log(e) 147 | } 148 | } 149 | 150 | // 删除电影 151 | exports.del = async function (ctx) { 152 | const id = ctx.request.query.id 153 | let body = { success: 1 } 154 | 155 | try { 156 | if (id) { 157 | await Movie.remove({ _id: id }) 158 | } 159 | } catch (e) { 160 | console.log(e) 161 | body = { success: 0 } 162 | } finally { 163 | ctx.body = body 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于 koa2 + mongodb 的电影网站 2 | 3 | ## 从 [expressjs-movie](https://github.com/savoygu/expressjs-movie) 迁移过程 4 | 5 | **中间件:** 6 | 7 | | 描述 | Express4 | Koa2 | 8 | | -------- | ------------------ | :---------------------------------------------------: | 9 | | 参数解析 | body-parser | [koa-bodyparser](https://github.com/koajs/bodyparser) | 10 | | 静态资源 | serve-static | [koa-static](https://github.com/koajs/static) | 11 | | session | express-session | [koa-session](https://github.com/koajs/session) | 12 | | 文件上传 | connect-multiparty | [koa-body](https://github.com/dlau/koa-body) | 13 | 14 | 路由:[koa-router](https://github.com/alexmingoia/koa-router) 15 | 16 | 模板引擎:[koa-pug](https://github.com/chrisyip/koa-pug) 和 [koa-views](https://github.com/queckezz/koa-views) 17 | 18 | `app.locals`:参考 [项目代码 1](https://github.com/savoygu/koajs-movie/blob/master/config/routes.js#L7) 和 [项目代码 2](https://github.com/savoygu/koajs-movie/blob/master/app.js#L23-L25) 19 | 20 | ## 运行环境 21 | 22 | - Mongodb 数据库 23 | 24 | ## 项目运行 25 | 26 | ```bash 27 | # 安装依赖 28 | bower install 29 | npm install 30 | 31 | # 开发 (http://localhost:3000) 32 | npm run dev 33 | ``` 34 | 35 | ## 项目布局 36 | 37 | 生成方式:[参考文档](https://github.com/jrainlau/filemap) 38 | 39 | ```bash 40 | npm run filemap 41 | ``` 42 | 43 | 目录改动: 路由(app/routes) 和 视图(app/views) 44 | 45 | ```bash 46 | |__ app 47 | |__ controllers // 控制层 48 | |__ category.js // 电影分类(列表,新增,更新,删除) 49 | |__ comment.js // 电影评论(评论) 50 | |__ index.js // 首页(按分类展示电影,搜索) 51 | |__ movie.js // 电影(列表,详情,新增,更新,删除) 52 | |__ user.js // 用户 (登录,注册,登出,列表) 53 | |__ middleware // 中间件 54 | |__ permission.js // 权限(登录,管理员) 55 | |__ models // 模型 56 | |__ category.js // 电影分类 57 | |__ comment.js // 电影评论 58 | |__ movie.js // 电影 59 | |__ user.js // 用户 60 | |__ routes // 路由 61 | |__ admin.js // 后台管理(电影管理,电影类别管理,用户管理) 62 | |__ index.js // 首页(搜索) 63 | |__ movie.js // 前台电影(详情) 64 | |__ user.js // 前台用户(登录,注册,登出,评论) 65 | |__ schemas // 模式 66 | |__ category.js // 电影类别 67 | |__ comment.js // 电影评论 68 | |__ movie.js // 电影 69 | |__ user.js // 用户 70 | |__ views // 视图 71 | |__ includes // 通用 72 | |__ head.pug // 静态资源 73 | |__ header.pug // 头部 74 | |__ layout.pug // 页面模板 75 | |__ pages // 页面 76 | |__ admin // 后台 77 | |__ category_add.pug // 电影类别添加 78 | |__ category_list.pug // 电影类别列表 79 | |__ movie_list.pug // 电影列表 80 | |__ mvoie_add.pug // 电影添加 81 | |__ user_list.pug // 用户列表 82 | |__ index.pug // 首页 83 | |__ movie // 电影 84 | |__ detail.pug // 详情 85 | |__ results.pug // 搜索 86 | |__ user // 用户 87 | |__ signin.pug // 登录 88 | |__ signup.pug // 注册 89 | |__ config // 配置 90 | |__ routes.js // 路由配置 91 | |__ public // 静态资源 92 | |__ js // javascript 93 | |__ admin.js // 电影删除,电影类别删除,豆瓣电影资源获取 94 | |__ detail.js // 电影详情页面评论 95 | |__ libs // 库 96 | |__ upload // 上传资源存放 97 | |__ .bowerrc // bower 相关配置 98 | |__ .gitignore // git 忽略文件列表 99 | |__ app.js // Nodejs 入口文件 100 | |__ bower.json // bower 项目配置 101 | |__ gruntfile.js // grunt 配置 102 | |__ package.json // npm 项目配置 103 | |__ package-lock.json // lock 依赖 104 | |__ README.md // README 105 | ``` 106 | --------------------------------------------------------------------------------