Browse Source

重新整理目录结构

郑国榕 8 years ago
commit
db8def1a80
100 changed files with 9702 additions and 0 deletions
  1. 5 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 3 0
      .eslintignore
  4. 22 0
      .eslintrc.js
  5. 7 0
      .eslintrc.json
  6. 8 0
      api.md
  7. 110 0
      api/file/file.controller.js
  8. 21 0
      api/file/file.model.js
  9. 16 0
      api/file/index.js
  10. 17 0
      api/pages/index.js
  11. 128 0
      api/pages/pages.controller.js
  12. 23 0
      api/pages/pages.model.js
  13. 17 0
      api/user/index.js
  14. 162 0
      api/user/user.controller.js
  15. 196 0
      api/user/user.model.js
  16. 65 0
      app.js
  17. 95 0
      auth/auth.service.js
  18. 22 0
      auth/index.js
  19. 89 0
      bin/www
  20. 16 0
      components/errors/index.js
  21. 14 0
      config/index.dev.js
  22. 7 0
      config/index.js
  23. 12 0
      config/index.local.js
  24. 14 0
      config/index.pro.js
  25. 14 0
      controller/pages.controller.js
  26. 115 0
      package.json
  27. 131 0
      public/css/main.css
  28. 0 0
      public/font/Cocogoose.otf
  29. 56 0
      public/js/main.js
  30. 2489 0
      public/libs/css/animate.css
  31. 6 0
      public/libs/css/animate.min.css
  32. 15 0
      public/libs/css/swiper.min.css
  33. 1 0
      public/libs/js/maps/swiper.jquery.min.js.map
  34. 1 0
      public/libs/js/maps/swiper.min.js.map
  35. 76 0
      public/libs/js/resLoader.js
  36. 43 0
      public/libs/js/resize.js
  37. 25 0
      public/libs/js/resizeBak.js
  38. 2 0
      public/libs/js/swiper.animate.min.js
  39. 17 0
      public/libs/js/swiper.min.js
  40. 87 0
      public/pages/592388b5d0d23c27aa555692.html
  41. 66 0
      public/pages/592388b9d0d23c27aa555693.html
  42. 19 0
      render/preview.js
  43. 20 0
      routers.js
  44. 48 0
      util/tools.js
  45. 10 0
      views/404.html
  46. 11 0
      views/error.html
  47. 74 0
      views/spa.html
  48. 73 0
      views/template.html
  49. BIN
      webapp/.DS_Store
  50. 35 0
      webapp/build/build.js
  51. 9 0
      webapp/build/dev-client.js
  52. 71 0
      webapp/build/dev-server.js
  53. 61 0
      webapp/build/utils.js
  54. 94 0
      webapp/build/webpack.base.conf.js
  55. 34 0
      webapp/build/webpack.dev.conf.js
  56. 98 0
      webapp/build/webpack.prod.conf.js
  57. 6 0
      webapp/config/dev.env.js
  58. 32 0
      webapp/config/index.js
  59. 3 0
      webapp/config/prod.env.js
  60. 6 0
      webapp/config/test.env.js
  61. 11 0
      webapp/index.html
  62. 10 0
      webapp/src/App.vue
  63. 31 0
      webapp/src/api/editor.js
  64. 12 0
      webapp/src/api/user.js
  65. BIN
      webapp/src/assets/addpic_large.png
  66. BIN
      webapp/src/assets/images/default.png
  67. BIN
      webapp/src/assets/images/logo.jpg
  68. BIN
      webapp/src/assets/login-bg.jpg
  69. BIN
      webapp/src/assets/logo.png
  70. 679 0
      webapp/src/assets/svg/icon.svg
  71. 83 0
      webapp/src/components/Element/FontElement.vue
  72. 159 0
      webapp/src/components/Element/PicElement.vue
  73. 142 0
      webapp/src/components/Element/ShapesElement.vue
  74. 98 0
      webapp/src/components/HeaderBar.vue
  75. 122 0
      webapp/src/components/Operate.vue
  76. 126 0
      webapp/src/components/OperateNew.vue
  77. 89 0
      webapp/src/components/Page.vue
  78. 44 0
      webapp/src/components/PicturePicker.vue
  79. 29 0
      webapp/src/main.js
  80. 30 0
      webapp/src/model/Element.js
  81. 8 0
      webapp/src/model/Page.js
  82. 12 0
      webapp/src/model/Theme.js
  83. 32 0
      webapp/src/routers.js
  84. 418 0
      webapp/src/style/main.css
  85. 10 0
      webapp/src/util/appConst.js
  86. 97 0
      webapp/src/util/http.js
  87. 8 0
      webapp/src/util/tools.js
  88. 556 0
      webapp/src/views/h5editor/index.vue
  89. 263 0
      webapp/src/views/h5editor/overview.vue
  90. 153 0
      webapp/src/views/h5editor/themeList.vue
  91. 598 0
      webapp/src/views/spaeditor/index.vue
  92. 267 0
      webapp/src/views/spaeditor/overview.vue
  93. 153 0
      webapp/src/views/spaeditor/themeList.vue
  94. 123 0
      webapp/src/views/user/login.vue
  95. 144 0
      webapp/src/views/user/register.vue
  96. 188 0
      webapp/src/vuex/editor/actions.js
  97. 32 0
      webapp/src/vuex/editor/getters.js
  98. 24 0
      webapp/src/vuex/editor/index.js
  99. 25 0
      webapp/src/vuex/editor/mutation-type.js
  100. 0 0
      webapp/src/vuex/editor/mutations.js

+ 5 - 0
.babelrc

@@ -0,0 +1,5 @@
+{
+  "presets": ["es2015", "stage-2"],
+  "plugins": ["transform-runtime", "babel-polyfill"],
+  "comments": false
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+build/*.js
+config/*.js
+src/libs/js/*.js

+ 22 - 0
.eslintrc.js

@@ -0,0 +1,22 @@
+module.exports = {
+    root: true,
+    parser: 'babel-eslint',
+    parserOptions: {
+        sourceType: 'module'
+    },
+    // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
+    extends: 'standard',
+    // required to lint *.vue files
+    plugins: [
+        'html'
+    ],
+    // add your custom rules here
+    'rules': {
+        // allow paren-less arrow functions
+        'arrow-parens': 0,
+        // allow async-await
+        'generator-star-spacing': 0,
+        // allow debugger during development
+        'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
+    }
+}

+ 7 - 0
.eslintrc.json

@@ -0,0 +1,7 @@
+{
+    "extends": "standard",
+    "plugins": [
+        "standard",
+        "promise"
+    ]
+}

+ 8 - 0
api.md

@@ -0,0 +1,8 @@
+##API地址速查
+
+### 用户相关
+
+> 用户登录 /auth/login
+
+> 用户注册 /auth/register
+

+ 110 - 0
api/file/file.controller.js

@@ -0,0 +1,110 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+var jsonpatch = require('fast-json-patch')
+var File = require('./file.model')
+var tools = require('../../util/tools')
+var uuid = require('node-uuid')
+
+const respondWithResult = (res, statusCode) => {
+  statusCode = statusCode || 200
+  return function (entity) {
+    if (entity) {
+      return res.status(statusCode).json(entity)
+    }
+    return null
+  }
+}
+
+const patchUpdates = (patches) => {
+  return function (entity) {
+    try {
+      jsonpatch.apply(entity, patches, /*validate*/ true)
+    } catch (err) {
+      return Promise.reject(err)
+    }
+
+    return entity.save()
+  }
+}
+
+const removeEntity = (res) => {
+  return function (entity) {
+    if (entity) {
+      return entity.remove()
+        .then(() => {
+          res.status(204).end()
+        })
+    }
+  }
+}
+
+const handleEntityNotFound = (res) => {
+  return function (entity) {
+    if (!entity) {
+      res.status(404).end()
+      return null
+    }
+    return entity
+  }
+}
+
+const handleError = (res, statusCode) => {
+  statusCode = statusCode || 500
+  return function (err) {
+    res.status(statusCode).send(err)
+  }
+}
+
+module.exports.index = (req, res) => {
+  return File.find().exec()
+    .then(respondWithResult(res))
+    .catch(handleError(res))
+}
+
+// Gets a single File from the DB
+module.exports.show = (req, res) => {
+  return File.findById(req.params.id).exec()
+    .then(handleEntityNotFound(res))
+    .then(respondWithResult(res))
+    .catch(handleError(res))
+}
+
+module.exports.getByThemeId = (req, res) => {
+  return File.find({ themeId: req.params.id }).exec()
+    .then(handleEntityNotFound(res))
+    .then(respondWithResult(res))
+    .catch(handleError(res))
+}
+
+// Creates a new File in the DB
+module.exports.create = (req, res) => {
+  var imageInfo = buildImgPath(req.body.themeId || 'all')
+  if (req.body.imgData) {
+    tools.base64ToImg(req.body.imgData, imageInfo.imagePath)
+    req.body.filePath = imageInfo.accessPath
+  }
+  return File.create(req.body)
+    .then(respondWithResult(res, 201))
+    .catch(handleError(res))
+}
+
+const buildImgPath = (themeId) => {
+  // 文件使用uuid生成别名
+  var fileName = uuid.v1().replace(/-/g, '') + '.png'
+  // 文件目录
+  var dirPath = 'public/upload/' + themeId
+  // 图片保存路径
+  var imagePath = dirPath + '/' + fileName
+  // 图片访问路径
+  var accessPath = '/upload/' + themeId + '/' + fileName
+  return { accessPath: accessPath, imagePath: imagePath, dirPath: dirPath }
+}
+
+// Deletes a File from the DB
+module.exports.destroy = (req, res) => {
+  return File.findById(req.params.id).exec()
+    .then(handleEntityNotFound(res))
+    .then(removeEntity(res))
+    .catch(handleError(res))
+}

+ 21 - 0
api/file/file.model.js

@@ -0,0 +1,21 @@
+/**
+ * Created by zhengguorong on 2016/11/30.
+ */
+const mongoose = require('mongoose')
+mongoose.Promise = require('bluebird')
+
+var FileSchema = new mongoose.Schema({
+  filePath: {
+    type: String,
+    required: true
+  },
+  width: Number,
+  height: Number,
+  fileName: String,
+  createDate: { type: Number, default: new Date().getTime() },
+  themeId: {
+    type: String
+  }
+})
+
+module.exports = mongoose.model('File', FileSchema)

+ 16 - 0
api/file/index.js

@@ -0,0 +1,16 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+var express = require('express')
+var controller = require('./file.controller')
+const auth = require('../../auth/auth.service')
+
+var router = express.Router()
+
+router.get('/', auth.isAuthenticated(), controller.index)
+router.get('/theme/:id', auth.isAuthenticated(), controller.getByThemeId)
+router.get('/:id', controller.show)
+router.post('/', controller.create)
+router.delete('/:id', auth.isAuthenticated(), controller.destroy)
+
+module.exports = router

+ 17 - 0
api/pages/index.js

@@ -0,0 +1,17 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+var express = require('express')
+var controller = require('./pages.controller')
+const auth = require('../../auth/auth.service')
+
+var router = express.Router()
+
+router.get('/', auth.isAuthenticated(), controller.findByLoginId)
+router.get('/:id', auth.isAuthenticated(), controller.show)
+router.post('/', auth.isAuthenticated(), controller.create)
+router.put('/:id', auth.isAuthenticated(), controller.update)
+router.patch('/:id', auth.isAuthenticated(), controller.patch)
+router.delete('/:id', auth.isAuthenticated(), controller.destroy)
+
+module.exports = router

+ 128 - 0
api/pages/pages.controller.js

@@ -0,0 +1,128 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+var jsonpatch = require('fast-json-patch')
+var Pages = require('./pages.model')
+var tools = require('../../util/tools')
+
+const respondWithResult = (res, statusCode) => {
+    statusCode = statusCode || 200
+    return function (entity) {
+        if (entity) {
+            return res.status(statusCode).json(entity)
+        }
+        return null
+    }
+}
+
+const patchUpdates = (patches) => {
+    return function (entity) {
+        try {
+            jsonpatch.apply(entity, patches, /*validate*/ true)
+        } catch (err) {
+            return Promise.reject(err)
+        }
+
+        return entity.save()
+    }
+}
+
+const removeEntity = (res) => {
+    return function (entity) {
+        if (entity) {
+            return entity.remove()
+                .then(() => {
+                    res.status(204).end()
+                })
+        }
+    }
+}
+
+const handleEntityNotFound = (res) => {
+    return function (entity) {
+        if (!entity) {
+            res.status(404).end()
+            return null
+        }
+        return entity
+    }
+}
+
+const handleError = (res, statusCode) => {
+    statusCode = statusCode || 500
+    return function (err) {
+        res.status(statusCode).send(err)
+    }
+}
+
+module.exports.index = (req, res) => {
+    return Pages.find().exec()
+        .then(respondWithResult(res))
+        .catch(handleError(res))
+}
+
+module.exports.findByLoginId = (req, res) => {
+    var loginId = req.user.loginId
+    var type = req.query.type;
+    return Pages.find({ loginId: loginId, type: type }).exec()
+        .then(respondWithResult(res))
+        .catch(handleError(res))
+}
+
+// Gets a single Pages from the DB
+module.exports.show = (req, res) => {
+    return Pages.findById(req.params.id).exec()
+        .then(handleEntityNotFound(res))
+        .then(respondWithResult(res))
+        .catch(handleError(res))
+}
+
+// Creates a new Pages in the DB
+module.exports.create = (req, res) => {
+    //添加作者信息
+    req.body.loginId = req.user.loginId
+    return Pages.create(req.body)
+        .then(respondWithResult(res, 201))
+        .catch(handleError(res))
+}
+
+// Upserts the given Pages in the DB at the specified ID
+module.exports.update = (req, res) => {
+    if (req.body._id) {
+        delete req.body._id
+    }
+    if (req.body.type === 'h5') {
+        tools.renderFile('template.html', req.body, (html) => {
+            tools.saveFile(req.params.id + '.html', html)
+        })
+    } else if (req.body.type === 'spa') {
+        tools.renderFile('spa.html', req.body, (html) => {
+            tools.saveFile(req.params.id + '.html', html)
+        })
+    }
+
+    return Pages.findOneAndUpdate({ _id: req.params.id }, req.body, { upsert: true, setDefaultsOnInsert: true, runValidators: true }).exec()
+        .then(respondWithResult(res))
+        .catch(handleError(res))
+}
+
+
+// Updates an existing Pages in the DB
+module.exports.patch = (req, res) => {
+    if (req.body._id) {
+        delete req.body._id
+    }
+    return Pages.findById(req.params.id).exec()
+        .then(handleEntityNotFound(res))
+        .then(patchUpdates(req.body))
+        .then(respondWithResult(res))
+        .catch(handleError(res))
+}
+
+// Deletes a Pages from the DB
+module.exports.destroy = (req, res) => {
+    return Pages.findById(req.params.id).exec()
+        .then(handleEntityNotFound(res))
+        .then(removeEntity(res))
+        .catch(handleError(res))
+}

+ 23 - 0
api/pages/pages.model.js

@@ -0,0 +1,23 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+const mongoose = require('mongoose')
+mongoose.Promise = require('bluebird')
+
+var PageSchema = new mongoose.Schema({
+    pages: {
+        type: Array,
+        required: true
+    },
+    title: String,
+    description: String,
+    html: String,
+    createDate: { type: Number, default: new Date().getTime() },
+    loginId: String,
+    type: {
+        type: String, required: true, default: 'h5', enum: ['h5', 'spa'] // 页面是单页还是多页 
+    },
+    canvasHeight: Number
+})
+
+module.exports = mongoose.model('Page', PageSchema)

+ 17 - 0
api/user/index.js

@@ -0,0 +1,17 @@
+/**
+ * Created by zhengguorong on 16/11/1.
+ */
+const express = require('express')
+const controller = require('./user.controller')
+const auth = require('../../auth/auth.service')
+
+var router = new express.Router()
+
+router.get('/', auth.hasRole('admin'), controller.index);
+router.delete('/:id', auth.hasRole('admin'), controller.destroy);
+router.get('/me', auth.isAuthenticated(), controller.me);
+router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);
+router.get('/:id', auth.isAuthenticated(), controller.show);
+router.post('/', controller.create);
+
+module.exports = router

+ 162 - 0
api/user/user.controller.js

