feat(admin): 批量上传改为手动点击开始
This commit is contained in:
1 parent
2e89f8460c
commit
f174ba46ae
5 files changed
+135
-32
No files matched your search
@@ -1,5 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
/** 剧集批量上传队列:文件并发、进度加权、断点会话、失败隔离 */
|
||||
/** 剧集批量上传队列:手动开始、文件并发、进度加权、断点会话、失败隔离 */
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { getUploadProgressApi } from '@/api/admin/media'
|
||||
@@ -58,6 +58,11 @@ function isHttpNotFound(error: unknown): boolean {
|
||||
return status === 404
|
||||
}
|
||||
|
||||
/** 文件名自然排序(数字感知),保证队列序号稳定。 */
|
||||
function compareFileName(a: string, b: string): number {
|
||||
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上传队列控制器。
|
||||
* @param options dramaId 与可选 bizType
|
||||
@@ -65,6 +70,8 @@ function isHttpNotFound(error: unknown): boolean {
|
||||
export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
const items = ref<UploadQueueItem[]>([])
|
||||
const paused = ref(false)
|
||||
/** 用户是否已点过「开始上传」;选文件阶段不派发、不占用并发。 */
|
||||
const started = ref(false)
|
||||
const bizType = options.bizType || DEFAULT_BIZ_TYPE
|
||||
let running = 0
|
||||
|
||||
@@ -77,18 +84,36 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
return Math.min(MAX_PERCENT, Math.round(weighted / totalBytes))
|
||||
})
|
||||
|
||||
const hasActiveUpload = computed(() =>
|
||||
items.value.some(
|
||||
const isUploading = computed(() =>
|
||||
items.value.some((item) => item.status === 'uploading' || item.status === 'merging')
|
||||
)
|
||||
|
||||
/** 关闭弹窗需二次确认:仅在已开始且仍有未完成任务时。 */
|
||||
const hasActiveUpload = computed(() => {
|
||||
if (!started.value) return false
|
||||
return items.value.some(
|
||||
(item) =>
|
||||
item.status === 'waiting' ||
|
||||
item.status === 'uploading' ||
|
||||
item.status === 'merging' ||
|
||||
item.status === 'paused'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const canStart = computed(() => {
|
||||
if (isUploading.value) return false
|
||||
return items.value.some((item) => item.status === 'waiting' || item.status === 'paused')
|
||||
})
|
||||
|
||||
const doneItems = computed(() => items.value.filter((item) => item.status === 'done'))
|
||||
|
||||
const failedCount = computed(() => items.value.filter((item) => item.status === 'failed').length)
|
||||
|
||||
/** 已开始且队列已无进行中任务时,可展示结果汇总。 */
|
||||
const showResultSummary = computed(
|
||||
() => started.value && items.value.length > 0 && !hasActiveUpload.value
|
||||
)
|
||||
|
||||
function persistSession(item: UploadQueueItem, uploadId: string): void {
|
||||
item.uploadId = uploadId
|
||||
upsertUploadSession({
|
||||
@@ -106,6 +131,10 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
if (uploadId) removeUploadSession(uploadId)
|
||||
}
|
||||
|
||||
function sortItemsByName(): void {
|
||||
items.value.sort((a, b) => compareFileName(a.fileName, b.fileName))
|
||||
}
|
||||
|
||||
async function runOne(item: UploadQueueItem, resumeId?: string): Promise<void> {
|
||||
item.status = 'uploading'
|
||||
item.error = ''
|
||||
@@ -140,7 +169,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
}
|
||||
|
||||
function pump(): void {
|
||||
if (paused.value) return
|
||||
if (!started.value || paused.value) return
|
||||
while (running < FILE_CONCURRENCY && !paused.value) {
|
||||
const next = items.value.find((item) => item.status === 'waiting')
|
||||
if (!next) return
|
||||
@@ -155,9 +184,10 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 入队本地文件(调用方已完成类型/大小校验)。 */
|
||||
/** 入队本地文件(调用方已完成类型/大小校验);只展示待上传,不发起请求。 */
|
||||
function enqueue(files: File[]): void {
|
||||
for (const file of files) {
|
||||
const sorted = [...files].sort((a, b) => compareFileName(a.name, b.name))
|
||||
for (const file of sorted) {
|
||||
items.value.push({
|
||||
id: nextItemId(),
|
||||
file,
|
||||
@@ -165,15 +195,18 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
fileSize: file.size,
|
||||
lastModified: file.lastModified,
|
||||
percent: 0,
|
||||
status: paused.value ? 'paused' : 'waiting',
|
||||
status: 'waiting',
|
||||
error: '',
|
||||
uploadId: ''
|
||||
})
|
||||
}
|
||||
sortItemsByName()
|
||||
}
|
||||
|
||||
/** 开始或继续派发。 */
|
||||
/** 开始或继续派发(含断点续传项上的 uploadId)。 */
|
||||
function start(): void {
|
||||
if (!items.value.length) return
|
||||
started.value = true
|
||||
paused.value = false
|
||||
for (const item of items.value) {
|
||||
if (item.status === 'paused') item.status = 'waiting'
|
||||
@@ -183,6 +216,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
|
||||
/** 暂停:不派新任务;进行中分片跑完。 */
|
||||
function pause(): void {
|
||||
if (!started.value) return
|
||||
paused.value = true
|
||||
for (const item of items.value) {
|
||||
if (item.status === 'waiting') item.status = 'paused'
|
||||
@@ -196,7 +230,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
item.error = ''
|
||||
item.percent = item.uploadId ? item.percent : 0
|
||||
item.status = paused.value ? 'paused' : 'waiting'
|
||||
if (!paused.value) void pump()
|
||||
if (started.value && !paused.value) void pump()
|
||||
}
|
||||
|
||||
/** 重试全部失败项。 */
|
||||
@@ -207,11 +241,12 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
item.status = paused.value ? 'paused' : 'waiting'
|
||||
}
|
||||
}
|
||||
if (!paused.value) void pump()
|
||||
if (started.value && !paused.value) void pump()
|
||||
}
|
||||
|
||||
/** 移除单项并清理会话。 */
|
||||
/** 仅选择阶段可移除;已开始上传后禁止直接移除,避免脏会话。 */
|
||||
function remove(id: string): void {
|
||||
if (started.value) return
|
||||
const index = items.value.findIndex((row) => row.id === id)
|
||||
if (index < 0) return
|
||||
const [removed] = items.value.splice(index, 1)
|
||||
@@ -225,6 +260,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
}
|
||||
items.value = []
|
||||
paused.value = false
|
||||
started.value = false
|
||||
running = 0
|
||||
}
|
||||
|
||||
@@ -234,7 +270,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用本地重选文件匹配会话并入队续传。
|
||||
* 用本地重选文件匹配会话并入队(待上传);不自动开始,由用户点「开始上传」续传。
|
||||
* @returns matched / unmatched 会话列表
|
||||
*/
|
||||
async function resumeFromSessions(
|
||||
@@ -267,7 +303,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
fileSize: file.size,
|
||||
lastModified: file.lastModified,
|
||||
percent: Math.min(MERGING_PERCENT, Number(progress.percent) || 0),
|
||||
status: paused.value ? 'paused' : 'waiting',
|
||||
status: 'waiting',
|
||||
error: '',
|
||||
uploadId: session.uploadId
|
||||
})
|
||||
@@ -280,6 +316,7 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
}
|
||||
}
|
||||
}
|
||||
sortItemsByName()
|
||||
return { matched, unmatched }
|
||||
}
|
||||
|
||||
@@ -294,7 +331,12 @@ export function useUploadQueue(options: UseUploadQueueOptions) {
|
||||
items,
|
||||
overallPercent,
|
||||
hasActiveUpload,
|
||||
isUploading,
|
||||
canStart,
|
||||
doneItems,
|
||||
failedCount,
|
||||
showResultSummary,
|
||||
started,
|
||||
paused,
|
||||
enqueue,
|
||||
start,
|
||||
|
||||
Reference in new issue
Block a user