2025-10-05 18:12:51 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="config-panel">
|
|
|
|
|
|
<!-- 厂商选择 -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="platform">选择厂商</label>
|
|
|
|
|
|
<select id="platform" v-model="selectedPlatform" @change="onPlatformChange">
|
|
|
|
|
|
<option value="">请选择厂商</option>
|
|
|
|
|
|
<option v-for="platform in platforms" :key="platform.id" :value="platform.id">
|
|
|
|
|
|
{{ platform.name }}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 密钥输入 -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="api-key">API 密钥</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="api-key"
|
|
|
|
|
|
v-model="apiKey"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
:placeholder="currentPlatform?.keyPlaceholder || '请输入API密钥'"
|
|
|
|
|
|
:disabled="!selectedPlatform"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 接入点输入 -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="endpoint">接入点</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="endpoint"
|
|
|
|
|
|
v-model="endpoint"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
:placeholder="currentPlatform?.endpoint || '请输入接入点'"
|
|
|
|
|
|
:disabled="!selectedPlatform"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 保存按钮 -->
|
2025-10-06 00:54:47 +08:00
|
|
|
|
<!-- TODO 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商 -->
|
2025-10-05 18:12:51 +08:00
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
|
|
|
|
|
|
保存配置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 验证状态 -->
|
|
|
|
|
|
<div v-if="validationMessage" class="validation-message" :class="validationClass">
|
|
|
|
|
|
{{ validationMessage }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 已配置的厂商列表 -->
|
|
|
|
|
|
<div v-if="configuredPlatforms.length > 0" class="configured-list">
|
|
|
|
|
|
<h3>已配置的厂商</h3>
|
|
|
|
|
|
<div class="platform-item" v-for="platform in configuredPlatforms" :key="platform.id">
|
|
|
|
|
|
<span>{{ platform.name }}</span>
|
|
|
|
|
|
<button @click="removeConfig(platform.id)" class="remove-btn">删除</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
// 厂商配置预设
|
|
|
|
|
|
const platforms = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'openai',
|
|
|
|
|
|
name: 'OpenAI',
|
|
|
|
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
|
|
|
|
keyPlaceholder: 'sk-...',
|
|
|
|
|
|
keyType: 'api_key'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-10-06 00:54:47 +08:00
|
|
|
|
id: 'zhipu',
|
|
|
|
|
|
name: '智谱',
|
|
|
|
|
|
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
|
2025-10-05 18:12:51 +08:00
|
|
|
|
keyPlaceholder: 'sk-...',
|
|
|
|
|
|
keyType: 'api_key'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-10-06 00:54:47 +08:00
|
|
|
|
id: 'deepseek',
|
|
|
|
|
|
name: 'DeepSeek',
|
|
|
|
|
|
endpoint: 'https://api.deepseek.com/chat/completions',
|
|
|
|
|
|
keyPlaceholder: 'sk-...',
|
2025-10-05 18:12:51 +08:00
|
|
|
|
keyType: 'api_key'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'qwen',
|
2025-10-06 00:54:47 +08:00
|
|
|
|
name: '通义千问',
|
|
|
|
|
|
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
2025-10-05 18:12:51 +08:00
|
|
|
|
keyPlaceholder: 'sk-...',
|
|
|
|
|
|
keyType: 'api_key'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-10-06 00:54:47 +08:00
|
|
|
|
id: 'siliconflow',
|
|
|
|
|
|
name: '硅基流动',
|
|
|
|
|
|
endpoint: 'https://api.siliconflow.cn/v1/chat/completions',
|
|
|
|
|
|
keyPlaceholder: 'sk-...',
|
2025-10-05 18:12:51 +08:00
|
|
|
|
keyType: 'api_key'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
|
const selectedPlatform = ref('')
|
|
|
|
|
|
const apiKey = ref('')
|
|
|
|
|
|
const endpoint = ref('')
|
|
|
|
|
|
const validationMessage = ref('')
|
|
|
|
|
|
const validationClass = ref('')
|
|
|
|
|
|
const configuredPlatforms = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
|
const currentPlatform = computed(() => {
|
|
|
|
|
|
if (!selectedPlatform.value) return null
|
|
|
|
|
|
return platforms.find(p => p.id === selectedPlatform.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canSave = computed(() => {
|
|
|
|
|
|
return selectedPlatform.value && apiKey.value && endpoint.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 从localStorage加载已配置的厂商
|
|
|
|
|
|
const loadConfiguredPlatforms = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stored = localStorage.getItem('ai-api-configs')
|
|
|
|
|
|
if (stored) {
|
|
|
|
|
|
const configs = JSON.parse(stored)
|
|
|
|
|
|
configuredPlatforms.value = configs.map(config => {
|
|
|
|
|
|
const platform = platforms.find(p => p.id === config.id)
|
|
|
|
|
|
return {
|
|
|
|
|
|
...platform,
|
|
|
|
|
|
...config
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载配置失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存配置
|
|
|
|
|
|
const saveConfig = () => {
|
|
|
|
|
|
if (!canSave.value) {
|
|
|
|
|
|
showValidation('请填写所有必填字段', 'error')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证密钥格式
|
|
|
|
|
|
if (!validateApiKey()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造配置对象
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
id: selectedPlatform.value,
|
2025-10-06 13:50:08 +08:00
|
|
|
|
apiKey: apiKey.value,
|
2025-10-05 18:12:51 +08:00
|
|
|
|
endpoint: endpoint.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到localStorage
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stored = localStorage.getItem('ai-api-configs')
|
|
|
|
|
|
let configs = stored ? JSON.parse(stored) : []
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在该厂商配置
|
|
|
|
|
|
const existingIndex = configs.findIndex(c => c.id === selectedPlatform.value)
|
|
|
|
|
|
if (existingIndex !== -1) {
|
|
|
|
|
|
configs[existingIndex] = config
|
|
|
|
|
|
} else {
|
|
|
|
|
|
configs.push(config)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('ai-api-configs', JSON.stringify(configs))
|
|
|
|
|
|
|
|
|
|
|
|
// 更新已配置列表
|
|
|
|
|
|
loadConfiguredPlatforms()
|
|
|
|
|
|
|
|
|
|
|
|
// 清空表单
|
|
|
|
|
|
resetForm()
|
|
|
|
|
|
|
|
|
|
|
|
showValidation('配置保存成功', 'success')
|
2025-10-06 01:13:37 +08:00
|
|
|
|
|
|
|
|
|
|
// TODO: 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商
|
|
|
|
|
|
// 触发全局事件,通知 ChatPanel 刷新配置
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('config-saved', { detail: { platformId: selectedPlatform.value } }))
|
|
|
|
|
|
}
|
2025-10-05 18:12:51 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存配置失败:', error)
|
|
|
|
|
|
showValidation('保存配置失败,请重试', 'error')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证密钥格式
|
|
|
|
|
|
const validateApiKey = () => {
|
|
|
|
|
|
const key = apiKey.value.trim()
|
|
|
|
|
|
const platform = currentPlatform.value
|
|
|
|
|
|
|
|
|
|
|
|
if (!platform) return true
|
|
|
|
|
|
|
|
|
|
|
|
// 根据厂商类型验证密钥格式
|
2025-10-06 00:54:47 +08:00
|
|
|
|
if (platform.keyType === 'api_key') {
|
|
|
|
|
|
if (!key.startsWith('sk-') || key.length < 20) {
|
|
|
|
|
|
showValidation('API 密钥格式不正确,应以 "sk-" 开头且长度不少于20位', 'error')
|
2025-10-05 18:12:51 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示验证消息
|
|
|
|
|
|
const showValidation = (message, type) => {
|
|
|
|
|
|
validationMessage.value = message
|
|
|
|
|
|
validationClass.value = type
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
validationMessage.value = ''
|
|
|
|
|
|
}, 3000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置表单
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
selectedPlatform.value = ''
|
|
|
|
|
|
apiKey.value = ''
|
|
|
|
|
|
endpoint.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 平台变更
|
|
|
|
|
|
const onPlatformChange = () => {
|
|
|
|
|
|
if (selectedPlatform.value) {
|
|
|
|
|
|
const platform = platforms.find(p => p.id === selectedPlatform.value)
|
|
|
|
|
|
if (platform) {
|
|
|
|
|
|
endpoint.value = platform.endpoint
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
endpoint.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除配置
|
|
|
|
|
|
const removeConfig = (platformId) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stored = localStorage.getItem('ai-api-configs')
|
|
|
|
|
|
if (stored) {
|
|
|
|
|
|
const configs = JSON.parse(stored)
|
|
|
|
|
|
const filtered = configs.filter(c => c.id !== platformId)
|
|
|
|
|
|
localStorage.setItem('ai-api-configs', JSON.stringify(filtered))
|
|
|
|
|
|
loadConfiguredPlatforms()
|
|
|
|
|
|
showValidation('配置已删除', 'success')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除配置失败:', error)
|
|
|
|
|
|
showValidation('删除配置失败', 'error')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadConfiguredPlatforms()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.config-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group label {
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input,
|
|
|
|
|
|
.form-group select {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input:disabled,
|
|
|
|
|
|
.form-group select:disabled {
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn:hover:not(:disabled) {
|
|
|
|
|
|
background-color: #0056b3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn:disabled {
|
|
|
|
|
|
background-color: #ccc;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.validation-message {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.validation-message.success {
|
|
|
|
|
|
background-color: #d4edda;
|
|
|
|
|
|
color: #155724;
|
|
|
|
|
|
border: 1px solid #c3e6cb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.validation-message.error {
|
|
|
|
|
|
background-color: #f8d7da;
|
|
|
|
|
|
color: #721c24;
|
|
|
|
|
|
border: 1px solid #f5c6cb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.configured-list {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.configured-list h3 {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.platform-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.remove-btn {
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background-color: #dc3545;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.remove-btn:hover {
|
|
|
|
|
|
background-color: #c82333;
|
|
|
|
|
|
}
|
2025-10-06 01:13:37 +08:00
|
|
|
|
</style>
|