505 lines
12 KiB
Vue
505 lines
12 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">
|
|||
|
|
<el-tag v-if="currentConfig.updated_at" type="info" size="large" effect="plain">
|
|||
|
|
<el-icon class="tag-icon"><Clock /></el-icon>
|
|||
|
|
上次更新: {{ formatDate(currentConfig.updated_at) }}
|
|||
|
|
</el-tag>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-row :gutter="24" v-loading="pageLoading" element-loading-text="加载中...">
|
|||
|
|
<el-col :xs="24" :sm="24" :md="12" :lg="12">
|
|||
|
|
<div class="config-card-wrapper">
|
|||
|
|
<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>
|
|||
|
|
</div>
|
|||
|
|
</el-col>
|
|||
|
|
|
|||
|
|
<el-col :xs="24" :sm="24" :md="12" :lg="12">
|
|||
|
|
<div class="right-column">
|
|||
|
|
<div class="test-panel-wrapper">
|
|||
|
|
<EmbeddingTestPanel
|
|||
|
|
:config="{ provider: currentConfig.provider, config: currentConfig.config }"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<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 {
|
|||
|
|
await embeddingStore.saveCurrentConfig()
|
|||
|
|
ElMessage.success('配置保存成功')
|
|||
|
|
} 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;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
min-height: calc(100vh - 60px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header {
|
|||
|
|
margin-bottom: 32px;
|
|||
|
|
animation: slideDown 0.6s ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideDown {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(-20px);
|
|||
|
|
}
|
|||
|
|
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 12px 0;
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #ffffff;
|
|||
|
|
letter-spacing: -0.5px;
|
|||
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-desc {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: rgba(255, 255, 255, 0.85);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-actions {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tag-icon {
|
|||
|
|
margin-right: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-card-wrapper,
|
|||
|
|
.test-panel-wrapper,
|
|||
|
|
.formats-card {
|
|||
|
|
animation: fadeInUp 0.6s ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fadeInUp {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(30px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-card {
|
|||
|
|
border-radius: 16px;
|
|||
|
|
border: none;
|
|||
|
|
background: rgba(255, 255, 255, 0.98);
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-card:hover {
|
|||
|
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
|
|||
|
|
transform: translateY(-4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
color: #ffffff;
|
|||
|
|
font-size: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #303133;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-label .el-icon {
|
|||
|
|
color: #667eea;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.provider-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
padding: 14px 16px;
|
|||
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #606266;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
border-left: 3px solid #667eea;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-icon {
|
|||
|
|
margin-top: 2px;
|
|||
|
|
color: #667eea;
|
|||
|
|
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: #f1f1f1;
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-form-section::-webkit-scrollbar-thumb {
|
|||
|
|
background: #c0c4cc;
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-form-section::-webkit-scrollbar-thumb:hover {
|
|||
|
|
background: #a0a4ac;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-footer {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
gap: 12px;
|
|||
|
|
padding-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-column {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.formats-card {
|
|||
|
|
border-radius: 16px;
|
|||
|
|
border: none;
|
|||
|
|
background: rgba(255, 255, 255, 0.98);
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.formats-card:hover {
|
|||
|
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
|
|||
|
|
transform: translateY(-4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-icon {
|
|||
|
|
width: 120px;
|
|||
|
|
height: 120px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-icon .el-icon {
|
|||
|
|
font-size: 60px;
|
|||
|
|
color: #c0c4cc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fade-enter-active,
|
|||
|
|
.fade-leave-active {
|
|||
|
|
transition: opacity 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fade-enter-from,
|
|||
|
|
.fade-leave-to {
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.slide-fade-enter-active {
|
|||
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.slide-fade-leave-active {
|
|||
|
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.slide-fade-enter-from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(-20px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.slide-fade-leave-to {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(20px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.embedding-config-page {
|
|||
|
|
padding: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-title {
|
|||
|
|
font-size: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title-section {
|
|||
|
|
min-width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-form-section {
|
|||
|
|
max-height: 300px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|