如何简单的建立一个浏览Excel和PDF文件的Web服务

通过Luckysheet ,可以部署一个静态的查看Excel文件的web服务,利用pdf.js库构建一个PDF浏览服务。

(注:Excel文件中不能包含数据连接,否则不能加载)

步骤如下:

一、 部署一个Nginx 静态web服务器

我这里选用Debian+宝塔面板,利用宝塔面板部署一个Nginx服务,优点是简单,易于管理,资源消耗少。
Debian13+宝塔面板的安装,这里就不再叙述了。下面创建这个静态网站。

1、网站->添加站点

如何简单的建立一个浏览Excel和PDF文件的Web服务

域名,填写你的访问域名或IP地址,目录填写你的网站存放目录,其他保持默认。

2、修改配置文件

选中刚刚添加的站点->设置->配置文件,禁用缓存

    # 针对所有静态资源禁用缓存,确保实时性
    location / 
    {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires 0;
    }
如何简单的建立一个浏览Excel和PDF文件的Web服务


二、建立index.html文件

在网站目录下,创建index.html,内容如下:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>库存Excel看板</title>
    <link rel="stylesheet" href="./static/luckysheet.css" />

    <style>
      body, html {
        margin: 0; padding: 0;
        width: 100%; height: 100%;
        /* 关键修改:允许原生滚动,开启 iOS/安卓 顺滑滚动 */
        overflow: auto; 
        -webkit-overflow-scrolling: touch;
        background-color: #f5f5f5;
      }

      #luckysheet {
        width: 100%; height: 100%;
        position: absolute; left: 0; top: 0;
        /* 关键修改:告诉浏览器这里允许手指上下滑动 */
        touch-action: pan-y; 
      }

      #scroll-controls {
        position: fixed; right: 40px; bottom: 100px;
        display: flex; flex-direction: column; gap: 20px;
        z-index: 1000000 !important;
      }

      .scroll-btn {
        width: 70px; height: 70px; border-radius: 50%;
        background-color: rgba(0, 0, 0, 0.4); color: white;
        border: 2px solid rgba(255, 255, 255, 0.6);
        font-size: 24px; display: flex; align-items: center; justify-content: center;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        -webkit-tap-highlight-color: transparent;
        pointer-events: auto !important;
        user-select: none; -webkit-user-select: none;
      }

      .scroll-btn:active { background-color: rgba(0, 0, 0, 0.8); transform: scale(0.9); }
    </style>
  </head>
  <body>
    <div id="luckysheet"></div>

    <script src="./static/plugin.js"></script>
    <script src="./static/luckysheet.umd.js"></script>
    <script src="./static/luckyexcel.umd.js"></script>

    <div id="scroll-controls">
      <button class="scroll-btn" onmousedown="startScroll('up')" onmouseup="stopScroll()" onmouseleave="stopScroll()" ontouchstart="event.preventDefault(); startScroll('up')" ontouchend="stopScroll()">▲</button>
      <button class="scroll-btn" onmousedown="startScroll('down')" onmouseup="stopScroll()" onmouseleave="stopScroll()" ontouchstart="event.preventDefault(); startScroll('down')" ontouchend="stopScroll()">▼</button>
    </div>

    <script>
      function getFileNameFromUrl() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get("file") || "data.xlsx";
      }

      function loadExcel() {
        const fileName = getFileNameFromUrl();
        const filePath = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")) + "/excel_data/" + fileName + "?t=" + new Date().getTime();

        const request = new XMLHttpRequest();
        request.open("GET", filePath, true);
        request.responseType = "blob";
        request.onload = function () {
          if (request.status === 200) {
            LuckyExcel.transformExcelToLucky(request.response, function (exportJson) {
              if (!exportJson.sheets || exportJson.sheets.length === 0) return;
              if (window.luckysheet && luckysheet.destroy) luckysheet.destroy();

              luckysheet.create({
                container: "luckysheet",
                data: exportJson.sheets,
                showinfobar: false, showtoolbar: false,
                allowEdit: false, enableAddRow: false,
                sheetFormulaBar: false, showsheetbar: false, showstatisticBar: false,
                hook: {
                  workbookCreateAfter: function() {
                    // 初始化后清空选区,彻底断开焦点
                    setTimeout(() => {
                        if(luckysheet.exitEditMode) luckysheet.exitEditMode();
                        console.log("看板就绪");
                    }, 500);
                  }
                }
              });
            });
          }
        };
        request.send();
      }

      let scrollTimer = null;

      function scrollSheet(direction) {
        // 1. 物理脱离焦点
        document.activeElement.blur();

        // 2. 寻找真正的滚动容器(适配不同版本的 Luckysheet)
        // 我们不找类名了,直接找 #luckysheet 下面那个有垂直滚动条的 div
        const container = document.getElementById("luckysheet");
        if (!container) return;
        
        const divs = container.getElementsByTagName("div");
        let target = null;
        for (let i = 0; i < divs.length; i++) {
            // 垂直滚动条通常存在于 overflow-y 为 scroll 或 auto 的 div 上
            const style = window.getComputedStyle(divs[i]);
            if (style.overflowY === 'scroll' || divs[i].className.includes('scroll-y')) {
                target = divs[i];
                break;
            }
        }

        const step = direction === "up" ? -250 : 250;

        if (target) {
          target.scrollTop += step;
          // 触发原生滚动事件通知 Canvas 重绘
          target.dispatchEvent(new Event("scroll"));
        } else {
          // 最后的兜底:如果没有滚动层,尝试使用官方 API (如果可用)
          if(window.luckysheet && luckysheet.scroll) {
             luckysheet.scroll({scrollTop: (direction === 'up' ? 0 : 500)});
          }
        }
      }

      function startScroll(direction) {
        scrollSheet(direction);
        if (scrollTimer) clearInterval(scrollTimer);
        scrollTimer = setInterval(() => scrollSheet(direction), 150);
      }

      function stopScroll() {
        if (scrollTimer) { clearInterval(scrollTimer); scrollTimer = null; }
      }

      window.oncontextmenu = (e) => e.preventDefault();
      window.onload = loadExcel;
      setInterval(loadExcel, 60000);
    </script>
  </body>
