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