fix(frontend): simplify processing imports

This commit is contained in:
2026-07-15 15:58:10 +08:00
parent a42f42091c
commit 55ca4fa713
2 changed files with 51 additions and 172 deletions
+33 -172
View File
@@ -1,7 +1,6 @@
<template>
<div class="processing-page">
<input ref="fileInput" class="hidden-input" type="file" multiple :accept="acceptTypes" @change="handlePickedFiles" />
<input ref="directoryInput" class="hidden-input" type="file" multiple webkitdirectory directory @change="handlePickedFiles" />
<section class="page-head">
<div>
@@ -14,8 +13,6 @@
<el-switch v-model="autoClassify" active-color="#e60012" />
</label>
<el-button type="danger" :icon="Upload" :loading="uploading" @click="fileInput?.click()">批量导入</el-button>
<el-button :icon="FolderChecked" :loading="uploading" @click="directoryInput?.click()">选择目录</el-button>
<el-button :icon="FolderOpened" :loading="serverImporting || runningImport" @click="importServerDirectory">服务端导入</el-button>
<el-button :icon="Refresh" :loading="loading" @click="loadOverview">刷新</el-button>
</div>
</section>
@@ -106,18 +103,18 @@
<main class="panel task-panel">
<div class="panel-title task-title">
<h2>解析任务</h2>
<el-select v-model="fileType" class="type-select" size="small">
<el-select v-model="fileType" class="type-select" size="small" @change="resetTaskPage">
<el-option v-for="item in fileTypes" :key="item" :label="`文件类型:${item}`" :value="item" />
</el-select>
</div>
<div class="task-tabs">
<button v-for="tab in statusTabs" :key="tab" type="button" :class="{ active: activeStatus === tab }" @click="activeStatus = tab">
<button v-for="tab in statusTabs" :key="tab" type="button" :class="{ active: activeStatus === tab }" @click="selectTaskStatus(tab)">
{{ tab }}
</button>
</div>
<el-table v-loading="loading" :data="filteredTasks" class="task-table" size="small">
<el-table v-loading="loading" :data="pagedTasks" class="task-table" size="small">
<el-table-column prop="fileName" label="文件名" min-width="210">
<template #default="{ row }">
<div class="file-cell">
@@ -156,7 +153,15 @@
<footer class="table-footer">
<span>共 {{ filteredTasks.length }} 条</span>
<el-pagination small background layout="prev, pager, next, sizes" :total="filteredTasks.length" :page-size="10" />
<el-pagination
v-model:current-page="taskPage"
v-model:page-size="taskPageSize"
small
background
layout="prev, pager, next, sizes"
:page-sizes="[10, 20, 50]"
:total="filteredTasks.length"
/>
</footer>
</main>
@@ -211,41 +216,6 @@
</aside>
</section>
<section v-if="importTasks.length > 0 || importTaskLoading" class="panel import-panel">
<div class="panel-title compact">
<h2>导入任务</h2>
<el-button :icon="Refresh" :loading="importTaskLoading" size="small" @click="loadImportTasks">刷新</el-button>
</div>
<el-table v-loading="importTaskLoading" :data="importTasks" size="small" class="import-table">
<el-table-column prop="directory" label="目录" min-width="180" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column label="状态" width="110">
<template #default="{ row }">
<span class="status-pill" :class="`pill-${row.statusType}`">{{ row.status }}</span>
</template>
</el-table-column>
<el-table-column label="进度" min-width="180">
<template #default="{ row }">
<el-progress :percentage="row.progress" :stroke-width="6" />
</template>
</el-table-column>
<el-table-column label="文件" width="150">
<template #default="{ row }">
<span>{{ row.importedFiles }}/{{ row.totalFiles }}</span>
<span v-if="row.failedFiles > 0" class="failed-count">失败 {{ row.failedFiles }}</span>
</template>
</el-table-column>
<el-table-column prop="currentFile" label="当前文件" min-width="220" show-overflow-tooltip />
<el-table-column prop="updatedAt" label="更新时间" width="138" />
<el-table-column label="操作" width="104">
<template #default="{ row }">
<el-button v-if="row.status === '导入中'" link type="danger" @click="cancelImportTask(row)">取消</el-button>
<el-button v-if="row.status !== '导入中'" link type="primary" @click="retryImportTask(row)">重试</el-button>
</template>
</el-table-column>
</el-table>
</section>
<section class="panel event-panel">
<div class="panel-title compact">
<h2>最近事件</h2>
@@ -267,28 +237,10 @@
</template>
<script setup name="KnowledgeProcessing" lang="ts">
import {
CircleCheck,
Document,
Files,
FolderChecked,
FolderOpened,
Operation,
Refresh,
Search,
Upload,
WarningFilled
} from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { CircleCheck, Document, Files, Operation, Refresh, Search, Upload, WarningFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import {
cancelLocalKnowledgeImportTask,
getProcessingOverview,
listLocalKnowledgeImportTasks,
startLocalKnowledgeImportTask,
type LocalImportTask,
type ProcessingOverview
} from '@/api/aihr/processing';
import { getProcessingOverview, type ProcessingOverview } from '@/api/aihr/processing';
import { uploadKnowledgeDocAsync, listUploadItems, retryUploadItem, type UploadItem } from '@/api/aihr/sop';
const acceptTypes = '.txt,.md,.markdown,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.webp,.bmp,.mp4,.mov,.avi,.mkv,.webm,.m4v,.zip';
@@ -303,24 +255,21 @@ const browserBatchWarnCount = 20;
const browserBatchWarnSize = 200 * 1024 * 1024;
const fileInput = ref<HTMLInputElement>();
const directoryInput = ref<HTMLInputElement>();
const loading = ref(false);
const uploading = ref(false);
const serverImporting = ref(false);
const importTaskLoading = ref(false);
const overview = ref<ProcessingOverview | null>(null);
const overviewFailed = ref(false);
const importTasks = ref<LocalImportTask[]>([]);
const categoryKeyword = ref('');
const selectedCategory = ref('住宅 SOP');
const autoClassify = ref(true);
const activeStatus = ref('全部');
const fileType = ref('全部');
const taskPage = ref(1);
const taskPageSize = ref(10);
const uploadBatch = ref<UploadItem[]>([]);
const uploadBatchId = ref('');
const enqueueFailures = ref<string[]>([]);
const retryingId = ref<number | null>(null);
let importPollTimer: ReturnType<typeof window.setTimeout> | undefined;
let batchPollTimer: ReturnType<typeof window.setTimeout> | undefined;
const statusTabs = ['全部', '等待解析', '解析中', '已完成', '失败'];
@@ -456,15 +405,15 @@ const filteredCategories = computed(() => {
return displayOverview.value.categories.filter((item) => !keyword || item.name.toLowerCase().includes(keyword));
});
const filteredTasks = computed(() => displayOverview.value.tasks.filter((item) => matchStatus(item.status) && matchFileType(item.fileType)));
const runningImport = computed(() => importTasks.value.some((item) => item.status === '导入中'));
const pagedTasks = computed(() => {
const start = (taskPage.value - 1) * taskPageSize.value;
return filteredTasks.value.slice(start, start + taskPageSize.value);
});
onMounted(async () => {
await Promise.all([loadOverview(), loadImportTasks()]);
scheduleImportPoll();
await loadOverview();
});
onUnmounted(() => {
clearImportPoll();
clearBatchPoll();
// 置空批次 ID,让在途的轮询请求返回后直接丢弃,不再重新挂起定时器
uploadBatchId.value = '';
@@ -477,6 +426,7 @@ async function loadOverview() {
overview.value = data;
overviewFailed.value = false;
selectedCategory.value = data.categories[0]?.name || selectedCategory.value;
resetTaskPage();
} catch {
overview.value = null;
overviewFailed.value = true;
@@ -509,7 +459,7 @@ async function uploadFiles(files: File[]) {
const totalSize = readyFiles.reduce((sum, file) => sum + file.size, 0);
if (readyFiles.length > browserBatchWarnCount || totalSize > browserBatchWarnSize) {
ElMessage.warning('浏览器批量适合少量文件;大批量资料请放到导入根目录后用服务端导入任务');
ElMessage.warning('文件较多,建议压缩为 ZIP 后批量导入');
}
uploading.value = true;
@@ -622,91 +572,6 @@ function uploadFailureMessage(error: unknown) {
return '上传失败';
}
async function importServerDirectory() {
let directory = '.';
try {
const result = await ElMessageBox.prompt('只允许导入后端 AIHR_IMPORT_ROOT / aihr.import.root 下的相对目录。', '服务端目录导入', {
confirmButtonText: '开始导入',
cancelButtonText: '取消',
inputValue: '.',
inputPlaceholder: '例如:住宅SOP 或 .'
});
directory = String(result.value || '.').trim() || '.';
} catch {
return;
}
serverImporting.value = true;
try {
const { data } = await startLocalKnowledgeImportTask({ directory, category: categoryPayload(), limit: 500 });
ElMessage.success(`导入任务已启动:${data.totalFiles} 个文件`);
await Promise.all([loadImportTasks(), loadOverview()]);
scheduleImportPoll();
} finally {
serverImporting.value = false;
}
}
async function loadImportTasks() {
importTaskLoading.value = true;
try {
const { data } = await listLocalKnowledgeImportTasks();
importTasks.value = data || [];
} catch {
importTasks.value = [];
} finally {
importTaskLoading.value = false;
}
}
async function retryImportTask(row: LocalImportTask) {
serverImporting.value = true;
try {
await startLocalKnowledgeImportTask({ directory: row.directory, category: row.category, limit: 500 });
ElMessage.success('已重新启动导入任务');
await loadImportTasks();
scheduleImportPoll();
} finally {
serverImporting.value = false;
}
}
async function cancelImportTask(row: LocalImportTask) {
try {
await ElMessageBox.confirm(`确认取消导入任务「${row.directory}」?`, '取消导入', {
confirmButtonText: '取消任务',
cancelButtonText: '返回',
type: 'warning'
});
} catch {
return;
}
importTaskLoading.value = true;
try {
await cancelLocalKnowledgeImportTask(row.id);
ElMessage.success('已取消导入任务');
await Promise.all([loadImportTasks(), loadOverview()]);
} finally {
importTaskLoading.value = false;
}
}
function scheduleImportPoll() {
clearImportPoll();
if (!runningImport.value) return;
importPollTimer = window.setTimeout(async () => {
await Promise.all([loadImportTasks(), loadOverview()]);
scheduleImportPoll();
}, 2500);
}
function clearImportPoll() {
if (importPollTimer) {
window.clearTimeout(importPollTimer);
importPollTimer = undefined;
}
}
function matchStatus(status: string) {
if (activeStatus.value === '全部') return true;
if (activeStatus.value === '等待解析') return ['等待解析', 'OCR排队'].includes(status);
@@ -717,6 +582,15 @@ function matchFileType(type: string) {
return fileType.value === '全部' || type === fileType.value;
}
function selectTaskStatus(status: string) {
activeStatus.value = status;
resetTaskPage();
}
function resetTaskPage() {
taskPage.value = 1;
}
function task(
id: number,
category: string,
@@ -921,19 +795,6 @@ function fileBadgeClass(type: string) {
align-items: start;
}
.import-panel {
margin-top: 14px;
}
.import-table {
width: 100%;
}
.failed-count {
margin-left: 8px;
color: #e60012;
}
.panel {
padding: 16px;
}