@@ -0,0 +1,162 @@
+/**
+ * Created by zhengguorong on 16/11/1.
+ */
+const User = require('./user.model')
+const config = require('../../config')
+const jwt = require('jsonwebtoken')
+
+/**
+ * 处理提交表单验证错误
+ * @param res
+ * @param statusCode
+ * @returns {Function}
+ */
+const validationError = (res, statusCode) => {
+    statusCode = statusCode || 422;
+    return function (err) {
+        return res.status(statusCode).json(err);
+    };
+}
+
+const handleError = (res, statusCode) => {
+    statusCode = statusCode || 500;
+    return function (err) {
+        return res.status(statusCode).send(err);
+    };
+}
+
+module.exports.index = (req, res) => {
+    return User.find({}, '-salt -password').exec()
+        .then(users => {
+            res.status(200).json(users);
+        })
+        .catch(handleError(res));
+}
+
+module.exports.findByToken = (token) => {
+    return User.findOne({ token: token }).exec()
+}
+
+/**
+ * 创建用户
+ * @param req
+ * @param res
+ */
+module.exports.create = (req, res) => {
+    let newUser = new User(req.body)
+    newUser.provider = 'local'
+    newUser.role = 'user'
+    newUser.save()
+        .then((user) => {
+            let token = jwt.sign({ _id: user._id }, config.secrets.session, {
+                expiresIn: 60 * 60 * 5
+            })
+            user.token = token
+            var updateUser = JSON.parse(JSON.stringify(user))
+            delete updateUser._id
+            User.findOneAndUpdate({ _id: user._id }, updateUser).exec()
+            res.json({ token })
+        })
+        .catch(validationError(res))
+}
+
+/**
+ * 获取单个用户信息
+ */
+module.exports.show = (req, res, next) => {
+    let userId = req.params.id
+    return User.findById(userId).exec()
+        .then(user => {
+            if (!user) {
+                return res.status(400).end()
+            }
+            res.json(user.profile)
+        })
+        .catch(err => next(err))
+}
+
+/**
+ * 删除用户
+ * @param req
+ * @param res
+ * @returns {Promise.<TResult>|Promise}
+ */
+module.exports.destroy = (req, res) => {
+    return User.findByIdAndRemove(req.params.id).exec()
+        .then(() => {
+            res.status(204).end()
+        })
+        .catch(handleError(res))
+}
+
+/**
+ * 修改密码
+ * @param req
+ * @param res
+ * @returns {Promise.<TResult>}
+ */
+module.exports.changePassword = (req, res) => {
+    var uesrId = req.user._id
+    var oldPass = String(req.body.oldPassword)
+    var newPass = String(req.body.newPassword)
+    return User.findById(uesrId).exec()
+        .then(user => {
+            if (user.authenticate(oldPass)) {
+                user.password = newPass
+                return user.save()
+                    .then(() => {
+                        res.status(204).end()
+                    })
+                    .catch(validationError(res))
+            } else {
+                return res.status(403).end()
+            }
+        })
+}
+
+
+/**
+ * 用户登陆
+ * @param req
+ * @param res
+ * @returns {Promise.<TResult>}
+ */
+module.exports.login = (req, res) => {
+    var loginId = req.body.loginId
+    var password = req.body.password
+    let token
+    return User.findOne({ loginId: loginId }).exec()
+        .then(user => {
+            if (user && user.authenticate(password)) {
+                token = jwt.sign({ _id: user._id }, config.secrets.session, {
+                    expiresIn: 60 * 60 * 5
+                })
+                user.token = token
+                var updateUser = JSON.parse(JSON.stringify(user))
+                delete updateUser._id
+                User.findOneAndUpdate({ _id: user._id }, updateUser).exec()
+                res.status(200).json({ token }).end()
+            } else {
+                return res.status(401).end()
+            }
+        })
+}
+
+/**
+ * 查看用户信息
+ * @param req
+ * @param res
+ * @param next
+ * @returns {Promise.<TResult>|Promise}
+ */
+module.exports.me = (req, res, next) => {
+    var userId = req.user._id
+    return User.findOne({ _id: userId }, '-salt -password').exec()
+        .then(user => { // don't ever give out the password or salt
+            if (!user) {
+                return res.status(401).end();
+            }
+            res.json(user);
+        })
+        .catch(err => next(err));
+}

+ 196 - 0
api/user/user.model.js

@@ -0,0 +1,196 @@
+const crypto = require('crypto')
+const mongoose = require('mongoose')
+mongoose.Promise = require('bluebird')
+
+var UserSchema = new mongoose.Schema({
+    name: String,
+    loginId: {
+        type: String,
+        lowercase: true,
+        required: true
+    },
+    role: {
+        type: String,
+        default: 'user'
+    },
+    password: {
+        type: String,
+        required: true
+    },
+    provider: String,
+    salt: String,
+    token: String
+});
+
+
+/**
+ * Validations
+ */
+
+// Validate empty email
+UserSchema
+    .path('loginId')
+    .validate((loginId) => {
+        return loginId.length;
+    }, '登陆名不能空');
+
+// Validate empty password
+UserSchema
+    .path('password')
+    .validate((password) => {
+        return password.length;
+    }, '密码不能空');
+
+// Validate loginId is not taken
+UserSchema
+    .path('loginId')
+    .validate(function (value, respond) {
+        return this.constructor.findOne({loginId: value}).exec()
+            .then(user => {
+                if (user) {
+                    if (this.id === user.id) {
+                        return respond(true);
+                    }
+                    return respond(false);
+                }
+                return respond(true);
+            })
+            .catch((err) => {
+                throw err;
+            });
+    }, '该用户已存在');
+
+var validatePresenceOf = (value) => {
+    return value && value.length;
+};
+
+/**
+ * Pre-save hook
+ */
+UserSchema
+    .pre('save', function (next) {
+        // Handle new/update passwords
+        if (!this.isModified('password')) {
+            return next();
+        }
+
+        if (!validatePresenceOf(this.password)) {
+            return next(new Error('密码错误'));
+        }
+
+        // Make salt with a callback
+        this.makeSalt((saltErr, salt) => {
+            if (saltErr) {
+                return next(saltErr);
+            }
+            this.salt = salt;
+            this.encryptPassword(this.password, (encryptErr, hashedPassword) => {
+                if (encryptErr) {
+                    return next(encryptErr);
+                }
+                this.password = hashedPassword;
+                return next();
+            });
+        });
+    });
+
+/**
+ * Methods
+ */
+UserSchema.methods = {
+    /**
+     * Authenticate - check if the passwords are the same
+     *
+     * @param {String} password
+     * @param {Function} callback
+     * @return {Boolean}
+     * @api public
+     */
+    authenticate(password, callback) {
+        if (!callback) {
+            return this.password === this.encryptPassword(password);
+        }
+
+        this.encryptPassword(password, (err, pwdGen) => {
+            if (err) {
+                return callback(err);
+            }
+
+            if (this.password === pwdGen) {
+                return callback(null, true);
+            } else {
+                return callback(null, false);
+            }
+        });
+    },
+
+    /**
+     * Make salt
+     *
+     * @param {Number} [byteSize] - Optional salt byte size, default to 16
+     * @param {Function} callback
+     * @return {String}
+     * @api public
+     */
+    makeSalt(byteSize, callback) {
+        var defaultByteSize = 16;
+
+        if (typeof arguments[0] === 'function') {
+            callback = arguments[0];
+            byteSize = defaultByteSize;
+        } else if (typeof arguments[1] === 'function') {
+            callback = arguments[1];
+        } else {
+            throw new Error('却少回调方法');
+        }
+
+        if (!byteSize) {
+            byteSize = defaultByteSize;
+        }
+
+        return crypto.randomBytes(byteSize, (err, salt) => {
+            if (err) {
+                return callback(err);
+            } else {
+                return callback(null, salt.toString('base64'));
+            }
+        });
+    },
+
+    /**
+     * Encrypt password
+     *
+     * @param {String} password
+     * @param {Function} callback
+     * @return {String}
+     * @api public
+     */
+    encryptPassword(password, callback) {
+        if (!password || !this.salt) {
+            if (!callback) {
+                return null;
+            } else {
+                return callback('却少密码或者加密内容');
+            }
+        }
+
+        var defaultIterations = 10000;
+        var defaultKeyLength = 64;
+        var salt = new Buffer(this.salt, 'base64');
+
+        if (!callback) {
+            return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength)
+                .toString('base64');
+        }
+
+        return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, (err, key) => {
+            if (err) {
+                return callback(err);
+            } else {
+                return callback(null, key.toString('base64'));
+            }
+        });
+    }
+};
+
+module.exports = mongoose.model('User', UserSchema);

+ 65 - 0
app.js

@@ -0,0 +1,65 @@
+/**
+ * Q&A
+ * 如果导出或导入模块?
+ *     因项目没有使用BABEL或者typescrip的转换工具,无法支持import/export的方式
+ *     import/export在ES7才得到支持,就算node7也无法使用
+ *     所以我们使用module.exports来导出require来导入
+ */
+var express = require('express')
+var path = require('path')
+var logger = require('morgan')
+var cookieParser = require('cookie-parser')
+var bodyParser = require('body-parser')
+var mongoose = require('mongoose')
+var ejs = require('ejs')
+var config = require('./config')
+mongoose.Promise = require('bluebird')
+
+mongoose.connect(config.mongo.uri, { user: config.mongo.user, pass: config.mongo.pass })
+mongoose.connection.on('error', function (err) {
+  console.error(`MongoDB connection error: ${err}`)
+  process.exit(-1);
+})
+var app = express()
+
+// view engine setup
+// app.set('views', path.join(__dirname, 'views'))
+// app.set('view engine', 'hbs'
+
+app.engine('.html', ejs.__express);
+app.set('view engine', 'html');
+app.set('views', path.join(__dirname, 'views'))
+
+
+app.all('*', function (req, res, next) {
+  res.header('Access-Control-Allow-Origin', '*');
+  res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
+  res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
+  if (req.method === 'OPTIONS') {
+    res.send(200); /让options请求快速返回/
+  }
+  else {
+    next();
+  }
+});
+
+// uncomment after placing your favicon in /public
+//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
+app.use(logger('dev'))
+app.use(bodyParser.json({ 'limit': '2000kb' }))
+app.use(bodyParser.urlencoded({ extended: false }))
+app.use(cookieParser())
+app.use(express.static(path.join(__dirname, 'public')))
+app.use(express.static(path.join(__dirname, 'webapp/dist')))
+require('./routers')(app)
+
+// error handler
+app.use(function (err, req, res, next) {
+  // set locals, only providing error in development
+  res.locals.message = err.message
+  res.locals.error = req.app.get('env') === 'development' ? err : {}
+  res.status(err.status || 500)
+  res.send(err.message)
+});
+
+module.exports = app

+ 95 - 0
auth/auth.service.js

@@ -0,0 +1,95 @@
+/**
+ * Created by zhengguorong on 16/11/2.
+ * 用户权限认证方法
+ *
+ *  * Q&A
+ * 为什么要使用composable-middleware,为了解决什么问题?
+ *     他的作用是合并两个中间件,让其不需要在挂在在express实例上,例如expressJwt中间件是在执行后操作req对象,在req对象
+ *     上加入user对象,但该中间件未提供回调方法,无法在验证后执行我们的代码,因此需要使用composable插件来完成两个中间件的
+ *     合并.
+ *     当然,你也可以像官方提供示例一样,router.get('/',jwtvalidate,function(req,res,next){req.user})获取结果,但是
+ *     我的路由第三个参数主要执行数据库相关操作,不想引入验证逻辑,所以在第二个参数这里完成权限的认证.
+ *
+ */
+const jwt = require('jsonwebtoken')
+const expressJwt = require('express-jwt')
+const config = require('../config')
+const compose = require('composable-middleware')
+const User = require('../api/user/user.model')
+const UserController = require('../api/user/user.controller')
+
+const validateJwt = expressJwt({
+    secret: config.secrets.session
+})
+
+/**
+ * 验证用户是否有权限操作
+ * @returns {function()}
+ */
+module.exports.isAuthenticated = () => {
+    return compose()
+        .use(function (req, res, next) {
+            // allow access_token to be passed through query parameter as well
+            if (req.query && req.query.hasOwnProperty('access_token')) {
+                req.headers.authorization = `Bearer ${req.query.access_token}`;
+            }
+            if(req.body && req.body.hasOwnProperty('access_token')) {
+                req.headers.authorization = `Bearer ${req.body.access_token}`;
+            }
+            // IE11 forgets to set Authorization header sometimes. Pull from cookie instead.
+            if (req.query && typeof req.headers.authorization === 'undefined') {
+                req.headers.authorization = `Bearer ${req.cookies.token}`;
+            }
+            //验证是否服务端生成的token
+            var token = req.headers.authorization.split('Bearer ')[1]
+            UserController.findByToken(token).then((user) => {
+                if (user) {
+                    //验证token是否过期
+                    validateJwt(req, res, next);
+                }else{
+                    return res.status(401).end();
+                }
+            })
+
+        })
+        // Attach user to request
+        .use(function (req, res, next) {
+            User.findById(req.user._id).exec()
+                .then(user => {
+                    if (!user) {
+                        return res.status(401).end();
+                    }
+                    req.user = user;
+                    next();
+                })
+                .catch(err => next(err));
+        });
+}
+
+module.exports.hasRole = (roleRequired) => {
+    if (!roleRequired) {
+        throw new Error('必须输入身份名称');
+    }
+
+    return compose()
+        .use(this.isAuthenticated())
+        .use(function meetsRequirements(req, res, next) {
+            if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
+                return next();
+            } else {
+                return res.status(403).send('没有访问权限');
+            }
+        });
+}
+
+/**
+ * 返回一个JWT TOKEN
+ * @param id 用户ID
+ * @param role 用户权限
+ * @returns {*} JWT TOKEN
+ */
+module.exports.signToken = (id, role) => {
+    return jwt.sign({_id: id, role}, config.secrets.session, {
+        expiresIn: 60 * 60 * 5
+    })
+}

+ 22 - 0
auth/index.js

@@ -0,0 +1,22 @@
+/**
+ * Created by zhengguorong on 16/11/2.
+ */
+const express = require('express')
+const User = require('../api/user/user.controller')
+
+const router = express.Router()
+
+/**
+ * 用户登录
+ */
+router.post('/login', (req, res, next) => {
+    User.login(req, res)
+})
+/**
+ * 用户注册
+ */
+router.post('/register', (req, res, next) =>{
+    User.create(req, res)
+})
+
+module.exports = router

+ 89 - 0
bin/www

@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var app = require('../app');
+var debug = require('debug')('bmblog:server');
+var http = require('http');
+/**
+ * Get port from environment and store in Express.
+ */
+console.log(process.env.NODE_ENV, '当前环境');
+var port = normalizePort(process.env.PORT || '3000');
+app.set('port', port);
+
+/**
+ * Create HTTP server.
+ */
+
+var server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+  var port = parseInt(val, 10);
+
+  if (isNaN(port)) {
+    // named pipe
+    return val;
+  }
+
+  if (port >= 0) {
+    // port number
+    return port;
+  }
+
+  return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  var bind = typeof port === 'string'
+    ? 'Pipe ' + port
+    : 'Port ' + port;
+
+  // handle specific listen errors with friendly messages
+  switch (error.code) {
+    case 'EACCES':
+      console.error(bind + ' requires elevated privileges');
+      process.exit(1);
+      break;
+    case 'EADDRINUSE':
+      console.error(bind + ' is already in use');
+      process.exit(1);
+      break;
+    default:
+      throw error;
+  }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+  var addr = server.address();
+  var bind = typeof addr === 'string'
+    ? 'pipe ' + addr
+    : 'port ' + addr.port;
+  debug('Listening on ' + bind);
+}

+ 16 - 0
components/errors/index.js

@@ -0,0 +1,16 @@
+let pageNotFound = (req, res) => {
+    var viewFilePath = '404'
+    var statusCode = 404
+    var result = {
+        status: statusCode
+    }
+    res.status(result.status)
+    res.render(viewFilePath, {}, function(err, html) {
+        if(err) {
+            return res.status(result.status).json(result)
+        }
+        res.send(html)
+    })
+}
+
+module.exports[404] = pageNotFound

+ 14 - 0
config/index.dev.js

@@ -0,0 +1,14 @@
+const all = {
+    port: 9000,
+    ip: process.env.ip || '0.0.0.0',
+    secrets: {
+        session: 'h5maker'
+    },
+    mongo: {
+        uri: 'mongodb://192.168.234.28:27017/h5maker',
+        user: 'h5maker',
+        pass: 'xgd$MPB37@8GALX#'
+    },
+    userRoles: ['guest', 'user', 'admin']
+}
+module.exports = all

+ 7 - 0
config/index.js

@@ -0,0 +1,7 @@
+if (process.env.NODE_ENV === 'production') {
+    module.exports = require('./index.pro.js')
+} else if(process.env.NODE_ENV === 'development') {
+    module.exports = require('./index.dev.js')
+} else {
+    module.exports = require('./index.local.js')
+}

+ 12 - 0
config/index.local.js

@@ -0,0 +1,12 @@
+const all = {
+    port: 9000,
+    ip: process.env.ip || '0.0.0.0',
+    secrets: {
+        session: 'h5maker'
+    },
+    mongo: {
+        uri: 'mongodb://127.0.0.1:27017/h5maker'
+    },
+    userRoles: ['guest', 'user', 'admin']
+}
+module.exports = all

+ 14 - 0
config/index.pro.js

@@ -0,0 +1,14 @@
+const all = {
+    port: 9000,
+    ip: process.env.ip || '0.0.0.0',
+    secrets: {
+        session: 'h5maker'
+    },
+    mongo: {
+        uri: 'mongodb://192.168.32.45:27017/h5maker',
+        user: 'h5maker',
+        pass: 'xgd$MPB37@8GALX#'
+    },
+    userRoles: ['guest', 'user', 'admin']
+}
+module.exports = all

+ 14 - 0
controller/pages.controller.js

@@ -0,0 +1,14 @@
+/**
+ * Created by zhengguorong on 16/11/4.
+ */
+var Pages = require('../api/pages/pages.model')
+
+module.exports.findByLoginId = (req, res) => {
+    var loginId = req.user.loginId
+    return Pages.find({ loginId: loginId }).exec()
+}
+
+// Gets a single Pages from the DB
+module.exports.findById = (id) => {
+    return Pages.findById(id).exec()
+}

+ 115 - 0
package.json

