feat: 完善模型管理功能
- 新增模型 API 路由,支持 CRUD 和测试连接 - 支持 MiniMax、GLM、OpenAI Compatible 三种供应商 - 添加连接状态持久化 (untested/connected/disconnected) - 修复 CORS 和数据库模型兼容性问题 - 前端 UI 优化:供应商默认 API 地址自动填充 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import type { Project, ProjectCreate, ProjectUpdate } from '@/types'
|
||||
import type { Project, ProjectCreate, ProjectUpdate, Model, ModelCreate } from '@/types'
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.PROD
|
||||
@@ -91,4 +91,14 @@ export const evalApi = {
|
||||
getResults: (projectId: string, taskId: string) => request.get(`/projects/${projectId}/eval-tasks/${taskId}`)
|
||||
}
|
||||
|
||||
export const modelApi = {
|
||||
list: () => request.get<Model[]>('/models/'),
|
||||
get: (id: string) => request.get<Model>(`/models/${id}`),
|
||||
create: (data: ModelCreate) => request.post<{ id: string }>('/models/', data),
|
||||
update: (id: string, data: Partial<Model>) => request.put<Model>(`/models/${id}`, data),
|
||||
delete: (id: string) => request.delete(`/models/${id}`),
|
||||
setDefault: (id: string) => request.post(`/models/${id}/set-default`),
|
||||
test: (id: string) => request.post<{ success: boolean; message: string }>(`/models/${id}/test`)
|
||||
}
|
||||
|
||||
export default request
|
||||
|
||||
@@ -51,11 +51,6 @@ const routes = [
|
||||
path: '/models',
|
||||
name: 'ModelSettings',
|
||||
component: () => import('@/views/ModelSettingsView.vue')
|
||||
},
|
||||
{
|
||||
path: '/data-square',
|
||||
name: 'DataSquare',
|
||||
component: () => import('@/views/DataSquareView.vue')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
15
frontend/src/types/model.d.ts
vendored
15
frontend/src/types/model.d.ts
vendored
@@ -2,6 +2,18 @@
|
||||
* Model Configuration Types
|
||||
*/
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
provider: ModelProvider
|
||||
model_name: string
|
||||
api_key?: string
|
||||
api_base?: string
|
||||
is_default: 'true' | 'false'
|
||||
connection_status?: 'untested' | 'connected' | 'disconnected'
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
id: string
|
||||
provider: ModelProvider
|
||||
@@ -9,11 +21,12 @@ export interface ModelConfig {
|
||||
api_key?: string
|
||||
api_base?: string
|
||||
is_default: 'true' | 'false'
|
||||
connection_status?: 'untested' | 'connected' | 'disconnected'
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type ModelProvider = 'openai' | 'anthropic' | 'google' | 'other'
|
||||
export type ModelProvider = 'minimax' | 'glm' | 'openai'
|
||||
|
||||
export interface ModelCreate {
|
||||
provider: ModelProvider
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<!-- Logo -->
|
||||
<div class="hero-logo">
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00d4ff"/>
|
||||
<stop offset="100%" style="stop-color:#7c3aed"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 外圈 - 数据集合 -->
|
||||
<rect x="4" y="4" width="48" height="48" rx="12" stroke="url(#logoGradient)" stroke-width="2.5" fill="none" opacity="0.3"/>
|
||||
<!-- Y 字母 - 数据流/分支 -->
|
||||
<path d="M18 42V22L28 12V18" stroke="url(#logoGradient)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M28 18L38 28" stroke="url(#logoGradient)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
||||
<!-- 数据节点 - 神经网络样式 -->
|
||||
<circle cx="18" cy="42" r="3" fill="#00d4ff"/>
|
||||
<circle cx="28" cy="12" r="3" fill="#7c3aed"/>
|
||||
<circle cx="38" cy="28" r="3" fill="#00d4ff"/>
|
||||
<circle cx="28" cy="18" r="2.5" fill="#00d4ff" opacity="0.7"/>
|
||||
<!-- 连接线 - 数据流向 -->
|
||||
<circle cx="28" cy="32" r="2" fill="#7c3aed" opacity="0.5"/>
|
||||
<circle cx="20" cy="32" r="1.5" fill="#00d4ff" opacity="0.4"/>
|
||||
<circle cx="36" cy="38" r="1.5" fill="#7c3aed" opacity="0.4"/>
|
||||
</svg>
|
||||
<span class="logo-text">YG<span class="logo-highlight">Datasets</span></span>
|
||||
</div>
|
||||
|
||||
<div class="hero-badge">
|
||||
<span class="badge-dot"></span>
|
||||
<span>AI 驱动数据生成</span>
|
||||
@@ -20,103 +47,85 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建项目
|
||||
</el-button>
|
||||
<el-button size="large" @click="goToDataSquare" class="btn-secondary">
|
||||
<el-icon><Grid /></el-icon>
|
||||
数据集广场
|
||||
<el-button size="large" @click="goToModels" class="btn-secondary">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
模型管理
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Visual - 全息粒子矩阵风格 -->
|
||||
<!-- Hero Visual - Modern Abstract Composition -->
|
||||
<div class="hero-visual">
|
||||
<!-- Card 1: 多格式支持 -->
|
||||
<div class="hologram-card card-1">
|
||||
<div class="card-bg"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="particles-container">
|
||||
<span class="particle" style="--x: 20%; --y: 30%"></span>
|
||||
<span class="particle" style="--x: 80%; --y: 20%"></span>
|
||||
<span class="particle" style="--x: 50%; --y: 70%"></span>
|
||||
<span class="particle" style="--x: 30%; --y: 60%"></span>
|
||||
<span class="particle" style="--x: 70%; --y: 80%"></span>
|
||||
<span class="particle" style="--x: 15%; --y: 85%"></span>
|
||||
<span class="particle" style="--x: 85%; --y: 45%"></span>
|
||||
<span class="particle" style="--x: 45%; --y: 15%"></span>
|
||||
<!-- Light rays -->
|
||||
<div class="light-rays">
|
||||
<div class="ray"></div>
|
||||
<div class="ray"></div>
|
||||
<div class="ray"></div>
|
||||
<div class="ray"></div>
|
||||
<div class="ray"></div>
|
||||
</div>
|
||||
|
||||
<!-- Ambient particles -->
|
||||
<span class="ambient-particle"></span>
|
||||
<span class="ambient-particle"></span>
|
||||
<span class="ambient-particle"></span>
|
||||
<span class="ambient-particle"></span>
|
||||
<span class="ambient-particle"></span>
|
||||
|
||||
<!-- Abstract background orbs -->
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="orb orb-3"></div>
|
||||
|
||||
<!-- Central floating UI element -->
|
||||
<div class="floating-ui">
|
||||
<div class="ui-header">
|
||||
<div class="ui-dot"></div>
|
||||
<div class="ui-dot"></div>
|
||||
<div class="ui-dot"></div>
|
||||
</div>
|
||||
<div class="pulse-ring"></div>
|
||||
<div class="card-content">
|
||||
<div class="icon-wrapper cyan">
|
||||
<div class="icon-glow"></div>
|
||||
<el-icon size="28"><Document /></el-icon>
|
||||
</div>
|
||||
<span class="card-label">多格式支持</span>
|
||||
<span class="card-sublabel">PDF DOCX EPUB Excel</span>
|
||||
<div class="ui-content">
|
||||
<div class="ui-line"></div>
|
||||
<div class="ui-line short"></div>
|
||||
<div class="ui-line"></div>
|
||||
</div>
|
||||
<div class="ui-badge">
|
||||
<el-icon><Check /></el-icon>
|
||||
<span>处理完成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: AI 生成 -->
|
||||
<div class="hologram-card card-2">
|
||||
<div class="card-bg"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="particles-container">
|
||||
<span class="particle" style="--x: 25%; --y: 35%"></span>
|
||||
<span class="particle" style="--x: 75%; --y: 25%"></span>
|
||||
<span class="particle" style="--x: 55%; --y: 65%"></span>
|
||||
<span class="particle" style="--x: 35%; --y: 55%"></span>
|
||||
<span class="particle" style="--x: 65%; --y: 85%"></span>
|
||||
<span class="particle" style="--x: 20%; --y: 80%"></span>
|
||||
<span class="particle" style="--x: 80%; --y: 50%"></span>
|
||||
<span class="particle" style="--x: 50%; --y: 20%"></span>
|
||||
</div>
|
||||
<div class="pulse-ring"></div>
|
||||
<div class="card-content">
|
||||
<div class="icon-wrapper violet">
|
||||
<div class="icon-glow"></div>
|
||||
<el-icon size="28"><MagicStick /></el-icon>
|
||||
</div>
|
||||
<span class="card-label">AI 生成</span>
|
||||
<span class="card-sublabel">智能问答 自动标注</span>
|
||||
</div>
|
||||
<!-- Floating feature pills - main features -->
|
||||
<div class="feature-pill pill-1">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>多格式支持</span>
|
||||
</div>
|
||||
<div class="feature-pill pill-2">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
<span>AI 生成</span>
|
||||
</div>
|
||||
<div class="feature-pill pill-3">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>智能评估</span>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: 智能评估 -->
|
||||
<div class="hologram-card card-3">
|
||||
<div class="card-bg"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="particles-container">
|
||||
<span class="particle" style="--x: 30%; --y: 25%"></span>
|
||||
<span class="particle" style="--x: 70%; --y: 35%"></span>
|
||||
<span class="particle" style="--x: 45%; --y: 75%"></span>
|
||||
<span class="particle" style="--x: 25%; --y: 65%"></span>
|
||||
<span class="particle" style="--x: 75%; --y: 85%"></span>
|
||||
<span class="particle" style="--x: 10%; --y: 75%"></span>
|
||||
<span class="particle" style="--x: 90%; --y: 40%"></span>
|
||||
<span class="particle" style="--x: 40%; --y: 10%"></span>
|
||||
</div>
|
||||
<div class="pulse-ring"></div>
|
||||
<div class="card-content">
|
||||
<div class="icon-wrapper teal">
|
||||
<div class="icon-glow"></div>
|
||||
<el-icon size="28"><DataAnalysis /></el-icon>
|
||||
</div>
|
||||
<span class="card-label">智能评估</span>
|
||||
<span class="card-sublabel">质量分析 模型对比</span>
|
||||
</div>
|
||||
<!-- Additional floating labels -->
|
||||
<div class="feature-pill pill-4">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<span>API 集成</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<section class="quick-actions">
|
||||
<div class="action-card" @click="goToModels">
|
||||
<div class="action-icon">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<div class="feature-pill pill-5">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>批量处理</span>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<h3>模型配置</h3>
|
||||
<p>管理 AI 模型 API 配置</p>
|
||||
<div class="feature-pill pill-6">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>数据安全</span>
|
||||
</div>
|
||||
<div class="feature-pill pill-7">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>可视化</span>
|
||||
</div>
|
||||
<el-icon class="action-arrow"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -179,7 +188,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { FolderAdd } from '@element-plus/icons-vue'
|
||||
import { FolderAdd, Check, Connection, Clock, Lock, TrendCharts } from '@element-plus/icons-vue'
|
||||
import { projectApi } from '@/api'
|
||||
import type { Project, ProjectCreate } from '@/types'
|
||||
|
||||
@@ -275,5 +284,5 @@ onMounted(() => fetchProjects())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/home.scss';
|
||||
@import '@/styles/pages/home.scss';
|
||||
</style>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<div class="header-content">
|
||||
<h1 class="page-title">
|
||||
<el-icon class="title-icon"><Cpu /></el-icon>
|
||||
模型配置
|
||||
模型管理
|
||||
</h1>
|
||||
<p class="page-subtitle">管理您的 AI 模型 API 配置</p>
|
||||
<p class="page-subtitle">管理您的 AI 模型 API</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" class="add-btn" @click="openAddDialog">
|
||||
@@ -31,19 +31,6 @@
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="page-main">
|
||||
<!-- 统计卡片 -->
|
||||
<section class="stats-grid">
|
||||
<div class="stat-card" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-icon" :class="stat.class">
|
||||
{{ stat.icon }}
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-value">{{ stat.value }}</span>
|
||||
<span class="stat-label">{{ stat.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<section class="models-section">
|
||||
<div class="section-header">
|
||||
@@ -98,9 +85,11 @@
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="card-footer">
|
||||
<div class="status-badge">
|
||||
<span class="status-dot online"></span>
|
||||
已配置
|
||||
<div class="status-badge" :class="model.connection_status">
|
||||
<span class="status-dot" :class="model.connection_status"></span>
|
||||
<template v-if="model.connection_status === 'connected'">已联通</template>
|
||||
<template v-else-if="model.connection_status === 'disconnected'">未联通</template>
|
||||
<template v-else>待测试</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-tooltip content="测试连接" placement="top">
|
||||
@@ -146,18 +135,24 @@
|
||||
<el-form :model="modelForm" label-position="top" class="model-form">
|
||||
<!-- 提供商选择 -->
|
||||
<el-form-item label="选择提供商">
|
||||
<div class="provider-grid">
|
||||
<div
|
||||
<el-select
|
||||
v-model="modelForm.provider"
|
||||
placeholder="选择 AI 服务提供商"
|
||||
size="large"
|
||||
class="provider-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="provider in providers"
|
||||
:key="provider.value"
|
||||
class="provider-option"
|
||||
:class="{ active: modelForm.provider === provider.value }"
|
||||
@click="modelForm.provider = provider.value"
|
||||
:label="provider.label"
|
||||
:value="provider.value"
|
||||
>
|
||||
<span class="provider-abbr">{{ provider.abbr }}</span>
|
||||
<span class="provider-name">{{ provider.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-option-item">
|
||||
<span class="provider-icon">{{ provider.abbr }}</span>
|
||||
<span>{{ provider.label }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 模型名称 -->
|
||||
@@ -246,6 +241,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { ModelConfig, ProviderOption, ModelCreate } from '@/types'
|
||||
import { modelApi } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -260,35 +256,34 @@ const models = ref<ModelConfig[]>([])
|
||||
|
||||
// 表单
|
||||
const modelForm = reactive<ModelCreate>({
|
||||
provider: 'openai',
|
||||
provider: 'minimax',
|
||||
model_name: '',
|
||||
api_key: '',
|
||||
api_base: '',
|
||||
api_base: 'https://api.minimax.chat/v1',
|
||||
is_default: false
|
||||
})
|
||||
|
||||
// 供应商默认 API 地址
|
||||
const providerDefaultUrls: Record<string, string> = {
|
||||
minimax: 'https://api.minimax.chat/v1',
|
||||
glm: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
openai: 'https://api.openai.com/v1'
|
||||
}
|
||||
|
||||
// 提供商
|
||||
const providers: ProviderOption[] = [
|
||||
{ value: 'openai', label: 'OpenAI', abbr: 'OP' },
|
||||
{ value: 'anthropic', label: 'Anthropic', abbr: 'AN' },
|
||||
{ value: 'google', label: 'Google', abbr: 'GO' },
|
||||
{ value: 'other', label: '其他', abbr: 'OT' }
|
||||
{ value: 'minimax', label: 'MiniMax', abbr: 'MM' },
|
||||
{ value: 'glm', label: 'GLM', abbr: 'GL' },
|
||||
{ value: 'openai', label: 'OpenAI Compatible', abbr: 'OP' }
|
||||
]
|
||||
|
||||
// Mock
|
||||
const mockModels: ModelConfig[] = [
|
||||
{ id: '1', provider: 'openai', model_name: 'gpt-4o', api_base: 'https://api.openai.com/v1', is_default: 'true' },
|
||||
{ id: '2', provider: 'openai', model_name: 'gpt-4o-mini', api_base: 'https://api.openai.com/v1', is_default: 'false' },
|
||||
{ id: '3', provider: 'anthropic', model_name: 'claude-3-5-sonnet', api_base: 'https://api.anthropic.com', is_default: 'false' }
|
||||
]
|
||||
|
||||
// 统计
|
||||
const stats = computed(() => [
|
||||
{ label: 'OpenAI', value: models.value.filter(m => m.provider === 'openai').length, icon: 'OP', class: 'openai' },
|
||||
{ label: 'Anthropic', value: models.value.filter(m => m.provider === 'anthropic').length, icon: 'AN', class: 'anthropic' },
|
||||
{ label: 'Google', value: models.value.filter(m => m.provider === 'google').length, icon: 'GO', class: 'google' },
|
||||
{ label: '默认模型', value: models.value.find(m => m.is_default === 'true')?.model_name || '未设置', icon: '★', class: 'default' }
|
||||
])
|
||||
// 监听 provider 变化,自动设置默认 API 地址
|
||||
import { watch } from 'vue'
|
||||
watch(() => modelForm.provider, (newProvider) => {
|
||||
if (providerDefaultUrls[newProvider]) {
|
||||
modelForm.api_base = providerDefaultUrls[newProvider]
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const goHome = () => router.push('/')
|
||||
@@ -301,20 +296,28 @@ const getProviderAbbr = (provider: string) => {
|
||||
const fetchModels = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
models.value = mockModels
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
const res = await modelApi.list()
|
||||
// Handle different response formats
|
||||
if (Array.isArray(res)) {
|
||||
models.value = res
|
||||
} else if (res?.data && Array.isArray(res.data)) {
|
||||
models.value = res.data
|
||||
} else {
|
||||
models.value = []
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取模型列表失败:', error)
|
||||
ElMessage.error(error?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openAddDialog = () => {
|
||||
modelForm.provider = 'openai'
|
||||
modelForm.provider = 'minimax'
|
||||
modelForm.model_name = ''
|
||||
modelForm.api_key = ''
|
||||
modelForm.api_base = ''
|
||||
modelForm.api_base = providerDefaultUrls['minimax']
|
||||
modelForm.is_default = false
|
||||
showAddDialog.value = true
|
||||
}
|
||||
@@ -327,12 +330,21 @@ const addModel = async () => {
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
// Convert is_default from boolean to string
|
||||
const data = {
|
||||
provider: modelForm.provider,
|
||||
model_name: modelForm.model_name,
|
||||
api_key: modelForm.api_key,
|
||||
api_base: modelForm.api_base,
|
||||
is_default: modelForm.is_default ? 'true' : 'false'
|
||||
}
|
||||
await modelApi.create(data)
|
||||
ElMessage.success('添加成功')
|
||||
showAddDialog.value = false
|
||||
fetchModels()
|
||||
} catch {
|
||||
ElMessage.error('添加失败')
|
||||
} catch (error: any) {
|
||||
console.error('添加模型失败:', error)
|
||||
ElMessage.error(error?.message || '添加失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -344,22 +356,44 @@ const confirmDelete = (model: ModelConfig) => {
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!modelToDelete.value?.id) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
await modelApi.delete(modelToDelete.value.id)
|
||||
ElMessage.success('删除成功')
|
||||
deleteDialogVisible.value = false
|
||||
modelToDelete.value = null
|
||||
fetchModels()
|
||||
} catch {
|
||||
ElMessage.error('删除失败')
|
||||
} catch (error: any) {
|
||||
console.error('删除模型失败:', error)
|
||||
ElMessage.error(error?.message || '删除失败')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testConnection = (model: ModelConfig) => {
|
||||
ElMessage.info(`测试 ${model.model_name}...`)
|
||||
const testConnection = async (model: ModelConfig) => {
|
||||
ElMessage.info(`正在测试 ${model.model_name}...`)
|
||||
try {
|
||||
const res = await modelApi.test(model.id)
|
||||
// Update model connection status from response
|
||||
const modelItem = models.value.find(m => m.id === model.id)
|
||||
if (modelItem && res?.model) {
|
||||
modelItem.connection_status = res.model.connection_status
|
||||
if (res.test_result?.success) {
|
||||
ElMessage.success('连接成功!')
|
||||
} else {
|
||||
ElMessage.error(res.test_result?.message || '连接失败')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('测试连接失败:', error)
|
||||
const modelItem = models.value.find(m => m.id === model.id)
|
||||
if (modelItem) {
|
||||
modelItem.connection_status = 'disconnected'
|
||||
}
|
||||
ElMessage.error(error?.message || '连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchModels())
|
||||
@@ -489,63 +523,6 @@ onMounted(() => fetchModels())
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: var(--border-default);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-icon.openai { background: linear-gradient(135deg, #10a37f, #0d8c6d); }
|
||||
.stat-icon.anthropic { background: linear-gradient(135deg, #d97757, #c45f3f); }
|
||||
.stat-icon.google { background: linear-gradient(135deg, #4285f4, #3367d6); }
|
||||
.stat-icon.default { background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); }
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* 模型列表 */
|
||||
.models-section {
|
||||
background: var(--bg-secondary);
|
||||
@@ -637,36 +614,38 @@ onMounted(() => fetchModels())
|
||||
.model-card {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--transition-base);
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: fadeInUp 0.4s ease backwards;
|
||||
animation-delay: var(--delay);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.model-card:hover {
|
||||
border-color: var(--accent-primary-muted);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--glow-primary);
|
||||
border-color: rgba(0, 212, 255, 0.4);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 40px rgba(0, 212, 255, 0.15);
|
||||
}
|
||||
|
||||
.model-card.is-default {
|
||||
border-color: rgba(52, 211, 153, 0.3);
|
||||
border-color: rgba(52, 211, 153, 0.4);
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: radial-gradient(circle at top right, var(--accent-primary-muted), transparent 60%);
|
||||
border-radius: 16px;
|
||||
background: radial-gradient(circle at top right, rgba(0, 212, 255, 0.15), transparent 60%);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-card:hover .card-glow {
|
||||
@@ -688,23 +667,20 @@ onMounted(() => fetchModels())
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
}
|
||||
|
||||
.provider-logo.openai { background: linear-gradient(135deg, #10a37f, #0d8c6d); }
|
||||
.provider-logo.anthropic { background: linear-gradient(135deg, #d97757, #c45f3f); }
|
||||
.provider-logo.google { background: linear-gradient(135deg, #4285f4, #3367d6); }
|
||||
.provider-logo.other { background: linear-gradient(135deg, #6b7280, #4b5563); }
|
||||
|
||||
.model-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -741,6 +717,18 @@ onMounted(() => fetchModels())
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-badge.connected {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-badge.disconnected {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-badge.untested {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -748,9 +736,29 @@ onMounted(() => fetchModels())
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
.status-dot.online,
|
||||
.status-dot.connected {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 8px var(--success);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.disconnected {
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 8px var(--danger);
|
||||
}
|
||||
|
||||
.status-dot.untested {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
@@ -780,30 +788,42 @@ onMounted(() => fetchModels())
|
||||
:deep(.model-dialog .el-dialog) {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-xl);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.model-dialog .el-dialog__header) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.model-dialog .el-dialog__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px 24px;
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, var(--accent-primary-muted), rgba(124, 58, 237, 0.1));
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
border-radius: 12px;
|
||||
color: #030407;
|
||||
box-shadow: 0 4px 16px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.dialog-title h3 {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
@@ -817,74 +837,104 @@ onMounted(() => fetchModels())
|
||||
|
||||
.dialog-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-tertiary);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.provider-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.provider-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.provider-option:hover {
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
.provider-option.active {
|
||||
border-color: var(--accent-primary);
|
||||
background: var(--accent-primary-muted);
|
||||
.provider-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.provider-option .provider-abbr {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
.provider-select :deep(.el-input__wrapper) {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
padding: 4px 16px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-input__wrapper:hover) {
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-input__wrapper.is-focus) {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-select__placeholder) {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-select__selected-item) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.provider-option .provider-name {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
.provider-select :deep(.el-select-dropdown) {
|
||||
background: var(--bg-elevated) !important;
|
||||
border: 1px solid var(--border-subtle) !important;
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.provider-option.active .provider-name {
|
||||
.provider-select :deep(.el-select-dropdown__item) {
|
||||
color: var(--text-primary) !important;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-select-dropdown__item:hover) {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.provider-select :deep(.el-select-dropdown__item.is-selected) {
|
||||
background: var(--accent-primary-muted);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.provider-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #030407;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
padding: 20px 24px;
|
||||
background: var(--bg-tertiary);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
@@ -893,31 +943,32 @@ onMounted(() => fetchModels())
|
||||
:deep(.delete-dialog .el-dialog) {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--danger-muted);
|
||||
border-radius: var(--radius-xl);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.delete-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
gap: 16px;
|
||||
padding: 32px 24px 24px;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--danger-muted);
|
||||
background: linear-gradient(135deg, rgba(248, 113, 113, 0.2), rgba(248, 113, 113, 0.1));
|
||||
border: 1px solid var(--danger-muted);
|
||||
border-radius: 50%;
|
||||
border-radius: 16px;
|
||||
color: var(--danger);
|
||||
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.2);
|
||||
}
|
||||
|
||||
.delete-header h3 {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
@@ -925,12 +976,13 @@ onMounted(() => fetchModels())
|
||||
|
||||
.delete-content {
|
||||
text-align: center;
|
||||
padding: 0 24px 24px;
|
||||
padding: 0 32px 24px;
|
||||
}
|
||||
|
||||
.delete-content p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.delete-content p strong {
|
||||
@@ -940,16 +992,21 @@ onMounted(() => fetchModels())
|
||||
.warning-text {
|
||||
color: var(--danger) !important;
|
||||
font-size: 13px;
|
||||
margin-top: 8px !important;
|
||||
margin-top: 12px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.delete-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
padding: 20px 24px;
|
||||
background: var(--bg-tertiary);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@@ -960,10 +1017,6 @@ onMounted(() => fetchModels())
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.page-main {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user