图生视频

This commit is contained in:
hrx 2026-04-01 18:01:39 +08:00
parent a3c0fba87a
commit c513814c0a
12 changed files with 1135 additions and 299 deletions

View File

@ -1 +1,2 @@
VITE_API_BASE_URL = 'https://opencomputing.ai' VITE_API_BASE_URL = 'https://opencomputing.ai'
VITE_IMG_BASE_URL = 'https://opencomputing.ai'

View File

@ -2,6 +2,10 @@ import {get, post} from '../utlis/request'
// 文生视频 // 文生视频
export const textvideo = <T,D>(data:D):Promise<T> =>post('/api/llmage/vidu_inference.dspy',data) export const textvideo = <T,D>(data:D):Promise<T> =>post('/api/llmage/vidu_inference.dspy',data)
// 图生视频
export const imagetovideo = <T,D>(data:D):Promise<T> =>post('/api/llmage/vidu_inference.dspy',data)
// 获取所有模型 // 获取所有模型
export const AllModel = <T,D>(data:D):Promise<T> =>get('/api/llmage/get_type_llms.dspy',data) export const AllModel = <T,D>(data:D):Promise<T> =>get('/api/llmage/get_type_llms.dspy',data)

View File

@ -142,3 +142,7 @@
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
color: #fff; color: #fff;
} }
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}

View File

@ -175,3 +175,7 @@
} }
} }
} }
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}

View File

@ -0,0 +1,206 @@
:deep(.el-select__wrapper) {
width: 100% !important;
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 10px !important;
font-size: 14px;
color: #fff !important;
resize: vertical;
outline: none !important;
}
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}
:deep(.el-select__selected-item) {
color: #fff !important;
}
:deep(.el-textarea) .el-textarea__inner {
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 10px !important;
padding: 14px 16px !important;
font-size: 14px;
line-height: 1.5;
color: #fff !important;
resize: none;
outline: none !important;
}
:deep(.el-textarea) .el-textarea__inner:focus {
border: none !important;
box-shadow: none !important;
outline: none !important;
}
:deep(.el-textarea) .el-textarea__inner::placeholder {
color: rgba(255, 255, 255, 0.36) !important;
font-size: 14px;
}
:deep(.el-textarea) .el-input__count {
background: transparent;
color: rgba(255, 255, 255, 0.5);
}
.card {
width: 100%;
height: 100%;
background-color: #1a1c20;
border: none;
flex: 1;
padding: 16px;
border-radius: 12px;
}
.card :deep(.el-card__body) {
padding: 12px 16px;
height: 100%;
}
.reference-container {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
color: #fff;
}
.upload-section {
display: flex;
flex-wrap: wrap;
gap: 16px;
flex-shrink: 0;
}
.upload-item {
flex: 1;
min-width: 200px;
}
.upload-item .upload-label {
font-size: 14px;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9);
}
.upload-item :deep(.el-upload--picture-card) {
background-color: rgba(0, 0, 0, 0.32);
border: 1px dashed rgba(255, 255, 255, 0.3);
border-radius: 10px;
width: 100%;
height: 100px;
line-height: 100px;
}
.upload-item :deep(.el-upload--picture-card):hover {
border-color: #409eff;
}
.upload-item :deep(.el-upload-list--picture-card) .el-upload-list__item {
width: 100%;
height: 100px;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
}
.required-star {
color: #ff4d4f;
margin-left: 4px;
}
.input {
width: 100%;
flex: 1;
min-height: 0;
}
.btmcard {
width: 100%;
background-color: #1a1c20;
border: none;
margin-top: 16px;
padding: 16px;
border-radius: 12px;
flex-shrink: 0;
}
.btmcard :deep(.el-card__body) {
padding: 12px 16px;
}
.select {
display: flex;
flex-direction: column;
gap: 16px;
}
.select-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.select-title {
width: 50%;
font-size: 14px;
color: #fff;
padding-right: 12px;
}
.select-content {
width: 50%;
display: flex;
gap: 8px;
}
.select-content :deep(.el-input-number) {
width: 100%;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
}
.select-content :deep(.el-input-number) .el-input-number__decrease,
.select-content :deep(.el-input-number) .el-input-number__increase {
background-color: transparent;
color: rgba(255, 255, 255, 0.6);
}
.select-content :deep(.el-input-number) .el-input-number__decrease:hover,
.select-content :deep(.el-input-number) .el-input-number__increase:hover {
color: #fff;
}
.select-content :deep(.el-input-number) .el-input__inner {
background-color: transparent;
color: #fff;
border: none;
}
.select-content :deep(.el-input) .el-input__inner {
background-color: rgba(0, 0, 0, 0.32);
border: none;
border-radius: 10px;
color: #fff;
}
.select-content :deep(.el-input) .el-input__inner::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.select-content :deep(.el-textarea) .el-textarea__inner {
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
border-radius: 10px !important;
color: #fff !important;
}
.audio-wrapper {
display: flex;
gap: 8px;
width: 100%;
}
.upload-btn {
flex-shrink: 0;
}
.custom-select-popup {
background-color: #2A2D34 !important;
border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
border: none !important;
}
.custom-select-popup .el-select-dropdown__item {
color: #ffffff !important;
background-color: transparent !important;
font-size: 14px !important;
}
.custom-select-popup .el-select-dropdown__item.selected,
.custom-select-popup .el-select-dropdown__item:hover {
background-color: #3a3d46 !important;
}
.el-select .el-input__inner {
background-color: #35383F !important;
border: none !important;
color: #fff !important;
}
.el-select .el-input__suffix-inner {
color: #fff !important;
}
:deep(.el-upload-list__item) {
transition: none !important;
}

