322 lines
7.9 KiB
Vue
322 lines
7.9 KiB
Vue
<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> |