通过Luckysheet ,可以部署一个静态的查看Excel文件的web服务,利用pdf.js库构建一个PDF浏览服务。
(注:Excel文件中不能包含数据连接,否则不能加载)
步骤如下:
一、 部署一个Nginx 静态web服务器
我这里选用Debian+宝塔面板,利用宝塔面板部署一个Nginx服务,优点是简单,易于管理,资源消耗少。
Debian13+宝塔面板的安装,这里就不再叙述了。下面创建这个静态网站。
1、网站->添加站点

域名,填写你的访问域名或IP地址,目录填写你的网站存放目录,其他保持默认。
2、修改配置文件
选中刚刚添加的站点->设置->配置文件,禁用缓存
# 针对所有静态资源禁用缓存,确保实时性
location /
{
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}

二、建立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分钟后也刷新了。
如果原始的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目录下