纯浏览器端Excel图片处理

7/31/2025 excelize-wasm

# Excel图片处理工具

# 📋 项目简介

这是一个基于Web的Excel图片处理工具,能够自动提取Excel文件中的图片,模拟上传到服务器,并在原位置设置超链接。该工具使用纯前端技术实现,无需服务器端支持。

# ✨ 主要功能

  • 📁 文件上传: 支持拖拽和点击上传Excel文件(.xlsx, .xls格式)
  • 🖼️ 图片提取: 自动识别并提取Excel中的图片
  • ☁️ 图片上传: 模拟图片上传到服务器(可替换为真实上传逻辑)
  • 🔗 超链接设置: 将图片替换为超链接文本
  • 📊 进度显示: 实时显示处理进度和状态
  • 💾 文件下载: 自动生成处理后的Excel文件供下载

# 🎯 技术特性

  • 纯前端实现: 使用HTML5 + CSS3 + JavaScript
  • Excel处理: 基于excelize-wasm库进行Excel文件操作
  • 响应式设计: 现代化的UI界面,支持各种设备
  • 拖拽上传: 支持文件拖拽上传功能
  • 实时反馈: 详细的处理状态和进度显示

# 🚀 使用方法

  1. 打开工具: 在浏览器中打开index.html文件
  2. 上传文件: 点击上传区域或拖拽Excel文件到指定区域
  3. 等待处理: 系统会自动处理文件,显示实时进度
  4. 下载结果: 处理完成后点击下载按钮获取结果文件

# 📁 项目结构

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, 渐变效果

# 📝 使用说明

  1. 环境要求: 现代浏览器(支持ES6+)
  2. 文件格式: 支持.xlsx和.xls格式的Excel文件
  3. 图片处理: 自动识别Sheet1中的图片
  4. 输出格式: 处理后的文件为.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;

# 🚨 注意事项

  1. 文件大小: 大文件处理可能需要较长时间
  2. 浏览器兼容性: 需要支持WebAssembly的现代浏览器
  3. 网络连接: 上传功能需要网络连接(当前为模拟)
  4. 内存使用: 大文件可能占用较多内存