@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

效果
