feat(embedding): 新增 EmbeddingModelSelector 组件并替换原有选择器

将嵌入模型选择逻辑提取为独立组件,提供模型状态显示功能
在 DataBaseView 和 SettingView 中使用新组件替代原有实现
This commit is contained in:
Wenjie Zhang 2025-12-07 17:17:20 +08:00
parent 55bb52c112
commit 82bc35a501
3 changed files with 149 additions and 94 deletions

View File

@ -0,0 +1,138 @@
<template>
<a-select
:style="style"
:size="size"
:value="value"
@change="handleSelect"
@dropdownVisibleChange="checkAllModelStatus"
:placeholder="placeholder"
:disabled="disabled"
>
<a-select-option
v-for="(name, idx) in embedModelChoices" :key="idx"
:value="name"
>
<div style="display: flex; align-items: center; gap: 8px; min-width: 0;">
<span style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ name }} ({{ configStore.config?.embed_model_names[name]?.dimension }})
</span>
<span
:style="{
color: getModelStatusColor(name),
fontSize: '11px',
fontWeight: 'bold',
flexShrink: 0,
padding: '2px 4px',
borderRadius: '3px',
}"
:title="getModelStatusTooltip(name)"
>
{{ getModelStatusIcon(name) }}
</span>
</div>
</a-select-option>
</a-select>
</template>
<script setup>
import { computed, reactive } from 'vue'
import { useConfigStore } from '@/stores/config'
import { embeddingApi } from '@/apis/knowledge_api'
import { message } from 'ant-design-vue'
const configStore = useConfigStore()
const props = defineProps({
value: {
type: String,
default: ''
},
size: {
type: String,
default: 'default',
validator: (value) => ['default', 'large', 'small'].includes(value)
},
placeholder: {
type: String,
default: '请选择嵌入模型'
},
style: {
type: Object,
default: () => ({ width: '320px' })
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:value', 'change'])
const state = reactive({
modelStatuses: {},
checkingStatus: false
})
const embedModelChoices = computed(() => {
return Object.keys(configStore?.config?.embed_model_names || {}) || []
})
// embedding
const checkAllModelStatus = async () => {
try {
state.checkingStatus = true
const response = await embeddingApi.getAllModelsStatus()
if (response.status.models) {
state.modelStatuses = response.status.models
}
} catch (error) {
console.error('检查所有模型状态失败:', error)
message.error('获取模型状态失败')
} finally {
state.checkingStatus = false
}
}
//
const getModelStatusIcon = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return '○'
if (status.status === 'available') return '✓'
if (status.status === 'unavailable') return '✗'
if (status.status === 'error') return '⚠'
return '○'
}
//
const getModelStatusColor = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return 'var(--gray-500)'
if (status.status === 'available') return 'var(--color-success-500)'
if (status.status === 'unavailable') return 'var(--color-error-500)'
if (status.status === 'error') return 'var(--color-warning-500)'
return 'var(--gray-500)'
}
//
const getModelStatusTooltip = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return '状态未知'
let statusText = ''
if (status.status === 'available') statusText = '可用'
else if (status.status === 'unavailable') statusText = '不可用'
else if (status.status === 'error') statusText = '错误'
const message = status.message || '无详细信息'
return `${statusText}: ${message}`
}
const handleSelect = (value) => {
emit('update:value', value)
emit('change', value)
}
</script>
<style lang="less" scoped>
//
</style>

View File

