前端全栈从零单排 -- 上传、点赞功能,悬浮球、弹窗...

本文简介:

① 基于express,multer中间件,mongose的上传功能,
② vue双向绑定ui,vueresource请求后台数据,mongose work表添加likeContract数组实现点赞
③悬浮球,弹窗,上传组件的实现
前端:https://github.com/woaigmz/postcard
后台:https://github.com/woaigmz/mp

wx.gif

①上传功能:

后台部分 ---

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}}

样式:

.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");发送事件

 



使用:
引入组件:

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;
   },

你可能感兴趣的:(前端全栈从零单排 -- 上传、点赞功能,悬浮球、弹窗...)