ai-vue3-admin/src/views/jg_gcb/jg_knowledge_files_add.vue
2025-12-09 10:52:00 +08:00

457 lines
12 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="knowledge-ingest">
<!-- 第一步上传区域 -->
<div class="section upload-section">
<h3>步骤1: 上传文件夹到MinIO</h3>
<!-- 文件夹选择 -->
<input
type="file"
ref="folderInput"
style="display: none"
webkitdirectory
multiple
@change="handleFolderSelect"
:disabled="processCompleted"
/>
<el-button
type="primary"
@click="$refs.folderInput.click()"
:disabled="processCompleted"
>
选择文件夹
</el-button>
<!-- 文件列表预览 -->
<div v-if="selectedFiles.length > 0" class="file-list">
<h4>待上传文件 {{ selectedFiles.length }} </h4>
<el-tree
:data="fileTree"
:props="{ label: 'name', children: 'children' }"
default-expand-all
class="tree-view"
/>
</div>
<!-- 上传按钮 -->
<el-button
type="success"
:loading="uploading"
@click="uploadFolder"
:disabled="selectedFiles.length === 0 || uploading || processCompleted"
class="action-btn"
>
{{ uploading ? '上传中...' : '开始上传' }}
</el-button>
</div>
<!-- 第二步RAG入库区域 -->
<div v-if="showIngestSection" class="section ingest-section">
<el-divider />
<h3>步骤2: RAG知识库入库</h3>
<el-alert title="上传成功!" type="success" :closable="false" show-icon>
<div class="upload-info">
<span>
文件夹ID:
<strong>{{ uploadResult.folderId }}</strong>
</span>
<span>
文件数量:
<strong>{{ uploadResult.fileCount }}</strong>
</span>
<span>
存储桶:
<strong>{{ uploadResult.bucketName }}</strong>
</span>
</div>
</el-alert>
<el-button
type="warning"
:loading="ingesting"
@click="startRagIngest"
:disabled="ingesting || ingestCompleted || processCompleted"
class="action-btn"
>
{{
ingesting ? '入库中...' : ingestCompleted ? '已完成' : '开始RAG入库'
}}
</el-button>
<!-- 进度条显示条件进行中 已完成 -->
<div v-if="ingesting || ingestCompleted" class="progress-area">
<h4>RAG入库进度</h4>
<el-progress
:percentage="ingestProgress"
:status="ingestStatus"
:stroke-width="20"
:text-inside="true"
/>
<p v-if="currentFile" class="current-file">
正在处理: {{ currentFile }}
</p>
<!-- 完成提示 -->
<div v-if="ingestCompleted" class="completion-status">
<el-icon class="success-icon" :size="48" color="#67C23A">
<CircleCheckFilled />
</el-icon>
<p class="complete-text"> RAG入库已完成</p>
<p class="complete-detail">
共处理 {{ uploadResult.fileCount }} 个文件
</p>
</div>
</div>
<!-- 修改后的刷新按钮区域 -->
<div v-if="processCompleted" class="refresh-section">
<el-divider />
<el-button
type="primary"
size="large"
@click="resetAllData"
class="refresh-btn"
>
<el-icon><RefreshRight /></el-icon>
开始新流程
</el-button>
<p class="refresh-hint">点击按钮将重置所有数据开始新的上传入库流程</p>
</div>
</div>
</div>
</template>
<script>
import { ElMessage, ElIcon } from 'element-plus'
import { CircleCheckFilled, RefreshRight } from '@element-plus/icons-vue'
import {
submitRagIngestTask,
getRagIngestProgress,
uploadFolder,
} from '@/api/rag'
import { getUserRoles } from '@/api/sysFRole'
import { GetUserinfo } from '@/api/login'
export default {
components: {
ElIcon,
CircleCheckFilled,
RefreshRight,
},
data() {
return {
selectedFiles: [],
fileTree: [],
uploading: false,
uploadResult: null,
ingesting: false,
ingestProgress: 0,
ingestStatus: '',
currentFile: '',
taskId: null,
ingestCompleted: false,
processCompleted: false, // ✅ 新增:标记整个流程是否结束
}
},
computed: {
showIngestSection() {
// ✅ 流程结束后仍然显示,只是按钮禁用
return Boolean(this.uploadResult?.folderId) && !this.uploading
},
},
methods: {
handleFolderSelect(event) {
// ✅ 流程结束后禁用选择
if (this.processCompleted) return
const files = Array.from(event.target.files)
if (files.length === 0) return
// ✅ 检查文件大小(例如:限制单个文件 100MB
const MAX_FILE_SIZE = 200 * 1024 * 1024 * 1024 // 100MB
const oversizedFiles = files.filter(f => f.size > MAX_FILE_SIZE)
if (oversizedFiles.length > 0) {
// 显示错误提示(自动换行)
ElMessage.error({
message: `以下文件超过 2GB 限制:\n${oversizedFiles
.map(f => f.name)
.join('\n')}`,
duration: 5000, // 显示5秒
showClose: true,
dangerouslyUseHTMLString: true, // 支持HTML可选
})
// 清空文件输入框和已选文件
this.$refs.folderInput.value = null
this.selectedFiles = []
this.fileTree = []
return // 中断后续操作
}
this.selectedFiles = files.map(file => ({
file: file,
relativePath: file.webkitRelativePath,
size: file.size,
}))
this.buildFileTree()
},
buildFileTree() {
const tree = { name: '根目录', children: {} }
this.selectedFiles.forEach(({ relativePath }) => {
const parts = relativePath.split('/').filter(p => p)
let current = tree.children
parts.forEach(part => {
if (!current[part]) {
current[part] = { name: part, children: {} }
}
current = current[part].children
})
})
this.fileTree = this.convertToTreeData(tree.children)
},
convertToTreeData(children) {
return Object.keys(children).map(key => {
const node = children[key]
return {
name: node.name,
children:
Object.keys(node.children).length > 0
? this.convertToTreeData(node.children)
: undefined,
}
})
},
async uploadFolder() {
// ✅ 流程结束后禁用上传
if (this.processCompleted) return
this.uploading = true
this.uploadResult = null
const formData = new FormData()
this.selectedFiles.forEach(({ file, relativePath }) => {
formData.append('files', file)
formData.append('relativePaths', relativePath)
})
const folderName = this.$refs.folderInput.files[0].webkitRelativePath.split(
'/'
)[0]
formData.append('sender', 'user')
formData.append('folderName', folderName)
try {
const res = await uploadFolder(formData)
if (res.code === 200 && res.data) {
this.uploadResult = res.data
ElMessage.success(`上传成功!共 ${res.data.fileCount} 个文件`)
} else {
throw new Error(res.message || `上传失败 (code: ${res.code})`)
}
} catch (error) {
const msg = error.response?.data?.message || error.message || '上传失败'
ElMessage.error(msg)
} finally {
this.uploading = false
}
},
async startRagIngest() {
// ✅ 流程结束后禁用入库
if (this.processCompleted) return
if (!this.uploadResult?.folderId) {
ElMessage.warning('请先完成文件上传')
return
}
this.ingesting = true
this.ingestCompleted = false
this.ingestProgress = 0
this.ingestStatus = ''
this.currentFile = ''
try {
const userInfoRes = await GetUserinfo()
const userId = userInfoRes.data.id
const roleRes = await getUserRoles(userId)
const userRoles = roleRes.data || []
const userRoleCode = userRoles.some(r => r?.roleCode === 'admin')
? 'admin'
: userRoles[0]?.roleCode || 'default'
const res = await submitRagIngestTask(
this.uploadResult.folderId,
userRoleCode
)
if (res.code === 200 && res.data) {
this.taskId = res.data.taskId
ElMessage.success(`入库任务已提交,角色: ${userRoleCode}`)
this.pollIngestProgress(this.taskId, userRoleCode)
} else {
throw new Error(res.message || '提交失败')
}
} catch (error) {
const msg = error.response?.data?.message || error.message || '入库失败'
ElMessage.error(msg)
this.ingesting = false
}
},
pollIngestProgress(taskId, userRoleCode) {
let retryCount = 0
const maxRetries = 10
const interval = setInterval(async () => {
try {
const res = await getRagIngestProgress(taskId, userRoleCode)
const { code, data } = res
if (code === 200 && data) {
this.ingestProgress = data.progress || 0
this.currentFile = data.currentFile || ''
switch (data.status) {
case 'completed':
this.ingestStatus = 'success'
clearInterval(interval)
this.ingesting = false
this.ingestCompleted = true
this.processCompleted = true // ✅ 标记整个流程结束
ElMessage.success('RAG入库完成')
// ✅ 可以添加一个提示
ElMessage.info('请点击"开始新流程"按钮进行下一次操作')
break
case 'failed':
this.ingestStatus = 'exception'
clearInterval(interval)
this.ingesting = false
ElMessage.error(`入库失败: ${data.errorMessage}`)
break
default:
retryCount = 0
}
} else {
throw new Error('查询进度失败')
}
} catch (error) {
console.error('轮询错误:', error)
if (retryCount++ >= maxRetries) {
clearInterval(interval)
this.ingesting = false
ElMessage.error('进度查询失败')
}
}
}, 1500)
},
// ✅ 新增:刷新页面方法
refreshPage() {
// 方式1: 使用 Vue Router (推荐)
this.$router.go(0)
// 方式2: 使用 location.reload()
// location.reload()
// 方式3: 重置所有数据(不刷新页面)
// this.resetAllData()
},
// ✅ 修改:重置所有数据而不刷新页面
resetAllData() {
// 重置所有数据
this.selectedFiles = []
this.fileTree = []
this.uploading = false
this.uploadResult = null
this.ingesting = false
this.ingestProgress = 0
this.ingestStatus = ''
this.currentFile = ''
this.taskId = null
this.ingestCompleted = false
this.processCompleted = false
// 清空文件输入框
if (this.$refs.folderInput) {
this.$refs.folderInput.value = null
}
// 显示成功提示
ElMessage.success('页面已重置,可以开始新流程')
},
},
}
</script>
<style lang="css" scoped>
@import 'knowledge-ingest.css';
/* ✅ 新增:完成状态样式 */
.complete-text {
margin-top: 10px;
font-size: 16px;
color: #67c23a;
font-weight: bold;
text-align: center;
}
.complete-detail {
font-size: 14px;
color: #909399;
text-align: center;
margin-top: 5px;
}
.completion-status {
margin-top: 20px;
text-align: center;
padding: 20px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #b3e19d;
}
.success-icon {
margin-bottom: 10px;
}
/* ✅ 新增:刷新按钮区域样式 */
.refresh-section {
margin-top: 30px;
text-align: center;
padding: 20px;
background: #fef0f0;
border-radius: 8px;
border: 1px solid #fbc4c4;
}
.refresh-btn {
margin-bottom: 10px;
}
.refresh-hint {
color: #f56c6c;
font-size: 12px;
margin: 5px 0 0 0;
}
/* 禁用状态按钮 */
.action-btn:disabled,
input[type='file']:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>