|
|
|
@ -35,13 +35,12 @@ |
|
|
|
<div v-if="fallbackChains.length > 0" class="fallback-section"> |
|
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;"> |
|
|
|
<span style="font-size:13px;font-weight:600;color:var(--color-info-text-strong);">🔗 Fallback 链(按优先级排列)</span> |
|
|
|
<t-tag size="small" variant="light" theme="primary" style="cursor:pointer;" @click="showFallbackHelp = !showFallbackHelp">❓ 帮助</t-tag> |
|
|
|
</div> |
|
|
|
<div v-if="showFallbackHelp" class="fallback-help"> |
|
|
|
<div style="font-weight:600;color:var(--color-info-text-strong);margin-bottom:8px;">📖 Fallback 链使用说明</div> |
|
|
|
<p>Fallback 链是<span style="color:var(--color-success-text-strong);font-weight:600;">高可用性机制</span>,主模型故障时自动切换到备用模型。</p> |
|
|
|
<p>✔ 勾选「设为活跃配置」启用 | 📊 调整「优先级」数值(越大越优先)| 🔀 拖拽列表项快速调整</p> |
|
|
|
<t-button size="small" variant="text" @click="showFallbackHelp = !showFallbackHelp">❓ 帮助</t-button> |
|
|
|
</div> |
|
|
|
<t-alert v-if="showFallbackHelp" theme="info" title="📖 Fallback 链使用说明" style="margin:0 0 8px;"> |
|
|
|
<p style="margin:4px 0;">Fallback 链是<span style="color:var(--color-success-text-strong);font-weight:600;">高可用性机制</span>,主模型故障时自动切换到备用模型。</p> |
|
|
|
<p style="margin:4px 0;">✔ 勾选「设为活跃配置」启用 | 📊 调整「优先级」数值(越大越优先)| 🔀 拖拽列表项快速调整</p> |
|
|
|
</t-alert> |
|
|
|
<div v-for="chain in fallbackChains" :key="chain.appType" style="margin-bottom:8px;"> |
|
|
|
<div style="font-size:12px;color:var(--color-info-text-strong);font-weight:600;">{{ getAppTypeLabel(chain.appType) }}</div> |
|
|
|
<div v-for="(item, idx) in chain.configs" :key="item.id" |
|
|
|
@ -96,7 +95,7 @@ |
|
|
|
<template #op="{ row }"> |
|
|
|
<t-space :size="4"> |
|
|
|
<t-button size="small" variant="text" @click="openEditModal(row)" title="编辑">✏️</t-button> |
|
|
|
<t-button size="small" variant="text" @click="testConnection(row)" :disabled="testLoading[row.id]" title="测试连接">{{ testLoading[row.id] ? '⏳' : '🔌' }}</t-button> |
|
|
|
<t-button size="small" variant="text" @click="testConnection(row)" :loading="testLoading[row.id]" title="测试连接">测试</t-button> |
|
|
|
<t-button size="small" variant="text" @click="duplicateConfig(row)" title="复制">📋</t-button> |
|
|
|
<t-button v-show="!row.is_active" size="small" variant="text" theme="primary" @click="activate(row.id)" title="激活">▶️</t-button> |
|
|
|
<t-button v-show="row.is_active" size="small" variant="text" @click="deactivateConfig(row.id)" title="停用">⏸️</t-button> |
|
|
|
@ -105,15 +104,6 @@ |
|
|
|
</template> |
|
|
|
</BaseTable> |
|
|
|
|
|
|
|
<!-- F1: 测试结果提示 --> |
|
|
|
<div v-if="testResult.visible" class="test-result-toast" |
|
|
|
:style="{ position: 'fixed', bottom: '20px', right: '20px', padding: '12px 20px', borderRadius: '8px', zIndex: 9999, color: 'white', maxWidth: '400px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', |
|
|
|
background: testResult.success ? 'var(--color-success-text)' : 'var(--color-error-text)' }"> |
|
|
|
<div style="font-weight:600;">{{ testResult.success ? '✅ 连接成功' : '❌ 连接失败' }}</div> |
|
|
|
<div style="font-size:12px;margin-top:4px;">延迟:{{ testResult.latencyMs }}ms<span v-if="testResult.dimensions"> · 维度:{{ testResult.dimensions }}</span></div> |
|
|
|
<div v-if="testResult.errorMessage" style="font-size:11px;margin-top:4px;opacity:0.9;">{{ testResult.errorMessage }}</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- ==================== 编辑/新建弹窗 ==================== --> |
|
|
|
<t-dialog |
|
|
|
v-model:visible="editModal.visible" |
|
|
|
@ -168,9 +158,9 @@ |
|
|
|
@click="editModal.form.model_name = ''; modelDropdownOpen = false; updateNameSuggestion()" style="color:var(--color-text-tertiary);">✕</t-button> |
|
|
|
</template> |
|
|
|
</t-input> |
|
|
|
<t-button variant="outline" size="small" @click="fetchModelsFromApi" :disabled="fetchLoading" |
|
|
|
<t-button variant="outline" size="small" @click="fetchModelsFromApi" :loading="fetchLoading" |
|
|
|
style="border-color:var(--td-brand-color);color:var(--td-brand-color);white-space:nowrap;"> |
|
|
|
{{ fetchLoading ? '⏳ 获取中...' : '🔍 获取模型' }} |
|
|
|
获取模型 |
|
|
|
</t-button> |
|
|
|
</t-space> |
|
|
|
<!-- 自定义下拉列表 --> |
|
|
|
@ -264,8 +254,8 @@ |
|
|
|
<div class="dialog-footer" style="gap:10px;margin-top:20px;"> |
|
|
|
<t-button variant="outline" @click="editModal.visible = false">取消</t-button> |
|
|
|
<t-button v-if="editModal.mode === 'edit'" variant="outline" @click="testConnectionById(editModal.editId!)" |
|
|
|
:disabled="testLoading[editModal.editId!]" style="border-color:var(--td-brand-color);color:var(--td-brand-color);"> |
|
|
|
{{ testLoading[editModal.editId!] ? '⏳ 测试中...' : '🔌 测试连接' }} |
|
|
|
:loading="testLoading[editModal.editId!]" style="border-color:var(--td-brand-color);color:var(--td-brand-color);"> |
|
|
|
测试连接 |
|
|
|
</t-button> |
|
|
|
<t-button theme="primary" @click="saveConfig">💾 保存</t-button> |
|
|
|
</div> |
|
|
|
@ -298,8 +288,7 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue' |
|
|
|
import { MessagePlugin } from 'tdesign-vue-next' |
|
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue' |
|
|
|
import * as api from '@/api/model-config' |
|
|
|
import { toast } from '@/utils/toast' |
|
|
|
import { useConfirm } from '@/composables/useConfirm' |
|
|
|
@ -396,8 +385,6 @@ let healthTimer: any = null |
|
|
|
|
|
|
|
// 测试 |
|
|
|
const testLoading = ref<Record<string, boolean>>({}) |
|
|
|
const testResult = reactive({ visible: false, success: false, latencyMs: 0, dimensions: null as any, errorMessage: null as any }) |
|
|
|
let testResultTimer: any = null |
|
|
|
|
|
|
|
// Fallback 链 |
|
|
|
const fallbackChains = ref<any[]>([]) |
|
|
|
@ -645,9 +632,14 @@ async function testConnectionById(id: string) { |
|
|
|
} |
|
|
|
|
|
|
|
function showTestResult(r: any) { |
|
|
|
if (testResultTimer) clearTimeout(testResultTimer) |
|
|
|
testResult.visible = true; testResult.success = r.success; testResult.latencyMs = r.latencyMs || 0; testResult.dimensions = r.dimensions || null; testResult.errorMessage = r.errorMessage || r.message || null |
|
|
|
testResultTimer = setTimeout(() => { testResult.visible = false }, 5000) |
|
|
|
if (r.success) { |
|
|
|
let msg = '连接成功' |
|
|
|
if (r.latencyMs) msg += '(延迟 ' + r.latencyMs + 'ms' + (r.dimensions ? ' · 维度 ' + r.dimensions : '') + ')' |
|
|
|
toast(msg, 'success') |
|
|
|
} else { |
|
|
|
const reason = r.errorMessage || r.message |
|
|
|
toast('连接失败' + (reason ? ':' + reason : ''), 'error') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 复制/激活/停用/删除 ==================== |
|
|
|
@ -709,11 +701,9 @@ function getProviderBaseUrl(p: string) { return PROVIDER_BASE_URLS[p] || '' } |
|
|
|
|
|
|
|
// ==================== 生命周期 ==================== |
|
|
|
onMounted(() => { load(); healthTimer = setInterval(loadHealthStatus, 30000); document.addEventListener('click', onDocumentClick, true) }) |
|
|
|
onUnmounted(() => { clearInterval(healthTimer); clearTimeout(testResultTimer); document.removeEventListener('click', onDocumentClick, true) }) |
|
|
|
onUnmounted(() => { clearInterval(healthTimer); document.removeEventListener('click', onDocumentClick, true) }) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
.fallback-section { margin:12px 0; padding:12px; background:var(--color-info-bg); border:1px solid var(--color-info-border); border-radius:8px; } |
|
|
|
.fallback-help { margin-bottom:12px; padding:12px; background:white; border:1px solid var(--color-info-border); border-radius:6px; font-size:12px; line-height:1.6; color:var(--color-text-primary); } |
|
|
|
.fallback-help p { margin:4px 0; } |
|
|
|
</style> |