Vue3集成“luckysheet”实现Excel在线编辑
2026/6/25 13:37:17 网站建设 项目流程

目录

📝 建议在方案中补充的“技术技术落地补丁”:

💻 共享编辑组件代码(views/LuckySheet/index.vue)


背景:

原来的功能实现是:

前端项目,在 Excel 共享编辑页面,通过点击新建图标(也就是新增 Excel)或者点击 Excel 表格列表其中的任意一个(也就是编辑文件),都可以跳转到另外一个项目(这个项目是前后端未分离)的 luckysheet 页面,跳转方式都是:window.open(url, "_blank");

现在的问题是:跳转的 url 地址,粘贴复制到别的浏览器,没有校验身份就可以直接打开访问其页面,这样感觉不安全,所以需要优化一下,看能不能将老项目里关于 luckysheet 的代码提取到前端项目里。但觉得不太好实现,因为老项目的 html 文件代码里是用 link 和 script 标签引入的 luckysheet 相关的资源,看能不能找一个第三方组件,直接在前端项目里 pnpm install 下载引入会更好一点呢。查看一下有没有类似于 websocket--luckysheet 的页面功能的第三方依赖。


方案完整分析:先解决你的核心痛点 + 推荐 npm 可直接安装的在线协同 Excel 组件

一、先梳理你当前的核心问题

  1. 安全漏洞根源:新窗口window.open直接带文件 ID 跳转独立页面,页面无登录态校验,任意人拿到 URL 就能访问;
  2. 现有架构缺陷:LuckySheet 页面独立在老项目,靠静态 html 引入link/script全局资源,无法复用前端登录鉴权;
  3. 你的优化思路:把表格能力移入主项目,用 pnpm/npm 安装组件,统一走登录 token 鉴权,从根源解决裸 URL 可访问问题。

两种改造路线对比(优先推荐方案 A)

方案 A:前端直接集成 npm 版在线 Excel 组件(最优,匹配你的想法)

不用跨项目跳转,表格页面作为项目内路由页面,全程携带登录 token,所有接口 /websocket 连接都带鉴权,复制 URL 到其他浏览器无登录态直接拦截,完美解决安全问题。

方案 B:保留老项目独立页面,加鉴权补丁(折中,不推荐长期)

跳转时携带临时签名 token,后端校验 URL 参数时效性;但依然存在独立页面、跨项目维护成本,治标不治本。


二、可 pnpm 直接安装、支持 WebSocket 多人协同的主流组件推荐

1. Univer(LuckySheet 官方升级替代,首选)★

核心优势

  • 原 Luckysheet 团队全新重构,官方 npm 包pnpm install @univerjs/core直接引入,无需手动引入静态 js/css 文件;
  • 完整兼容 Excel:公式、图表、冻结、合并单元格、导入导出 xlsx;
  • 原生支持 WebSocket 多人实时协同,内置 CRDT 冲突算法,和你现有 websocket 协同逻辑完全对齐;
  • 完美适配 Vue3/React,前后端分离架构友好,鉴权统一走项目 token;
  • 持续维护,解决老 Luckysheet 停止维护、打包笨重的问题。

安装方式

pnpm add @univerjs/core @univerjs/sheets @univerjs/sheets-collaboration

适配你的业务优势

可以直接在XP内部新建路由页面,不再window.open新开空白页;所有加载文件、websocket 连接请求自动带上项目登录 token,后端校验 token + 文件权限,裸 URL 无登录态直接 403。

2. LuckySheet(你当前在用的旧库,备选过渡)⭐️

现状

  • 官方已停止维护,但有成熟 npm 包可安装:
pnpm add luckysheet
  • 缺点:打包体积大、部分 API 老旧、无持续更新;
  • 适配:如果不想大幅改业务逻辑,可先迁移这个,后续再升级 Univer;
  • 集成方式:在 Vue 页面内引入,不再使用独立 html 文件,统一复用项目登录鉴权。

3. x-spreadsheet(轻量开源免费,简单表格场景)

