This commit is contained in:
2026-03-04 03:49:03 +08:00
parent b5ec2bf3d0
commit 8dc3501990
3 changed files with 186 additions and 31 deletions

View File

@@ -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

View 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('下载失败,请稍后重试')

View File

@@ -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;