|
@@ -89,6 +89,9 @@
|
|
<i data-feather="stop-circle" class="icons"></i>
|
|
<i data-feather="stop-circle" class="icons"></i>
|
|
终止所有任务
|
|
终止所有任务
|
|
</el-button>
|
|
</el-button>
|
|
|
|
+ <el-button type="primary" @click="exportData()">
|
|
|
|
+ <i data-feather="download" class="icons"></i> 导出
|
|
|
|
+ </el-button>
|
|
</div>
|
|
</div>
|
|
<div class="selection-control" v-if="selectedRows.length > 0">
|
|
<div class="selection-control" v-if="selectedRows.length > 0">
|
|
<el-alert
|
|
<el-alert
|
|
@@ -272,6 +275,21 @@
|
|
>
|
|
>
|
|
</el-button>
|
|
</el-button>
|
|
</el-tooltip>
|
|
</el-tooltip>
|
|
|
|
+ <el-tooltip
|
|
|
|
+ class="item"
|
|
|
|
+ effect="dark"
|
|
|
|
+ content="查看问答对"
|
|
|
|
+ placement="top"
|
|
|
|
+ >
|
|
|
|
+ <el-button
|
|
|
|
+ type="text"
|
|
|
|
+ size="small"
|
|
|
|
+ circle
|
|
|
|
+ icon="el-icon-receiving"
|
|
|
|
+ @click="getQa(scope.row.id)"
|
|
|
|
+ style="font-size: 15px"
|
|
|
|
+ ></el-button>
|
|
|
|
+ </el-tooltip>
|
|
<el-tooltip
|
|
<el-tooltip
|
|
class="item"
|
|
class="item"
|
|
effect="dark"
|
|
effect="dark"
|
|
@@ -463,6 +481,67 @@
|
|
>
|
|
>
|
|
</span>
|
|
</span>
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
+
|
|
|
|
+ <!-- 问答对弹窗 -->
|
|
|
|
+ <el-dialog
|
|
|
|
+ title="问答对"
|
|
|
|
+ :visible.sync="qaDialogVisible"
|
|
|
|
+ width="80%"
|
|
|
|
+ :before-close="handleQaDialogClose"
|
|
|
|
+ >
|
|
|
|
+ <div class="qa-dialog-content">
|
|
|
|
+ <div class="qa-dialog-header">
|
|
|
|
+ <el-tag>文档ID: {{ currentDocumentId }}</el-tag>
|
|
|
|
+ <el-tag type="success" v-if="qaList.length">
|
|
|
|
+ 共 {{ qaList.length }} 个问答对
|
|
|
|
+ </el-tag>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="qa-content" v-loading="qaLoading">
|
|
|
|
+ <div v-if="qaList.length === 0 && !qaLoading" class="empty-state">
|
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
|
+ <p>暂无问答对数据</p>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div v-else class="qa-list">
|
|
|
|
+ <div
|
|
|
|
+ v-for="(item, index) in qaList"
|
|
|
|
+ :key="index"
|
|
|
|
+ class="qa-item"
|
|
|
|
+ >
|
|
|
|
+ <div class="qa-question">
|
|
|
|
+ <div class="qa-label">问:</div>
|
|
|
|
+ <div class="qa-text">{{ item.question }}</div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="qa-answer">
|
|
|
|
+ <div class="qa-label">答:</div>
|
|
|
|
+ <div class="qa-text">{{ item.answer }}</div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="qa-meta" v-if="item.confidence || item.source">
|
|
|
|
+ <el-tag size="mini" v-if="item.confidence">
|
|
|
|
+ 置信度: {{ item.confidence }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ <el-tag size="mini" type="info" v-if="item.source">
|
|
|
|
+ 来源: {{ item.source }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <span slot="footer" class="dialog-footer">
|
|
|
|
+ <el-button
|
|
|
|
+ type="primary"
|
|
|
|
+ icon="el-icon-refresh"
|
|
|
|
+ @click="fetchQAData"
|
|
|
|
+ :loading="qaLoading"
|
|
|
|
+ >
|
|
|
|
+ 刷新数据
|
|
|
|
+ </el-button>
|
|
|
|
+ <el-button @click="handleQaDialogClose">关闭</el-button>
|
|
|
|
+ </span>
|
|
|
|
+ </el-dialog>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@@ -492,6 +571,8 @@ import {
|
|
resultTask,
|
|
resultTask,
|
|
endClear,
|
|
endClear,
|
|
} from "@/api/knowledge";
|
|
} from "@/api/knowledge";
|
|
|
|
+import { exportQaToExcel,queryDocumentContent } from "@/api/document";
|
|
|
|
+import { validateExportParams, logJSON, checkJSONSize } from "@/utils/data-validator";
|
|
export default {
|
|
export default {
|
|
components: {
|
|
components: {
|
|
dataSearch,
|
|
dataSearch,
|
|
@@ -574,6 +655,11 @@ export default {
|
|
selectedRows: [], // 已选择的行
|
|
selectedRows: [], // 已选择的行
|
|
allSelectedRows: new Map(), // 存储所有选中的行
|
|
allSelectedRows: new Map(), // 存储所有选中的行
|
|
isAllDataSelected: false, // 是否选中所有数据
|
|
isAllDataSelected: false, // 是否选中所有数据
|
|
|
|
+ /* 问答对弹窗 */
|
|
|
|
+ qaDialogVisible: false,
|
|
|
|
+ currentDocumentId: null,
|
|
|
|
+ qaList: [],
|
|
|
|
+ qaLoading: false,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
mounted() {
|
|
mounted() {
|
|
@@ -589,6 +675,185 @@ export default {
|
|
Object.values(this.analysisStatusCheckers).forEach(clearTimeout);
|
|
Object.values(this.analysisStatusCheckers).forEach(clearTimeout);
|
|
},
|
|
},
|
|
methods: {
|
|
methods: {
|
|
|
|
+ /* 导出 */
|
|
|
|
+ async exportData() {
|
|
|
|
+ try {
|
|
|
|
+ // 显示加载状态
|
|
|
|
+ const loading = this.$loading({
|
|
|
|
+ lock: true,
|
|
|
|
+ text: '正在导出,请稍候...',
|
|
|
|
+ spinner: 'el-icon-loading',
|
|
|
|
+ background: 'rgba(0, 0, 0, 0.7)'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 准备导出参数
|
|
|
|
+ const exportParams = {
|
|
|
|
+ kb_id: this.queryForm.bucket_id, // 知识库ID
|
|
|
|
+ doc_type_id: this.queryForm.doc_type_id === 0 ? null : this.queryForm.doc_type_id, // 目录ID,0表示全部
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // 如果有选中的文档,只导出选中的
|
|
|
|
+ if (this.selectedRows.length > 0) {
|
|
|
|
+ exportParams.document_ids = this.selectedRows.map(row => row.id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 验证和清理导出参数
|
|
|
|
+ const validation = validateExportParams(exportParams);
|
|
|
|
+
|
|
|
|
+ if (!validation.isValid) {
|
|
|
|
+ loading.close();
|
|
|
|
+ this.$message.error(`参数验证失败: ${validation.errors.join(', ')}`);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (validation.warnings.length > 0) {
|
|
|
|
+ console.warn('参数警告:', validation.warnings);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 使用清理后的参数
|
|
|
|
+ const finalParams = validation.cleanedParams;
|
|
|
|
+
|
|
|
|
+ // 检查JSON数据大小
|
|
|
|
+ const sizeInfo = checkJSONSize(finalParams);
|
|
|
|
+ if (sizeInfo.isLarge) {
|
|
|
|
+ console.warn(`导出数据较大: ${sizeInfo.kb}KB`);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 日志输出
|
|
|
|
+ logJSON(finalParams, '最终导出参数');
|
|
|
|
+
|
|
|
|
+ // 调用导出接口
|
|
|
|
+ const response = await exportQaToExcel(finalParams);
|
|
|
|
+ // 检查响应格式
|
|
|
|
+ if (response && response.status === 200 ) {
|
|
|
|
+ const exportData = response.data.data;
|
|
|
|
+ console.log(exportData);
|
|
|
|
+ // 如果任务正在处理中,显示提示信息
|
|
|
|
+ if (exportData.status === 'processing') {
|
|
|
|
+ loading.close();
|
|
|
|
+ /* this.$message.info('导出任务已提交,正在处理中...'); */
|
|
|
|
+
|
|
|
|
+ // 如果有下载链接,直接下载
|
|
|
|
+ if (exportData.download_url) {
|
|
|
|
+ this.downloadFile(exportData.download_url, exportData.file_name);
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 如果任务已完成,直接下载
|
|
|
|
+ if (exportData.download_url) {
|
|
|
|
+ this.downloadFile(exportData.download_url, exportData.file_name);
|
|
|
|
+ loading.close();
|
|
|
|
+ this.$message.success('导出成功!');
|
|
|
|
+ } else {
|
|
|
|
+ loading.close();
|
|
|
|
+ this.$message.error('获取下载链接失败');
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // 兼容原来的直接返回文件流的情况
|
|
|
|
+ const blob = new Blob([response], {
|
|
|
|
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
+ link.href = url;
|
|
|
|
+
|
|
|
|
+ // 设置文件名
|
|
|
|
+ const fileName = `问答对导出_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
|
|
|
+ link.download = fileName;
|
|
|
|
+
|
|
|
|
+ // 触发下载
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
+ link.click();
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
|
+
|
|
|
|
+ loading.close();
|
|
|
|
+ this.$message.success('导出成功!');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('导出失败:', error);
|
|
|
|
+ this.$message.error('导出失败,请稍后重试');
|
|
|
|
+ if (loading) loading.close();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 下载文件的通用方法
|
|
|
|
+ downloadFile(downloadUrl, fileName) {
|
|
|
|
+ try {
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
+ link.href = downloadUrl;
|
|
|
|
+ link.download = fileName || `问答对导出_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
|
|
|
+ link.target = '_blank'; // 在新窗口打开,避免跨域问题
|
|
|
|
+
|
|
|
|
+ // 触发下载
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
+ link.click();
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
+
|
|
|
|
+ this.$message.success('文件下载已开始!');
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('下载失败:', error);
|
|
|
|
+ this.$message.error('文件下载失败,请稍后重试');
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ /* 查看问答对 */
|
|
|
|
+ getQa(id) {
|
|
|
|
+ this.currentDocumentId = id;
|
|
|
|
+ this.qaDialogVisible = true;
|
|
|
|
+ this.fetchQAData();
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /* 获取问答对数据 */
|
|
|
|
+ async fetchQAData() {
|
|
|
|
+ if (!this.currentDocumentId) return;
|
|
|
|
+
|
|
|
|
+ this.qaLoading = true;
|
|
|
|
+ try {
|
|
|
|
+ const response = await queryDocumentContent({
|
|
|
|
+ document_id: this.currentDocumentId
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (response.status === 200 ) {
|
|
|
|
+ // 根据接口返回的数据结构调整
|
|
|
|
+ this.qaList = response.data[0].qa_pairs || response.result || [];
|
|
|
|
+
|
|
|
|
+ // 如果接口返回的不是数组格式,进行处理
|
|
|
|
+ if (!Array.isArray(this.qaList)) {
|
|
|
|
+ if (this.qaList.items) {
|
|
|
|
+ this.qaList = this.qaList.items;
|
|
|
|
+ } else if (this.qaList.qa_pairs) {
|
|
|
|
+ this.qaList = this.qaList.qa_pairs;
|
|
|
|
+ console.log(this.qaList);
|
|
|
|
+ } else {
|
|
|
|
+ this.qaList = [];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (this.qaList.length === 0) {
|
|
|
|
+ this.$message.info('该文档暂无问答对数据');
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ this.$message.error(response.message || '获取问答对数据失败');
|
|
|
|
+ this.qaList = [];
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('获取问答对数据失败:', error);
|
|
|
|
+ this.$message.error('获取问答对数据失败,请稍后重试');
|
|
|
|
+ this.qaList = [];
|
|
|
|
+ } finally {
|
|
|
|
+ this.qaLoading = false;
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ /* 关闭问答对弹窗 */
|
|
|
|
+ handleQaDialogClose() {
|
|
|
|
+ this.qaDialogVisible = false;
|
|
|
|
+ this.currentDocumentId = null;
|
|
|
|
+ this.qaList = [];
|
|
|
|
+ },
|
|
// 获取行的唯一标识
|
|
// 获取行的唯一标识
|
|
getRowKey(row) {
|
|
getRowKey(row) {
|
|
return row.id;
|
|
return row.id;
|
|
@@ -1755,5 +2020,95 @@ export default {
|
|
justify-content: flex-end;
|
|
justify-content: flex-end;
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
|
|
+/* 问答对弹窗样式 */
|
|
|
|
+.qa-dialog-content {
|
|
|
|
+ padding: 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-dialog-header {
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
+ padding-bottom: 12px;
|
|
|
|
+ border-bottom: 1px solid #e8e8e8;
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 8px;
|
|
|
|
+ align-items: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-content {
|
|
|
|
+ min-height: 300px;
|
|
|
|
+ max-height: 500px;
|
|
|
|
+ overflow-y: auto;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.empty-state {
|
|
|
|
+ text-align: center;
|
|
|
|
+ padding: 60px 0;
|
|
|
|
+ color: #999;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.empty-state i {
|
|
|
|
+ font-size: 48px;
|
|
|
|
+ color: #ddd;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+ display: block;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.empty-state p {
|
|
|
|
+ margin: 0;
|
|
|
|
+ font-size: 16px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-list {
|
|
|
|
+ space-y: 20px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-item {
|
|
|
|
+ background: #f8f9fa;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ padding: 20px;
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-item:hover {
|
|
|
|
+ box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-question, .qa-answer {
|
|
|
|
+ display: flex;
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
+ align-items: flex-start;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-answer {
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-label {
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
+ width: 40px;
|
|
|
|
+ font-weight: bold;
|
|
|
|
+ color: #409eff;
|
|
|
|
+ margin-right: 8px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-question .qa-label {
|
|
|
|
+ color: #e6a23c;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-text {
|
|
|
|
+ flex: 1;
|
|
|
|
+ line-height: 1.6;
|
|
|
|
+ color: #333;
|
|
|
|
+ word-break: break-word;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.qa-meta {
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 8px;
|
|
|
|
+ padding-top: 8px;
|
|
|
|
+ border-top: 1px solid #eee;
|
|
|
|
+}
|
|
</style>
|
|
</style>
|
|
|
|
|