昆仑山:眼中无形心中有穴之穴人合一

夫君子之行,静以修身,俭以养德;非澹泊无以明志,非宁静无以致远。夫学须静也,才须学也;非学无以广才,非志无以成学。怠慢则不能励精,险躁则不能冶性。年与时驰,意与岁去,遂成枯落,多不接世。悲守穷庐,将复何及!

 

利用 Spring Boot 实现一个文件上传和下载功能的系统

项目的目录总体结构

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.12</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.jd</groupId>
	<artifactId>HandleFileUpload</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>HandleFileUpload</name>
	<description>Demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.springframework.boot</groupId>
							<artifactId>spring-boot-configuration-processor</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>


添加配置信息application.yml

# 配置Spring框架的文件上传属性
spring:
  servlet:
    multipart:
      # 设置单个文件的最大大小为10MB
      max-file-size: 10MB
      # 设置整个HTTP请求的最大大小为10MB,限制一次上传的总文件大小
      max-request-size: 10MB
      # 启用文件上传功能
      enabled: true

# 设置文件上传的目录
file:
  upload-dir: D:/uploads

# 配置服务器端口
server:
  port: 8081

# 配置日志级别
logging:
  level:
    # 设置特定包的日志级别为DEBUG,以便捕获文件上传处理过程中的调试信息
    com.jd.com.jd.HandleFileUpload: DEBUG

dao层的UploadFileResponse

package com.jd.HandleFileUpload.dao;

/**
 * 文件上传响应类
 * 用于封装文件上传后的相关信息,包括文件名、下载URI、文件类型和文件大小
 */
public class UploadFileResponse {
    // 文件名
    private String fileName;
    // 文件下载的URI
    private String downloadUri;
    // 文件类型
    private String fileType;
    // 文件大小,以字节为单位
    private long size;

    /**
     * 构造函数
     * 用于创建UploadFileResponse对象,并初始化文件名、下载URI、文件类型和文件大小
     *
     * @param fileName     文件名
     * @param downloadUri  文件下载的URI
     * @param fileType     文件类型
     * @param size         文件大小,以字节为单位
     */
    public UploadFileResponse(String fileName, String downloadUri, String fileType, long size) {
        this.fileName = fileName;
        this.downloadUri = downloadUri;
        this.fileType = fileType;
        this.size = size;
    }

    // Getters and Setters
    /**
     * 获取文件名
     *
     * @return 文件名
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * 设置文件名
     *
     * @param fileName 文件名
     */
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    /**
     * 获取文件下载的URI
     *
     * @return 文件下载的URI
     */
    public String getDownloadUri() {
        return downloadUri;
    }

    /**
     * 设置文件下载的URI
     *
     * @param downloadUri 文件下载的URI
     */
    public void setDownloadUri(String downloadUri) {
        this.downloadUri = downloadUri;
    }

    /**
     * 获取文件类型
     *
     * @return 文件类型
     */
    public String getFileType() {
        return fileType;
    }

    /**
     * 设置文件类型
     *
     * @param fileType 文件类型
     */
    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    /**
     * 获取文件大小
     *
     * @return 文件大小,以字节为单位
     */
    public long getSize() {
        return size;
    }

    /**
     * 设置文件大小
     *
     * @param size 文件大小,以字节为单位
     */
    public void setSize(long size) {
        this.size = size;
    }
}


service层FileStorageService

package com.jd.HandleFileUpload.service;

import com.jd.HandleFileUpload.config.FileStorageConfig;
import com.jd.HandleFileUpload.exception.FileStorageException;
import com.jd.HandleFileUpload.exception.MyFileNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 文件存储服务类,负责处理文件上传、检索、删除等操作
 */
@Service
public class FileStorageService {

    // 存储文件的位置
    private final Path fileStorageLocation;

    /**
     * 构造函数,初始化文件存储位置
     *
     * @param fileStorageConfig 文件存储配置,包含上传目录等信息
     */
    @Autowired
    public FileStorageService(FileStorageConfig fileStorageConfig) {
        // 获取配置中的上传目录,并转换为绝对路径
        this.fileStorageLocation = Paths.get(fileStorageConfig.getUploadDir())
                .toAbsolutePath().normalize();

        try {
            // 尝试创建上传目录,如果目录已存在则不执行任何操作
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            // 如果创建目录失败,则抛出异常
            throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
        }
    }

