ai-robot-core/ai-service-admin/src/views/kb/index.vue

233 lines
6.8 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>
<div class="kb-container">
<el-card>
<template #header>
<div class="card-header">
<span>知识库列表</span>
<el-button type="primary" @click="handleUploadClick">上传文档</el-button>
</div>
</template>
<el-table v-loading="loading" :data="tableData" style="width: 100%">
<el-table-column prop="name" label="文件名" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="jobId" label="任务ID" width="120" />
<el-table-column prop="createTime" label="上传时间" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button link type="primary" @click="handleViewJob(scope.row)">查看详情</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="jobDialogVisible" title="索引任务详情" width="500px">
<el-descriptions :column="1" border v-if="currentJob">
<el-descriptions-item label="任务ID">{{ currentJob.jobId }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(currentJob.status)">
{{ getStatusText(currentJob.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="进度">{{ currentJob.progress }}%</el-descriptions-item>
<el-descriptions-item label="错误信息" v-if="currentJob.errorMsg">
<el-alert type="error" :closable="false">{{ currentJob.errorMsg }}</el-alert>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="jobDialogVisible = false">关闭</el-button>
<el-button v-if="currentJob?.status === 'pending' || currentJob?.status === 'processing'" type="primary" @click="refreshJobStatus">
刷新状态
</el-button>
</template>
</el-dialog>
<input ref="fileInput" type="file" style="display: none" @change="handleFileChange" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { uploadDocument, listDocuments, getIndexJob, deleteDocument } from '@/api/kb'
interface DocumentItem {
docId: string
name: string
status: string
jobId: string
createTime: string
}
const tableData = ref<DocumentItem[]>([])
const loading = ref(false)
const jobDialogVisible = ref(false)
const currentJob = ref<any>(null)
const pollingJobs = ref<Set<string>>(new Set())
let pollingInterval: number | null = null
const getStatusType = (status: string) => {
const typeMap: Record<string, string> = {
completed: 'success',
processing: 'warning',
pending: 'info',
failed: 'danger'
}
return typeMap[status] || 'info'
}
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
completed: '已完成',
processing: '处理中',
pending: '等待中',
failed: '失败'
}
return textMap[status] || status
}
const fetchDocuments = async () => {
try {
const res = await listDocuments({})
tableData.value = res.data.map((doc: any) => ({
docId: doc.docId,
name: doc.fileName,
status: doc.status,
jobId: doc.jobId,
createTime: new Date(doc.createdAt).toLocaleString('zh-CN')
}))
} catch (error) {
console.error('Failed to fetch documents:', error)
}
}
const fetchJobStatus = async (jobId: string) => {
try {
const res = await getIndexJob(jobId)
return res
} catch (error) {
console.error('Failed to fetch job status:', error)
return null
}
}
const handleViewJob = async (row: DocumentItem) => {
if (!row.jobId) {
ElMessage.warning('该文档没有任务ID')
return
}
currentJob.value = await fetchJobStatus(row.jobId)
jobDialogVisible.value = true
}
const handleDelete = async (row: DocumentItem) => {
try {
await ElMessageBox.confirm(`确定要删除文档 "${row.name}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteDocument(row.docId)
ElMessage.success('文档删除成功')
fetchDocuments()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
const refreshJobStatus = async () => {
if (currentJob.value?.jobId) {
currentJob.value = await fetchJobStatus(currentJob.value.jobId)
}
}
const startPolling = (jobId: string) => {
pollingJobs.value.add(jobId)
if (!pollingInterval) {
pollingInterval = window.setInterval(async () => {
for (const jobId of pollingJobs.value) {
const job = await fetchJobStatus(jobId)
if (job) {
if (job.status === 'completed') {
pollingJobs.value.delete(jobId)
ElMessage.success('文档索引任务已完成')
fetchDocuments()
} else if (job.status === 'failed') {
pollingJobs.value.delete(jobId)
ElMessage.error('文档索引任务失败')
ElMessage.warning(`错误: ${job.errorMsg}`)
}
}
}
if (pollingJobs.value.size === 0 && pollingInterval) {
clearInterval(pollingInterval)
pollingInterval = null
}
}, 3000)
}
}
onMounted(() => {
fetchDocuments()
})
onUnmounted(() => {
if (pollingInterval) {
clearInterval(pollingInterval)
}
})
const fileInput = ref<HTMLInputElement | null>(null)
const handleUploadClick = () => {
fileInput.value?.click()
}
const handleFileChange = async (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
const formData = new FormData()
formData.append('file', file)
formData.append('kb_id', 'kb_default')
try {
loading.value = true
const res = await uploadDocument(formData)
ElMessage.success(`文档上传成功任务ID: ${res.jobId}`)
console.log('Upload response:', res)
const newDoc: DocumentItem = {
name: file.name,
status: res.status || 'pending',
jobId: res.jobId,
createTime: new Date().toLocaleString('zh-CN')
}
tableData.value.unshift(newDoc)
startPolling(res.jobId)
} catch (error) {
ElMessage.error('文档上传失败')
console.error('Upload error:', error)
} finally {
loading.value = false
target.value = ''
}
}
</script>
<style scoped>
.kb-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>