pnpm add x-spreadsheet
  • 轻量、体积小,基础 Excel 功能齐全;
  • 协同需要自己基于 WebSocket 封装,社区协同案例少;
  • 适合简单表格,不推荐你这种复杂多人协同 Excel 场景。

4. Handsontable / SpreadJS(企业商用,付费)

  • SpreadJS:葡萄城商用组件,协同能力极强,Excel 兼容性天花板,但需要商业授权;
  • Handsontable:免费版阉割协同,企业版付费;
  • 适合预算充足、极致兼容性需求,中小企业一般不优先选。

三、最优落地实施步骤(Univer 方案,解决你的安全问题)

步骤 1:XP前端集成 Univer,新增内部路由页面

不再用window.open新开页面,改为项目内路由跳转(如/excel/edit/:wbId/:userId),页面属于 SPA,天然携带登录 token。

  • 跳转逻辑改造:
// 原来不安全写法 window.open(url, "_blank") // 改造后,内部路由跳转,同页面携带登录态 router.push({ path: '/excel/edit', params: { wbId, userId } })

步骤 2:统一鉴权,彻底解决裸 URL 访问漏洞

  1. XP所有接口、Univer 的 websocket 连接,请求头统一携带Authorization: Bearer ${token}
  2. 后端改造:
    • 加载表格文件loadUrl、websocket 连接updateUrl、保存接口全部校验 token;
    • 校验当前登录用户是否拥有该 Excel 文件的读写权限;
  3. 效果:复制 URL 到其他浏览器,无登录 token 直接拦截,无法加载表格。

步骤 3:WebSocket 协同鉴权改造

你现有代码里的 ws 地址:

updateUrl: "ws://" + localurl + "/ws/" + "${wb.userId}" + "/${wb.id}"

改造后在连接 ws 时携带 token 参数:

const token = localStorage.getItem('token') updateUrl: `ws://${localurl}/ws/${wb.userId}/${wb.id}?token=${token}`

后端 WebSocket 握手阶段校验 token,非法连接直接断开。

步骤 4:迁移现有业务配置

你原来页面里的配置(隐藏打印、图表按钮、容器、多语言、文件加载地址)全部平移到 Vue 页面内的 Univer 配置项,无需维护两套 html 静态文件。

四、对比:提取 cbit 静态 html vs npm 安装 Univer

维度提取 cbit 里 luckySheet 静态 htmlpnpm 安装 Univer 新组件
工程化手动拷贝 js/css 静态资源,无版本管理,更新麻烦npm 包管理,一键升级,无静态文件冗余
鉴权安全独立页面,脱离XP登录上下文,难统一 token嵌入 SPA,天然复用全局登录态
维护成本双项目维护表格页面,修改需要改两处仅XP一个项目维护,统一迭代
协同能力老 Luckysheet 停止维护,bug 无人修复Univer 持续更新,官方协同方案
打包体积全局引入完整静态资源,体积大模块化按需引入,可分包优化

五、兜底方案:如果短期不能换组件,优化现有 Luckysheet 安全

若短期内只能沿用老 Luckysheet,不迁移组件,最小改动加固安全:

  1. 跳转时生成一次性临时签名 token,拼在 url 参数里;
  2. cbit 后端页面加载、websocket、文件接口同时校验:登录 session + 临时签名时效性;
  3. 限制签名有效期(5 分钟),过期 URL 直接失效;
  4. 劣势:依然是独立页面,只是增加一层校验,架构不优雅,长期还是推荐迁移到XP内部集成 npm 组件。

总结建议

  1. 长期最优方案:选用Univer,pnpm 安装集成到前端,内部路由打开表格,统一 token 鉴权,彻底杜绝裸 URL 越权访问;
  2. 短期过渡方案:npm 安装原版 luckysheet 快速迁移,后续迭代升级 Univer;
  3. 不建议直接复制 cbit 项目的静态 html 资源到XP,会带来资源版本混乱、鉴权割裂、维护成本高等遗留问题。

