gaoluyang
6 天以前 6ef3449ab07299b34f7ad1e6e4c3d3da4b3d2d1b
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -244,6 +244,7 @@
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            :before-upload="beforeUpload"
            name="files"
            multiple
            :show-file-list="false"
            accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
@@ -259,6 +260,22 @@
          >
            保存文件关联
          </el-button>
          <el-button
            v-if="uploadedBlobIds.length > 0"
            type="text"
            @click="clearUploadedFiles"
            style="margin-left: 10px"
          >
            清空待保存列表
          </el-button>
        </div>
        <!-- 待保存的文件列表 -->
        <div v-if="uploadedBlobIds.length > 0" class="uploaded-list">
          <div class="uploaded-tip">
            <el-icon style="color: #409eff"><InfoFilled /></el-icon>
            <span>已上传 {{ uploadedBlobIds.length }} 个文件,请点击"保存文件关联"按钮触发向量化处理</span>
          </div>
        </div>
        <!-- 文件列表与向量化状态 -->
@@ -273,6 +290,12 @@
            </template>
          </el-table-column>
          <el-table-column prop="chunkCount" label="切片数" width="100" align="center" />
          <el-table-column label="错误信息" width="200" show-overflow-tooltip>
            <template #default="{ row }">
              <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span>
              <span v-else style="color: #909399">-</span>
            </template>
          </el-table-column>
          <el-table-column prop="createTime" label="上传时间" width="180" />
          <el-table-column label="操作" width="150" align="center">
            <template #default="{ row }">
@@ -326,7 +349,7 @@
        <div class="chat-input">
          <el-input
            v-model="inputQuestion"
            placeholder="请输入问题,按回车发送"
            placeholder="请输入问题,按回车发送(Ctrl+Enter快捷发送)"
            @keyup.enter="sendMessage"
            :disabled="chatLoading"
          >
@@ -334,6 +357,9 @@
              <el-button @click="sendMessage" :loading="chatLoading">发送</el-button>
            </template>
          </el-input>
          <div class="chat-actions">
            <el-button type="text" size="small" @click="clearMessages">清空对话</el-button>
          </div>
        </div>
      </div>
    </FormDialog>
@@ -341,7 +367,7 @@
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { Search, InfoFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
@@ -354,7 +380,7 @@
  getVectorStatus,
  reprocessVector,
  saveKnowledgeBaseFiles,
  deleteKnowledgeBaseFiles,
  deleteKnowledgeBaseFile,
  knowledgeChat
} from "@/api/collaborativeApproval/knowledgeBase.js";
import useUserStore from '@/store/modules/user';
@@ -412,6 +438,7 @@
  fileList: [],
  uploadedBlobIds: [],
  savingFiles: false,
  vectorStatusTimer: null, // 向量化状态轮询定时器
  chatDialogVisible: false,
  messages: [],
  inputQuestion: "",
@@ -436,6 +463,7 @@
  fileList,
  uploadedBlobIds,
  savingFiles,
  vectorStatusTimer,
  chatDialogVisible,
  messages,
  inputQuestion,
