| New file |
| | |
| | | <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> |