如何在Vue3项目中使用Luckysheet?

参考地址:https://github.com/jwhx/luckysheet-vue3-vite


Vue3 + Vite 项目集成 Luckysheet 完整实操(参考仓库适配你的内网环境)

一、前置安装(解决你内网私服 404、workspace 警告)

1. WEB 目录执行安装(带-w适配 pnpm-workspace,临时外网源拉包)

pnpm add luckysheet -w --registry=https://registry.npmmirror.com

安装完成后切回公司内网源:

pnpm config set registry http://xxx

2. 解决 Vite 静态资源引入报错(关键)

Luckysheet 内部依赖字体、图片静态资源,Vite 默认路径解析异常,在vite.config.ts配置别名:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src'), // 映射luckysheet静态资源路径 luckysheet: resolve(__dirname, 'node_modules/luckysheet/dist') } }, // 复制luckysheet静态资源到打包目录 build: { rollupOptions: { external: [], } } })

二、新建表格页面src/views/LuckySheetEdit/index.vue

完全对齐你原有业务:WebSocket 协同、隐藏打印 / 图表、Token 鉴权、全屏容器

<template> <!-- 表格挂载容器,100%铺满页面 --> <div ref="sheetContainer" class="sheet-wrap"></div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' // 引入luckysheet主包+全部样式 import luckysheet from 'luckysheet' import 'luckysheet/dist/plugins/css/pluginsCss.css' import 'luckysheet/dist/plugins/plugins.css' import 'luckysheet/dist/css/luckysheet.css' import 'luckysheet/dist/assets/iconfont/iconfont.css' const route = useRoute() const router = useRouter() const sheetContainer = ref<HTMLDivElement | null>(null) let luckysheetIns: any = null // 获取全局登录token(鉴权核心) const getToken = () => localStorage.getItem('token') || '' // 路由参数:文件ID、操作用户ID const wbId = ref(route.params.wbId as string) const userId = ref(route.params.userId as string) // 销毁表格实例 const destroySheet = () => { if (luckysheetIns) { luckysheet.destroy() luckysheetIns = null } } // 初始化在线协同表格 const initSheet = () => { if (!sheetContainer.value || !getToken()) return destroySheet() const host = window.location.host const baseHttp = `${window.location.protocol}//${host}` const token = getToken() luckysheetIns = luckysheet.create({ container: sheetContainer.value, title: '汽车企业数据.xlsx', // 表格标题,可动态替换 lang: 'zh', allowUpdate: true, // 开启多人编辑 // 隐藏打印、图表按钮(和你原HTML配置一致) showToolbarConfig: { print: false, chart: false }, // 文件加载接口 loadUrl: `${baseHttp}/load/${wbId.value}/${userId.value}`, loadSheetUrl: `${baseHttp}/loadSheet/${wbId.value}/${userId.value}`, // WebSocket协同地址,携带token握手鉴权 updateUrl: `ws://${host}/ws/${userId.value}/${wbId.value}?token=${token}`, // 全局ajax请求拦截,所有接口携带鉴权头 hook: { beforeAjaxSend: (xhr: XMLHttpRequest) => { xhr.setRequestHeader('Authorization', `Bearer ${token}`) } } }) } // 路由参数变化重载表格 watch([wbId, userId], () => { initSheet() }) onMounted(() => { // 无登录token直接拦截,跳登录页,解决裸URL越权访问 if (!getToken()) { router.push('/login') return } initSheet() }) onUnmounted(() => { destroySheet() }) </script> <style scoped> .sheet-wrap { width: 100%; height: 100vh; margin: 0; padding: 0; position: absolute; left: 0; top: 0; } </style>

三、路由配置(内部路由跳转,不再 window.open 新开页面)

src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router' const routes = [ // 其他业务路由... { path: '/sheet/edit/:wbId/:userId', name: 'LuckySheetEdit', component: () => import('@/views/LuckySheetEdit/index.vue'), meta: { requiresAuth: true } // 路由守卫强制校验登录 } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局鉴权守卫:复制URL到其他浏览器无token直接拦截 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } }) export default router