</html>

把Luckysheet 4 个核心文件,通过浏览器右键另存下来,放在网站的static目录下

  • CSS https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/css/luckysheet.css
  • 核心JS https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/luckysheet.umd.js
  • 插件JS https://cdn.jsdelivr.net/npm/luckysheet@latest/dist/plugins/js/plugin.js
  • LuckyExcel (解析xlsx) https://cdn.jsdelivr.net/npm/luckyexcel@latest/dist/luckyexcel.umd.js

三、使用

把Excel文件放在网站目录下,通过URL访问,
http://x.x.x.x/dashboard/打开的是默认的data.xlsx,
http://x.x.x.x/dashboard/index.html?file=inventory.xlsx 访问的是指定的Excel文件

四、Excel文件更新

Excel文件的更新, 可以通过Syncthing ,从你本地电脑上同步过去。这样你自己电脑上改动了Excel, 网页上1分钟后也刷新了。

Syncthing部署参看

如果原始的Excel比较复杂,可以通过Excel的 power query来呈现一个适合看板的Excel文件

五、在平板、智能屏、电视上显示

在智能电视上显示,就是浏览器访问,不过最好配置一个鼠标, 在智能屏上,本身有触摸屏,比较方便。

在平板上,非常方便,浏览器就能访问。

六、在平板上创建快捷方式

方法一:使用浏览器内置功能(以 Chrome 为例)
打开 Chrome 浏览器,访问你想要保存的网页;
点击右上角的 三个点图标(菜单)
在下拉菜单中找到并点击 “添加到主屏幕” (Add to Home screen);
在弹出的对话框中,你可以自定义快捷方式的名称;
点击 “添加”
此时会弹出一个预览图标,你可以长按它手动拖放到桌面,或者直接点击 “自动添加”

方法二:通过桌面小组件(手动输入 URL)

