yuanjing/src/components/video/VideoLeft.vue
2026-06-04 11:17:47 +08:00

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>