From 05a058cd18c7848d60cccc5c55971da4d758b027 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Tue, 13 Jan 2026 15:45:49 +0800
Subject: [PATCH] feat(ai): 新增大文件上传功能支持分片上传和视频播放
---
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java | 237 ++++++++++++++
src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java | 27 +
src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html | 676 ++++++++++++++++++++++++++++++++++++++++++
src/main/resources/application.yml | 2
4 files changed, 941 insertions(+), 1 deletions(-)
diff --git a/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
new file mode 100644
index 0000000..1f1c849
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/FileUploadController.java
@@ -0,0 +1,237 @@
+package cc.mrbird.febs.ai.controller.fileUpload;
+
+import cc.mrbird.febs.common.controller.BaseController;
+import cc.mrbird.febs.common.entity.FebsResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Administrator
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping(value = "/fileUpload")
+public class FileUploadController extends BaseController {
+
+ // @Value("${file.upload.dir}")
+ private String uploadDir = "d:/upload/files";
+
+ // @Value("${file.chunk.dir}")
+ private String chunkDir = "d:/upload/chunks";
+
+ /**
+ * 上传文件分片
+ */
+ @PostMapping("/uploadChunk")
+ public FebsResponse uploadChunk(@RequestParam("file") MultipartFile file,
+ @RequestParam("fileName") String fileName,
+ @RequestParam("chunk") int chunk,
+ @RequestParam("chunks") int chunks,
+ @RequestParam("fileMd5") String fileMd5) {
+ try {
+ // 确保分片目录存在
+ Path chunkPath = Paths.get(chunkDir, fileMd5);
+ if (!Files.exists(chunkPath)) {
+ Files.createDirectories(chunkPath);
+ }
+
+ // 保存分片文件
+ Path chunkFilePath = chunkPath.resolve(chunk + ".part");
+ Files.write(chunkFilePath, file.getBytes());
+
+ return new FebsResponse().success().message("分片上传成功");
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new FebsResponse().fail().message("分片上传失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 合并文件分片
+ */
+ @PostMapping("/mergeChunks")
+ public FebsResponse mergeChunks(@RequestParam("fileName") String fileName,
+ @RequestParam("fileMd5") String fileMd5,
+ @RequestParam("chunks") int chunks) {
+ try {
+ // 确保上传目录存在
+ Path uploadPath = Paths.get(uploadDir);
+ if (!Files.exists(uploadPath)) {
+ Files.createDirectories(uploadPath);
+ }
+
+ // 生成唯一文件名
+ String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
+ Path targetFilePath = uploadPath.resolve(uniqueFileName);
+
+ // 确保分片目录存在
+ Path chunkPath = Paths.get(chunkDir, fileMd5);
+ if (!Files.exists(chunkPath)) {
+ return new FebsResponse().fail().message("分片目录不存在");
+ }
+
+ // 合并分片
+ try (FileOutputStream outputStream = new FileOutputStream(targetFilePath.toFile())) {
+ for (int i = 0; i < chunks; i++) {
+ Path chunkFilePath = chunkPath.resolve(i + ".part");
+ if (!Files.exists(chunkFilePath)) {
+ return new FebsResponse().fail().message("分片文件不存在: " + chunkFilePath);
+ }
+ byte[] chunkBytes = Files.readAllBytes(chunkFilePath);
+ outputStream.write(chunkBytes);
+ // 删除已合并的分片
+ Files.deleteIfExists(chunkFilePath);
+ }
+ }
+
+ // 删除分片目录
+ Files.deleteIfExists(chunkPath);
+
+ // 保存文件信息到数据库或文件系统(这里简化处理)
+ saveFileInfo(uniqueFileName, targetFilePath.toFile().length());
+
+ return new FebsResponse().success().message("文件上传成功").data(uniqueFileName);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new FebsResponse().fail().message("文件合并失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 播放视频文件
+ */
+ @GetMapping("/play/{fileName}")
+ public void playVideo(@PathVariable("fileName") String fileName, HttpServletResponse response) {
+ try {
+ Path filePath = Paths.get(uploadDir, fileName);
+ if (!Files.exists(filePath)) {
+ response.setStatus(HttpStatus.NOT_FOUND.value());
+ return;
+ }
+
+ // 设置响应头
+ response.setContentType("video/mp4");
+ response.setContentLengthLong(Files.size(filePath));
+ response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\"");
+
+ // 流式传输文件
+ try (InputStream inputStream = Files.newInputStream(filePath);
+ OutputStream outputStream = response.getOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+ }
+ }
+
+ /**
+ * 获取文件列表
+ */
+ @GetMapping("/list")
+ public FebsResponse getFileList() {
+ try {
+ Path uploadPath = Paths.get(uploadDir);
+ if (!Files.exists(uploadPath)) {
+ return new FebsResponse().data(new ArrayList<>());
+ }
+
+ List<FileInfo> fileList = new ArrayList<>();
+ Files.list(uploadPath).forEach(path -> {
+ if (Files.isRegularFile(path)) {
+ try {
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setFileName(path.getFileName().toString());
+ fileInfo.setFileSize(Files.size(path));
+ fileInfo.setUploadTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Files.getLastModifiedTime(path).toMillis())));
+ fileList.add(fileInfo);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ return new FebsResponse().success().data(fileList);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new FebsResponse().fail().message("获取文件列表失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 删除文件
+ */
+ @PostMapping("/delete")
+ public FebsResponse deleteFile(@RequestParam("fileName") String fileName) {
+ try {
+ Path filePath = Paths.get(uploadDir, fileName);
+ if (Files.exists(filePath)) {
+ Files.delete(filePath);
+ }
+ return new FebsResponse().success().message("文件删除成功");
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new FebsResponse().fail().message("文件删除失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 保存文件信息
+ */
+ private void saveFileInfo(String fileName, long fileSize) {
+ // 这里可以实现保存文件信息到数据库的逻辑
+ // 简化处理,暂时不做数据库操作
+ }
+
+ /**
+ * 文件信息实体类
+ */
+ public static class FileInfo {
+ private String fileName;
+ private long fileSize;
+ private String uploadTime;
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public long getFileSize() {
+ return fileSize;
+ }
+
+ public void setFileSize(long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public String getUploadTime() {
+ return uploadTime;
+ }
+
+ public void setUploadTime(String uploadTime) {
+ this.uploadTime = uploadTime;
+ }
+ }
+}
diff --git a/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java
new file mode 100644
index 0000000..2c601ff
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/controller/fileUpload/ViewController.java
@@ -0,0 +1,27 @@
+package cc.mrbird.febs.ai.controller.fileUpload;
+
+import cc.mrbird.febs.ai.service.AiCompanyMemberApplyService;
+import cc.mrbird.febs.common.entity.FebsConstant;
+import cc.mrbird.febs.common.utils.FebsUtil;
+import lombok.RequiredArgsConstructor;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+/**
+ * @author Administrator
+ */
+@Controller("fileUpload")
+@RequestMapping(FebsConstant.VIEW_PREFIX + "modules/ai/fileUpload")
+@RequiredArgsConstructor
+public class ViewController {
+
+ @GetMapping("index")
+ @RequiresPermissions("fileUpload:index")
+ public String index() {
+
+ return FebsUtil.view("modules/ai/fileUpload/index");
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 8cff713..087a39f 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,5 +1,5 @@
server:
- port: 8085
+ port: 8095
tomcat:
uri-encoding: utf-8
diff --git a/src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html b/src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html
new file mode 100644
index 0000000..74cc6ec
--- /dev/null
+++ b/src/main/resources/templates/febs/views/modules/ai/fileUpload/index.html
@@ -0,0 +1,676 @@
+<div class="layui-fluid layui-anim febs-anim" id="febs-aiCompany-Info" lay-title="编辑">
+ <div class="layui-row febs-container">
+ <div class="layui-col-md12">
+ <div class="layui-fluid" id="aiCompany-info">
+ <form class="layui-form" action="" lay-filter="aiCompany-info-form">
+ <div class="layui-tab layui-tab-brief" lay-filter="docDemoTabBrief">
+ <ul class="layui-tab-title">
+ <li class="layui-this">基础信息</li>
+ </ul>
+ <div class="layui-tab-content">
+ <div class="upload-container">
+ <div class="upload-header">
+ <h2>大文件上传系统</h2>
+ <p>支持分片上传,最大支持10GB文件</p>
+ </div>
+
+ <!-- 上传区域 -->
+ <div class="upload-area" id="uploadArea">
+ <i class="layui-icon layui-icon-upload-drag upload-icon"></i>
+ <div class="upload-text">
+ <p>点击或拖拽文件到此处上传</p>
+ <small>支持视频文件上传</small>
+ </div>
+ </div>
+
+ <!-- 文件信息 -->
+ <div class="file-info" style="display: none;" id="fileInfo">
+ <div class="layui-row">
+ <div class="layui-col-xs6">
+ <p><strong>文件名:</strong><span id="fileName"></span></p>
+ <p><strong>文件大小:</strong><span id="fileSize"></span></p>
+ <p><strong>分片数量:</strong><span id="chunkCount"></span></p>
+ </div>
+ <div class="layui-col-xs6">
+ <p><strong>上传进度:</strong><span id="uploadProgress">0%</span></p>
+ <p><strong>状态:</strong><span id="uploadStatus">等待上传</span></p>
+ <p><strong>速度:</strong><span id="uploadSpeed">0KB/s</span></p>
+ </div>
+ </div>
+ </div>
+
+ <!-- 进度条 -->
+ <div class="progress-container" style="display: none;" id="progressContainer">
+ <div class="progress-bar">
+ <div class="progress-fill" id="progressFill">0%</div>
+ </div>
+ </div>
+
+ <!-- 操作按钮 -->
+ <div class="btn-group" style="display: none;" id="btnGroup">
+ <button class="layui-btn layui-btn-normal" id="startUpload">开始上传</button>
+ <button class="layui-btn layui-btn-danger" id="cancelUpload">取消上传</button>
+ </div>
+
+ <!-- 播放区域 -->
+ <div class="play-container" id="playContainer">
+ <h3 style="margin-bottom: 20px; color: #333;">视频播放</h3>
+ <video id="videoPlayer" controls>
+ <source id="videoSource" src="" type="video/mp4">
+ 您的浏览器不支持视频播放
+ </video>
+ <div class="layui-row layui-col-space10" style="margin-top: 20px;">
+ <div class="layui-col-xs12">
+ <button class="layui-btn layui-btn-primary" id="refreshList">刷新文件列表</button>
+ </div>
+ </div>
+
+ <!-- 服务器文件列表 -->
+ <div style="margin-top: 20px;">
+ <h4 style="margin-bottom: 10px; color: #666;">服务器文件列表</h4>
+ <table class="layui-table" id="fileList">
+ <thead>
+ <tr>
+ <th>文件名</th>
+ <th>大小</th>
+ <th>上传时间</th>
+ <th>操作</th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- 文件列表将通过JS动态生成 -->
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="layui-form-item febs-hide">
+ <button class="layui-btn" lay-submit="" lay-filter="aiCompany-info-form-submit" id="submit">保存</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+</div>
+<style>
+ .upload-container {
+ max-width: 900px;
+ margin: 30px auto;
+ padding: 30px;
+ background: #f8f8f8;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ }
+
+ .upload-header {
+ text-align: center;
+ margin-bottom: 40px;
+ }
+
+ .upload-header h2 {
+ color: #333;
+ margin-bottom: 10px;
+ font-size: 24px;
+ }
+
+ .upload-header p {
+ color: #666;
+ font-size: 14px;
+ }
+
+ .upload-area {
+ border: 2px dashed #1E9FFF;
+ border-radius: 8px;
+ padding: 50px;
+ text-align: center;
+ margin-bottom: 30px;
+ cursor: pointer;
+ transition: all 0.3s;
+ background: #fff;
+ }
+
+ .upload-area:hover {
+ border-color: #009688;
+ background: #f0f9ff;
+ }
+
+ .upload-icon {
+ font-size: 80px;
+ color: #1E9FFF;
+ margin-bottom: 20px;
+ }
+
+ .upload-text {
+ font-size: 18px;
+ color: #666;
+ }
+
+ .upload-text small {
+ font-size: 14px;
+ color: #999;
+ display: block;
+ margin-top: 10px;
+ }
+
+ .file-info {
+ margin-top: 20px;
+ padding: 20px;
+ background: #fff;
+ border-radius: 6px;
+ border-left: 4px solid #1E9FFF;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ .file-info .layui-row {
+ margin: 0;
+ }
+
+ .file-info p {
+ margin: 5px 0;
+ color: #333;
+ }
+
+ .file-info strong {
+ color: #666;
+ }
+
+ .progress-container {
+ margin: 20px 0;
+ background: #fff;
+ padding: 20px;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ .progress-bar {
+ height: 24px;
+ background: #f0f0f0;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #1E9FFF, #009688);
+ border-radius: 12px;
+ width: 0%;
+ transition: width 0.3s;
+ text-align: center;
+ line-height: 24px;
+ color: #fff;
+ font-size: 12px;
+ font-weight: bold;
+ }
+
+ .btn-group {
+ margin: 20px 0;
+ text-align: center;
+ }
+
+ .btn-group .layui-btn {
+ margin: 0 10px;
+ padding: 0 30px;
+ height: 40px;
+ line-height: 40px;
+ font-size: 16px;
+ }
+
+ .play-container {
+ margin-top: 40px;
+ padding: 30px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ }
+
+ .play-container h3 {
+ margin-bottom: 20px;
+ color: #333;
+ font-size: 20px;
+ }
+
+ video {
+ width: 100%;
+ max-width: 800px;
+ height: auto;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ }
+
+ .list-controls {
+ margin: 20px 0;
+ text-align: center;
+ }
+
+ .file-list-section {
+ margin-top: 30px;
+ }
+
+ .file-list-section h4 {
+ margin-bottom: 15px;
+ color: #666;
+ font-size: 16px;
+ border-bottom: 1px solid #f0f0f0;
+ padding-bottom: 10px;
+ }
+
+ .layui-table {
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ .layui-table th {
+ background: #f8f8f8;
+ font-weight: bold;
+ }
+
+ .layui-table tr:hover {
+ background: #f9f9f9;
+ }
+
+ .layui-table .play-btn,
+ .layui-table .delete-btn {
+ margin: 0 5px;
+ }
+
+ @media (max-width: 768px) {
+ .upload-container {
+ margin: 20px;
+ padding: 20px;
+ }
+
+ .upload-area {
+ padding: 30px;
+ }
+
+ .upload-icon {
+ font-size: 60px;
+ }
+
+ .btn-group .layui-btn {
+ margin: 5px;
+ padding: 0 20px;
+ }
+ }
+</style>
+<!-- 表格操作栏 end -->
+<script data-th-inline="javascript">
+ layui.use(['layer', 'jquery'], function() {
+ var layer = layui.layer,
+ $ = layui.jquery;
+
+ // 全局变量
+ var file = null;
+ var fileMd5 = '';
+ var chunkSize = 5 * 1024 * 1024; // 5MB分片
+ var chunks = 0;
+ var currentChunk = 0;
+ var isUploading = false;
+ var startTime = 0;
+ var uploadedSize = 0;
+
+ // 初始化
+ init();
+
+ function init() {
+ // 拖拽上传
+ var uploadArea = document.getElementById('uploadArea');
+
+ uploadArea.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ $(this).css('border-color', '#009688');
+ });
+
+ uploadArea.addEventListener('dragleave', function(e) {
+ e.preventDefault();
+ $(this).css('border-color', '#1E9FFF');
+ });
+
+ uploadArea.addEventListener('drop', function(e) {
+ e.preventDefault();
+ $(this).css('border-color', '#1E9FFF');
+ handleFile(e.dataTransfer.files[0]);
+ });
+
+ // 点击上传
+ uploadArea.addEventListener('click', function() {
+ var input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'video/*';
+ input.onchange = function() {
+ if (this.files.length > 0) {
+ handleFile(this.files[0]);
+ }
+ };
+ input.click();
+ });
+
+ // 按钮事件 - 使用事件委托确保每次都能正确绑定
+ $(document).on('click', '#startUpload', startUpload);
+ $(document).on('click', '#cancelUpload', cancelUpload);
+ $(document).on('click', '#refreshList', refreshFileList);
+
+ // 初始化文件列表
+ refreshFileList();
+ }
+
+ // 处理文件
+ function handleFile(selectedFile) {
+ if (!selectedFile) return;
+
+ if (!selectedFile.type.startsWith('video/')) {
+ layer.msg('请选择视频文件', {icon: 2});
+ return;
+ }
+
+ // 重置之前的状态
+ currentChunk = 0;
+ uploadedSize = 0;
+ $('#uploadProgress').text('0%');
+ $('#uploadStatus').text('等待上传');
+ $('#uploadSpeed').text('0KB/s');
+ $('#progressFill').css('width', '0%').text('0%');
+
+ file = selectedFile;
+ chunks = Math.ceil(file.size / chunkSize);
+
+ // 显示文件信息
+ $('#fileName').text(file.name);
+ $('#fileSize').text(formatFileSize(file.size));
+ $('#chunkCount').text(chunks);
+ $('#fileInfo').show();
+ $('#progressContainer').show();
+ $('#btnGroup').show();
+
+ // 计算文件MD5(用于断点续传和文件标识)
+ calculateFileMd5();
+ }
+
+ // 计算文件MD5
+ function calculateFileMd5() {
+ layer.msg('正在计算文件MD5...', {icon: 16, time: 0});
+
+ var fileReader = new FileReader();
+ var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
+ var chunk = blobSlice.call(file, 0, Math.min(file.size, 10 * 1024 * 1024)); // 只取前10MB计算MD5
+
+ fileReader.onload = function(e) {
+ var spark = new SparkMD5.ArrayBuffer();
+ spark.append(e.target.result);
+ fileMd5 = spark.end();
+ layer.closeAll();
+ layer.msg('文件MD5计算完成', {icon: 1});
+ };
+
+ fileReader.readAsArrayBuffer(chunk);
+ }
+
+ // 开始上传
+ function startUpload() {
+ if (!file) {
+ layer.msg('请先选择文件', {icon: 2});
+ return;
+ }
+
+ if (isUploading) {
+ layer.msg('上传已在进行中', {icon: 2});
+ return;
+ }
+
+ isUploading = true;
+ startTime = Date.now();
+ uploadedSize = 0;
+ currentChunk = 0;
+
+ $('#uploadStatus').text('上传中');
+ $('#startUpload').prop('disabled', true);
+ $('#cancelUpload').prop('disabled', false);
+
+ uploadNextChunk();
+ }
+
+ // 上传下一个分片
+ function uploadNextChunk() {
+ if (!isUploading) return;
+ if (currentChunk >= chunks) {
+ // 所有分片上传完成,请求合并
+ mergeChunks();
+ return;
+ }
+
+ var start = currentChunk * chunkSize;
+ var end = Math.min(start + chunkSize, file.size);
+ var chunk = file.slice(start, end);
+
+ var formData = new FormData();
+ formData.append('file', chunk);
+ formData.append('fileName', file.name);
+ formData.append('chunk', currentChunk);
+ formData.append('chunks', chunks);
+ formData.append('fileMd5', fileMd5);
+
+ $.ajax({
+ url: '/fileUpload/uploadChunk',
+ type: 'POST',
+ data: formData,
+ processData: false,
+ contentType: false,
+ xhr: function() {
+ var xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener('progress', function(e) {
+ if (e.lengthComputable) {
+ var chunkProgress = e.loaded / e.total;
+ var totalProgress = (currentChunk + chunkProgress) / chunks;
+ updateProgress(totalProgress);
+
+ // 计算上传速度
+ var elapsed = (Date.now() - startTime) / 1000;
+ var speed = (uploadedSize + e.loaded) / elapsed;
+ $('#uploadSpeed').text(formatFileSize(speed) + '/s');
+ }
+ });
+ return xhr;
+ },
+ success: function(response) {
+ if (response.code === 200 || response.success) {
+ uploadedSize += (end - start);
+ currentChunk++;
+ uploadNextChunk();
+ } else {
+ layer.msg('分片上传失败: ' + response.message, {icon: 2});
+ isUploading = false;
+ $('#uploadStatus').text('上传失败');
+ $('#startUpload').prop('disabled', false);
+ }
+ },
+ error: function() {
+ layer.msg('网络错误,请重试', {icon: 2});
+ isUploading = false;
+ $('#uploadStatus').text('上传失败');
+ $('#startUpload').prop('disabled', false);
+ }
+ });
+ }
+
+ // 合并分片
+ function mergeChunks() {
+ $.ajax({
+ url: '/fileUpload/mergeChunks',
+ type: 'POST',
+ data: {
+ fileName: file.name,
+ fileMd5: fileMd5,
+ chunks: chunks
+ },
+ success: function(response) {
+ if (response.code === 200 || response.success) {
+ layer.msg('文件上传成功', {icon: 1});
+ $('#uploadStatus').text('上传成功');
+ $('#uploadProgress').text('100%');
+ $('#progressFill').css('width', '100%').text('100%');
+ $('#playContainer').show();
+ refreshFileList();
+ } else {
+ layer.msg('文件合并失败: ' + response.message, {icon: 2});
+ $('#uploadStatus').text('上传失败');
+ }
+ },
+ error: function() {
+ layer.msg('网络错误,请重试', {icon: 2});
+ $('#uploadStatus').text('上传失败');
+ },
+ complete: function() {
+ isUploading = false;
+ $('#startUpload').prop('disabled', false);
+ $('#cancelUpload').prop('disabled', true);
+ }
+ });
+ }
+
+ // 取消上传
+ function cancelUpload() {
+ console.log('取消上传按钮被点击');
+ // 无论之前状态如何,都重置上传状态
+ isUploading = false;
+
+ // 重置状态
+ currentChunk = 0;
+ uploadedSize = 0;
+
+ // 重置UI显示
+ $('#uploadProgress').text('0%');
+ $('#uploadStatus').text('等待上传');
+ $('#uploadSpeed').text('0KB/s');
+ $('#progressFill').css('width', '0%').text('0%');
+
+ // 重置按钮状态
+ $('#startUpload').prop('disabled', false);
+ $('#cancelUpload').prop('disabled', true);
+
+ // 隐藏相关元素
+ $('#fileInfo').hide();
+ $('#progressContainer').hide();
+ $('#btnGroup').hide();
+
+ // 重置文件信息显示
+ $('#fileName').text('');
+ $('#fileSize').text('');
+ $('#chunkCount').text('');
+
+ // 重置文件对象
+ file = null;
+ fileMd5 = '';
+ chunks = 0;
+
+ console.log('取消上传完成,状态已重置');
+ }
+
+ // 更新进度
+ function updateProgress(progress) {
+ var percent = Math.round(progress * 100);
+ $('#uploadProgress').text(percent + '%');
+ $('#progressFill').css('width', percent + '%').text(percent + '%');
+ }
+
+ // 刷新文件列表
+ function refreshFileList() {
+ layer.msg('正在获取文件列表...', {icon: 16, time: 0});
+ $.ajax({
+ url: '/fileUpload/list',
+ type: 'GET',
+ success: function(response) {
+ layer.closeAll();
+ if (response.code === 200 || response.success || response.data) {
+ var fileList = response.data || [];
+ var tbody = $('#fileList tbody');
+ tbody.empty();
+
+ if (fileList.length === 0) {
+ var emptyTr = $('<tr></tr>');
+ emptyTr.html(`
+ <td colspan="4" style="text-align: center; color: #999;">暂无文件</td>
+ `);
+ tbody.append(emptyTr);
+ } else {
+ fileList.forEach(function(item) {
+ var tr = $('<tr></tr>');
+ tr.html(`
+ <td>
+ <input type="radio" name="fileRadio" value="${item.fileName}">
+ ${item.fileName}
+ </td>
+ <td>${formatFileSize(item.fileSize)}</td>
+ <td>${item.uploadTime}</td>
+ <td>
+ <button class="layui-btn layui-btn-xs layui-btn-normal play-btn" data-file="${item.fileName}">
+ 播放
+ </button>
+ <button class="layui-btn layui-btn-xs layui-btn-danger delete-btn" data-file="${item.fileName}">
+ 删除
+ </button>
+ </td>
+ `);
+ tbody.append(tr);
+ });
+
+ // 绑定播放按钮事件
+ $('.play-btn').click(function() {
+ var fileName = $(this).data('file');
+ var videoPlayer = document.getElementById('videoPlayer');
+ var videoSource = document.getElementById('videoSource');
+
+ videoSource.src = '/fileUpload/play/' + encodeURIComponent(fileName);
+ videoPlayer.load();
+ videoPlayer.play();
+ });
+
+ // 绑定删除按钮事件
+ $('.delete-btn').click(function() {
+ var fileName = $(this).data('file');
+ layer.confirm('确定要删除此文件吗?', {
+ btn: ['确定', '取消']
+ }, function() {
+ $.ajax({
+ url: '/fileUpload/delete',
+ type: 'POST',
+ data: {fileName: fileName},
+ success: function(response) {
+ if (response.code === 200 || response.success) {
+ layer.msg('删除成功', {icon: 1});
+ refreshFileList();
+ } else {
+ layer.msg('删除失败: ' + (response.message || response.msg), {icon: 2});
+ }
+ },
+ error: function() {
+ layer.msg('网络错误,请重试', {icon: 2});
+ }
+ });
+ });
+ });
+ }
+ } else {
+ layer.msg('获取文件列表失败: ' + (response.message || response.msg), {icon: 2});
+ }
+ },
+ error: function() {
+ layer.closeAll();
+ layer.msg('网络错误,请重试', {icon: 2});
+ }
+ });
+ }
+
+ // 格式化文件大小
+ function formatFileSize(bytes) {
+ if (bytes === 0) return '0 B';
+ var k = 1024;
+ var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+ });
+</script>
+
+<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
\ No newline at end of file
--
Gitblit v1.9.1