    /**
     * 存储上传的文件
     *
     * @param file 要存储的文件
     * @return 存储的文件名
     * @throws FileStorageException 如果文件名包含无效字符或文件无法存储
     */
    public String storeFile(MultipartFile file) {
        // 规范化文件名,去除路径中的冗余信息
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());

        try {
            // 检查文件名是否包含无效字符
            if (fileName.contains("..")) {
                throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
            }

            // 将文件复制到目标位置,如果文件已存在则替换
            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            return fileName;
        } catch (IOException ex) {
            // 如果文件无法存储,则抛出异常
            throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
        }
    }

    /**
     * 加载文件作为资源
     *
     * @param fileName 要加载的文件名
     * @return 文件对应的资源对象
     * @throws MyFileNotFoundException 如果文件未找到
     * @throws MalformedURLException    如果文件路径无效
     */
    public Resource loadFileAsResource(String fileName) {
        try {
            // 获取文件路径并创建资源对象
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            if (resource.exists()) {
                return resource;
            } else {
                // 如果文件不存在,则抛出异常
                throw new MyFileNotFoundException("File not found " + fileName);
            }
        } catch (MalformedURLException ex) {
            // 如果文件路径无效,则抛出异常
            throw new MyFileNotFoundException("File not found " + fileName, ex);
        }
    }

    /**
     * 加载所有文件名
     *
     * @return 文件名列表
     * @throws FileStorageException 如果无法加载文件
     */
    public List<String> loadAllFiles() {
        try (Stream<Path> paths = Files.walk(this.fileStorageLocation, 1)) {
            // 遍历上传目录,筛选出所有常规文件并收集文件名
            return paths
                    .filter(Files::isRegularFile)
                    .map(Path::getFileName)
                    .map(Path::toString)
                    .collect(Collectors.toList());
        } catch (IOException ex) {
            // 如果无法加载文件,则抛出异常
            throw new FileStorageException("Could not load files", ex);
        }
    }

    /**
     * 删除文件
     *
     * @param fileName 要删除的文件名
     * @return 如果文件被成功删除则返回true,否则返回false
     * @throws FileStorageException 如果无法删除文件
     */
    public boolean deleteFile(String fileName) {
        try {
            // 获取文件路径并尝试删除文件
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            return Files.deleteIfExists(filePath);
        } catch (IOException ex) {
            // 如果无法删除文件,则抛出异常
            throw new FileStorageException("Could not delete file " + fileName, ex);
        }
    }
}

controller层FileController

package com.jd.HandleFileUpload.controller;

import com.jd.HandleFileUpload.dao.UploadFileResponse;
import com.jd.HandleFileUpload.service.FileStorageService;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 文件控制器类
 * 提供文件上传、下载、列表和删除等接口
 */
@RestController
@RequestMapping("/api/files")
public class FileController {

    // 日志记录器
    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    // 文件存储服务
    @Autowired
    private FileStorageService fileStorageService;

    /**
     * 单个文件上传接口
     *
     * @param file 上传的文件对象
     * @return 返回包含文件信息的响应对象
     */
    @PostMapping("/upload")
    public UploadFileResponse uploadFile(@RequestParam("file") MultipartFile file) {
        // 存储文件并获取文件名
        String fileName = fileStorageService.storeFile(file);

        // 构建文件下载链接
        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/api/files/download/")
                .path(fileName)
                .toUriString();

        // 创建并返回响应对象
        return new UploadFileResponse(fileName, fileDownloadUri,
                file.getContentType(), file.getSize());
    }