@@ -0,0 +1,115 @@
+{
+  "name": "h5maker",
+  "version": "1.0.0",
+  "description": "h5编辑器",
+  "author": "郑国榕 <zhengguorong@bluemoon.com.cn>",
+  "private": true,
+  "scripts": {
+    "start": "NODE_ENV=production node ./bin/www",
+    "dev": "NODE_ENV=development node ./bin/www",
+    "local": "NODE_ENV=localhost node ./bin/www",
+    "build": "cd webapp && node build/build.js",
+    "webapp": "cd webapp && node build/dev-server.js"
+  },
+  "dependencies": {
+    "art-template": "^3.0.3",
+    "bluebird": "^3.4.6",
+    "body-parser": "~1.15.2",
+    "composable-middleware": "^0.3.0",
+    "cookie-parser": "~1.4.3",
+    "debug": "~2.2.0",
+    "ejs": "^2.5.3",
+    "express": "~4.14.0",
+    "express-jwt": "^5.1.0",
+    "fast-json-patch": "^1.1.1",
+    "hbs": "~4.0.1",
+    "jsonwebtoken": "^7.1.9",
+    "less-middleware": "~2.2.0",
+    "mkdirp": "^0.5.1",
+    "mongoose": "^4.6.5",
+    "morgan": "~1.7.0",
+    "node-uuid": "^1.4.7",
+    "serve-favicon": "~2.3.0",
+    "animate.css": "^3.5.2",
+    "axios": "^0.15.3",
+    "element-ui": "^1.3.3",
+    "fastclick": "^1.0.6",
+    "github-markdown-css": "^2.4.1",
+    "lrz": "^4.9.40",
+    "marked": "^0.3.6",
+    "moment": "^2.15.2",
+    "qrcode": "^0.8.1",
+    "vue": "^2.3.3",
+    "vue-router": "^2.5.3",
+    "vuex": "^2.3.0"
+  },
+  "devDependencies": {
+    "eslint-plugin-html": "^1.5.5",
+    "autoprefixer": "^6.4.0",
+    "babel-core": "^6.0.0",
+    "babel-eslint": "^7.0.0",
+    "babel-loader": "^6.0.0",
+    "babel-plugin-transform-runtime": "^6.0.0",
+    "babel-polyfill": "^6.16.0",
+    "babel-preset-es2015": "^6.0.0",
+    "babel-preset-stage-2": "^6.0.0",
+    "babel-register": "^6.0.0",
+    "chai": "^3.5.0",
+    "chalk": "^1.1.3",
+    "chromedriver": "^2.21.2",
+    "connect-history-api-fallback": "^1.1.0",
+    "cross-spawn": "^4.0.2",
+    "css-loader": "^0.26.1",
+    "element-ui": "^1.0.6",
+    "eslint": "^3.7.1",
+    "eslint-config-standard": "^6.1.0",
+    "eslint-friendly-formatter": "^2.0.5",
+    "eslint-loader": "^1.5.0",
+    "eslint-plugin-html": "^1.3.0",
+    "eslint-plugin-promise": "*",
+    "eslint-plugin-standard": "^2.0.1",
+    "eventsource-polyfill": "^0.9.6",
+    "express": "^4.13.3",
+    "extract-text-webpack-plugin": "^1.0.1",
+    "file-loader": "^0.9.0",
+    "function-bind": "^1.0.2",
+    "html-webpack-plugin": "^2.8.1",
+    "http-proxy-middleware": "^0.17.2",
+    "inject-loader": "^2.0.1",
+    "isparta-loader": "^2.0.0",
+    "json-loader": "^0.5.4",
+    "karma": "^1.3.0",
+    "karma-coverage": "^1.1.1",
+    "karma-mocha": "^1.2.0",
+    "karma-phantomjs-launcher": "^1.0.0",
+    "karma-sinon-chai": "^1.2.0",
+    "karma-sourcemap-loader": "^0.3.7",
+    "karma-spec-reporter": "0.0.26",
+    "karma-webpack": "^1.7.0",
+    "less": "^2.7.1",
+    "less-loader": "^2.2.3",
+    "lolex": "^1.4.0",
+    "mocha": "^3.1.0",
+    "nightwatch": "^0.9.9",
+    "node-sass": "^4.0.0",
+    "normalize.css": "^5.0.0",
+    "opn": "^4.0.2",
+    "ora": "^0.3.0",
+    "phantomjs-prebuilt": "*",
+    "promise-polyfill": "^6.0.2",
+    "sass-loader": "^4.1.0",
+    "selenium-server": "2.53.1",
+    "semver": "^5.3.0",
+    "shelljs": "^0.7.4",
+    "sinon": "^2.2.0",
+    "sinon-chai": "^2.8.0",
+    "url-loader": "^0.5.7",
+    "vue-loader": "^10.0.0",
+    "vue-style-loader": "^1.0.0",
+    "vue-template-compiler": "^2.1.4",
+    "webpack": "^1.13.2",
+    "webpack-dev-middleware": "^1.8.3",
+    "webpack-hot-middleware": "^2.12.2",
+    "webpack-merge": "^0.14.1"
+  }
+}

+ 131 - 0
public/css/main.css

@@ -0,0 +1,131 @@
+/* 2017/01/10 */
+*, *:before, *:after {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+}
+a:hover, a:visited, a:link, a:active {
+    text-decoration: none;
+}
+.resize-container {
+    overflow: hidden;
+}
+
+.content-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    z-index: 10;
+}
+
+.ele {
+    position: absolute;
+}
+
+.ele-bg {
+    height: 100%;
+    width: auto;
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translateX(-50%);
+    z-index: 0;    
+}
+
+.ele-img {
+    display: block;
+    width: 100%;
+    height: 100%;
+}
+
+.mediate {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    margin: auto;
+}
+
+.loop {
+    animation-iteration-count: infinite;
+}
+
+.flat {
+    transform-style: flat;
+}
+
+#wx_pic {
+    display: none;
+}
+
+.button-next {
+    position: absolute;
+    left: 50%;
+    bottom: 20px;
+    width: 40px;
+    height: 20px;
+    margin-left: -20px;
+    z-index: 10;
+    cursor: pointer;
+    animation: buttonNext 2s infinite;
+}
+
+@keyframes buttonNext {
+    0% {
+        transform: translateY(-5px);
+        opacity: .8;
+    }
+    50% {
+        transform: translateY(10px);
+        opacity: 1;
+    }
+    100% {
+        transform: translateY(-5px);
+    }
+}
+
+.button-next.swiper-button-disabled {
+    display: none;
+}
+
+.swiper-container {
+    opacity: 0;
+    transition: opacity 1s;
+}
+
+#loadingCont {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 0;
+    background-color: #fff;
+    transition: opacity 1s;
+}
+
+#loadingSvg {
+    width: 100px;
+    height: 100px;
+    transform: rotate(-90deg);
+}
+
+#loadingCircle {
+    fill: transparent;
+    stroke: #0074d9;
+    stroke-width: 50px;
+    transition: stroke-dashoffset 0.1s;
+}
+
+#loadingTxt {
+    color: #b4b4b4;
+    font-size: 10px;
+    width: 4em;
+    height: 1em;
+    text-align: center;
+    top: 120px;
+}

+ 0 - 0
public/font/Cocogoose.otf


+ 56 - 0
public/js/main.js

@@ -0,0 +1,56 @@
+var swiper = new Swiper('.swiper-container', {
+  direction: 'vertical',
+  mousewheelControl: true,
+  nextButton:'.button-next',
+  onInit: function (swiper) {
+    swiperAnimate(swiper);
+  },
+  onSlideChangeEnd: function (swiper) {
+    swiperAnimate(swiper);
+  },
+})
+
+var loader = (function () {
+  var loadingContainer = document.getElementById('loadingCont')
+  var loadingTxt = document.getElementById('loadingTxt')
+  var loadingCircle = document.getElementById('loadingCircle')
+  var loadingCircleP = loadingCircle.getAttribute('stroke-dasharray')
+  var imgs = document.getElementsByTagName('img')
+  var srcList = []
+  var imgList = []
+  var imgSrc, i
+  for (i = 0; i < imgs.length; i++) {
+    imgSrc = imgs[i].getAttribute('pre-src')
+    if (imgSrc) {
+      srcList.push(imgSrc)
+      imgList.push(imgs[i])
+    }
+  }
+  if (srcList.length === 0) {
+      swiper.container[0].style.opacity = 1
+      loadingContainer.style.opacity = 0
+      swiperAnimate(swiper)
+  }
+  return new resLoader({
+    resources: srcList,
+    onStart: function (total) {
+      console.log('start:' + total)
+    },
+    onProgress: function (current, total) {
+      console.log(current + '/' + total)
+      var p = current / total
+      loadingTxt.textContent = Math.round(p * 100) + '%'
+      loadingCircle.style.strokeDashoffset = (1 - p) * loadingCircleP
+    },
+    onComplete: function (total) {
+      console.log('加载完毕:' + total + '个资源')
+      for (i = 0; i < imgList.length; i++) {
+        imgList[i].setAttribute('src', srcList[i])
+      }
+      swiper.container[0].style.opacity = 1
+      loadingContainer.style.opacity = 0
+      swiperAnimate(swiper)
+    }
+  })
+})()
+loader.start()

File diff suppressed because it is too large
+ 2489 - 0
public/libs/css/animate.css


File diff suppressed because it is too large
+ 6 - 0
public/libs/css/animate.min.css


File diff suppressed because it is too large
+ 15 - 0
public/libs/css/swiper.min.css


File diff suppressed because it is too large
+ 1 - 0
public/libs/js/maps/swiper.jquery.min.js.map


File diff suppressed because it is too large
+ 1 - 0
public/libs/js/maps/swiper.min.js.map


+ 76 - 0
public/libs/js/resLoader.js

@@ -0,0 +1,76 @@
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        //AMD
+        define(factory);
+    } else if (typeof exports === 'object') {
+        //Node, CommonJS之类的
+        module.exports = factory();
+    } else {
+        //浏览器全局变量(root 即 window)
+        root.resLoader = factory(root);
+    }
+}(this, function () {
+    var isFunc = function(f){
+        return typeof f === 'function';
+    }
+    //构造器函数
+    function resLoader(config){
+        this.option = {
+            resourceType : 'image', //资源类型,默认为图片
+            baseUrl : './', //基准url
+            resources : [], //资源路径数组
+            onStart : null, //加载开始回调函数,传入参数total
+            onProgress : null, //正在加载回调函数,传入参数currentIndex, total
+            onComplete : null //加载完毕回调函数,传入参数total
+        }
+        if(config){
+            for(i in config){
+                this.option[i] = config[i];
+            }
+        }
+        else{
+            alert('参数错误!');
+            return;
+        }
+        this.status = 0; //加载器的状态,0:未启动   1:正在加载   2:加载完毕
+        this.total = this.option.resources.length || 0; //资源总数
+        this.currentIndex = 0; //当前正在加载的资源索引
+    };
+
+    resLoader.prototype.start = function(){
+        this.status = 1;
+        var _this = this;
+        var baseUrl = this.option.baseUrl;
+        for(var i=0,l=this.option.resources.length; i<l; i++){
+            var r = this.option.resources[i], url = '';
+            if(/^(\/|http(s?):\/\/)/.test(r)){
+                url = r;
+            } else {
+                url = baseUrl + r;
+            }
+
+            var image = new Image();
+            image.onload = function(){_this.loaded();};
+            image.onerror = function(){_this.loaded();};
+            image.src = url;
+        }
+        if(isFunc(this.option.onStart)){
+            this.option.onStart(this.total);
+        }
+    }
+
+    resLoader.prototype.loaded = function(){
+        if(isFunc(this.option.onProgress)){
+            this.option.onProgress(++this.currentIndex, this.total);
+        }
+        //加载完毕
+        if(this.currentIndex===this.total){
+            if(isFunc(this.option.onComplete)){
+                this.option.onComplete(this.total);
+            }
+        }
+    }
+
+    //暴露公共方法
+    return resLoader;
+}));

+ 43 - 0
public/libs/js/resize.js

@@ -0,0 +1,43 @@
+/**
+ * Created by zhengguorong on 2016/11/9.
+ */
+
+
+//设计图宽高比
+var designwhScale = 320/508;
+//现窗口宽高比
+var curwhScale = window.innerWidth/window.innerHeight;
+
+var resizes = document.querySelectorAll('.resize');
+
+//外层容器定位
+var swiperContainer = document.querySelector('.container');
+var containerWidth = 320;
+var containerHeight = 508;
+var containerTop = 0;
+var containerLeft = 0;
+if (curwhScale < designwhScale) {
+	containerWidth = window.innerWidth
+	containerHeight = window.innerWidth / designwhScale
+	containerTop = (window.innerHeight - containerHeight)/2
+}else {
+	containerWidth = window.innerHeight * designwhScale;
+	containerHeight = window.innerHeight;
+	containerLeft = (window.innerWidth - containerWidth)/2
+}
+swiperContainer.style.width = containerWidth;
+swiperContainer.style.height = containerHeight;
+swiperContainer.style.left = containerLeft;
+swiperContainer.style.top = containerTop;
+
+ //放大比例
+var scale = containerWidth / 320 
+//元素缩放
+for (var j = 0; j < resizes.length; j++) {
+	resizes[j].style.width = parseInt(resizes[j].style.width) * scale + 'px';
+	resizes[j].style.height = parseInt(resizes[j].style.height) * scale + 'px';
+	resizes[j].style.left = parseInt(resizes[j].style.left) * scale + 'px';
+	resizes[j].style.top = parseInt(resizes[j].style.top) * scale + 'px';
+	resizes[j].style.right = parseInt(resizes[j].style.right) * scale + 'px';
+}
+

+ 25 - 0
public/libs/js/resizeBak.js

@@ -0,0 +1,25 @@
+(function () {
+  var designW = 320
+  var designH = 508
+  var designR = designW / designH
+
+  var actualW = document.documentElement.clientWidth
+  var actualH = document.documentElement.clientHeight
+  var actualR = actualW / actualH
+
+  var scale = (actualR > designR) ? (actualH / designH) : (actualW / designW)
+  var style = 'width: ' + designW + 'px; height: ' + designH + 'px; -webkit-transform: scale(' + scale + '); transform: scale(' + scale + '); '
+  if (actualW < designW) {
+    if (actualR > designR) {
+      style += 'margin-left: ' + (actualW - designW) / 2 + 'px;'
+    } else {
+      style += '-webkit-transform-origin: left; transform-origin: left;'
+    }
+  }
+
+  var containers = document.getElementsByClassName('resize-container')
+  var i
+  for (i = 0; i < containers.length; i++) {
+    containers[i].setAttribute('style', style)
+  }
+})()

File diff suppressed because it is too large
+ 2 - 0
public/libs/js/swiper.animate.min.js


File diff suppressed because it is too large
+ 17 - 0
public/libs/js/swiper.min.js


File diff suppressed because it is too large
+ 87 - 0
public/pages/592388b5d0d23c27aa555692.html


+ 66 - 0
public/pages/592388b9d0d23c27aa555693.html

@@ -0,0 +1,66 @@
+<html>
+
+<head>
+  <meta charset="UTF-8">
+  <title>蓝月亮</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+  <!-- build:css css/vendor.css -->
+  <link rel="stylesheet" href="/css/main.css">
+  <!-- endbuild -->
+</head>
+
+<body>
+  <div id="wx_pic"><img src=""></div>
+  <div class="content-container">
+        
+            
+              
+            
+            <div class="resize-container mediate flat">
+              <section style="height:5204px">
+              
+                
+                
+                  <div class="ele" style="z-index: 1; width: 320px; left: 3px; top: 108px;">
+                    <div>
+                        <div style="opacity: 100; transform: rotate(0deg); color: #000000; text-align: center; line-height: 1.5; font-family: 微软雅黑; font-size: 32px; white-space: pre-line;font-weight: bold">请输入文本</div>
+                    </div>
+                  </div>
+                
+              
+        </section>
+        
+      </div>
+  </div>
+  <!-- build:js scripts/vendor.js -->
+  <script>
+  (function () {
+  var designW = 320
+  var designH = 508
+  var designR = designW / designH
+
+  var actualW = document.documentElement.clientWidth
+  var actualH = document.documentElement.clientHeight
+  var actualR = actualW / actualH
+
+  var scale = (actualR > designR) ? (actualH / designH) : (actualW / designW)
+  var style = 'width: ' + designW + 'px;overflow: auto; -webkit-transform: scale(' + scale + '); transform: scale(' + scale + '); '
+  if (actualW < designW) {
+    if (actualR > designR) {
+      style += 'margin-left: ' + (actualW - designW) / 2 + 'px;'
+    } else {
+      style += '-webkit-transform-origin: left; transform-origin: left;'
+    }
+  }
+
+  var containers = document.getElementsByClassName('resize-container')
+  var i
+  for (i = 0; i < containers.length; i++) {
+    containers[i].setAttribute('style', style)
+  }
+})()
+  </script>
+  <!-- endbuild -->
+</body>
+
+</html>

+ 19 - 0
render/preview.js

@@ -0,0 +1,19 @@
+var Pages = require('../api/pages/pages.model')
+var pageController = require('../controller/pages.controller')
+
+const render = (req, res) => {
+    const id = req.params.id
+    if (id) {
+        pageController.findById(id)
+        .then(function (entity) {
+            res.render('template', entity);
+        })
+        .catch ((err) => {
+            res.render('error',{message: '找不到数据', error: err})
+        })
+    }else {
+        res.render('error',{message: '请加入查询ID', error: {}})
+    }
+
+}
+module.exports = render

+ 20 - 0
routers.js

@@ -0,0 +1,20 @@
+var errors = require('./components/errors')
+
+module.exports = function (app) {
+  app.use('/api/users', require('./api/user'));
+  app.use('/api/pages', require('./api/pages'));
+  app.use('/api/upload', require('./api/file'));
+  app.use('/auth', require('./auth'))
+  // 404错误处理
+  app.route('/:url(api|auth|components|app|bower_components|assets)/*')
+    .get(errors[404]);
+
+  // 前端页面渲染路由
+  app.route('/perview/:id').get(require('./render/preview'))
+
+  // 其他资源路由
+  app.route('/*')
+    .get((req, res) => {
+      res.render('index')
+    });
+}

+ 48 - 0
util/tools.js

@@ -0,0 +1,48 @@
+/**
+ * Created by zhengguorong on 2016/11/30.
+ */
+var fs = require('fs')
+var mkdirp = require('mkdirp')
+var path = require('path')
+var ejs = require('ejs')
+var fs = require('fs')
+
+const base64ToImg = (imgData, filePath) => {
+    var base64Data = imgData.replace(/^data:image\/\w+;base64,/, "")
+    var dataBuffer = new Buffer(base64Data, 'base64')
+    var fileDir = path.dirname(filePath)
+    mkdirp(fileDir, (err) => {
+        fs.writeFile(filePath, dataBuffer, (err) => {
+        })
+    })
+}
+const renderFile = (filePath, data, successCallback) => {
+    var rootPath = path.join(__dirname, '../views/')
+    fs.readFile(rootPath + filePath, { flag: 'r+', encoding: 'utf8' }, function (err, result) {
+        if (err) {
+            console.log(err)
+            return;
+        }
+        let html = ejs.render(result, data)
+        successCallback(html)
+    });
+}
+const saveFile = (filePath, data, successCallback) => {
+    var rootPath = path.join(__dirname, '../public/pages/')
+    mkdirp(rootPath, (err) => {
+        fs.writeFile(rootPath + filePath, data, function (err) {
+            if (err) {
+                console.error(err);
+            } else {
+                successCallback && successCallback()
+            }
+        });
+    })
+
+}
+
+module.exports = {
+    base64ToImg,
+    renderFile,
+    saveFile
+}

+ 10 - 0
views/404.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+
+</body>
+</html>

+ 11 - 0
views/error.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>出错了</title>
+</head>
+<body>
+<%=message%>
+<p>详情:<%=JSON.stringify(error)%></p>
+</body>
+</html>

File diff suppressed because it is too large
+ 74 - 0
views/spa.html


File diff suppressed because it is too large
+ 73 - 0
views/template.html


BIN
webapp/.DS_Store


+ 35 - 0
webapp/build/build.js

