Files
schoolNews/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue
2025-11-06 19:08:20 +08:00

557 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="knowledge-basic-container">
<!-- 查看模式 -->
<div v-if="type === 'view'" class="view-mode">
<div class="info-section">
<div class="avatar-section">
<img v-if="knowledge?.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
<div v-else class="default-avatar">📚</div>
</div>
<div class="info-details">
<h2 class="knowledge-name">{{ knowledge?.title || '未命名' }}</h2>
<p class="knowledge-description">{{ knowledge?.description || '暂无描述' }}</p>
<div class="meta-info">
<div class="meta-item">
<span class="label">状态</span>
<el-tag :type="knowledge?.status === 1 ? 'success' : 'info'" size="small">
{{ knowledge?.status === 1 ? '已启用' : '已禁用' }}
</el-tag>
</div>
<div class="meta-item" v-if="knowledge?.difyDatasetId">
<span class="label">Dify数据集ID</span>
<span class="value">{{ knowledge.difyDatasetId }}</span>
</div>
<div class="meta-item">
<span class="label">索引方式</span>
<span class="value">{{ getIndexingText(knowledge?.difyIndexingTechnique) }}</span>
</div>
<div class="meta-item" v-if="knowledge?.difyIndexingTechnique == 'high_quality'">
<span class="label">Embedding模型</span>
<span class="value">{{ knowledge?.embeddingModel || '-' }}</span>
</div>
<div class="meta-item">
<span class="label">文档数量</span>
<span class="value">{{ knowledge?.documentCount || 0 }}</span>
</div>
<div class="meta-item">
<span class="label">创建时间</span>
<span class="value">{{ formatDate(knowledge?.createTime) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 新增/编辑模式 -->
<div v-else class="edit-mode">
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-position="top"
class="knowledge-form"
>
<el-form-item label="知识库标题" prop="title" required>
<el-input
v-model="formData.title"
placeholder="请输入知识库标题"
maxlength="100"
show-word-limit
:disabled="props.type === 'view'"
/>
</el-form-item>
<el-form-item label="知识库头像" prop="avatar">
<FileUpload
:cover-url="formData.avatar"
@update:cover-url="handleAvatarUpdate"
:as-dialog="false"
list-type="cover"
accept="image/*"
:max-size="2"
module="ai-knowledge"
tip="点击上传知识库头像"
/>
</el-form-item>
<el-form-item label="知识库描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="4"
placeholder="请输入知识库描述,介绍知识库的内容、用途等..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="索引方式" prop="difyIndexingTechnique" required>
<el-radio-group v-model="formData.difyIndexingTechnique">
<el-radio value="high_quality">
<span class="radio-label">高质量</span>
<span class="radio-desc">更精确的检索结果消耗更多tokens</span>
</el-radio>
<el-radio
value="economy"
:disabled="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'"
>
<span class="radio-label">经济</span>
<span class="radio-desc">快速检索消耗较少tokens</span>
</el-radio>
</el-radio-group>
<div v-if="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'" class="form-tip">
高质量模式不能降级为经济模式
</div>
</el-form-item>
<el-form-item v-if="formData.difyIndexingTechnique == 'high_quality'" label="Embedding模型" prop="embeddingModel">
<el-select
v-model="formData.embeddingModel"
placeholder="请选择Embedding模型可选"
clearable
filterable
:loading="modelsLoading"
@change="handleModelChange"
>
<el-option-group
v-for="provider in embeddingModels"
:key="provider.provider"
:label="provider.label || provider.provider"
>
<el-option
v-for="model in provider.models"
:key="model.model"
:label="getModelLabel(model)"
:value="model.model"
:data-provider="model.provider"
>
<span>{{ getModelLabel(model) }}</span>
<span v-if="model.contextSize" style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
上下文: {{ model.contextSize }}
</span>
</el-option>
</el-option-group>
</el-select>
<div v-if="type === 'edit'" class="form-tip form-tip-success">
可以切换Embedding模型以优化检索效果
</div>
</el-form-item>
<el-form-item label="Dify数据集ID" prop="difyDatasetId">
<el-input
v-model="formData.difyDatasetId"
placeholder="可选如需关联现有Dify数据集请填写"
:disabled="type === 'edit'"
/>
<div v-if="type === 'edit'" class="form-tip">
Dify数据集ID创建后不可修改
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="formData.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
<div class="form-actions">
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ type === 'add' ? '创建知识库' : '保存修改' }}
</el-button>
<el-button @click="handleCancel">
取消
</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { FileUpload } from '@/components/file';
import type { AiKnowledge } from '@/types/ai';
import { knowledgeApi } from '@/apis/ai';
import { FILE_DOWNLOAD_URL } from '@/config';
defineOptions({
name: 'KnowledgeBasic'
});
interface KnowledgeBasicProps {
type: 'view' | 'add' | 'edit';
knowledge?: AiKnowledge | null;
}
const props = defineProps<KnowledgeBasicProps>();
const emit = defineEmits<{
success: [knowledge: AiKnowledge];
cancel: [];
}>();
// 表单引用
const formRef = ref<FormInstance>();
const submitting = ref(false);
const modelsLoading = ref(false);
const embeddingModels = ref<any[]>([]);
// 表单数据
const formData = reactive<Partial<AiKnowledge>>({
title: '',
avatar: '',
description: '',
difyIndexingTechnique: 'high_quality',
embeddingModel: '',
embeddingModelProvider: '',
difyDatasetId: '',
status: 1
});
// 表单验证规则
const rules: FormRules = {
title: [
{ required: true, message: '请输入知识库标题', trigger: 'blur' },
{ min: 2, max: 100, message: '标题长度在2-100个字符之间', trigger: 'blur' }
],
difyIndexingTechnique: [
{ required: true, message: '请选择索引方式', trigger: 'change' }
]
};
// 监听知识库数据变化
watch(() => props.knowledge, (newVal) => {
if (newVal && (props.type === 'view' || props.type === 'edit')) {
Object.assign(formData, {
id: newVal.id,
title: newVal.title,
avatar: newVal.avatar,
description: newVal.description,
difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality',
embeddingModel: newVal.embeddingModel,
embeddingModelProvider: newVal.embeddingModelProvider,
difyDatasetId: newVal.difyDatasetId,
status: newVal.status ?? 1
});
}
}, { immediate: true });
// 处理头像更新
function handleAvatarUpdate(val: string) {
formData.avatar = val;
}
// 加载嵌入模型列表
async function loadEmbeddingModels() {
try {
modelsLoading.value = true;
const result = await knowledgeApi.getAvailableEmbeddingModels();
if (result.success && result.data) {
// 按提供商分组
const providers = result.data.providers || [];
embeddingModels.value = providers.map((provider: any) => ({
provider: provider.provider,
label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider,
models: (provider.models || []).map((model: any) => ({
model: model.model,
provider: model.provider || provider.provider, // 添加 provider 到 model 对象
label: model.label?.zh_Hans || model.label?.en_US || model.model,
contextSize: model.model_properties?.context_size,
status: model.status
}))
}));
} else {
ElMessage.warning('获取嵌入模型列表失败,使用默认选项');
// 设置默认模型列表
embeddingModels.value = [{
provider: 'openai',
label: 'OpenAI',
models: [
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
]
}];
}
} catch (error: any) {
console.error('加载嵌入模型列表失败:', error);
// 设置默认模型列表
embeddingModels.value = [{
provider: 'openai',
label: 'OpenAI',
models: [
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
]
}];
} finally {
modelsLoading.value = false;
}
}
// 获取模型显示标签
function getModelLabel(model: any): string {
return model.label || model.model;
}
// 处理模型变化
function handleModelChange(modelName: string) {
if (!modelName) {
formData.embeddingModelProvider = '';
return;
}
// 查找选中模型的提供商
for (const providerGroup of embeddingModels.value) {
const foundModel = providerGroup.models.find((m: any) => m.model === modelName);
if (foundModel) {
formData.embeddingModelProvider = foundModel.provider;
console.log('选择模型:', modelName, '提供商:', foundModel.provider);
break;
}
}
}
// 组件挂载时加载模型列表
loadEmbeddingModels();
// 提交表单
async function handleSubmit() {
if (!formRef.value) return;
try {
const valid = await formRef.value.validate();
if (!valid) return;
submitting.value = true;
if (props.type === 'add') {
// 创建知识库
const result = await knowledgeApi.createKnowledge(formData as AiKnowledge);
if (result.success && result.data) {
ElMessage.success('创建成功');
emit('success', result.data);
} else {
ElMessage.error(result.message || '创建失败');
}
} else if (props.type === 'edit') {
// 更新知识库
const result = await knowledgeApi.updateKnowledge(formData as AiKnowledge);
if (result.success && result.data) {
ElMessage.success('保存成功');
emit('success', result.data);
} else {
ElMessage.error(result.message || '保存失败');
}
}
} catch (error: any) {
console.error('提交失败:', error);
ElMessage.error(props.type === 'add' ? '创建失败' : '保存失败');
} finally {
submitting.value = false;
}
}
// 取消操作
function handleCancel() {
emit('cancel');
}
// 获取索引方式文本
function getIndexingText(technique: string | undefined): string {
switch (technique) {
case 'high_quality':
return '高质量';
case 'economy':
return '经济';
default:
return '-';
}
}
// 格式化日期
function formatDate(date: string | Date | undefined): string {
if (!date) return '-';
const d = new Date(date);
if (isNaN(d.getTime())) return '-';
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<style scoped lang="scss">
.knowledge-basic-container {
padding: 24px;
background: #FFFFFF;
border-radius: 14px;
min-height: 400px;
}
// 查看模式样式
.view-mode {
.info-section {
display: flex;
gap: 24px;
}
.avatar-section {
width: 120px;
height: 120px;
border-radius: 12px;
overflow: hidden;
background: #F8F9FA;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.default-avatar {
font-size: 48px;
}
}
.info-details {
flex: 1;
min-width: 0;
.knowledge-name {
font-size: 24px;
font-weight: 600;
color: #101828;
margin: 0 0 12px 0;
letter-spacing: -0.02em;
}
.knowledge-description {
font-size: 14px;
color: #667085;
line-height: 1.6;
margin: 0 0 24px 0;
}
.meta-info {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
.label {
color: #667085;
font-weight: 500;
}
.value {
color: #101828;
}
}
}
}
}
// 编辑模式样式
.edit-mode {
.knowledge-form {
max-width: 600px;
:deep(.el-form-item__label) {
font-weight: 500;
color: #101828;
margin-bottom: 8px;
}
:deep(.el-radio) {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
height: auto;
.el-radio__label {
display: flex;
flex-direction: column;
gap: 4px;
white-space: normal;
}
.radio-label {
font-weight: 500;
color: #101828;
}
.radio-desc {
font-size: 13px;
color: #667085;
line-height: 1.4;
}
}
.form-tip {
margin-top: 8px;
font-size: 13px;
color: #F59E0B;
line-height: 1.4;
}
.form-tip-success {
color: #67C23A;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #F3F3F5;
.el-button {
border-radius: 8px;
font-weight: 500;
padding: 12px 24px;
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
}
}
}
}
}
}
:deep(.el-switch) {
.el-switch__label {
font-size: 14px;
color: #667085;
&.is-active {
color: #E7000B;
}
}
}
</style>