12 KiB
FMS 文件管理组件设计方案(三栏布局)
状态:待执行(计划稿) 目标:参考旧系统
g3file,为新 FMS 系统设计可复用的文件管理组件。 布局:左 = 分类树,中 = 文件列表 + 操作区,右 = 预览区。
1. 背景与新旧差异
旧系统 g3file 是分两层的组件:
- 参数化通用库
file-preview/g3-file(基于 Element Plus + 阿里云 OSS),支持modelValue、module-id、father、cate-id、readonly、search-condition、upload-config.aliyun、preview-url、global-permission、iconfont及多种回调。 - 业务包装层
g3hd/frontend/src/components/g3-file/index.vue,业务页只用一行调用:<G3File v-model="showFile" :father="id" module-id="oa_notices" />。
新 FMS 的差异与决策:
| 维度 | 旧系统 | 新系统决策 |
|---|---|---|
| 框架 | Vue3 + Element Plus | Vue3 + antdv-next(自动导入,勿手导) |
| 上传存储 | 阿里云 OSS | 后端本地磁盘,文件名 {subid}.{ext};预留 OssStorageAdapter 抽象便于将来切换 |
| moduleid | 存字符串业务 code(如 oa_notices) |
bf_files.mx_moduleid 为 bigint 外键 → s_module.b_id |
| 预览 | 自研/第三方 | 复用已安装的 @file-viewer/vue3-full(FileViewer,dashboard 已验证 withFullViewerOptions()) |
| 调用方式 | module-id 字符串 |
module-code(如 oa_notices);组件内解析为 b_id 写入 mx_moduleid |
关于 moduleid 的关键决策
- 保留
mx_moduleid字段,但它是派生字段,由组件根据module-code查s_module.b_id后写入。 - 业务页不单独传 moduleid,只传
module-code与father。组件内部完成 code → b_id 解析并随上传/保存一并提交。 - 说明:旧系统把字符串 code 直接当 moduleid 存,新系统
mx_moduleid是指向s_module.b_id的 bigint 外键,二者语义不同;用module-code解析可兼容旧习惯又满足新外键约束。
2. 数据库现状(已存在,无需新建表)
表 bf_files / bf_files_cateid / bf_files_type 已在迁移 017_create_contract_file_tables.sql 定义,字段如下(节选相关项):
bf_files:
subidbigint(主键,Snowflake ID)fatherbigint(业务单据 ID,如合同/通知 b_id)mx_xhint(显示序号)mx_filenamenvarchar(500)(原始文件名)mx_filesizevarchar(50)mx_fileextvarchar(50)mx_mapfilenamenvarchar(500)(新系统存服务端存储文件名,如1234567890.pdf,不再存阿里云 URL)mx_moduleidbigint(FK →s_module.b_id,派生字段)mx_cutfilename、mx_cutfilesize(缩略图,可选)mx_cate_idbigint(→bf_files_cateid.b_id,分类)mx_bznvarchar(500)mx_orderid、mx_sh、mx_shdatetime、mx_shuser_idmx_inputuser_id、mx_inputdatetime、mx_updateuser_id、mx_updatedatetime
索引:IX_bf_files_father、IX_bf_files_mx_cate_id、IX_bf_files_mx_shuser_id。
bf_files_cateid:b_id、b_name、b_xh、b_bz、b_canuse、b_files_type、b_code(唯一)等。
bf_files_type:b_id、b_name、b_module_id、b_module_name、b_xh 等。
注意:bigint ID(subid / father / mx_moduleid / mx_cate_id)经后端返回前端时必须序列化为字符串。
3. 后端新增文件端点(按开发规范例外处理)
通用端点(/api/data/...)无法处理二进制流与 multipart 上传,因此按规范例外新增与业务表无关的 3 个通用文件端点。需在新增前于回复中说明原因(已在本文 §1 论证)。
3.1 POST /api/file/upload
- 请求:
multipart/form-datafile(必填,二进制)father(必填,bigint 字符串,业务单据 ID)moduleCode(必填,如oa_notices)cateId(可选,bigint 字符串 →mx_cate_id)bz(可选)
- 处理流程:
- 校验 JWT(过
JwtAuthFilter),取OrgContext.orgId选库。 - 用
IdGenerator生成subid(Snowflake)。 - 解析
moduleCode→s_module.b_id(查s_module表;查不到则报错 400)。 - 计算扩展名
ext、存储名{subid}.{ext},写字节到fms.file.storage-dir。 - 只写磁盘文件,不写
bf_files行(行由前端用saveObjectApi批量保存,便于统一事务/权限)。
- 校验 JWT(过
- 响应(JSON):
{ "subid": "1234567890", "storedName": "1234567890.pdf", "mx_filename": "合同.pdf", "mx_filesize": "102400", "mx_fileext": "pdf", "mx_mapfilename": "1234567890.pdf" }
3.2 GET /api/file/{subid}
- 查
bf_files取mx_mapfilename,从storage-dir流式返回字节。 - 响应头:
Content-Type(按扩展名映射)、Content-Disposition: inline; filename*=UTF-8''<mx_filename>(inline 便于浏览器/pdf 预览)。 - 需过
JwtAuthFilter(带 Token 才能取)。
3.3 DELETE /api/file/{subid}
- 删磁盘文件 + 删
bf_files行(业务页也可改为走saveObjectApi的deletes[],二选一;推荐端点方式以便同时清理磁盘)。 - 返回
{ success: true }。
3.4 配置
fms-api/src/main/resources/application.yaml 新增:
fms:
file:
storage-dir: ./data/files # 绝对或相对(相对应用工作目录),需 git-ignore
storage-dir 目录加入 .gitignore。
4. 前端组件设计(三栏布局)
目录:fms-vue/src/components/file/
FileManager.vue 三栏容器(左树 / 中列表操作 / 右预览)
FileCategoryTree.vue 左栏:分类树(虚拟根“全部附件” + bf_files_cateid 节点)
FileList.vue 中栏:文件列表 + 工具栏(上传/下载/删除/刷新)
FileUpload.vue a-upload 包装(调用 fileService.upload)
复用:@file-viewer/vue3-full 的 FileViewer 用于右栏预览;antdv-next 的 a-upload / a-table / a-tree / a-button 等(自动导入)。
服务层:fms-vue/src/services/fileService.js。
4.1 FileManager.vue(容器)
- 三栏 flex 布局:左
FileCategoryTree(宽 ~220px,受showTree控制),中FileList(flex:1),右预览区(宽 ~40% 或固定 ~480px,仅当选中文件时显示)。 - 选中文件(来自
FileList的select事件)→ 右栏渲染FileViewer并传入预览 URL(fileService.getContentUrl(subid))。 - 负责
moduleCode→moduleId(b_id) 解析:挂载时调loadModuleConfiguration/ 通用loaddata查s_module得到b_id,下传FileList备用(上传时带回)。 - 工具栏按钮(上传/下载/删除)通过
permissions.canAction(moduleIdOrCode, actionCode)读取有效操作授权并控制显隐。
Props
| 名称 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
modelValue |
Boolean | 是 | — | 控制显隐(v-model) |
moduleCode |
String | 是 | — | 模块 code,如 oa_notices |
father |
[String, Number] | 是 | — | 业务单据 ID(bigint 字符串) |
moduleId |
[String, Number] | 否 | — | 已解析的 s_module.b_id;不传则组件内解析 |
cateId |
[String, Number] | 否 | — | 初始分类过滤 |
categories |
Array | 否 | — | 预置分类列表,避免请求树 |
readonly |
Boolean | 否 | false | 只读(隐藏上传/删除) |
showTree |
Boolean | 否 | true | 是否显示左栏分类树 |
Events
| 名称 | 载荷 | 说明 |
|---|---|---|
update:modelValue |
Boolean | v-model |
uploaded |
fileRow | 上传完成(含回写 bf_files 后的行) |
deleted |
subid | 删除完成 |
selected |
fileRow | null | 选中文件变化(驱动右栏预览) |
error |
Error | 异常 |
4.2 FileCategoryTree.vue(左栏)
- 虚拟根节点“全部附件”(值为
null或'__all__'),下挂bf_files_cateid节点(来自loaddata查bf_files_cateid,按b_xh排序)。 - 选中分类 emit
select(cateId),供FileList过滤。 - 若
categoriesprop 已提供则直接用,否则自行加载。
4.3 FileList.vue(中栏)
a-table展示当前father+(可选)cateId下的bf_files行(走pageDataApi/loadDataApi,view_name='bf_files')。- 工具栏:
FileUpload(上传)、下载、删除、刷新。 - 行选中 → emit
select(row)→ 容器驱动右栏。 - 上传完成后调用
saveObjectApi写bf_files行(inserts:[{table:'bf_files', key_field:'subid', ...}],带father/mx_moduleid/mx_filename/mx_filesize/mx_fileext/mx_mapfilename/mx_cate_id)。 - 删除:调
fileService.remove(subid)(或saveObjectApideletes + 端点删磁盘)。
4.4 FileUpload.vue
- 包装
a-upload(:before-upload拦截,手动调fileService.upload)。 - 入参:
father、moduleCode、moduleId、cateId;上传成功后把响应行 emit 给FileList继续写元数据。
4.5 fileService.js
封装以下方法(基于 src/services/http.js):
upload({file, father, moduleCode, cateId, bz})→POST /api/file/upload(multipart)getContentUrl(subid)→ 返回GET /api/file/{subid}的可访问 URL(需带 Token;若 axios 拦截器不自动带,则改用带 Token 的fetch/blob 下载后URL.createObjectURL喂给FileViewer)remove(subid)→DELETE /api/file/{subid}listFiles({father, cateId})→pageDataApi(bf_files)saveMeta(rows)→saveObjectApi
预览 URL 处理:因
FileViewer需要可直接访问的资源地址,而/api/file/{subid}需鉴权,推荐在fileService内用带Authorization的fetch取 blob 再createObjectURL,避免 Token 泄漏到 URL。
5. 业务页调用示例
<!-- 业务页(如通知管理) -->
<FileManager
v-model="showFile"
module-code="oa_notices"
:father="rowId"
/>
- 业务页只需传
module-code与father,不再传 moduleid。 - 只读场景:
readonly+show-tree可组合使用。
6. 落地步骤清单(执行顺序)
- 后端
application.yaml增加fms.file.storage-dir,并把该目录加入.gitignore。- 新建
FileController(或并入现有 controller 包),实现upload/GET {subid}/DELETE {subid}三个端点,复用OrgContext+DbUtils+IdGenerator。 - 实现本地磁盘存储(
StorageService,预留OssStorageAdapter接口)。 moduleCode → b_id解析逻辑(查s_module)。
- 前端服务
src/services/fileService.js五个方法。
- 前端组件
FileCategoryTree.vue(左树)FileList.vue(中栏列表+操作,含写元数据/删除)FileUpload.vue(a-upload 包装)FileManager.vue(三栏容器 + 权限 + moduleCode 解析 + 右栏预览)
- 集成验证
- 选一个业务页(如通知
oa_notices)挂载<FileManager>,验证上传/列表/分类过滤/预览/删除全链路。 - 确认 bigint ID 前端为字符串、预览 URL 鉴权正常。
- 选一个业务页(如通知
7. 注意事项(沿用开发规范)
- 不新增业务专属数据端点(文件端点属通用基础设施例外,已在 §1 论证)。
- 不手动
importantdv-next 组件(vite resolver 已自动导入)。 - 不引入新 vue-i18n;文案沿用 DB 驱动 i18n(必要时走
b_i18n)。 - 改动保持聚焦,不触碰无关文件;保留用户未提交的工作。
- 交付时说明是否运行了测试、是否执行了 build(默认不主动 build/起服务,除非用户要求)。