Files
workspace/code/g3soft-libs/docs/content/ui/data/attachment.md
T
2026-10-11 22:14:13 +08:00

13 KiB
Raw Blame History

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:emit upload(files, ctx),并立即清空队列(文件所有权移交业务);
  • 传了 upload-request:组件托管请求,期间「上传」按钮 loading;成功后 emit upload-success(files, result),result 是数组时自动追加进 files;失败 emit upload-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 处理。