Files
xiaozhi-esp32-server/main/manager-web/src/views/KnowledgeFileUpload.vue
T

2103 lines
58 KiB
Vue

<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<div class="left-operations">
<el-button class="btn-back" type="text" @click="$router.back()">
&lt;
</el-button>
<h2 class="knowledge-base-title">{{ knowledgeBaseName }}</h2>
</div>
<div class="right-operations">
<el-input :placeholder="$t('knowledgeFileUpload.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('knowledgeFileUpload.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="params-card" shadow="never">
<div class="table-wrapper">
<el-table ref="fileTable" :data="fileList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column :label="$t('knowledgeFileUpload.documentName')" prop="name" align="left">
<template slot-scope="scope">
<span class="document-name">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('knowledgeFileUpload.uploadTime')" prop="createdAt" align="center"
width="300">
<template slot-scope="scope">
<span>{{ formatDate(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('knowledgeFileUpload.status')" align="center" width="120">
<template slot-scope="scope">
<el-tag :type="getParseStatusType(scope.row.parseStatusCode)" size="small">
{{ getParseStatusText(scope.row.parseStatusCode) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('knowledgeFileUpload.sliceCount')" align="center" width="100">
<template slot-scope="scope">
<span>{{ scope.row.sliceCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('knowledgeFileUpload.operation')" align="center" width="200">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleParse(scope.row)"
:disabled="scope.row.parseStatusCode === 1 || scope.row.parseStatusCode === 3 || scope.row.parseStatusCode === 4" v-if="!scope.row.sliceCount || scope.row.sliceCount <= 0">
{{ $t('knowledgeFileUpload.parse') }}
</el-button>
<el-button size="mini" type="text" @click="handleViewSlices(scope.row)"
v-if="scope.row.sliceCount && scope.row.sliceCount > 0">
{{ $t('knowledgeFileUpload.viewSlices') }}
</el-button>
<el-button size="mini" type="text" @click="handleDelete(scope.row)">
{{ $t('knowledgeFileUpload.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button type="primary" @click="handleSelectAll">
{{ isAllSelected ? $t('knowledgeFileUpload.deselectAll') : $t('knowledgeFileUpload.selectAll') }}
</el-button>
<el-button type="success" @click="showUploadDialog">
{{ $t('knowledgeFileUpload.add') }}
</el-button>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedFiles.length === 0">
{{ $t('knowledgeFileUpload.batchDelete') }}
</el-button>
<el-button type="primary" icon="el-icon-search" @click="showRetrievalTestDialog"
style="background-color: #409EFF; border-color: #409EFF;">
{{ $t('knowledgeFileUpload.retrievalTest') }}
</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="`${item}${$t('knowledgeFileUpload.itemsPerPage')}`" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('knowledgeFileUpload.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('knowledgeFileUpload.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('knowledgeFileUpload.nextPage') }}
</button>
<span class="total-text">{{ $t('knowledgeFileUpload.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<!-- 上传文档对话框 -->
<el-dialog :title="$t('knowledgeFileUpload.uploadDocument')" :visible.sync="uploadDialogVisible" width="800px">
<el-upload class="document-uploader" drag :action="uploadUrl" :auto-upload="false" :on-change="handleFileChange"
:multiple="true" :show-file-list="false" accept=".doc,.docx,.pdf,.txt,.md,.mdx,.csv,.xls,.xlsx,.ppt,.pptx">
<i class="el-icon-upload"></i>
<div class="el-upload__text">{{ $t('knowledgeFileUpload.dragOrClick') }}</div>
<div class="el-upload__tip">{{ $t('knowledgeFileUpload.uploadTip') }}</div>
</el-upload>
<!-- 已选择文件列表 -->
<div class="selected-files-section" v-if="selectedFilesList.length > 0">
<h4>{{ $t('knowledgeFileUpload.selectedFiles') }} ({{ selectedFilesList.length }})</h4>
<div class="selected-files-list">
<div v-for="(file, index) in selectedFilesList" :key="index" class="selected-file-item">
<div class="file-info">
<i class="el-icon-document"></i>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
</div>
<el-button type="text" class="remove-btn" @click="removeSelectedFile(index)">
<i class="el-icon-close"></i>
</el-button>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false">{{ $t('knowledgeFileUpload.cancel') }}</el-button>
<el-button type="primary" @click="handleBatchUploadSubmit" :loading="uploading"
:disabled="selectedFilesList.length === 0">
{{ $t('knowledgeFileUpload.confirm') }} {{ selectedFilesList.length > 0 ?
`(${selectedFilesList.length}${$t('knowledgeFileUpload.itemsPerPage').replace('条/页', '个文件')})` : '' }}
</el-button>
</div>
</el-dialog>
<!-- 切片管理弹窗 -->
<el-dialog :title="`${$t('knowledgeFileUpload.viewSlices')} - ${currentDocumentName}`"
:visible.sync="sliceDialogVisible" width="1200px" class="slice-dialog">
<div class="slice-management">
<!-- 切片列表 -->
<div class="slice-list-section">
<!-- 切片内容卡片式布局 -->
<div v-loading="sliceLoading" class="slice-content-container">
<div v-if="sliceList.length > 0" class="slice-cards-container">
<div v-for="(slice, index) in sliceList" :key="index" class="slice-card">
<div class="slice-header-info">
<p><strong>{{ $t('knowledgeFileUpload.slice') }} {{ (sliceCurrentPage - 1) * slicePageSize + index + 1
}}</strong></p>
</div>
<div class="slice-card-content">
<div class="content-text">{{ slice.content }}</div>
</div>
</div>
</div>
<div v-else class="no-slice-data">
<el-alert type="info" :title="$t('knowledgeFileUpload.noSliceData')"></el-alert>
</div>
</div>
<!-- 切片分页 -->
<div class="slice-pagination" style="margin-top: 20px; text-align: right;">
<div class="custom-pagination">
<!-- /页选择器 -->
<el-select v-model="slicePageSize" @change="handleSliceSizeChange" class="page-size-select"
:popper-append-to-body="false">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="`${item}${$t('knowledgeFileUpload.itemsPerPage')}`" :value="item">
</el-option>
</el-select>
<!-- 首页按钮 -->
<button class="pagination-btn" :disabled="sliceCurrentPage === 1" @click="goToSliceFirstPage">
{{ $t('knowledgeFileUpload.firstPage') }}
</button>
<!-- 上一页按钮 -->
<button class="pagination-btn" :disabled="sliceCurrentPage === 1" @click="goToSlicePrevPage">
{{ $t('knowledgeFileUpload.prevPage') }}
</button>
<!-- 页码按钮 -->
<button v-for="page in sliceVisiblePages" :key="page" class="pagination-btn"
:class="{ active: page === sliceCurrentPage }" @click="goToSlicePage(page)">
{{ page }}
</button>
<!-- 下一页按钮 -->
<button class="pagination-btn" :disabled="sliceCurrentPage === slicePageCount" @click="goToSliceNextPage">
{{ $t('knowledgeFileUpload.nextPage') }}
</button>
<!-- 总记录数 -->
<span class="total-text">{{ $t('knowledgeFileUpload.totalRecords', { total: sliceTotal }) }}</span>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 召回测试弹窗 -->
<el-dialog :title="$t('knowledgeFileUpload.retrievalTest')" :visible.sync="retrievalTestDialogVisible"
width="1200px" class="retrieval-test-dialog">
<div class="retrieval-test-form">
<el-form :model="retrievalTestForm" label-width="80px" @submit.native.prevent="runRetrievalTest">
<el-form-item :label="$t('knowledgeFileUpload.testQuestion')" required>
<el-input v-model="retrievalTestForm.question"
:placeholder="$t('knowledgeFileUpload.testQuestionPlaceholder')" style="width: 100%; max-height: 80px;"
@keyup.enter.native="runRetrievalTest">
</el-input>
</el-form-item>
</el-form>
<div class="retrieval-test-actions" style="text-align: center;">
<el-button type="primary" @click="runRetrievalTest" :loading="retrievalTestLoading">
{{ $t('knowledgeFileUpload.runTest') }}
</el-button>
<el-button @click="retrievalTestDialogVisible = false">{{ $t('knowledgeFileUpload.cancel') }}</el-button>
</div>
<div v-if="retrievalTestResult" class="retrieval-test-result">
<h4>{{ $t('knowledgeFileUpload.testResult') }}</h4>
<el-card v-if="retrievalTestResult.chunks && retrievalTestResult.chunks.length > 0">
<div class="result-chunk-container">
<div v-for="(chunk, index) in retrievalTestResult.chunks" :key="index" class="result-chunk">
<p><strong>{{ $t('knowledgeFileUpload.slice') }} {{ index + 1 }}</strong></p>
<div class="similarity-scores">
<div class="score-item">
<span class="score-label">{{ $t('knowledgeFileUpload.comprehensiveSimilarity') }}</span>
<span class="score-value">{{ (chunk.similarity || 0).toFixed(4) }}</span>
</div>
</div>
<div class="chunk-content">
<p><strong>{{ $t('knowledgeFileUpload.content') }}</strong></p>
<p>{{ chunk.content }}</p>
</div>
<el-divider v-if="index < retrievalTestResult.chunks.length - 1"></el-divider>
</div>
</div>
</el-card>
<el-alert v-else type="info" :title="$t('knowledgeFileUpload.noRelatedSlices')"></el-alert>
</div>
</div>
</el-dialog>
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
<script>
import Api from "@/apis/api";
import KnowledgeBaseAPI from "@/apis/module/knowledgeBase";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default {
components: { HeaderBar, VersionFooter },
data() {
return {
datasetId: '',
knowledgeBaseName: '',
searchName: "",
fileList: [],
selectedFiles: [],
currentPage: 1,
loading: false,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
uploadDialogVisible: false,
uploading: false,
uploadForm: {
file: null
},
uploadUrl: '',
isAllSelected: false,
selectedFilesList: [], // 批量上传选择的文件列表
// 切片管理相关数据
sliceDialogVisible: false,
currentDocumentId: '',
currentDocumentName: '',
sliceList: [],
sliceLoading: false,
sliceCurrentPage: 1,
slicePageSize: 10,
sliceTotal: 0,
// 召回测试相关数据
retrievalTestDialogVisible: false,
retrievalTestForm: {
question: ''
},
retrievalTestResult: null,
retrievalTestLoading: false,
// 状态轮询相关数据
statusPollingTimer: null,
statusPollingInterval: 5000, // 5秒轮询一次
maxStatusPollingTime: 300000, // 最大轮询时间5分钟
statusPollingStartTime: null
};
},
created() {
this.datasetId = this.$route.query.datasetId || '';
this.knowledgeBaseName = this.$route.query.knowledgeBaseName || '';
this.uploadUrl = `${Api.getServiceUrl()}/api/v1/documents/upload`;
this.fetchFileList();
},
beforeDestroy() {
this.stopStatusPolling();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
// 切片分页页数计算
slicePageCount() {
return Math.ceil(this.sliceTotal / this.slicePageSize);
},
// 切片分页可见页码计算(最多显示3个页码)
sliceVisiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.sliceCurrentPage - 1);
let end = Math.min(this.slicePageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
goBack() {
this.$router.push('/knowledge-base-management');
},
handlePageSizeChange: function (val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchFileList();
},
fetchFileList: function () {
this.loading = true;
const params = {
page: this.currentPage,
page_size: this.pageSize,
name: this.searchName
};
KnowledgeBaseAPI.getDocumentList(this.datasetId, params,
async ({ data }) => {
this.loading = false;
if (data && data.code === 0) {
this.fileList = data.data.list;
this.total = data.data.total;
// 为每个文档获取切片数量
await this.fetchSliceCountsForDocuments();
// 自动为处理中的文档启动状态检测
this.startStatusPolling();
} else {
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.getListFailed'));
this.fileList = [];
this.total = 0;
}
},
(err) => {
this.loading = false;
console.log('Error callback received:', err);
if (err && err.data) {
console.log('后端返回错误消息:', err.data.msg || err.msg);
this.$message.error(err.data.msg || err.msg || this.$t('knowledgeFileUpload.getListFailed'));
} else {
this.$message.error(this.$t('knowledgeFileUpload.getListFailed'));
}
console.error('获取文档列表失败:', err);
this.fileList = [];
this.total = 0;
}
);
},
// 启动文档状态轮询
startStatusPolling: function () {
// 检查是否已经有轮询在进行
if (this.statusPollingTimer) {
console.log('状态轮询已在运行');
return;
}
// 检查是否有处理中的文档
const hasProcessingDocuments = this.fileList.some(document =>
document.parseStatusCode === 1
);
if (!hasProcessingDocuments) {
console.log('没有处理中的文档,不启动状态轮询');
return;
}
console.log('启动文档状态轮询');
this.statusPollingStartTime = Date.now();
// 立即执行一次状态检查
this.pollDocumentStatus();
// 开始轮询
this.statusPollingTimer = setInterval(() => {
this.pollDocumentStatus();
}, this.statusPollingInterval);
},
// 停止文档状态轮询
stopStatusPolling: function () {
if (this.statusPollingTimer) {
clearInterval(this.statusPollingTimer);
this.statusPollingTimer = null;
console.log('停止文档状态轮询');
}
},
// 轮询文档状态
pollDocumentStatus: async function () {
// 检查是否超过最大轮询时间
if (Date.now() - this.statusPollingStartTime > this.maxStatusPollingTime) {
console.log('达到最大轮询时间,停止状态轮询');
this.stopStatusPolling();
return;
}
try {
const params = {
page: this.currentPage,
page_size: this.pageSize,
name: this.searchName
};
const response = await new Promise((resolve, reject) => {
KnowledgeBaseAPI.getDocumentList(this.datasetId, params,
({ data }) => resolve(data),
(err) => reject(err)
);
});
if (response && response.code === 0) {
const updatedFileList = response.data.list;
// 更新文档状态
this.updateDocumentStatuses(updatedFileList);
// 检查是否还有处理中的文档
const hasProcessingDocuments = updatedFileList.some(document =>
document.parseStatusCode === 1
);
if (!hasProcessingDocuments) {
console.log('所有文档处理完成,停止状态轮询');
this.stopStatusPolling();
}
}
} catch (error) {
console.warn('轮询文档状态失败:', error);
}
},
// 更新文档状态
updateDocumentStatuses: function (updatedFileList) {
let hasChanges = false;
updatedFileList.forEach(updatedDoc => {
const existingDoc = this.fileList.find(doc => doc.id === updatedDoc.id);
if (existingDoc && existingDoc.parseStatusCode !== updatedDoc.parseStatusCode) {
// 状态发生变化,更新文档
Object.assign(existingDoc, updatedDoc);
hasChanges = true;
console.log(`文档 ${existingDoc.name} 状态已更新: ${existingDoc.parseStatusCode} -> ${updatedDoc.parseStatusCode}`);
// 如果状态变为完成,启动切片数量检测
if (updatedDoc.parseStatusCode === 3) {
this.fetchSliceCountForSingleDocument(updatedDoc.id);
}
}
});
if (hasChanges) {
this.$forceUpdate();
}
},
// 为文档列表中的每个文档获取切片数量
fetchSliceCountsForDocuments: async function () {
if (!this.fileList || this.fileList.length === 0) {
return;
}
// 为每个文档获取切片数量
for (const document of this.fileList) {
this.fetchSliceCountForSingleDocument(document.id);
}
},
// 获取单个文档的切片数量
fetchSliceCountForSingleDocument: function (documentId) {
const document = this.fileList.find(doc => doc.id === documentId);
if (!document) {
console.warn('未找到文档:', documentId);
return;
}
const params = {
page: 1,
page_size: 1 // 只需要获取总数,所以每页1条记录即可
};
KnowledgeBaseAPI.listChunks(this.datasetId, documentId, params,
({ data }) => {
if (data && data.code === 0) {
const sliceCount = data.data.total || 0;
// 更新文档的切片数量
this.$set(document, 'sliceCount', sliceCount);
// 强制更新视图
this.$forceUpdate();
console.log(`文档 ${document.name} 切片数量已更新为:`, sliceCount);
} else {
console.warn(`获取文档 ${document.name} 切片数量失败:`, data?.msg);
}
},
(err) => {
console.warn(`获取文档 ${document.name} 切片数量失败:`, err);
}
);
},
// 智能检测切片生成状态并自动刷新
smartRefreshSliceCount: function (documentId) {
const document = this.fileList.find(doc => doc.id === documentId);
if (!document) {
console.warn('未找到文档:', documentId);
return;
}
// 延迟2秒后获取切片数量,给后端更多处理时间
setTimeout(() => {
this.fetchSliceCountForSingleDocument(documentId);
}, 2000);
},
handleSearch: function () {
this.currentPage = 1;
this.fetchFileList();
},
headerCellClassName: function ({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
return 'header-cell-first';
}
return 'header-cell';
},
showUploadDialog: function () {
this.uploadForm = {
name: '',
file: null
};
this.selectedFilesList = []; // 清空已选择文件列表
this.uploadDialogVisible = true;
},
handleFileChange: function (file, fileList) {
if (!file || !file.raw) return;
// 文件上传前的验证
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
this.$message.error('文件大小不能超过10MB!');
return;
}
// 添加到已选择文件列表
this.selectedFilesList.push({
name: file.name,
size: file.size,
raw: file.raw
});
},
beforeUpload: function (file) {
// 文件上传前的验证
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
this.$message.error('文件大小不能超过10MB!');
return false;
}
// 保存文件到uploadForm
this.uploadForm.file = file;
return false; // 阻止自动上传,使用自定义上传逻辑
},
// 移除已选择的文件
removeSelectedFile: function (index) {
this.selectedFilesList.splice(index, 1);
},
// 格式化文件大小
formatFileSize: function (bytes) {
if (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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
// 批量上传提交
handleBatchUploadSubmit: function () {
if (this.selectedFilesList.length === 0) {
this.$message.error('请选择要上传的文件');
return;
}
this.uploading = true;
// 创建上传任务数组
const uploadPromises = this.selectedFilesList.map(file => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file.raw);
KnowledgeBaseAPI.uploadDocument(this.datasetId, formData,
({ data }) => {
if (data && data.code === 0) {
resolve({ success: true, fileName: file.name });
} else {
reject({ success: false, fileName: file.name, error: data?.msg || this.$t('knowledgeFileUpload.uploadFailed') });
}
},
(err) => {
// 错误回调处理后端返回的错误信息
if (err && err.data) {
reject({ success: false, fileName: file.name, error: err.data.msg || err.msg || this.$t('knowledgeFileUpload.uploadFailed') });
} else {
reject({ success: false, fileName: file.name, error: this.$t('knowledgeFileUpload.uploadFailed') });
}
console.error('上传文档失败:', err);
}
);
});
});
// 执行所有上传任务
Promise.all(uploadPromises.map(p => p.catch(e => e)))
.then(results => {
this.uploading = false;
const successCount = results.filter(r => r.success).length;
const failedCount = results.filter(r => !r.success).length;
if (successCount > 0) {
this.$message.success(`成功上传 ${successCount} 个文件`);
}
if (failedCount > 0) {
const failedFiles = results.filter(r => !r.success).map(r => r.fileName);
this.$message.error(`上传失败 ${failedCount} 个文件: ${failedFiles.join(', ')}`);
}
if (successCount > 0) {
this.uploadDialogVisible = false;
this.fetchFileList();
}
})
.catch(error => {
this.uploading = false;
this.$message.error('批量上传失败');
console.error('批量上传失败:', error);
});
},
// 单文件上传(保留原有功能)
handleUploadSubmit: function () {
if (!this.uploadForm.file) {
this.$message.error(this.$t('knowledgeFileUpload.fileRequired'));
return;
}
this.uploading = true;
const formData = new FormData();
formData.append('file', this.uploadForm.file);
KnowledgeBaseAPI.uploadDocument(this.datasetId, formData,
({ data }) => {
this.uploading = false;
if (data && data.code === 0) {
this.$message.success(this.$t('knowledgeFileUpload.uploadSuccess'));
this.uploadDialogVisible = false;
this.fetchFileList();
} else {
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.uploadFailed'));
}
},
(err) => {
this.uploading = false;
// 错误回调处理后端返回的错误信息
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || this.$t('knowledgeFileUpload.uploadFailed'));
} else {
this.$message.error(this.$t('knowledgeFileUpload.uploadFailed'));
}
console.error('上传文档失败:', err);
}
);
},
handleParse: function (row) {
this.$confirm(this.$t('knowledgeFileUpload.confirmParse'), this.$t('warning'), {
confirmButtonText: this.$t('knowledgeFileUpload.confirm'),
cancelButtonText: this.$t('knowledgeFileUpload.cancel'),
type: 'warning'
}).then(() => {
KnowledgeBaseAPI.parseDocument(this.datasetId, row.id,
({ data }) => {
if (data && data.code === 0) {
this.$message.success('请求已提交,解析中');
// 立即更新文档状态为处理中
const document = this.fileList.find(doc => doc.id === row.id);
if (document) {
document.parseStatusCode = 1; // 处理中状态
this.$forceUpdate();
}
// 启动状态轮询
this.startStatusPolling();
// 使用智能检测自动刷新切片数量
this.smartRefreshSliceCount(row.id);
} else {
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.parseFailed'));
}
},
(err) => {
// 错误回调处理后端返回的错误信息
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || this.$t('knowledgeFileUpload.parseFailed'));
} else {
this.$message.error(this.$t('knowledgeFileUpload.parseFailed'));
}
console.error('解析文档失败:', err);
}
);
}).catch(() => {
this.$message.info(this.$t('knowledgeFileUpload.parseCancelled'));
});
},
handleViewSlices: function (row) {
// 查看切片
this.currentDocumentId = row.id;
this.currentDocumentName = row.name;
this.sliceDialogVisible = true;
this.sliceCurrentPage = 1;
this.sliceSearchKeyword = '';
this.fetchSlices();
},
handleDelete: function (row) {
this.$confirm(this.$t('knowledgeFileUpload.confirmDelete'), this.$t('warning'), {
confirmButtonText: this.$t('knowledgeFileUpload.confirm'),
cancelButtonText: this.$t('knowledgeFileUpload.cancel'),
type: 'warning'
}).then(() => {
KnowledgeBaseAPI.deleteDocument(this.datasetId, row.id,
({ data }) => {
if (data && data.code === 0) {
this.$message.success(this.$t('knowledgeFileUpload.deleteSuccess'));
this.fetchFileList();
} else {
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.deleteFailed'));
}
},
(err) => {
// 错误回调处理后端返回的错误信息
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || this.$t('knowledgeFileUpload.deleteFailed'));
} else {
this.$message.error(this.$t('knowledgeFileUpload.deleteFailed'));
}
console.error('删除文档失败:', err);
}
);
}).catch(() => {
this.$message.info(this.$t('knowledgeFileUpload.deleteCancelled'));
});
},
handleSelectionChange: function (selection) {
this.selectedFiles = selection;
},
handleSelectAll: function () {
if (this.isAllSelected) {
this.$refs.fileTable.clearSelection();
this.isAllSelected = false;
} else {
this.$refs.fileTable.clearSelection();
this.fileList.forEach(row => {
this.$refs.fileTable.toggleRowSelection(row, true);
});
this.isAllSelected = true;
}
},
handleBatchDelete: function () {
if (this.selectedFiles.length === 0) {
this.$message.warning(this.$t('knowledgeFileUpload.selectFilesFirst'));
return;
}
this.$confirm(this.$t('knowledgeFileUpload.confirmBatchDelete', { count: this.selectedFiles.length }), this.$t('warning'), {
confirmButtonText: this.$t('knowledgeFileUpload.confirm'),
cancelButtonText: this.$t('knowledgeFileUpload.cancel'),
type: 'warning'
}).then(() => {
const deletePromises = this.selectedFiles.map(file => {
return new Promise((resolve, reject) => {
KnowledgeBaseAPI.deleteDocument(this.datasetId, file.id,
({ data }) => {
if (data && data.code === 0) {
resolve();
} else {
reject(data?.msg || this.$t('knowledgeFileUpload.deleteFailed'));
}
},
(err) => {
// 错误回调处理后端返回的错误信息
if (err && err.data) {
reject(err.data.msg || err.msg || this.$t('knowledgeFileUpload.deleteFailed'));
} else {
reject(this.$t('knowledgeFileUpload.deleteFailed'));
}
console.error('删除文档失败:', err);
}
);
});
});
Promise.all(deletePromises)
.then(() => {
this.$message.success(this.$t('knowledgeFileUpload.batchDeleteSuccess', { count: this.selectedFiles.length }));
this.selectedFiles = [];
this.fetchFileList();
})
.catch((error) => {
this.$message.error(error || this.$t('knowledgeFileUpload.batchDeleteFailed'));
});
}).catch(() => {
this.$message.info(this.$t('knowledgeFileUpload.deleteCancelled'));
});
},
getParseStatusType: function (parseStatusCode) {
switch (parseStatusCode) {
case 0:
return 'info'; // 灰色 - 未开始
case 1:
return 'primary'; // 蓝色 - 处理中
case 2:
return 'warning'; // 黄色 - 已取消
case 3:
return 'success'; // 绿色 - 完成
case 4:
return 'danger'; // 红色 - 失败
default:
return 'info'; // 默认灰色
}
},
getParseStatusText: function (parseStatusCode) {
switch (parseStatusCode) {
case 0:
return this.$t('knowledgeFileUpload.statusNotStarted');
case 1:
return this.$t('knowledgeFileUpload.statusProcessing');
case 2:
return this.$t('knowledgeFileUpload.statusCancelled');
case 3:
return this.$t('knowledgeFileUpload.statusCompleted');
case 4:
return this.$t('knowledgeFileUpload.statusFailed');
default:
return this.$t('knowledgeFileUpload.statusNotStarted');
}
},
goToPage: function (page) {
if (page !== this.currentPage) {
this.currentPage = page;
this.fetchFileList();
}
},
goFirst: function () {
if (this.currentPage !== 1) {
this.currentPage = 1;
this.fetchFileList();
}
},
goPrev: function () {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchFileList();
}
},
goNext: function () {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchFileList();
}
},
formatDate: function (dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
},
// 切片管理相关方法
fetchSlices: function () {
this.sliceLoading = true;
const params = {
page: this.sliceCurrentPage,
page_size: this.slicePageSize
};
if (this.sliceSearchKeyword) {
params.keywords = this.sliceSearchKeyword;
}
KnowledgeBaseAPI.listChunks(this.datasetId, this.currentDocumentId, params,
({ data }) => {
this.sliceLoading = false;
if (data && data.code === 0) {
// 解析切片列表数据
this.parseSliceData(data.data);
} else {
this.$message.error(data?.msg || '获取切片列表失败');
this.sliceList = [];
this.sliceTotal = 0;
}
},
(err) => {
this.sliceLoading = false;
// 错误回调处理后端返回的错误信息
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || '获取切片列表失败');
} else {
this.$message.error('获取切片列表失败');
}
console.error('获取切片列表失败:', err);
this.sliceList = [];
this.sliceTotal = 0;
}
);
},
parseSliceData: function (data) {
try {
if (data && data.list) {
// 后端已经解析过的格式
this.sliceList = data.list;
this.sliceTotal = data.total || data.list.length;
} else if (data && data.chunks && Array.isArray(data.chunks)) {
// RAGFlow API原始格式
this.sliceList = data.chunks;
this.sliceTotal = data.total || data.chunks.length;
} else if (data && Array.isArray(data)) {
this.sliceList = data;
this.sliceTotal = data.length;
} else {
this.sliceList = [];
this.sliceTotal = 0;
}
console.log('解析后的切片数据:', {
list: this.sliceList,
total: this.sliceTotal
});
} catch (error) {
console.error('解析切片数据失败:', error);
this.sliceList = [];
this.sliceTotal = 0;
}
},
handleSliceSizeChange: function (pageSize) {
this.slicePageSize = pageSize;
this.sliceCurrentPage = 1;
this.fetchSlices();
},
handleSlicePageChange: function (page) {
this.sliceCurrentPage = page;
this.fetchSlices();
},
// 跳转到切片管理第一页
goToSliceFirstPage: function () {
if (this.sliceCurrentPage !== 1) {
this.sliceCurrentPage = 1;
this.fetchSlices();
}
},
// 切片管理上一页
goToSlicePrevPage: function () {
if (this.sliceCurrentPage > 1) {
this.sliceCurrentPage--;
this.fetchSlices();
}
},
// 切片管理跳转到指定页
goToSlicePage: function (page) {
if (page !== this.sliceCurrentPage) {
this.sliceCurrentPage = page;
this.fetchSlices();
}
},
// 切片管理下一页
goToSliceNextPage: function () {
if (this.sliceCurrentPage < this.slicePageCount) {
this.sliceCurrentPage++;
this.fetchSlices();
}
},
// 召回测试相关方法
showRetrievalTestDialog: function () {
// 初始化召回测试表单
this.retrievalTestForm = {
question: ''
};
this.retrievalTestResult = null;
this.retrievalTestDialogVisible = true;
},
runRetrievalTest: function () {
if (!this.retrievalTestForm.question.trim()) {
this.$message.error(this.$t('knowledgeFileUpload.testQuestionRequired'));
return;
}
this.retrievalTestLoading = true;
this.retrievalTestResult = null;
// 准备请求数据
const requestData = {
question: this.retrievalTestForm.question.trim()
};
// 调用召回测试API
KnowledgeBaseAPI.retrievalTest(this.datasetId, requestData,
({ data }) => {
this.retrievalTestLoading = false;
if (data && data.code === 0) {
this.retrievalTestResult = data.data || data;
this.$message.success('召回测试完成');
} else {
this.$message.error(data?.msg || '召回测试失败');
}
},
(err) => {
this.retrievalTestLoading = false;
// 错误回调处理后端返回的错误信息
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || '召回测试失败');
} else {
this.$message.error('召回测试失败');
}
console.error('召回测试失败:', err);
}
);
},
handleRetrievalTestDialogClose: function () {
this.retrievalTestDialogVisible = false;
this.retrievalTestResult = null;
}
}
};
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
// 顶部 63px 底部 35px 查询58px
height: calc(100vh - 63px - 35px - 58px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.left-operations {
display: flex;
align-items: center;
gap: 16px;
}
.btn-back {
font-size: 20px;
font-weight: bold;
color: #606266;
padding: 8px 12px;
border-radius: 4px;
transition: all 0.3s ease;
}
.btn-back:hover {
background-color: #f5f7fa;
color: #409eff;
}
.knowledge-base-title {
font-size: 24px;
margin: 0;
color: #303133;
font-weight: 600;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
}
.params-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-bottom: 10px;
width: 100%;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 5px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
}
.total-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
:deep(.el-table__empty-block) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 300px;
}
/* 拖拽上传区域样式 */
.document-uploader {
:deep(.el-upload-dragger) {
width: 600px;
height: 200px;
min-height: 200px;
border: 2px dashed #c0c4cc;
border-radius: 16px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s ease;
&:hover {
border-color: #409eff;
}
.el-icon-upload {
font-size: 48px;
color: #c0c4cc;
margin: 20px 0 16px;
line-height: 1;
}
.el-upload__text {
font-size: 16px;
color: #606266;
margin-bottom: 8px;
line-height: 1.5;
}
.el-upload__tip {
font-size: 14px;
color: #909399;
line-height: 1.5;
}
}
}
/* 召回测试弹窗样式 */
.retrieval-test-dialog {
::v-deep .el-dialog__wrapper {
display: block !important;
}
::v-deep .el-dialog {
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
margin: 0 !important;
width: 1200px !important;
height: 90vh !important;
max-height: 90vh !important;
min-height: 90vh !important;
}
::v-deep .el-dialog__body {
height: calc(100% - 90px) !important;
max-height: calc(100% - 90px) !important;
overflow: hidden;
padding: 15px 25px;
display: flex;
flex-direction: column;
}
}
.retrieval-test-form {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.retrieval-test-result {
margin-top: 20px;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
flex-shrink: 0;
}
:deep(.el-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.el-card__body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
}
}
.result-chunk-container {
flex: 1;
overflow-y: auto;
max-height: 100%;
padding: 16px;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: #fafafa;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 3px;
border: 1px solid #f0f0f0;
}
&::-webkit-scrollbar-thumb:hover {
background: #d0d0d0;
}
}
.result-chunk {
background-color: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
p {
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
strong {
color: #303133;
font-weight: 600;
}
}
.similarity-scores {
display: flex;
gap: 16px;
margin-bottom: 12px;
.score-item {
display: flex;
flex-direction: column;
.score-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.score-value {
font-size: 14px;
font-weight: 600;
color: #409eff;
}
}
}
.chunk-content {
background-color: white;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
margin-top: 8px;
max-height: 120px;
overflow-y: auto;
/* 内容滚动条样式 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #fafafa;
}
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb:hover {
background: #d0d0d0;
}
}
:deep(.el-divider) {
margin: 12px 0;
}
}
/* 已选择文件列表样式 */
.selected-files-section {
margin-top: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
background-color: #f8f9fa;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #606266;
}
}
.selected-files-list {
max-height: 200px;
overflow-y: auto;
}
.selected-file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
flex: 1;
.el-icon-document {
color: #409eff;
margin-right: 8px;
font-size: 16px;
}
.file-name {
font-size: 14px;
color: #303133;
margin-right: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
.file-size {
font-size: 12px;
color: #909399;
}
}
.remove-btn {
color: #f56c6c;
padding: 4px;
&:hover {
color: #f78989;
background-color: #fef0f0;
border-radius: 4px;
}
}
}
/* 上传对话框容器样式 */
:deep(.el-dialog) {
border-radius: 16px !important;
overflow: hidden;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
flex: 1;
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
.slice-dialog-content {
max-height: 70vh;
overflow-y: auto;
}
/* 切片管理弹窗固定容器大小 */
.slice-dialog {
::v-deep .el-dialog__wrapper {
display: block !important;
}
/* 切片管理弹窗滚动条样式 */
::v-deep .el-dialog::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::v-deep .el-dialog::-webkit-scrollbar-track {
background: #f8f9fa;
border-radius: 4px;
}
::v-deep .el-dialog::-webkit-scrollbar-thumb {
background: #f0f0f0;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
::v-deep .el-dialog::-webkit-scrollbar-thumb:hover {
background: #e8e8e8;
}
::v-deep .el-dialog {
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
margin: 0 !important;
width: 1200px !important;
height: 90vh !important;
max-height: 90vh !important;
min-height: 90vh !important;
}
:deep(.el-dialog__body) {
height: calc(100% - 90px) !important;
max-height: calc(100% - 90px) !important;
overflow: hidden;
padding: 15px 20px;
}
.slice-management {
height: 100%;
display: flex;
flex-direction: column;
}
.slice-list-section {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* 切片内容容器样式 */
.slice-content-container {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.slice-cards-container {
flex: 1;
overflow-y: auto;
max-height: 100%;
padding: 16px;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: #fafafa;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 3px;
border: 1px solid #f0f0f0;
}
&::-webkit-scrollbar-thumb:hover {
background: #d0d0d0;
}
}
.slice-card {
background-color: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.slice-header-info {
margin-bottom: 12px;
p {
margin: 0;
font-size: 14px;
line-height: 1.5;
strong {
color: #303133;
font-weight: 600;
}
}
}
.slice-card-content {
background-color: white;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
max-height: 280px;
min-height: 120px;
overflow-y: auto;
/* 内容滚动条样式 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #fafafa;
}
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb:hover {
background: #d0d0d0;
}
.content-text {
font-size: 14px;
line-height: 1.6;
text-align: left;
color: #333;
word-wrap: break-word;
white-space: pre-wrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
/* 确保文本正常显示,包括空格和换行 */
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
/* 段落样式 */
p {
margin: 0 0 12px 0;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
}
/* 列表样式 */
ul,
ol {
margin: 8px 0;
padding-left: 24px;
li {
margin-bottom: 4px;
line-height: 1.5;
}
}
/* 标题样式 */
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 16px 0 8px 0;
font-weight: 600;
line-height: 1.4;
}
h1 {
font-size: 18px;
}
h2 {
font-size: 16px;
}
h3 {
font-size: 15px;
}
h4,
h5,
h6 {
font-size: 14px;
}
/* 强调文本 */
strong,
b {
font-weight: 600;
color: #1a1a1a;
}
/* 代码样式 */
code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
/* 引用样式 */
blockquote {
margin: 12px 0;
padding: 8px 12px;
border-left: 4px solid #e0e0e0;
background-color: #f9f9f9;
font-style: italic;
color: #666;
}
}
}
}
.no-slice-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
.slice-count {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #606266;
background-color: #f5f7fa;
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.count-label {
font-weight: 500;
color: #303133;
}
.count-value {
font-weight: 600;
color: #409eff;
font-size: 15px;
}
.count-unit {
color: #909399;
}
.slice-actions {
display: flex;
align-items: center;
gap: 10px;
}
.slice-pagination {
text-align: right;
margin-top: 20px;
.custom-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
</style>