├── .babelrc ├── .gitignore ├── README.md ├── app.js ├── app ├── controllers │ ├── h5.server.controller.js │ └── h5.server.rpc.js ├── models │ └── h5.server.model.js └── routes │ └── h5.server.routes.js ├── bin └── www ├── config ├── config.js ├── express.js └── mongoose.js ├── images ├── createNewWork.png ├── design.gif ├── design.png ├── design2.gif ├── login.gif ├── login.png ├── music.png ├── newWork.gif ├── output.gif └── work.png ├── package.json ├── views ├── app.js ├── components │ ├── Alert.vue │ ├── DesignWorkPage.vue │ ├── LoginImage.vue │ ├── LoginInput.vue │ ├── Navigation.vue │ ├── createWorkDialog.vue │ └── shadow.vue ├── images │ ├── H5.png │ ├── add.png │ ├── mobilePhone.png │ └── music.png ├── index.html ├── layouts │ ├── Home.vue │ ├── Login.vue │ ├── MobilePhone.vue │ ├── PageList.vue │ ├── Setup.vue │ ├── Work.vue │ ├── imageDialog.vue │ ├── modifyDesign.vue │ └── musicDialog.vue ├── pages │ ├── 404.vue │ ├── Design.vue │ ├── Home.vue │ ├── Main.vue │ └── Register.vue ├── routes.js ├── show.ejs ├── static │ ├── animation.css │ ├── jquery-3.0.0.js │ ├── show.css │ ├── show.js │ └── toucher.js └── store │ ├── actions.js │ ├── modules │ ├── Design.js │ ├── User.js │ └── Work.js │ ├── mutations.js │ ├── state.js │ └── store.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-1", 5 | "stage-2", 6 | "stage-3", 7 | ], 8 | "plugins": ["transform-async-to-generator"] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | views/static/ 3 | .DS_Store 4 | npm-debug.log 5 | npm-debug.log.* 6 | yarn-debug.log* 7 | yarn-error.log*· -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # H5 2 | 3 | 4 | 5 | ## 简介 6 | 7 | H5是一款可视化编辑手机H5页面的单页应用WebApp,可以根据用户的设计自动产生精美的H5界面。 8 | 9 | 类似市面上易企秀、简客这样的app。 10 | 11 | 这是我的个人项目,旨在学习Vue.js以及前后端开发流程。 12 | 13 | 14 | 15 | ## 运行 16 | 17 | ```bash 18 | cd H5 19 | 20 | # install dependencies 21 | npm install 22 | 23 | # app running 24 | npm start 25 | ``` 26 | 27 | 28 | open localhost:2000 29 | 30 | 31 | ## 技术 32 | 33 | 前端:Vue.js + Vuex 34 | 35 | 后端:Node.js 36 | 37 | 构建:webpack 38 | 39 | 数据库: MongoDB 40 | 41 | UI: Muse-UI (https://museui.github.io/#/index) 42 | 43 | 44 | ## 目录结构 45 |
46 | ├─ package.json # 项目配置 47 | ├─ README.md # 项目说明 48 | ├─ app.js # 项目入口文件 49 | ├─ images # 静态图片 50 | ├─ userData # 用户数据 51 | ├─ node_modules # npm依赖包 52 | ├─ webpack.config.js # webpack配置文件 53 | ├─ .babelrc # babel配置 54 | │ 55 | │ 56 | ├─ app # node后端业务 57 | │ 58 | │ controllers # 控制器 59 | │ models # 数据模型 60 | │ routes # 路由分发 61 | │ 62 | │ 63 | ├─ bin # node启动 64 | │ 65 | │ 66 | └─ views # 前端代码 67 | │ app.js # 前端页面入口文件 68 | │ components # 基础组件 69 | │ dist # 编译生成文件 70 | │ index.html # 首页 71 | │ layouts # 布局 72 | │ music # 用户静态音乐 73 | │ pages # 根据路由切换页面 74 | │ routes.js # 路由表 75 | │ show.ejs # ejs模板,用以产生H5页面 76 | │ static # 静态html以及css文件 77 | │ store # vuex store 78 | │ 79 | │ 80 | └─ config # node配置 81 |82 | 83 | 84 | ## 效果图 85 | 86 |  87 | 88 |  89 | 90 |  91 | 92 |  93 | 94 |  95 | 96 |  97 | 98 |  99 | 100 |  101 | 102 |  103 | 104 |  105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let express = require('./config/express'); 4 | let mongodb = require('./config/mongoose'); 5 | 6 | let db = mongodb(); 7 | let app = express(); 8 | 9 | module.exports = app; -------------------------------------------------------------------------------- /app/controllers/h5.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let mongoose = require('mongoose'); 4 | let rpc = require('./h5.server.rpc'); 5 | let formidable = require('formidable'); 6 | let fs = require('fs'); 7 | let cfg = require('../../config/config'); 8 | let qr = require('qr-image') 9 | 10 | /*mongoose*/ 11 | let Users = mongoose.model('Users'); 12 | let UserMusics = mongoose.model('UserMusics'); 13 | let UserImages = mongoose.model('UserImages'); 14 | let UserDesigns = mongoose.model('UserDesigns'); 15 | 16 | /*产生4位随机数带上时间*/ 17 | function RandomKey(){ 18 | function getNum(){ 19 | return Math.ceil(Math.random() * 10) - 1; /*产生0-9的随机数*/ 20 | } 21 | 22 | let Key = ""; 23 | for(let i=0; i<4; i++){ 24 | Key += getNum(); 25 | } 26 | 27 | return (Key + Date.parse(new Date())); 28 | } 29 | 30 | module.exports = { 31 | 32 | /** 33 | * 用户登陆 34 | * 35 | * 用户登陆接口,返回是否登陆成功。 36 | * 37 | * @param req 38 | * @param res 39 | * @param next 40 | * @returns void 41 | * 42 | * @date 2016-12-27 43 | * @author Cao Yang 44 | */ 45 | login(req,res,next){ 46 | let params = req.body.params || {}; 47 | 48 | if (!params.userName) { 49 | res.json({result: false, content: '用户名为空'}); 50 | return; 51 | } 52 | 53 | if (!params.passWord) { 54 | res.json({result: false, content: '密码为空'}); 55 | return; 56 | } 57 | 58 | Users.find({userName:params.userName}, null,{},function(err,result){ 59 | if (err) { 60 | console.log('login find err!'); 61 | res.json({result: false, content: '登陆失败'}); 62 | return next(err); 63 | } 64 | else{ 65 | if (result.length && (result[0].passWord === params.passWord)) { 66 | req.session.user = {'userName': params.userName}; 67 | console.log('login req.session.user',req.session.user) 68 | res.json({ 69 | result: true, 70 | params: { 71 | userInfo: result[0], 72 | } 73 | }); 74 | } 75 | else if(result.length === 0){ 76 | res.json({result: false, content: '用户不存在'}); 77 | } 78 | else{ 79 | res.json({result: false, content: '用户名或密码错误,请重试'}); 80 | } 81 | } 82 | }) 83 | 84 | }, 85 | 86 | /** 87 | * 用户注册 88 | * 89 | * 用户注册接口,注册新用户。 90 | * 91 | * @param req 92 | * @param res 93 | * @param next 94 | * @returns void 95 | * 96 | * @date 2016-12-27 97 | * @author Cao Yang 98 | */ 99 | register(req,res,next){ 100 | if (req.body && req.body.params) { 101 | let params = req.body.params || {}; 102 | 103 | if (!params.userName) { 104 | res.json({result: false, content: '用户名为空'}); 105 | return; 106 | } 107 | 108 | if (!params.passWord) { 109 | res.json({result: false, content: '密码为空'}); 110 | return; 111 | } 112 | 113 | if (!params.eMail) { 114 | res.json({result: false, content: '邮箱为空'}); 115 | return; 116 | } 117 | 118 | let user = new Users({ 119 | userName: params.userName, 120 | passWord: params.passWord, 121 | eMail: params.eMail, 122 | nikeName: params.userName, 123 | userImage: "/image/defaultHeadPortrait.png", 124 | Gender: 'Man', 125 | age: '18', 126 | personalizedSignature: '', 127 | place: '', 128 | }); 129 | 130 | user.save(function(err){ 131 | if (err) { 132 | console.log('Register err!' + err); 133 | res.json({result: false, content: '注册失败'}); 134 | return next(err); 135 | } 136 | else{ 137 | console.log('Register ' + params.userName + ' successed.'); 138 | res.json({result: true}); 139 | } 140 | }) 141 | 142 | } 143 | else{ 144 | res.json({result: false, content: '数据格式有误'}); 145 | } 146 | }, 147 | 148 | /** 149 | * RPC接口 150 | * 151 | * 对RPC接口做同一操作。 152 | * 153 | * @param req 154 | * @param res 155 | * @param next 156 | * @returns void 157 | * 158 | * @date 2017-1-1 159 | * @author Cao Yang 160 | */ 161 | rpcMethon(req,res,next){ 162 | let params = req.body.params || {}; 163 | let method = req.body.method; 164 | if ((typeof rpc[method]) === 'function') { 165 | rpc[method](req,res,next); 166 | } 167 | else{ 168 | res.json({result: false, content: '未找到RPC接口'}); 169 | } 170 | }, 171 | 172 | /** 173 | * 上传音乐 174 | * 175 | * 上传本地音乐文件接口 176 | * 177 | * @param req 178 | * @param res 179 | * @param next 180 | * @returns void 181 | * 182 | * @date 2017-1-12 183 | * @author Cao Yang 184 | */ 185 | UploadMusic(req, res, next){ 186 | let form = new formidable.IncomingForm(); 187 | form.uploadDir = __dirname+'/../../userData/musics'; 188 | form.keepExtensions = true; 189 | form.maxFieldsSize = 2*1024*1024;/*限制图片大小最大为2M*/ 190 | 191 | form.parse(req,(err,fields,files) => { 192 | if (files.music.type.indexOf('audio') >= 0) { 193 | let path = files.music.path; 194 | let newPath = path.slice(0, path.lastIndexOf('/'))+'/'+req.session.user.userName+'-'+RandomKey()+ path.slice(path.lastIndexOf('.'),path.length); 195 | fs.rename(path, newPath); 196 | 197 | /*save in mongoDB*/ 198 | let music = new UserMusics({ 199 | userName: req.session.user.userName, 200 | path: newPath, 201 | musicName: files.music.name, 202 | type: files.music.type, 203 | size: files.music.size, 204 | }); 205 | 206 | music.save(err => { 207 | if (err) { 208 | res.json({result: false, content: '上传音乐失败'}); 209 | return next(err); 210 | } 211 | else{ 212 | res.json({result: true}); 213 | } 214 | }) 215 | } 216 | else{ 217 | res.json({result: false, content: '音频文件格式有误'}); 218 | } 219 | }) 220 | }, 221 | 222 | /** 223 | * 播放音乐 224 | * 225 | * 播放音乐接口 226 | * 227 | * @param req 228 | * @param res 229 | * @param next 230 | * @returns void 231 | * 232 | * @date 2017-1-16 233 | * @author Cao Yang 234 | */ 235 | PlayMusic(req, res, next){ 236 | UserMusics.findById(req.query.id, (err, result) => { 237 | if (result) { 238 | res.writeHead(200, {'Content-Type': 'video/mp4'}); 239 | let rs = fs.createReadStream(result.path); 240 | 241 | rs.pipe(res); 242 | rs.on('end',function(){ 243 | res.end(); 244 | }); 245 | } 246 | }); 247 | }, 248 | 249 | /** 250 | * 上传图片 251 | * 252 | * 上传本地图片文件接口 253 | * 254 | * @param req 255 | * @param res 256 | * @param next 257 | * @returns void 258 | * 259 | * @date 2017-1-27 260 | * @author Cao Yang 261 | */ 262 | UploadImage(req, res, next){ 263 | let form = new formidable.IncomingForm(); 264 | form.uploadDir = __dirname+'/../../userData/images'; 265 | form.encoding = 'utf-8'; 266 | form.keepExtensions = true; 267 | form.maxFieldsSize = 2*1024*1024;/*限制图片大小最大为2M*/ 268 | 269 | form.parse(req,(err,fields,files) => { 270 | let fileType; 271 | switch(files.image.type){ 272 | case 'image/jpeg': 273 | fileType = 'jpeg'; 274 | break; 275 | case 'image/png': 276 | fileType = 'png'; 277 | break; 278 | case 'image/jpg': 279 | fileType = 'jpg'; 280 | break; 281 | } 282 | 283 | if (fileType === undefined) {/*上传的图片格式没有按照指定要求*/ 284 | res.send('uploadIcon img type err'); 285 | return; 286 | }; 287 | 288 | let path = files.image.path; 289 | let newPath = path.slice(0, path.lastIndexOf('/'))+'/'+req.session.user.userName+'-'+RandomKey()+ path.slice(path.lastIndexOf('.'),path.length); 290 | fs.rename(path, newPath); 291 | 292 | /*save in mongoDB*/ 293 | let image = new UserImages({ 294 | userName: req.session.user.userName, 295 | path: newPath, 296 | type: files.image.type, 297 | size: files.image.size, 298 | }); 299 | 300 | image.save(err => { 301 | if (err) { 302 | res.json({result: false, content: '上传图片失败'}); 303 | return next(err); 304 | } 305 | else{ 306 | res.json({result: true}); 307 | } 308 | }) 309 | }) 310 | 311 | }, 312 | 313 | /** 314 | * 显示图片 315 | * 316 | * 显示图片接口 317 | * 318 | * @param req 319 | * @param res 320 | * @param next 321 | * @returns void 322 | * 323 | * @date 2017-1-29 324 | * @author Cao Yang 325 | */ 326 | showImage(req, res, next){ 327 | UserImages.findById(req.query.id, (err, result) => { 328 | if (result) { 329 | 330 | res.writeHead(200, {'Content-Type': result.type}); 331 | let rs = fs.createReadStream(result.path); 332 | 333 | rs.pipe(res); 334 | rs.on('end',function(){ 335 | res.end(); 336 | }); 337 | } 338 | else{ 339 | next(); 340 | } 341 | }); 342 | }, 343 | 344 | /** 345 | * 显示生成页面 346 | * 347 | * 显示生成页面接口 348 | * 349 | * @param req 350 | * @param res 351 | * @param next 352 | * @returns void 353 | * 354 | * @date 2017-2-3 355 | * @author Cao Yang 356 | */ 357 | showPage(req, res, next){ 358 | UserDesigns.find({userName: req.query.userName, workName: req.query.workName,}, null,{}, (err, result) => { 359 | if (err) { 360 | console.log('showPage err!' + err); 361 | res.json({result: false, content: '获取页面失败'}); 362 | return next(err); 363 | } 364 | else{ 365 | console.log(result) 366 | if (result.length) { 367 | 368 | res.render("show",{cfg,"designInfos":result[0].designInfos}); 369 | } 370 | else{ 371 | res.send('404 no found.'); 372 | } 373 | } 374 | }) 375 | }, 376 | 377 | /** 378 | * 二维码接口 379 | * 380 | * 根据URL生成对应的二维码图片 381 | * 382 | * @param req 383 | * @param res 384 | * @param next 385 | * @returns void 386 | * 387 | * @date 2017-3-19 388 | * @author Cao Yang 389 | */ 390 | QRcode(req, res, next){ 391 | let userName = req.query.userName || ''; 392 | let workName = req.query.workName || ''; 393 | let url = cfg.ip + ':' + cfg.port + '/H5/Show?userName=' + userName + '&workName=' + workName; 394 | try { 395 | var img = qr.image(url,{size :10}); 396 | res.writeHead(200, {'Content-Type': 'image/png'}); 397 | img.pipe(res); 398 | } catch (e) { 399 | res.writeHead(414, {'Content-Type': 'text/html'}); 400 | res.end('
404 No Find!
3 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /views/pages/Design.vue: -------------------------------------------------------------------------------- 1 | 2 |