zip-html-preview
项目介绍
这是一个基于 Spring Boot 开发的在线 ZIP 文件预览工具,主要用于预览 ZIP 压缩包中的 HTML 文件及其相关资源。
主要功能
- 支持拖拽上传或点击选择多个 ZIP 文件
- 自动解压并提取 ZIP 文件中的 HTML 文件
- 在线预览 HTML 文件及其相关的 CSS、JavaScript 和图片等资源
- 支持多文件批量处理
技术栈
- 后端:Spring Boot 2.3.4
- 前端:HTML5 + JavaScript
- 构建工具:Maven
快速开始
环境要求
- JDK 8+
- Maven 3.x
配置说明
在 application.properties
中配置解压文件存储路径:
zip.extract.path=D:/unzip
运行步骤
- 克隆项目
git clone https://gitee.com/anxwefndu/zip-html-preview.git
- 进入项目目录
cd zip-html-preview/SpringBoot
- 编译打包
mvn clean package
- 运行项目
java -jar target/SpringBoot-1.0-SNAPSHOT-execute.jar
- 访问应用
打开浏览器访问:http://localhost:8080
使用说明
- 打开网页后,可以通过拖拽或点击选择按钮上传 ZIP 文件
- 支持同时选择多个 ZIP 文件
- 上传完成后会自动显示可预览的 HTML 文件列表
- 点击文件名即可在新标签页中预览对应的 HTML 文件
源码下载
演示截图
1.系统首页
2.预览压缩包
3.预览页面1
4.预览页面2
核心源码
SpringBoot/src/main/resources/static/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>zip-html-preview</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5',
secondary: '#6366F1'
},
borderRadius: {
'none': '0px',
'sm': '2px',
DEFAULT: '4px',
'md': '8px',
'lg': '12px',
'xl': '16px',
'2xl': '20px',
'3xl': '24px',
'full': '9999px',
'button': '4px'
}
}
}
}
</script>
<style>
body::-webkit-scrollbar {
width: 0.5rem;
}
body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 0.5rem;
}
body::-webkit-scrollbar-thumb {
background: #3498db;
border-radius: 0.5rem;
}
body::-webkit-scrollbar-thumb:hover {
background: #2980b9;
}
.drag-area {
border: 2px dashed #E5E7EB;
transition: all 0.3s ease;
}
.drag-area:hover {
border-color: #4F46E5;
}
.file-input {
display: none;
}
.button-hover {
transition: transform 0.2s ease;
}
.button-hover:active {
transform: translateY(2px);
}
</style>
</head>
<body class="bg-gray-50">
<div class="w-[1200px] mx-auto">
<nav class="h-16 bg-white shadow-sm flex items-center justify-between px-8">
<div class="flex items-center space-x-2">
<span class="text-2xl font-['Pacifico'] text-primary">logo</span>
<span class="text-lg font-medium">zip-html-preview</span>
</div>
<button class="w-10 h-10 rounded-button flex items-center justify-center hover:bg-gray-100 transition-colors">
<i class="fas fa-sun text-gray-600"></i>
</button>
</nav>
<main class="px-8 py-12">
<div class="max-w-3xl mx-auto">
<div class="bg-white rounded-lg shadow-sm p-8">
<div id="dropArea" class="drag-area rounded-lg p-12 text-center">
<input type="file" id="fileInput" class="file-input" multiple accept=".zip">
<div class="space-y-4">
<div class="w-20 h-20 mx-auto bg-gray-50 rounded-full flex items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400"></i>
</div>
<div>
<h3 class="text-lg font-medium">拖拽文件到这里,或</h3>
<button class="mt-2 px-6 py-2 bg-primary text-white rounded-button hover:bg-primary/90 button-hover whitespace-nowrap">
点击选择文件
</button>
</div>
<p class="text-sm text-gray-500">
支持的文件格式:ZIP
</p>
</div>
</div>
<div id="fileList" class="hidden mt-8 space-y-4">
<div class="text-lg font-medium mb-4">已选择的文件</div>
<div class="space-y-3"></div>
<div class="flex justify-end space-x-3 mt-6">
<button id="clearButton" class="px-6 py-2 text-gray-600 bg-gray-100 rounded-button hover:bg-gray-200 button-hover whitespace-nowrap">
清空列表
</button>
<button id="previewButton" class="px-6 py-2 text-white bg-primary rounded-button hover:bg-primary/90 button-hover whitespace-nowrap">
预览
</button>
</div>
</div>
<div id="previewLinks" class="hidden mt-8 space-y-4">
<div class="text-lg font-medium mb-4">可预览的文件</div>
<ul class="space-y-2" id="htmlFilesList">
</ul>
</div>
</div>
<div class="mt-12 bg-white rounded-lg shadow-sm p-8">
<h2 class="text-xl font-medium mb-6">使用说明</h2>
<div class="space-y-4 text-gray-600">
<div class="flex items-start space-x-3">
<div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-check text-sm text-primary"></i>
</div>
<p>支持拖拽上传或点击选择文件,可以一次选择多个ZIP文件</p>
</div>
<div class="flex items-start space-x-3">
<div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-check text-sm text-primary"></i>
</div>
<p>系统会自动解析ZIP文件中的HTML文件,并提供在线预览功能</p>
</div>
<div class="flex items-start space-x-3">
<div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-check text-sm text-primary"></i>
</div>
<p>支持预览ZIP包中的HTML文件及其相关的CSS、JavaScript和图片等资源</p>
</div>
<div class="flex items-start space-x-3">
<div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-check text-sm text-primary"></i>
</div>
<p>提供文件树视图,方便浏览ZIP包中的文件结构和快速切换不同的HTML文件</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const previewButton = document.getElementById('previewButton');
const clearButton = document.getElementById('clearButton');
const previewLinks = document.getElementById('previewLinks');
const htmlFilesList = document.getElementById('htmlFilesList');
// 存储所有文件的数组
let selectedFiles = [];
// 点击拖拽区域触发文件选择
dropArea.addEventListener('click', () => {
fileInput.click();
});
// 拖拽进入时改变样式
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('border-primary');
});
// 拖拽离开时恢复样式
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('border-primary');
});
// 拖拽放下时处理文件
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('border-primary');
const files = e.dataTransfer.files;
handleFiles(files);
});
// 文件输入框变化时处理文件
fileInput.addEventListener('change', () => {
const files = fileInput.files;
handleFiles(files);
fileInput.value = null;
});
// 处理文件并更新文件列表
function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!selectedFiles.some(f => f.name === file.name)) {
selectedFiles.push(file);
}
}
updateFileList();
}
// 更新文件列表 DOM
function updateFileList() {
if (selectedFiles.length > 0) {
fileList.classList.remove('hidden');
} else {
fileList.classList.add('hidden');
}
const listContainer = fileList.querySelector('.space-y-3');
listContainer.innerHTML = ''; // 清空旧的文件列表
selectedFiles.forEach((file, index) => {
const fileType = file.type || 'text/plain';
const fileSize = formatFileSize(file.size);
const listItem = document.createElement('div');
listItem.className = 'flex items-center justify-between p-4 bg-gray-50 rounded-lg';
// 左侧:文件图标和信息
const leftSide = document.createElement('div');
leftSide.className = 'flex items-center space-x-3';
const icon = document.createElement('i');
icon.className = getIconClass(fileType);
icon.style.color = '#4F46E5'; // 设置颜色
const fileInfo = document.createElement('div');
fileInfo.innerHTML = `
<div class="font-medium">${escapeHTML(file.name)}</div>
<div class="text-sm text-gray-500">${fileSize}</div>
`;
leftSide.appendChild(icon);
leftSide.appendChild(fileInfo);
// 右侧:操作按钮
const rightSide = document.createElement('div');
rightSide.className = 'flex items-center space-x-2';
// 删除按钮
const deleteButton = document.createElement('button');
deleteButton.className = 'p-2 text-gray-400 hover:text-gray-600 rounded-button button-hover';
deleteButton.innerHTML = '<i class="fas fa-times"></i>';
deleteButton.addEventListener('click', () => removeFile(index));
rightSide.appendChild(deleteButton);
listItem.appendChild(leftSide);
listItem.appendChild(rightSide);
listContainer.appendChild(listItem);
});
}
// 格式化文件大小
function formatFileSize(size) {
const units = ['B', 'KB', 'MB', 'GB'];
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
// 获取文件类型的图标类名
function getIconClass(type) {
if (type.startsWith('image/')) {
return 'fas fa-file-image text-primary w-8 h-8 flex items-center justify-center';
} else if (type === 'text/plain' || type.endsWith('.md')) {
return 'fas fa-file-alt text-primary w-8 h-8 flex items-center justify-center';
} else {
return 'fas fa-file text-primary w-8 h-8 flex items-center justify-center';
}
}
// 移除指定索引的文件
function removeFile(index) {
selectedFiles.splice(index, 1);
updateFileList();
}
// 清空所有文件
document.querySelector('#fileList .px-6.py-2.text-gray-600').addEventListener('click', () => {
selectedFiles = [];
updateFileList();
});
// 转义 HTML 特殊字符
function escapeHTML(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
// 清空所有文件
clearButton.addEventListener('click', () => {
selectedFiles = [];
updateFileList();
previewLinks.classList.add('hidden');
htmlFilesList.innerHTML = '';
});
// 预览按钮点击事件
previewButton.addEventListener('click', () => {
if (selectedFiles.length === 0) return;
const formData = new FormData();
selectedFiles.forEach(file => formData.append('file', file));
fetch('/api/upload', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
previewLinks.classList.remove('hidden');
htmlFilesList.innerHTML = '';
data.forEach(htmlFile => {
const listItem = document.createElement('li');
listItem.className = 'flex items-center justify-between p-4 bg-gray-50 rounded-lg';
const fileName = document.createElement('a');
fileName.href = htmlFile.filePath;
fileName.target = '_blank';
fileName.className = 'text-blue-500 hover:underline';
fileName.textContent = htmlFile.fileName;
listItem.appendChild(fileName);
htmlFilesList.appendChild(listItem);
});
}).catch(error => {
alert('上传失败,请稍后再试。');
console.error('Error:', error);
});
});
</script>
</body>
</html>
SpringBoot/src/main/java/com/boot/service/ZipService.java
package com.boot.service;
import com.boot.config.ZipConfig;
import com.boot.model.HtmlFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Service
public class ZipService {
@Autowired
private ZipConfig zipConfig;
public List<HtmlFile> processZipFile(MultipartFile file) throws IOException {
String extractDir = zipConfig.getExtract().getPath();
File dir = new File(extractDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 解压文件
try (ZipInputStream zis = new ZipInputStream(file.getInputStream(), Charset.forName("GBK"))) {
ZipEntry zipEntry;
byte[] buffer = new byte[1024];
while ((zipEntry = zis.getNextEntry()) != null) {
File newFile = new File(extractDir + File.separator + zipEntry.getName());
if (zipEntry.isDirectory()) {
newFile.mkdirs();
continue;
}
// 创建父目录
new File(newFile.getParent()).mkdirs();
// 写入文件
try (FileOutputStream fos = new FileOutputStream(newFile)) {
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
}
}
// 查找第一层的HTML文件
List<HtmlFile> htmlFiles = new ArrayList<>();
// 提取文件夹名称(去掉扩展名)
String folderName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf('.'));
extractDir = extractDir + File.separator + folderName;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(extractDir))) {
for (Path path : stream) {
if (Files.isRegularFile(path) && path.toString().toLowerCase().endsWith(".html")) {
htmlFiles.add(new HtmlFile(
path.getFileName().toString(), "/preview" + path.toString().substring(zipConfig.getExtract().getPath().length())
));
}
}
}
return htmlFiles;
}
}