node + mysql实现图片上传接口并实现本地图片预览(接口)

@desc 后端 图片上传接口, 前端使用el-upload实现图片上传

效果如下:

后端:

文件目录

server/app.js
// 用于配置服务器相关信息
let express = require('express')
let app = express()
let cors = require('cors')
let bodyParser = require('body-parser')
let router = require('./router')

app.use(bodyParser.json());  //配置解析,用于解析json和urlencoded格式的数据
app.use(bodyParser.urlencoded({extended: false}));
app.use(cors())              //配置跨域,必须在路由之前
app.use('/api', router)      //配置路由

app.listen(5500, () => {
    console.log('服务器启动成功。 ---> http://127.0.0.1:5500/api');
})
server/API/picList.js
const multer = require('multer');
const db = require('../db/index')
const fs = require('fs');
const makeUuid = require('../utils/makeUuid')

const upload = multer({
    storage: multer.diskStorage({
        //设置文件存储位置
        destination: function (req, file, cb) {
            let date = new Date();
            let year = date.getFullYear();
            let month = (date.getMonth() + 1).toString().padStart(2, '0');
            let day = date.getDate();
            // 设置存储路径,由于我的静态资源目录是设置的 public,所以设置在 public 文件下
            let dir = `public/uploads/${file.fieldname}/${year}${month}${day}`;

            //判断目录是否存在,没有则创建
            if (!fs.existsSync(dir)) {
                fs.mkdirSync(dir, {
                    recursive: true
                });
            }
            cb(null, dir);
        },
        //设置文件名称
        filename(req, file, cb) {
            // 重命名文件名,防止重复
            let fileName = file.fieldname + '-' + Date.now() + '-' + file.originalname
            cb(null, fileName);
        }
    })
});

// 常用的两方法:多选用 array(),单选用single()
const multipleFile = upload.array('file', 3)

/* 图片上传 */
exports.upload = (req, res, next) => {
    multipleFile(req, res, err => {
        if (err instanceof multer.MulterError) {
            // console.log('---errMulterError---', err);
        } else if (err) {
            // console.log('---err---', err);
        }
        // console.log(req.files, 'req.files');
        for (let i = 0; i < req.files.length; i++) {
            // ?,? => id,url
            let id = makeUuid().toString()
            let sql = `INSERT INTO piclist VALUES ('${id}',?)`
            // 重新设置存储在数据库的 url 地址,去掉前面的public字符串方便读取
            let destination = req.files[i].destination.substring(6)
            let url = `${destination}/${req.files[i].filename}`
            let resData = {
                name: req.files[i].filename,
                url
            }

            db.query(sql, [url], function (err, data) {
                if (err) {
                    res.json({
                        code: 500,
                        msg: '服务器报错,请稍后重试'
                    })
                } else {
                    res.json({
                        code: 200,
                        msg: '成功',
                        data: data,
                        resData
                    })
                }
            })

        }

    })
}

/* 图片预览 */
exports.getImg = (req, res, next) => {
    fs.readFile(`./public/${req.query.url}`, function (err, data) {
        if (err) console.log(err)
        console.log(data)
        res.send(data)
    })
}
server/db/index.js
// 用于配置数据库相关信息
let mysql = require('mysql')
// 创建连接池
let db = mysql.createPool({
    host: '127.0.0.1',     //数据库IP地址
    user: 'root',          //数据库登录账号
    password: '',      //数据库登录密码
    database: 'onlinefilm'       //要操作的数据库
})
module.exports = db
server/utils/makeUuid.js
// 生成随机 uuid
module.exports = function makeUuid() {
    let s = [];
    let hexDigits = "0123456789abcdef";
    for (let i = 0; i < 36; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    s[8] = s[13] = s[18] = s[23] = "-";
    let uuid = s.join("");
    return uuid;
}
server/router.js
// 用于配置对应路由
let express = require('express')
let router = express.Router()
let picList = require('./API/picList')

router.get('/', (req, res) => {
    res.send('express启动成功!');
})

/* 图片上传相关的接口 */
router.post('/upload', picList.upload) // 图片上传
router.get('/getImg', picList.getImg) // 图片预览
/* 图片上传相关的接口 */

module.exports = router
图片上传成功后的文件目录

前端

文件目录

一些基础的就不多写了例如axios那些...

src/utils/request.js
/**axios封装
 * 请求拦截、相应拦截、错误统一处理
 */
import axios from "axios";
import QS from "qs";
import store from "../store";

// 环境的切换
if (process.env.NODE_ENV === "development") {
  // 本地开发环境
  axios.defaults.baseURL = "http://127.0.0.1:5500/"; // 对应后端接口地址
} else if (process.env.NODE_ENV === "debug") {
  // 本地测试环境
  axios.defaults.baseURL = "";
} else if (process.env.NODE_ENV === "production") {
  // todo will fix 线上环境
  axios.defaults.baseURL = "";
}

// 请求超时时间
axios.defaults.timeout = 10000;

// post请求头
axios.defaults.headers.post["Content-Type"] =
  "application/x-www-form-urlencoded;charset=UTF-8";

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    return config;
  },
  (error) => {
    return error;
  }
);

// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    if (response.status === 200) {
      return Promise.resolve(response);
    } else {
      return Promise.reject(response);
    }
  },
  // todo will fix 服务器状态码不是200的情况
  (error) => {
    if (error) throw error;
  }
);

/**
 * get方法,对应get请求
 * @param {String} url [请求的url地址]
 * @param {Object} params [请求时携带的参数]
 */
export function get(url, params) {
  return new Promise((resolve, reject) => {
    axios
      .get(url, {
        params: params,
      })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
}

/**
 * post方法,对应post请求
 * @param {String} url [请求的url地址]
 * @param {Object} params [请求时携带的参数]
 */
export function post(url, params) {
  return new Promise((resolve, reject) => {
    axios
      .post(url, QS.stringify(params))
      .then((res) => {
        resolve(res.data);
      })
      .catch((err) => {
        reject(err.data);
      });
  });
}
src/components/autoUploadComp.vue
<!-- 图片上传组件
 自动上传 且只能上传单张
 -->
<template>
  <div class="upload">
    <el-upload
      ref="upload"
      list-type="picture-card"
      :action="actionUrl"
      :class="{ hide: isUpload }"
      :auto-upload="true"
      :http-request="upload"
      :limit="limit"
      :file-list="fileList"
      :accept="accept"
      :name="name"
      :on-change="handleChange"
      :on-exceed="handleExceed"
      :on-success="handleSuccess"
      :on-remove="handleRemove"
    >
      <i slot="default" class="el-icon-plus"></i>
      <div slot="file" slot-scope="{ file }">
        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
        <span class="el-upload-list__item-actions">
          <span
            class="el-upload-list__item-preview"
            @click="handlePictureCardPreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            v-if="!disabled"
            class="el-upload-list__item-delete"
            @click="handleRemove(file)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </div>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt="" />
    </el-dialog>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "autoUploadComp",
  props: {
    accept: {
      type: String,
      default: "image/*",
    },
    limit: {
      default: 1,
    },
    name: {
      type: String,
      default: "file",
    },
    fileList: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      dialogImageUrl: "",
      dialogVisible: false,
      disabled: false,
      isUpload: false,
      actionUrl: axios.defaults.baseURL + "/api/upload",
    };
  },
  methods: {
    handleChange(file, fileList) {
      // 判断上传文件是否达到限制
      this.isUpload = fileList.length >= this.limit;
      // 由于设置自动上传为false,before-upload钩子失效,所以在on-change中检验文件是否符合要求
      const isJPG = ~["image/jpeg", "image/png"].indexOf(file.raw.type);
      const isLt2M = file.raw.size / 1024 / 1024 < 2;
      if (!isJPG) {
        this.$message.error("上传图片只能是 jpg 格式!");
      }
      if (!isLt2M) {
        this.$message.error("上传头像图片大小不能超过 2MB!");
      }
      if (!isJPG || !isLt2M) {
        // 不符合直接删除该文件
        this.handleRemove(file);
      }
    },

    // 删除图片
    handleRemove(file) {
      // let fileList = (this.$refs.upload).uploadFiles;
      // let index = fileList.findIndex((fileItem) => {
      //     return fileItem.uid === file.uid;
      // });
      // fileList.splice(index, 1);

      // 上传单张,直接清空fileList
      this.$emit("on-response"); // 此处为了避免直接修改父组件的数据以免产生问题
      // 删除组件有动画功能,设置个延迟显示
      setTimeout(() => {
        this.isUpload = false;
      }, 1000);
    },

    // 预览图片
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },

    // 文件超出限制的提示
    handleExceed(file, fileList) {
      this.$message.error("文件个数超出限制");
    },

    // 文件上传成功的回调
    handleSuccess(file, fileList) {
      this.fileList.push({
        name: fileList.name,
        url: fileList.url,
      });
      console.log(this.fileList, 130);
      console.log(file, fileList);
      // 通过派发自定义事件 getFileList 向父组件传值
      this.$emit("getFileList", this.fileList);
    },

    async upload() {
      // 使用的是multer中间件,所以需要传递formdata格式的数据
      const formData = new FormData();
      // 找到需要传递的文件
      const file = this.$refs.upload.uploadFiles;
      // 设置请求头
      const headerConfig = {
        headers: { "Content-Type": "multipart/form-data" },
      };
      // 遍历 添加文件信息
      // 注意:添加的字段名,需要与后端一样 "file"
      file.forEach((item) => {
        formData.append("file", item.raw);
      });
      let { data: res } = await this.$axios
        .post("/api/upload", formData, headerConfig)
        .then((res) => {
          if (res.data.code === 200) {
            localStorage.setItem("url", res.data.resData.url);
          }
        });
    },
  },
};
</script>

