修复
This commit is contained in:
@@ -323,7 +323,7 @@ const formData = ref({
|
||||
produceCount: Number(localStorage.getItem('mix-produce-count')) || 3,
|
||||
totalDuration: 15,
|
||||
clipDuration: 5,
|
||||
cropMode: 'center'
|
||||
cropMode: localStorage.getItem('mix-crop-mode') || 'center'
|
||||
})
|
||||
|
||||
const uiState = ref({
|
||||
@@ -377,6 +377,11 @@ const saveProduceCount = () => {
|
||||
localStorage.setItem('mix-produce-count', formData.value.produceCount.toString())
|
||||
}
|
||||
|
||||
// 监听裁剪模式变化,保存到 localStorage
|
||||
watch(() => formData.value.cropMode, (newVal) => {
|
||||
localStorage.setItem('mix-crop-mode', newVal)
|
||||
})
|
||||
|
||||
const getFileById = (fileId) => {
|
||||
let file = dataState.value.groupFiles.find(f => f.id === fileId)
|
||||
if (file) return file
|
||||
|
||||
@@ -118,15 +118,25 @@ export function useTaskOperations(apiHandlers, onSuccess) {
|
||||
})
|
||||
}
|
||||
|
||||
// 下载单个文件
|
||||
function downloadFile(url, filename = 'download') {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
// 下载单个文件(使用 fetch + blob 强制下载)
|
||||
async function downloadFile(url, filename = 'download') {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('下载失败')
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
// 降级:直接打开让浏览器处理
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取签名URL
|
||||
@@ -156,17 +166,21 @@ export function useTaskOperations(apiHandlers, onSuccess) {
|
||||
}
|
||||
|
||||
message.destroy()
|
||||
message.loading('正在下载文件...', 0)
|
||||
message.loading(`正在下载 ${downloadUrls.length} 个文件...`, 0)
|
||||
|
||||
// 逐个触发下载,避免浏览器阻止多个弹窗
|
||||
downloadUrls.forEach((url, index) => {
|
||||
setTimeout(() => {
|
||||
downloadFile(url)
|
||||
}, index * DOWNLOAD_INTERVAL)
|
||||
})
|
||||
// 逐个下载文件
|
||||
for (let i = 0; i < downloadUrls.length; i++) {
|
||||
const url = downloadUrls[i]
|
||||
const filename = `video_${taskId}_${i + 1}.mp4`
|
||||
await downloadFile(url, filename)
|
||||
// 短暂延迟避免浏览器阻止
|
||||
if (i < downloadUrls.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, DOWNLOAD_INTERVAL))
|
||||
}
|
||||
}
|
||||
|
||||
message.destroy()
|
||||
message.success(`已触发 ${downloadUrls.length} 个文件的下载`)
|
||||
message.success(`成功下载 ${downloadUrls.length} 个文件`)
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
message.error('下载失败,请稍后重试')
|
||||
|
||||
@@ -50,10 +50,37 @@
|
||||
:row-key="record => record.id"
|
||||
:pagination="paginationConfig"
|
||||
:expanded-row-keys="expandedRowKeys"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
@change="handleTableChange"
|
||||
@expandedRowsChange="handleExpandedRowsChange"
|
||||
>
|
||||
<!-- 批量操作工具栏 -->
|
||||
<template #title>
|
||||
<div class="batch-toolbar">
|
||||
<a-space>
|
||||
<span v-if="selectedRowKeys.length > 0">
|
||||
已选择 <strong>{{ selectedRowKeys.length }}</strong> 项
|
||||
</span>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!hasDownloadableSelected"
|
||||
:loading="batchDownloading"
|
||||
@click="handleBatchDownloadSelected"
|
||||
>
|
||||
<DownloadOutlined /> 批量下载 ({{ downloadableCount }})
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handleBatchDeleteSelected"
|
||||
>
|
||||
<DeleteOutlined /> 批量删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 标题列 -->
|
||||
<template v-if="column.key === 'title'">
|
||||
@@ -187,9 +214,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
import { formatDate } from '@/utils/file'
|
||||
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
|
||||
@@ -199,7 +226,7 @@ import TaskStatusTag from '@/views/system/task-management/components/TaskStatusT
|
||||
|
||||
// Composables
|
||||
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(MixTaskService.getTaskPage)
|
||||
const { handleDelete, handleCancel, handleRetry, handleBatchDownload } = useTaskOperations(
|
||||
const { handleDelete, handleCancel, handleRetry, handleBatchDownload, handleBatchDelete } = useTaskOperations(
|
||||
{ deleteApi: MixTaskService.deleteTask, cancelApi: MixTaskService.cancelTask, retryApi: MixTaskService.retryTask, getSignedUrlsApi: MixTaskService.getSignedUrls },
|
||||
fetchList
|
||||
)
|
||||
@@ -209,6 +236,114 @@ useTaskPolling(MixTaskService.getTaskPage, { onTaskUpdate: fetchList })
|
||||
const expandedRowKeys = ref([])
|
||||
const handleExpandedRowsChange = (keys) => { expandedRowKeys.value = keys }
|
||||
|
||||
// 批量选择
|
||||
const selectedRowKeys = ref([])
|
||||
const batchDownloading = ref(false)
|
||||
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
}))
|
||||
|
||||
// 可下载的选中项
|
||||
const downloadableCount = computed(() => {
|
||||
return list.value.filter(item =>
|
||||
selectedRowKeys.value.includes(item.id) &&
|
||||
isStatus(item.status, 'success') &&
|
||||
item.outputUrls?.length > 0
|
||||
).length
|
||||
})
|
||||
|
||||
const hasDownloadableSelected = computed(() => downloadableCount.value > 0)
|
||||
|
||||
// 批量下载选中的任务(控制并发)
|
||||
const handleBatchDownloadSelected = async () => {
|
||||
const downloadableTasks = list.value.filter(item =>
|
||||
selectedRowKeys.value.includes(item.id) &&
|
||||
isStatus(item.status, 'success') &&
|
||||
item.outputUrls?.length > 0
|
||||
)
|
||||
|
||||
if (downloadableTasks.length === 0) {
|
||||
message.warning('没有可下载的任务')
|
||||
return
|
||||
}
|
||||
|
||||
batchDownloading.value = true
|
||||
message.loading(`正在准备下载 ${downloadableTasks.length} 个任务的视频...`, 0)
|
||||
|
||||
try {
|
||||
// 并发控制:同时最多下载 3 个任务
|
||||
const CONCURRENCY = 3
|
||||
let completed = 0
|
||||
const total = downloadableTasks.length
|
||||
|
||||
for (let i = 0; i < downloadableTasks.length; i += CONCURRENCY) {
|
||||
const batch = downloadableTasks.slice(i, i + CONCURRENCY)
|
||||
await Promise.all(batch.map(async (task) => {
|
||||
try {
|
||||
const res = await MixTaskService.getSignedUrls(task.id)
|
||||
if (res.code === 0 && res.data?.length > 0) {
|
||||
// 下载该任务的所有视频
|
||||
for (let j = 0; j < res.data.length; j++) {
|
||||
await downloadFile(res.data[j], `video_${task.id}_${j + 1}.mp4`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`任务 ${task.id} 下载失败:`, e)
|
||||
}
|
||||
completed++
|
||||
message.destroy()
|
||||
message.loading(`下载进度: ${completed}/${total}`, 0)
|
||||
}))
|
||||
}
|
||||
|
||||
message.destroy()
|
||||
message.success(`成功下载 ${total} 个任务的视频`)
|
||||
} catch (e) {
|
||||
message.destroy()
|
||||
message.error('批量下载失败')
|
||||
console.error('批量下载失败:', e)
|
||||
} finally {
|
||||
batchDownloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除选中的任务
|
||||
const handleBatchDeleteSelected = () => {
|
||||
const count = selectedRowKeys.value.length
|
||||
Modal.confirm({
|
||||
title: '确认批量删除',
|
||||
content: `确定要删除选中的 ${count} 个任务吗?删除后无法恢复。`,
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
for (const id of selectedRowKeys.value) {
|
||||
await MixTaskService.deleteTask(id)
|
||||
}
|
||||
message.success(`成功删除 ${count} 个任务`)
|
||||
selectedRowKeys.value = []
|
||||
fetchList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 下载单个文件(使用 fetch + blob 强制下载)
|
||||
const downloadFile = async (url, filename) => {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('下载失败')
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
|
||||
// 预览状态
|
||||
const preview = reactive({ visible: false, title: '', url: '' })
|
||||
|
||||
@@ -243,21 +378,18 @@ const previewVideo = async (record, index) => {
|
||||
|
||||
const openPreview = (record) => previewVideo(record, 0)
|
||||
|
||||
// 下载视频
|
||||
// 下载视频(使用 fetch + blob 强制下载)
|
||||
const downloadVideo = async (taskId, index) => {
|
||||
try {
|
||||
const res = await MixTaskService.getSignedUrls(taskId)
|
||||
if (res.code === 0 && res.data?.[index]) {
|
||||
const link = document.createElement('a')
|
||||
link.href = res.data[index]
|
||||
link.download = `video_${taskId}_${index + 1}.mp4`
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
const url = res.data[index]
|
||||
const filename = `video_${taskId}_${index + 1}.mp4`
|
||||
await downloadFile(url, filename)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取下载链接失败:', e)
|
||||
console.error('下载失败:', e)
|
||||
message.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +447,10 @@ onMounted(fetchList)
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.batch-toolbar {
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.title-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user