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{
// 文件上传 url
getObjectUploadUrl(userId:string){
return `${request.defaults.baseURL}/minioObject/upload/${userId}`
},
// wang-editor 富文本编辑器上传文件 url
getWangEditorUploadUrl(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 -> B
formatFileUnitToB(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、TB
formatFileUnit( 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-pagination
v-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"
background
style="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()
// 上传url
const url = minioApi.getObjectUploadUrl(userInfoStore.userInfo.id)
// 图片预览 URL
const 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. 设置图片URL
imageUrl.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>