fix: 支持关闭自动导入状态提示
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
"pendingCount": "等待",
|
"pendingCount": "等待",
|
||||||
"skippedCount": "跳过",
|
"skippedCount": "跳过",
|
||||||
"continueBatch": "继续",
|
"continueBatch": "继续",
|
||||||
|
"dismissBatchStatus": "关闭自动导入状态提示",
|
||||||
"batchStatusLoadFailed": "自动导入状态暂时无法获取,将自动重试",
|
"batchStatusLoadFailed": "自动导入状态暂时无法获取,将自动重试",
|
||||||
"interruptFallback": "自动导入发生异常,批次已中断,请确认服务恢复后继续",
|
"interruptFallback": "自动导入发生异常,批次已中断,请确认服务恢复后继续",
|
||||||
"interruptCode": "错误标识",
|
"interruptCode": "错误标识",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user