@@ -0,0 +1,35 @@
+// https://github.com/shelljs/shelljs
+require('shelljs/global')
+env.NODE_ENV = 'production'
+
+var path = require('path')
+var config = require('../config')
+var ora = require('ora')
+var webpack = require('webpack')
+var webpackConfig = require('./webpack.prod.conf')
+
+console.log(
+  '  Tip:\n' +
+  '  Built files are meant to be served over an HTTP server.\n' +
+  '  Opening index.html over file:// won\'t work.\n'
+)
+
+var spinner = ora('building for production...')
+spinner.start()
+
+var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
+rm('-rf', assetsPath)
+mkdir('-p', assetsPath)
+cp('-R', 'static/*', assetsPath)
+
+webpack(webpackConfig, function (err, stats) {
+  spinner.stop()
+  if (err) throw err
+  process.stdout.write(stats.toString({
+    colors: true,
+    modules: false,
+    children: false,
+    chunks: false,
+    chunkModules: false
+  }) + '\n')
+})

+ 9 - 0
webapp/build/dev-client.js

@@ -0,0 +1,9 @@
+/* eslint-disable */
+require('eventsource-polyfill')
+var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
+
+hotClient.subscribe(function (event) {
+  if (event.action === 'reload') {
+    window.location.reload()
+  }
+})

+ 71 - 0
webapp/build/dev-server.js

@@ -0,0 +1,71 @@
+var config = require('../config')
+if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env
+var path = require('path')
+var express = require('express')
+var webpack = require('webpack')
+var opn = require('opn')
+var proxyMiddleware = require('http-proxy-middleware')
+var webpackConfig = require('./webpack.dev.conf')
+
+// default port where dev server listens for incoming traffic
+var port = process.env.PORT || config.dev.port
+// Define HTTP proxies to your custom API backend
+// https://github.com/chimurai/http-proxy-middleware
+var proxyTable = config.dev.proxyTable
+
+var app = express()
+var compiler = webpack(webpackConfig)
+
+var devMiddleware = require('webpack-dev-middleware')(compiler, {
+  publicPath: webpackConfig.output.publicPath,
+  stats: {
+    colors: true,
+    chunks: false
+  }
+})
+
+var hotMiddleware = require('webpack-hot-middleware')(compiler)
+// force page reload when html-webpack-plugin template changes
+compiler.plugin('compilation', function (compilation) {
+  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
+    hotMiddleware.publish({ action: 'reload' })
+    cb()
+  })
+})
+
+// proxy api requests
+Object.keys(proxyTable).forEach(function (context) {
+  var options = proxyTable[context]
+  if (typeof options === 'string') {
+    options = { target: options }
+  }
+  app.use(proxyMiddleware(context, options))
+})
+
+// handle fallback for HTML5 history API
+app.use(require('connect-history-api-fallback')())
+
+// serve webpack bundle output
+app.use(devMiddleware)
+
+// enable hot-reload and state-preserving
+// compilation error display
+app.use(hotMiddleware)
+
+// serve pure static assets
+var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
+app.use(staticPath, express.static('./static'))
+
+module.exports = app.listen(port, function (err) {
+  if (err) {
+    console.log(err)
+    return
+  }
+  var uri = 'http://localhost:' + port
+  console.log('Listening at ' + uri + '\n')
+
+  // when env is testing, don't need open it
+  if (process.env.NODE_ENV !== 'testing') {
+    opn(uri)
+  }
+})

+ 61 - 0
webapp/build/utils.js

@@ -0,0 +1,61 @@
+var path = require('path')
+var config = require('../config')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+
+exports.assetsPath = function (_path) {
+  var assetsSubDirectory = process.env.NODE_ENV === 'production'
+    ? config.build.assetsSubDirectory
+    : config.dev.assetsSubDirectory
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loaders) {
+    var sourceLoader = loaders.map(function (loader) {
+      var extraParamChar
+      if (/\?/.test(loader)) {
+        loader = loader.replace(/\?/, '-loader?')
+        extraParamChar = '&'
+      } else {
+        loader = loader + '-loader'
+        extraParamChar = '?'
+      }
+      return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
+    }).join('!')
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
+    } else {
+      return ['vue-style-loader', sourceLoader].join('!')
+    }
+  }
+
+  // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(['css']),
+    postcss: generateLoaders(['css']),
+    less: generateLoaders(['css', 'less']),
+    sass: generateLoaders(['css', 'sass?indentedSyntax']),
+    scss: generateLoaders(['css', 'sass']),
+    stylus: generateLoaders(['css', 'stylus']),
+    styl: generateLoaders(['css', 'stylus'])
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+  var output = []
+  var loaders = exports.cssLoaders(options)
+  for (var extension in loaders) {
+    var loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      loader: loader
+    })
+  }
+  return output
+}

+ 94 - 0
webapp/build/webpack.base.conf.js

@@ -0,0 +1,94 @@
+var path = require('path')
+var config = require('../config')
+var utils = require('./utils')
+var projectRoot = path.resolve(__dirname, '../')
+
+var env = process.env.NODE_ENV
+// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
+// various preprocessor loaders added to vue-loader at the end of this file
+var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
+var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
+var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
+
+module.exports = {
+  entry: {
+    app: './src/main.js'
+  },
+  output: {
+    path: config.build.assetsRoot,
+    publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
+    filename: '[name].js'
+  },
+  resolve: {
+    extensions: ['', '.js', '.vue'],
+    fallback: [path.join(__dirname, '../node_modules')],
+    alias: {
+      'vue$': 'vue/dist/vue',
+      'src': path.resolve(__dirname, '../src'),
+      'assets': path.resolve(__dirname, '../src/assets'),
+      'components': path.resolve(__dirname, '../src/components')
+    }
+  },
+  resolveLoader: {
+    fallback: [path.join(__dirname, '../node_modules')]
+  },
+  module: {
+    preLoaders: [
+      {
+        test: /\.vue$/,
+        loader: 'eslint',
+        include: projectRoot,
+        exclude: /node_modules/
+      },
+      {
+        test: /\.js$/,
+        loader: 'eslint',
+        include: projectRoot,
+        exclude: /node_modules/
+      }
+    ],
+    loaders: [
+      {
+        test: /\.vue$/,
+        loader: 'vue'
+      },
+      {
+        test: /\.js$/,
+        loader: 'babel',
+        include: projectRoot,
+        exclude: /node_modules/
+      },
+      {
+        test: /\.json$/,
+        loader: 'json'
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[hash:7].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url',
+        query: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+        }
+      }
+    ]
+  },
+  eslint: {
+    formatter: require('eslint-friendly-formatter')
+  },
+  vue: {
+    loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }),
+    postcss: [
+      require('autoprefixer')({
+        browsers: ['last 2 versions']
+      })
+    ]
+  }
+}

+ 34 - 0
webapp/build/webpack.dev.conf.js

@@ -0,0 +1,34 @@
+var config = require('../config')
+var webpack = require('webpack')
+var merge = require('webpack-merge')
+var utils = require('./utils')
+var baseWebpackConfig = require('./webpack.base.conf')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+
+// add hot-reload related code to entry chunks
+Object.keys(baseWebpackConfig.entry).forEach(function (name) {
+  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
+})
+
+module.exports = merge(baseWebpackConfig, {
+  module: {
+    loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
+  },
+  // eval-source-map is faster for development
+  devtool: '#eval-source-map',
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': config.dev.env
+    }),
+    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
+    new webpack.optimize.OccurenceOrderPlugin(),
+    new webpack.HotModuleReplacementPlugin(),
+    new webpack.NoErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: 'index.html',
+      template: 'index.html',
+      inject: true
+    })
+  ]
+})

+ 98 - 0
webapp/build/webpack.prod.conf.js

@@ -0,0 +1,98 @@
+var path = require('path')
+var config = require('../config')
+var utils = require('./utils')
+var webpack = require('webpack')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var env = config.build.env
+
+var webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
+  },
+  devtool: config.build.productionSourceMap ? '#source-map' : false,
+  output: {
+    path: config.build.assetsRoot,
+    filename: utils.assetsPath('js/[name].[chunkhash].js'),
+    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
+  },
+  vue: {
+    loaders: utils.cssLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true
+    })
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {
+        warnings: false
+      }
+    }),
+    new webpack.optimize.OccurenceOrderPlugin(),
+    // extract css into its own file
+    new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
+    // generate dist index.html with correct asset hash for caching.
+    // you can customize output by editing /index.html
+    // see https://github.com/ampedandwired/html-webpack-plugin
+    new HtmlWebpackPlugin({
+      filename: config.build.index,
+      template: 'index.html',
+      inject: true,
+      minify: {
+        removeComments: true,
+        collapseWhitespace: true,
+        removeAttributeQuotes: true
+        // more options:
+        // https://github.com/kangax/html-minifier#options-quick-reference
+      },
+      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+      chunksSortMode: 'dependency'
+    }),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf(
+            path.join(__dirname, '../node_modules')
+          ) === 0
+        )
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'manifest',
+      chunks: ['vendor']
+    })
+  ]
+})
+
+if (config.build.productionGzip) {
+  var CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+  webpackConfig.plugins.push(
+    new CompressionWebpackPlugin({
+      asset: '[path].gz[query]',
+      algorithm: 'gzip',
+      test: new RegExp(
+        '\\.(' +
+        config.build.productionGzipExtensions.join('|') +
+        ')$'
+      ),
+      threshold: 10240,
+      minRatio: 0.8
+    })
+  )
+}
+
+module.exports = webpackConfig

+ 6 - 0
webapp/config/dev.env.js

@@ -0,0 +1,6 @@
+var merge = require('webpack-merge')
+var prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"'
+})

+ 32 - 0
webapp/config/index.js

@@ -0,0 +1,32 @@
+// see http://vuejs-templates.github.io/webpack for documentation.
+var path = require('path')
+
+module.exports = {
+  build: {
+    env: require('./prod.env'),
+    index: path.resolve(__dirname, '../dist/index.html'),
+    assetsRoot: path.resolve(__dirname, '../dist'),
+    assetsSubDirectory: 'static',
+    assetsPublicPath: './',
+    productionSourceMap: true,
+    // Gzip off by default as many popular static hosts such as
+    // Surge or Netlify already gzip all static assets for you.
+    // Before setting to `true`, make sure to:
+    // npm install --save-dev compression-webpack-plugin
+    productionGzip: false,
+    productionGzipExtensions: ['js', 'css']
+  },
+  dev: {
+    env: require('./dev.env'),
+    port: 8080,
+    assetsSubDirectory: 'static',
+    assetsPublicPath: '/',
+    proxyTable: {},
+    // CSS Sourcemaps off by default because relative paths are "buggy"
+    // with this option, according to the CSS-Loader README
+    // (https://github.com/webpack/css-loader#sourcemaps)
+    // In our experience, they generally work as expected,
+    // just be aware of this issue when enabling this option.
+    cssSourceMap: false
+  }
+}

+ 3 - 0
webapp/config/prod.env.js

@@ -0,0 +1,3 @@
+module.exports = {
+  NODE_ENV: '"production"'
+}

+ 6 - 0
webapp/config/test.env.js

@@ -0,0 +1,6 @@
+var merge = require('webpack-merge')
+var devEnv = require('./dev.env')
+
+module.exports = merge(devEnv, {
+  NODE_ENV: '"testing"'
+})

+ 11 - 0
webapp/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>蓝月亮前端组</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 10 - 0
webapp/src/App.vue

@@ -0,0 +1,10 @@
+<template>
+  <router-view></router-view>
+</template>
+
+<script>
+  export default { }
+</script>
+
+<style>
+</style>

+ 31 - 0
webapp/src/api/editor.js

@@ -0,0 +1,31 @@
+/**
+ * Created by zhengguorong on 2016/11/30.
+ */
+import * as http from '../util/http'
+
+const getUserThemeList = (type = 'h5') => {
+  return http.get('/api/pages?type=' + type)
+}
+const getPageByThemeId = (id) => {
+  return http.get('/api/pages/' + id)
+}
+const saveTheme = (theme) => {
+  return http.post('/api/pages', theme)
+}
+const delTheme = (theme) => {
+  return http.del('/api/pages', theme)
+}
+const updateTheme = (theme) => {
+  return http.put('/api/pages/' + theme._id, theme)
+}
+
+const uploadPic = (data) => {
+  return http.post('/api/upload', data)
+}
+
+const getPicListByThemeId = (_id) => {
+  return http.get('/api/upload/theme/' + _id)
+}
+module.exports = {
+  getUserThemeList, saveTheme, updateTheme, uploadPic, getPageByThemeId, getPicListByThemeId, delTheme
+}

+ 12 - 0
webapp/src/api/user.js

@@ -0,0 +1,12 @@
+import * as http from '../util/http'
+
+const login = (userInfo) => {
+  return http.post('/auth/login', userInfo)
+}
+const register = (userInfo) => {
+  return http.post('/auth/register', userInfo)
+}
+
+module.exports = {
+  login, register
+}

BIN
webapp/src/assets/addpic_large.png


BIN
webapp/src/assets/images/default.png


BIN
webapp/src/assets/images/logo.jpg


BIN
webapp/src/assets/login-bg.jpg


BIN
webapp/src/assets/logo.png


File diff suppressed because it is too large
+ 679 - 0
webapp/src/assets/svg/icon.svg


+ 83 - 0
webapp/src/components/Element/FontElement.vue

@@ -0,0 +1,83 @@
+<template>
+  <aside class='element' @mousedown="mousedown">
+    <Operate class="operate" v-show="element === editingElement" :element="element" />
+    <section class="content">
+      <div :class="element['playing'] && 'animated ' + this.element['animatedName']" :style="styleAnime">
+        <div :style="styleBasic">{{ element.text }}</div>
+      </div>
+    </section>
+  </aside>
+</template>
+
+<script>
+  import Operate from './../OperateNew'
+  export default {
+    props: ['element'],
+    computed: {
+      editingElement () {
+        return this.$store.getters['editingElement']
+      },
+      styleAnime () {
+        return {
+          animationIterationCount: this.element['loop'] ? 'infinite' : 'initial',
+          animationDuration: this.element['duration'] + 's',
+          animationDelay: this.element['delay'] + 's'
+        }
+      },
+      styleBasic () {
+        return {
+          width: this.element['width'] + 'px',
+          lineHeight: this.element['lineHeight'],
+          color: this.element['color'],
+          textAlign: this.element['textAlign'],
+          fontSize: this.element['fontSize'] + 'px',
+          fontWeight: this.element['fontWeight'],
+          fontFamily: this.element['fontFamily'],
+          opacity: this.element['opacity'] / 100,
+          transform: 'rotate(' + this.element['transform'] + 'deg' + ')'
+        }
+      }
+    },
+    methods: {
+      mousedown (downEvent) {
+        let ele = this.element
+        let startY = downEvent.clientY
+        let startX = downEvent.clientX
+        let startTop = ele['top']
+        let startLeft = ele['left']
+        let move = moveEvent => {
+          let currX = moveEvent.clientX
+          let currY = moveEvent.clientY
+          ele['top'] = currY - startY + startTop
+          ele['left'] = currX - startX + startLeft
+        }
+        let up = () => {
+          document.removeEventListener('mousemove', move)
+          document.removeEventListener('mouseup', up)
+        }
+        document.addEventListener('mousemove', move)
+        document.addEventListener('mouseup', up)
+      }
+    },
+    components: { Operate }
+  }
+</script>
+
+<style lang='less' scoped>
+  .element {
+    position: absolute;
+    cursor: move;
+  }
+
+  .operate {
+    z-index: 2;
+  }
+
+  .content {
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    position: relative;
+    z-index: 1;
+  }
+
+</style>

+ 159 - 0
webapp/src/components/Element/PicElement.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class='wrap' @mousedown="mousedown" @mouseup="mouseup">
+    <img @dragstart="dragstart" style="width:100%;height:100%;" :src="http + element.imgSrc">
+    <Operate v-show="showOperate" @mousedown.native.stop="scaleMousedown" @mouseup.native.stop="scaleMouseup" @mousemove.native.stop="scaleMousemove"
+    />
+  </div>
+</template>
+
+<script>
+    import Operate from '../Operate'
+    import appConst from '../../util/appConst'
+    export default{
+      props: {
+        element: {
+          type: Object,
+          require: true
+        },
+        showOperate: {
+          type: Boolean
+        },
+        type: ''
+      },
+      data () {
+        return {
+          left: 0,
+          top: 0,
+          width: 0,
+          height: 0,
+          currentX: 0,
+          currentY: 0,
+          flag: false,
+          scaleFlag: false,
+          direction: '',
+          http: appConst.BACKEND_DOMAIN
+        }
+      },
+      methods: {
+        // 处理元素拖动
+        move () {
+          document.querySelector('.editor').onmousemove = (event) => {
+            var e = event || window.event
+            if (this.flag) {
+              let nowX = e.clientX
+              let nowY = e.clientY
+              let disX = nowX - this.currentX
+              let disY = nowY - this.currentY
+              this.element.top = parseInt(this.top) + disY
+              this.element.left = parseInt(this.left) + disX
+            }
+          }
+        },
+        // 处理元素伸缩
+        scaleMousemove () {
+          document.querySelector('.editor').onmouseup = (event) => {
+            this.scaleFlag = false
+          }
+          document.querySelector('.editor').onmousemove = (event) => {
+            var e = event || window.event
+            if (this.scaleFlag) {
+              let nowX = e.clientX
+              let nowY = e.clientY
+              let disX = nowX - this.currentX
+              let disY = nowY - this.currentY
+              switch (this.direction) {
+                // 左边
+                case 'w':
+                  this.element.width = parseInt(this.width) - disX
+                  this.element.left = parseInt(this.left) + disX
+                  break
+                // 右边
+                case 'e':
+                  this.element.width = parseInt(this.width) + disX
+                  break
+                // 上边
+                case 'n':
+                  this.element.height = parseInt(this.height) - disY
+                  this.element.top = parseInt(this.top) + disY
+                  break
+                // 下边
+                case 's':
+                  this.element.height = parseInt(this.height) + disY
+                  break
+                // 左上
+                case 'nw':
+                  this.element.width = parseInt(this.width) - disX
+                  this.element.left = parseInt(this.left) + disX
+                  this.element.height = parseInt(this.height) - disY
+                  this.element.top = parseInt(this.top) + disY
+                  break
+                // 左下
+                case 'sw':
+                  this.element.width = parseInt(this.width) - disX
+                  this.element.left = parseInt(this.left) + disX
+                  this.element.height = parseInt(this.height) + disY
+                  break
+                // 右上
+                case 'ne':
+                  this.element.height = parseInt(this.height) - disY
+                  this.element.top = parseInt(this.top) + disY
+                  this.element.width = parseInt(this.width) + disX
+                  break
+                // 右下
+                case 'se':
+                  this.element.height = parseInt(this.height) + disY
+                  this.element.width = parseInt(this.width) + disX
+                  break
+              }
+            }
+          }
+        },
+        mousedown (e) {
+          this.flag = true
+          this.currentX = e.clientX
+          this.currentY = e.clientY
+          this.top = this.element.top
+          this.left = this.element.left
+          this.move()
+        },
+        mouseup (e) {
+          this.flag = false
+          this.scaleFlag = false
+        },
+        scaleMousedown (e) {
+          this.scaleFlag = true
+          this.currentX = e.clientX
+          this.currentY = e.clientY
+          this.top = this.element.top
+          this.left = this.element.left
+          this.width = this.element.width
+          this.height = this.element.height
+          this.direction = e.target.getAttribute('data-direction')
+          this.scaleMousemove()
+        },
+        scaleMouseup (e) {
+          this.scaleFlag = false
+        },
+        dragstart (event) {
+          console.log('dragstart')
+          event.preventDefault()
+        }
+      },
+      components: {
+        Operate
+      }
+    }
+</script>
+
+<style lang='less' scoped>
+  .wrap {
+    position: absolute;
+    cursor: move;
+  }
+
+  .wrap img {
+    position: absolute;
+    user-select: none;
+    /*-webkit-user-drag: none;*/
+  }
+</style>

