fix: 新需求 对接火山seedance
This commit is contained in:
parent
8959b775a4
commit
abb8d279c4
|
|
@ -0,0 +1,412 @@
|
||||||
|
<template>
|
||||||
|
<div class="rich-editor-root">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('bold')"><b>B</b></button>
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('italic')"><i>I</i></button>
|
||||||
|
<button class="tool-btn" type="button" @click="openImagePicker">{{ $t('common.insertImage') || '插入图片' }}</button>
|
||||||
|
<button class="tool-btn" type="button" @click="clear">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入 -->
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
@change="handleSelectFiles"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 编辑器区域 -->
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
class="user-input rich-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
:data-placeholder="placeholder || '请输入文本...'"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup"
|
||||||
|
@click="handleEditorClick"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- @ 图片选择面板 -->
|
||||||
|
<div v-if="mentionVisible" class="mention-panel">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in mentionImageList"
|
||||||
|
:key="item.url"
|
||||||
|
class="mention-item"
|
||||||
|
@mousedown.prevent="insertMentionImage(item)"
|
||||||
|
>
|
||||||
|
<img :src="item.url" class="mention-thumb" />
|
||||||
|
<span class="mention-label">@图{{ idx + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="mentionImageList.length === 0" class="mention-empty">
|
||||||
|
{{ $t('common.noImageToMention') || '暂无可引用图片' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
import { uploadFile } from '@/utils/file'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RichTextEditor',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入文本生成视频...'
|
||||||
|
},
|
||||||
|
uploadedImages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'text-change', 'image-upload'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const fileInputRef = ref(null)
|
||||||
|
const savedSelectionRange = ref(null)
|
||||||
|
const mentionVisible = ref(false)
|
||||||
|
const mentionImageList = ref([])
|
||||||
|
const MAX_IMAGE_COUNT = 4
|
||||||
|
|
||||||
|
// 执行编辑器命令
|
||||||
|
const execCommand = (command) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
document.execCommand(command, false, null)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开图片选择器
|
||||||
|
const openImagePicker = () => {
|
||||||
|
if (fileInputRef.value) {
|
||||||
|
fileInputRef.value.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件选择 - 上传到服务器
|
||||||
|
const handleSelectFiles = async (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
// 显示上传中提示
|
||||||
|
const loading = window.$message ? window.$message.loading('上传中...') : null
|
||||||
|
|
||||||
|
// 上传到后端
|
||||||
|
const res = await uploadFile({
|
||||||
|
url: '/api/cos/upload', // 使用腾讯云COS上传接口
|
||||||
|
file: file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res && res.code === 200 && res.data) {
|
||||||
|
const imageUrl = typeof res.data === 'string' ? res.data : (res.data.url || res.data)
|
||||||
|
insertImage(imageUrl, file.name)
|
||||||
|
|
||||||
|
// 通知父组件有新图片上传成功(用于@功能)
|
||||||
|
emit('image-upload', { url: imageUrl, name: file.name })
|
||||||
|
} else {
|
||||||
|
console.error('上传失败:', res)
|
||||||
|
alert('图片上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传图片出错:', error)
|
||||||
|
alert('图片上传失败: ' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空input
|
||||||
|
event.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入图片
|
||||||
|
const insertImage = (url, name = 'image') => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = url
|
||||||
|
img.alt = name
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.margin = '4px 0'
|
||||||
|
img.setAttribute('data-image-name', name)
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const content = editorRef.value.innerHTML
|
||||||
|
emit('update:modelValue', content)
|
||||||
|
emit('text-change', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理按键
|
||||||
|
const handleKeyup = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
showMentionPanel()
|
||||||
|
} else if (e.key === 'Escape' && mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
// 保存当前选择位置
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
savedSelectionRange.value = selection.getRangeAt(0).cloneRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 @ 面板
|
||||||
|
const showMentionPanel = () => {
|
||||||
|
if (props.uploadedImages && props.uploadedImages.length > 0) {
|
||||||
|
mentionImageList.value = props.uploadedImages.slice(0, MAX_IMAGE_COUNT)
|
||||||
|
mentionVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入 @ 图片
|
||||||
|
const insertMentionImage = (imageItem) => {
|
||||||
|
if (!editorRef.value || !savedSelectionRange.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(savedSelectionRange.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = imageItem.url
|
||||||
|
img.alt = '@图片'
|
||||||
|
img.style.maxWidth = '60px'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.verticalAlign = 'middle'
|
||||||
|
img.setAttribute('data-mention', 'true')
|
||||||
|
|
||||||
|
const range = savedSelectionRange.value
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
mentionVisible.value = false
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空编辑器
|
||||||
|
const clear = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.innerHTML = ''
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑器点击
|
||||||
|
const handleEditorClick = () => {
|
||||||
|
if (mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 props 变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (editorRef.value && editorRef.value.innerHTML !== newValue) {
|
||||||
|
editorRef.value.innerHTML = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听上传的图片列表更新
|
||||||
|
watch(() => props.uploadedImages, (newImages) => {
|
||||||
|
mentionImageList.value = newImages || []
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = props.modelValue || ''
|
||||||
|
|
||||||
|
// 添加粘贴事件处理
|
||||||
|
editorRef.value.addEventListener('paste', (e) => {
|
||||||
|
const items = e.clipboardData.items
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf('image') !== -1) {
|
||||||
|
const blob = items[i].getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
insertImage(event.target.result, 'pasted-image')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
e.preventDefault()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorRef,
|
||||||
|
fileInputRef,
|
||||||
|
mentionVisible,
|
||||||
|
mentionImageList,
|
||||||
|
execCommand,
|
||||||
|
openImagePicker,
|
||||||
|
handleSelectFiles,
|
||||||
|
handleInput,
|
||||||
|
handleKeyup,
|
||||||
|
handleKeydown,
|
||||||
|
handleEditorClick,
|
||||||
|
insertMentionImage,
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.rich-editor-root {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&:empty:before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: #1f2128;
|
||||||
|
border: 1px solid #3a3d47;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2a2d38;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-label {
|
||||||
|
color: #a1a4b3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,436 @@
|
||||||
|
<template>
|
||||||
|
<div :class="prefixCls">
|
||||||
|
<div class="left">
|
||||||
|
<!-- 模式切换 -->
|
||||||
|
<div class="mode-tabs">
|
||||||
|
<button
|
||||||
|
:class="['mode-tab', { active: mode === 'text-to-video' }]"
|
||||||
|
@click="switchMode('text-to-video')">
|
||||||
|
文生视频
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['mode-tab', { active: mode === 'image-to-video' }]"
|
||||||
|
@click="switchMode('image-to-video')">
|
||||||
|
图生视频
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传区域 - 根据模式显示 -->
|
||||||
|
<div class="upload" v-if="mode === 'image-to-video'">
|
||||||
|
<div class="upload-title">
|
||||||
|
<div class="upload-title-left">
|
||||||
|
{{ $t('common.uploadFirstImage') || '上传参考图' }}
|
||||||
|
</div>
|
||||||
|
<div class="upload-title-tip">
|
||||||
|
{{ $t('common.uploadImageTip') || '支持PNG/JPG,最大10MB' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mf-image-upload
|
||||||
|
listType="picture-card"
|
||||||
|
uploadHeight="180px"
|
||||||
|
:show-file-list="false"
|
||||||
|
:content="$t('common.uploadFirstPlaceholder') || '点击上传参考图片'"
|
||||||
|
v-model="firstUrl" />
|
||||||
|
|
||||||
|
<!-- 可选:上传尾帧 -->
|
||||||
|
<div class="last-frame" v-if="showLastFrame">
|
||||||
|
<div class="upload-title-left">
|
||||||
|
{{ $t('common.uploadLastPlaceholder') || '上传尾帧(可选)' }}
|
||||||
|
</div>
|
||||||
|
<mf-image-upload
|
||||||
|
listType="picture-card"
|
||||||
|
uploadHeight="120px"
|
||||||
|
:show-file-list="false"
|
||||||
|
:content="$t('common.uploadLastPlaceholder') || '点击上传尾帧'"
|
||||||
|
v-model="lastUrl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型选择 -->
|
||||||
|
<div class="model-select">
|
||||||
|
<div class="model-title">{{ $t('common.selectModel') || '选择模型' }}</div>
|
||||||
|
<a-select v-model="selectedModel" style="width: 100%;">
|
||||||
|
<a-option
|
||||||
|
v-for="option in modelOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 - 加大输入框 -->
|
||||||
|
<div class="rich-editor-container">
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editorContent"
|
||||||
|
:placeholder="$t('common.textVideoPlaceholder') || '描述视频内容,例如:一个女孩在海边跳舞...'"
|
||||||
|
:uploaded-images="uploadedImages"
|
||||||
|
@text-change="handleTextChange"
|
||||||
|
@image-upload="handleImageUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mf-button
|
||||||
|
class="submit"
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="generateLoading"
|
||||||
|
@click="generateVideo">
|
||||||
|
{{
|
||||||
|
price
|
||||||
|
? `${$t('common.createVideo', { price: price })}`
|
||||||
|
: $t('common.generateVideo')
|
||||||
|
}}
|
||||||
|
</mf-button>
|
||||||
|
<div class="submit-tip">
|
||||||
|
{{ $t('common.generateTip') || '生成视频需要消耗余额' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧结果区 -->
|
||||||
|
<div class="right" :class="{ 'has-result': showResult }">
|
||||||
|
<div v-if="!showResult" class="empty-state">
|
||||||
|
<img src="/images/empty-video.png" alt="视频生成" />
|
||||||
|
<p>生成的视频将在这里显示</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="result-container">
|
||||||
|
<div class="result-video-wrapper">
|
||||||
|
<video
|
||||||
|
v-if="videoUrl"
|
||||||
|
:src="videoUrl"
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
class="result-video"
|
||||||
|
:poster="firstUrl?.url || ''">
|
||||||
|
您的浏览器不支持视频播放。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-actions">
|
||||||
|
<mf-button v-if="canCancel" @click="cancelTask" type="danger">
|
||||||
|
取消任务
|
||||||
|
</mf-button>
|
||||||
|
<mf-button @click="saveVideo" :disabled="!videoUrl">
|
||||||
|
<a-image src="/images/btn_save.png" /> 保存视频
|
||||||
|
</mf-button>
|
||||||
|
<mf-button @click="closeResult">
|
||||||
|
<a-image src="/images/btn_close.png" /> 关闭
|
||||||
|
</mf-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VideoGen',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
prefixCls: 'video-gen',
|
||||||
|
mode: 'text-to-video', // 'text-to-video' 或 'image-to-video'
|
||||||
|
firstUrl: '',
|
||||||
|
lastUrl: '',
|
||||||
|
editorContent: '',
|
||||||
|
interval: null,
|
||||||
|
videoUrl: null,
|
||||||
|
videoLoading: false,
|
||||||
|
generateLoading: false,
|
||||||
|
videoId: null,
|
||||||
|
showResult: false,
|
||||||
|
price: null,
|
||||||
|
id: null,
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' },
|
||||||
|
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' }
|
||||||
|
],
|
||||||
|
selectedModel: 'ep-20260326165811-dlkth',
|
||||||
|
uploadedImages: [],
|
||||||
|
showLastFrame: false,
|
||||||
|
maxPollAttempts: 40
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
RichTextEditor
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['lang'])
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadPriceInfo()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadPriceInfo() {
|
||||||
|
this.$axios({
|
||||||
|
url: 'api/manager/selectInfo',
|
||||||
|
method: 'GET',
|
||||||
|
data: { aiType: '21' }
|
||||||
|
}).then((res) => {
|
||||||
|
this.price = res.data?.price
|
||||||
|
this.id = res.data?.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTextChange(content) {
|
||||||
|
this.editorContent = content
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageUpload(imageInfo) {
|
||||||
|
if (imageInfo && imageInfo.url) {
|
||||||
|
this.uploadedImages.push({
|
||||||
|
url: imageInfo.url,
|
||||||
|
name: imageInfo.name || 'image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
generateVideo() {
|
||||||
|
if (this.mode === 'image-to-video') {
|
||||||
|
const firstImageUrl = typeof this.firstUrl === 'object' ? this.firstUrl.url : this.firstUrl
|
||||||
|
if (!firstImageUrl) {
|
||||||
|
this.$message.error(this.$t('common.uploadFirstImageError') || '请上传参考图')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (!this.editorContent.trim()) {
|
||||||
|
this.$message.error('请输入视频描述文本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generateLoading = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
text: this.editorContent || '一个优雅的女孩在阳光下跳舞',
|
||||||
|
functionType: '21',
|
||||||
|
model: this.selectedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图生视频模式需要上传图片
|
||||||
|
if (this.mode === 'image-to-video') {
|
||||||
|
const firstImageUrl = typeof this.firstUrl === 'object' ? this.firstUrl.url : this.firstUrl
|
||||||
|
params.firstUrl = firstImageUrl
|
||||||
|
|
||||||
|
if (this.lastUrl) {
|
||||||
|
params.lastUrl = typeof this.lastUrl === 'object' ? this.lastUrl.url : this.lastUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios({
|
||||||
|
url: 'api/ai/imgToVideo',
|
||||||
|
method: 'POST',
|
||||||
|
data: params
|
||||||
|
}).then((res) => {
|
||||||
|
this.generateLoading = false
|
||||||
|
if (res.code == 200) {
|
||||||
|
this.videoId = res.data.id
|
||||||
|
this.showResult = true
|
||||||
|
this.getVideo(res.data.id)
|
||||||
|
} else if (res.code == -1) {
|
||||||
|
this.$confirm({
|
||||||
|
title: this.$t('common.notice'),
|
||||||
|
content: this.$t('common.balenceLow'),
|
||||||
|
onOk: () => this.$router.push('/recharge')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.generateLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getVideo(videoId) {
|
||||||
|
let attempts = 0
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
attempts++
|
||||||
|
if (attempts > this.maxPollAttempts) {
|
||||||
|
this.$message.warning('视频生成超时')
|
||||||
|
this.destroyInterval()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios({
|
||||||
|
url: `api/ai/${videoId}`,
|
||||||
|
method: 'GET'
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code == 200 && res.data.status === 'succeeded') {
|
||||||
|
this.videoUrl = res.data.content?.video_url || res.data.video_url
|
||||||
|
this.destroyInterval()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyInterval() {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
this.interval = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveVideo() {
|
||||||
|
if (!this.videoUrl) return
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = this.videoUrl
|
||||||
|
link.download = `video_${Date.now()}.mp4`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
},
|
||||||
|
|
||||||
|
closeResult() {
|
||||||
|
this.showResult = false
|
||||||
|
this.videoUrl = null
|
||||||
|
this.videoId = null
|
||||||
|
this.taskStatus = null
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消任务
|
||||||
|
async cancelTask() {
|
||||||
|
if (!this.videoId) return
|
||||||
|
|
||||||
|
this.$confirm({
|
||||||
|
title: '取消任务',
|
||||||
|
content: '确定要取消当前视频生成任务吗?取消后余额将退回。',
|
||||||
|
okText: '确定取消',
|
||||||
|
cancelText: '关闭',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: `api/ai/${this.videoId}/cancel`,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('任务已取消,余额已退回')
|
||||||
|
this.destroyInterval()
|
||||||
|
this.showResult = false
|
||||||
|
this.videoUrl = null
|
||||||
|
this.videoId = null
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '取消失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('取消请求失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算属性:是否可以取消
|
||||||
|
canCancel() {
|
||||||
|
return this.videoId && !this.videoUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.destroyInterval()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.video-gen {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #0f1014;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 480px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
color: #ddd;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 320px;
|
||||||
|
margin: 16px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.rich-editor-root) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.rich-editor) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 280px !important;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.75;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.has-result {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 120px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-video-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-video {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue