fix: 支持关闭自动导入状态提示

This commit is contained in:
2026-09-02 16:47:02 +08:00
parent 1f37b0a8ae
commit 6498f1049a
4 changed files with 138 additions and 3 deletions

View File

@@ -116,6 +116,7 @@
"pendingCount": "Pending",
"skippedCount": "Skipped",
"continueBatch": "Continue",
"dismissBatchStatus": "Dismiss automatic import status",
"batchStatusLoadFailed": "Unable to load the automatic import status. Retrying automatically.",
"interruptFallback": "Automatic import was interrupted. Resume after the service recovers.",
"interruptCode": "Error code",

View File

@@ -116,6 +116,7 @@
"pendingCount": "等待",
"skippedCount": "跳过",
"continueBatch": "继续",
"dismissBatchStatus": "关闭自动导入状态提示",
"batchStatusLoadFailed": "自动导入状态暂时无法获取,将自动重试",
"interruptFallback": "自动导入发生异常,批次已中断,请确认服务恢复后继续",
"interruptCode": "错误标识",

View File

@@ -1,6 +1,6 @@
import { flushPromises, mount } from '@vue/test-utils';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import DocumentImportBatchStatus from './DocumentImportBatchStatus.vue';
@@ -31,6 +31,10 @@ function createBatch(status: 'COMPLETED' | 'RUNNING', completedCount: number) {
}
describe('documentImportBatchStatus', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
@@ -77,6 +81,65 @@ describe('documentImportBatchStatus', () => {
wrapper.unmount();
});
it('关闭当前批次提示后停止轮询且不影响导入接口', async () => {
vi.useFakeTimers();
apiMocks.get.mockResolvedValue({
data: createBatch('RUNNING', 1),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
const closeButton = wrapper.find('.batch-status__close');
expect(closeButton.attributes('aria-label')).toBe(
'documentCollection.importDoc.dismissBatchStatus',
);
await closeButton.trigger('click');
expect(wrapper.find('.batch-status').exists()).toBe(false);
await vi.advanceTimersByTimeAsync(30_000);
expect(apiMocks.get).toHaveBeenCalledTimes(1);
expect(apiMocks.post).not.toHaveBeenCalled();
wrapper.unmount();
});
it('当前批次关闭状态在本地保留且新批次仍会展示', async () => {
apiMocks.get
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockResolvedValueOnce({
data: { ...createBatch('RUNNING', 0), batchId: 'batch-2' },
errorCode: 0,
});
const firstWrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
await firstWrapper.find('.batch-status__close').trigger('click');
firstWrapper.unmount();
const secondWrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(secondWrapper.find('.batch-status').exists()).toBe(false);
await secondWrapper.setProps({ refreshKey: 1 });
await flushPromises();
expect(secondWrapper.find('.batch-status').exists()).toBe(true);
secondWrapper.unmount();
});
it('失败批次关联任务实际完成后不再展示提示', async () => {
apiMocks.get.mockResolvedValue({
data: {

View File

@@ -3,6 +3,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { $t } from '@easyflow/locales';
import { Close } from '@element-plus/icons-vue';
import { ElAlert, ElButton, ElProgress } from 'element-plus';
import { api } from '#/api/request';
@@ -48,6 +49,7 @@ const props = defineProps({
const emit = defineEmits(['continued']);
const batch = ref<BatchStatus>();
const continuing = ref(false);
const dismissedBatchId = ref('');
const loadError = ref('');
const refreshing = ref(false);
let pollTimer: null | ReturnType<typeof setTimeout> = null;
@@ -55,6 +57,14 @@ let disposed = false;
let refreshGeneration = 0;
let pollDelayMs = 3000;
const TERMINAL_RECHECK_DELAY_MS = 15_000;
const DISMISSED_BATCH_STORAGE_PREFIX =
'easyflow.documentImport.dismissedBatch.';
const batchDismissed = computed(
() =>
Boolean(batch.value?.batchId) &&
batch.value?.batchId === dismissedBatchId.value,
);
const canContinue = computed(
() =>
@@ -124,6 +134,35 @@ const interruptMetadata = computed(() => {
return details.join(' · ');
});
function dismissalStorageKey(knowledgeId: string) {
return `${DISMISSED_BATCH_STORAGE_PREFIX}${knowledgeId}`;
}
function readDismissedBatchId(knowledgeId: string) {
if (!knowledgeId || typeof window === 'undefined') {
return '';
}
try {
return (
window.localStorage.getItem(dismissalStorageKey(knowledgeId))?.trim() ||
''
);
} catch {
return '';
}
}
function persistDismissedBatchId(knowledgeId: string, batchId: string) {
if (!knowledgeId || !batchId || typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(dismissalStorageKey(knowledgeId), batchId);
} catch {
// 浏览器禁用存储时,关闭操作在当前组件生命周期内仍然有效。
}
}
async function refresh() {
if (!props.knowledgeId) return;
const currentGeneration = ++refreshGeneration;
@@ -173,6 +212,9 @@ function schedulePoll() {
pollTimer = setTimeout(refresh, retryDelay);
return;
}
if (batchDismissed.value) {
return;
}
if (
!batch.value ||
batch.value.status === 'COMPLETED' ||
@@ -188,6 +230,18 @@ function schedulePoll() {
pollTimer = setTimeout(refresh, delay);
}
function dismissBatchStatus() {
if (!batch.value?.batchId) {
return;
}
dismissedBatchId.value = batch.value.batchId;
persistDismissedBatchId(props.knowledgeId, batch.value.batchId);
loadError.value = '';
refreshGeneration += 1;
if (pollTimer) clearTimeout(pollTimer);
pollTimer = null;
}
async function continueBatch() {
if (!batch.value || continuing.value) return;
continuing.value = true;
@@ -208,6 +262,7 @@ async function continueBatch() {
onMounted(() => {
disposed = false;
dismissedBatchId.value = readDismissedBatchId(props.knowledgeId);
refresh();
});
@@ -223,6 +278,7 @@ watch(
const knowledgeChanged = knowledgeId !== previousKnowledgeId;
if (knowledgeChanged) {
batch.value = undefined;
dismissedBatchId.value = readDismissedBatchId(knowledgeId);
loadError.value = '';
pollDelayMs = 3000;
}
@@ -233,7 +289,7 @@ watch(
<template>
<section
v-if="batch"
v-if="batch && !batchDismissed"
class="batch-status"
:aria-label="$t('documentCollection.importDoc.batchStatus')"
>
@@ -295,6 +351,16 @@ watch(
>
{{ $t('documentCollection.importDoc.continueBatch') }}
</ElButton>
<ElButton
class="batch-status__close"
:icon="Close"
circle
text
size="small"
:aria-label="$t('documentCollection.importDoc.dismissBatchStatus')"
:title="$t('documentCollection.importDoc.dismissBatchStatus')"
@click="dismissBatchStatus"
/>
<ElAlert
v-if="batch.status === 'INTERRUPTED'"
class="batch-status__alert"
@@ -321,7 +387,7 @@ watch(
</div>
</section>
<section
v-else-if="loadError"
v-else-if="loadError && !dismissedBatchId"
class="batch-status batch-status--error"
:aria-label="$t('documentCollection.importDoc.batchStatus')"
>
@@ -407,6 +473,10 @@ watch(
min-width: 48px;
}
.batch-status__close {
flex-shrink: 0;
}
.batch-status__alert {
flex: 1 0 100%;
}