Files
admin-ui/src/views/knowledge/index.vue

1044 lines
27 KiB
Vue
Raw Normal View History

<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>
<el-button type="primary" @click="onAddknowledge">
<el-icon><ele-Plus /></el-icon>
新建知识库
</el-button>
</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" @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" @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" @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="row.vectorStatus === 2 ? 'success' : 'warning'" size="small">
{{ row.vectorStatus === 2 ? '已完成' : '未完成' }}
</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" @click="onPreviewFile(row)">预览</el-button>
<el-button text size="small" type="primary" @click="onGenerateVector(row)">生成向量</el-button>
<el-button text size="small" type="danger" @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" @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" @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" @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" @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"
/>
</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 } from '/@/api/knowledge/document';
// 数据集相关
const knowledgeLoading = ref(false);
const knowledgeList = ref<any[]>([]);
const currentknowledge = ref<any>(null);
const showknowledgeDialog = ref(false);
const knowledgeSaving = ref(false);
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) {
ElMessage.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) {
ElMessage.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) {
ElMessage.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) {
ElMessage.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;
ElMessage.error('状态更新失败');
}
};
// 生成向量
const onGenerateVector = async (row: any) => {
try {
// 调用后端API来生成向量传递id和datasetId
await generateVector(row.id, currentknowledge.value.id);
ElMessage.success('生成向量任务已提交');
// 模拟更新状态
setTimeout(() => {
getFileList();
}, 1000);
} catch (error) {
ElMessage.error('生成向量失败');
}
};
// 查看文档详情
const onViewDocumentDetail = async (row: any) => {
try {
// 调用getDocument接口获取最新的文件详情
const response = await getDocument(row.id);
currentDocument.value = response.data;
showDocumentDetailDialog.value = true;
} catch (error) {
ElMessage.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 = () => {
ElMessage.success('配置保存成功');
};
// 页面加载
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>