useUploader()

useUploader() 把 framework 内置的 createUploader 工厂与当前项目的 SystemStore(接口地址)+ UserStore(当前用户 ID)自动接线,返回开箱即用的上传器实例。无需手动传 endpoint / token / userId, 每次调用都从 store 读最新值,避免 stale closure。

前置条件

  • 项目已启用 framework(modules: ['@swift-web/framework'])+ 处于已登录状态
  • 后端已下发 system/info,并包含 upload_image_url、upload_file_url、upload_file_query_url、upload_file_max_length、upload_image_max_length 等字段(uploader 实际接口地址完全由后端 systemInfo 控制)
  • 仅浏览器可用(依赖 File / FormData / Blob),SSR 阶段不要调用

基础用法

<script setup lang="ts">
const uploader = useUploader()

async function handleAvatar(file: File) {
  // 第二个参数 thumbnail:true=生成缩略图,false=原图
  const { id, value, width, height } = await uploader.uploadImage(file, false)
  // value 是后端返回的 URL;id 用于业务侧引用
  console.log({ id, value, width, height })
}
</script>

返回值

useUploader() 返回一个对象,含以下四个方法:

方法类型签名用途
uploadImage(file: File, thumbnail: boolean) => Promise<ImageUploadResult>单次 POST 上传图片,本地预校验大小
uploadVideo(files: File[], options?: VideoUploadOptions) => void并发分片上传多个视频文件(默认 2 MB / 片、重试 3 次)
uploadFile(file: File, options?: FileUploadOptions) => Promise<FileUploadResult>分片上传普通文件(PDF / 文档 / zip 等),无转码二段
queryVideo(id: string, retries?: number) => Promise<VideoQueryResult>轮询后端转码完成(每 3s 一次,默认最多 20 次)

uploadImage

POST 单次请求,本地先校验字节数(图片按 upload_image_max_length,未配置才回退 upload_file_max_length)(超出抛 Error('文件超出大小')),返回值如下:

type ImageUploadResult = {
  id: string      // 后端文件 ID(业务侧外键)
  value: string   // 文件访问 URL
  width: number   // 解析自后端 file_info "1280x720"
  height: number
}
<script setup lang="ts">
const uploader = useUploader()

async function onPick(e: Event) {
  const file = (e.target as HTMLInputElement).files?.[0]
  if (!file) return
  try {
    const cover = await uploader.uploadImage(file, false)
    form.value.cover = { id: cover.id, url: cover.value, w: cover.width, h: cover.height }
  } catch (err) {
    // 可能错误:'文件超出大小' / 后端 status='n' / '图片尺寸解析失败:file_info="..."'
    console.error(err)
  }
}
</script>

uploadVideo

callback 风格,调用立即返回 void。对每个 File 独立实例化 UploadChunk,并发互不阻塞。

type VideoUploadOptions = {
  retries?: number   // 单分片最大重试次数,默认 3
  chunkSize?: number  // 分片字节数,默认 1024 * 1024 * 2(2 MB)
  onBefore?:  (file: File) => void
  onProcess?: (r: ChunkProgress) => void
  onSuccess?: (r: ChunkSuccess) => void
  onError?:   (r: ChunkError) => void
}

interface ChunkProgress { file: File; processed_num: number; chunk_total: number; chunk_index: number; file_md5: string }
interface ChunkSuccess  { file: File; id: string; file_md5: string }
interface ChunkError    { file: File; error: unknown; chunk_total: number; chunk_index: number; file_md5: string }

典型组合:上传视频 → 拿到 id → 调 queryVideo 等转码完成 → 写表单。

<script setup lang="ts">
const uploader = useUploader()
const progress = ref(0)
const videoMeta = ref<Awaited<ReturnType<typeof uploader.queryVideo>> | null>(null)

function pickVideo(file: File) {
  uploader.uploadVideo([file], {
    onProcess: ({ processed_num, chunk_total }) => {
      progress.value = Math.round((processed_num / chunk_total) * 100)
    },
    onSuccess: async ({ id }) => {
      videoMeta.value = await uploader.queryVideo(id)
    },
    onError: ({ error }) => {
      console.error('视频上传失败', error)
    },
  })
}
</script>

uploadFile

上传普通文件(PDF / 文档 / zip 等非图非视频)。与 uploadVideo 复用同一分片端点(upload_file_url), 但无转码二段——后端分片成功响应即含可访问 URL,因此不需要 queryVideo。范式贴近 uploadImage: 返回 Promise,await 直接拿结果,失败 reject Error。

type FileUploadOptions = {
  chunkSize?: number   // 分片字节数,默认 1024 * 1024 * 2(2 MB)
  retries?: number    // 单分片最大重试次数,默认 3
  concurrency?: number   // 并发分片数,不传则用默认 6
  onProcess?: (r: ChunkProgress) => void  // 进度回调(复用 upload-chunk 的 ChunkProgress)
  onBefore?:  (file: File) => void        // 开始上传前(md5 计算前)回调
}

type FileUploadResult = {
  id: string        // 后端文件 ID(业务侧外键)
  value: string     // 文件访问 URL(对齐 uploadImage 的 value 语义)
  file_md5: string  // 整文件 MD5
}
<script setup lang="ts">
const uploader = useUploader()
const progress = ref(0)

async function pickFile(file: File) {
  try {
    const { id, value } = await uploader.uploadFile(file, {
      onProcess: ({ processed_num, chunk_total }) => {
        progress.value = Math.round((processed_num / chunk_total) * 100)
      },
    })
    form.value.attachment = { id, url: value }
  } catch (err) {
    // 失败终态:网络故障 / 后端 status='n' / 空文件等,统一 reject Error
    console.error('文件上传失败', err)
  }
}
</script>

uploadFile vs uploadVideo:两者同端点、同分片机制,差别在结果获取方式—— uploadFile 无转码、await 拿 URL;uploadVideo 走回调 + 后续 queryVideo 等转码。 上传图片用 uploadImage、视频用 uploadVideo、其余文件用 uploadFile。

queryVideo

视频上传成功后,后端转码需要时间。queryVideo 每隔 3 秒请求一次转码状态,直到 file_m3u8 可用或重试耗尽。

type VideoQueryResult = {
  id: string
  file_m3u8: string         // 转码后的播放地址
  file_duration: string
  file_preview_m3u8: string
  file_status: string
  width: number             // 解析自 file_quality "1920x1080"
  height: number
}

// 重试默认 20 次(≈ 60s);耗尽抛 Error('查询重试次数已用尽')
const result = await uploader.queryVideo(id, 30)

注意事项

  • 接口地址完全由后端 system/info 驱动,前端无配置项可改。本地若想换上传后端,需调 mock 的 system/info。
  • uploadVideo 不是 Promise——它通过 onSuccess / onError 回调通知结果。需要 await 的写法把它包一层 Promise 即可。
  • 单次失败的分片由 UploadChunk 内部按 retries 重试,业务侧拿到 onError 时已是终态。
  • queryVideo 静默吞掉单次失败重试(只在重试耗尽时 reject),开发期可加全局 request 日志看真实 HTTP 失败。
  • 上传超时由 runtimeConfig.public.swiftWebFramework.request.uploadTimeout 控制,默认 600_000(10 分钟)。