Files
admin-ui/src/views/knowledge/index.vue
2910410219 29838b030f 添加会话模型和API Key配置功能
- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
2026-05-11 20:01:03 +08:00

1520 lines
42 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-page">
<!-- 数据集列表页 -->
<div class="knowledge-list-view" v-if="!currentknowledge">
<div class="page-header">
<div class="header-left">
<el-icon class="header-icon"><ele-Folder /></el-icon>
<span class="header-title">知识库</span>
</div>
<div class="header-actions">
<el-button type="primary" v-debounce @click="onAddknowledge">
<el-icon><ele-Plus /></el-icon>
新建知识库
</el-button>
<el-button type="success" v-debounce @click="onOpenModelConfig">
<el-icon><ele-Setting /></el-icon>
模型配置
</el-button>
</div>
</div>
<div class="knowledge-cards" v-loading="knowledgeLoading">
<!-- 数据集卡片 -->
<div
class="knowledge-card"
v-for="item in knowledgeList"
:key="item.id"
@click="onSelectknowledge(item)"
@contextmenu.prevent="onCardContextMenu($event, item)"
>
<div class="card-icon">
<span class="icon-text">{{ item.name?.charAt(0)?.toUpperCase() || 'D' }}</span>
</div>
<div class="card-info">
<div class="card-name">{{ item.name }}</div>
<div class="card-meta">{{ item.documentCount || 0 }} 个文件</div>
<div class="card-time">{{ item.createdAt }}</div>
</div>
<!-- 悬停操作按钮 -->
<div class="card-actions" @click.stop>
<el-tooltip content="重命名" placement="top">
<el-button text size="small" v-debounce @click="onRenameknowledge(item)">
<el-icon><ele-Edit /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button text size="small" type="danger" v-debounce @click="onDeleteknowledge(item)">
<el-icon><ele-Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
<!-- 查看全部卡片 -->
<div class="see-all-card" v-if="knowledgeList.length > 0">
<span>See All</span>
<el-icon><ele-ArrowRight /></el-icon>
</div>
<el-empty v-if="knowledgeList.length === 0 && !knowledgeLoading" description="暂无知识库,点击上方按钮创建" :image-size="100" />
</div>
</div>
<!-- 数据集详情页 -->
<div class="knowledge-detail-view" v-else>
<!-- 顶部导航 -->
<div class="detail-header">
<div class="header-left">
<el-breadcrumb separator=">">
<el-breadcrumb-item>
<span class="back-link" @click="onBackToList">知识库</span>
</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentknowledge.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div class="detail-body">
<!-- 左侧信息面板 -->
<div class="info-sidebar">
<div class="knowledge-profile">
<div class="profile-icon">
<span class="icon-text">{{ currentknowledge.name?.charAt(0)?.toUpperCase() || 'D' }}</span>
</div>
<div class="profile-info">
<div class="profile-name">{{ currentknowledge.name }}</div>
<div class="profile-meta">{{ currentknowledge.documentCount || 0 }} 个文件 · {{ formatFileSize(currentknowledge.totalSize || 0) }}</div>
<div class="profile-time">创建于 {{ currentknowledge.createdAt }}</div>
</div>
</div>
<!-- 功能菜单 -->
<div class="func-menu">
<div class="menu-item" :class="{ active: activeMenu === 'files' }" @click="activeMenu = 'files'">
<el-icon><ele-Document /></el-icon>
<span>文件列表</span>
</div>
<div class="menu-item" :class="{ active: activeMenu === 'search' }" @click="activeMenu = 'search'">
<el-icon><ele-Search /></el-icon>
<span>检索测试</span>
</div>
<div class="menu-item" :class="{ active: activeMenu === 'logs' }" @click="activeMenu = 'logs'">
<el-icon><ele-List /></el-icon>
<span>日志</span>
</div>
<div class="menu-item" :class="{ active: activeMenu === 'settings' }" @click="activeMenu = 'settings'">
<el-icon><ele-Setting /></el-icon>
<span>配置</span>
</div>
</div>
</div>
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 文件列表 -->
<template v-if="activeMenu === 'files'">
<div class="content-header">
<div class="header-title">
<h3>文件列表</h3>
<span class="subtitle">解析成功后才能问答哦</span>
</div>
<div class="header-actions">
<el-input v-model="searchKeyword" placeholder="搜索文件" clearable style="width: 200px" @keyup.enter="getFileList">
<template #prefix>
<el-icon><ele-Search /></el-icon>
</template>
</el-input>
<el-button type="primary" v-debounce @click="onUploadFile">
<el-icon><ele-Plus /></el-icon>
新增文件
</el-button>
</div>
</div>
<div class="file-table" v-loading="fileLoading">
<el-table :data="fileList" style="width: 100%" row-key="id" border>
<el-table-column prop="title" label="名称" min-width="200">
<template #default="{ row }">
<span class="file-link" @click="onViewDocumentDetail(row)" style="cursor: pointer; color: #409eff">{{ row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="chunkCount" label="分块数" width="80" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-switch v-model="row.statusEnabled" inline-prompt active-text="启" inactive-text="停" @change="onFileStatusChange(row)" />
</template>
</el-table-column>
<el-table-column prop="vectorStatus" label="向量化" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getVectorStatusType(row.vectorStatus)" size="small">
{{ getVectorStatusText(row.vectorStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="上传日期" width="180" />
<el-table-column label="动作" width="180" align="center">
<template #default="{ row }">
<el-button text size="small" v-debounce @click="onPreviewFile(row)">预览</el-button>
<el-button v-if="row.vectorStatus === 1" text size="small" type="primary" v-debounce @click="onGenerateVector(row)"
>生成向量</el-button
>
<el-button v-else text size="small" type="primary" v-debounce @click="onViewTaskList(row)">查看任务</el-button>
<el-button text size="small" type="danger" v-debounce @click="onDeleteFile(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="fileList.length === 0 && !fileLoading" description="暂无文件,点击上方按钮上传" />
</div>
</template>
<!-- 检索测试 -->
<template v-if="activeMenu === 'search'">
<div class="panel-card">
<h3>检索测试</h3>
<el-input v-model="searchQuery" type="textarea" :rows="3" placeholder="输入问题进行检索测试..." />
<el-button type="primary" class="mt15" v-debounce @click="onSearchTest">测试检索</el-button>
<div class="search-results mt15" v-if="searchResults.length > 0">
<h4>检索结果</h4>
<div class="result-item" v-for="(item, index) in searchResults" :key="index">
<div class="result-score">相似度: {{ (item.score * 100).toFixed(1) }}%</div>
<div class="result-content">{{ item.content }}</div>
</div>
</div>
</div>
</template>
<!-- 日志 -->
<template v-if="activeMenu === 'logs'">
<div class="panel-card">
<h3>操作日志</h3>
<el-timeline>
<el-timeline-item v-for="(log, index) in logList" :key="index" :timestamp="log.time" placement="top">
<span>{{ log.content }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-if="logList.length === 0" description="暂无日志" />
</div>
</template>
<!-- 配置 -->
<template v-if="activeMenu === 'settings'">
<div class="panel-card">
<h3>数据集配置</h3>
<el-form label-width="120px" style="max-width: 500px">
<el-form-item label="数据集名称">
<el-input v-model="currentknowledge.name" disabled />
</el-form-item>
<el-form-item label="向量模型">
<el-select v-model="settingsForm.embeddingModel" style="width: 100%">
<el-option label="text-embedding-ada-002" value="text-embedding-ada-002" />
<el-option label="bge-large-zh" value="bge-large-zh" />
<el-option label="m3e-base" value="m3e-base" />
</el-select>
</el-form-item>
<el-form-item label="分段长度">
<el-input-number v-model="settingsForm.chunkSize" :min="100" :max="2000" />
</el-form-item>
<el-form-item label="重叠长度">
<el-input-number v-model="settingsForm.chunkOverlap" :min="0" :max="500" />
</el-form-item>
<el-form-item>
<el-button type="primary" v-debounce @click="onSaveSettings">保存配置</el-button>
</el-form-item>
</el-form>
</div>
</template>
</div>
</div>
</div>
<!-- 新增/编辑数据集弹窗 -->
<el-dialog :title="knowledgeForm.id ? '编辑知识库' : '新建知识库'" v-model="showknowledgeDialog" width="500px" :close-on-click-modal="false">
<el-form ref="knowledgeFormRef" :model="knowledgeForm" :rules="knowledgeRules" label-width="100px">
<el-form-item label="名称" prop="name">
<el-input v-model="knowledgeForm.name" placeholder="请输入知识库名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="knowledgeForm.description" type="textarea" :rows="3" placeholder="请输入描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showknowledgeDialog = false">取消</el-button>
<el-button type="primary" v-debounce @click="onSaveknowledge" :loading="knowledgeSaving">确定</el-button>
</template>
</el-dialog>
<!-- 上传文件弹窗 -->
<el-dialog title="上传文件" v-model="showUploadDialog" width="600px" :close-on-click-modal="false">
<el-upload
ref="uploadRef"
class="upload-area"
drag
multiple
:auto-upload="false"
:file-list="uploadFileList"
:on-change="onUploadChange"
:on-remove="onUploadRemove"
accept=".pdf,.docx,.doc,.txt,.md,.html,.csv"
>
<el-icon class="el-icon--upload"><ele-UploadFilled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">支持 PDFWordTXTMarkdownHTMLCSV 格式</div>
</template>
</el-upload>
<template #footer>
<el-button @click="showUploadDialog = false">取消</el-button>
<el-button type="primary" v-debounce @click="onConfirmUpload" :loading="uploading" :disabled="uploadFileList.length === 0">
上传 ({{ uploadFileList.length }} 个文件)
</el-button>
</template>
</el-dialog>
<!-- 文档详情弹窗 -->
<DocumentDetailDialog
v-model="showDocumentDetailDialog"
:knowledgeId="currentknowledge?.id || ''"
:knowledgeName="currentknowledge?.name || ''"
:document="currentDocument"
/>
<!-- 模型配置弹窗 -->
<el-dialog title="模型配置" v-model="showModelConfigDialog" width="1000px" :close-on-click-modal="false">
<div class="model-config-list" v-loading="modelConfigLoading">
<el-button type="primary" style="margin-bottom: 16px" v-debounce @click="onCreateModelConfig">
<el-icon><ele-Plus /></el-icon>
创建模型配置
</el-button>
<el-table :data="modelConfigList" style="width: 100%" border>
<el-table-column prop="modelName" label="模型名称" min-width="120" />
<el-table-column prop="modelType" label="模型类型" min-width="100" />
<el-table-column prop="modelDesc" label="模型描述" min-width="150" />
<el-table-column prop="configType" label="配置类型" min-width="100" />
<el-table-column prop="createTime" label="创建时间" min-width="150">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="修改时间" min-width="150">
<template #default="{ row }">
{{ formatDateTime(row.updateTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button text size="small" type="primary" v-debounce @click="onEditModelConfig(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="modelConfigList.length === 0 && !modelConfigLoading" description="暂无模型配置" :image-size="60" />
</div>
<template #footer>
<el-button @click="showModelConfigDialog = false">关闭</el-button>
</template>
</el-dialog>
<!-- 任务列表弹窗 -->
<el-dialog title="任务列表" v-model="showTaskListDialog" width="900px" :close-on-click-modal="false">
<el-table :data="taskList" style="width: 100%" border v-loading="taskListLoading">
<el-table-column prop="taskType" label="任务类型" width="150">
<template #default="{ row }">
{{ getTaskTypeText(row.taskType) }}
</template>
</el-table-column>
<el-table-column prop="status" label="任务状态" width="120">
<template #default="{ row }">
<el-tag :type="getTaskStatusType(row.status)" size="small">
{{ getTaskStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" />
<el-table-column prop="startTime" label="开始时间" width="180">
<template #default="{ row }">
{{ row.startTime ? formatDateTime(row.startTime) : '-' }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" width="180">
<template #default="{ row }">
{{ row.endTime ? formatDateTime(row.endTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button
v-if="row.status === 'FAILED' || row.status === 'COMPLETED'"
text
size="small"
type="primary"
v-debounce
@click="onReexecuteTask(row)"
>重新执行</el-button
>
<el-button v-else text size="small" type="info" disabled>执行中</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="taskList.length === 0 && !taskListLoading" description="暂无任务" :image-size="60" />
<template #footer>
<el-button @click="showTaskListDialog = false">关闭</el-button>
</template>
</el-dialog>
<!-- 创建/编辑模型配置弹窗 -->
<el-dialog :title="isEditMode ? '编辑模型配置' : '创建模型配置'" v-model="showCreateModelDialog" width="600px" :close-on-click-modal="false">
<div v-loading="modelEnumsLoading || modelFormLoading">
<el-form :model="modelFormData" label-width="100px">
<!-- 模型类型选择 -->
<el-form-item label="模型类型" required>
<el-select v-model="selectedModelType" style="width: 100%" @change="onModelTypeChange">
<el-option v-for="item in modelEnums" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-form-item>
<!-- 配置类型选择 -->
<el-form-item label="配置类型" required>
<el-select v-model="selectedConfigType" style="width: 100%" @change="onConfigTypeChange" :disabled="!selectedModelType">
<el-option v-for="item in getConfigTypes()" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-form-item>
<!-- 动态表单字段 -->
<template v-if="modelFormFields.length > 0">
<el-form-item v-for="(field, index) in modelFormFields" :key="field.name || index" :label="field.label" :required="field.required">
<template v-if="field.type === 'textarea'">
<el-input
v-model="modelFormData[field.name]"
type="textarea"
:rows="3"
:placeholder="field.placeholder"
:disabled="field.disabled"
style="width: 100%"
/>
</template>
<template v-else-if="field.type === 'switch'">
<el-switch v-model="modelFormData[field.name]" :disabled="field.disabled" />
</template>
<template v-else>
<el-input v-model="modelFormData[field.name]" :placeholder="field.placeholder" :disabled="field.disabled" style="width: 100%" />
</template>
</el-form-item>
</template>
<!-- 无表单字段提示 -->
<el-empty v-else description="暂无表单字段" :image-size="60" />
</el-form>
</div>
<template #footer>
<el-button @click="showCreateModelDialog = false">取消</el-button>
<el-button type="primary" v-debounce @click="onSaveModelConfig" :disabled="!selectedModelType || !selectedConfigType"> 保存 </el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
export default {
name: 'knowledge',
};
</script>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules, UploadFile } from 'element-plus';
import DocumentDetailDialog from './component/documentDetailDialog.vue';
import { listknowledges, createknowledge, updateknowledge, deleteknowledge } from '/@/api/knowledge/dataset';
import {
listDocuments,
uploadFile,
createDocument,
deleteDocument,
updateDocument,
generateVector,
getDocument,
listTasks,
reexecuteTask,
} from '/@/api/knowledge/document';
import { listModelConfigs, createModelConfig, updateModelConfig, getModelConfig, getAllModelEnums, getModelFormField } from '/@/api/knowledge/model';
// 数据集相关
const knowledgeLoading = ref(false);
const knowledgeList = ref<any[]>([]);
const currentknowledge = ref<any>(null);
const showknowledgeDialog = ref(false);
const knowledgeSaving = ref(false);
// 模型配置相关
const showModelConfigDialog = ref(false);
const modelConfigList = ref<any[]>([]);
const modelConfigLoading = ref(false);
// 任务列表相关
const showTaskListDialog = ref(false);
const taskList = ref<any[]>([]);
const taskListLoading = ref(false);
// 创建模型配置相关
const showCreateModelDialog = ref(false);
const modelEnums = ref<any[]>([]);
const selectedModelType = ref('');
const selectedConfigType = ref('');
const modelFormFields = ref<any[]>([]);
const modelFormData = ref<any>({});
const modelFormLoading = ref(false);
const modelEnumsLoading = ref(false);
const isEditMode = ref(false);
const currentModelId = ref<number | null>(null);
const knowledgeFormRef = ref<FormInstance>();
const knowledgeForm = reactive({
id: '',
name: '',
description: '',
});
const knowledgeRules = reactive<FormRules>({
name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
});
// 文件列表含OSS上传结果
interface UploadFileItem {
file: UploadFile;
filePath: string;
fileSize: number;
fileFormat: string;
fileName: string;
uploading: boolean;
error: boolean;
}
const uploadFileItems = ref<UploadFileItem[]>([]);
// 文件相关
const fileLoading = ref(false);
const fileList = ref<any[]>([]);
const searchKeyword = ref('');
const showUploadDialog = ref(false);
const uploadFileList = ref<UploadFile[]>([]);
const uploading = ref(false);
// 文档详情弹窗
const showDocumentDetailDialog = ref(false);
const currentDocument = ref<any>(null);
// 菜单相关
const activeMenu = ref('files');
// 检索测试
const searchQuery = ref('');
const searchResults = ref<any[]>([]);
// 日志
const logList = ref<any[]>([]);
// 配置
const settingsForm = reactive({
embeddingModel: 'text-embedding-ada-002',
chunkSize: 500,
chunkOverlap: 50,
});
// 格式化文件大小
const formatFileSize = (size: number) => {
if (!size) return '0 B';
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
return (size / 1024 / 1024).toFixed(1) + ' MB';
};
// 获取文件图标颜色
const _getFileIconColor = (fileType: string) => {
const colors: Record<string, string> = {
pdf: '#f56c6c',
docx: '#409eff',
doc: '#409eff',
txt: '#909399',
md: '#67c23a',
html: '#e6a23c',
csv: '#67c23a',
};
return colors[fileType] || '#909399';
};
// 获取解析状态类型
const _getParseStatusType = (status: string) => {
const types: Record<string, string> = {
general: 'success',
pending: 'warning',
failed: 'danger',
};
return types[status] || 'info';
};
// 获取数据集列表
const getknowledgeList = async () => {
knowledgeLoading.value = true;
try {
const response = await listknowledges({
pageNum: 1,
pageSize: 100,
});
knowledgeList.value = response.data.list || [];
} catch (_error) {
// 错误已由全局拦截器处理
} finally {
knowledgeLoading.value = false;
}
};
// 选择数据集
const onSelectknowledge = (item: any) => {
currentknowledge.value = item;
activeMenu.value = 'files';
getFileList();
getLogList();
};
// 新增数据集
const onAddknowledge = () => {
knowledgeForm.id = '';
knowledgeForm.name = '';
knowledgeForm.description = '';
showknowledgeDialog.value = true;
};
// 返回列表
const onBackToList = () => {
currentknowledge.value = null;
};
// 右键菜单
const onCardContextMenu = (event: MouseEvent, item: any) => {
// 可以在这里实现右键菜单,暂时用悬停按钮代替
};
// 重命名数据集
const onRenameknowledge = (item: any) => {
knowledgeForm.id = item.id;
knowledgeForm.name = item.name;
knowledgeForm.description = item.description || '';
showknowledgeDialog.value = true;
};
// 删除数据集
const onDeleteknowledge = (item: any) => {
ElMessageBox.confirm(`确定要删除知识库【${item.name}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
await deleteknowledge(item.id);
ElMessage.success('删除成功');
if (currentknowledge.value?.id === item.id) {
currentknowledge.value = null;
}
getknowledgeList();
} catch (_error) {
ElMessage.error('删除失败,请重试');
}
})
.catch(() => {});
};
// 保存数据集
const onSaveknowledge = async () => {
const form = knowledgeFormRef.value;
if (!form) return;
form.validate(async (valid: boolean) => {
if (valid) {
knowledgeSaving.value = true;
try {
if (knowledgeForm.id) {
// 更新知识库
await updateknowledge({
id: knowledgeForm.id,
name: knowledgeForm.name,
description: knowledgeForm.description,
});
} else {
// 创建知识库
await createknowledge({
name: knowledgeForm.name,
description: knowledgeForm.description,
});
}
ElMessage.success(knowledgeForm.id ? '保存成功' : '创建成功');
showknowledgeDialog.value = false;
getknowledgeList();
} catch (_error) {
// 错误已由全局拦截器处理
} finally {
knowledgeSaving.value = false;
}
}
});
};
// 获取文件列表
const getFileList = async () => {
if (!currentknowledge.value) return;
fileLoading.value = true;
try {
const response = await listDocuments({
datasetId: currentknowledge.value.id,
...(searchKeyword.value ? { keyword: searchKeyword.value } : {}),
pageNum: 1,
pageSize: 100,
});
fileList.value = (response.data?.list || []).map((item: any) => ({
...item,
statusEnabled: item.status === 1,
}));
} catch (_error) {
// 错误已由全局拦截器处理
} finally {
fileLoading.value = false;
}
};
// 上传文件
const onUploadFile = () => {
uploadFileItems.value = [];
uploadFileList.value = [];
showUploadDialog.value = true;
};
// 选择文件时立即上传到OSS
const onUploadChange = async (file: UploadFile, files: UploadFile[]) => {
uploadFileList.value = files;
// 找出新增的文件(还没有对应的 item
const exists = uploadFileItems.value.some((i) => i.file.uid === file.uid);
if (exists || !file.raw) return;
const item: UploadFileItem = {
file,
filePath: '',
fileSize: 0,
fileFormat: '',
fileName: file.name,
uploading: true,
error: false,
};
uploadFileItems.value.push(item);
try {
const ossRes = await uploadFile(file.raw as File);
item.filePath = ossRes.data?.fileURL || '';
item.fileSize = ossRes.data?.fileSize || file.size || 0;
item.fileFormat = ossRes.data?.fileFormat || file.name.split('.').pop() || '';
item.fileName = ossRes.data?.fileName || file.name;
item.uploading = false;
} catch (_e) {
item.uploading = false;
item.error = true;
ElMessage.error(`${file.name} 上传失败`);
}
};
// 移除上传文件
const onUploadRemove = (file: UploadFile, files: UploadFile[]) => {
uploadFileList.value = files;
uploadFileItems.value = uploadFileItems.value.filter((i) => i.file.uid !== file.uid);
};
// 确认上传所有文件已上传OSS直接创建文档
const onConfirmUpload = async () => {
const readyItems = uploadFileItems.value.filter((i) => !i.uploading && !i.error && i.filePath);
if (readyItems.length === 0) {
ElMessage.warning('请等待文件上传完成或移除上传失败的文件');
return;
}
uploading.value = true;
try {
for (const item of readyItems) {
const ext = item.file.name.split('.').pop() || '';
await createDocument({
datasetId: currentknowledge.value.id,
filePath: item.filePath,
fileSize: item.fileSize || item.file.size || 0,
format: item.fileFormat || ext,
title: item.fileName || item.file.name,
});
}
ElMessage.success(`成功创建 ${readyItems.length} 个文件`);
showUploadDialog.value = false;
getFileList();
} catch (_error) {
// 错误已由全局拦截器处理
} finally {
uploading.value = false;
}
};
// 文件状态变化
const onFileStatusChange = async (row: any) => {
const newStatus = row.statusEnabled ? 1 : 0;
try {
// 调用后端API来更新文件状态
await updateDocument({
id: row.id,
status: newStatus,
});
ElMessage.success(row.statusEnabled ? '已启用' : '已禁用');
} catch (error) {
// 失败时恢复原状态
row.statusEnabled = !row.statusEnabled;
// 错误已由全局拦截器处理
}
};
// 生成向量
const onGenerateVector = async (row: any) => {
try {
// 调用后端API来生成向量传递id和datasetId
await generateVector(row.id, currentknowledge.value.id);
ElMessage.success('生成向量任务已提交');
// 模拟更新状态
setTimeout(() => {
getFileList();
}, 1000);
} catch (error) {
// 错误已由全局拦截器处理
}
};
// 查看文档详情
const onViewDocumentDetail = async (row: any) => {
try {
// 调用getDocument接口获取最新的文件详情
const response = await getDocument(row.id);
currentDocument.value = response.data;
showDocumentDetailDialog.value = true;
} catch (error) {
// 错误已由全局拦截器处理
}
};
// 预览文件
const onPreviewFile = (row: any) => {
onViewDocumentDetail(row);
};
// 下载文件
const _onDownloadFile = (row: any) => {
ElMessage.info(`下载文件: ${row.name}`);
};
// 删除文件
const onDeleteFile = (row: any) => {
ElMessageBox.confirm(`确定要删除文件【${row.name}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
await deleteDocument(row.id);
ElMessage.success('删除成功');
getFileList();
} catch (_error) {
ElMessage.error('删除失败,请重试');
}
})
.catch(() => {});
};
// 检索测试
const onSearchTest = () => {
if (!searchQuery.value.trim()) {
ElMessage.warning('请输入检索内容');
return;
}
// 模拟检索结果
searchResults.value = [
{ score: 0.92, content: '这是检索到的第一条相关内容...' },
{ score: 0.85, content: '这是检索到的第二条相关内容...' },
{ score: 0.78, content: '这是检索到的第三条相关内容...' },
];
};
// 获取日志列表
const getLogList = () => {
logList.value = [
{ time: '2026-01-21 16:53:32', content: '上传文件 456_product(1).txt' },
{ time: '2026-01-21 16:53:26', content: '上传文件 123_speech(1).txt' },
{ time: '2026-01-21 14:39:41', content: '上传文件 123_product.txt' },
{ time: '2026-01-17 10:00:00', content: '创建知识库 knowledge_tenant_1' },
];
};
// 保存配置
const onSaveSettings = async () => {
ElMessage.success('保存成功');
};
// 打开模型配置弹窗
const onOpenModelConfig = async () => {
await getModelConfigList();
showModelConfigDialog.value = true;
};
// 打开创建模型配置弹窗
const onCreateModelConfig = async () => {
// 重置状态
selectedModelType.value = '';
selectedConfigType.value = '';
modelFormFields.value = [];
modelFormData.value = {};
isEditMode.value = false;
currentModelId.value = null;
// 获取模型类型和配置类型枚举
await getModelEnums();
// 打开创建弹窗
showCreateModelDialog.value = true;
};
// 编辑模型配置
const onEditModelConfig = async (row: any) => {
try {
// 重置状态
selectedModelType.value = '';
selectedConfigType.value = '';
modelFormFields.value = [];
modelFormData.value = {};
isEditMode.value = true;
currentModelId.value = row.id;
// 获取模型类型和配置类型枚举
await getModelEnums();
// 调用获取详情接口
const response = await getModelConfig(row.id, row.modelType);
const modelData = response.data;
// 设置模型类型和配置类型
selectedModelType.value = modelData.modelType;
selectedConfigType.value = modelData.configType;
// 填充表单数据
modelFormData.value = {
modelName: modelData.modelName,
modelDesc: modelData.modelDesc,
};
// 将configContent中的数据添加到表单数据中
if (modelData.configContent) {
Object.keys(modelData.configContent).forEach((key) => {
modelFormData.value[key] = modelData.configContent[key];
});
}
// 获取动态表单字段
await getModelFormFields();
// 打开弹窗
showCreateModelDialog.value = true;
} catch (error) {
// 错误已由全局拦截器处理
}
};
// 获取模型类型和配置类型枚举
const getModelEnums = async () => {
modelEnumsLoading.value = true;
try {
const response = await getAllModelEnums();
modelEnums.value = response.data?.options || [];
} catch (error) {
// 错误已由全局拦截器处理
modelEnums.value = [];
} finally {
modelEnumsLoading.value = false;
}
};
// 模型类型选择变化
const onModelTypeChange = async () => {
selectedConfigType.value = '';
modelFormFields.value = [];
// 在编辑模式下,只保留模型名称和描述,清空其他字段
if (isEditMode.value) {
const { modelName, modelDesc } = modelFormData.value;
modelFormData.value = {
modelName,
modelDesc,
};
} else {
// 创建模式下清空所有字段
modelFormData.value = {};
}
};
// 配置类型选择变化
const onConfigTypeChange = async () => {
if (selectedModelType.value && selectedConfigType.value) {
await getModelFormFields();
}
};
// 获取模型表单字段
const getModelFormFields = async () => {
modelFormLoading.value = true;
try {
const response = await getModelFormField(selectedModelType.value, selectedConfigType.value);
// 过滤掉模型类型和配置类型字段,避免重复显示
modelFormFields.value = (response.data?.fields || []).filter((field: any) => {
return field.name !== 'modelType' && field.name !== 'configType';
});
// 设置字段的默认值,但保留已有的表单数据
modelFormFields.value.forEach((field: any) => {
if (field.value !== undefined && modelFormData.value[field.name] === undefined) {
modelFormData.value[field.name] = field.value;
}
});
} catch (error) {
// 错误已由全局拦截器处理
modelFormFields.value = [];
} finally {
modelFormLoading.value = false;
}
};
// 保存模型配置
const onSaveModelConfig = async () => {
try {
// 构建请求数据,只传递接口需要的字段
const data = {
modelType: selectedModelType.value,
configType: selectedConfigType.value,
modelName: modelFormData.value.modelName,
modelDesc: modelFormData.value.modelDesc,
configContent: {} as Record<string, any>,
};
// 将动态表单字段除了modelType、configType、modelName、modelDesc添加到configContent中以key-value形式
Object.keys(modelFormData.value).forEach((key) => {
if (!['modelType', 'configType', 'modelName', 'modelDesc'].includes(key)) {
data.configContent[key] = modelFormData.value[key];
}
});
// 根据模式调用不同的接口
if (isEditMode.value && currentModelId.value) {
// 编辑模式,调用更新接口
await updateModelConfig({ ...data, id: currentModelId.value });
ElMessage.success('更新模型配置成功');
} else {
// 创建模式,调用创建接口
await createModelConfig(data);
ElMessage.success('创建模型配置成功');
}
// 关闭弹窗并刷新列表
showCreateModelDialog.value = false;
getModelConfigList();
} catch (error) {
// 错误已由全局拦截器处理
}
};
// 根据选中的模型类型获取配置类型列表
const getConfigTypes = () => {
if (!selectedModelType.value) {
return [];
}
const selectedModel = modelEnums.value.find((item: any) => item.key === selectedModelType.value);
return selectedModel?.configTypes || [];
};
// 获取模型配置列表
const getModelConfigList = async () => {
modelConfigLoading.value = true;
try {
const response = await listModelConfigs({
pageNum: 1,
pageSize: 100,
});
modelConfigList.value = response.data?.list || [];
} catch (error) {
// 错误已由全局拦截器处理
modelConfigList.value = [];
} finally {
modelConfigLoading.value = false;
}
};
// 格式化时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-';
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 获取向量状态文本
const getVectorStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '待处理',
2: '处理中',
3: '已完成',
4: '失败',
};
return statusMap[status] || '未知';
};
// 获取向量状态标签类型
const getVectorStatusType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'warning',
2: 'primary',
3: 'success',
4: 'danger',
};
return typeMap[status] || 'info';
};
// 获取任务类型文本
const getTaskTypeText = (taskType: string) => {
const typeMap: Record<string, string> = {
EXTRACT_KEYWORDS: '提取关键词',
GENERATE_VECTOR: '生成向量',
FULL_TEXT_SEARCH: '全文检索',
DOC_PARSE: '文档解析',
};
return typeMap[taskType] || taskType;
};
// 获取任务状态文本
const getTaskStatusText = (status: string) => {
const statusMap: Record<string, string> = {
PENDING: '待执行',
RUNNING: '执行中',
COMPLETED: '已完成',
FAILED: '执行失败',
};
return statusMap[status] || status;
};
// 获取任务状态标签类型
const getTaskStatusType = (status: string) => {
const typeMap: Record<string, any> = {
PENDING: 'warning',
RUNNING: 'primary',
COMPLETED: 'success',
FAILED: 'danger',
};
return typeMap[status] || 'info';
};
// 查看任务列表
const onViewTaskList = async (row: any) => {
showTaskListDialog.value = true;
await getTaskList();
};
// 获取任务列表
const getTaskList = async () => {
taskListLoading.value = true;
try {
const response = await listTasks();
taskList.value = response.data?.list || [];
} catch (error) {
// 错误已由全局拦截器处理
taskList.value = [];
} finally {
taskListLoading.value = false;
}
};
// 重新执行任务
const onReexecuteTask = async (task: any) => {
ElMessageBox.confirm(`确定要重新执行任务【${getTaskTypeText(task.taskType)}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
try {
await reexecuteTask(task.id);
ElMessage.success('重新执行任务成功');
// 重新获取任务列表
await getTaskList();
} catch (error) {
// 错误已由全局拦截器处理
}
})
.catch(() => {});
};
// 页面加载
onMounted(() => {
getknowledgeList();
});
</script>
<style scoped lang="scss">
.knowledge-page {
height: 100%;
padding: 15px;
box-sizing: border-box;
// 数据集列表页
.knowledge-list-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.header-left {
display: flex;
align-items: center;
.header-icon {
font-size: 24px;
color: #409eff;
margin-right: 10px;
}
.header-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
}
}
.knowledge-cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
.knowledge-card {
width: 200px;
padding: 16px;
background: #fff;
border-radius: 8px;
border: 1px solid #ebeef5;
cursor: pointer;
transition: all 0.3s;
position: relative;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
.card-actions {
opacity: 1;
}
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
.icon-text {
color: #fff;
font-size: 18px;
font-weight: 600;
}
}
.card-info {
.card-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-meta {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.card-time {
font-size: 11px;
color: #c0c4cc;
}
}
.card-actions {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.2s;
display: flex;
gap: 4px;
background: #fff;
padding: 4px;
border-radius: 4px;
}
}
.see-all-card {
width: 200px;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
border: 1px dashed #dcdfe6;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #909399;
transition: all 0.3s;
&:hover {
border-color: #409eff;
color: #409eff;
}
}
}
}
// 数据集详情页
.knowledge-detail-view {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.detail-header {
padding: 12px 20px;
border-bottom: 1px solid #ebeef5;
.back-link {
color: #409eff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.detail-body {
flex: 1;
display: flex;
overflow: hidden;
// 左侧信息面板
.info-sidebar {
width: 200px;
border-right: 1px solid #ebeef5;
display: flex;
flex-direction: column;
background: #fafafa;
.knowledge-profile {
padding: 20px 16px;
border-bottom: 1px solid #ebeef5;
.profile-icon {
width: 48px;
height: 48px;
border-radius: 8px;
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
.icon-text {
color: #fff;
font-size: 20px;
font-weight: 600;
}
}
.profile-info {
.profile-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
word-break: break-all;
}
.profile-meta {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.profile-time {
font-size: 11px;
color: #c0c4cc;
}
}
}
.func-menu {
flex: 1;
padding: 12px 0;
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
color: #606266;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
.el-icon {
margin-right: 8px;
font-size: 16px;
}
&:hover {
color: #409eff;
background: #f0f0f0;
}
&.active {
color: #409eff;
background: #ecf5ff;
border-left: 3px solid #409eff;
font-weight: 500;
}
}
}
}
// 右侧主内容
.main-content {
flex: 1;
padding: 20px;
overflow: auto;
background: #f5f7fa;
.content-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
.header-title {
h3 {
margin: 0;
color: #303133;
font-size: 18px;
font-weight: 600;
}
.subtitle {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
}
.header-actions {
display: flex;
gap: 12px;
}
}
.file-table {
background: #fff;
border-radius: 4px;
padding: 16px;
.file-name {
display: flex;
align-items: center;
cursor: pointer;
.file-icon {
margin-right: 8px;
font-size: 16px;
}
.file-link {
color: #409eff;
&:hover {
text-decoration: underline;
}
}
}
}
.panel-card {
background: #fff;
border-radius: 4px;
padding: 20px;
h3 {
color: #303133;
margin-top: 0;
margin-bottom: 20px;
font-size: 16px;
font-weight: 600;
}
h4 {
color: #303133;
margin-bottom: 12px;
}
}
.search-results {
.result-item {
background: #f5f7fa;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 12px;
border: 1px solid #ebeef5;
.result-score {
color: #67c23a;
font-size: 12px;
margin-bottom: 8px;
font-weight: 500;
}
.result-content {
color: #606266;
line-height: 1.6;
}
}
}
}
}
}
}
.upload-area {
width: 100%;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
}
}
</style>