feat: 统一恢复管理端列表上下文
- 统一列表路由状态、分页初始化与安全返回契约 - 接入知识库、工作流、插件、审批、Skill、Agent、Bot、反馈和定时任务链路 - 补齐公共能力与关键返回路径自动化测试
This commit is contained in:
@@ -14,6 +14,7 @@ import { ElButton, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import {
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
@@ -291,8 +292,7 @@ async function loadMcpToolsForOption(id: number | string) {
|
||||
String(item.value) === key
|
||||
? {
|
||||
...item,
|
||||
label:
|
||||
currentOption.label || 'MCP',
|
||||
label: currentOption.label || 'MCP',
|
||||
raw: mergedResource,
|
||||
}
|
||||
: item,
|
||||
@@ -481,8 +481,13 @@ function handleCloseTryout() {
|
||||
selectBase();
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
router.push(AGENT_TAB_PAGE_KEY);
|
||||
async function handleBack() {
|
||||
await navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
[AGENT_TAB_PAGE_KEY],
|
||||
AGENT_TAB_PAGE_KEY,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ import type {
|
||||
ActionButton,
|
||||
CardPrimaryAction,
|
||||
} from '#/components/page/CardList.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { defaultAssistantAvatar } from '@easyflow/common-ui';
|
||||
@@ -27,9 +26,17 @@ import { ElIcon, ElMessage, ElMessageBox, ElPopover } from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardList 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 { $t } from '#/locales';
|
||||
import { withListReturnTo } from '#/router/list-return-context';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
@@ -40,6 +47,7 @@ import {
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import { agentListRouteSchema } from './agent-list-route-state';
|
||||
import {
|
||||
getAgentCategories,
|
||||
submitAgentDeleteApproval,
|
||||
@@ -48,9 +56,35 @@ import {
|
||||
updateAgentVisibilityScope,
|
||||
} from './api';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const initialListState = parseListRouteState(route.query, agentListRouteSchema);
|
||||
const pageDataRef = ref();
|
||||
const sideList = ref<any[]>([]);
|
||||
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, agentListRouteSchema, (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,
|
||||
description: state.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
name: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
const AGENT_TAB_PAGE_KEY = '/ai/agents';
|
||||
const DEFAULT_AGENT_TITLE = '未命名智能体';
|
||||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||||
@@ -104,6 +138,7 @@ const primaryAction: CardPrimaryAction = {
|
||||
query: {
|
||||
pageKey: AGENT_TAB_PAGE_KEY,
|
||||
navTitle: resolveNavTitle(row),
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -149,10 +184,12 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
function handleSearch(keyword: string) {
|
||||
searchKeyword.value = keyword;
|
||||
pageDataRef.value?.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
isQueryOr: true,
|
||||
name: keyword,
|
||||
description: keyword,
|
||||
name: keyword || undefined,
|
||||
description: keyword || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,6 +200,7 @@ function handleButtonClick(payload: any) {
|
||||
query: {
|
||||
pageKey: AGENT_TAB_PAGE_KEY,
|
||||
navTitle: DEFAULT_AGENT_TITLE,
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -221,7 +259,19 @@ async function updateVisibilityScope(
|
||||
}
|
||||
|
||||
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,
|
||||
isQueryOr: true,
|
||||
name: searchKeyword.value || undefined,
|
||||
description: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
@@ -231,9 +281,57 @@ async function loadCategories() {
|
||||
{ id: '', categoryName: $t('common.allCategories') },
|
||||
...(res.data || []),
|
||||
];
|
||||
if (
|
||||
selectedCategoryId.value &&
|
||||
!sideList.value.some(
|
||||
(item) => String(item.id) === selectedCategoryId.value,
|
||||
)
|
||||
) {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
pageDataRef.value?.setQuery?.({
|
||||
isQueryOr: true,
|
||||
name: searchKeyword.value || undefined,
|
||||
description: searchKeyword.value || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
{
|
||||
categoryId: selectedCategoryId.value,
|
||||
keyword: searchKeyword.value,
|
||||
pageNumber: currentPageNumber.value,
|
||||
pageSize: currentPageSize.value,
|
||||
},
|
||||
agentListRouteSchema,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
agentListRouteSchema,
|
||||
);
|
||||
const target = router.resolve({ path: agentListRouteSchema.path, query });
|
||||
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||
}
|
||||
|
||||
function resolvePublishStatusMeta(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
@@ -333,6 +431,7 @@ async function handleDeleteAction(row: AgentInfo) {
|
||||
<div class="agent-list-page">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
:initial-value="searchKeyword"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
@@ -341,6 +440,7 @@ async function handleDeleteAction(row: AgentInfo) {
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="sideList"
|
||||
:default-selected="selectedCategoryId"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="agent-list-page__content">
|
||||
@@ -348,7 +448,15 @@ async function handleDeleteAction(row: AgentInfo) {
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/agent/page"
|
||||
:page-sizes="[12, 18, 24]"
|
||||
:page-size="12"
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
isQueryOr: true,
|
||||
name: initialListState.keyword || undefined,
|
||||
description: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardList
|
||||
@@ -366,13 +474,12 @@ async function handleDeleteAction(row: AgentInfo) {
|
||||
<template #publish>
|
||||
<div
|
||||
class="agent-publish-chip"
|
||||
:class="
|
||||
'agent-publish-chip--' +
|
||||
:class="`agent-publish-chip--${
|
||||
resolvePublishStatusMeta(
|
||||
item.displayPublishStatus,
|
||||
item.publishStatus,
|
||||
).tone
|
||||
"
|
||||
}`"
|
||||
>
|
||||
<span class="agent-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
defineListRouteStateSchema,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface AgentListRouteState {
|
||||
categoryId: string;
|
||||
keyword: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const agentListRouteSchema = defineListRouteStateSchema<AgentListRouteState>({
|
||||
path: '/ai/agents',
|
||||
fields: {
|
||||
categoryId: stringListRouteField(),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 18, 24],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export { agentListRouteSchema };
|
||||
export type { AgentListRouteState };
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
defineListRouteStateSchema,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface BotListRouteState {
|
||||
categoryId: string;
|
||||
keyword: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const botListRouteSchema = defineListRouteStateSchema<BotListRouteState>({
|
||||
path: '/ai/bots',
|
||||
fields: {
|
||||
categoryId: stringListRouteField(),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 18, 24],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export { botListRouteSchema };
|
||||
export type { BotListRouteState };
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
@@ -37,6 +37,13 @@ import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardList 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 {
|
||||
confirmPublishSubmission,
|
||||
} from '#/views/ai/shared/approval-application-reason';
|
||||
@@ -50,6 +57,7 @@ import {
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import { botListRouteSchema } from './bot-list-route-state';
|
||||
import Modal from './modal.vue';
|
||||
|
||||
interface FieldDefinition {
|
||||
@@ -70,8 +78,33 @@ onMounted(() => {
|
||||
getSideList();
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const initialListState = parseListRouteState(route.query, botListRouteSchema);
|
||||
const pageDataRef = ref();
|
||||
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, botListRouteSchema, (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 modalRef = ref<InstanceType<typeof Modal>>();
|
||||
const dictStore = useDictStore();
|
||||
|
||||
@@ -98,6 +131,7 @@ const primaryAction: CardPrimaryAction = {
|
||||
query: {
|
||||
pageKey: '/ai/bots',
|
||||
navTitle: resolveNavTitle(row),
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -273,7 +307,12 @@ function resolvePublishStatusMetaByInstance(
|
||||
}
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
searchKeyword.value = params;
|
||||
pageDataRef.value.setQuery({
|
||||
categoryId: selectedCategoryId.value || undefined,
|
||||
title: params || undefined,
|
||||
isQueryOr: true,
|
||||
});
|
||||
};
|
||||
const handleButtonClick = () => {
|
||||
modalRef.value?.open('create');
|
||||
@@ -347,7 +386,18 @@ function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
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 showControlDialog(item: any) {
|
||||
formRef.value?.resetFields();
|
||||
@@ -412,14 +462,60 @@ const getSideList = async () => {
|
||||
},
|
||||
...res.data,
|
||||
];
|
||||
if (
|
||||
selectedCategoryId.value &&
|
||||
!sideList.value.some(
|
||||
(category) => String(category.id) === selectedCategoryId.value,
|
||||
)
|
||||
) {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
pageDataRef.value?.setQuery?.({
|
||||
title: searchKeyword.value || undefined,
|
||||
isQueryOr: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
{
|
||||
categoryId: selectedCategoryId.value,
|
||||
keyword: searchKeyword.value,
|
||||
pageNumber: currentPageNumber.value,
|
||||
pageSize: currentPageSize.value,
|
||||
},
|
||||
botListRouteSchema,
|
||||
);
|
||||
}
|
||||
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,
|
||||
},
|
||||
botListRouteSchema,
|
||||
);
|
||||
const target = router.resolve({ path: botListRouteSchema.path, query });
|
||||
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-6">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
:initial-value="searchKeyword"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
@@ -430,6 +526,7 @@ const getSideList = async () => {
|
||||
:menus="sideList"
|
||||
:control-btns="controlBtns"
|
||||
:footer-button="footerButton"
|
||||
:default-selected="selectedCategoryId"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
|
||||
@@ -437,7 +534,14 @@ const getSideList = async () => {
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/bot/page"
|
||||
:page-sizes="[12, 18, 24]"
|
||||
:page-size="12"
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
title: initialListState.keyword || undefined,
|
||||
isQueryOr: true,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardList
|
||||
@@ -462,7 +566,7 @@ const getSideList = async () => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- 创建&编辑Bot弹窗 -->
|
||||
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
|
||||
<Modal ref="modalRef" @success="pageDataRef.reload()" />
|
||||
|
||||
<EasyFlowFormModal
|
||||
v-model:open="dialogVisible"
|
||||
|
||||
@@ -4,10 +4,14 @@ import type { BotInfo } from '@easyflow/types';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { ElButton } from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { getBotDetails } from '#/api';
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import { $t } from '#/locales';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
|
||||
import Config from './config.vue';
|
||||
import Preview from './preview.vue';
|
||||
@@ -55,10 +59,16 @@ const fetchBotDetail = async (id: string) => {
|
||||
syncNavTitle((res.data?.title || res.data?.name || '') as string);
|
||||
}
|
||||
};
|
||||
async function backToBotList() {
|
||||
await navigateBackToList(router, route.query, ['/ai/bots'], '/ai/bots');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<ElButton :icon="ArrowLeft" class="settings-back" @click="backToBotList">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<div class="row-container">
|
||||
<div class="row-item">
|
||||
<Prompt :bot="bot" :has-save-permission="hasSavePermission" />
|
||||
@@ -74,14 +84,22 @@ const fetchBotDetail = async (id: string) => {
|
||||
</template>
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: calc(100vh - 90px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-back {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.row-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.row-item {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { navigateBackToList } from '#/router/list-return-context';
|
||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||
import DocumentImportBatchStatus from '#/views/ai/documentCollection/DocumentImportBatchStatus.vue';
|
||||
|
||||
@@ -150,8 +151,13 @@ const getKnowledge = () => {
|
||||
onMounted(() => {
|
||||
getKnowledge();
|
||||
});
|
||||
const back = () => {
|
||||
router.push({ path: '/ai/documentCollection' });
|
||||
const back = async () => {
|
||||
await navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
['/ai/documentCollection'],
|
||||
'/ai/documentCollection',
|
||||
);
|
||||
};
|
||||
const isFaqCollection = computed(
|
||||
() => knowledgeInfo.value.collectionType === 'FAQ',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
defineListRouteStateSchema,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface DocumentCollectionListRouteState {
|
||||
categoryId: string;
|
||||
keyword: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const documentCollectionListRouteSchema =
|
||||
defineListRouteStateSchema<DocumentCollectionListRouteState>({
|
||||
path: '/ai/documentCollection',
|
||||
fields: {
|
||||
categoryId: stringListRouteField(),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 24, 36, 48],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export { documentCollectionListRouteSchema };
|
||||
export type { DocumentCollectionListRouteState };
|
||||
@@ -29,13 +29,18 @@ 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 {
|
||||
buildListRouteFullPath,
|
||||
watchListRouteState,
|
||||
} from '#/composables/useListRouteState';
|
||||
import { withListReturnTo } from '#/router/list-return-context';
|
||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||
|
||||
import { buildPluginPageQueryParams } from './plugin-query';
|
||||
import {
|
||||
buildPluginToolsReturnQuery,
|
||||
mergePluginListRouteQuery,
|
||||
parsePluginListRouteState,
|
||||
pluginListRouteSchema,
|
||||
} from './plugin-route-state';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -95,7 +100,7 @@ function openPluginTools(item: PluginRecord) {
|
||||
id: item.id,
|
||||
pageKey: '/ai/plugin',
|
||||
navTitle: resolveNavTitle(item),
|
||||
...buildPluginToolsReturnQuery(currentListState()),
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -217,6 +222,20 @@ const handleDelete = (item: PluginRecord) => {
|
||||
};
|
||||
|
||||
const pageDataRef = ref();
|
||||
watchListRouteState(route, pluginListRouteSchema, (state) => {
|
||||
if (String(state.categoryId) !== String(selectedCategoryId.value)) {
|
||||
suppressInitialCategoryChange.value = true;
|
||||
}
|
||||
selectedCategoryId.value = state.categoryId;
|
||||
searchKeyword.value = state.keyword.trim();
|
||||
currentPageNumber.value = state.pageNumber;
|
||||
currentPageSize.value = state.pageSize;
|
||||
pageDataRef.value?.restoreState?.({
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: buildPluginPageQueryParams(state.categoryId, state.keyword),
|
||||
});
|
||||
});
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'add',
|
||||
@@ -265,6 +284,15 @@ function currentListState(): PluginListRouteState {
|
||||
};
|
||||
}
|
||||
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
currentListState(),
|
||||
pluginListRouteSchema,
|
||||
);
|
||||
}
|
||||
|
||||
function syncListRouteState() {
|
||||
const target = {
|
||||
path: '/ai/plugin',
|
||||
@@ -325,7 +353,7 @@ const handleDeleteCategory = (params: PluginCategory) => {
|
||||
const handleClickCategory = (item: PluginCategory) => {
|
||||
if (
|
||||
suppressInitialCategoryChange.value &&
|
||||
String(item.id) === initialListState.categoryId
|
||||
String(item.id) === String(selectedCategoryId.value)
|
||||
) {
|
||||
suppressInitialCategoryChange.value = false;
|
||||
selectedCategoryId.value = item.id;
|
||||
|
||||
@@ -19,16 +19,11 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||
|
||||
import {
|
||||
buildPluginListRouteQuery,
|
||||
buildPluginToolsRouteQueryFromEdit,
|
||||
parsePluginToolsReturnState,
|
||||
} from './plugin-route-state';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -169,19 +164,20 @@ function handleClickHeader(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function back() {
|
||||
async function back() {
|
||||
const pluginId = String(route.query.pluginId || '');
|
||||
if (pluginId) {
|
||||
void router.replace({
|
||||
path: '/ai/plugin/tools',
|
||||
query: buildPluginToolsRouteQueryFromEdit(route.query, pluginId),
|
||||
});
|
||||
return;
|
||||
}
|
||||
void router.replace({
|
||||
path: '/ai/plugin',
|
||||
query: buildPluginListRouteQuery(parsePluginToolsReturnState(route.query)),
|
||||
});
|
||||
const fallbackPath = pluginId
|
||||
? router.resolve({
|
||||
path: '/ai/plugin/tools',
|
||||
query: { id: pluginId },
|
||||
}).fullPath
|
||||
: '/ai/plugin';
|
||||
await navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
pluginId ? ['/ai/plugin/tools'] : ['/ai/plugin'],
|
||||
fallbackPath,
|
||||
);
|
||||
}
|
||||
|
||||
function updatePluginTool(index: number) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { withListReturnTo } from '#/router/list-return-context';
|
||||
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
||||
|
||||
import { buildPluginToolPageQueryParams } from './plugin-query';
|
||||
@@ -31,6 +32,10 @@ const props = defineProps({
|
||||
default: 1,
|
||||
type: Number,
|
||||
},
|
||||
returnTo: {
|
||||
default: '/ai/plugin/tools',
|
||||
type: String,
|
||||
},
|
||||
initialKeyword: {
|
||||
default: '',
|
||||
type: String,
|
||||
@@ -53,7 +58,6 @@ const emit = defineEmits<{
|
||||
},
|
||||
): void;
|
||||
}>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
defineExpose({
|
||||
openPluginToolModal() {
|
||||
@@ -65,17 +69,27 @@ defineExpose({
|
||||
handleSearch: (params: string) => {
|
||||
pageDataRef.value.setQuery(buildPluginToolPageQueryParams(params));
|
||||
},
|
||||
restoreState: (state: {
|
||||
keyword: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
return pageDataRef.value?.restoreState?.({
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: buildPluginToolPageQueryParams(state.keyword),
|
||||
});
|
||||
},
|
||||
});
|
||||
const pageDataRef = ref();
|
||||
const handleEdit = (row: any) => {
|
||||
router.push({
|
||||
path: '/ai/plugin/tool/edit',
|
||||
query: {
|
||||
...route.query,
|
||||
query: withListReturnTo(props.returnTo, {
|
||||
id: row.id,
|
||||
pageKey: '/ai/plugin',
|
||||
pluginId: props.pluginId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -10,13 +10,17 @@ import { Back, Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import {
|
||||
buildListRouteFullPath,
|
||||
watchListRouteState,
|
||||
} from '#/composables/useListRouteState';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||
|
||||
import {
|
||||
buildPluginListRouteQuery,
|
||||
mergePluginToolListRouteQuery,
|
||||
parsePluginToolListRouteState,
|
||||
parsePluginToolsReturnState,
|
||||
pluginToolListRouteSchema,
|
||||
} from './plugin-route-state';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -29,6 +33,20 @@ const pluginToolRef = ref();
|
||||
const toolSearchKeyword = ref(initialToolListState.keyword.trim());
|
||||
const currentToolPageNumber = ref(initialToolListState.pageNumber);
|
||||
const currentToolPageSize = ref(initialToolListState.pageSize);
|
||||
watchListRouteState(route, pluginToolListRouteSchema, (state) => {
|
||||
const nextPluginId = String(route.query.id || '');
|
||||
const pluginChanged = nextPluginId !== pluginId.value;
|
||||
pluginId.value = nextPluginId;
|
||||
toolSearchKeyword.value = state.keyword.trim();
|
||||
currentToolPageNumber.value = state.pageNumber;
|
||||
currentToolPageSize.value = state.pageSize;
|
||||
const restored = pluginToolRef.value?.restoreState?.(state);
|
||||
if (pluginChanged) {
|
||||
pluginInfo.value = {};
|
||||
if (!restored) pluginToolRef.value?.reload?.();
|
||||
void loadPluginInfo();
|
||||
}
|
||||
});
|
||||
|
||||
const headerButtons = computed<any[]>(() => {
|
||||
const buttons: any[] = [
|
||||
@@ -73,6 +91,15 @@ function currentToolListState(): PluginToolListRouteState {
|
||||
};
|
||||
}
|
||||
|
||||
function currentToolListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
currentToolListState(),
|
||||
pluginToolListRouteSchema,
|
||||
);
|
||||
}
|
||||
|
||||
function syncToolListRouteState() {
|
||||
const target = {
|
||||
path: '/ai/plugin/tools',
|
||||
@@ -100,12 +127,12 @@ function handleSearch(params: string) {
|
||||
function handleButtonClick(event: any) {
|
||||
switch (event.key) {
|
||||
case 'back': {
|
||||
void router.replace({
|
||||
path: '/ai/plugin',
|
||||
query: buildPluginListRouteQuery(
|
||||
parsePluginToolsReturnState(route.query),
|
||||
),
|
||||
});
|
||||
void navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
['/ai/plugin'],
|
||||
'/ai/plugin',
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'createTool': {
|
||||
@@ -133,6 +160,7 @@ function handleButtonClick(event: any) {
|
||||
:initial-keyword="initialToolListState.keyword"
|
||||
:initial-page-number="initialToolListState.pageNumber"
|
||||
:initial-page-size="initialToolListState.pageSize"
|
||||
:return-to="currentToolListFullPath()"
|
||||
@state-change="handleToolPageStateChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,11 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildPluginListRouteQuery,
|
||||
buildPluginToolsReturnQuery,
|
||||
buildPluginToolsRouteQueryFromEdit,
|
||||
buildPluginToolListRouteQuery,
|
||||
mergePluginListRouteQuery,
|
||||
mergePluginToolListRouteQuery,
|
||||
parsePluginListRouteState,
|
||||
parsePluginToolListRouteState,
|
||||
parsePluginToolsReturnState,
|
||||
} from './plugin-route-state';
|
||||
|
||||
describe('plugin route state', () => {
|
||||
@@ -28,22 +26,13 @@ describe('plugin route state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('round trips the plugin list state through plugin tools', () => {
|
||||
it('serializes plugin list state into canonical list query', () => {
|
||||
const state = {
|
||||
categoryId: '7',
|
||||
keyword: '天气',
|
||||
pageNumber: 3,
|
||||
pageSize: 24,
|
||||
};
|
||||
const returnQuery = buildPluginToolsReturnQuery(state);
|
||||
|
||||
expect(returnQuery).toEqual({
|
||||
returnCategoryId: '7',
|
||||
returnKeyword: '天气',
|
||||
returnPageNumber: '3',
|
||||
returnPageSize: '24',
|
||||
});
|
||||
expect(parsePluginToolsReturnState(returnQuery)).toEqual(state);
|
||||
expect(buildPluginListRouteQuery(state)).toEqual({
|
||||
categoryId: '7',
|
||||
keyword: '天气',
|
||||
@@ -66,8 +55,8 @@ describe('plugin route state', () => {
|
||||
});
|
||||
expect(
|
||||
parsePluginToolListRouteState({
|
||||
toolPageNumber: '0',
|
||||
toolPageSize: '999',
|
||||
pageNumber: '0',
|
||||
pageSize: '999',
|
||||
}),
|
||||
).toEqual({
|
||||
keyword: '',
|
||||
@@ -99,9 +88,8 @@ describe('plugin route state', () => {
|
||||
mergePluginToolListRouteQuery(
|
||||
{
|
||||
id: '88',
|
||||
returnCategoryId: '7',
|
||||
returnPageNumber: '3',
|
||||
toolKeyword: '旧工具',
|
||||
returnTo: '/ai/plugin?categoryId=7&pageNumber=3',
|
||||
keyword: '旧工具',
|
||||
},
|
||||
{
|
||||
keyword: '查询工具',
|
||||
@@ -111,29 +99,24 @@ describe('plugin route state', () => {
|
||||
),
|
||||
).toEqual({
|
||||
id: '88',
|
||||
returnCategoryId: '7',
|
||||
returnPageNumber: '3',
|
||||
toolKeyword: '查询工具',
|
||||
toolPageNumber: '2',
|
||||
toolPageSize: '20',
|
||||
keyword: '查询工具',
|
||||
pageNumber: '2',
|
||||
pageSize: '20',
|
||||
returnTo: '/ai/plugin?categoryId=7&pageNumber=3',
|
||||
});
|
||||
});
|
||||
|
||||
it('restores plugin tools query after editing a tool', () => {
|
||||
it('serializes plugin tool list state with common query keys', () => {
|
||||
expect(
|
||||
buildPluginToolsRouteQueryFromEdit(
|
||||
{
|
||||
id: 'tool-1',
|
||||
pluginId: 'plugin-1',
|
||||
returnCategoryId: '7',
|
||||
toolPageNumber: '2',
|
||||
},
|
||||
'plugin-1',
|
||||
),
|
||||
buildPluginToolListRouteQuery({
|
||||
keyword: '查询工具',
|
||||
pageNumber: 2,
|
||||
pageSize: 20,
|
||||
}),
|
||||
).toEqual({
|
||||
id: 'plugin-1',
|
||||
returnCategoryId: '7',
|
||||
toolPageNumber: '2',
|
||||
keyword: '查询工具',
|
||||
pageNumber: '2',
|
||||
pageSize: '20',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
import type { LocationQuery } from 'vue-router';
|
||||
import type { LocationQuery, LocationQueryRaw } from 'vue-router';
|
||||
|
||||
const DEFAULT_PLUGIN_PAGE_NUMBER = 1;
|
||||
const DEFAULT_PLUGIN_PAGE_SIZE = 12;
|
||||
const DEFAULT_PLUGIN_CATEGORY_ID = '0';
|
||||
const PLUGIN_PAGE_SIZES = new Set([12, 24, 36, 48]);
|
||||
|
||||
const DEFAULT_TOOL_PAGE_NUMBER = 1;
|
||||
const DEFAULT_TOOL_PAGE_SIZE = 10;
|
||||
const TOOL_PAGE_SIZES = new Set([10, 20, 50, 100]);
|
||||
|
||||
const PLUGIN_LIST_QUERY_KEYS = {
|
||||
categoryId: 'categoryId',
|
||||
keyword: 'keyword',
|
||||
pageNumber: 'pageNumber',
|
||||
pageSize: 'pageSize',
|
||||
} as const;
|
||||
|
||||
const PLUGIN_RETURN_QUERY_KEYS = {
|
||||
categoryId: 'returnCategoryId',
|
||||
keyword: 'returnKeyword',
|
||||
pageNumber: 'returnPageNumber',
|
||||
pageSize: 'returnPageSize',
|
||||
} as const;
|
||||
|
||||
const TOOL_LIST_QUERY_KEYS = {
|
||||
keyword: 'toolKeyword',
|
||||
pageNumber: 'toolPageNumber',
|
||||
pageSize: 'toolPageSize',
|
||||
} as const;
|
||||
import {
|
||||
buildListRouteStateQuery,
|
||||
defineListRouteStateSchema,
|
||||
mergeListRouteStateQuery,
|
||||
parseListRouteState,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface PluginListRouteState {
|
||||
categoryId: string;
|
||||
@@ -42,156 +22,74 @@ interface PluginToolListRouteState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
function readQueryValue(query: LocationQuery, key: string): string {
|
||||
const value = query[key];
|
||||
const normalized = Array.isArray(value) ? value[0] : value;
|
||||
return normalized === null || normalized === undefined
|
||||
? ''
|
||||
: String(normalized);
|
||||
const pluginListRouteSchema = defineListRouteStateSchema<PluginListRouteState>({
|
||||
path: '/ai/plugin',
|
||||
fields: {
|
||||
categoryId: stringListRouteField({ defaultValue: '0' }),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 24, 36, 48],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const pluginToolListRouteSchema =
|
||||
defineListRouteStateSchema<PluginToolListRouteState>({
|
||||
path: '/ai/plugin/tools',
|
||||
fields: {
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [10, 20, 50, 100],
|
||||
defaultValue: 10,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
function parsePluginListRouteState(query: LocationQuery) {
|
||||
return parseListRouteState(query, pluginListRouteSchema);
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: string, fallback: number): number {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parsePluginState(
|
||||
query: LocationQuery,
|
||||
keys: typeof PLUGIN_LIST_QUERY_KEYS | typeof PLUGIN_RETURN_QUERY_KEYS,
|
||||
): PluginListRouteState {
|
||||
const pageSize = parsePositiveInteger(
|
||||
readQueryValue(query, keys.pageSize),
|
||||
DEFAULT_PLUGIN_PAGE_SIZE,
|
||||
);
|
||||
return {
|
||||
categoryId:
|
||||
readQueryValue(query, keys.categoryId) || DEFAULT_PLUGIN_CATEGORY_ID,
|
||||
keyword: readQueryValue(query, keys.keyword),
|
||||
pageNumber: parsePositiveInteger(
|
||||
readQueryValue(query, keys.pageNumber),
|
||||
DEFAULT_PLUGIN_PAGE_NUMBER,
|
||||
),
|
||||
pageSize: PLUGIN_PAGE_SIZES.has(pageSize)
|
||||
? pageSize
|
||||
: DEFAULT_PLUGIN_PAGE_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPluginStateQuery(
|
||||
function buildPluginListRouteQuery(
|
||||
state: PluginListRouteState,
|
||||
keys: typeof PLUGIN_LIST_QUERY_KEYS | typeof PLUGIN_RETURN_QUERY_KEYS,
|
||||
): LocationQuery {
|
||||
return {
|
||||
...(state.pageNumber === DEFAULT_PLUGIN_PAGE_NUMBER
|
||||
? {}
|
||||
: { [keys.pageNumber]: String(state.pageNumber) }),
|
||||
...(state.pageSize === DEFAULT_PLUGIN_PAGE_SIZE
|
||||
? {}
|
||||
: { [keys.pageSize]: String(state.pageSize) }),
|
||||
...(state.categoryId === DEFAULT_PLUGIN_CATEGORY_ID
|
||||
? {}
|
||||
: { [keys.categoryId]: state.categoryId }),
|
||||
...(state.keyword ? { [keys.keyword]: state.keyword } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parsePluginListRouteState(query: LocationQuery): PluginListRouteState {
|
||||
return parsePluginState(query, PLUGIN_LIST_QUERY_KEYS);
|
||||
}
|
||||
|
||||
function parsePluginToolsReturnState(
|
||||
query: LocationQuery,
|
||||
): PluginListRouteState {
|
||||
return parsePluginState(query, PLUGIN_RETURN_QUERY_KEYS);
|
||||
}
|
||||
|
||||
function buildPluginListRouteQuery(state: PluginListRouteState): LocationQuery {
|
||||
return buildPluginStateQuery(state, PLUGIN_LIST_QUERY_KEYS);
|
||||
}
|
||||
|
||||
function buildPluginToolsReturnQuery(
|
||||
state: PluginListRouteState,
|
||||
): LocationQuery {
|
||||
return buildPluginStateQuery(state, PLUGIN_RETURN_QUERY_KEYS);
|
||||
): LocationQueryRaw {
|
||||
return buildListRouteStateQuery(state, pluginListRouteSchema);
|
||||
}
|
||||
|
||||
function mergePluginListRouteQuery(
|
||||
query: LocationQuery,
|
||||
state: PluginListRouteState,
|
||||
): LocationQuery {
|
||||
const listKeys = new Set<string>(Object.values(PLUGIN_LIST_QUERY_KEYS));
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(query).filter(([key]) => !listKeys.has(key)),
|
||||
),
|
||||
...buildPluginListRouteQuery(state),
|
||||
};
|
||||
): LocationQueryRaw {
|
||||
return mergeListRouteStateQuery(query, state, pluginListRouteSchema);
|
||||
}
|
||||
|
||||
function parsePluginToolListRouteState(
|
||||
query: LocationQuery,
|
||||
): PluginToolListRouteState {
|
||||
const pageSize = parsePositiveInteger(
|
||||
readQueryValue(query, TOOL_LIST_QUERY_KEYS.pageSize),
|
||||
DEFAULT_TOOL_PAGE_SIZE,
|
||||
);
|
||||
return {
|
||||
keyword: readQueryValue(query, TOOL_LIST_QUERY_KEYS.keyword),
|
||||
pageNumber: parsePositiveInteger(
|
||||
readQueryValue(query, TOOL_LIST_QUERY_KEYS.pageNumber),
|
||||
DEFAULT_TOOL_PAGE_NUMBER,
|
||||
),
|
||||
pageSize: TOOL_PAGE_SIZES.has(pageSize) ? pageSize : DEFAULT_TOOL_PAGE_SIZE,
|
||||
};
|
||||
function parsePluginToolListRouteState(query: LocationQuery) {
|
||||
return parseListRouteState(query, pluginToolListRouteSchema);
|
||||
}
|
||||
|
||||
function buildPluginToolListRouteQuery(
|
||||
state: PluginToolListRouteState,
|
||||
): LocationQuery {
|
||||
return {
|
||||
...(state.pageNumber === DEFAULT_TOOL_PAGE_NUMBER
|
||||
? {}
|
||||
: { [TOOL_LIST_QUERY_KEYS.pageNumber]: String(state.pageNumber) }),
|
||||
...(state.pageSize === DEFAULT_TOOL_PAGE_SIZE
|
||||
? {}
|
||||
: { [TOOL_LIST_QUERY_KEYS.pageSize]: String(state.pageSize) }),
|
||||
...(state.keyword ? { [TOOL_LIST_QUERY_KEYS.keyword]: state.keyword } : {}),
|
||||
};
|
||||
): LocationQueryRaw {
|
||||
return buildListRouteStateQuery(state, pluginToolListRouteSchema);
|
||||
}
|
||||
|
||||
function mergePluginToolListRouteQuery(
|
||||
query: LocationQuery,
|
||||
state: PluginToolListRouteState,
|
||||
): LocationQuery {
|
||||
const listKeys = new Set<string>(Object.values(TOOL_LIST_QUERY_KEYS));
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(query).filter(([key]) => !listKeys.has(key)),
|
||||
),
|
||||
...buildPluginToolListRouteQuery(state),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPluginToolsRouteQueryFromEdit(
|
||||
query: LocationQuery,
|
||||
pluginId: string,
|
||||
): LocationQuery {
|
||||
const nextQuery: LocationQuery = { ...query, id: pluginId };
|
||||
delete nextQuery.pluginId;
|
||||
return nextQuery;
|
||||
): LocationQueryRaw {
|
||||
return mergeListRouteStateQuery(query, state, pluginToolListRouteSchema);
|
||||
}
|
||||
|
||||
export {
|
||||
buildPluginListRouteQuery,
|
||||
buildPluginToolsReturnQuery,
|
||||
buildPluginToolsRouteQueryFromEdit,
|
||||
buildPluginToolListRouteQuery,
|
||||
mergePluginListRouteQuery,
|
||||
mergePluginToolListRouteQuery,
|
||||
parsePluginListRouteState,
|
||||
parsePluginToolListRouteState,
|
||||
parsePluginToolsReturnState,
|
||||
pluginListRouteSchema,
|
||||
pluginToolListRouteSchema,
|
||||
};
|
||||
export type { PluginListRouteState, PluginToolListRouteState };
|
||||
|
||||
@@ -37,6 +37,10 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import {
|
||||
navigateBackToList,
|
||||
resolveListReturnPath,
|
||||
} from '#/router/list-return-context';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
@@ -211,15 +215,21 @@ async function confirmUnsavedNavigation() {
|
||||
onBeforeRouteLeave(confirmUnsavedNavigation);
|
||||
onBeforeRouteUpdate(confirmUnsavedNavigation);
|
||||
|
||||
async function replaceRouteDuringOperation(path: string) {
|
||||
async function returnToSkillListDuringOperation() {
|
||||
allowOperationNavigation = true;
|
||||
try {
|
||||
await router.replace(path);
|
||||
await router.replace(
|
||||
resolveListReturnPath(router, route.query, ['/ai/skill'], '/ai/skill'),
|
||||
);
|
||||
} finally {
|
||||
allowOperationNavigation = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function backToSkillList() {
|
||||
await navigateBackToList(router, route.query, ['/ai/skill'], '/ai/skill');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const request = ++initRequest;
|
||||
loading.value = true;
|
||||
@@ -227,7 +237,7 @@ async function init() {
|
||||
loadAccessDenied.value = false;
|
||||
try {
|
||||
if (isNew.value) {
|
||||
await replaceRouteDuringOperation('/ai/skill');
|
||||
await returnToSkillListDuringOperation();
|
||||
return;
|
||||
}
|
||||
await loadCategories(request);
|
||||
@@ -355,9 +365,8 @@ async function saveFiles(allowLifecycleAction = false, showFeedback = true) {
|
||||
}
|
||||
|
||||
async function flushPendingChanges(allowLifecycleAction = false) {
|
||||
if (resourceDirty.value) {
|
||||
if (!(await saveFiles(allowLifecycleAction, false))) return false;
|
||||
}
|
||||
if (resourceDirty.value && !(await saveFiles(allowLifecycleAction, false)))
|
||||
return false;
|
||||
const panel = capabilityPanelRef.value;
|
||||
if (capabilityDirty.value || panel?.hasDirty()) {
|
||||
if (!canBindCapabilities.value || !panel) {
|
||||
@@ -524,7 +533,7 @@ async function remove() {
|
||||
if (res.errorCode === 0) {
|
||||
if (res.data === null || res.data === undefined) {
|
||||
ElMessage.success(res.message || 'Skill 已删除');
|
||||
await replaceRouteDuringOperation('/ai/skill');
|
||||
await returnToSkillListDuringOperation();
|
||||
} else {
|
||||
ElMessage.success(res.message || '已提交删除审批');
|
||||
await init();
|
||||
@@ -561,7 +570,7 @@ function handleSaveShortcut(event: KeyboardEvent) {
|
||||
text
|
||||
:disabled="operationLocked"
|
||||
aria-label="返回 Skill 列表"
|
||||
@click="router.push('/ai/skill')"
|
||||
@click="backToSkillList"
|
||||
/>
|
||||
<div class="skill-detail-page__title">
|
||||
<div>
|
||||
@@ -674,7 +683,7 @@ function handleSaveShortcut(event: KeyboardEvent) {
|
||||
<template #extra>
|
||||
<ElButton
|
||||
:type="loadAccessDenied ? 'primary' : 'default'"
|
||||
@click="router.push('/ai/skill')"
|
||||
@click="backToSkillList"
|
||||
>
|
||||
返回 Skill 列表
|
||||
</ElButton>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
reactive,
|
||||
ref,
|
||||
} from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { downloadFileFromBlob, formatDate } from '@easyflow/utils';
|
||||
|
||||
@@ -61,7 +61,14 @@ import { hasPermission } from '#/api/common/hasPermission';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
buildListRouteFullPath,
|
||||
mergeListRouteStateQuery,
|
||||
parseListRouteState,
|
||||
watchListRouteState,
|
||||
} from '#/composables/useListRouteState';
|
||||
import { $t } from '#/locales';
|
||||
import { withListReturnTo } from '#/router/list-return-context';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
@@ -95,19 +102,43 @@ import {
|
||||
resolveSkillImportConflictReasonLabel,
|
||||
resolveSkillImportStep,
|
||||
} from './skill-import';
|
||||
import { skillListRouteSchema } from './skill-list-route-state';
|
||||
import SkillCategoryFormDialog from './SkillCategoryFormDialog.vue';
|
||||
import SkillCreateDialog from './SkillCreateDialog.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const initialListState = parseListRouteState(route.query, skillListRouteSchema);
|
||||
const pageDataRef = ref<any>();
|
||||
const headerSearchRef = ref<{ reset: () => void }>();
|
||||
const importInputRef = ref<HTMLInputElement>();
|
||||
const categories = ref<SkillCategory[]>([]);
|
||||
const categoryLoading = ref(false);
|
||||
const selectedRows = ref<SkillInfo[]>([]);
|
||||
const selectedCategoryId = ref<number | string>('');
|
||||
const selectedCategoryId = ref<number | string>(initialListState.categoryId);
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
keyword: initialListState.keyword,
|
||||
});
|
||||
const currentPageNumber = ref(initialListState.pageNumber);
|
||||
const currentPageSize = ref(initialListState.pageSize);
|
||||
let initialCategoryChangePending = Boolean(initialListState.categoryId);
|
||||
watchListRouteState(route, skillListRouteSchema, (state) => {
|
||||
if (String(state.categoryId) !== String(selectedCategoryId.value)) {
|
||||
initialCategoryChangePending = Boolean(state.categoryId);
|
||||
}
|
||||
selectedCategoryId.value = state.categoryId;
|
||||
filters.keyword = state.keyword;
|
||||
currentPageNumber.value = state.pageNumber;
|
||||
currentPageSize.value = state.pageSize;
|
||||
selectedRows.value = [];
|
||||
pageDataRef.value?.restoreState?.({
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: {
|
||||
categoryId: state.categoryId || undefined,
|
||||
displayName: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const importDialogOpen = ref(false);
|
||||
@@ -311,6 +342,16 @@ async function loadCategories() {
|
||||
const res = await getSkillCategories();
|
||||
if (res.errorCode !== 0) throw new Error(res.message);
|
||||
categories.value = res.data || [];
|
||||
if (
|
||||
selectedCategoryId.value &&
|
||||
!flatCategories.value.some(
|
||||
(category) => String(category.id) === String(selectedCategoryId.value),
|
||||
)
|
||||
) {
|
||||
selectedCategoryId.value = '';
|
||||
initialCategoryChangePending = false;
|
||||
applyFilters();
|
||||
}
|
||||
} catch (error) {
|
||||
categories.value = [];
|
||||
ElMessage.error(
|
||||
@@ -362,10 +403,53 @@ function selectedTargetCategory() {
|
||||
}
|
||||
|
||||
function selectCategory(data?: SkillCategory) {
|
||||
selectedCategoryId.value = data?.id || '';
|
||||
const categoryId = data?.id || '';
|
||||
if (
|
||||
initialCategoryChangePending &&
|
||||
String(categoryId) === String(selectedCategoryId.value)
|
||||
) {
|
||||
initialCategoryChangePending = false;
|
||||
return;
|
||||
}
|
||||
initialCategoryChangePending = false;
|
||||
selectedCategoryId.value = categoryId;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
{
|
||||
categoryId: String(selectedCategoryId.value || ''),
|
||||
keyword: filters.keyword,
|
||||
pageNumber: currentPageNumber.value,
|
||||
pageSize: currentPageSize.value,
|
||||
},
|
||||
skillListRouteSchema,
|
||||
);
|
||||
}
|
||||
|
||||
function handlePageStateChange(state: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
currentPageNumber.value = state.pageNumber;
|
||||
currentPageSize.value = state.pageSize;
|
||||
const query = mergeListRouteStateQuery(
|
||||
route.query,
|
||||
{
|
||||
categoryId: String(selectedCategoryId.value || ''),
|
||||
keyword: filters.keyword,
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
},
|
||||
skillListRouteSchema,
|
||||
);
|
||||
const target = router.resolve({ path: skillListRouteSchema.path, query });
|
||||
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||
}
|
||||
|
||||
function formatModified(value?: string) {
|
||||
return value ? formatDate(value, 'YYYY-MM-DD HH:mm') : '—';
|
||||
}
|
||||
@@ -388,6 +472,7 @@ function openDetail(row: SkillInfo) {
|
||||
query: {
|
||||
navTitle: row.displayName || row.name || 'Skill 详情',
|
||||
pageKey: '/ai/skill',
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -402,6 +487,7 @@ function handleSkillCreated(payload: {
|
||||
...(payload.intent === 'PUBLISH' ? { publishIntent: '1' } : {}),
|
||||
navTitle: payload.skill.displayName || payload.skill.name || 'Skill 详情',
|
||||
pageKey: '/ai/skill',
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1045,6 +1131,7 @@ function isRowBusy(row: SkillInfo) {
|
||||
<HeaderSearch
|
||||
ref="headerSearchRef"
|
||||
:buttons="headerButtons"
|
||||
:initial-value="filters.keyword"
|
||||
search-placeholder="请输入 Skill 名称或描述"
|
||||
@search="handleHeaderSearch"
|
||||
@button-click="handleHeaderButtonClick"
|
||||
@@ -1094,8 +1181,14 @@ function isRowBusy(row: SkillInfo) {
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/skill/page"
|
||||
:page-size="12"
|
||||
:page-size="initialListState.pageSize"
|
||||
:page-sizes="[12, 24, 48]"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
displayName: initialListState.keyword || undefined,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
defineListRouteStateSchema,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface SkillListRouteState {
|
||||
categoryId: string;
|
||||
keyword: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const skillListRouteSchema = defineListRouteStateSchema<SkillListRouteState>({
|
||||
path: '/ai/skill',
|
||||
fields: {
|
||||
categoryId: stringListRouteField(),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 24, 48],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export { skillListRouteSchema };
|
||||
export type { SkillListRouteState };
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ref,
|
||||
shallowRef,
|
||||
} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import {usePreferences} from '@easyflow/preferences';
|
||||
import {getOptions, sortNodes} from '@easyflow/utils';
|
||||
@@ -22,6 +22,7 @@ import {api} from '#/api/request';
|
||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||
import {$t} from '#/locales';
|
||||
import {router} from '#/router';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import {
|
||||
resolveWorkflowShareFailureReason,
|
||||
resolveWorkflowShareWorkflowId,
|
||||
@@ -51,11 +52,6 @@ import {
|
||||
isWorkflowDataEmpty,
|
||||
normalizeWorkflowStartNodes,
|
||||
} from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
||||
import {
|
||||
buildWorkflowListRouteQuery,
|
||||
parseWorkflowDesignReturnState,
|
||||
} from './workflow-list-route-state';
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const WORKFLOW_VISIBLE_RENDER_NODE_THRESHOLD = 40;
|
||||
@@ -161,12 +157,13 @@ async function initializeWorkflow() {
|
||||
}
|
||||
}
|
||||
|
||||
function backToWorkflowList() {
|
||||
const listState = parseWorkflowDesignReturnState(route.query);
|
||||
router.replace({
|
||||
path: '/ai/workflow',
|
||||
query: buildWorkflowListRouteQuery(listState),
|
||||
});
|
||||
async function backToWorkflowList() {
|
||||
await navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
['/ai/workflow'],
|
||||
'/ai/workflow',
|
||||
);
|
||||
}
|
||||
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
||||
let draftWriteTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
@@ -60,8 +60,13 @@ import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
buildListRouteFullPath,
|
||||
watchListRouteState,
|
||||
} from '#/composables/useListRouteState';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { withListReturnTo } from '#/router/list-return-context';
|
||||
import { useDictStore } from '#/store';
|
||||
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||
@@ -79,9 +84,9 @@ import {
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import {
|
||||
buildWorkflowDesignReturnQuery,
|
||||
mergeWorkflowListRouteQuery,
|
||||
parseWorkflowListRouteState,
|
||||
workflowListRouteSchema,
|
||||
} from './workflow-list-route-state';
|
||||
|
||||
const ElXMarkdown = defineAsyncComponent(
|
||||
@@ -193,6 +198,7 @@ const actions: ActionButton[] = [
|
||||
name: 'RunPage',
|
||||
query: {
|
||||
id: row.id,
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -219,6 +225,7 @@ const actions: ActionButton[] = [
|
||||
name: 'ExecRecord',
|
||||
query: {
|
||||
workflowId: row.id,
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -305,6 +312,23 @@ const initialQueryParams = {
|
||||
categoryId: initialListState.categoryId || undefined,
|
||||
title: initialListState.keyword || undefined,
|
||||
};
|
||||
watchListRouteState(route, workflowListRouteSchema, (state) => {
|
||||
if (String(state.categoryId) !== String(selectedCategoryId.value)) {
|
||||
suppressInitialCategoryChange.value = 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,
|
||||
title: state.keyword || undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
const headerButtons = [
|
||||
{
|
||||
@@ -389,6 +413,14 @@ function currentListState(): WorkflowListRouteState {
|
||||
pageSize: pageState?.pageSize ?? currentPageSize.value,
|
||||
};
|
||||
}
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
route.query,
|
||||
currentListState(),
|
||||
workflowListRouteSchema,
|
||||
);
|
||||
}
|
||||
function syncListRouteState() {
|
||||
const target = {
|
||||
path: '/ai/workflow',
|
||||
@@ -1134,7 +1166,7 @@ function toDesignPage(row: any) {
|
||||
id: row.id,
|
||||
pageKey: '/ai/workflow',
|
||||
navTitle: resolveNavTitle(row),
|
||||
...buildWorkflowDesignReturnQuery(currentListState()),
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1258,7 +1290,7 @@ function changeCategory(category: any) {
|
||||
const categoryId = category?.id ?? '';
|
||||
if (
|
||||
suppressInitialCategoryChange.value &&
|
||||
String(categoryId) === initialListState.categoryId
|
||||
String(categoryId) === String(selectedCategoryId.value)
|
||||
) {
|
||||
suppressInitialCategoryChange.value = false;
|
||||
selectedCategoryId.value = categoryId;
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { api, SseClient } from '#/api/request';
|
||||
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
||||
import { router } from '#/router';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
|
||||
import { resolveWorkflowShareFailureReason } from '#/utils/workflow-share-context';
|
||||
@@ -266,6 +267,15 @@ function initializeAdditionalValues() {
|
||||
extraSubmitting.value = false;
|
||||
}
|
||||
|
||||
async function backToWorkflowList() {
|
||||
await navigateBackToList(
|
||||
router,
|
||||
route.query,
|
||||
['/ai/workflow'],
|
||||
'/ai/workflow',
|
||||
);
|
||||
}
|
||||
|
||||
async function submitAdditionalInputs() {
|
||||
if (extraSubmitted.value) {
|
||||
return true;
|
||||
@@ -865,7 +875,7 @@ function executionTraceText(
|
||||
circle
|
||||
text
|
||||
aria-label="返回工作流"
|
||||
@click="router.replace({ path: '/ai/workflow' })"
|
||||
@click="backToWorkflowList"
|
||||
/>
|
||||
<ElAvatar
|
||||
:size="40"
|
||||
|
||||
@@ -22,18 +22,53 @@ import {
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import {
|
||||
buildListRouteFullPath,
|
||||
mergeListRouteStateQuery,
|
||||
parseListRouteState,
|
||||
watchListRouteState,
|
||||
} from '#/composables/useListRouteState';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
navigateBackToList,
|
||||
withListReturnTo,
|
||||
} from '#/router/list-return-context';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import { workflowExecListRouteSchema } from './workflow-exec-list-route-state';
|
||||
|
||||
const router = useRouter();
|
||||
const $route = useRoute();
|
||||
const initialListState = parseListRouteState(
|
||||
$route.query,
|
||||
workflowExecListRouteSchema,
|
||||
);
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const pageDataRef = ref();
|
||||
const formInline = ref({
|
||||
execKey: '',
|
||||
execKey: initialListState.execKey,
|
||||
});
|
||||
const appliedExecKey = ref(initialListState.execKey);
|
||||
const currentPageNumber = ref(initialListState.pageNumber);
|
||||
const currentPageSize = ref(initialListState.pageSize);
|
||||
let currentWorkflowId = String($route.query.workflowId || '');
|
||||
watchListRouteState($route, workflowExecListRouteSchema, (state) => {
|
||||
const nextWorkflowId = String($route.query.workflowId || '');
|
||||
const workflowChanged = nextWorkflowId !== currentWorkflowId;
|
||||
currentWorkflowId = nextWorkflowId;
|
||||
formInline.value.execKey = state.execKey;
|
||||
appliedExecKey.value = state.execKey;
|
||||
currentPageNumber.value = state.pageNumber;
|
||||
currentPageSize.value = state.pageSize;
|
||||
const restored = pageDataRef.value?.restoreState?.({
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
queryParams: { execKey: state.execKey || undefined },
|
||||
});
|
||||
if (workflowChanged && !restored) pageDataRef.value?.reload?.();
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
function initDict() {
|
||||
@@ -42,12 +77,17 @@ function initDict() {
|
||||
function search(formEl: FormInstance | undefined) {
|
||||
formEl?.validate((valid) => {
|
||||
if (valid) {
|
||||
pageDataRef.value.setQuery(formInline.value);
|
||||
appliedExecKey.value = formInline.value.execKey;
|
||||
pageDataRef.value.setQuery({
|
||||
execKey: appliedExecKey.value || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
formInline.value.execKey = '';
|
||||
appliedExecKey.value = '';
|
||||
formEl?.clearValidate();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function remove(row: any) {
|
||||
@@ -64,7 +104,7 @@ function remove(row: any) {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset(formRef.value);
|
||||
pageDataRef.value?.reload?.();
|
||||
done();
|
||||
}
|
||||
})
|
||||
@@ -83,9 +123,51 @@ function toStepPage(row: any) {
|
||||
query: {
|
||||
recordId: row.id,
|
||||
workflowId: $route.query.workflowId,
|
||||
...withListReturnTo(currentListFullPath()),
|
||||
},
|
||||
});
|
||||
}
|
||||
function currentListFullPath() {
|
||||
return buildListRouteFullPath(
|
||||
router,
|
||||
$route.query,
|
||||
{
|
||||
execKey: appliedExecKey.value,
|
||||
pageNumber: currentPageNumber.value,
|
||||
pageSize: currentPageSize.value,
|
||||
},
|
||||
workflowExecListRouteSchema,
|
||||
);
|
||||
}
|
||||
function handlePageStateChange(state: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
currentPageNumber.value = state.pageNumber;
|
||||
currentPageSize.value = state.pageSize;
|
||||
const query = mergeListRouteStateQuery(
|
||||
$route.query,
|
||||
{
|
||||
execKey: appliedExecKey.value,
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
},
|
||||
workflowExecListRouteSchema,
|
||||
);
|
||||
const target = router.resolve({
|
||||
path: workflowExecListRouteSchema.path,
|
||||
query,
|
||||
});
|
||||
if (target.fullPath !== $route.fullPath) void router.replace(target);
|
||||
}
|
||||
async function backToWorkflowList() {
|
||||
await navigateBackToList(
|
||||
router,
|
||||
$route.query,
|
||||
['/ai/workflow'],
|
||||
'/ai/workflow',
|
||||
);
|
||||
}
|
||||
function getTagType(row: any) {
|
||||
switch (row.status) {
|
||||
case 1: {
|
||||
@@ -113,10 +195,7 @@ function getTagType(row: any) {
|
||||
<template>
|
||||
<div class="page-container border-border border">
|
||||
<div class="mb-3">
|
||||
<ElButton
|
||||
:icon="ArrowLeft"
|
||||
@click="router.replace({ path: '/ai/workflow' })"
|
||||
>
|
||||
<ElButton :icon="ArrowLeft" @click="backToWorkflowList">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -140,10 +219,15 @@ function getTagType(row: any) {
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/workflowExecResult/page"
|
||||
:page-size="10"
|
||||
:page-size="initialListState.pageSize"
|
||||
:initial-page-number="initialListState.pageNumber"
|
||||
:initial-query-params="{
|
||||
execKey: initialListState.execKey || undefined,
|
||||
}"
|
||||
:extra-query-params="{
|
||||
workflowId: $route.query.workflowId,
|
||||
}"
|
||||
@state-change="handlePageStateChange"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { navigateBackToList } from '#/router/list-return-context';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -44,12 +45,18 @@ function reset(formEl: FormInstance | undefined) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function backToExecRecords() {
|
||||
async function backToExecRecords() {
|
||||
const workflowId = $route.query.workflowId;
|
||||
void router.replace({
|
||||
const fallbackPath = router.resolve({
|
||||
name: 'ExecRecord',
|
||||
query: workflowId ? { workflowId } : {},
|
||||
});
|
||||
}).fullPath;
|
||||
await navigateBackToList(
|
||||
router,
|
||||
$route.query,
|
||||
['/ai/workflow/executeRecords'],
|
||||
fallbackPath,
|
||||
);
|
||||
}
|
||||
function getTagType(row: any) {
|
||||
switch (row.status) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
defineListRouteStateSchema,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface WorkflowExecListRouteState {
|
||||
execKey: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const workflowExecListRouteSchema =
|
||||
defineListRouteStateSchema<WorkflowExecListRouteState>({
|
||||
path: '/ai/workflow/executeRecords',
|
||||
fields: {
|
||||
execKey: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [10, 20, 50, 100],
|
||||
defaultValue: 10,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export { workflowExecListRouteSchema };
|
||||
export type { WorkflowExecListRouteState };
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildWorkflowDesignReturnQuery,
|
||||
buildWorkflowListRouteQuery,
|
||||
mergeWorkflowListRouteQuery,
|
||||
parseWorkflowDesignReturnState,
|
||||
parseWorkflowListRouteState,
|
||||
} from './workflow-list-route-state';
|
||||
|
||||
@@ -39,7 +37,7 @@ describe('workflow list route state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('round trips list state through workflow design return query', () => {
|
||||
it('serializes workflow list state into canonical list query', () => {
|
||||
const state = {
|
||||
categoryId: '7',
|
||||
keyword: '月报',
|
||||
@@ -47,15 +45,6 @@ describe('workflow list route state', () => {
|
||||
pageSize: 18,
|
||||
};
|
||||
|
||||
const designQuery = buildWorkflowDesignReturnQuery(state);
|
||||
|
||||
expect(designQuery).toEqual({
|
||||
returnCategoryId: '7',
|
||||
returnKeyword: '月报',
|
||||
returnPageNumber: '3',
|
||||
returnPageSize: '18',
|
||||
});
|
||||
expect(parseWorkflowDesignReturnState(designQuery)).toEqual(state);
|
||||
expect(buildWorkflowListRouteQuery(state)).toEqual({
|
||||
categoryId: '7',
|
||||
keyword: '月报',
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import type { LocationQuery } from 'vue-router';
|
||||
import type { LocationQuery, LocationQueryRaw } from 'vue-router';
|
||||
|
||||
const DEFAULT_PAGE_NUMBER = 1;
|
||||
const DEFAULT_PAGE_SIZE = 12;
|
||||
const WORKFLOW_PAGE_SIZES = new Set([12, 18, 24]);
|
||||
|
||||
const LIST_QUERY_KEYS = {
|
||||
categoryId: 'categoryId',
|
||||
keyword: 'keyword',
|
||||
pageNumber: 'pageNumber',
|
||||
pageSize: 'pageSize',
|
||||
} as const;
|
||||
|
||||
const RETURN_QUERY_KEYS = {
|
||||
categoryId: 'returnCategoryId',
|
||||
keyword: 'returnKeyword',
|
||||
pageNumber: 'returnPageNumber',
|
||||
pageSize: 'returnPageSize',
|
||||
} as const;
|
||||
|
||||
interface WorkflowListQueryKeys {
|
||||
readonly categoryId: string;
|
||||
readonly keyword: string;
|
||||
readonly pageNumber: string;
|
||||
readonly pageSize: string;
|
||||
}
|
||||
import {
|
||||
buildListRouteStateQuery,
|
||||
defineListRouteStateSchema,
|
||||
mergeListRouteStateQuery,
|
||||
parseListRouteState,
|
||||
positiveIntegerListRouteField,
|
||||
stringListRouteField,
|
||||
} from '#/composables/useListRouteState';
|
||||
|
||||
interface WorkflowListRouteState {
|
||||
categoryId: string;
|
||||
@@ -32,100 +16,43 @@ interface WorkflowListRouteState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
function readQueryValue(query: LocationQuery, key: string): string {
|
||||
const value = query[key];
|
||||
const normalized = Array.isArray(value) ? value[0] : value;
|
||||
return normalized === null || normalized === undefined
|
||||
? ''
|
||||
: String(normalized);
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: string, fallback: number): number {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseState(
|
||||
query: LocationQuery,
|
||||
keys: WorkflowListQueryKeys,
|
||||
): WorkflowListRouteState {
|
||||
const pageSize = parsePositiveInteger(
|
||||
readQueryValue(query, keys.pageSize),
|
||||
DEFAULT_PAGE_SIZE,
|
||||
);
|
||||
return {
|
||||
categoryId: readQueryValue(query, keys.categoryId),
|
||||
keyword: readQueryValue(query, keys.keyword),
|
||||
pageNumber: parsePositiveInteger(
|
||||
readQueryValue(query, keys.pageNumber),
|
||||
DEFAULT_PAGE_NUMBER,
|
||||
),
|
||||
pageSize: WORKFLOW_PAGE_SIZES.has(pageSize) ? pageSize : DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
function buildStateQuery(
|
||||
state: WorkflowListRouteState,
|
||||
keys: WorkflowListQueryKeys,
|
||||
): LocationQuery {
|
||||
return {
|
||||
...(state.pageNumber === DEFAULT_PAGE_NUMBER
|
||||
? {}
|
||||
: { [keys.pageNumber]: String(state.pageNumber) }),
|
||||
...(state.pageSize === DEFAULT_PAGE_SIZE
|
||||
? {}
|
||||
: { [keys.pageSize]: String(state.pageSize) }),
|
||||
...(state.categoryId ? { [keys.categoryId]: state.categoryId } : {}),
|
||||
...(state.keyword ? { [keys.keyword]: state.keyword } : {}),
|
||||
};
|
||||
}
|
||||
const workflowListRouteSchema =
|
||||
defineListRouteStateSchema<WorkflowListRouteState>({
|
||||
path: '/ai/workflow',
|
||||
fields: {
|
||||
categoryId: stringListRouteField(),
|
||||
keyword: stringListRouteField(),
|
||||
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||
pageSize: positiveIntegerListRouteField({
|
||||
allowedValues: [12, 18, 24],
|
||||
defaultValue: 12,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
function parseWorkflowListRouteState(
|
||||
query: LocationQuery,
|
||||
): WorkflowListRouteState {
|
||||
return parseState(query, LIST_QUERY_KEYS);
|
||||
}
|
||||
|
||||
function parseWorkflowDesignReturnState(
|
||||
query: LocationQuery,
|
||||
): WorkflowListRouteState {
|
||||
return parseState(query, RETURN_QUERY_KEYS);
|
||||
return parseListRouteState(query, workflowListRouteSchema);
|
||||
}
|
||||
|
||||
function buildWorkflowListRouteQuery(
|
||||
state: WorkflowListRouteState,
|
||||
): LocationQuery {
|
||||
return buildStateQuery(state, LIST_QUERY_KEYS);
|
||||
}
|
||||
|
||||
function buildWorkflowDesignReturnQuery(
|
||||
state: WorkflowListRouteState,
|
||||
): LocationQuery {
|
||||
return buildStateQuery(state, RETURN_QUERY_KEYS);
|
||||
): LocationQueryRaw {
|
||||
return buildListRouteStateQuery(state, workflowListRouteSchema);
|
||||
}
|
||||
|
||||
function mergeWorkflowListRouteQuery(
|
||||
query: LocationQuery,
|
||||
state: WorkflowListRouteState,
|
||||
): LocationQuery {
|
||||
const listQueryKeys = new Set<string>(Object.values(LIST_QUERY_KEYS));
|
||||
const nextQuery = Object.fromEntries(
|
||||
Object.entries(query).filter(([key]) => !listQueryKeys.has(key)),
|
||||
) as LocationQuery;
|
||||
return {
|
||||
...nextQuery,
|
||||
...buildWorkflowListRouteQuery(state),
|
||||
};
|
||||
): LocationQueryRaw {
|
||||
return mergeListRouteStateQuery(query, state, workflowListRouteSchema);
|
||||
}
|
||||
|
||||
export {
|
||||
buildWorkflowDesignReturnQuery,
|
||||
buildWorkflowListRouteQuery,
|
||||
mergeWorkflowListRouteQuery,
|
||||
parseWorkflowDesignReturnState,
|
||||
parseWorkflowListRouteState,
|
||||
workflowListRouteSchema,
|
||||
};
|
||||
export type { WorkflowListRouteState };
|
||||
|
||||
Reference in New Issue
Block a user