# 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`,业务页只用一行调用:``。 新 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`: - `subid` bigint(主键,Snowflake ID) - `father` bigint(业务单据 ID,如合同/通知 b_id) - `mx_xh` int(显示序号) - `mx_filename` nvarchar(500)(原始文件名) - `mx_filesize` varchar(50) - `mx_fileext` varchar(50) - `mx_mapfilename` nvarchar(500)(**新系统存服务端存储文件名,如 `1234567890.pdf`,不再存阿里云 URL**) - `mx_moduleid` bigint(FK → `s_module.b_id`,派生字段) - `mx_cutfilename`、`mx_cutfilesize`(缩略图,可选) - `mx_cate_id` bigint(→ `bf_files_cateid.b_id`,分类) - `mx_bz` nvarchar(500) - `mx_orderid`、`mx_sh`、`mx_shdatetime`、`mx_shuser_id` - `mx_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-data` - `file`(必填,二进制) - `father`(必填,bigint 字符串,业务单据 ID) - `moduleCode`(必填,如 `oa_notices`) - `cateId`(可选,bigint 字符串 → `mx_cate_id`) - `bz`(可选) - 处理流程: 1. 校验 JWT(过 `JwtAuthFilter`),取 `OrgContext.orgId` 选库。 2. 用 `IdGenerator` 生成 `subid`(Snowflake)。 3. 解析 `moduleCode` → `s_module.b_id`(查 `s_module` 表;查不到则报错 400)。 4. 计算扩展名 `ext`、存储名 `{subid}.{ext}`,写字节到 `fms.file.storage-dir`。 5. **只写磁盘文件,不写 `bf_files` 行**(行由前端用 `saveObjectApi` 批量保存,便于统一事务/权限)。 - 响应(JSON): ```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''`(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` 新增: ```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` 备用(上传时带回)。 - 工具栏按钮(上传/下载/删除)按 `moduleCode` 查 `b_user_power` 控制显隐/禁用(前端权限,沿用现有 `permissions` store 模式)。 **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` 过滤。 - 若 `categories` prop 已提供则直接用,否则自行加载。 ### 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)`(或 `saveObjectApi` deletes + 端点删磁盘)。 ### 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. 业务页调用示例 ```vue ``` - 业务页**只需传 `module-code` 与 `father`**,不再传 moduleid。 - 只读场景:`readonly` + `show-tree` 可组合使用。 --- ## 6. 落地步骤清单(执行顺序) 1. **后端** - [ ] `application.yaml` 增加 `fms.file.storage-dir`,并把该目录加入 `.gitignore`。 - [ ] 新建 `FileController`(或并入现有 controller 包),实现 `upload` / `GET {subid}` / `DELETE {subid}` 三个端点,复用 `OrgContext` + `DbUtils` + `IdGenerator`。 - [ ] 实现本地磁盘存储(`StorageService`,预留 `OssStorageAdapter` 接口)。 - [ ] `moduleCode → b_id` 解析逻辑(查 `s_module`)。 2. **前端服务** - [ ] `src/services/fileService.js` 五个方法。 3. **前端组件** - [ ] `FileCategoryTree.vue`(左树) - [ ] `FileList.vue`(中栏列表+操作,含写元数据/删除) - [ ] `FileUpload.vue`(a-upload 包装) - [ ] `FileManager.vue`(三栏容器 + 权限 + moduleCode 解析 + 右栏预览) 4. **集成验证** - [ ] 选一个业务页(如通知 `oa_notices`)挂载 ``,验证上传/列表/分类过滤/预览/删除全链路。 - [ ] 确认 bigint ID 前端为字符串、预览 URL 鉴权正常。 --- ## 7. 注意事项(沿用开发规范) - 不新增业务专属数据端点(文件端点属通用基础设施例外,已在 §1 论证)。 - 不手动 `import` antdv-next 组件(vite resolver 已自动导入)。 - 不引入新 vue-i18n;文案沿用 DB 驱动 i18n(必要时走 `b_i18n`)。 - 改动保持聚焦,不触碰无关文件;保留用户未提交的工作。 - 交付时说明是否运行了测试、是否执行了 build(默认不主动 build/起服务,除非用户要求)。