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 分鐘)。