2026-03-07 13:53:41 +08:00
|
|
|
<script setup lang="ts">
|
2026-03-10 17:38:57 +08:00
|
|
|
import { ref, watch, computed } from 'vue'
|
2026-03-07 13:53:41 +08:00
|
|
|
import { ElMessage } from 'element-plus'
|
2026-03-07 14:28:50 +08:00
|
|
|
import { useModelSettings } from './settings/useModelSettings'
|
2026-03-08 10:47:08 +08:00
|
|
|
import FormDialog from '@/components/FormDialog.vue'
|
2026-03-07 14:28:50 +08:00
|
|
|
import './settings/settings.css'
|
|
|
|
|
import './settings/modelSettings.css'
|
2026-03-07 13:53:41 +08:00
|
|
|
|
|
|
|
|
// 当前选中的设置菜单
|
|
|
|
|
const activeMenu = ref('general')
|
|
|
|
|
|
2026-03-07 14:28:50 +08:00
|
|
|
// 导入 Model Settings 逻辑
|
|
|
|
|
const {
|
|
|
|
|
models,
|
|
|
|
|
modelsLoading,
|
|
|
|
|
modelTypeOptions,
|
|
|
|
|
providerOptions,
|
|
|
|
|
showAddModelForm,
|
2026-03-07 16:21:44 +08:00
|
|
|
showEditModelForm,
|
2026-03-07 14:28:50 +08:00
|
|
|
newModelForm,
|
2026-03-07 16:21:44 +08:00
|
|
|
editForm,
|
2026-03-07 14:28:50 +08:00
|
|
|
testingConnection,
|
|
|
|
|
connectionStatus,
|
|
|
|
|
fetchModels,
|
|
|
|
|
testConnection,
|
|
|
|
|
addModel,
|
|
|
|
|
cancelAddModel,
|
|
|
|
|
deleteModel,
|
2026-03-07 16:21:44 +08:00
|
|
|
openEditDialog,
|
|
|
|
|
updateModel,
|
|
|
|
|
cancelEditModel,
|
|
|
|
|
testingEditConnection,
|
|
|
|
|
editConnectionStatus,
|
|
|
|
|
testConnectionEdit,
|
2026-03-07 14:28:50 +08:00
|
|
|
} = useModelSettings()
|
|
|
|
|
|
|
|
|
|
// 监听菜单切换,获取模型列表
|
|
|
|
|
watch(activeMenu, (newVal) => {
|
|
|
|
|
if (newVal === 'modelSettings') {
|
|
|
|
|
fetchModels()
|
2026-03-07 13:53:41 +08:00
|
|
|
}
|
2026-03-07 14:28:50 +08:00
|
|
|
})
|
2026-03-07 13:53:41 +08:00
|
|
|
|
|
|
|
|
// 设置菜单列表
|
|
|
|
|
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' },
|
2026-03-10 17:38:57 +08:00
|
|
|
{ key: 'logs', label: 'Logs', icon: 'fa-file-lines' },
|
2026-03-07 13:53:41 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 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')
|
|
|
|
|
}
|
2026-03-10 17:38:57 +08:00
|
|
|
|
|
|
|
|
// ========== Logs 功能 ==========
|
|
|
|
|
interface Log {
|
|
|
|
|
id: number
|
|
|
|
|
level: 'info' | 'warning' | 'error' | 'debug'
|
|
|
|
|
source: string
|
|
|
|
|
message: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
user?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const logs = ref<Log[]>([
|
|
|
|
|
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
|
|
|
|
|
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
|
|
|
|
|
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
|
|
|
|
|
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
|
|
|
|
|
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
|
|
|
|
|
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
|
|
|
|
|
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
|
|
|
|
|
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
|
|
|
|
|
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
|
|
|
|
|
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const logSearchQuery = ref('')
|
|
|
|
|
const logFilterLevel = ref('')
|
|
|
|
|
const logFilterSource = ref('')
|
|
|
|
|
|
|
|
|
|
const logLevelOptions = [
|
|
|
|
|
{ value: '', label: 'All Levels' },
|
|
|
|
|
{ value: 'info', label: 'Info' },
|
|
|
|
|
{ value: 'warning', label: 'Warning' },
|
|
|
|
|
{ value: 'error', label: 'Error' },
|
|
|
|
|
{ value: 'debug', label: 'Debug' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const logSourceOptions = computed(() => {
|
|
|
|
|
const sources = [...new Set(logs.value.map(l => l.source))]
|
|
|
|
|
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const filteredLogs = computed(() => {
|
|
|
|
|
return logs.value.filter(log => {
|
|
|
|
|
const matchSearch = logSearchQuery.value === '' ||
|
|
|
|
|
log.message.toLowerCase().includes(logSearchQuery.value.toLowerCase()) ||
|
|
|
|
|
log.source.toLowerCase().includes(logSearchQuery.value.toLowerCase())
|
|
|
|
|
const matchLevel = logFilterLevel.value === '' || log.level === logFilterLevel.value
|
|
|
|
|
const matchSource = logFilterSource.value === '' || log.source === logFilterSource.value
|
|
|
|
|
return matchSearch && matchLevel && matchSource
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const logLevelClass = (level: string) => {
|
|
|
|
|
switch (level) {
|
|
|
|
|
case 'info': return 'bg-blue-500/20 text-blue-400'
|
|
|
|
|
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
|
|
|
|
|
case 'error': return 'bg-red-500/20 text-red-400'
|
|
|
|
|
case 'debug': return 'bg-gray-500/20 text-gray-400'
|
|
|
|
|
default: return 'bg-gray-500/20 text-gray-400'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedLog = ref<Log | null>(null)
|
|
|
|
|
const showLogDetail = ref(false)
|
|
|
|
|
|
|
|
|
|
const viewLogDetail = (log: Log) => {
|
|
|
|
|
selectedLog.value = log
|
|
|
|
|
showLogDetail.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const closeLogDetail = () => {
|
|
|
|
|
showLogDetail.value = false
|
|
|
|
|
selectedLog.value = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clearLogs = () => {
|
|
|
|
|
logs.value = []
|
|
|
|
|
}
|
2026-03-07 13:53:41 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="settings-page">
|
|
|
|
|
<!-- 页面标题 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
<div class="flex items-center gap-2 mb-6">
|
|
|
|
|
<i class="fa-solid fa-gear text-orange-500"></i>
|
|
|
|
|
<span class="font-medium">Settings</span>
|
2026-03-07 13:53:41 +08:00
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
|
<!-- 模型列表 -->
|
2026-03-07 14:28:50 +08:00
|
|
|
<div v-if="modelsLoading" class="loading-spinner">
|
|
|
|
|
<i class="fa-solid fa-spinner"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<table v-else class="model-table">
|
|
|
|
|
<thead>
|
2026-03-07 13:53:41 +08:00
|
|
|
<tr>
|
2026-03-07 14:28:50 +08:00
|
|
|
<th>Model Name</th>
|
|
|
|
|
<th class="text-center">Provider</th>
|
|
|
|
|
<th class="text-center">Model</th>
|
|
|
|
|
<th class="text-center">Model Type</th>
|
|
|
|
|
<th class="text-center">Base URL</th>
|
|
|
|
|
<th class="text-center">Status</th>
|
|
|
|
|
<th class="text-center">Actions</th>
|
2026-03-07 13:53:41 +08:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="model in models" :key="model.id" class="table-row">
|
2026-03-07 14:28:50 +08:00
|
|
|
<td>
|
2026-03-07 13:53:41 +08:00
|
|
|
<span class="font-medium">{{ model.name }}</span>
|
|
|
|
|
</td>
|
2026-03-07 14:28:50 +08:00
|
|
|
<td class="text-center text-sm">
|
|
|
|
|
{{ model.provider }}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-center text-sm">
|
|
|
|
|
{{ model.model }}
|
2026-03-07 13:53:41 +08:00
|
|
|
</td>
|
2026-03-07 14:28:50 +08:00
|
|
|
<td class="text-center">
|
2026-03-07 17:19:11 +08:00
|
|
|
<span class="model-type-tag" :class="model.model_type">{{ model.model_type }}</span>
|
2026-03-07 13:53:41 +08:00
|
|
|
</td>
|
2026-03-07 14:28:50 +08:00
|
|
|
<td class="text-center text-sm">
|
|
|
|
|
{{ model.base_url }}
|
2026-03-07 13:53:41 +08:00
|
|
|
</td>
|
2026-03-07 14:28:50 +08:00
|
|
|
<td class="text-center">
|
|
|
|
|
<span v-if="model.status === 'active'" class="status-active">Active</span>
|
|
|
|
|
<span v-else class="status-inactive">Inactive</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-center">
|
2026-03-07 16:21:44 +08:00
|
|
|
<button class="btn-icon" title="Edit" @click="openEditDialog(model)">
|
2026-03-07 14:28:50 +08:00
|
|
|
<i class="fa-solid fa-pen"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-icon" title="Delete" @click="deleteModel(model.id)">
|
|
|
|
|
<i class="fa-solid fa-trash"></i>
|
|
|
|
|
</button>
|
2026-03-07 13:53:41 +08:00
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<!-- 新增模型弹窗 -->
|
2026-03-08 10:47:08 +08:00
|
|
|
<FormDialog
|
|
|
|
|
:model-value="showAddModelForm"
|
|
|
|
|
@update:model-value="showAddModelForm = $event"
|
2026-03-07 13:53:41 +08:00
|
|
|
title="Add New Model"
|
2026-03-08 10:47:08 +08:00
|
|
|
description="Configure your model settings"
|
|
|
|
|
icon="fa-solid fa-brain"
|
|
|
|
|
icon-class="bg-gradient-to-br from-primary-orange to-red-500"
|
2026-03-09 15:42:55 +08:00
|
|
|
class="add-model-dialog"
|
2026-03-07 13:53:41 +08:00
|
|
|
>
|
2026-03-08 10:47:08 +08:00
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model Name</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="newModelForm.name"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., My GPT-4 Model"
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model Type</label>
|
|
|
|
|
<el-select v-model="newModelForm.modelType" placeholder="Select model type" class="w-full" size="large" popper-class="dark-select-dropdown">
|
2026-03-07 13:53:41 +08:00
|
|
|
<el-option
|
|
|
|
|
v-for="type in modelTypeOptions"
|
|
|
|
|
:key="type.value"
|
|
|
|
|
:label="type.label"
|
|
|
|
|
:value="type.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
|
|
|
|
<el-select v-model="newModelForm.provider" placeholder="Select provider" class="w-full" size="large" popper-class="dark-select-dropdown">
|
2026-03-07 13:53:41 +08:00
|
|
|
<el-option
|
|
|
|
|
v-for="provider in providerOptions"
|
|
|
|
|
:key="provider.value"
|
|
|
|
|
:label="provider.label"
|
|
|
|
|
:value="provider.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="newModelForm.model"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., gpt-4o (OpenAI) or llama3 (Ollama)"
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">API Key</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="newModelForm.apiKey"
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="sk-xxxxx (required for OpenAI)"
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Base URL</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="newModelForm.baseUrl"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="https://api.openai.com/v1 (OpenAI) or http://localhost:11434 (Ollama)"
|
|
|
|
|
class="input-field"
|
2026-03-07 13:53:41 +08:00
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-08 10:47:08 +08:00
|
|
|
|
|
|
|
|
<!-- 连接状态提示 -->
|
2026-03-07 13:53:41 +08:00
|
|
|
<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>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<button class="btn-secondary" @click="cancelAddModel">Cancel</button>
|
|
|
|
|
<button
|
|
|
|
|
:disabled="testingConnection"
|
|
|
|
|
class="btn-primary"
|
|
|
|
|
@click="testConnection"
|
|
|
|
|
>
|
|
|
|
|
<i class="fa-solid fa-plug"></i>
|
|
|
|
|
Test Connection
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-primary" @click="addModel">
|
|
|
|
|
Confirm
|
|
|
|
|
</button>
|
2026-03-07 13:53:41 +08:00
|
|
|
</template>
|
2026-03-08 10:47:08 +08:00
|
|
|
</FormDialog>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
|
|
|
|
<!-- 编辑模型弹窗 -->
|
2026-03-08 10:47:08 +08:00
|
|
|
<FormDialog
|
|
|
|
|
:model-value="showEditModelForm"
|
|
|
|
|
@update:model-value="showEditModelForm = $event"
|
2026-03-07 16:21:44 +08:00
|
|
|
title="Edit Model"
|
2026-03-08 10:47:08 +08:00
|
|
|
description="Update your model settings"
|
|
|
|
|
icon="fa-solid fa-pen-to-square"
|
|
|
|
|
icon-class="bg-gradient-to-br from-primary-cyan to-blue-500"
|
2026-03-07 16:21:44 +08:00
|
|
|
>
|
2026-03-08 10:47:08 +08:00
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model Name</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="editForm.name"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., My GPT-4 Model"
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
|
|
|
|
<el-select v-model="editForm.provider" placeholder="Select provider" class="w-full" size="large" popper-class="dark-select-dropdown">
|
2026-03-07 16:21:44 +08:00
|
|
|
<el-option
|
|
|
|
|
v-for="option in providerOptions"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
:label="option.label"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="editForm.model"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., gpt-4, llama2"
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model Type</label>
|
|
|
|
|
<el-select v-model="editForm.modelType" placeholder="Select model type" class="w-full" size="large" popper-class="dark-select-dropdown">
|
2026-03-07 16:21:44 +08:00
|
|
|
<el-option
|
|
|
|
|
v-for="option in modelTypeOptions"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
:label="option.label"
|
|
|
|
|
:value="option.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">API Key</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="editForm.apiKey"
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="sk-..."
|
|
|
|
|
class="input-field"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-07 16:21:44 +08:00
|
|
|
|
2026-03-08 10:47:08 +08:00
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Base URL</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="editForm.baseUrl"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., https://api.openai.com/v1"
|
|
|
|
|
class="input-field"
|
2026-03-07 16:21:44 +08:00
|
|
|
>
|
|
|
|
|
</div>
|
2026-03-08 10:47:08 +08:00
|
|
|
|
|
|
|
|
<!-- 连接状态提示 -->
|
2026-03-07 16:21:44 +08:00
|
|
|
<div v-if="editConnectionStatus === 'success'" class="connection-status success">
|
|
|
|
|
<i class="fa-solid fa-check-circle"></i> Connection successful
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="editConnectionStatus === 'error'" class="connection-status error">
|
|
|
|
|
<i class="fa-solid fa-xmark-circle"></i> Connection failed
|
|
|
|
|
</div>
|
2026-03-08 10:47:08 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
<button class="btn-secondary" @click="cancelEditModel">Cancel</button>
|
|
|
|
|
<button
|
|
|
|
|
:disabled="testingEditConnection"
|
|
|
|
|
class="btn-primary"
|
|
|
|
|
@click="testConnectionEdit"
|
|
|
|
|
>
|
|
|
|
|
<i class="fa-solid fa-plug"></i>
|
|
|
|
|
Test Connection
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn-primary" @click="updateModel">
|
|
|
|
|
Confirm
|
|
|
|
|
</button>
|
2026-03-07 16:21:44 +08:00
|
|
|
</template>
|
2026-03-08 10:47:08 +08:00
|
|
|
</FormDialog>
|
2026-03-07 13:53:41 +08:00
|
|
|
</div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
|
|
|
|
<!-- Logs 设置 -->
|
|
|
|
|
<div v-if="activeMenu === 'logs'" class="settings-section">
|
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="section-title">Logs</h2>
|
|
|
|
|
<p class="section-desc">View system logs</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
|
|
|
|
<i class="fa-solid fa-trash"></i>
|
|
|
|
|
Clear Logs
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 搜索和筛选 -->
|
|
|
|
|
<div class="flex gap-4 mb-6">
|
|
|
|
|
<div class="flex-1 relative">
|
|
|
|
|
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
|
|
|
|
<input
|
|
|
|
|
v-model="logSearchQuery"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search logs..."
|
|
|
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
<el-select v-model="logFilterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
|
|
|
|
|
<el-option v-for="opt in logLevelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-select v-model="logFilterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
|
|
|
|
|
<el-option v-for="opt in logSourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 日志列表 -->
|
|
|
|
|
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
|
|
|
|
<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">Level</th>
|
|
|
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
|
|
|
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
|
|
|
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
|
|
|
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
|
|
|
|
|
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
|
|
|
|
<td class="px-5 py-4">
|
|
|
|
|
<span :class="['px-2 py-1 rounded text-xs font-medium', logLevelClass(log.level)]">
|
|
|
|
|
{{ log.level.toUpperCase() }}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
|
|
|
|
|
<td class="px-5 py-4 text-gray-300 max-w-md">
|
|
|
|
|
<div class="truncate">{{ log.message }}</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
|
|
|
|
|
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
|
|
|
|
|
<td class="px-5 py-4">
|
|
|
|
|
<div class="flex items-center justify-end">
|
|
|
|
|
<button
|
|
|
|
|
@click="viewLogDetail(log)"
|
|
|
|
|
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
|
|
|
|
title="View Details"
|
|
|
|
|
>
|
|
|
|
|
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
|
|
|
|
|
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
|
|
|
|
|
<p>No logs found</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 日志详情弹窗 -->
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeLogDetail">
|
|
|
|
|
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
|
|
|
|
|
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
|
|
|
|
<h3 class="text-lg font-semibold">Log Details</h3>
|
|
|
|
|
<button @click="closeLogDetail" class="text-gray-400 hover:text-white transition-colors">
|
|
|
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="selectedLog" class="p-5 space-y-4">
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
<span :class="['px-3 py-1 rounded text-sm font-medium', logLevelClass(selectedLog.level)]">
|
|
|
|
|
{{ selectedLog.level.toUpperCase() }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
|
|
|
|
|
<div class="text-white">{{ selectedLog.source }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
|
|
|
|
|
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
|
|
|
|
|
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
|
|
|
|
|
{{ selectedLog.message }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
|
|
|
|
|
<div class="text-gray-500">#{{ selectedLog.id }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
|
|
|
|
<button
|
|
|
|
|
@click="closeLogDetail"
|
|
|
|
|
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Close
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Teleport>
|
|
|
|
|
</div>
|
2026-03-07 13:53:41 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|