diff --git a/easyflow-ui-admin/app/src/assets/ai/workflow/workflowIcon.png b/easyflow-ui-admin/app/src/assets/ai/workflow/workflowIcon.png index 69d3c8ba..a9ff1dc4 100644 Binary files a/easyflow-ui-admin/app/src/assets/ai/workflow/workflowIcon.png and b/easyflow-ui-admin/app/src/assets/ai/workflow/workflowIcon.png differ diff --git a/easyflow-ui-admin/app/src/utils/lazy-component.test.ts b/easyflow-ui-admin/app/src/utils/lazy-component.test.ts new file mode 100644 index 00000000..769bb2a7 --- /dev/null +++ b/easyflow-ui-admin/app/src/utils/lazy-component.test.ts @@ -0,0 +1,86 @@ +/* eslint-disable vue/one-component-per-file -- Test-specific host components keep lazy-mount scenarios isolated. */ +import { flushPromises, mount } from '@vue/test-utils'; +import { defineComponent, h } from 'vue'; + +import { describe, expect, it, vi } from 'vitest'; + +import { createLazyComponentController } from './lazy-component'; + +interface DeferredInstance { + openDialog: () => void; +} + +describe('createLazyComponentController', () => { + it('loads and mounts the component only when its instance is requested', async () => { + const openDialog = vi.fn(); + const DeferredComponent = defineComponent({ + setup(_, { expose }) { + expose({ openDialog }); + return () => h('div', 'deferred'); + }, + }); + const loader = vi.fn().mockResolvedValue({ default: DeferredComponent }); + const controller = createLazyComponentController(loader); + const Host = defineComponent({ + setup() { + return () => + controller.component.value + ? h(controller.component.value, { + ref: controller.componentRef, + }) + : null; + }, + }); + + const wrapper = mount(Host); + + expect(loader).not.toHaveBeenCalled(); + expect(controller.loading.value).toBe(false); + + const instance = await controller.ensureInstance(); + instance.openDialog(); + + expect(loader).toHaveBeenCalledTimes(1); + expect(openDialog).toHaveBeenCalledTimes(1); + expect(wrapper.text()).toBe('deferred'); + expect(controller.loading.value).toBe(false); + + await controller.ensureInstance(); + expect(loader).toHaveBeenCalledTimes(1); + }); + + it('clears loading state and allows retry after a loading failure', async () => { + const DeferredComponent = defineComponent({ + setup() { + return () => h('div', 'deferred'); + }, + }); + const loader = vi + .fn() + .mockRejectedValueOnce(new Error('chunk failed')) + .mockResolvedValueOnce({ default: DeferredComponent }); + const controller = createLazyComponentController(loader); + const Host = defineComponent({ + setup() { + return () => + controller.component.value + ? h(controller.component.value, { + ref: controller.componentRef, + }) + : null; + }, + }); + + mount(Host); + + await expect(controller.ensureInstance()).rejects.toThrow('chunk failed'); + expect(controller.loading.value).toBe(false); + + const retryPromise = controller.ensureInstance(); + await flushPromises(); + + await expect(retryPromise).resolves.toBeDefined(); + expect(loader).toHaveBeenCalledTimes(2); + expect(controller.loading.value).toBe(false); + }); +}); diff --git a/easyflow-ui-admin/app/src/utils/lazy-component.ts b/easyflow-ui-admin/app/src/utils/lazy-component.ts new file mode 100644 index 00000000..ccfe9682 --- /dev/null +++ b/easyflow-ui-admin/app/src/utils/lazy-component.ts @@ -0,0 +1,84 @@ +import type { Component, DeepReadonly, Ref, ShallowRef } from 'vue'; + +import { markRaw, nextTick, readonly, ref, shallowRef } from 'vue'; + +interface LazyComponentModule { + default: Component; +} + +interface LazyComponentController { + component: ShallowRef; + componentRef: ShallowRef; + ensureInstance: () => Promise; + loading: DeepReadonly>; +} + +/** + * 延迟加载并挂载通过模板 ref 调用的组件。 + * + * @param loader 组件模块加载函数。 + * @returns 延迟组件、组件实例引用、加载状态和实例获取函数。 + */ +function createLazyComponentController( + loader: () => Promise, +): LazyComponentController { + const component = shallowRef(); + const componentRef = shallowRef(); + const loading = ref(false); + let loadPromise: Promise | undefined; + let mountPromise: Promise | undefined; + + async function loadComponent() { + if (component.value) { + return component.value; + } + if (!loadPromise) { + loadPromise = loader() + .then((module) => { + if (!module.default) { + throw new Error('Lazy component module has no default export'); + } + return markRaw(module.default); + }) + .catch((error) => { + loadPromise = undefined; + throw error; + }); + } + const resolvedComponent = await loadPromise; + component.value = resolvedComponent; + return resolvedComponent; + } + + function ensureInstance() { + if (componentRef.value) { + return Promise.resolve(componentRef.value); + } + if (mountPromise) { + return mountPromise; + } + loading.value = true; + mountPromise = loadComponent() + .then(async () => { + await nextTick(); + if (!componentRef.value) { + throw new Error('Lazy component failed to mount'); + } + return componentRef.value; + }) + .finally(() => { + loading.value = false; + mountPromise = undefined; + }); + return mountPromise; + } + + return { + component, + componentRef, + ensureInstance, + loading: readonly(loading), + }; +} + +export { createLazyComponentController }; diff --git a/easyflow-ui-admin/app/src/views/ai/documentCollection/Document.vue b/easyflow-ui-admin/app/src/views/ai/documentCollection/Document.vue index e189391b..1366906e 100644 --- a/easyflow-ui-admin/app/src/views/ai/documentCollection/Document.vue +++ b/easyflow-ui-admin/app/src/views/ai/documentCollection/Document.vue @@ -1,5 +1,5 @@