import { completeUploadApi, getUploadProgressApi, initUploadApi, uploadChunkApi } from '@/api/admin/media' import type { MediaAsset } from '@/api/admin/types' export interface UploadFileOptions { /** DRAMA_COVER / EPISODE / AVATAR */ bizType?: string /** 并发分片数,默认 3 */ concurrency?: number /** 视频时长(秒),由调用方探测后传入 */ durationSec?: number onProgress?: (percent: number) => void /** init 建好会话就回调一次:调用方可以把它记下来,失败/刷新后走 resumeUpload 续传 */ onSession?: (uploadId: string) => void } /** * 分片上传:init 建会话 → 并发传分片 → complete 合并。 * 服务端返回合并后的媒资(含可访问 URL)。 */ export async function uploadFile(file: File, options: UploadFileOptions = {}): Promise { const { bizType = 'EPISODE', concurrency = 3, onProgress } = options const initRes = await initUploadApi({ fileName: file.name, fileSize: file.size, bizType }) const info = initRes.data options.onSession?.(info.uploadId) const total = Math.max(info.totalChunks, 1) const pending: number[] = Array.from({ length: total }, (_, index) => index) let finished = 0 const worker = async () => { for (;;) { const index = pending.shift() if (index === undefined) return const start = index * info.chunkSize const chunk = file.slice(start, Math.min(start + info.chunkSize, file.size)) await uploadChunkApi(info.uploadId, index, chunk, file.name) finished += 1 onProgress?.(Math.min(95, Math.round((finished / total) * 95))) } } await Promise.all(Array.from({ length: Math.min(concurrency, total) }, worker)) const doneRes = await completeUploadApi(info.uploadId, options.durationSec) onProgress?.(100) return doneRes.data } /** * 断点续传:先问服务端传到第几片,从断点接着传,再合并。 * * 服务端只记账"已收到多少片",不回传具体是哪几片,所以这里按"已收片数之后的分片全部重传"处理 —— * 重传同一片是幂等覆盖,不会产生脏数据;片数落后于实际时最多多传几片。 */ export async function resumeUpload( file: File, uploadId: string, options: UploadFileOptions = {} ): Promise { const { concurrency = 3, onProgress } = options const progress = (await getUploadProgressApi(uploadId)).data const total = Math.max(progress.totalChunks, 1) const received = Math.min(progress.receivedChunks || 0, total) const pending: number[] = [] for (let index = received; index < total; index += 1) { pending.push(index) } let finished = received onProgress?.(Math.min(95, Math.round((finished / total) * 95))) const worker = async () => { for (;;) { const index = pending.shift() if (index === undefined) return const start = index * progress.chunkSize const chunk = file.slice(start, Math.min(start + progress.chunkSize, file.size)) await uploadChunkApi(uploadId, index, chunk, file.name) finished += 1 onProgress?.(Math.min(95, Math.round((finished / total) * 95))) } } await Promise.all( Array.from({ length: Math.min(concurrency, Math.max(pending.length, 1)) }, worker) ) const doneRes = await completeUploadApi(uploadId, options.durationSec) onProgress?.(100) return doneRes.data } /** 读取本地视频时长,取不到时返回 0。 */ export function probeVideoDuration(file: File): Promise { return new Promise((resolve) => { const url = URL.createObjectURL(file) const video = document.createElement('video') video.preload = 'metadata' const cleanup = (value: number) => { URL.revokeObjectURL(url) resolve(value) } video.onloadedmetadata = () => { cleanup(Number.isFinite(video.duration) ? Math.round(video.duration) : 0) } video.onerror = () => cleanup(0) video.src = url }) }