- 添加 Knowledge 知识库页面 - 添加 Settings 设置页面 - 添加 Model 设置需求文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1091 lines
31 KiB
Vue
1091 lines
31 KiB
Vue
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
// 当前选中的设置菜单
|
||
const activeMenu = ref('general')
|
||
|
||
// Model 列表
|
||
const models = ref([
|
||
{ id: 1, name: 'OpenAI', endpoint: 'api.openai.com', status: 'active', modelType: 'chat' },
|
||
{ id: 2, name: 'Anthropic', endpoint: 'api.anthropic.com', status: 'active', modelType: 'chat' },
|
||
{ id: 3, name: 'text-embedding-3-small', endpoint: 'api.openai.com', status: 'active', modelType: 'embedding' },
|
||
])
|
||
|
||
// Model Type 选项
|
||
const modelTypeOptions = [
|
||
{ value: 'chat', label: 'Chat' },
|
||
{ value: 'embedding', label: 'Embedding' },
|
||
{ value: 'rerank', label: 'Rerank' },
|
||
{ value: 'vlm', label: 'VLM' },
|
||
]
|
||
|
||
// 是否显示新增模型表单
|
||
const showAddModelForm = ref(false)
|
||
|
||
// 新增模型表单
|
||
const newModelForm = ref({
|
||
name: '',
|
||
apiKey: '',
|
||
apiEndpoint: '',
|
||
baseUrl: '',
|
||
provider: '',
|
||
model: '',
|
||
modelType: '',
|
||
})
|
||
|
||
// 测试连接状态
|
||
const testingConnection = ref(false)
|
||
const connectionStatus = ref<'idle' | 'success' | 'error'>('idle')
|
||
|
||
// 测试连接
|
||
const testConnection = async () => {
|
||
if (!newModelForm.value.apiKey || !newModelForm.value.baseUrl) {
|
||
ElMessage.warning('Please enter API Key and Base URL')
|
||
return
|
||
}
|
||
|
||
testingConnection.value = true
|
||
connectionStatus.value = 'idle'
|
||
|
||
try {
|
||
// 模拟API测试(实际项目中应该调用后端接口)
|
||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||
// 假设连接成功
|
||
connectionStatus.value = 'success'
|
||
ElMessage.success('Connection successful!')
|
||
} catch (error) {
|
||
connectionStatus.value = 'error'
|
||
ElMessage.error('Connection failed')
|
||
} finally {
|
||
testingConnection.value = false
|
||
}
|
||
}
|
||
|
||
// Provider 选项
|
||
const providerOptions = [
|
||
{ value: 'OpenAI', label: 'OpenAI' },
|
||
{ value: 'Ollama', label: 'Ollama' },
|
||
]
|
||
|
||
// 添加新模型
|
||
const addModel = () => {
|
||
if (newModelForm.value.name && newModelForm.value.apiEndpoint) {
|
||
models.value.push({
|
||
id: Date.now(),
|
||
name: newModelForm.value.name,
|
||
endpoint: newModelForm.value.apiEndpoint,
|
||
status: 'active',
|
||
modelType: newModelForm.value.modelType || 'chat',
|
||
})
|
||
newModelForm.value = { name: '', apiKey: '', apiEndpoint: '', baseUrl: '', provider: '', model: '', modelType: '' }
|
||
connectionStatus.value = 'idle'
|
||
showAddModelForm.value = false
|
||
ElMessage.success('Model added successfully')
|
||
}
|
||
}
|
||
|
||
// 取消添加
|
||
const cancelAddModel = () => {
|
||
newModelForm.value = { name: '', apiKey: '', apiEndpoint: '', baseUrl: '', provider: '', model: '', modelType: '' }
|
||
connectionStatus.value = 'idle'
|
||
showAddModelForm.value = false
|
||
}
|
||
|
||
// 设置菜单列表
|
||
const menuItems = [
|
||
{ key: 'general', label: 'General', icon: 'fa-gear' },
|
||
{ key: 'members', label: 'Members', icon: 'fa-users' },
|
||
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
|
||
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
|
||
{ key: 'parsing', label: 'Parsing', icon: 'fa-code' },
|
||
{ key: 'storage', label: 'Storage', icon: 'fa-database' },
|
||
]
|
||
|
||
// General 设置表单
|
||
const generalForm = ref({
|
||
name: 'Alex Smith',
|
||
email: 'alex@gmail.com',
|
||
password: '********',
|
||
language: 'English',
|
||
timezone: 'UTC +08:00 Beijing',
|
||
})
|
||
|
||
// 语言选项
|
||
const languageOptions = [
|
||
{ value: 'English', label: 'English' },
|
||
{ value: 'Chinese', label: '中文' },
|
||
{ value: 'Japanese', label: '日本語' },
|
||
]
|
||
|
||
// 时区选项
|
||
const timezoneOptions = [
|
||
{ value: 'UTC +08:00 Beijing', label: 'UTC +08:00 Beijing' },
|
||
{ value: 'UTC +00:00 London', label: 'UTC +00:00 London' },
|
||
{ value: 'UTC -05:00 New York', label: 'UTC -05:00 New York' },
|
||
{ value: 'UTC -08:00 Los Angeles', label: 'UTC -08:00 Los Angeles' },
|
||
]
|
||
|
||
// 保存设置
|
||
const saveChanges = () => {
|
||
ElMessage.success('Settings saved successfully')
|
||
}
|
||
|
||
// 显示密码修改弹窗
|
||
const showChangePassword = () => {
|
||
ElMessage.info('Password change dialog would open here')
|
||
}
|
||
|
||
// Parsing 配置表单
|
||
const parsingForm = ref({
|
||
// LLM Provider
|
||
provider: 'OpenAI',
|
||
model: 'gpt-4o',
|
||
apiKey: '',
|
||
apiEndpoint: '',
|
||
// 通用配置
|
||
maxWorkers: 5,
|
||
maxRetries: 3,
|
||
requestTimeout: 60,
|
||
enableProxy: false,
|
||
httpProxyUrl: '',
|
||
// 文本解析
|
||
enableMultimodal: true,
|
||
visionModel: 'gpt-4o',
|
||
imageUnderstandingPrice: 0.00125,
|
||
enableJsonMode: false,
|
||
jsonModeModel: 'gpt-4o',
|
||
// 文件解析
|
||
enableFileParsing: true,
|
||
enableTableRecognition: true,
|
||
enableFormulaRecognition: true,
|
||
ocrLanguage: 'en',
|
||
})
|
||
|
||
// Vision Model 选项
|
||
const visionModelOptions = [
|
||
{ value: 'gpt-4o', label: 'GPT-4o' },
|
||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
|
||
{ value: 'claude-3-5-sonnet', label: 'Claude 3.5 Sonnet' },
|
||
{ value: 'claude-3-haiku', label: 'Claude 3 Haiku' },
|
||
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
|
||
]
|
||
|
||
// OCR 语言选项
|
||
const ocrLanguageOptions = [
|
||
{ value: 'en', label: 'English' },
|
||
{ value: 'zh', label: 'Chinese' },
|
||
{ value: 'ja', label: 'Japanese' },
|
||
{ value: 'ko', label: 'Korean' },
|
||
{ value: 'auto', label: 'Auto Detect' },
|
||
]
|
||
|
||
// 保存 Parsing 设置
|
||
const saveParsingSettings = () => {
|
||
ElMessage.success('Parsing settings saved successfully')
|
||
}
|
||
|
||
// Storage 配置表单
|
||
const storageForm = ref({
|
||
storageType: 'local',
|
||
// Local 存储
|
||
localPath: './storage',
|
||
// MinIO 存储
|
||
minioEndpoint: 'localhost:9000',
|
||
minioAccessKey: '',
|
||
minioSecretKey: '',
|
||
minioBucket: 'x-agents',
|
||
minioUseSSL: false,
|
||
})
|
||
|
||
// 保存 Storage 设置
|
||
const saveStorageSettings = () => {
|
||
ElMessage.success('Storage settings saved successfully')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="settings-page">
|
||
<!-- 页面标题 -->
|
||
<div class="page-header">
|
||
<h1 class="page-title">Settings</h1>
|
||
</div>
|
||
|
||
<div class="settings-container">
|
||
<!-- 左侧菜单 -->
|
||
<nav class="settings-nav">
|
||
<ul>
|
||
<li
|
||
v-for="item in menuItems"
|
||
:key="item.key"
|
||
:class="['nav-item', { active: activeMenu === item.key }]"
|
||
@click="activeMenu = item.key"
|
||
>
|
||
<i :class="['fa-solid', item.icon]"></i>
|
||
<span>{{ item.label }}</span>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
|
||
<!-- 右侧内容 -->
|
||
<div class="settings-content">
|
||
<!-- General 设置 -->
|
||
<div v-if="activeMenu === 'general'" class="settings-section">
|
||
<h2 class="section-title">General Settings</h2>
|
||
<p class="section-desc">Manage your personal information and preferences</p>
|
||
|
||
<el-form :model="generalForm" label-position="top" class="settings-form">
|
||
<el-form-item label="Name">
|
||
<el-input v-model="generalForm.name" placeholder="Enter your name" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Email">
|
||
<el-input v-model="generalForm.email" placeholder="Enter your email" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Password">
|
||
<div class="password-field">
|
||
<el-input v-model="generalForm.password" type="password" disabled />
|
||
<el-button @click="showChangePassword">Change</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Language">
|
||
<el-select v-model="generalForm.language" placeholder="Select language">
|
||
<el-option
|
||
v-for="lang in languageOptions"
|
||
:key="lang.value"
|
||
:label="lang.label"
|
||
:value="lang.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Timezone">
|
||
<el-select v-model="generalForm.timezone" placeholder="Select timezone">
|
||
<el-option
|
||
v-for="tz in timezoneOptions"
|
||
:key="tz.value"
|
||
:label="tz.label"
|
||
:value="tz.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="saveChanges">Save Changes</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
|
||
<!-- Parsing 设置 -->
|
||
<div v-if="activeMenu === 'parsing'" class="settings-section">
|
||
<h2 class="section-title">Parsing</h2>
|
||
<p class="section-desc">Configure parsing settings</p>
|
||
|
||
<!-- LLM Provider -->
|
||
<div class="config-card">
|
||
<h3 class="config-title">LLM Provider</h3>
|
||
<el-form label-position="top" class="settings-form">
|
||
<div class="form-row">
|
||
<el-form-item label="Provider" class="flex-1">
|
||
<el-select v-model="parsingForm.provider" placeholder="Select provider">
|
||
<el-option v-for="p in providerOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Model" class="flex-1">
|
||
<el-select v-model="parsingForm.model" placeholder="Select model">
|
||
<el-option v-for="m in modelOptionsByProvider[parsingForm.provider]" :key="m.value" :label="m.label" :value="m.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="API Key">
|
||
<el-input v-model="parsingForm.apiKey" type="password" placeholder="Enter API key" show-password />
|
||
</el-form-item>
|
||
<el-form-item label="API Endpoint">
|
||
<el-input v-model="parsingForm.apiEndpoint" placeholder="Enter API endpoint" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- Parsing Configuration -->
|
||
<div class="config-card">
|
||
<h3 class="config-title">Parsing Configuration</h3>
|
||
|
||
<!-- 通用配置 -->
|
||
<div class="config-section">
|
||
<h4 class="config-subtitle">General</h4>
|
||
<el-form label-position="top" class="settings-form">
|
||
<div class="form-row">
|
||
<el-form-item label="Max Workers" class="flex-1">
|
||
<el-input-number v-model="parsingForm.maxWorkers" :min="1" :max="100" controls-position="right" />
|
||
</el-form-item>
|
||
<el-form-item label="Max Retries" class="flex-1">
|
||
<el-input-number v-model="parsingForm.maxRetries" :min="0" :max="10" controls-position="right" />
|
||
</el-form-item>
|
||
<el-form-item label="Request Timeout (seconds)" class="flex-1">
|
||
<el-input-number v-model="parsingForm.requestTimeout" :min="10" :max="600" controls-position="right" />
|
||
</el-form-item>
|
||
</div>
|
||
<div class="form-row">
|
||
<el-form-item label="Enable HTTP Proxy" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableProxy" class="parsing-switch" />
|
||
</el-form-item>
|
||
<el-form-item v-if="parsingForm.enableProxy" label="HTTP Proxy URL" class="flex-1">
|
||
<el-input v-model="parsingForm.httpProxyUrl" placeholder="http://proxy:8080" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 文本解析 -->
|
||
<div class="config-section">
|
||
<h4 class="config-subtitle">Text Parsing</h4>
|
||
<el-form label-position="top" class="settings-form">
|
||
<div class="form-row">
|
||
<el-form-item label="Enable Multimodal" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableMultimodal" class="parsing-switch" />
|
||
</el-form-item>
|
||
<el-form-item v-if="parsingForm.enableMultimodal" label="Vision Model" class="flex-1">
|
||
<el-select v-model="parsingForm.visionModel" placeholder="Select model">
|
||
<el-option v-for="m in visionModelOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="parsingForm.enableMultimodal" label="Image Understanding Price" class="flex-1">
|
||
<el-input v-model="parsingForm.imageUnderstandingPrice" placeholder="0.00125">
|
||
<template #append>$/1k tokens</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="form-row">
|
||
<el-form-item label="Enable JSON Mode" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableJsonMode" class="parsing-switch" />
|
||
</el-form-item>
|
||
<el-form-item v-if="parsingForm.enableJsonMode" label="JSON Mode Model" class="flex-1">
|
||
<el-select v-model="parsingForm.jsonModeModel" placeholder="Select model">
|
||
<el-option v-for="m in visionModelOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 文件解析 -->
|
||
<div class="config-section">
|
||
<h4 class="config-subtitle">File Parsing</h4>
|
||
<el-form label-position="top" class="settings-form">
|
||
<div class="form-row">
|
||
<el-form-item label="Enable File Parsing" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableFileParsing" class="parsing-switch" />
|
||
</el-form-item>
|
||
<el-form-item label="Enable Table Recognition" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableTableRecognition" class="parsing-switch" />
|
||
</el-form-item>
|
||
<el-form-item label="Enable Formula Recognition" class="flex-1">
|
||
<el-switch v-model="parsingForm.enableFormulaRecognition" class="parsing-switch" />
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="OCR Language">
|
||
<el-select v-model="parsingForm.ocrLanguage" placeholder="Select language">
|
||
<el-option v-for="lang in ocrLanguageOptions" :key="lang.value" :label="lang.label" :value="lang.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存按钮 -->
|
||
<div class="form-actions mt-6">
|
||
<el-button type="primary" @click="saveParsingSettings">Save</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Storage 设置 -->
|
||
<div v-if="activeMenu === 'storage'" class="settings-section">
|
||
<h2 class="section-title">Storage</h2>
|
||
<p class="section-desc">Configure storage settings</p>
|
||
|
||
<!-- Storage Type -->
|
||
<div class="config-card">
|
||
<h3 class="config-title">Storage Type</h3>
|
||
<el-form label-position="top" class="settings-form">
|
||
<el-form-item label="Storage Type">
|
||
<el-select v-model="storageForm.storageType" placeholder="Select storage type">
|
||
<el-option label="Local" value="local" />
|
||
<el-option label="MinIO" value="minio" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- Local Storage -->
|
||
<div v-if="storageForm.storageType === 'local'" class="config-card">
|
||
<h3 class="config-title">Local Storage</h3>
|
||
<el-form label-position="top" class="settings-form">
|
||
<el-form-item label="Storage Path">
|
||
<el-input v-model="storageForm.localPath" placeholder="Enter local storage path" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- MinIO Storage -->
|
||
<div v-if="storageForm.storageType === 'minio'" class="config-card">
|
||
<h3 class="config-title">MinIO Storage</h3>
|
||
<el-form label-position="top" class="settings-form">
|
||
<el-form-item label="Endpoint">
|
||
<el-input v-model="storageForm.minioEndpoint" placeholder="e.g., localhost:9000" />
|
||
</el-form-item>
|
||
<div class="form-row">
|
||
<el-form-item label="Access Key" class="flex-1">
|
||
<el-input v-model="storageForm.minioAccessKey" placeholder="Enter access key" />
|
||
</el-form-item>
|
||
<el-form-item label="Secret Key" class="flex-1">
|
||
<el-input v-model="storageForm.minioSecretKey" type="password" placeholder="Enter secret key" show-password />
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="Bucket">
|
||
<el-input v-model="storageForm.minioBucket" placeholder="Enter bucket name" />
|
||
</el-form-item>
|
||
<el-form-item label="Use SSL">
|
||
<el-switch v-model="storageForm.minioUseSSL" class="parsing-switch" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 保存按钮 -->
|
||
<div class="form-actions mt-6">
|
||
<el-button type="primary" @click="saveStorageSettings">Save</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Members 设置 -->
|
||
<div v-if="activeMenu === 'members'" class="settings-section">
|
||
<h2 class="section-title">Members</h2>
|
||
<p class="section-desc">Manage team members</p>
|
||
</div>
|
||
|
||
<!-- Notifications 设置 -->
|
||
<div v-if="activeMenu === 'notifications'" class="settings-section">
|
||
<h2 class="section-title">Notifications</h2>
|
||
<p class="section-desc">Configure notification preferences</p>
|
||
</div>
|
||
|
||
<!-- Model Settings 设置 -->
|
||
<div v-if="activeMenu === 'modelSettings'" class="settings-section">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div>
|
||
<h2 class="section-title">Model Settings</h2>
|
||
<p class="section-desc">Configure AI model settings</p>
|
||
</div>
|
||
<el-button type="primary" class="add-model-btn" @click="showAddModelForm = true">
|
||
<i class="fa-solid fa-plus mr-2"></i>
|
||
Add New Model
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 模型列表 -->
|
||
<table class="w-full">
|
||
<thead class="bg-dark-600">
|
||
<tr>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Name</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Model Type</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">API Endpoint</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="model in models" :key="model.id" class="table-row">
|
||
<td class="px-5 py-4">
|
||
<span class="font-medium">{{ model.name }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-center">
|
||
<span class="bg-primary-orange/20 text-primary-orange px-2 py-1 rounded text-sm capitalize">{{ model.modelType }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-center text-gray-400 text-sm">
|
||
{{ model.endpoint }}
|
||
</td>
|
||
<td class="px-5 py-4 text-center">
|
||
<span class="bg-primary-success/20 text-primary-success px-2 py-1 rounded text-sm capitalize">{{ model.status }}</span>
|
||
</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<button class="btn-icon" title="Edit">
|
||
<i class="fa-solid fa-pen text-gray-400"></i>
|
||
</button>
|
||
<button class="btn-icon" title="Delete">
|
||
<i class="fa-solid fa-trash text-gray-400"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 新增模型弹窗 -->
|
||
<el-dialog
|
||
v-model="showAddModelForm"
|
||
title="Add New Model"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
class="add-model-dialog"
|
||
>
|
||
<p class="dialog-desc">Configure your model settings</p>
|
||
|
||
<el-form label-position="top" class="settings-form">
|
||
<el-form-item label="Model Name">
|
||
<el-input v-model="newModelForm.name" placeholder="e.g., My GPT-4 Model" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Model Type">
|
||
<el-select v-model="newModelForm.modelType" placeholder="Select model type">
|
||
<el-option
|
||
v-for="type in modelTypeOptions"
|
||
:key="type.value"
|
||
:label="type.label"
|
||
:value="type.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Provider">
|
||
<el-select v-model="newModelForm.provider" placeholder="Select provider">
|
||
<el-option
|
||
v-for="provider in providerOptions"
|
||
:key="provider.value"
|
||
:label="provider.label"
|
||
:value="provider.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Model">
|
||
<el-input v-model="newModelForm.model" placeholder="e.g., gpt-4o (OpenAI) or llama3 (Ollama)" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="API Key">
|
||
<el-input v-model="newModelForm.apiKey" type="password" placeholder="sk-xxxxx (required for OpenAI)" show-password />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="Base URL">
|
||
<el-input v-model="newModelForm.baseUrl" placeholder="https://api.openai.com/v1 (OpenAI) or http://localhost:11434 (Ollama)" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="API Endpoint">
|
||
<el-input v-model="newModelForm.apiEndpoint" placeholder="e.g., /chat/completions" />
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button text @click="cancelAddModel">Cancel</el-button>
|
||
<el-button
|
||
:loading="testingConnection"
|
||
@click="testConnection"
|
||
class="test-btn"
|
||
>
|
||
<i v-if="!testingConnection" class="fa-solid fa-plug"></i>
|
||
Test Connection
|
||
</el-button>
|
||
<el-button type="primary" @click="addModel">Confirm</el-button>
|
||
</div>
|
||
<div v-if="connectionStatus === 'success'" class="connection-status success">
|
||
<i class="fa-solid fa-check-circle"></i> Connection successful
|
||
</div>
|
||
<div v-else-if="connectionStatus === 'error'" class="connection-status error">
|
||
<i class="fa-solid fa-xmark-circle"></i> Connection failed
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.settings-page {
|
||
padding: 24px;
|
||
min-height: 100vh;
|
||
background-color: #0a0a0f;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: white;
|
||
}
|
||
|
||
.settings-container {
|
||
display: flex;
|
||
gap: 24px;
|
||
background-color: #121218;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
min-height: calc(100vh - 120px);
|
||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* 左侧导航 */
|
||
.settings-nav {
|
||
width: 280px;
|
||
background-color: #0d0d12;
|
||
padding: 16px 0;
|
||
flex-shrink: 0;
|
||
border-right: 1px solid #1e1e28;
|
||
}
|
||
|
||
.settings-nav ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 20px;
|
||
color: #9ca3af;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background-color: #1a1a24;
|
||
color: white;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background-color: #1a1a24;
|
||
color: #f97316;
|
||
border-left: 3px solid #f97316;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.nav-item i {
|
||
width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 子菜单 */
|
||
.sub-menu {
|
||
list-style: none;
|
||
padding-left: 20px;
|
||
margin: 0;
|
||
}
|
||
|
||
.sub-menu .nav-item {
|
||
padding: 10px 16px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.nav-item.sub-item {
|
||
padding-left: 40px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 右侧内容 */
|
||
.settings-content {
|
||
flex: 1;
|
||
padding: 32px;
|
||
overflow-y: auto;
|
||
background-color: #121218;
|
||
}
|
||
|
||
.settings-section {
|
||
width: 100%;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: white;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.section-desc {
|
||
font-size: 14px;
|
||
color: #9ca3af;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
/* 模型列表 */
|
||
table {
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.table-row {
|
||
border-bottom: 1px solid #252530;
|
||
}
|
||
|
||
.table-row:hover {
|
||
background-color: #1a1a24;
|
||
transition: background-color 0.25s ease;
|
||
}
|
||
|
||
.btn-icon {
|
||
padding: 6px;
|
||
border-radius: 6px;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.btn-icon:hover {
|
||
background-color: #1e1e28;
|
||
}
|
||
|
||
.btn-icon:hover i {
|
||
color: #f97316 !important;
|
||
}
|
||
|
||
/* 添加模型按钮 */
|
||
.add-model-btn {
|
||
background-color: #f97316;
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.add-model-btn:hover {
|
||
background-color: #ea580c;
|
||
border-color: #ea580c;
|
||
}
|
||
|
||
/* 添加模型表单 */
|
||
.add-model-form {
|
||
width: 100%;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-start;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
/* 表单样式 */
|
||
.settings-form :deep(.el-form-item__label) {
|
||
color: #d1d5db;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.settings-form :deep(.el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.settings-form :deep(.el-input__inner) {
|
||
color: white;
|
||
}
|
||
|
||
.settings-form :deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
.settings-form :deep(.el-select .el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
}
|
||
|
||
.password-field {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.password-field .el-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.settings-form :deep(.el-button--primary) {
|
||
background-color: #f97316;
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.settings-form :deep(.el-button--primary:hover) {
|
||
background-color: #ea580c;
|
||
border-color: #ea580c;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.add-model-dialog :deep(.el-dialog) {
|
||
background-color: #16161e;
|
||
border-radius: 12px;
|
||
border: 1px solid #252530;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||
animation: dialogFadeIn 0.25s ease;
|
||
}
|
||
|
||
@keyframes dialogFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__header) {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #252530;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__title) {
|
||
color: white;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__headerbtn) {
|
||
top: 20px;
|
||
right: 20px;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__headerbtn:hover .el-dialog__close) {
|
||
color: #f97316;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__body) {
|
||
padding: 24px;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-dialog__footer) {
|
||
padding: 16px 24px;
|
||
border-top: 1px solid #252530;
|
||
}
|
||
|
||
.dialog-desc {
|
||
font-size: 14px;
|
||
color: #9ca3af;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.test-btn {
|
||
background-color: #1e1e28;
|
||
border: 1px solid #3a3a4a;
|
||
color: #d1d5db;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.test-btn:hover {
|
||
background-color: #2a2a3a;
|
||
border-color: #4a4a5a;
|
||
color: white;
|
||
}
|
||
|
||
.connection-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.connection-status.success {
|
||
color: #10b981;
|
||
}
|
||
|
||
.connection-status.error {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-form-item__label) {
|
||
color: #d1d5db;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-input__inner) {
|
||
color: white;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select .el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select .el-input__inner) {
|
||
color: white;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select-dropdown) {
|
||
background-color: #1a1a24;
|
||
border: 1px solid #2a2a3a;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select-dropdown__item) {
|
||
color: #d1d5db;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select-dropdown__item.hover),
|
||
.add-model-dialog :deep(.el-select-dropdown__item:hover) {
|
||
background-color: #4b5563;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-select-dropdown__item.selected) {
|
||
color: #f97316;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-button--primary) {
|
||
background-color: #f97316;
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.add-model-dialog :deep(.el-button--primary:hover) {
|
||
background-color: #ea580c;
|
||
border-color: #ea580c;
|
||
}
|
||
|
||
/* Parsing 配置卡片 */
|
||
.config-card {
|
||
background-color: #0d0d12;
|
||
border: 1px solid #252530;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.config-card:hover {
|
||
border-color: #353545;
|
||
}
|
||
|
||
.config-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: white;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #252530;
|
||
}
|
||
|
||
.config-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.config-section:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.config-subtitle {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 表单行 */
|
||
.form-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.form-row .flex-1 {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
}
|
||
|
||
/* Switch 样式 */
|
||
.parsing-switch :deep(.el-switch.is-checked .el-switch__core) {
|
||
background-color: #f97316;
|
||
border-color: #f97316;
|
||
}
|
||
|
||
/* Input Number 样式 */
|
||
.settings-form :deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
|
||
.settings-form :deep(.el-input-number .el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
}
|
||
|
||
/* Select 样式 */
|
||
.settings-form :deep(.el-select .el-input__wrapper) {
|
||
background-color: #171922;
|
||
border: 1px solid #4b5563;
|
||
}
|
||
|
||
/* 连接状态 */
|
||
.connection-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.connection-status.success {
|
||
color: #10b981;
|
||
}
|
||
|
||
.connection-status.error {
|
||
color: #ef4444;
|
||
}
|
||
|
||
/* API Endpoint 字段 */
|
||
.api-endpoint-field {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.api-endpoint-field .el-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.test-btn {
|
||
background-color: #1e1e28;
|
||
border: 1px solid #3a3a4a;
|
||
color: #d1d5db;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.test-btn:hover {
|
||
background-color: #2a2a3a;
|
||
border-color: #4a4a5a;
|
||
color: white;
|
||
}
|
||
</style>
|