    /**
     * 多个文件上传接口
     *
     * @param files 上传的多个文件对象
     * @return 返回包含每个文件信息的响应对象列表
     */
    @PostMapping("/uploadMultiple")
    public List<UploadFileResponse> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        // 遍历所有文件并分别上传
        return Arrays.asList(files)
                .stream()
                .map(this::uploadFile)
                .collect(Collectors.toList());
    }

    /**
     * 文件下载接口
     *
     * @param fileName 要下载的文件名
     * @param request  HTTP请求对象,用于获取上下文信息
     * @return 返回封装了文件资源的响应实体
     */
    @GetMapping("/download/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        // 加载文件为资源对象
        Resource resource = fileStorageService.loadFileAsResource(fileName);

        // 尝试获取文件MIME类型
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info("无法确定文件类型。");
        }

        // 如果未获取到MIME类型,则使用默认类型
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // 构造并返回响应实体
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }

    /**
     * 获取所有文件名列表
     *
     * @return 返回文件名列表
     */
    @GetMapping("/list")
    public List<String> listFiles() {
        // 从服务层加载所有文件名
        return fileStorageService.loadAllFiles();
    }

    /**
     * 删除指定文件
     *
     * @param fileName 要删除的文件名
     * @return 返回操作结果的响应实体
     */
    @DeleteMapping("/delete/{fileName:.+}")
    public ResponseEntity<String> deleteFile(@PathVariable String fileName) {
        // 执行文件删除操作
        boolean deleted = fileStorageService.deleteFile(fileName);

        // 根据删除结果返回对应的HTTP响应
        if (deleted) {
            return ResponseEntity.ok("文件删除成功: " + fileName);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}


config包的配置操作FileStorageConfig

package com.jd.HandleFileUpload.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 文件存储配置类
 * 用于获取文件上传目录的配置
 */
@Component
public class FileStorageConfig {
    // 从应用配置中获取的文件上传目录路径
    @Value("${file.upload-dir}")
    private String uploadDir;

    /**
     * 获取文件上传目录的路径
     *
     * @return 文件上传目录的路径
     */
    public String getUploadDir() {
        return uploadDir;
    }

    /**
     * 设置文件上传目录的路径
     *
     * @param uploadDir 文件上传目录的路径
     */
    public void setUploadDir(String uploadDir) {
        this.uploadDir = uploadDir;
    }
}


exception异常处理

FileStorageException.java

/**
 * 自定义文件存储异常类
 * 用于在文件上传或管理过程中出现错误时抛出
 * 它提供了与异常相关联的详细消息,以及可选的原始异常(cause)
 */
package com.jd.HandleFileUpload.exception;

public class FileStorageException extends RuntimeException {
    /**
     * 构造一个带有详细消息的FileStorageException
     * 
     * @param message 异常的详细消息
     */
    public FileStorageException(String message) {
        super(message);
    }

    /**
     * 构造一个带有详细消息和原始异常的FileStorageException
     * 
     * @param message 异常的详细消息
     * @param cause   异常的原始原因
     */
    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}


MyFileNotFoundException

package com.jd.HandleFileUpload.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * 当文件未找到时抛出的异常类
 *
 * 该类继承自RuntimeException,用于处理文件上传过程中遇到的文件不存在的情况
 * 使用@ResponseStatus注解,当抛出此类异常时,返回HTTP 404状态码
 */
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
    /**
     * 构造一个MyFileNotFoundException实例,包含详细的错误信息
     *
     * @param message 错误信息,描述文件未找到的情况
     */
    public MyFileNotFoundException(String message) {
        super(message);
    }

    /**
     * 构造一个MyFileNotFoundException实例,包含详细的错误信息和原始异常
     *
     * @param message 错误信息,描述文件未找到的情况
     * @param cause   原始异常,导致文件未找到的底层原因
     */
    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

前端页面代码

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <!-- 页面元信息 -->
    <meta charset="UTF-8">
    <!-- 设置视口以支持响应式布局 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 页面标题 -->
    <title>文件上传和下载</title>
    <!-- 引入外部样式表 -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- 页面主容器 -->
    <div class="container">
        <!-- 页面主标题 -->
        <h1>文件管理系统</h1>

        <!-- 单文件上传区域 -->
        <section class="upload-section">
            <!-- 子标题 -->
            <h2>单文件上传</h2>
            <!-- 用于上传单个文件的表单 -->
            <form id="singleUploadForm" enctype="multipart/form-data">
                <!-- 文件输入与上传按钮布局容器 -->
                <div class="file-input-wrapper">
                    <!-- 文件选择框 -->
                    <input type="file" id="singleFileUpload" name="file" required>
                    <!-- 提交按钮 -->
                    <button type="submit">上传</button>
                </div>
            </form>
            <!-- 上传结果展示区域 -->
            <div id="singleFileUploadResult"></div>
        </section>

        <!-- 多文件上传区域 -->
        <section class="upload-section">
            <!-- 子标题 -->
            <h2>多文件上传</h2>
            <!-- 用于上传多个文件的表单 -->
            <form id="multipleUploadForm" enctype="multipart/form-data">
                <!-- 文件输入与上传按钮布局容器 -->
                <div class="file-input-wrapper">
                    <!-- 文件选择框(支持多选) -->
                    <input type="file" id="multipleFileUpload" name="files" multiple required>
                    <!-- 提交按钮 -->
                    <button type="submit">批量上传</button>
                </div>
            </form>
            <!-- 批量上传结果展示区域 -->
            <div id="multipleFileUploadResult"></div>
        </section>

        <!-- 文件列表展示区域 -->
        <section class="file-list-section">
            <!-- 子标题 -->
            <h2>文件列表</h2>
            <!-- 刷新按钮 -->
            <button id="refreshBtn">刷新列表</button>
            <!-- 文件列表动态加载区域 -->
            <div id="fileList"></div>
        </section>
    </div>

    <!-- 引入前端交互脚本 -->
    <script src="app.js"></script>
</body>
</html>


style.css

/* 全局样式重置 */
* {
    margin: 0; /* 清除默认外边距 */
    padding: 0; /* 清除默认内边距 */
    box-sizing: border-box; /* 设置盒模型为 border-box,包含 padding 和 border 在 width/height 内 */
}

/* 页面主体样式 */
body {
    font-family: 'Arial', sans-serif; /* 使用 Arial 字体,备选无衬线字体 */
    background-color: #f5f5f5; /* 设置浅灰色背景 */
    color: #333; /* 设置文字颜色为深灰色 */
    line-height: 1.6; /* 增加行高,提升可读性 */
}

/* 页面内容容器 */
.container {
    max-width: 800px; /* 最大宽度限制为 800px */
    margin: 0 auto; /* 居中显示 */
    padding: 20px; /* 内边距 */
}

/* 主标题样式 */
h1 {
    text-align: center; /* 居中对齐 */
    color: #2c3e50; /* 深蓝色标题 */
    margin-bottom: 30px; /* 底部留白 */
    font-size: 2.5em; /* 加大字号 */
}

/* 子标题样式 */
h2 {
    color: #34495e; /* 稍浅的蓝色标题 */
    margin-bottom: 15px; /* 底部留白 */
    border-bottom: 2px solid #3498db; /* 添加蓝色下划线 */
    padding-bottom: 5px; /* 下划线下方留白 */
}

/* 单文件上传区域样式 */
.upload-section {
    background: white; /* 白色背景 */
    padding: 25px; /* 内边距 */
    margin-bottom: 25px; /* 底部留白 */
    border-radius: 10px; /* 圆角 */
    box-shadow: 0 2px 10px rgba(0,0,0,0.1); /* 添加轻微阴影,增强立体感 */
}

/* 文件输入和按钮的布局容器 */
.file-input-wrapper {
    display: flex; /* 使用 Flex 布局 */
    gap: 10px; /* 子元素之间间距 */
    align-items: center; /* 垂直居中 */
    margin-bottom: 15px; /* 底部留白 */
}

/* 文件选择框样式 */
input[type="file"] {
    flex: 1; /* 自动扩展以填充可用空间 */
    padding: 10px; /* 内边距 */
    border: 2px dashed #3498db; /* 蓝色虚线边框 */
    border-radius: 5px; /* 圆角 */
    background: #ecf0f1; /* 浅灰色背景 */
}

/* 按钮通用样式 */
button {
    padding: 12px 25px; /* 内边距 */
    background: #3498db; /* 蓝色背景 */
    color: white; /* 白色文字 */
    border: none; /* 移除默认边框 */
    border-radius: 5px; /* 圆角 */
    cursor: pointer; /* 鼠标悬停变为手形 */
    font-size: 14px; /* 字号 */
    transition: background 0.3s; /* 添加背景过渡动画 */
}

/* 按钮悬停效果 */
button:hover {
    background: #2980b9; /* 悬停时更深的蓝色 */
}

/* 文件列表区域样式 */
.file-list-section {
    background: white; /* 白色背景 */
    padding: 25px; /* 内边距 */
    border-radius: 10px; /* 圆角 */
    box-shadow: 0 2px 10px rgba(0,0,0,0.1); /* 阴影效果 */
}

/* 刷新按钮样式 */
#refreshBtn {
    margin-bottom: 20px; /* 底部留白 */
    background: #27ae60; /* 绿色背景 */
}

