混剪功能
This commit is contained in:
File diff suppressed because it is too large
Load Diff
71
frontend/app/web-gold/src/api/mixTask.js
Normal file
71
frontend/app/web-gold/src/api/mixTask.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 混剪任务 API 服务
|
||||
*/
|
||||
|
||||
import http from './http'
|
||||
import { API_BASE } from '@gold/config/api'
|
||||
|
||||
const BASE_URL = `${API_BASE.APP}/api/mix`
|
||||
|
||||
/**
|
||||
* 创建混剪任务
|
||||
*/
|
||||
export const MixTaskService = {
|
||||
/**
|
||||
* 创建混剪任务
|
||||
*/
|
||||
createTask(data) {
|
||||
return http.post(`${BASE_URL}/create`, data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新混剪任务
|
||||
*/
|
||||
updateTask(data) {
|
||||
return http.put(`${BASE_URL}/update`, data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除混剪任务
|
||||
*/
|
||||
deleteTask(id) {
|
||||
return http.delete(`${BASE_URL}/delete/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取混剪任务详情
|
||||
*/
|
||||
getTask(id) {
|
||||
return http.get(`${BASE_URL}/get/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取混剪任务分页
|
||||
*/
|
||||
getTaskPage(params) {
|
||||
return http.get(`${BASE_URL}/page`, { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
*/
|
||||
getTaskStatus(id) {
|
||||
return http.get(`${BASE_URL}/status/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新生成失败的任务
|
||||
*/
|
||||
retryTask(id) {
|
||||
return http.post(`${BASE_URL}/retry/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*/
|
||||
cancelTask(id) {
|
||||
return http.post(`${BASE_URL}/cancel/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default MixTaskService
|
||||
@@ -43,6 +43,7 @@ const routes = [
|
||||
children: [
|
||||
{ path: '', redirect: '/material/list' },
|
||||
{ path: 'list', name: '素材列表', component: () => import('../views/material/MaterialList.vue') },
|
||||
{ path: 'mix-task', name: '混剪任务', component: () => import('../views/material/MixTaskList.vue') },
|
||||
{ path: 'group', name: '素材分组', component: () => import('../views/material/MaterialGroup.vue') },
|
||||
]
|
||||
},
|
||||
|
||||
@@ -260,6 +260,7 @@ import { MaterialService, MaterialGroupService } from '@/api/material'
|
||||
import { MixService } from '@/api/mix'
|
||||
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue'
|
||||
import { formatFileSize, formatDate } from '@/utils/file'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
|
||||
// 数据
|
||||
const loading = ref(false)
|
||||
@@ -633,21 +634,23 @@ const handleMixConfirm = async () => {
|
||||
|
||||
mixing.value = true
|
||||
try {
|
||||
const { data } = await MixService.batchProduceAlignment({
|
||||
const { data } = await MixTaskService.createTask({
|
||||
title,
|
||||
text,
|
||||
videoUrls: allVideoUrls.value,
|
||||
bgMusicUrls: allAudioUrls.value,
|
||||
produceCount
|
||||
})
|
||||
const jobIds = Array.isArray(data) ? data : []
|
||||
message.success(
|
||||
jobIds.length > 0
|
||||
? `混剪任务提交成功,JobId:${jobIds.join(', ')}`
|
||||
: '混剪任务提交成功'
|
||||
)
|
||||
mixModalVisible.value = false
|
||||
resetMixForm()
|
||||
if (data) {
|
||||
message.success('混剪任务提交成功,正在处理中...')
|
||||
mixModalVisible.value = false
|
||||
resetMixForm()
|
||||
|
||||
// 跳转到任务列表页面
|
||||
setTimeout(() => {
|
||||
window.open('/material/mix-task', '_blank')
|
||||
}, 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('混剪失败:', error)
|
||||
message.error(error?.message || '混剪任务提交失败,请重试')
|
||||
|
||||
537
frontend/app/web-gold/src/views/material/MixTaskList.vue
Normal file
537
frontend/app/web-gold/src/views/material/MixTaskList.vue
Normal file
@@ -0,0 +1,537 @@
|
||||
<template>
|
||||
<div class="mix-task-list">
|
||||
<div class="mix-task-list__header">
|
||||
<h1 class="mix-task-list__title">混剪任务</h1>
|
||||
<div class="mix-task-list__actions">
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<div class="mix-task-list__filters">
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
style="width: 120px"
|
||||
placeholder="任务状态"
|
||||
@change="handleFilterChange"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="running">处理中</a-select-option>
|
||||
<a-select-option value="success">已完成</a-select-option>
|
||||
<a-select-option value="failed">失败</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-input
|
||||
v-model="filters.title"
|
||||
placeholder="搜索标题"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@press-enter="handleFilterChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-range-picker
|
||||
v-model:value="filters.createTime"
|
||||
style="width: 300px"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="['开始日期', '结束日期']"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
|
||||
<a-button type="primary" @click="handleFilterChange">查询</a-button>
|
||||
<a-button @click="handleResetFilters">重置</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="mix-task-list__content">
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<template v-if="taskList.length > 0">
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="task in taskList"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item__header">
|
||||
<div class="task-item__title">
|
||||
<h3>{{ task.title }}</h3>
|
||||
<a-tag :color="getStatusColor(task.status)">
|
||||
{{ getStatusText(task.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="task-item__actions">
|
||||
<a-button
|
||||
v-if="task.status === 'failed'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleRetry(task.id)"
|
||||
>
|
||||
重新生成
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="task.status === 'running'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleCancel(task.id)"
|
||||
>
|
||||
取消
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleDelete(task.id)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-item__content">
|
||||
<div class="task-item__progress">
|
||||
<div class="progress-info">
|
||||
<span>进度:{{ task.progress }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="task.progress"
|
||||
:status="getProgressStatus(task.status)"
|
||||
:show-info="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="task-item__meta">
|
||||
<span>创建时间:{{ formatDate(task.createTime) }}</span>
|
||||
<span v-if="task.finishTime">
|
||||
完成时间:{{ formatDate(task.finishTime) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-item__text" v-if="task.text">
|
||||
<p>{{ task.text }}</p>
|
||||
</div>
|
||||
|
||||
<div class="task-item__results" v-if="task.outputUrls && task.outputUrls.length > 0">
|
||||
<h4>生成结果 ({{ task.outputUrls.length }}):</h4>
|
||||
<div class="result-list">
|
||||
<div
|
||||
v-for="(url, index) in task.outputUrls"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
>
|
||||
<a :href="url" target="_blank">
|
||||
<PlayCircleOutlined />
|
||||
视频 {{ index + 1 }}
|
||||
</a>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleDownload(url)"
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-item__error" v-if="task.errorMsg">
|
||||
<a-alert
|
||||
type="error"
|
||||
:message="task.errorMsg"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-empty description="暂无混剪任务" />
|
||||
</template>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mix-task-list__pagination">
|
||||
<a-pagination
|
||||
v-model:current="pagination.pageNo"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:show-total="(total) => `共 ${total} 条`"
|
||||
:show-size-changer="true"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
PlayCircleOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
import { formatDate } from '@/utils/file'
|
||||
|
||||
// 数据
|
||||
const loading = ref(false)
|
||||
const taskList = ref([])
|
||||
const timer = ref(null) // 定时器
|
||||
|
||||
// 筛选条件
|
||||
const filters = reactive({
|
||||
status: '',
|
||||
title: '',
|
||||
createTime: undefined
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 构建查询参数
|
||||
const buildQueryParams = () => {
|
||||
const params = {
|
||||
pageNo: pagination.pageNo,
|
||||
pageSize: pagination.pageSize,
|
||||
status: filters.status || undefined,
|
||||
title: filters.title || undefined
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (filters.createTime && Array.isArray(filters.createTime) && filters.createTime.length === 2) {
|
||||
params.createTimeStart = `${filters.createTime[0]} 00:00:00`
|
||||
params.createTimeEnd = `${filters.createTime[1]} 23:59:59`
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 加载任务列表
|
||||
const loadTaskList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await MixTaskService.getTaskPage(buildQueryParams())
|
||||
if (res.code === 0) {
|
||||
taskList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else {
|
||||
message.error(res.msg || '加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务列表失败:', error)
|
||||
message.error('加载失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilterChange = () => {
|
||||
pagination.pageNo = 1
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
filters.status = ''
|
||||
filters.title = ''
|
||||
filters.createTime = undefined
|
||||
pagination.pageNo = 1
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handlePageChange = (page, pageSize) => {
|
||||
pagination.pageNo = page
|
||||
if (pageSize && pageSize !== pagination.pageSize) {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.pageNo = 1
|
||||
}
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
loadTaskList()
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const handleRetry = (id) => {
|
||||
Modal.confirm({
|
||||
title: '确认重新生成',
|
||||
content: '确定要重新生成这个任务吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await MixTaskService.retryTask(id)
|
||||
message.success('已重新提交任务')
|
||||
loadTaskList()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
const handleCancel = (id) => {
|
||||
Modal.confirm({
|
||||
title: '确认取消',
|
||||
content: '确定要取消这个任务吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await MixTaskService.cancelTask(id)
|
||||
message.success('已取消任务')
|
||||
loadTaskList()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const handleDelete = (id) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个任务吗?删除后无法恢复。',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await MixTaskService.deleteTask(id)
|
||||
message.success('删除成功')
|
||||
loadTaskList()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 下载
|
||||
const handleDownload = (url) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'video'
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
running: '处理中',
|
||||
success: '已完成',
|
||||
failed: '失败'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
pending: 'default',
|
||||
running: 'processing',
|
||||
success: 'success',
|
||||
failed: 'error'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取进度条状态
|
||||
const getProgressStatus = (status) => {
|
||||
const statusMap = {
|
||||
pending: 'normal',
|
||||
running: 'active',
|
||||
success: 'success',
|
||||
failed: 'exception'
|
||||
}
|
||||
return statusMap[status] || 'normal'
|
||||
}
|
||||
|
||||
// 定时刷新
|
||||
const startAutoRefresh = () => {
|
||||
// 每5秒刷新一次
|
||||
timer.value = setInterval(() => {
|
||||
loadTaskList()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadTaskList()
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mix-task-list {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mix-task-list__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mix-task-list__title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mix-task-list__actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mix-task-list__filters {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.mix-task-list__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-item__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.task-item__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-item__title h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-item__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-item__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-item__progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.task-item__meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.task-item__text {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.task-item__text p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-item__results h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--color-bg-2);
|
||||
border-radius: var(--radius-card);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mix-task-list__pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user