fix:修复图像参数映射
This commit is contained in:
1 parent
6809081f37
commit
8f3fe333c7
4 files changed
+416
-39
No files matched your search
@@ -20,6 +20,18 @@ func convertImageRequestToChat(requestBody []byte, info *common.RelayInfo) (io.R
|
||||
return nil, fmt.Errorf("parse image request: %w", err)
|
||||
}
|
||||
|
||||
generationConfig := &dto.GeminiGenerationConfig{
|
||||
ResponseModalities: []string{"TEXT", "IMAGE"},
|
||||
}
|
||||
|
||||
// 将 OpenAI 的 size 参数映射为 Gemini 的 imageConfig(宽高比 + 分辨率)
|
||||
if imgReq.Size != "" || imgReq.Quality != "" {
|
||||
generationConfig.ImageConfig = &dto.GeminiImageConfig{
|
||||
AspectRatio: imageSizeToAspectRatio(imgReq.Size),
|
||||
ImageSize: qualityToImageSize(imgReq.Quality),
|
||||
}
|
||||
}
|
||||
|
||||
chatReq := dto.GeminiChatRequest{
|
||||
Contents: []dto.GeminiContent{
|
||||
{
|
||||
@@ -29,9 +41,7 @@ func convertImageRequestToChat(requestBody []byte, info *common.RelayInfo) (io.R
|
||||
},
|
||||
},
|
||||
},
|
||||
GenerationConfig: &dto.GeminiGenerationConfig{
|
||||
ResponseModalities: []string{"TEXT", "IMAGE"},
|
||||
},
|
||||
GenerationConfig: generationConfig,
|
||||
}
|
||||
|
||||
result, err := json.Marshal(chatReq)
|
||||
@@ -177,8 +187,13 @@ func handleImagenResponse(_ context.Context, resp *http.Response, info *common.R
|
||||
return &common.Usage{}, nil
|
||||
}
|
||||
|
||||
// imageSizeToAspectRatio 将 OpenAI 图片尺寸映射为 Imagen 宽高比
|
||||
// imageSizeToAspectRatio 将图片尺寸映射为 Gemini 宽高比
|
||||
// 支持 OpenAI 格式(1024x1024)和 Gemini 原生比例格式(1:1/16:9 等)直接透传
|
||||
func imageSizeToAspectRatio(size string) string {
|
||||
if size == "" {
|
||||
return ""
|
||||
}
|
||||
// OpenAI 格式映射
|
||||
switch size {
|
||||
case "1024x1024":
|
||||
return "1:1"
|
||||
@@ -198,7 +213,7 @@ func imageSizeToAspectRatio(size string) string {
|
||||
return "21:9"
|
||||
}
|
||||
|
||||
// 支持原生比例格式(如 "1:1"、"16:9")
|
||||
// Gemini 原生比例格式(如 "1:1"、"16:9"、"3:4")直接透传
|
||||
if strings.Contains(size, ":") {
|
||||
return size
|
||||
}
|
||||
@@ -206,9 +221,20 @@ func imageSizeToAspectRatio(size string) string {
|
||||
return "1:1"
|
||||
}
|
||||
|
||||
// qualityToImageSize 将 OpenAI quality 映射为 Imagen imageSize
|
||||
// qualityToImageSize 将 OpenAI quality 映射为 Gemini imageSize
|
||||
// 支持 OpenAI 格式(hd/standard)和 Gemini 原生格式(256/512/1K/2K/4K)直接透传
|
||||
func qualityToImageSize(quality string) string {
|
||||
switch strings.ToLower(quality) {
|
||||
if quality == "" {
|
||||
return ""
|
||||
}
|
||||
// Gemini 原生格式直接透传(256/512/1K/2K/4K)
|
||||
lower := strings.ToLower(quality)
|
||||
switch lower {
|
||||
case "256", "512", "1k", "2k", "4k":
|
||||
return quality // 保持用户原始大小写
|
||||
}
|
||||
// OpenAI 兼容格式映射
|
||||
switch lower {
|
||||
case "hd", "high":
|
||||
return "2K"
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { createPlaygroundApi } from '@/utils/playgroundApi'
|
||||
import { calculateCost } from './calculateCost'
|
||||
import ParamRefModal from './ParamRefModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseSelect from '../../../components/common/BaseSelect.vue'
|
||||
|
||||
@@ -22,21 +23,106 @@ const selectedModelItem = computed(() =>
|
||||
props.models.find(m => m.model_id === selectedModel.value),
|
||||
)
|
||||
const prompt = ref('')
|
||||
const size = ref('1920x1080')
|
||||
const quality = ref('standard')
|
||||
const modelOptions = computed(() => props.models.map(m => ({ value: m.model_id, label: m.model_name || m.model_id })))
|
||||
|
||||
const sizePresets = [
|
||||
{ value: '1280x720', label: '720p' },
|
||||
{ value: '1920x1080', label: '1080p' },
|
||||
{ value: '2560x1440', label: '2K' },
|
||||
{ value: '3840x2160', label: '4K' },
|
||||
]
|
||||
// ── 自定义参数系统 ──
|
||||
|
||||
function applySizePreset(val: string) {
|
||||
size.value = val
|
||||
interface CustomParam {
|
||||
id: number
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const customParams = ref<CustomParam[]>([])
|
||||
let nextParamId = 1
|
||||
const showParamRef = ref(false)
|
||||
|
||||
// 模型预设模板
|
||||
const MODEL_PRESETS: Record<string, Record<string, string>> = {
|
||||
'gpt-image-1': {
|
||||
size: '1024x1024',
|
||||
quality: 'auto',
|
||||
response_format: 'b64_json',
|
||||
output_format: 'png',
|
||||
},
|
||||
'dall-e-3': {
|
||||
size: '1024x1024',
|
||||
quality: 'standard',
|
||||
style: 'vivid',
|
||||
response_format: 'url',
|
||||
},
|
||||
'dall-e-2': {
|
||||
size: '1024x1024',
|
||||
response_format: 'url',
|
||||
},
|
||||
}
|
||||
|
||||
// 当前模型的预设(支持子串匹配,如 openai/gpt-image-1)
|
||||
const currentPreset = computed(() => {
|
||||
const id = selectedModel.value
|
||||
if (MODEL_PRESETS[id]) return MODEL_PRESETS[id]
|
||||
for (const key of Object.keys(MODEL_PRESETS)) {
|
||||
if (id.includes(key)) return MODEL_PRESETS[key]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 预设标签名(用于按钮显示)
|
||||
const currentPresetLabel = computed(() => {
|
||||
const id = selectedModel.value
|
||||
for (const key of Object.keys(MODEL_PRESETS)) {
|
||||
if (id === key || id.includes(key)) {
|
||||
// 美化显示:gpt-image-1 → GPT Image 1
|
||||
return key.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 有效参数数量
|
||||
const validParamsCount = computed(() =>
|
||||
customParams.value.filter(p => p.key.trim() && p.value.trim()).length,
|
||||
)
|
||||
|
||||
function addParam(key = '', value = '') {
|
||||
customParams.value.push({ id: nextParamId++, key, value })
|
||||
}
|
||||
|
||||
function removeParam(id: number) {
|
||||
const idx = customParams.value.findIndex(p => p.id === id)
|
||||
if (idx !== -1) customParams.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function applyPreset() {
|
||||
const preset = currentPreset.value
|
||||
if (!preset) return
|
||||
customParams.value = []
|
||||
for (const [key, value] of Object.entries(preset)) {
|
||||
addParam(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 智能类型推断:字符串 → boolean / number / string
|
||||
function parseValue(raw: string): string | number | boolean {
|
||||
if (raw.toLowerCase() === 'true') return true
|
||||
if (raw.toLowerCase() === 'false') return false
|
||||
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
||||
const num = Number(raw)
|
||||
if (!isNaN(num) && isFinite(num)) return num
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// 切换模型时自动应用预设
|
||||
watch(selectedModel, () => {
|
||||
const preset = currentPreset.value
|
||||
if (preset) {
|
||||
applyPreset()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ── 图片生成 ──
|
||||
|
||||
interface ImageResult { b64_json?: string; url?: string; revised_prompt?: string }
|
||||
const images = ref<ImageResult[]>([])
|
||||
const tokenUsage = reactive({ promptTokens: 0, totalTokens: 0, cost: '' })
|
||||
@@ -48,12 +134,21 @@ async function generate() {
|
||||
|
||||
try {
|
||||
const api = createPlaygroundApi(props.apiKey)
|
||||
const res = await api.post('/v1/images/generations', {
|
||||
|
||||
// 组装请求体:固定字段 + 自定义参数
|
||||
const body: Record<string, any> = {
|
||||
model: selectedModel.value,
|
||||
prompt: prompt.value,
|
||||
size: size.value,
|
||||
quality: quality.value,
|
||||
}, { timeout: 300_000 })
|
||||
}
|
||||
for (const param of customParams.value) {
|
||||
const key = param.key.trim()
|
||||
const val = param.value.trim()
|
||||
if (key && val) {
|
||||
body[key] = parseValue(val)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await api.post('/v1/images/generations', body, { timeout: 300_000 })
|
||||
|
||||
const data = res.data
|
||||
images.value = data.data || []
|
||||
@@ -96,35 +191,95 @@ function downloadImage(img: ImageResult, idx: number) {
|
||||
<h3 class="text-sm font-semibold text-gray-900">图片生成参数</h3>
|
||||
</div>
|
||||
<div class="card-body space-y-4">
|
||||
<!-- 模型 -->
|
||||
<div>
|
||||
<label class="input-label">模型</label>
|
||||
<BaseSelect v-model="selectedModel" :options="modelOptions" />
|
||||
</div>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<div>
|
||||
<label class="input-label">提示词</label>
|
||||
<textarea v-model="prompt" class="input" rows="4" placeholder="描述你想生成的图片..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">尺寸</label>
|
||||
<input v-model="size" class="input" placeholder="如 1920x1080" />
|
||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||
<button
|
||||
v-for="p in sizePresets"
|
||||
:key="p.value"
|
||||
class="text-xs px-2 py-1 rounded-lg border transition-colors duration-150 cursor-pointer"
|
||||
:class="size === p.value
|
||||
? 'bg-primary-50 border-primary-300 text-primary-700'
|
||||
: 'bg-white border-gray-200 text-gray-600 hover:border-primary-200 hover:text-primary-600'"
|
||||
@click="applySizePreset(p.value)"
|
||||
|
||||
<!-- 自定义参数 -->
|
||||
<div class="border-t border-gray-100 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-gray-900">自定义参数</h4>
|
||||
<span v-if="validParamsCount > 0" class="badge badge-primary">{{ validParamsCount }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
class="text-xs px-2.5 py-1.5 rounded-lg text-gray-600 hover:border-primary-300 hover:text-primary-600 transition-colors duration-150 cursor-pointer flex items-center gap-1.5 border border-gray-200"
|
||||
@click="showParamRef = true"
|
||||
>
|
||||
<Icon name="bookOpen" size="xs" />
|
||||
参数参考
|
||||
</button>
|
||||
<button
|
||||
v-if="currentPreset && customParams.length > 0"
|
||||
class="text-xs px-2 py-1 rounded-lg text-primary-600 hover:bg-primary-50 transition-colors duration-150 cursor-pointer"
|
||||
@click="applyPreset"
|
||||
>
|
||||
重置预设
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="addParam()"
|
||||
>
|
||||
<Icon name="plus" size="xs" />
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="customParams.length === 0"
|
||||
class="rounded-xl border-2 border-dashed border-gray-200 py-6 text-center cursor-pointer hover:border-primary-300 hover:bg-primary-50/30 transition-colors duration-200"
|
||||
@click="currentPreset ? applyPreset() : addParam()"
|
||||
>
|
||||
<Icon name="plus" size="sm" class="text-gray-300 mx-auto mb-2" />
|
||||
<p class="text-xs text-gray-400">
|
||||
{{ currentPreset ? '点击应用 ' + currentPresetLabel + ' 预设' : '点击添加自定义参数' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 参数行 -->
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="param in customParams"
|
||||
:key="param.id"
|
||||
class="flex items-center gap-2 group"
|
||||
>
|
||||
{{ p.label }}
|
||||
<input
|
||||
v-model="param.key"
|
||||
class="input flex-1"
|
||||
placeholder="参数名"
|
||||
/>
|
||||
<input
|
||||
v-model="param.value"
|
||||
class="input flex-1"
|
||||
placeholder="值"
|
||||
/>
|
||||
<button
|
||||
class="h-9 w-9 rounded-xl flex items-center justify-center text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors duration-150 flex-shrink-0 cursor-pointer"
|
||||
@click="removeParam(param.id)"
|
||||
>
|
||||
<Icon name="x" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="w-full text-xs py-1.5 rounded-lg border border-dashed border-gray-200 text-gray-400 hover:border-primary-300 hover:text-primary-600 transition-colors duration-150 cursor-pointer"
|
||||
@click="addParam()"
|
||||
>
|
||||
+ 添加参数
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">质量</label>
|
||||
<BaseSelect v-model="quality" :options="[{value:'standard',label:'标准'},{value:'hd',label:'高清'}]" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-full" :disabled="sending || !prompt.trim()" @click="generate">
|
||||
{{ sending ? '生成中...' : '生成图片' }}
|
||||
</button>
|
||||
@@ -169,4 +324,7 @@ function downloadImage(img: ImageResult, idx: number) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数参考弹窗 -->
|
||||
<ParamRefModal :show="showParamRef" mode="image" @close="showParamRef = false" />
|
||||
</template>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
|
||||
export interface ParamDef {
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
default?: string
|
||||
values?: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ModelParamGroup {
|
||||
key: string
|
||||
label: string
|
||||
params: ParamDef[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
mode: 'image' | 'video'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const activeGroup = ref('')
|
||||
|
||||
// ── 图像模型参数参考 ──
|
||||
|
||||
const imageGroups: ModelParamGroup[] = [
|
||||
{
|
||||
key: 'gemini-image',
|
||||
label: 'Gemini',
|
||||
params: [
|
||||
{ name: 'prompt', type: 'string', required: true, description: '图像描述文本,支持中英文,可描述画面内容、风格、构图等' },
|
||||
{ name: 'model', type: 'string', required: true, description: '模型 ID,如 gemini-2.5-flash-image、gemini-3-pro-image-preview' },
|
||||
{ name: 'n', type: 'integer', required: false, default: '1', values: ['1'], description: '生成图像数量,目前仅支持 1' },
|
||||
{ name: 'size', type: 'string', required: false, default: '1024x1024', values: ['1024x1024', '1536x1024', '1024x1536'], description: '图像尺寸(宽x高)。支持正方形和横/纵向' },
|
||||
{ name: 'response_format', type: 'string', required: false, default: 'b64_json', values: ['b64_json', 'url'], description: '返回格式。b64_json 返回 Base64 编码;url 返回临时下载链接' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'gpt-image',
|
||||
label: 'GPT Image',
|
||||
params: [
|
||||
{ name: 'prompt', type: 'string', required: true, description: '图像描述文本,最长 32000 字符' },
|
||||
{ name: 'model', type: 'string', required: true, description: '模型 ID:gpt-image-1、gpt-image-1-mini、gpt-image-1.5、gpt-image-2' },
|
||||
{ name: 'n', type: 'integer', required: false, default: '1', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], description: '生成图像数量,1-10' },
|
||||
{ name: 'size', type: 'string', required: false, default: '1024x1024', values: ['1024x1024', '1536x1024', '1024x1536', 'auto'], description: '图像尺寸。gpt-image-2 支持灵活分辨率(最大边 ≤ 3840px,16 的倍数)' },
|
||||
{ name: 'quality', type: 'string', required: false, default: 'auto', values: ['auto', 'high', 'medium', 'low'], description: '图像质量' },
|
||||
{ name: 'response_format', type: 'string', required: false, default: 'b64_json', values: ['b64_json', 'url'], description: '返回格式' },
|
||||
{ name: 'background', type: 'string', required: false, default: 'auto', values: ['auto', 'opaque', 'transparent'], description: '背景透明度。仅支持 PNG/WebP。gpt-image-2 不支持 transparent' },
|
||||
{ name: 'output_format', type: 'string', required: false, default: 'png', values: ['png', 'jpeg', 'webp'], description: '输出图像格式' },
|
||||
{ name: 'output_compression', type: 'integer', required: false, description: '输出压缩率 0-100,仅 jpeg/webp 有效。100 = 无压缩' },
|
||||
{ name: 'moderation', type: 'string', required: false, default: 'auto', values: ['auto', 'low'], description: '内容审核严格度' },
|
||||
{ name: 'partial_images', type: 'integer', required: false, default: '0', values: ['0', '1', '2', '3'], description: '流式中间图数量 0-3,设为非 0 自动启用流式' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'dall-e',
|
||||
label: 'DALL-E',
|
||||
params: [
|
||||
{ name: 'prompt', type: 'string', required: true, description: '图像描述文本。dall-e-2 最长 1000 字符,dall-e-3 最长 4000 字符' },
|
||||
{ name: 'model', type: 'string', required: true, description: '模型 ID:dall-e-2 或 dall-e-3' },
|
||||
{ name: 'n', type: 'integer', required: false, default: '1', values: ['1'], description: '生成数量,目前仅支持 1' },
|
||||
{ name: 'size', type: 'string', required: false, default: '1024x1024', values: ['256x256', '512x512', '1024x1024', '1024x1792', '1792x1024'], description: 'dall-e-2: 256/512/1024 正方形;dall-e-3: 1024 正方形 + 1792 横纵' },
|
||||
{ name: 'quality', type: 'string', required: false, default: 'standard', values: ['standard', 'hd'], description: '仅 dall-e-3 支持。standard 标准,hd 高清' },
|
||||
{ name: 'response_format', type: 'string', required: false, default: 'url', values: ['url', 'b64_json'], description: '返回格式' },
|
||||
{ name: 'style', type: 'string', required: false, default: 'vivid', values: ['vivid', 'natural'], description: '仅 dall-e-3 支持。vivid 生动,natural 自然' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ── 视频模型参数参考(预留) ──
|
||||
|
||||
const videoGroups: ModelParamGroup[] = [
|
||||
{
|
||||
key: 'gemini-video',
|
||||
label: 'Gemini Veo',
|
||||
params: [
|
||||
{ name: 'prompt', type: 'string', required: true, description: '视频描述文本' },
|
||||
{ name: 'model', type: 'string', required: true, description: '模型 ID,如 veo-3.1-generate-preview' },
|
||||
{ name: 'resolution', type: 'string', required: false, default: '1080p', values: ['720p', '1080p', '4K'], description: '输出分辨率' },
|
||||
{ name: 'duration_seconds', type: 'integer', required: false, values: ['4', '6', '8'], description: '视频时长(秒)' },
|
||||
{ name: 'aspect_ratio', type: 'string', required: false, description: '视频宽高比' },
|
||||
{ name: 'negative_prompt', type: 'string', required: false, description: '排除的内容描述' },
|
||||
{ name: 'style', type: 'string', required: false, values: ['cinematic', 'creative'], description: '视觉风格' },
|
||||
{ name: 'seed', type: 'integer', required: false, description: '确定性生成的种子值' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const groups = computed(() => props.mode === 'image' ? imageGroups : videoGroups)
|
||||
|
||||
// 自动选中第一个 group
|
||||
watch(() => props.show, (val) => {
|
||||
if (val && !activeGroup.value) {
|
||||
activeGroup.value = groups.value[0]?.key || ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(groups, (val) => {
|
||||
if (val.length > 0 && !val.find(g => g.key === activeGroup.value)) {
|
||||
activeGroup.value = val[0].key
|
||||
}
|
||||
})
|
||||
|
||||
const currentGroup = computed(() => groups.value.find(g => g.key === activeGroup.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseModal :show="show" title="参数参考" width="extra-wide" @close="emit('close')">
|
||||
<!-- 模型 Tab 栏 -->
|
||||
<div class="tabs mb-4">
|
||||
<button
|
||||
v-for="g in groups"
|
||||
:key="g.key"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeGroup === g.key }"
|
||||
@click="activeGroup = g.key"
|
||||
>
|
||||
{{ g.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 参数表格 -->
|
||||
<div v-if="currentGroup" class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-36">参数名</th>
|
||||
<th class="w-20">类型</th>
|
||||
<th class="w-14">必填</th>
|
||||
<th class="w-24">默认值</th>
|
||||
<th>可选值</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in currentGroup.params" :key="p.name">
|
||||
<td>
|
||||
<code class="text-xs font-mono text-primary-600 bg-primary-50 px-1.5 py-0.5 rounded">{{ p.name }}</code>
|
||||
</td>
|
||||
<td class="text-xs text-gray-500">{{ p.type }}</td>
|
||||
<td>
|
||||
<span v-if="p.required" class="text-xs font-medium text-red-500">是</span>
|
||||
<span v-else class="text-xs text-gray-400">否</span>
|
||||
</td>
|
||||
<td class="text-xs text-gray-500">{{ p.default || '—' }}</td>
|
||||
<td>
|
||||
<template v-if="p.values?.length">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span v-for="v in p.values" :key="v" class="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">{{ v }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="text-xs text-gray-400">—</span>
|
||||
</td>
|
||||
<td class="text-xs text-gray-600">{{ p.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50/60 px-4 py-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon name="infoCircle" size="sm" class="text-amber-500 mt-0.5 flex-shrink-0" />
|
||||
<div class="text-xs text-amber-700 space-y-1">
|
||||
<p>以上参数通过 <code class="bg-amber-100 px-1 rounded">/v1/images/generations</code> 端点发送,使用自定义参数区域逐行添加。</p>
|
||||
<p>参数名为 JSON 键名,值为对应格式的字符串(系统会自动识别布尔值和数字)。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { createPlaygroundApi } from '@/utils/playgroundApi'
|
||||
import ParamRefModal from './ParamRefModal.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import BaseSelect from '../../../components/common/BaseSelect.vue'
|
||||
|
||||
@@ -14,6 +15,7 @@ const props = defineProps<{ models: ModelItem[]; apiKey: string }>()
|
||||
const submitting = ref(false)
|
||||
const selectedModel = ref(props.models[0]?.model_id || '')
|
||||
const prompt = ref('')
|
||||
const showParamRef = ref(false)
|
||||
const resolution = ref('1280x720')
|
||||
const duration = ref(5)
|
||||
|
||||
@@ -212,6 +214,15 @@ function downloadVideo() {
|
||||
<label class="input-label">时长({{ duration }} 秒)</label>
|
||||
<input v-model.number="duration" type="number" class="input" min="5" max="15" step="1" />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class="text-xs px-2.5 py-1.5 rounded-lg text-gray-600 hover:border-primary-300 hover:text-primary-600 transition-colors duration-150 cursor-pointer flex items-center gap-1.5 border border-gray-200"
|
||||
@click="showParamRef = true"
|
||||
>
|
||||
<Icon name="bookOpen" size="xs" />
|
||||
参数参考
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary w-full" :disabled="submitting || polling || !prompt.trim()" @click="submitTask">
|
||||
{{ submitting ? '提交中...' : polling ? '生成中...' : '生成视频' }}
|
||||
</button>
|
||||
@@ -292,4 +303,7 @@ function downloadVideo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数参考弹窗 -->
|
||||
<ParamRefModal :show="showParamRef" mode="video" @close="showParamRef = false" />
|
||||
</template>
|
||||
Reference in new issue
Block a user