yuanjing/src/components/video/TextVideo.vue
2026-04-17 17:41:38 +08:00

464 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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