1026 lines
30 KiB
Vue
1026 lines
30 KiB
Vue
<template>
|
||
<div class="doc-upload-container">
|
||
<!-- 步骤条 -->
|
||
<el-steps :active="currentStep" align-center class="upload-steps">
|
||
<el-step :title="props.replaceFileId ? '选择替换文件' : '选择文件'" icon="Upload" />
|
||
<el-step title="解析模式" icon="Setting" />
|
||
<el-step :title="getStep3Title()" :icon="getStep3Icon()" />
|
||
</el-steps>
|
||
|
||
<!-- 步骤内容 -->
|
||
<div class="step-content">
|
||
<!-- 第一步:文件选择 -->
|
||
<div v-if="currentStep === 0" class="step-panel file-selection">
|
||
<div class="panel-header">
|
||
<h3>{{ props.replaceFileId ? '选择要替换的文件' : '选择要上传的文件' }}</h3>
|
||
<p class="panel-description">支持多种文档格式,拖拽或点击选择文件</p>
|
||
</div>
|
||
|
||
<el-upload
|
||
ref="uploadRef"
|
||
multiple
|
||
drag
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:file-list="[]"
|
||
:show-file-list="false"
|
||
accept=".txt,.md,.markdown,.mdx,.pdf,.html,.htm,.xlsx,.xls,.docx,.csv,.vtt,.properties"
|
||
class="upload-area"
|
||
:class="{ 'is-dragover': isDragOver }"
|
||
@drop="handleDrop"
|
||
@dragover="handleDragOver"
|
||
@dragleave="handleDragLeave"
|
||
>
|
||
<div class="upload-content">
|
||
<el-icon class="upload-icon" :class="{ 'bounce': isDragOver }">
|
||
<Upload />
|
||
</el-icon>
|
||
<div class="upload-text">
|
||
<h4>拖拽文件到此处 或 <span class="link-text">点击选择</span></h4>
|
||
<p>支持 TXT、MD、PDF、DOCX、Excel、HTML、CSV 等格式(不支持DOC格式)</p>
|
||
<p>单个文件不超过 100MB,最多可选择 10 个文件</p>
|
||
</div>
|
||
</div>
|
||
</el-upload>
|
||
|
||
<!-- 已选文件列表 -->
|
||
<div v-if="uploadFiles.length > 0" class="selected-files">
|
||
<h4>已选择文件 ({{ uploadFiles.length }})</h4>
|
||
<div class="file-list">
|
||
<div v-for="(file, index) in uploadFiles" :key="index" class="file-item">
|
||
<el-icon class="file-icon"><Document /></el-icon>
|
||
<span class="file-name">{{ file.name }}</span>
|
||
<span class="file-size">{{ formatFileSize(file.size) }}</span>
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
text
|
||
size="small"
|
||
@click="removeFile(index)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第二步:解析模式选择 -->
|
||
<div v-if="currentStep === 1" class="step-panel mode-selection">
|
||
<div class="panel-header">
|
||
<h3>选择解析模式</h3>
|
||
<p class="panel-description">根据您的需求选择合适的文档解析策略</p>
|
||
</div>
|
||
|
||
<div class="mode-options">
|
||
<div
|
||
v-for="mode in analysisMode"
|
||
:key="mode.value"
|
||
class="mode-card"
|
||
:class="{ 'selected': uploadForm.analysisStrategyType === mode.value }"
|
||
@click="selectMode(mode.value)"
|
||
>
|
||
<div class="mode-icon">
|
||
<el-icon :size="32"><component :is="mode.icon" /></el-icon>
|
||
</div>
|
||
<h4>{{ mode.title }}</h4>
|
||
<p>{{ mode.description }}</p>
|
||
<div class="mode-features">
|
||
<el-tag v-for="feature in mode.features" :key="feature" size="small" type="info">
|
||
{{ feature }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第三步:参数设置或进度显示 -->
|
||
<div v-if="currentStep === 2" class="step-panel config-panel">
|
||
<!-- 自定义模式 - 参数设置 -->
|
||
<div v-if="uploadForm.analysisStrategyType === 'custom'" class="custom-config">
|
||
<div class="panel-header">
|
||
<h3>自定义解析参数</h3>
|
||
<p class="panel-description">配置详细的文档解析参数</p>
|
||
</div>
|
||
|
||
<el-form :model="uploadForm.docAnalysisStrategy" label-width="140px" class="config-form">
|
||
<div class="form-section">
|
||
<h4 class="section-title">基础设置</h4>
|
||
<!--
|
||
<el-form-item label="策略名称">
|
||
<el-input v-model="uploadForm.docAnalysisStrategy.name" placeholder="请输入策略名称(可选)" />
|
||
</el-form-item>
|
||
-->
|
||
<el-form-item label="分段模式">
|
||
<el-select v-model="uploadForm.docAnalysisStrategy.segmentationMode" placeholder="选择分段模式">
|
||
<el-option label="普通分段" value="custom" />
|
||
<el-option label="父子分段" value="hierarchical" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'parent_child'" label="父分段召回模式">
|
||
<el-select v-model="uploadForm.docAnalysisStrategy.parentSegmentMode" placeholder="选择父分段召回模式">
|
||
<el-option label="全文召回" value="full-doc" />
|
||
<el-option label="段落召回" value="paragraph" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="索引技术">
|
||
<el-select v-model="uploadForm.docAnalysisStrategy.indexingTechnique" placeholder="选择索引技术">
|
||
<el-option label="高质量索引" value="high_quality" />
|
||
<el-option label="经济模式" value="economy" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<!-- 普通分段参数 -->
|
||
<div v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'custom'" class="form-section">
|
||
<h4 class="section-title">分段参数</h4>
|
||
<el-form-item label="段分隔符">
|
||
<el-input v-model="uploadForm.docAnalysisStrategy.segmentSeparator" placeholder="###" />
|
||
</el-form-item>
|
||
<el-form-item label="最大长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.maxLength"
|
||
:min="100"
|
||
:max="2000"
|
||
placeholder="500"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="重叠长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.overlapLength"
|
||
:min="0"
|
||
:max="200"
|
||
placeholder="50"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<!-- 父子分段参数 -->
|
||
<div v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'hierarchical'" class="form-section">
|
||
<!-- 父分段设置 (仅在段落召回模式下显示) -->
|
||
<div v-if="uploadForm.docAnalysisStrategy.parentSegmentMode === 'paragraph'">
|
||
<h4 class="section-title">父分段设置</h4>
|
||
<el-form-item label="父段分隔符">
|
||
<el-input v-model="uploadForm.docAnalysisStrategy.segmentSeparator" placeholder="###" />
|
||
</el-form-item>
|
||
<el-form-item label="父段最大长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.maxLength"
|
||
:min="100"
|
||
:max="2000"
|
||
placeholder="500"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="父段重叠长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.overlapLength"
|
||
:min="0"
|
||
:max="200"
|
||
placeholder="50"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<!-- 子分段设置 (始终显示) -->
|
||
<h4 class="section-title">子分段设置</h4>
|
||
<el-form-item label="子段分隔符">
|
||
<el-input v-model="uploadForm.docAnalysisStrategy.childSegmentSeparator" placeholder="\n\n" />
|
||
</el-form-item>
|
||
<el-form-item label="子段最大长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.childMaxLength"
|
||
:min="50"
|
||
:max="1000"
|
||
placeholder="200"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="子段重叠长度">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.childOverlapLength"
|
||
:min="0"
|
||
:max="100"
|
||
placeholder="20"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">检索设置</h4>
|
||
<el-form-item label="搜索方法">
|
||
<el-select v-model="uploadForm.docAnalysisStrategy.searchMethod" placeholder="选择搜索方法">
|
||
<el-option label="语义搜索" value="semantic_search" />
|
||
<el-option label="全文搜索" value="full_text_search" />
|
||
<el-option label="混合搜索" value="hybrid_search" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Top K">
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.topK"
|
||
:min="1"
|
||
:max="20"
|
||
placeholder="3"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="分数阈值">
|
||
<div class="score-threshold-config">
|
||
<el-switch
|
||
v-model="uploadForm.docAnalysisStrategy.scoreThresholdEnabled"
|
||
active-text="启用"
|
||
inactive-text="关闭"
|
||
/>
|
||
<el-input-number
|
||
v-model="uploadForm.docAnalysisStrategy.scoreThreshold"
|
||
:min="0"
|
||
:max="1"
|
||
:step="0.1"
|
||
:precision="1"
|
||
:disabled="!uploadForm.docAnalysisStrategy.scoreThresholdEnabled"
|
||
placeholder="0.5"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h4 class="section-title">重排序设置</h4>
|
||
<el-form-item label="启用重排序">
|
||
<el-switch
|
||
v-model="uploadForm.docAnalysisStrategy.rerankingEnable"
|
||
active-text="启用"
|
||
inactive-text="关闭"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 深度解析模式 - 进度显示 -->
|
||
<div v-else-if="uploadForm.analysisStrategyType === 'deep'" class="deep-analysis-progress">
|
||
<div class="panel-header">
|
||
<h3>数据预处理进度</h3>
|
||
<p class="panel-description">正在进行深度分析,请耐心等待</p>
|
||
</div>
|
||
|
||
<div class="progress-container">
|
||
<div class="progress-item" v-for="(task, index) in processingTasks" :key="index">
|
||
<div class="task-info">
|
||
<span class="task-name">{{ task.name }}</span>
|
||
<span class="task-status">{{ task.status }}</span>
|
||
</div>
|
||
<el-progress
|
||
:percentage="task.progress"
|
||
:status="task.progress === 100 ? 'success' : undefined"
|
||
:stroke-width="8"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自适应模式 - 确认信息 -->
|
||
<div v-else class="adaptive-confirm">
|
||
<div class="panel-header">
|
||
<h3>{{ props.replaceFileId ? '确认替换' : '确认上传' }}</h3>
|
||
<p class="panel-description">使用自适应模式,系统将自动选择最佳解析策略</p>
|
||
</div>
|
||
|
||
<div class="confirm-info">
|
||
<div class="info-card">
|
||
<el-icon class="info-icon"><InfoFilled /></el-icon>
|
||
<div class="info-content">
|
||
<h4>自适应模式特点</h4>
|
||
<ul>
|
||
<li>自动识别文档类型和结构</li>
|
||
<li>智能选择最佳分段策略</li>
|
||
<li>优化索引质量和检索效果</li>
|
||
<li>无需手动配置参数</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="step-actions">
|
||
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
|
||
<el-button
|
||
v-if="currentStep < 2"
|
||
type="primary"
|
||
@click="nextStep"
|
||
:disabled="!canProceedToNext()"
|
||
>
|
||
下一步
|
||
</el-button>
|
||
<el-button
|
||
v-if="currentStep === 2"
|
||
type="primary"
|
||
@click="handleUpload"
|
||
:loading="uploading"
|
||
:disabled="!canUpload()"
|
||
>
|
||
{{ props.replaceFileId ? '开始替换' : '开始上传' }}
|
||
</el-button>
|
||
<el-button @click="handleCancel">取消</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, nextTick, watch } from 'vue'
|
||
import { ElMessage, ElNotification, ElLoading } from 'element-plus'
|
||
import type { UploadFile as ElUploadFile } from 'element-plus'
|
||
import {
|
||
Upload,
|
||
Setting,
|
||
Document,
|
||
Delete,
|
||
InfoFilled,
|
||
Star,
|
||
Cpu,
|
||
Tools,
|
||
Check
|
||
} from '@element-plus/icons-vue'
|
||
import { uploadDocument, deleteDocument } from '@/api/dataset'
|
||
|
||
// Props
|
||
interface Props {
|
||
datasetId: string
|
||
visible: boolean
|
||
parentId?: number
|
||
replaceFileId?: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
datasetId: '',
|
||
visible: false,
|
||
parentId: undefined,
|
||
replaceFileId: undefined
|
||
})
|
||
|
||
// Emits
|
||
const emit = defineEmits<{
|
||
'update:visible': [value: boolean]
|
||
'success': []
|
||
}>()
|
||
|
||
// 响应式数据
|
||
const currentStep = ref(0)
|
||
const uploading = ref(false)
|
||
const uploadFiles = ref<ElUploadFile[]>([])
|
||
const isDragOver = ref(false)
|
||
const uploadRef = ref()
|
||
const isProcessingFileLimit = ref(false)
|
||
|
||
// 解析模式选项
|
||
const analysisMode = [
|
||
{
|
||
value: 'adaption',
|
||
title: '自适应模式',
|
||
description: '智能识别文档结构,自动选择最佳解析策略',
|
||
icon: 'Star',
|
||
features: ['智能识别', '自动优化', '快速处理']
|
||
},
|
||
{
|
||
value: 'deep',
|
||
title: '深度解析',
|
||
description: '深度分析扫描文档及图片内容,提供最高质量的解析结果',
|
||
icon: 'Cpu',
|
||
features: ['深度分析', '高质量', '扫描件']
|
||
},
|
||
{
|
||
value: 'custom',
|
||
title: '自定义模式',
|
||
description: '完全自定义解析参数,满足特殊需求',
|
||
icon: 'Tools',
|
||
features: ['完全自定义', '灵活配置', '专业级']
|
||
}
|
||
]
|
||
|
||
// 上传表单数据
|
||
const uploadForm = reactive({
|
||
analysisStrategyType: 'adaption',
|
||
docAnalysisStrategy: {
|
||
name: '',
|
||
segmentationMode: 'custom',
|
||
indexingTechnique: 'high_quality',
|
||
docForm: 'text_model',
|
||
parentSegmentMode: 'paragraph',
|
||
segmentSeparator: '###',
|
||
maxLength: 500,
|
||
overlapLength: 50,
|
||
childSegmentSeparator: '\n\n',
|
||
childMaxLength: 200,
|
||
childOverlapLength: 20,
|
||
searchMethod: 'hybrid_search',
|
||
rerankingEnable: false,
|
||
rerankingProviderName: 'langgenius/huggingface_tei/huggingface_tei',
|
||
rerankingModelName: 'bge-reanker-v2-m3',
|
||
topK: 3,
|
||
scoreThresholdEnabled: false,
|
||
scoreThreshold: 0.5,
|
||
describe: ''
|
||
}
|
||
})
|
||
|
||
// 深度解析进度任务
|
||
const processingTasks = ref([
|
||
{ name: '文档解析', progress: 0, status: '等待中' },
|
||
{ name: '内容提取', progress: 0, status: '等待中' },
|
||
{ name: '结构分析', progress: 0, status: '等待中' },
|
||
{ name: '索引构建', progress: 0, status: '等待中' }
|
||
])
|
||
|
||
// 监听分段模式变化,自动设置docForm
|
||
watch(() => uploadForm.docAnalysisStrategy.segmentationMode, (newMode) => {
|
||
if (newMode === 'custom') {
|
||
uploadForm.docAnalysisStrategy.docForm = 'text_model'
|
||
} else if (newMode === 'hierarchical') {
|
||
uploadForm.docAnalysisStrategy.docForm = 'hierarchical_model'
|
||
}
|
||
})
|
||
|
||
// 计算属性
|
||
const getStep3Title = () => {
|
||
switch (uploadForm.analysisStrategyType) {
|
||
case 'custom': return '参数设置'
|
||
case 'deep': return '处理进度'
|
||
default: return props.replaceFileId ? '确认替换' : '确认上传'
|
||
}
|
||
}
|
||
|
||
const getStep3Icon = () => {
|
||
switch (uploadForm.analysisStrategyType) {
|
||
case 'custom': return 'Tools'
|
||
case 'deep': return 'Loading'
|
||
default: return 'Check'
|
||
}
|
||
}
|
||
|
||
// 方法
|
||
const handleFileChange = (file: ElUploadFile, fileList: ElUploadFile[]) => {
|
||
// 防止无限循环
|
||
if (isProcessingFileLimit.value) {
|
||
return
|
||
}
|
||
|
||
// 文件大小检查
|
||
if (file.size && file.size > 100 * 1024 * 1024) {
|
||
ElMessage.error('文件大小不能超过 100MB')
|
||
return false
|
||
}
|
||
|
||
// 文件数量检查,如果超过10个,只保留前10个
|
||
if (fileList.length > 10) {
|
||
isProcessingFileLimit.value = true
|
||
|
||
const truncatedList = fileList.slice(0, 10)
|
||
const removedCount = fileList.length - 10
|
||
|
||
ElMessage.warning({
|
||
message: `已选择 ${fileList.length} 个文件,超出限制。系统已自动保留前 10 个文件,移除了 ${removedCount} 个文件。`,
|
||
duration: 4000,
|
||
showClose: true
|
||
})
|
||
|
||
// 更新我们的文件列表
|
||
uploadFiles.value = truncatedList
|
||
|
||
// 清除并重新设置上传组件的文件列表
|
||
nextTick(() => {
|
||
if (uploadRef.value) {
|
||
uploadRef.value.clearFiles()
|
||
|
||
// 重新添加前10个文件
|
||
truncatedList.forEach(uploadFile => {
|
||
if (uploadFile.raw) {
|
||
uploadRef.value.handleStart(uploadFile.raw)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 重置标志
|
||
setTimeout(() => {
|
||
isProcessingFileLimit.value = false
|
||
}, 100)
|
||
})
|
||
} else {
|
||
// 更新文件列表
|
||
uploadFiles.value = fileList
|
||
}
|
||
}
|
||
|
||
const handleDrop = () => {
|
||
isDragOver.value = false
|
||
}
|
||
|
||
const handleDragOver = () => {
|
||
isDragOver.value = true
|
||
}
|
||
|
||
const handleDragLeave = () => {
|
||
isDragOver.value = false
|
||
}
|
||
|
||
const removeFile = (index: number) => {
|
||
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) => {
|
||
if (!size) return '0 B'
|
||
const units = ['B', 'KB', 'MB', 'GB']
|
||
let index = 0
|
||
while (size >= 1024 && index < units.length - 1) {
|
||
size /= 1024
|
||
index++
|
||
}
|
||
return `${size.toFixed(1)} ${units[index]}`
|
||
}
|
||
|
||
const selectMode = (mode: string) => {
|
||
uploadForm.analysisStrategyType = mode
|
||
}
|
||
|
||
const canProceedToNext = () => {
|
||
if (currentStep.value === 0) {
|
||
return uploadFiles.value.length > 0
|
||
}
|
||
if (currentStep.value === 1) {
|
||
return uploadForm.analysisStrategyType !== ''
|
||
}
|
||
return true
|
||
}
|
||
|
||
const canUpload = () => {
|
||
// 自定义模式不需要策略名称验证,因为UI中已经隐藏了该字段
|
||
// 只要有基本的配置参数就可以上传
|
||
return true
|
||
}
|
||
|
||
const nextStep = () => {
|
||
if (canProceedToNext()) {
|
||
currentStep.value++
|
||
|
||
// 如果选择深度解析,模拟进度
|
||
if (currentStep.value === 2 && uploadForm.analysisStrategyType === 'deep') {
|
||
simulateDeepAnalysisProgress()
|
||
}
|
||
}
|
||
}
|
||
|
||
const prevStep = () => {
|
||
if (currentStep.value > 0) {
|
||
currentStep.value--
|
||
}
|
||
}
|
||
|
||
const simulateDeepAnalysisProgress = () => {
|
||
processingTasks.value.forEach((task, index) => {
|
||
setTimeout(() => {
|
||
task.status = '处理中'
|
||
const interval = setInterval(() => {
|
||
task.progress += Math.random() * 10
|
||
if (task.progress >= 100) {
|
||
task.progress = 100
|
||
task.status = '已完成'
|
||
clearInterval(interval)
|
||
}
|
||
}, 200)
|
||
}, index * 1000)
|
||
})
|
||
}
|
||
|
||
const handleUpload = async () => {
|
||
if (!uploadFiles.value.length) {
|
||
ElMessage.warning('请选择要上传的文件')
|
||
return
|
||
}
|
||
|
||
const loading = ElLoading.service({
|
||
lock: true,
|
||
text: props.replaceFileId ? '正在替换文件...' : '正在上传文件...',
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
})
|
||
|
||
try {
|
||
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()
|
||
|
||
// 重新显示上传进度X
|
||
const uploadLoading = ElLoading.service({
|
||
lock: true,
|
||
text: '正在上传新文件...',
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
})
|
||
// 更新loading引用
|
||
loading.close = uploadLoading.close
|
||
}
|
||
|
||
// 构造请求数据,符合后端DocUploadReq结构
|
||
const requestData: any = {
|
||
datasetId: props.datasetId,
|
||
analysisStrategyType: uploadForm.analysisStrategyType,
|
||
parentId: props.parentId, // 添加parentId参数,根目录时为undefined/null
|
||
// 注意:删除 replaceFileId,因为我们已经手动删除了旧文件
|
||
}
|
||
|
||
// 如果是自定义模式,添加解析策略
|
||
if (uploadForm.analysisStrategyType === 'custom') {
|
||
requestData.docAnalysisStrategy = {
|
||
name: uploadForm.docAnalysisStrategy.name || '自定义策略',
|
||
segmentationMode: uploadForm.docAnalysisStrategy.segmentationMode,
|
||
indexingTechnique: uploadForm.docAnalysisStrategy.indexingTechnique,
|
||
docForm: uploadForm.docAnalysisStrategy.docForm,
|
||
parentSegmentMode: uploadForm.docAnalysisStrategy.parentSegmentMode,
|
||
segmentSeparator: uploadForm.docAnalysisStrategy.segmentSeparator,
|
||
maxLength: uploadForm.docAnalysisStrategy.maxLength,
|
||
overlapLength: uploadForm.docAnalysisStrategy.overlapLength,
|
||
childSegmentSeparator: uploadForm.docAnalysisStrategy.childSegmentSeparator || '',
|
||
childMaxLength: uploadForm.docAnalysisStrategy.childMaxLength || 200,
|
||
childOverlapLength: uploadForm.docAnalysisStrategy.childOverlapLength || 20,
|
||
searchMethod: uploadForm.docAnalysisStrategy.searchMethod,
|
||
rerankingEnable: uploadForm.docAnalysisStrategy.rerankingEnable,
|
||
rerankingProviderName: uploadForm.docAnalysisStrategy.rerankingProviderName || '',
|
||
rerankingModelName: uploadForm.docAnalysisStrategy.rerankingModelName || '',
|
||
topK: uploadForm.docAnalysisStrategy.topK,
|
||
scoreThresholdEnabled: uploadForm.docAnalysisStrategy.scoreThresholdEnabled,
|
||
scoreThreshold: uploadForm.docAnalysisStrategy.scoreThreshold,
|
||
describe: uploadForm.docAnalysisStrategy.describe || ''
|
||
}
|
||
}
|
||
|
||
// 遍历上传文件
|
||
for (const file of uploadFiles.value) {
|
||
const formData = new FormData()
|
||
formData.append('file', file.raw!)
|
||
formData.append('request', new Blob([JSON.stringify(requestData)], {
|
||
type: 'application/json'
|
||
}))
|
||
|
||
await uploadDocument(formData)
|
||
}
|
||
|
||
ElNotification({
|
||
title: props.replaceFileId ? '替换成功' : '上传成功',
|
||
message: props.replaceFileId
|
||
? `成功替换 ${uploadFiles.value.length} 个文件`
|
||
: `成功上传 ${uploadFiles.value.length} 个文件`,
|
||
type: 'success'
|
||
})
|
||
|
||
emit('success')
|
||
handleCancel()
|
||
} catch (error) {
|
||
console.error(props.replaceFileId ? '替换失败:' : '上传失败:', error)
|
||
//ElMessage.error(props.replaceFileId ? '替换失败,请重试' : '上传失败,请重试')
|
||
} finally {
|
||
uploading.value = false
|
||
loading.close()
|
||
}
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
// 重置状态
|
||
currentStep.value = 0
|
||
uploadFiles.value = []
|
||
uploadForm.analysisStrategyType = 'adaption'
|
||
uploadForm.docAnalysisStrategy.name = ''
|
||
|
||
emit('update:visible', false)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.doc-upload-container {
|
||
padding: 24px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
|
||
.upload-steps {
|
||
margin-bottom: 32px;
|
||
|
||
:deep(.el-step__title) {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.step-content {
|
||
min-height: 400px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.step-panel {
|
||
.panel-header {
|
||
text-align: center;
|
||
margin-bottom: 24px;
|
||
|
||
h3 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.panel-description {
|
||
margin: 0;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 文件选择步骤
|
||
.file-selection {
|
||
.upload-area {
|
||
margin-bottom: 24px;
|
||
|
||
:deep(.el-upload-dragger) {
|
||
width: 100%;
|
||
height: 200px;
|
||
border: 2px dashed #dcdfe6;
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
transition: all 0.3s;
|
||
|
||
&:hover, &.is-dragover {
|
||
border-color: #409eff;
|
||
background: #f0f9ff;
|
||
}
|
||
}
|
||
|
||
.upload-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: #c0c4cc;
|
||
margin-bottom: 16px;
|
||
transition: all 0.3s;
|
||
|
||
&.bounce {
|
||
animation: bounce 0.6s infinite alternate;
|
||
}
|
||
}
|
||
|
||
.upload-text {
|
||
text-align: center;
|
||
|
||
h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 16px;
|
||
color: #303133;
|
||
|
||
.link-text {
|
||
color: #409eff;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.selected-files {
|
||
h4 {
|
||
margin: 0 0 12px 0;
|
||
font-size: 14px;
|
||
color: #303133;
|
||
}
|
||
|
||
.file-list {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 16px;
|
||
color: #409eff;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.file-name {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: #303133;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-right: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 模式选择步骤
|
||
.mode-selection {
|
||
.mode-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px;
|
||
|
||
.mode-card {
|
||
padding: 24px;
|
||
border: 2px solid #ebeef5;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1);
|
||
}
|
||
|
||
&.selected {
|
||
border-color: #409eff;
|
||
background: #f0f9ff;
|
||
}
|
||
|
||
.mode-icon {
|
||
margin-bottom: 16px;
|
||
color: #409eff;
|
||
}
|
||
|
||
h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
p {
|
||
margin: 0 0 16px 0;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.mode-features {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 配置面板
|
||
.config-panel {
|
||
.custom-config {
|
||
.config-form {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
|
||
.form-section {
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: 6px;
|
||
|
||
.section-title {
|
||
margin: 0 0 16px 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
border-bottom: 1px solid #ebeef5;
|
||
padding-bottom: 8px;
|
||
}
|
||
}
|
||
|
||
.score-threshold-config {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.deep-analysis-progress {
|
||
.progress-container {
|
||
max-width: 500px;
|
||
margin: 0 auto;
|
||
|
||
.progress-item {
|
||
margin-bottom: 20px;
|
||
|
||
.task-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.task-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.task-status {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.adaptive-confirm {
|
||
.confirm-info {
|
||
max-width: 500px;
|
||
margin: 0 auto;
|
||
|
||
.info-card {
|
||
display: flex;
|
||
padding: 20px;
|
||
background: #f0f9ff;
|
||
border: 1px solid #b3d8ff;
|
||
border-radius: 8px;
|
||
|
||
.info-icon {
|
||
font-size: 24px;
|
||
color: #409eff;
|
||
margin-right: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.info-content {
|
||
h4 {
|
||
margin: 0 0 12px 0;
|
||
font-size: 16px;
|
||
color: #303133;
|
||
}
|
||
|
||
ul {
|
||
margin: 0;
|
||
padding-left: 16px;
|
||
|
||
li {
|
||
margin-bottom: 6px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.step-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0% { transform: translateY(0); }
|
||
100% { transform: translateY(-10px); }
|
||
}
|
||
</style> |