feat(data-process): 优化上传切分流程与PDF高亮预览

This commit is contained in:
caoxiaozhu
2026-07-24 11:28:02 +08:00
parent 33d0ed2e01
commit ad64e44860
18 changed files with 1940 additions and 395 deletions

View File

@@ -9,6 +9,8 @@ const props = defineProps<{
externalSource: ExternalDataSource
externalPulling: boolean
externalConnected: boolean
previewBuilding: boolean
sourceUploading: boolean
}>()
const emit = defineEmits<{
@@ -31,14 +33,23 @@ const AUTH_MODES = [
const FILE_PAGE_SIZE = 10
const currentFilePage = ref(1)
type FileStage = 'queued' | 'uploading' | 'waiting' | 'processing' | 'success' | 'upload-failed' | 'preview-failed'
const FILE_STAGE_META: Record<FileStage, { label: string; icon: string }> = {
queued: { label: '等待上传', icon: 'fa-clock-o' },
uploading: { label: '正在上传', icon: 'fa-cloud-upload' },
waiting: { label: '等待切分', icon: 'fa-clock-o' },
processing: { label: '正在切分', icon: 'fa-spinner fa-spin' },
success: { label: '切分完成', icon: 'fa-check-circle' },
'upload-failed': { label: '上传失败', icon: 'fa-exclamation-circle' },
'preview-failed': { label: '切分失败', icon: 'fa-exclamation-circle' },
}
const isExternal = computed(() => props.processType === 'external')
// 后端首版严格支持这些可验证的文本格式;不要把无法解析的二进制文档
// 静默替换成示例正文。
const uploadAccept = computed(() => props.processType === 'unstructured'
? '.txt,.md,.json,.jsonl'
: '.json,.jsonl,.csv,.txt,.md')
? '.txt,.md,.markdown,.pdf,.docx,.pptx,.json,.jsonl,.ndjson'
: '.json,.jsonl,.ndjson,.csv,.tsv,.xlsx')
const pagedUploadedFiles = computed(() => {
const start = (currentFilePage.value - 1) * FILE_PAGE_SIZE
@@ -67,6 +78,48 @@ function formatSize(size: number) {
if (size >= 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
return `${(size / 1024).toFixed(1)} KB`
}
function getFileStage(file: UploadedDataFile): FileStage {
if (file.status === 'queued' || file.status === 'uploading') return file.status
if (file.status === 'failed') return 'upload-failed'
if (file.previewStatus === 'processing' || file.previewStatus === 'success') return file.previewStatus
if (file.previewStatus === 'failed') return 'preview-failed'
return 'waiting'
}
function getFileProgress(file: UploadedDataFile) {
const stage = getFileStage(file)
const progress = stage === 'queued' || stage === 'uploading' || stage === 'upload-failed'
? file.uploadProgress
: stage === 'waiting' ? 100 : file.previewProgress ?? 0
return Math.min(100, Math.max(0, progress))
}
function isFileProcessing(file: UploadedDataFile) {
return getFileStage(file) === 'processing'
}
function getFileBarPercentage(file: UploadedDataFile) {
// Element Plus 的不定进度动画需要非零宽度;这里不作为完成百分比展示。
return isFileProcessing(file) ? 100 : getFileProgress(file)
}
function getFileProgressStatus(file: UploadedDataFile): 'success' | 'exception' | undefined {
const stage = getFileStage(file)
if (stage === 'waiting' || stage === 'success') return 'success'
if (stage === 'upload-failed' || stage === 'preview-failed') return 'exception'
return undefined
}
function getFileProgressText(file: UploadedDataFile) {
if (isFileProcessing(file)) return '处理中'
if (getFileStage(file) === 'waiting') return '已上传'
return `${getFileProgress(file)}%`
}
function getFileError(file: UploadedDataFile) {
return file.uploadError || file.previewError
}
</script>
<template>
@@ -167,7 +220,7 @@ function formatSize(size: number) {
<div class="external-actions">
<el-button
:loading="externalPulling && !externalConnected"
:disabled="externalPulling"
:disabled="externalPulling || previewBuilding"
plain
@click="emit('test-connection')"
>
@@ -176,7 +229,7 @@ function formatSize(size: number) {
<el-button
type="primary"
:loading="externalPulling"
:disabled="externalPulling"
:disabled="externalPulling || previewBuilding"
@click="emit('pull-data')"
>
拉取数据
@@ -200,10 +253,34 @@ function formatSize(size: number) {
<template v-if="file.count"> · {{ file.count.toLocaleString() }} </template>
</span>
</div>
<span class="file-status"><i class="fa fa-check-circle" aria-hidden="true" /> 拉取成功</span>
<div
class="file-preview-progress"
:class="`is-${getFileStage(file)}`"
:title="getFileError(file)"
>
<div class="file-status" role="status" aria-live="polite">
<span>
<i class="fa" :class="FILE_STAGE_META[getFileStage(file)].icon" aria-hidden="true" />
{{ FILE_STAGE_META[getFileStage(file)].label }}
</span>
<span class="file-progress-value">{{ getFileProgressText(file) }}</span>
</div>
<el-progress
:percentage="getFileBarPercentage(file)"
:indeterminate="isFileProcessing(file)"
:duration="1.5"
:stroke-width="5"
:show-text="false"
:status="getFileProgressStatus(file)"
:aria-valuenow="isFileProcessing(file) ? undefined : getFileProgress(file)"
:aria-valuetext="isFileProcessing(file) ? '正在切分进度未知' : getFileProgressText(file)"
/>
<small v-if="getFileError(file)" class="file-preview-error">{{ getFileError(file) }}</small>
</div>
<el-button
link
type="danger"
:disabled="previewBuilding || file.status === 'uploading'"
:aria-label="`删除数据集 ${file.name}`"
@click="emit('remove-file', file.uid)"
>
@@ -233,7 +310,13 @@ function formatSize(size: number) {
<h3 id="source-upload-title">源数据上传</h3>
<p>上传后可在下一步检查内容和切分效果支持同时添加多个文件</p>
</div>
<el-button v-if="uploadedFiles.length === 0" link type="primary" @click="emit('use-sample')">
<el-button
v-if="uploadedFiles.length === 0"
link
type="primary"
:disabled="previewBuilding"
@click="emit('use-sample')"
>
使用示例数据
</el-button>
</div>
@@ -243,6 +326,7 @@ function formatSize(size: number) {
drag
multiple
:accept="uploadAccept"
:disabled="previewBuilding"
:auto-upload="false"
:show-file-list="false"
:on-change="(file: UploadFile) => emit('file-change', file)"
@@ -253,25 +337,29 @@ function formatSize(size: number) {
<template #tip>
<div class="el-upload__tip">
{{ processType === 'unstructured'
? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'
: '支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB' }}
? '支持 TXT、MD、MARKDOWN、PDF、DOCX、PPTX、JSON、JSONL、NDJSON旧版 DOC/PPT 请先转换,单文件不超过 200MB'
: '支持 JSON、JSONL、NDJSON、CSV、TSV、XLSX旧版 XLS 请先转换,单文件不超过 200MB' }}
</div>
</template>
</el-upload>
<section v-else class="uploaded-file-list" aria-label="已上传文件列表">
<div class="uploaded-file-list-header">
<span>已添加 {{ uploadedFiles.length }} 个文件</span>
<span>
已选择 {{ uploadedFiles.length }} 个文件
<small v-if="sourceUploading" class="upload-queue-status"> · 正在逐个上传</small>
</span>
<div class="continue-upload">
<el-upload
multiple
:accept="uploadAccept"
:disabled="previewBuilding"
:auto-upload="false"
:show-file-list="false"
:on-change="(file: UploadFile) => emit('file-change', file)"
aria-label="继续添加源数据文件"
>
<el-button size="small" type="primary">继续上传</el-button>
<el-button size="small" type="primary" :disabled="previewBuilding">继续上传</el-button>
</el-upload>
</div>
</div>
@@ -285,10 +373,34 @@ function formatSize(size: number) {
<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template>
</span>
</div>
<span class="file-status"><i class="fa fa-check-circle" aria-hidden="true" /> 校验通过</span>
<div
class="file-preview-progress"
:class="`is-${getFileStage(file)}`"
:title="getFileError(file)"
>
<div class="file-status" role="status" aria-live="polite">
<span>
<i class="fa" :class="FILE_STAGE_META[getFileStage(file)].icon" aria-hidden="true" />
{{ FILE_STAGE_META[getFileStage(file)].label }}
</span>
<span class="file-progress-value">{{ getFileProgressText(file) }}</span>
</div>
<el-progress
:percentage="getFileBarPercentage(file)"
:indeterminate="isFileProcessing(file)"
:duration="1.5"
:stroke-width="5"
:show-text="false"
:status="getFileProgressStatus(file)"
:aria-valuenow="isFileProcessing(file) ? undefined : getFileProgress(file)"
:aria-valuetext="isFileProcessing(file) ? '正在切分,进度未知' : getFileProgressText(file)"
/>
<small v-if="getFileError(file)" class="file-preview-error">{{ getFileError(file) }}</small>
</div>
<el-button
link
type="danger"
:disabled="previewBuilding || file.status === 'uploading'"
:aria-label="`删除文件 ${file.name}`"
@click="emit('remove-file', file.uid)"
>
@@ -441,6 +553,11 @@ function formatSize(size: number) {
margin-top: 0;
}
.upload-queue-status {
color: #5b50f2;
font-size: inherit;
}
.uploaded-file-pagination {
display: flex;
justify-content: flex-end;
@@ -495,9 +612,59 @@ function formatSize(size: number) {
}
}
.file-preview-progress {
display: flex;
flex: 0 1 220px;
flex-direction: column;
gap: 5px;
min-width: 150px;
&.is-queued .file-status,
&.is-waiting .file-status {
color: #8a93a3;
}
&.is-uploading .file-status,
&.is-processing .file-status {
color: #5b50f2;
}
&.is-success .file-status {
color: #2ca66a;
}
&.is-upload-failed .file-status,
&.is-preview-failed .file-status,
.file-preview-error {
color: #d94b4b;
}
}
.file-status {
color: #2ca66a;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 12px;
> span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.file-progress-value {
flex: 0 0 auto;
font-variant-numeric: tabular-nums;
}
.file-preview-error {
overflow: hidden;
font-size: 11px;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.uploaded-file :deep(.el-button) {
@@ -514,8 +681,12 @@ function formatSize(size: number) {
padding: 8px 10px;
}
.file-status {
.file-preview-progress {
flex: 0 1 auto;
min-width: 130px;
}
.file-status {
line-height: 1.4;
white-space: normal;
}
@@ -545,7 +716,8 @@ function formatSize(size: number) {
min-width: calc(100% - 40px);
}
.file-status {
.file-preview-progress {
flex: 1 0 calc(100% - 38px);
margin-left: 38px;
}
}