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:
@@ -2,7 +2,7 @@
|
||||
API Dependencies
|
||||
API 依赖项
|
||||
"""
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Optional
|
||||
from fastapi import Depends
|
||||
from app.core.auth import verify_api_key
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ API v1 Router
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import files, projects, chunks, questions, datasets, eval
|
||||
from app.api.v1 import files, projects, chunks, questions, datasets, eval, models
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -15,3 +15,4 @@ api_router.include_router(chunks.router, prefix="/chunks", tags=["chunks"])
|
||||
api_router.include_router(questions.router, prefix="/questions", tags=["questions"])
|
||||
api_router.include_router(datasets.router, prefix="/datasets", tags=["datasets"])
|
||||
api_router.include_router(eval.router, prefix="/eval", tags=["eval"])
|
||||
api_router.include_router(models.router, prefix="/models", tags=["models"])
|
||||
|
||||
257
backend/app/api/v1/models/__init__.py
Normal file
257
backend/app/api/v1/models/__init__.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Model API Router
|
||||
"""
|
||||
import uuid
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.response import ApiResponse
|
||||
from app.models.models import ModelConfig
|
||||
from app.schemas.model import ModelCreate, ModelUpdate, ModelResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def test_model_connection(model: ModelConfig) -> dict:
|
||||
"""Test model connection by calling the API"""
|
||||
if not model.api_key:
|
||||
return {"success": False, "message": "API Key is missing"}
|
||||
|
||||
api_base = model.api_base or ""
|
||||
provider = model.provider
|
||||
model_name = model.model_name
|
||||
api_key = model.api_key
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
if provider == "openai":
|
||||
# OpenAI compatible API test
|
||||
response = await client.post(
|
||||
f"{api_base.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json={
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": "Hi"}],
|
||||
"max_tokens": 5
|
||||
}
|
||||
)
|
||||
elif provider == "minimax":
|
||||
# MiniMax API test
|
||||
response = await client.post(
|
||||
f"{api_base.rstrip('/')}/chat/completions_v2",
|
||||
headers={
|
||||
**headers,
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
},
|
||||
json={
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}
|
||||
)
|
||||
elif provider == "glm":
|
||||
# GLM API test
|
||||
response = await client.post(
|
||||
f"{api_base.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json={
|
||||
"model": model_name,
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}
|
||||
)
|
||||
else:
|
||||
return {"success": False, "message": f"Unsupported provider: {provider}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"success": True, "message": "Connection successful"}
|
||||
else:
|
||||
return {"success": False, "message": f"API error: {response.status_code} - {response.text[:100]}"}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {"success": False, "message": "Connection timeout"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Connection failed: {str(e)}"}
|
||||
|
||||
|
||||
# Helper to convert string to UUID
|
||||
def parse_uuid(id_str: str) -> uuid.UUID:
|
||||
"""Parse string to UUID"""
|
||||
try:
|
||||
return uuid.UUID(id_str)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid UUID format")
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse)
|
||||
async def list_models(db: AsyncSession = Depends(get_db)):
|
||||
"""Get all models"""
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(ModelConfig.project_id == None) # noqa: E711
|
||||
)
|
||||
models = result.scalars().all()
|
||||
# Convert to Pydantic schema
|
||||
model_responses = [ModelResponse.model_validate(m) for m in models]
|
||||
return ApiResponse(data=model_responses)
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse)
|
||||
async def create_model(model: ModelCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Create a new model"""
|
||||
# If setting as default, unset other defaults first
|
||||
if model.is_default == "true":
|
||||
await db.execute(
|
||||
update(ModelConfig)
|
||||
.where(ModelConfig.project_id == None) # noqa: E711
|
||||
.values(is_default="false")
|
||||
)
|
||||
|
||||
db_model = ModelConfig(
|
||||
provider=model.provider,
|
||||
model_name=model.model_name,
|
||||
api_key=model.api_key,
|
||||
api_base=model.api_base,
|
||||
is_default=model.is_default,
|
||||
project_id=None # Global model config
|
||||
)
|
||||
db.add(db_model)
|
||||
await db.commit()
|
||||
await db.refresh(db_model)
|
||||
# Convert to Pydantic schema
|
||||
response = ModelResponse.model_validate(db_model)
|
||||
return ApiResponse(data=response)
|
||||
|
||||
|
||||
@router.get("/{model_id}", response_model=ApiResponse)
|
||||
async def get_model(model_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a model by ID"""
|
||||
model_uuid = parse_uuid(model_id)
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(
|
||||
ModelConfig.id == model_uuid,
|
||||
ModelConfig.project_id == None # noqa: E711
|
||||
)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
response = ModelResponse.model_validate(model)
|
||||
return ApiResponse(data=response)
|
||||
|
||||
|
||||
@router.put("/{model_id}", response_model=ApiResponse)
|
||||
async def update_model(model_id: str, model_update: ModelUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""Update a model"""
|
||||
model_uuid = parse_uuid(model_id)
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(
|
||||
ModelConfig.id == model_uuid,
|
||||
ModelConfig.project_id == None # noqa: E711
|
||||
)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
# If setting as default, unset other defaults first
|
||||
if model_update.is_default == "true":
|
||||
await db.execute(
|
||||
update(ModelConfig)
|
||||
.where(
|
||||
ModelConfig.project_id == None, # noqa: E711
|
||||
ModelConfig.id != model_uuid
|
||||
)
|
||||
.values(is_default="false")
|
||||
)
|
||||
|
||||
update_data = model_update.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(model, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(model)
|
||||
response = ModelResponse.model_validate(model)
|
||||
return ApiResponse(data=response)
|
||||
|
||||
|
||||
@router.delete("/{model_id}", response_model=ApiResponse)
|
||||
async def delete_model(model_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Delete a model"""
|
||||
model_uuid = parse_uuid(model_id)
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(
|
||||
ModelConfig.id == model_uuid,
|
||||
ModelConfig.project_id == None # noqa: E711
|
||||
)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
await db.delete(model)
|
||||
await db.commit()
|
||||
return ApiResponse(message="Model deleted successfully")
|
||||
|
||||
|
||||
@router.post("/{model_id}/set-default", response_model=ApiResponse)
|
||||
async def set_default_model(model_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Set a model as default"""
|
||||
model_uuid = parse_uuid(model_id)
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(
|
||||
ModelConfig.id == model_uuid,
|
||||
ModelConfig.project_id == None # noqa: E711
|
||||
)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
# Unset all other defaults
|
||||
await db.execute(
|
||||
update(ModelConfig)
|
||||
.where(
|
||||
ModelConfig.project_id == None, # noqa: E711
|
||||
ModelConfig.id != model_uuid
|
||||
)
|
||||
.values(is_default="false")
|
||||
)
|
||||
|
||||
model.is_default = "true"
|
||||
await db.commit()
|
||||
await db.refresh(model)
|
||||
response = ModelResponse.model_validate(model)
|
||||
return ApiResponse(data=response)
|
||||
|
||||
|
||||
@router.post("/{model_id}/test", response_model=ApiResponse)
|
||||
async def test_model(model_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Test model connection"""
|
||||
model_uuid = parse_uuid(model_id)
|
||||
result = await db.execute(
|
||||
select(ModelConfig).where(
|
||||
ModelConfig.id == model_uuid,
|
||||
ModelConfig.project_id == None # noqa: E711
|
||||
)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="Model not found")
|
||||
|
||||
# Test the connection
|
||||
test_result = await test_model_connection(model)
|
||||
|
||||
# Save connection status to database
|
||||
model.connection_status = "connected" if test_result["success"] else "disconnected"
|
||||
await db.commit()
|
||||
await db.refresh(model)
|
||||
|
||||
# Return updated model
|
||||
response = ModelResponse.model_validate(model)
|
||||
return ApiResponse(data={"test_result": test_result, "model": response})
|
||||
@@ -100,3 +100,8 @@ async def get_db() -> AsyncSession:
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
# Import all models to register them with Base.metadata
|
||||
# This ensures all models are loaded before create_all is called
|
||||
from app.models.models import * # noqa: F401, F403, E402
|
||||
|
||||
@@ -21,6 +21,9 @@ from app.core.database import init_db, close_db
|
||||
from app.core.exceptions import AppException
|
||||
from app.core.logging import logger
|
||||
|
||||
# Import all models to register them with Base.metadata
|
||||
from app.models.models import * # noqa: F401, F403
|
||||
|
||||
|
||||
class RequestIDMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware to add request ID to each request"""
|
||||
@@ -83,7 +86,7 @@ app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
# CORS - Configure properly for production
|
||||
# For development, you can use ["*"] but for production, specify exact origins
|
||||
ALLOWED_ORIGINS = settings.ALLOWED_ORIGINS.split(",") if settings.ALLOWED_ORIGINS else ["*"]
|
||||
ALLOWED_ORIGINS = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
@@ -135,12 +135,13 @@ class ModelConfig(Base, UUIDMixin, TimestampMixin):
|
||||
"""Model configuration for LLM providers"""
|
||||
__tablename__ = "model_configs"
|
||||
|
||||
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
|
||||
provider = Column(String(50), nullable=False) # openai, anthropic, ollama, custom
|
||||
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE"), nullable=True)
|
||||
provider = Column(String(50), nullable=False) # minimax, glm, openai
|
||||
model_name = Column(String(100))
|
||||
api_key = Column(String(500))
|
||||
api_base = Column(String(500))
|
||||
is_default = Column(String(10), default="false")
|
||||
connection_status = Column(String(20), default="untested") # untested, connected, disconnected
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", back_populates="model_configs")
|
||||
|
||||
@@ -50,6 +50,13 @@ from app.schemas.eval import (
|
||||
TaskResponse,
|
||||
)
|
||||
|
||||
from app.schemas.model import (
|
||||
ModelBase,
|
||||
ModelCreate,
|
||||
ModelUpdate,
|
||||
ModelResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
"TimestampMixin",
|
||||
@@ -86,4 +93,9 @@ __all__ = [
|
||||
"EvalDatasetResponse",
|
||||
"TaskBase",
|
||||
"TaskResponse",
|
||||
# Model
|
||||
"ModelBase",
|
||||
"ModelCreate",
|
||||
"ModelUpdate",
|
||||
"ModelResponse",
|
||||
]
|
||||
|
||||
41
backend/app/schemas/model.py
Normal file
41
backend/app/schemas/model.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Model Schema
|
||||
"""
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class ModelBase(BaseModel):
|
||||
"""Base model schema"""
|
||||
provider: str = Field(..., description="Model provider: minimax, glm, openai")
|
||||
model_name: str = Field(..., description="Model name")
|
||||
api_key: Optional[str] = Field(None, description="API key")
|
||||
api_base: Optional[str] = Field(None, description="API base URL")
|
||||
is_default: str = Field(default="false", description="Is default model: true/false")
|
||||
|
||||
|
||||
class ModelCreate(ModelBase):
|
||||
"""Model creation schema"""
|
||||
pass
|
||||
|
||||
|
||||
class ModelUpdate(BaseModel):
|
||||
"""Model update schema"""
|
||||
provider: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
api_base: Optional[str] = None
|
||||
is_default: Optional[str] = None
|
||||
|
||||
|
||||
class ModelResponse(ModelBase):
|
||||
"""Model response schema"""
|
||||
id: UUID
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
project_id: Optional[UUID] = None
|
||||
connection_status: Optional[str] = Field(default="untested")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -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