View File

@ -0,0 +1,253 @@
:deep(.el-select__wrapper) {
width: 100% !important;
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 10px !important;
font-size: 14px;
color: #fff !important;
resize: vertical;
outline: none !important;
}
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}
:deep(.el-select__selected-item) {
color: #fff !important;
}
:deep(.el-textarea) {
.el-textarea__inner {
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 10px !important;
padding: 14px 16px !important;
font-size: 14px;
line-height: 1.5;
color: #fff !important;
resize: none;
outline: none !important;
&:focus {
border: none !important;
box-shadow: none !important;
outline: none !important;
}
&::placeholder {
color: rgba(255, 255, 255, 0.36) !important;
font-size: 14px;
}
}
.el-input__count {
background: transparent;
color: rgba(255, 255, 255, 0.5);
}
}
.card {
width: 100%;
height: 100%;
background-color: #1a1c20;
border: none;
flex: 1;
padding: 16px;
border-radius: 12px;
:deep(.el-card__body) {
padding: 12px 16px;
height: 100%;
}
}
.reference-container {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
color: #fff;
}
.upload-section {
display: flex;
flex-wrap: wrap;
gap: 16px;
flex-shrink: 0;
}
.upload-item {
flex: 1;
min-width: 200px;
.upload-label {
font-size: 14px;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9);
}
:deep(.el-upload--picture-card) {
background-color: rgba(0, 0, 0, 0.32);
border: 1px dashed rgba(255, 255, 255, 0.3);
border-radius: 10px;
width: 100%;
height: 100px;
line-height: 100px;
&:hover {
border-color: #409eff;
}
}
:deep(.el-upload-list--picture-card) {
.el-upload-list__item {
width: 100%;
height: 100px;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
}
}
}
.required-star {
color: #ff4d4f;
margin-left: 4px;
}
.input {
width: 100%;
flex: 1;
min-height: 0;
}
.btmcard {
width: 100%;
background-color: #1a1c20;
border: none;
margin-top: 16px;
padding: 16px;
border-radius: 12px;
flex-shrink: 0;
:deep(.el-card__body) {
padding: 12px 16px;
}
}
.select {
display: flex;
flex-direction: column;
gap: 16px;
}
.select-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.select-title {
width: 50%;
font-size: 14px;
color: #fff;
padding-right: 12px;
}
.select-content {
width: 50%;
display: flex;
gap: 8px;
:deep(.el-input-number) {
width: 100%;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
.el-input-number__decrease,
.el-input-number__increase {
background-color: transparent;
color: rgba(255, 255, 255, 0.6);
&:hover {
color: #fff;
}
}
.el-input__inner {
background-color: transparent;
color: #fff;
border: none;
}
}
:deep(.el-input) {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.32);
border: none;
border-radius: 10px;
color: #fff;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
}
}
:deep(.el-textarea) {
.el-textarea__inner {
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
border-radius: 10px !important;
color: #fff !important;
}
}
}
.audio-wrapper {
display: flex;
gap: 8px;
width: 100%;
}
.upload-btn {
flex-shrink: 0;
}
.custom-select-popup {
background-color: #2A2D34 !important;
border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
border: none !important;
.el-select-dropdown__item {
color: #ffffff !important;
background-color: transparent !important;
font-size: 14px !important;
}
.el-select-dropdown__item.selected,
.el-select-dropdown__item:hover {
background-color: #3a3d46 !important;
}
}
.el-select {
.el-input__inner {
background-color: #35383F !important;
border: none !important;
color: #fff !important;
}
.el-input__suffix-inner {
color: #fff !important;
}
}
:deep(.el-upload-list__item) {
transition: none !important;
}

View File

@ -184,3 +184,39 @@
align-items: center; align-items: center;
height: 32px; height: 32px;
} }
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}
.upload-section {
display: flex;
flex-wrap: wrap;
gap: 16px;
flex-shrink: 0;
}
.upload-item {
flex: 1;
min-width: 200px;
}
.upload-item .upload-label {
font-size: 14px;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9);
}
.upload-item :deep(.el-upload--picture-card) {
background-color: rgba(0, 0, 0, 0.32);
border: 1px dashed rgba(255, 255, 255, 0.3);
border-radius: 10px;
width: 100%;
height: 100px;
line-height: 100px;
}
.upload-item :deep(.el-upload--picture-card):hover {
border-color: #409eff;
}
.upload-item :deep(.el-upload-list--picture-card) .el-upload-list__item {
width: 100%;
height: 100px;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
}

