欢迎光临
我们一直在努力

03- Luckysheet与Vue3+Electron集成实战指南

03- Luckysheet与Vue3+Electron集成实战指南

本文基于「工会预决算报表填报查询系统」项目经验,手把手教你将 Luckysheet 电子表格集成到 Vue 3 + Electron 桌面应用中,涵盖组件封装、生命周期管理、IPC 通信、开发环境配置等完整流程。


目录

  • 一、项目架构总览
  • 二、环境准备与项目搭建
  • 三、Luckysheet Vue 组件封装
  • 四、生命周期与内存管理
  • 五、Electron 主进程与渲染进程通信
  • 六、开发/生产环境适配
  • 七、与 Pinia 状态管理结合
  • 八、实战:报表填报页面完整实现
  • 九、常见问题与解决方案

一、项目架构总览

1.1 技术栈

层级技术作用
桌面框架 Electron 37 桌面应用容器
前端框架 Vue 3.5 + TypeScript UI 框架
构建工具 Vite 7 开发构建
状态管理 Pinia 3 全局状态
UI 组件库 Element Plus 2.10 基础组件
电子表格 Luckysheet 在线表格
Excel 处理 ExcelJS + LuckyExcel 导入导出
本地数据库 SQLite3 数据持久化
路由 Vue Router 4.5 页面路由

1.2 整体架构图

┌─────────────────────────────────────────────────────┐
│ Electron 主进程 │
│ ┌───────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ main.ts │ │ SQLite │ │ 文件系统操作 │ │
│ │ (窗口) │ │ (数据) │ │ (xlsx/备份) │ │
│ └─────┬─────┘ └─────┬─────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────────┘ │
│ │ IPC │
└───────────────────────┼──────────────────────────────┘
│
┌───────────────────────┼──────────────────────────────┐
│ Electron 渲染进程 │
│ ┌────────────────────┴────────────────────┐ │
│ │ Vue 3 应用 │ │
│ │ ┌─────────┐ ┌───────┐ ┌───────────┐ │ │
│ │ │ 路由 │ │ Pinia │ │ Element + │ │ │
│ │ │ Router │ │ Store │ │ Luckysheet│ │ │
│ │ └─────────┘ └───────┘ └─────┬─────┘ │ │
│ │ │ │ │
│ │ ┌─────────┐ ┌───────┐ ┌────┴────┐ │ │
│ │ │ 填报页 │ │ 查询页 │ │ 分析页 │ │ │
│ │ │ Fill │ │ Query │ │Analysis │ │ │
│ │ └─────────┘ └───────┘ └─────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

1.3 项目目录结构

ghbbxt/
├── electron/ # Electron 主进程代码
│ ├── main.ts # 主进程入口
│ ├── preload.ts # 预加载脚本
│ ├── menu.ts # 应用菜单
│ ├── taskScheduler.ts # 定时任务
│ ├── db/ # 数据库层
│ │ ├── db.ts # 数据库连接
│ │ ├── index.ts # 模型聚合
│ │ ├── migrations/ # 迁移脚本
│ │ └── models/ # 数据模型
│ ├── fs/ # 文件系统操作
│ │ ├── xlsx.ts # Excel 读写
│ │ ├── options.ts # 选项读写
│ │ └── organization.ts # 组织结构导入
│ └── auth/ # 授权认证
├── src/ # 渲染进程(Vue)代码
│ ├── main.ts # Vue 入口
│ ├── App.vue # 根组件
│ ├── router/ # 路由
│ ├── stores/ # Pinia 状态
│ ├── components/ # 组件
│ │ └── luckysheet/ # Luckysheet 封装组件
│ ├── views/ # 页面视图
│ │ ├── fill/ # 报表填报
│ │ ├── query/ # 报表查询
│ │ ├── analysis/ # 数据分析
│ │ └── …
│ ├── api/ # API 封装
│ ├── configs/ # 配置文件
│ │ ├── data.ts # 表格模板数据
│ │ ├── editCell.ts # 可编辑单元格配置
│ │ └── calcCell.ts # 公式配置
│ └── utils/ # 工具函数
│ └── exportXlsx.ts # Excel 导出工具
├── public/ # 静态资源
│ ├── static/luckysheet/ # Luckysheet 静态文件
│ ├── static/model.xlsx # Excel 模板
│ └── config/config.json # 配置文件
└── package.json


