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 的部署和使用!

相关推荐
软件开发技术深度爱好者1 天前
HTML5实现数学函数画图器
前端·javascript·html5
软件开发技术深度爱好者4 天前
国际音标魔法实验室工具HTML5实现
前端·html5·英语学习
逝水无殇5 天前
HTML 属性(HTML Attributes)详解
开发语言·前端·html·html5
软件开发技术深度爱好者6 天前
小学英语单词魔法学园HTML5实现
javascript·html5·英语学习
小白巨白7 天前
玫瑰花园管理系统:AI识病+3D可视化,一套面向中小型玫瑰种植园的数字化管理工具
css·人工智能·计算机视觉·html5
星河耀银海13 天前
框架结合:Vue+HTML5+AI实现智能前端应用开发
前端·vue.js·html5
阿酷tony15 天前
纯HTML5播放器带倍速、带画质切换的播放器
前端·html·html5
aichitang202420 天前
Claude fable 5与GPT5.5模型能力实测
javascript·css·人工智能·ai·html·html5
AI行业学习24 天前
Notepad++快速下载-Notepad++下载方式【2026-7-4】
开发语言·python·前端框架·html·notepad++·html5