mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor(frontend): 统一 routing 数据加载,消除子组件重复请求
将 Model 和 Provider 详情抽屉中的 routing/mapping 数据加载逻辑 从各子组件(RoutingTab、ModelMappingsTab、ModelsTab、ModelMappingTab) 提升到父组件统一管理,通过 props 向下传递数据,避免多个子组件独立发起相同的 API 请求,提升页面加载性能。
This commit is contained in:
@@ -408,6 +408,10 @@
|
||||
v-if="model"
|
||||
ref="routingTabRef"
|
||||
:global-model-id="model.id"
|
||||
:routing-data="routingData"
|
||||
:loading="routingLoading"
|
||||
:error="routingError"
|
||||
@refresh="loadRoutingData"
|
||||
@add-provider="$emit('addProvider')"
|
||||
@edit-provider="handleEditProviderFromRouting"
|
||||
@toggle-provider-status="handleToggleProviderFromRouting"
|
||||
@@ -423,7 +427,10 @@
|
||||
:global-model-id="model.id"
|
||||
:model-name="model.name"
|
||||
:mappings="model.config?.model_mappings || []"
|
||||
:routing-data="routingData"
|
||||
:loading-preview="routingLoading"
|
||||
@update="handleMappingsUpdate"
|
||||
@refresh="loadRoutingData"
|
||||
@link-provider="(providerId) => $emit('linkProvider', providerId)"
|
||||
@link-providers="(providerIds) => $emit('linkProviders', providerIds)"
|
||||
/>
|
||||
@@ -462,10 +469,11 @@ import TableCell from '@/components/ui/table-cell.vue'
|
||||
import RoutingTab from './RoutingTab.vue'
|
||||
import ModelMappingsTab from './ModelMappingsTab.vue'
|
||||
import { sortResolutionEntries } from '@/utils/form'
|
||||
import { getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||
|
||||
// 使用外部类型定义
|
||||
import type { GlobalModelResponse } from '@/api/global-models'
|
||||
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
||||
import type { TieredPricingConfig, PricingTier, ModelRoutingPreviewResponse } from '@/api/endpoints/types'
|
||||
import type { CapabilityDefinition } from '@/api/endpoints'
|
||||
import type { RoutingProviderInfo } from '@/api/global-models'
|
||||
|
||||
@@ -498,6 +506,27 @@ const routingTabRef = ref<InstanceType<typeof RoutingTab> | null>(null)
|
||||
// ModelMappingsTab 引用
|
||||
const modelMappingsTabRef = ref<InstanceType<typeof ModelMappingsTab> | null>(null)
|
||||
|
||||
// 统一管理 routing 数据,避免子组件重复请求
|
||||
const routingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||
const routingLoading = ref(false)
|
||||
const routingError = ref<string | null>(null)
|
||||
|
||||
// 加载 routing 数据(统一入口)
|
||||
async function loadRoutingData() {
|
||||
if (!props.model?.id) return
|
||||
|
||||
routingLoading.value = true
|
||||
routingError.value = null
|
||||
|
||||
try {
|
||||
routingData.value = await getGlobalModelRoutingPreview(props.model.id)
|
||||
} catch (err: any) {
|
||||
routingError.value = err.response?.data?.detail || '加载失败'
|
||||
} finally {
|
||||
routingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 将 RoutingProviderInfo 转换为父组件期望的格式
|
||||
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
|
||||
return {
|
||||
@@ -525,8 +554,7 @@ function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) {
|
||||
|
||||
// 刷新路由数据
|
||||
function refreshRoutingData() {
|
||||
routingTabRef.value?.loadRoutingData?.()
|
||||
modelMappingsTabRef.value?.refresh?.()
|
||||
loadRoutingData()
|
||||
}
|
||||
|
||||
// 处理模型映射更新
|
||||
@@ -632,11 +660,17 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
|
||||
return get1hCachePrice(tieredPricing.tiers[0])
|
||||
}
|
||||
|
||||
// 监听 open 变化,重置 tab
|
||||
// 监听 open 变化,重置 tab 并加载数据
|
||||
watch(() => props.open, (newOpen) => {
|
||||
if (newOpen) {
|
||||
// 直接设置为 basic,不需要先重置为空
|
||||
detailTab.value = 'basic'
|
||||
// 加载 routing 数据
|
||||
loadRoutingData()
|
||||
} else {
|
||||
// 关闭时清空数据
|
||||
routingData.value = null
|
||||
routingError.value = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, watch, onUnmounted, computed } from 'vue'
|
||||
import { Card, Button, Input, Badge } from '@/components/ui'
|
||||
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next'
|
||||
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||
@@ -241,6 +241,8 @@ const props = defineProps<{
|
||||
modelName: string
|
||||
mappings: string[]
|
||||
loading?: boolean
|
||||
routingData?: ModelRoutingPreviewResponse | null // 外部传入的 routing 数据
|
||||
loadingPreview?: boolean // 外部传入的加载状态
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
update: [mappings: string[]]
|
||||
@@ -267,9 +269,13 @@ const mappingValidations = computed<ValidationResult[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 匹配预览状态
|
||||
const loadingPreview = ref(false)
|
||||
const routingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||
// 匹配预览状态 - 优先使用外部传入的数据
|
||||
const internalLoadingPreview = ref(false)
|
||||
const internalRoutingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||
|
||||
// 计算属性:优先使用外部传入的数据
|
||||
const loadingPreview = computed(() => props.loadingPreview ?? internalLoadingPreview.value)
|
||||
const routingData = computed(() => props.routingData ?? internalRoutingData.value)
|
||||
|
||||
const REGEX_CACHE_MAX_SIZE = 100
|
||||
const regexCache = createLRURegexCache(REGEX_CACHE_MAX_SIZE)
|
||||
@@ -542,22 +548,31 @@ async function saveMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据(通知父组件刷新,或在无外部数据时自行加载)
|
||||
async function loadMatchPreview() {
|
||||
// 如果有外部传入的数据,通知父组件刷新
|
||||
if (props.routingData !== undefined) {
|
||||
emit('refresh')
|
||||
return
|
||||
}
|
||||
|
||||
// 清空正则缓存,确保使用最新数据
|
||||
regexCache.clear()
|
||||
matchCountCache.clear()
|
||||
loadingPreview.value = true
|
||||
internalLoadingPreview.value = true
|
||||
try {
|
||||
routingData.value = await getGlobalModelRoutingPreview(props.globalModelId)
|
||||
internalRoutingData.value = await getGlobalModelRoutingPreview(props.globalModelId)
|
||||
} catch (err) {
|
||||
log.error('加载匹配预览失败:', err)
|
||||
} finally {
|
||||
loadingPreview.value = false
|
||||
internalLoadingPreview.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMatchPreview()
|
||||
// 监听外部 routingData 变化,清空缓存
|
||||
watch(() => props.routingData, () => {
|
||||
regexCache.clear()
|
||||
matchCountCache.clear()
|
||||
})
|
||||
|
||||
// 组件卸载时清理缓存,防止内存泄漏
|
||||
|
||||
@@ -611,6 +611,9 @@ import { MAX_MODEL_NAME_LENGTH, createLRURegexCache, getCompiledModelMappingRege
|
||||
|
||||
const props = defineProps<{
|
||||
globalModelId: string
|
||||
routingData?: ModelRoutingPreviewResponse | null
|
||||
loading?: boolean
|
||||
error?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -624,9 +627,16 @@ const emit = defineEmits<{
|
||||
const { success: showSuccess, error: showError } = useToast()
|
||||
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const routingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||
// 使用外部传入的数据或内部状态
|
||||
const internalRoutingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||
const internalLoading = ref(false)
|
||||
const internalError = ref<string | null>(null)
|
||||
|
||||
// 计算属性:优先使用外部传入的数据
|
||||
const routingData = computed(() => props.routingData ?? internalRoutingData.value)
|
||||
const loading = computed(() => props.loading ?? internalLoading.value)
|
||||
const error = computed(() => props.error ?? internalError.value)
|
||||
|
||||
const modelMappingRegexCache = createLRURegexCache(200)
|
||||
const keyMatchedModelsCache = new Map<string, string[]>()
|
||||
const compiledGlobalModelMappingRegexes = ref<RegExp[]>([])
|
||||
@@ -808,16 +818,22 @@ function toggleProviderInFormat(format: string, providerId: string, endpointId?:
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
// 加载数据(仅在没有外部数据时使用)
|
||||
async function loadRoutingData() {
|
||||
// 如果有外部传入的数据,通知父组件刷新
|
||||
if (props.routingData !== undefined) {
|
||||
emit('refresh')
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.globalModelId) return
|
||||
|
||||
modelMappingRegexCache.clear()
|
||||
keyMatchedModelsCache.clear()
|
||||
compiledGlobalModelMappingRegexes.value = []
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
internalLoading.value = true
|
||||
internalError.value = null
|
||||
|
||||
try {
|
||||
const data = await getGlobalModelRoutingPreview(props.globalModelId)
|
||||
@@ -828,15 +844,30 @@ async function loadRoutingData() {
|
||||
if (regex) compiled.push(regex)
|
||||
}
|
||||
|
||||
routingData.value = data
|
||||
internalRoutingData.value = data
|
||||
compiledGlobalModelMappingRegexes.value = compiled
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '加载失败'
|
||||
internalError.value = err.response?.data?.detail || '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
internalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部 routingData 变化,更新编译后的正则
|
||||
watch(() => props.routingData, (data) => {
|
||||
if (data) {
|
||||
modelMappingRegexCache.clear()
|
||||
keyMatchedModelsCache.clear()
|
||||
|
||||
const compiled: RegExp[] = []
|
||||
for (const pattern of data.global_model_mappings || []) {
|
||||
const regex = getCompiledModelMappingRegex(pattern, modelMappingRegexCache)
|
||||
if (regex) compiled.push(regex)
|
||||
}
|
||||
compiledGlobalModelMappingRegexes.value = compiled
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 获取调度模式标签
|
||||
function getSchedulingModeLabel(mode: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
@@ -1086,9 +1117,9 @@ function getKeyProbeCountdown(key: RoutingKeyInfo): string {
|
||||
async function handleRecoverKey(keyId: string, apiFormat: string) {
|
||||
try {
|
||||
const result = await recoverKeyHealth(keyId, apiFormat)
|
||||
await loadRoutingData()
|
||||
showSuccess(result.message || 'Key 已恢复')
|
||||
// 通知父组件刷新数据
|
||||
emit('refresh')
|
||||
showSuccess(result.message || 'Key 已恢复')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Key 恢复失败', '错误')
|
||||
}
|
||||
@@ -1098,11 +1129,17 @@ async function handleRecoverKey(keyId: string, apiFormat: string) {
|
||||
watch(() => props.globalModelId, () => {
|
||||
expandedFormats.value.clear()
|
||||
expandedProvidersInFormat.value.clear()
|
||||
loadRoutingData()
|
||||
// 如果没有外部数据,才自己加载
|
||||
if (props.routingData === undefined) {
|
||||
loadRoutingData()
|
||||
}
|
||||
}, { immediate: false })
|
||||
|
||||
onMounted(() => {
|
||||
loadRoutingData()
|
||||
// 如果没有外部数据,才自己加载
|
||||
if (props.routingData === undefined) {
|
||||
loadRoutingData()
|
||||
}
|
||||
startCountdownTimer()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user