Appearance
图片上传OSS弹窗组件
使用vue3和element UI plus,主要上传图片和视频,目前只支持单文件,多文件不太适用目前的UI,功能包括图片、视频上传,上传的百分比进度条,可取消上传,图片、视频的分页展示,点击图片会把链接赋值到页面的img标签上
MediaUpload.vue
vue
<template>
<el-dialog v-model="dialogVisible" title="媒体管理" width="650px">
<!-- 上传区域 -->
<div class="upload-container">
<el-upload
ref="uploadRef"
:show-file-list="false"
:http-request="handleUpload"
name="file"
:before-upload="beforeUpload"
class="upload-area"
>
<div v-if="!uploading" class="upload-button">
<el-icon :size="22"><Plus /></el-icon>
<div class="el-upload__text">点击上传</div>
</div>
<!-- 上传进度条 -->
<div v-else class="upload-progress">
<el-progress
:percentage="uploadProgress"
:show-text="false"
style="width: 90px"
/>
<div class="progress-text">{{ uploadProgress }}%</div>
<el-button
size="small"
type="danger"
@click.stop="cancelUpload"
class="cancel-btn"
>
取消
</el-button>
</div>
</el-upload>
<!-- 图片展示区域 -->
<div
v-for="(item, index) in paginatedImages"
:key="index"
class="media-item"
@click="handleImageSelect(item.url, item.type)"
>
<!-- <img :src="item.url" alt="图片" /> -->
<template v-if="item.type === 'image'">
<img :src="item.url" alt="图片" />
</template>
<template v-else>
<div class="video-preview">
<video :src="item.url"></video>
<div class="video-icon">
<el-icon :size="24"><VideoPlay /></el-icon>
</div>
</div>
</template>
<div class="media-hover">点击选择</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="total"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
</el-dialog>
</template>
<script setup>
import axios from 'axios'
import { ref, computed, watchEffect, defineProps } from 'vue'
import {
ElDialog,
ElUpload,
ElProgress,
ElPagination,
ElIcon,
ElMessage
} from 'element-plus'
import { Plus, VideoPlay } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue', 'select'])
// API配置
const uploadUrl = 'http://localhost:3000/api/upload'
const listUrl = 'http://localhost:3000/api/images'
// 状态管理
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)
const imageList = ref([])
const uploadRef = ref(null)
const uploading = ref(false)
const uploadProgress = ref(0)
const controller = ref(null)
// 计算分页后的图片
const paginatedImages = computed(() => {
return imageList.value
})
// 自定义上传逻辑
const handleUpload = async (options) => {
try {
uploading.value = true
uploadProgress.value = 0
controller.value = new AbortController()
const formData = new FormData()
formData.append('file', options.file) // 确保字段名与后端一致
const response = await axios.post(uploadUrl, formData, {
// headers: {
// 'Content-Type': 'multipart/form-data'
// },
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
uploadProgress.value = progress
}
},
signal: controller.value.signal
})
if (response.data?.code === 200) {
setTimeout(() => {
ElMessage.success(response.data.message || '上传成功')
uploading.value = false
uploadProgress.value = 0
fetchImages() // 等待列表刷新完成
}, 1000)
} else {
throw new Error(response.data.message || '上传失败')
}
} catch (error) {
if (axios.isCancel(error)) {
console.log('用户的上传已取消')
} else {
console.error('Upload error:', error)
}
// ElMessage.error(`上传失败: ${error.message}`)
} finally {
// uploading.value = false
// uploadProgress.value = 0
}
}
// 获取图片列表
const fetchImages = async () => {
try {
const params = {
page: currentPage.value,
pageSize: pageSize.value
}
const { data } = await axios.get(listUrl, { params })
imageList.value = data.data.images
currentPage.value = data.data.pagination.page
pageSize.value = data.data.pagination.pageSize
total.value = data.data.pagination.total
} catch (error) {
ElMessage.error('获取图片列表失败')
console.error('Error fetching images:', error)
}
}
// 取消上传
const cancelUpload = () => {
controller.value.abort()
uploading.value = false
uploadProgress.value = 0
// 清除上传组件内部文件列表
ElMessage.warning('上传已取消')
setTimeout(() => {
fetchImages()
}, 500)
}
// 文件上传前校验
const beforeUpload = (file) => {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'video/mp4',
'video/webm',
'video/ogg'
]
const isAllowed = allowedTypes.includes(file.type)
const isLt500M = file.size / 1024 / 1024 < 500
if (!isAllowed) {
ElMessage.error('只能上传图片或视频文件!')
return false
}
if (!isLt500M) {
ElMessage.error('文件大小不能超过500MB!')
return false
}
return true
}
// 分页操作
const handleSizeChange = (val) => {
pageSize.value = val
fetchImages()
}
const handleCurrentChange = (val) => {
currentPage.value = val
fetchImages()
}
// 图片选择处理
const handleImageSelect = (url, type) => {
emit('select', { url, type })
dialogVisible.value = false
}
// 控制弹窗显示
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
if (val) fetchImages() // 打开弹窗时自动加载
}
})
// 初始化加载
watchEffect(() => {
if (dialogVisible.value) {
fetchImages()
}
})
</script>
<style scoped>
.upload-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
height: 236px;
}
.upload-button {
width: 100px;
height: 100px;
padding: 5px;
color: #999;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s;
}
.el-upload__text {
font-size: 12px;
}
.media-item {
width: 100px;
height: 100px;
position: relative;
cursor: pointer;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.media-item img,
.media-item video {
width: 100%;
height: 100%;
object-fit: contain;
}
.video-preview {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.video-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(255, 255, 255, 0.8);
}
.media-hover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
display: none;
align-items: center;
justify-content: center;
}
.media-item:hover .media-hover {
display: flex;
}
.media-hover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
display: none;
align-items: center;
justify-content: center;
}
.media-item:hover .media-hover {
display: flex;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
.total {
margin-left: 10px;
}
.upload-progress {
display: flex;
flex-direction: column;
align-items: center;
padding: 26px 12px;
background: #f5f7fa;
border-radius: 4px;
}
.progress-text {
font-size: 12px;
color: #409eff;
margin-top: 8px;
}
.cancel-btn {
margin-top: 8px;
width: 100%;
}
</style>
<template>
<el-dialog v-model="dialogVisible" title="媒体管理" width="650px">
<!-- 上传区域 -->
<div class="upload-container">
<el-upload
ref="uploadRef"
:show-file-list="false"
:http-request="handleUpload"
name="file"
:before-upload="beforeUpload"
class="upload-area"
>
<div v-if="!uploading" class="upload-button">
<el-icon :size="22"><Plus /></el-icon>
<div class="el-upload__text">点击上传</div>
</div>
<!-- 上传进度条 -->
<div v-else class="upload-progress">
<el-progress
:percentage="uploadProgress"
:show-text="false"
style="width: 90px"
/>
<div class="progress-text">{{ uploadProgress }}%</div>
<el-button
size="small"
type="danger"
@click.stop="cancelUpload"
class="cancel-btn"
>
取消
</el-button>
</div>
</el-upload>
<!-- 图片展示区域 -->
<div
v-for="(item, index) in paginatedImages"
:key="index"
class="media-item"
@click="handleImageSelect(item.url, item.type)"
>
<!-- <img :src="item.url" alt="图片" /> -->
<template v-if="item.type === 'image'">
<img :src="item.url" alt="图片" />
</template>
<template v-else>
<div class="video-preview">
<video :src="item.url"></video>
<div class="video-icon">
<el-icon :size="24"><VideoPlay /></el-icon>
</div>
</div>
</template>
<div class="media-hover">点击选择</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="total"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
</el-dialog>
</template>
<script setup>
import axios from 'axios'
import { ref, computed, watchEffect, defineProps } from 'vue'
import {
ElDialog,
ElUpload,
ElProgress,
ElPagination,
ElIcon,
ElMessage
} from 'element-plus'
import { Plus, VideoPlay } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue', 'select'])
// API配置
const uploadUrl = 'http://localhost:3000/api/upload'
const listUrl = 'http://localhost:3000/api/images'
// 状态管理
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)
const imageList = ref([])
const uploadRef = ref(null)
const uploading = ref(false)
const uploadProgress = ref(0)
const controller = ref(null)
// 计算分页后的图片
const paginatedImages = computed(() => {
return imageList.value
})
// 自定义上传逻辑
const handleUpload = async (options) => {
try {
uploading.value = true
uploadProgress.value = 0
controller.value = new AbortController()
const formData = new FormData()
formData.append('file', options.file) // 确保字段名与后端一致
const response = await axios.post(uploadUrl, formData, {
// headers: {
// 'Content-Type': 'multipart/form-data'
// },
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
uploadProgress.value = progress
}
},
signal: controller.value.signal
})
if (response.data?.code === 200) {
setTimeout(() => {
ElMessage.success(response.data.message || '上传成功')
uploading.value = false
uploadProgress.value = 0
fetchImages() // 等待列表刷新完成
}, 1000)
} else {
throw new Error(response.data.message || '上传失败')
}
} catch (error) {
if (axios.isCancel(error)) {
console.log('用户的上传已取消')
} else {
console.error('Upload error:', error)
}
// ElMessage.error(`上传失败: ${error.message}`)
} finally {
// uploading.value = false
// uploadProgress.value = 0
}
}
// 获取图片列表
const fetchImages = async () => {
try {
const params = {
page: currentPage.value,
pageSize: pageSize.value
}
const { data } = await axios.get(listUrl, { params })
imageList.value = data.data.images
currentPage.value = data.data.pagination.page
pageSize.value = data.data.pagination.pageSize
total.value = data.data.pagination.total
} catch (error) {
ElMessage.error('获取图片列表失败')
console.error('Error fetching images:', error)
}
}
// 取消上传
const cancelUpload = () => {
controller.value.abort()
uploading.value = false
uploadProgress.value = 0
// 清除上传组件内部文件列表
ElMessage.warning('上传已取消')
setTimeout(() => {
fetchImages()
}, 500)
}
// 文件上传前校验
const beforeUpload = (file) => {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'video/mp4',
'video/webm',
'video/ogg'
]
const isAllowed = allowedTypes.includes(file.type)
const isLt500M = file.size / 1024 / 1024 < 500
if (!isAllowed) {
ElMessage.error('只能上传图片或视频文件!')
return false
}
if (!isLt500M) {
ElMessage.error('文件大小不能超过500MB!')
return false
}
return true
}
// 分页操作
const handleSizeChange = (val) => {
pageSize.value = val
fetchImages()
}
const handleCurrentChange = (val) => {
currentPage.value = val
fetchImages()
}
// 图片选择处理
const handleImageSelect = (url, type) => {
emit('select', { url, type })
dialogVisible.value = false
}
// 控制弹窗显示
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
if (val) fetchImages() // 打开弹窗时自动加载
}
})
// 初始化加载
watchEffect(() => {
if (dialogVisible.value) {
fetchImages()
}
})
</script>
<style scoped>
.upload-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
height: 236px;
}
.upload-button {
width: 100px;
height: 100px;
padding: 5px;
color: #999;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s;
}
.el-upload__text {
font-size: 12px;
}
.media-item {
width: 100px;
height: 100px;
position: relative;
cursor: pointer;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.media-item img,
.media-item video {
width: 100%;
height: 100%;
object-fit: contain;
}
.video-preview {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.video-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(255, 255, 255, 0.8);
}
.media-hover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
display: none;
align-items: center;
justify-content: center;
}
.media-item:hover .media-hover {
display: flex;
}
.media-hover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
display: none;
align-items: center;
justify-content: center;
}
.media-item:hover .media-hover {
display: flex;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
.total {
margin-left: 10px;
}
.upload-progress {
display: flex;
flex-direction: column;
align-items: center;
padding: 26px 12px;
background: #f5f7fa;
border-radius: 4px;
}
.progress-text {
font-size: 12px;
color: #409eff;
margin-top: 8px;
}
.cancel-btn {
margin-top: 8px;
width: 100%;
}
</style>
另外我也用nodejs写了一个上传到阿里云OSS的接口(上传、获取图片(分页))
后端阿里云OSS上传代码
需要安装库
npm i express multer ali-oss cors
npm i express multer ali-oss cors
完整代码
js
const express = require('express');
const multer = require('multer');
const OSS = require('ali-oss');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors());
// 初始化OSS客户端
const client = new OSS({
region: '你的region',
accessKeyId: '你的accessKeyId',
accessKeySecret: '你的accessKeySecret',
bucket: '你的bucket'
});
// 配置临时文件存储
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'temp/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${Date.now()}${ext}`);
}
}),
fileFilter: (req, file, cb) => {
if(file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'), false);
}
},
limits: {
fileSize: 1024 * 1024 * 5 // 限制5MB
}
});
// 上传接口
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if(!req.file) {
return res.status(400).json({ code: 400, message: '未选择文件',data: null });
}
// 上传到OSS
const result = await client.put(
`images/${req.file.filename}`, // OSS存储路径
req.file.path
);
// 返回结果
res.json({
code: 200,
data: {
url: result.url,
name: result.name
}
});
} catch (error) {
console.error('上传失败:', error);
res.status(500).json({
code: 500,
data: null,
message: '文件上传失败',
error: error.message
});
}
});
// 新增获取图片列表接口
app.get('/api/images', async (req, res) => {
try {
const { page = 1, pageSize = 9 } = req.query;
const pageNum = parseInt(page);
const pageSizeNum = parseInt(pageSize);
// 获取全量文件列表(OSS限制需循环获取)
let allObjects = [];
let marker;
do {
const result = await client.list({
prefix: 'images/',
marker,
'max-keys': 1000
});
allObjects = allObjects.concat(result.objects);
marker = result.nextMarker;
} while (marker);
// 按修改时间倒序排序
allObjects.sort((a, b) => {
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime();
});
// 手动分页逻辑
const startIdx = (pageNum - 1) * pageSizeNum;
const endIdx = startIdx + pageSizeNum;
const pageObjects = allObjects.slice(startIdx, endIdx);
const images = pageObjects.map(item => ({
url: client.generateObjectUrl(item.name),
name: item.name,
lastModified: new Date(item.lastModified).toISOString()
}))
res.json({
code: 200,
data: {
images,
pagination: {
total: allObjects.length,
page: pageNum,
pageSize: pageSizeNum,
}
}
});
} catch (error) {
console.error('获取图片列表失败:', error);
res.status(500).json({
code: 500,
message: '获取图片列表失败',
error: error.message
});
}
});
// 启动服务器
const PORT = process.env. PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
const express = require('express');
const multer = require('multer');
const OSS = require('ali-oss');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors());
// 初始化OSS客户端
const client = new OSS({
region: '你的region',
accessKeyId: '你的accessKeyId',
accessKeySecret: '你的accessKeySecret',
bucket: '你的bucket'
});
// 配置临时文件存储
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'temp/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${Date.now()}${ext}`);
}
}),
fileFilter: (req, file, cb) => {
if(file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'), false);
}
},
limits: {
fileSize: 1024 * 1024 * 5 // 限制5MB
}
});
// 上传接口
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if(!req.file) {
return res.status(400).json({ code: 400, message: '未选择文件',data: null });
}
// 上传到OSS
const result = await client.put(
`images/${req.file.filename}`, // OSS存储路径
req.file.path
);
// 返回结果
res.json({
code: 200,
data: {
url: result.url,
name: result.name
}
});
} catch (error) {
console.error('上传失败:', error);
res.status(500).json({
code: 500,
data: null,
message: '文件上传失败',
error: error.message
});
}
});
// 新增获取图片列表接口
app.get('/api/images', async (req, res) => {
try {
const { page = 1, pageSize = 9 } = req.query;
const pageNum = parseInt(page);
const pageSizeNum = parseInt(pageSize);
// 获取全量文件列表(OSS限制需循环获取)
let allObjects = [];
let marker;
do {
const result = await client.list({
prefix: 'images/',
marker,
'max-keys': 1000
});
allObjects = allObjects.concat(result.objects);
marker = result.nextMarker;
} while (marker);
// 按修改时间倒序排序
allObjects.sort((a, b) => {
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime();
});
// 手动分页逻辑
const startIdx = (pageNum - 1) * pageSizeNum;
const endIdx = startIdx + pageSizeNum;
const pageObjects = allObjects.slice(startIdx, endIdx);
const images = pageObjects.map(item => ({
url: client.generateObjectUrl(item.name),
name: item.name,
lastModified: new Date(item.lastModified).toISOString()
}))
res.json({
code: 200,
data: {
images,
pagination: {
total: allObjects.length,
page: pageNum,
pageSize: pageSizeNum,
}
}
});
} catch (error) {
console.error('获取图片列表失败:', error);
res.status(500).json({
code: 500,
message: '获取图片列表失败',
error: error.message
});
}
});
// 启动服务器
const PORT = process.env. PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
后端图片上传代码
一直测试导致OSS的流量起飞,所以我又写了一个本地的文件上传,这里写了一个中间件,就是为了监听前端取消图片上传,监听请求中断事件,OSS中如果前端取消上传是不会存储图片的,因为图片不完整,但是一般的后端会,就是你上传图片,期间中断了上传,但是他会上传中断前的那一部分,所以你会看到图片只显示上面一小部分
js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const app = express();
// 启用 CORS
// app.use(cors());
app.use(cors())
// 确保upload目录存在
const uploadDir = path.join(__dirname, 'upload');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 配置multer存储
const storage = multer.diskStorage({
destination: uploadDir,
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const uniqueSuffix = Date.now() + '' + Math.round(Math.random() * 1E9);
const filename = file.fieldname + '-' + uniqueSuffix + ext;
req.uploadingFileName = filename; // 关键:存储文件名到请求对象
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
// 文件过滤器
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif','video/mp4', 'video/webm', 'video/ogg'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('只允许上传图片和视频文件 (JPEG, PNG, GIF, MP4, WEBM, OGG)'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 500 * 1024 * 1024 // 限制5MB
}
});
// 中间件:监听请求中断事件
const handleUploadAbort = (req, res, next) => {
req.on('aborted', () => {
console.log('请求被中止');
if (req.uploadingFileName) {
const filePath = path.join(uploadDir, req.uploadingFileName);
fs.unlink(filePath, (err) => {
if (err) console.error('删除未完成文件失败:', err);
else console.log('已清理未完成文件:', req.uploadingFileName);
});
// 防止重复删除
req.uploadingFileName = null;
}
});
next();
};
// 1. 图片上传接口
app.post('/api/upload', handleUploadAbort, upload.single('file'), (req, res) => {
console.log('收到上传请求,文件名:', req.file.filename) // 添加日志
if (!req.file) {
return res.status(400).json({
code: 400,
message: '未选择文件或文件类型不正确'
});
}
res.json({
code: 200,
message: '上传成功',
data: {
filename: req.file.filename,
url: `http://localhost:3000/upload/${req.file.filename}`,
size: req.file.size,
mimetype: req.file.mimetype
}
});
});
// 2. 图片列表接口(带分页)
app.get('/api/images', (req, res) => {
const { page = 1, pageSize = 9 } = req.query;
const pageNum = parseInt(page);
const size = parseInt(pageSize);
// 读取upload目录下的所有文件
fs.readdir(uploadDir, (err, files) => {
if (err) {
console.error(' 读取目录失败:', err);
return res.status(500).json({
code: 500,
message: '获取图片列表失败',
error: err.message
});
}
// 过滤出图片文件
const imageFiles = files.filter(file => {
const ext = path.extname(file).toLowerCase();
// return ['.jpg', '.jpeg', '.png', '.gif'].includes(ext);
return ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.webm', '.ogg'].includes(ext);
});
// 按修改时间排序(最新的在前面)
const sortedImages = imageFiles.map(file => {
const ext = path.extname(file).toLowerCase();
const isVideo = ['.mp4', '.webm', '.ogg'].includes(ext);
const stat = fs.statSync(path.join(uploadDir, file));
return {
filename: file,
url: `http://localhost:3000/upload/${file}`,
size: stat.size,
lastModified: stat.mtime,
created: stat.birthtime,
type: isVideo ? 'video' : 'image'
};
}).sort((a, b) => b.lastModified - a.lastModified);
// 分页处理
const total = sortedImages.length;
const startIndex = (pageNum - 1) * size;
const endIndex = startIndex + size;
const paginatedImages = sortedImages.slice(startIndex, endIndex);
res.json({
code: 200,
data: {
images: paginatedImages,
pagination: {
total: total,
page: pageNum,
pageSize: size,
totalPages: Math.ceil(total / size)
}
}
});
});
});
// 静态文件服务(用于访问上传的图片)
app.use('/upload', express.static(uploadDir));
// 启动服务器
const PORT = process.env. PORT || 3000;
app.listen(PORT, () => {
console.log(` 服务器运行在 http://localhost:${PORT}`);
console.log(` 上传目录: ${uploadDir}`);
});
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const app = express();
// 启用 CORS
// app.use(cors());
app.use(cors())
// 确保upload目录存在
const uploadDir = path.join(__dirname, 'upload');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 配置multer存储
const storage = multer.diskStorage({
destination: uploadDir,
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const uniqueSuffix = Date.now() + '' + Math.round(Math.random() * 1E9);
const filename = file.fieldname + '-' + uniqueSuffix + ext;
req.uploadingFileName = filename; // 关键:存储文件名到请求对象
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
// 文件过滤器
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif','video/mp4', 'video/webm', 'video/ogg'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('只允许上传图片和视频文件 (JPEG, PNG, GIF, MP4, WEBM, OGG)'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 500 * 1024 * 1024 // 限制5MB
}
});
// 中间件:监听请求中断事件
const handleUploadAbort = (req, res, next) => {
req.on('aborted', () => {
console.log('请求被中止');
if (req.uploadingFileName) {
const filePath = path.join(uploadDir, req.uploadingFileName);
fs.unlink(filePath, (err) => {
if (err) console.error('删除未完成文件失败:', err);
else console.log('已清理未完成文件:', req.uploadingFileName);
});
// 防止重复删除
req.uploadingFileName = null;
}
});
next();
};
// 1. 图片上传接口
app.post('/api/upload', handleUploadAbort, upload.single('file'), (req, res) => {
console.log('收到上传请求,文件名:', req.file.filename) // 添加日志
if (!req.file) {
return res.status(400).json({
code: 400,
message: '未选择文件或文件类型不正确'
});
}
res.json({
code: 200,
message: '上传成功',
data: {
filename: req.file.filename,
url: `http://localhost:3000/upload/${req.file.filename}`,
size: req.file.size,
mimetype: req.file.mimetype
}
});
});
// 2. 图片列表接口(带分页)
app.get('/api/images', (req, res) => {
const { page = 1, pageSize = 9 } = req.query;
const pageNum = parseInt(page);
const size = parseInt(pageSize);
// 读取upload目录下的所有文件
fs.readdir(uploadDir, (err, files) => {
if (err) {
console.error(' 读取目录失败:', err);
return res.status(500).json({
code: 500,
message: '获取图片列表失败',
error: err.message
});
}
// 过滤出图片文件
const imageFiles = files.filter(file => {
const ext = path.extname(file).toLowerCase();
// return ['.jpg', '.jpeg', '.png', '.gif'].includes(ext);
return ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.webm', '.ogg'].includes(ext);
});
// 按修改时间排序(最新的在前面)
const sortedImages = imageFiles.map(file => {
const ext = path.extname(file).toLowerCase();
const isVideo = ['.mp4', '.webm', '.ogg'].includes(ext);
const stat = fs.statSync(path.join(uploadDir, file));
return {
filename: file,
url: `http://localhost:3000/upload/${file}`,
size: stat.size,
lastModified: stat.mtime,
created: stat.birthtime,
type: isVideo ? 'video' : 'image'
};
}).sort((a, b) => b.lastModified - a.lastModified);
// 分页处理
const total = sortedImages.length;
const startIndex = (pageNum - 1) * size;
const endIndex = startIndex + size;
const paginatedImages = sortedImages.slice(startIndex, endIndex);
res.json({
code: 200,
data: {
images: paginatedImages,
pagination: {
total: total,
page: pageNum,
pageSize: size,
totalPages: Math.ceil(total / size)
}
}
});
});
});
// 静态文件服务(用于访问上传的图片)
app.use('/upload', express.static(uploadDir));
// 启动服务器
const PORT = process.env. PORT || 3000;
app.listen(PORT, () => {
console.log(` 服务器运行在 http://localhost:${PORT}`);
console.log(` 上传目录: ${uploadDir}`);
});