perf: 优化旧版浏览器页面恢复与资源加载

- 页面恢复时延后版本检查并补齐根背景,减少白闪与首帧卡顿

- 面向 Chrome 90 收敛公共分包,按需加载资源弹窗和知识库面板

- 压缩工作流图标并补充延迟加载与可见性生命周期测试
This commit is contained in:
2026-07-31 16:11:14 +08:00
parent 5c29ca9407
commit 12080ceedb
15 changed files with 505 additions and 68 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue';
import { computed, defineAsyncComponent, nextTick, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAccess } from '@easyflow/access';
@@ -7,25 +7,64 @@ import { $t } from '@easyflow/locales';
import { useUserStore } from '@easyflow/stores';
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
import { ElButton, ElIcon, ElImage } from 'element-plus';
import { ElButton, ElIcon, ElImage, ElMessage } from 'element-plus';
import { api } from '#/api/request';
import bookIcon from '#/assets/ai/knowledge/book.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import ChunkDocumentTable from '#/views/ai/documentCollection/ChunkDocumentTable.vue';
import DocumentCollectionDataConfig from '#/views/ai/documentCollection/DocumentCollectionDataConfig.vue';
import DocumentTable from '#/views/ai/documentCollection/DocumentTable.vue';
import FaqTable from '#/views/ai/documentCollection/FaqTable.vue';
import ImportKnowledgeDocFile from '#/views/ai/documentCollection/ImportKnowledgeDocFile.vue';
import KnowledgeSearch from '#/views/ai/documentCollection/KnowledgeSearch.vue';
import KnowledgeShareManagement from '#/views/ai/documentCollection/KnowledgeShareManagement.vue';
import SegmenterDoc from '#/views/ai/documentCollection/SegmenterDoc.vue';
import { createLazyComponentController } from '#/utils/lazy-component';
const ChunkDocumentTable = defineAsyncComponent(
() => import('#/views/ai/documentCollection/ChunkDocumentTable.vue'),
);
const DocumentCollectionDataConfig = defineAsyncComponent(
() =>
import('#/views/ai/documentCollection/DocumentCollectionDataConfig.vue'),
);
const DocumentTable = defineAsyncComponent(
() => import('#/views/ai/documentCollection/DocumentTable.vue'),
);
const FaqTable = defineAsyncComponent(
() => import('#/views/ai/documentCollection/FaqTable.vue'),
);
const KnowledgeSearch = defineAsyncComponent(
() => import('#/views/ai/documentCollection/KnowledgeSearch.vue'),
);
const KnowledgeShareManagement = defineAsyncComponent(
() => import('#/views/ai/documentCollection/KnowledgeShareManagement.vue'),
);
const SegmenterDoc = defineAsyncComponent(
() => import('#/views/ai/documentCollection/SegmenterDoc.vue'),
);
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const { hasAccessByCodes } = useAccess();
interface ImportKnowledgeDocFileInstance {
openDialog: () => void;
}
const {
component: ImportKnowledgeDocFileComponent,
componentRef: importDocModalRef,
ensureInstance: ensureImportKnowledgeDocFile,
loading: importDialogLoading,
} = createLazyComponentController<ImportKnowledgeDocFileInstance>(
() => import('#/views/ai/documentCollection/ImportKnowledgeDocFile.vue'),
);
async function openImportDialog() {
try {
const modal = await ensureImportKnowledgeDocFile();
modal.openDialog();
} catch (error) {
console.error('Failed to load document import modal:', error);
ElMessage.error($t('message.getDataError'));
}
}
const knowledgeId = ref<string>((route.query.id as string) || '');
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
const knowledgeInfo = ref<any>({});
@@ -149,10 +188,9 @@ const headerButtons = [
];
const panelMode = ref<'chunk' | 'list' | 'process'>('list');
const documentTableRef = ref();
const importDocModalRef = ref<InstanceType<typeof ImportKnowledgeDocFile>>();
const documentTitle = ref('');
const handleSearch = (searchParams: string) => {
documentTableRef.value.search(searchParams);
documentTableRef.value?.search?.(searchParams);
};
const handleButtonClick = (event: any) => {
// 根据按钮 key 执行不同操作
@@ -162,7 +200,7 @@ const handleButtonClick = (event: any) => {
break;
}
case 'importFile': {
importDocModalRef.value?.openDialog?.();
void openImportDialog();
break;
}
}
@@ -193,7 +231,7 @@ const backDoc = async () => {
</script>
<template>
<div class="document-container">
<div v-loading="importDialogLoading" class="document-container">
<div class="doc-header-container">
<div class="doc-knowledge-container">
<div @click="back()" style="cursor: pointer">
@@ -301,7 +339,9 @@ const backDoc = async () => {
</div>
</div>
</div>
<ImportKnowledgeDocFile
<component
:is="ImportKnowledgeDocFileComponent"
v-if="ImportKnowledgeDocFileComponent"
ref="importDocModalRef"
:knowledge-id-prop="String(knowledgeId)"
@imported="backDoc"

View File

@@ -44,8 +44,8 @@ import CardPage from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
import { createLazyComponentController } from '#/utils/lazy-component';
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
import {
@@ -70,6 +70,29 @@ const collectionTypeLabelMap = {
};
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
interface DocumentCollectionModalInstance {
openDialog: (row: Record<string, unknown>) => void;
}
const {
component: DocumentCollectionModalComponent,
componentRef: aiKnowledgeModalRef,
ensureInstance: ensureDocumentCollectionModal,
loading: knowledgeModalLoading,
} = createLazyComponentController<DocumentCollectionModalInstance>(
() => import('#/views/ai/documentCollection/DocumentCollectionModal.vue'),
);
async function openKnowledgeModal(row: Record<string, unknown>) {
try {
const modal = await ensureDocumentCollectionModal();
modal.openDialog(row);
} catch (error) {
console.error('Failed to load knowledge modal:', error);
ElMessage.error($t('message.getDataError'));
}
}
const canManageKnowledgePermission = computed(() =>
hasAccessByCodes(['/api/v1/documentCollection/save']),
);
@@ -205,7 +228,7 @@ const actions: ActionButton[] = [
if (!ensureManageKnowledgeItem(row)) {
return;
}
aiKnowledgeModalRef.value.openDialog(row);
void openKnowledgeModal(row);
},
},
{
@@ -460,7 +483,6 @@ function resolvePublishStatusMeta(
}
const pageDataRef = ref();
const aiKnowledgeModalRef = ref();
const headerButtons = [
{
key: 'add',
@@ -474,7 +496,7 @@ const headerButtons = [
const handleButtonClick = (event: any, _item: any) => {
switch (event.key) {
case 'add': {
aiKnowledgeModalRef.value.openDialog({});
void openKnowledgeModal({});
break;
}
}
@@ -667,7 +689,7 @@ function changeCategory(category: any) {
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<div v-loading="knowledgeModalLoading" class="flex h-full flex-col gap-6 p-6">
<div class="knowledge-header">
<HeaderSearch
:buttons="headerButtons"
@@ -865,7 +887,9 @@ function changeCategory(category: any) {
</EasyFlowFormModal>
<!-- 新增知识库模态框-->
<DocumentCollectionModal
<component
:is="DocumentCollectionModalComponent"
v-if="DocumentCollectionModalComponent"
ref="aiKnowledgeModalRef"
@reload="reloadKnowledgeList"
/>

View File

@@ -29,7 +29,7 @@ import PluginToolIcon from '#/components/icons/PluginToolIcon.vue';
import CardPage from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import AddPluginModal from '#/views/ai/plugin/AddPluginModal.vue';
import { createLazyComponentController } from '#/utils/lazy-component';
import { buildPluginPageQueryParams } from './plugin-query';
import {
@@ -61,6 +61,29 @@ interface CategoryFormData {
name: string;
}
interface AddPluginModalInstance {
openDialog: (row: Record<string, unknown>) => void;
}
const {
component: AddPluginModalComponent,
componentRef: aiPluginModalRef,
ensureInstance: ensureAddPluginModal,
loading: pluginModalLoading,
} = createLazyComponentController<AddPluginModalInstance>(
() => import('#/views/ai/plugin/AddPluginModal.vue'),
);
async function openPluginDialog(row: Record<string, unknown>) {
try {
const modal = await ensureAddPluginModal();
modal.openDialog(row);
} catch (error) {
console.error('Failed to load plugin modal:', error);
ElMessage.error($t('message.getDataError'));
}
}
function resolveNavTitle(item: PluginRecord) {
return (item.title as string) || (item.name as string) || '';
}
@@ -93,7 +116,7 @@ const actions: ActionButton[] = [
permission: '/api/v1/plugin/save',
placement: 'inline',
onClick(item) {
aiPluginModalRef.value.openDialog(item);
void openPluginDialog(item);
},
},
{
@@ -194,7 +217,6 @@ const handleDelete = (item: PluginRecord) => {
};
const pageDataRef = ref();
const aiPluginModalRef = ref();
const headerButtons = [
{
key: 'add',
@@ -221,7 +243,7 @@ const handleSubmit = () => {
const handleButtonClick = (event: HeaderActionEvent, _item: unknown) => {
switch (event.key) {
case 'add': {
aiPluginModalRef.value.openDialog({});
void openPluginDialog({});
break;
}
}
@@ -316,7 +338,7 @@ const handleClickCategory = (item: PluginCategory) => {
</script>
<template>
<div class="knowledge-container">
<div v-loading="pluginModalLoading" class="knowledge-container">
<div class="knowledge-header">
<HeaderSearch
:buttons="headerButtons"
@@ -378,7 +400,12 @@ const handleClickCategory = (item: PluginCategory) => {
</PageData>
</div>
</div>
<AddPluginModal ref="aiPluginModalRef" @reload="reloadCurrentList" />
<component
:is="AddPluginModalComponent"
v-if="AddPluginModalComponent"
ref="aiPluginModalRef"
@reload="reloadCurrentList"
/>
<EasyFlowFormModal
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
v-model:open="dialogVisible"

View File

@@ -64,6 +64,7 @@ import { $t } from '#/locales';
import { router } from '#/router';
import { useDictStore } from '#/store';
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
import { createLazyComponentController } from '#/utils/lazy-component';
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
@@ -82,7 +83,6 @@ import {
mergeWorkflowListRouteQuery,
parseWorkflowListRouteState,
} from './workflow-list-route-state';
import WorkflowModal from './WorkflowModal.vue';
const ElXMarkdown = defineAsyncComponent(
() => import('vue-element-plus-x/es/XMarkdown/index.js'),
@@ -116,6 +116,19 @@ interface ApiFieldDoc {
placeholder: string;
}
interface WorkflowModalInstance {
openDialog: (row: Record<string, unknown>, importMode?: boolean) => void;
}
const {
component: WorkflowModalComponent,
componentRef: saveDialog,
ensureInstance: ensureWorkflowModal,
loading: workflowModalLoading,
} = createLazyComponentController<WorkflowModalInstance>(
() => import('./WorkflowModal.vue'),
);
const primaryAction: CardPrimaryAction = {
showIndicator: false,
text: $t('button.design'),
@@ -283,7 +296,6 @@ onMounted(() => {
getSideList();
});
const pageDataRef = ref();
const saveDialog = ref();
const selectedCategoryId = ref<number | string>(initialListState.categoryId);
const searchKeyword = ref(initialListState.keyword);
const currentPageNumber = ref(initialListState.pageNumber);
@@ -401,8 +413,14 @@ function handleSearch(keyword: string) {
function reloadCurrentList() {
pageDataRef.value?.reload?.();
}
function showDialog(row: any, importMode = false) {
saveDialog.value.openDialog({ ...row }, importMode);
async function showDialog(row: any, importMode = false) {
try {
const modal = await ensureWorkflowModal();
modal.openDialog({ ...row }, importMode);
} catch (error) {
console.error('Failed to load workflow modal:', error);
ElMessage.error($t('message.getDataError'));
}
}
function resolveNavTitle(row: any) {
return row?.title || row?.name || '';
@@ -1144,8 +1162,13 @@ function handleHeaderButtonClick(data: any) {
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<WorkflowModal ref="saveDialog" @reload="reloadCurrentList" />
<div v-loading="workflowModalLoading" class="flex h-full flex-col gap-6 p-6">
<component
:is="WorkflowModalComponent"
v-if="WorkflowModalComponent"
ref="saveDialog"
@reload="reloadCurrentList"
/>
<ElDialog
v-model="apiInstructionVisible"
:title="$t('aiWorkflow.apiInstruction')"