+ {{ index }}
{{ item.fileName }}
{{ sizeLabel }}
{{ statusLabel }}
@@ -71,7 +73,13 @@ const canRemove = computed(
{{ t('episodeBatch.retry') }}
-
+
{{ t('episodeBatch.remove') }}
@@ -91,6 +99,14 @@ const canRemove = computed(
margin-bottom: 8px;
}
+.upload-task-row__index {
+ flex: 0 0 auto;
+ min-width: 1.5em;
+ color: var(--el-text-color-secondary);
+ font-size: 13px;
+ font-variant-numeric: tabular-nums;
+}
+
.upload-task-row__name {
flex: 1;
overflow: hidden;
diff --git a/src/views/contentManagement/upload/useUploadQueue.ts b/src/views/contentManagement/upload/useUploadQueue.ts
index dd0312b..e945f41 100644
--- a/src/views/contentManagement/upload/useUploadQueue.ts
+++ b/src/views/contentManagement/upload/useUploadQueue.ts
@@ -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
([])
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 {
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,