|
|
@@ -0,0 +1,722 @@
|
|
|
+$(document).ready(function() {
|
|
|
+ // 配置
|
|
|
+ const CONFIG = {
|
|
|
+ maxConcurrentUploads: 3, // 最大并发上传数
|
|
|
+ uploadUrl: '/api/upload', // 上传接口地址
|
|
|
+ maxRetries: 2 // 最大重试次数
|
|
|
+ };
|
|
|
+
|
|
|
+ // 状态管理
|
|
|
+ const state = {
|
|
|
+ files: [], // 所有文件
|
|
|
+ queue: [], // 等待上传的文件ID
|
|
|
+ uploading: [], // 正在上传的文件ID
|
|
|
+ completed: [], // 已完成的文件ID
|
|
|
+ selectedFiles: new Set(), // 选中的文件ID
|
|
|
+ uploadSpeed: 0 // 上传速度
|
|
|
+ };
|
|
|
+
|
|
|
+ // 危险文件类型配置
|
|
|
+ const DANGEROUS_EXTENSIONS = [
|
|
|
+ '.exe', '.bat', '.cmd', '.ps1', '.vbs', '.vbe', '.js', '.jse',
|
|
|
+ '.wsf', '.wsh', '.msi', '.msc', '.scr', '.pif', '.com',
|
|
|
+ '.sh', '.bash', '.csh', '.ksh', '.zsh', '.run',
|
|
|
+ '.jar', '.app', '.apk', '.dmg', '.pkg',
|
|
|
+ '.php', '.php3', '.php4', '.php5', '.php7', '.phtml',
|
|
|
+ '.py', '.pyc', '.pyo', '.pl', '.cgi',
|
|
|
+ '.dll', '.so', '.sys', '.drv', '.bin'
|
|
|
+ ];
|
|
|
+
|
|
|
+ const DANGEROUS_MIME_TYPES = [
|
|
|
+ 'application/x-msdownload',
|
|
|
+ 'application/x-ms-installer',
|
|
|
+ 'application/x-dosexec',
|
|
|
+ 'application/x-executable',
|
|
|
+ 'application/x-shellscript',
|
|
|
+ 'application/java-archive',
|
|
|
+ 'application/x-bat',
|
|
|
+ 'application/x-msdos-program',
|
|
|
+ 'application/x-sh',
|
|
|
+ 'application/x-csh',
|
|
|
+ 'application/x-perl',
|
|
|
+ 'application/x-python',
|
|
|
+ 'application/x-php'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ init();
|
|
|
+
|
|
|
+ function init() {
|
|
|
+ setupEventListeners();
|
|
|
+ updateStats();
|
|
|
+ }
|
|
|
+
|
|
|
+ function setupEventListeners() {
|
|
|
+ // 选择文件按钮
|
|
|
+ $('#selectFilesBtn').on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ $('#fileInput').click();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 选择文件夹按钮
|
|
|
+ $('#selectFolderBtn').on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ $('#folderInput').click();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 文件选择变化
|
|
|
+ $('#fileInput').on('change', function(e) {
|
|
|
+ handleFiles(e.target.files);
|
|
|
+ $(this).val('');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 文件夹选择变化
|
|
|
+ $('#folderInput').on('change', function(e) {
|
|
|
+ handleFiles(e.target.files);
|
|
|
+ $(this).val('');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 拖放事件
|
|
|
+ $('#uploadArea')
|
|
|
+ .on('dragover', function(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ $(this).addClass('dragover');
|
|
|
+ })
|
|
|
+ .on('dragleave', function(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ $(this).removeClass('dragover');
|
|
|
+ })
|
|
|
+ .on('drop', function(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ $(this).removeClass('dragover');
|
|
|
+
|
|
|
+ const files = e.originalEvent.dataTransfer.files;
|
|
|
+ if (files.length > 0) {
|
|
|
+ handleFiles(files);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 批量操作
|
|
|
+ $('#batchRemoveBtn').on('click', removeSelectedFiles);
|
|
|
+ $('#batchUploadBtn').on('click', uploadSelectedFiles);
|
|
|
+
|
|
|
+ // 关闭提示
|
|
|
+ $(document).on('click', '.alert .btn-close', function() {
|
|
|
+ $(this).closest('.alert').hide();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理文件
|
|
|
+ async function handleFiles(fileList) {
|
|
|
+ const files = Array.from(fileList);
|
|
|
+ if (files.length === 0) return;
|
|
|
+
|
|
|
+ let addedCount = 0;
|
|
|
+
|
|
|
+ for (const file of files) {
|
|
|
+ try {
|
|
|
+ // 安全检查
|
|
|
+ const securityCheck = await checkFileSecurity(file);
|
|
|
+
|
|
|
+ if (!securityCheck.isSafe) {
|
|
|
+ showError(`文件 "${file.name}" 安全检查失败: ${securityCheck.message}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到文件列表
|
|
|
+ if (addFile(file)) {
|
|
|
+ addedCount++;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理文件失败:', error);
|
|
|
+ showError(`处理文件 "${file.name}" 时出错: ${error.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (addedCount > 0) {
|
|
|
+ showSuccess(`成功添加 ${addedCount} 个文件到上传列表`);
|
|
|
+ updateStats();
|
|
|
+ processQueue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全检查
|
|
|
+ async function checkFileSecurity(file) {
|
|
|
+ // 1. 检查扩展名
|
|
|
+ const extension = '.' + file.name.toLowerCase().split('.').pop();
|
|
|
+ if (DANGEROUS_EXTENSIONS.includes(extension)) {
|
|
|
+ return {
|
|
|
+ isSafe: false,
|
|
|
+ message: `禁止上传 ${extension} 类型的文件`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查MIME类型
|
|
|
+ if (file.type) {
|
|
|
+ for (const mimeType of DANGEROUS_MIME_TYPES) {
|
|
|
+ if (file.type.includes(mimeType)) {
|
|
|
+ return {
|
|
|
+ isSafe: false,
|
|
|
+ message: `禁止上传 ${file.type} 类型的文件`
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查文件签名
|
|
|
+ const signatureCheck = await checkFileSignature(file);
|
|
|
+ if (!signatureCheck.isSafe) {
|
|
|
+ return signatureCheck;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ isSafe: true,
|
|
|
+ message: '安全检查通过'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件签名
|
|
|
+ function checkFileSignature(file) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ // 如果文件很小,直接跳过签名检查
|
|
|
+ if (file.size < 4) {
|
|
|
+ resolve({ isSafe: true, message: '文件过小,跳过签名检查' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const reader = new FileReader();
|
|
|
+
|
|
|
+ reader.onload = function(e) {
|
|
|
+ try {
|
|
|
+ const arr = new Uint8Array(e.target.result);
|
|
|
+ const hexArray = Array.from(arr).map(b =>
|
|
|
+ b.toString(16).padStart(2, '0').toUpperCase()
|
|
|
+ );
|
|
|
+ const hexString = hexArray.join('');
|
|
|
+
|
|
|
+ // 危险文件签名
|
|
|
+ const dangerousSignatures = [
|
|
|
+ '4D5A', // Windows EXE
|
|
|
+ '5A4D', // Windows EXE
|
|
|
+ '2321', // Shell脚本
|
|
|
+ 'CAFEBABE', // Java类文件
|
|
|
+ '504B0304', // ZIP/JAR
|
|
|
+ '7F454C46', // ELF可执行文件
|
|
|
+ 'CFFAEDFE', // Mach-O
|
|
|
+ 'FEEDFACE' // Mach-O
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const signature of dangerousSignatures) {
|
|
|
+ if (hexString.startsWith(signature)) {
|
|
|
+ resolve({
|
|
|
+ isSafe: false,
|
|
|
+ message: `检测到危险文件签名 (${signature})`
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve({
|
|
|
+ isSafe: true,
|
|
|
+ message: '文件签名检查通过'
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ resolve({
|
|
|
+ isSafe: true,
|
|
|
+ message: '签名检查出错,跳过检查'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.onerror = function() {
|
|
|
+ resolve({
|
|
|
+ isSafe: true,
|
|
|
+ message: '读取文件失败,跳过签名检查'
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 读取文件前256字节
|
|
|
+ const blob = file.slice(0, 256);
|
|
|
+ reader.readAsArrayBuffer(blob);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加文件到列表
|
|
|
+ function addFile(file) {
|
|
|
+ // 检查是否已存在
|
|
|
+ const existingFile = state.files.find(f =>
|
|
|
+ f.name === file.name &&
|
|
|
+ f.size === file.size &&
|
|
|
+ f.lastModified === file.lastModified
|
|
|
+ );
|
|
|
+
|
|
|
+ if (existingFile) {
|
|
|
+ showWarning(`文件 "${file.name}" 已存在`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建文件对象
|
|
|
+ const fileObj = {
|
|
|
+ id: 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
|
|
+ file: file,
|
|
|
+ name: file.name,
|
|
|
+ size: file.size,
|
|
|
+ type: file.type || '未知类型',
|
|
|
+ status: 'pending',
|
|
|
+ progress: 0,
|
|
|
+ retries: 0,
|
|
|
+ error: null
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加到状态
|
|
|
+ state.files.push(fileObj);
|
|
|
+
|
|
|
+ // 创建DOM元素
|
|
|
+ createFileElement(fileObj);
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建文件元素
|
|
|
+ function createFileElement(fileObj) {
|
|
|
+ const fileElement = $(`
|
|
|
+ <div class="file-item ${fileObj.status}" id="${fileObj.id}">
|
|
|
+ <div class="file-icon">
|
|
|
+ ${getFileIcon(fileObj.type, fileObj.name)}
|
|
|
+ </div>
|
|
|
+ <div class="file-info">
|
|
|
+ <div class="file-name">${escapeHtml(fileObj.name)}</div>
|
|
|
+ <div class="file-details">
|
|
|
+ <span class="file-size">${formatFileSize(fileObj.size)}</span>
|
|
|
+ <span class="file-type">${fileObj.type}</span>
|
|
|
+ </div>
|
|
|
+ <div class="file-progress">
|
|
|
+ <div class="progress-bar" id="${fileObj.id}_progress"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="file-status" id="${fileObj.id}_status">
|
|
|
+ <span class="status-${fileObj.status}">
|
|
|
+ ${getStatusText(fileObj.status)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="file-actions">
|
|
|
+ <button class="btn btn-outline-primary btn-sm select-btn" title="选择文件">
|
|
|
+ <i class="far fa-square"></i>
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-outline-danger btn-sm remove-btn" title="移除文件">
|
|
|
+ <i class="fas fa-times"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `);
|
|
|
+
|
|
|
+ // 添加到列表
|
|
|
+ $('#filesList').append(fileElement);
|
|
|
+
|
|
|
+ // 绑定事件
|
|
|
+ bindFileEvents(fileObj.id);
|
|
|
+
|
|
|
+ updateListCount();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绑定文件事件
|
|
|
+ function bindFileEvents(fileId) {
|
|
|
+ const fileElement = $(`#${fileId}`);
|
|
|
+
|
|
|
+ // 选择按钮
|
|
|
+ fileElement.find('.select-btn').on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ toggleFileSelection(fileId, $(this));
|
|
|
+ });
|
|
|
+
|
|
|
+ // 移除按钮
|
|
|
+ fileElement.find('.remove-btn').on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ removeFile(fileId);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 点击预览图片
|
|
|
+ if (fileElement.has('.file-icon .fa-image, .file-icon .fa-file-image').length) {
|
|
|
+ fileElement.on('click', '.file-icon, .file-name', function(e) {
|
|
|
+ if (!$(e.target).closest('button').length) {
|
|
|
+ previewImage(fileId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理上传队列
|
|
|
+ async function processQueue() {
|
|
|
+ updateQueueInfo();
|
|
|
+
|
|
|
+ // 如果已达最大并发数或没有待上传文件,返回
|
|
|
+ if (state.uploading.length >= CONFIG.maxConcurrentUploads) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 找出待上传的文件
|
|
|
+ const pendingFiles = state.files.filter(f =>
|
|
|
+ f.status === 'pending' &&
|
|
|
+ !state.queue.includes(f.id) &&
|
|
|
+ !state.uploading.includes(f.id)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (pendingFiles.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到队列
|
|
|
+ pendingFiles.forEach(file => {
|
|
|
+ state.queue.push(file.id);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理队列
|
|
|
+ while (state.uploading.length < CONFIG.maxConcurrentUploads && state.queue.length > 0) {
|
|
|
+ const fileId = state.queue.shift();
|
|
|
+ const fileObj = state.files.find(f => f.id === fileId);
|
|
|
+
|
|
|
+ if (fileObj) {
|
|
|
+ state.uploading.push(fileId);
|
|
|
+ await uploadFile(fileObj);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传文件
|
|
|
+ function uploadFile(fileObj) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ updateFileStatus(fileObj.id, 'uploading', '上传中...');
|
|
|
+
|
|
|
+ // 模拟上传过程(实际使用时替换为真实的上传逻辑)
|
|
|
+ // let progress = 0;
|
|
|
+ // const interval = setInterval(() => {
|
|
|
+ // progress += Math.random() * 10;
|
|
|
+ // if (progress >= 100) {
|
|
|
+ // progress = 100;
|
|
|
+ // clearInterval(interval);
|
|
|
+ //
|
|
|
+ // // 模拟上传成功
|
|
|
+ // setTimeout(() => {
|
|
|
+ // state.uploading = state.uploading.filter(id => id !== fileObj.id);
|
|
|
+ // state.completed.push(fileObj.id);
|
|
|
+ //
|
|
|
+ // updateFileStatus(fileObj.id, 'success', '上传成功');
|
|
|
+ // updateProgress(fileObj.id, 100);
|
|
|
+ //
|
|
|
+ // // 更新隐藏字段
|
|
|
+ // updateHiddenFields();
|
|
|
+ // updateStats();
|
|
|
+ //
|
|
|
+ // resolve();
|
|
|
+ // }, 300);
|
|
|
+ // } else {
|
|
|
+ // updateProgress(fileObj.id, progress);
|
|
|
+ // }
|
|
|
+ // }, 200);
|
|
|
+
|
|
|
+ // 实际的上传代码(注释掉,需要后端接口)
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', fileObj.file);
|
|
|
+ formData.append('fileName', fileObj.name);
|
|
|
+ formData.append('fileType', fileObj.type);
|
|
|
+ formData.append('fileSize', fileObj.size);
|
|
|
+
|
|
|
+ $.ajax({
|
|
|
+ url: CONFIG.uploadUrl,
|
|
|
+ type: 'POST',
|
|
|
+ data: formData,
|
|
|
+ processData: false,
|
|
|
+ contentType: false,
|
|
|
+ xhr: function() {
|
|
|
+ const xhr = new window.XMLHttpRequest();
|
|
|
+ xhr.upload.addEventListener('progress', function(e) {
|
|
|
+ if (e.lengthComputable) {
|
|
|
+ const percent = Math.round((e.loaded / e.total) * 100);
|
|
|
+ updateProgress(fileObj.id, percent);
|
|
|
+ }
|
|
|
+ }, false);
|
|
|
+ return xhr;
|
|
|
+ },
|
|
|
+ success: function(response) {
|
|
|
+ if (response.success) {
|
|
|
+ state.uploading = state.uploading.filter(id => id !== fileObj.id);
|
|
|
+ state.completed.push(fileObj.id);
|
|
|
+
|
|
|
+ fileObj.serverPath = response.data.filePath;
|
|
|
+ fileObj.fileHash = response.data.fileHash;
|
|
|
+
|
|
|
+ updateFileStatus(fileObj.id, 'success', '上传成功');
|
|
|
+ updateHiddenFields();
|
|
|
+ updateStats();
|
|
|
+ resolve();
|
|
|
+ } else {
|
|
|
+ reject(new Error(response.message));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ error: function(xhr, status, error) {
|
|
|
+ reject(new Error('上传失败: ' + error));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新文件状态
|
|
|
+ function updateFileStatus(fileId, status, message) {
|
|
|
+ const fileElement = $(`#${fileId}`);
|
|
|
+ const statusElement = $(`#${fileId}_status`);
|
|
|
+
|
|
|
+ // 更新状态类
|
|
|
+ fileElement
|
|
|
+ .removeClass('pending checking uploading success error')
|
|
|
+ .addClass(status);
|
|
|
+
|
|
|
+ // 更新状态文本
|
|
|
+ statusElement.html(`<span class="status-${status}">${message}</span>`);
|
|
|
+
|
|
|
+ // 更新状态对象
|
|
|
+ const fileObj = state.files.find(f => f.id === fileId);
|
|
|
+ if (fileObj) {
|
|
|
+ fileObj.status = status;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新上传进度
|
|
|
+ function updateProgress(fileId, progress) {
|
|
|
+ const progressBar = $(`#${fileId}_progress`);
|
|
|
+ progressBar.css('width', progress + '%');
|
|
|
+
|
|
|
+ const fileObj = state.files.find(f => f.id === fileId);
|
|
|
+ if (fileObj) {
|
|
|
+ fileObj.progress = progress;
|
|
|
+
|
|
|
+ if (progress < 100) {
|
|
|
+ $(`#${fileId}_status`).html(`<span class="status-uploading">上传中 ${Math.round(progress)}%</span>`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除文件
|
|
|
+ function removeFile(fileId) {
|
|
|
+ // 从状态中移除
|
|
|
+ state.files = state.files.filter(f => f.id !== fileId);
|
|
|
+ state.queue = state.queue.filter(id => id !== fileId);
|
|
|
+ state.uploading = state.uploading.filter(id => id !== fileId);
|
|
|
+ state.selectedFiles.delete(fileId);
|
|
|
+
|
|
|
+ // 移除DOM元素
|
|
|
+ $(`#${fileId}`).remove();
|
|
|
+
|
|
|
+ // 更新统计
|
|
|
+ updateStats();
|
|
|
+ updateListCount();
|
|
|
+ updateBatchActions();
|
|
|
+ updateQueueInfo();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换文件选择
|
|
|
+ function toggleFileSelection(fileId, button) {
|
|
|
+ if (state.selectedFiles.has(fileId)) {
|
|
|
+ state.selectedFiles.delete(fileId);
|
|
|
+ button.html('<i class="far fa-square"></i>');
|
|
|
+ button.removeClass('btn-primary').addClass('btn-outline-primary');
|
|
|
+ } else {
|
|
|
+ state.selectedFiles.add(fileId);
|
|
|
+ button.html('<i class="fas fa-check-square"></i>');
|
|
|
+ button.removeClass('btn-outline-primary').addClass('btn-primary');
|
|
|
+ }
|
|
|
+
|
|
|
+ updateBatchActions();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除选中的文件
|
|
|
+ function removeSelectedFiles() {
|
|
|
+ state.selectedFiles.forEach(fileId => {
|
|
|
+ removeFile(fileId);
|
|
|
+ });
|
|
|
+ state.selectedFiles.clear();
|
|
|
+ updateBatchActions();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传选中的文件
|
|
|
+ function uploadSelectedFiles() {
|
|
|
+ state.selectedFiles.forEach(fileId => {
|
|
|
+ const fileObj = state.files.find(f => f.id === fileId);
|
|
|
+ if (fileObj && fileObj.status === 'pending') {
|
|
|
+ updateFileStatus(fileId, 'pending', '等待上传');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ state.selectedFiles.clear();
|
|
|
+ updateBatchActions();
|
|
|
+ processQueue();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 预览图片
|
|
|
+ function previewImage(fileId) {
|
|
|
+ const fileObj = state.files.find(f => f.id === fileId);
|
|
|
+ if (!fileObj || !fileObj.file.type.startsWith('image/')) return;
|
|
|
+
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = function(e) {
|
|
|
+ $('#previewImage').attr('src', e.target.result);
|
|
|
+ $('#previewContainer').show();
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(fileObj.file);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新隐藏字段
|
|
|
+ function updateHiddenFields() {
|
|
|
+ const completedFiles = state.files.filter(f => f.status === 'success');
|
|
|
+
|
|
|
+ const paths = completedFiles.map(f => f.serverPath || '').join('|');
|
|
|
+ const names = completedFiles.map(f => f.name).join('|');
|
|
|
+ const types = completedFiles.map(f => f.type).join('|');
|
|
|
+ const sizes = completedFiles.map(f => f.size).join('|');
|
|
|
+ const hashes = completedFiles.map(f => f.fileHash || '').join('|');
|
|
|
+
|
|
|
+ $('#filePaths').val(paths);
|
|
|
+ $('#fileNames').val(names);
|
|
|
+ $('#fileTypes').val(types);
|
|
|
+ $('#fileSizes').val(sizes);
|
|
|
+ $('#fileHashes').val(hashes);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新统计信息
|
|
|
+ function updateStats() {
|
|
|
+ const total = state.files.length;
|
|
|
+ const success = state.files.filter(f => f.status === 'success').length;
|
|
|
+ const failed = state.files.filter(f => f.status === 'error').length;
|
|
|
+
|
|
|
+ $('#totalFiles').text(total);
|
|
|
+ $('#successFiles').text(success);
|
|
|
+ $('#failedFiles').text(failed);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新列表计数
|
|
|
+ function updateListCount() {
|
|
|
+ const count = state.files.length;
|
|
|
+ $('#listCount').text(count);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新批量操作栏
|
|
|
+ function updateBatchActions() {
|
|
|
+ const count = state.selectedFiles.size;
|
|
|
+ $('#selectedCount').text(count);
|
|
|
+
|
|
|
+ if (count > 0) {
|
|
|
+ $('#batchActions').show();
|
|
|
+ } else {
|
|
|
+ $('#batchActions').hide();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新队列信息
|
|
|
+ function updateQueueInfo() {
|
|
|
+ const queueCount = state.queue.length;
|
|
|
+ const uploadingCount = state.uploading.length;
|
|
|
+
|
|
|
+ if (queueCount > 0 || uploadingCount > 0) {
|
|
|
+ $('#queueInfo').show();
|
|
|
+ $('#queueStatus').html(`队列中: <strong>${queueCount}</strong> | 上传中: <strong>${uploadingCount}</strong>`);
|
|
|
+ } else {
|
|
|
+ $('#queueInfo').hide();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示成功提示
|
|
|
+ function showSuccess(message) {
|
|
|
+ $('#successAlert').html(`
|
|
|
+ <i class="fas fa-check-circle me-2"></i>${message}
|
|
|
+ <button type="button" class="btn-close float-end" aria-label="Close"></button>
|
|
|
+ `).show();
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ $('#successAlert').fadeOut();
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示错误提示
|
|
|
+ function showError(message) {
|
|
|
+ $('#errorAlert').html(`
|
|
|
+ <i class="fas fa-exclamation-circle me-2"></i>${message}
|
|
|
+ <button type="button" class="btn-close float-end" aria-label="Close"></button>
|
|
|
+ `).show();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示警告提示
|
|
|
+ function showWarning(message) {
|
|
|
+ $('#warningAlert').html(`
|
|
|
+ <i class="fas fa-exclamation-triangle me-2"></i>${message}
|
|
|
+ <button type="button" class="btn-close float-end" aria-label="Close"></button>
|
|
|
+ `).show();
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ $('#warningAlert').fadeOut();
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具函数
|
|
|
+ function getFileIcon(mimeType, fileName) {
|
|
|
+ const ext = fileName.split('.').pop().toLowerCase();
|
|
|
+
|
|
|
+ if (mimeType) {
|
|
|
+ if (mimeType.startsWith('image/')) return '<i class="fas fa-file-image"></i>';
|
|
|
+ if (mimeType.startsWith('video/')) return '<i class="fas fa-file-video"></i>';
|
|
|
+ if (mimeType.startsWith('audio/')) return '<i class="fas fa-file-audio"></i>';
|
|
|
+ if (mimeType.includes('pdf')) return '<i class="fas fa-file-pdf"></i>';
|
|
|
+ if (mimeType.includes('word') || mimeType.includes('document')) return '<i class="fas fa-file-word"></i>';
|
|
|
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '<i class="fas fa-file-excel"></i>';
|
|
|
+ if (mimeType.includes('zip') || mimeType.includes('compressed')) return '<i class="fas fa-file-archive"></i>';
|
|
|
+ if (mimeType.includes('text')) return '<i class="fas fa-file-alt"></i>';
|
|
|
+ }
|
|
|
+
|
|
|
+ const iconMap = {
|
|
|
+ 'jpg': 'fa-file-image', 'jpeg': 'fa-file-image', 'png': 'fa-file-image',
|
|
|
+ 'gif': 'fa-file-image', 'bmp': 'fa-file-image', 'webp': 'fa-file-image',
|
|
|
+ 'svg': 'fa-file-image',
|
|
|
+ 'mp4': 'fa-file-video', 'avi': 'fa-file-video', 'mov': 'fa-file-video',
|
|
|
+ 'wmv': 'fa-file-video', 'flv': 'fa-file-video', 'mkv': 'fa-file-video',
|
|
|
+ 'mp3': 'fa-file-audio', 'wav': 'fa-file-audio', 'flac': 'fa-file-audio',
|
|
|
+ 'pdf': 'fa-file-pdf',
|
|
|
+ 'doc': 'fa-file-word', 'docx': 'fa-file-word',
|
|
|
+ 'xls': 'fa-file-excel', 'xlsx': 'fa-file-excel',
|
|
|
+ 'ppt': 'fa-file-powerpoint', 'pptx': 'fa-file-powerpoint',
|
|
|
+ 'zip': 'fa-file-archive', 'rar': 'fa-file-archive', '7z': 'fa-file-archive',
|
|
|
+ 'tar': 'fa-file-archive', 'gz': 'fa-file-archive',
|
|
|
+ 'txt': 'fa-file-alt', 'md': 'fa-file-alt',
|
|
|
+ 'js': 'fa-file-code', 'html': 'fa-file-code', 'css': 'fa-file-code',
|
|
|
+ 'json': 'fa-file-code', 'xml': 'fa-file-code',
|
|
|
+ 'exe': 'fa-file-exclamation', 'dll': 'fa-file-exclamation'
|
|
|
+ };
|
|
|
+
|
|
|
+ return `<i class="fas ${iconMap[ext] || 'fa-file'}"></i>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getStatusText(status) {
|
|
|
+ const statusMap = {
|
|
|
+ 'pending': '等待上传',
|
|
|
+ 'checking': '检查中',
|
|
|
+ 'uploading': '上传中',
|
|
|
+ 'success': '上传成功',
|
|
|
+ 'error': '上传失败'
|
|
|
+ };
|
|
|
+ return statusMap[status] || status;
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatFileSize(bytes) {
|
|
|
+ if (bytes === 0) return '0 B';
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
+ }
|
|
|
+
|
|
|
+ function escapeHtml(text) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = text;
|
|
|
+ return div.innerHTML;
|
|
|
+ }
|
|
|
+});
|