495 lines
11 KiB
Vue
495 lines
11 KiB
Vue
<template>
|
||
<div class="embedding-config-page">
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<div class="title-section">
|
||
<h1 class="page-title">嵌入模型配置</h1>
|
||
<p class="page-desc">配置和管理系统使用的嵌入模型,支持多种提供者切换。配置修改后需保存才能生效。</p>
|
||
</div>
|
||
<div class="header-actions" v-if="currentConfig.updated_at">
|
||
<div class="update-info">
|
||
<el-icon class="update-icon"><Clock /></el-icon>
|
||
<span>上次更新: {{ formatDate(currentConfig.updated_at) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-row :gutter="24" v-loading="pageLoading" element-loading-text="加载中...">
|
||
<el-col :xs="24" :sm="24" :md="12" :lg="12">
|
||
<el-card shadow="hover" class="config-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<div class="icon-wrapper">
|
||
<el-icon><Setting /></el-icon>
|
||
</div>
|
||
<span class="header-title">模型配置</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="card-content">
|
||
<div class="provider-select-section">
|
||
<div class="section-label">
|
||
<el-icon><Connection /></el-icon>
|
||
<span>选择提供者</span>
|
||
</div>
|
||
<EmbeddingProviderSelect
|
||
v-model="currentConfig.provider"
|
||
:providers="providers"
|
||
:loading="providersLoading"
|
||
placeholder="请选择嵌入模型提供者"
|
||
@change="handleProviderChange"
|
||
/>
|
||
<transition name="fade">
|
||
<div v-if="currentProvider" class="provider-info">
|
||
<el-icon class="info-icon"><InfoFilled /></el-icon>
|
||
<span class="info-text">{{ currentProvider.description }}</span>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
|
||
<el-divider />
|
||
|
||
<transition name="slide-fade" mode="out-in">
|
||
<div v-if="currentConfig.provider" key="form" class="config-form-section">
|
||
<EmbeddingConfigForm
|
||
ref="configFormRef"
|
||
:schema="configSchema"
|
||
v-model="currentConfig.config"
|
||
label-width="140px"
|
||
/>
|
||
</div>
|
||
<el-empty v-else key="empty" description="请先选择一个嵌入模型提供者" :image-size="120">
|
||
<template #image>
|
||
<div class="empty-icon">
|
||
<el-icon><Box /></el-icon>
|
||
</div>
|
||
</template>
|
||
</el-empty>
|
||
</transition>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="card-footer">
|
||
<el-button size="large" @click="handleReset">
|
||
<el-icon><RefreshLeft /></el-icon>
|
||
重置
|
||
</el-button>
|
||
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
|
||
<el-icon><Check /></el-icon>
|
||
保存配置
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<el-col :xs="24" :sm="24" :md="12" :lg="12">
|
||
<div class="right-column">
|
||
<EmbeddingTestPanel
|
||
:config="{ provider: currentConfig.provider, config: currentConfig.config }"
|
||
/>
|
||
|
||
<el-card shadow="hover" class="formats-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<div class="header-left">
|
||
<div class="icon-wrapper">
|
||
<el-icon><Document /></el-icon>
|
||
</div>
|
||
<span class="header-title">支持的文档格式</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<SupportedFormats />
|
||
</el-card>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Setting, Connection, InfoFilled, Box, RefreshLeft, Check, Clock, Document } from '@element-plus/icons-vue'
|
||
import { useEmbeddingStore } from '@/stores/embedding'
|
||
import EmbeddingProviderSelect from '@/components/embedding/EmbeddingProviderSelect.vue'
|
||
import EmbeddingConfigForm from '@/components/embedding/EmbeddingConfigForm.vue'
|
||
import EmbeddingTestPanel from '@/components/embedding/EmbeddingTestPanel.vue'
|
||
import SupportedFormats from '@/components/embedding/SupportedFormats.vue'
|
||
|
||
const embeddingStore = useEmbeddingStore()
|
||
|
||
const configFormRef = ref<InstanceType<typeof EmbeddingConfigForm>>()
|
||
const saving = ref(false)
|
||
const pageLoading = ref(false)
|
||
|
||
const providers = computed(() => embeddingStore.providers)
|
||
const currentConfig = computed(() => embeddingStore.currentConfig)
|
||
const currentProvider = computed(() => embeddingStore.currentProvider)
|
||
const configSchema = computed(() => embeddingStore.configSchema)
|
||
const providersLoading = computed(() => embeddingStore.providersLoading)
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
|
||
const handleProviderChange = (provider: any) => {
|
||
if (provider) {
|
||
embeddingStore.setProvider(provider.name)
|
||
}
|
||
}
|
||
|
||
const handleSave = async () => {
|
||
if (!currentConfig.value.provider) {
|
||
ElMessage.warning('请先选择嵌入模型提供者')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const valid = await configFormRef.value?.validate()
|
||
if (!valid) {
|
||
return
|
||
}
|
||
} catch (error) {
|
||
ElMessage.warning('请检查配置表单中的必填项')
|
||
return
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
const response: any = await embeddingStore.saveCurrentConfig()
|
||
ElMessage.success('配置保存成功')
|
||
|
||
if (response?.warning || response?.requires_reindex) {
|
||
ElMessageBox.alert(
|
||
response.warning || '嵌入模型已更改,请重新上传文档以确保检索效果正常。',
|
||
'重要提示',
|
||
{
|
||
confirmButtonText: '我知道了',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('配置保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const handleReset = async () => {
|
||
try {
|
||
await ElMessageBox.confirm('确定要重置配置吗?将恢复为当前保存的配置。', '确认重置', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
await embeddingStore.loadConfig()
|
||
ElMessage.success('配置已重置')
|
||
} catch (error) {
|
||
}
|
||
}
|
||
|
||
const initPage = async () => {
|
||
pageLoading.value = true
|
||
try {
|
||
await Promise.all([
|
||
embeddingStore.loadProviders(),
|
||
embeddingStore.loadConfig(),
|
||
embeddingStore.loadFormats()
|
||
])
|
||
} catch (error) {
|
||
ElMessage.error('初始化页面失败')
|
||
} finally {
|
||
pageLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
initPage()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.embedding-config-page {
|
||
padding: 24px;
|
||
min-height: calc(100vh - 60px);
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
animation: slideDown 0.4s ease-out;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-16px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.title-section {
|
||
flex: 1;
|
||
min-width: 300px;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0 0 8px 0;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
.page-desc {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.update-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
background-color: var(--bg-tertiary);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.update-icon {
|
||
font-size: 14px;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.config-card {
|
||
animation: fadeInUp 0.5s ease-out;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.icon-wrapper {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--primary-lighter);
|
||
border-radius: 10px;
|
||
color: var(--primary-color);
|
||
font-size: 18px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.card-content {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.provider-select-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.section-label .el-icon {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.provider-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
padding: 14px 16px;
|
||
background-color: var(--bg-tertiary);
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.6;
|
||
border-left: 3px solid var(--primary-color);
|
||
}
|
||
|
||
.info-icon {
|
||
margin-top: 2px;
|
||
color: var(--primary-color);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.info-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.config-form-section {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
padding-right: 8px;
|
||
}
|
||
|
||
.config-form-section::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.config-form-section::-webkit-scrollbar-track {
|
||
background: var(--bg-tertiary);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.config-form-section::-webkit-scrollbar-thumb {
|
||
background: var(--text-tertiary);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.config-form-section::-webkit-scrollbar-thumb:hover {
|
||
background: var(--text-secondary);
|
||
}
|
||
|
||
.card-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.right-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.formats-card {
|
||
animation: fadeInUp 0.6s ease-out;
|
||
}
|
||
|
||
.empty-icon {
|
||
width: 100px;
|
||
height: 100px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--bg-tertiary);
|
||
border-radius: 50%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.empty-icon .el-icon {
|
||
font-size: 48px;
|
||
color: var(--text-tertiary);
|
||
}
|
||
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.slide-fade-enter-active {
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.slide-fade-leave-active {
|
||
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
|
||
}
|
||
|
||
.slide-fade-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(-16px);
|
||
}
|
||
|
||
.slide-fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(16px);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.embedding-config-page {
|
||
padding: 16px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.header-content {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.title-section {
|
||
min-width: 100%;
|
||
}
|
||
|
||
.config-form-section {
|
||
max-height: 300px;
|
||
}
|
||
}
|
||
</style>
|