@@ -597,21 +625,31 @@
const getList = () => {
  tableLoading.value = true;
  listKnowledgeBase({...page.value, ...searchForm.value})
  // ✅ GET请求使用params传参
  listKnowledgeBase({
    current: page.value.current,
    size: page.value.size,
    title: searchForm.value.title,
    type: searchForm.value.type
  })
  .then(res => {
    tableLoading.value = false;
    page.value.total = res.data.total;
    // 如果当前页数超过总页数,重置到第1页并重新查询
    // 如果当前页数超过总页数,重置到第1页并重新查询
    const maxPage = Math.ceil(res.data.total / page.value.size) || 1;
    if (page.value.current > maxPage && maxPage > 0) {
      page.value.current = 1;
      // 重新查询第1页数据
      return getList();
    }
    tableData.value = res.data.records;
  }).catch(err => {
    tableLoading.value = false;
  })
  .catch(err => {
    tableLoading.value = false;
    console.error("查询知识库列表失败:", err);
  });
};
// 分页处理
@@ -786,27 +824,47 @@
const submitForm = async () => {
  try {
    await formRef.value.validate();
    // ✅ POST请求使用data传参,明确参数结构
    const formData = {
      title: form.value.title,
      type: form.value.type,
      scenario: form.value.scenario || "",
      efficiency: form.value.efficiency || "",
      problem: form.value.problem,
      solution: form.value.solution,
      keyPoints: form.value.keyPoints || "",
      creator: form.value.creator || "",
      usageCount: form.value.usageCount || 0
    };
    if (dialogType.value === "add") {
      // 新增知识
      addKnowledgeBase({...form.value}).then(res => {
      addKnowledgeBase(formData).then(res => {
        if(res.code == 200){
          ElMessage.success("添加成功");
          closeKnowledgeDialog();
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
        console.error("添加知识库失败:", err);
        ElMessage.error(err.msg || "添加失败");
      });
    } else {
      updateKnowledgeBase({...form.value}).then(res => {
      // 更新知识 - 添加id参数
      updateKnowledgeBase({
        id: form.value.id,
        ...formData
      }).then(res => {
        if(res.code == 200){
          ElMessage.success("更新成功");
          closeKnowledgeDialog();
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
        console.error("更新知识库失败:", err);
        ElMessage.error(err.msg || "更新失败");
      });
    }
  } catch (error) {
    console.error("表单验证失败:", error);
@@ -820,19 +878,22 @@
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // console.log(selectedIds.value);
    // ✅ DELETE请求使用data传递ID数组
    delKnowledgeBase(selectedIds.value).then(res => {
      if(res.code == 200){
        ElMessage.success("删除成功");
        selectedIds.value = [];
        getList();
      }
    })
    }).catch(err => {
      console.error("删除知识库失败:", err);
      ElMessage.error(err.msg || "删除失败");
    });
  }).catch(() => {
    // 用户取消
  });
@@ -872,9 +933,45 @@
  try {
    const res = await getVectorStatus(currentKnowledgeBase.value.id);
    fileList.value = res.data || [];
    // 检查是否有处理中的文件,如果有则启动轮询
    const hasProcessing = res.data.some(item => item.vectorStatus === 1);
    if (hasProcessing && !vectorStatusTimer.value) {
      startVectorStatusPolling();
    } else if (!hasProcessing && vectorStatusTimer.value) {
      stopVectorStatusPolling();
    }
  } catch (error) {
    console.error("加载文件列表失败:", error);
    ElMessage.error("加载文件列表失败");
  }
};
// 开始轮询向量化状态
const startVectorStatusPolling = () => {
  vectorStatusTimer.value = setInterval(async () => {
    try {
      const res = await getVectorStatus(currentKnowledgeBase.value.id);
      fileList.value = res.data || [];
      // 检查是否还有处理中的文件
      const hasProcessing = res.data.some(item => item.vectorStatus === 1);
      if (!hasProcessing) {
        stopVectorStatusPolling();
        ElMessage.success("所有文件向量化处理完成");
      }
    } catch (error) {
      console.error("轮询向量化状态失败:", error);
      stopVectorStatusPolling();
    }
  }, 3000); // 每3秒轮询一次
};
// 停止轮询向量化状态
const stopVectorStatusPolling = () => {
  if (vectorStatusTimer.value) {
    clearInterval(vectorStatusTimer.value);
    vectorStatusTimer.value = null;
  }
};
@@ -900,9 +997,24 @@
// 上传成功
const handleUploadSuccess = (response, file) => {
  console.log("上传响应:", response);  // 调试日志
  if (response.code === 200) {
    uploadedBlobIds.value.push(response.data.id);
    ElMessage.success(`文件 ${file.name} 上传成功`);
    // ✅ 后端返回的是 List<StorageBlobVO>,所以data是数组
    if (Array.isArray(response.data) && response.data.length > 0) {
      // 取数组第一个元素的id
      const blobId = response.data[0].id;
      if (blobId) {
        uploadedBlobIds.value.push(blobId);
        ElMessage.success(`文件 ${file.name} 上传成功`);
      } else {
        console.error("上传响应中未找到id:", response.data[0]);
        ElMessage.error("上传失败: 未获取到文件ID");
      }
    } else {
      console.error("上传响应格式错误:", response);
      ElMessage.error("上传失败: 响应格式错误");
    }
  } else {
    ElMessage.error(response.msg || "上传失败");
  }
@@ -915,22 +1027,30 @@
// 保存文件关联
const saveFiles = async () => {
  // 参数校验
  if (!currentKnowledgeBase.value?.id) {
    ElMessage.error("知识库信息异常");
    return;
  }
  if (uploadedBlobIds.value.length === 0) {
    ElMessage.warning("请先上传文件");
    return;
  }
  savingFiles.value = true;
  try {
    // ✅ POST请求使用data传参,明确参数结构
    await saveKnowledgeBaseFiles({
      knowledgeBaseId: currentKnowledgeBase.value.id,
      storageBlobIds: uploadedBlobIds.value
      knowledgeBaseId: currentKnowledgeBase.value.id,  // 知识库ID
      storageBlobIds: uploadedBlobIds.value             // 文件blob ID数组
    });
    ElMessage.success("文件关联保存成功,正在后台处理向量化");
    ElMessage.success("文件关联保存成功,正在后台处理向量化");
    uploadedBlobIds.value = [];
    // 延迟刷新文件列表,给后台处理时间
    // 延迟刷新文件列表,给后台处理时间
    setTimeout(() => {
      loadFileList();
    }, 1000);
@@ -957,11 +1077,17 @@
  }
};
// 清空待保存的文件列表
const clearUploadedFiles = () => {
  uploadedBlobIds.value = [];
  ElMessage.success("已清空待保存文件列表");
};
// 删除文件
const deleteFile = async (row) => {
  try {
    await ElMessageBox.confirm(
      "确定要删除该文件吗?删除后将无法恢复向量数据",
      "确定要删除该文件吗?删除后将无法恢复向量数据",
      "删除确认",
      {
        confirmButtonText: "确定",
@@ -970,7 +1096,8 @@
      }
    );
    await deleteKnowledgeBaseFiles([row.id]);
    // ✅ DELETE请求使用data传递ID数组
    await deleteKnowledgeBaseFile([row.id]);  // 注意: row.id是向量记录ID,不是storageBlobId
    ElMessage.success("删除成功");
    loadFileList();
  } catch (error) {
@@ -1009,24 +1136,44 @@
  currentKnowledgeBase.value = null;
  fileList.value = [];
  uploadedBlobIds.value = [];
  getList(); // 刷新主列表,更新文件数量
  stopVectorStatusPolling(); // 停止轮询
  getList(); // 刷新主列表,更新文件数量
};
// ============ 知识库问答相关 ============
// 生成UUID的fallback方案
const generateUUID = () => {
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
    return crypto.randomUUID();
  }
  // fallback: 兼容不支持 crypto.randomUUID 的环境
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    const r = Math.random() * 16 | 0;
    const v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
};
// 打开问答弹窗
const openChatDialog = (row) => {
  currentKnowledgeBase.value = row;
  chatDialogVisible.value = true;
  memoryId.value = crypto.randomUUID();
  memoryId.value = generateUUID();
  messages.value = [];
  inputQuestion.value = "";
};
// 发送消息
const sendMessage = async () => {
  // 参数校验
  if (!inputQuestion.value.trim()) {
    ElMessage.warning("请输入问题");
    return;
  }
  if (!currentKnowledgeBase.value?.id) {
    ElMessage.error("知识库信息异常");
    return;
  }
@@ -1046,25 +1193,19 @@
  scrollToBottom();
  try {
    // 流式请求
    const response = await fetch('/api/ai/knowledge/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + getToken()
      },
      body: JSON.stringify({
        knowledgeBaseId: currentKnowledgeBase.value.id,
        memoryId: memoryId.value,
        question: question
      })
    // ✅ 流式请求使用Fetch API
    const response = await knowledgeChat({
      knowledgeBaseId: currentKnowledgeBase.value.id,  // 知识库ID
      memoryId: memoryId.value,                         // 会话ID
      question: question                                // 用户问题
    });
    if (!response.ok) {
      throw new Error('请求失败');
      const errorText = await response.text();
      throw new Error(errorText || '请求失败');
    }
    // 处理SSE流式响应
    // ✅ 后端返回 text/stream;charset=utf-8
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let aiContent = '';
@@ -1075,7 +1216,7 @@
      const { done, value } = await reader.read();
      if (done) break;
      const text = decoder.decode(value);
      const text = decoder.decode(value, { stream: true });  // ✅ 添加stream选项
      aiContent += text;
      messages.value[messages.value.length - 1].content = aiContent;
@@ -1083,16 +1224,40 @@
      await nextTick();
      scrollToBottom();
    }
    // 如果AI返回空内容,显示提示
    if (!aiContent.trim()) {
      messages.value[messages.value.length - 1].content = '抱歉,知识库中未找到相关内容,请尝试其他问题。';
    }
  } catch (error) {
    console.error("问答请求失败:", error);
    ElMessage.error("问答请求失败,请稍后重试");
    ElMessage.error("问答请求失败,请稍后重试");
    messages.value.push({
      role: 'assistant',
      content: '抱歉,发生了错误,请稍后重试'
      content: '抱歉,发生了错误,请稍后重试'
    });
  } finally {
    chatLoading.value = false;
  }
};
// 清空对话
const clearMessages = () => {
  ElMessageBox.confirm(
    "确定要清空所有对话记录吗?",
    "清空确认",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    messages.value = [];
    memoryId.value = generateUUID(); // 重新生成会话ID
    ElMessage.success("对话已清空");
  }).catch(() => {
    // 用户取消
  });
};
// 滚动到底部
@@ -1195,6 +1360,22 @@
  align-items: center;
}
.uploaded-list {
  margin-top: 16px;
  padding: 12px;
  background: #f0f9ff;
  border-radius: 6px;
  border: 1px solid #b3d8ff;
}
.uploaded-tip {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #409eff;
  font-size: 14px;
}
/* 知识库问答样式 */
.knowledge-chat {
  display: flex;
@@ -1272,4 +1453,9 @@
  margin-top: auto;
}
.chat-actions {
  margin-top: 8px;
  text-align: right;
}
</style>