View File

@ -211,3 +211,46 @@
align-items: center; align-items: center;
height: 32px; height: 32px;
} }
:deep(.el-upload--picture-card) {
width: 140px !important;
height: 140px !important;
}
.upload-section {
display: flex;
flex-wrap: wrap;
gap: 16px;
flex-shrink: 0;
}
.upload-item {
flex: 1;
min-width: 200px;
.upload-label {
font-size: 14px;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9);
}
:deep(.el-upload--picture-card) {
background-color: rgba(0, 0, 0, 0.32);
border: 1px dashed rgba(255, 255, 255, 0.3);
border-radius: 10px;
width: 100%;
height: 100px;
line-height: 100px;
&:hover {
border-color: #409eff;
}
}
:deep(.el-upload-list--picture-card) {
.el-upload-list__item {
width: 100%;
height: 100px;
background-color: rgba(0, 0, 0, 0.32);
border-radius: 10px;
}
}
}

View File

@ -1,333 +1,593 @@
<template> <template>
<div class="reference-to-video"> <div class="reference-to-video">
<!-- 顶部卡片 --> <!-- 顶部卡片:图片上传和提示词输入区 -->
<el-card class="card"> <el-card class="card">
<div class="reference-container"> <div class="reference-container">
<!-- 上传模块 --> <!-- 动态上传区域:根据模型生成图片上传项 -->
<div v-if="currentModel" class="upload-section">
<div v-for="field in imageFields" :key="field.name" class="upload-item">
<div class="upload-label">
{{ field.label }}
<span v-if="field.required" class="required-star">*</span>
</div>
<el-upload action="#" list-type="picture-card" :auto-upload="false"
:on-change="(file: UploadFile) => handleFileChange(file, field.name)"
:on-remove="() => handleRemove(field.name)" :file-list="getFileList(field.name)" :limit="1"
:on-exceed="() => ElMessage.warning('每个字段只能上传一张图片')"
:disabled="generating" class="upload">
<el-icon>
<Plus />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete"
@click="handleDownload(field.name)">
<el-icon>
<Download />
</el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete"
@click="handleRemove(field.name)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
</div>
</div>
<!-- 文本输入框(绑定 prompt 字段) -->
<!-- 文本输入框 --> <div v-if="hasPromptField" class="input">
<div class="input"> <el-input type="textarea" :rows="6" maxlength="400" :placeholder="promptPlaceholder"
<el-input type="textarea" :rows="10" maxlength="400" v-model="prompt" show-word-limit :disabled="generating" />
placeholder="支持上传图片、视频或主体作为参考。可参考视频中的动作、表情、运镜、特效、风格与构图等,并支持对元素进行增删和修改" />
</div> </div>
</div> </div>
</el-card> </el-card>
<!-- 图片预览对话框 -->
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<!-- 底部配置卡片 --> <!-- 底部配置卡片 -->
<el-card class="btmcard"> <el-card class="btmcard">
<div class="select"> <div class="select">
<!-- 模型 --> <!-- 模型选择(动态从接口加载) -->
<div class="select-item"> <div class="select-item">
<div class="select-title">模型</div> <div class="select-title">模型</div>
<div class="select-content"> <div class="select-content">
<el-select v-model="selectModel" style="width: 100%" @change="handleChange" <el-select v-model="currentModelId" style="width: 100%" popper-class="custom-select-popup"
popper-class="custom-select-popup"> @change="handleModelChange" :disabled="generating">
<el-option label="Jack" value="jack" /> <el-option v-for="model in models" :key="model.id" :label="model.name" :value="model.id" />
<el-option label="Lucy" value="lucy" />
<el-option label="yiminghe" value="Yiminghe" />
<el-option label="Disabled" value="disabled" disabled />
</el-select> </el-select>
</div> </div>
</div> </div>
<!-- 动态生成其他配置项(排除 prompt 和图片字段) -->
<template v-for="field in visibleFields" :key="field.name">
<div class="select-item">
<div class="select-title">
{{ field.label }}
<span v-if="field.required" class="required-star">*</span>
</div>
<div class="select-content">
<!-- 下拉选择类型(code) -->
<el-select v-if="field.uitype === 'code'" v-model="configData[field.name]"
style="width: 100%" popper-class="custom-select-popup"
:placeholder="`请选择${field.label}`" :disabled="generating">
<el-option v-for="option in field.data" :key="option.value"
:label="option.text || option.value" :value="option.value" />
</el-select>
<!-- 数字输入类型(int) -->
<el-input-number v-else-if="field.uitype === 'int'" v-model="configData[field.name]"
:min="1" :max="field.max" :step="1" controls-position="right" style="width: 100%"
:disabled="generating" />
<el-input-number v-else-if="field.uitype === 'int'" v-model="configData[field.name]"
:min="1" :max="field.max || 15" :step="1" controls-position="right" style="width: 100%"
:disabled="generating" />
<!-- 文本输入(text 类型,但非 prompt) -->
<el-input v-else-if="field.uitype === 'text'" v-model="configData[field.name]"
:placeholder="`请输入${field.label}`"
:type="field.name === 'negative_prompt' ? 'textarea' : 'text'"
:rows="field.name === 'negative_prompt' ? 3 : 1" clearable :disabled="generating" />
<!-- 音频上传 -->
<!-- 清晰度 & 编码 --> <div v-else-if="field.uitype === 'audio'" class="audio-wrapper">
<div class="select-item"> <el-input v-model="configData[field.name]" placeholder="请输入音频文件URL或点击上传" clearable
<div class="select-title"> :disabled="generating" />
清晰度<span class="and">&</span>编码格式 <el-button size="small" @click="simulateUpload" :disabled="generating"
class="upload-btn">
上传
</el-button>
</div>
</div>
</div> </div>
<div class="select-content"> </template>
<el-select v-model="selectQuality" style="width: 50%" @change="handleChange"
popper-class="custom-select-popup">
<el-option label="1080P" value="1080P" />
</el-select>
<el-select v-model="selectCodec" style="width: 50%" @change="handleChange"
popper-class="custom-select-popup">
<el-option label="H265" value="H265" />
<el-option label="H264" value="H264" />
</el-select>
</div>
</div>
<!-- 生成模式 -->
<div class="select-item">
<div class="select-title">生成模式</div>
<div class="select-content">
<el-select v-model="selectModelviedo" style="width: 100%" @change="handleChange"
popper-class="custom-select-popup">
<el-option label="电影大片" value="电影大片" />
</el-select>
</div>
</div>
<!-- 数量 -->
<div class="select-item">
<div class="select-title">数量</div>
<div class="radio-group">
<button v-for="num in [1, 2, 3, 4]" :key="num" class="radio-item"
:class="{ selected: selectedCount === num }" @click="selectedCount = num" type="button">
{{ num }}
</button>
</div>
</div>
</div> </div>
</el-card> </el-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, defineEmits } from 'vue' import { ref, computed, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue';
import type { UploadFile } from 'element-plus';
import { imagetovideo, AllModel, queryModelPrice } from '../../apis/Model';
import type {
Model,
AllModelResponse,
PriceQueryParams,
PriceQueryResponse,
TextVideoResponse,
ModelParams,
} from '../../types/model';
// 定义 emit 事件 // ==================== 环境变量 ====================
const IMG_BASE_URL = import.meta.env.VITE_IMG_BASE_URL || '';
// ==================== 事件定义 ====================
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'price-update', price: number): void; (e: 'price-update', price: number): void;
(e: 'prompt-change', isEmpty: boolean): void; (e: 'prompt-change', isEmpty: boolean): void;
(e: 'task-created', taskid: string): void;
}>(); }>();
// 下拉选中项 // ==================== 类型定义 ====================
const selectModel = ref('lucy') type ConfigValue = string | number | boolean;
const selectQuality = ref('1080P')
const selectCodec = ref('H265')
const selectModelviedo = ref('电影大片')
// 单选按钮 // ==================== 数据定义 ====================
const selectedDuration = ref(5) const models = ref<Model[]>([]);
const selectedCount = ref(1) const currentModelId = ref('');
const currentModel = ref<Model | null>(null);
// 价格计算 const prompt = ref('');
const price = computed(() => { const uploadedFiles = ref<Record<string, string>>({});
const basePrice = selectedDuration.value > 5 ? 2 : 1 const uploadedFileNames = ref<Record<string, string>>({});
return basePrice * selectedCount.value const configData = ref<Record<string, ConfigValue>>({});
})
// 监听价格变化 const generating = ref(false);
watch(price, (newPrice) => { const price = ref(0);
emit('price-update', newPrice)
})
// 统一 change 事件 const dialogImageUrl = ref('');
const handleChange = (value: string) => { const dialogVisible = ref(false);
console.log('selected', value) const disabled = ref(false);
}
// ==================== 计算属性 ====================
const imageFields = computed(() => {
if (!currentModel.value) return [];
return currentModel.value.input_fields.filter(
(field: any) => field.uitype === 'image'
);
});
const hasPromptField = computed(() => {
if (!currentModel.value) return false;
return currentModel.value.input_fields.some(f => f.name === 'prompt');
});
const visibleFields = computed(() => {
if (!currentModel.value) return [];
return currentModel.value.input_fields.filter(
(field: any) => field.uitype !== 'image' && field.name !== 'prompt'
);
});
const promptPlaceholder = computed(() => {
if (!currentModel.value) return '请输入提示词';
const promptField = currentModel.value.input_fields.find(f => f.name === 'prompt');
if (promptField?.required) return '请输入提示词(必填)';
return '请输入提示词';
});
// ==================== 表单初始化 ====================
const initFormData = () => {
if (!currentModel.value) return;
const newConfigData: Record<string, ConfigValue> = {};
currentModel.value.input_fields.forEach(field => {
if (field.uitype === 'image' || field.name === 'prompt') {
return;
}
let defaultValue: ConfigValue = field.defaultvalue ?? '';
if (defaultValue === undefined || defaultValue === null) {
if (field.uitype === 'code' && field.data && field.data.length > 0) {
defaultValue = field.data[0].value;
} else if (field.uitype === 'int') {
defaultValue = 1;
} else {
defaultValue = '';
}
}
newConfigData[field.name] = defaultValue;
});
configData.value = newConfigData;
};
// ==================== 价格查询 ====================
const buildPriceParams = (): PriceQueryParams | null => {
if (!currentModel.value) return null;
const configDataForPrice: PriceQueryParams['config_data'] = { action: '' };
currentModel.value.input_fields.forEach(field => {
if (field.uitype === 'image' || field.name === 'prompt') {
return;
}
const value = configData.value[field.name];
if (value !== undefined) {
if (field.uitype === 'audio') {
configDataForPrice.audio = Boolean(value);
} else {
configDataForPrice[field.name] = value;
}
}
});
configDataForPrice.action = currentModel.value.apiname;
const priceParams = {
product_type: 'llm',
product_id: currentModel.value.id,
config_data: configDataForPrice,
};
console.log('=== 构建价格查询参数 ===');
console.log('价格查询参数:', JSON.stringify(priceParams, null, 2));
return priceParams;
};
let priceTimer: ReturnType<typeof setTimeout> | null = null;
const fetchPrice = () => {
if (priceTimer) clearTimeout(priceTimer);
priceTimer = setTimeout(async () => {
const hasRequiredContent = checkRequiredFields();
if (!hasRequiredContent) {
price.value = 0;
emit('price-update', 0);
return;
}
if (!currentModel.value) return;
const priceParams = buildPriceParams();
if (!priceParams) return;
try {
const res = (await queryModelPrice(priceParams)) as PriceQueryResponse;
if (res.status === 'ok' && res.data) {
price.value = res.data.amount;
emit('price-update', price.value);
} else {
price.value = 0;
emit('price-update', 0);
}
} catch (error) {
console.error('价格查询失败:', error);
price.value = 0;
emit('price-update', 0);
}
}, 500);
};
const checkRequiredFields = (): boolean => {
if (!currentModel.value) return false;
const hasPrompt = hasPromptField.value && prompt.value?.trim();
const hasUploads = imageFields.value.length > 0 &&
imageFields.value.some(f => uploadedFileNames.value[f.name]?.trim());
return hasPrompt || hasUploads;
};
// ==================== 模型加载 ====================
const loadModels = async () => {
try {
const params: ModelParams = { type: '图生视频' };
const res = await AllModel<AllModelResponse, ModelParams>(params);
if (res.status === 'ok' && Array.isArray(res.data)) {
models.value = res.data;
if (models.value.length > 0) {
currentModelId.value = models.value[0].id;
currentModel.value = models.value[0];
initFormData();
fetchPrice();
}
} else {
ElMessage.error('获取模型列表失败');
}
} catch (error) {
console.error('加载模型失败:', error);
ElMessage.error('加载模型列表失败,请刷新重试');
}
};
const handleModelChange = (modelId: string) => {
const selected = models.value.find(m => m.id === modelId);
if (selected) {
console.log('=== 切换模型 ===');
console.log('新模型:', selected.name);
console.log('清空的文件:', uploadedFileNames.value);
currentModel.value = selected;
uploadedFiles.value = {};
uploadedFileNames.value = {};
initFormData();
fetchPrice();
}
};
// ==================== 监听器 ====================
watch(
() => [configData.value, currentModelId.value],
() => {
if (currentModel.value) fetchPrice();
},
{ deep: true }
);
watch(
() => [prompt.value, uploadedFileNames.value],
() => {
const isEmpty = !checkRequiredFields();
emit('prompt-change', isEmpty);
if (isEmpty) {
price.value = 0;
emit('price-update', 0);
} else {
fetchPrice();
}
},
{ deep: true, immediate: true }
);
// ==================== 上传处理 ====================
const getFileList = (fieldName: string) => {
const url = uploadedFiles.value[fieldName];
if (url) {
return [{ name: fieldName, url }];
}
return [];
};
const handleFileChange = (file: UploadFile, fieldName: string) => {
// 检查是否已存在该字段的文件
if (uploadedFiles.value[fieldName]) {
ElMessage.warning('每个字段只能上传一张图片,请先删除已有图片再上传');
return;
}
const url = URL.createObjectURL(file.raw as File);
const fileName = file.name;
uploadedFiles.value[fieldName] = url;
uploadedFileNames.value[fieldName] = fileName;
console.log('=== 上传图片 ===');
console.log('字段名:', fieldName);
console.log('文件名:', fileName);
console.log('预览 URL:', url);
console.log('提交 URL:', `${IMG_BASE_URL}/${fileName}`);
console.log('当前所有上传文件:', uploadedFileNames.value);
ElMessage.success('上传成功');
};
const handleRemove = (fieldName: string) => {
uploadedFiles.value[fieldName] = '';
uploadedFileNames.value[fieldName] = '';
console.log('=== 删除图片 ===');
console.log('字段名:', fieldName);
console.log('剩余上传文件:', uploadedFileNames.value);
ElMessage.info('已移除图片');
};
const handlePictureCardPreview = (file: UploadFile) => {
dialogImageUrl.value = file.url!;
dialogVisible.value = true;
};
const handleDownload = (fieldName: string) => {
const url = uploadedFiles.value[fieldName];
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = fieldName + '.png';
a.click();
}
};
const simulateUpload = () => {
ElMessage.info('此处可对接实际音频上传服务');
configData.value.audio_file = 'https://example.com/demo_audio.mp3';
};
// ==================== 生成视频 ====================
const buildSubmitParams = (): Record<string, unknown> => {
const baseParams: Record<string, unknown> = {
llmid: currentModel.value!.id,
};
// 添加提示词(如果存在)
if (hasPromptField.value && prompt.value) {
baseParams.prompt = prompt.value;
}
// 添加上传的文件
Object.keys(uploadedFileNames.value).forEach(key => {
if (uploadedFileNames.value[key]) {
// 使用环境变量中的图片前缀 + 文件名
baseParams[key] = `${IMG_BASE_URL}/${uploadedFileNames.value[key]}`;
console.log('123',baseParams[key]);
}
});
// 添加其他配置字段
Object.keys(configData.value).forEach(key => {
if (configData.value[key] !== undefined) {
const value = configData.value[key];
// 确保 audio 字段是布尔值
if (key === 'audio_file') {
baseParams.audio = Boolean(value);
} else {
baseParams[key] = value;
}
}
});
console.log('=== 构建提交参数 ===');
console.log('完整参数:', JSON.stringify(baseParams, null, 2));
console.log('上传的图片字段:', uploadedFileNames.value);
console.log('图片基础 URL:', IMG_BASE_URL);
return baseParams;
};
const generate = async (): Promise<boolean> => {
if (!currentModel.value) {
ElMessage.error('请选择模型');
return false;
}
console.log('=== 开始验证必填字段 ===');
console.log('当前模型:', currentModel.value.name);
console.log('图片文件:', uploadedFileNames.value);
console.log('提示词:', prompt.value);
console.log('配置数据:', configData.value);
const missingFields: string[] = [];
currentModel.value.input_fields.forEach(field => {
if (field.required) {
if (field.name === 'prompt') {
if (!prompt.value?.trim()) missingFields.push(field.label);
} else if (field.uitype === 'image') {
if (!uploadedFileNames.value[field.name]?.trim()) missingFields.push(field.label);
} else {
const val = configData.value[field.name];
if (!val) missingFields.push(field.label);
}
}
});
if (missingFields.length > 0) {
console.error('=== 必填字段验证失败 ===');
console.error('缺失的字段:', missingFields);
ElMessage.error(`请填写必填项:${missingFields.join('、')}`);
return false;
}
console.log('=== 必填字段验证通过 ===');
generating.value = true;
try {
const params = buildSubmitParams();
console.log('=== 准备提交接口 ===');
console.log('接口路径: /api/llmage/vidu_inference.dspy');
console.log('请求参数:', JSON.stringify(params, null, 2));
const res = (await imagetovideo(params)) as TextVideoResponse;
console.log('=== 图生视频接口响应 ===');
console.log('响应状态:', res.status);
console.log('响应数据:', res.data);
console.log('完整响应:', JSON.stringify(res, null, 2));
let taskid = res.data?.taskid || res.taskid;
console.log('提取的 taskid:', taskid);
if (res.status === 'created' || res.status === 'processing' || res.status === 'queueing') {
const statusMsg = res.status === 'created' ? '视频生成任务已提交' : (res.status === 'processing' ? '进行中' : '排队中');
console.log('任务状态:', statusMsg);
console.log('任务创建成功,等待处理...');
ElMessage.success(`${statusMsg},请稍后在任务列表中查看`);
if (taskid) {
console.log('向上传递 task-created 事件,taskid:', taskid);
emit('task-created', taskid);
}
resetForm();
return true;
} else {
console.error('任务创建失败,状态:', res.status);
ElMessage.error(res.message || '生成失败,请重试');
return false;
}
} catch (error) {
console.error('=== 生成失败 ===');
console.error('错误详情:', error);
ElMessage.error(error instanceof Error ? error.message : '生成失败,请重试');
return false;
} finally {
generating.value = false;
}
};
const resetForm = () => {
console.log('=== 重置表单 ===');
console.log('清空的图片文件:', uploadedFileNames.value);
prompt.value = '';
uploadedFiles.value = {};
uploadedFileNames.value = {};
initFormData();
price.value = 0;
emit('price-update', 0);
emit('prompt-change', true);
};
// ==================== 暴露给父组件 ====================
defineExpose({
generate,
resetForm,
});
// ==================== 生命周期 ====================
onMounted(() => {
loadModels();
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
:deep(.el-select__wrapper) { @import url('../../assets/less/Video/text/text.less');
</style>
width: 100% !important;
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 0 !important;
font-size: 14px;
color: #fff !important;
resize: vertical;
outline: none !important;
border-radius: 10px !important;
color: #fff;
}
:deep(.el-select__selected-item) {
color: #fff !important;
}
:deep(.el-textarea) {
.el-textarea__inner {
border: none !important;
background-color: rgba(0, 0, 0, 0.32) !important;
box-shadow: none !important;
border-radius: 0 !important;
padding: 12px 0 !important;
font-size: 14px;
line-height: 1.5;
color: #fff !important;
resize: vertical;
outline: none !important;
border-radius: 10px !important;
color: #fff;
backdrop-filter: blur(30px);
padding: 14px 16px !important;
&:focus {
border: none !important;
box-shadow: none !important;
outline: none !important;
}
&::placeholder {
color: #ffffff7a !important;
font-size: 14px;
}
}
// 确保没有任何附加的边框或装饰出现
.el-input__count {
background: transparent;
color: #ffffff7a;
}
}
.card {
width: 100%;
height: 100%;
background-color: #1a1c20;
border: none;
flex: 1;
padding: 16px;
border-radius: 12px;
:deep(.el-card__body) {
padding: 12px 16px;
height: 100%;
}
}
.reference-container {
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
color: #fff;
}
.input {
width: 100%;
:deep(.el-input__inner) {
background-color: rgba(0, 0, 0, 0.32);
border: none;
border-radius: 10px;
color: #fff;
backdrop-filter: blur(30px);
padding: 14px 16px;
font-size: 14px;
line-height: 24px;
&::placeholder {
color: rgba(255, 255, 255, 0.36);
}
&:focus {
box-shadow: none;
border-color: transparent;
}
}
}
.btmcard {
width: 100%;
height: 100%;
background-color: #1a1c20;
border: none;
margin-top: 16px;
padding: 16px;
border-radius: 12px;
:deep(.el-card__body) {
padding: 12px 16px;
}
}
.select {
display: flex;
flex-direction: column;
gap: 16px;
}
.select-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.select-title {
width: 50%;
font-size: 14px;
color: #fff;
padding-right: 12px;
}
.select-content {
width: 50%;
display: flex;
gap: 8px;
}
.radio-group {
width: 50%;
display: flex;
background-color: rgba(255, 255, 255, 0.04);
border-radius: 8px;
overflow: hidden;
}
.and {
color: #ffffff66;
margin: 0 4px;
}
.radio-item {
flex: 1;
height: 36px;
border: none;
background: transparent;
color: white;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.radio-item.selected {
background-color: rgba(255, 255, 255, 0.08);
position: relative;
}
.radio-item.premium {
background-clip: text;
-webkit-background-clip: text;
color: transparent;
background-image: linear-gradient(to bottom, #7280FF, #DADFFF);
font-weight: 600;
}
<style>
.custom-select-popup { .custom-select-popup {
background-color: #2A2D34 !important; background-color: #2A2D34 !important;
border-radius: 12px !important; border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
border: none !important; border: none !important;
.el-select-dropdown__item {
color: #ffffff !important;
background-color: transparent !important;
font-size: 14px !important;
}
.el-select-dropdown__item.selected,
.el-select-dropdown__item:hover {
background-color: #3a3d46 !important;
}
} }
.el-select { .custom-select-popup .el-select-dropdown__item {
.el-input__inner { color: #ffffff !important;
background-color: #35383F !important; background-color: transparent !important;
border: none !important; font-size: 14px !important;
color: #fff !important; }
}
.el-input__suffix-inner { .custom-select-popup .el-select-dropdown__item.selected,
color: #fff !important; .custom-select-popup .el-select-dropdown__item:hover {
} background-color: #3a3d46 !important;
} }
</style> </style>

View File

@ -128,6 +128,7 @@ import type {
PriceQueryResponse, PriceQueryResponse,
TextVideoParams, TextVideoParams,
TextVideoResponse, TextVideoResponse,
ModelParams,
} from '../../types/model'; } from '../../types/model';
// ==================== 事件定义 ==================== // ==================== 事件定义 ====================
@ -264,8 +265,11 @@ const fetchPrice = () => {
// ==================== 模型加载 ==================== // ==================== 模型加载 ====================
const loadModels = async () => { const loadModels = async () => {
try { const Props = {
const res = (await AllModel({})) as AllModelResponse; type:'文生视频'
}
const res = await AllModel<AllModelResponse,ModelParams>(Props);
if (res.status === 'ok' && Array.isArray(res.data)) { if (res.status === 'ok' && Array.isArray(res.data)) {
models.value = res.data; models.value = res.data;
if (models.value.length > 0) { if (models.value.length > 0) {
@ -277,10 +281,7 @@ const loadModels = async () => {
} else { } else {
ElMessage.error('获取模型列表失败'); ElMessage.error('获取模型列表失败');
} }
} catch (error) {
console.error('加载模型失败:', error);
ElMessage.error('加载模型列表失败,请刷新重试');
}
}; };
const handleModelChange = (modelId: string) => { const handleModelChange = (modelId: string) => {
@ -341,7 +342,7 @@ const buildSubmitParams = (): TextVideoParams => {
if (configData.value.size !== undefined && !baseParams.resolution) { if (configData.value.size !== undefined && !baseParams.resolution) {
baseParams.resolution = String(configData.value.size); baseParams.resolution = String(configData.value.size);
} }
baseParams.audio = !!configData.value.audio_file; baseParams.audio = Boolean(configData.value.audio_file);
return baseParams; return baseParams;
}; };
@ -389,7 +390,7 @@ const generate = async (): Promise<boolean> => {
ElMessage.success(`${statusMsg},请稍后在任务列表中查看`); ElMessage.success(`${statusMsg},请稍后在任务列表中查看`);
if (taskid) { if (taskid) {
console.log('触发 task-created 事件, taskid:', taskid); console.log('触发 task-created 事件, taskid:', taskid);
emit('task-created', taskid); emit('task-created', taskid);
console.log('task-created 事件已发送'); console.log('task-created 事件已发送');
} else { } else {
@ -400,7 +401,7 @@ const generate = async (): Promise<boolean> => {
resetForm(); resetForm();
return true; return true;
} else { } else {
ElMessage.error(res.data?.message || '生成失败,请重试'); ElMessage.error(res.message || '生成失败,请重试');
return false; return false;
} }
} catch (error) { } catch (error) {
@ -433,7 +434,7 @@ onMounted(() => {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@import url('../../assets/less/Video/text.less'); @import url('../../assets/less/Video/text/text.less');
</style> </style>
<style> <style>

View File

@ -17,6 +17,7 @@
v-else-if="activeTab === 'image'" v-else-if="activeTab === 'image'"
@price-update="handleImagePriceUpdate" @price-update="handleImagePriceUpdate"
@prompt-change="handleImagePromptChange" @prompt-change="handleImagePromptChange"
@task-created="handleTaskCreated"
/> />
<TextVideo <TextVideo
ref="textVideoRef" ref="textVideoRef"
@ -59,13 +60,15 @@ interface TextVideoExposed {
resetForm: () => void; resetForm: () => void;
} }
/** 图生视频组件暴露的方法(示例,根据实际情况补充) */ /** 图生视频组件暴露的方法 */
interface ImgVideoExposed { interface ImgVideoExposed {
generate: () => Promise<boolean>;
resetForm: () => void; resetForm: () => void;
} }
/** 参考生视频组件暴露的方法(示例) */ /** 参考生视频组件暴露的方法 */
interface ReferenceVideoExposed { interface ReferenceVideoExposed {
generate?: () => Promise<boolean>;
resetForm: () => void; resetForm: () => void;
} }
@ -123,7 +126,11 @@ const handleTabChange = (tab: 'reference' | 'image' | 'text') => {
// 创作按钮点击 // 创作按钮点击
const handleCreate = async () => { const handleCreate = async () => {
if (isCreateButtonDisabled.value) return; if (isCreateButtonDisabled.value) {
ElMessage.warning('请先填写必要信息');
return;
}
let result = false; let result = false;
switch (activeTab.value) { switch (activeTab.value) {
case 'text': case 'text':
@ -131,6 +138,18 @@ const handleCreate = async () => {
result = await textVideoRef.value.generate(); result = await textVideoRef.value.generate();
} }
break; 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: default:
ElMessage.info('该功能暂未开放'); ElMessage.info('该功能暂未开放');
break; break;
@ -200,7 +219,7 @@ onMounted(() => updateActiveTabFromRoute());
left: 0; left: 0;
width: 100%; width: 100%;
padding: 16px; padding: 16px;
background: linear-gradient(180deg, rgba(2, 11, 19, 0) 0%, #0f0f19 100%); // background: linear-gradient(180deg, rgba(2, 11, 19, 0) 0%, #0f0f19 100%);
// position: relative; // position: relative;
} }
.create-button { .create-button {

View File

@ -227,4 +227,9 @@ export interface TaskItem {
model?: string; model?: string;
use_time?: string; use_time?: string;
action?: string; action?: string;
}
export interface ModelParams {
type: string;
} }