<style lang="scss" scoped>
// 设置上传为none,可以加个动画什么之类的
::v-deep .hide .el-upload--picture-card {
  display: none;
}

::v-deep .el-upload--picture-card {
  width: 100px !important;
  height: 100px !important;
  line-height: 104px;
}

.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.avatar-uploader-icon:hover {
  border-color: #409eff !important;
}

.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 80px;
  height: 80px;
  line-height: 80px;
  text-align: center;
  border: 1px dashed #d9d9d9;
}

.avatar {
  width: 120px;
  height: 120px;
  display: block;
}
</style>
src/views/待使用的页面中
<template>
  <div class="main-view">
    <div class="r-content-header">
      <div class="r-content-header-left">
        <i class="el-icon-picture-outline"></i>
        <span class="font"> 图片管理 </span>
      </div>
    </div>

    <div class="pic-content">
      <span class="public-span"> 上传图片: </span>
      <auto-upload-comp
        class="upload-comp"
        :fileList="fileList"
        @on-response="handleRemoveFile"
        @getFileList="getFileList"
        ref="uploadImg"
        name="avater"
      />
      <el-button @click="commitPush" size="small" type="primary">
        确定上传</el-button
      >
    </div>

    <div class="pic-content1">
      <span class="public-span"> 上传地址: </span>
      <div v-show="url">{{ publicUrl }}{{ url }}</div>
      <br />

      <div v-show="url" style="margin-top: 30px">
        <span class="public-span"> 上传图片是: </span>
        <el-image
          style="width: 100px; height: 100px"
          :preview-src-list="[`${publicUrl}${url}`]"
          :src="`${publicUrl}${url}`"
        ></el-image>
      </div>
    </div>
  </div>
</template>

<script>
import autoUploadComp from "@/components/autoUploadComp";
import { getImg } from "@/api/backHome";

export default {
  name: "picManage",
  components: { autoUploadComp },
  data() {
    return {
      fileList: [],
      publicUrl: "http://127.0.0.1:5500/api/getImg?url=", // 默认地址
      url: "", // 需要拼接的地址
    };
  },
  methods: {
    handleRemoveFile() {
      this.fileList = [];
    },
    getFileList(data) {
      this.fileList = data;
      console.log(data);
    },
    commitPush() {
      this.url = localStorage.getItem("url");
    },
  },
  created() {
    // getImg({
    //   url: '/uploads/file/20230218/file-1676701697044-girl.jpg'
    // }).then(res => {
    //   console.log(res)
    // })
  },
};
</script>

<style lang="scss" scoped>
.r-content-header {
  height: 40px;
  border-bottom: 1px solid #eee;
  margin: 0px 18px 0;
  display: flex;
  justify-content: space-between;

  &-left {
    color: #f08080;

    .font {
      font-size: 15px;
      font-weight: 600;
      color: #666;
      line-height: 40px;
    }
  }

  &-right {
  }
}

.pic-content {
  width: 96%;
  margin: 16px auto 0;
  display: flex;
  align-items: center;

  .upload-comp {
    margin: 0 20px 0 10px;
  }
}

.pic-box {
  width: 100px;
  height: 100px;

  img {
    width: 100%;
  }
}

.public-span {
  font-size: 14px;
  color: #606266;
}

.pic-content1 {
  width: 96%;
  margin: 16px auto 0;
}
</style>

数据库

表名: piclist

效果

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值