源码顶级干货 | 本地图片直传服务器
先看效果
已关注
关注
重播 分享 赞
单图上传
后端部分(koa)
const Router = require('@koa/router')const router = new Router()const multer = require('@koa/multer')// 使用内存存储,文件不落地const upload = multer({ storage: multer.memoryStorage() })const { aliyun } = require('@/config/Account')const OSS = require('ali-oss')// 图片上传接口router.post('/upload', upload.single('file'), async ctx => {try {const file = ctx.file// console.log(file, 'file');if (!file) {ctx.send([], 422, '上传图片不能为空')return}// 获取文件后缀const ext = file.originalname.split('.').pop()// 重新命名文件const timestamp = Date.now()const randomNum = Math.floor(Math.random() * 1000)const uploadKey = `${aliyun.folder}${timestamp}${randomNum}.${ext}`// 初始化 OSSconst client = new OSS({region: aliyun.region,accessKeyId: aliyun.accessKeyId,accessKeySecret: aliyun.accessKeySecret,bucket: aliyun.bucket,secure: true})const headers = {'Content-Disposition': 'inline','Content-Type': file.mimetype || 'image/jpg'}// 上传 Bufferconst result = await client.put(uploadKey, file.buffer, { headers })// 成功if (result.res.statusCode === 200) {ctx.send('SUCCESS', 200, result.url)} else {throw result}} catch (error) {ctx.send('上传失败', 500, error)}})module.exports = router.routes()
H5部分(原生js)
<!DOCTYPE html><html><head><style>.upload-btn{position: relative;display: inline-block;padding: 10px 20px;background: #409eff;color: white;border-radius: 4px;cursor: pointer;overflow: hidden;}.upload-btn input{position: absolute;left: 0;top: 0;width: 100%;height: 100%;opacity: 0;cursor: pointer;}</style></head><body><buttonclass="upload-btn">上传图片<inputtype="file"accept="image/*"onchange="upload(event)"></button><br><br><imgid="preview"width="200"><script>async function upload(e){const file = e.target.files[0]if(!file){alert("请选择图片")return}// 限制文件类型if(!file.type.startsWith("image/")){alert("只能上传图片")return}// URL.createObjectURL(file) 是浏览器提供的一个 原生方法,用于把 本地的 File 或 Blob 对象 转换成一个 可以在网页中访问的临时 URL,这样你就可以 不上传就预览图片或视频。const preview = document.getElementById("preview")preview.src = URL.createObjectURL(file)const formData = new FormData()formData.append("file", file)try{const res = await fetch("http://localhost:8912/apif/upload",{method:"POST",body:formData})const data = await res.json()console.log("接口返回:",data)const imgUrl = data.data// 上传成功后替换为服务器图片preview.src = imgUrl}catch(err){console.log(err)alert("上传失败")}}</script></body></html>
小程序部分(uniapp)
<template><viewclass="container"><!-- <imageclass="haibaoDIban"src="https://xcwh-svgall.oss-cn-wuhan-lr.aliyuncs.com/uniapp/myProject/wxcomponents/face/haibaoDIban.png"mode="scaleToFill"/> --><viewclass="center-box"><viSwiperv-model="current"></viSwiper></view><viewclass="bgImage"></view><imageclass="configBtn"src="https://xcwh-svgall.oss-cn-wuhan-lr.aliyuncs.com/uniapp/myProject/wxcomponents/face/configBtn.png"mode="scaleToFill"@click="chooseImage"/></view></template><scriptsetup>import { ref, watch } from 'vue'import viSwiper from './vi-swiper.vue'import { uploadUrl, request } from '@/api/request'const current = ref(0)const chooseImage = () => {uni.chooseImage({count: 1,success: async function (res) {try {const tempFilePath = res.tempFilePaths[0]// 1、 上传中uni.showLoading({title: '图片上传中...',mask: true})const uploadAvatar = await uni.uploadFile({url: uploadUrl,filePath: tempFilePath,name: 'file'})const fileurl = JSON.parse(uploadAvatar.data).data// 2、 人脸融合中uni.showLoading({title: '人脸融合中...',mask: true})const result = await request('/faceApi', 'POST', {url: fileurl,modelIndex: current.value})uni.hideLoading()// 3、 跳转结果页uni.navigateTo({url: `/pages/apiPage/components/faceApi/resultFeceHaibao?str=${encodeURIComponent(result.data.FusedImage)}`})} catch (err) {console.error(err)uni.hideLoading()uni.showToast({title: '处理失败,请重试',icon: 'none'})}}})}</script><stylescopedlang="less">.container {width: 100vw;height: 100vh;position: fixed;display: flex;align-items: center;justify-content: center;.bgImage {width: 100vw;height: 100vh;position: absolute;pointer-events: none;background-image: url('https://xcwh-svgall.oss-cn-wuhan-lr.aliyuncs.com/uniapp/myProject/wxcomponents/face/haibaoDIban.png');background-size: 100% 100%;background-repeat: no-repeat;background-position: center;}}.container {width: 100vw;height: 100vh;position: fixed;}.center-box {width: 800rpx;height: 1050rpx;}.haibaoDIban {width: 100vw;height: 100vh;position: absolute;left: 0;top: 0;z-index: 99;pointer-events: none;}.configBtn {width: 260rpx;height: auto;aspect-ratio: 874 / 346;position: absolute;left: 50%;bottom: 8%;transform: translateX(-50%);z-index: 999;}</style>
多图上传
后端部分(koa)
const Router = require('@koa/router')const router = new Router()const multer = require('@koa/multer')const OSS = require('ali-oss')const { aliyun } = require('@/config/Account')// 使用内存存储const upload = multer({ storage: multer.memoryStorage() })// 多图上传接口router.post('/upload', upload.array('files', 10), async ctx => { // 最多10张try {const files = ctx.filesif(!files || files.length === 0) {ctx.send([], 422, '上传图片不能为空')return}const client = new OSS({region: aliyun.region,accessKeyId: aliyun.accessKeyId,accessKeySecret: aliyun.accessKeySecret,bucket: aliyun.bucket,secure: true})const uploadedUrls = []for(let file of files) {const ext = file.originalname.split('.').pop()const timestamp = Date.now()const randomNum = Math.floor(Math.random() * 1000)const uploadKey = `${aliyun.folder}${timestamp}${randomNum}.${ext}`const headers = {'Content-Disposition': 'inline','Content-Type': file.mimetype || 'image/jpg'}const result = await client.put(uploadKey, file.buffer, { headers })if(result.res.statusCode === 200) {uploadedUrls.push(result.url)} else {throw result}}ctx.send('SUCCESS', 200, uploadedUrls) // 返回数组} catch(error) {ctx.send('上传失败', 500, error)}})module.exports = router.routes()
H5部分(原生js)
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>多图上传示例</title><style>.upload-btn{position: relative;display: inline-block;padding: 10px 20px;background: #409eff;color: white;border-radius: 4px;cursor: pointer;overflow: hidden;}.upload-btn input{position: absolute;left: 0;top: 0;width: 100%;height: 100%;opacity: 0;cursor: pointer;}.preview-container img{width: 120px;height: 120px;margin: 5px;object-fit: cover;}</style></head><body><buttonclass="upload-btn">上传图片<inputtype="file"accept="image/*"multipleonchange="handleFiles(event)"></button><divclass="preview-container"id="previewContainer"></div><script>async function handleFiles(e){const files = Array.from(e.target.files) // 多文件if(files.length === 0) returnconst previewContainer = document.getElementById('previewContainer')previewContainer.innerHTML = ''// 本地预览files.forEach(file => {if(!file.type.startsWith('image/')) returnconst img = document.createElement('img')img.src = URL.createObjectURL(file)previewContainer.appendChild(img)})// 上传到后端const uploadedUrls = []for(let file of files){try{const formData = new FormData()formData.append('files', file) // 后端字段名是 filesconst res = await fetch('http://localhost:8912/apif/upload', {method: 'POST',body: formData})const data = await res.json()if(data.code === 200){uploadedUrls.push(...data.data)} else {alert('上传失败: ' + (data.msg || '未知错误'))}} catch(err){console.error(err)alert('上传失败')}}// 上传成功后替换预览为 OSS 地址previewContainer.innerHTML = ''uploadedUrls.forEach(url => {const img = document.createElement('img')img.src = urlpreviewContainer.appendChild(img)})}</script></body></html>
小程序部分(uniapp)
<template><viewclass="container"><viewclass="center-box"><viSwiperv-model="current"></viSwiper></view><viewclass="bgImage"></view><imageclass="configBtn"src="https://xcwh-svgall.oss-cn-wuhan-lr.aliyuncs.com/uniapp/myProject/wxcomponents/face/configBtn.png"mode="scaleToFill"@click="chooseImages"/><!-- 多图预览 --><viewclass="preview-container"v-if="previews.length > 0"><imagev-for="(src, index) in previews":key="index":src="src"style="width:150rpx;height:150rpx;margin:5rpx;"/></view></view></template><scriptsetup>import { ref } from 'vue'import viSwiper from './vi-swiper.vue'import { uploadUrl, request } from '@/api/request'const current = ref(0)const previews = ref([]) // 本地预览或OSS地址数组const chooseImages = () => {uni.chooseImage({count: 5, // 最多5张sizeType: ['original', 'compressed'],sourceType: ['album', 'camera'],success: async (res) => {try {const tempFilePaths = res.tempFilePaths// 先显示本地预览previews.value = tempFilePathsconst uploadedUrls = []// 上传每张图片for (let path of tempFilePaths) {uni.showLoading({ title: '上传中...', mask: true })const uploadRes = await uni.uploadFile({url: uploadUrl,filePath: path,name: 'files' // 多图字段名,对应后端 upload.array('files')})const data = JSON.parse(uploadRes.data)if (data.code === 200) {// 如果返回是数组,这里取第一张,否则直接 pushif(Array.isArray(data.data)){uploadedUrls.push(...data.data)} else {uploadedUrls.push(data.data)}} else {uni.showToast({ title: '上传失败', icon: 'none' })}}// 上传完成,替换预览为OSS地址previews.value = uploadedUrlsuni.hideLoading()// 调用 faceApi 处理多张图片uni.showLoading({ title: '人脸融合中...', mask: true })const result = await request('/faceApi', 'POST', {urls: uploadedUrls, // 后端接口改为接收数组modelIndex: current.value})uni.hideLoading()// 跳转结果页(你可以改成显示多张融合图)uni.navigateTo({url: `/pages/apiPage/components/faceApi/resultFeceHaibao?str=${encodeURIComponent(result.data.FusedImage)}`})} catch (err) {console.error(err)uni.hideLoading()uni.showToast({ title: '处理失败,请重试', icon: 'none' })}}})}</script><stylescopedlang="less">.container {width: 100vw;height: 100vh;position: fixed;display: flex;align-items: center;justify-content: center;.bgImage {width: 100vw;height: 100vh;position: absolute;pointer-events: none;background-image: url('https://xcwh-svgall.oss-cn-wuhan-lr.aliyuncs.com/uniapp/myProject/wxcomponents/face/haibaoDIban.png');background-size: 100% 100%;background-repeat: no-repeat;background-position: center;}}.center-box {width: 800rpx;height: 1050rpx;}.configBtn {width: 260rpx;height: auto;aspect-ratio: 874 / 346;position: absolute;left: 50%;bottom: 8%;transform: translateX(-50%);z-index: 999;}.preview-container {position: absolute;bottom: 20%;left: 50%;transform: translateX(-50%);display: flex;flex-wrap: wrap;}</style>
夜雨聆风
