mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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"
|
v-if="model"
|
||||||
ref="routingTabRef"
|
ref="routingTabRef"
|
||||||
:global-model-id="model.id"
|
:global-model-id="model.id"
|
||||||
|
:routing-data="routingData"
|
||||||
|
:loading="routingLoading"
|
||||||
|
:error="routingError"
|
||||||
|
@refresh="loadRoutingData"
|
||||||
@add-provider="$emit('addProvider')"
|
@add-provider="$emit('addProvider')"
|
||||||
@edit-provider="handleEditProviderFromRouting"
|
@edit-provider="handleEditProviderFromRouting"
|
||||||
@toggle-provider-status="handleToggleProviderFromRouting"
|
@toggle-provider-status="handleToggleProviderFromRouting"
|
||||||
@@ -423,7 +427,10 @@
|
|||||||
:global-model-id="model.id"
|
:global-model-id="model.id"
|
||||||
:model-name="model.name"
|
:model-name="model.name"
|
||||||
:mappings="model.config?.model_mappings || []"
|
:mappings="model.config?.model_mappings || []"
|
||||||
|
:routing-data="routingData"
|
||||||
|
:loading-preview="routingLoading"
|
||||||
@update="handleMappingsUpdate"
|
@update="handleMappingsUpdate"
|
||||||
|
@refresh="loadRoutingData"
|
||||||
@link-provider="(providerId) => $emit('linkProvider', providerId)"
|
@link-provider="(providerId) => $emit('linkProvider', providerId)"
|
||||||
@link-providers="(providerIds) => $emit('linkProviders', providerIds)"
|
@link-providers="(providerIds) => $emit('linkProviders', providerIds)"
|
||||||
/>
|
/>
|
||||||
@@ -462,10 +469,11 @@ import TableCell from '@/components/ui/table-cell.vue'
|
|||||||
import RoutingTab from './RoutingTab.vue'
|
import RoutingTab from './RoutingTab.vue'
|
||||||
import ModelMappingsTab from './ModelMappingsTab.vue'
|
import ModelMappingsTab from './ModelMappingsTab.vue'
|
||||||
import { sortResolutionEntries } from '@/utils/form'
|
import { sortResolutionEntries } from '@/utils/form'
|
||||||
|
import { getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||||
|
|
||||||
// 使用外部类型定义
|
// 使用外部类型定义
|
||||||
import type { GlobalModelResponse } 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 { CapabilityDefinition } from '@/api/endpoints'
|
||||||
import type { RoutingProviderInfo } from '@/api/global-models'
|
import type { RoutingProviderInfo } from '@/api/global-models'
|
||||||
|
|
||||||
@@ -498,6 +506,27 @@ const routingTabRef = ref<InstanceType<typeof RoutingTab> | null>(null)
|
|||||||
// ModelMappingsTab 引用
|
// ModelMappingsTab 引用
|
||||||
const modelMappingsTabRef = ref<InstanceType<typeof ModelMappingsTab> | null>(null)
|
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 转换为父组件期望的格式
|
// 将 RoutingProviderInfo 转换为父组件期望的格式
|
||||||
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
|
function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) {
|
||||||
return {
|
return {
|
||||||
@@ -525,8 +554,7 @@ function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) {
|
|||||||
|
|
||||||
// 刷新路由数据
|
// 刷新路由数据
|
||||||
function refreshRoutingData() {
|
function refreshRoutingData() {
|
||||||
routingTabRef.value?.loadRoutingData?.()
|
loadRoutingData()
|
||||||
modelMappingsTabRef.value?.refresh?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理模型映射更新
|
// 处理模型映射更新
|
||||||
@@ -632,11 +660,17 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
|
|||||||
return get1hCachePrice(tieredPricing.tiers[0])
|
return get1hCachePrice(tieredPricing.tiers[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听 open 变化,重置 tab
|
// 监听 open 变化,重置 tab 并加载数据
|
||||||
watch(() => props.open, (newOpen) => {
|
watch(() => props.open, (newOpen) => {
|
||||||
if (newOpen) {
|
if (newOpen) {
|
||||||
// 直接设置为 basic,不需要先重置为空
|
// 直接设置为 basic,不需要先重置为空
|
||||||
detailTab.value = 'basic'
|
detailTab.value = 'basic'
|
||||||
|
// 加载 routing 数据
|
||||||
|
loadRoutingData()
|
||||||
|
} else {
|
||||||
|
// 关闭时清空数据
|
||||||
|
routingData.value = null
|
||||||
|
routingError.value = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { Card, Button, Input, Badge } from '@/components/ui'
|
||||||
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next'
|
import { Plus, Trash2, GitMerge, RefreshCw, ChevronRight, Save, AlertCircle, Link } from 'lucide-vue-next'
|
||||||
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
|
import { updateGlobalModel, getGlobalModel, getGlobalModelRoutingPreview } from '@/api/global-models'
|
||||||
@@ -241,6 +241,8 @@ const props = defineProps<{
|
|||||||
modelName: string
|
modelName: string
|
||||||
mappings: string[]
|
mappings: string[]
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
routingData?: ModelRoutingPreviewResponse | null // 外部传入的 routing 数据
|
||||||
|
loadingPreview?: boolean // 外部传入的加载状态
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
update: [mappings: string[]]
|
update: [mappings: string[]]
|
||||||
@@ -267,9 +269,13 @@ const mappingValidations = computed<ValidationResult[]>(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 匹配预览状态
|
// 匹配预览状态 - 优先使用外部传入的数据
|
||||||
const loadingPreview = ref(false)
|
const internalLoadingPreview = ref(false)
|
||||||
const routingData = ref<ModelRoutingPreviewResponse | null>(null)
|
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 REGEX_CACHE_MAX_SIZE = 100
|
||||||
const regexCache = createLRURegexCache(REGEX_CACHE_MAX_SIZE)
|
const regexCache = createLRURegexCache(REGEX_CACHE_MAX_SIZE)
|
||||||
@@ -542,22 +548,31 @@ async function saveMappings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新数据(通知父组件刷新,或在无外部数据时自行加载)
|
||||||
async function loadMatchPreview() {
|
async function loadMatchPreview() {
|
||||||
|
// 如果有外部传入的数据,通知父组件刷新
|
||||||
|
if (props.routingData !== undefined) {
|
||||||
|
emit('refresh')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 清空正则缓存,确保使用最新数据
|
// 清空正则缓存,确保使用最新数据
|
||||||
regexCache.clear()
|
regexCache.clear()
|
||||||
matchCountCache.clear()
|
matchCountCache.clear()
|
||||||
loadingPreview.value = true
|
internalLoadingPreview.value = true
|
||||||
try {
|
try {
|
||||||
routingData.value = await getGlobalModelRoutingPreview(props.globalModelId)
|
internalRoutingData.value = await getGlobalModelRoutingPreview(props.globalModelId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('加载匹配预览失败:', err)
|
log.error('加载匹配预览失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
loadingPreview.value = false
|
internalLoadingPreview.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 监听外部 routingData 变化,清空缓存
|
||||||
loadMatchPreview()
|
watch(() => props.routingData, () => {
|
||||||
|
regexCache.clear()
|
||||||
|
matchCountCache.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件卸载时清理缓存,防止内存泄漏
|
// 组件卸载时清理缓存,防止内存泄漏
|
||||||
|
|||||||
@@ -611,6 +611,9 @@ import { MAX_MODEL_NAME_LENGTH, createLRURegexCache, getCompiledModelMappingRege
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
globalModelId: string
|
globalModelId: string
|
||||||
|
routingData?: ModelRoutingPreviewResponse | null
|
||||||
|
loading?: boolean
|
||||||
|
error?: string | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -624,9 +627,16 @@ const emit = defineEmits<{
|
|||||||
const { success: showSuccess, error: showError } = useToast()
|
const { success: showSuccess, error: showError } = useToast()
|
||||||
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
|
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
|
||||||
|
|
||||||
const loading = ref(false)
|
// 使用外部传入的数据或内部状态
|
||||||
const error = ref<string | null>(null)
|
const internalRoutingData = ref<ModelRoutingPreviewResponse | null>(null)
|
||||||
const routingData = 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 modelMappingRegexCache = createLRURegexCache(200)
|
||||||
const keyMatchedModelsCache = new Map<string, string[]>()
|
const keyMatchedModelsCache = new Map<string, string[]>()
|
||||||
const compiledGlobalModelMappingRegexes = ref<RegExp[]>([])
|
const compiledGlobalModelMappingRegexes = ref<RegExp[]>([])
|
||||||
@@ -808,16 +818,22 @@ function toggleProviderInFormat(format: string, providerId: string, endpointId?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据(仅在没有外部数据时使用)
|
||||||
async function loadRoutingData() {
|
async function loadRoutingData() {
|
||||||
|
// 如果有外部传入的数据,通知父组件刷新
|
||||||
|
if (props.routingData !== undefined) {
|
||||||
|
emit('refresh')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.globalModelId) return
|
if (!props.globalModelId) return
|
||||||
|
|
||||||
modelMappingRegexCache.clear()
|
modelMappingRegexCache.clear()
|
||||||
keyMatchedModelsCache.clear()
|
keyMatchedModelsCache.clear()
|
||||||
compiledGlobalModelMappingRegexes.value = []
|
compiledGlobalModelMappingRegexes.value = []
|
||||||
|
|
||||||
loading.value = true
|
internalLoading.value = true
|
||||||
error.value = null
|
internalError.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getGlobalModelRoutingPreview(props.globalModelId)
|
const data = await getGlobalModelRoutingPreview(props.globalModelId)
|
||||||
@@ -828,15 +844,30 @@ async function loadRoutingData() {
|
|||||||
if (regex) compiled.push(regex)
|
if (regex) compiled.push(regex)
|
||||||
}
|
}
|
||||||
|
|
||||||
routingData.value = data
|
internalRoutingData.value = data
|
||||||
compiledGlobalModelMappingRegexes.value = compiled
|
compiledGlobalModelMappingRegexes.value = compiled
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.response?.data?.detail || '加载失败'
|
internalError.value = err.response?.data?.detail || '加载失败'
|
||||||
} finally {
|
} 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 {
|
function getSchedulingModeLabel(mode: string): string {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
@@ -1086,9 +1117,9 @@ function getKeyProbeCountdown(key: RoutingKeyInfo): string {
|
|||||||
async function handleRecoverKey(keyId: string, apiFormat: string) {
|
async function handleRecoverKey(keyId: string, apiFormat: string) {
|
||||||
try {
|
try {
|
||||||
const result = await recoverKeyHealth(keyId, apiFormat)
|
const result = await recoverKeyHealth(keyId, apiFormat)
|
||||||
await loadRoutingData()
|
// 通知父组件刷新数据
|
||||||
showSuccess(result.message || 'Key 已恢复')
|
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
|
showSuccess(result.message || 'Key 已恢复')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(err.response?.data?.detail || 'Key 恢复失败', '错误')
|
showError(err.response?.data?.detail || 'Key 恢复失败', '错误')
|
||||||
}
|
}
|
||||||
@@ -1098,11 +1129,17 @@ async function handleRecoverKey(keyId: string, apiFormat: string) {
|
|||||||
watch(() => props.globalModelId, () => {
|
watch(() => props.globalModelId, () => {
|
||||||
expandedFormats.value.clear()
|
expandedFormats.value.clear()
|
||||||
expandedProvidersInFormat.value.clear()
|
expandedProvidersInFormat.value.clear()
|
||||||
loadRoutingData()
|
// 如果没有外部数据,才自己加载
|
||||||
|
if (props.routingData === undefined) {
|
||||||
|
loadRoutingData()
|
||||||
|
}
|
||||||
}, { immediate: false })
|
}, { immediate: false })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadRoutingData()
|
// 如果没有外部数据,才自己加载
|
||||||
|
if (props.routingData === undefined) {
|
||||||
|
loadRoutingData()
|
||||||
|
}
|
||||||
startCountdownTimer()
|
startCountdownTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -876,8 +876,11 @@
|
|||||||
:key="`models-${provider.id}`"
|
:key="`models-${provider.id}`"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
|
:models="providerModels"
|
||||||
|
:mapping-preview="providerMappingPreview"
|
||||||
@edit-model="handleEditModel"
|
@edit-model="handleEditModel"
|
||||||
@batch-assign="handleBatchAssign"
|
@batch-assign="handleBatchAssign"
|
||||||
|
@refresh="loadEndpoints"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 模型映射 -->
|
<!-- 模型映射 -->
|
||||||
@@ -887,6 +890,9 @@
|
|||||||
:key="`mapping-${provider.id}`"
|
:key="`mapping-${provider.id}`"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:provider-keys="providerKeys"
|
:provider-keys="providerKeys"
|
||||||
|
:models="providerModels"
|
||||||
|
:mapping-preview="providerMappingPreview"
|
||||||
|
:endpoints="endpoints"
|
||||||
@refresh="handleModelMappingChanged"
|
@refresh="handleModelMappingChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1027,7 +1033,14 @@ import { useToast } from '@/composables/useToast'
|
|||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||||
import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints'
|
import {
|
||||||
|
getProvider,
|
||||||
|
getProviderEndpoints,
|
||||||
|
updateProvider,
|
||||||
|
getProviderModels,
|
||||||
|
getProviderMappingPreview,
|
||||||
|
type ProviderMappingPreviewResponse
|
||||||
|
} from '@/api/endpoints'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import {
|
import {
|
||||||
KeyFormDialog,
|
KeyFormDialog,
|
||||||
@@ -1092,6 +1105,8 @@ const loading = ref(false)
|
|||||||
const provider = ref<any>(null)
|
const provider = ref<any>(null)
|
||||||
const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
||||||
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
||||||
|
const providerModels = ref<Model[]>([]) // Provider 级别的 models
|
||||||
|
const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览
|
||||||
|
|
||||||
// 系统级格式转换配置
|
// 系统级格式转换配置
|
||||||
const systemFormatConversionEnabled = ref(false)
|
const systemFormatConversionEnabled = ref(false)
|
||||||
@@ -2539,13 +2554,17 @@ async function loadEndpoints() {
|
|||||||
if (!props.providerId) return
|
if (!props.providerId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 并行加载端点列表和 Provider 级别的 keys
|
// 并行加载端点列表、Provider 级别的 keys、models 和映射预览
|
||||||
const [endpointsList, providerKeysResult] = await Promise.all([
|
const [endpointsList, providerKeysResult, modelsResult, mappingPreviewResult] = await Promise.all([
|
||||||
getProviderEndpoints(props.providerId),
|
getProviderEndpoints(props.providerId),
|
||||||
getProviderKeys(props.providerId).catch(() => []),
|
getProviderKeys(props.providerId).catch(() => []),
|
||||||
|
getProviderModels(props.providerId).catch(() => []),
|
||||||
|
getProviderMappingPreview(props.providerId).catch(() => null),
|
||||||
])
|
])
|
||||||
|
|
||||||
providerKeys.value = providerKeysResult
|
providerKeys.value = providerKeysResult
|
||||||
|
providerModels.value = modelsResult
|
||||||
|
providerMappingPreview.value = mappingPreviewResult
|
||||||
// 按 API 格式排序
|
// 按 API 格式排序
|
||||||
endpoints.value = endpointsList.sort((a, b) => {
|
endpoints.value = endpointsList.sort((a, b) => {
|
||||||
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
||||||
|
|||||||
@@ -343,7 +343,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useSmartPagination } from '@/composables/useSmartPagination'
|
import { useSmartPagination } from '@/composables/useSmartPagination'
|
||||||
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
import { Tag, Plus, Edit, Trash2, ChevronRight, Loader2, Play } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
@@ -354,15 +354,12 @@ import AlertDialog from '@/components/common/AlertDialog.vue'
|
|||||||
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import {
|
import {
|
||||||
getProviderModels,
|
|
||||||
getProviderMappingPreview,
|
|
||||||
testModel,
|
testModel,
|
||||||
type Model,
|
type Model,
|
||||||
type ProviderModelAlias,
|
type ProviderModelAlias,
|
||||||
type ProviderMappingPreviewResponse
|
type ProviderMappingPreviewResponse
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
import { getProviderEndpoints } from '@/api/endpoints/endpoints'
|
import { type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||||
import { getProviderKeys, type EndpointAPIKey } from '@/api/endpoints/keys'
|
|
||||||
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
||||||
import { updateModel } from '@/api/endpoints/models'
|
import { updateModel } from '@/api/endpoints/models'
|
||||||
import { parseTestModelError } from '@/utils/errorParser'
|
import { parseTestModelError } from '@/utils/errorParser'
|
||||||
@@ -394,6 +391,9 @@ interface CombinedMapping {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
provider: any
|
provider: any
|
||||||
providerKeys?: EndpointAPIKey[]
|
providerKeys?: EndpointAPIKey[]
|
||||||
|
models?: Model[]
|
||||||
|
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||||
|
endpoints?: ProviderEndpoint[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -404,8 +404,6 @@ const { error: showError, success: showSuccess } = useToast()
|
|||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const models = ref<Model[]>([])
|
|
||||||
const aliasMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
|
||||||
const dialogOpen = ref(false)
|
const dialogOpen = ref(false)
|
||||||
const deleteConfirmOpen = ref(false)
|
const deleteConfirmOpen = ref(false)
|
||||||
const editingGroup = ref<AliasGroup | null>(null)
|
const editingGroup = ref<AliasGroup | null>(null)
|
||||||
@@ -413,22 +411,21 @@ const deletingGroup = ref<AliasGroup | null>(null)
|
|||||||
const testingMapping = ref<string | null>(null)
|
const testingMapping = ref<string | null>(null)
|
||||||
const preselectedModelId = ref<string | null>(null)
|
const preselectedModelId = ref<string | null>(null)
|
||||||
|
|
||||||
// 端点数据(用于测试格式选择)
|
|
||||||
const providerEndpoints = ref<ProviderEndpoint[]>([])
|
|
||||||
|
|
||||||
// Key 数据(用于判断支持的格式)
|
|
||||||
const providerKeysState = ref<EndpointAPIKey[]>([])
|
|
||||||
|
|
||||||
// 测试下拉菜单状态
|
// 测试下拉菜单状态
|
||||||
const formatMenuOpen = ref<Record<string, boolean>>({})
|
const formatMenuOpen = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// 使用 props 传入的数据
|
||||||
|
const models = computed(() => props.models ?? [])
|
||||||
|
const aliasMappingPreview = computed(() => props.mappingPreview ?? null)
|
||||||
|
const providerEndpoints = computed(() => props.endpoints ?? [])
|
||||||
|
const providerKeysState = computed(() => props.providerKeys ?? [])
|
||||||
|
|
||||||
// 展开状态
|
// 展开状态
|
||||||
const expandedItems = ref<Set<string>>(new Set())
|
const expandedItems = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// 是否有 key 配置了自动获取上游模型
|
// 是否有 key 配置了自动获取上游模型
|
||||||
const hasAutoFetchKey = computed(() => {
|
const hasAutoFetchKey = computed(() => {
|
||||||
const keys = props.providerKeys || providerKeysState.value
|
return providerKeysState.value.some(k => k.auto_fetch_models)
|
||||||
return keys.some(k => k.auto_fetch_models)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生成作用域唯一键
|
// 生成作用域唯一键
|
||||||
@@ -562,25 +559,9 @@ const {
|
|||||||
paginatedItems: paginatedMappings,
|
paginatedItems: paginatedMappings,
|
||||||
} = useSmartPagination(combinedMappings, mappingsListRef)
|
} = useSmartPagination(combinedMappings, mappingsListRef)
|
||||||
|
|
||||||
// 加载数据
|
// 刷新数据(通知父组件刷新)
|
||||||
async function loadData() {
|
function refresh() {
|
||||||
try {
|
emit('refresh')
|
||||||
loading.value = true
|
|
||||||
const [modelsData, previewData, endpointsData, keysData] = await Promise.all([
|
|
||||||
getProviderModels(props.provider.id),
|
|
||||||
getProviderMappingPreview(props.provider.id).catch(() => null),
|
|
||||||
getProviderEndpoints(props.provider.id).catch(() => []),
|
|
||||||
getProviderKeys(props.provider.id).catch(() => [])
|
|
||||||
])
|
|
||||||
models.value = modelsData
|
|
||||||
aliasMappingPreview.value = previewData
|
|
||||||
providerEndpoints.value = endpointsData
|
|
||||||
providerKeysState.value = keysData
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除确认描述
|
// 删除确认描述
|
||||||
@@ -653,7 +634,6 @@ async function confirmDelete() {
|
|||||||
showSuccess('映射已删除')
|
showSuccess('映射已删除')
|
||||||
deleteConfirmOpen.value = false
|
deleteConfirmOpen.value = false
|
||||||
deletingGroup.value = null
|
deletingGroup.value = null
|
||||||
await loadData()
|
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||||
@@ -662,7 +642,6 @@ async function confirmDelete() {
|
|||||||
|
|
||||||
// 对话框保存后回调
|
// 对话框保存后回调
|
||||||
async function onDialogSaved() {
|
async function onDialogSaved() {
|
||||||
await loadData()
|
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,16 +749,9 @@ async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听 provider 变化
|
|
||||||
watch(() => props.provider?.id, (newId) => {
|
|
||||||
if (newId) {
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 暴露给父组件
|
// 暴露给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value),
|
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value),
|
||||||
reload: loadData
|
reload: refresh
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -246,7 +246,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useSmartPagination } from '@/composables/useSmartPagination'
|
import { useSmartPagination } from '@/composables/useSmartPagination'
|
||||||
import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
|
import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
@@ -261,8 +261,6 @@ import { useToast } from '@/composables/useToast'
|
|||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { sortResolutionEntries } from '@/utils/form'
|
import { sortResolutionEntries } from '@/utils/form'
|
||||||
import {
|
import {
|
||||||
getProviderModels,
|
|
||||||
getProviderMappingPreview,
|
|
||||||
testModel,
|
testModel,
|
||||||
type Model,
|
type Model,
|
||||||
type ProviderMappingPreviewResponse
|
type ProviderMappingPreviewResponse
|
||||||
@@ -280,11 +278,14 @@ interface Endpoint {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
provider: any
|
provider: any
|
||||||
endpoints?: Endpoint[]
|
endpoints?: Endpoint[]
|
||||||
|
models?: Model[]
|
||||||
|
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'editModel': [model: Model]
|
'editModel': [model: Model]
|
||||||
'batchAssign': []
|
'batchAssign': []
|
||||||
|
'refresh': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { error: showError, success: showSuccess } = useToast()
|
const { error: showError, success: showSuccess } = useToast()
|
||||||
@@ -292,12 +293,16 @@ const { copyToClipboard } = useClipboard()
|
|||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const models = ref<Model[]>([])
|
const localModels = ref<Model[]>([])
|
||||||
const mappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
const localMappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
||||||
const togglingModelId = ref<string | null>(null)
|
const togglingModelId = ref<string | null>(null)
|
||||||
const testingModelId = ref<string | null>(null)
|
const testingModelId = ref<string | null>(null)
|
||||||
const formatMenuOpen = ref<Record<string, boolean>>({})
|
const formatMenuOpen = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// 使用 props 传入的数据,或使用本地数据
|
||||||
|
const models = computed(() => props.models ?? localModels.value)
|
||||||
|
const mappingPreview = computed(() => props.mappingPreview ?? localMappingPreview.value)
|
||||||
|
|
||||||
// 获取可用的 API 格式(有活跃端点且有活跃 Key)
|
// 获取可用的 API 格式(有活跃端点且有活跃 Key)
|
||||||
const availableApiFormats = computed(() => {
|
const availableApiFormats = computed(() => {
|
||||||
if (!props.endpoints) return []
|
if (!props.endpoints) return []
|
||||||
@@ -329,24 +334,9 @@ async function copyModelId(modelId: string) {
|
|||||||
await copyToClipboard(modelId)
|
await copyToClipboard(modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载模型和映射预览
|
// 刷新数据(通知父组件刷新)
|
||||||
async function loadModels() {
|
function refresh() {
|
||||||
try {
|
emit('refresh')
|
||||||
loading.value = true
|
|
||||||
const [modelsData, previewData] = await Promise.all([
|
|
||||||
getProviderModels(props.provider.id),
|
|
||||||
getProviderMappingPreview(props.provider.id).catch((err) => {
|
|
||||||
console.warn('Failed to load mapping preview:', err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
])
|
|
||||||
models.value = modelsData
|
|
||||||
mappingPreview.value = previewData
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化价格显示
|
// 格式化价格显示
|
||||||
@@ -547,12 +537,8 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadModels()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 暴露给父组件
|
// 暴露给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
reload: loadModels
|
reload: refresh
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user