知识库创建

This commit is contained in:
2025-11-06 19:08:20 +08:00
parent 0bb4853d54
commit d9947e273c
23 changed files with 2563 additions and 1018 deletions

View File

@@ -0,0 +1,556 @@
<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>

View File

@@ -0,0 +1,250 @@
<template>
<div class="knowledge-card" @click="handleClick">
<div class="card-header">
<!-- 头像 -->
<div class="knowledge-avatar">
<img v-if="knowledge.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
<div v-else class="default-avatar">📚</div>
</div>
<!-- 标题和状态 -->
<div class="knowledge-title-section">
<h3 class="knowledge-title">{{ knowledge.title }}</h3>
<span
class="knowledge-status"
:class="knowledge.status === 1 ? 'status-active' : 'status-inactive'"
>
{{ knowledge.status === 1 ? '启用' : '禁用' }}
</span>
</div>
</div>
<!-- 描述 -->
<div class="knowledge-description">
{{ knowledge.description || '暂无描述' }}
</div>
<!-- 统计信息 -->
<div class="knowledge-stats">
<div class="stat-item">
<span class="stat-label">文档数量</span>
<span class="stat-value">{{ knowledge.documentCount || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">状态</span>
<span class="stat-value">
{{ getStatusText(knowledge.status) }}
</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="knowledge-actions" @click.stop>
<el-button size="small" @click="handleEdit">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete">删除</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import type { AiKnowledge } from '@/types/ai';
import { FILE_DOWNLOAD_URL } from '@/config';
defineOptions({
name: 'KnowledgeCard'
});
interface Props {
knowledge: AiKnowledge;
}
const props = defineProps<Props>();
const emit = defineEmits(['click', 'edit', 'delete', 'sync']);
function handleClick() {
emit('click', props.knowledge);
}
function handleEdit() {
emit('edit', props.knowledge);
}
function handleDelete() {
emit('delete', props.knowledge);
}
function getStatusText(status?: number) {
switch (status) {
case 0: return '禁用';
case 1: return '启用';
case 2: return '处理中';
default: return '未知';
}
}
</script>
<style scoped lang="scss">
.knowledge-card {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 20px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #E7000B;
box-shadow: 0 4px 12px rgba(231, 0, 11, 0.12);
transform: translateY(-2px);
}
}
.card-header {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.knowledge-avatar {
width: 48px;
height: 48px;
border-radius: 12px;
overflow: hidden;
background: #F3F3F5;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.default-avatar {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
.knowledge-title-section {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.knowledge-title {
font-size: 16px;
font-weight: 500;
color: #101828;
margin: 0;
letter-spacing: -0.02em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.knowledge-status {
padding: 2px 8px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.status-active {
background: #DCFCE7;
color: #008236;
}
&.status-inactive {
background: #FEF2F2;
color: #DC2626;
}
}
.knowledge-description {
font-size: 14px;
color: #6B7280;
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
letter-spacing: -0.01em;
}
.knowledge-stats {
display: flex;
gap: 16px;
padding: 12px 0;
border-top: 1px solid #F3F3F5;
border-bottom: 1px solid #F3F3F5;
margin-bottom: 16px;
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.stat-label {
font-size: 12px;
color: #9CA3AF;
letter-spacing: -0.01em;
}
.stat-value {
font-size: 16px;
font-weight: 500;
color: #101828;
letter-spacing: -0.01em;
}
}
}
.knowledge-actions {
display: flex;
gap: 8px;
:deep(.el-button) {
flex: 1;
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
&.el-button--small {
padding: 5px 12px;
}
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
}
}
&.el-button--danger {
background: #FEF2F2;
border-color: transparent;
color: #DC2626;
&:hover {
background: #FEE2E2;
}
}
}
}
</style>

View File

@@ -0,0 +1,639 @@
<template>
<div class="knowledge-info-panel" v-if="knowledge">
<!-- 顶部导航栏 -->
<div class="panel-header">
<el-button text @click="$emit('close')">
<el-icon><ArrowLeft /></el-icon>
返回列表
</el-button>
<div class="header-actions">
<el-button @click="handleEdit">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button type="primary" @click="handleUpload">
<el-icon><Upload /></el-icon>
上传文档
</el-button>
</div>
</div>
<!-- 知识库信息卡片 -->
<div class="knowledge-header">
<div class="knowledge-avatar">
<el-image
v-if="knowledge.avatar"
:src="knowledge.avatar"
fit="cover"
/>
<el-icon v-else :size="48"><Document /></el-icon>
</div>
<div class="knowledge-details">
<h2 class="knowledge-name">{{ knowledge.title }}</h2>
<p class="knowledge-description">{{ knowledge.description || '暂无描述' }}</p>
<div class="knowledge-meta">
<el-tag :type="knowledge.status === 1 ? 'success' : 'info'" size="small">
{{ knowledge.status === 1 ? '已启用' : '已禁用' }}
</el-tag>
<span class="meta-item">
<el-icon><Clock /></el-icon>
{{ formatDate(knowledge.createTime) }}
</span>
<span class="meta-item" v-if="knowledge.difyDatasetId">
<el-icon><Link /></el-icon>
Dify: {{ knowledge.difyDatasetId }}
</span>
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<el-icon color="#409EFF"><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ knowledge.documentCount || 0 }}</div>
<div class="stat-label">文档数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon color="#67C23A"><Files /></el-icon>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon color="#E6A23C"><Paperclip /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ documents.length }}</div>
<div class="stat-label">上传文件</div>
</div>
</div>
</div>
<!-- 文档列表 -->
<div class="documents-section">
<div class="section-header">
<h3 class="section-title">文档列表</h3>
<el-input
v-model="searchQuery"
placeholder="搜索文档..."
clearable
style="width: 300px;"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<el-table
:data="filteredDocuments"
v-loading="loading"
style="width: 100%"
:empty-text="loading ? '加载中...' : '暂无文档'"
>
<el-table-column prop="fileName" label="文件名" min-width="200">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon><Document /></el-icon>
<span>{{ row.fileName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="fileType" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small">{{ row.fileType || 'unknown' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="大小" width="120">
<template #default="{ row }">
{{ formatFileSize(row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
size="small"
>
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="chunkCount" label="分段数" width="100" />
<el-table-column prop="createTime" label="上传时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewSegments(row)">
查看分段
</el-button>
<el-button link type="primary" @click="handleDownload(row)">
下载
</el-button>
<el-button link type="danger" @click="handleDeleteDocument(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 上传对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传文档到知识库"
width="600px"
:close-on-click-modal="false"
>
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
drag
multiple
accept=".txt,.md,.pdf,.doc,.docx"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持 txt/md/pdf/doc/docx 格式文件单个文件不超过50MB
</div>
</template>
</el-upload>
<template #footer>
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmUpload" :loading="uploading">
确认上传
</el-button>
</template>
</el-dialog>
<!-- 文档分段对话框 -->
<DocumentSegmentDialog
v-if="selectedDocument && selectedDocument.difyDocumentId && props.knowledge?.difyDatasetId"
:model-value="segmentDialogVisible"
:dataset-id="props.knowledge.difyDatasetId"
:document-id="selectedDocument.difyDocumentId"
@update:model-value="segmentDialogVisible = $event"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { UploadUserFile, UploadInstance } from 'element-plus';
import {
ArrowLeft,
Upload,
Refresh,
Edit,
Document,
Clock,
Link,
Files,
Paperclip,
Search,
UploadFilled
} from '@element-plus/icons-vue';
import type { AiKnowledge, AiUploadFile } from '@/types/ai';
import { fileUploadApi, knowledgeApi } from '@/apis/ai';
import { DocumentSegmentDialog } from './';
defineOptions({
name: 'KnowledgeInfo'
});
interface Props {
knowledge: AiKnowledge | null;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
refresh: [];
edit: [knowledge: AiKnowledge];
}>();
// 数据状态
const documents = ref<AiUploadFile[]>([]);
const loading = ref(false);
const searchQuery = ref('');
const uploadDialogVisible = ref(false);
const segmentDialogVisible = ref(false);
const selectedDocument = ref<AiUploadFile | null>(null);
const uploading = ref(false);
// 上传相关
const uploadRef = ref<UploadInstance>();
const fileList = ref<UploadUserFile[]>([]);
// 计算属性
const filteredDocuments = computed(() => {
if (!searchQuery.value) return documents.value;
const query = searchQuery.value.toLowerCase();
return documents.value.filter(doc =>
doc.fileName?.toLowerCase().includes(query)
);
});
// 监听知识库变化
watch(() => props.knowledge, (newVal) => {
if (newVal?.id) {
loadDocuments();
}
}, { immediate: true });
// 加载文档列表
async function loadDocuments() {
if (!props.knowledge?.id) return;
try {
loading.value = true;
const result = await fileUploadApi.listFilesByKnowledge(props.knowledge.id);
if (result.success) {
documents.value = (result.dataList || []) as AiUploadFile[];
} else {
ElMessage.error(result.message || '加载文档列表失败');
}
} catch (error: any) {
console.error('加载文档列表失败:', error);
ElMessage.error('加载文档列表失败');
} finally {
loading.value = false;
}
}
// 编辑知识库
function handleEdit() {
if (props.knowledge) {
emit('edit', props.knowledge);
}
}
// 处理上传
function handleUpload() {
uploadDialogVisible.value = true;
fileList.value = [];
}
// 文件变化
function handleFileChange(file: any, fileListParam: any) {
fileList.value = fileListParam;
}
// 确认上传
async function handleConfirmUpload() {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要上传的文件');
return;
}
if (!props.knowledge?.id) {
ElMessage.error('知识库ID不存在');
return;
}
try {
uploading.value = true;
const files = fileList.value
.map(f => f.raw)
.filter(f => f !== null && f !== undefined) as File[];
// 逐个上传文件
for (const file of files) {
const result = await fileUploadApi.uploadFile(props.knowledge.id, file);
if (!result.success) {
ElMessage.error(`文件 ${file.name} 上传失败: ${result.message}`);
}
}
ElMessage.success('文件上传成功');
uploadDialogVisible.value = false;
fileList.value = [];
await loadDocuments();
emit('refresh');
} catch (error: any) {
console.error('上传文件失败:', error);
ElMessage.error('上传文件失败');
} finally {
uploading.value = false;
}
}
// 查看分段
function handleViewSegments(document: AiUploadFile) {
selectedDocument.value = document;
segmentDialogVisible.value = true;
}
// 下载文件
function handleDownload(document: AiUploadFile) {
if (document.sysFileId) {
window.open(`/api/file/download/${document.sysFileId}`, '_blank');
} else if (document.filePath) {
window.open(`/api/file/download/${document.filePath}`, '_blank');
} else {
ElMessage.warning('文件路径不存在');
}
}
// 删除文档
async function handleDeleteDocument(document: AiUploadFile) {
try {
await ElMessageBox.confirm(
`确定要删除文档"${document.fileName}"吗?此操作不可恢复。`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
if (document.id) {
const result = await fileUploadApi.deleteFile(document.id);
if (result.success) {
ElMessage.success('删除成功');
await loadDocuments();
emit('refresh');
} else {
ElMessage.error(result.message || '删除失败');
}
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除文档失败:', error);
ElMessage.error('删除失败');
}
}
}
// 工具函数
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'
});
}
function formatFileSize(bytes: number | undefined): string {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
function getStatusText(status: number | undefined): string {
switch (status) {
case 1:
return '处理中';
case 2:
return '已完成';
case 3:
return '失败';
default:
return '未知';
}
}
</script>
<style scoped lang="scss">
.knowledge-info-panel {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
border-radius: 14px;
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 10;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #F3F3F5;
background: #FFFFFF;
.header-actions {
display: flex;
gap: 12px;
}
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
}
}
}
}
.knowledge-header {
display: flex;
gap: 20px;
padding: 24px;
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
border-bottom: 1px solid #F3F3F5;
.knowledge-avatar {
width: 80px;
height: 80px;
border-radius: 12px;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
:deep(.el-image) {
width: 100%;
height: 100%;
border-radius: 12px;
}
.el-icon {
color: #E7000B;
}
}
.knowledge-details {
flex: 1;
min-width: 0;
.knowledge-name {
font-size: 24px;
font-weight: 600;
color: #101828;
margin: 0 0 8px 0;
letter-spacing: -0.02em;
}
.knowledge-description {
font-size: 14px;
color: #667085;
margin: 0 0 12px 0;
line-height: 1.5;
}
.knowledge-meta {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #667085;
.el-icon {
font-size: 14px;
}
}
}
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
padding: 24px;
border-bottom: 1px solid #F3F3F5;
.stat-card {
background: #F8F9FA;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
}
}
.stat-content {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: 600;
color: #101828;
line-height: 1.2;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #667085;
}
}
}
}
.documents-section {
flex: 1;
padding: 24px;
overflow-y: auto;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #101828;
margin: 0;
}
}
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
.el-icon {
color: #E7000B;
font-size: 16px;
}
}
}
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
.el-table__header th {
background: #F8F9FA;
color: #667085;
font-weight: 500;
}
}
:deep(.el-dialog) {
border-radius: 12px;
.el-dialog__header {
border-bottom: 1px solid #F3F3F5;
padding: 20px 24px;
}
.el-dialog__body {
padding: 24px;
}
.el-upload-dragger {
border-radius: 8px;
}
}
</style>

View File

@@ -0,0 +1,4 @@
export { default as KnowledgeCard } from './KnowledgeCard.vue';
export { default as KnowledgeInfo } from './KnowledgeInfo.vue';
export { default as KnowledgeBasic } from './KnowledgeBasic.vue';
export { default as DocumentSegmentDialog } from './DocumentSegmentDialog.vue';