@ -50,7 +50,12 @@
<a-input v-model:value="newDatabase.name" placeholder="新建知识库名称" size="large" />
<h3>嵌入模型</h3>
<a-select v-model:value="newDatabase.embed_model_name" :options="embedModelOptions" style="width: 100%;" size="large" />
<EmbeddingModelSelector
v-model:value="newDatabase.embed_model_name"
style="width: 100%;"
size="large"
placeholder="请选择嵌入模型"
/>
<!-- 仅对 LightRAG 提供语言选择和LLM选择 -->
<div v-if="newDatabase.kb_type === 'lightrag'">
@ -237,6 +242,7 @@ import { LockOutlined, InfoCircleOutlined, QuestionCircleOutlined, PlusOutlined
import { databaseApi, typeApi } from '@/apis/knowledge_api';
import HeaderComponent from '@/components/HeaderComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue';
import dayjs, { parseToShanghai } from '@/utils/time';
const route = useRoute()
@ -250,12 +256,6 @@ const state = reactive({
openNewDatabaseModel: false,
})
const embedModelOptions = computed(() => {
return Object.keys(configStore.config?.embed_model_names || {}).map(key => ({
label: `${key} (${configStore.config?.embed_model_names[key]?.dimension})`,
value: key,
}))
})
// 使/LightRAG 便
const languageOptions = [

View File

@ -39,35 +39,11 @@
检查状态
</a-button> -->
</div>
<a-select style="width: 320px"
<EmbeddingModelSelector
:value="configStore.config?.embed_model"
@change="handleChange('embed_model', $event)"
@dropdownVisibleChange="checkAllModelStatus"
>
<a-select-option
v-for="(name, idx) in embedModelChoices" :key="idx"
:value="name"
>
<div style="display: flex; align-items: center; gap: 8px; min-width: 0;">
<span style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ name }}
</span>
<span
:style="{
color: getModelStatusColor(name),
fontSize: '11px',
fontWeight: 'bold',
flexShrink: 0,
padding: '2px 4px',
borderRadius: '3px',
}"
:title="getModelStatusTooltip(name)"
>
{{ getModelStatusIcon(name) }}
</span>
</div>
</a-select-option>
</a-select>
style="width: 320px"
/>
</div>
<div class="card card-select">
<span class="label">{{ items?.reranker.des }}</span>
@ -184,8 +160,8 @@ import {
import HeaderComponent from '@/components/HeaderComponent.vue';
import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue';
import UserManagementComponent from '@/components/UserManagementComponent.vue';
import { embeddingApi } from '@/apis/knowledge_api'
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue';
const configStore = useConfigStore()
const userStore = useUserStore()
@ -194,12 +170,6 @@ const state = reactive({
loading: false,
section: 'base',
windowWidth: window?.innerWidth || 0,
modelStatuses: {}, // embedding
checkingStatus: false //
})
const embedModelChoices = computed(() => {
return Object.keys(configStore?.config?.embed_model_names || {}) || []
})
const rerankerChoices = computed(() => {
@ -252,65 +222,12 @@ onMounted(() => {
updateWindowWidth()
window.addEventListener('resize', updateWindowWidth)
state.section = userStore.isSuperAdmin ? 'base' : 'user'
checkAllModelStatus()
})
onUnmounted(() => {
window.removeEventListener('resize', updateWindowWidth)
})
// embedding
const checkAllModelStatus = async () => {
try {
state.checkingStatus = true
const response = await embeddingApi.getAllModelsStatus()
if (response.status.models) {
state.modelStatuses = response.status.models
}
} catch (error) {
console.error('检查所有模型状态失败:', error)
message.error('获取模型状态失败')
} finally {
state.checkingStatus = false
}
}
//
const getModelStatusIcon = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return '○'
if (status.status === 'available') return '✓'
if (status.status === 'unavailable') return '✗'
if (status.status === 'error') return '⚠'
return '○'
}
//
const getModelStatusColor = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return 'var(--gray-500)'
if (status.status === 'available') return 'var(--color-success-500)'
if (status.status === 'unavailable') return 'var(--color-error-500)'
if (status.status === 'error') return 'var(--color-warning-500)'
return 'var(--gray-500)'
}
//
const getModelStatusTooltip = (modelId) => {
const status = state.modelStatuses[modelId]
if (!status) return '状态未知'
let statusText = ''
if (status.status === 'available') statusText = '可用'
else if (status.status === 'unavailable') statusText = '不可用'
else if (status.status === 'error') statusText = '错误'
const message = status.message || '无详细信息'
return `${statusText}: ${message}`
}
const openLink = (url) => {
window.open(url, '_blank')
}