Files
api-box/frontend/src/components/ConfigPanel.vue
T
2025-10-06 13:50:08 +08:00

368 lines
8.4 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>
<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>
<!-- 保存按钮 -->
<!-- TODO 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商 -->
<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'
},
{
id: 'zhipu',
name: '智谱',
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'deepseek',
name: 'DeepSeek',
endpoint: 'https://api.deepseek.com/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'qwen',
name: '通义千问',
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'siliconflow',
name: '硅基流动',
endpoint: 'https://api.siliconflow.cn/v1/chat/completions',
keyPlaceholder: 'sk-...',
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,
apiKey: apiKey.value,
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')
// TODO: 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商
// 触发全局事件,通知 ChatPanel 刷新配置
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('config-saved', { detail: { platformId: selectedPlatform.value } }))
}
} catch (error) {
console.error('保存配置失败:', error)
showValidation('保存配置失败,请重试', 'error')
}
}
// 验证密钥格式
const validateApiKey = () => {
const key = apiKey.value.trim()
const platform = currentPlatform.value
if (!platform) return true
// 根据厂商类型验证密钥格式
if (platform.keyType === 'api_key') {
if (!key.startsWith('sk-') || key.length < 20) {
showValidation('API 密钥格式不正确,应以 "sk-" 开头且长度不少于20位', 'error')
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;
}
</style>