219 lines
12 KiB
Markdown
219 lines
12 KiB
Markdown
# 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`:
|
||
- `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''<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` 新增:
|
||
```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
|
||
<!-- 业务页(如通知管理) -->
|
||
<FileManager
|
||
v-model="showFile"
|
||
module-code="oa_notices"
|
||
:father="rowId"
|
||
/>
|
||
```
|
||
- 业务页**只需传 `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`)挂载 `<FileManager>`,验证上传/列表/分类过滤/预览/删除全链路。
|
||
- [ ] 确认 bigint ID 前端为字符串、预览 URL 鉴权正常。
|
||
|
||
---
|
||
|
||
## 7. 注意事项(沿用开发规范)
|
||
- 不新增业务专属数据端点(文件端点属通用基础设施例外,已在 §1 论证)。
|
||
- 不手动 `import` antdv-next 组件(vite resolver 已自动导入)。
|
||
- 不引入新 vue-i18n;文案沿用 DB 驱动 i18n(必要时走 `b_i18n`)。
|
||
- 改动保持聚焦,不触碰无关文件;保留用户未提交的工作。
|
||
- 交付时说明是否运行了测试、是否执行了 build(默认不主动 build/起服务,除非用户要求)。
|