图生视频

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 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)

View File

@ -142,3 +142,7 @@
background-color: rgba(255, 255, 255, 0.1);
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;
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;
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>
<div class="reference-to-video">
<!-- 顶部卡片 -->
<!-- 顶部卡片图片上传和提示词输入区 -->
<el-card class="card">
<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>
<!-- 文本输入框 -->
<div class="input">
<el-input type="textarea" :rows="10" maxlength="400"
placeholder="支持上传图片、视频或主体作为参考。可参考视频中的动作、表情、运镜、特效、风格与构图等,并支持对元素进行增删和修改" />
<!-- 文本输入框绑定 prompt 字段 -->
<div v-if="hasPromptField" class="input">
<el-input type="textarea" :rows="6" maxlength="400" :placeholder="promptPlaceholder"
v-model="prompt" show-word-limit :disabled="generating" />
</div>
</div>
</el-card>
<!-- 图片预览对话框 -->
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<!-- 底部配置卡片 -->
<el-card class="btmcard">
<div class="select">
<!-- 模型 -->
<!-- 模型选择动态从接口加载 -->
<div class="select-item">
<div class="select-title">模型</div>
<div class="select-content">
<el-select v-model="selectModel" style="width: 100%" @change="handleChange"
popper-class="custom-select-popup">
<el-option label="Jack" value="jack" />
<el-option label="Lucy" value="lucy" />
<el-option label="yiminghe" value="Yiminghe" />
<el-option label="Disabled" value="disabled" disabled />
<el-select v-model="currentModelId" style="width: 100%" popper-class="custom-select-popup"
@change="handleModelChange" :disabled="generating">
<el-option v-for="model in models" :key="model.id" :label="model.name" :value="model.id" />
</el-select>
</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 class="select-item">
<div class="select-title">
清晰度<span class="and">&</span>编码格式
<!-- 音频上传 -->
<div v-else-if="field.uitype === 'audio'" class="audio-wrapper">
<el-input v-model="configData[field.name]" placeholder="请输入音频文件URL或点击上传" clearable
:disabled="generating" />
<el-button size="small" @click="simulateUpload" :disabled="generating"
class="upload-btn">
上传
</el-button>
</div>
</div>
</div>
<div class="select-content">
<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>
</template>
</div>
</el-card>
</div>
</template>
<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<{
(e: 'price-update', price: number): void;
(e: 'prompt-change', isEmpty: boolean): void;
(e: 'price-update', price: number): void;
(e: 'prompt-change', isEmpty: boolean): void;
(e: 'task-created', taskid: string): void;
}>();
//
const selectModel = ref('lucy')
const selectQuality = ref('1080P')
const selectCodec = ref('H265')
const selectModelviedo = ref('电影大片')
// ==================== ====================
type ConfigValue = string | number | boolean;
//
const selectedDuration = ref(5)
const selectedCount = ref(1)
// ==================== ====================
const models = ref<Model[]>([]);
const currentModelId = ref('');
const currentModel = ref<Model | null>(null);
//
const price = computed(() => {
const basePrice = selectedDuration.value > 5 ? 2 : 1
return basePrice * selectedCount.value
})
const prompt = ref('');
const uploadedFiles = ref<Record<string, string>>({});
const uploadedFileNames = ref<Record<string, string>>({});
const configData = ref<Record<string, ConfigValue>>({});
//
watch(price, (newPrice) => {
emit('price-update', newPrice)
})
const generating = ref(false);
const price = ref(0);
// change
const handleChange = (value: string) => {
console.log('selected', value)
}
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
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>
<style scoped lang="less">
: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: 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;
}
@import url('../../assets/less/Video/text/text.less');
</style>
<style>
.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;
}
.custom-select-popup .el-select-dropdown__item {
color: #ffffff !important;
background-color: transparent !important;
font-size: 14px !important;
}
.el-input__suffix-inner {
color: #fff !important;
}
.custom-select-popup .el-select-dropdown__item.selected,
.custom-select-popup .el-select-dropdown__item:hover {
background-color: #3a3d46 !important;
}
</style>

View File

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

View File

@ -17,6 +17,7 @@
v-else-if="activeTab === 'image'"
@price-update="handleImagePriceUpdate"
@prompt-change="handleImagePromptChange"
@task-created="handleTaskCreated"
/>
<TextVideo
ref="textVideoRef"
@ -59,13 +60,15 @@ interface TextVideoExposed {
resetForm: () => void;
}
/** 图生视频组件暴露的方法(示例,根据实际情况补充) */
/** 图生视频组件暴露的方法 */
interface ImgVideoExposed {
generate: () => Promise<boolean>;
resetForm: () => void;
}
/** 参考生视频组件暴露的方法(示例) */
/** 参考生视频组件暴露的方法 */
interface ReferenceVideoExposed {
generate?: () => Promise<boolean>;
resetForm: () => void;
}
@ -123,7 +126,11 @@ const handleTabChange = (tab: 'reference' | 'image' | 'text') => {
//
const handleCreate = async () => {
if (isCreateButtonDisabled.value) return;
if (isCreateButtonDisabled.value) {
ElMessage.warning('请先填写必要信息');
return;
}
let result = false;
switch (activeTab.value) {
case 'text':
@ -131,6 +138,18 @@ const handleCreate = async () => {
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;
@ -200,7 +219,7 @@ onMounted(() => updateActiveTabFromRoute());
left: 0;
width: 100%;
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;
}
.create-button {

View File

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