#refreshBtn:hover {
    background: #229954; /* 悬停时更深的绿色 */
}

/* 每个文件条目样式 */
.file-item {
    display: flex; /* 使用 Flex 布局 */
    justify-content: space-between; /* 左右内容拉开 */
    align-items: center; /* 垂直居中 */
    padding: 10px; /* 内边距 */
    margin: 5px 0; /* 上下间隔 */
    background: #f8f9fa; /* 浅灰色背景 */
    border-radius: 5px; /* 圆角 */
    border-left: 4px solid #3498db; /* 左侧蓝色条,突出视觉层次 */
}

/* 文件名样式 */
.file-name {
    flex: 1; /* 自动扩展 */
    font-weight: 500; /* 稍粗字体 */
}

/* 文件操作按钮组 */
.file-actions {
    display: flex; /* 使用 Flex 布局 */
    gap: 10px; /* 按钮之间间距 */
}

/* 下载按钮样式 */
.download-btn {
    background: #27ae60; /* 绿色背景 */
    padding: 5px 15px; /* 小尺寸内边距 */
    font-size: 12px; /* 更小字号 */
}

.download-btn:hover {
    background: #229954; /* 悬停更深绿色 */
}

/* 删除按钮样式 */
.delete-btn {
    background: #e74c3c; /* 红色背景 */
    padding: 5px 15px; /* 小尺寸内边距 */
    font-size: 12px; /* 更小字号 */
}

