feat(ui): support batch endpoint creation with multiple API formats (#76)

Replace single API format selector with multi-select checkbox interface in endpoint creation dialog. Users can now select multiple API formats to create multiple endpoints simultaneously with shared configuration (URL, path, timeout, etc.).

- Change API format selection from dropdown to checkbox grid layout
- Add selectedFormats array to track multiple format selections
- Implement batch creation logic with individual error handling
- Update submit button to show endpoint count being created
- Adjust form layout to improve visual hierarchy
- Display appropriate success/failure messages for batch operations
- Reset selectedFormats on form reset
This commit is contained in:
AAEE86
2026-01-08 10:42:14 +08:00
committed by GitHub
parent 1521ce5a96
commit 1cf18b6e12

View File

@@ -20,44 +20,45 @@
API 配置 API 配置
</h3> </h3>
<div class="grid grid-cols-2 gap-4"> <!-- API 格式 -->
<!-- API 格式 --> <div class="space-y-2">
<div class="space-y-2"> <Label for="api_format">API 格式 *</Label>
<Label for="api_format">API 格式 *</Label> <template v-if="isEditMode">
<template v-if="isEditMode"> <Input
<Input id="api_format"
id="api_format" v-model="form.api_format"
v-model="form.api_format" disabled
disabled class="bg-muted"
class="bg-muted" />
/> <p class="text-xs text-muted-foreground">
<p class="text-xs text-muted-foreground"> API 格式创建后不可修改
API 格式创建后不可修改 </p>
</p> </template>
</template> <template v-else>
<template v-else> <div class="grid grid-cols-2 gap-3">
<Select <label
v-model="form.api_format" v-for="format in apiFormats"
v-model:open="selectOpen" :key="format.value"
required class="flex items-center gap-3 rounded-lg border p-4 cursor-pointer transition-colors hover:bg-accent"
:class="selectedFormats.includes(format.value) ? 'border-primary bg-accent' : 'border-border'"
> >
<SelectTrigger> <input
<SelectValue placeholder="请选择 API 格式" /> type="checkbox"
</SelectTrigger> :value="format.value"
<SelectContent> v-model="selectedFormats"
<SelectItem class="h-4 w-4 text-primary focus:ring-2 focus:ring-primary rounded"
v-for="format in apiFormats" />
:key="format.value" <span class="text-sm font-medium">{{ format.label }}</span>
:value="format.value" </label>
> </div>
{{ format.label }} <p class="text-xs text-muted-foreground">
</SelectItem> 选择一个或多个 API 格式将为每个格式创建独立的端点
</SelectContent> </p>
</Select> </template>
</template> </div>
</div>
<!-- API URL --> <!-- API URL 和自定义路径 -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2"> <div class="space-y-2">
<Label for="base_url">API URL *</Label> <Label for="base_url">API URL *</Label>
<Input <Input
@@ -67,16 +68,18 @@
required required
/> />
</div> </div>
</div>
<!-- 自定义路径 --> <div class="space-y-2">
<div class="space-y-2"> <Label for="custom_path">自定义请求路径可选</Label>
<Label for="custom_path">自定义请求路径可选</Label> <Input
<Input id="custom_path"
id="custom_path" v-model="form.custom_path"
v-model="form.custom_path" :placeholder="isEditMode ? defaultPathPlaceholder : '留空使用各格式的默认路径'"
:placeholder="defaultPathPlaceholder" />
/> <p v-if="!isEditMode && selectedFormats.length > 0" class="text-xs text-muted-foreground">
将为所有选中的格式使用相同的 URL 和路径配置
</p>
</div>
</div> </div>
</div> </div>
@@ -217,10 +220,10 @@
取消 取消
</Button> </Button>
<Button <Button
:disabled="loading || !form.base_url || (!isEditMode && !form.api_format)" :disabled="loading || !form.base_url || (!isEditMode && selectedFormats.length === 0)"
@click="handleSubmit()" @click="handleSubmit()"
> >
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存修改' : '创建') }} {{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存修改' : `创建 ${selectedFormats.length} 个端点`) }}
</Button> </Button>
</template> </template>
</Dialog> </Dialog>
@@ -306,6 +309,9 @@ const form = ref({
proxy_password: '', proxy_password: '',
}) })
// 选中的 API 格式(多选)
const selectedFormats = ref<string[]>([])
// API 格式列表 // API 格式列表
const apiFormats = ref<Array<{ value: string; label: string; default_path: string; aliases: string[] }>>([]) const apiFormats = ref<Array<{ value: string; label: string; default_path: string; aliases: string[] }>>([])
@@ -400,6 +406,7 @@ function resetForm() {
proxy_username: '', proxy_username: '',
proxy_password: '', proxy_password: '',
} }
selectedFormats.value = []
proxyEnabled.value = false proxyEnabled.value = false
} }
@@ -479,6 +486,8 @@ const handleSubmit = async (skipCredentialCheck = false) => {
} }
loading.value = true loading.value = true
let successCount = 0
try { try {
const proxyConfig = buildProxyConfig() const proxyConfig = buildProxyConfig()
@@ -497,27 +506,52 @@ const handleSubmit = async (skipCredentialCheck = false) => {
success('端点已更新', '保存成功') success('端点已更新', '保存成功')
emit('endpointUpdated') emit('endpointUpdated')
emit('update:modelValue', false)
} else if (props.provider) { } else if (props.provider) {
// 创建端点 // 批量创建端点
await createEndpoint(props.provider.id, { let failCount = 0
provider_id: props.provider.id, const errors: string[] = []
api_format: form.value.api_format,
base_url: form.value.base_url,
custom_path: form.value.custom_path || undefined,
timeout: form.value.timeout,
max_retries: form.value.max_retries,
max_concurrent: form.value.max_concurrent,
rate_limit: form.value.rate_limit,
is_active: form.value.is_active,
proxy: proxyConfig,
})
success('端点创建成功', '成功') for (const apiFormat of selectedFormats.value) {
emit('endpointCreated') try {
resetForm() await createEndpoint(props.provider.id, {
provider_id: props.provider.id,
api_format: apiFormat,
base_url: form.value.base_url,
custom_path: form.value.custom_path || undefined,
timeout: form.value.timeout,
max_retries: form.value.max_retries,
max_concurrent: form.value.max_concurrent,
rate_limit: form.value.rate_limit,
is_active: form.value.is_active,
proxy: proxyConfig,
})
successCount++
} catch (error: any) {
failCount++
const formatLabel = apiFormats.value.find((f: any) => f.value === apiFormat)?.label || apiFormat
errors.push(`${formatLabel}: ${error.response?.data?.detail || '创建失败'}`)
}
}
// 显示结果
if (successCount > 0 && failCount === 0) {
success(`成功创建 ${successCount} 个端点`, '创建成功')
} else if (successCount > 0 && failCount > 0) {
success(`成功创建 ${successCount} 个端点,${failCount} 个失败`, '部分成功')
if (errors.length > 0) {
log.error('创建端点失败:', errors)
}
} else {
showError(errors.join('\n') || '创建端点失败', '创建失败')
}
if (successCount > 0) {
emit('endpointCreated')
resetForm()
emit('update:modelValue', false)
}
} }
emit('update:modelValue', false)
} catch (error: any) { } catch (error: any) {
const action = isEditMode.value ? '更新' : '创建' const action = isEditMode.value ? '更新' : '创建'
showError(error.response?.data?.detail || `${action}端点失败`, '错误') showError(error.response?.data?.detail || `${action}端点失败`, '错误')