<template>
|
<div class="app-container">
|
<div class="search_form" style="margin-bottom: 20px;">
|
<div>
|
<span class="search_title">知识标题:</span>
|
<el-input
|
v-model="searchForm.title"
|
style="width: 240px"
|
placeholder="请输入知识标题搜索"
|
@change="handleQuery"
|
clearable
|
:prefix-icon="Search"
|
/>
|
<span class="search_title ml10">知识类型:</span>
|
<el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
|
<el-option
|
v-for="item in knowledgeTypeOptions"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
<el-button type="primary" @click="handleQuery" style="margin-left: 10px">
|
搜索
|
</el-button>
|
</div>
|
<div>
|
<el-button @click="handleExport" style="margin-right: 10px">导出</el-button>
|
<el-button type="primary" @click="openForm('add')">新增知识</el-button>
|
<el-button type="danger" plain @click="handleDelete">删除</el-button>
|
</div>
|
</div>
|
|
<div class="table_list">
|
<PIMTable
|
rowKey="id"
|
:column="tableColumn"
|
:tableData="tableData"
|
:page="page"
|
:isSelection="true"
|
@selection-change="handleSelectionChange"
|
:tableLoading="tableLoading"
|
@pagination="pagination"
|
:total="page.total"
|
></PIMTable>
|
</div>
|
|
<!-- 新增/编辑知识弹窗 -->
|
<FormDialog
|
v-model="dialogVisible"
|
:title="dialogTitle"
|
:width="'800px'"
|
@close="closeKnowledgeDialog"
|
@confirm="submitForm"
|
@cancel="closeKnowledgeDialog"
|
>
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="知识标题" prop="title">
|
<el-input v-model="form.title" placeholder="请输入知识标题" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="知识类型" prop="type">
|
<el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%">
|
<el-option
|
v-for="item in knowledgeTypeOptions"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="适用场景" prop="scenario">
|
<el-input v-model="form.scenario" placeholder="请输入适用场景" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="解决效率" prop="efficiency">
|
<el-select v-model="form.efficiency" placeholder="请选择解决效率" style="width: 100%">
|
<el-option label="显著提升" value="high" />
|
<el-option label="一般提升" value="medium" />
|
<el-option label="轻微提升" value="low" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-form-item label="问题描述" prop="problem">
|
<el-input
|
v-model="form.problem"
|
type="textarea"
|
:rows="3"
|
placeholder="请描述遇到的问题"
|
/>
|
</el-form-item>
|
<el-form-item label="解决方案" prop="solution">
|
<el-input
|
v-model="form.solution"
|
type="textarea"
|
:rows="4"
|
placeholder="请详细描述解决方案"
|
/>
|
</el-form-item>
|
<el-form-item label="关键要点" prop="keyPoints">
|
<el-input
|
v-model="form.keyPoints"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入关键要点,用逗号分隔"
|
/>
|
</el-form-item>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="创建人" prop="creator">
|
<el-select v-model="form.creator" placeholder="请选择创建人" style="width: 100%" filterable>
|
<el-option
|
v-for="user in userList"
|
:key="user.userId"
|
:label="user.nickName"
|
:value="user.nickName"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="使用次数" prop="usageCount">
|
<el-input-number v-model="form.usageCount" :min="0" style="width: 100%" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</FormDialog>
|
|
<!-- 查看知识详情弹窗 -->
|
<FormDialog
|
v-model="viewDialogVisible"
|
title="知识详情"
|
:width="'900px'"
|
@close="closeViewDialog"
|
@confirm="handleViewDialogConfirm"
|
@cancel="closeViewDialog"
|
>
|
<div class="knowledge-detail">
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="知识标题" :span="2">
|
<span class="detail-title">{{ currentKnowledge.title }}</span>
|
</el-descriptions-item>
|
<el-descriptions-item label="知识类型">
|
<el-tag :type="getTypeTagType(currentKnowledge.type)">
|
{{ getTypeLabel(currentKnowledge.type) }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="适用场景">
|
{{ currentKnowledge.scenario }}
|
</el-descriptions-item>
|
<el-descriptions-item label="解决效率">
|
<el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)">
|
{{ getEfficiencyLabel(currentKnowledge.efficiency) }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="使用次数">
|
<el-tag type="info">{{ currentKnowledge.usageCount }} 次</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="创建人">
|
{{ currentKnowledge.creator }}
|
</el-descriptions-item>
|
<el-descriptions-item label="创建时间">
|
{{ currentKnowledge.createTime }}
|
</el-descriptions-item>
|
</el-descriptions>
|
|
<div class="detail-section">
|
<h4>问题描述</h4>
|
<div class="detail-content">{{ currentKnowledge.problem }}</div>
|
</div>
|
|
<div class="detail-section">
|
<h4>解决方案</h4>
|
<div class="detail-content">{{ currentKnowledge.solution }}</div>
|
</div>
|
|
<div class="detail-section">
|
<h4>关键要点</h4>
|
<div class="key-points">
|
<el-tag
|
v-for="(point, index) in currentKnowledge.keyPoints?.split(',') || []"
|
:key="index"
|
type="success"
|
style="margin-right: 8px; margin-bottom: 8px;"
|
>
|
{{ point.trim() }}
|
</el-tag>
|
</div>
|
</div>
|
|
<div class="detail-section">
|
<h4>使用统计</h4>
|
<div class="usage-stats">
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-number">{{ currentKnowledge.usageCount }}</div>
|
<div class="stat-label">使用次数</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div>
|
<div class="stat-label">效率提升</div>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="stat-item">
|
<div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div>
|
<div class="stat-label">平均节省时间</div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</div>
|
</div>
|
</FormDialog>
|
|
<!-- 文件管理弹窗 -->
|
<FormDialog
|
v-model="filesDialogVisible"
|
title="文件管理"
|
:width="'900px'"
|
@close="closeFilesDialog"
|
@confirm="closeFilesDialog"
|
@cancel="closeFilesDialog"
|
>
|
<div class="file-manager">
|
<!-- 文件上传 -->
|
<div class="upload-section">
|
<el-upload
|
:action="uploadUrl"
|
:headers="uploadHeaders"
|
:on-success="handleUploadSuccess"
|
:on-error="handleUploadError"
|
:before-upload="beforeUpload"
|
name="files"
|
multiple
|
:show-file-list="false"
|
accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
|
>
|
<el-button type="primary">上传文件</el-button>
|
</el-upload>
|
<el-button
|
type="success"
|
@click="saveFiles"
|
:disabled="uploadedBlobIds.length === 0"
|
:loading="savingFiles"
|
style="margin-left: 10px"
|
>
|
保存文件关联
|
</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>
|
|
<!-- 文件列表与向量化状态 -->
|
<el-table :data="fileList" style="margin-top: 20px" border>
|
<el-table-column prop="fileName" label="文件名" show-overflow-tooltip />
|
<el-table-column prop="fileType" label="文件类型" width="100" />
|
<el-table-column label="向量化状态" width="120">
|
<template #default="{ row }">
|
<el-tag :type="getStatusType(row.vectorStatus)">
|
{{ getStatusText(row.vectorStatus) }}
|
</el-tag>
|
</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 }">
|
<el-button
|
v-if="row.vectorStatus === 3"
|
type="text"
|
@click="reprocessFile(row)"
|
>
|
重新处理
|
</el-button>
|
<el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
|
删除
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</FormDialog>
|
|
<!-- 知识库问答弹窗 -->
|
<FormDialog
|
v-model="chatDialogVisible"
|
title="知识库问答"
|
:width="'800px'"
|
@close="closeChatDialog"
|
@confirm="closeChatDialog"
|
@cancel="closeChatDialog"
|
>
|
<div class="knowledge-chat">
|
<div class="chat-header">
|
<el-tag type="success">当前知识库: {{ currentKnowledgeBase?.title }}</el-tag>
|
</div>
|
|
<!-- 对话区域 -->
|
<div class="chat-messages" ref="chatMessagesRef">
|
<div
|
v-for="(msg, index) in messages"
|
:key="index"
|
:class="['message', msg.role]"
|
>
|
<div class="message-role">{{ msg.role === 'user' ? '我' : 'AI助手' }}</div>
|
<div class="message-content">{{ msg.content }}</div>
|
</div>
|
<div v-if="chatLoading" class="message assistant">
|
<div class="message-role">AI助手</div>
|
<div class="message-content typing">正在思考中...</div>
|
</div>
|
</div>
|
|
<!-- 输入框 -->
|
<div class="chat-input">
|
<el-input
|
v-model="inputQuestion"
|
placeholder="请输入问题,按回车发送(Ctrl+Enter快捷发送)"
|
@keyup.enter="sendMessage"
|
:disabled="chatLoading"
|
>
|
<template #append>
|
<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>
|
</div>
|
</template>
|
|
<script setup>
|
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";
|
import FormDialog from '@/components/Dialog/FormDialog.vue';
|
import {
|
listKnowledgeBase,
|
delKnowledgeBase,
|
addKnowledgeBase,
|
updateKnowledgeBase,
|
getVectorStatus,
|
reprocessVector,
|
saveKnowledgeBaseFiles,
|
deleteKnowledgeBaseFile,
|
knowledgeChat
|
} from "@/api/collaborativeApproval/knowledgeBase.js";
|
import useUserStore from '@/store/modules/user';
|
import { userListNoPageByTenantId } from '@/api/system/user.js';
|
import { getToken } from "@/utils/auth";
|
|
// 表单验证规则
|
const rules = {
|
title: [
|
{ required: true, message: "请输入知识标题", trigger: "blur" }
|
],
|
type: [
|
{ required: true, message: "请选择知识类型", trigger: "change" }
|
],
|
problem: [
|
{ required: true, message: "请描述遇到的问题", trigger: "blur" }
|
],
|
solution: [
|
{ required: true, message: "请详细描述解决方案", trigger: "blur" }
|
]
|
};
|
|
// 响应式数据
|
const data = reactive({
|
searchForm: {
|
title: "",
|
type: "",
|
},
|
tableLoading: false,
|
page: {
|
current: 1,
|
size: 20,
|
total: 0,
|
},
|
tableData: [],
|
selectedIds: [],
|
form: {
|
title: "",
|
type: "",
|
scenario: "",
|
efficiency: "",
|
problem: "",
|
solution: "",
|
keyPoints: "",
|
creator: "",
|
usageCount: 0
|
},
|
dialogVisible: false,
|
dialogTitle: "",
|
dialogType: "add",
|
viewDialogVisible: false,
|
currentKnowledge: {},
|
filesDialogVisible: false,
|
currentKnowledgeBase: null,
|
fileList: [],
|
uploadedBlobIds: [],
|
savingFiles: false,
|
vectorStatusTimer: null, // 向量化状态轮询定时器
|
chatDialogVisible: false,
|
messages: [],
|
inputQuestion: "",
|
chatLoading: false,
|
memoryId: ""
|
});
|
|
const {
|
searchForm,
|
tableLoading,
|
page,
|
tableData,
|
selectedIds,
|
form,
|
dialogVisible,
|
dialogTitle,
|
dialogType,
|
viewDialogVisible,
|
currentKnowledge,
|
filesDialogVisible,
|
currentKnowledgeBase,
|
fileList,
|
uploadedBlobIds,
|
savingFiles,
|
vectorStatusTimer,
|
chatDialogVisible,
|
messages,
|
inputQuestion,
|
chatLoading,
|
memoryId
|
} = toRefs(data);
|
|
// 表单引用
|
const formRef = ref();
|
// 用户相关
|
const userStore = useUserStore();
|
const userList = ref([]);
|
// 聊天消息容器引用
|
const chatMessagesRef = ref();
|
|
// 文件上传相关
|
const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
|
const uploadHeaders = { Authorization: "Bearer " + getToken() };
|
|
// 表格列配置
|
const tableColumn = ref([
|
{
|
label: "知识标题",
|
prop: "title",
|
showOverflowTooltip: true,
|
},
|
{
|
label: "知识类型",
|
prop: "type",
|
dataType: "tag",
|
formatData: (params) => {
|
return getKnowledgeTypeLabel(params);
|
},
|
formatType: (params) => {
|
return getKnowledgeTypeTagType(params);
|
}
|
},
|
{
|
label: "适用场景",
|
prop: "scenario",
|
width: 150,
|
showOverflowTooltip: true,
|
},
|
{
|
label: "解决效率",
|
prop: "efficiency",
|
dataType: "tag",
|
formatData: (params) => {
|
const efficiencyMap = {
|
high: "显著提升",
|
medium: "一般提升",
|
low: "轻微提升"
|
};
|
return efficiencyMap[params] || params;
|
},
|
formatType: (params) => {
|
const typeMap = {
|
high: "success",
|
medium: "warning",
|
low: "info"
|
};
|
return typeMap[params] || "info";
|
}
|
},
|
{
|
label: "文件数量",
|
prop: "fileCount",
|
width: 100,
|
align: "center"
|
},
|
{
|
label: "切片数量",
|
prop: "totalChunkCount",
|
width: 100,
|
align: "center"
|
},
|
{
|
label: "使用次数",
|
prop: "usageCount",
|
width: 100,
|
align: "center"
|
},
|
{
|
label: "创建人",
|
prop: "creator",
|
width: 120,
|
},
|
{
|
label: "创建时间",
|
prop: "createTime",
|
width: 180,
|
},
|
{
|
dataType: "action",
|
label: "操作",
|
align: "center",
|
fixed: "right",
|
width: 200,
|
operation: [
|
{
|
name: "编辑",
|
type: "text",
|
clickFun: (row) => {
|
openForm("edit", row);
|
}
|
},
|
{
|
name: "文件",
|
type: "text",
|
clickFun: (row) => {
|
openFilesDialog(row);
|
}
|
},
|
{
|
name: "问答",
|
type: "text",
|
clickFun: (row) => {
|
openChatDialog(row);
|
}
|
},
|
{
|
name: "详情",
|
type: "text",
|
clickFun: (row) => {
|
viewKnowledge(row);
|
}
|
}
|
]
|
}
|
]);
|
|
// 监听对话框打开,获取用户列表
|
watch(dialogVisible, (newVal) => {
|
if (newVal) {
|
userListNoPageByTenantId().then((res) => {
|
userList.value = res.data || [];
|
});
|
}
|
});
|
|
// 生命周期
|
onMounted(() => {
|
getList();
|
startAutoRefresh();
|
});
|
|
// 开始自动刷新
|
const startAutoRefresh = () => {
|
setInterval(() => {
|
getList();
|
}, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
|
};
|
|
// 查询数据
|
const handleQuery = () => {
|
page.value.current = 1;
|
getList();
|
};
|
|
const getList = () => {
|
tableLoading.value = true;
|
|
// ✅ 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页并重新查询
|
const maxPage = Math.ceil(res.data.total / page.value.size) || 1;
|
if (page.value.current > maxPage && maxPage > 0) {
|
page.value.current = 1;
|
return getList();
|
}
|
|
tableData.value = res.data.records;
|
})
|
.catch(err => {
|
tableLoading.value = false;
|
console.error("查询知识库列表失败:", err);
|
});
|
};
|
|
// 分页处理
|
const pagination = (obj) => {
|
const oldSize = page.value.size;
|
page.value.current = obj.page;
|
page.value.size = obj.limit;
|
// 如果 size 改变了,重置到第1页,避免当前页超出范围
|
if (oldSize !== obj.limit) {
|
page.value.current = 1;
|
}
|
getList();
|
};
|
|
// 选择变化处理
|
const handleSelectionChange = (selection) => {
|
selectedIds.value = selection.map(item => item.id);
|
};
|
|
// 打开表单
|
const openForm = (type, row = null) => {
|
dialogType.value = type;
|
if (type === "add") {
|
dialogTitle.value = "新增知识";
|
// 重置表单,默认创建人为当前用户
|
Object.assign(form.value, {
|
title: "",
|
type: "",
|
scenario: "",
|
efficiency: "",
|
problem: "",
|
solution: "",
|
keyPoints: "",
|
creator: userStore.nickName || "",
|
usageCount: 0
|
});
|
} else if (type === "edit" && row) {
|
dialogTitle.value = "编辑知识";
|
Object.assign(form.value, {
|
id: row.id,
|
title: row.title,
|
type: row.type,
|
scenario: row.scenario,
|
efficiency: row.efficiency,
|
problem: row.problem,
|
solution: row.solution,
|
keyPoints: row.keyPoints,
|
creator: row.creator,
|
usageCount: row.usageCount
|
});
|
}
|
dialogVisible.value = true;
|
};
|
|
// 查看知识详情
|
const viewKnowledge = (row) => {
|
currentKnowledge.value = { ...row };
|
viewDialogVisible.value = true;
|
};
|
|
// 获取类型标签类型
|
const getTypeTagType = (type) => {
|
const typeMap = {
|
contract: "success",
|
approval: "warning",
|
solution: "primary",
|
experience: "info",
|
guide: "danger"
|
};
|
return typeMap[type] || "info";
|
};
|
|
// 获取类型标签文本
|
const getTypeLabel = (type) => {
|
return getKnowledgeTypeLabel(type);
|
};
|
|
// 获取效率标签类型
|
const getEfficiencyTagType = (efficiency) => {
|
const typeMap = {
|
high: "success",
|
medium: "warning",
|
low: "info"
|
};
|
return typeMap[efficiency] || "info";
|
};
|
|
// 获取效率标签文本
|
const getEfficiencyLabel = (efficiency) => {
|
const efficiencyMap = {
|
high: "显著提升",
|
medium: "一般提升",
|
low: "轻微提升"
|
};
|
return efficiencyMap[efficiency] || efficiency;
|
};
|
|
// 获取效率提升百分比
|
const getEfficiencyScore = (efficiency) => {
|
const scoreMap = {
|
high: 40,
|
medium: 25,
|
low: 15
|
};
|
return scoreMap[efficiency] || 0;
|
};
|
|
// 获取平均节省时间
|
const getTimeSaved = (efficiency) => {
|
const timeMap = {
|
high: "2-3天",
|
medium: "1-2天",
|
low: "0.5-1天"
|
};
|
return timeMap[efficiency] || "未知";
|
};
|
|
// 复制知识
|
const copyKnowledge = () => {
|
const knowledgeText = `
|
知识标题:${currentKnowledge.value.title}
|
知识类型:${getTypeLabel(currentKnowledge.value.type)}
|
适用场景:${currentKnowledge.value.scenario}
|
问题描述:${currentKnowledge.value.problem}
|
解决方案:${currentKnowledge.value.solution}
|
关键要点:${currentKnowledge.value.keyPoints}
|
创建人:${currentKnowledge.value.creator}
|
`.trim();
|
|
// 复制到剪贴板
|
navigator.clipboard.writeText(knowledgeText).then(() => {
|
ElMessage.success("知识内容已复制到剪贴板");
|
}).catch(() => {
|
ElMessage.error("复制失败,请手动复制");
|
});
|
};
|
|
// 关闭知识表单对话框
|
const closeKnowledgeDialog = () => {
|
// 清空表单数据,默认创建人为当前用户
|
Object.assign(form.value, {
|
id: undefined,
|
title: "",
|
type: "",
|
scenario: "",
|
efficiency: "",
|
problem: "",
|
solution: "",
|
keyPoints: "",
|
creator: userStore.nickName || "",
|
usageCount: 0
|
});
|
// 清除表单验证状态
|
if (formRef.value) {
|
formRef.value.clearValidate();
|
}
|
dialogVisible.value = false;
|
};
|
|
// 关闭查看详情对话框
|
const closeViewDialog = () => {
|
viewDialogVisible.value = false;
|
};
|
|
// 处理查看详情对话框确认(执行复制操作)
|
const handleViewDialogConfirm = () => {
|
copyKnowledge();
|
closeViewDialog();
|
};
|
|
// 提交知识表单
|
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(formData).then(res => {
|
if(res.code == 200){
|
ElMessage.success("添加成功");
|
closeKnowledgeDialog();
|
getList();
|
}
|
}).catch(err => {
|
console.error("添加知识库失败:", err);
|
ElMessage.error(err.msg || "添加失败");
|
});
|
} else {
|
// 更新知识 - 添加id参数
|
updateKnowledgeBase({
|
id: form.value.id,
|
...formData
|
}).then(res => {
|
if(res.code == 200){
|
ElMessage.success("更新成功");
|
closeKnowledgeDialog();
|
getList();
|
}
|
}).catch(err => {
|
console.error("更新知识库失败:", err);
|
ElMessage.error(err.msg || "更新失败");
|
});
|
}
|
} catch (error) {
|
console.error("表单验证失败:", error);
|
}
|
};
|
|
// 删除知识
|
const handleDelete = () => {
|
if (selectedIds.value.length === 0) {
|
ElMessage.warning("请选择要删除的知识");
|
return;
|
}
|
|
ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
}).then(() => {
|
// ✅ 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(() => {
|
// 用户取消
|
});
|
};
|
|
// 导出
|
const { proxy } = getCurrentInstance()
|
const { knowledge_type } = proxy.useDict("knowledge_type")
|
|
// 字典工具
|
const knowledgeTypeOptions = computed(() => knowledge_type?.value || [])
|
const getKnowledgeTypeLabel = (val) => {
|
const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
|
return item ? item.label : val
|
}
|
const getKnowledgeTypeTagType = (val) => {
|
const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
|
return item?.elTagType || "info"
|
}
|
const handleExport = () => {
|
proxy.download('/knowledgeBase/export', { ...searchForm.value }, '知识库.xlsx')
|
}
|
|
// ============ 文件管理相关 ============
|
|
// 打开文件管理弹窗
|
const openFilesDialog = (row) => {
|
currentKnowledgeBase.value = row;
|
filesDialogVisible.value = true;
|
loadFileList();
|
};
|
|
// 加载文件列表
|
const loadFileList = async () => {
|
if (!currentKnowledgeBase.value?.id) return;
|
|
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;
|
}
|
};
|
|
// 上传前校验
|
const beforeUpload = (file) => {
|
const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
|
const fileName = file.name.toLowerCase();
|
const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
|
|
if (!isAllowed) {
|
ElMessage.error('只支持 txt、md、docx、xlsx、xls、pdf 格式的文件');
|
return false;
|
}
|
|
const isLt50M = file.size / 1024 / 1024 < 50;
|
if (!isLt50M) {
|
ElMessage.error('文件大小不能超过 50MB');
|
return false;
|
}
|
|
return true;
|
};
|
|
// 上传成功
|
const handleUploadSuccess = (response, file) => {
|
console.log("上传响应:", response); // 调试日志
|
|
if (response.code === 200) {
|
// ✅ 后端返回的是 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 || "上传失败");
|
}
|
};
|
|
// 上传失败
|
const handleUploadError = (error, file) => {
|
ElMessage.error(`文件 ${file.name} 上传失败`);
|
};
|
|
// 保存文件关联
|
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, // 知识库ID
|
storageBlobIds: uploadedBlobIds.value // 文件blob ID数组
|
});
|
|
ElMessage.success("文件关联保存成功,正在后台处理向量化");
|
uploadedBlobIds.value = [];
|
|
// 延迟刷新文件列表,给后台处理时间
|
setTimeout(() => {
|
loadFileList();
|
}, 1000);
|
} catch (error) {
|
console.error("保存文件关联失败:", error);
|
ElMessage.error("保存文件关联失败");
|
} finally {
|
savingFiles.value = false;
|
}
|
};
|
|
// 重新处理向量化的文件
|
const reprocessFile = async (row) => {
|
try {
|
await reprocessVector(row.id);
|
ElMessage.success("已重新提交向量化任务");
|
// 延迟刷新
|
setTimeout(() => {
|
loadFileList();
|
}, 1000);
|
} catch (error) {
|
console.error("重新处理失败:", error);
|
ElMessage.error("重新处理失败");
|
}
|
};
|
|
// 清空待保存的文件列表
|
const clearUploadedFiles = () => {
|
uploadedBlobIds.value = [];
|
ElMessage.success("已清空待保存文件列表");
|
};
|
|
// 删除文件
|
const deleteFile = async (row) => {
|
try {
|
await ElMessageBox.confirm(
|
"确定要删除该文件吗?删除后将无法恢复向量数据",
|
"删除确认",
|
{
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning"
|
}
|
);
|
|
// ✅ DELETE请求使用data传递ID数组
|
await deleteKnowledgeBaseFile([row.id]); // 注意: row.id是向量记录ID,不是storageBlobId
|
ElMessage.success("删除成功");
|
loadFileList();
|
} catch (error) {
|
if (error !== 'cancel') {
|
console.error("删除文件失败:", error);
|
ElMessage.error("删除文件失败");
|
}
|
}
|
};
|
|
// 状态文本映射
|
const getStatusText = (status) => {
|
const map = {
|
0: '待处理',
|
1: '处理中',
|
2: '已完成',
|
3: '失败'
|
};
|
return map[status] || '未知';
|
};
|
|
// 状态标签类型映射
|
const getStatusType = (status) => {
|
const map = {
|
0: 'info',
|
1: 'warning',
|
2: 'success',
|
3: 'danger'
|
};
|
return map[status] || 'info';
|
};
|
|
// 关闭文件管理弹窗
|
const closeFilesDialog = () => {
|
filesDialogVisible.value = false;
|
currentKnowledgeBase.value = null;
|
fileList.value = [];
|
uploadedBlobIds.value = [];
|
stopVectorStatusPolling(); // 停止轮询
|
getList(); // 刷新主列表,更新文件数量
|
};
|
|
// ============ 知识库问答相关 ============
|
|
// 打开问答弹窗
|
const openChatDialog = (row) => {
|
currentKnowledgeBase.value = row;
|
chatDialogVisible.value = true;
|
memoryId.value = crypto.randomUUID();
|
messages.value = [];
|
inputQuestion.value = "";
|
};
|
|
// 发送消息
|
const sendMessage = async () => {
|
// 参数校验
|
if (!inputQuestion.value.trim()) {
|
ElMessage.warning("请输入问题");
|
return;
|
}
|
|
if (!currentKnowledgeBase.value?.id) {
|
ElMessage.error("知识库信息异常");
|
return;
|
}
|
|
const question = inputQuestion.value.trim();
|
|
// 添加用户消息
|
messages.value.push({
|
role: 'user',
|
content: question
|
});
|
|
inputQuestion.value = "";
|
chatLoading.value = true;
|
|
// 滚动到底部
|
await nextTick();
|
scrollToBottom();
|
|
try {
|
// ✅ 流式请求使用Fetch API
|
const response = await knowledgeChat({
|
knowledgeBaseId: currentKnowledgeBase.value.id, // 知识库ID
|
memoryId: memoryId.value, // 会话ID
|
question: question // 用户问题
|
});
|
|
if (!response.ok) {
|
const errorText = await response.text();
|
throw new Error(errorText || '请求失败');
|
}
|
|
// ✅ 后端返回 text/stream;charset=utf-8
|
const reader = response.body.getReader();
|
const decoder = new TextDecoder();
|
let aiContent = '';
|
|
messages.value.push({ role: 'assistant', content: '' });
|
|
while (true) {
|
const { done, value } = await reader.read();
|
if (done) break;
|
|
const text = decoder.decode(value, { stream: true }); // ✅ 添加stream选项
|
aiContent += text;
|
messages.value[messages.value.length - 1].content = aiContent;
|
|
// 滚动到底部
|
await nextTick();
|
scrollToBottom();
|
}
|
|
// 如果AI返回空内容,显示提示
|
if (!aiContent.trim()) {
|
messages.value[messages.value.length - 1].content = '抱歉,知识库中未找到相关内容,请尝试其他问题。';
|
}
|
} catch (error) {
|
console.error("问答请求失败:", error);
|
ElMessage.error("问答请求失败,请稍后重试");
|
messages.value.push({
|
role: 'assistant',
|
content: '抱歉,发生了错误,请稍后重试'
|
});
|
} finally {
|
chatLoading.value = false;
|
}
|
};
|
|
// 清空对话
|
const clearMessages = () => {
|
ElMessageBox.confirm(
|
"确定要清空所有对话记录吗?",
|
"清空确认",
|
{
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning"
|
}
|
).then(() => {
|
messages.value = [];
|
memoryId.value = crypto.randomUUID(); // 重新生成会话ID
|
ElMessage.success("对话已清空");
|
}).catch(() => {
|
// 用户取消
|
});
|
};
|
|
// 滚动到底部
|
const scrollToBottom = () => {
|
if (chatMessagesRef.value) {
|
chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
|
}
|
};
|
|
// 关闭问答弹窗
|
const closeChatDialog = () => {
|
chatDialogVisible.value = false;
|
currentKnowledgeBase.value = null;
|
messages.value = [];
|
inputQuestion.value = "";
|
};
|
</script>
|
|
<style scoped>
|
.auto-refresh-info {
|
margin-bottom: 15px;
|
}
|
|
.auto-refresh-info .el-alert {
|
border-radius: 8px;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
.knowledge-detail {
|
padding: 20px 0;
|
}
|
|
.detail-title {
|
font-size: 18px;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.detail-section {
|
margin-top: 24px;
|
}
|
|
.detail-section h4 {
|
margin: 0 0 12px 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
border-left: 4px solid #409eff;
|
padding-left: 12px;
|
}
|
|
.detail-content {
|
background: #f8f9fa;
|
padding: 16px;
|
border-radius: 6px;
|
line-height: 1.6;
|
color: #606266;
|
white-space: pre-wrap;
|
}
|
|
.key-points {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.usage-stats {
|
margin-top: 16px;
|
}
|
|
.stat-item {
|
text-align: center;
|
padding: 20px;
|
background: #f8f9fa;
|
border-radius: 8px;
|
}
|
|
.stat-number {
|
font-size: 24px;
|
font-weight: bold;
|
color: #409eff;
|
margin-bottom: 8px;
|
}
|
|
.stat-label {
|
font-size: 14px;
|
color: #909399;
|
}
|
|
/* 文件管理样式 */
|
.file-manager {
|
padding: 20px 0;
|
}
|
|
.upload-section {
|
display: flex;
|
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;
|
flex-direction: column;
|
height: 500px;
|
}
|
|
.chat-header {
|
margin-bottom: 16px;
|
}
|
|
.chat-messages {
|
flex: 1;
|
overflow-y: auto;
|
padding: 16px;
|
background: #f5f7fa;
|
border-radius: 8px;
|
margin-bottom: 16px;
|
}
|
|
.message {
|
margin-bottom: 16px;
|
max-width: 80%;
|
}
|
|
.message.user {
|
margin-left: auto;
|
text-align: right;
|
}
|
|
.message.assistant {
|
margin-right: auto;
|
}
|
|
.message-role {
|
font-size: 12px;
|
color: #909399;
|
margin-bottom: 4px;
|
}
|
|
.message-content {
|
display: inline-block;
|
padding: 10px 14px;
|
border-radius: 8px;
|
line-height: 1.6;
|
word-wrap: break-word;
|
white-space: pre-wrap;
|
}
|
|
.message.user .message-content {
|
background: #409eff;
|
color: white;
|
}
|
|
.message.assistant .message-content {
|
background: white;
|
color: #303133;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
}
|
|
.typing {
|
animation: typing 1.5s infinite;
|
}
|
|
@keyframes typing {
|
0%, 50%, 100% {
|
opacity: 1;
|
}
|
25%, 75% {
|
opacity: 0.5;
|
}
|
}
|
|
.chat-input {
|
margin-top: auto;
|
}
|
|
.chat-actions {
|
margin-top: 8px;
|
text-align: right;
|
}
|
|
</style>
|