Vue 3 项目开发 MinIO 文件管理模块
SpringBoot 3 项目集成 MinIO
Vue 3 项目开发 MinIO 文件管理模块(正在浏览)
1. 页面效果
2. 前端封装 request 向后端发请求
import axios from 'axios'
import { ElMessage } from 'element-plus'
import {useTokenStore} from '@/stores/token'
import router from '@/router';// 创建请求实例
let request = axios.create({baseURL:"http://localhost:8080"
})// 添加 request 拦截器
request.interceptors.request.use(config=>{if(useTokenStore().token){config.headers['X-Token'] = useTokenStore().token;}return config;},error=>{return Promise.reject(error);}
)// 添加 response 拦截器
request.interceptors.response.use(response=>{// 如果是下载请求(responseType为blob),直接返回完整响应if (response.config.responseType === 'blob') {return response;}if(response.data.code === 200){return response.data;}else if(response.data.code === 401){ElMessage.error('请先登录');router.push('/login');return Promise.reject(response.data);}else{ElMessage.error(response.data.msg || '服务异常');return Promise.reject(response.data);}},error=>{if(error.response.status === 401){ElMessage.error('请先登录');}else{ElMessage.error('服务异常');}return Promise.reject(error);}
)export default request
3. 前端封装请求 API
import request from "@/utils/request";// MinIO Object API
export default{// 文件上传 urlgetObjectUploadUrl(userId:string){return `${request.defaults.baseURL}/minioObject/upload/${userId}`},// wang-editor 富文本编辑器上传文件 urlgetWangEditorUploadUrl(userId:string){return `${request.defaults.baseURL}/minioObject/wangEditorUpload/${userId}`},listObject(value:any){return request.post('/minioObject/list',value);},removeObject(str:string){return request.delete('/minioObject/remove',{ data:{ str } });},downloadObject(id:string){return request.get(`/minioObject/download/${id}`,{ responseType: 'blob' }); // 接口设置 { responseType: 'blob' },否则无法正确处理二进制流},downloadObjectByUrl(url:string){// 如果 URL 作为字符串传输(如通过 JSON/API),可能因未转义 & 或 ? 导致解析错误,所以前端 encodeURIComponent 加密URL,后端解密// 接口设置 { responseType: 'blob' },否则无法正确处理二进制流return request.post(`/minioObject/downloadByUrl`, { str: encodeURIComponent(url) } ,{ responseType: 'blob' });},
}
4. 前端封装文件下载、存储单位转换方法(common->utils.ts)
import minioApi from '@/api/sys/minio'// 前端公共方法
export default {async downloadById(id:string) {try {// 发起下载请求,确保设置了 responseType: 'blob'const response = await minioApi.downloadObject(id);// 解析 Content-Disposition 获取文件名const contentDisposition = response.headers['content-disposition'];const fileName = contentDisposition.split('filename=')[1];// 创建 Blob 对象const blob = new Blob([response.data]);// 创建下载链接const downloadUrl = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = downloadUrl;link.download = decodeURIComponent(fileName); // 解码文件名document.body.appendChild(link);link.click();// 清理window.URL.revokeObjectURL(downloadUrl);document.body.removeChild(link);} catch (error) {console.error('下载失败:', error);}},// 根据 URL,使用二进制流下载async downloadByUrl(url : string) {try {// 发起下载请求,确保设置了 responseType: 'blob'const response = await minioApi.downloadObjectByUrl(url);// 解析 Content-Disposition 获取文件名const contentDisposition = response.headers['content-disposition'];const fileName = contentDisposition.split('filename=')[1];// 创建 Blob 对象const blob = new Blob([response.data]);// 创建下载链接const downloadUrl = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = downloadUrl;link.download = decodeURIComponent(fileName); // 解码文件名document.body.appendChild(link);link.click();// 清理window.URL.revokeObjectURL(downloadUrl);document.body.removeChild(link);} catch (error) {console.error('下载失败:', error);}},// 格式化文件单位(sizeStr中间必须要有空格):1 KB、1 MB、1 GB、1 TB -> BformatFileUnitToB(sizeStr:string) {const parts = sizeStr.split(' ');if (parts.length !== 2) return 0; // 无效格式const value = parseFloat(parts[0]);const unit = parts[1].toUpperCase();// 根据单位计算字节数switch (unit) {case 'KB':return value * 1024;case 'MB':return value * 1024 * 1024;case 'GB':return value * 1024 * 1024 * 1024;case 'TB':return value * 1024 * 1024 * 1024 * 1024;case 'B':default:return value; // 已经是字节或无效单位}},// 格式化文件单位,B -> KB、MB、GB、TBformatFileUnit( size:number ) {if (size <= 0) return "0 B";const units = ["B", "KB", "MB", "GB", "TB"];let unitIndex = 0;while (size >= 1024 && unitIndex < units.length - 1) {size /= 1024;unitIndex++;}return `${size.toFixed(2)} ${units[unitIndex]}`;},
}
5.常量
6. MinioObject.vue
6.1 import
6.2 文件上传
6.3 图片预览、根据ID下载和删除
6.4 根据 URL 上传和下载
6.5 MinioObject.vue 完整代码
<template><el-card class="container"><template #header><div class="header"><el-breadcrumb :separator-icon="ArrowRight"><el-breadcrumb-item :to="{ path: '/home/index' }" class="title">首页</el-breadcrumb-item><el-breadcrumb-item class="title">系统管理</el-breadcrumb-item><el-breadcrumb-item class="title">Minio Object 对象管理</el-breadcrumb-item></el-breadcrumb><div class="right"><el-upload :action="url" :show-file-list="false" :headers="{'X-Token': tokenStore.token}":on-success="handleUploadSuccess" :on-error="handleUploadError" :before-upload="beforeUpload"><el-button :icon="Upload" type="primary">上传</el-button></el-upload><el-link class="minioConsole" :href="constant.MINIO_WEB_URL" type="primary" target="_blank" >MinIO 控制台</el-link></div></div></template><!-- 搜索表单 --><el-form inline><el-form-item label="文件名"><el-input v-model="searchModel.name" placeholder="请输入文件名" style="width: 150px" clearable></el-input></el-form-item><el-form-item label="桶名"><el-input v-model="searchModel.bucket" placeholder="请输入桶名" style="width: 150px" clearable></el-input></el-form-item><el-form-item label="文件类型"><el-input v-model="searchModel.type" placeholder="请输入对象类型" style="width: 150px" clearable></el-input></el-form-item><el-form-item><el-button type="primary" @click="getObjectList">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item></el-form><!-- 列表 --><el-table :data="objectList" border stripe style="width: 100%" height="550"><el-table-column label="文件名" prop="name"></el-table-column><el-table-column label="桶名" prop="bucket"></el-table-column><el-table-column label="对象名" prop="object" width="200px"></el-table-column><el-table-column label="上传用户" prop="userName"></el-table-column><el-table-column label="文件类型" prop="type"></el-table-column><el-table-column label="文件大小" prop="size"> <template #default="{ row }">{{ utils.formatFileUnit(row.size) }}</template></el-table-column><el-table-column label="创建时间" prop="createTime" width="160px"></el-table-column><el-table-column label="更新时间" prop="ts" width="160px"></el-table-column><el-table-column label="操作" width="150" header-align="left" align="right"><template #default="{ row }"><el-tooltip effect="dark" placement="top" content="预览"><el-button v-if="row.type=='jpg' || row.type=='png' || row.type=='jpeg'" :icon="View" circle plain type="primary" @click="showDialog(row)"></el-button></el-tooltip><el-tooltip effect="dark" placement="top" content="下载"><el-button :icon="Download" circle plain type="primary" @click="download(row.id)"></el-button></el-tooltip><el-tooltip effect="dark" placement="top" content="删除"><el-button :icon="Delete" circle plain type="danger" @click="remove(row)"></el-button></el-tooltip></template></el-table-column><template #empty><el-empty description="没有数据" /></template></el-table><!-- 图片预览(隐藏图片容器) --><div style="width: 0; height: 0; overflow: hidden;"><el-image ref="previewImageRef" v-if="imageUrl" :src="imageUrl" :preview-src-list="previewUrlList" fit="contain" /></div><!-- 分页 --><el-paginationv-model:current-page="searchModel.currentPage"v-model:page-size="searchModel.pageSize":page-sizes="[10, 30, 50, 100]"layout="jumper, total, sizes, prev, pager, next":total="searchModel.total"@size-change="handleSizeChange"@current-change="handleCurrentChange"backgroundstyle="margin: 10px 0; justify-content: flex-end"/></el-card>
</template><script setup lang="ts">import { ref,reactive,onMounted,computed,watch,nextTick } from 'vue'import { Delete,ArrowRight,Upload,Download,View } from '@element-plus/icons-vue'import { ElMessage, ElMessageBox } from 'element-plus'import minioApi from '@/api/sys/minio'import constant from '@/common/constant'import { useUserInfoStore } from '@/stores/userInfo'import { useTokenStore } from '@/stores/token'import utils from '@/common/utils'const objectList=ref()const userInfoStore = useUserInfoStore()const tokenStore = useTokenStore()// 上传urlconst url = minioApi.getObjectUploadUrl(userInfoStore.userInfo.id)// 图片预览 URLconst imageUrl = ref('')const previewImageRef = ref()// 图片预览 URL 列表const previewUrlList = ref()// 分页&搜索模型const searchModel=reactive({currentPage:1,pageSize:10,total:0,object:'',bucket:'',name:'',type:''})const initSearchModel={ ...searchModel }// pageSize 变化时触发const handleSizeChange = (val: number) => {searchModel.pageSize=val;getObjectList();}// currentPage 变化时触发const handleCurrentChange = (val: number) => {searchModel.currentPage=val;getObjectList();}// 菜单列表const getObjectList= async()=>{const response= await minioApi.listObject(searchModel);objectList.value=response.data.records;searchModel.currentPage=response.data.current;searchModel.pageSize=response.data.size;searchModel.total=response.data.total;}// 重置搜索表单const reset= ()=>{Object.assign(searchModel, initSearchModel);getObjectList();}// 图片预览const showDialog = (row:any) => {// 1. 设置图片URLimageUrl.value = row.url;previewUrlList.value = [imageUrl.value]// 2. 模拟点击图片,触发预览(需等待DOM更新)nextTick(() => {if (previewImageRef.value?.clickHandler) {previewImageRef.value.clickHandler()} else {previewImageRef.value?.$el?.querySelector('img')?.click()}})};// 单条删除const remove= async(row:any)=>{ElMessageBox.confirm(`是否删除 [ ${row.object} ] 文件?`,'温馨提示',{confirmButtonText: '确认',cancelButtonText: '取消',type: 'warning',}).then(async() => {await minioApi.removeObject(row.id);ElMessage({ type: 'success', message: '删除成功' });getObjectList();})}// 使用二进制流下载const download = async (id:string) => {await utils.downloadById(id);};// 上传成功处理的事件const handleUploadSuccess = () => {ElMessage.success("上传成功");getObjectList();}// 上传失败处理的事件const handleUploadError = () => {ElMessage.error("上传失败");}// 上传前的回调函数,检查上传文件是否过大const beforeUpload= (file:any)=>{if (file.size > utils.formatFileUnitToB(constant.FILE_MAX_SIZE)) {ElMessage.error(`上传文件大小不能超过 ${constant.FILE_MAX_SIZE} !`);return false;}return true;}onMounted(()=>{getObjectList();})</script><style scoped lang="less">.container{height: 100%;box-sizing: border-box; }.header{display: flex;align-items: center;justify-content: space-between;}.right{display: flex;}.batchRemove{margin-left: 10px;}.title{font-size: large;font-weight: 600;}.image{height: 100px;}.previewDialog{width: 50%;display: flex;justify-content: center;align-items: center;.el-image{height: 200px;width: 200px;}}.minioConsole{margin-left: 20px;font-size: 20px;}
</style>