13 KiB
title, description
| title | description |
|---|---|
| Attachment 附件管理器 | 分类 / 列表(可选预览)的附件管理面板,上传(选择、拖拽、粘贴)、批量下载删除与分页 |
Attachment 附件管理器
Attachment 是「一个业务对象的附件集合」管理器:左栏分类、中栏文件(卡片 / 列表双视图),支持选择、拖拽、粘贴上传,多选批量下载 / 删除与分页。右栏预览默认关闭,需要时 show-preview 打开。
它不请求任何接口,也不认识「模块 / 业务主键」这类概念:数据从 files / categories 进来,用户意图以事件出去;需要组件代跑请求时,传 uploadRequest / removeRequest 即可。
何时使用
- 需要管理某个业务单据 / 项目的附件集合(上传、分类、下载、删除);
- 文件可能有很多,需要分类过滤、分页、批量操作;
- 只需要一个简单的「选择文件」入口时,用 Input 或原生
<input type="file">更轻。
基础用法
给 files(附件)和 categories(分类,可选)两份数据,上传与删除交给业务:
多选与批量操作
没勾选时工具栏只有「全选」;一旦勾选,工具栏浮出「已选 N 项 + 下载 + 删除」。示例关掉了 builtin-download,下载只上报事件:
只读
readonly 会隐藏上传 / 删除入口,并让拖拽、粘贴失效,适合审批查看场景:
弹窗用法
附件组件本身不是弹窗,需要弹窗时用 Modal 包一层即可(高度用 height 给):
上传与校验
三种入队方式:点底栏「选择」、把文件拖进列表区、paste-upload 打开后在页面任意处 Ctrl/Cmd + V。
入队前按 accept / maxSize / maxCount 校验,被拒的文件通过 invalid 事件抛出(info.message 是组件按当前语言包生成的提示原文,可直接 toast):
function onInvalid(info: AttachmentInvalidInfo) {
// reason: 'accept' | 'max-size' | 'max-count'
message.warning(info.message)
}
队列里的文件先渲染在列表最前面(带「待上传」标记),点「上传」后组件把它们交出去:
- 没传
upload-request:emitupload(files, ctx),并立即清空队列(文件所有权移交业务); - 传了
upload-request:组件托管请求,期间「上传」按钮 loading;成功后 emitupload-success(files, result),result是数组时自动追加进files;失败 emitupload-error(err, files)且保留队列(可重试)。
数据契约
列表项的字段是中性的,不含任何业务字段名:
const files = ref<AttachmentItem[]>([
{ id: 12, name: '合同.pdf', size: 512_000, categoryId: 'contract', url: '/files/12.pdf' },
])
业务侧字段名不同也没关系 —— 组件的所有事件都把原始数据放在 item.raw 里原样回传,字段映射写在业务侧的一个 adapter 就够:
// 后端行 → 组件项
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<number | string> |
[10, 20, 50, 100] |
每页条数选项 |
showSizeChanger |
boolean |
false |
显示每页条数切换器 |
height |
number | string |
— | 根容器高度,数字按 px;默认撑满父容器(父级无高度时保底 320px) |
uploadRequest |
(files: File[], ctx: AttachmentUploadContext) => Promise<AttachmentItem[] | void> | AttachmentItem[] | void |
— | 托管上传 |
removeRequest |
(items: AttachmentItem[]) => Promise<void> | 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() |
— | 清空待上传队列(业务自行接管上传时调用) |
类型定义
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——分类栏固定 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 的danger-outline/outline变体,在工具栏里有明确边界(ghost的纯文字按钮会像裸文本飘着)。窄面板走容器查询(按面板宽度、不是视口宽度):先收掉按钮文案只留图标,再窄一点连「全选」文案也收起,保证一行永远放得下。 - 操作藏在 hover 里:卡片右下角的「下载 / 删除」平时不显示,hover 或键盘聚焦(
:focus-within)才浮现,@media (hover: none)的触屏设备下常显。工具栏同理——没勾选任何文件时不渲染「下载 / 删除」,只留全选;勾选后浮出「已选 N 项 + 操作」。 - 空态就是投放区:虚线框铺满列表区,hover 转主色;文案两行(「暂无文件」+「拖拽文件到此处,或点击选择」),并把
accept/maxSize/maxCount的限制拼成第三行提示,省得用户上传后才被拒。 - 底栏是"脏"状态指示:待上传队列非空时,底栏整块染主色淡底、计数变主色;上传与分页合成同一块(只有一条顶边),不会出现两条并排的分隔线。
- 分类树用 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等),宿主<html dir="rtl">下自动镜像,分隔条拖拽与键盘方向由 Splitter 的useDirection处理。