464 lines
13 KiB
Vue
464 lines
13 KiB
Vue
<template>
|
||
<div class="reference-to-video">
|
||
<!-- 顶部卡片:提示词输入区 -->
|
||
<el-card class="card">
|
||
<div class="reference-container">
|
||
<div class="input">
|
||
<el-input
|
||
type="textarea"
|
||
:rows="8"
|
||
maxlength="400"
|
||
:placeholder="promptPlaceholder"
|
||
v-model="prompt"
|
||
show-word-limit
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 底部卡片:模型及配置项 -->
|
||
<el-card class="btmcard">
|
||
<div class="select">
|
||
<!-- 模型选择 -->
|
||
<div class="select-item">
|
||
<div class="select-title">模型</div>
|
||
<div class="select-content">
|
||
<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">
|
||
<!-- 下拉选择 -->
|
||
<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>
|
||
|
||
<!-- 文本输入 -->
|
||
<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"
|
||
/>
|
||
|
||
<!-- 整数输入 -->
|
||
<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"
|
||
/>
|
||
|
||
<!-- 音频上传 -->
|
||
<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>
|
||
|
||
<!-- 其他未知类型 -->
|
||
<el-input
|
||
v-else
|
||
v-model="configData[field.name]"
|
||
:placeholder="`请输入${field.label}`"
|
||
clearable
|
||
:disabled="generating"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch } from 'vue';
|
||
import { ElMessage } from 'element-plus';
|
||
import { textvideo, AllModel, queryModelPrice } from '../../apis/Model';
|
||
import type {
|
||
Model,
|
||
AllModelResponse,
|
||
PriceQueryParams,
|
||
PriceQueryResponse,
|
||
TextVideoParams,
|
||
TextVideoResponse,
|
||
ModelParams,
|
||
} from '../../types/model';
|
||
|
||
// ==================== 事件定义 ====================
|
||
const emit = defineEmits<{
|
||
(e: 'price-update', price: number): void;
|
||
(e: 'prompt-change', isEmpty: boolean): void;
|
||
(e: 'task-created', taskid: string): void;
|
||
}>();
|
||
|
||
// ==================== 类型定义 ====================
|
||
/** 配置字段的值类型(不包括 prompt) */
|
||
type ConfigValue = string | number | boolean;
|
||
|
||
// ==================== 数据定义 ====================
|
||
const models = ref<Model[]>([]);
|
||
const currentModelId = ref('');
|
||
const currentModel = ref<Model | null>(null);
|
||
|
||
/** 单独的提示词字段,确保是 string 类型 */
|
||
const prompt = ref('');
|
||
|
||
/** 其他配置字段的集合 */
|
||
const configData = ref<Record<string, ConfigValue>>({});
|
||
|
||
const generating = ref(false);
|
||
const price = ref(0);
|
||
|
||
// ==================== 计算属性 ====================
|
||
const visibleFields = computed(() => {
|
||
if (!currentModel.value) return [];
|
||
return currentModel.value.input_fields.filter(field => 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.name === 'prompt') {
|
||
// 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.name === 'prompt') return;
|
||
const value = configData.value[field.name];
|
||
if (value !== undefined) {
|
||
if (field.uitype === 'audio') {
|
||
configDataForPrice.audio = !!value;
|
||
} else {
|
||
configDataForPrice[field.name] = value;
|
||
}
|
||
}
|
||
});
|
||
|
||
configDataForPrice.action = currentModel.value.apiname;
|
||
|
||
if (!configDataForPrice.num && !configDataForPrice.count) {
|
||
configDataForPrice.num = 1;
|
||
}
|
||
|
||
return {
|
||
product_type: 'llm',
|
||
product_id: currentModel.value.id,
|
||
config_data: configDataForPrice,
|
||
};
|
||
};
|
||
|
||
let priceTimer: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
const fetchPrice = () => {
|
||
if (priceTimer) clearTimeout(priceTimer);
|
||
|
||
priceTimer = setTimeout(async () => {
|
||
const trimmedPrompt = prompt.value?.trim();
|
||
if (!trimmedPrompt) {
|
||
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 loadModels = async () => {
|
||
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) {
|
||
currentModelId.value = models.value[0].id;
|
||
currentModel.value = models.value[0];
|
||
initFormData();
|
||
fetchPrice();
|
||
}
|
||
} else {
|
||
ElMessage.error('获取模型列表失败');
|
||
}
|
||
|
||
};
|
||
|
||
const handleModelChange = (modelId: string) => {
|
||
const selected = models.value.find(m => m.id === modelId);
|
||
if (selected) {
|
||
currentModel.value = selected;
|
||
initFormData();
|
||
fetchPrice();
|
||
}
|
||
};
|
||
|
||
// ==================== 监听器 ====================
|
||
watch(
|
||
() => [configData.value, currentModelId.value],
|
||
() => {
|
||
if (currentModel.value) fetchPrice();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
watch(
|
||
() => prompt.value,
|
||
(newVal) => {
|
||
const isEmpty = !newVal?.trim();
|
||
emit('prompt-change', isEmpty);
|
||
if (isEmpty) {
|
||
price.value = 0;
|
||
emit('price-update', 0);
|
||
} else {
|
||
fetchPrice();
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// ==================== 辅助方法 ====================
|
||
const simulateUpload = () => {
|
||
ElMessage.info('此处可对接实际音频上传服务');
|
||
configData.value.audio_file = 'https://example.com/demo_audio.mp3';
|
||
};
|
||
|
||
// ==================== 生成视频 ====================
|
||
const buildSubmitParams = (): TextVideoParams => {
|
||
const baseParams: TextVideoParams = {
|
||
llmid: currentModel.value!.id,
|
||
prompt: prompt.value, // 确保是 string
|
||
};
|
||
|
||
if (configData.value.duration !== undefined) {
|
||
baseParams.duration = Number(configData.value.duration);
|
||
}
|
||
if (configData.value.ratio !== undefined) {
|
||
baseParams.ratio = String(configData.value.ratio);
|
||
}
|
||
if (configData.value.resolution !== undefined) {
|
||
baseParams.resolution = String(configData.value.resolution);
|
||
}
|
||
if (configData.value.size !== undefined && !baseParams.resolution) {
|
||
baseParams.resolution = String(configData.value.size);
|
||
}
|
||
baseParams.audio = Boolean(configData.value.audio_file);
|
||
|
||
return baseParams;
|
||
};
|
||
|
||
const generate = async (): Promise<boolean> => {
|
||
if (!currentModel.value) {
|
||
ElMessage.error('请选择模型');
|
||
return false;
|
||
}
|
||
|
||
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 {
|
||
const val = configData.value[field.name];
|
||
if (!val) missingFields.push(field.label);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (missingFields.length > 0) {
|
||
ElMessage.error(`请填写必填项:${missingFields.join('、')}`);
|
||
return false;
|
||
}
|
||
|
||
generating.value = true;
|
||
|
||
try {
|
||
const params = buildSubmitParams();
|
||
const res = (await textvideo(params)) as TextVideoResponse;
|
||
|
||
// 尝试从多个位置获取 taskid
|
||
let taskid = res.data?.taskid || res.taskid;
|
||
|
||
if (res.status === 'created' || res.status === 'processing' || res.status === 'queueing') {
|
||
const statusMsg = res.status === 'created' ? '视频生成任务已提交' : (res.status === 'processing' ? '进行中' : '排队中');
|
||
ElMessage.success(`${statusMsg},请稍后在任务列表中查看`);
|
||
|
||
if (taskid) {
|
||
emit('task-created', taskid);
|
||
} else {
|
||
console.warn('响应中没有 taskid,尝试使用其他方式获取');
|
||
// 如果响应中没有 taskid,可能需要从其他字段获取
|
||
// 这里先记录日志,不做其他处理
|
||
}
|
||
resetForm();
|
||
return true;
|
||
} else {
|
||
ElMessage.error(res.message || '生成失败,请重试');
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error('生成失败:', error);
|
||
ElMessage.error(error instanceof Error ? error.message : '生成失败,请重试');
|
||
return false;
|
||
} finally {
|
||
generating.value = false;
|
||
}
|
||
};
|
||
|
||
const resetForm = () => {
|
||
prompt.value = '';
|
||
initFormData();
|
||
price.value = 0;
|
||
emit('price-update', 0);
|
||
emit('prompt-change', true);
|
||
};
|
||
|
||
// ==================== 暴露给父组件 ====================
|
||
defineExpose({
|
||
generate,
|
||
resetForm,
|
||
});
|
||
|
||
// ==================== 生命周期 ====================
|
||
onMounted(() => {
|
||
loadModels();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@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;
|
||
}
|
||
.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__wrapper {
|
||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
border-radius: 10px !important;
|
||
}
|
||
.el-select .el-input__inner {
|
||
color: #fff !important;
|
||
}
|
||
.el-select .el-input__suffix-inner {
|
||
color: #fff !important;
|
||
}
|
||
.el-textarea .el-input__count {
|
||
background: transparent;
|
||
color: #ffffff7a;
|
||
}
|
||
</style> |