Files
urbanLifeline/urbanLifelineWeb/packages/workcase/src/views/admin/knowledge/KnowLedgeView.vue
2025-12-31 15:43:02 +08:00

421 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AdminLayout title="知识库管理" info="管理外部和内部知识库文档">
<template #action>
<!-- 上传文档组件 -->
<FileUpload
ref="fileUploadRef"
mode="dialog"
:title="'上传文档到:' + currentKnowledgeName"
button-text="上传文档"
accept=".pdf,.doc,.docx,.txt,.md"
:max-size="FILE_MAX_SIZE"
:max-count="10"
:custom-upload="customKnowledgeUpload"
@upload-error="handleUploadError"
/>
</template>
<div class="knowledge-container">
<el-card>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane v-for="tab in tabConfig" :key="tab.name" :label="tab.label" :name="tab.name" />
</el-tabs>
<p class="tab-desc">{{ currentTabDesc }}</p>
<div class="kb-categories">
<div v-for="kb in currentKnowledges" :key="kb.knowledgeId" class="kb-category-card"
:class="{ active: activeKnowledgeId === kb.knowledgeId }" @click="selectKnowledge(kb.knowledgeId || '')">
<el-icon :style="{ color: currentTabColor }"><Document /></el-icon>
<span class="cat-name">{{ kb.title }}</span>
<span class="cat-count">{{ kb.documentCount || 0 }} 个文件</span>
</div>
<el-empty v-if="currentKnowledges.length === 0" :description="'暂无' + currentTabLabel" :image-size="60" />
</div>
<div class="kb-files-section">
<div class="section-toolbar">
<h3>{{ currentKnowledgeName }}</h3>
<el-input v-model="searchKeyword" placeholder="搜索文件名" style="width: 240px;" :prefix-icon="Search" clearable />
</div>
<el-table :data="filteredDocuments" style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="文件名" min-width="280">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon class="file-icon"><Document /></el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="uploader" label="上传人员" width="120" />
<el-table-column prop="uploadTime" label="上传时间" width="180" />
<!-- <el-table-column prop="wordCount" label="字数" width="100" /> -->
<!-- <el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
{{ row.enabled ? '已启用' : '已禁用' }}
</el-tag>
</template>
</el-table-column> -->
<el-table-column label="操作" width="280" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openSegmentDialog(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="warning" link size="small" @click="openHistoryDialog(row)">
<el-icon><Clock /></el-icon>历史
</el-button>
<el-button type="primary" link size="small" @click="previewFile(row)">
<el-icon><View /></el-icon>预览
</el-button>
<el-button type="success" link size="small" @click="downloadFile(row)">
<el-icon><Download /></el-icon>下载
</el-button>
<el-button type="danger" link size="small" @click="deleteFile(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<!-- 分段编辑弹窗包含版本更新功能 -->
<DocumentSegment
v-model="showSegmentDialog"
:dataset-id="currentDatasetId"
:document-id="currentDifyDocId"
:knowledge-id="currentKnowledgeId"
:file-root-id="currentFileRootId"
@file-updated="handleFileUpdated"
/>
<!-- 历史版本弹窗 -->
<FileHistory
v-model="showHistoryDialog"
:data="historyList"
:loading="historyLoading"
@preview="previewHistoryFile"
@download="downloadHistoryFile"
/>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import AdminLayout from '@/views/admin/AdminLayout.vue'
import { Upload, Search, FileText as Document, Eye as View, Download, Trash2 as Delete, Pencil as Edit, Clock } from 'lucide-vue-next'
import { ElMessage, ElMessageBox } from 'element-plus'
import { aiKnowledgeAPI } from 'shared/api/ai'
import { FileUpload, FileHistory } from 'shared/components'
import DocumentSegment from 'shared/components/ai/knowledge/DocumentSegment.vue'
import { FILE_DOWNLOAD_URL, FILE_MAX_SIZE } from '@/config/index'
import type { TbKnowledge } from 'shared/types'
// Tab 配置
const tabConfig = [
{ name: 'external', label: '外部知识库', desc: '面向客户的知识库内容,包含设备操作指南、故障解决方案等', color: '#409eff' },
{ name: 'internal', label: '内部知识库', desc: '内部技术资料与服务规范,仅供内部员工使用', color: '#67c23a' }
]
const activeTab = ref('external')
const searchKeyword = ref('')
const loading = ref(false)
const fileUploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
// 知识库列表
const knowledges = ref<TbKnowledge[]>([])
const activeKnowledgeId = ref('')
// 文档列表
interface DocumentItem {
id: string
name: string
uploader: string
uploadTime: string
fileId?: string
fileRootId?: string
knowledgeId?: string
difyFileId?: string
version?: number
}
const documents = ref<DocumentItem[]>([])
// 当前 Tab 配置
const currentTabConfig = computed(() => tabConfig.find(t => t.name === activeTab.value) || tabConfig[0])
const currentTabDesc = computed(() => currentTabConfig.value.desc)
const currentTabLabel = computed(() => currentTabConfig.value.label)
const currentTabColor = computed(() => currentTabConfig.value.color)
// 当前 Tab 下的知识库列表(直接使用查询结果,不再前端过滤)
const currentKnowledges = computed(() => knowledges.value)
// 当前选中的知识库名称
const currentKnowledgeName = computed(() => {
const kb = knowledges.value.find((k: TbKnowledge) => k.knowledgeId === activeKnowledgeId.value)
return kb?.title || '请选择知识库'
})
// 搜索过滤后的文档列表
const filteredDocuments = computed(() => {
if (!searchKeyword.value) return documents.value
return documents.value.filter(f =>
f.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// 获取知识库列表(根据当前 Tab 的 category 查询)
const fetchKnowledges = async () => {
loading.value = true
try {
const result = await aiKnowledgeAPI.listKnowledges({
service: 'workcase',
category: activeTab.value
})
console.log('知识库列表响应:', result)
// API 返回的是 dataList 字段
knowledges.value = result.dataList || []
selectFirstKnowledge()
} catch (error) {
console.error('获取知识库列表失败:', error)
ElMessage.error('获取知识库列表失败')
} finally {
loading.value = false
}
}
// 选中当前 Tab 下的第一个知识库
const selectFirstKnowledge = () => {
const firstKb = currentKnowledges.value[0]
if (firstKb?.knowledgeId) {
activeKnowledgeId.value = firstKb.knowledgeId
} else {
activeKnowledgeId.value = ''
documents.value = []
}
}
// 获取文档列表
const fetchDocuments = async (knowledgeId: string) => {
if (!knowledgeId) {
documents.value = []
return
}
loading.value = true
try {
const result = await aiKnowledgeAPI.getDocumentList(knowledgeId, 1, 100)
if (result.success && result.pageDomain) {
documents.value = (result.pageDomain.dataList || []).map((file: any) => ({
id: file.fileId,
name: file.fileName || '-',
uploader: file.uploaderName || '-',
uploadTime: file.createTime ? new Date(file.createTime).toLocaleString() : '-',
fileId: file.fileId,
fileRootId: file.fileRootId,
knowledgeId: file.knowledgeId,
difyFileId: file.difyFileId,
version: file.version
}))
}
} catch (error) {
console.error('获取文档列表失败:', error)
ElMessage.error('获取文档列表失败')
} finally {
loading.value = false
}
}
// Tab 切换时重新查询对应类别的知识库
const handleTabChange = () => {
searchKeyword.value = ''
knowledges.value = []
activeKnowledgeId.value = ''
documents.value = []
fetchKnowledges()
}
// 选择知识库
const selectKnowledge = (knowledgeId: string) => {
activeKnowledgeId.value = knowledgeId
}
// 监听知识库选择变化
watch(activeKnowledgeId, (newVal) => {
if (newVal) fetchDocuments(newVal)
})
const previewFile = async (row: DocumentItem) => {
if (!row.fileId) {
ElMessage.warning('文件信息不完整')
return
}
// 使用 FILE_DOWNLOAD_URL 构建文件 URL 并在新窗口打开
const fileUrl = `${FILE_DOWNLOAD_URL}${row.fileId}`
window.open(fileUrl, '_blank')
}
const downloadFile = (row: DocumentItem) => {
if (!row.fileId) {
ElMessage.warning('文件信息不完整')
return
}
// 创建隐藏的下载链接并触发下载
const fileUrl = `${FILE_DOWNLOAD_URL}${row.fileId}`
const link = document.createElement('a')
link.href = fileUrl
link.download = row.name || 'file'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
ElMessage.success('开始下载')
}
const deleteFile = async (row: DocumentItem) => {
try {
await ElMessageBox.confirm(`确定要删除文件 "${row.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const result = await aiKnowledgeAPI.deleteFile(row.fileRootId)
if (result.success) {
ElMessage.success('删除成功')
fetchDocuments(activeKnowledgeId.value)
} else {
ElMessage.error(result.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除文件失败:', error)
ElMessage.error('删除文件失败')
}
}
}
// 自定义知识库文件上传
const customKnowledgeUpload = async (files: File[]) => {
if (!activeKnowledgeId.value) {
ElMessage.error('请先选择知识库')
throw new Error('请先选择知识库')
}
const targetKnowledgeId = activeKnowledgeId.value
// 单文件上传
if (files.length === 1) {
const result = await aiKnowledgeAPI.uploadToKnowledge(files[0], targetKnowledgeId)
if (result.success) {
ElMessage.success('文件上传成功')
// fetchKnowledges()
fetchDocuments(activeKnowledgeId.value)
} else {
throw new Error(result.message || '上传失败')
}
} else {
// 批量上传
const result = await aiKnowledgeAPI.batchUploadToKnowledge(files, targetKnowledgeId)
if (result.success) {
ElMessage.success('文件上传成功')
// fetchKnowledges()
fetchDocuments(activeKnowledgeId.value)
} else {
throw new Error(result.message || '上传失败')
}
}
}
const handleUploadError = (error: string) => {
ElMessage.error(error)
}
// ====================== 分段编辑功能 ======================
const showSegmentDialog = ref(false)
const currentDatasetId = ref('')
const currentDifyDocId = ref('')
const currentKnowledgeId = ref('')
const currentFileRootId = ref('')
const openSegmentDialog = (row: DocumentItem) => {
if (!row.difyFileId) {
ElMessage.warning('该文件暂无分段信息')
return
}
// 获取当前知识库的 difyDatasetId
const kb = knowledges.value.find((k: TbKnowledge) => k.knowledgeId === activeKnowledgeId.value)
if (!kb?.difyDatasetId) {
ElMessage.warning('知识库信息不完整')
return
}
currentDatasetId.value = kb.difyDatasetId
currentDifyDocId.value = row.difyFileId
currentKnowledgeId.value = row.knowledgeId || ''
currentFileRootId.value = row.fileRootId || ''
showSegmentDialog.value = true
}
// 文件更新后刷新列表
const handleFileUpdated = () => {
fetchDocuments(activeKnowledgeId.value)
}
// ====================== 历史版本功能 ======================
const showHistoryDialog = ref(false)
const historyLoading = ref(false)
const historyList = ref<any[]>([])
const openHistoryDialog = async (row: DocumentItem) => {
if (!row.fileRootId) {
ElMessage.warning('文件信息不完整')
return
}
showHistoryDialog.value = true
historyLoading.value = true
try {
const result = await aiKnowledgeAPI.getFileHistory(row.fileRootId)
console.log('历史版本响应:', result)
if (result.success) {
// 兼容 data 和 dataList 两种返回格式
historyList.value = result.data || result.dataList || []
} else {
ElMessage.error(result.message || '获取历史版本失败')
}
} catch (error) {
console.error('获取历史版本失败:', error)
ElMessage.error('获取历史版本失败')
} finally {
historyLoading.value = false
}
}
const previewHistoryFile = (row: any) => {
if (!row.fileId) {
ElMessage.warning('文件信息不完整')
return
}
const fileUrl = `${FILE_DOWNLOAD_URL}${row.fileId}`
window.open(fileUrl, '_blank')
}
const downloadHistoryFile = (row: any) => {
if (!row.fileId) {
ElMessage.warning('文件信息不完整')
return
}
const fileUrl = `${FILE_DOWNLOAD_URL}${row.fileId}`
const link = document.createElement('a')
link.href = fileUrl
link.download = row.fileName || 'file'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
ElMessage.success('开始下载')
}
onMounted(() => {
fetchKnowledges()
})
</script>
<style lang="scss" scoped>
@import url("./KnowLedgeView.scss");
</style>