feat: 优化前端显示&修复若干bug

This commit is contained in:
CaasianVale
2025-03-07 03:33:18 +08:00
parent ff5b820a57
commit 4c115cf325
29 changed files with 3726 additions and 1209 deletions

View File

@@ -5,16 +5,17 @@
size="small"
@click="toggleConfig"
:quaternary="true"
:type="expanded ? 'primary' : 'default'"
>
<template #icon>
<n-icon :component="expanded ? ChevronUpIcon : ChevronDownIcon" />
</template>
API配置 {{ expanded ? '收起' : '展开' }}
<span class="toggle-text">API配置 {{ expanded ? '收起' : '展开' }}</span>
</n-button>
<n-collapse-transition :show="expanded">
<n-card class="api-config-card" content-style="padding: 0.75rem;">
<n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible">
<n-card class="api-config-card" :bordered="false">
<n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible" class="api-info-alert">
<template #icon>
<n-icon :component="InformationCircleIcon" />
</template>
@@ -28,21 +29,32 @@
</div>
</n-alert>
<n-grid :cols="24" :x-gap="12">
<n-grid-item :span="14">
<n-grid :cols="24" :x-gap="16" :y-gap="16">
<n-grid-item :span="24" :lg-span="14">
<n-form-item label="API URL" path="apiUrl">
<n-input
v-model:value="apiConfig.apiUrl"
placeholder="https://api.openai.com/v1/chat/completions"
@update:value="handleConfigChange"
/>
round
>
<template #prefix>
<n-icon :component="GlobeIcon" />
</template>
</n-input>
<template #feedback>
<span class="formatted-url">{{ formattedUrl }}</span>
<div class="url-feedback">
<span class="formatted-url">实际请求地址: {{ formattedUrl }}</span>
<div class="url-tips">
<div>提示: URL以/结尾将忽略v1路径</div>
<div>URL以#结尾将使用原始地址</div>
</div>
</div>
</template>
</n-form-item>
</n-grid-item>
<n-grid-item :span="10">
<n-grid-item :span="24" :lg-span="10">
<n-form-item label="API Key" path="apiKey">
<n-input
v-model:value="apiConfig.apiKey"
@@ -50,21 +62,63 @@
placeholder="sk-..."
show-password-on="click"
@update:value="handleConfigChange"
/>
round
>
<template #prefix>
<n-icon :component="KeyIcon" />
</template>
</n-input>
</n-form-item>
</n-grid-item>
<n-grid-item :span="12">
<n-grid-item :span="12" :lg-span="12">
<n-form-item label="模型" path="apiModel">
<n-input
v-model:value="apiConfig.apiModel"
placeholder="gpt-3.5-turbo"
<n-input
v-model:value="apiConfig.apiModel"
placeholder="输入或选择模型名称"
@update:value="handleConfigChange"
/>
round
>
<template #prefix>
<n-icon :component="CodeIcon" />
</template>
<template #suffix>
<n-dropdown
trigger="click"
:options="modelOptions"
@select="selectModel"
placement="bottom-end"
>
<n-button quaternary circle size="small" class="model-dropdown-btn">
<template #icon>
<n-icon :component="ChevronDownIcon" />
</template>
</n-button>
</n-dropdown>
</template>
</n-input>
<template #feedback>
<div class="model-suggestions">
<div class="model-tip">您可以直接输入模型名称或点击右侧按钮从下拉菜单选择</div>
<span>常用模型:</span>
<div class="model-chips">
<n-tag
v-for="model in commonModels"
:key="model.key"
size="small"
round
clickable
@click="selectModel(model.key)"
>
{{ model.label }}
</n-tag>
</div>
</div>
</template>
</n-form-item>
</n-grid-item>
<n-grid-item :span="12">
<n-grid-item :span="12" :lg-span="12">
<n-form-item label="超时时间(秒)" path="apiTimeout">
<n-input-number
v-model:value="apiTimeout"
@@ -72,7 +126,20 @@
:min="1"
:max="300"
@update:value="handleTimeoutChange"
/>
:show-button="false"
class="timeout-input"
>
<template #suffix>
<div class="timeout-controls">
<n-button size="tiny" quaternary @click="decreaseTimeout">
<template #icon><n-icon :component="RemoveIcon" /></template>
</n-button>
<n-button size="tiny" quaternary @click="increaseTimeout">
<template #icon><n-icon :component="AddIcon" /></template>
</n-button>
</div>
</template>
</n-input-number>
</n-form-item>
</n-grid-item>
</n-grid>
@@ -92,22 +159,36 @@
:loading="testingConnection"
:disabled="!isConfigValid"
@click="testConnection"
round
>
<template #icon>
<n-icon :component="CheckmarkIcon" />
</template>
测试连接
</n-button>
<n-button @click="resetConfig">
<n-button @click="resetConfig" round>
<template #icon>
<n-icon :component="RefreshIcon" />
</template>
重置
</n-button>
</div>
</div>
<n-divider v-if="connectionStatus" style="margin: 16px 0 12px" />
<div v-if="connectionStatus" class="connection-status" :class="connectionStatus.type">
<n-icon :component="connectionStatus.icon" class="status-icon" />
<span class="status-message">{{ connectionStatus.message }}</span>
</div>
</n-card>
</n-collapse-transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, reactive } from 'vue';
import {
NButton,
NIcon,
@@ -120,13 +201,25 @@ import {
NInputNumber,
NSwitch,
NAlert,
NDivider,
NDropdown,
NTag,
useMessage
} from 'naive-ui';
import {
ChevronDown as ChevronDownIcon,
ChevronUp as ChevronUpIcon,
InformationCircleOutline as InformationCircleIcon,
Close as CloseIcon
Close as CloseIcon,
Globe as GlobeIcon,
Key as KeyIcon,
CheckmarkCircleOutline as CheckmarkIcon,
RefreshOutline as RefreshIcon,
AddOutline as AddIcon,
RemoveOutline as RemoveIcon,
CheckmarkCircle as SuccessIcon,
CloseCircle as ErrorIcon,
CodeSlashOutline as CodeIcon
} from '@vicons/ionicons5';
import { apiService } from '@/services/api';
import { saveApiConfigToLocalStorage, loadApiConfig } from '@/utils';
@@ -147,11 +240,41 @@ const expanded = ref(false);
const testingConnection = ref(false);
const isApiInfoVisible = ref(true);
// 连接状态
const connectionStatus = ref<{
type: 'success' | 'error';
message: string;
icon: any;
} | null>(null);
// 模型选项
const modelOptions = [
{ label: 'GPT-3.5', key: 'gpt-3.5-turbo' },
{ label: 'GPT-4o', key: 'gpt-4o' },
{ label: 'DeepSeek V3', key: 'deepseek-chat' },
{ label: 'DeepSeek R1', key: 'deepseek-reasoner' },
{ label: 'Claude 3.5 Sonnet', key: 'claude-3-5-sonnet' },
{ label: 'Claude 3.5 Sonnet 20241022', key: 'claude-3-5-sonnet-20241022' },
{ label: 'Gemini 1.5 Pro', key: 'gemini-1.5-pro' },
{ label: 'Gemini 1.5 Flash', key: 'gemini-1.5-flash' },
{ label: 'Gemini 2.0 Pro', key: 'gemini-2.0-pro' },
{ label: 'Gemini 2.0 Flash', key: 'gemini-2.0-flash' }
];
// 常用模型(用于快速选择)
const commonModels = [
{ label: 'GPT-3.5', key: 'gpt-3.5-turbo' },
{ label: 'GPT-4o', key: 'gpt-4o' },
{ label: 'Claude 3.5', key: 'claude-3-5-sonnet' },
{ label: 'Gemini 2.0', key: 'gemini-2.0-flash' },
{ label: 'DeepSeek V3', key: 'deepseek-chat' },
];
// API配置
const apiConfig = ref<ApiConfig>({
apiUrl: props.defaultApiUrl || '',
apiKey: '',
apiModel: props.defaultApiModel || 'gpt-3.5-turbo',
apiModel: props.defaultApiModel || '',
apiTimeout: props.defaultApiTimeout || '60',
saveApiConfig: false
});
@@ -176,6 +299,8 @@ function toggleConfig() {
}
function handleConfigChange() {
console.log('API配置变更:', apiConfig.value);
// 如果选择了保存配置,则自动保存
if (apiConfig.value.saveApiConfig) {
saveApiConfigToLocalStorage({
@@ -198,13 +323,32 @@ function handleTimeoutChange(value: number | null) {
}
}
function increaseTimeout() {
if (apiTimeout.value < 300) {
apiTimeout.value += 10;
handleTimeoutChange(apiTimeout.value);
}
}
function decreaseTimeout() {
if (apiTimeout.value > 10) {
apiTimeout.value -= 10;
handleTimeoutChange(apiTimeout.value);
}
}
function formatApiUrl(url: string): string {
if (!url) return '';
try {
// 尝试解析URL
const parsedUrl = new URL(url);
return `${parsedUrl.origin}${parsedUrl.pathname}`;
// 使用与后端一致的URL格式化逻辑
if (url.endsWith('/')) {
return `${url}chat/completions`;
} else if (url.endsWith('#')) {
return url.replace('#', '');
} else {
return `${url}/v1/chat/completions`;
}
} catch (e) {
// 如果URL格式错误则返回原始字符串
return url;
@@ -218,6 +362,7 @@ async function testConnection() {
}
testingConnection.value = true;
connectionStatus.value = null;
try {
const response = await apiService.testApiConnection({
@@ -229,6 +374,11 @@ async function testConnection() {
if (response.success) {
message.success('API连接测试成功');
connectionStatus.value = {
type: 'success',
message: '连接成功API配置有效。',
icon: SuccessIcon
};
// 如果选择了保存配置,则保存
if (apiConfig.value.saveApiConfig) {
@@ -242,9 +392,19 @@ async function testConnection() {
}
} else {
message.error(`API连接测试失败: ${response.message}`);
connectionStatus.value = {
type: 'error',
message: `连接失败: ${response.message}`,
icon: ErrorIcon
};
}
} catch (error: any) {
message.error(`测试连接出错: ${error.message || '未知错误'}`);
connectionStatus.value = {
type: 'error',
message: `连接错误: ${error.message || '未知错误'}`,
icon: ErrorIcon
};
} finally {
testingConnection.value = false;
}
@@ -254,7 +414,7 @@ function resetConfig() {
apiConfig.value = {
apiUrl: props.defaultApiUrl || '',
apiKey: '',
apiModel: props.defaultApiModel || 'gpt-3.5-turbo',
apiModel: props.defaultApiModel || '',
apiTimeout: props.defaultApiTimeout || '60',
saveApiConfig: false
};
@@ -264,10 +424,18 @@ function resetConfig() {
localStorage.removeItem('apiConfig');
}
connectionStatus.value = null;
message.success('已重置API配置');
emit('update:apiConfig', { ...apiConfig.value });
}
// 选择模型
function selectModel(key: string) {
console.log('选择模型:', key);
apiConfig.value.apiModel = key;
handleConfigChange();
}
onMounted(() => {
// 加载保存的配置
const savedConfig = loadApiConfig();
@@ -276,7 +444,7 @@ onMounted(() => {
apiConfig.value = {
apiUrl: savedConfig.apiUrl || props.defaultApiUrl || '',
apiKey: savedConfig.apiKey || '',
apiModel: savedConfig.apiModel || props.defaultApiModel || 'gpt-3.5-turbo',
apiModel: savedConfig.apiModel || props.defaultApiModel || '',
apiTimeout: savedConfig.apiTimeout || props.defaultApiTimeout || '60',
saveApiConfig: true
};
@@ -289,25 +457,58 @@ onMounted(() => {
<style scoped>
.api-config-section {
margin-bottom: 1rem;
margin-bottom: 1.5rem;
position: relative;
}
.toggle-button {
margin-bottom: 0.5rem;
margin-bottom: 0.75rem;
font-weight: 500;
transition: all 0.3s ease;
border-radius: 16px;
padding: 4px 12px;
}
.toggle-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.toggle-text {
margin-left: 4px;
}
.api-config-card {
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
background: linear-gradient(to bottom, rgba(240, 240, 245, 0.5), rgba(250, 250, 252, 0.8));
padding: 16px;
transition: all 0.3s ease;
}
.api-info-alert {
margin-bottom: 16px;
border-radius: 8px;
}
.url-feedback {
padding: 6px 0;
}
.formatted-url {
color: var(--n-text-color-info);
font-size: 0.85rem;
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
.button-group {
display: flex;
gap: 0.75rem;
.url-tips {
color: var(--n-text-color-info);
font-size: 0.75rem;
opacity: 0.8;
line-height: 1.4;
}
.alert-actions {
@@ -319,21 +520,123 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
gap: 12px;
}
.api-save-option {
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.02);
padding: 6px 12px;
border-radius: 16px;
}
.save-label {
margin-left: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
}
.api-buttons {
display: flex;
gap: 0.75rem;
}
.timeout-input {
width: 100%;
}
.timeout-controls {
display: flex;
align-items: center;
margin-left: 8px;
}
.connection-status {
display: flex;
align-items: center;
padding: 10px 16px;
border-radius: 8px;
margin-top: 8px;
font-weight: 500;
animation: fadeIn 0.3s ease;
}
.connection-status.success {
background-color: rgba(24, 160, 88, 0.1);
color: var(--n-success-color);
}
.connection-status.error {
background-color: rgba(208, 48, 80, 0.1);
color: var(--n-error-color);
}
.status-icon {
margin-right: 8px;
font-size: 1.25rem;
}
.status-message {
font-size: 0.9rem;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.api-actions {
flex-direction: column;
align-items: flex-start;
}
.api-buttons {
width: 100%;
justify-content: space-between;
}
}
.model-suggestions {
margin-top: 6px;
font-size: 0.75rem;
color: var(--n-text-color-3);
}
.model-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.model-chips :deep(.n-tag) {
cursor: pointer;
transition: all 0.2s ease;
}
.model-chips :deep(.n-tag:hover) {
background-color: rgba(32, 128, 240, 0.1);
transform: translateY(-1px);
}
.model-tip {
margin-bottom: 6px;
font-size: 0.75rem;
color: var(--n-text-color-3);
font-style: italic;
}
.model-dropdown-btn {
background-color: rgba(32, 128, 240, 0.1);
transition: all 0.2s ease;
}
.model-dropdown-btn:hover {
background-color: rgba(32, 128, 240, 0.2);
transform: translateY(-1px);
}
</style>