优化
This commit is contained in:
@@ -8,29 +8,21 @@ import { API_BASE } from '@gold/config/api'
|
||||
const BASE_URL = `${API_BASE.APP}/api/media`
|
||||
|
||||
/**
|
||||
* 提交素材混剪任务
|
||||
* 提交素材混剪任务(纯画面模式)
|
||||
* @param {Object} data
|
||||
* @param {string} data.title
|
||||
* @param {string} data.text
|
||||
* @param {string[]} data.videoUrls
|
||||
* @param {string[]} data.bgMusicUrls
|
||||
* @param {number} data.produceCount
|
||||
*/
|
||||
export const MixService = {
|
||||
batchProduceAlignment({ title, text, videoUrls = [], bgMusicUrls = [], produceCount = 1 }) {
|
||||
batchProduceAlignment({ title, videoUrls = [], produceCount = 1 }) {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('title', title)
|
||||
formData.append('text', text)
|
||||
videoUrls.forEach((url) => {
|
||||
if (url) {
|
||||
formData.append('videoArray', url)
|
||||
}
|
||||
})
|
||||
bgMusicUrls.forEach((url) => {
|
||||
if (url) {
|
||||
formData.append('bgMusicArray', url)
|
||||
}
|
||||
})
|
||||
formData.append('produceCount', produceCount)
|
||||
|
||||
return http.post(`${BASE_URL}/batchProduceAlignment`, formData, {
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
<div class="mix-modal__summary">
|
||||
<p>视频分组:{{ getGroupName(videoGroupId) || '未选择' }}</p>
|
||||
<p>视频数量:{{ videoGroupFiles.length }} 个</p>
|
||||
<p>背景音乐:{{ selectedBgMusic?.fileName || '未选择' }}</p>
|
||||
<p style="margin-top: 8px; font-size: 12px; color: var(--color-text-3);">
|
||||
系统将根据文案自动生成配音并匹配视频片段
|
||||
纯画面模式:仅拼接视频片段,无配音、无背景音乐
|
||||
</p>
|
||||
</div>
|
||||
<a-form layout="vertical">
|
||||
@@ -31,41 +30,12 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="选择背景音乐" required>
|
||||
<a-select
|
||||
v-model:value="selectedBgMusic"
|
||||
placeholder="请选择背景音乐"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="(input, option) => option.children.toLowerCase().includes(input.toLowerCase())"
|
||||
>
|
||||
<a-select-option v-for="audio in allAudioFiles" :key="audio.id" :value="audio.id">
|
||||
{{ audio.fileName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="视频标题" required>
|
||||
<a-input v-model:value="mixForm.title" placeholder="请输入生成视频标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="文案内容" required>
|
||||
<a-textarea
|
||||
v-model:value="mixForm.text"
|
||||
placeholder="请输入文案(每句话用句号分隔)"
|
||||
:rows="4"
|
||||
/>
|
||||
<a-input v-model:value="mixForm.title" placeholder="请输入生成视频标题(仅用于记录)" />
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--color-text-3);">
|
||||
文案将用于生成 TTS 配音,每句话对应一个视频片段
|
||||
标题仅用于任务记录,不会在视频上显示
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="生成成片数量" required>
|
||||
<a-input-number
|
||||
v-model:value="mixForm.produceCount"
|
||||
:min="1"
|
||||
:max="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -87,10 +57,6 @@ const props = defineProps({
|
||||
groupList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allAudioFiles: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,13 +65,10 @@ const emit = defineEmits(['update:open', 'confirm', 'cancel'])
|
||||
// 数据
|
||||
const modalVisible = ref(false)
|
||||
const videoGroupId = ref(null)
|
||||
const selectedBgMusic = ref(null)
|
||||
const videoGroupFiles = ref([])
|
||||
|
||||
const mixForm = reactive({
|
||||
title: '',
|
||||
text: '',
|
||||
produceCount: 1
|
||||
title: ''
|
||||
})
|
||||
|
||||
// 获取分组名称
|
||||
@@ -120,7 +83,6 @@ watch(() => props.open, (newVal) => {
|
||||
modalVisible.value = newVal
|
||||
if (newVal) {
|
||||
resetForm()
|
||||
// 组件打开时,可以在这里加载音频文件,但最好由父组件传入
|
||||
}
|
||||
})
|
||||
|
||||
@@ -159,10 +121,7 @@ const handleVideoGroupChange = async (groupId) => {
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
mixForm.title = ''
|
||||
mixForm.text = ''
|
||||
mixForm.produceCount = 1
|
||||
videoGroupId.value = null
|
||||
selectedBgMusic.value = null
|
||||
videoGroupFiles.value = []
|
||||
}
|
||||
|
||||
@@ -174,24 +133,15 @@ defineExpose({
|
||||
// 处理确认
|
||||
const handleConfirm = async () => {
|
||||
const title = mixForm.title.trim()
|
||||
const text = mixForm.text.trim()
|
||||
|
||||
if (!videoGroupId.value) {
|
||||
message.warning('请选择视频分组')
|
||||
return
|
||||
}
|
||||
if (!selectedBgMusic.value) {
|
||||
message.warning('请选择背景音乐')
|
||||
return
|
||||
}
|
||||
if (!title) {
|
||||
message.warning('请输入视频标题')
|
||||
return
|
||||
}
|
||||
if (!text) {
|
||||
message.warning('请输入文案内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果当前没有视频文件,重新加载一次
|
||||
if (videoGroupFiles.value.length === 0) {
|
||||
@@ -205,30 +155,19 @@ const handleConfirm = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 提取视频URL和音频URL
|
||||
// 提取视频URL
|
||||
const videoUrls = videoGroupFiles.value
|
||||
.map(file => file?.fileUrl || file?.previewUrl)
|
||||
.filter(Boolean)
|
||||
|
||||
const bgMusicUrls = [selectedBgMusic.value?.fileUrl || selectedBgMusic.value?.previewUrl].filter(Boolean)
|
||||
|
||||
if (videoUrls.length === 0) {
|
||||
message.warning('视频分组中没有有效的视频文件')
|
||||
return
|
||||
}
|
||||
if (bgMusicUrls.length === 0) {
|
||||
message.warning('所选背景音乐无效')
|
||||
return
|
||||
}
|
||||
|
||||
const produceCount = Math.max(1, Math.min(10, Number(mixForm.produceCount) || 1))
|
||||
|
||||
emit('confirm', {
|
||||
title,
|
||||
text,
|
||||
videoUrls,
|
||||
bgMusicUrls,
|
||||
produceCount
|
||||
videoUrls
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { InboxOutlined, SoundOutlined, LoadingOutlined } from '@ant-design/icons
|
||||
import { VoiceService } from '@/api/voice'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
|
||||
import { extractVideoCover } from '@/utils/video-cover'
|
||||
|
||||
// 导入 voiceStore 用于获取用户音色
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
@@ -381,8 +382,22 @@ const handleVideoUpload = async (file) => {
|
||||
try {
|
||||
uploadedVideo.value = await toDataURL(file)
|
||||
uploadedVideoFile.value = file // 保存文件对象
|
||||
|
||||
// 提取视频封面
|
||||
try {
|
||||
const cover = await extractVideoCover(file, {
|
||||
maxWidth: 800,
|
||||
quality: 0.8
|
||||
})
|
||||
uploadedVideoFile.value.coverBase64 = cover.base64 // 保存封面到文件对象
|
||||
} catch (coverError) {
|
||||
console.warn('视频封面提取失败:', coverError)
|
||||
// 封面提取失败不影响主流程
|
||||
}
|
||||
|
||||
message.success('视频上传成功')
|
||||
} catch (error) {
|
||||
console.error('视频上传失败:', error)
|
||||
message.error('视频上传失败')
|
||||
}
|
||||
return false
|
||||
@@ -489,7 +504,9 @@ const generateVideo = async () => {
|
||||
// 上传视频文件到后端
|
||||
const uploadVideoFile = async (file) => {
|
||||
try {
|
||||
const res = await MaterialService.uploadFile(file, 'video')
|
||||
// 获取封面base64
|
||||
const coverBase64 = file.coverBase64 || null
|
||||
const res = await MaterialService.uploadFile(file, 'video', coverBase64)
|
||||
if (res.code === 0) {
|
||||
return res.data // res.data就是文件ID
|
||||
} else {
|
||||
|
||||
@@ -216,7 +216,6 @@ import MaterialBatchGroupModal from '@/components/material/MaterialBatchGroupMod
|
||||
import MaterialMixModal from '@/components/material/MaterialMixModal.vue'
|
||||
import { formatFileSize, formatDate } from '@/utils/file'
|
||||
import { MixTaskService } from '@/api/mixTask'
|
||||
import coverCache from '@/utils/coverCache'
|
||||
|
||||
// 数据
|
||||
const loading = ref(false)
|
||||
@@ -230,7 +229,6 @@ const mixing = ref(false)
|
||||
// 分组相关
|
||||
const groupModalVisible = ref(false)
|
||||
const groupList = ref([])
|
||||
const selectedGroupId = ref(null)
|
||||
const groupingFileId = ref(null) // 当前正在分组的单个文件ID
|
||||
|
||||
// 获取单个文件分组的文件名
|
||||
@@ -249,7 +247,6 @@ const getGroupName = (groupId) => {
|
||||
|
||||
// 混剪相关
|
||||
const mixModalRef = ref(null) // 混剪模态框的 ref
|
||||
const allAudioFiles = ref([]) // 所有音频文件
|
||||
|
||||
// 筛选条件
|
||||
const filters = reactive({
|
||||
@@ -293,52 +290,8 @@ const loadFileList = async () => {
|
||||
if (res.code === 0) {
|
||||
const files = res.data.list || []
|
||||
|
||||
// 优先从缓存获取封面,避免重复请求OSS
|
||||
const coverList = []
|
||||
files.forEach(file => {
|
||||
// 视频文件:只使用缓存的coverBase64,绝对不使用coverUrl
|
||||
// 所有视频都不允许从OSS获取封面(包括generate/其他分类)
|
||||
if (file.isVideo) {
|
||||
// 从缓存获取coverBase64
|
||||
const cachedCover = coverCache.getCover(file.id)
|
||||
if (cachedCover) {
|
||||
file.coverBase64 = cachedCover
|
||||
} else if (file.coverBase64) {
|
||||
// 服务端返回了coverBase64,保存到缓存
|
||||
coverCache.setCover(file.id, file.coverBase64)
|
||||
} else {
|
||||
// 没有缓存和base64,设置空值
|
||||
file.coverBase64 = null
|
||||
}
|
||||
|
||||
// 彻底删除coverUrl,防止任何OSS请求
|
||||
delete file.coverUrl
|
||||
|
||||
// 即使有previewUrl也不给视频使用
|
||||
if (file.previewUrl && !file.coverBase64) {
|
||||
delete file.previewUrl
|
||||
}
|
||||
}
|
||||
|
||||
// 收集有coverBase64的文件,用于批量缓存
|
||||
if (file.coverBase64) {
|
||||
coverList.push({ fileId: file.id, base64: file.coverBase64 })
|
||||
}
|
||||
})
|
||||
|
||||
// 批量保存缓存(仅保存还没有缓存的文件)
|
||||
if (coverList.length > 0) {
|
||||
coverCache.batchSetCovers(coverList)
|
||||
}
|
||||
|
||||
fileList.value = files
|
||||
pagination.total = res.data.total || 0
|
||||
|
||||
// 输出缓存统计信息(开发环境)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const stats = coverCache.getStats()
|
||||
console.log(`[MaterialList] 封面缓存统计: 总数${stats.total}, 有效${stats.valid}, 过期${stats.expired}`)
|
||||
}
|
||||
} else {
|
||||
message.error(res.msg || '加载失败')
|
||||
}
|
||||
@@ -411,9 +364,6 @@ const handleBatchDelete = () => {
|
||||
await MaterialService.deleteFiles(selectedFileIds.value)
|
||||
message.success('删除成功')
|
||||
|
||||
// 清除所有封面缓存(简化处理)
|
||||
coverCache.clearAll()
|
||||
|
||||
selectedFileIds.value = []
|
||||
loadFileList()
|
||||
} catch (error) {
|
||||
@@ -453,6 +403,7 @@ const handleDeleteFile = (file) => {
|
||||
|
||||
// 下载文件
|
||||
const handleDownloadFile = (file) => {
|
||||
console.log('下载文件:', file)
|
||||
if (!file?.previewUrl) {
|
||||
message.warning('文件地址无效')
|
||||
return
|
||||
@@ -534,31 +485,12 @@ const handleOpenMixModal = async () => {
|
||||
// 重置混剪表单(调用子组件方法)
|
||||
mixModalRef.value?.resetForm()
|
||||
|
||||
// 加载所有音频文件和分组列表
|
||||
await Promise.all([
|
||||
loadAllAudioFiles(),
|
||||
loadGroupList() // 重新加载分组列表,确保显示最新数据
|
||||
])
|
||||
// 加载分组列表
|
||||
await loadGroupList()
|
||||
|
||||
mixModalVisible.value = true
|
||||
}
|
||||
|
||||
// 加载所有音频文件
|
||||
const loadAllAudioFiles = async () => {
|
||||
try {
|
||||
const res = await MaterialService.getFilePage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
fileCategory: 'audio'
|
||||
})
|
||||
if (res.code === 0) {
|
||||
allAudioFiles.value = res.data.list || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载音频文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMixCancel = () => {
|
||||
mixModalVisible.value = false
|
||||
}
|
||||
@@ -566,7 +498,16 @@ const handleMixCancel = () => {
|
||||
const handleMixConfirm = async (params) => {
|
||||
mixing.value = true
|
||||
try {
|
||||
const { data } = await MixTaskService.createTask(params)
|
||||
// 纯画面模式:仅传入 title 和 videoUrls
|
||||
const { title, videoUrls } = params
|
||||
|
||||
const taskParams = {
|
||||
title,
|
||||
videoUrls,
|
||||
produceCount: 1 // 固定生成1个
|
||||
}
|
||||
|
||||
const { data } = await MixTaskService.createTask(taskParams)
|
||||
if (data) {
|
||||
message.success('混剪任务提交成功,正在处理中...')
|
||||
mixModalVisible.value = false
|
||||
@@ -631,8 +572,8 @@ const handleSingleGroup = (file) => {
|
||||
}
|
||||
|
||||
// 执行批量分组
|
||||
const handleBatchGroup = async () => {
|
||||
if (!selectedGroupId.value) {
|
||||
const handleBatchGroup = async (groupId) => {
|
||||
if (!groupId) {
|
||||
message.warning('请选择分组')
|
||||
return
|
||||
}
|
||||
@@ -653,13 +594,12 @@ const handleBatchGroup = async () => {
|
||||
|
||||
await MaterialGroupService.addFilesToGroups({
|
||||
fileIds: fileIds,
|
||||
groupIds: [selectedGroupId.value]
|
||||
groupIds: [groupId]
|
||||
})
|
||||
message.success(successMessage)
|
||||
|
||||
// 重置状态
|
||||
groupModalVisible.value = false
|
||||
selectedGroupId.value = null
|
||||
groupingFileId.value = null
|
||||
|
||||
// 如果是批量分组,清除选中状态
|
||||
@@ -677,7 +617,6 @@ const handleBatchGroup = async () => {
|
||||
// 取消分组操作
|
||||
const handleGroupCancel = () => {
|
||||
groupModalVisible.value = false
|
||||
selectedGroupId.value = null
|
||||
groupingFileId.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user