task-106(任务与重复分析中心): 实现视频任务批量下载任务
新增 image-video-download.ts(选择键解析/打包请求体/响应头计数/下载 job 状态), image-video-api.ts 增加 requestVideoZipDownload(POST /download-zip, blob)。 TDD: task-106.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/** 视频任务列表/详情适配(任务 103/104):GET /api/admin/image-video-tasks(/detail)。 */
|
||||
/** 视频任务列表/详情/批量下载适配(任务 103-106)。 */
|
||||
import { http } from '@/api/http'
|
||||
import { parseImageVideoDetail, parseImageVideoPage, type ImageVideoDetail, type ImageVideoPageResult } from './image-video-model.ts'
|
||||
import { toImageVideoQuery, type ImageVideoFilter } from './image-video-filter.ts'
|
||||
import { downloadZipHeaderCounts, toVideoZipRequest } from './image-video-download.ts'
|
||||
|
||||
export const IMAGE_VIDEO_TASKS_ENDPOINT = '/api/admin/image-video-tasks'
|
||||
|
||||
@@ -21,3 +22,18 @@ export async function fetchImageVideoTaskDetail(taskId: string | number): Promis
|
||||
const { data } = await http.get<unknown>(`${IMAGE_VIDEO_TASKS_ENDPOINT}/${taskId}`)
|
||||
return parseImageVideoDetail(data)
|
||||
}
|
||||
|
||||
export interface VideoZipDownloadResult {
|
||||
blob: Blob
|
||||
fileCount?: number
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
/** 批量打包下载视频:POST /download-zip(blob);从响应头读成功/失败计数。 */
|
||||
export async function requestVideoZipDownload(keys: readonly string[]): Promise<VideoZipDownloadResult> {
|
||||
const { data, headers } = await http.post<Blob>(`${IMAGE_VIDEO_TASKS_ENDPOINT}/download-zip`, toVideoZipRequest(keys), {
|
||||
responseType: 'blob',
|
||||
})
|
||||
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
|
||||
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/** 视频批量打包下载任务模型(任务 106):选择键→打包请求体、下载 job 状态与响应头计数;纯逻辑。 */
|
||||
|
||||
export type BatchDownloadStatus = 'idle' | 'running' | 'done' | 'failed'
|
||||
|
||||
export interface BatchDownloadJob {
|
||||
status: BatchDownloadStatus
|
||||
message?: string
|
||||
fileCount?: number
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
/** 解析 "taskId:videoIndex" 卡片选择键;非法键返回 null。 */
|
||||
export function parseVideoSelectionKey(key: string): { taskId: number; videoIndex: number } | null {
|
||||
const match = /^(\d+):(\d+)$/.exec(typeof key === 'string' ? key.trim() : '')
|
||||
if (!match) return null
|
||||
return { taskId: Number(match[1]), videoIndex: Number(match[2]) }
|
||||
}
|
||||
|
||||
/** 选择键 → POST /download-zip 请求体(去重、跳过非法键)。 */
|
||||
export function toVideoZipRequest(keys: readonly string[]): { items: Array<{ task_id: number; video_index: number }> } {
|
||||
const items: Array<{ task_id: number; video_index: number }> = []
|
||||
const seen = new Set<string>()
|
||||
for (const key of keys) {
|
||||
const parsed = parseVideoSelectionKey(key)
|
||||
if (!parsed) continue
|
||||
const token = `${parsed.taskId}:${parsed.videoIndex}`
|
||||
if (seen.has(token)) continue
|
||||
seen.add(token)
|
||||
items.push({ task_id: parsed.taskId, video_index: parsed.videoIndex })
|
||||
}
|
||||
return { items }
|
||||
}
|
||||
|
||||
/** 大小写不敏感解析 zip 响应头里的成功/失败计数。 */
|
||||
export function downloadZipHeaderCounts(headers: Record<string, string | number | undefined> | undefined): { fileCount?: number; errorCount?: number } {
|
||||
const read = (name: string): number | undefined => {
|
||||
if (!headers || typeof headers !== 'object') return undefined
|
||||
const lower = name.toLowerCase()
|
||||
for (const key of Object.keys(headers)) {
|
||||
if (key.toLowerCase() === lower) {
|
||||
const value = headers[key]
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
|
||||
if (typeof value === 'string' && /^\d+$/.test(value.trim())) return Number(value.trim())
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
const fileCount = read('x-archive-file-count')
|
||||
const errorCount = read('x-archive-error-count')
|
||||
if (fileCount === undefined && errorCount === undefined) return {}
|
||||
const counts: { fileCount?: number; errorCount?: number } = {}
|
||||
if (fileCount !== undefined) counts.fileCount = fileCount
|
||||
if (errorCount !== undefined) counts.errorCount = errorCount
|
||||
return counts
|
||||
}
|
||||
|
||||
export function createBatchDownloadJob(): BatchDownloadJob {
|
||||
return { status: 'idle' }
|
||||
}
|
||||
|
||||
export function startBatchDownload(job: BatchDownloadJob): BatchDownloadJob {
|
||||
return { status: 'running' }
|
||||
}
|
||||
|
||||
export function succeedBatchDownload(job: BatchDownloadJob, fileCount: number, errorCount: number): BatchDownloadJob {
|
||||
return { status: 'done', fileCount, errorCount }
|
||||
}
|
||||
|
||||
export function failBatchDownload(job: BatchDownloadJob, message: string): BatchDownloadJob {
|
||||
return { status: 'failed', message }
|
||||
}
|
||||
Reference in New Issue
Block a user