--- title: Attachment 附件管理器 description: 分类 / 列表(可选预览)的附件管理面板,上传(选择、拖拽、粘贴)、批量下载删除与分页 --- # Attachment 附件管理器 `Attachment` 是「一个业务对象的附件集合」管理器:左栏分类、中栏文件(卡片 / 列表双视图),支持选择、拖拽、粘贴上传,多选批量下载 / 删除与分页。右栏预览默认关闭,需要时 `show-preview` 打开。 它**不请求任何接口**,也不认识「模块 / 业务主键」这类概念:数据从 `files` / `categories` 进来,用户意图以事件出去;需要组件代跑请求时,传 `uploadRequest` / `removeRequest` 即可。 ## 何时使用 - 需要管理某个业务单据 / 项目的附件集合(上传、分类、下载、删除); - 文件可能有很多,需要分类过滤、分页、批量操作; - 只需要一个简单的「选择文件」入口时,用 [Input](/ui/form/input) 或原生 `` 更轻。 ## 基础用法 给 `files`(附件)和 `categories`(分类,可选)两份数据,上传与删除交给业务: ## 多选与批量操作 没勾选时工具栏只有「全选」;一旦勾选,工具栏浮出「已选 N 项 + 下载 + 删除」。示例关掉了 `builtin-download`,下载只上报事件: ## 只读 `readonly` 会隐藏上传 / 删除入口,并让拖拽、粘贴失效,适合审批查看场景: ## 弹窗用法 附件组件本身不是弹窗,需要弹窗时用 [Modal](/ui/overlay/modal) 包一层即可(高度用 `height` 给): ## 上传与校验 三种入队方式:点底栏「选择」、把文件拖进列表区、`paste-upload` 打开后在页面任意处 `Ctrl/Cmd + V`。 入队前按 `accept` / `maxSize` / `maxCount` 校验,被拒的文件通过 `invalid` 事件抛出(`info.message` 是组件按当前语言包生成的提示原文,可直接 toast): ```ts function onInvalid(info: AttachmentInvalidInfo) { // reason: 'accept' | 'max-size' | 'max-count' message.warning(info.message) } ``` 队列里的文件先渲染在列表最前面(带「待上传」标记),点「上传」后组件把它们交出去: - **没传 `upload-request`**:emit `upload(files, ctx)`,并**立即清空队列**(文件所有权移交业务); - **传了 `upload-request`**:组件托管请求,期间「上传」按钮 loading;成功后 emit `upload-success(files, result)`,`result` 是数组时自动追加进 `files`;失败 emit `upload-error(err, files)` 且**保留队列**(可重试)。 ## 数据契约 列表项的字段是中性的,不含任何业务字段名: ```ts const files = ref([ { id: 12, name: '合同.pdf', size: 512_000, categoryId: 'contract', url: '/files/12.pdf' }, ]) ``` 业务侧字段名不同也没关系 —— 组件的所有事件都把原始数据放在 `item.raw` 里原样回传,字段映射写在业务侧的一个 adapter 就够: ```ts // 后端行 → 组件项 const toItem = (row: BfFilesRow): AttachmentItem => ({ id: row.subid, name: row.mx_filename, size: Number(row.mx_filesize), ext: row.mx_fileext, url: row.mx_mapfilename, categoryId: row.mx_cate_id, raw: row, }) ``` ## 分页 两种模式,由 `total` 区分: | 模式 | 传参 | 行为 | | --- | --- | --- | | 客户端分页 | 只给 `files` | `files` 是完整列表,组件按 `page` / `pageSize` 自行切片 | | 服务端分页 | 再给 `total` | `files` 已经是当前页数据,组件只画不切,翻页靠 `update:page` | `page` / `pageSize` 是**非受控**的:不传时组件内部维护(默认 1 / 20),传了就完全听你的(`v-model:page-size` 等)。 ## API ### Props | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `files` | `AttachmentItem[]` | `[]` | 附件列表,配合 `v-model:files` | | `categories` | `AttachmentCategory[]` | `[]` | 分类数据;为空则不渲染左栏 | | `categoryKey` | `string \| number` | `''` | 选中分类,`''` 表示「全部」,配合 `v-model:categoryKey` | | `showCategory` | `boolean` | `true` | 是否显示左栏(无 `categories` 时自动隐藏) | | `showPreview` | `boolean` | `false` | 是否显示右栏预览;关掉时 `preview` 事件照常抛出,可自行开弹窗预览 | | `readonly` | `boolean` | `false` | 只读:隐藏上传 / 删除,拖拽与粘贴失效 | | `loading` | `boolean` | `false` | 列表加载中 | | `categoryLoading` | `boolean` | `false` | 分类加载中 | | `selectable` | `boolean` | `true` | 是否可勾选 | | `multiple` | `boolean` | `true` | 是否允许多选 | | `accept` | `string` | — | 接受的文件类型,同原生 `accept`(支持 `.pdf` / `image/*` / `application/pdf`) | | `maxSize` | `number` | `0` | 单文件大小上限(字节),`0` 表示不限制 | | `maxCount` | `number` | `0` | 待上传队列上限,`0` 表示不限制 | | `pasteUpload` | `boolean` | `false` | 监听 `document` 的粘贴上传(多实例场景请只开一个) | | `confirmRemove` | `boolean` | `true` | 删除前二次确认(内建 Modal) | | `builtinDownload` | `boolean` | `true` | 依据 `item.url` 内建触发下载;关掉后只上报 `download` | | `pagination` | `boolean` | `true` | 是否显示分页(数据不足一页时自动隐藏) | | `page` | `number` | — | 当前页,非受控(内部默认 `1`) | | `pageSize` | `number` | — | 每页条数,非受控(内部默认 `20`) | | `total` | `number` | — | 总条数;**传了即为服务端分页**,`files` 视为当前页 | | `pageSizeOptions` | `Array` | `[10, 20, 50, 100]` | 每页条数选项 | | `showSizeChanger` | `boolean` | `false` | 显示每页条数切换器 | | `height` | `number \| string` | — | 根容器高度,数字按 px;默认撑满父容器(父级无高度时保底 320px) | | `uploadRequest` | `(files: File[], ctx: AttachmentUploadContext) => Promise \| AttachmentItem[] \| void` | — | 托管上传 | | `removeRequest` | `(items: AttachmentItem[]) => Promise \| void` | — | 托管删除 | ### Events | 名称 | 参数 | 说明 | | --- | --- | --- | | `update:files` | `(files: AttachmentItem[])` | 删除成功 / 托管上传回填后更新列表 | | `update:categoryKey` | `(key: string \| number)` | 分类切换 | | `update:page` | `(page: number)` | 翻页 | | `update:pageSize` | `(size: number)` | 每页条数变化 | | `change` | `(files: AttachmentItem[])` | 列表增删(上传成功 / 删除成功) | | `select` | `(items: AttachmentItem[])` | 勾选变化 | | `categoryChange` | `(key: string \| number, category: AttachmentCategory \| null)` | 分类切换,带原始分类对象 | | `preview` | `(item: AttachmentItem \| null)` | 点击某项预览(翻页同样触发) | | `download` | `(items: AttachmentItem[])` | 下载 | | `remove` | `(items: AttachmentItem[])` | 用户确认删除且请求成功 | | `removeError` | `(error: unknown, items: AttachmentItem[])` | 删除失败 | | `upload` | `(files: File[], ctx: AttachmentUploadContext)` | 未传 `uploadRequest` 时把待上传文件交给业务 | | `uploadSuccess` | `(files: File[], result: AttachmentItem[] \| void)` | 托管上传成功 | | `uploadError` | `(error: unknown, files: File[])` | 托管上传失败(队列保留) | | `invalid` | `(info: AttachmentInvalidInfo)` | 文件被 `accept` / `maxSize` / `maxCount` 拒绝 | ### Exposed(通过 ref 调用) | 名称 | 类型 | 说明 | | --- | --- | --- | | `selectedItems` | `AttachmentItem[]` | 当前勾选的附件 | | `pendingFiles` | `AttachmentPendingFile[]` | 待上传队列 | | `openFileDialog()` | — | 打开系统文件选择框 | | `clearSelection()` | — | 清空勾选 | | `selectAll()` | — | 全选当前页 | | `addFiles(files)` | `File[] \| FileList => File[]` | 入队(走 `accept` / `maxSize` / `maxCount` 校验),返回被接受的文件 | | `clearPending()` | — | 清空待上传队列(业务自行接管上传时调用) | ### 类型定义 ```ts interface AttachmentItem { id: string | number name: string size?: number ext?: string url?: string thumbnailUrl?: string categoryId?: string | number status?: 'ready' | 'uploading' | 'error' percent?: number raw?: unknown [key: string]: unknown } interface AttachmentCategory { id: string | number name: string count?: number disabled?: boolean children?: AttachmentCategory[] } interface AttachmentPendingFile { file: File name: string size: number ext: string url?: string } interface AttachmentInvalidInfo { reason: 'accept' | 'max-size' | 'max-count' files: File[] message: string } interface AttachmentUploadContext { categoryId?: string | number } ``` ### 样式变量 类型色(图标 + 扩展名那一小块)按文件族取色,全部可覆盖;`--g3-color-*` 未加载时组件自带兜底值: | 变量 | 说明 | | --- | --- | | `--g3-attachment-type-image` | 图片:默认 `--g3-color-primary` | | `--g3-attachment-type-doc` | 文档(pdf / doc / txt …):默认 `--g3-color-danger` | | `--g3-attachment-type-sheet` | 表格:默认 `--g3-color-success` | | `--g3-attachment-type-slide` | 演示:默认 `--g3-color-warning` | | `--g3-attachment-type-archive` | 压缩包:默认 `#d98e04`(暗色 `#e0a63c`) | | `--g3-attachment-type-media` | 音视频:默认 `#1f9ea8`(暗色 `#38bcc6`) | | `--g3-attachment-type-other` | 未知类型:默认 `--g3-text-3` | 其余尺寸 / 间距 / 圆角 / 过渡全部走令牌(`--g3-control-height`、`--g3-space-*`、`--g3-radius*`、`--g3-duration*`),换肤时不需要动组件。 ## 实现说明 - **默认两栏,预览按需开**:布局用 [Splitter](/ui/data/splitter)——分类栏固定 200px(150–320 可拖),列表栏自适应(≥280);`show-preview` 打开后追加 360px(≥280)的预览栏。`showCategory` / `showPreview` 为假时对应面板直接不渲染,只剩两栏或一栏。预览栏关闭时 `preview` 事件照常抛出,业务可以接自己的弹窗 / 抽屉。 - **面板之间有常显的分割线**:关掉了 Splitter 的 `barOnHover`(默认只在 hover 时显示),并把线宽收成 1px——否则分类栏与卡片区之间看不出边界。拖拽命中区仍是 10px,只是视觉线更细。 - **工具栏整行同一档规格**:全选(复选框文案)、已选计数、批量按钮统一到 **12px 字 / 24px 控件**(按钮 `size="small"`),高度**写死** `24px + 上下各 8px`——必须用 `height` 而不是 `min-height`:默认 `content-box` 下 `min-height` 只管内容盒,实际高度 = 内容盒 + 上下 padding,勾选前后仍会变。行内元素一律 `flex: none`(不收缩)+ `flex-wrap: nowrap` + `overflow: hidden`。勾选后浮出「| 已选 N 项 · 下载 · 删除」,用 1px 竖线把「选择」与「操作」分组——字号一致、控件等高,才不会看着"歪"或空荡。删除与下载用 [Button](/ui/general/button) 的 `danger-outline` / `outline` 变体,在工具栏里有明确边界(`ghost` 的纯文字按钮会像裸文本飘着)。**窄面板**走容器查询(按面板宽度、不是视口宽度):先收掉按钮文案只留图标,再窄一点连「全选」文案也收起,保证一行永远放得下。 - **操作藏在 hover 里**:卡片右下角的「下载 / 删除」平时不显示,hover 或键盘聚焦(`:focus-within`)才浮现,`@media (hover: none)` 的触屏设备下常显。工具栏同理——没勾选任何文件时不渲染「下载 / 删除」,只留全选;勾选后浮出「已选 N 项 + 操作」。 - **空态就是投放区**:虚线框铺满列表区,hover 转主色;文案两行(「暂无文件」+「拖拽文件到此处,或点击选择」),并把 `accept` / `maxSize` / `maxCount` 的限制拼成第三行提示,省得用户上传后才被拒。 - **底栏是"脏"状态指示**:待上传队列非空时,底栏整块染主色淡底、计数变主色;上传与分页合成同一块(只有一条顶边),不会出现两条并排的分隔线。 - **分类树用 [Tree](/ui/data/tree)**:`categories` 之上合成了一个「全部」根节点(对外用 `categoryKey === ''` 表示),因此可以给层级数据;`categoryChange` 会把原始分类对象一起带出来。 - **删除确认是内建 Modal**:不走 `Modal.confirm()` 命令式(那会让组件依赖全局挂载容器)。`confirmRemove` 关掉后直接进请求。 - **`update:files` 的语义**:受控组件不擅自改数据。删除成功、或托管上传返回数组时,组件会 emit 过滤 / 追加后的新数组,方便 `v-model:files` 一行接上;完全自管数据时忽略它即可。 - **blob URL 的回收**:待上传图片的本地预览地址由组件创建,在出队、清空、切换分类、卸载时释放,业务无需关心。 - **上传进度**:`files` 里某项 `status: 'uploading'` 时,卡片封面底边出现 2px 进度条、列表视图在大小列显示细轨 + 百分比(进度值读 `percent`)。 - **粘贴上传默认关闭**:它监听 `document`,一个页面多个附件组件同时开会互相抢,所以做成显式 opt-in。 - **RTL**:布局全部用逻辑属性(`inset-inline-start` / `inset-block-end` / `text-align: start` 等),宿主 `` 下自动镜像,分隔条拖拽与键盘方向由 Splitter 的 `useDirection` 处理。