Vue3集成“luckysheet”实现Excel在线编辑

目录

[📝 建议在方案中补充的"技术技术落地补丁":](#📝 建议在方案中补充的“技术技术落地补丁”:)

[💻 共享编辑组件代码(views/LuckySheet/index.vue)](#💻 共享编辑组件代码(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 停止维护、打包笨重的问题。

安装方式

bash 复制代码
pnpm add @univerjs/core @univerjs/sheets @univerjs/sheets-collaboration

适配你的业务优势

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

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

现状

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

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

bash 复制代码
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。

  • 跳转逻辑改造:
TypeScript 复制代码
// 原来不安全写法
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 地址:

TypeScript 复制代码
updateUrl: "ws://" + localurl + "/ws/" + "${wb.userId}" + "/${wb.id}"

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

TypeScript 复制代码
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 静态 html pnpm 安装 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

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

TypeScript 复制代码
pnpm config set registry http://xxx

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

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

TypeScript 复制代码
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 鉴权、全屏容器

html 复制代码
<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

TypeScript 复制代码
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

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

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

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

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

TypeScript 复制代码
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 路径冲突报错

安装前清理旧依赖:

bash 复制代码
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 配置(根据部署路径调整):

TypeScript 复制代码
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 类型声明文件:

TypeScript 复制代码
declare module 'luckysheet' {
  const luckysheet: any
  export default luckysheet
}

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

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

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

TypeScript 复制代码
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 全套样式资源,避免图标、表格样式缺失:

TypeScript 复制代码
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 都会直接闭嘴,不再爆红:

TypeScript 复制代码
// 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 转换:

TypeScript 复制代码
// 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

html 复制代码
<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 进行站内传参:

TypeScript 复制代码
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 会直接绑定该虚拟节点,非常安全。