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, produceCount: Number(localStorage.getItem('mix-produce-count')) || 3,
totalDuration: 15, totalDuration: 15,
clipDuration: 5, clipDuration: 5,
cropMode: 'center' cropMode: localStorage.getItem('mix-crop-mode') || 'center'
}) })
const uiState = ref({ const uiState = ref({
@@ -377,6 +377,11 @@ const saveProduceCount = () => {
localStorage.setItem('mix-produce-count', formData.value.produceCount.toString()) localStorage.setItem('mix-produce-count', formData.value.produceCount.toString())
} }
// 监听裁剪模式变化,保存到 localStorage
watch(() => formData.value.cropMode, (newVal) => {
localStorage.setItem('mix-crop-mode', newVal)
})
const getFileById = (fileId) => { const getFileById = (fileId) => {
let file = dataState.value.groupFiles.find(f => f.id === fileId) let file = dataState.value.groupFiles.find(f => f.id === fileId)
if (file) return file if (file) return file

View File

@@ -118,15 +118,25 @@ export function useTaskOperations(apiHandlers, onSuccess) {
}) })
} }
// 下载单个文件 // 下载单个文件(使用 fetch + blob 强制下载)
function downloadFile(url, filename = 'download') { async function downloadFile(url, filename = 'download') {
const link = document.createElement('a') try {
link.href = url const response = await fetch(url)
link.download = filename if (!response.ok) throw new Error('下载失败')
link.target = '_blank' const blob = await response.blob()
document.body.appendChild(link) const blobUrl = URL.createObjectURL(blob)
link.click() const link = document.createElement('a')
document.body.removeChild(link) 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 // 获取签名URL
@@ -156,17 +166,21 @@ export function useTaskOperations(apiHandlers, onSuccess) {
} }
message.destroy() message.destroy()
message.loading('正在下载文件...', 0) message.loading(`正在下载 ${downloadUrls.length}文件...`, 0)
// 逐个触发下载,避免浏览器阻止多个弹窗 // 逐个下载文件
downloadUrls.forEach((url, index) => { for (let i = 0; i < downloadUrls.length; i++) {
setTimeout(() => { const url = downloadUrls[i]
downloadFile(url) const filename = `video_${taskId}_${i + 1}.mp4`
}, index * DOWNLOAD_INTERVAL) await downloadFile(url, filename)
}) // 短暂延迟避免浏览器阻止
if (i < downloadUrls.length - 1) {
await new Promise(resolve => setTimeout(resolve, DOWNLOAD_INTERVAL))
}
}
message.destroy() message.destroy()
message.success(`已触发 ${downloadUrls.length} 个文件的下载`) message.success(`成功下载 ${downloadUrls.length} 个文件`)
} catch (error) { } catch (error) {
message.destroy() message.destroy()
message.error('下载失败,请稍后重试') message.error('下载失败,请稍后重试')

View File

@@ -50,10 +50,37 @@
:row-key="record => record.id" :row-key="record => record.id"
:pagination="paginationConfig" :pagination="paginationConfig"
:expanded-row-keys="expandedRowKeys" :expanded-row-keys="expandedRowKeys"
:row-selection="rowSelection"
:scroll="{ x: 'max-content' }" :scroll="{ x: 'max-content' }"
@change="handleTableChange" @change="handleTableChange"
@expandedRowsChange="handleExpandedRowsChange" @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 #bodyCell="{ column, record }">
<!-- 标题列 --> <!-- 标题列 -->
<template v-if="column.key === 'title'"> <template v-if="column.key === 'title'">
@@ -187,9 +214,9 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue' import { SearchOutlined, PlayCircleOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { Modal } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import { MixTaskService } from '@/api/mixTask' import { MixTaskService } from '@/api/mixTask'
import { formatDate } from '@/utils/file' import { formatDate } from '@/utils/file'
import { useTaskList } from '@/views/system/task-management/composables/useTaskList' import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
@@ -199,7 +226,7 @@ import TaskStatusTag from '@/views/system/task-management/components/TaskStatusT
// Composables // Composables
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(MixTaskService.getTaskPage) 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 }, { deleteApi: MixTaskService.deleteTask, cancelApi: MixTaskService.cancelTask, retryApi: MixTaskService.retryTask, getSignedUrlsApi: MixTaskService.getSignedUrls },
fetchList fetchList
) )
@@ -209,6 +236,114 @@ useTaskPolling(MixTaskService.getTaskPage, { onTaskUpdate: fetchList })
const expandedRowKeys = ref([]) const expandedRowKeys = ref([])
const handleExpandedRowsChange = (keys) => { expandedRowKeys.value = keys } 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: '' }) const preview = reactive({ visible: false, title: '', url: '' })
@@ -243,21 +378,18 @@ const previewVideo = async (record, index) => {
const openPreview = (record) => previewVideo(record, 0) const openPreview = (record) => previewVideo(record, 0)
// 下载视频 // 下载视频(使用 fetch + blob 强制下载)
const downloadVideo = async (taskId, index) => { const downloadVideo = async (taskId, index) => {
try { try {
const res = await MixTaskService.getSignedUrls(taskId) const res = await MixTaskService.getSignedUrls(taskId)
if (res.code === 0 && res.data?.[index]) { if (res.code === 0 && res.data?.[index]) {
const link = document.createElement('a') const url = res.data[index]
link.href = res.data[index] const filename = `video_${taskId}_${index + 1}.mp4`
link.download = `video_${taskId}_${index + 1}.mp4` await downloadFile(url, filename)
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} }
} catch (e) { } catch (e) {
console.error('获取下载链接失败:', e) console.error('下载失败:', e)
message.error('下载失败')
} }
} }
@@ -315,6 +447,10 @@ onMounted(fetchList)
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.batch-toolbar {
padding: var(--space-2) 0;
}
.title-cell { .title-cell {
display: flex; display: flex;
align-items: center; align-items: center;