在桌面空白处 长按
选择 “小组件” (Widgets);
找到 Chrome 或你使用的浏览器插件;
寻找名为 “书签”“Chrome 书签” 的小插件,将其拖到桌面;
系统会让你从书签列表中选择一个网页。如果你还没收藏该网页,请先在浏览器里将其设为书签。
七、PDF显示

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>PDF 看板 - 智能滑动翻页版</title>
    <script src="./static/pdf.min.js"></script>

    <style>
      body, html {
        margin: 0; padding: 0;
        width: 100%; height: 100%;
        overflow: hidden;
        background-color: #525659;
      }

      /* 核心修改:让容器可以被 JS 操控滚动 */
      #pdf-viewport-container {
        width: 100%;
        height: 100%;
        overflow: auto; 
        display: block; /* 改为块级,利于精准计算滚动 */
        -webkit-overflow-scrolling: touch;
        scroll-behavior: smooth; /* 让按钮触发的滑动变丝滑 */
      }

      canvas {
        display: block;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
        background-color: white;
        margin: 20px auto; /* 上下留点边距,滑到底部时更好看 */
      }

      #scroll-controls {
        position: fixed;
        right: 30px;
        bottom: 150px;
        display: flex;
        flex-direction: column;
        gap: 15px;
        z-index: 100000;
      }

      .scroll-btn {
        width: 70px;
        height: 70px;
        border-radius: 50%;
        background-color: rgba(0, 0, 0, 0.6);
        color: white;
        border: 2px solid rgba(255, 255, 255, 0.6);
        font-size: 26px;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
        -webkit-tap-highlight-color: transparent;
        user-select: none;
        -webkit-user-select: none;
      }

      .scroll-btn:active {
        background-color: rgba(0, 0, 0, 0.9);
        transform: scale(0.9);
      }

      #page-indicator {
        position: fixed;
        bottom: 30px;
        left: 50%;
        transform: translateX(-50%);
        background-color: rgba(0, 0, 0, 0.7);
        color: white;
        padding: 8px 16px;
        border-radius: 20px;
        font-family: sans-serif;
        font-size: 16px;
        pointer-events: none;
        z-index: 100000;
      }
    </style>
  </head>
  <body style="overscroll-behavior-y: contain">

    <div id="pdf-viewport-container">
      <canvas id="pdf-canvas"></canvas>
    </div>

    <div id="page-indicator">加载中...</div>

    <div id="scroll-controls">
      <button class="scroll-btn" onclick="zoom('in')" ontouchstart="event.preventDefault(); zoom('in')">+</button>
      <button class="scroll-btn" onclick="zoom('out')" ontouchstart="event.preventDefault(); zoom('out')">-</button>
      
      <!-- 这里改用新的智能控制函数 handleArrowClick -->
      <button class="scroll-btn" onclick="handleArrowClick('up')" ontouchstart="event.preventDefault(); handleArrowClick('up')">▲</button>
      <button class="scroll-btn" onclick="handleArrowClick('down')" ontouchstart="event.preventDefault(); handleArrowClick('down')">▼</button>
    </div>

    <script>
      pdfjsLib.GlobalWorkerOptions.workerSrc = './static/pdf.worker.min.js';

      let pdfDoc = null;
      let pageNum = 1;
      let pageRendering = false;
      let pageNumPending = null;
      let zoomScaleModifier = 1.0; 

      const container = document.getElementById('pdf-viewport-container');
      const canvas = document.getElementById('pdf-canvas');
      const ctx = canvas.getContext('2d');

      function getFileNameFromUrl() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get("file") || "default.pdf";
      }

      function renderPage(num, keepScrollPosition = false) {
        pageRendering = true;
        
        // 如果不需要保留滚动位置(比如正常切到了新的一页),先把滚动条拉回最顶部
        if (!keepScrollPosition) {
          container.scrollTop = 0;
        }

        pdfDoc.getPage(num).then(function(page) {
          const unscaledViewport = page.getViewport({ scale: 1 });
          const containerWidth = window.innerWidth;
          const containerHeight = window.innerHeight;

          const scaleX = containerWidth / unscaledViewport.width;
          const scaleY = containerHeight / unscaledViewport.height;
          const baseScale = Math.min(scaleX, scaleY) * 0.95;

          const finalScale = baseScale * zoomScaleModifier;
          const viewport = page.getViewport({ scale: finalScale });

          const outputScale = window.devicePixelRatio || 1;
          canvas.width = Math.floor(viewport.width * outputScale);
          canvas.height = Math.floor(viewport.height * outputScale);
          
          canvas.style.width = Math.floor(viewport.width) + "px";
          canvas.style.height = Math.floor(viewport.height) + "px";

          const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;

          const renderContext = {
            canvasContext: ctx,
            transform: transform,
            viewport: viewport
          };
          
          const renderTask = page.render(renderContext);

          renderTask.promise.then(function() {
            pageRendering = false;
            if (pageNumPending !== null) {
              renderPage(pageNumPending);
              pageNumPending = null;
            }
          });
        });

        const zoomPercent = Math.round(zoomScaleModifier * 100);
        document.getElementById('page-indicator').textContent = `第 ${num} 页 / 共 ${pdfDoc.numPages} 页 (${zoomPercent}%)`;
      }

      // 【核心升级】:处理 ▲ 和 ▼ 按钮的智能逻辑
      function handleArrowClick(direction) {
        if (!pdfDoc || pageRendering) return;

        // 每次点击按钮,画面滚动的像素距离(比如半个屏幕高)
        const scrollAmount = window.innerHeight * 0.4; 
        
        // 计算当前滚动的临界点(容留 5 像素的误差)
        const currentScroll = container.scrollTop;
        const maxScroll = container.scrollHeight - container.clientHeight;

        if (direction === 'down') {
          // 情况 1:如果页面放大了,并且还没滚到当前页的最底部
          if (currentScroll < maxScroll - 5) {
            container.scrollBy({ top: scrollAmount, behavior: 'smooth' });
          } else {
            // 情况 2:已经到这页底部了,点击触发翻下一页
            changePage('next');
          }
        } else {
          // 情况 1:如果还没滚到当前页的最顶部
          if (currentScroll > 5) {
            container.scrollBy({ top: -scrollAmount, behavior: 'smooth' });
          } else {
            // 情况 2:已经在这页最顶部了,点击触发回上一页
            changePage('prev');
          }
        }
      }

      function changePage(direction) {
        if (direction === 'prev') {
          if (pageNum <= 1) return;
          pageNum--;
        } else {
          if (pageNum >= pdfDoc.numPages) {
             pageNum = 1; 
          } else {
             pageNum++;
          }
        }
        renderPage(pageNum, false); // 换页时重置滚动条到顶部
      }

      function zoom(type) {
        if (!pdfDoc || pageRendering) return;

        if (type === 'in') {
          if (zoomScaleModifier >= 3.0) return;
          zoomScaleModifier += 0.2;
        } else {
          if (zoomScaleModifier <= 0.4) return;
          zoomScaleModifier -= 0.2;
        }
        
        // 缩放时,需要传入 true,保留操作员当前的滚动查看视线,不要粗暴弹回顶部
        renderPage(pageNum, true);
      }

      function loadPDF() {
        const fileName = getFileNameFromUrl();
        const currentPath = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/"));
        const pdfPath = currentPath + "/pdf_data/" + fileName + "?t=" + new Date().getTime();

        pdfjsLib.getDocument(pdfPath).promise.then(function(pdfDoc_) {
          pdfDoc = pdfDoc_;
          if (pageNum > pdfDoc.numPages) pageNum = 1;
          renderPage(pageNum, true); // 自动定时刷新时,保留当前的滚动和缩放状态
        }).catch(function(error) {
          console.error("PDF 加载失败:", error);
          document.getElementById('page-indicator').textContent = "PDF 文件加载失败";
        });
      }

      window.onresize = function() {
        if (pdfDoc && !pageRendering) {
          renderPage(pageNum, true);
        }
      };

      window.oncontextmenu = (e) => e.preventDefault();
      window.onload = loadPDF;
      setInterval(loadPDF, 60000);
    </script>
  </body>
</html>

把pdf.js的两个核心文件,通过浏览器右键另存下来,放在网站的static目录下

原创文章,作者:Gary,如若转载,请注明出处:https://www.cpw5.top/1755.html

淘宝小店:陈皮王五工作室

公司网址:海灵德(六安)环境科技有限公司

(0)
GaryGary
上一篇 2026-04-13 下午1:14
下一篇 2025-03-20 下午1:56

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注