.delete-btn:hover {
    background: #c0392b; /* 悬停更深红色 */
}

/* 提示信息通用样式 */
.result {
    margin-top: 15px; /* 顶部留白 */
    padding: 10px; /* 内边距 */
    border-radius: 5px; /* 圆角 */
}

/* 成功提示样式 */
.success {
    background: #d4edda; /* 浅绿色背景 */
    color: #155724; /* 深绿色文字 */
    border: 1px solid #c3e6cb; /* 浅绿边框 */
}

/* 错误提示样式 */
.error {
    background: #f8d7da; /* 浅红背景 */
    color: #721c24; /* 深红文字 */
    border: 1px solid #f5c6cb; /* 浅红边框 */
}

/* 进度提示样式 */
.progress {
    background: #d1ecf1; /* 浅蓝背景 */
    color: #0c5460; /* 深蓝文字 */
    border: 1px solid #bee5eb; /* 浅蓝边框 */
}

app.js

// 工具函数:在指定的元素中显示操作结果信息,并在5秒后自动清除
function showResult(elementId, message, type) {
    const element = document.getElementById(elementId);
    // 使用模板字符串插入带样式的提示框
    element.innerHTML = `<div class="result ${type}">${message}</div>`;
    // 5秒后自动清除提示信息
    setTimeout(() => {
        element.innerHTML = '';
    }, 5000);
}

// 单文件上传功能
document.getElementById('singleUploadForm').addEventListener('submit', function (e) {
    e.preventDefault(); // 阻止表单默认提交行为

    // 获取文件输入框和选中的文件
    const fileInput = document.getElementById('singleFileUpload');
    const file = fileInput.files[0];

    // 如果没有选择文件,显示错误提示并返回
    if (!file) {
        showResult('singleFileUploadResult', '请选择文件', 'error');
        return;
    }

    // 创建 FormData 对象用于封装上传数据
    const formData = new FormData();
    formData.append('file', file); // 添加文件字段

    // 显示上传进度提示
    showResult('singleFileUploadResult', '上传中...', 'progress');

    // 发送 POST 请求到服务器上传文件
    fetch('/api/files/upload', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json()) // 解析响应为 JSON
        .then(data => {
            // 显示成功消息,并格式化文件大小为 KB
            showResult('singleFileUploadResult',
                `文件上传成功: ${data.fileName} (${(data.size / 1024).toFixed(2)} KB)`, 'success');
            fileInput.value = ''; // 清空文件选择框
            loadFileList(); // 刷新文件列表
        })
        .catch(error => {
            console.error('Error:', error); // 打印错误日志
            showResult('singleFileUploadResult', '上传失败: ' + error.message, 'error'); // 显示错误提示
        });
});