+ 142 - 0
webapp/src/components/Element/ShapesElement.vue

@@ -0,0 +1,142 @@
+<template>
+    <div class='wrap' @mousedown="mousedown" @mouseup="mouseup">
+      <!--<icon></icon>-->
+      <svg style="width: 100%;height: 100%;position: absolute">
+        <use v-bind:xlink:href="'/static/svg/icon.svg#'+ iconKey"/>
+      </svg>
+      <Operate v-show="showOperate" @mousedown.native.stop="scaleMousedown" @mouseup.native.stop="scaleMouseup" @mousemove.native.stop="scaleMousemove"
+      />
+    </div>
+</template>
+<style lang='less' scoped>
+  .wrap {
+    position: absolute;
+    cursor: move;
+  }
+  .wrap img {
+    position: absolute;
+  }
+</style>
+<script>
+//  import icon from './svg/icon'
+  import Operate from '../Operate'
+  export default{
+    data () {
+      return {
+      }
+    },
+    props: {
+      element: {
+        type: Object,
+        require: true
+      },
+      showOperate: {
+        type: Boolean
+      },
+      iconKey: '',
+      style: ''
+    },
+    methods: {
+      // 处理元素拖动
+      move () {
+        document.querySelector('.canvas').onmousemove = (event) => {
+          var e = event || window.event
+          if (this.flag) {
+            let nowX = e.clientX
+            let nowY = e.clientY
+            let disX = nowX - this.currentX
+            let disY = nowY - this.currentY
+            this.element.top = parseInt(this.top) + disY
+            this.element.left = parseInt(this.left) + disX
+          }
+        }
+      },
+      // 处理元素伸缩
+      scaleMousemove () {
+        document.querySelector('.canvas').onmouseup = (event) => {
+          this.scaleFlag = false
+        }
+        document.querySelector('.canvas').onmousemove = (event) => {
+          var e = event || window.event
+          if (this.scaleFlag) {
+            let nowX = e.clientX
+            let nowY = e.clientY
+            let disX = nowX - this.currentX
+            let disY = nowY - this.currentY
+            switch (this.direction) {
+              // 左边
+              case 'w':
+                this.element.width = parseInt(this.width) - disX
+                this.element.left = parseInt(this.left) + disX
+                break
+              // 右边
+              case 'e':
+                this.element.width = parseInt(this.width) + disX
+                break
+              // 上边
+              case 'n':
+                this.element.height = parseInt(this.height) - disY
+                this.element.top = parseInt(this.top) + disY
+                break
+              // 下边
+              case 's':
+                this.element.height = parseInt(this.height) + disY
+                break
+              // 左上
+              case 'nw':
+                this.element.width = parseInt(this.width) - disX
+                this.element.left = parseInt(this.left) + disX
+                this.element.height = parseInt(this.height) - disY
+                this.element.top = parseInt(this.top) + disY
+                break
+              // 左下
+              case 'sw':
+                this.element.width = parseInt(this.width) - disX
+                this.element.left = parseInt(this.left) + disX
+                this.element.height = parseInt(this.height) + disY
+                break
+              case 'ne':
+                this.element.height = parseInt(this.height) - disY
+                this.element.top = parseInt(this.top) + disY
+                this.element.width = parseInt(this.width) + disX
+                break
+              case 'se':
+                this.element.height = parseInt(this.height) + disY
+                this.element.width = parseInt(this.width) + disX
+                break
+            }
+          }
+        }
+      },
+      mousedown (e) {
+        this.flag = true
+        this.currentX = e.clientX
+        this.currentY = e.clientY
+        this.top = this.element.top
+        this.left = this.element.left
+        this.move()
+      },
+      mouseup (e) {
+        this.flag = false
+        this.scaleFlag = false
+      },
+      scaleMousedown (e) {
+        this.scaleFlag = true
+        this.currentX = e.clientX
+        this.currentY = e.clientY
+        this.top = this.element.top
+        this.left = this.element.left
+        this.width = this.element.width
+        this.height = this.element.height
+        this.direction = e.target.getAttribute('data-direction')
+        this.scaleMousemove()
+      },
+      scaleMouseup (e) {
+        this.scaleFlag = false
+      }
+    },
+    components: {
+      Operate
+    }
+  }
+</script>

+ 98 - 0
webapp/src/components/HeaderBar.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="header">
+    <div class="container">
+      <a href="">
+        <div class="logo">
+          <img src="../assets/images/logo.jpg"
+               alt="">
+        </div>
+      </a>
+      <div class="nav">
+        <ul>
+          <li :class="{'active': item.active}" v-for="item in navList" @click="select(item)">
+            <div>{{item.name}}</div>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      navList: [{
+        path: 'themeList',
+        name: 'h5作品',
+        active: false
+      }, {
+        path: 'spaList',
+        name: '单页作品',
+        active: false
+      }
+      ]
+    }
+  },
+  methods: {
+    select (item) {
+      this.$router.push(item.path)
+    }
+  },
+  mounted () {
+    this.navList.forEach((element) => {
+      if (element.path === this.$route.name) {
+        element.active = true
+      }
+    })
+  }
+}
+</script>
+<style lang="less">
+.header {
+  width: 100%;
+  height: 60px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+}
+
+.header .container {
+  margin: 0 auto;
+  width: 1024px;
+  text-align: center;
+}
+
+.header .logo img {
+  float: left;
+  height: 50px;
+}
+
+.header .nav {
+  float: left;
+  padding-left: 50px;
+}
+
+.header .nav li {
+  float: left;
+  width: 110px;
+  line-height: 52px;
+  border-top: solid 5px rgba(0, 0, 0, 0);
+  text-align: center;
+  list-style: none;
+}
+
+.header .nav li.active {
+  border-top: solid 5px #0059f1;
+}
+
+.header .nav li div {
+  height: 60px;
+  font-size: 16px;
+  color: #000;
+  cursor: pointer;
+}
+
+.header .nav li.active div {
+  color: #0059f1;
+}
+</style>

