# Excel图片处理工具
# 📋 项目简介
这是一个基于Web的Excel图片处理工具,能够自动提取Excel文件中的图片,模拟上传到服务器,并在原位置设置超链接。该工具使用纯前端技术实现,无需服务器端支持。
# ✨ 主要功能
- 📁 文件上传: 支持拖拽和点击上传Excel文件(.xlsx, .xls格式)
- 🖼️ 图片提取: 自动识别并提取Excel中的图片
- ☁️ 图片上传: 模拟图片上传到服务器(可替换为真实上传逻辑)
- 🔗 超链接设置: 将图片替换为超链接文本
- 📊 进度显示: 实时显示处理进度和状态
- 💾 文件下载: 自动生成处理后的Excel文件供下载
# 🎯 技术特性
- 纯前端实现: 使用HTML5 + CSS3 + JavaScript
- Excel处理: 基于excelize-wasm库进行Excel文件操作
- 响应式设计: 现代化的UI界面,支持各种设备
- 拖拽上传: 支持文件拖拽上传功能
- 实时反馈: 详细的处理状态和进度显示
# 🚀 使用方法
- 打开工具: 在浏览器中打开index.html文件
- 上传文件: 点击上传区域或拖拽Excel文件到指定区域
- 等待处理: 系统会自动处理文件,显示实时进度
- 下载结果: 处理完成后点击下载按钮获取结果文件
# 📁 项目结构
publish-moments/
├── index.html # 主页面文件
├── README.md # 项目说明文档
└── package.json # 项目配置文件
# 🔧 核心代码
# HTML结构
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel图片处理工具</title>
<script src="https://cdn.jsdelivr.net/npm/excelize-wasm@0.0.9/index.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 800px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #666;
font-size: 1.1em;
}
.upload-area {
border: 3px dashed #ddd;
border-radius: 15px;
padding: 40px;
text-align: center;
margin-bottom: 30px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.upload-area:hover {
border-color: #667eea;
background-color: #f8f9ff;
}
.upload-area.dragover {
border-color: #667eea;
background-color: #f0f4ff;
}
.upload-icon {
font-size: 3em;
color: #667eea;
margin-bottom: 15px;
}
.upload-text {
font-size: 1.2em;
color: #333;
margin-bottom: 10px;
}
.upload-hint {
color: #999;
font-size: 0.9em;
}
#file-input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.progress-container {
display: none;
margin: 20px 0;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
color: #666;
font-size: 0.9em;
}
.status-container {
margin: 20px 0;
padding: 20px;
border-radius: 10px;
background-color: #f8f9fa;
display: none;
}
.status-item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.status-icon {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: white;
}
.status-icon.success {
background-color: #28a745;
}
.status-icon.processing {
background-color: #ffc107;
animation: pulse 1.5s infinite;
}
.status-icon.error {
background-color: #dc3545;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.status-text {
flex: 1;
color: #333;
}
.download-btn {
display: none;
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.download-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.error-message {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
display: none;
}
.success-message {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Excel图片处理工具</h1>
<p>上传Excel文件,自动处理图片并设置超链接</p>
</div>
<div class="upload-area" id="upload-area">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽Excel文件到此处</div>
<div class="upload-hint">支持 .xlsx, .xls 格式</div>
<input type="file" id="file-input" accept=".xlsx,.xls" />
</div>
<div class="progress-container" id="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">准备处理...</div>
</div>
<div class="status-container" id="status-container">
<div class="status-item">
<div class="status-icon processing" id="status-loading">⏳</div>
<div class="status-text" id="status-loading-text">正在加载Excel文件...</div>
</div>
<div class="status-item">
<div class="status-icon" id="status-pictures">⏳</div>
<div class="status-text" id="status-pictures-text">正在获取图片信息...</div>
</div>
<div class="status-item">
<div class="status-icon" id="status-upload">⏳</div>
<div class="status-text" id="status-upload-text">正在上传图片...</div>
</div>
<div class="status-item">
<div class="status-icon" id="status-links">⏳</div>
<div class="status-text" id="status-links-text">正在设置超链接...</div>
</div>
<div class="status-item">
<div class="status-icon" id="status-save">⏳</div>
<div class="status-text" id="status-save-text">正在保存文件...</div>
</div>
</div>
<div class="error-message" id="error-message"></div>
<div class="success-message" id="success-message"></div>
<button class="download-btn" id="download-btn" disabled>
📥 下载处理后的Excel文件
</button>
</div>
<script type="module">
let excelize = null;
let processedFile = null;
// 初始化excelize
async function initExcelize() {
try {
excelize = await excelizeWASM.init(
"https://cdn.jsdelivr.net/npm/excelize-wasm@0.0.9/excelize.wasm.gz"
);
console.log("Excelize initialized successfully");
} catch (error) {
console.error("Failed to initialize Excelize:", error);
showError("初始化Excelize失败,请刷新页面重试");
}
}
// 显示错误信息
function showError(message) {
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.style.display = 'block';
document.getElementById('success-message').style.display = 'none';
}
// 显示成功信息
function showSuccess(message) {
const successElement = document.getElementById('success-message');
successElement.textContent = message;
successElement.style.display = 'block';
document.getElementById('error-message').style.display = 'none';
}
// 更新进度条
function updateProgress(percentage, text) {
document.getElementById('progress-fill').style.width = percentage + '%';
document.getElementById('progress-text').textContent = text;
}
// 更新状态
function updateStatus(statusId, type, text) {
const statusElement = document.getElementById(statusId).parentElement;
const iconElement = statusElement.querySelector('.status-icon');
const textElement = statusElement.querySelector('.status-text');
iconElement.className = `status-icon ${type}`;
textElement.textContent = text;
if (type === 'success') {
iconElement.textContent = '✓';
} else if (type === 'error') {
iconElement.textContent = '✗';
} else if (type === 'processing') {
iconElement.textContent = '⏳';
}
}
// 模拟异步上传
async function simulateUpload(fileData, extension) {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟上传成功,返回一个模拟的URL
const mockUrl = `https://example.com/uploads/${Date.now()}${extension}`;
resolve(mockUrl);
}, 1000);
});
}
// 处理Excel文件
async function processExcelFile(file) {
try {
// 显示进度容器
document.getElementById('progress-container').style.display = 'block';
document.getElementById('status-container').style.display = 'block';
document.getElementById('download-btn').style.display = 'none';
updateProgress(10, '正在读取文件...');
updateStatus('status-loading', 'processing', '正在加载Excel文件...');
// 读取文件
const arrayBuffer = await file.arrayBuffer();
const byteArray = new Uint8Array(arrayBuffer);
updateProgress(20, '正在解析Excel...');
updateStatus('status-loading', 'success', 'Excel文件加载成功');
// 打开Excel文件
const f = excelize.OpenReader(byteArray);
console.log('f', f);
updateProgress(30, '正在获取图片信息...');
updateStatus('status-pictures', 'processing', '正在获取图片信息...');
// 获取所有包含图片的单元格
const pictureCells = f.GetPictureCells("Sheet1")?.cells;
console.log('Picture cells:', pictureCells);
if (!pictureCells || pictureCells.length === 0) {
updateStatus('status-pictures', 'success', '未发现图片,跳过图片处理');
updateStatus('status-upload', 'success', '无需上传图片');
updateStatus('status-links', 'success', '无需设置超链接');
updateProgress(90, '正在保存文件...');
updateStatus('status-save', 'processing', '正在保存文件...');
// 直接保存文件
const output = f.WriteToBuffer();
processedFile = new Blob([output], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
updateProgress(100, '处理完成!');
updateStatus('status-save', 'success', '文件保存成功');
document.getElementById('download-btn').disabled = false;
showSuccess('文件处理完成!未发现图片,已保存原文件。');
return;
}
updateStatus('status-pictures', 'success', `发现 ${pictureCells.length} 个图片单元格`);
updateProgress(40, '正在处理图片...');
updateStatus('status-upload', 'processing', '正在上传图片...');
// 处理每个图片
const uploadPromises = [];
for (let i = 0; i < pictureCells.length; i++) {
const cell = pictureCells[i];
const pictures = f.GetPictures("Sheet1", cell);
if (pictures && pictures.pictures && pictures.pictures.length > 0) {
for (const picture of pictures.pictures) {
const uploadPromise = simulateUpload(picture.File, picture.Extension)
.then(url => ({ cell, url }));
uploadPromises.push(uploadPromise);
}
}
}
console.log('uploadPromises', uploadPromises);
// 等待所有上传完成
const uploadResults = await Promise.all(uploadPromises);
updateStatus('status-upload', 'success', `成功上传 ${uploadResults.length} 个图片`);
updateProgress(60, '正在设置超链接...');
updateStatus('status-links', 'processing', '正在设置超链接...');
console.log('uploadResults', uploadResults);
// 按单元格分组,处理同一个单元格的多个图片
const cellGroups = {};
for (const result of uploadResults) {
if (!cellGroups[result.cell]) {
cellGroups[result.cell] = [];
}
cellGroups[result.cell].push(result.url);
}
const style = f.NewStyle({
Alignment: {
WrapText: true,
}
});
// 设置超链接
for (const [cell, urls] of Object.entries(cellGroups)) {
// 删除图片
f.DeletePicture("Sheet1", cell);
f.SetCellStyle("Sheet1", cell, cell, style?.style);
// 设置单元格值 - 多个URL用换行符拼接
const cellText = urls.join('\n\n');
f.SetCellValue("Sheet1", cell, cellText);
// 设置超链接
// f.SetCellHyperLink("Sheet1", cell, urls[0], "External", {
// Display: cellText,
// Tooltip: cellText,
// });
}
updateStatus('status-links', 'success', `成功设置 ${uploadResults.length} 个超链接`);
updateProgress(80, '正在保存文件...');
updateStatus('status-save', 'processing', '正在保存文件...');
// 保存文件
const output = f.WriteToBuffer();
console.log('output', output);
if (output.buffer) {
processedFile = new Blob([output.buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
}
updateProgress(100, '处理完成!');
updateStatus('status-save', 'success', '文件保存成功');
document.getElementById('download-btn').disabled = false;
document.getElementById('download-btn').style.display = 'block';
showSuccess(`处理完成!共处理 ${uploadResults.length} 个图片,已设置超链接。`);
} catch (error) {
console.error('处理Excel文件时出错:', error);
showError(`处理文件时出错: ${error.message}`);
updateProgress(0, '处理失败');
}
}
// 下载处理后的文件
function downloadProcessedFile() {
if (!processedFile) {
showError('没有可下载的文件');
return;
}
const url = URL.createObjectURL(processedFile);
const link = document.createElement('a');
link.href = url;
link.download = `processed_${new Date().getTime()}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// 事件监听器
document.addEventListener('DOMContentLoaded', async () => {
await initExcelize();
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const downloadBtn = document.getElementById('download-btn');
// 文件选择事件
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
processExcelFile(file);
}
});
// 拖拽事件
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) {
processExcelFile(file);
} else {
showError('请选择有效的Excel文件 (.xlsx 或 .xls)');
}
});
// 点击上传区域
uploadArea.addEventListener('click', () => {
fileInput.click();
});
// 下载按钮
downloadBtn.addEventListener('click', downloadProcessedFile);
});
</script>
</body>
</html>
# 🔍 核心功能解析
# 1. Excel文件处理
- 使用excelize-wasm库读取和操作Excel文件
- 支持.xlsx和.xls格式
- 自动识别包含图片的单元格
# 2. 图片提取与上传
- 从Excel中提取图片数据
- 模拟异步上传过程(可替换为真实上传逻辑)
- 支持批量处理多个图片
# 3. 超链接设置
- 删除原图片
- 设置单元格样式(自动换行)
- 将图片URL设置为单元格内容
# 4. 用户界面
- 现代化的渐变背景设计
- 拖拽上传功能
- 实时进度条和状态显示
- 响应式布局
# 🛠️ 技术栈
- 前端框架: 原生HTML5 + CSS3 + JavaScript
- Excel处理: excelize-wasm
- 文件操作: File API, Blob API
- UI设计: CSS Grid, Flexbox, 渐变效果
# 📝 使用说明
- 环境要求: 现代浏览器(支持ES6+)
- 文件格式: 支持.xlsx和.xls格式的Excel文件
- 图片处理: 自动识别Sheet1中的图片
- 输出格式: 处理后的文件为.xlsx格式
# 🔧 自定义配置
# 修改上传逻辑
将simulateUpload
函数替换为真实的上传API调用:
async function realUpload(fileData, extension) {
const formData = new FormData();
formData.append('file', new Blob([fileData]));
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
return result.url;
}
# 修改工作表名称
将代码中的"Sheet1"替换为实际的工作表名称:
const pictureCells = f.GetPictureCells("YourSheetName")?.cells;
# 🚨 注意事项
- 文件大小: 大文件处理可能需要较长时间
- 浏览器兼容性: 需要支持WebAssembly的现代浏览器
- 网络连接: 上传功能需要网络连接(当前为模拟)
- 内存使用: 大文件可能占用较多内存