Luckysheet 内网部署完整指南:从资源下载到 Vue3 集成


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 文件。同样需要下载到本地:


二、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>

四、进阶:内网协同编辑后端

如果未来需要升级到多人协同编辑,可以参考开源的后端项目:

  1. 官方 Java 后端:LuckysheetServer 提供了完整的协同编辑后端,支持 WebSocket、Redis、PostgreSQL
  2. 轻量版 Java 后端:去除了 Redis 和 PostgreSQL 依赖,只需 SpringBoot 即可运行,适合简单场景
  3. 核心流程:前端通过 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 未正确加载,或使用了错误的调用方式。

解决

  1. 确保在 index.html 中引入了 luckyexcel.umd.js
  2. 使用 window.LuckyExcel.transformExcelToLucky 调用
  3. 注意:方法名是 transformExcelToLucky,不是 transformExcelToLuckyByUrl(后者用于远程文件)

5.4 .xls 老版本文件不支持

原因 :Luckysheet 仅支持 .xlsx 格式,不支持老版本的 .xls

解决

  1. 前端提示用户上传 .xlsx 格式
  2. 或后端先将 .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()

核心要点:

  1. 所有资源必须本地化,不能依赖外网 CDN
  2. 引入顺序固定pluginsCss.cssplugins.cssluckysheet.cssplugin.jsluckysheet.umd.js
  3. Vue 中始终用 window.luckysheet,避免未定义错误
  4. Luckyexcel 用于解析 xlsx,是独立库,需单独引入

希望这份教程能帮助你在内网顺利完成 Luckysheet 的部署和使用!

相关推荐
SendTomo2 天前
P2P文件传输工具对比:SendTomo vs send.wang
javascript·网络协议·webrtc·html5·p2p
SendTomo2 天前
WebRTC文件互传工具实测对比
javascript·网络·webrtc·html5·p2p
SendTomo2 天前
send.wang自托管+WSS保障信令安全方案
网络·网络协议·webrtc·html5·p2p
SendTomo4 天前
跨设备文件互传新方案
javascript·网络·webrtc·html5·p2p
我命由我123454 天前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
海天鹰7 天前
屏幕颜色检测
javascript·html5
芳心粽伙饭9 天前
HTML第一章 注释与快捷键
前端·html5
大鹏说大话12 天前
HTML5 地理定位 Geolocation:获取用户位置的“红线”与最佳实践
前端·html·html5
冷咖啡离 我的笨笨13 天前
通过(Node Js||.Net)基于HTML5的WebSocket实现实时视频文字传输(上)
javascript·.net·html5
并不喜欢吃鱼15 天前
一.前端web开发:零基础吃透 HTML5 核心知识
前端·html·html5