307 lines
8.0 KiB
Vue
307 lines
8.0 KiB
Vue
<template>
|
|
<div>
|
|
<div class="left">
|
|
<div class="tab">
|
|
<VideoTab :activeTab="activeTab" @tabChange="handleTabChange" />
|
|
</div>
|
|
|
|
<div class="content">
|
|
<Referencevideo
|
|
ref="referenceVideoRef"
|
|
v-if="activeTab === 'reference'"
|
|
@price-update="handleReferencePriceUpdate"
|
|
@prompt-change="handlePromptChange"
|
|
@task-created="handleTaskCreated"
|
|
/>
|
|
<ImgVideo
|
|
ref="imgVideoRef"
|
|
v-else-if="activeTab === 'image'"
|
|
@price-update="handleImagePriceUpdate"
|
|
@prompt-change="handleImagePromptChange"
|
|
@task-created="handleTaskCreated"
|
|
/>
|
|
<TextVideo
|
|
ref="textVideoRef"
|
|
v-else-if="activeTab === 'text'"
|
|
@price-update="handleTextPriceUpdate"
|
|
@prompt-change="handleTextPromptChange"
|
|
@task-created="handleTaskCreated"
|
|
/>
|
|
</div>
|
|
|
|
<div class="create-button-container">
|
|
<div
|
|
class="create-button"
|
|
:class="{ disabled: isCreateButtonDisabled }"
|
|
@click="handleCreate"
|
|
>
|
|
创作
|
|
<span v-if="currentPrice > 0" class="price-info">
|
|
¥ {{ currentPrice }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { ElMessage } from 'element-plus';
|
|
import VideoTab from './VideoTab.vue';
|
|
import Referencevideo from './ReferenceVideo.vue';
|
|
import TextVideo from './TextVideo.vue';
|
|
import ImgVideo from './ImgVideo.vue';
|
|
import { debounce } from '@/utlis/debounce';
|
|
import { useLoginStore } from '@/store/LoginStore';
|
|
|
|
// ==================== 子组件暴露的方法类型定义 ====================
|
|
/** 文生视频组件暴露的方法 */
|
|
interface TextVideoExposed {
|
|
generate: () => Promise<boolean>;
|
|
resetForm: () => void;
|
|
}
|
|
|
|
/** 图生视频组件暴露的方法 */
|
|
interface ImgVideoExposed {
|
|
generate: () => Promise<boolean>;
|
|
resetForm: () => void;
|
|
}
|
|
|
|
/** 参考生视频组件暴露的方法 */
|
|
interface ReferenceVideoExposed {
|
|
generate?: () => Promise<boolean>;
|
|
resetForm: () => void;
|
|
}
|
|
|
|
// ==================== 子组件引用(使用精确类型) ====================
|
|
const referenceVideoRef = ref<ReferenceVideoExposed | null>(null);
|
|
const imgVideoRef = ref<ImgVideoExposed | null>(null);
|
|
const textVideoRef = ref<TextVideoExposed | null>(null);
|
|
|
|
// ==================== 其他状态和逻辑(保持不变) ====================
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const loginStore = useLoginStore();
|
|
|
|
const activeTab = ref<'reference' | 'image' | 'text'>('reference');
|
|
const referencePrice = ref(0);
|
|
const textPrice = ref(0);
|
|
const imagePrice = ref(0);
|
|
const isPromptEmpty = ref(true);
|
|
const isTextPromptEmpty = ref(true);
|
|
const isImagePromptEmpty = ref(true);
|
|
const isCreating = ref(false);
|
|
|
|
// 价格更新处理
|
|
const handleReferencePriceUpdate = (price: number) => { referencePrice.value = price; };
|
|
const handleTextPriceUpdate = (price: number) => { textPrice.value = price; };
|
|
const handleImagePriceUpdate = (price: number) => { imagePrice.value = price; };
|
|
|
|
// 提示词变化处理
|
|
const handlePromptChange = (isEmpty: boolean) => { isPromptEmpty.value = isEmpty; };
|
|
const handleTextPromptChange = (isEmpty: boolean) => { isTextPromptEmpty.value = isEmpty; };
|
|
const handleImagePromptChange = (isEmpty: boolean) => { isImagePromptEmpty.value = isEmpty; };
|
|
|
|
// 计算属性
|
|
const currentPrice = computed(() => {
|
|
switch (activeTab.value) {
|
|
case 'reference': return referencePrice.value;
|
|
case 'text': return textPrice.value;
|
|
case 'image': return imagePrice.value;
|
|
default: return 0;
|
|
}
|
|
});
|
|
|
|
const isCreateButtonDisabled = computed(() => {
|
|
if (!loginStore.isLoggedIn || isCreating.value) {
|
|
return true;
|
|
}
|
|
|
|
switch (activeTab.value) {
|
|
case 'reference': return isPromptEmpty.value;
|
|
case 'text': return isTextPromptEmpty.value;
|
|
case 'image': return isImagePromptEmpty.value;
|
|
default: return true;
|
|
}
|
|
});
|
|
|
|
// 切换标签页(类型已明确)
|
|
const handleTabChange = debounce((tab: 'reference' | 'image' | 'text') => {
|
|
activeTab.value = tab;
|
|
router.push(`/videoGenerate/${tab}`);
|
|
}, 300);
|
|
|
|
// 创作按钮点击
|
|
const handleCreate = debounce(async () => {
|
|
if (!loginStore.isLoggedIn) {
|
|
ElMessage.warning('请先登录后再创作');
|
|
return;
|
|
}
|
|
|
|
if (isCreating.value) {
|
|
return;
|
|
}
|
|
|
|
if (isCreateButtonDisabled.value) {
|
|
ElMessage.warning('请先填写必要信息');
|
|
return;
|
|
}
|
|
|
|
isCreating.value = true;
|
|
let result = false;
|
|
try {
|
|
switch (activeTab.value) {
|
|
case 'text':
|
|
if (textVideoRef.value?.generate) {
|
|
result = await textVideoRef.value.generate();
|
|
}
|
|
break;
|
|
case 'image':
|
|
if (imgVideoRef.value?.generate) {
|
|
result = await imgVideoRef.value.generate();
|
|
}
|
|
break;
|
|
case 'reference':
|
|
if (referenceVideoRef.value?.generate) {
|
|
result = await referenceVideoRef.value.generate();
|
|
} else {
|
|
ElMessage.info('参考生视频功能暂未开放');
|
|
}
|
|
break;
|
|
default:
|
|
ElMessage.info('该功能暂未开放');
|
|
break;
|
|
}
|
|
return result;
|
|
} finally {
|
|
isCreating.value = false;
|
|
}
|
|
}, 500);
|
|
|
|
const emit = defineEmits<{ (e: 'task-created', taskid: string): void }>();
|
|
|
|
const handleTaskCreated = (taskid: string) => {
|
|
emit('task-created', taskid);
|
|
switch (activeTab.value) {
|
|
case 'text':
|
|
textVideoRef.value?.resetForm?.();
|
|
break;
|
|
case 'image':
|
|
imgVideoRef.value?.resetForm?.();
|
|
break;
|
|
case 'reference':
|
|
referenceVideoRef.value?.resetForm?.();
|
|
break;
|
|
}
|
|
};
|
|
|
|
// 路由同步
|
|
const updateActiveTabFromRoute = () => {
|
|
const path = route.path;
|
|
if (path.includes('/videoGenerate/reference')) activeTab.value = 'reference';
|
|
else if (path.includes('/videoGenerate/image')) activeTab.value = 'image';
|
|
else if (path.includes('/videoGenerate/text')) activeTab.value = 'text';
|
|
else activeTab.value = 'reference';
|
|
};
|
|
|
|
watch(() => route.path, () => updateActiveTabFromRoute());
|
|
onMounted(() => updateActiveTabFromRoute());
|
|
|
|
onUnmounted(() => {
|
|
handleTabChange.cancel();
|
|
handleCreate.cancel();
|
|
});
|
|
</script>
|
|
|
|
|
|
<style scoped lang="less">
|
|
.left {
|
|
width: 100%;
|
|
height: calc(100vh - 80px);
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
}
|
|
.create-button-container {
|
|
z-index: 999;
|
|
position: sticky;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
padding: 16px;
|
|
pointer-events: none;
|
|
// background: linear-gradient(180deg, rgba(2, 11, 19, 0) 0%, #0f0f19 100%);
|
|
// position: relative;
|
|
}
|
|
.create-button {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
height: 48px;
|
|
border-radius: 12px !important;
|
|
background-image: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
|
border: none !important;
|
|
color: #000 !important;
|
|
font-size: 18px !important;
|
|
font-weight: 600 !important;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
pointer-events: auto;
|
|
}
|
|
.create-button .price-info {
|
|
font-size: 16px;
|
|
font-weight: normal;
|
|
}
|
|
.create-button:hover {
|
|
opacity: 0.9;
|
|
transform: translateY(-1px);
|
|
}
|
|
.create-button:active {
|
|
transform: translateY(0);
|
|
}
|
|
.create-button.disabled {
|
|
background-image: linear-gradient(270deg, #cccccc 11.11%, #aaaaaa);
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
.create-button.disabled:hover {
|
|
transform: none;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
&::-webkit-scrollbar {
|
|
// 滚动条宽度(垂直滚动条)/ 高度(水平滚动条)
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
|
|
&::-webkit-scrollbar-track {
|
|
// 轨道背景色
|
|
background: #0F0F19;
|
|
// 轨道圆角
|
|
border-radius: 3px;
|
|
}
|
|
|
|
// ========== 滚动条滑块样式 ==========
|
|
&::-webkit-scrollbar-thumb {
|
|
// 滑块背景色
|
|
background: #0F0F19;
|
|
// 滑块圆角
|
|
border-radius: 3px;
|
|
// 鼠标悬浮时的样式
|
|
&:hover {
|
|
background: #0F0F19;
|
|
}
|
|
}
|
|
|
|
</style> |