Luckysheet 内网部署完整指南:从资源下载到 Vue3 集成
一、核心思路与资源获取
1.1 为什么需要本地部署?
内网环境无法访问公网 CDN,所以需要将 Luckysheet 的所有静态资源(JS、CSS、字体文件等)全部下载到项目中。搜索结果显示,本地引入方式被推荐用于生产环境,资源可控、加载速度快、不受网络波动影响。
1.2 获取打包好的资源文件
你有三种方式获取 Luckysheet 的打包文件:
方式一:直接从 CDN 网站下载(最简单)
在浏览器中打开以下任一链接,你会看到 dist 目录下的完整文件结构:
在页面中右键点击需要的文件,选择"另存为"即可逐个下载。需要下载的核心文件包括:
dist/
├── css/
│ └── luckysheet.css
├── plugins/
│ ├── css/
│ │ └── pluginsCss.css
│ ├── plugins.js
│ └── ... (其他插件文件)
├── assets/
│ └── iconfont/
│ └── iconfont.css 及相关字体文件
└── luckysheet.umd.js
方式二:从 GitHub Release 下载源码自行打包
bash
# 1. 克隆源码
git clone https://github.com/dream-num/Luckysheet.git
# 2. 安装依赖
npm install
# 3. 安装打包工具
npm install gulp -g
# 4. 执行打包
npm run build
# 或 gulp build
打包成功后,项目根目录会生成 dist 文件夹,里面就是所需的全部静态资源。
方式三:下载现成的打包文件
部分第三方资源站会提供 Luckysheet 打包文件的下载,可以搜索"Luckysheet 本地资源"获取。
1.3 获取 Luckyexcel(xlsx 解析库)
Luckyexcel 是独立于 Luckysheet 的库,用于解析 .xlsx 文件。同样需要下载到本地:
- CDN 地址:https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js
- GitHub:https://github.com/dream-num/Luckyexcel
二、HTML 方式:纯内网预览页面
2.1 项目目录结构
你的项目/
├── public/ # 静态资源目录
│ └── luckysheet/ # Luckysheet 相关资源
│ ├── css/
│ │ └── luckysheet.css
│ ├── plugins/
│ │ ├── css/
│ │ │ └── pluginsCss.css
│ │ ├── plugins.js
│ │ └── ...
│ ├── assets/
│ │ └── iconfont/
│ ├── luckyexcel.umd.js # 解析 xlsx 用
│ └── luckysheet.umd.js # 核心库
├── index.html # 页面入口
└── ...
2.2 完整 HTML 代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Luckysheet 内网 Excel 预览</title>
<!-- ===== 注意:所有路径均为本地相对路径 ===== -->
<!-- 样式引入顺序:插件样式 -> 核心样式 -> 图标样式 -->
<link rel="stylesheet" href="./public/luckysheet/plugins/css/pluginsCss.css" />
<link rel="stylesheet" href="./public/luckysheet/plugins/plugins.css" />
<link rel="stylesheet" href="./public/luckysheet/css/luckysheet.css" />
<link rel="stylesheet" href="./public/luckysheet/assets/iconfont/iconfont.css" />
<!-- JS 引入顺序:插件 -> 核心库 -> Luckyexcel -->
<script src="./public/luckysheet/plugins/plugins.js"></script>
<script src="./public/luckysheet/luckysheet.umd.js"></script>
<script src="./public/luckysheet/luckyexcel.umd.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; background: #f0f2f5; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
.header { padding: 20px 30px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
.header h1 { font-size: 22px; }
.upload-btn-wrapper { position: relative; overflow: hidden; display: inline-block; }
.upload-btn-wrapper input[type="file"] { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
.upload-btn { padding: 10px 24px; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.5); border-radius: 6px; color: #fff; font-size: 14px; cursor: pointer; transition: 0.3s; }
.upload-btn:hover { background: rgba(255,255,255,0.35); border-color: #fff; }
.file-info { color: rgba(255,255,255,0.95); font-size: 14px; background: rgba(255,255,255,0.15); padding: 8px 16px; border-radius: 6px; }
#container { width: 100%; height: 650px; background: #fff; }
.footer { padding: 12px 30px; background: #fafafa; border-top: 1px solid #e8e8e8; font-size: 13px; color: #999; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 内网 Excel 预览</h1>
<div style="display:flex;align-items:center;gap:15px;flex-wrap:wrap;">
<div class="upload-btn-wrapper">
<button class="upload-btn">📁 选择文件</button>
<input type="file" id="fileInput" accept=".xlsx,.xls" />
</div>
<span class="file-info" id="fileInfo">未选择文件</span>
</div>
</div>
<div id="container"></div>
<div class="footer">内网部署 · Luckysheet</div>
</div>
<script>
(function() {
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
function destroySheet() {
// 使用全局 luckysheet 对象(因为是通过 script 标签引入的)
if (window.luckysheet && window.luckysheet.destroy) {
try { window.luckysheet.destroy(); } catch(e) {}
}
}
function renderSheet(exportJson, fileName) {
if (!exportJson.sheets || exportJson.sheets.length === 0) {
alert('文件解析失败:没有有效的工作表');
return;
}
destroySheet();
// 关键:使用 window.luckysheet.create,避免 Vue 等框架中的未定义错误
window.luckysheet.create({
container: 'container',
data: exportJson.sheets,
title: fileName || '未命名',
lang: 'zh',
// ===== 只读预览核心配置 =====
allowEdit: false, // 禁止编辑
showtoolbar: false, // 隐藏工具栏
showinfobar: false, // 隐藏信息栏
sheetFormulaBar: false, // 隐藏公式栏
enableAddRow: false, // 禁止增加行
enableAddCol: false, // 禁止增加列
showstatisticBar: false, // 隐藏统计栏
showsheetbarConfig: {
add: false, // 禁止新增 Sheet
menu: false,
sheet: true // 保留切换标签
},
contextMenu: [{ text: '复制', onclick: function() {} }]
});
fileInfo.textContent = '📄 ' + fileName;
}
function handleFile(file) {
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!['xlsx', 'xls'].includes(ext)) {
alert('请上传 .xlsx 或 .xls 文件');
return;
}
fileInfo.textContent = '⏳ 正在解析 ' + file.name + '...';
// 使用 LuckyExcel 解析(注意:是 window.LuckyExcel,不是 luckysheet.xxx)
window.LuckyExcel.transformExcelToLucky(
file,
function(exportJson) {
renderSheet(exportJson, file.name);
},
function(err) {
console.error('解析失败:', err);
alert('文件解析失败,请确认文件格式正确');
fileInfo.textContent = '❌ 解析失败';
}
);
}
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) handleFile(file);
this.value = '';
});
// 支持拖拽上传
document.querySelector('.container').addEventListener('drop', function(e) {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) handleFile(files[0]);
});
document.querySelector('.container').addEventListener('dragover', function(e) {
e.preventDefault();
});
console.log('✅ Luckysheet 内网预览已启动');
})();
</script>
</body>
</html>
三、Vue3 + Vite 项目内网集成
3.1 项目结构
your-vue-project/
├── public/
│ └── luckysheet/ # 静态资源(Vite 会自动复制到构建目录)
│ ├── css/
│ ├── plugins/
│ ├── assets/
│ ├── luckyexcel.umd.js
│ └── luckysheet.umd.js
├── src/
│ └── components/
│ └── ExcelPreview.vue
├── index.html # 关键:在这里引入资源
└── vite.config.js
3.2 在 index.html 中引入本地资源
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 + Luckysheet 内网预览</title>
<!-- ===== 本地路径引入 ===== -->
<link rel="stylesheet" href="/luckysheet/plugins/css/pluginsCss.css" />
<link rel="stylesheet" href="/luckysheet/plugins/plugins.css" />
<link rel="stylesheet" href="/luckysheet/css/luckysheet.css" />
<link rel="stylesheet" href="/luckysheet/assets/iconfont/iconfont.css" />
<script src="/luckysheet/plugins/plugins.js"></script>
<script src="/luckysheet/luckysheet.umd.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
注意 :
/luckysheet/...路径指向public/luckysheet/...。Vite 会自动将public目录下的文件原样复制到构建输出目录,不会对其编译或混淆。
3.3 创建 ExcelPreview.vue 组件
vue
<template>
<div class="preview-wrapper">
<!-- 上传区域 -->
<div v-if="!hasData" class="upload-zone" @dragover.prevent @drop.prevent="handleDrop">
<div class="upload-content">
<div class="icon">📊</div>
<h3>点击或拖拽上传 Excel 文件</h3>
<p class="tip">支持 .xlsx、.xls 格式,仅预览不可编辑</p>
<button class="btn" @click="$refs.fileInput.click()">选择文件</button>
<input ref="fileInput" type="file" accept=".xlsx,.xls" style="display:none" @change="handleFileSelect" />
</div>
</div>
<!-- 工具栏 -->
<div v-else class="toolbar">
<span class="file-name">📄 {{ fileName }}</span>
<span class="sheet-count">{{ sheetCount }} 个工作表</span>
<button class="clear-btn" @click="handleClear">清空</button>
</div>
<!-- 表格容器 -->
<div ref="containerRef" class="sheet-container"></div>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, nextTick } from 'vue';
const props = defineProps({
height: { type: String, default: '600px' }
});
const emit = defineEmits(['loaded', 'error']);
const containerRef = ref(null);
const fileInput = ref(null);
const hasData = ref(false);
const fileName = ref('');
const sheetCount = ref(0);
let instance = null;
// 销毁表格
function destroySheet() {
if (instance) {
try { instance.destroy(); } catch (e) {}
instance = null;
}
// 也尝试全局销毁
if (window.luckysheet && window.luckysheet.destroy) {
try { window.luckysheet.destroy(); } catch (e) {}
}
}
// 渲染只读表格
function renderSheet(exportJson, name) {
if (!exportJson.sheets || exportJson.sheets.length === 0) {
emit('error', new Error('没有有效的工作表'));
return;
}
destroySheet();
nextTick(() => {
if (!containerRef.value) return;
// 关键:使用 window.luckysheet 而不是直接 luckysheet
// 避免 ESLint 报错 "luckysheet is not defined"
instance = window.luckysheet.create({
container: containerRef.value,
data: exportJson.sheets,
title: name || '未命名',
lang: 'zh',
// 只读配置
allowEdit: false,
showtoolbar: false,
showinfobar: false,
sheetFormulaBar: false,
enableAddRow: false,
enableAddCol: false,
showstatisticBar: false,
showsheetbarConfig: { add: false, menu: false, sheet: true },
contextMenu: [{ text: '复制', onclick: () => {} }]
});
hasData.value = true;
fileName.value = name;
sheetCount.value = exportJson.sheets.length;
emit('loaded', { data: exportJson, name });
});
}
// 处理文件
function processFile(file) {
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!['xlsx', 'xls'].includes(ext)) {
emit('error', new Error('请上传 .xlsx 或 .xls 文件'));
return;
}
// 动态导入 Luckyexcel(如果已经在 index.html 中全局引入,直接使用 window.LuckyExcel)
// 如果在 index.html 中引入了,这里直接用 window.LuckyExcel
// 如果没有,可以用动态 import
if (!window.LuckyExcel) {
// 动态加载 luckyexcel.umd.js
const script = document.createElement('script');
script.src = '/luckysheet/luckyexcel.umd.js';
script.onload = () => {
parseFile(file);
};
document.head.appendChild(script);
} else {
parseFile(file);
}
}
function parseFile(file) {
window.LuckyExcel.transformExcelToLucky(
file,
(exportJson) => renderSheet(exportJson, file.name),
(err) => {
console.error('解析失败:', err);
emit('error', new Error('文件解析失败: ' + err.message));
}
);
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) processFile(file);
e.target.value = '';
}
function handleDrop(e) {
const files = e.dataTransfer.files;
if (files.length > 0) processFile(files[0]);
}
function handleClear() {
destroySheet();
hasData.value = false;
fileName.value = '';
sheetCount.value = 0;
if (containerRef.value) containerRef.value.innerHTML = '';
}
// 暴露方法
defineExpose({ renderSheet, handleClear, destroySheet, hasData });
onBeforeUnmount(() => {
destroySheet();
});
</script>
<style scoped>
.preview-wrapper { width: 100%; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
.upload-zone { padding: 60px 20px; border: 2px dashed #d9d9d9; border-radius: 12px; margin: 20px; text-align: center; cursor: pointer; transition: all 0.3s; background: #fafafa; }
.upload-zone:hover { border-color: #667eea; background: #f0f2ff; }
.upload-content .icon { font-size: 48px; margin-bottom: 16px; }
.upload-content h3 { margin: 0 0 8px; font-size: 18px; color: #333; }
.upload-content .tip { margin: 0 0 20px; color: #999; font-size: 14px; }
.upload-content .btn { padding: 10px 32px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; border-radius: 6px; font-size: 15px; cursor: pointer; transition: 0.3s; }
.upload-content .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
.toolbar { display: flex; align-items: center; gap: 16px; padding: 12px 20px; background: #fafafa; border-bottom: 1px solid #e8e8e8; font-size: 14px; color: #333; flex-wrap: wrap; }
.clear-btn { margin-left: auto; padding: 4px 16px; border: 1px solid #d9d9d9; border-radius: 4px; background: #fff; cursor: pointer; font-size: 13px; color: #555; transition: 0.2s; }
.clear-btn:hover { border-color: #ff4d4f; color: #ff4d4f; }
.sheet-container { width: 100%; height: v-bind(height); min-height: 400px; background: #fff; }
</style>
3.4 在页面中使用组件
vue
<template>
<div class="page">
<h1>Excel 在线预览(内网部署)</h1>
<ExcelPreview ref="previewRef" height="650px" @loaded="onLoaded" @error="onError" />
<div v-if="errorMsg" class="error">⚠️ {{ errorMsg }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ExcelPreview from '@/components/ExcelPreview.vue';
const previewRef = ref(null);
const errorMsg = ref('');
function onLoaded(data) {
console.log('加载成功:', data);
errorMsg.value = '';
}
function onError(err) {
errorMsg.value = err.message;
console.error('预览错误:', err);
}
</script>
<style scoped>
.page { max-width: 1400px; margin: 0 auto; padding: 20px; }
.page h1 { font-size: 24px; margin-bottom: 20px; }
.error { margin-top: 16px; padding: 12px 16px; background: #fff2f0; border: 1px solid #ffccc7; border-radius: 6px; color: #ff4d4f; }
</style>
四、进阶:内网协同编辑后端
如果未来需要升级到多人协同编辑,可以参考开源的后端项目:
- 官方 Java 后端:LuckysheetServer 提供了完整的协同编辑后端,支持 WebSocket、Redis、PostgreSQL
- 轻量版 Java 后端:去除了 Redis 和 PostgreSQL 依赖,只需 SpringBoot 即可运行,适合简单场景
- 核心流程:前端通过 WebSocket 与后端通信,后端负责数据存储和广播变更
java
// 轻量版后端特点
// - 无需 Redis
// - 无需 PostgreSQL
// - 直接 SpringBoot 运行
// - 参考:https://gitee.com/mengshukeji/LuckysheetServer
五、常见问题与解决方案
5.1 luckysheet is not defined
原因 :在 Vue 的 <script setup> 中直接使用 luckysheet 会被 ESLint 认为未定义。
解决 :始终使用 window.luckysheet 调用。
javascript
// ❌ 错误
luckysheet.create(options);
// ✅ 正确
window.luckysheet.create(options);
5.2 样式错乱或图标不显示
原因 :assets/iconfont/ 目录中的字体文件路径没有被正确复制,或目录层级被破坏。
解决 :确保完整复制 dist 目录下的所有内容,保持原有目录结构。
5.3 LuckyExcel.transformExcelToLucky is not a function
原因:Luckyexcel 未正确加载,或使用了错误的调用方式。
解决:
- 确保在
index.html中引入了luckyexcel.umd.js - 使用
window.LuckyExcel.transformExcelToLucky调用 - 注意:方法名是
transformExcelToLucky,不是transformExcelToLuckyByUrl(后者用于远程文件)
5.4 .xls 老版本文件不支持
原因 :Luckysheet 仅支持 .xlsx 格式,不支持老版本的 .xls。
解决:
- 前端提示用户上传
.xlsx格式 - 或后端先将
.xls转换为.xlsx再返回给前端
java
// 后端转换示例(使用 Apache POI)
// 将 xls 读取后另存为 xlsx
5.5 保存时丢失最后编辑的数据
问题 :如果当前有单元格处于编辑状态,调用 getAllSheets() 可能获取不到最新数据。
解决:在获取数据前,先退出编辑模式。
javascript
// 退出编辑模式,确保数据完整
window.luckysheet.exitEditMode();
const data = window.luckysheet.getAllSheets();
六、总结
| 步骤 | 关键操作 |
|---|---|
| 资源获取 | 从 CDN 网站下载 dist 目录,或自行打包 |
| HTML 集成 | 在 index.html 中用本地路径引入所有 CSS 和 JS |
| Vue3 集成 | 资源放入 public/,在 index.html 中引入,组件中用 window.luckysheet |
| 文件解析 | 使用 window.LuckyExcel.transformExcelToLucky |
| 只读模式 | allowEdit: false + 隐藏工具栏 |
| 数据保存 | 先 exitEditMode(),再 getAllSheets() |
核心要点:
- 所有资源必须本地化,不能依赖外网 CDN
- 引入顺序固定 :
pluginsCss.css→plugins.css→luckysheet.css→plugin.js→luckysheet.umd.js - Vue 中始终用
window.luckysheet,避免未定义错误 - Luckyexcel 用于解析 xlsx,是独立库,需单独引入
希望这份教程能帮助你在内网顺利完成 Luckysheet 的部署和使用!