码桶
发现社区成员的开源项目
script.js34 KB
/**
* 在线解压缩工具核心脚本
*
* @author ArsnYeon
* @website https://yasuo.zip/
* @source https://github.com/arsnyeon/online-unzip-tool
* @license MIT License
*
* 版权声明:
* 本代码遵循MIT开源协议,允许自由使用、修改和分发。
* 但请保留此版权声明和作者信息,禁止删除此文本注释。
* 如需商业使用或二次开发,请遵守开源协议的相关条款。
*
* 实现功能:
* 1. 文件上传(支持拖拽和点击上传)
* 2. 文件大小验证(限制500MB)
* 3. 解压ZIP/GZ/TAR格式文件
* 4. 压缩文件和文件夹为ZIP格式
* 5. 解压进度显示
* 6. 文件浏览和下载功能
*/
/**
* 智能检测文本编码并解码
* @param {Uint8Array} bytes - 字节数组
* @returns {string} 解码后的字符串
*/
function smartDecodeFileName(bytes) {
// 检查是否为ASCII(最常见且最安全)
let isAscii = true;
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] > 127) {
isAscii = false;
break;
}
}
if (isAscii) {
return new TextDecoder('ascii').decode(bytes);
}
// 尝试UTF-8(现代标准)
try {
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
const decoded = utf8Decoder.decode(bytes);
return decoded;
} catch (e) {
// UTF-8失败,继续尝试其他编码
}
// 检测可能的中文编码
const encodings = ['gbk', 'gb2312', 'big5', 'shift-jis'];
for (const encoding of encodings) {
try {
const decoder = new TextDecoder(encoding);
const decoded = decoder.decode(bytes);
// 简单的启发式检查:如果解码结果不包含太多替换字符,认为是正确的
const replacementCount = (decoded.match(/\uFFFD/g) || []).length;
if (replacementCount / decoded.length < 0.1) { // 替换字符少于10%
return decoded;
}
} catch (e) {
continue;
}
}
// 所有编码都失败,使用latin1作为最后的fallback
return new TextDecoder('latin1').decode(bytes);
}
// 定义最大允许上传大小:500MB
const MAX_FILE_SIZE = 500 * 1024 * 1024;
// DOM元素缓存
const elements = {
dropArea: document.getElementById('drop-area'),
fileInput: document.getElementById('file-input'),
uploadBtn: document.getElementById('upload-btn'),
uploadSection: document.getElementById('upload-section'),
processingSection: document.getElementById('processing-section'),
resultSection: document.getElementById('result-section'),
progressBar: document.getElementById('progress-bar'),
progressText: document.getElementById('progress-text'),
processingText: document.getElementById('processing-text'),
fileList: document.getElementById('file-list'),
fileName: document.getElementById('file-name'),
fileSize: document.getElementById('file-size'),
downloadAllBtn: document.getElementById('download-all-btn'),
errorPopup: document.getElementById('error-popup'),
errorMessage: document.getElementById('error-message'),
errorClose: document.getElementById('error-close'),
compressBtn: document.getElementById('compress-btn'),
extractBtn: document.getElementById('extract-btn'),
imageCompressBtn: document.getElementById('image-compress-btn'),
compressFileInput: document.getElementById('compress-file-input'),
compressFilesInput: document.getElementById('compress-files-input'),
imageCompressInput: document.getElementById('image-compress-input'),
selectFilesBtn: document.getElementById('select-files-btn'),
selectFolderBtn: document.getElementById('select-folder-btn'),
selectImagesBtn: document.getElementById('select-images-btn'),
uploadTitle: document.getElementById('upload-title'),
uploadDescription: document.getElementById('upload-description'),
uploadButtons: document.getElementById('upload-buttons'),
compressButtons: document.getElementById('compress-buttons'),
imageCompressButtons: document.getElementById('image-compress-buttons'),
extractIcon: document.getElementById('extract-icon'),
compressIcon: document.getElementById('compress-icon'),
imageCompressIcon: document.getElementById('image-compress-icon'),
qualitySlider: document.getElementById('quality-slider'),
qualityValue: document.getElementById('quality-value')
};
// 存储解压后的文件或待压缩的文件
let extractedFiles = {};
let currentFile = null;
let currentMode = 'compress'; // 当前模式:'extract'、'compress' 或 'image-compress'
/**
* 初始化事件监听器
*/
function initEventListeners() {
const { dropArea, fileInput, uploadBtn, downloadAllBtn, errorClose, compressBtn, extractBtn, imageCompressBtn,
selectFilesBtn, selectFolderBtn, selectImagesBtn, compressFilesInput, compressFileInput,
imageCompressInput, qualitySlider, qualityValue } = elements;
// 上传按钮点击事件
uploadBtn.addEventListener('click', () => currentMode === 'extract' && fileInput.click());
// 文件选择事件
fileInput.addEventListener('change', handleFileSelect);
// 拖放区域事件
['dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, handleDragEvent);
});
// 点击拖放区域触发文件选择
dropArea.addEventListener('click', (e) => {
if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
if (currentMode === 'extract') {
fileInput.click();
}
// 删除了图片压缩模式的点击事件,只保留拖拽功能
}
});
// 其他事件监听器
downloadAllBtn.addEventListener('click', downloadAllFiles);
errorClose.addEventListener('click', () => elements.errorPopup.style.display = 'none');
compressBtn?.addEventListener('click', () => switchMode('compress'));
extractBtn?.addEventListener('click', () => switchMode('extract'));
imageCompressBtn?.addEventListener('click', () => switchMode('image-compress'));
selectFilesBtn?.addEventListener('click', () => compressFilesInput.click());
selectFolderBtn?.addEventListener('click', () => compressFileInput.click());
selectImagesBtn?.addEventListener('click', () => imageCompressInput.click());
compressFilesInput?.addEventListener('change', handleCompressFilesSelect);
compressFileInput?.addEventListener('change', handleCompressFolderSelect);
imageCompressInput?.addEventListener('change', handleImageCompressSelect);
// 质量滑块事件
qualitySlider?.addEventListener('input', (e) => {
qualityValue.textContent = e.target.value;
});
}
/**
* 处理拖放事件
*/
function handleDragEvent(e) {
e.preventDefault();
e.stopPropagation();
const { dropArea } = elements;
if (e.type === 'dragover') {
dropArea.classList.add('drag-over');
} else if (e.type === 'dragleave') {
dropArea.classList.remove('drag-over');
} else if (e.type === 'drop') {
dropArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length) {
if (currentMode === 'extract') {
handleFiles(files);
} else if (currentMode === 'compress') {
handleCompressFiles(files);
} else if (currentMode === 'image-compress') {
handleImageCompressFiles(files);
}
}
}
}
/**
* 切换工具模式
* @param {string} mode - 模式:'compress'、'extract' 或 'image-compress'
*/
function switchMode(mode) {
currentMode = mode;
const { compressBtn, extractBtn, imageCompressBtn, uploadTitle, uploadDescription, uploadButtons,
compressButtons, imageCompressButtons, extractIcon, compressIcon, imageCompressIcon, fileInput } = elements;
// 更新按钮状态
compressBtn?.classList.toggle('active', mode === 'compress');
extractBtn?.classList.toggle('active', mode === 'extract');
imageCompressBtn?.classList.toggle('active', mode === 'image-compress');
// 更新界面显示
if (mode === 'compress') {
uploadTitle.textContent = '选择文件进行压缩';
uploadDescription.innerHTML = '拖拽文件到此处上传<br>(支持多种文件格式,最大500MB)';
uploadButtons.style.display = 'none';
compressButtons.style.display = 'flex';
imageCompressButtons.style.display = 'none';
extractIcon.style.display = 'none';
compressIcon.style.display = 'block';
imageCompressIcon.style.display = 'none';
fileInput.accept = '';
} else if (mode === 'extract') {
uploadTitle.textContent = '选择压缩文件进行解压';
uploadDescription.innerHTML = '拖拽压缩文件到此处上传<br>(支持ZIP、GZ、TAR格式,最大500MB)';
uploadButtons.style.display = 'flex';
compressButtons.style.display = 'none';
imageCompressButtons.style.display = 'none';
extractIcon.style.display = 'block';
compressIcon.style.display = 'none';
imageCompressIcon.style.display = 'none';
fileInput.accept = '.zip,.gz,.tar';
} else if (mode === 'image-compress') {
uploadTitle.textContent = '选择图片进行压缩';
uploadDescription.innerHTML = '拖拽图片到此处上传<br>(支持JPG、PNG、WebP格式,最大500MB)';
uploadButtons.style.display = 'none';
compressButtons.style.display = 'none';
imageCompressButtons.style.display = 'flex';
extractIcon.style.display = 'none';
compressIcon.style.display = 'none';
imageCompressIcon.style.display = 'block';
fileInput.accept = '';
}
resetInterface();
}
/**
* 处理压缩文件选择
*/
function handleCompressFilesSelect(e) {
const files = e.target.files;
if (files.length) {
handleCompressFiles(files);
}
}
/**
* 处理压缩文件夹选择
*/
function handleCompressFolderSelect(e) {
const files = e.target.files;
if (files.length) {
handleCompressFiles(files);
}
}
/**
* 处理图片压缩文件选择
*/
function handleImageCompressSelect(e) {
const files = e.target.files;
if (files.length) {
handleImageCompressFiles(files);
}
}
/**
* 处理要压缩的图片文件
* @param {FileList} files - 要压缩的图片文件列表
*/
function handleImageCompressFiles(files) {
// 验证文件类型和大小
const validFiles = [];
const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!supportedTypes.includes(file.type)) {
showError(`文件 ${file.name} 不是支持的图片格式\n支持的格式:JPG、PNG、WebP`);
return;
}
if (file.size > MAX_FILE_SIZE) {
showError(`文件 ${file.name} 大小超过限制(${formatFileSize(MAX_FILE_SIZE)})`);
return;
}
validFiles.push(file);
}
if (validFiles.length === 0) {
showError('没有找到有效的图片文件');
return;
}
showProcessingUI('正在压缩图片...');
compressImages(validFiles);
}
/**
* 压缩图片文件
* @param {Array} files - 要压缩的图片文件数组
*/
async function compressImages(files) {
try {
const quality = parseInt(elements.qualitySlider.value) / 100;
const totalFiles = files.length;
let processedFiles = 0;
extractedFiles = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 压缩图片
const compressedBlob = await compressImage(file, quality);
// 生成压缩后的文件名
const originalName = file.name;
const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.'));
const ext = originalName.substring(originalName.lastIndexOf('.'));
const compressedName = `${nameWithoutExt}(yasuo.zip)${ext}`;
// 存储压缩后的文件
extractedFiles[compressedName] = {
name: compressedName,
path: compressedName,
size: compressedBlob.size,
type: compressedBlob.type,
content: compressedBlob,
originalSize: file.size,
compressionRatio: ((file.size - compressedBlob.size) / file.size * 100).toFixed(1)
};
// 更新进度
processedFiles++;
updateProgress(Math.floor((processedFiles / totalFiles) * 100));
}
// 计算总体压缩信息
const totalOriginalSize = files.reduce((sum, file) => sum + file.size, 0);
const totalCompressedSize = Object.values(extractedFiles).reduce((sum, file) => sum + file.size, 0);
const overallCompressionRatio = ((totalOriginalSize - totalCompressedSize) / totalOriginalSize * 100).toFixed(1);
// 更新文件信息
elements.fileName.textContent = `${files.length}个图片文件 (压缩率: ${overallCompressionRatio}%)`;
elements.fileSize.textContent = `${formatFileSize(totalCompressedSize)} (原始: ${formatFileSize(totalOriginalSize)})`;
showResults('图片压缩结果');
} catch (error) {
console.error('图片压缩失败:', error);
showError('图片压缩失败: ' + error.message);
resetInterface();
}
}
/**
* 压缩单个图片
* @param {File} file - 要压缩的图片文件
* @param {number} quality - 压缩质量 (0-1)
* @returns {Promise<Blob>} 压缩后的图片Blob
*/
function compressImage(file, quality) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
// 设置画布尺寸
canvas.width = img.width;
canvas.height = img.height;
// 绘制图片到画布
ctx.drawImage(img, 0, 0);
// 根据原始格式选择输出格式
let outputFormat = 'image/jpeg';
if (file.type === 'image/png') {
outputFormat = 'image/png';
} else if (file.type === 'image/webp') {
outputFormat = 'image/webp';
}
// 转换为Blob
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('图片压缩失败'));
}
}, outputFormat, quality);
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = URL.createObjectURL(file);
});
}
/**
* 处理要压缩的文件
* @param {FileList} files - 要压缩的文件列表
*/
function handleCompressFiles(files) {
// 验证文件总大小
const totalSize = Array.from(files).reduce((sum, file) => sum + file.size, 0);
if (totalSize > MAX_FILE_SIZE) {
showError(`文件总大小超过限制(${formatFileSize(MAX_FILE_SIZE)})`);
return;
}
showProcessingUI('正在压缩中...');
compressFiles(files);
}
/**
* 压缩文件为ZIP
* @param {FileList} files - 要压缩的文件列表
*/
async function compressFiles(files) {
try {
const zip = new JSZip();
const totalFiles = files.length;
let processedFiles = 0;
// 添加文件到ZIP
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 处理文件路径,保持文件夹结构
let filePath = file.webkitRelativePath || file.name;
// 读取文件内容
const fileContent = await readFileAsArrayBuffer(file);
// 添加到ZIP
zip.file(filePath, fileContent);
// 更新进度
processedFiles++;
updateProgress(Math.floor((processedFiles / totalFiles) * 100));
}
// 添加说明文本文件
const readmeContent = '本压缩包由【在线解压缩工具yasuo.zip】在线压缩,感谢您的使用!';
zip.file('在线解压缩工具(yasuo.zip).txt', readmeContent);
// 生成ZIP文件
const zipBlob = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
});
// 创建压缩结果
let zipName;
if (files.length === 1 && !files[0].webkitRelativePath) {
// 单个文件
zipName = files[0].name.replace(/\.[^/.]+$/, '') + '.zip';
} else {
// 文件夹压缩或多个文件
zipName = 'yasuo.zip';
}
extractedFiles = {
[zipName]: {
name: zipName,
path: zipName,
size: zipBlob.size,
type: 'application/zip',
content: zipBlob
}
};
// 更新文件信息
elements.fileName.textContent = zipName;
elements.fileSize.textContent = formatFileSize(zipBlob.size);
showResults('压缩结果');
} catch (error) {
console.error('压缩失败:', error);
showError('压缩失败: ' + error.message);
resetInterface();
}
}
/**
* 读取文件为ArrayBuffer
* @param {File} file - 要读取的文件
* @returns {Promise<ArrayBuffer>} 文件内容
*/
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
/**
* 文件选择处理函数
*/
function handleFileSelect(e) {
const files = e.target.files;
if (files.length) {
handleFiles(files);
}
}
/**
* 显示处理界面
* @param {string} text - 处理提示文本
*/
function showProcessingUI(text) {
const { uploadSection, processingSection, resultSection, processingText } = elements;
uploadSection.style.display = 'none';
processingSection.style.display = 'block';
resultSection.style.display = 'none';
processingText.textContent = text;
updateProgress(0);
}
/**
* 显示结果界面
* @param {string} title - 结果标题
*/
function showResults(title) {
const { processingSection, resultSection } = elements;
const resultHeader = document.querySelector('.result-header h2');
if (resultHeader) resultHeader.textContent = title;
displayFiles();
processingSection.style.display = 'none';
resultSection.style.display = 'block';
}
/**
* 处理上传的文件
* @param {FileList} files - 用户选择的文件列表
*/
function handleFiles(files) {
const file = files[0];
// 验证文件大小和类型
if (file.size > MAX_FILE_SIZE) {
showError(`文件大小超过限制(${formatFileSize(MAX_FILE_SIZE)})`);
return;
}
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
const supportedFormats = ['.zip', '.gz', '.tar'];
if (!supportedFormats.includes(fileExt)) {
showError('目前支持的格式:ZIP、GZ、TAR\n(RAR、7Z等格式由于浏览器限制暂不支持)');
return;
}
// 保存当前文件信息
currentFile = file;
elements.fileName.textContent = file.name;
elements.fileSize.textContent = formatFileSize(file.size);
showProcessingUI('正在解压中...');
extractArchive(file);
}
/**
* 解压文件
* @param {File} file - 要解压的文件
*/
async function extractArchive(file) {
try {
extractedFiles = {}; // 重置已提取文件
// 获取文件扩展名
const fileExtension = file.name.split('.').pop().toLowerCase();
// 根据文件格式选择解压方法
switch (fileExtension) {
case 'zip':
await extractZip(file);
break;
case 'gz':
await extractGzip(file);
break;
case 'tar':
await extractTar(file);
break;
default:
throw new Error('不支持的文件格式');
}
showResults('解压结果');
} catch (error) {
console.error('解压失败:', error);
showError('解压失败: ' + error.message);
resetInterface();
}
}
/**
* 使用JSZip解压ZIP文件
* @param {File} file - 要解压的ZIP文件
*/
async function extractZip(file) {
try {
const zip = new JSZip();
// 读取zip文件,使用智能编码检测
const zipData = await zip.loadAsync(file, {
decodeFileName: smartDecodeFileName
});
const totalFiles = Object.keys(zipData.files).length;
let processedFiles = 0;
// 遍历所有文件
for (const [path, zipEntry] of Object.entries(zipData.files)) {
// 跳过目录
if (zipEntry.dir) {
processedFiles++;
updateProgress(Math.floor((processedFiles / totalFiles) * 100));
continue;
}
// 获取文件内容
const content = await zipEntry.async('blob');
// 存储提取的文件
extractedFiles[path] = {
name: path.split('/').pop(),
path: path,
size: content.size,
type: content.type || getMimeType(path),
content: content
};
// 更新进度
processedFiles++;
updateProgress(Math.floor((processedFiles / totalFiles) * 100));
}
} catch (error) {
console.error('ZIP解压失败:', error);
throw new Error('ZIP解压失败');
}
}
/**
* 使用pako解压GZIP文件
* @param {File} file - 要解压的GZIP文件
*/
async function extractGzip(file) {
try {
// 检查pako是否可用
if (typeof pako === 'undefined') {
throw new Error('GZIP解压库未加载,请刷新页面重试');
}
const arrayBuffer = await file.arrayBuffer();
const compressed = new Uint8Array(arrayBuffer);
// 使用pako解压GZIP
const decompressed = pako.ungzip(compressed);
// 获取原始文件名(去掉.gz扩展名)
let originalName = file.name;
if (originalName.toLowerCase().endsWith('.gz')) {
originalName = originalName.slice(0, -3);
}
const blob = new Blob([decompressed]);
extractedFiles[originalName] = {
name: originalName,
path: originalName,
size: blob.size,
type: getMimeType(originalName),
content: blob
};
updateProgress(100);
} catch (error) {
console.error('GZIP解压失败:', error);
throw new Error('GZIP解压失败,可能是文件损坏或格式不支持');
}
}
/**
* 解压TAR文件(未压缩的TAR格式)
* @param {File} file - 要解压的TAR文件
*/
async function extractTar(file) {
try {
const arrayBuffer = await file.arrayBuffer();
const view = new DataView(arrayBuffer);
let offset = 0;
let processedFiles = 0;
const fileCount = Math.floor(arrayBuffer.byteLength / 512); // 估算文件数量
// TAR文件格式解析
while (offset < arrayBuffer.byteLength - 512) {
// 检查是否到达文件末尾(连续的零字节)
let isEnd = true;
for (let i = 0; i < 512 && offset + i < arrayBuffer.byteLength; i++) {
if (view.getUint8(offset + i) !== 0) {
isEnd = false;
break;
}
}
if (isEnd) break;
// 读取文件名(前100字节)- 使用智能编码检测
const fileNameBytes = new Uint8Array(100);
let fileNameLength = 0;
for (let i = 0; i < 100; i++) {
const byte = view.getUint8(offset + i);
if (byte === 0) break;
fileNameBytes[i] = byte;
fileNameLength++;
}
// 使用智能编码检测解码文件名
const fileName = smartDecodeFileName(fileNameBytes.slice(0, fileNameLength));
if (!fileName) break;
// 读取文件大小(124-135字节,八进制)
let sizeStr = '';
for (let i = 124; i < 136; i++) {
const byte = view.getUint8(offset + i);
if (byte === 0 || byte === 32) break; // 空格或null结束
sizeStr += String.fromCharCode(byte);
}
const fileSize = parseInt(sizeStr.trim(), 8) || 0;
// 读取文件类型(156字节)
const fileType = view.getUint8(offset + 156);
// 跳过头部(512字节)
offset += 512;
// 如果是普通文件且有内容
if (fileSize > 0 && !fileName.endsWith('/') && (fileType === 0 || fileType === 48)) {
// 读取文件内容
const fileData = new Uint8Array(arrayBuffer.slice(offset, offset + fileSize));
const blob = new Blob([fileData]);
extractedFiles[fileName] = {
name: fileName.split('/').pop(),
path: fileName,
size: blob.size,
type: getMimeType(fileName),
content: blob
};
processedFiles++;
updateProgress(Math.floor((processedFiles / Math.max(fileCount / 10, 1)) * 100));
}
// 移动到下一个文件(文件大小向上舍入到512的倍数)
offset += Math.ceil(fileSize / 512) * 512;
}
if (Object.keys(extractedFiles).length === 0) {
throw new Error('未找到有效的文件');
}
} catch (error) {
console.error('TAR解压失败:', error);
throw new Error('TAR解压失败,可能是文件损坏或格式不支持');
}
}
/**
* 根据文件路径获取MIME类型
* @param {string} path - 文件路径
* @returns {string} MIME类型
*/
function getMimeType(path) {
const extension = path.split('.').pop().toLowerCase();
const mimeTypes = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'pdf': 'application/pdf',
'txt': 'text/plain',
'html': 'text/html',
'htm': 'text/html',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'mp3': 'audio/mpeg',
'mp4': 'video/mp4',
'zip': 'application/zip',
'tar': 'application/x-tar',
'gz': 'application/gzip'
};
return mimeTypes[extension] || 'application/octet-stream';
}
/**
* 显示解压后的文件列表
*/
function displayFiles() {
const { fileList } = elements;
fileList.innerHTML = '';
Object.keys(extractedFiles).sort().forEach(path => {
const file = extractedFiles[path];
const fileItem = createFileItem(file);
fileList.appendChild(fileItem);
});
}
/**
* 创建文件项元素
* @param {Object} file - 文件对象
* @returns {HTMLElement} 文件项元素
*/
function createFileItem(file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const fileIcon = getFileIcon(file.name);
// 如果是图片压缩结果,显示压缩信息
let sizeDisplay = formatFileSize(file.size);
if (file.originalSize && file.compressionRatio) {
sizeDisplay = `${formatFileSize(file.size)} <small style="color: var(--secondary-color);">(-${file.compressionRatio}%)</small>`;
}
fileItem.innerHTML = `
<div class="file-name">
<i class="${fileIcon}"></i>
<span title="${file.path}">${file.path}</span>
</div>
<div class="file-size">${sizeDisplay}</div>
<div class="file-action">
<button onclick="downloadFile(extractedFiles['${file.path}'])">
<i class="fas fa-download"></i> 下载
</button>
</div>
`;
return fileItem;
}
/**
* 根据文件名获取适当的图标类
* @param {string} fileName - 文件名
* @returns {string} 图标类名
*/
function getFileIcon(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
// 根据文件类型设置图标
const iconMap = {
// 图片
'jpg': 'fas fa-file-image',
'jpeg': 'fas fa-file-image',
'png': 'fas fa-file-image',
'gif': 'fas fa-file-image',
'svg': 'fas fa-file-image',
// 文档
'pdf': 'fas fa-file-pdf',
'doc': 'fas fa-file-word',
'docx': 'fas fa-file-word',
'txt': 'fas fa-file-alt',
'rtf': 'fas fa-file-alt',
// 表格
'xls': 'fas fa-file-excel',
'xlsx': 'fas fa-file-excel',
'csv': 'fas fa-file-csv',
// 演示文稿
'ppt': 'fas fa-file-powerpoint',
'pptx': 'fas fa-file-powerpoint',
// 代码
'html': 'fas fa-file-code',
'htm': 'fas fa-file-code',
'css': 'fas fa-file-code',
'js': 'fas fa-file-code',
'json': 'fas fa-file-code',
'xml': 'fas fa-file-code',
'py': 'fas fa-file-code',
'java': 'fas fa-file-code',
'c': 'fas fa-file-code',
'cpp': 'fas fa-file-code',
// 音频
'mp3': 'fas fa-file-audio',
'wav': 'fas fa-file-audio',
'ogg': 'fas fa-file-audio',
// 视频
'mp4': 'fas fa-file-video',
'avi': 'fas fa-file-video',
'mov': 'fas fa-file-video',
'wmv': 'fas fa-file-video',
// 压缩文件
'zip': 'fas fa-file-archive',
'tar': 'fas fa-file-archive',
'gz': 'fas fa-file-archive'
};
return iconMap[extension] || 'fas fa-file';
}
/**
* 下载单个文件
* @param {Object} file - 要下载的文件对象
*/
function downloadFile(file) {
const url = URL.createObjectURL(file.content);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* 下载所有文件(创建一个ZIP)
*/
async function downloadAllFiles() {
try {
// 如果只有一个文件,直接下载
const fileCount = Object.keys(extractedFiles).length;
if (fileCount === 1) {
const file = extractedFiles[Object.keys(extractedFiles)[0]];
downloadFile(file);
return;
}
// 创建新的ZIP文件
const zip = new JSZip();
// 添加所有文件到ZIP
for (const path in extractedFiles) {
const file = extractedFiles[path];
zip.file(path, file.content);
}
// 生成ZIP文件
const zipBlob = await zip.generateAsync({ type: 'blob' });
// 创建下载链接
const zipName = currentFile ?
currentFile.name.replace(/\.[^/.]+$/, '') + '_extracted.zip' :
'files.zip';
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = zipName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('创建ZIP失败:', error);
showError('创建ZIP失败: ' + error.message);
}
}
/**
* 更新进度条
* @param {number} percent - 进度百分比(0-100)
*/
function updateProgress(percent) {
const { progressBar, progressText } = elements;
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
}
/**
* 显示错误弹窗
* @param {string} message - 错误消息
*/
function showError(message) {
const { errorMessage, errorPopup } = elements;
errorMessage.textContent = message;
errorPopup.style.display = 'flex';
}
/**
* 重置界面到初始状态
*/
function resetInterface() {
const { uploadSection, processingSection, resultSection, fileInput, compressFilesInput, compressFileInput, imageCompressInput } = elements;
uploadSection.style.display = 'block';
processingSection.style.display = 'none';
resultSection.style.display = 'none';
fileInput.value = '';
compressFilesInput && (compressFilesInput.value = '');
compressFileInput && (compressFileInput.value = '');
imageCompressInput && (imageCompressInput.value = '');
}
/**
* 格式化文件大小显示
* @param {number} bytes - 文件大小(字节)
* @returns {string} 格式化后的大小
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
// 初始化页面
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
// 页面已经默认为压缩模式,无需再次切换
});