feat: enhance provider selection with a new drawer interface and localization updates

This commit is contained in:
Soulter
2025-12-17 10:39:16 +08:00
parent 67c33b842d
commit 1acac0cac2
4 changed files with 133 additions and 8 deletions
@@ -14,8 +14,20 @@
<!-- Provider Selection Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
{{ tm('providerSelector.dialogTitle') }}
<v-card-title
class="text-h3 py-4 d-flex align-center justify-space-between gap-4 flex-wrap"
style="font-weight: normal;"
>
<span>{{ tm('providerSelector.dialogTitle') }}</span>
<v-btn
size="small"
color="primary"
variant="tonal"
prepend-icon="mdi-plus"
@click="openProviderDrawer"
>
{{ tm('providerSelector.createProvider') }}
</v-btn>
</v-card-title>
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
@@ -79,12 +91,33 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-overlay
v-model="providerDrawer"
class="provider-drawer-overlay"
location="right"
transition="slide-x-reverse-transition"
:scrim="true"
@click:outside="closeProviderDrawer"
>
<v-card class="provider-drawer-card" elevation="12">
<div class="provider-drawer-header">
<v-btn icon variant="text" @click="closeProviderDrawer">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div class="provider-drawer-content">
<ProviderPage :default-tab="defaultTab" />
</div>
</v-card>
</v-overlay>
</template>
<script setup>
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
import ProviderPage from '@/views/ProviderPage.vue'
const props = defineProps({
modelValue: {
@@ -112,12 +145,26 @@ const dialog = ref(false)
const providerList = ref([])
const loading = ref(false)
const selectedProvider = ref('')
const providerDrawer = ref(false)
const defaultTab = computed(() => {
if (props.providerType === 'agent_runner' && props.providerSubtype) {
return `select_agent_runner_provider:${props.providerSubtype}`
}
return props.providerType || 'chat_completion'
})
// 监听 modelValue 变化,同步到 selectedProvider
watch(() => props.modelValue, (newValue) => {
selectedProvider.value = newValue || ''
}, { immediate: true })
watch(providerDrawer, (isOpen, wasOpen) => {
if (!isOpen && wasOpen) {
loadProviders()
}
})
async function openDialog() {
selectedProvider.value = props.modelValue || ''
dialog.value = true
@@ -170,6 +217,14 @@ function cancelSelection() {
selectedProvider.value = props.modelValue || ''
dialog.value = false
}
function openProviderDrawer() {
providerDrawer.value = true
}
function closeProviderDrawer() {
providerDrawer.value = false
}
</script>
<style scoped>
@@ -184,4 +239,35 @@ function cancelSelection() {
.v-list-item.v-list-item--active {
background-color: rgba(var(--v-theme-primary), 0.08);
}
.provider-drawer-overlay {
align-items: stretch;
justify-content: flex-end;
}
.provider-drawer-card {
width: clamp(360px, 70vw, 1200px);
height: calc(100vh - 32px);
margin: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.provider-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px 20px;
}
.provider-drawer-content {
flex: 1;
overflow: hidden;
}
.provider-drawer-content > * {
height: 100%;
overflow: auto;
}
</style>
@@ -40,6 +40,8 @@
"cancelSelection": "Cancel",
"clearSelection": "None",
"clearSelectionSubtitle": "Clear current selection",
"unknownType": "Unknown type"
"unknownType": "Unknown type",
"createProvider": "Create Provider",
"manageProviders": "Provider Management"
}
}
@@ -40,6 +40,8 @@
"cancelSelection": "取消",
"clearSelection": "不选择",
"clearSelectionSubtitle": "清除当前选择",
"unknownType": "未知类型"
"unknownType": "未知类型",
"createProvider": "创建提供商",
"manageProviders": "提供商管理"
}
}
+38 -3
View File
@@ -36,7 +36,6 @@
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
<div class="d-flex align-center ga-2">
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
<v-chip size="x-small" color="primary" variant="tonal">{{ displayedProviderSources.length }}</v-chip>
</div>
<v-menu>
<template v-slot:activator="{ props }">
@@ -126,7 +125,6 @@
<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>
@@ -359,6 +357,13 @@ import ItemCard from '@/components/shared/ItemCard.vue'
import AddNewProvider from '@/components/provider/AddNewProvider.vue'
import { getProviderIcon } from '@/utils/providerUtils'
const props = defineProps({
defaultTab: {
type: String,
default: 'chat_completion'
}
})
const { tm } = useModuleI18n('features/provider')
const router = useRouter()
@@ -367,7 +372,7 @@ const config = ref({})
const metadata = ref({})
const providerSources = ref([])
const providers = ref([])
const selectedProviderType = ref('chat_completion')
const selectedProviderType = ref(resolveDefaultTab(props.defaultTab))
const selectedProviderSource = ref(null)
const selectedProviderSourceOriginalId = ref(null)
const editableProviderSource = ref(null)
@@ -607,6 +612,32 @@ function isTypeMatchingProviderType(type, providerType) {
return type && type.includes(providerType)
}
function resolveDefaultTab(value) {
const normalized = (value || '').toLowerCase()
if (normalized.startsWith('select_agent_runner_provider') || normalized === 'agent_runner') {
return 'agent_runner'
}
if (normalized === 'select_provider_stt' || normalized === 'speech_to_text' || normalized.includes('stt')) {
return 'speech_to_text'
}
if (normalized === 'select_provider_tts' || normalized === 'text_to_speech' || normalized.includes('tts')) {
return 'text_to_speech'
}
if (normalized.includes('embedding')) {
return 'embedding'
}
if (normalized.includes('rerank')) {
return 'rerank'
}
return 'chat_completion'
}
function resolveSourceIcon(source) {
if (!source) return ''
@@ -959,6 +990,10 @@ onMounted(async () => {
await loadProviderTemplate()
})
watch(() => props.defaultTab, (val) => {
selectedProviderType.value = resolveDefaultTab(val)
})
// 跟踪编辑中的 provider source 是否被修改
watch(editableProviderSource, () => {
if (suppressSourceWatch) return