Vue 3 项目开发 MinIO 文件管理模块

发布于:2025-05-22 ⋅ 阅读:(20) ⋅ 点赞:(0)

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{
  // 文件上传 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>

 

 


网站公告

今日签到

点亮在社区的每一天
去签到