feat: 增加文档替换功能,文档上传成功/失败 消息提示优化

This commit is contained in:
wenjinbo 2025-09-18 15:50:12 +08:00
parent 676c76d1c2
commit fb1237b1d3
3 changed files with 218 additions and 21 deletions

View File

@ -2,7 +2,7 @@
<div class="doc-upload-container"> <div class="doc-upload-container">
<!-- 步骤条 --> <!-- 步骤条 -->
<el-steps :active="currentStep" align-center class="upload-steps"> <el-steps :active="currentStep" align-center class="upload-steps">
<el-step title="选择文件" icon="Upload" /> <el-step :title="props.replaceFileId ? '选择替换文件' : '选择文件'" icon="Upload" />
<el-step title="解析模式" icon="Setting" /> <el-step title="解析模式" icon="Setting" />
<el-step :title="getStep3Title()" :icon="getStep3Icon()" /> <el-step :title="getStep3Title()" :icon="getStep3Icon()" />
</el-steps> </el-steps>
@ -12,7 +12,7 @@
<!-- 第一步文件选择 --> <!-- 第一步文件选择 -->
<div v-if="currentStep === 0" class="step-panel file-selection"> <div v-if="currentStep === 0" class="step-panel file-selection">
<div class="panel-header"> <div class="panel-header">
<h3>选择要上传的文件</h3> <h3>{{ props.replaceFileId ? '选择要替换的文件' : '选择要上传的文件' }}</h3>
<p class="panel-description">支持多种文档格式拖拽或点击选择文件</p> <p class="panel-description">支持多种文档格式拖拽或点击选择文件</p>
</div> </div>
@ -22,7 +22,8 @@
drag drag
:auto-upload="false" :auto-upload="false"
:on-change="handleFileChange" :on-change="handleFileChange"
:file-list="uploadFiles" :file-list="[]"
:show-file-list="false"
:limit="10" :limit="10"
accept=".txt,.md,.markdown,.mdx,.pdf,.html,.htm,.xlsx,.xls,.doc,.docx,.csv,.vtt,.properties" accept=".txt,.md,.markdown,.mdx,.pdf,.html,.htm,.xlsx,.xls,.doc,.docx,.csv,.vtt,.properties"
class="upload-area" class="upload-area"
@ -281,7 +282,7 @@
<!-- 自适应模式 - 确认信息 --> <!-- 自适应模式 - 确认信息 -->
<div v-else class="adaptive-confirm"> <div v-else class="adaptive-confirm">
<div class="panel-header"> <div class="panel-header">
<h3>确认上传</h3> <h3>{{ props.replaceFileId ? '确认替换' : '确认上传' }}</h3>
<p class="panel-description">使用自适应模式系统将自动选择最佳解析策略</p> <p class="panel-description">使用自适应模式系统将自动选择最佳解析策略</p>
</div> </div>
@ -321,7 +322,7 @@
:loading="uploading" :loading="uploading"
:disabled="!canUpload()" :disabled="!canUpload()"
> >
开始上传 {{ props.replaceFileId ? '开始替换' : '开始上传' }}
</el-button> </el-button>
<el-button @click="handleCancel">取消</el-button> <el-button @click="handleCancel">取消</el-button>
</div> </div>
@ -343,19 +344,21 @@ import {
Tools, Tools,
Check Check
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { uploadDocument } from '@/api/dataset' import { uploadDocument, deleteDocument } from '@/api/dataset'
// Props // Props
interface Props { interface Props {
datasetId: string datasetId: string
visible: boolean visible: boolean
parentId?: number parentId?: number
replaceFileId?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
datasetId: '', datasetId: '',
visible: false, visible: false,
parentId: undefined parentId: undefined,
replaceFileId: undefined
}) })
// Emits // Emits
@ -435,7 +438,7 @@ const getStep3Title = () => {
switch (uploadForm.analysisStrategyType) { switch (uploadForm.analysisStrategyType) {
case 'custom': return '参数设置' case 'custom': return '参数设置'
case 'deep': return '处理进度' case 'deep': return '处理进度'
default: return '确认上传' default: return props.replaceFileId ? '确认替换' : '确认上传'
} }
} }
@ -473,6 +476,14 @@ const handleDragLeave = () => {
const removeFile = (index: number) => { const removeFile = (index: number) => {
uploadFiles.value.splice(index, 1) uploadFiles.value.splice(index, 1)
// el-upload
if (uploadRef.value) {
uploadRef.value.clearFiles()
// el-upload
uploadFiles.value.forEach(file => {
uploadRef.value.handleStart(file.raw)
})
}
} }
const formatFileSize = (size?: number) => { const formatFileSize = (size?: number) => {
@ -548,18 +559,42 @@ const handleUpload = async () => {
const loading = ElLoading.service({ const loading = ElLoading.service({
lock: true, lock: true,
text: '正在上传文件...', text: props.replaceFileId ? '正在替换文件...' : '正在上传文件...',
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}) })
try { try {
uploading.value = true uploading.value = true
//
if (props.replaceFileId) {
// loading
loading.close()
const deleteLoading = ElLoading.service({
lock: true,
text: '正在删除旧文件...',
background: 'rgba(0, 0, 0, 0.7)'
})
await deleteDocument(parseInt(props.replaceFileId))
deleteLoading.close()
//
const uploadLoading = ElLoading.service({
lock: true,
text: '正在上传新文件...',
background: 'rgba(0, 0, 0, 0.7)'
})
// loading
loading.close = uploadLoading.close
}
// DocUploadReq // DocUploadReq
const requestData: any = { const requestData: any = {
datasetId: props.datasetId, datasetId: props.datasetId,
analysisStrategyType: uploadForm.analysisStrategyType, analysisStrategyType: uploadForm.analysisStrategyType,
parentId: props.parentId // parentIdundefined/null parentId: props.parentId, // parentIdundefined/null
// replaceFileId
} }
// //
@ -599,16 +634,18 @@ const handleUpload = async () => {
} }
ElNotification({ ElNotification({
title: '上传成功', title: props.replaceFileId ? '替换成功' : '上传成功',
message: `成功上传 ${uploadFiles.value.length} 个文件`, message: props.replaceFileId
? `成功替换 ${uploadFiles.value.length} 个文件`
: `成功上传 ${uploadFiles.value.length} 个文件`,
type: 'success' type: 'success'
}) })
emit('success') emit('success')
handleCancel() handleCancel()
} catch (error) { } catch (error) {
console.error('上传失败:', error) console.error(props.replaceFileId ? '替换失败:' : '上传失败:', error)
ElMessage.error('上传失败,请重试') //ElMessage.error(props.replaceFileId ? '' : '')
} finally { } finally {
uploading.value = false uploading.value = false
loading.close() loading.close()

View File

@ -139,7 +139,7 @@
<span v-else class="folder-indicator">--</span> <span v-else class="folder-indicator">--</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="320" fixed="right"> <el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="380" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="action-buttons"> <div class="action-buttons">
<!-- 文件操作 --> <!-- 文件操作 -->
@ -166,6 +166,15 @@
> >
{{t('vabI18n.knowledge.document.buttons.download')}} {{t('vabI18n.knowledge.document.buttons.download')}}
</el-button> </el-button>
<el-button
type="warning"
:icon="Upload"
text
class="action-btn"
@click="handleFileReplaceFromRow(row)"
>
替换
</el-button>
</template> </template>
<!-- 文件夹操作 --> <!-- 文件夹操作 -->
<template v-else> <template v-else>
@ -235,6 +244,43 @@
/> />
</el-dialog> </el-dialog>
<!-- 文件替换对话框 -->
<el-dialog
v-model="replaceDialogVisible"
title="替换文件"
width="900px"
:close-on-click-modal="false"
class="upload-dialog"
>
<div v-if="replaceTargetFile" class="replace-info">
<div class="target-file-info">
<h4>将要替换的文件</h4>
<div class="file-info-card">
<img
:src="getFileTypeIcon(replaceTargetFile.fileType)"
alt="文件图标"
class="file-icon"
/>
<div class="file-details">
<div class="file-name">{{ replaceTargetFile.fileName }}</div>
<div class="file-meta">
<span>大小{{ formatFileSize(replaceTargetFile.charCount) }}</span>
<span>创建时间{{ replaceTargetFile.createDate }}</span>
</div>
</div>
</div>
</div>
</div>
<DocUpload
:visible="replaceDialogVisible"
:dataset-id="datasetId"
:parent-id="currentParentId"
:replace-file-id="replaceTargetFile?.id"
@update:visible="replaceDialogVisible = $event"
@success="handleReplaceSuccess"
/>
</el-dialog>
<!-- 预览抽屉 --> <!-- 预览抽屉 -->
<el-drawer <el-drawer
v-model="previewDrawerVisible" v-model="previewDrawerVisible"
@ -467,6 +513,8 @@ const datasetName = ref('');
const previewLoading = ref(true) const previewLoading = ref(true)
const uploadDialogVisible = ref(false) const uploadDialogVisible = ref(false)
const replaceDialogVisible = ref(false)
const replaceTargetFile = ref<FileItem | null>(null)
// //
const renameDialogVisible = ref(false) const renameDialogVisible = ref(false)
@ -518,6 +566,8 @@ name: string;
onUnmounted(() => { onUnmounted(() => {
// //
document.removeEventListener('click', hideContextMenu) document.removeEventListener('click', hideContextMenu)
//
stopAutoRefresh()
}) })
const getFileTypeIcon = (fileType: string) => { const getFileTypeIcon = (fileType: string) => {
@ -766,6 +816,12 @@ const triggerFileInput = () => {
uploadDialogVisible.value = true uploadDialogVisible.value = true
} }
// -
const handleFileReplaceFromRow = (row: FileItem) => {
replaceTargetFile.value = row
replaceDialogVisible.value = true
}
const indexMethod = (index: number) => { const indexMethod = (index: number) => {
return (pagination.current - 1) * pagination.size + index + 1 return (pagination.current - 1) * pagination.size + index + 1
} }
@ -776,9 +832,22 @@ return (pagination.current - 1) * pagination.size + index + 1
const handleUploadSuccess = async () => { const handleUploadSuccess = async () => {
uploadDialogVisible.value = false uploadDialogVisible.value = false
await fetchDocuments() await fetchDocuments()
// ElNotification({
// title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
// message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'),
// type: 'success'
// })
}
//
const handleReplaceSuccess = async () => {
replaceDialogVisible.value = false
replaceTargetFile.value = null
selectedRows.value = [] //
await fetchDocuments()
ElNotification({ ElNotification({
title: t('vabI18n.knowledge.document.messages.uploadSuccess'), title: '替换成功',
message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'), message: '文件替换成功',
type: 'success' type: 'success'
}) })
} }
@ -896,6 +965,28 @@ const handlePreview = async (row: FileItem) => {
// script setuploading // script setuploading
const tableLoading = ref(false) const tableLoading = ref(false)
//
const refreshTimer = ref<NodeJS.Timeout | null>(null)
//
const startAutoRefresh = () => {
stopAutoRefresh() //
refreshTimer.value = setInterval(() => {
//
if (!tableLoading.value) {
fetchDocuments()
}
}, 30000) // 5
}
//
const stopAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
refreshTimer.value = null
}
}
// fetchDocuments // fetchDocuments
const fetchDocuments = async () => { const fetchDocuments = async () => {
tableLoading.value = true tableLoading.value = true
@ -988,7 +1079,7 @@ const typeMap: Record<string, 'success' | 'primary' | 'warning' | 'info' | 'dang
'preprocessing': 'info', 'preprocessing': 'info',
'indexing': 'warning', 'indexing': 'warning',
'waiting': 'info', 'waiting': 'info',
'queued': 'primary', 'queued': 'info', // info
'failed': 'danger', 'failed': 'danger',
'completed': 'success' 'completed': 'success'
} }
@ -1044,6 +1135,8 @@ datasetId.value = route.params.id as string
datasetName.value = route.query.name as string datasetName.value = route.query.name as string
// API // API
await fetchDocuments() await fetchDocuments()
//
startAutoRefresh()
}) })
// //
@ -1677,6 +1770,67 @@ border-top: 1px solid #ebeef5;
//
.replace-info {
margin-bottom: 24px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #e6a23c;
.target-file-info {
h4 {
margin: 0 0 16px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.file-info-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid #ebeef5;
.file-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
object-fit: contain;
}
.file-details {
flex: 1;
.file-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.file-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #909399;
span {
&:not(:last-child)::after {
content: '•';
margin-left: 8px;
color: #dcdfe6;
}
}
}
}
}
}
}
// //
.preview-drawer { .preview-drawer {
::v-deep .el-drawer__header { ::v-deep .el-drawer__header {

View File

@ -111,10 +111,16 @@ public class DatasetDocController {
@PostMapping("/upload") @PostMapping("/upload")
public ResultUtils upload(@RequestPart("file") MultipartFile file, public ResultUtils upload(@RequestPart("file") MultipartFile file,
@RequestPart("request") DocUploadReq req) throws Exception{ @RequestPart("request") DocUploadReq req) throws Exception{
//1. 上传文件并解析到dify平台 try {
ResponseEntity<Map> documentByFile = difyDatasetApiService.createDocByFile(req, file,""); //1. 上传文件并解析到dify平台
ResponseEntity<Map> documentByFile = difyDatasetApiService.createDocByFile(req, file,"");
}catch (IllegalArgumentException e){
return ResultUtils.error(e.getMessage());
}catch (Exception e){
return ResultUtils.error("上传文件失败");
}
return ResultUtils.success(documentByFile); return ResultUtils.success(null);
} }