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

This commit is contained in:
2026-09-02 16:47:02 +08:00
parent 9ef20119a9
commit e9fc0bd810
4 changed files with 138 additions and 3 deletions

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { flushPromises, mount } from '@vue/test-utils'; 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'; import DocumentImportBatchStatus from './DocumentImportBatchStatus.vue';
@@ -31,6 +31,10 @@ function createBatch(status: 'COMPLETED' | 'RUNNING', completedCount: number) {
} }
describe('documentImportBatchStatus', () => { describe('documentImportBatchStatus', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.useRealTimers(); vi.useRealTimers();
@@ -77,6 +81,65 @@ describe('documentImportBatchStatus', () => {
wrapper.unmount(); 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 () => { it('失败批次关联任务实际完成后不再展示提示', async () => {
apiMocks.get.mockResolvedValue({ apiMocks.get.mockResolvedValue({
data: { data: {

View File

@@ -3,6 +3,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { $t } from '@easyflow/locales'; import { $t } from '@easyflow/locales';
import { Close } from '@element-plus/icons-vue';
import { ElAlert, ElButton, ElProgress } from 'element-plus'; import { ElAlert, ElButton, ElProgress } from 'element-plus';
import { api } from '#/api/request'; import { api } from '#/api/request';
@@ -48,6 +49,7 @@ const props = defineProps({
const emit = defineEmits(['continued']); const emit = defineEmits(['continued']);
const batch = ref<BatchStatus>(); const batch = ref<BatchStatus>();
const continuing = ref(false); const continuing = ref(false);
const dismissedBatchId = ref('');
const loadError = ref(''); const loadError = ref('');
const refreshing = ref(false); const refreshing = ref(false);
let pollTimer: null | ReturnType<typeof setTimeout> = null; let pollTimer: null | ReturnType<typeof setTimeout> = null;
@@ -55,6 +57,14 @@ let disposed = false;
let refreshGeneration = 0; let refreshGeneration = 0;
let pollDelayMs = 3000; let pollDelayMs = 3000;
const TERMINAL_RECHECK_DELAY_MS = 15_000; 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( const canContinue = computed(
() => () =>
@@ -124,6 +134,35 @@ const interruptMetadata = computed(() => {
return details.join(' · '); 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() { async function refresh() {
if (!props.knowledgeId) return; if (!props.knowledgeId) return;
const currentGeneration = ++refreshGeneration; const currentGeneration = ++refreshGeneration;
@@ -173,6 +212,9 @@ function schedulePoll() {
pollTimer = setTimeout(refresh, retryDelay); pollTimer = setTimeout(refresh, retryDelay);
return; return;
} }
if (batchDismissed.value) {
return;
}
if ( if (
!batch.value || !batch.value ||
batch.value.status === 'COMPLETED' || batch.value.status === 'COMPLETED' ||
@@ -188,6 +230,18 @@ function schedulePoll() {
pollTimer = setTimeout(refresh, delay); 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() { async function continueBatch() {
if (!batch.value || continuing.value) return; if (!batch.value || continuing.value) return;
continuing.value = true; continuing.value = true;
@@ -208,6 +262,7 @@ async function continueBatch() {
onMounted(() => { onMounted(() => {
disposed = false; disposed = false;
dismissedBatchId.value = readDismissedBatchId(props.knowledgeId);
refresh(); refresh();
}); });
@@ -223,6 +278,7 @@ watch(
const knowledgeChanged = knowledgeId !== previousKnowledgeId; const knowledgeChanged = knowledgeId !== previousKnowledgeId;
if (knowledgeChanged) { if (knowledgeChanged) {
batch.value = undefined; batch.value = undefined;
dismissedBatchId.value = readDismissedBatchId(knowledgeId);
loadError.value = ''; loadError.value = '';
pollDelayMs = 3000; pollDelayMs = 3000;
} }
@@ -233,7 +289,7 @@ watch(
<template> <template>
<section <section
v-if="batch" v-if="batch && !batchDismissed"
class="batch-status" class="batch-status"
:aria-label="$t('documentCollection.importDoc.batchStatus')" :aria-label="$t('documentCollection.importDoc.batchStatus')"
> >
@@ -295,6 +351,16 @@ watch(
> >
{{ $t('documentCollection.importDoc.continueBatch') }} {{ $t('documentCollection.importDoc.continueBatch') }}
</ElButton> </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 <ElAlert
v-if="batch.status === 'INTERRUPTED'" v-if="batch.status === 'INTERRUPTED'"
class="batch-status__alert" class="batch-status__alert"
@@ -321,7 +387,7 @@ watch(
</div> </div>
</section> </section>
<section <section
v-else-if="loadError" v-else-if="loadError && !dismissedBatchId"
class="batch-status batch-status--error" class="batch-status batch-status--error"
:aria-label="$t('documentCollection.importDoc.batchStatus')" :aria-label="$t('documentCollection.importDoc.batchStatus')"
> >
@@ -407,6 +473,10 @@ watch(
min-width: 48px; min-width: 48px;
} }
.batch-status__close {
flex-shrink: 0;
}
.batch-status__alert { .batch-status__alert {
flex: 1 0 100%; flex: 1 0 100%;
} }