img,word,excel,pdf文件在线预览

最近在前端实现了附件预览。原本已经实现了文件的下载功能,但是业务表示需要直接在线预览,后端没有时间,只好由前端实现了。总体方案,使用fetch请求后端服务地址,将响应内容转换为blob流,然后前端使用相关插件,解析blob流内容,将附件内容以html的方式渲染在浏览器中。

1.图片预览

  • 核心代码
js 复制代码
   
   //将响应内容转成blob

  response.blob()

 
 //创建对象地址
const imgUrl = URL.createObjectURL(blob);

const img = document.createElement('img');

img.src = imgUrl;

img.onload = () => {

  URL.revokeObjectURL(imgUrl);

};

img.onerror = () => {

  console.error('图片加载失败');

};

document.body.appendChild(img);

2.word预览

利用了docx-preview插件和jszip插件,但是只能实现docx后缀文档的预览,需要后端将doc转成docx 下面是DeepSeek给的方案,最终选择了docx-preview

  • 核心代码
js 复制代码
 //将响应内容转成blob

 response.blob()


  
 // 渲染 Word 文档到预览容器

docx.renderAsync(blob, document.getElementById('word-preview'))

    .then(() => {

        console.log('Word document rendered successfully!');

  });
  • 后端解决示例

blog.csdn.net/weixin_4555...

blog.csdn.net/m0_74825678...

3.excel预览

excel预览相对比较复杂了,借助xlsx进行表格数据的解析和handsontable进行表格样式的渲染。涉及到数据量大,多sheet的情况,比如几千条数据,在前端解析并渲染,响应非常慢,很卡顿,于是做了分页,每页100条.

  • 核心代码
