表单与文件联动:公文/合同在线预览 + 签章 + 套红实现方案
🌐演示地址:http://ruoyioffice.com | 📦源码1:ruoyi-office-vben | 📦源码2:ruoyi-office | 📦源码3:ruoyi-office | 💬微信:17156169080(备注「RuoYi Office」)
审批表单里挂了 5 个附件,领导点开全是下载——这是 OA/合同系统里最常见的体验断层。公文正文要套红预览、合同 PDF 要在线核对、用印材料要留痕,预览能力不是锦上添花,而是业务闭环的硬门槛。RuoYi Office 在
attachment-list.vue里用「双模预览」统一解决:图片/PDF/Office/视频按扩展名分流;KKFileView 未配置时 docx/xlsx 走 vue-office 零部署;配置了VITE_GLOB_KKFILEVIEW_URL后旧版 Office 格式全兜底。本文把这条链路从组件到公文 GB/T 9704 套红、用印签章完整拆开。
▲ 双模预览决策流程:扩展名分流 → 原生/vue-office/KKFileView 三路径 → 业务层套红/签章/合同复用同一 AttachmentList 组件
引言:表单与文件联动到底难在哪?
AttachmentList(附件列表组件)是 RuoYi Office 里连接「业务表单」与「文件存储」的通用桥梁——请假单、合同、用印申请、公文发文都复用它。难点不在上传,而在预览:
痛点一:格式碎片化。一张审批单可能同时有扫描件 PDF、Word 正文、Excel 明细、现场照片——每种格式的浏览器能力不同,不能「一个 iframe 打天下」。
痛点二:部署与能力的 trade-off。KKFileView 能预览 doc/ppt/xls 等旧格式,但要额外部署 LibreOffice 转码服务;中小团队往往希望「先跑起来」,再按需增强。
痛点三:私有文件鉴权。附件 URL 可能是 MinIO 预签名地址,也可能是带 Bearer Token 的后端/admin-api/infra/file/路径——预览前必须刷新签名、携带租户头,否则 iframe 里全是 401。
痛点四:公文套红是「渲染」不是「预览文件」。发文模块的正文预览来自套红模板 + 表单字段实时合成,符合GB/T 9704《党政机关公文格式》排版规范——这和「打开一个 docx 附件」是两条不同的技术路径。
痛点五:签章是流程节点,不是 UI 装饰。用印审批通过后才有「确认用印」操作;印章图片来自台账,和公文套红模板里的seal_image_url是同一套文件基础设施。
| 现状 | 后果 |
|---|---|
| 所有附件只能下载 | 审批人懒得看,合规风险上升 |
| 只接 KKFileView | 小团队部署门槛高,POC 周期长 |
| 预览不带鉴权 | 私有桶链接过期、跨域失败 |
| 公文套红与附件预览混为一谈 | 产品方案不清晰,开发返工 |
结论前置:RuoYi Office 用getPreviewType(ext)做扩展名分流,默认 vue-office 覆盖 docx/xlsx,可选 KKFileView 增强;公文套红走独立DocPreview组件;业务表单统一挂载AttachmentList。
一、业务全景:三类「文件场景」
1.1 三类场景对照
| 场景 | 典型模块 | 预览对象 | 实现方式 |
|---|---|---|---|
| 附件预览 | 合同、用印、请假、报销 | 用户上传的 docx/pdf/图片 | attachment-list.vue双模预览 |
| 套红预览 | 公文发文 | 模板 + 表单字段合成的正式版式 | DocPreview+ GB/T 9704 排版 |
| 签章留痕 | 用印管理、公文签发 | 印章图片 + 审批状态 | 台账seal_image_url+ BPM 节点 |
1.2 组件路径与复用关系
核心源码路径:
ruoyi-office-vben/apps/web-antd/src/components/attachment-list/ ├── attachment-list.vue # 预览分流 + 表格 + 弹窗 ├── attachment-upload-modal.vue ├── data.ts # 列定义与操作按钮 └── index.ts环境变量(KKFileView 可选):
VITE_GLOB_KKFILEVIEW_URL=https://preview.example.com未配置时kkFileViewUrl为空字符串,Office 旧格式无法在线预览时会提示下载——不影响 docx/xlsx 的 vue-office 路径。
二、双模预览架构设计
2.1 设计目标
| 决策点 | 方案 | 理由 |
|---|---|---|
| 默认 Office 预览 | vue-office(docx/xlsx) | 纯前端、零额外服务、POC 10 分钟可演示 |
| 全格式增强 | KKFileView iframe | doc/ppt/xls 等旧格式、复杂排版 |
| 浏览器 iframe | 原生能力足够 | |
| 图片 | Ant Design Image | 轻量 |
| 不支持格式 | 自动下载 | 避免空白弹窗 |
| 私有文件 | fetch + Token + 预签名刷新 | KKFileView 需完整 HTTP URL |
2.2 预览类型枚举
组件内部定义PreviewType,由扩展名映射:
typePreviewType=|'audio'|'docx'|'excel'|'iframe'|'image'|'kkfileview'|'none'|'text'|'video';分流规则(结论):
- 图片扩展名 →
image - pdf →
iframe - 视频/音频 →
video/audio - 文本类(txt/json/md/sql…)→
text - docx →
docx(vue-office,优先于 KKFileView) - xlsx →
excel(vue-office) - 若配置了
kkFileViewUrl,其余 Office 扩展名 →kkfileview - 都不匹配 →
none(提示并下载)
2.3 双模决策流程(文字版)
用户点击预览 ↓ 解析 fileExtension / fileName ↓ getPreviewType(ext) ├─ 图片/PDF/音视频/文本 → 浏览器原生能力 ├─ docx/xlsx → fetch Blob → VueOfficeDocx / VueOfficeExcel ├─ kkFileViewUrl 已配置且为 Office 类 → iframe → KKFileView └─ none → message.info + handleDownload()三、AttachmentList 核心实现
3.1 扩展名集合与分流函数
组件顶部声明扩展名白名单,避免魔法字符串散落:
constDOCX_EXTS=newSet(['docx']);constEXCEL_EXTS=newSet(['xlsx']);constOFFICE_EXTS=newSet(['csv','doc','docx','ppt','pptx','xls','xlsx',]);functiongetPreviewType(ext?:string):PreviewType{conste=(ext||'').toLowerCase();if(IMAGE_EXTS.has(e))return'image';if(PDF_EXTS.has(e))return'iframe';if(VIDEO_EXTS.has(e))return'video';if(AUDIO_EXTS.has(e))return'audio';if(TEXT_EXTS.has(e))return'text';if(DOCX_EXTS.has(e))return'docx';if(EXCEL_EXTS.has(e))return'excel';if(kkFileViewUrl){if(OFFICE_EXTS.has(e))return'kkfileview';return'kkfileview';}return'none';}设计要点:docx/xlsx 在 KKFileView 判断之前 return,保证「零部署模式」下新 Office 格式依然可预览。
3.2 私有 URL 与预签名刷新
getAccessibleUrl处理三类地址:
- 同源或
/admin-api/infra/file/→ 直接带 Token fetch - 有效 MinIO 预签名 → 原样使用
- 过期或外部 URL → 调
getFilePresignedGetUrl刷新
asyncfunctiongetAccessibleUrl(fileUrl:string):Promise<string>{if(!fileUrl||fileUrl.startsWith('blob:'))returnfileUrl;constparsed=newURL(fileUrl);if(isBackendFileUrl(parsed)){returnparsed.pathname+parsed.search;}if(parsed.searchParams.has('X-Amz-Signature')&&isValidPresignedUrl(parsed)){returnparsed.href;}try{returnawaitgetFilePresignedGetUrl(fileUrl);}catch{returnparsed.href;}}admin-api 文件请求自动附加 OAuth2 与多租户头:
functionbuildPreviewFetchOptions(fileUrl:string):RequestInit|undefined{if(!isAdminApiFileUrl(fileUrl))returnundefined;constaccessStore=useAccessStore();constheaders:Record<string,string>={};if(accessStore.accessToken){headers.Authorization=`Bearer${accessStore.accessToken}`;}if(tenantEnable&&accessStore.tenantId!==null){headers['tenant-id']=String(accessStore.tenantId);}return{headers};}3.3 vue-office 预览链路
docx/xlsx 不直接把 URL 塞给组件,而是fetch 成 Blob再渲染——这样可以走鉴权头,也避免跨域:
case'docx':case'excel':{constresp=awaitfetchOfficePreviewFile(accessUrl,row.fileUrl);previewBlob.value=awaitresp.blob();break;}模板中分别挂载:
<VueOfficeDocx v-else :src="previewBlob" @error="handleOfficePreviewError" /> <VueOfficeExcel v-else :src="previewBlob" @error="handleOfficePreviewError" />渲染失败时previewRenderFailed置 true,提示下载——不 silently 失败。
3.4 KKFileView 集成
KKFileView 要求从其容器内能 HTTP 访问源文件,因此需要完整 URL + Base64 编码:
functionbuildKKFileViewUrl(fileUrl:string):string{constfullUrl=getFullFileUrl(fileUrl);return`${kkFileViewUrl}/onlinePreview?url=${encodeURIComponent(btoa(unescape(encodeURIComponent(fullUrl))),)}`;}配置读取来自useAppConfig:
const{kkFileViewUrl}=useAppConfig(import.meta.env,import.meta.env.PROD);// 对应环境变量 VITE_GLOB_KKFILEVIEW_URL部署注意:KKFileView 容器必须能访问 MinIO/后端文件 URL;内网部署时常把文件域名与 KKFileView 放同一 VPC。
四、vue-office 零部署方案详解
4.1 适用场景
| 条件 | 推荐 |
|---|---|
| POC / 内网无外网 | vue-office 即可 |
| 仅 docx/xlsx 附件 | vue-office 足够 |
| 大量 doc/ppt/xls 老文件 | 建议加 KKFileView |
| 复杂 Excel 公式/宏 | 预览有限,建议下载或 KKFileView |
4.2 与 OnlyOffice / WOPI 对比
| 方案 | 部署成本 | 编辑能力 | 预览 fidelity |
|---|---|---|---|
| vue-office | ★ 零服务 | 只读 | docx/xlsx 中等 |
| KKFileView | ★★ 单 Docker | 只读 | 全格式较好 |
| OnlyOffice | ★★★ 集群 | 可协同编辑 | 高 |
RuoYi Office 当前定位是审批只读预览——编辑在业务表单或外部 Word 完成,系统侧重「看」而不是「改」。
4.3 依赖版本
前端使用@vue-office/docx与@vue-office/excelV3 分支,与 Vue 3.5 兼容。打包体积会增加,但可按路由/组件异步加载控制。
五、KKFileView 增强模式
5.1 何时启用
在.env或部署环境配置:
VITE_GLOB_KKFILEVIEW_URL=http://127.0.0.1:8012启用后,getPreviewType对 doc/ppt/xls/csv 等走kkfileview分支,iframe 全屏 80vh 展示。
5.2 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| KKFileView 空白 | 文件 URL 容器不可达 | 检查网络/域名/预签名 |
| 中文文件名乱码 | Base64 编码 | 组件已用encodeURIComponent + btoa |
| 401 | 私有桶未刷新签名 | 先getAccessibleUrl再拼预览 URL |
| docx 走 KKFileView 而非 vue-office | 不应发生 | docx 在分流里优先 return docx |
六、公文套红:GB/T 9704 实时预览
6.1 套红与附件预览的区别
套红预览不是打开上传的 docx,而是:
- 用户在发文表单选择套红模板(机关名称、发文字号前缀、印章图、分隔线)
- 填写标题、主送、正文、附件列表
- 右侧
DocPreview按GB/T 9704排版实时渲染红头、版心、落款
国标要点(简化):
| 要素 | 规范要求 | 系统实现 |
|---|---|---|
| 版头 | 机关名称、标志、发文字号 | 模板orgName、docPrefix |
| 主体 | 标题、主送、正文 | 表单字段绑定 |
| 版记 | 抄送、印发机关 | 可选扩展字段 |
| 印章 | 发文机关印章 | 模板seal_image_url |
6.2 套红模板数据模型
独立表oa_office_doc_template(示意):
| 字段 | 说明 |
|---|---|
org_name | 机关名称(红头大字) |
doc_prefix | 发文字号前缀,如「XX发」 |
seal_image_url | 印章图片,走统一 FileApi |
divider_style | 分隔线样式 |
发文单template_id外键关联——同一模板被多次复用,改模板即可统一升级红头样式。
6.3 左右分栏交互
详情页布局:
- 左侧:form-create / 自定义表单(套红模板选择弹窗、密级、紧急程度、主送部门、正文富文本、AttachmentList)
- 右侧:DocPreview 实时同步
选中模板后「HelpInput + 弹窗选择」支持搜索、双击快选——模板名与字号前缀自动回填,预览立即刷新。
6.4 正式 PDF 生成
审批签发通过后,后端根据同一套模板数据生成正式 PDF 归档——预览 DOM 与 PDF 生成共享字段模型,避免「预览一套、归档另一套」。
七、签章与用印:流程驱动的「盖印」
7.1 用印 vs 公文套红印章
| 维度 | 公文套红 | 用印申请 |
|---|---|---|
| 目的 | 版式展示 | 实物/外借印章 |
| 印章来源 | 套红模板图片 | 印章台账 |
| 触发 | 选模板即见 | BPM 审批通过后「确认用印」 |
| 冲突检测 | 无 | 外借时间段冲突算法 |
用印模块 2 张表 + 5 种状态机:审批通过前不能操作印章;外借需检测时间重叠——签章是业务状态,不是预览组件职责。
7.2 与 AttachmentList 的协作
用印申请单上的「用印文件」仍用 AttachmentList——审批人预览材料走本文双模链路;行政确认用印时核对的是同一份在线预览。
合同模块同理:合同正文附件、补充协议 PDF 在审批节点打开预览,减少下载往返。
八、后端文件服务要点
8.1 统一 FileApi
上传写入 MinIO(或本地),返回fileUrl;下载/预览统一走:
GET /admin-api/infra/file/{configId}/get/{path}(鉴权)getFilePresignedGetUrl刷新外部对象存储
8.2 多租户与 visit-tenant
预览 fetch 时传递tenant-id/visit-tenant-id,与网关 Security 过滤器一致——预览请求和 API 请求同一套租户上下文。
九、设计决策对比表
| 设计要点 | 传统 OA | RuoYi Office 方案 | 价值 |
|---|---|---|---|
| Office 预览 | 仅下载或单一 KKFileView | vue-office + 可选 KKFileView | 零部署与全格式可兼得 |
| 扩展名策略 | 硬编码 if-else | Set 集合 + PreviewType | 易扩展新格式 |
| 私有文件 | 直链常 403 | 预签名刷新 + Bearer | 预览成功率提升 |
| 公文 | 上传 Word 当正文 | GB/T 9704 套红实时预览 | 合规 + 体验 |
| 组件复用 | 每模块一套上传 | AttachmentList 统一 | 降维护成本 |
| 失败策略 | 白屏 | 明确提示 + 降级下载 | 可预期 |
十、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 双模预览 | getPreviewType 分流 | 部署灵活 |
| 零部署 Office | @vue-office/docx excel | POC 快 |
| 全格式兜底 | VITE_GLOB_KKFILEVIEW_URL | 老格式覆盖 |
| 鉴权预览 | fetch + OAuth2 + 租户头 | 私有桶可用 |
| 套红合规 | DocPreview + 模板表 | GB/T 9704 |
| 签章闭环 | 用印台账 + BPM | 一印一审 |
| 表单联动 | v-model 附件列表 | 全模块复用 |
十一、快速体验
- 在线演示:http://ruoyioffice.com/web/(admin / admin123)
- 推荐路径:
OA → 发文管理:选套红模板,看右侧 GB/T 9704 预览OA → 用印申请:上传 pdf/docx,点附件预览测双模链路CRM → 合同管理:合同附件预览- 本地不配 KKFileView,验证 docx/xlsx 仍可预览
- 配置
VITE_GLOB_KKFILEVIEW_URL后测 doc/ppt 老格式
本地启动:
cd W:\ruoyi-office\ruoyi-office-vben pnpm dev:antd源码:
| 仓库 | 地址 |
|---|---|
| 前端 | ruoyi-office-vben |
| 后端 | ruoyi-office |
结语
表单与文件的联动,本质是让审批人「看得见」。RuoYi Office 用 AttachmentList 的双模预览把 vue-office 的零部署与 KKFileView 的全格式增强组合在一起;公文套红则用 GB/T 9704 实时渲染解决「正式版式」;用印签章用 BPM 状态机守住合规底线。三者共用同一套文件基础设施,却各走最适合的 presentation 路径——这是「一套组件、多种场景」的典型落地方式。
你们现在预览是只用 KKFileView,还是前端 vue-office?公文套红有没有踩过「预览和 PDF 不一致」的坑?欢迎评论区交流。
常见问题(FAQ)
KKFileView 不配也能预览 Office 吗?
能。docx/xlsx 走@vue-office/docx/@vue-office/excel前端渲染,无需额外服务。doc/ppt/xls 等旧格式需要配置VITE_GLOB_KKFILEVIEW_URL或下载查看。
vue-office 和 KKFileView 会同时启用吗?
会协同而非互斥。docx/xlsx 固定走 vue-office;配置 KKFileView 后,其他 Office 扩展名走 iframe 预览。
公文套红预览和附件预览是同一个组件吗?
不是。套红是DocPreview按 GB/T 9704 合成版式;附件是attachment-list.vue打开已上传文件。发文表单可同时包含「正文套红预览 + 附件列表」。
预览私有 MinIO 文件 403 怎么办?
检查预签名是否过期、KKFileView 容器能否访问文件 URL;组件会先调getFilePresignedGetUrl刷新,admin-api 路径会自动带 Bearer Token。
支持在线编辑吗?
当前 AttachmentList 定位为只读预览。编辑在业务表单或外部 Word 完成;如需 OnlyOffice 协同编辑需另行集成。
💡想要体验 RuoYi Office 的强大功能?
🌐在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦源码仓库:GitCode | GitHub
💬技术咨询:添加微信17156169080,备注「RuoYi Office」
⭐如果觉得不错,请给个 Star 支持一下!