fix(frontend): simplify processing imports
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
PAGE="$ROOT_DIR/frontend/src/views/knowledge/processing.vue"
|
||||
|
||||
grep -Fq '>批量导入</el-button>' "$PAGE"
|
||||
grep -Fq ':data="pagedTasks"' "$PAGE"
|
||||
grep -Fq 'v-model:current-page="taskPage"' "$PAGE"
|
||||
grep -Fq 'const pagedTasks = computed' "$PAGE"
|
||||
for removed in '选择目录' '服务端导入' 'webkitdirectory'; do
|
||||
if grep -Fq "$removed" "$PAGE"; then
|
||||
echo "FAIL: processing page still contains $removed" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo 'PASS: processing page exposes batch import only'
|
||||
Reference in New Issue
Block a user