+ 122 - 0
webapp/src/components/Operate.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="operate">
+    <div class="operate-hor-line"></div>
+    <div class="operate-ver-line"></div>
+    <div class="scale scale-nw" data-direction="nw"></div>
+    <div class="scale scale-ne" data-direction="ne"></div>
+    <div class="scale scale-sw" data-direction="sw"></div>
+    <div class="scale scale-se" data-direction="se"></div>
+    <div class="scale scale-n" data-direction="n"></div>
+    <div class="scale scale-e" data-direction="e"></div>
+    <div class="scale scale-s" data-direction="s"></div>
+    <div class="scale scale-w" data-direction="w"></div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+  .operate {
+    width: 100%;
+    height: 100%;
+  }
+  
+  .operate-hor-line::before, .operate-hor-line::after, .operate-ver-line::before, .operate-ver-line::after {
+    content: '';
+    position: absolute;
+    border-color: #000;
+    border-style: dashed;
+    border-width: 0px;
+  }
+
+  .operate-hor-line::before {
+    left: 0;
+    top: 0;
+    width: 100%;
+    border-top-width: 1px;
+  }
+  
+  .operate-hor-line::after {
+    left: 0;
+    bottom: 0;
+    width: 100%;
+    border-bottom-width: 1px;
+  }
+
+  .operate-ver-line::before {
+    left: 0;
+    top: 0;
+    height: 100%;
+    border-left-width: 1px;
+  }
+
+  .operate-ver-line::after {
+    right: 0;
+    top: 0;
+    height: 100%;
+    border-right-width: 1px;
+  }
+  
+  .scale {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #000;
+    width: 7px;
+    height: 7px;
+    z-index: 1;
+  }
+  
+  .scale-nw {
+    top: -3px;
+    left: -2px;
+    cursor: nw-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-ne {
+    top: -3px;
+    right: -2px;
+    cursor: ne-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-sw {
+    bottom: -3px;
+    left: -2px;
+    cursor: sw-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-se {
+    bottom: -3px;
+    right: -2px;
+    cursor: se-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-n {
+    top: -2px;
+    left: 50%;
+    margin-left: -5px;
+    cursor: n-resize;
+  }
+  
+  .scale-e {
+    right: -3px;
+    top: 50%;
+    margin-top: -5px;
+    cursor: e-resize;
+  }
+  
+  .scale-s {
+    bottom: -3px;
+    left: 50%;
+    margin-left: -5px;
+    cursor: s-resize;
+  }
+  
+  .scale-w {
+    left: -3px;
+    top: 50%;
+    margin-top: -5px;
+    cursor: w-resize;
+  }
+</style>

+ 126 - 0
webapp/src/components/OperateNew.vue

@@ -0,0 +1,126 @@
+<template>
+  <div class="operate">
+    <div class="scale scale-nw" @mousedown.stop="mousedown($event, 'nw')"></div>
+    <div class="scale scale-ne" @mousedown.stop="mousedown($event, 'ne')"></div>
+    <div class="scale scale-sw" @mousedown.stop="mousedown($event, 'sw')"></div>
+    <div class="scale scale-se" @mousedown.stop="mousedown($event, 'se')"></div>
+    <div class="scale scale-n" @mousedown.stop="mousedown($event, 'n')"></div>
+    <div class="scale scale-e" @mousedown.stop="mousedown($event, 'e')"></div>
+    <div class="scale scale-s" @mousedown.stop="mousedown($event, 's')"></div>
+    <div class="scale scale-w" @mousedown.stop="mousedown($event, 'w')"></div>
+  </div>
+</template>
+
+<script>
+  export default {
+    props: ['element'],
+    methods: {
+      mousedown (downEvent, mark) {
+        let startX = downEvent.clientX
+        let startY = downEvent.clientY
+        let ele = this.element
+        let height = ele['height']
+        let width = ele['width']
+        let top = ele['top']
+        let left = ele['left']
+        let move = moveEvent => {
+          let currX = moveEvent.clientX
+          let currY = moveEvent.clientY
+          let disY = currY - startY
+          let disX = currX - startX
+          let hasN = /n/.test(mark)
+          let hasS = /s/.test(mark)
+          let hasW = /w/.test(mark)
+          let hasE = /e/.test(mark)
+          let newHeight = +height + (hasN ? -disY : hasS ? disY : 0)
+          let newWidth = +width + (hasW ? -disX : hasE ? disX : 0)
+          ele['height'] = newHeight > 0 ? newHeight : 0
+          ele['width'] = newWidth > 0 ? newWidth : 0
+          ele['left'] = +left + (hasW ? disX : 0)
+          ele['top'] = +top + (hasN ? disY : 0)
+        }
+        let up = () => {
+          document.removeEventListener('mousemove', move)
+          document.removeEventListener('mouseup', up)
+        }
+        document.addEventListener('mousemove', move)
+        document.addEventListener('mouseup', up)
+      }
+    }
+  }
+</script>
+<style lang="less" scoped>
+  .operate {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    border: 1px dashed #000;
+  }
+  
+  .scale {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #000;
+    width: 7px;
+    height: 7px;
+    z-index: 1;
+  }
+  
+  .scale-nw {
+    top: -3.5px;
+    left: -3.5px;
+    cursor: nw-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-ne {
+    top: -3.5px;
+    right: -3.5px;
+    cursor: ne-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-sw {
+    bottom: -3.5px;
+    left: -3.5px;
+    cursor: sw-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-se {
+    bottom: -3.5px;
+    right: -3.5px;
+    cursor: se-resize;
+    border-radius: 50%;
+  }
+  
+  .scale-n {
+    top: -3.5px;
+    left: 50%;
+    margin-left: -3.5px;
+    cursor: n-resize;
+  }
+  
+  .scale-e {
+    right: -3px;
+    top: 50%;
+    margin-top: -3.5px;
+    cursor: e-resize;
+  }
+  
+  .scale-s {
+    bottom: -3px;
+    left: 50%;
+    margin-left: -3.5px;
+    cursor: s-resize;
+  }
+  
+  .scale-w {
+    left: -3.5px;
+    top: 50%;
+    margin-top: -3.5px;
+    cursor: w-resize;
+  }
+</style>

File diff suppressed because it is too large
+ 89 - 0
webapp/src/components/Page.vue


+ 44 - 0
webapp/src/components/PicturePicker.vue

@@ -0,0 +1,44 @@
+<template>
+  <label class="lable"><input class="input" type="file" @change="fileChange"></label>
+</template>
+
+<style scoped>
+  .lable {
+    display: block;
+    cursor: pointer;
+    width: 3em;
+    height: 3em;
+    background: url("../assets/addpic_large.png") no-repeat;
+    background-size: cover;
+  }
+
+  .input {
+    display: none;
+  }
+</style>
+
+<script>
+  export default {
+    methods: {
+      fileChange (event) {
+        let file = event.target.files[0]
+        if (file) {
+          let reader = new window.FileReader()
+          reader.onload = (ev) => {
+            let img = document.createElement('img')
+            let base64 = ev.target.result
+            img.onload = () => {
+              this.$emit('uploaded', {
+                'base64': base64,
+                'width': img.width,
+                'height': img.height
+              })
+            }
+            img.src = base64
+          }
+          reader.readAsDataURL(file)
+        }
+      }
+    }
+  }
+</script>

+ 29 - 0
webapp/src/main.js

@@ -0,0 +1,29 @@
+// 全局插件
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import Vuex from 'vuex'
+import ElementUI from 'element-ui'
+import App from './App'
+
+// 插件配置
+import RouterConfig from './routers'
+import Store from './vuex/store'
+
+// 加载插件
+Vue.use(VueRouter)
+Vue.use(Vuex)
+Vue.use(ElementUI)
+
+// 全局样式
+import 'normalize.css'
+import 'element-ui/lib/theme-default/index.css'
+import './style/main.css'
+
+// 初始化
+/* eslint-disable no-new */
+new Vue({
+  router: new VueRouter({ routes: RouterConfig }),
+  store: new Vuex.Store(Store),
+  el: '#app',
+  render: h => h(App)
+})

+ 30 - 0
webapp/src/model/Element.js

@@ -0,0 +1,30 @@
+/**
+ * Created by zhengguorong on 2016/11/21.
+ */
+export default class Element {
+  constructor (ele = {}) {
+    this.type = ele.type || 'pic'
+    this.imgSrc = ele.imgSrc || ''
+    this.left = ele.left || 0
+    this.top = ele.top || 0
+    this.width = ele.width || 0
+    this.height = ele.height || 0
+    this.lineHeight = ele.lineHeight || 0
+    this.animatedName = ele.animatedName || ''
+    this.duration = ele.duration || 1
+    this.delay = ele.delay || 0
+    this.playing = false
+    this.loop = false
+    this.opacity = ele.opacity || 100
+    this.transform = ele.transform || 0
+    this.text = ele.text || ''
+    this.textAlign = ele.textAlign || 'left'
+    this.iconKey = ele.iconKey || ''
+    this.bg = ele.bg || ''
+    this.fontSize = ele.fontSize || 18
+    this.fontFamily = ele.fontFamily || '微软雅黑'
+    this.fontWeight = ele.fontWeight || 'normal'
+    this.color = ele.color || '#000000'
+    this.zindex = ele.zindex || 1
+  }
+}

+ 8 - 0
webapp/src/model/Page.js

@@ -0,0 +1,8 @@
+/**
+ * Created by zhengguorong on 2016/11/24.
+ */
+export default class Page {
+  constructor (page = {}) {
+    this.elements = page.elements || []
+  }
+}

+ 12 - 0
webapp/src/model/Theme.js

@@ -0,0 +1,12 @@
+/**
+ * Created by zhengguorong on 2016/11/24.
+ */
+export default class Theme {
+  constructor (theme = {}) {
+    this.title = theme.title || '蓝月亮'
+    this.description = theme.description || '蓝月亮'
+    this.pages = theme.pages || []
+    this.type = theme.type || 'h5'
+    this.canvasHeight = theme.canvasHeight || 504
+  }
+}

+ 32 - 0
webapp/src/routers.js

@@ -0,0 +1,32 @@
+/**
+ * Created by zhengguorong on 16/11/3.
+ */
+export default [{
+  path: '/',
+  name: 'index',
+  component: require('./views/h5editor/themeList.vue')
+}, {
+  path: '/login',
+  name: 'login',
+  component: require('./views/user/login')
+}, {
+  path: '/register',
+  name: 'register',
+  component: require('./views/user/register')
+}, {
+  path: '/h5editor',
+  name: 'h5editor',
+  component: require('./views/h5editor/index.vue')
+}, {
+  path: '/spaeditor',
+  name: 'spaeditor',
+  component: require('./views/spaeditor/index.vue')
+}, {
+  path: '/themeList',
+  name: 'themeList',
+  component: require('./views/h5editor/themeList.vue')
+}, {
+  path: '/spaList',
+  name: 'spaList',
+  component: require('./views/spaeditor/themeList.vue')
+}]

+ 418 - 0
webapp/src/style/main.css

@@ -0,0 +1,418 @@
+/* CSS Rest */
+body,
+div,
+dl,
+dt,
+dd,
+ul,
+ol,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+pre,
+form,
+fieldset,
+input,
+textarea,
+span,
+p,
+blockquote,
+th,
+td {margin: 0;
+  padding: 0;}
+input,
+select {-webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;}
+input,
+select,
+textarea,
+/*button:focus{outline: none;
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+  -webkit-tap-highlight-color: transparent;
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+  outline: none!important;}*/
+input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {color: #999;}
+input:-moz-placeholder, textarea:-moz-placeholder {color: #999;}
+input::-moz-placeholder, textarea::-moz-placeholder {color: #999;}
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {color: #999;}
+table {border-collapse: collapse;
+  border-spacing: 0;}
+fieldset,
+img {border: 0;}
+address,
+caption,
+cite,
+code,
+dfn,
+em,
+strong,
+th,
+var {font-style: normal;
+  font-weight: normal;}
+ol,
+ul {list-style: none;}
+caption,
+th {text-align: left;}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {font-size: 100%;
+  font-weight: normal;}
+q:before,
+q:after {
+  content: '';
+}
+abbr,
+acronym {border: 0;}
+
+input:focus, textarea:focus, keygen:focus, select:focus {
+  outline: none;
+}
+:focus {
+  outline: none;
+}
+
+/* font-size */
+.f9{font-size: 0.225rem;}
+.f10 {font-size: 0.25rem;}
+.f12 {font-size: 0.3rem;}
+.f14 {font-size: 0.35rem;}
+.f16 {font-size: 0.4rem;}
+.f18 {font-size: 0.45rem;}
+.f20 {font-size: 0.5rem;}
+.f22 {font-size: 0.55rem;}
+.f24 {font-size: 0.6rem;}
+.f26 {font-size: 0.65rem;}
+.f28 {font-size: 0.7rem;}
+.f30 {font-size: 0.75rem;}
+.f32 {font-size: 0.8rem;}
+.f34 {font-size: 0.85rem;}
+.f36 {font-size: 0.9rem;}
+.f40 {font-size: 1rem;}
+
+/* font-style */
+.fb {font-weight: bold;}
+.fn {font-weight: normal;}
+.fi {font-style: italic;}
+
+/* line-height */
+.lh100 {line-height: 1;}
+.lh150 {line-height: 1.5;}
+.lh200 {line-height: 2;}
+
+/* text-decoration */
+.line-through {text-decoration: line-through;}
+.line-unl {text-decoration: underline;}
+.line-none {text-decoration: none;}
+.tdn,
+.tdn:hover,
+.tdn a:hover,
+a.tdl:hover {text-decoration: none;}
+
+/* text-style */
+.text-nowrap {white-space: nowrap;}
+.text-pre {white-space: pre;}
+.text-brake {word-wrap: break-word;}
+
+/* text-align */
+.tl {text-align: left;}
+.tc {text-align: center;}
+.tr {text-align: right;}
+.t2 {text-indent: 2em;}
+
+/* opacity */
+.opacity0{opacity: 0;}
+.opacity0{opacity: .5;}
+
+/* box-sizing */
+.boxb {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.boxc {
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+/* 单行文字溢出虚点显 示*/
+.ell{text-overflow:ellipsis; white-space:nowrap; overflow:hidden;}
+
+/* vertical */
+.vt {vertical-align: top;}
+.vm {vertical-align: middle;}
+.vb {vertical-align: bottom;}
+
+/* float */
+.fl {float: left;}
+.fr {float: right;}
+
+/* clear */
+.cb {clear: both;}
+.cl {clear: left;}
+.cr {clear: right;}
+
+/* display */
+.none {display: none;}
+.db {display: block;}
+.di {display: inline;}
+.dib {display: inline-block;}
+.dt {display: table;}
+.dtc {display: table-cell;}
+
+/* overflow */
+.ovh {overflow: hidden;}
+.ova {overflow: auto;}
+
+/* visibility */
+.vh {visibility: hidden;}
+.vv {visibility: visible;}
+
+/* cursor */
+.poi {cursor: pointer;}
+.def {cursor: default;}
+
+/* position */
+.pr {position: relative;}
+.pa {position: absolute;}
+.pf {position: fixed;}
+.top {top: 0;}
+.bottom {bottom: 0;}
+.right {right: 0;}
+.left {left: 0;}
+
+/* 清除浮动 */
+.clearfix:after {
+  content: "";
+  display: block;
+  clear: both;
+}
+
+/* 居中 */
+.auto {margin: auto;}
+.bc {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 自适应布局 */
+/* flexbox */
+.flex {
+  display: -webkit-box;
+  display: -moz-box;
+  display: -ms-flexbox;
+  display: flex;
+}
+.flex-1 {
+  -webkit-box-flex: 1;
+  -moz-box-flex: 1;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+}
+.ai-center {
+  -webkit-align-items:center;
+  align-items:center;
+}
+.jc-center {
+  -webkit-justify-content: center;
+  justify-content: center;
+}
+.jc-around {
+  -webkit-justify-content: space-around;
+  justify-content: space-around;
+}
+.jc-between {
+  -webkit-justify-content: space-between;
+  justify-content: space-between;
+}
+.flex-basis {
+  -webkit-flex-basis:100%;
+  flex-basis:100%;
+}
+
+/* table-cell两栏自适应 */
+.cell {
+  display: table-cell;
+  *display: inline-block;
+  width: 100%;
+  *zoom: 1;
+}
+
+/* 宽度边距 */
+.w0 {width: 0;}
+.w50 {width: 50%;}
+.w80 {width: 80%;}
+.w {width: 100%;}
+.h {height: 100%;}
+.m10 {margin: 0.25rem;}
+.m15 {margin: 0.375rem;}
+.m20 {margin: 0.5rem;}
+.m30 {margin: 0.75rem;}
+.mt5 {margin-top: 0.125rem;}
+.mt10 {margin-top: 0.25rem;}
+.mt15 {margin-top: 0.375rem;}
+.mt20 {margin-top: 0.5rem;}
+.mt30 {margin-top: 0.75rem;}
+.mt50 {margin-top: 1.25rem;}
+.mt100 {margin-top: 2.5rem;}
+.mb5 {margin-bottom: 0.125rem;}
+.mb10 {margin-bottom: 0.25rem;}
+.mb15 {margin-bottom: 0.375rem;}
+.mb20 {margin-bottom: 0.5rem;}
+.mb30 {margin-bottom: 0.75rem;}
+.mb50 {margin-bottom: 1.25rem;}
+.mb100 {margin-bottom: 2.5rem;}
+.ml5 {margin-left: 0.125rem;}
+.ml10 {margin-left: 0.25rem;}
+.ml15 {margin-left: 0.375rem;}
+.ml20 {margin-left: 0.5rem;}
+.ml30 {margin-left: 0.75rem;}
+.ml50 {margin-left: 1.25rem;}
+.ml100 {margin-left: 2.5rem;}
+.mr5 {margin-right: 0.125rem;}
+.mr10 {margin-right: 0.25rem;}
+.mr15 {margin-right: 0.375rem;}
+.mr20 {margin-right: 0.5rem;}
+.mr30 {margin-right: 0.75rem;}
+.mr50 {margin-right: 1.25rem;}
+.mr100 {margin-right: 2.5rem;}
+.p10 {padding: 0.25rem;}
+.p15 {padding: 0.375rem;}
+.p20 {padding: 0.5rem;}
+.p30 {padding: 0.75rem;}
+.pt5 {padding-top: 0.125rem;}
+.pt10 {padding-top: 0.25rem;}
+.pt15 {padding-top: 0.375rem;}
+.pt20 {padding-top: 0.5rem;}
+.pt30 {padding-top: 0.75rem;}
+.pt50 {padding-top: 1.25rem;}
+.pt100 {padding-top: 2.5rem;}
+.pb5 {padding-bottom: 0.125rem;}
+.pb10 {padding-bottom: 0.25rem;}
+.pb15 {padding-bottom: 0.375rem;}
+.pb20 {padding-bottom: 0.5rem;}
+.pb30 {padding-bottom: 0.75rem;}
+.pb50 {padding-bottom: 1.25rem;}
+.pb100 {padding-bottom: 2.5rem;}
+.pl5 {padding-left: 0.125rem;}
+.pl10 {padding-left: 0.25rem;}
+.pl15 {padding-left: 0.375rem;}
+.pl20 {padding-left: 0.5rem;}
+.pl30 {padding-left: 0.75rem;}
+.pl50 {padding-left: 1.25rem;}
+.pl100 {padding-left: 2.5rem;}
+.pr5 {padding-right: 0.125rem;}
+.pr10 {padding-right: 0.25rem;}
+.pr15 {padding-right: 0.375rem;}
+.pr20 {padding-right: 0.5rem;}
+.pr30 {padding-right: 0.75rem;}
+.pr50 {padding-right: 1.25rem;}
+.pr100 {padding-right: 2.5rem;}
+/* 字体颜色 */
+.color-fff {color: #fff;}
+.color-999 {color: #999;}
+.color-333 {color: #333;}
+.color-666 {color: #666;}
+.color-cfcfcf {color: #cfcfcf;}
+.color-7d7f86 {color: #7d7f86;}
+.color-fc5500 {color: #fc5500;}
+.color-0a85cc {color: #0a85cc;}
+.color-ff6c47 {color: #ff6c47;}
+.color-1fb8ff {color: #1fb8ff;}
+.color-0a58cc {color: #0a58cc;}
+.color-1352e2 {color: #1352e2;}
+
+/* 背景颜色 */
+.bg-transparent {background-color: transparent;}
+.bg-fff {background-color: #ffffff;}
+.bg-f5f5f5 {background-color: #f5f5f5;}
+.bg-fbfbfb {background-color: #fbfbfb;}
+.bg-ff6c47{background: #ff6c47;}
+.bg-F1F1F1{background: #F1F1F1;}
+.bg-1fb8ff {background-color: #1fb8ff;}
+
+/* border */
+.border-none {border: none;}
+.bbd7d7d7 {border-bottom: solid 1px #d7d7d7;}
+.btd7d7d7 {border-top: solid 1px #d7d7d7;}
+.bld7d7d7 {border-left: solid 1px #d7d7d7;}
+.brd7d7d7 {border-right: solid 1px #d7d7d7;}
+.bbe5e5e5 {border-bottom: solid 1px #e5e5e5;}
+.bte5e5e5 {border-top: solid 1px #e5e5e5;}
+.ble5e5e5 {border-left: solid 1px #e5e5e5;}
+.bre5e5e5 {border-right: solid 1px #e5e5e5;}
+.bbd5d5d5 {border-bottom: solid 1px #d5d5d5;}
+.btd5d5d5 {border-top: solid 1px #d5d5d5;}
+.bld5d5d5 {border-left: solid 1px #d5d5d5;}
+.brd5d5d5 {border-right: solid 1px #d5d5d5;}
+/*0.5px细线*/
+.halfBBorder{
+  position: relative;
+}
+.halfBBorder:after{
+  content: "";
+  display: block;
+  position: absolute;
+  left: -50%;
+  width: 200%;
+  height: 1px;
+  background: #ccc;
+  -webkit-transform:scale(0.5);
+  transform:scale(0.5);
+}
+html {
+  height: 100%;
+}
+body {
+  height: 100%;
+  margin: 0 auto;
+}
+a:link {
+  text-decoration: none;
+  color: #000;
+}
+
+*, *:before, *:after {
+  box-sizing: border-box;
+}
+
+a, a:link, a:visited, a:hover, a:active {
+  color: inherit;
+  text-decoration: inherit;
+}
+
+.custom-scrollbar::-webkit-scrollbar {
+  display: block;
+  width: 5px;
+  background: #FAFAFA;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb:hover {
+  background: #BDBDBD;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb {
+  border-radius: 5px;
+  background: #E0E0E0;
+}
+
+.reset-btn {
+  border: none;
+  outline: none;
+  background: inherit;
+  appearance: none;
+  margin: 0;
+  padding: 0;
+  color: inherit;
+}

+ 10 - 0
webapp/src/util/appConst.js

@@ -0,0 +1,10 @@
+
+let BACKEND_DOMAIN = 'http://localhost:3000'
+if (process.env.NODE_ENV === 'production') {
+  BACKEND_DOMAIN = 'http://node.bluemoon.com.cn'
+} else if (process.env.NODE_ENV === 'development') {
+  BACKEND_DOMAIN = 'http://localhost:3000'
+}
+export default {
+  BACKEND_DOMAIN
+}

+ 97 - 0
webapp/src/util/http.js

@@ -0,0 +1,97 @@
+import axios from 'axios'
+import appConst from './appConst'
+export const get = (url, query) => {
+  const token = 'Bearer ' + window.localStorage.token
+  let _url
+  if (query) {
+    _url = `${appConst.BACKEND_DOMAIN}${url}?${query}`
+  } else {
+    _url = `${appConst.BACKEND_DOMAIN}${url}`
+  }
+  return axios.get(_url, {
+    headers: { authorization: token }
+  })
+    .then((res) => {
+      if (res.status >= 200 && res.status < 300) {
+        return res.data
+      }
+    })
+    .catch((err) => {
+      errorProcess(err)
+      return Promise.reject(err)
+    })
+}
+
+export const post = (url, query) => {
+  const token = 'Bearer ' + window.localStorage.token
+  let _url = `${appConst.BACKEND_DOMAIN}${url}`
+  return axios.post(_url, query, {
+    headers: { authorization: token }
+  })
+    .then((res) => {
+      if (res.status >= 200 && res.status < 300) {
+        return res.data
+      }
+    })
+    .catch((err) => {
+      errorProcess(err)
+      return Promise.reject(err)
+    })
+}
+
+export const put = (url, query) => {
+  const token = 'Bearer ' + window.localStorage.token
+  let _url = `${appConst.BACKEND_DOMAIN}${url}`
+  return axios.put(_url, query, {
+    headers: { authorization: token }
+  })
+    .then((res) => {
+      if (res.status >= 200 && res.status < 300) {
+        return res.data
+      }
+    })
+    .catch((err) => {
+      errorProcess(err)
+      return Promise.reject(err)
+    })
+}
+
+export const patch = (url, query) => {
+  const token = 'Bearer ' + window.localStorage.token
+  let _url = `${appConst.BACKEND_DOMAIN}${url}`
+  return axios.patch(_url, query, {
+    headers: { authorization: token }
+  })
+    .then((res) => {
+      if (res.status >= 200 && res.status < 300) {
+        return res.data
+      }
+    })
+    .catch((err) => {
+      errorProcess(err)
+      return Promise.reject(err)
+    })
+}
+
+export const del = (url, article) => {
+  const token = 'Bearer ' + window.localStorage.token
+  let _url = `${appConst.BACKEND_DOMAIN}${url}/${article._id}`
+  return axios.delete(_url, {
+    headers: { authorization: token }
+  })
+    .then((res) => {
+      if (res.status >= 200 && res.status < 300) {
+        return res.data
+      }
+    })
+    .catch((err) => {
+      errorProcess(err)
+      return Promise.reject(err)
+    })
+}
+
+const errorProcess = (err) => {
+  if (err.response.status === 401) {
+    window.location.href = '#/login'
+  }
+}

+ 8 - 0
webapp/src/util/tools.js

@@ -0,0 +1,8 @@
+const vue2json = (vue) => {
+  if (vue) {
+    return JSON.parse(JSON.stringify(vue))
+  }
+}
+export default {
+  vue2json
+}

File diff suppressed because it is too large
+ 556 - 0
webapp/src/views/h5editor/index.vue


+ 263 - 0
webapp/src/views/h5editor/overview.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="overview">
+    <div class="clearfix">
+      <span class="panel" :class="{ active: viewState === 0 }" @click="function () { viewState = 0 }">页面</span>
+      <span class="panel" :class="{ active: viewState === 1 }" @click="function () { viewState = 1 }">图层</span>
+    </div>
+    <ul class="list custom-scrollbar" style="z-index: 1;">
+      <li v-for="page in pages">
+        <div class="page" :class="{ active: page === editingPage }" :style="{ width: 131 + 8 + 'px', height: (131 / canvasWidth) * canvasHeight + 34 + 'px' }" @click="setEditingPage(page)">
+          <Page class="content" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', transform: 'scale(' + 131 / canvasWidth +')' }" :elements="page.elements" type="see" />
+          <div class="icons clearfix">
+            <i class="icon el-icon-delete" @click="delPage(page)"></i>
+            <i class="icon el-icon-document" @click="copyPage(page)"></i>
+          </div>
+        </div>
+      </li>
+    </ul>
+    <div class="list custom-scrollbar" style="z-index: 2;" v-show="viewState === 1" :class="{ dragging: dragState === 1 }">
+      <ul>
+        <li v-for="layer in layersNoBg">
+          <div class="layer" :class="{ active: editingLayer === layer}" @click="setEditingLayer($event, layer)" @mousedown="moveLayer">
+            <span class="thumb" :style="{ backgroundImage: 'url(' + http + layer.imgSrc + ')' }"></span>{{ layer.type }}
+          </div>
+        </li>
+      </ul>
+      <div v-for="layer in layersBg" class="layer" :class="{ active: editingLayer === layer}" @click="setEditingLayer($event, layer)">
+        <span class="thumb" :style="{ backgroundImage: 'url(' + http + layer.imgSrc + ')' }"></span>{{ layer.type }}
+      </div>
+    </div>
+    <button class="add el-icon-plus" @click="addPage"></button>
+  </div>
+</template>
+<script>
+  import Page from './../../components/Page'
+  import AppConst from '../../util/appConst'
+  export default {
+    data () {
+      return {
+        viewState: 0,
+        dragState: 0,
+        http: AppConst.BACKEND_DOMAIN,
+        canvasWidth: 320,
+        canvasHeight: 504
+      }
+    },
+    computed: {
+      vxEditor () {
+        return this.$store.state['editor']
+      },
+      pages () {
+        return this.vxEditor['editorTheme']['pages']
+      },
+      editingPage () {
+        return this.vxEditor['editorPage']
+      },
+      layers () {
+        return this.editingPage['elements']
+      },
+      layersNoBg () {
+        return this.layers && this.layers.filter(v => v['type'] !== 'bg').reverse()
+      },
+      layersBg () {
+        return this.layers && this.layers.filter(v => v['type'] === 'bg')
+      },
+      editingLayer () {
+        return this.vxEditor['editorElement']
+      }
+    },
+    methods: {
+      moveLayer (downEvent) {
+        let height = 30
+        let timer = null
+        let layer = downEvent.target
+        let li = layer.parentNode
+        let parent = li.parentNode
+        let liLen = parent.childNodes.length
+        let startTop = li.offsetTop
+        let startIndex = Math.round(startTop / height)
+        let targetIndex = null
+        let placeholder = document.createElement('li')
+        placeholder.style = 'height: ' + height + 'px; background-color: #d6d6d6'
+        let move = (moveEvent) => {
+          if (!timer) {
+            // 被拖动的层
+            let top = moveEvent.clientY - downEvent.clientY + startTop
+            layer.setAttribute('data-moving', true)
+            layer.style.top = top + 'px'
+            this.dragState = 1
+            // 占位层
+            let nowIndex = Math.round(top / height)
+            nowIndex = nowIndex <= 0 ? 0 : nowIndex > liLen - 1 ? liLen - 1 : nowIndex
+            if (targetIndex !== nowIndex) {
+              (targetIndex || targetIndex === 0) && parent.removeChild(placeholder)
+              targetIndex = nowIndex
+              parent.insertBefore(placeholder, parent.childNodes[nowIndex + (startIndex >= targetIndex ? 0 : 1)])
+            }
+            // timer负责减少onmousemove对客户端的负担
+            timer = setTimeout(() => {
+              timer = null
+            }, 20)
+          }
+        }
+        let up = (upEvent) => {
+          if (layer.getAttribute('data-moving')) {
+            layer.removeAttribute('data-moving')
+            layer.style.top = ''
+            parent.removeChild(placeholder)
+            this.layersNoBg[startIndex]['zindex'] = this.layersNoBg[targetIndex]['zindex'] + (targetIndex > startIndex ? -0.5 : 0.5)
+            this.updateLayersSort()
+          }
+          document.removeEventListener('mousemove', move)
+          document.removeEventListener('mouseup', up)
+          this.dragState = 0
+        }
+        if (liLen > 1) {
+          document.addEventListener('mousemove', move)
+          document.addEventListener('mouseup', up)
+        }
+      },
+      copyPage (page) {
+        this.$store.dispatch('copyPage', page)
+      },
+      delPage (page) {
+        this.$store.dispatch('delPage', page)
+      },
+      addPage () {
+        this.$store.dispatch('addPage')
+      },
+      setEditingPage (page) {
+        this.$store.dispatch('setEditorPage', page)
+      },
+      setEditingLayer (event, layer) {
+        this.$store.dispatch('setEditorElement', layer)
+      },
+      updateLayersSort () {
+        this.$store.dispatch('sortElementsByZindex')
+      }
+    },
+    components: { Page }
+  }
+</script>
+<style lang="less" scoped>
+  .overview {
+    position: relative;
+    border-right: 1px solid #d6d6d6;
+    background-color: #ececec;
+    height: 100%;
+    .panel {
+      float: left;
+      line-height: 40px;
+      width: 50%;
+      text-align: center;
+      background-color: #d6d6d6;
+      cursor: pointer;
+      &.active {
+        background-color: transparent;
+      }
+    }
+    .list {
+      background-color: #ececec;
+      position: absolute;
+      top: 40px;
+      bottom: 50px;
+      width: 100%;
+      overflow-y: auto;
+      overflow-x: hidden;
+    }
+    .dragging:before {
+      content: "";
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 10;
+    }
+    .page {
+      position: relative;
+      border-color: transparent;
+      border-style: solid;
+      border-width: 4px 4px 30px;
+      margin: 10px;
+      &.active {
+        border-color: #18ccc0;
+        .icons {
+          display: block;
+        }
+      }
+      &:before {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+        z-index: 2;
+      }
+      .content {
+        transform-origin: left top;
+        background-color: #fff;
+        overflow: hidden;
+        position: relative;
+      }
+      .icons {
+        position: absolute;
+        bottom: -1.5em;
+        right: 0.5em;
+        display: none;
+        width: 100%;
+        color: #fff;
+        .icon {
+          float: right;
+          margin-left: 1em;
+          opacity: 0.5;
+          cursor: pointer;
+          &:hover {
+            opacity: 1;
+          }
+        }
+      }
+    }
+    .layer {
+      padding-left: 20px;
+      height: 30px;
+      line-height: 30px;
+      border-bottom: 1px solid #d6d6d6;
+      cursor: pointer;
+      &[data-moving] {
+        background-color: #d6d6d6;
+        position: absolute;
+        width: 100%;
+      }
+      &:hover {
+        background-color: #d6d6d6;
+      }
+      &.active {
+        background-color: #18ccc0;
+        color: #fff;
+      }
+      .thumb {
+        display: inline-block;
+        width: 15px;
+        height: 15px;
+        margin-right: 1em;
+        background: white center no-repeat;
+        background-size: cover;
+      }
+    }
+    .add {
+      border: none;
+      position: absolute;
+      bottom: 0;
+      height: 50px;
+      line-height: 50px;
+      width: 100%;
+      left: 0;
+      background-color: #373f42;
+      text-align: center;
+      color: #fff;
+      cursor: pointer;
+    }
+  }
+</style>

+ 153 - 0
webapp/src/views/h5editor/themeList.vue

@@ -0,0 +1,153 @@
+<template>
+  <div>
+    <HeaderBar/>
+    <div class="my-themes">
+      <div class="container">
+        <ul class="theme-list">
+          <li class="theme-item create" @click="create">
+            <div class="create-area">
+              <p>创建作品</p>
+            </div>
+          </li>
+          <template v-for="item in list">
+            <li class="theme-item" @click="toEditor(item)">
+              <div class="thumb" >
+                <img src="../../assets/images/default.png" alt="">
+              </div>
+              <div class="footer">
+                <div class="title">{{item.title}}</div>
+                <div class="content">{{item.description}}</div>
+                <el-button class="delete" @click.stop="deleteTheme(item)" type="danger">删除</el-button>
+              </div>
+            </li>
+          </template>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import HeaderBar from '../../components/HeaderBar'
+  import tools from '../../util/tools'
+  // import ThemeItem from '../../components/ThemeItem'
+  export default {
+    computed: {
+      list () {
+        return this.$store.state.editor.themeList
+      }
+    },
+    mounted () {
+      this.$store.dispatch('getUserThemeList', 'h5')
+    },
+    methods: {
+      toEditor (item) {
+        this.$store.dispatch('setEditorTheme', item)
+        this.$store.dispatch('setEditorPage', item.pages[0])
+        this.$router.replace({ path: '/h5editor', query: { itemId: item._id } })
+      },
+      deleteTheme (item) {
+        this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$store.dispatch('deleteTheme', item)
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          })
+        }).catch(() => {
+          this.$message({
+            type: 'info',
+            message: '已取消删除'
+          })
+        })
+      },
+      create () {
+        this.$store.dispatch('createTheme', 'h5')
+        this.$store.dispatch('addPage')
+        let $this = this
+        this.$store.dispatch('saveTheme', tools.vue2json(this.$store.state.editor.editorTheme)).then(() => {
+          this.$router.replace({ path: '/h5editor', query: { itemId: $this.$store.state.editor.editorTheme._id } })
+        })
+      }
+    },
+    components: {
+      HeaderBar
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  .my-themes {
+    width: 100%;
+    height: 100%;
+    background-color: #f2f5f6;
+  }
+
+  .my-themes .container {
+    width: 1024px;
+    margin: 0 auto;
+    padding-top: 20px;
+  }
+
+  .my-themes .theme-list {
+    overflow: hidden;
+  }
+
+  .theme-item {
+    width: 230px;
+    height: 328px;
+    float: left;
+    margin-right: 20px;
+    margin-bottom: 20px;
+    background: #fff;
+  }
+
+  .theme-item .thumb img {
+    width: 100%;
+    height: 230px;
+  }
+
+  .theme-item .footer {
+    height: 98px;
+    padding: 10px;
+    background-color: #fff;
+    box-sizing: border-box;
+    position: relative;
+  }
+
+  .theme-item .footer > .title {
+    color: #4a4a4a;
+    font-size: 14px;
+    width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .theme-item .footer > .content {
+    color: #83817b;
+    margin-top: 12px;
+    font-size: 14px;
+    max-height: 40px;
+    overflow: hidden;
+    line-height: 1.5;
+  }
+  .footer .delete {
+    position: absolute;
+    right: 10px;
+    bottom: 10px;
+  }
+
+  .theme-item.create {
+    text-align: center;
+  }
+
+  .theme-item.create .create-area p {
+    font-size: 20px;
+    cursor: pointer;
+    margin-top: 100px;
+  }
+</style>

File diff suppressed because it is too large
+ 598 - 0
webapp/src/views/spaeditor/index.vue


+ 267 - 0
webapp/src/views/spaeditor/overview.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="overview">
+    <div class="clearfix">
+      <!--<span class="panel" :class="{ active: viewState === 0 }" @click="function () { viewState = 0 }">页面</span>-->
+      <span class="panel" :class="{ active: viewState === 1 }" @click="function () { viewState = 1 }">图层</span>
+    </div>
+    <ul class="list custom-scrollbar" style="z-index: 1;">
+      <li v-for="page in pages">
+        <div class="page" :class="{ active: page === editingPage }" :style="{ width: 131 + 8 + 'px', height: (131 / canvasWidth) * canvasHeight + 34 + 'px' }" @click="setEditingPage(page)">
+          <Page class="content" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', transform: 'scale(' + 131 / canvasWidth +')' }" :elements="page.elements" type="see" />
+          <div class="icons clearfix">
+            <i class="icon el-icon-delete" @click="delPage(page)"></i>
+            <i class="icon el-icon-document" @click="copyPage(page)"></i>
+          </div>
+        </div>
+      </li>
+    </ul>
+    <div class="list custom-scrollbar" style="z-index: 2;" v-show="viewState === 1" :class="{ dragging: dragState === 1 }">
+      <ul>
+        <li v-for="layer in layersNoBg">
+          <div class="layer" :class="{ active: editingLayer === layer}" @click="setEditingLayer($event, layer)" @mousedown="moveLayer">
+            <span class="thumb" :style="{ backgroundImage: 'url(' + http + layer.imgSrc + ')' }"></span>{{ layer.type }}
+          </div>
+        </li>
+      </ul>
+      <div v-for="layer in layersBg" class="layer" :class="{ active: editingLayer === layer}" @click="setEditingLayer($event, layer)">
+        <span class="thumb" :style="{ backgroundImage: 'url(' + http + layer.imgSrc + ')' }"></span>{{ layer.type }}
+      </div>
+    </div>
+    <!--<button class="add el-icon-plus" @click="addPage"></button>-->
+  </div>
+</template>
+<script>
+  import Page from './../../components/Page'
+  import AppConst from '../../util/appConst'
+  export default {
+    data () {
+      return {
+        viewState: 1,
+        dragState: 0,
+        http: AppConst.BACKEND_DOMAIN
+      }
+    },
+    computed: {
+      vxEditor () {
+        return this.$store.state['editor']
+      },
+      canvasWidth () {
+        return this.vxEditor['canvasWidth']
+      },
+      canvasHeight () {
+        return this.vxEditor['canvasHeight']
+      },
+      pages () {
+        return this.vxEditor['editorTheme']['pages']
+      },
+      editingPage () {
+        return this.vxEditor['editorPage']
+      },
+      layers () {
+        return this.editingPage['elements']
+      },
+      layersNoBg () {
+        return this.layers && this.layers.filter(v => v['type'] !== 'bg').reverse()
+      },
+      layersBg () {
+        return this.layers && this.layers.filter(v => v['type'] === 'bg')
+      },
+      editingLayer () {
+        return this.vxEditor['editorElement']
+      }
+    },
+    methods: {
+      moveLayer (downEvent) {
+        let height = 30
+        let timer = null
+        let layer = downEvent.target
+        let li = layer.parentNode
+        let parent = li.parentNode
+        let liLen = parent.childNodes.length
+        let startTop = li.offsetTop
+        let startIndex = Math.round(startTop / height)
+        let targetIndex = null
+        let placeholder = document.createElement('li')
+        placeholder.style = 'height: ' + height + 'px; background-color: #d6d6d6'
+        let move = (moveEvent) => {
+          if (!timer) {
+            // 被拖动的层
+            let top = moveEvent.clientY - downEvent.clientY + startTop
+            layer.setAttribute('data-moving', true)
+            layer.style.top = top + 'px'
+            this.dragState = 1
+            // 占位层
+            let nowIndex = Math.round(top / height)
+            nowIndex = nowIndex <= 0 ? 0 : nowIndex > liLen - 1 ? liLen - 1 : nowIndex
+            if (targetIndex !== nowIndex) {
+              (targetIndex || targetIndex === 0) && parent.removeChild(placeholder)
+              targetIndex = nowIndex
+              parent.insertBefore(placeholder, parent.childNodes[nowIndex + (startIndex >= targetIndex ? 0 : 1)])
+            }
+            // timer负责减少onmousemove对客户端的负担
+            timer = setTimeout(() => {
+              timer = null
+            }, 20)
+          }
+        }
+        let up = (upEvent) => {
+          if (layer.getAttribute('data-moving')) {
+            layer.removeAttribute('data-moving')
+            layer.style.top = ''
+            parent.removeChild(placeholder)
+            this.layersNoBg[startIndex]['zindex'] = this.layersNoBg[targetIndex]['zindex'] + (targetIndex > startIndex ? -0.5 : 0.5)
+            this.updateLayersSort()
+          }
+          document.removeEventListener('mousemove', move)
+          document.removeEventListener('mouseup', up)
+          this.dragState = 0
+        }
+        if (liLen > 1) {
+          document.addEventListener('mousemove', move)
+          document.addEventListener('mouseup', up)
+        }
+      },
+      copyPage (page) {
+        this.$store.dispatch('copyPage', page)
+      },
+      delPage (page) {
+        this.$store.dispatch('delPage', page)
+      },
+      addPage () {
+        this.$store.dispatch('addPage')
+      },
+      setEditingPage (page) {
+        this.$store.dispatch('setEditorPage', page)
+      },
+      setEditingLayer (event, layer) {
+        this.$store.dispatch('setEditorElement', layer)
+      },
+      updateLayersSort () {
+        this.$store.dispatch('sortElementsByZindex')
+      }
+    },
+    components: { Page }
+  }
+</script>
+<style lang="less" scoped>
+  .overview {
+    position: relative;
+    border-right: 1px solid #d6d6d6;
+    background-color: #ececec;
+    height: 100%;
+    .panel {
+      float: left;
+      line-height: 40px;
+      width: 50%;
+      text-align: center;
+      background-color: #d6d6d6;
+      cursor: pointer;
+      &.active {
+        background-color: transparent;
+      }
+    }
+    .list {
+      background-color: #ececec;
+      position: absolute;
+      top: 40px;
+      bottom: 50px;
+      width: 100%;
+      overflow-y: auto;
+      overflow-x: hidden;
+    }
+    .dragging:before {
+      content: "";
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 10;
+    }
+    .page {
+      position: relative;
+      border-color: transparent;
+      border-style: solid;
+      border-width: 4px 4px 30px;
+      margin: 10px;
+      &.active {
+        border-color: #18ccc0;
+        .icons {
+          display: block;
+        }
+      }
+      &:before {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+        z-index: 2;
+      }
+      .content {
+        transform-origin: left top;
+        background-color: #fff;
+        overflow: hidden;
+        position: relative;
+      }
+      .icons {
+        position: absolute;
+        bottom: -1.5em;
+        right: 0.5em;
+        display: none;
+        width: 100%;
+        color: #fff;
+        .icon {
+          float: right;
+          margin-left: 1em;
+          opacity: 0.5;
+          cursor: pointer;
+          &:hover {
+            opacity: 1;
+          }
+        }
+      }
+    }
+    .layer {
+      padding-left: 20px;
+      height: 30px;
+      line-height: 30px;
+      border-bottom: 1px solid #d6d6d6;
+      cursor: pointer;
+      &[data-moving] {
+        background-color: #d6d6d6;
+        position: absolute;
+        width: 100%;
+      }
+      &:hover {
+        background-color: #d6d6d6;
+      }
+      &.active {
+        background-color: #18ccc0;
+        color: #fff;
+      }
+      .thumb {
+        display: inline-block;
+        width: 15px;
+        height: 15px;
+        margin-right: 1em;
+        background: white center no-repeat;
+        background-size: cover;
+      }
+    }
+    .add {
+      border: none;
+      position: absolute;
+      bottom: 0;
+      height: 50px;
+      line-height: 50px;
+      width: 100%;
+      left: 0;
+      background-color: #373f42;
+      text-align: center;
+      color: #fff;
+      cursor: pointer;
+    }
+  }
+</style>

+ 153 - 0
webapp/src/views/spaeditor/themeList.vue

@@ -0,0 +1,153 @@
+<template>
+  <div>
+    <HeaderBar/>
+    <div class="my-themes">
+      <div class="container">
+        <ul class="theme-list">
+          <li class="theme-item create" @click="create">
+            <div class="create-area">
+              <p>创建作品</p>
+            </div>
+          </li>
+          <template v-for="item in list">
+            <li class="theme-item" @click="toEditor(item)">
+              <div class="thumb" >
+                <img src="../../assets/images/default.png" alt="">
+              </div>
+              <div class="footer">
+                <div class="title">{{item.title}}</div>
+                <div class="content">{{item.description}}</div>
+                <el-button class="delete" @click.stop="deleteTheme(item)" type="danger">删除</el-button>
+              </div>
+            </li>
+          </template>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import HeaderBar from '../../components/HeaderBar'
+  import tools from '../../util/tools'
+  // import ThemeItem from '../../components/ThemeItem'
+  export default {
+    computed: {
+      list () {
+        return this.$store.state.editor.themeList
+      }
+    },
+    mounted () {
+      this.$store.dispatch('getUserThemeList', 'spa')
+    },
+    methods: {
+      toEditor (item) {
+        this.$store.dispatch('setEditorTheme', item)
+        this.$store.dispatch('setEditorPage', item.pages[0])
+        this.$router.replace({ path: '/spaeditor', query: { itemId: item._id } })
+      },
+      deleteTheme (item) {
+        this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          this.$store.dispatch('deleteTheme', item)
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          })
+        }).catch(() => {
+          this.$message({
+            type: 'info',
+            message: '已取消删除'
+          })
+        })
+      },
+      create () {
+        this.$store.dispatch('createTheme', 'spa')
+        this.$store.dispatch('addPage')
+        let $this = this
+        this.$store.dispatch('saveTheme', tools.vue2json(this.$store.state.editor.editorTheme)).then(() => {
+          this.$router.replace({ path: '/spaeditor', query: { itemId: $this.$store.state.editor.editorTheme._id } })
+        })
+      }
+    },
+    components: {
+      HeaderBar
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  .my-themes {
+    width: 100%;
+    height: 100%;
+    background-color: #f2f5f6;
+  }
+
+  .my-themes .container {
+    width: 1024px;
+    margin: 0 auto;
+    padding-top: 20px;
+  }
+
+  .my-themes .theme-list {
+    overflow: hidden;
+  }
+
+  .theme-item {
+    width: 230px;
+    height: 328px;
+    float: left;
+    margin-right: 20px;
+    margin-bottom: 20px;
+    background: #fff;
+  }
+
+  .theme-item .thumb img {
+    width: 100%;
+    height: 230px;
+  }
+
+  .theme-item .footer {
+    height: 98px;
+    padding: 10px;
+    background-color: #fff;
+    box-sizing: border-box;
+    position: relative;
+  }
+
+  .theme-item .footer > .title {
+    color: #4a4a4a;
+    font-size: 14px;
+    width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .theme-item .footer > .content {
+    color: #83817b;
+    margin-top: 12px;
+    font-size: 14px;
+    max-height: 40px;
+    overflow: hidden;
+    line-height: 1.5;
+  }
+  .footer .delete {
+    position: absolute;
+    right: 10px;
+    bottom: 10px;
+  }
+
+  .theme-item.create {
+    text-align: center;
+  }
+
+  .theme-item.create .create-area p {
+    font-size: 20px;
+    cursor: pointer;
+    margin-top: 100px;
+  }
+</style>

+ 123 - 0
webapp/src/views/user/login.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="container">
+    <div class="login-main">
+      <div class="login-title">
+        蓝月亮前端
+      </div>
+      <div class="content">
+        <el-form :model="loginForm" ref="loginForm" :rules="loginRule">
+          <div class="error-info" v-if="loginResult">
+            <div><i class="el-icon-warning"></i><span>{{loginResult}}</span></div>
+          </div>
+          <el-form-item prop="loginId">
+            <el-input class="login-id" type="text" v-model="loginForm.loginId" placeholder="帐号(邮箱或者手机号)"></el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input class="password" type="password" v-model="loginForm.password" placeholder="密码"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button style="width:30%" :plain="true" type="success" @click.native.prevent="register">注册</el-button>
+            <el-button style="width:65%;float:right" type="primary" class="login-btn" @click.native.prevent="login">登录</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    data () {
+      return {
+        loginForm: {
+          loginId: '',
+          password: ''
+        },
+        loginRule: {
+          loginId: [
+            {required: true, message: '请输入邮箱或手机号', trigger: 'blur'}
+          ],
+          password: [
+            {required: true, message: '请输入密码', trigger: 'blur'}
+          ]
+        }
+      }
+    },
+    mounted () {
+      // 初始化错误提示
+      this.$store.commit('SET_ERROR_INFO', '')
+    },
+    computed: {
+      loginResult () {
+        return this.$store.state.user.loginResult
+      }
+    },
+    methods: {
+      login (ev) {
+        this.$refs.loginForm.validate((valid) => {
+          if (valid) {
+            this.$store.dispatch('login', {loginId: this.loginForm.loginId, password: this.loginForm.password})
+          } else {
+            return false
+          }
+        })
+      },
+      register () {
+        window.location.href = '#register'
+      }
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  .container {
+    background-image: url('../../assets/login-bg.jpg');
+    background-size: auto;
+    min-height: calc(100vh);
+  }
+
+  .error-info {
+    text-align: left;
+    background: #ffeeed;
+    padding: 7px 9px 7px;
+    margin: 0 0 10px;
+    border-radius: 6px;
+    line-height: 1.5;
+    color: #666;
+    font-size: 12px;
+    i {
+      color: #f60;
+    }
+    span {
+      padding-left: 10px;
+    }
+  }
+
+  .login-main {
+    width: 450px;
+    margin: 0 auto;
+    background-color: #fff;
+    position: relative;
+    top: 100px;
+  }
+
+  .login-title {
+    margin: 0;
+    padding: 30px 20px 26px;
+    text-align: center;
+    font-size: 18px;
+  }
+
+  .content {
+    padding: 0 25px 10px;
+  }
+
+  .form-item {
+    line-height: 1.5;
+    margin-bottom: 17px;
+  }
+
+  .login-btn {
+    width: 100%;
+  }
+</style>

+ 144 - 0
webapp/src/views/user/register.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="container">
+    <div class="login-main">
+      <div class="login-title">
+        蓝月亮前端
+      </div>
+      <div class="content">
+        <el-form :model="loginForm" ref="loginForm" :rules="loginRule">
+          <div class="error-info" v-if="errorInfo">
+            <div><i class="el-icon-warning"></i><span>{{errorInfo}}</span></div>
+          </div>
+          <el-form-item prop="loginId">
+            <el-input type="text" v-model="loginForm.loginId" placeholder="帐号(邮箱或者手机号)"></el-input>
+          </el-form-item>
+          <el-form-item prop="name">
+            <el-input type="text" v-model="loginForm.name" placeholder="昵称"></el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input type="password" v-model="loginForm.password" auto-complete="off" placeholder="密码"></el-input>
+          </el-form-item>
+          <el-form-item prop="checkPassword">
+            <el-input type="password" v-model="loginForm.checkPassword" auto-complete="off" placeholder="再次输入密码"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" class="login-btn" @click.native.prevent="register">注册</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    data () {
+      var validatePass = (rule, value, callback) => {
+        if (value === '') {
+          callback(new Error('请输入密码'))
+        } else {
+          if (this.loginForm.checkPassword !== '') {
+            this.$refs.loginForm.validateField('checkPassword')
+          }
+          callback()
+        }
+      }
+      var validatePass2 = (rule, value, callback) => {
+        if (value === '') {
+          callback(new Error('请再次输入密码'))
+        } else if (value !== this.loginForm.password) {
+          callback(new Error('两次输入密码不一致!'))
+        } else {
+          callback()
+        }
+      }
+      return {
+        errorInfo: '',
+        loginForm: {
+          loginId: '',
+          name: '',
+          password: '',
+          checkPassword: ''
+        },
+        loginRule: {
+          loginId: [
+            {required: true, message: '请输入邮箱或手机号', trigger: 'blur'}
+          ],
+          name: [
+            {required: true, message: '请输入昵称', trigger: 'blur'}
+          ],
+          password: [
+            { validator: validatePass, trigger: 'blur' }
+          ],
+          checkPassword: [
+            { validator: validatePass2, trigger: 'blur' }
+          ]
+        }
+      }
+    },
+    methods: {
+      register (ev) {
+        this.errorInfo = ''
+        this.$refs.loginForm.validate((valid) => {
+          if (valid) {
+            this.$store.dispatch('register', {loginId: this.loginForm.loginId, name: this.loginForm.name, password: this.loginForm.password})
+          } else {
+            return false
+          }
+        }
+        )
+      }
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  .container {
+    background-image: url('../../assets/login-bg.jpg');
+    background-size: auto;
+    min-height: calc(100vh);
+  }
+  .error-info {
+    text-align: left;
+    background: #ffeeed;
+    padding: 7px 9px 7px ;
+    margin: 0 0 10px;
+    border-radius: 6px;
+    line-height: 1.5;
+    color: #666;
+    font-size: 12px;
+  i {
+    color: #f60;
+  }
+  span {
+    padding-left: 10px;
+  }
+  }
+  .login-main {
+    width: 450px;
+    margin: 0 auto;
+    background-color: #fff;
+    position: relative;
+    top: 100px;
+  }
+
+  .login-title {
+    margin: 0;
+    padding: 30px 20px 26px;
+    text-align: center;
+    font-size: 18px;
+  }
+
+  .content {
+    padding: 0 25px 10px;
+  }
+
+  .form-item {
+    line-height: 1.5;
+    margin-bottom: 17px;
+  }
+
+  .login-btn {
+    width: 100%;
+  }
+</style>

+ 188 - 0
webapp/src/vuex/editor/actions.js

@@ -0,0 +1,188 @@
+import * as types from './mutation-type'
+import api from '../../api/editor'
+import Page from '../../model/Page'
+import Theme from '../../model/Theme'
+import Element from '../../model/Element'
+import tools from '../../util/tools'
+
+/**
+ * 保存页面数据
+ */
+export const saveTheme = ({commit}, theme) => {
+  if (theme && theme._id) {
+    return Promise.resolve(api.updateTheme(theme).then((res) => {
+      commit(types.UPDATE_THEME_SUCCESS, res)
+    }))
+  } else {
+    return Promise.resolve(api.saveTheme(theme).then((res) => {
+      commit(types.ADD_THEME_SUCCESS, res)
+    }))
+  }
+}
+
+/**
+ * 获取用户所有场景主题
+ * @param commit
+ */
+export const getUserThemeList = ({commit}, type) => {
+  api.getUserThemeList(type).then((res) => {
+    commit(types.GET_USER_THEME_LIST, res)
+  })
+}
+
+/**
+ * 创建场景主题
+ * @param commit
+ */
+
+export const createTheme = ({commit}, type) => {
+  var theme = new Theme({type: type})
+  commit(types.CREATE_THEME, theme)
+  commit(types.SET_CUR_EDITOR_THEME, theme)
+}
+
+/**
+ * 设置当前编辑的主题
+ */
+export const setEditorTheme = ({commit}, theme) => {
+  var newTheme = new Theme(theme)
+  commit(types.SET_CUR_EDITOR_THEME, newTheme)
+}
+
+/**
+ * 设置当前正在编辑的页面
+ * @param commit
+ * @param page
+ */
+export const setEditorPage = ({commit}, page) => {
+  commit(types.SET_CUR_EDITOR_PAGE, page)
+}
+
+/**
+ * 给主题添加页面
+ * @param commit
+ */
+export const addPage = ({commit}) => {
+  var page = new Page()
+  commit(types.ADD_PAGE, page)
+  commit(types.SET_CUR_EDITOR_PAGE, page)
+}
+
+/**
+ * 添加页面元素
+ */
+export const addElement = ({commit, state}, data) => {
+  commit(types.ADD_PIC_ELEMENT, new Element(data))
+  var list = state.editorPage.elements
+  var lastIndex = list.length - 1
+  list[lastIndex]['zindex'] = lastIndex ? list[lastIndex - 1]['zindex'] + 1 : 1
+  commit(types.SET_CUR_EDITOR_ELEMENT, state.editorPage.elements[lastIndex])
+}
+
+/**
+ * 添加背景图片
+ */
+export const addBGElement = ({commit}, data) => {
+  var element = new Element(data)
+  commit(types.SET_BG_ELEMENT, element)
+  commit(types.SET_CUR_EDITOR_ELEMENT, null)
+}
+
+/**
+ * 保存图片
+ * @param commit
+ * @param data
+ */
+export const savePic = ({commit}, data) => {
+  api.uploadPic(data).then((res) => {
+    // commit(types.SAVE_PIC, res)
+    commit(types.PUSH_PIC_LIST, res)
+  })
+}
+/**
+ * 清除背景
+ * @param commit
+ */
+export const cleanBG = ({commit}) => {
+  commit(types.CLEAN_BG)
+}
+
+export const cleanEle = ({commit}, ele) => {
+  commit(types.CLEAN_ELE, ele)
+}
+/**
+ * 复制页面
+ * @param commit
+ */
+export const copyPage = ({commit}, data) => {
+  var page = tools.vue2json(data)
+  commit(types.ADD_PAGE, page)
+}
+
+/**
+ * 删除页面
+ * @param commit
+ */
+export const delPage = ({commit}, page) => {
+  commit(types.DELETE_PAGE, page)
+}
+
+export const getPageByThemeId = ({dispatch, commit}, id) => {
+  api.getPageByThemeId(id).then((res) => {
+    console.log(id)
+    commit(types.SET_CUR_EDITOR_THEME, res)
+    commit(types.SET_CUR_EDITOR_PAGE, res.pages[0])
+  }).then(() => {
+    dispatch('sortElementsByZindex')
+  })
+}
+
+export const setEditorElement = ({commit}, element) => {
+  commit(types.SET_CUR_EDITOR_ELEMENT, element)
+}
+
+// 删除元素
+export const deleteElement = ({commit}, element) => {
+  commit(types.DELETE_ELEMENT, element)
+}
+
+export const deleteSelectedElement = ({commit, state}) => {
+  commit(types.DELETE_ELEMENT, state.editorElement)
+}
+
+export const playAnimate = ({state, commit, getters}) => {
+  commit(types.PLAY_ANIMATE)
+  let target = getters['editingElement'] || getters['editingPageElements'] || null
+  let time = 0
+  if (target instanceof Array) {
+    target.forEach(v => {
+      time = v['animatedName'] && (v['duration'] + v['delay']) > time ? (v['duration'] + v['delay']) : time
+    })
+  } else if (target instanceof Object) {
+    time = (target['duration'] + target['delay'])
+  }
+  setTimeout(() => {
+    commit(types.STOP_ANIMATE, target)
+  }, time * 1000)
+}
+
+export const getPicListByThemeId = ({commit}, _id) => {
+  api.getPicListByThemeId(_id).then((res) => {
+    commit(types.FETCH_PIC_LIST, res)
+  })
+}
+
+export const cleanPicList = ({commit}) => {
+  commit(types.CLEAN_PIC_LIST)
+}
+
+export const sortElementsByZindex = ({commit}, location) => {
+  commit(types.SORTELEMENTS_BY_ZINDEX, location)
+}
+
+export const deleteTheme = ({commit}, theme) => {
+  return Promise.resolve(api.delTheme(theme).then((res) => {
+    commit(types.DELETE_THEME, theme)
+  }))
+}
+

+ 32 - 0
webapp/src/vuex/editor/getters.js

@@ -0,0 +1,32 @@
+export const editingElement = state => {
+  return state['editorElement']
+}
+
+export const editingTheme = state => {
+  return state['editorTheme']
+}
+
+export const editingPage = state => {
+  return state['editorPage']
+}
+
+export const editingPageElements = state => {
+  console.log(state['editorPage'])
+  console.log(state['editorPage']['elements'])
+  return state['editorPage']['elements']
+}
+
+export const elements = (state, getter) => {
+  let pages = getter['editingTheme']['pages']
+  if (pages) {
+    let result = []
+    pages.forEach(v => {
+      v['elements'] && v['elements'].forEach(v => {
+        result.push(v)
+      })
+    })
+    return result
+  } else {
+    return null
+  }
+}

+ 24 - 0
webapp/src/vuex/editor/index.js

@@ -0,0 +1,24 @@
+import mutations from './mutations'
+import * as actions from './actions'
+import * as getters from './getters'
+
+const state = {
+  editorElement: {}, // 正在编辑的元素
+  editorPage: {
+    elements: []
+  }, // 正在编辑的页面
+  themeList: [], // 用户所有主题列表
+  editorTheme: {
+    title: '蓝月亮',
+    description: '蓝月亮',
+    canvasHeight: 504
+  }, // 正在编辑的主题
+  picList: [] // 图片列表
+}
+
+export default{
+  state,
+  getters,
+  actions,
+  mutations
+}

+ 25 - 0
webapp/src/vuex/editor/mutation-type.js

@@ -0,0 +1,25 @@
+export const SET_CUR_EDITOR_ELEMENT = 'SET_CUR_EDITOR_ELEMENT' // 设置当前编辑的h5元素
+export const ADD_PIC_ELEMENT = 'ADD_PIC_ELEMENT' // 添加图片元素
+export const PLAY_ANIMATE = 'PLAY_ANIMATE' // 播放动画
+export const STOP_ANIMATE = 'STOP_ANIMATE' // 播放动画
+export const ADD_PAGE = 'ADD_PAGE' // 添加页面
+export const DELETE_PAGE = 'DELETE_PAGE' // 删除页面
+export const SET_CUR_EDITOR_PAGE = 'SET_CUR_EDITOR_PAGE' // 设置当前编辑的页面
+export const GET_USER_THEME_LIST = 'GET_USER_THEME_LIST' // 获取用户h5列表
+export const SET_CUR_EDITOR_THEME = 'SET_CUR_EDITOR_THEME' // 设置当前编辑h5页面
+export const DELETE_ELEMENT = 'DELETE_ELEMENT' // 删除元素
+export const CREATE_THEME = 'CREATE_THEME' // 创建主题
+export const UPDATE_THEME_DES = 'UPDATE_THEME_DES' // 更新主题描述
+export const UPDATE_THEME_SUCCESS = 'UPDATE_THEME_SUCCESS' // 更新数据成功
+export const ADD_THEME_SUCCESS = 'ADD_THEME_SUCCESS' // 新增页面数据成功
+export const SAVE_PIC = 'SAVE_PIC' // 保存图片
+export const COPY_PAGE = 'COPY_PAGE' // 复制页面
+export const GET_PAGE_THEMEID = 'GET_PAGE_THEMEID' // 根据主题ID获取页面
+export const CLEAN_BG = 'CLEAN_BG' // 清除背景图
+export const CLEAN_ELE = 'CLEAN_ELE' // 清除元素
+export const FETCH_PIC_LIST = 'FETCH_PIC_LIST' // 获取图片列表
+export const SET_BG_ELEMENT = 'SET_BG_ELEMENT' // 设置背景图
+export const PUSH_PIC_LIST = 'PUSH_PIC_LIST' // 图片列表
+export const CLEAN_PIC_LIST = 'CLEAN_PIC_LIST' // 清除图片列表
+export const DELETE_THEME = 'DELETE_THEME' // 删除主题
+export const SORTELEMENTS_BY_ZINDEX = 'SORTELEMENTS_BY_ZINDEX' // 元素重新排序

+ 0 - 0
webapp/src/vuex/editor/mutations.js


Some files were not shown because too many files changed in this diff