2.1 需求说明
1)展示最多50条用户的许愿信息;
2)许愿信息使用便签的形式粘贴在页面上;
3)便签背景色随机生成,并展示在页面的任意位置;
4)可以拖动便签;
5)可以添加许愿信息;
6)添加许愿信息的时候要进行验证,不允许提交空的姓名或空的许愿内容。
2.2 效果展示
本节项目对这本书的“许愿墙”demo做了优化,整个页面样式功能以及后端接口处理都做了优化,效果如下:
http://mpvideo.qpic.cn/0b78iqaaaaaakqamvix5jjpfargdabcaaaaa.f10002.mp4?dis_k=68ecb5078dcac6e528e122c80647d541&dis_t=1649318630&vid=wxv_1410936510232526848&format_id=10002&support_redirect=0&mmversion=false
2.3 创建MySQL数据库表
可以使用 mysql 可视化工具进行创建,数据库和数据库表都叫 wish。
wish表各字段及其作用:
可以添加一些模拟数据:
2.4 创建目录
2.4.1 生成项目目录 wish
2.4.2 安装依赖包
2.4.3 更改默认端口
默认端口为 3000,为了方便演示以及避免与其他项目冲突,将端口号改为 3001。
2.4.4 更换模板引擎
修改 app.js 文件:
// app.set('view engine', 'jade');
app.engine('html', require('express-art-template'));
app.set('view engine', 'html');
2.4.5 增加配置文件
//config.js
const config = {
DEBUG: true,
MYSQL: {
host: 'localhost',
database: 'wish',
username: 'root',
password: '123456'
}
};
module.exports = config;
2.4.6 增加数据库配置文件
//db.js
const Sequelize = require('sequelize');
const config = require('./config');
const sequelize = new Sequelize(
config.MYSQL.database,
config.MYSQL.username,
config.MYSQL.password, {
host: config.MYSQL.host,
dialect: 'mysql',
logging: config.DEBUG ? console.log : false,//是否打印日志
pool: {//配置数据库连接池
max: 5,
min: 0,
idle: 10000
},
timezone: '+08:00'//时区设置
}
);
module.exports = sequelize;
2.4.7 增加常量文件
//constant/constant.js
const obj = {
DEFAULT_SUCCESS: {
code: '00',
msg: '操作成功'
},
DEFAULT_ERROR: {
code: '01',
msg: '操作失败'
},
};
module.exports = obj;
2.4.8 增加数据库映射文件
//models/wish.js
const Sequelize = require('sequelize');
const db = require('../db');
const Wish = db.define('Wish', {
id: {type: Sequelize.INTEGER, primaryKey: true, allowNull: false, autoIncrement: true},
name: {type: Sequelize.STRING(20), allowNull: false},
content: {type: Sequelize.STRING, allowNull: false}
}, {
underscored: true,//是否支持驼峰
tableName: 'wish'
});
module.exports = Wish;
2.4.9 增加路由处理方法文件
//controllers/index.js
const async = require('async');
const wishModel = require('../models/wish');
const constant = require('../constant/constant');
const exportObj = {
getList,
add
};
module.exports = exportObj;
// 获取许愿列表
function getList(req, res){
//定义一个async任务
let tasks = {
//执行查询方法
query: cb => {
wishModel.findAll({
limit: 50,
order: [['created_at', 'DESC']]
}).then(result => {
let list = [];
result.forEach(val => {
let date = new Date(val.createdAt);
date = date.getFullYear() + '.' + date.getMonth() + '.' + date.getDate();
let obj = {
id: val.id,
name: val.name,
content: val.content,
createdAt: date
};
list.push(obj);
});
cb(null, list);
}).catch(err => {
console.log(err);
cb(constant.DEFAULT_ERROR);
})
}
};
async.auto(tasks, (err, result) => {
if(err){
console.log(err);
}else {
res.render('index', {
list: result['query']
})
}
})
}
// 添加许愿方法
function add(req, res){
let tasks = {
add: cb => {
wishModel.create({
name: req.body.name,
content: req.body.content,
}).then(result => {
cb(constant.DEFAULT_SUCCESS);
}).catch(err => {
console.log(err);
cb(constant.DEFAULT_ERROR);
})
}
};
async.auto(tasks, (err, result) => {
if(err){
console.log(err);
res.send(err);
}else {
res.send(result);
}
})
}
2.4.10 增加路由文件
//routes/index.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/index');
router.get('/', controller.getList);
router.post('/add', controller.add);
module.exports = router;
2.5 前端页面
2.5.1 增加页面文件
//views/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>许愿墙</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
<!--便签列表-->
<div id="container" data-list="{{list}}" onmousemove="moveNodeObj.mouseMove()" onmouseup="moveNodeObj.mouseStop()"></div>
<!--填写心愿弹窗-->
<div class="mask-layer">
<div class="container">
<input id="name" class="input" name="name" placeholder="请输入您的姓名">
<textarea id="content" class="input" name="content" rows="5" placeholder="请填写您的心愿"></textarea>
<button class="form-btn close" onclick="closeAddNodeWin()">关闭</button>
<button class="form-btn submit" onclick="submit()">提交</button>
<!--提示信息-->
<div id="msg" class="warn-msg"></div>
</div>
</div>
<!--操作按钮-->
<div class="opt-btn">
<div class="add-btn" onclick="openAddNodeWin()">添加</div>
</div>
<script src="/javascripts/jquery-1.11.3.js"></script>
<script src="/javascripts/index.js"></script>
</body>
</html>
2.5.2 添加样式
//public/stylesheets/style.css
/*便签样式*/
.node {
color: #fff;
font-size: 12px;
position: absolute;
padding: 15px;
max-width: 150px;
border-radius: 5px;
box-shadow: 0 0 2px 2px #ccc;
cursor: move;
}
.node .content {
margin-bottom: 10px;
}
.name-date {
float: right;
text-align: center;
}
/*提示框样式*/
.warn-msg {
color: #fff;
font-size: 14px;
position: absolute;
top: -30px; left: 50%;
transform: translate(-50%);
padding: 5px 10px;
border-radius: 5px;
min-width: 200px;
text-align: center;
display: none;
}
/*浮动菜单样式*/
.opt-btn {
position: absolute;
top: 0; left: 0;
background: #f5f5f5;
padding: 10px;
}
.opt-btn .add-btn {
color: #fff;
cursor: pointer;
padding: 8px 16px;
font-size: 12px;
background: #3993e8;
border-radius: 5px;
display: inline-block;
}
/*遮罩层样式*/
.mask-layer {
width: 100%;
height: 100vh;
position: absolute;
left: 0; top: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
}
.mask-layer .container {
width: 500px;
margin: 100px auto;
background: #28cde4;
text-align: center;
padding: 40px 0;
position: relative;
border-radius: 5px;
}
.mask-layer .input {
color: #333;
display: block;
width: 360px;
margin: auto;
outline: none;
margin-bottom: 20px;
padding: 5px;
border: 1px solid transparent;
border-radius: 5px;
}
.mask-layer .form-btn {
color: #fff;
outline: none;
cursor: pointer;
padding: 5px 16px;
background: #eb73dd;
border-radius: 5px;
border: 1px solid transparent;
}
.mask-layer .close {
background: #6ba2eb;
}
2.5.3 处理逻辑
//public/javascripts/index.js
//生成随机颜色
function createRandomColor(){
let r = Math.floor(Math.random() * (200 - 100 + 1)) + 100;
let g = Math.floor(Math.random() * (200 - 100 + 1)) + 100;
let b = Math.floor(Math.random() * (200 - 100 + 1)) + 100;
return `rgb(${r}, ${g}, ${b})`;
}
//生成随机位置
function createRandomPosition(){
let width = window.screen.width - 300;
let height = window.screen.height - 300;
let left = Math.floor(Math.random() * width - 200 + 1) + 200;
let top = Math.floor(Math.random() * height - 200 + 1) + 200;
return {left, top}
}
//移动便签
let moveNodeObj = {
contentId: '',//当前移动的便签id
isMouse: false,//是否移动
startX: 0,//移动起始x
startY: 0,//移动起始y
endX: 0,//移动终点x
endY: 0,//移动终点y
mouseStart(ev){
let e = ev || window.event;
this.startX = e.clientX;
this.startY = e.clientY;
this.isMouse = true;
this.contentId = '#' + $(e.target).attr('id');
},
mouseMove(ev){
if(this.isMouse){
let e = ev || window.event;
this.endX = e.clientX;
this.endY = e.clientY;
let mouseX = this.endX - this.startX;
let mouseY = this.endY - this.startY;
let content = $(this.contentId);
let offset = content.offset();
if(offset){
let left = offset.left + mouseX;
let top = offset.top + mouseY;
content.css({left: left, top: top});
this.startX = this.endX;
this.startY = this.endY;
}
}
},
mouseStop(){
this.isMouse = false;
}
};
// 初始化数据
$(() => {
let container = $('#container');
let list = JSON.parse(container.attr('data-list'));
list.forEach(val => {
createNode(val.id, val.name, val.content, val.createdAt, container);
})
});
//创建许愿便签
function createNode(id, name, content, createdAt, container){
let bgColor = createRandomColor();
let p = createRandomPosition();
$(`<div class="node" id="id${id}" onmousedown="moveNodeObj.mouseStart()">
<div class="content">${content}</div>
<div class="name-date">
<div>${name}</div>
<div>${createdAt}</div>
</div>
</div>`).css({
background: bgColor,
top: p.top,
left: p.left,
}).appendTo(container);
}
// 显示提示信息
function showWarnMsg(text, color){
let msg = $('#msg');
msg.css({background: color}).html(text);
msg.show();
setTimeout(() => {
msg.hide();
}, 1500)
}
// 打开添加心愿窗口
function openAddNodeWin(){
$('.mask-layer').show();
}
//关闭添加心愿窗口
function closeAddNodeWin(){
$('.mask-layer').hide();
}
//提交许愿
function submit(){
let name = $('#name').val();
let content = $('#content').val();
if(name && content){
$.ajax({
url: '/add',
type: 'POST',
data: {
name: name,
content: content
},
success: data => {
showWarnMsg(data.msg, '#2fcc4f');
setTimeout(() => {
closeAddNodeWin();
}, 1500);
},
error: (err) => {
showWarnMsg(err.msg, '#eaba1b');
}
});
}else {
showWarnMsg('请填写您的姓名和心愿!', '#eaba1b');
}
}