Files
map-client-vue/web/src/components/ProviderForm.vue
2025-10-14 14:18:20 +08:00

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