js 复制代码
 // 第一步:使用 fetch 获取文件流,并使用XLSX解析文件流数据
    async function previewExcel() {
      try {
        const response = await fetch(excelUrl, {
          responseType: 'arraybuffer', // 关键:指定响应类型
        });
        const arrayBuffer = await response.arrayBuffer();
        
        // 解析Excel
        const workbook = XLSX.read(arrayBuffer, { type: 'array',cellStyles: true });
         // 遍历所有 Sheet 页
        workbook.SheetNames.forEach((sheetName) => {
          const worksheet = workbook.Sheets[sheetName];
          const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 'A', defval: '' ,blankrows:true,cellStyles: true  });
          const mergeCells = getMergeCells(worksheet); // 获取合并单元格信息
          const cellStyles = getCellStyles(worksheet); // 获取单元格样式信息
          sheetsData[sheetName] = {
            data: jsonData,
            mergeCells,
            cellStyles,
          };
        });

        // 默认渲染第一个 Sheet 页
        currentSheetName = workbook.SheetNames[0];
        const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
        renderTable(data, mergeCells, cellStyles);

        // 渲染 Sheet 页切换菜单
        renderSheetTabs(workbook.SheetNames);

      } catch (error) {
        console.error('预览失败:', error);
      }
    }
    
    //获取合并单元格的数据
    function getMergeCells(worksheet) {
      const merges = worksheet['!merges'] || [];
      return merges.map(merge => ({
        row: merge.s.r,
        col: merge.s.c,
        rowspan: merge.e.r - merge.s.r + 1,
        colspan: merge.e.c - merge.s.c + 1,
      }));
    }

    function convertHexToCssHex(hex) {
        // 检查输入是否为有效的十六进制颜色值(6 位字符)
        if (/^[0-9A-Fa-f]{6}$/.test(hex)) {
            return "#" + hex;
        } else {
            return hex; 
        }
    }
    
    //获取单元格的样式
    function getCellStyles(worksheet) {
      const styles = [];
      //  防止空sheet页整个excel都不渲染
      if (!worksheet) {
        return styles; 
      }
      if (!worksheet['!ref']) {
        return styles;
      }
      const range = XLSX.utils.decode_range(worksheet['!ref']); // 获取工作表范围
      for (let row = range.s.r; row <= range.e.r; row++) {
        for (let col = range.s.c; col <= range.e.c; col++) {
          const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
          const cell = worksheet[cellAddress];
          if (cell && cell.s) {
            // XLSX 返回的颜色格式为"FFFFFF",需要在前面加"#"才能正常渲染颜色
            // XLSX未能解析出字体样式
            const fgColor = cell.s.fgColor?.rgb  // "FFFF00"
            const background = convertHexToCssHex(fgColor) ?? 'white'
            styles.push({
              row,
              col,
              style: {
                background,
                color: cell.s.font?.color?.rgb || 'black', // 字体颜色
                fontWeight: cell.s.font?.bold ? 'bold' : 'normal', // 字体加粗
              },
            });
          }
        }
      }
      return styles;
    }

      //  自定义样式渲染器(通用)
      const styleRenderer = (hotInstance, TD, row, col, prop, value, cellProperties) => {
        // 调用默认文本渲染器
        Handsontable.renderers.TextRenderer(hotInstance, TD, row, col, prop, value, cellProperties);
        
        // 获取当前单元格的样式配置
        const { styles } = hotInstance.getSettings();
        const cell = styles.find(s => s.row === row && s.col === col);

        // 应用样式
        if (cell) {
          TD.style.background = cell.style.background || '';
          TD.style.color = cell.style.color || '';
          TD.style.fontWeight = cell.style.fontWeight || '';
        }
      };

      // 注册渲染器
      Handsontable.renderers.registerRenderer('styleRenderer', styleRenderer);

    // 渲染表格到页面
    let hot;
    function renderTable(data,mergeCells, cellStyles) {
        const container = document.getElementById('excel-preview');
        if (!container) {
          console.error('容器元素未找到!');
          return;
        }
        container.innerHTML = ''; // 清空容器
          // 分页数据
        const start = (currentPage - 1) * pageSize;
        const end = start + pageSize;
        const pageData = data.slice(start, end);

          // 生成动态 cell 配置
        const cellConfig = cellStyles.map(style => ({
          row: style.row,
          col: style.col,
          renderer: 'styleRenderer', // 使用注册的自定义渲染器
        }));
        hot = new Handsontable(container, {
          data: pageData, // 表格数据
          rowHeaders: true, // 显示行号
          colHeaders: true, // 显示列号
          mergeCells: mergeCells, // 设置合并单元格
          styles:cellStyles,
          cell: cellConfig, // 动态样式配置
          contextMenu: true, // 启用右键菜单
          manualRowResize: true, // 允许调整行高
          manualColumnResize: true, // 允许调整列宽
          stretchH: 'all', // 拉伸列宽以适应容器
          height: 'auto', // 自动高度
          licenseKey: 'non-commercial-and-evaluation', // 免费版 License
        });

         // 渲染分页导航
        renderPagination(data.length);
    }

    function renderPagination(totalRows) {
      const paginationContainer = document.getElementById('pagination');
      paginationContainer.innerHTML = '';

      const totalPages = Math.ceil(totalRows / pageSize);

      // 上一页按钮
      const prevButton = document.createElement('button');
      prevButton.textContent = '上一页';
      prevButton.disabled = currentPage === 1;
      prevButton.addEventListener('click', () => {
        currentPage--;
        const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
        renderTable(data, mergeCells, cellStyles);
      });
      paginationContainer.appendChild(prevButton);
      // 页码显示
      const pageInfo = document.createElement('span');
      pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
      paginationContainer.appendChild(pageInfo);

      // 下一页按钮
      const nextButton = document.createElement('button');
      nextButton.textContent = '下一页';
      nextButton.disabled = currentPage === totalPages;
      nextButton.addEventListener('click', () => {
        currentPage++;
        const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
        renderTable(data, mergeCells, cellStyles);
      });
      paginationContainer.appendChild(nextButton);
    }
   
    function renderSheetTabs(sheetNames) {
      const tabsContainer = document.getElementById('sheet-tabs');
      tabsContainer.innerHTML = ''; // 清空旧内容

      sheetNames.forEach((sheetName) => {
        const tab = document.createElement('button');
        tab.textContent = sheetName;
        tab.className = sheetName === currentSheetName ? 'active' : ''; // 高亮当前 Sheet 页
        tab.addEventListener('click', () => {
          currentSheetName = sheetName;
          currentPage = 1; // 切换 Sheet 页时重置页码
          
          const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
          renderTable(data, mergeCells, cellStyles);
          updateActiveTab(tab); // 更新高亮状态
        });
        tabsContainer.appendChild(tab);
      });
    }

    function updateActiveTab(activeTab) {
      const tabs = document.querySelectorAll('#sheet-tabs button');
      tabs.forEach((tab) => tab.classList.remove('active'));
      activeTab.classList.add('active');
    }

    document.addEventListener('DOMContentLoaded', function () {
       previewExcel()
    });

以上实现了多sheet,分页,合并单元格,单元格背景色,非标准的表头和表体类表格的渲染,借助了AI和官方文档实现了对应功能,供参考!

  • 参考文档
  1. 15.1.0 handsontable
  2. sheetjs

4.pdf预览

pdf预览是后端实现的,直接打开一个链接就是pdf形式,借助的是pdfjs。chrome浏览器其实也可以直接打开pdf文件,其内置的也是pdfjs

相关推荐
lichenyang4532 小时前
从 H5 到 HarmonyOS:我做了一个 npm + OHPM 双端发布的 ArkWeb JSBridge 框架
前端
用户84298142418102 小时前
AI时代,JS混淆加密还有用吗?
前端·javascript
xiaobaoyu2 小时前
如何理解前端项目中的绝对路径和相对路径
前端
xiaobaoyu2 小时前
vue3+vite+vant4项目实战总结
前端
咖啡星人k2 小时前
【无标题】
前端·ai·github
颜进强2 小时前
LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象
前端·后端·ai编程
颜进强2 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程
朕的剑还未配妥3 小时前
CSS 实现渐变毛玻璃:从 backdrop-filter 到多层 mask
前端·css
程序员黑豆3 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos