Luckysheet 内网部署教程:后端生成 Excel,前端只读预览

Luckysheet 内网部署教程:后端生成 Excel,前端只读预览 Luckysheet 内网部署教程后端生成 Excel前端只读预览一、场景说明在企业后台管理系统中常见需求是后端根据业务数据生成 Excel 报表前端直接从接口下载该文件并进行在线预览用户只能查看、不能编辑。典型场景包括财务报表生成与查阅数据报表导出与审批系统日志导出查看用户数据报表预览本文聚焦于内网部署场景所有资源均为本地化不依赖外网 CDN。后端使用Spring Boot EasyExcel生成.xlsx文件前端使用Luckysheet Luckyexcel进行只读预览。二、整体架构与数据流┌─────────┐ 请求导出 ┌─────────────┐ 生成Excel ┌─────────────┐ │ 前端 │ ──────────────────▶ │ 后端接口 │ ──────────────────▶ │ Excel文件 │ │ (Vue3) │ │ (SpringBoot)│ │ (.xlsx) │ └─────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │◀─────────────────────────────────│ │ │ 返回文件流(Blob) │ │ │ │ │ 接收Blob并解析 │ │ │ 使用Luckyexcel转换为Luckysheet格式│ │ │ 只读渲染预览 │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────────┐ ┌─────────────┐ │ 用户 │ │ Luckysheet │ │ Luckyexcel │ │ 查看 │ │ 只读预览 │ │ 解析xlsx │ └─────────┘ └─────────────┘ └─────────────┘核心流程前端发起导出请求→ 调用后端接口后端生成 Excel 文件→ 使用 EasyExcel 或 Apache POI 将数据写入.xlsx以文件流形式返回前端接收 Blob 数据→ 将二进制数据转换为File对象Luckyexcel 解析→ 将.xlsx转换为 Luckysheet 可识别的 JSON 数据只读渲染→ 以allowEdit: false模式展示表格用户仅可查看三、后端Spring Boot 生成 Excel3.1 添加依赖!-- Maven 依赖 --dependencygroupIdcom.alibaba/groupIdartifactIdeasyexcel/artifactIdversion3.3.4/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencyEasyExcel 是阿里开源的 Excel 处理库性能优于 Apache POI适合大量数据导出场景 。3.2 定义导出数据实体// UserExportVO.javapackagecom.example.excel.vo;importcom.alibaba.excel.annotation.ExcelProperty;importlombok.Data;DatapublicclassUserExportVO{ExcelProperty(value用户ID,index0)privateLonguserId;ExcelProperty(value用户名,index1)privateStringuserName;ExcelProperty(value手机号,index2)privateStringphone;ExcelProperty(value注册时间,index3)privateStringregisterTime;}3.3 后端导出接口// ExcelExportController.javapackagecom.example.excel.controller;importcom.alibaba.excel.EasyExcel;importcom.example.excel.vo.UserExportVO;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.net.URLEncoder;importjava.util.ArrayList;importjava.util.List;RestControllerRequestMapping(/excel)publicclassExcelExportController{GetMapping(/export)publicvoidexportUserList(HttpServletResponseresponse)throwsIOException{// 1. 模拟数据实际从数据库获取ListUserExportVOdataListgetMockData();// 2. 设置响应头response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet);response.setCharacterEncoding(utf-8);// 处理中文文件名乱码StringfileNameURLEncoder.encode(用户数据报表,UTF-8).replaceAll(\\,%20);response.setHeader(Content-disposition,attachment;filename*utf-8fileName.xlsx);// 3. 写入 Excel 到响应输出流EasyExcel.write(response.getOutputStream(),UserExportVO.class).sheet(用户信息).doWrite(dataList);}privateListUserExportVOgetMockData(){ListUserExportVOlistnewArrayList();for(inti1;i10;i){UserExportVOusernewUserExportVO();user.setUserId((long)i);user.setUserName(用户i);user.setPhone(188888888String.format(%02d,i));user.setRegisterTime(2026-07-String.format(%02d,i));list.add(user);}returnlist;}}后端返回文件流的关键配置Content-Type必须设置为application/vnd.openxmlformats-officedocument.spreadsheetml.sheetContent-Disposition用于告知浏览器下载文件名 。四、前端Excel 只读预览4.1 资源本地化内网部署将 Luckysheet 和 Luckyexcel 的静态资源下载到项目的public/luckysheet/目录下public/luckysheet/ ├── css/ │ └── luckysheet.css ├── plugins/ │ ├── css/ │ │ └── pluginsCss.css │ └── plugins.js ├── assets/ │ └── iconfont/ ├── luckyexcel.umd.js └── luckysheet.umd.js4.2 在index.html中引入资源!DOCTYPEhtmlhtmlhead!-- 样式顺序固定 --linkrelstylesheethref/luckysheet/plugins/css/pluginsCss.css/linkrelstylesheethref/luckysheet/plugins/plugins.css/linkrelstylesheethref/luckysheet/css/luckysheet.css/linkrelstylesheethref/luckysheet/assets/iconfont/iconfont.css/!-- 脚本顺序固定 --scriptsrc/luckysheet/plugins/plugins.js/scriptscriptsrc/luckysheet/luckysheet.umd.js/script/head/html4.3 创建ExcelPreview.vue组件template div classpreview-wrapper !-- 加载状态 -- div v-ifloading classloading-overlay span classloading-spinner⏳/span p{{ loadingText }}/p /div !-- 预览工具栏 -- div v-ifhasData classtoolbar span classfile-name {{ fileName }}/span span classsheet-count{{ sheetCount }} 个工作表/span button classbtn-download clickhandleDownload 下载原始文件/button button classbtn-clear clickhandleClear清空/button /div !-- 空状态 -- div v-if!hasData !loading classempty-state div classempty-icon/div h3暂无数据/h3 p点击下方按钮从后端获取报表/p button classbtn-load clickfetchExcel加载报表/button /div !-- 表格容器 -- div refcontainerRef classsheet-container/div /div /template script setup import { ref, onBeforeUnmount, nextTick } from vue; import axios from axios; const props defineProps({ apiUrl: { type: String, default: /excel/export // 后端接口地址 }, height: { type: String, default: 600px } }); const emit defineEmits([loaded, error, downloaded]); const containerRef ref(null); const loading ref(false); const loadingText ref(正在生成报表...); const hasData ref(false); const fileName ref(); const sheetCount ref(0); let instance null; let currentBlob null; // 存储原始文件 Blob供下载使用 // 销毁表格 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; // 关键只读预览配置 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, // 禁止新增 Sheet menu: false, sheet: true // 保留切换标签 }, contextMenu: [ { text: 复制, onclick: () {} } ], // 禁止编辑快捷键 hook: { cellMousedown() { return false; } } }); hasData.value true; fileName.value name; sheetCount.value exportJson.sheets.length; loading.value false; emit(loaded, { data: exportJson, name }); }); } // 从后端获取 Excel async function fetchExcel() { loading.value true; loadingText.value 正在生成报表...; hasData.value false; try { const response await axios.get(props.apiUrl, { responseType: blob, timeout: 60000 }); // 检查响应类型 const contentType response.headers[content-type] || ; if (!contentType.includes(sheet) !contentType.includes(octet-stream)) { const text await response.data.text(); const error JSON.parse(text); throw new Error(error.message || 导出失败); } // 从 Content-Disposition 提取文件名 const disposition response.headers[content-disposition] || ; let name 报表.xlsx; const match disposition.match(/filename\*(?:UTF-8|utf-8)(.)/); if (match) { name decodeURIComponent(match[1]); } else { const simpleMatch disposition.match(/filename(.)/); if (simpleMatch) { name decodeURIComponent(simpleMatch[1].replace(//g, )); } } currentBlob response.data; loadingText.value 正在解析文件...; // 将 Blob 转换为 File 对象 const file new File([response.data], name, { type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet }); // 使用 Luckyexcel 解析 if (!window.LuckyExcel) { // 如果未全局引入动态加载 const script document.createElement(script); script.src /luckysheet/luckyexcel.umd.js; script.onload () parseFile(file, name); document.head.appendChild(script); } else { parseFile(file, name); } } catch (error) { console.error(加载失败:, error); loading.value false; emit(error, new Error(获取报表失败: error.message)); } } function parseFile(file, name) { window.LuckyExcel.transformExcelToLucky( file, (exportJson) renderSheet(exportJson, name), (err) { console.error(解析失败:, err); loading.value false; emit(error, new Error(文件解析失败: err.message)); } ); } // 下载原始 Excel 文件 function handleDownload() { if (!currentBlob) return; const link document.createElement(a); link.href URL.createObjectURL(currentBlob); link.download fileName.value || 报表.xlsx; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); emit(downloaded, fileName.value); } function handleClear() { destroySheet(); hasData.value false; fileName.value ; sheetCount.value 0; currentBlob null; if (containerRef.value) containerRef.value.innerHTML ; } // 暴露方法 defineExpose({ fetchExcel, handleClear, destroySheet, hasData }); onBeforeUnmount(() { destroySheet(); }); /script style scoped .preview-wrapper { position: relative; width: 100%; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.08); } /* 加载遮罩 */ .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10; } .loading-spinner { font-size: 48px; animation: spin 1.5s linear infinite; } keyframes spin { 100% { transform: rotate(360deg); } } .loading-overlay p { margin-top: 16px; color: #666; font-size: 16px; } /* 空状态 */ .empty-state { padding: 80px 20px; text-align: center; background: #fafafa; } .empty-icon { font-size: 64px; margin-bottom: 16px; } .empty-state h3 { font-size: 20px; color: #333; margin-bottom: 8px; } .empty-state p { color: #999; font-size: 14px; margin-bottom: 24px; } .btn-load { padding: 10px 32px; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border: none; border-radius: 6px; font-size: 15px; cursor: pointer; transition: 0.3s; } .btn-load: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; } .file-name { font-weight: 500; color: #333; } .sheet-count { color: #999; font-size: 13px; } .toolbar .btn-download, .toolbar .btn-clear { 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; } .toolbar .btn-download { margin-left: auto; } .toolbar .btn-download:hover { border-color: #667eea; color: #667eea; } .toolbar .btn-clear:hover { border-color: #ff4d4f; color: #ff4d4f; } .sheet-container { width: 100%; height: v-bind(height); min-height: 400px; background: #fff; } /style4.4 在页面中使用template div classpage h1 数据报表预览/h1 p classsubtitle报表由后端生成前端仅可查看/p ExcelPreview refpreviewRef api-url/excel/export height650px loadedonLoaded erroronError downloadedonDownloaded / div v-iferrorMsg classerror-message ⚠️ {{ errorMsg }} button clickerrorMsg classclose-btn×/button /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); } function onDownloaded(name) { console.log( 已下载:, name); } // 也可通过代码触发加载 function loadReport() { previewRef.value?.fetchExcel(); } /script style scoped .page { max-width: 1400px; margin: 0 auto; padding: 20px; } .page h1 { font-size: 24px; margin-bottom: 4px; } .subtitle { color: #999; font-size: 14px; margin-bottom: 20px; } .error-message { margin-top: 16px; padding: 12px 16px; background: #fff2f0; border: 1px solid #ffccc7; border-radius: 6px; color: #ff4d4f; display: flex; justify-content: space-between; align-items: center; } .close-btn { background: none; border: none; font-size: 20px; cursor: pointer; color: #999; } .close-btn:hover { color: #ff4d4f; } /style五、接口规范与配置详解5.1 后端导出接口规范项目说明请求方式GETURL/excel/export响应类型application/vnd.openxmlformats-officedocument.spreadsheetml.sheet响应头Content-Disposition: attachment;filename*utf-8报表名称.xlsx响应体Excel 文件二进制流5.2 只读预览核心配置配置项值作用allowEditfalse禁止所有编辑操作showtoolbarfalse隐藏顶部功能栏showinfobarfalse隐藏信息栏sheetFormulaBarfalse隐藏公式输入栏enableAddRow/enableAddColfalse禁止增删行列showsheetbarConfig.addfalse禁止新增工作表hook.cellMousedown() false禁止单元格点击编辑六、常见问题与解决方案6.1 文件名中文乱码问题下载的文件名显示为乱码。解决使用URLEncoder.encode()编码并替换为%20。StringfileNameURLEncoder.encode(用户报表,UTF-8).replaceAll(\\,%20);response.setHeader(Content-disposition,attachment;filename*utf-8fileName.xlsx);6.2 前端解析 Blob 失败问题LuckyExcel.transformExcelToLucky报错提示不是有效 xlsx。解决确保后端返回的 Content-Type 正确前端请求时添加responseType: blob。constresponseawaitaxios.get(apiUrl,{responseType:blob// 关键});6.3 大文件导出超时问题数据量大时请求超过默认超时时间。解决增加前端超时配置后端使用 EasyExcel 的流式写入。// 前端constresponseawaitaxios.get(apiUrl,{responseType:blob,timeout:120000// 120秒});6.4 内网资源加载失败问题浏览器控制台报 404找不到 CSS 或 JS 文件。解决确认资源已放入public/luckysheet/目录确认index.html中路径正确以/luckysheet/开头检查目录结构是否完整含assets/iconfont/字体文件七、总结本方案实现了后端生成 Excel → 前端只读预览的完整内网部署流程层级技术选型职责后端Spring Boot EasyExcel根据业务数据生成.xlsx文件流前端预览Luckysheet只读展示 Excel 内容文件解析Luckyexcel将 Blob 转换为 Luckysheet 数据格式传输方式REST API Blob文件流下载与前端解析核心要点后端返回文件流前端通过responseType: blob接收Luckyexcel 解析 Blob转为 Luckysheet 可识别的 JSON只读预览模式allowEdit: false是核心配置所有资源本地化适应内网部署场景这样就完整实现了后端渲染生成 Excel前端下载后只做预览的需求。