fix(frontend): simplify processing imports
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="processing-page">
|
<div class="processing-page">
|
||||||
<input ref="fileInput" class="hidden-input" type="file" multiple :accept="acceptTypes" @change="handlePickedFiles" />
|
<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">
|
<section class="page-head">
|
||||||
<div>
|
<div>
|
||||||
@@ -14,8 +13,6 @@
|
|||||||
<el-switch v-model="autoClassify" active-color="#e60012" />
|
<el-switch v-model="autoClassify" active-color="#e60012" />
|
||||||
</label>
|
</label>
|
||||||
<el-button type="danger" :icon="Upload" :loading="uploading" @click="fileInput?.click()">批量导入</el-button>
|
<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>
|
<el-button :icon="Refresh" :loading="loading" @click="loadOverview">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -106,18 +103,18 @@
|
|||||||
<main class="panel task-panel">
|
<main class="panel task-panel">
|
||||||
<div class="panel-title task-title">
|
<div class="panel-title task-title">
|
||||||
<h2>解析任务</h2>
|
<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-option v-for="item in fileTypes" :key="item" :label="`文件类型:${item}`" :value="item" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-tabs">
|
<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 }}
|
{{ tab }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<el-table-column prop="fileName" label="文件名" min-width="210">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="file-cell">
|
<div class="file-cell">
|
||||||
@@ -156,7 +153,15 @@
|
|||||||
|
|
||||||
<footer class="table-footer">
|
<footer class="table-footer">
|
||||||
<span>共 {{ filteredTasks.length }} 条</span>
|
<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>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -211,41 +216,6 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</section>
|
</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">
|
<section class="panel event-panel">
|
||||||
<div class="panel-title compact">
|
<div class="panel-title compact">
|
||||||
<h2>最近事件</h2>
|
<h2>最近事件</h2>
|
||||||
@@ -267,28 +237,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="KnowledgeProcessing" lang="ts">
|
<script setup name="KnowledgeProcessing" lang="ts">
|
||||||
import {
|
import { CircleCheck, Document, Files, Operation, Refresh, Search, Upload, WarningFilled } from '@element-plus/icons-vue';
|
||||||
CircleCheck,
|
import { ElMessage } from 'element-plus';
|
||||||
Document,
|
|
||||||
Files,
|
|
||||||
FolderChecked,
|
|
||||||
FolderOpened,
|
|
||||||
Operation,
|
|
||||||
Refresh,
|
|
||||||
Search,
|
|
||||||
Upload,
|
|
||||||
WarningFilled
|
|
||||||
} from '@element-plus/icons-vue';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import {
|
import { getProcessingOverview, type ProcessingOverview } from '@/api/aihr/processing';
|
||||||
cancelLocalKnowledgeImportTask,
|
|
||||||
getProcessingOverview,
|
|
||||||
listLocalKnowledgeImportTasks,
|
|
||||||
startLocalKnowledgeImportTask,
|
|
||||||
type LocalImportTask,
|
|
||||||
type ProcessingOverview
|
|
||||||
} from '@/api/aihr/processing';
|
|
||||||
import { uploadKnowledgeDocAsync, listUploadItems, retryUploadItem, type UploadItem } from '@/api/aihr/sop';
|
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';
|
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 browserBatchWarnSize = 200 * 1024 * 1024;
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement>();
|
const fileInput = ref<HTMLInputElement>();
|
||||||
const directoryInput = ref<HTMLInputElement>();
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const serverImporting = ref(false);
|
|
||||||
const importTaskLoading = ref(false);
|
|
||||||
const overview = ref<ProcessingOverview | null>(null);
|
const overview = ref<ProcessingOverview | null>(null);
|
||||||
const overviewFailed = ref(false);
|
const overviewFailed = ref(false);
|
||||||
const importTasks = ref<LocalImportTask[]>([]);
|
|
||||||
const categoryKeyword = ref('');
|
const categoryKeyword = ref('');
|
||||||
const selectedCategory = ref('住宅 SOP');
|
const selectedCategory = ref('住宅 SOP');
|
||||||
const autoClassify = ref(true);
|
const autoClassify = ref(true);
|
||||||
const activeStatus = ref('全部');
|
const activeStatus = ref('全部');
|
||||||
const fileType = ref('全部');
|
const fileType = ref('全部');
|
||||||
|
const taskPage = ref(1);
|
||||||
|
const taskPageSize = ref(10);
|
||||||
const uploadBatch = ref<UploadItem[]>([]);
|
const uploadBatch = ref<UploadItem[]>([]);
|
||||||
const uploadBatchId = ref('');
|
const uploadBatchId = ref('');
|
||||||
const enqueueFailures = ref<string[]>([]);
|
const enqueueFailures = ref<string[]>([]);
|
||||||
const retryingId = ref<number | null>(null);
|
const retryingId = ref<number | null>(null);
|
||||||
let importPollTimer: ReturnType<typeof window.setTimeout> | undefined;
|
|
||||||
let batchPollTimer: ReturnType<typeof window.setTimeout> | undefined;
|
let batchPollTimer: ReturnType<typeof window.setTimeout> | undefined;
|
||||||
|
|
||||||
const statusTabs = ['全部', '等待解析', '解析中', '已完成', '失败'];
|
const statusTabs = ['全部', '等待解析', '解析中', '已完成', '失败'];
|
||||||
@@ -456,15 +405,15 @@ const filteredCategories = computed(() => {
|
|||||||
return displayOverview.value.categories.filter((item) => !keyword || item.name.toLowerCase().includes(keyword));
|
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 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 () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadOverview(), loadImportTasks()]);
|
await loadOverview();
|
||||||
scheduleImportPoll();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearImportPoll();
|
|
||||||
clearBatchPoll();
|
clearBatchPoll();
|
||||||
// 置空批次 ID,让在途的轮询请求返回后直接丢弃,不再重新挂起定时器
|
// 置空批次 ID,让在途的轮询请求返回后直接丢弃,不再重新挂起定时器
|
||||||
uploadBatchId.value = '';
|
uploadBatchId.value = '';
|
||||||
@@ -477,6 +426,7 @@ async function loadOverview() {
|
|||||||
overview.value = data;
|
overview.value = data;
|
||||||
overviewFailed.value = false;
|
overviewFailed.value = false;
|
||||||
selectedCategory.value = data.categories[0]?.name || selectedCategory.value;
|
selectedCategory.value = data.categories[0]?.name || selectedCategory.value;
|
||||||
|
resetTaskPage();
|
||||||
} catch {
|
} catch {
|
||||||
overview.value = null;
|
overview.value = null;
|
||||||
overviewFailed.value = true;
|
overviewFailed.value = true;
|
||||||
@@ -509,7 +459,7 @@ async function uploadFiles(files: File[]) {
|
|||||||
|
|
||||||
const totalSize = readyFiles.reduce((sum, file) => sum + file.size, 0);
|
const totalSize = readyFiles.reduce((sum, file) => sum + file.size, 0);
|
||||||
if (readyFiles.length > browserBatchWarnCount || totalSize > browserBatchWarnSize) {
|
if (readyFiles.length > browserBatchWarnCount || totalSize > browserBatchWarnSize) {
|
||||||
ElMessage.warning('浏览器批量适合少量文件;大批量资料请放到导入根目录后用服务端导入任务');
|
ElMessage.warning('文件较多,建议压缩为 ZIP 后批量导入');
|
||||||
}
|
}
|
||||||
|
|
||||||
uploading.value = true;
|
uploading.value = true;
|
||||||
@@ -622,91 +572,6 @@ function uploadFailureMessage(error: unknown) {
|
|||||||
return '上传失败';
|
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) {
|
function matchStatus(status: string) {
|
||||||
if (activeStatus.value === '全部') return true;
|
if (activeStatus.value === '全部') return true;
|
||||||
if (activeStatus.value === '等待解析') return ['等待解析', 'OCR排队'].includes(status);
|
if (activeStatus.value === '等待解析') return ['等待解析', 'OCR排队'].includes(status);
|
||||||
@@ -717,6 +582,15 @@ function matchFileType(type: string) {
|
|||||||
return fileType.value === '全部' || type === fileType.value;
|
return fileType.value === '全部' || type === fileType.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectTaskStatus(status: string) {
|
||||||
|
activeStatus.value = status;
|
||||||
|
resetTaskPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTaskPage() {
|
||||||
|
taskPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
function task(
|
function task(
|
||||||
id: number,
|
id: number,
|
||||||
category: string,
|
category: string,
|
||||||
@@ -921,19 +795,6 @@ function fileBadgeClass(type: string) {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-panel {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.failed-count {
|
|
||||||
margin-left: 8px;
|
|
||||||
color: #e60012;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 16px;
|
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