feat: 统一恢复管理端列表上下文

- 统一列表路由状态、分页初始化与安全返回契约

- 接入知识库、工作流、插件、审批、Skill、Agent、Bot、反馈和定时任务链路

- 补齐公共能力与关键返回路径自动化测试
This commit is contained in:
2026-08-10 16:12:43 +08:00
parent 9d2fa39a2d
commit 6bfd440214
42 changed files with 2359 additions and 570 deletions

View File

@@ -5,9 +5,10 @@ import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useAccess } from '@easyflow/access';
import { EasyFlowFormModal } from '@easyflow/common-ui';
@@ -43,15 +44,20 @@ import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardPage from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import {
buildListRouteFullPath,
mergeListRouteStateQuery,
parseListRouteState,
watchListRouteState,
} from '#/composables/useListRouteState';
import { withListReturnTo } from '#/router/list-return-context';
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
import { createLazyComponentController } from '#/utils/lazy-component';
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
import { documentCollectionListRouteSchema } from '#/views/ai/documentCollection/document-collection-list-route-state';
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
import {
buildOfflineImpactMessage,
type OfflineImpactCheck,
} from '#/views/ai/shared/offline-impact';
import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
import {
canAiResourceDelete,
canAiResourceOffline,
@@ -61,7 +67,35 @@ import {
resolveAiResourceDisplayStatus,
} from '#/views/ai/shared/publish-status';
const route = useRoute();
const router = useRouter();
const initialListState = parseListRouteState(
route.query,
documentCollectionListRouteSchema,
);
const selectedCategoryId = ref(initialListState.categoryId);
const searchKeyword = ref(initialListState.keyword);
const currentPageNumber = ref(initialListState.pageNumber);
const currentPageSize = ref(initialListState.pageSize);
let initialCategoryChangePending = Boolean(initialListState.categoryId);
watchListRouteState(route, documentCollectionListRouteSchema, (state) => {
if (state.categoryId !== selectedCategoryId.value) {
initialCategoryChangePending = Boolean(state.categoryId);
}
selectedCategoryId.value = state.categoryId;
searchKeyword.value = state.keyword;
currentPageNumber.value = state.pageNumber;
currentPageSize.value = state.pageSize;
pageDataRef.value?.restoreState?.({
pageNumber: state.pageNumber,
pageSize: state.pageSize,
queryParams: {
categoryId: state.categoryId || undefined,
isQueryOr: true,
title: state.keyword || undefined,
},
});
});
const userStore = useUserStore();
const { hasAccessByCodes } = useAccess();
const collectionTypeLabelMap = {
@@ -153,6 +187,20 @@ function resolveNavTitle(row: Record<string, any>) {
return row?.title || row?.name || '';
}
function currentListFullPath() {
return buildListRouteFullPath(
router,
route.query,
{
categoryId: selectedCategoryId.value,
keyword: searchKeyword.value,
pageNumber: currentPageNumber.value,
pageSize: currentPageSize.value,
},
documentCollectionListRouteSchema,
);
}
function openKnowledgeDetail(row: {
id: string;
name?: string;
@@ -164,6 +212,7 @@ function openKnowledgeDetail(row: {
id: row.id,
pageKey: '/ai/documentCollection',
navTitle: resolveNavTitle(row),
...withListReturnTo(currentListFullPath()),
},
});
}
@@ -243,6 +292,7 @@ const actions: ActionButton[] = [
pageKey: '/ai/documentCollection',
navTitle: resolveNavTitle(row),
activeMenu: 'knowledgeSearch',
...withListReturnTo(currentListFullPath()),
},
});
},
@@ -451,18 +501,18 @@ function resolvePublishStatusMeta(
tone: 'danger',
};
}
case 'OFFLINE_PENDING': {
return {
label: $t('documentCollection.publishStatusOfflinePending'),
tone: 'pending',
};
}
case 'OFFLINE': {
return {
label: $t('documentCollection.publishStatusOffline'),
tone: 'draft',
};
}
case 'OFFLINE_PENDING': {
return {
label: $t('documentCollection.publishStatusOfflinePending'),
tone: 'pending',
};
}
case 'PUBLISH_PENDING': {
return {
label: $t('documentCollection.publishStatusPublishPending'),
@@ -537,7 +587,12 @@ const formRules = computed(() => {
return rules;
});
const handleSearch = (params: any) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
searchKeyword.value = String(params || '');
pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined,
title: searchKeyword.value || undefined,
isQueryOr: true,
});
};
const reloadKnowledgeList = () => {
pageDataRef.value?.reload?.();
@@ -567,6 +622,19 @@ const getCategoryList = async () => {
},
...res.data,
];
if (
selectedCategoryId.value &&
!categoryList.value.some(
(item) => String(item.id) === selectedCategoryId.value,
)
) {
selectedCategoryId.value = '';
initialCategoryChangePending = false;
pageDataRef.value?.setQuery?.({
title: searchKeyword.value || undefined,
isQueryOr: true,
});
}
}
};
function removeCategory(row: any) {
@@ -686,7 +754,42 @@ function handleSubmit() {
});
}
function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id });
const categoryId = String(category?.id || '');
if (initialCategoryChangePending && categoryId === selectedCategoryId.value) {
initialCategoryChangePending = false;
return;
}
initialCategoryChangePending = false;
selectedCategoryId.value = categoryId;
pageDataRef.value.setQuery({
categoryId: categoryId || undefined,
title: searchKeyword.value || undefined,
isQueryOr: true,
});
}
function handlePageStateChange(state: {
pageNumber: number;
pageSize: number;
}) {
currentPageNumber.value = state.pageNumber;
currentPageSize.value = state.pageSize;
const query = mergeListRouteStateQuery(
route.query,
{
categoryId: selectedCategoryId.value,
keyword: searchKeyword.value,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
},
documentCollectionListRouteSchema,
);
const target = router.resolve({
path: documentCollectionListRouteSchema.path,
query,
});
if (target.fullPath !== route.fullPath) {
void router.replace(target);
}
}
</script>
@@ -695,6 +798,7 @@ function changeCategory(category: any) {
<div class="knowledge-header">
<HeaderSearch
:buttons="headerButtons"
:initial-value="searchKeyword"
@search="handleSearch"
@button-click="handleButtonClick"
/>
@@ -706,15 +810,22 @@ function changeCategory(category: any) {
:menus="categoryList"
:control-btns="controlBtns"
:footer-button="footerButton"
:default-selected="selectedCategoryId"
@change="changeCategory"
/>
<div class="h-full flex-1 overflow-auto">
<PageData
ref="pageDataRef"
page-url="/api/v1/documentCollection/page"
:page-size="12"
:page-size="initialListState.pageSize"
:page-sizes="[12, 24, 36, 48]"
:init-query-params="{ status: 1 }"
:initial-page-number="initialListState.pageNumber"
:initial-query-params="{
categoryId: initialListState.categoryId || undefined,
title: initialListState.keyword || undefined,
isQueryOr: true,
}"
@state-change="handlePageStateChange"
>
<template #default="{ pageList }">
<CardPage