first commit

This commit is contained in:
douboer
2025-10-14 14:18:20 +08:00
commit d93bc02772
66 changed files with 21393 additions and 0 deletions

View 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>