// 多文件上传功能
document.getElementById('multipleUploadForm').addEventListener('submit', function (e) {
    e.preventDefault(); // 阻止表单默认提交行为

    // 获取多文件输入框和所有选中的文件
    const fileInput = document.getElementById('multipleFileUpload');
    const files = fileInput.files;

    // 如果没有选择任何文件,显示错误提示并返回
    if (files.length === 0) {
        showResult('multipleFileUploadResult', '请选择文件', 'error');
        return;
    }

    // 创建 FormData 并将多个文件添加进去
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
        formData.append('files', files[i]); // 每个文件作为一个条目
    }

    // 显示批量上传进度提示
    showResult('multipleFileUploadResult', '批量上传中...', 'progress');

    // 发送 POST 请求到批量上传接口
    fetch('/api/files/uploadMultiple', {
        method: 'POST',
        body: formData
    })
        .then(response => response.json()) // 解析响应为 JSON
        .then(data => {
            // 提取所有上传成功的文件名并拼接成字符串
            const fileNames = data.map(file => file.fileName).join(', ');
            // 显示成功消息
            showResult('multipleFileUploadResult',
                `${data.length} 个文件上传成功: ${fileNames}`, 'success');
            fileInput.value = ''; // 清空文件选择框
            loadFileList(); // 刷新文件列表
        })
        .catch(error => {
            console.error('Error:', error); // 打印错误日志
            showResult('multipleFileUploadResult', '批量上传失败: ' + error.message, 'error'); // 显示错误提示
        });
});

// 加载文件列表函数
function loadFileList() {
    // 向服务器请求文件列表
    fetch('/api/files/list')
        .then(response => response.json()) // 解析响应为 JSON
        .then(files => {
            const fileListDiv = document.getElementById('fileList');

            // 如果没有文件,显示“暂无文件”提示
            if (files.length === 0) {
                fileListDiv.innerHTML = '<p>暂无文件</p>';
                return;
            }

            // 将每个文件名渲染为 HTML 元素
            const fileListHTML = files.map(fileName => `
<div class="file-item">
    <span class="file-name">${fileName}</span>
    <div class="file-actions">
        <button class="download-btn" onclick="downloadFile('${fileName}')">下载</button>
        <button class="delete-btn" onclick="deleteFile('${fileName}')">删除</button>
    </div>
</div>
`).join(''); // 将数组转换为字符串

            // 插入到页面中
            fileListDiv.innerHTML = fileListHTML;
        })
        .catch(error => {
            console.error('Error loading file list:', error); // 打印错误日志
            // 显示加载失败提示
            document.getElementById('fileList').innerHTML = '<p class="error">加载文件列表失败</p>';
        });
}

// 下载文件函数
function downloadFile(fileName) {
    // 构造下载链接
    const downloadUrl = `/api/files/download/${encodeURIComponent(fileName)}`;
    // 创建一个临时的 <a> 标签来触发下载
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = fileName; // 设置下载时的文件名
    document.body.appendChild(link);
    link.click(); // 模拟点击
    document.body.removeChild(link); // 移除标签
}

// 删除文件函数
function deleteFile(fileName) {
    // 弹出确认框让用户确认是否删除
    if (!confirm(`确定要删除文件 "${fileName}" 吗?`)) {
        return;
    }

    // 发送 DELETE 请求到服务器删除文件
    fetch(`/api/files/delete/${encodeURIComponent(fileName)}`, {
        method: 'DELETE'
    })
        .then(response => {
            if (response.ok) {
                alert('文件删除成功'); // 删除成功提示
                loadFileList(); // 刷新文件列表
            } else {
                alert('文件删除失败'); // 删除失败提示
            }
        })
        .catch(error => {
            console.error('Error deleting file:', error); // 打印错误日志
            alert('文件删除失败: ' + error.message); // 显示错误提示
        });
}

// 绑定刷新按钮事件监听器
document.getElementById('refreshBtn').addEventListener('click', loadFileList);

// 页面加载完成后自动加载文件列表
document.addEventListener('DOMContentLoaded', loadFileList);


posted on 2025-06-17 14:33  Indian_Mysore  阅读(72)  评论(0)    收藏  举报

导航