四、列表页跳转逻辑改造(根治安全漏洞)

废弃旧代码(新开空白页,脱离登录上下文)

// 危险,复制URL可直接访问 const openFile = (row) => { const url = `/websocket.html?wbId=${row.id}&userId=${row.userId}` window.open(url, '_blank') }

新安全写法(项目内路由跳转,复用全局登录态)

import { useRouter } from 'vue-router' const router = useRouter() const openFile = (row) => { router.push({ name: 'LuckySheetEdit', params: { wbId: row.id, userId: row.userId } }) }

五、参考仓库适配说明(https://github.com/jwhx/luckysheet-vue3-vite)

1、该仓库基础逻辑一致,区别:

  • 仓库未处理 WebSocket 协同、Token 鉴权;本模板补充了你业务需要的 ws 鉴权、接口请求头拦截;
  • 仓库无路由守卫,本方案增加全局登录拦截,解决裸 URL 访问漏洞;

2、仓库样式 / 资源兼容问题:

原仓库使用import 'luckysheet/dist/...'全量引入样式,和本文代码引入方式完全一致,可直接复用;

3、仓库仅实现基础单人表格,你可直接复制本文 WebSocket 协同代码替换仓库示例。


六、常见坑解决方案

1. 图标 / 图片丢失

确认 vite.config.ts 配置luckysheet别名,若仍失效,可将node_modules/luckysheet/dist/assets复制到项目public目录,修改 iconfont 路径。

2. WebSocket 连接失败

  • ws 地址必须携带?token=${token}参数,后端握手时解析 token 校验;
  • 后端所有文件读取接口校验Authorization请求头,无 token 返回 403。

3. pnpm store 路径冲突报错

安装前清理旧依赖:

rmdir /s /q node_modules del pnpm-lock.yaml pnpm store prune pnpm add luckysheet -w --registry=https://registry.npmmirror.com

4. 打包后空白 / 样式丢失

vite 打包时静态资源路径异常,在vite.config.ts增加 base 配置(根据部署路径调整):

export default defineConfig({ base: './', // ...其余配置 })

七、架构优势对比你原独立 html 方案

  1. 统一鉴权:页面属于 Vue SPA,路由守卫 + 接口双重 token 校验,复制 URL 到其他浏览器直接拦截;
  2. 工程化管理:依赖通过 pnpm 安装,无需跨项目拷贝 cbit 静态 html/js/css;
  3. 维护单一:表格逻辑全部在前端,不用维护两套页面;
  4. 兼容原有业务:完全保留你现有的 WebSocket 多人协同、隐藏打印 / 图表等配置。

汇报方案(简洁正式、条理清晰,可直接复制发消息 / 文档)

Excel 在线共享编辑功能优化改造三套方案对比汇报

背景说明

当前 Excel 共享页面通过 window.open 新开独立页面跳转,裸 URL 可直接复制访问,存在无身份校验的安全漏洞,现提供三套优化落地方案,从安全、开发成本、长期维护、内网环境适配维度对比说明:

方案一:❌

短期过渡方案 —— 一次性临时签名 Ticket(改动最小,不改动现有页面)

1、实现逻辑

后端新增接口,基于当前登录用户、文件 ID 生成限时一次性加密临时票据 ticket;前端跳转新页面时,将 ticket 拼接到 URL 参数中。

2、校验逻辑

独立 LuckySheet 页面加载、WebSocket 协同连接时,后端双重校验 ticket:验证签名合法性、Redis 判断票据是否过期 / 已使用,校验通过后立即销毁票据,实现单次有效、限时失效。

3、优缺点

✅ 优势:无需重构现有 cbit 项目 LuckySheet 页面,前端改造量极小,可快速上线封堵安全漏洞;

❌ 劣势:属于补丁式优化,未从架构层面解决问题;URL 携带凭证会留存于浏览器、服务器日志,存在票据泄露风险;仅作为临时过渡方案,不适合长期使用。


⭐️方案二:✅

中长期兼容方案 ——Vue3 项目内集成原版 Luckysheet

将原独立页面逻辑迁移至前端 Vue 项目,内部路由跳转,复用全局登录 Token 统一鉴权,彻底杜绝裸 URL 越权访问。

1、现存短板

Luckysheet 官方已停止维护,架构老旧,打包体积臃肿,API 不再迭代更新,长期存在技术债务;

2、落地需解决 2 类技术问题

① TS 类型报错:该包无内置类型声明文件,TS 项目导入会爆红,需手动新建.d.ts声明文件或使用any类型绕过;

② Vite 打包兼容问题:底层依赖大量混淆 jQuery 插件,Rollup 打包解析异常,需在 vite 配置中单独处理 CommonJS 兼容、外部化打包;

3、优缺点

✅ 优势:完全复用现有 WebSocket 多人协同逻辑,业务改动小,内网 npm 仓库可正常安装无 404 缺失包问题;统一项目鉴权,根除安全漏洞;

❌ 劣势:技术老旧无维护,存在打包、TS 兼容额外开发工作量,后续无法获取官方功能更新与 bug 修复。


方案三:长期技术优选方案 —— 迁移 Univer 新一代表格组件

Univer 为原 Luckysheet 团队重构升级产品,模块化架构、原生支持 Vue3/Vite,官方持续迭代,是行业长期推荐方案。

1、当前落地阻碍(内网环境限制)

Univer 采用多分包管理,实现多人实时协同必须引入@univerjs/sheets-collaboration协同插件;但公司内网 npm 私服缺失该依赖包,无法下载安装,仅能安装基础表格包,无法支撑现有多人协同业务需求,仅能实现单人查看编辑;

2、额外维护成本

当前版本 0.25,迭代速度快,1.0 正式大版本即将发布,后续 API 存在变更风险,该功能需要持续跟进适配维护;

3、优缺点

✅ 优势:架构现代化、无静态资源冗余、官方长期维护,统一 SPA 登录鉴权,从根源解决安全问题,无技术债务;

❌ 劣势:内网仓库缺失协同依赖包,短期无法实现多人实时编辑;版本迭代快,存在持续适配成本;如需完整能力需协调运维同步全量 Univer 分包至内网私服。


方案选型建议

  1. 若要求1-3 天快速上线、临时封堵安全漏洞:优先选择【方案一】,作为过渡方案,后续择机重构;
  2. 若可接受少量前端改造、长期稳定使用、不新增运维工作量:优先选择【方案二】,兼容现有协同业务,适配内网环境;⭐️
  3. 若可协调运维同步内网 npm 全量依赖、愿意承担持续版本适配成本,追求长期技术架构优化:选择【方案三】。

最终选择【方案二】。且版本号为:

“luckysheet”:“^2.1.13”


落地执行汇报(选定方案二:Vue3 集成 Luckysheet 2.1.13)

一、方案确认

经评估,放弃临时 ticket 过渡方案,确定采用方案二:将 Luckysheet v2.1.13 完整集成至 Vue3 前端项目,废弃原老项目独立 html 页面,统一项目内路由跳转,从底层解决裸 URL 无鉴权访问的安全漏洞。 依赖版本锁定:luckysheet": "^2.1.13

二、核心改造收益

1、安全问题根治

表格页面为项目内部 SPA 路由页面,复用全局登录 Token,搭配全局路由守卫;复制链接到其他浏览器无登录态直接拦截,接口、WebSocket 连接统一携带鉴权 Header,无需 URL 传参,彻底消除越权访问风险。

2、业务能力完全保留

原生复用现有 WebSocket 多人协同逻辑、隐藏打印 / 图表工具栏、Excel 加载 / 保存接口,业务功能无改动,用户操作无感知。

3、内网环境适配

仅单包依赖,内网 npm 私服可正常安装,无 Univer 多分包缺失、404 下载失败问题。

三、需处理的两类技术难点及解决办法

1. TS 类型爆红问题(无内置 d.ts 声明)

解决方案

在项目src/目录新建luckysheet.d.ts类型声明文件:

declare module 'luckysheet' { const luckysheet: any export default luckysheet }

全局声明后,import luckysheet from 'luckysheet'不再报 TS 类型错误,无需大面积使用any强制绕过。

2. Vite 打包兼容问题(jQuery 混淆插件解析异常)

修改vite.config.ts,增加 CommonJS 兼容、外部依赖转换配置,规避打包空白、样式丢失、构建失败:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' import commonjs from '@vitejs/plugin-commonjs' export default defineConfig({ base: './', plugins: [ vue(), commonjs({ include: /luckysheet/ // 强制转换luckysheet的CommonJS依赖 }) ], resolve: { alias: { '@': resolve(__dirname, 'src'), luckysheet: resolve(__dirname, 'node_modules/luckysheet/dist') } }, build: { rollupOptions: { output: { manualChunks: { luckysheet: ['luckysheet'] // 单独分包,减小主包体积 } } } } })

同时页面完整引入 Luckysheet 全套样式资源,避免图标、表格样式缺失:

import luckysheet from 'luckysheet' import 'luckysheet/dist/plugins/css/pluginsCss.css' import 'luckysheet/dist/plugins/plugins.css' import 'luckysheet/dist/css/luckysheet.css' import 'luckysheet/dist/assets/iconfont/iconfont.css'

四、开发改造范围

1、前端页面

新增src/views/LuckySheetEdit表格编辑页面,实现全屏容器、WebSocket 协同、请求头统一携带 Token 鉴权;

2、路由改造

新增专属路由,配置requiresAuth鉴权标识,全局路由守卫拦截未登录访问;

3、列表跳转逻辑重构

删除原window.open新开空白页写法,替换为项目内部路由跳转,全程复用登录上下文;

4、后端接口微调

原有load/loadSheet/ws接口增加Authorization请求头校验逻辑,校验登录用户文件权限,无权限返回 403;WebSocket 握手同步校验 Token。

五、风险与兜底说明

1、技术债务说明

Luckysheet 2.1.13 官方已停止维护,后续无官方更新、bug 修复;本次仅满足当前共享编辑业务需求,长期技术迭代可后续再评估 Univer 升级改造;

2、打包体积优化兜底

配置分包拆分,将 luckysheet 单独拆为独立 chunk,避免主包体积过大影响页面首屏加载速度;

3、静态资源兜底

若打包后图标 / 图片丢失,可将node_modules/luckysheet/dist/assets静态目录复制至项目public目录,手动修正 iconfont 资源路径。

六、排期预估

  1. 依赖安装、TS 声明、Vite 配置改造:1 天
  2. 表格页面开发、WebSocket 协同、鉴权逻辑开发:2 天
  3. 路由、列表跳转逻辑改造:1 天
  4. 前后端联调、打包测试、边界场景验证:2 天

合计:6 个工作日可完成全量上线。


为了让这份方案在落地评审时更加无懈可击,我建议你在“落地需解决 2 类技术问题”的下方,直接附上切实可行的具体代码补丁。这样评审时,一看就知道你不仅发现了问题,连“解药”都准备好了:

📝 建议在方案中补充的“技术技术落地补丁”:

针对问题 ①:TS 类型报错的“解药”

在项目的src/目录下(通常是src/types/或根目录),新建一个luckysheet.d.ts文件。只要加上下面这几行,整个项目里无论是import还是window.luckysheet都会直接闭嘴,不再爆红:

// src/luckysheet.d.ts declare module 'luckysheet' { const luckysheet: any; export default luckysheet; } interface Window { luckysheet: any; }

针对问题 ②:Vite 打包兼容问题的“解药”

直接给出修改vite.config.ts的具体配置。既然 Luckysheet 体积臃肿(纯 JS 压缩后都有几兆),最完美的 Vite 处理方式是用vite-plugin-external或者修改build.rollupOptions将其外部化(External),或者强制进行 CommonJS 转换:

// vite.config.ts 核心配置补丁 import { defineConfig } from 'vite' export default defineConfig({ // ... 其他配置 optimizeDeps: { include: ['luckysheet'] // 确保本地开发(dev)时,Vite 预构建能正确识别其内部的 CommonJS 依赖 }, build: { rollupOptions: { // 如果打包时因为体积过大报 Warning,可以调大块大小限制 chunkSizeWarningLimit: 2000, // 或者是通过配置将其分包,避免主包(index.js)被撑爆 output: { manualChunks(id) { if (id.includes('node_modules/luckysheet')) { return 'luckysheet-vendor'; } } } } } })

💡 总结点评

这份方案整体上扬长避短。你强调了“完全复用现有 WebSocket 多人协同逻辑,业务改动小”这个巨大优势,这往往是项目组和后端开发最喜欢的(因为后端协同代码完全不用动


这份代码专门针对你的中长期兼容方案:

(Luckysheet^2.1.13+ Vue 3 + TS + Vite 内网打包)量身定制。

为了实现“从列表页跳转进来、复用 Token 鉴权、闭环 WebSocket 协同”的完整业务流程,我们在代码中实现了:

  1. 统一路由/状态鉴权:无 Token 拒绝初始化并强行拦截。

  2. TS 声明平替:直接在组件内进行了局部类型消解,无需担心打包爆红。

  3. Ajax 统一拦截器:确保 Luckysheet 内部拉取配置、初始化单元格(loadUrl)时,自动带上前端的Bearer Token凭证。

💻 共享编辑组件代码(views/LuckySheet/index.vue

<template> <div class="lucky-sheet-container"> <div class="sheet-header"> <div class="left-box"> <button class="back-btn" @click="goBack"> <span class="icon">←</span> 返回列表 </button> <span class="file-title">{{ fileName }}</span> <span class="status-tag">多人在线协同中</span> </div> <div class="right-box"> </div> </div> <div class="sheet-body"> <div ref="sheetContainer" class="luckysheet-canvas"></div> </div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' // 💡 强行引入 node_modules 中的 luckysheet 主包与样式 // @ts-ignore import luckysheet from 'luckysheet' import 'luckysheet/dist/plugins/css/pluginsCss.css' import 'luckysheet/dist/plugins/plugins.css' import 'luckysheet/dist/css/luckysheet.css' import 'luckysheet/dist/assets/iconfont/iconfont.css' const route = useRoute() const router = useRouter() // DOM 容器引用 const sheetContainer = ref<HTMLDivElement | null>(null) const fileName = ref<string>((route.query.name as string) || '汽车企业数据.xlsx') // 核心动态凭证(动态响应路由传参) // 期望路由结构如: /luckysheet/edit/:wbId/:userId const wbId = ref<string>(route.params.wbId as string) const userId = ref<string>(route.params.userId as string) /** * 安全凭证获取(统一登录 Token) */ const getActiveToken = (): string => { return localStorage.getItem('token') || '' } /** * 安全退场:释放内存,销毁协同 */ const destroySheetInstance = () => { if (luckysheet && luckysheet.destroy) { try { luckysheet.destroy() } catch (e) { console.warn('Luckysheet 实例销毁异常:', e) } } } /** * 初始化 Luckysheet 协同画布 */ const initLuckySheet = async () => { await nextTick() // 1. 安全卡点:未登录或容器不存在,直接拒绝初始化 const token = getActiveToken() if (!sheetContainer.value || !token) return // 2. 清理旧实例,防止多次握手 destroySheetInstance() const host = window.location.host const protocol = window.location.protocol const baseHttp = `${protocol}//${host}` // 3. 核心配置注入 luckysheet.create({ container: sheetContainer.value, // 绑定 ref 节点而非 String ID,规避 Vite 路由跳转下的 DOM 冲突 title: fileName.value, lang: 'zh', allowUpdate: true, // 核心:开启基于 WebSocket 的多人协同变更广播 // 工具栏裁剪:隐藏无用或易引发大体积数据崩溃的按钮(对应老项目配置) showtoolbarConfig: { print: false, chart: false }, // 【后端鉴权闭环 ①】:HTTP 初始化拉取数据接口,追加 URL 动态参数 loadUrl: `${baseHttp}/load/${wbId.value}/${userId.value}`, loadSheetUrl: `${baseHttp}/loadSheet/${wbId.value}/${userId.value}`, // 【后端鉴权闭环 ②】:WebSocket 握手通道,以 Query 形式强行携带 Token updateUrl: `ws://${host}/ws/${userId.value}/${wbId.value}?token=${encodeURIComponent(token)}`, // 【后端鉴权闭环 ③】:底层 Ajax 拦截钩子,为 Luckysheet 内部所有原生三方 XHR 请求补全 Authorization 请求头 hook: { beforeAjaxSend: (xhr: XMLHttpRequest) => { xhr.setRequestHeader('Authorization', `Bearer ${token}`) } } }) } /** * 返回列表页 */ const goBack = () => { router.push('/views/LuckySheet/index') // 根据你实际的列表页路由修改 } // 监听路由参数变化(例如在协同页面直接切换编辑另一个表格) watch([() => route.params.wbId, () => route.params.userId], ([newWbId, newUserId]) => { if (newWbId && newUserId) { wbId.value = newWbId as string userId.value = newUserId as string initLuckySheet() } }) onMounted(() => { // 【前端路由级安全防护】:无登录态强行拦截,直接重定向至登录页,阻断裸 URL 越权 if (!getActiveToken()) { console.error('检测到未授权流窜,正在拦截并重定向...'); router.push('/login') return } initLuckySheet() }) onUnmounted(() => { destroySheetInstance() }) </script> <style scoped> .lucky-sheet-container { width: 100%; height: 100vh; display: flex; flex-direction: column; background-color: #f5f7fa; overflow: hidden; } /* 顶部状态条样式 */ .sheet-header { height: 48px; background-color: #ffffff; border-bottom: 1px solid #e4e7ed; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03); z-index: 10; } .left-box { display: flex; align-items: center; gap: 16px; } .back-btn { display: flex; align-items: center; gap: 4px; padding: 6px 12px; font-size: 14px; color: #606266; background: #ffffff; border: 1px solid #dcdfe6; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .back-btn:hover { color: #409eff; border-color: #c0c4cc; background-color: #f5f7fa; } .file-title { font-size: 15px; font-weight: 600; color: #303133; } .status-tag { font-size: 12px; background-color: #e1f3d8; color: #67c23a; padding: 2px 8px; border-radius: 10px; border: 1px solid #e1f3d8; } /* Luckysheet 画布主体区域 */ .sheet-body { flex: 1; position: relative; width: 100%; background-color: #ffffff; } .luckysheet-canvas { margin: 0; padding: 0; position: absolute; width: 100%; height: 100%; left: 0; top: 0; } </style>

💡 针对此组件的系统配套说明:

1、列表页(views/LuckySheet/index.vue)如何调用并跳转进来:

在你的 Excel 列表页中,当点击“编辑文件”或者“新建文件”时,不再使用window.open,而是直接通过 Vue Router 进行站内传参:

const handleEditExcel = (row: any) => { router.push({ name: 'LuckySheetEdit', // 对应你配置的编辑页路由配置名 params: { wbId: row.id, userId: currentUser.id }, query: { name: row.fileName // 动态传递文件名 } }) }

2、完美规避全局ID重复冲突:

老代码里使用的是字符串container: 'luckysheet-share-logo'。在单页面应用(SPA)中,如果多次进出路由,可能会因为旧 DOM 未及时销毁导致document.getElementById拿错节点。这里改用 Vue 3 的ref对象container: sheetContainer.value,Luckysheet 会直接绑定该虚拟节点,非常安全。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询