二、环境准备与项目搭建

2.1 依赖安装

# 核心依赖
npm install electron –save-dev
npm install vue@3 vue-router@4 pinia
npm install element-plus @element-plus/icons-vue
npm install vite @vitejs/plugin-vue –save-dev

# 电子表格相关
npm install exceljs luckyexcel xlsx big.js

# Electron 构建工具
npm install electron-builder –save-dev

# 本地数据库
npm install sqlite3
npm install @types/sqlite3 –save-dev

# 工具库
npm install nodemon –save-dev
npm install sass –save-dev

2.2 Vite 配置

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import path from "path";

export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
// 注意:Luckysheet 是通过 index.html 静态引入的
// 不需要在 vite 中配置
base: "./", // Electron 打包需要相对路径
});

2.3 HTML 中引入 Luckysheet

<!– index.html –>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />

<!– Luckysheet 样式 –>
<link rel="stylesheet" href="/static/luckysheet/plugins/css/pluginsCss.css">
<link rel="stylesheet" href="/static/luckysheet/plugins/plugins.css">
<link rel="stylesheet" href="/static/luckysheet/css/luckysheet.css">
<link rel="stylesheet" href="/static/luckysheet/assets/iconfont/iconfont.css">

<!– Luckysheet 脚本 –>
<script src="/static/luckysheet/plugins/js/plugin.js"></script>
<script src="/static/luckysheet/luckysheet.umd.js"></script>

<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>工会预决算报表填报查询系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

