环境:
JDK11
Minio8
服务器搭建Minio:https://www.cnblogs.com/warmNest-llb/p/18233203
完成项目
1. pom.xml
<!-- MinIO Client --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.2.2</version></dependency>
2. application.yml
# Spring配置 spring:# minio配置servlet:multipart:enabled: true# 单个文件大小max-file-size: 100MB# 设置总上传的文件大小max-request-size: 200MBlocation: /usr/local/minio/data_file/temp_file/
注意:配置的存放文件的路径。
# minio配置 minio:endpoint: http://120.服务器:9001 accessKey: minioadminsecretKey: minioadminbucket: jx-file
注意:endpoint- ip + 端口
账号 密码
bucket-创建的桶
3. Minio工具类
/*** MinIO 服务*/ @Component public class MinIOUtil {// 指定MinIO服务的访问地址(包括协议、域名或IP以及端口)private static String endpoint;// MinIO的访问密钥(Access Key),用于身份验证private static String accessKey;// MinIO的秘密密钥(Secret Key),与访问密钥配对使用,也是认证的一部分。private static String secretKey;// 指定默认的存储桶(Bucket)名称,MinIO中用于组织和存储对象(文件)的基本容器。private static String bucket;@Value("${minio.endpoint}")public void setEndpoint(String endpoint) {MinIOUtil.endpoint = endpoint;}@Value("${minio.accessKey}")public void setAccessKey(String accessKey) {MinIOUtil.accessKey = accessKey;}@Value("${minio.secretKey}")public void setSecretKey(String secretKey) {MinIOUtil.secretKey = secretKey;}@Value("${minio.bucket}")public void setBucket(String bucket) {MinIOUtil.bucket = bucket;}/*** 创建并返回一个配置好的MinioClient实例* 用于与MinIO服务器交互,上传文件、下载文件、删除文件** @return 配置*/private static MinioClient getMinioClient() {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();}/*** 上传:将一个输入流中的文件上传到MinIO服务器上指定的存储桶(bucket)里** @param objectName .object(objectName)指定了上传后对象的名称。* @param inputStream 转换为 流* @param size 文件大小* @param contentType 内容类型* @throws Exception 异常*/public static void uploadFile(String objectName, InputStream inputStream, long size, String contentType) throws Exception {MinioClient minioClient = getMinioClient();minioClient.putObject(// .bucket(bucket) 指定了目标存储桶的名称。.object(objectName)指定了上传后对象的名称。PutObjectArgs.builder().bucket(bucket).object(objectName).stream(inputStream, size, -1)// .contentType(contentType)指定了上传文件的内容类型。 .contentType(contentType).build());}/*** 下载:从MinIO服务器下载指定存储桶(bucket)中的文件** @param objectName 指定要从MinIO下载的文件对象名称(即文件路径和文件名)。* @return 流 对象* @throws Exception 异常*/public static InputStream downloadFile(String objectName) throws Exception {MinioClient minioClient = getMinioClient();return minioClient.getObject(GetObjectArgs.builder()// .bucket(bucket)指定了文件所在的存储桶名称。 .bucket(bucket)// .object(objectName)指定了要下载的对象名称。 .object(objectName).build());}/*** 预览:生成一个预签名的URL,允许用户通过浏览器或其他HTTP客户端以GET方法访问MinIO存储桶中指定对象(文件)的临时链接** @param objectName 指定需要获取预览链接的文件对象名称(包括路径)。* @return 对象* @throws Exception .method(Method.GET)指定了请求的方法为GET,这是预览文件时的标准HTTP方法。* .bucket(bucket)指定了存储桶的名称。* .object(objectName)指定了对象(文件)的名称。*/public static String getPreviewUrl(String objectName) throws Exception {MinioClient minioClient = getMinioClient();return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucket).object(objectName).build());}/*** 删除:从MinIO服务器上的指定存储桶中删除一个文件** @param objectName 指定要删除的文件对象名称(包括路径和文件名)* @throws Exception*/public static void deleteFile(String objectName) throws Exception {MinioClient minioClient = getMinioClient();minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());} }
4. 实体类
/*** 文件的实体类*/ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class JxAttachment {/** 文件ID */@TableId(value = "fileId")private Integer fileId;/** 文件名称 */private String fileName;/** 文件大小 */private String fileSize;/** 上传人员 */private String uploadingStaff;/** 上传时间 */private String uploadTime;/** 文件新名词 */private String fileNewName;/** 文件地址 */private String fileAddress;/** 部门编号 */private String deptNumber;}
/*** 文件预览DTO*/ @Data @AllArgsConstructor @NoArgsConstructor public class JxAttachmentPreviewDTO {/** 文件ID */private Integer fileId;/** 文件名称 */private String fileName;/** 文件大小 */private String fileSize;/** 上传人员 */private String uploadingStaff;/** 文件地址 */private String fileAddress;/** 文件新名词 */private String fileNewName;/** 上传时间 */private String uploadTime;}
5. Mapper层
@Mapper public interface JxFileMapper {/** 添加上传信息 */int insertFiles(JxAttachment attachment);/** 部门编号 查询文件信息 */List<JxAttachmentPreviewDTO> previewFileByDeptNum(String deptNumber);/** 文件删除 */void deleteFile(@Param("fileName") String fileName, @Param("deptNumber") String deptNumber); }<!-- 添加上传信息 --><insert id="insertFiles">insert into jx_attachment (file_id, file_name, file_size, uploading_staff, upload_time, file_new_name,file_address, dept_number)values (#{fileId}, #{fileName}, #{fileSize}, #{uploadingStaff}, #{uploadTime}, #{fileNewName}, #{fileAddress},#{deptNumber})</insert><!-- 部门编号 查询文件信息 --><select id="previewFileByDeptNum" resultType="com.ruoyi.xinzhi.model.dto.JxAttachmentPreviewDTO">select file_id as fileId,file_name as fileName,file_size as fileSize,uploading_staff as uploadingStaff,file_address as fileAddress,upload_time as uploadTime,file_new_name as fileNewNamefrom jx_attachmentwhere dept_number = #{deptNumber};</select><!-- 文件删除 --><delete id="deleteFile">delete from jx_attachment where dept_number = #{deptNumber} and file_new_name = #{fileName};</delete>
PS:预览功能使用了部门编号进行作为条件。可做参考。
删除功能使用了部门编号与文件名配合。
6. Service业务层
只需要实现类与Controller交互
/*** 文件上传、下载、预览、删除*/ @Service public class JxFileService {@Autowiredprivate JxUserMapper jxUserMapper;@Autowiredprivate JxFileMapper jxFileMapper;@Value("${spring.servlet.multipart.location}")private String tempDir;/*** 文件上传** @param file 文件属性* @param deptNumber 部门编号* @return 成功 ? 失败* @throws Exception*/@Transactionalpublic AjaxResult uploadFile(MultipartFile file, String deptNumber) throws Exception {String filename = file.getOriginalFilename(); // 获取文件名long size = file.getSize(); // 文件大小 File tempDirectory = new File(tempDir);if (!tempDirectory.exists()) {tempDirectory.mkdir();}try (InputStream inputStream = file.getInputStream()) {// 获取文件扩展名String fileExtension = FilenameUtils.getExtension(filename);// 获取不带后缀文件名String baseName = FilenameUtils.getBaseName(filename);// 生成5位时间戳String timestamp = new SimpleDateFormat("ssSSS").format(new Date());// 生成唯一文件名String fileName = baseName + "_" + timestamp + "." + fileExtension;// 文件的路径String fileAddress = "/usr/local/minio/data_file/" + fileName;// 文件上传到 minio 客户端 MinIOUtil.uploadFile(fileName, inputStream, size, file.getContentType());// 使用 部门编号 获取对应 用户String userFullName = jxUserMapper.selectUserFullName(deptNumber);// 将当前格式转为年月日String date = formatDate(new Date());// 文件大小转换String fileSize = formatFileSize(size);// 信息存入数据库JxAttachment attachment = JxAttachment.builder().fileName(filename).fileSize(fileSize).uploadingStaff(userFullName).uploadTime(date).fileNewName(fileName).fileAddress(fileAddress).deptNumber(deptNumber).build();// 添加数据库 jxFileMapper.insertFiles(attachment);return AjaxResult.success("上传成功");} catch (Exception e) {return AjaxResult.error("上传有误" + e.getMessage());}}/*** 预览文件* @param deptNumber 部门编号* @return 文件预览路径* @throws Exception 异常*/public AjaxResult previewFile(String deptNumber) throws Exception {// 部门编号 查询文件信息List<JxAttachmentPreviewDTO> attachments = jxFileMapper.previewFileByDeptNum(deptNumber);for (JxAttachmentPreviewDTO attachment : attachments) {String fileName = attachment.getFileNewName(); // 文件名String fileUrl = MinIOUtil.getPreviewUrl(fileName); // 生成文件路径attachment.setFileAddress(fileUrl); // 文件地址设置为预览地址 }return AjaxResult.success(attachments);}/*** 文件大小转换* @param size 文件大小* @return 文件大小*/private String formatFileSize(long size) {DecimalFormat decimalFormat = new DecimalFormat("0.00");if (size < 1024) {return size + "B";} else if (size < 1024 * 1024) {return decimalFormat.format((double) size / 1024) + "KB";} else {return decimalFormat.format((double) size / (1024 * 1024)) + " MB";}}/*** 将 Date 转换为 yyyy-MM-dd 格式的字符串** @param date 当前时间* @return 方法*/private String formatDate(Date date) {SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");return formatter.format(date);} }
7. Controller控制层
/*** 文件*/ @RestController @RequestMapping("/file") public class JxFileController {@Autowiredprivate JxFileService jxFileService;@Autowiredprivate JxFileMapper jxFileMapper;/*** 文件上传** @param file 文件属性* @param deptNumber 部门编号* @return 成功 ? 失败*/@PostMapping("/upload")public AjaxResult uploadFile(@RequestPart @RequestParam("file") MultipartFile file, @RequestParam("deptNumber") String deptNumber) {try {return jxFileService.uploadFile(file, deptNumber);} catch (MultipartException e) {throw new BusinessException("请求错误");} catch (Exception e) {throw new BusinessException("上传有误");}}/*** 文件下载* @param fileName 文件名* @param response 响应* @return 成功 ? 失败*/@GetMapping("/download")@ResponseBodypublic void downloadFile(@RequestParam String fileName, HttpServletResponse response) {try (InputStream inputStream = MinIOUtil.downloadFile(fileName)) {// 文件名进行编码String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");// 设置响应内容类型response.setContentType("application/octet-stream");// 设置响应头,告知浏览器进行下载处理response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);// 获取响应输出流ServletOutputStream outputStream = response.getOutputStream();// 文件内容写入响应输出流byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}outputStream.flush();} catch (IOException e) {throw new BusinessException("IO异常");} catch (Exception e) {throw new BusinessException("下载有误");}}/*** 预览文件** @param deptNumber 部门编号* @return 文件预览路径*/@GetMapping("/preview")public AjaxResult previewFile(@RequestParam String deptNumber) {try {AjaxResult result = jxFileService.previewFile(deptNumber);return AjaxResult.success("预览路径", result);} catch (Exception e) {throw new BusinessException("生成路径有误");}}/*** 删除文件* @param fileName 文件名* @return 成功 ? 失败* @throws Exception 异常*/@DeleteMapping("/delete")private AjaxResult deleteFile(@RequestParam String fileName, String deptNumber){try{MinIOUtil.deleteFile(fileName);jxFileMapper.deleteFile(fileName, deptNumber);return AjaxResult.success("删除成功");} catch (Exception e) {throw new BusinessException("删除有误");}} }
8. 测试
PS:文件可以存储多种格式的。但越大响应速度越慢。
1) 上传
参数:file 文件;deptNumber 部门编号
在上传时,使用当前时间时间戳生成5为数,防止文件名重复。
成功 为返回。
上传文件时,传入服务器,同时生成 别的信息存储数据库。
可以看到 服务器 与 桶 里也存在。
2) 预览
传参:deptNumber 部门编号去查询到对应的文件。
返回的是:fileAddress 文件路径。使用文件路径可以直接访问预览。
查到的两个文件,使用链接浏览器是可以访问的。
3) 下载
下载时 使用服务器存储的文件名。(在上传时生成的文件名+时间戳+后缀)
与浏览器做了响应,postman无法测试。
4) 删除
此时服务器、桶、数据库存储了两个数据。
参数:文件名,使用服务器存储的文件名。(在上传时生成的文件名+时间戳+后缀)
部门编号
删除pdf文件。
9. 联调
前端:vue2 + ElementUI
一个上传按钮(未截图) + 四个图标按钮。
<!-- 上传文件按钮 --><el-uploadref="upload"action="":http-request="uploadFile":before-upload="beforeUpload":show-file-list="false"><el-buttontype="primary"class="vertical-center"style="background-color: rgb(2, 167, 240);height: 40px;margin-right: 10px;"@click="handleUpload">上传文件</el-button></el-upload>
<div><!-- 表格里的字段 --><el-table :data="tableData" border style="width: 100%"><!-- 添加复选框 --><el-table-column type="selection" width="55"></el-table-column><!-- 表格列 --><el-table-columnfixedprop="fileName"label="文件名称"border></el-table-column><el-table-columnprop="fileSize"label="文件大小"show-overflow-tooltipborderwidth="200px"></el-table-column><el-table-columnprop="uploadingStaff"label="上传人员"borderwidth="200px"></el-table-column><el-table-columnprop="uploadTime"label="上传时间"show-overflow-tooltipborderwidth="200px"></el-table-column><!-- slot-scope="scope" --><el-table-column prop="operation" label="操作" border width="200px"><template v-slot:default="scope"><div class="operation-icons"><!-- 查看按钮 @click="handleView(scope.row)"--><el-tooltip content="查看" placement="bottom"><el-buttontype="text"icon="el-icon-search"@click="handleView(scope.row)"></el-button></el-tooltip><!-- 删除按钮 @click="handleDelete(scope.row)"--><el-tooltip content="删除" placement="bottom"><el-buttontype="text"icon="el-icon-delete"@click="handleDelete(scope.row)"></el-button></el-tooltip><!-- 下载按钮 @click="handleDownload(scope.row)"--><el-tooltip content="下载" placement="bottom"><el-buttontype="text"icon="el-icon-download"@click="handleDownload(scope.row)"></el-button></el-tooltip><!-- 上传按钮 @click="handleUpload"--><el-tooltip content="上传" placement="bottom"><el-uploadref="upload"action="":http-request="uploadFile":before-upload="beforeUpload":show-file-list="false"@change="handleUpload"><el-buttontype="text"icon="el-icon-upload2"@click="handleUpload"></el-button></el-upload></el-tooltip></div></template></el-table-column></el-table></div>
.operation-icons {display: flex;justify-content: space-around;align-items: center;width: 150px; /* 宽度调整 */ }
四个方法:
// 文件上传 async uploadFile(option) {// 可以以同步的方式编写异步代码,暂停函数的执行,等待一个Promise解析(resolve或reject),然后继续执行函数。const formData = new FormData();formData.append("file", option.file);try {const response = await request.post("http://localhost:8080/file/upload",formData);if (response.code === 200) {this.$message.success("成功上传");this.fetchTableDate(); // 重新加载表格数据} else {this.$message.error(`上传失败: ${response ? response.message : "未知错误"}`);}} catch (error) {this.$message.error(`上传失败: ${error.message}`);}},// 文件上传 handleUpload() {this.$refs.upload.submit(); // 引用为upload的表单元素并提交 },// 预上传处理函数 beforeUpload(file) {return true; // 可以在此处添加例如检查文件类型、大小等逻辑},
// 文件预览 触发查询 fetchTableDate() {request.get("http://localhost:8080/file/preview").then((res) => {this.tableData = res.data.data;}).catch((error) => {this.$message.error("获取文件有误", error);});},// 点击预览 handleView(row) {const fileUrl = row.fileAddress;window.open(fileUrl, "_blank"); // 在新窗口中打开预览URL},
// 文件下载 handleDownload(row) {const fileName = row.fileNewName;// 发起请求到后端下载 request({url: "http://localhost:8080/file/download",method: "get",responseType: "blob", // 指定类型为 blob params: { fileName },}).then((res) => {// this.$message.success(res.msg);// 创建一个 URL 链接到文件内容const url = window.URL.createObjectURL(res);const link = document.createElement("a");link.href = url;link.setAttribute("download", fileName); // 设置下载属性 document.body.appendChild(link);link.click();document.body.removeChild(link);window.URL.revokeObjectURL(url); // 释放URL对象 }).catch((error) => {this.$message.error("下载有误", error.message);});},
// 删除文件 handleDelete(row) {const fileName = row.fileNewName; // 文件名称console.log("删除文件--", fileName);this.$confirm("确定文件永久删除吗,是否继续", "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {request({url: "http://localhost:8080/file/delete",method: "delete",params: { fileName },}).then((res) => {this.$message.success(res.msg);this.fetchTableDate(); // 重新加载表格数据 }).catch((error) => {this.$message.error(res.msg);});}).catch((error) => {this.$message.error("文件删除有误,请稍后重试");});},
点击上传成功。
完成代码:
# 自定义的请求。请求头内存储了token与部门编号
import request from "@/utils/request";
// 导入axios库,用于发起HTTP请求 import axios from 'axios';// 使用axios.create方法创建一个axios实例,以便进行定制化配置 const service = axios.create({// 设置基础URL,所有使用此axios实例发起的请求都会以此作为前缀// 部署服务器时替换服务器域名baseURL: 'http://localhost:8089',// 设置请求超时时间,单位为毫秒。此处设置为150000毫秒,即15秒timeout: 150000 });// 添加请求拦截器。此拦截器会在每个请求发送出去之前被调用 service.interceptors.request.use(// 这个函数会在请求发出前执行request => {// 从localStorage中尝试获取token,通常在用户登录后存储const token = localStorage.getItem('token');// 从localStorage 中获取 部门编号const deptNumber = localStorage.getItem("deptNumber");// 如果token存在,则将其添加到请求头中,以便服务器进行权限验证// 注意:这里使用'token'作为请求头的key可能需要根据后端要求调整,常见的还有'Authorization'if (token) {request.headers.token = token;}// 如果部门ID存在,将其添加到请求参数中if (deptNumber) {if (request.method === 'get' || request.method === 'delete') {// GET 请求的参数request.params = { ...request.params, deptNumber };} else if (request.method === 'post' || request.method === 'put') {if (request.data instanceof FormData) {request.data.append('deptNumber', deptNumber);} else {// POST 请求的参数request.data = { ...request.data, deptNumber };}}}// 必须返回修改后的请求配置,以便axios继续处理return request;},// 如果在请求拦截器中抛出了错误,此函数会被调用error => {// 将错误传递给下一个处理错误的逻辑(例如响应拦截器或全局错误处理)return Promise.reject(error);} );// 添加响应拦截器。此拦截器会在每个请求的响应到达后被调用 service.interceptors.response.use(// 当请求成功时(HTTP状态码为2xx),此函数会被调用// response => {response => response.data,error => {if (error.response) {const { code, message } = error.response.data;console.error(`Error ${code}: ${message}`);if (code === 401) {alert(message);// 跳转到登录页面或执行其他操作router.push('/login');}}return Promise.reject(error);} );// 导出定制化的axios实例,供其他模块使用 export default service;
<template><div><!-- 上传文件按钮 --><el-uploadref="upload"action="":http-request="uploadFile":before-upload="beforeUpload":show-file-list="false"><el-buttontype="primary"class="vertical-center"style="background-color: rgb(2, 167, 240);height: 40px;margin-right: 10px;"@click="handleUpload">上传文件</el-button></el-upload><!-- 表格里的字段 --><el-table :data="tableData" border style="width: 100%; margin-top: 20px"><!-- 添加复选框 --><el-table-column type="selection" width="55"></el-table-column><!-- 表格列 --><el-table-columnfixedprop="fileName"label="文件名称"border></el-table-column><el-table-columnprop="fileSize"label="文件大小"show-overflow-tooltipborderwidth="200px"></el-table-column><el-table-columnprop="uploadingStaff"label="上传人员"borderwidth="200px"></el-table-column><el-table-columnprop="uploadTime"label="上传时间"show-overflow-tooltipborderwidth="200px"></el-table-column><!-- slot-scope="scope" --><el-table-column prop="operation" label="操作" border width="200px"><template v-slot:default="scope"><div class="operation-icons"><!-- 查看按钮 @click="handleView(scope.row)"--><el-tooltip content="查看" placement="bottom"><el-buttontype="text"icon="el-icon-search"@click="handleView(scope.row)"></el-button></el-tooltip><!-- 删除按钮 @click="handleDelete(scope.row)"--><el-tooltip content="删除" placement="bottom"><el-buttontype="text"icon="el-icon-delete"@click="handleDelete(scope.row)"></el-button></el-tooltip><!-- 下载按钮 @click="handleDownload(scope.row)"--><el-tooltip content="下载" placement="bottom"><el-buttontype="text"icon="el-icon-download"@click="handleDownload(scope.row)"></el-button></el-tooltip><!-- 上传按钮 @click="handleUpload"--><el-tooltip content="上传" placement="bottom"><el-uploadref="upload"action="":http-request="uploadFile":before-upload="beforeUpload":show-file-list="false"@change="handleUpload"><el-buttontype="text"icon="el-icon-upload2"@click="handleUpload"></el-button></el-upload></el-tooltip></div></template></el-table-column></el-table></div> </template><script> import request from "@/utils/request";export default {data() {return {tableData: [],};},mounted() {// 预览挂载this.fetchTableDate();},methods: {// 文件上传 async uploadFile(option) {// 可以以同步的方式编写异步代码,暂停函数的执行,等待一个Promise解析(resolve或reject),然后继续执行函数。const formData = new FormData();formData.append("file", option.file);try {const response = await request.post("http://localhost:8080/file/upload",formData);if (response.code === 200) {this.$message.success("成功上传");this.fetchTableDate(); // 重新加载表格数据} else {this.$message.error(`上传失败: ${response ? response.message : "未知错误"}`);}} catch (error) {this.$message.error(`上传失败: ${error.message}`);}},// 文件上传 handleUpload() {this.$refs.upload.submit(); // 引用为upload的表单元素并提交 },// 预上传处理函数 beforeUpload(file) {return true; // 可以在此处添加例如检查文件类型、大小等逻辑 },// 文件预览 触发查询 fetchTableDate() {request.get("http://localhost:8080/file/preview").then((res) => {this.tableData = res.data.data;}).catch((error) => {this.$message.error("获取文件有误", error);});},// 点击预览 handleView(row) {const fileUrl = row.fileAddress;window.open(fileUrl, "_blank"); // 在新窗口中打开预览URL },// 文件下载 handleDownload(row) {const fileName = row.fileNewName;// 发起请求到后端下载 request({url: "http://localhost:8080/file/download",method: "get",responseType: "blob", // 指定类型为 blob params: { fileName },}).then((res) => {// this.$message.success(res.msg);// 创建一个 URL 链接到文件内容const url = window.URL.createObjectURL(res);const link = document.createElement("a");link.href = url;link.setAttribute("download", fileName); // 设置下载属性 document.body.appendChild(link);link.click();document.body.removeChild(link);window.URL.revokeObjectURL(url); // 释放URL对象 }).catch((error) => {this.$message.error("下载有误", error.message);});},// 删除文件 handleDelete(row) {const fileName = row.fileNewName; // 文件名称console.log("删除文件--", fileName);this.$confirm("确定文件永久删除吗,是否继续", "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {request({url: "http://localhost:8080/file/delete",method: "delete",params: { fileName },}).then((res) => {this.$message.success(res.msg);this.fetchTableDate(); // 重新加载表格数据 }).catch((error) => {this.$message.error(res.msg);});}).catch((error) => {this.$message.error("文件删除有误,请稍后重试");});},}, }; </script><style scoped> .operation-icons {display: flex;justify-content: space-around;align-items: center;width: 150px; /* 宽度调整 */ }.operation-icons .el-button {padding: 0; } </style>
点击下载,通过浏览器下载本地路径。