本文简介:
① 基于express,multer中间件,mongose的上传功能,
② vue双向绑定ui,vueresource请求后台数据,mongose work表添加likeContract数组实现点赞
③悬浮球,弹窗,上传组件的实现
前端:https://github.com/woaigmz/postcard
后台:https://github.com/woaigmz/mp
①上传功能:
后台部分 ---
work(作品)表:
const mongoose = require('mongoose')
const workSchema = mongoose.Schema({
imgurl: String,
userId: String,
content: String,
username: String,
like: Number,
share: Number,
likeContract:Array //存储点赞用户,建立关联
}, { collection: 'work' })
const Work = module.exports = mongoose.model('work', workSchema);
control层:定义接口,
const express = require('express');
const router = express.Router();
const WorkModel = require('../model/workSchema');
const StringUtil = require('../utils/StringUtil');
const JsonUtil = require('../utils/JsonUtil');
//const TokenCheckUtil = require('../utils/TokenCheckUtil');
const multer = require('../utils/MulterUtil');
//接收 image 并静态存储
const upload = multer.single('image');
//上传作品 image:blob name:string content:string
router.post('/upload', upload, function (req, res) {
WorkModel.create({
username: req.body.name,
content: req.body.content,
imgurl: 'http://' + req.headers.host + '/images/' + req.file.filename,
userId: req.body.userId,
like: 0,
share: 0,
likeContract: []
}, (err, success) => {
if (err) {
console.log(err);
JsonUtil.response(res, '201', err, "返回错误");
} else {
console.log(success);
JsonUtil.response(res, '200', success, "上传图片成功");
}
});
})
上传需要用到中间件multer,具体MulterUtil (npm install multer ..省去):
const multer = require('multer');
const storage = multer.diskStorage({
destination: function (req, file, callback) {
// 注: window / linux 不会自动创建 images 文件夹时要给予权限或手动创建
callback(null, "./images");
},
filename: function (req, file, callback) {
//data拼接防止上传同一个作品造成覆盖
callback(null, Date.now() + "_" + file.originalname);
}
});
const m = multer({
storage: storage
});
module.exports = m;
前端请求上传部分:
//引入upload_form和api
import {
items,
cards,
works,
upload_form,
like_form
} from "../data/localData.js";
import Api from "../data/api.js";
//上传
upload: function() {
let that = this;
this.upload_form.name = this.username;
console.log(this.upload_form.data);
console.log(this.upload_form.name);
console.log(this.upload_form.content);
if (
!isEmpty(this.upload_form.name) &&
!isEmpty(this.upload_form.content) &&
!isEmpty(this.upload_form.data)
) {
let formData = new window.FormData();
formData.append("image", this.upload_form.data, ".jpg");
formData.append("name", this.upload_form.name);
formData.append("content", this.upload_form.content);
this.$http.post(Api.UPLOAD, formData).then(
response => {
if (response.ok && response.body.code == "201") {
that.showSnap("error", "上传失败");
} else {
that.showSnap("success", response.body.message);
that.closeDialog();
that.works.splice(0, 0, {
_id: response.body.data._id,
username: response.body.data.username,
content: response.body.data.content,
imgurl: response.body.data.imgurl,
like: response.body.data.like,
share: response.body.data.share,
isLike: response.body.data.isLike
});
console.log(response.body);
console.log(that.works);
that.upload_form.data = "";
that.upload_form.content = "";
that.upload_form.name = "";
}
},
() => {
that.showSnap("error", "上传失败");
}
);
} else {
that.showSnap("error", "请保证您的明信片完整");
}
},
api.js:抽取便于维护
module.exports = {
REGISTER: "http://localhost:3001/api/register",
LOGIN: "http://localhost:3001/api/login",
UPLOAD:"http://localhost:3001/api/upload",
GETWORKLIST:"http://localhost:3001/api/getWorkList",
GETCARDLIST:"http://localhost:3001/api/getCardList",
LIKE:"http://localhost:3001/api/like"
};
localData.js: 不至于你的vue文件里过多出现数据结构
exports.login_form = {
name: "",
password: "",
token: ""
};
exports.register_form = {
name: "",
age: "",
sex: "",
address: "",
imgArr: "",
phone: "",
password: "",
token: ""
};
exports.items = [
{
href: "",
name: "粉丝",
count: 0
},
{
href: "",
name: "关注",
count: 0
},
{
href: "",
name: "获赞",
count: 0
}
];
exports.cards = [];
exports.works = [];
exports.like_form = {
type: "",
workId: "",
username: ""
};
exports.upload_form = {
data: "",
content: "",
name: ""
}
具体点击上传的组件后面聊 :D 感谢大家阅读
②点赞功能:
后台部分:
点赞接口 ---
like路由,通过1或0定义点赞或取消,记录当前用户和点赞作品
$addToSet 增加到不重复元素 Arrray
$pull 移除 Array 里的对象
$inc 运算 只针对 Number 类型
//点赞/取消 1/0 type:1/0 username:string workId:string
router.post('/like', function (req, res) {
if (req.body.type === "1") {
//点赞
console.log("点赞");
WorkModel.update({ _id: req.body.workId }, { $addToSet: { likeContract: req.body.username }, $inc: { like: 1 } }, (err, success) => {
if (err) {
JsonUtil.response(res, '201', err, "点赞失败");
} else {
console.log(success);
JsonUtil.response(res, '200', success, "点赞成功");
}
});
} else {
//取消
console.log("取消");
WorkModel.update({ _id: req.body.workId }, { $pull: { likeContract: req.body.username }, $inc: { like: -1 } }, (err, success) => {
if (err) {
JsonUtil.response(res, '201', err, "取消失败");
} else {
console.log(success);
JsonUtil.response(res, '200', success, "取消成功");
}
});
}
})
推荐作品接口(后期会用推荐算法优化):
//推荐列表
router.post('/getCardList', function (req, res) {
WorkModel.where({ 'like': { $gte: StringUtil.isEmpty(req.body.max) ? 100 : req.body.max } }).find((err, success) => {
if (err) {
JsonUtil.response(res, '201', err, "返回错误");
} else {
if (!StringUtil.isEmpty(success)) {
let arr = new Array();
success.forEach(function (value, index, array) {
let isLike = StringUtil.isInArray(value.likeContract, req.body.name);
let newObj = {
_id: value._id,
username: value.username,
content: value.content,
imgurl: value.imgurl,
like: value.like,
share: value.share,
isLike: isLike
}
arr.push(newObj)
})
console.log(arr);
JsonUtil.response(res, '200', arr, "返回成功");
} else {
console.log("e" + success);
JsonUtil.response(res, '201', success, "数据为空");
}
}
}).sort({ like: -1 })
})
1) 返回isLike便于前端判断和通过vm更新视图操作;
2) 传入username便于后期推荐和判断是否对某项作品点过赞;
3) $gte: 大于等于100的作品上推荐列表;
4) sort({ like: -1 }) 降序排列
前端部分:
v-for 列表中对 item (推荐作品)点赞,
{{item.username}}
{{item.content}}
{{item.like}}
样式:
.likebefore {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: bottom;
background: url(/static/imgs/unlike.svg) 0 0 no-repeat;
}
.likeafter {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: bottom;
background: url(/static/imgs/like.svg) 0 0 no-repeat;
}
script逻辑部分:
//点赞 like_form 承载了 调用点赞接口的请求参数(type\workId\username)
likeForCards: function(index) {
console.log( "点赞数量:" + this.cards[index].like + "是否点赞:" + this.cards[index].isLike);
let that = this;
//如果作品目前的状态是点赞状态,则进行取消点赞的操作( type:"0") ;未点赞,则进行点赞操作( type:"1")
this.like_form.type = this.cards[index].isLike ? "0" : "1";
this.like_form.workId = this.cards[index]._id;
this.like_form.username = this.username;
//请求网络
this.$http.post(Api.LIKE, like_form).then(
response => {
if (response.ok && response.code == "201") {
that.showSnap("error", response.body.message);
} else {
console.log(response.body);
that.showSnap("success", response.body.message);
// vm 更新数据来做视图更新
that.cards[index].like = that.cards[index].isLike? parseInt(that.cards[index].like) - 1:parseInt(that.cards[index].like) + 1;
that.cards[index].isLike = !that.cards[index].isLike;
console.log( "点赞数量:" + that.cards[index].like + "是否点赞:" + that.cards[index].isLike);
}
},
() => {
that.showSnap("error", "点赞失败");
}
);
}
③悬浮球:
template
style
#float-ball {
position: fixed;
border-radius: 50%;
bottom: 100px;
right: 100px;
width: 60px;
height: 60px;
z-index: 100;
background: #409eff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
background-image: url("../assets/publish.svg");
background-position: center;
background-repeat: no-repeat;
}
mount,dom形成以后做一些初始化操作
mounted() {
window.addEventListener("scroll", this.handleScroll);
//... 省略
this.initWorks();
this.initCards();
},
页面销毁要重置标记
destroyed() {
window.removeEventListener("scroll", this.handleScroll);
this.start_pos = 0;
},
//悬浮球隐藏出现逻辑,滑动完成记录位置到标记位,开始滑动时比较判断向上还是向下
//记录标志位和是否隐藏,通过更改数据更新ui显示
data: function() {
return {
...
start_pos: 0,
showFloatBall: true,
...
};
},
methods: {
handleScroll: function() {
//适配chrome、safari 、firfox
let scrollTop =window.pageYOffset || document.documentElement.scrollTop ||document.body.scrollTop;
let offsetTop = document.querySelector("#float-ball").offsetTop;
//console.log("scrollTop:" + scrollTop);
//console.log(offsetTop);
if (scrollTop > this.start_pos) {
this.showFloatBall = false;
} else {
this.showFloatBall = true;
}
this.start_pos = scrollTop;
},
}
④弹窗:
Dialog.vue 通过slot插槽引入div,this.$emit("on-close");发送事件
header
body
使用:
引入组件:
export default {
name: "HomePage",
个人感觉这种引入方式比较优雅
components: {
toolbar: require("../components/Toolbar.vue").default,
workDialog: require("../components/Dialog.vue").default,
imgUpload: require("../components/upload-img.vue").default
},
data: function() {
.....省略
templete布局
我的明信片:D
作者:{{username}}
剩余字数 {{surplus}}/140
点击上传
⑤上传组件(参考github项目)
upload-img.vue 隐藏input样式,易于定制个性化上传框样式,压缩图片(瓦片上传和canvas两种方式),EXIF判断图片方向并适当旋转图片
使用方式:同上(dialog)压缩完成拿到的数据是blob二进制对象
赋值给上传参数对象
secelted(data) {
console.log(data);
this.upload_form.data = data;
},