1023 lines
25 KiB
Vue
1023 lines
25 KiB
Vue
<template>
|
||
<div class="knowledge-page">
|
||
<!-- 数据集列表页 -->
|
||
<div class="dataset-list-view" v-if="!currentDataset">
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<el-icon class="header-icon"><ele-Folder /></el-icon>
|
||
<span class="header-title">知识库</span>
|
||
</div>
|
||
<el-button type="primary" @click="onAddDataset">
|
||
<el-icon><ele-Plus /></el-icon>
|
||
新建知识库
|
||
</el-button>
|
||
</div>
|
||
|
||
<div class="dataset-cards" v-loading="datasetLoading">
|
||
<!-- 数据集卡片 -->
|
||
<div
|
||
class="dataset-card"
|
||
v-for="item in datasetList"
|
||
:key="item.id"
|
||
@click="onSelectDataset(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.fileCount || 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" @click="onRenameDataset(item)">
|
||
<el-icon><ele-Edit /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-tooltip content="删除" placement="top">
|
||
<el-button text size="small" type="danger" @click="onDeleteDataset(item)">
|
||
<el-icon><ele-Delete /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 查看全部卡片 -->
|
||
<div class="see-all-card" v-if="datasetList.length > 0">
|
||
<span>See All</span>
|
||
<el-icon><ele-ArrowRight /></el-icon>
|
||
</div>
|
||
|
||
<el-empty v-if="datasetList.length === 0 && !datasetLoading" description="暂无知识库,点击上方按钮创建" :image-size="100" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据集详情页 -->
|
||
<div class="dataset-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>{{ currentDataset.name }}</el-breadcrumb-item>
|
||
</el-breadcrumb>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-body">
|
||
<!-- 左侧信息面板 -->
|
||
<div class="info-sidebar">
|
||
<div class="dataset-profile">
|
||
<div class="profile-icon">
|
||
<span class="icon-text">{{ currentDataset.name?.charAt(0)?.toUpperCase() || 'D' }}</span>
|
||
</div>
|
||
<div class="profile-info">
|
||
<div class="profile-name">{{ currentDataset.name }}</div>
|
||
<div class="profile-meta">{{ currentDataset.fileCount || 0 }} 个文件 · {{ formatFileSize(currentDataset.totalSize || 0) }}</div>
|
||
<div class="profile-time">创建于 {{ currentDataset.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" @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 type="selection" width="50" align="center" />
|
||
<el-table-column prop="name" label="名称" min-width="200" sortable>
|
||
<template #default="scope">
|
||
<div class="file-name" @click="onViewDocumentDetail(scope.row)">
|
||
<el-icon class="file-icon" :style="{ color: getFileIconColor(scope.row.fileType) }">
|
||
<ele-Document />
|
||
</el-icon>
|
||
<span class="file-link">{{ scope.row.name }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="createdAt" label="上传日期" width="180" sortable />
|
||
<el-table-column prop="source" label="来源" width="80" align="center">
|
||
<template #default>
|
||
<el-icon><ele-Monitor /></el-icon>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="enabled" label="启用" width="80" align="center">
|
||
<template #default="scope">
|
||
<el-switch
|
||
v-model="scope.row.enabled"
|
||
size="small"
|
||
@change="onFileStatusChange(scope.row)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="chunkCount" label="分块数" width="80" align="center" />
|
||
<el-table-column prop="parseStatus" label="解析" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag :type="getParseStatusType(scope.row.parseStatus)" size="small">
|
||
{{ scope.row.parseStatus }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="动作" width="140" align="center">
|
||
<template #default="scope">
|
||
<el-button text size="small" @click="onPreviewFile(scope.row)">
|
||
<el-icon><ele-View /></el-icon>
|
||
</el-button>
|
||
<el-button text size="small" @click="onDownloadFile(scope.row)">
|
||
<el-icon><ele-Download /></el-icon>
|
||
</el-button>
|
||
<el-button text size="small" type="danger" @click="onDeleteFile(scope.row)">
|
||
<el-icon><ele-Delete /></el-icon>
|
||
</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" @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="currentDataset.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" @click="onSaveSettings">保存配置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑数据集弹窗 -->
|
||
<el-dialog
|
||
:title="datasetForm.id ? '编辑知识库' : '新建知识库'"
|
||
v-model="showDatasetDialog"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form ref="datasetFormRef" :model="datasetForm" :rules="datasetRules" label-width="100px">
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="datasetForm.name" placeholder="请输入知识库名称" />
|
||
</el-form-item>
|
||
<el-form-item label="描述" prop="description">
|
||
<el-input v-model="datasetForm.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showDatasetDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="onSaveDataset" :loading="datasetSaving">确定</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">支持 PDF、Word、TXT、Markdown、HTML、CSV 格式</div>
|
||
</template>
|
||
</el-upload>
|
||
<template #footer>
|
||
<el-button @click="showUploadDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="onConfirmUpload" :loading="uploading" :disabled="uploadFileList.length === 0">
|
||
上传 ({{ uploadFileList.length }} 个文件)
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 文档详情弹窗 -->
|
||
<DocumentDetailDialog
|
||
v-model="showDocumentDetailDialog"
|
||
:datasetId="currentDataset?.id || ''"
|
||
:datasetName="currentDataset?.name || ''"
|
||
:document="currentDocument"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'knowledge',
|
||
};
|
||
</script>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import type { FormInstance, FormRules, UploadFile } from 'element-plus';
|
||
import DocumentDetailDialog from './component/documentDetailDialog.vue';
|
||
|
||
const router = useRouter();
|
||
|
||
// 数据集相关
|
||
const datasetLoading = ref(false);
|
||
const datasetList = ref<any[]>([]);
|
||
const currentDataset = ref<any>(null);
|
||
const showDatasetDialog = ref(false);
|
||
const datasetSaving = ref(false);
|
||
const datasetFormRef = ref<FormInstance>();
|
||
const datasetForm = reactive({
|
||
id: '',
|
||
name: '',
|
||
description: '',
|
||
});
|
||
const datasetRules = reactive<FormRules>({
|
||
name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
|
||
});
|
||
|
||
// 文件相关
|
||
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 formatDate = (date: string) => {
|
||
if (!date) return '';
|
||
return date.split(' ')[0];
|
||
};
|
||
|
||
// 获取文件图标颜色
|
||
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 getDatasetList = async () => {
|
||
datasetLoading.value = true;
|
||
try {
|
||
// 模拟数据
|
||
datasetList.value = [
|
||
{
|
||
id: '1',
|
||
name: 'dataset_tenant_1',
|
||
fileCount: 3,
|
||
totalSize: 30,
|
||
createdAt: '28/01/2026 14:09:45',
|
||
},
|
||
];
|
||
} catch (error) {
|
||
console.error('获取数据集列表失败:', error);
|
||
} finally {
|
||
datasetLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 选择数据集
|
||
const onSelectDataset = (item: any) => {
|
||
currentDataset.value = item;
|
||
activeMenu.value = 'files';
|
||
getFileList();
|
||
getLogList();
|
||
};
|
||
|
||
// 新增数据集
|
||
const onAddDataset = () => {
|
||
datasetForm.id = '';
|
||
datasetForm.name = '';
|
||
datasetForm.description = '';
|
||
showDatasetDialog.value = true;
|
||
};
|
||
|
||
// 返回列表
|
||
const onBackToList = () => {
|
||
currentDataset.value = null;
|
||
};
|
||
|
||
// 右键菜单
|
||
const onCardContextMenu = (event: MouseEvent, item: any) => {
|
||
// 可以在这里实现右键菜单,暂时用悬停按钮代替
|
||
};
|
||
|
||
// 重命名数据集
|
||
const onRenameDataset = (item: any) => {
|
||
datasetForm.id = item.id;
|
||
datasetForm.name = item.name;
|
||
datasetForm.description = item.description || '';
|
||
showDatasetDialog.value = true;
|
||
};
|
||
|
||
// 删除数据集
|
||
const onDeleteDataset = (item: any) => {
|
||
ElMessageBox.confirm(`确定要删除知识库【${item.name}】吗?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
ElMessage.success('删除成功');
|
||
if (currentDataset.value?.id === item.id) {
|
||
currentDataset.value = null;
|
||
}
|
||
getDatasetList();
|
||
}).catch(() => {});
|
||
};
|
||
|
||
// 保存数据集
|
||
const onSaveDataset = async () => {
|
||
const form = datasetFormRef.value;
|
||
if (!form) return;
|
||
|
||
form.validate(async (valid: boolean) => {
|
||
if (valid) {
|
||
datasetSaving.value = true;
|
||
try {
|
||
ElMessage.success(datasetForm.id ? '保存成功' : '创建成功');
|
||
showDatasetDialog.value = false;
|
||
getDatasetList();
|
||
} catch (error) {
|
||
console.error('保存失败:', error);
|
||
} finally {
|
||
datasetSaving.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 获取文件列表
|
||
const getFileList = async () => {
|
||
if (!currentDataset.value) return;
|
||
|
||
fileLoading.value = true;
|
||
try {
|
||
// 模拟数据
|
||
fileList.value = [
|
||
{
|
||
id: '1',
|
||
name: '456_product(1).txt',
|
||
fileType: 'txt',
|
||
createdAt: '21/01/2026 16:53:32',
|
||
enabled: true,
|
||
chunkCount: 1,
|
||
parseStatus: 'general',
|
||
},
|
||
{
|
||
id: '2',
|
||
name: '123_speech(1).txt',
|
||
fileType: 'txt',
|
||
createdAt: '21/01/2026 16:53:26',
|
||
enabled: true,
|
||
chunkCount: 1,
|
||
parseStatus: 'general',
|
||
},
|
||
{
|
||
id: '3',
|
||
name: '123_product.txt',
|
||
fileType: 'txt',
|
||
createdAt: '21/01/2026 14:39:41',
|
||
enabled: true,
|
||
chunkCount: 1,
|
||
parseStatus: 'general',
|
||
},
|
||
];
|
||
} catch (error) {
|
||
console.error('获取文件列表失败:', error);
|
||
} finally {
|
||
fileLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 上传文件
|
||
const onUploadFile = () => {
|
||
uploadFileList.value = [];
|
||
showUploadDialog.value = true;
|
||
};
|
||
|
||
// 上传文件变化
|
||
const onUploadChange = (file: UploadFile, files: UploadFile[]) => {
|
||
uploadFileList.value = files;
|
||
};
|
||
|
||
// 移除上传文件
|
||
const onUploadRemove = (file: UploadFile, files: UploadFile[]) => {
|
||
uploadFileList.value = files;
|
||
};
|
||
|
||
// 确认上传
|
||
const onConfirmUpload = async () => {
|
||
uploading.value = true;
|
||
try {
|
||
ElMessage.success(`成功上传 ${uploadFileList.value.length} 个文件`);
|
||
showUploadDialog.value = false;
|
||
getFileList();
|
||
} catch (error) {
|
||
console.error('上传失败:', error);
|
||
} finally {
|
||
uploading.value = false;
|
||
}
|
||
};
|
||
|
||
// 文件状态变化
|
||
const onFileStatusChange = (row: any) => {
|
||
ElMessage.success(row.enabled ? '已启用' : '已禁用');
|
||
};
|
||
|
||
// 查看文档详情
|
||
const onViewDocumentDetail = (row: any) => {
|
||
currentDocument.value = row;
|
||
showDocumentDetailDialog.value = true;
|
||
};
|
||
|
||
// 预览文件
|
||
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(() => {
|
||
ElMessage.success('删除成功');
|
||
getFileList();
|
||
}).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: '创建知识库 dataset_tenant_1' },
|
||
];
|
||
};
|
||
|
||
// 保存配置
|
||
const onSaveSettings = () => {
|
||
ElMessage.success('配置保存成功');
|
||
};
|
||
|
||
// 页面加载
|
||
onMounted(() => {
|
||
getDatasetList();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.knowledge-page {
|
||
height: 100%;
|
||
padding: 15px;
|
||
box-sizing: border-box;
|
||
|
||
// 数据集列表页
|
||
.dataset-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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.dataset-cards {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
|
||
.dataset-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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 数据集详情页
|
||
.dataset-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;
|
||
|
||
.dataset-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>
|