first commit
This commit is contained in:
322
web/src/components/ProviderForm.vue
Normal file
322
web/src/components/ProviderForm.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-placement="left"
|
||||
label-width="100px"
|
||||
>
|
||||
<n-form-item label="服务类型" path="type">
|
||||
<n-select
|
||||
v-model:value="formData.type"
|
||||
:options="providerTypeOptions"
|
||||
placeholder="选择服务类型"
|
||||
@update:value="handleTypeChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="服务名称" path="name">
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="输入自定义名称"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="API密钥" path="apiKey" v-if="needsApiKey">
|
||||
<n-input
|
||||
v-model:value="formData.apiKey"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="输入API密钥"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="API地址" path="baseUrl" v-if="needsBaseUrl">
|
||||
<n-input
|
||||
v-model:value="formData.baseUrl"
|
||||
placeholder="输入API基础地址"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="组织ID" path="organization" v-if="formData.type === 'openai'">
|
||||
<n-input
|
||||
v-model:value="formData.organization"
|
||||
placeholder="输入组织ID(可选)"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- 高级配置 -->
|
||||
<n-collapse>
|
||||
<n-collapse-item title="高级配置" name="advanced">
|
||||
<div class="advanced-settings">
|
||||
<n-form-item label="请求超时">
|
||||
<n-input-number
|
||||
v-model:value="formData.timeout"
|
||||
:min="1"
|
||||
:max="120"
|
||||
placeholder="秒"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>秒</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="最大重试">
|
||||
<n-input-number
|
||||
v-model:value="formData.maxRetries"
|
||||
:min="0"
|
||||
:max="5"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="代理设置">
|
||||
<n-input
|
||||
v-model:value="formData.proxy"
|
||||
placeholder="http://proxy:port(可选)"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="自定义头部">
|
||||
<n-dynamic-input
|
||||
v-model:value="formData.headers"
|
||||
:on-create="() => ({ key: '', value: '' })"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div style="display: flex; align-items: center; width: 100%; gap: 8px;">
|
||||
<n-input
|
||||
v-model:value="value.key"
|
||||
placeholder="Header名称"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="value.value"
|
||||
placeholder="Header值"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</n-dynamic-input>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleSubmit" :loading="testing">
|
||||
{{ provider ? '保存' : '添加' }}
|
||||
</n-button>
|
||||
<n-button @click="handleTest" :loading="testing">测试连接</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
NButton,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NDynamicInput,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
|
||||
interface Props {
|
||||
provider?: any
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'save', data: any): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const message = useMessage()
|
||||
const formRef = ref()
|
||||
const testing = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
type: 'openai',
|
||||
name: '',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
organization: '',
|
||||
timeout: 30,
|
||||
maxRetries: 3,
|
||||
proxy: '',
|
||||
headers: [] as Array<{ key: string; value: string }>
|
||||
})
|
||||
|
||||
// 服务类型选项
|
||||
const providerTypeOptions = [
|
||||
{ label: 'OpenAI', value: 'openai' },
|
||||
{ label: 'Claude (Anthropic)', value: 'claude' },
|
||||
{ label: 'Google Gemini', value: 'google' },
|
||||
{ label: 'Ollama', value: 'ollama' },
|
||||
{ label: '自定义API', value: 'custom' }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const needsApiKey = computed(() => {
|
||||
return ['openai', 'claude', 'google', 'custom'].includes(formData.value.type)
|
||||
})
|
||||
|
||||
const needsBaseUrl = computed(() => {
|
||||
return ['ollama', 'custom'].includes(formData.value.type)
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
type: {
|
||||
required: true,
|
||||
message: '请选择服务类型',
|
||||
trigger: 'change'
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入服务名称',
|
||||
trigger: ['blur', 'input']
|
||||
},
|
||||
apiKey: {
|
||||
required: needsApiKey.value,
|
||||
message: '请输入API密钥',
|
||||
trigger: ['blur', 'input']
|
||||
},
|
||||
baseUrl: {
|
||||
required: needsBaseUrl.value,
|
||||
message: '请输入API地址',
|
||||
trigger: ['blur', 'input']
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 props 变化
|
||||
watch(() => props.provider, (newProvider) => {
|
||||
if (newProvider) {
|
||||
Object.assign(formData.value, {
|
||||
type: newProvider.type || 'openai',
|
||||
name: newProvider.name || '',
|
||||
apiKey: newProvider.apiKey || '',
|
||||
baseUrl: newProvider.baseUrl || '',
|
||||
organization: newProvider.config?.organization || '',
|
||||
timeout: newProvider.config?.timeout || 30,
|
||||
maxRetries: newProvider.config?.maxRetries || 3,
|
||||
proxy: newProvider.config?.proxy || '',
|
||||
headers: newProvider.config?.headers || []
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 处理类型变化
|
||||
const handleTypeChange = (type: string) => {
|
||||
// 设置默认名称
|
||||
const defaultNames = {
|
||||
openai: 'OpenAI',
|
||||
claude: 'Claude',
|
||||
google: 'Google Gemini',
|
||||
ollama: 'Ollama',
|
||||
custom: '自定义服务'
|
||||
}
|
||||
|
||||
if (!formData.value.name || Object.values(defaultNames).includes(formData.value.name)) {
|
||||
formData.value.name = defaultNames[type as keyof typeof defaultNames]
|
||||
}
|
||||
|
||||
// 设置默认baseUrl
|
||||
const defaultUrls = {
|
||||
openai: 'https://api.openai.com/v1',
|
||||
claude: 'https://api.anthropic.com',
|
||||
google: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
ollama: 'http://localhost:11434',
|
||||
custom: ''
|
||||
}
|
||||
|
||||
if (needsBaseUrl.value) {
|
||||
formData.value.baseUrl = defaultUrls[type as keyof typeof defaultUrls]
|
||||
}
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
const handleTest = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
testing.value = true
|
||||
|
||||
// 模拟测试连接
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
message.success('连接测试成功!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(`连接测试失败: ${error.message}`)
|
||||
} else {
|
||||
message.error('请检查表单填写')
|
||||
}
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
const config = {
|
||||
organization: formData.value.organization,
|
||||
timeout: formData.value.timeout,
|
||||
maxRetries: formData.value.maxRetries,
|
||||
proxy: formData.value.proxy,
|
||||
headers: formData.value.headers.filter(h => h.key && h.value)
|
||||
}
|
||||
|
||||
emit('save', {
|
||||
type: formData.value.type,
|
||||
name: formData.value.name,
|
||||
apiKey: formData.value.apiKey,
|
||||
baseUrl: formData.value.baseUrl,
|
||||
config
|
||||
})
|
||||
} catch (error) {
|
||||
message.error('请检查表单填写')
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.advanced-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user