perf: better UI
This commit is contained in:
@@ -162,7 +162,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
|
||||
<v-col cols="12" sm="7" class="property-info">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
@@ -180,7 +180,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<div v-if="metadata[metadataKey].items[key]" class="w-100">
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-if="metadata[metadataKey].items[key]?._special === 'select_provider'">
|
||||
@@ -542,7 +542,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.config-row:hover {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between ga-2">
|
||||
<div v-if="isSingleItemMode" class="flex-grow-1 d-flex align-center ga-2">
|
||||
<v-text-field
|
||||
v-model="singleItemValue"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="flex-grow-1"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
{{ t('core.common.list.noItems') }}
|
||||
</span>
|
||||
@@ -14,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText || t('core.common.list.modifyButton') }}
|
||||
{{ preferSingleItem ? '添加更多' : (buttonText || t('core.common.list.modifyButton')) }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -167,6 +176,10 @@ const props = defineProps({
|
||||
maxDisplayItems: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
preferSingleItem: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,6 +193,21 @@ const editIndex = ref(-1)
|
||||
const editItem = ref('')
|
||||
const showBatchImport = ref(false)
|
||||
const batchImportText = ref('')
|
||||
const isSingleItemMode = computed(() => (props.modelValue?.length ?? 0) <= 1 && props.preferSingleItem)
|
||||
const singleItemValue = computed({
|
||||
get: () => props.modelValue?.[0] ?? '',
|
||||
set: (value) => {
|
||||
const newItems = [...(props.modelValue || [])]
|
||||
|
||||
if (newItems.length === 0) {
|
||||
newItems.push(value)
|
||||
} else {
|
||||
newItems[0] = value
|
||||
}
|
||||
|
||||
emit('update:modelValue', newItems)
|
||||
}
|
||||
})
|
||||
|
||||
// 计算要显示的项目
|
||||
const displayItems = computed(() => {
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
.v-input--density-default,
|
||||
.v-field--variant-solo,
|
||||
.v-field--variant-filled {
|
||||
--v-input-control-height: 51px;
|
||||
--v-input-padding-top: 14px;
|
||||
}
|
||||
.v-input--density-comfortable {
|
||||
--v-input-control-height: 56px;
|
||||
--v-input-padding-top: 17px;
|
||||
}
|
||||
.v-label {
|
||||
font-size: 0.975rem;
|
||||
}
|
||||
|
||||
@@ -1,70 +1,3 @@
|
||||
.v-text-field input {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.v-input--density-default {
|
||||
.v-field__input {
|
||||
min-height: 51px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-field__outline {
|
||||
color: rgb(var(--v-theme-inputBorder));
|
||||
}
|
||||
|
||||
// 亮色主题样式
|
||||
.v-theme--PurpleTheme .v-field__outline {
|
||||
--v-field-border-width: 1.2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
border-color: #d1cfcf;
|
||||
}
|
||||
|
||||
.v-theme--PurpleTheme .v-text-field .v-field--focused .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
}
|
||||
|
||||
// 深色主题样式
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field:hover .v-field__outline {
|
||||
--v-field-border-width: 2px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field--focused .v-field__outline {
|
||||
--v-field-border-width: 2.5px !important;
|
||||
--v-field-border-opacity: 1 !important;
|
||||
color: rgb(129, 102, 176) !important;
|
||||
border-color: rgb(126, 99, 171) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field input {
|
||||
color: #ffffff !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__label {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__prepend-inner .v-icon,
|
||||
.v-theme--PurpleThemeDark .v-text-field .v-field__append-inner .v-icon {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.inputWithbg {
|
||||
.v-field--variant-outlined {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -19,24 +19,6 @@
|
||||
top: -85px;
|
||||
right: -95px;
|
||||
}
|
||||
|
||||
// &.bubble-primary-shape {
|
||||
// &::before {
|
||||
// background: rgb(var(--v-theme-darkprimary));
|
||||
// }
|
||||
// &::after {
|
||||
// background: rgb(var(--v-theme-darkprimary));
|
||||
// }
|
||||
// }
|
||||
|
||||
// &.bubble-secondary-shape {
|
||||
// &::before {
|
||||
// background: rgb(var(--v-theme-darksecondary));
|
||||
// }
|
||||
// &::after {
|
||||
// background: rgb(var(--v-theme-darksecondary));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.z-1 {
|
||||
@@ -54,11 +36,6 @@
|
||||
top: -160px;
|
||||
right: -130px;
|
||||
}
|
||||
// &.bubble-primary {
|
||||
// &::before {
|
||||
// background: linear-gradient(140.9deg, rgb(var(--v-theme-lightprimary)) -14.02%, rgba(var(--v-theme-darkprimary), 0) 77.58%);
|
||||
// }
|
||||
// }
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -68,23 +45,6 @@
|
||||
top: -30px;
|
||||
right: -180px;
|
||||
}
|
||||
// &.bubble-primary {
|
||||
// &::after {
|
||||
// background: linear-gradient(210.04deg, rgb(var(--v-theme-lightprimary)) -50.94%, rgba(var(--v-theme-darkprimary), 0) 83.49%);
|
||||
// }
|
||||
// }
|
||||
|
||||
// &.bubble-warning {
|
||||
// &::before {
|
||||
// background: linear-gradient(140.9deg, rgb(var(--v-theme-warning)) -14.02%, rgba(144, 202, 249, 0) 70.5%);
|
||||
// }
|
||||
// }
|
||||
|
||||
// &.bubble-warning {
|
||||
// &::after {
|
||||
// background: linear-gradient(210.04deg, rgb(var(--v-theme-warning)) -50.94%, rgba(144, 202, 249, 0) 83.49%);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.rounded-square {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="provider-page">
|
||||
<v-container fluid class="pa-0">
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-4">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-creation</v-icon>{{ tm('title') }}
|
||||
@@ -30,24 +30,23 @@
|
||||
|
||||
<!-- Chat Completion: 左侧列表 + 右侧上下卡片布局 -->
|
||||
<div v-if="selectedProviderType === 'chat_completion'">
|
||||
<v-row class="mt-2" style="height: calc(100vh - 300px);">
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" md="4" lg="3" class="pr-md-4">
|
||||
<v-card class="provider-sources-panel h-100" elevation="0">
|
||||
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<h2 class="mb-0">{{ tm('providerSources.title') }}</h2>
|
||||
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
|
||||
<v-chip size="x-small" color="primary" variant="tonal">{{ filteredProviderSources.length }}</v-chip>
|
||||
</div>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" prepend-icon="mdi-plus" color="primary" variant="tonal" rounded="xl" size="small">
|
||||
<v-btn v-bind="props" prepend-icon="mdi-plus" color="primary" variant="tonal" rounded="xl"
|
||||
size="small">
|
||||
新增
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="sourceType in availableSourceTypes"
|
||||
:key="sourceType.value"
|
||||
<v-list-item v-for="sourceType in availableSourceTypes" :key="sourceType.value"
|
||||
@click="addProviderSource(sourceType.value)">
|
||||
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -57,14 +56,10 @@
|
||||
|
||||
<div v-if="filteredProviderSources.length > 0">
|
||||
<v-list class="provider-source-list" nav density="compact" lines="two">
|
||||
<v-list-item
|
||||
v-for="source in filteredProviderSources"
|
||||
:key="source.id"
|
||||
:value="source.id"
|
||||
<v-list-item v-for="source in filteredProviderSources" :key="source.id" :value="source.id"
|
||||
:active="selectedProviderSource?.id === source.id"
|
||||
:class="['provider-source-list-item', { 'provider-source-list-item--active': selectedProviderSource?.id === source.id }]"
|
||||
rounded="lg"
|
||||
@click="selectProviderSource(source)">
|
||||
rounded="lg" @click="selectProviderSource(source)">
|
||||
<template #prepend>
|
||||
<v-avatar size="32" class="bg-grey-lighten-4" rounded="0">
|
||||
<v-img v-if="source?.provider" :src="resolveSourceIcon(source)" alt="logo" cover></v-img>
|
||||
@@ -75,8 +70,8 @@
|
||||
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn icon="mdi-pencil" variant="text" size="x-small" color="primary" @click.stop="openSourceEditor(source)"></v-btn>
|
||||
<v-btn icon="mdi-delete" variant="text" size="x-small" color="error" @click.stop="deleteProviderSource(source)"></v-btn>
|
||||
<v-btn icon="mdi-delete" variant="text" size="x-small" color="error"
|
||||
@click.stop="deleteProviderSource(source)"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -90,127 +85,113 @@
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="8" lg="9" class="pl-md-2">
|
||||
<v-row align="stretch">
|
||||
<v-col cols="12">
|
||||
<v-card class="provider-config-card h-100" elevation="0">
|
||||
<v-card-title class="d-flex align-center justify-space-between flex-wrap ga-3 pt-4 pl-5">
|
||||
<div class="d-flex align-center ga-3" v-if="selectedProviderSource">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ selectedProviderSource.id }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ selectedProviderSource.api_base || 'N/A' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-medium-emphasis" v-else>
|
||||
{{ tm('providerSources.selectHint') }}
|
||||
<v-card class="provider-config-card h-100" elevation="0">
|
||||
<v-card-title class="d-flex align-center justify-space-between flex-wrap ga-3 pt-4 pl-5">
|
||||
<div class="d-flex align-center ga-3" v-if="selectedProviderSource">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ selectedProviderSource.id }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ selectedProviderSource.api_base || 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center ga-2" v-if="selectedProviderSource">
|
||||
<v-btn color="primary" prepend-icon="mdi-download" :loading="loadingModels" @click="fetchAvailableModels" variant="tonal">
|
||||
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-content-save" :loading="savingSource" @click="saveProviderSource" variant="flat">
|
||||
{{ tm('providerSources.save') }}
|
||||
<div class="d-flex align-center ga-2" v-if="selectedProviderSource">
|
||||
<v-btn color="success" prepend-icon="mdi-check" :loading="savingSource"
|
||||
:disabled="!isSourceModified"
|
||||
@click="saveProviderSource" variant="flat">
|
||||
{{ tm('providerSources.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<template v-if="selectedProviderSource">
|
||||
<div>
|
||||
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</div>
|
||||
|
||||
<v-expansion-panels variant="accordion" class="mb-2">
|
||||
<v-expansion-panel elevation="0" class="border rounded-lg">
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium">{{ tm('providerSources.advancedConfig') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig"
|
||||
:metadata="configSchema" metadataKey="provider" :is-editing="true" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex align-center ga-2 mb-2">
|
||||
<h3 class="text-h5 font-weight-bold mb-0">{{ tm('models.configured') }}</h3>
|
||||
<!-- <v-chip color="success" variant="tonal" size="small">{{ displayedChatProviders.length }}</v-chip> -->
|
||||
<small style="color: grey;" v-if="availableModels.length">{{ tm('models.available') }} {{ availableModels.length }}</small>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" prepend-icon="mdi-download" :loading="loadingModels"
|
||||
@click="fetchAvailableModels" variant="tonal" size="small">
|
||||
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') :
|
||||
tm('providerSources.fetchModels') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<template v-if="selectedProviderSource">
|
||||
<div class="mb-4">
|
||||
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</div>
|
||||
<v-list density="compact" class="rounded-lg border"
|
||||
style="max-height: 520px; overflow-y: auto; font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;">
|
||||
<template v-if="mergedModelEntries.length > 0">
|
||||
<template v-for="entry in mergedModelEntries" :key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`">
|
||||
<v-list-item v-if="entry.type === 'configured'" class="provider-compact-item"
|
||||
@click="openProviderEdit(entry.provider)">
|
||||
<v-list-item-title class="font-weight-medium text-truncate">
|
||||
{{ entry.provider.id }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey text-truncate">
|
||||
{{ entry.provider.model }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1" @click.stop>
|
||||
<v-switch v-model="entry.provider.enable" density="compact" inset hide-details
|
||||
color="primary" class="mr-1"
|
||||
@update:modelValue="toggleProviderEnable(entry.provider, $event)"></v-switch>
|
||||
<v-tooltip location="top" max-width="300">
|
||||
{{ tm('availability.test') }}
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon="mdi-wrench" size="small" variant="text"
|
||||
:loading="testingProviders.includes(entry.provider.id)" v-bind="props"
|
||||
@click.stop="testProvider(entry.provider)"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-expansion-panels variant="accordion" class="mb-4">
|
||||
<v-expansion-panel elevation="0" class="border rounded-lg">
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium">{{ tm('providerSources.advancedConfig') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
|
||||
@click.stop="deleteProvider(entry.provider)"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div v-if="availableModels.length > 0" class="mt-2">
|
||||
<h3 class="text-h6 font-weight-bold mb-2">{{ tm('models.available') }}</h3>
|
||||
<v-list density="compact" class="rounded-lg border" style="max-height: 200px; overflow-y: auto; font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;">
|
||||
<v-list-item v-for="model in availableModels" :key="model" @click="addModelProvider(model)" class="cursor-pointer">
|
||||
<v-list-item-title>{{ model }}</v-list-item-title>
|
||||
<v-list-item v-else class="cursor-pointer"
|
||||
@click="addModelProvider(entry.model)">
|
||||
<v-list-item-title>{{ entry.model }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-plus" size="small" variant="text" color="primary"></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-center py-8 text-medium-emphasis">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-2">{{ tm('providerSources.selectHint') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-card class="provider-models-card h-100" elevation="0">
|
||||
<v-card-title class="d-flex align-center ga-3 pt-4 pl-5">
|
||||
<h4 class="mb-0 font-weight-bold">
|
||||
{{ tm('models.configured') }}
|
||||
</h4>
|
||||
<v-chip color="success" variant="tonal" size="small">{{ displayedChatProviders.length }}</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="mt-2">
|
||||
<template v-if="selectedProviderSource">
|
||||
<div v-if="sourceProviders.length > 0">
|
||||
<v-expansion-panels v-model="sourceProviderPanels" variant="accordion" class="mb-2">
|
||||
<v-expansion-panel
|
||||
v-for="provider in sourceProviders"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
elevation="0"
|
||||
class="border mb-2 rounded-lg">
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex align-center justify-space-between" style="width: 100%;">
|
||||
<div>
|
||||
<strong>{{ provider.id }}</strong>
|
||||
<span class="text-caption text-grey ml-2">{{ provider.model }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center" @click.stop>
|
||||
<v-switch v-model="provider.enable" density="compact" hide-details color="primary" class="mr-2"></v-switch>
|
||||
<v-btn icon="mdi-test-tube" size="small" variant="text" color="info"
|
||||
:loading="testingProviders.includes(provider.id)" @click.stop="testProvider(provider)"
|
||||
class="mr-1"></v-btn>
|
||||
<v-btn icon="mdi-content-save" size="small" variant="text" color="success"
|
||||
:loading="savingProviders.includes(provider.id)" @click.stop="saveSingleProvider(provider)"
|
||||
class="mr-1"></v-btn>
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
|
||||
@click.stop="deleteProvider(provider)"></v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig :iterable="provider" :metadata="configSchema" metadataKey="provider"
|
||||
:is-editing="true" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
<div v-else class="text-center pa-4 border rounded-lg">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
|
||||
<p class="text-grey mt-2">{{ tm('models.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-center py-8 text-medium-emphasis">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-2">{{ tm('providerSources.selectHint') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
|
||||
<p class="text-grey mt-2">{{ tm('models.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-center py-8 text-medium-emphasis">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-2">{{ tm('providerSources.selectHint') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
@@ -289,6 +270,26 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 已配置模型编辑对话框 -->
|
||||
<v-dialog v-model="showProviderEditDialog" width="800">
|
||||
<v-card :title="providerEditData?.id || tm('dialogs.config.editTitle')">
|
||||
<v-card-text class="py-4">
|
||||
<AstrBotConfig v-if="providerEditData" :iterable="providerEditData" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showProviderEditDialog = false"
|
||||
:disabled="savingProviders.includes(providerEditData?.id)">
|
||||
{{ tm('dialogs.config.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="saveEditedProvider" :loading="savingProviders.includes(providerEditData?.id)">
|
||||
{{ tm('dialogs.config.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000" location="top">
|
||||
{{ snackbar.message }}
|
||||
@@ -361,6 +362,8 @@ const loading = ref(false)
|
||||
const providerStatuses = ref([])
|
||||
const showAgentRunnerDialog = ref(false)
|
||||
const sourceProviderPanels = ref(null)
|
||||
const showProviderEditDialog = ref(false)
|
||||
const providerEditData = ref(null)
|
||||
|
||||
let suppressSourceWatch = false
|
||||
|
||||
@@ -442,6 +445,38 @@ const displayedChatProviders = computed(() => {
|
||||
return chatProviders.value
|
||||
})
|
||||
|
||||
const existingModelsForSelectedSource = computed(() => {
|
||||
if (!selectedProviderSource.value) return new Set()
|
||||
return new Set(sourceProviders.value.map(p => p.model))
|
||||
})
|
||||
|
||||
const sortedAvailableModels = computed(() => {
|
||||
const existing = existingModelsForSelectedSource.value
|
||||
return [...(availableModels.value || [])].sort((a, b) => {
|
||||
const aExists = existing.has(a)
|
||||
const bExists = existing.has(b)
|
||||
if (aExists && !bExists) return -1
|
||||
if (!aExists && bExists) return 1
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
const mergedModelEntries = computed(() => {
|
||||
const configuredEntries = (sourceProviders.value || []).map(provider => ({
|
||||
type: 'configured',
|
||||
provider
|
||||
}))
|
||||
|
||||
const availableEntries = (sortedAvailableModels.value || [])
|
||||
.filter(model => !existingModelsForSelectedSource.value.has(model))
|
||||
.map(model => ({
|
||||
type: 'available',
|
||||
model
|
||||
}))
|
||||
|
||||
return [...configuredEntries, ...availableEntries]
|
||||
})
|
||||
|
||||
// 基础配置:只包含常用字段
|
||||
const basicSourceConfig = computed(() => {
|
||||
if (!editableProviderSource.value) return null
|
||||
@@ -507,39 +542,15 @@ function isTypeMatchingProviderType(type, providerType) {
|
||||
return type && type.includes(providerType)
|
||||
}
|
||||
|
||||
function extractSourceFieldsFromProvider(provider) {
|
||||
// provider 只保留这些字段,其他都是 source 字段
|
||||
const providerOnlyKeys = ['id', 'provider_source_id', 'model', 'modalities', 'custom_extra_body']
|
||||
const sourceFields = {}
|
||||
|
||||
for (const [key, value] of Object.entries(provider)) {
|
||||
if (!providerOnlyKeys.includes(key)) {
|
||||
sourceFields[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return sourceFields
|
||||
}
|
||||
|
||||
function resolveSourceIcon(source) {
|
||||
if (!source) return ''
|
||||
|
||||
return getProviderIcon(source.provider) || ''
|
||||
}
|
||||
|
||||
function editProviderSourceFromModel(provider) {
|
||||
if (!provider) return
|
||||
|
||||
const source = providerSources.value.find(s => s.id === provider.provider_source_id)
|
||||
if (!source) {
|
||||
showMessage(tm('providerSources.empty'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
openSourceEditor(source)
|
||||
nextTick(() => {
|
||||
sourceProviderPanels.value = provider.id
|
||||
})
|
||||
function openProviderEdit(provider) {
|
||||
providerEditData.value = JSON.parse(JSON.stringify(provider))
|
||||
showProviderEditDialog.value = true
|
||||
}
|
||||
|
||||
// ===== Methods =====
|
||||
@@ -547,15 +558,6 @@ function showMessage(message, color = 'success') {
|
||||
snackbar.value = { show: true, message, color }
|
||||
}
|
||||
|
||||
function selectProviderType(type) {
|
||||
selectedProviderType.value = type
|
||||
selectedProviderSource.value = null
|
||||
selectedProviderSourceOriginalId.value = null
|
||||
editableProviderSource.value = null
|
||||
availableModels.value = []
|
||||
sourceProviderPanels.value = null
|
||||
}
|
||||
|
||||
function selectProviderSource(source) {
|
||||
selectedProviderSource.value = source
|
||||
selectedProviderSourceOriginalId.value = source?.id || null
|
||||
@@ -569,11 +571,6 @@ function selectProviderSource(source) {
|
||||
sourceProviderPanels.value = null
|
||||
}
|
||||
|
||||
function openSourceEditor(source) {
|
||||
selectProviderSource(source)
|
||||
}
|
||||
|
||||
|
||||
function addProviderSource(templateKey) {
|
||||
// 从模板中找到对应的配置,使用 key 而不是 type
|
||||
const template = providerTemplates.value[templateKey]
|
||||
@@ -727,22 +724,40 @@ async function fetchAvailableModels() {
|
||||
}
|
||||
}
|
||||
|
||||
function addModelProvider(modelName) {
|
||||
async function addModelProvider(modelName) {
|
||||
if (!selectedProviderSource.value) return
|
||||
|
||||
const sourceId = editableProviderSource.value?.id || selectedProviderSource.value.id
|
||||
const newId = `${sourceId}/${modelName}`
|
||||
const newProvider = {
|
||||
id: newId,
|
||||
enable: false,
|
||||
provider_source_id: sourceId,
|
||||
model: modelName,
|
||||
modalities: [],
|
||||
custom_extra_body: {}
|
||||
}
|
||||
|
||||
providers.value.push(newProvider)
|
||||
isSourceModified.value = true
|
||||
showMessage(tm('models.addSuccess', { model: modelName }))
|
||||
try {
|
||||
const res = await axios.post('/api/config/provider/new', newProvider)
|
||||
if (res.data.status === 'error') {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
|
||||
if (Array.isArray(config.value.provider)) {
|
||||
config.value.provider.push(newProvider)
|
||||
} else {
|
||||
config.value.provider = [newProvider]
|
||||
}
|
||||
|
||||
showMessage(res.data.message || tm('models.addSuccess', { model: modelName }))
|
||||
} catch (error) {
|
||||
showMessage(error.response?.data?.message || error.message || tm('providerSources.saveError'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function modelAlreadyConfigured(modelName) {
|
||||
return existingModelsForSelectedSource.value.has(modelName)
|
||||
}
|
||||
|
||||
async function deleteProvider(provider) {
|
||||
@@ -987,6 +1002,30 @@ async function newProvider() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEditedProvider() {
|
||||
if (!providerEditData.value) return
|
||||
|
||||
savingProviders.value.push(providerEditData.value.id)
|
||||
try {
|
||||
const res = await axios.post('/api/config/provider/update', {
|
||||
id: providerEditData.value.id,
|
||||
config: providerEditData.value
|
||||
})
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
|
||||
showMessage(res.data.message || tm('providerSources.saveSuccess'))
|
||||
showProviderEditDialog.value = false
|
||||
await loadConfig()
|
||||
} catch (err) {
|
||||
showMessage(err.response?.data?.message || err.message || tm('providerSources.saveError'), 'error')
|
||||
} finally {
|
||||
savingProviders.value = savingProviders.value.filter(id => id !== providerEditData.value?.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyProvider(providerToCopy) {
|
||||
const newProviderConfig = JSON.parse(JSON.stringify(providerToCopy))
|
||||
|
||||
@@ -1034,6 +1073,26 @@ function providerStatusChange(provider) {
|
||||
})
|
||||
}
|
||||
|
||||
async function toggleProviderEnable(provider, value) {
|
||||
provider.enable = value
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/config/provider/update', {
|
||||
id: provider.id,
|
||||
config: provider
|
||||
})
|
||||
|
||||
if (res.data.status === 'error') {
|
||||
throw new Error(res.data.message)
|
||||
}
|
||||
|
||||
showMessage(res.data.message || tm('messages.success.statusUpdate'))
|
||||
} catch (error) {
|
||||
provider.enable = !value
|
||||
showMessage(error.response?.data?.message || error.message || tm('providerSources.saveError'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function isProviderTesting(providerId) {
|
||||
return testingProviders.value.includes(providerId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user