为什么不通过 npm 安装 Luckysheet?

  • Luckysheet 的 UMD 包包含了所有插件,体积较大
  • 静态资源(字体、样式图片等)需要手动处理路径
  • 静态引入更稳定,不受构建工具影响
  • 通过 CDN 或本地静态目录加载,可利用浏览器缓存
  • 2.4 TypeScript 类型声明

    由于 Luckysheet 是全局变量,需要在 TypeScript 中声明:

    // src/vite-env.d.ts
    /// <reference types="vite/client" />

    declare global {
    interface Window {
    luckysheet: any;
    electronAPI: any;
    myService: any;
    }
    }

    export {};


    三、Luckysheet Vue 组件封装

    3.1 组件设计原则

    封装 Luckysheet 组件时,遵循以下原则:

  • 单一职责:组件只负责表格的创建、销毁和基本配置
  • 可配置:通过 props 控制工具栏、编辑权限等
  • 生命周期友好:正确处理创建和销毁
  • 事件驱动:通过事件通知父组件表格状态
  • 3.2 完整组件实现

    <!– src/components/luckysheet/index.vue –>
    <template>
    <div
    id="luckysheet"
    style="width: 100%; height: 100%; margin: 0; padding: 0"
    ></div>
    </template>

    <script lang="ts" setup>
    import { ref, onMounted, onBeforeUnmount, watch } from "vue";
    import { useUserStore } from "@/stores/userStore";
    import { data } from "../../configs/data";

    // 定义事件
    const emit = defineEmits(["sheet-ready"]);

    // 状态
    const user = useUserStore();
    const isSheetReady = ref(false);

    // Props 定义
    const props = defineProps({
    sheetFormulaBar: {
    type: Boolean,
    default: true, // 是否显示公式栏
    },
    showinfobar: {
    type: Boolean,
    default: true, // 是否显示信息栏
    },
    showtoolbar: {
    type: Boolean,
    default: true, // 是否显示工具栏
    },
    allowEdit: {
    type: Boolean,
    default: true, // 是否允许编辑
    },
    });

    /**
    * 初始化 Luckysheet
    * 使用配置文件中的模板数据
    */
    const initXlsx = async () => {
    window.luckysheet.create({
    container: "luckysheet", // 容器 ID
    data: data, // 表格数据
    title: "工会财务报表填报查询系统", // 标题
    userInfo: user.name, // 用户信息(用于协作显示)
    lang: "zh", // 语言
    autoCalc: true, // 自动计算
    sheetFormulaBar: props.sheetFormulaBar,
    showinfobar: props.showinfobar,
    showtoolbar: props.showtoolbar,
    allowEdit: props.allowEdit,
    forceCalculation: true, // 强制重算公式
    });

    isSheetReady.value = true;
    };

    // 暴露方法给父组件
    defineExpose({ initXlsx });

    // 监听表格就绪状态,触发事件
    watch(isSheetReady, (newValue) => {
    if (newValue === true) {
    setTimeout(() => {
    emit("sheet-ready");
    isSheetReady.value = false;
    }, 100);
    }
    });

    onMounted(async () => {
    // 不在挂载时自动初始化,由父组件控制时机
    // 因为可能需要等待数据加载
    });

    onBeforeUnmount(() => {
    isSheetReady.value = false;
    // 组件卸载时销毁 Luckysheet 实例,释放内存
    if (window.luckysheet && window.luckysheet.destroy) {
    window.luckysheet.destroy();
    }
    });
    </script>

    3.3 组件使用方式

    <template>
    <div>
    <LuckySheet
    ref="luckySheetRef"
    :sheetFormulaBar="true"
    :showinfobar="false"
    :showtoolbar="false"
    :allowEdit="true"
    @sheet-ready="handleInitData"
    />
    </div>
    </template>

    <script setup>
    import { ref, nextTick, onMounted } from "vue";
    import LuckySheet from "@/components/luckysheet/index.vue";

    const luckySheetRef = ref<any>(null);

    const handleInitData = () => {
    console.log("表格已就绪,可以加载数据了");
    // 从数据库加载数据并填充到表格
    };

    onMounted(() => {
    nextTick(async () => {
    if (luckySheetRef.value) {
    await luckySheetRef.value.initXlsx();
    }
    });
    });
    </script>

    3.4 为什么用 nextTick + ref 调用

    // ❌ 直接在 onMounted 中调用可能失败
    onMounted(() => {
    luckySheetRef.value.initXlsx(); // 可能还没渲染完成
    });

    // ✅ 用 nextTick 确保 DOM 已渲染
    onMounted(() => {
    nextTick(async () => {
    if (luckySheetRef.value) {
    await luckySheetRef.value.initXlsx();
    }
    });
    });

    原因:

  • Luckysheet 需要容器 DOM 存在才能初始化
  • Vue 的 onMounted 只保证组件挂载,不保证子组件内部 DOM 完全就绪
  • nextTick 确保下一次 DOM 更新后再执行

  • 四、生命周期与内存管理

    4.1 Luckysheet 生命周期管理

    Luckysheet 实例是全局的,管理不当容易导致内存泄漏:

    组件挂载
    ↓
    initXlsx() → luckysheet.create()
    ↓
    用户操作(编辑、保存等)
    ↓
    组件卸载
    ↓
    luckysheet.destroy() → 清理内存

    4.2 内存泄漏的常见原因

    原因后果解决方案
    未调用 destroy() Canvas 上下文未释放,内存持续增长 在 onBeforeUnmount 中销毁
    事件监听未移除 回调函数持有组件引用 Luckysheet 销毁时自动清理
    定时器未清理 定时任务持续运行 组件卸载时 clearInterval
    全局变量引用 数据无法被 GC 销毁时置空引用

    4.3 多页面切换的正确姿势

    在 SPA 应用中,用户可能在多个表格页面间切换:

    // 填报页 → 查询页 → 填报页
    // 每次切换都要重新创建 Luckysheet

    // fill/index.vue
    onBeforeUnmount(() => {
    if (window.luckysheet && window.luckysheet.destroy) {
    window.luckysheet.destroy();
    }
    });

    // query/index.vue
    onBeforeUnmount(() => {
    if (window.luckysheet && window.luckysheet.destroy) {
    window.luckysheet.destroy();
    }
    });

    注意:Luckysheet 是单实例的(全局只有一个 window.luckysheet),如果同时存在两个表格组件,会互相覆盖。每个页面应该在自己的生命周期内管理实例。

    4.4 编辑模式退出

    保存或导出前,必须确保单元格已退出编辑模式,否则最后一个编辑的单元格数据可能丢失:

    // 保存前退出编辑模式
    const handleSave = async () => {
    await window.luckysheet.exitEditMode();
    // 然后再获取数据…
    };

    // 导出前同样需要
    const handleExport = async () => {
    await window.luckysheet.exitEditMode();
    // 然后导出…
    };


    五、Electron 主进程与渲染进程通信

    5.1 IPC 通信架构

    ┌──────────────┐ ipcMain.handle ┌──────────────┐
    │ 渲染进程 │ ──────────────────→ │ 主进程 │
    │ (Vue 页面) │ │ (Node.js) │
    │ │ ←────────────────── │ │
    └──────────────┘ invoke 返回值 └──────────────┘

    5.2 主进程注册 IPC 处理器

    // electron/main.ts
    import { app, BrowserWindow, ipcMain } from "electron";
    import { readXlsx, writeXlsx } from "./fs/xlsx";

    app.whenReady().then(() => {
    // 读取 Excel 模板
    ipcMain.handle("readXlsx", async () => readXlsx());

    // 写入 Excel 文件
    ipcMain.handle("writeXlsx", async (event, data) => writeXlsx(data));

    // 数据库操作
    ipcMain.handle("getFillReport", async (event, report_order, row_num, column_num) => {
    return db.fillReport.get(report_order, row_num, column_num);
    });

    ipcMain.handle("insertFillReport", async (event, data) => {
    return db.fillReport.insert(data);
    });

    ipcMain.handle("updateFillReport", async (event, data) => {
    return db.fillReport.update(data);
    });

    // 文件对话框
    ipcMain.handle("dialog:openFile", async (event, options = {}) => {
    const result = await dialog.showOpenDialog({
    properties: ["openFile"],
    filters: [{ name: "Excel Files", extensions: ["xlsx", "xls"] }],
    …options,
    });
    return {
    canceled: result.canceled,
    filePaths: result.filePaths,
    };
    });
    });

    5.3 Preload 脚本暴露 API

    // electron/preload.ts
    import { contextBridge, ipcRenderer } from "electron";

    // 通过 contextBridge 安全地暴露 API
    contextBridge.exposeInMainWorld("electronAPI", {
    // 报表操作
    getFillReport: (report_order: number, row_num: number, column_num: number) =>
    ipcRenderer.invoke("getFillReport", report_order, row_num, column_num),
    insertFillReport: (data: any) =>
    ipcRenderer.invoke("insertFillReport", data),
    updateFillReport: (data: any) =>
    ipcRenderer.invoke("updateFillReport", data),
    getAllFillReport: () =>
    ipcRenderer.invoke("getAllFillReport"),
    resetFillReport: () =>
    ipcRenderer.invoke("resetFillReport"),

    // 文件操作
    readXlsx: () => ipcRenderer.invoke("readXlsx"),
    writeXlsx: (data: any) => ipcRenderer.invoke("writeXlsx", data),

    // 对话框
    openFileDialog: (options: any) =>
    ipcRenderer.invoke("dialog:openFile", options),
    saveFileDialog: (options: any) =>
    ipcRenderer.invoke("dialog:saveFile", options),
    openDirectoryDialog: (options: any) =>
    ipcRenderer.invoke("dialog:openDirectory", options),
    });

    5.4 渲染进程中使用

    // src/api/report.ts
    export const ReportAPI = {
    async getFillReport(report_order: number, row_num: number, column_num: number) {
    return window.electronAPI.getFillReport(report_order, row_num, column_num);
    },

    async insertFillReport(data: any) {
    return window.electronAPI.insertFillReport(data);
    },

    async updateFillReport(data: any) {
    return window.electronAPI.updateFillReport(data);
    },

    async getAllFillReport() {
    return window.electronAPI.getAllFillReport();
    },

    async resetFillReport() {
    return window.electronAPI.resetFillReport();
    },
    };

    5.5 为什么用 contextBridge

    Electron 推荐使用 contextBridge 而不是直接暴露 ipcRenderer:

    方式安全性便利性
    nodeIntegration: true ❌ 高风险 高
    直接暴露 ipcRenderer ⚠️ 中风险 高
    contextBridge 白名单 ✅ 安全 中

    contextBridge 的好处是只暴露明确指定的 API,渲染进程无法访问 Node.js API,降低了 XSS 攻击的风险。


    六、开发/生产环境适配

    6.1 开发环境 vs 生产环境

    项目开发环境生产环境
    前端加载方式 http://localhost:5173 file://dist/index.html
    静态资源路径 public/static/ resources/static/
    配置文件路径 public/config/ resources/config/
    调试工具 打开 DevTools 关闭

    6.2 主进程中的环境判断

    // electron/main.ts
    const isDev = process.env.NODE_ENV === "development";
    const url = isDev
    ? "http://localhost:5173"
    : `file://${path.join(__dirname, "../dist/index.html")}`;

    win.loadURL(url);

    6.3 文件路径适配

    // electron/fs/xlsx.ts
    const modelXlsxPath = process.env.NODE_ENV === "development"
    ? path.join(__dirname, "../../public/static/model.xlsx") // 开发环境
    : path.join(process.resourcesPath, "static/model.xlsx"); // 生产环境

    6.4 electron-builder 配置

    // package.json
    {
    "build": {
    "appId": "com.gh.bbxt",
    "productName": "ghbbxt",
    "directories": {
    "buildResources": "build",
    "output": "dist_electron"
    },
    "files": [
    "dist/**/*",
    "dist-electron/**/*",
    "node_modules/**/*",
    "public/**/*"
    ],
    "extraResources": [
    { "from": "public/static", "to": "static", "filter": ["**/*"] },
    { "from": "public/config", "to": "config", "filter": ["**/*"] },
    { "from": "public/docs", "to": "docs", "filter": ["**/*"] },
    { "from": "public/images", "to": "images", "filter": ["**/*"] }
    ],
    "win": {
    "target": "nsis",
    "icon": "./build/icon.ico"
    }
    }
    }

    extraResources 中的文件会被复制到安装目录的 resources 文件夹下,通过 process.resourcesPath 访问。

    6.5 启动脚本

    {
    "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build-electron": "tsc -p electron/tsconfig.json",
    "start": "npm run build-electron && nodemon –exec \\"npm run build-electron && electron .\\" –watch ./electron –ext .ts",
    "build:win": "npm run clean && npm run build && npm run build-electron && electron-builder –win"
    }
    }

    开发时需要同时启动两个进程:

  • Vite 开发服务器:npm run dev(端口 5173)
  • Electron 应用:npm run start(加载 localhost:5173)

  • 七、与 Pinia 状态管理结合

    7.1 用户状态 Store

    // src/stores/userStore.ts
    import { defineStore } from "pinia";

    export const useUserStore = defineStore("user", {
    state: () => ({
    name: "管理员",
    reportName: "",
    reportCode: "",
    organization: null as any,
    }),
    actions: {
    setName(name: string) {
    this.name = name;
    },
    },
    });

    7.2 操作状态 Store

    // src/stores/operateStore.ts
    import { defineStore } from "pinia";

    export const useOperateStore = defineStore("operate", {
    state: () => ({
    currentOperate: "首页",
    }),
    actions: {
    updateCurrentOperate(operate: string) {
    this.currentOperate = operate;
    },
    },
    });

    7.3 在表格组件中使用

    const user = useUserStore();

    // 初始化时传入用户信息
    luckysheet.create({
    userInfo: user.name,
    // …
    });


    八、实战:报表填报页面完整实现

    8.1 页面结构

    报表填报页
    ├── 工具栏(保存、重置、导出)
    └── Luckysheet 表格区域

    8.2 完整代码

    <!– src/views/fill/index.vue –>
    <template>
    <div class="container">
    <div class="content">
    <!– 工具栏 –>
    <div class="tools">
    <el-button @click="handleSave">保存</el-button>
    <el-button @click="handleReset">重置</el-button>
    <el-button @click="handleExport">导出</el-button>
    </div>
    <!– 表格区域 –>
    <div class="sheet" v-loading="loading" element-loading-text="数据加载中…">
    <LuckySheet
    ref="luckySheetRef"
    :sheetFormulaBar="true"
    :showinfobar="false"
    :showtoolbar="false"
    :allowEdit="true"
    @sheet-ready="handleInitData"
    />
    </div>
    </div>
    </div>
    </template>

    <script lang="ts" setup>
    import { ref, onBeforeUnmount, onMounted, nextTick } from "vue";
    import { useOperateStore } from "@/stores/operateStore";
    import LuckySheet from "@/components/luckysheet/index.vue";
    import { ElMessage, ElMessageBox } from "element-plus";
    import exportSheetExcel from "@/utils/exportXlsx";
    import { editCell } from "../../configs/editCell";
    import { ReportAPI } from "@/api/report";

    const operate = useOperateStore();
    const loading = ref(true);
    const data = ref<any>([]);
    const luckySheetRef = ref<any>(null);

    // ===== 保存功能 =====
    const handleSave = async () => {
    await window.luckysheet.exitEditMode();
    try {
    await Promise.all(
    editCell
    .map((reportGroup) => {
    return reportGroup.cell.map(async (cellItem) => {
    // 获取单元格值
    const temp = luckysheet.getCellValue(
    cellItem.row_num,
    cellItem.column_num,
    { order: reportGroup.report_order }
    );

    const fillReport = {
    report_order: reportGroup.report_order,
    row_num: cellItem.row_num,
    column_num: cellItem.column_num,
    cell_value: temp,
    };

    // 查询已有数据,存在则更新,不存在则插入
    const res = await ReportAPI.getFillReport(
    reportGroup.report_order,
    cellItem.row_num,
    cellItem.column_num
    );

    if (res?.cell_value !== undefined) {
    await ReportAPI.updateFillReport(fillReport);
    } else {
    await ReportAPI.insertFillReport(fillReport);
    }
    });
    })
    .flat()
    );

    // 重新获取最新数据
    const newData = await ReportAPI.getAllFillReport();
    data.value = newData;
    ElMessage.success("保存成功");
    } catch (error: any) {
    ElMessage.error("保存失败: " + error.message);
    }
    };

    // ===== 重置功能 =====
    const handleReset = async () => {
    try {
    await ElMessageBox.confirm(
    "重置将清空所有已填入数据重新填报!",
    "确认重置",
    { type: "warning" }
    );

    await ReportAPI.resetFillReport();
    const newData = await ReportAPI.getAllFillReport();
    data.value = newData;
    await setCellValues();
    ElMessage.success("重置成功");
    } catch (error) {
    if (error !== "cancel") {
    ElMessage.error("重置失败: " + error);
    }
    }
    };

    // ===== 导出功能 =====
    const handleExport = async () => {
    await window.luckysheet.exitEditMode();
    try {
    const allSheetData = luckysheet.getluckysheetfile();
    const { downloadExcel } = exportSheetExcel(allSheetData);
    await downloadExcel();
    } catch (error: any) {
    ElMessage.error("导出失败: " + error.message);
    }
    };

    // ===== 设置单元格值(从数据库加载) =====
    const setCellValues = async () => {
    await window.luckysheet.setSheetActive(0);
    try {
    const fillReportData = await ReportAPI.getAllFillReport();
    if (Array.isArray(fillReportData) && fillReportData.length > 0) {
    data.value = fillReportData;
    fillReportData.forEach((item) => {
    if (
    typeof item.row_num === "number" &&
    typeof item.column_num === "number" &&
    item.cell_value !== undefined
    ) {
    luckysheet.setCellValue(
    item.row_num,
    item.column_num,
    item.cell_value,
    { order: item.report_order }
    );
    }
    });
    } else {
    data.value = [];
    }
    } catch (error) {
    console.error("加载数据失败:", error);
    } finally {
    luckysheet.refresh();
    loading.value = false;
    }
    };

    // 表格就绪后加载数据
    const handleInitData = async () => {
    setCellValues();
    };

    // ===== 生命周期 =====
    onMounted(async () => {
    operate.updateCurrentOperate("报表填报页");
    loading.value = true;
    nextTick(async () => {
    if (luckySheetRef.value) {
    await luckySheetRef.value.initXlsx();
    }
    });
    });

    onBeforeUnmount(() => {
    loading.value = true;
    if (window.luckysheet && window.luckysheet.destroy) {
    window.luckysheet.destroy();
    }
    });
    </script>

    <style lang="scss" scoped>
    .container {
    display: flex;
    width: 100%;
    height: 100%;
    .content {
    width: 100%;
    height: 100%;
    .tools {
    display: flex;
    width: 100%;
    height: 32px;
    }
    .sheet {
    width: 100%;
    height: calc(100% – 32px);
    }
    }
    }
    </style>

    8.3 保存逻辑详解

    保存时使用 Upsert 模式(存在则更新,不存在则插入):

    遍历配置的可编辑单元格
    ↓
    获取单元格当前值
    ↓
    查询数据库中是否已有记录
    ├─ 有 → 执行 UPDATE
    └─ 无 → 执行 INSERT
    ↓
    全部完成后提示保存成功

    使用 Promise.all 并发执行所有数据库操作,提升保存速度。


    九、常见问题与解决方案

    9.1 Luckysheet 找不到 / undefined

    问题:window.luckysheet is undefined

    原因:

    • Luckysheet 的 JS 文件没加载成功
    • 加载顺序不对(在 Vue 挂载后才加载)

    解决方案:

  • 确认 index.html 中的脚本路径正确
  • 检查控制台 404 错误
  • 在 onMounted 中加延迟或检查 window.luckysheet 是否存在
  • onMounted(() => {
    // 等待 Luckysheet 加载完成
    const checkLuckysheet = () => {
    if (window.luckysheet) {
    initXlsx();
    } else {
    setTimeout(checkLuckysheet, 100);
    }
    };
    checkLuckysheet();
    });

    9.2 生产环境白屏

    问题:开发环境正常,打包后白屏

    原因:

    • base 路径配置不对
    • 静态资源路径问题
    • Luckysheet 资源未正确打包

    解决方案:

    // vite.config.ts
    export default defineConfig({
    base: "./", // 使用相对路径
    });

    确认 electron-builder 的 files 配置包含了 public 目录。

    9.3 IPC 调用无响应

    问题:渲染进程调用 API 没反应

    排查步骤:

  • 检查主进程是否注册了对应的 ipcMain.handle
  • 检查 preload.ts 是否暴露了对应方法
  • 检查 contextIsolation 是否为 true(默认值)
  • 查看主进程控制台是否有报错
  • 9.4 表格高度不正确

    问题:表格只显示一部分,或者出现滚动条

    解决方案:

    /* 确保父元素有确定的高度 */
    .sheet-container {
    width: 100%;
    height: 100%; /* 或者 calc(100vh – xxxpx) */
    }

    #luckysheet {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    }

    9.5 切换页面后表格不显示

    问题:从其他页面切回表格页面,表格区域空白

    原因:Luckysheet 实例在上一次卸载时被销毁了,但没有重新创建

    解决方案:确保每次进入页面时都调用 initXlsx(),使用 onActivated(keep-alive 场景)或 onMounted。


    总结

    本文从项目架构、组件封装、生命周期、IPC 通信、环境适配等多个维度,详细介绍了 Luckysheet 与 Vue 3 + Electron 的集成方案。

    核心要点回顾:

  • Luckysheet 通过静态资源引入,组件化封装管理生命周期
  • Electron 使用 IPC 进行主渲染进程通信,通过 contextBridge 安全暴露 API
  • 开发/生产环境的路径差异需要特别处理
  • 正确的生命周期管理是避免内存泄漏的关键
  • 掌握这些知识,你就能在 Electron 桌面应用中优雅地集成 Luckysheet 电子表格了。


    参考资料

    • Luckysheet 官方文档
    • Electron 官方文档
    • Vue 3 官方文档
    • Pinia 官方文档

    关于作者:本文基于工会预决算报表填报查询系统的实际开发经验撰写,涵盖了 Luckysheet 在 Vue3 + Electron 项目中的完整集成方案。如果觉得有帮助,欢迎点赞、收藏、关注!

    赞(0)
    未经允许不得转载:171主机测评 » 03- Luckysheet与Vue3+Electron集成实战指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址