feat: 统一恢复管理端列表上下文
- 统一列表路由状态、分页初始化与安全返回契约 - 接入知识库、工作流、插件、审批、Skill、Agent、Bot、反馈和定时任务链路 - 补齐公共能力与关键返回路径自动化测试
This commit is contained in:
@@ -8,7 +8,7 @@ describe('page data recovery', () => {
|
|||||||
it('loads the restored page and query without requesting the first page', async () => {
|
it('loads the restored page and query without requesting the first page', async () => {
|
||||||
const get = vi
|
const get = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ data: { records: [], totalRow: 30 } });
|
.mockResolvedValue({ data: { records: [], totalRow: 50 } });
|
||||||
const wrapper = mount(PageData, {
|
const wrapper = mount(PageData, {
|
||||||
global: {
|
global: {
|
||||||
directives: { loading: {} },
|
directives: { loading: {} },
|
||||||
@@ -60,6 +60,37 @@ describe('page data recovery', () => {
|
|||||||
expect(wrapper.text()).not.toContain('数据加载失败,请重试');
|
expect(wrapper.text()).not.toContain('数据加载失败,请重试');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('contracts an out-of-range page to the latest page with one retry', async () => {
|
||||||
|
const get = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ data: { records: [], totalRow: 21 } })
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: { records: [{ id: 'last-record' }], totalRow: 21 },
|
||||||
|
});
|
||||||
|
const wrapper = mount(PageData, {
|
||||||
|
global: {
|
||||||
|
directives: { loading: {} },
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initialPageNumber: 4,
|
||||||
|
pageSize: 10,
|
||||||
|
pageUrl: '/page',
|
||||||
|
requestClient: { get },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledTimes(2);
|
||||||
|
expect(get.mock.calls.map((call) => call[1].params.pageNumber)).toEqual([
|
||||||
|
4, 3,
|
||||||
|
]);
|
||||||
|
expect((wrapper.vm as any).getPageState()).toEqual({
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('coalesces repeated reloads while a page request is still running', async () => {
|
it('coalesces repeated reloads while a page request is still running', async () => {
|
||||||
let resolveInitialRequest: (value: {
|
let resolveInitialRequest: (value: {
|
||||||
data: { records: never[]; totalRow: number };
|
data: { records: never[]; totalRow: number };
|
||||||
@@ -90,4 +121,85 @@ describe('page data recovery', () => {
|
|||||||
|
|
||||||
expect(get).toHaveBeenCalledTimes(2);
|
expect(get).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores a mounted list to a new route state with one target request', async () => {
|
||||||
|
const get = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: { records: [], totalRow: 100 } });
|
||||||
|
const wrapper = mount(PageData, {
|
||||||
|
global: {
|
||||||
|
directives: { loading: {} },
|
||||||
|
},
|
||||||
|
props: { pageUrl: '/page', requestClient: { get } },
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(wrapper.vm as any).restoreState({
|
||||||
|
pageNumber: 4,
|
||||||
|
pageSize: 20,
|
||||||
|
queryParams: { title: '月报' },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledTimes(2);
|
||||||
|
expect(get).toHaveBeenLastCalledWith('/page', {
|
||||||
|
params: { pageNumber: 4, pageSize: 20, title: '月报' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not request again when the restored state is unchanged', async () => {
|
||||||
|
const get = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: { records: [], totalRow: 50 } });
|
||||||
|
const wrapper = mount(PageData, {
|
||||||
|
global: {
|
||||||
|
directives: { loading: {} },
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initialPageNumber: 3,
|
||||||
|
initialQueryParams: { categoryId: undefined, title: '月报' },
|
||||||
|
pageSize: 20,
|
||||||
|
pageUrl: '/page',
|
||||||
|
requestClient: { get },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(wrapper.vm as any).restoreState({
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 20,
|
||||||
|
queryParams: { title: '月报' },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detaches query state from the caller reactive object', async () => {
|
||||||
|
const get = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: { records: [], totalRow: 0 } });
|
||||||
|
const wrapper = mount(PageData, {
|
||||||
|
global: {
|
||||||
|
directives: { loading: {} },
|
||||||
|
},
|
||||||
|
props: { pageUrl: '/page', requestClient: { get } },
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const query = { title: '月报' };
|
||||||
|
(wrapper.vm as any).setQuery(query);
|
||||||
|
await flushPromises();
|
||||||
|
query.title = '未提交草稿';
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledTimes(2);
|
||||||
|
expect(get).toHaveBeenLastCalledWith('/page', {
|
||||||
|
params: { pageNumber: 1, pageSize: 10, title: '月报' },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ interface PageDataState {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PageDataRestoreState extends PageDataState {
|
||||||
|
queryParams?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
interface PageDataReloadOptions {
|
interface PageDataReloadOptions {
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
}
|
}
|
||||||
@@ -49,7 +53,9 @@ const emit = defineEmits<{
|
|||||||
const pageList = ref<PageDataRow[]>([]);
|
const pageList = ref<PageDataRow[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadError = ref<unknown>();
|
const loadError = ref<unknown>();
|
||||||
const queryParams = ref<Record<string, any>>({ ...props.initialQueryParams });
|
const queryParams = ref<Record<string, any>>(
|
||||||
|
normalizeQueryParams(props.initialQueryParams),
|
||||||
|
);
|
||||||
let activePageRequest: null | Promise<void> = null;
|
let activePageRequest: null | Promise<void> = null;
|
||||||
let pendingPageRequest: null | PageDataRequest = null;
|
let pendingPageRequest: null | PageDataRequest = null;
|
||||||
let pageRequestVersion = 0;
|
let pageRequestVersion = 0;
|
||||||
@@ -80,8 +86,16 @@ const loadPageListOnce = async (request: PageDataRequest) => {
|
|||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
});
|
});
|
||||||
if (request.version === pageRequestVersion) {
|
if (request.version === pageRequestVersion) {
|
||||||
|
const rawTotal = Number(res.data?.totalRow || 0);
|
||||||
|
const total = Number.isFinite(rawTotal) ? Math.max(0, rawTotal) : 0;
|
||||||
|
const lastPage = Math.max(1, Math.ceil(total / pageInfo.pageSize));
|
||||||
|
pageInfo.total = total;
|
||||||
|
if (pageInfo.pageNumber > lastPage) {
|
||||||
|
pageInfo.pageNumber = lastPage;
|
||||||
|
emitPageState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
pageList.value = res.data?.records || [];
|
pageList.value = res.data?.records || [];
|
||||||
pageInfo.total = res.data?.totalRow || 0;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (request.version === pageRequestVersion) {
|
if (request.version === pageRequestVersion) {
|
||||||
@@ -151,6 +165,24 @@ const getPageState = (): PageDataState => ({
|
|||||||
pageSize: pageInfo.pageSize,
|
pageSize: pageInfo.pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function normalizeQueryParams(value: Record<string, any> = {}) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).filter(([, item]) => item !== undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryParamsEqual(
|
||||||
|
left: Record<string, any>,
|
||||||
|
right: Record<string, any>,
|
||||||
|
) {
|
||||||
|
const leftKeys = Object.keys(left);
|
||||||
|
const rightKeys = Object.keys(right);
|
||||||
|
return (
|
||||||
|
leftKeys.length === rightKeys.length &&
|
||||||
|
leftKeys.every((key) => Object.is(left[key], right[key]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const emitPageState = () => {
|
const emitPageState = () => {
|
||||||
emit('stateChange', getPageState());
|
emit('stateChange', getPageState());
|
||||||
};
|
};
|
||||||
@@ -178,15 +210,37 @@ const patchRowById = (
|
|||||||
const setQuery = (newQueryParams: Record<string, any>) => {
|
const setQuery = (newQueryParams: Record<string, any>) => {
|
||||||
pageInfo.pageNumber = 1;
|
pageInfo.pageNumber = 1;
|
||||||
pageInfo.pageSize = props.pageSize;
|
pageInfo.pageSize = props.pageSize;
|
||||||
queryParams.value = newQueryParams;
|
queryParams.value = normalizeQueryParams(newQueryParams);
|
||||||
emitPageState();
|
emitPageState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将已挂载的列表恢复为路由指定状态,同一状态不会重复发起请求。
|
||||||
|
*/
|
||||||
|
const restoreState = (state: PageDataRestoreState) => {
|
||||||
|
const nextPageNumber = Math.max(1, Math.trunc(state.pageNumber));
|
||||||
|
const nextPageSize = Math.max(1, Math.trunc(state.pageSize));
|
||||||
|
const nextQueryParams = normalizeQueryParams(state.queryParams);
|
||||||
|
if (
|
||||||
|
pageInfo.pageNumber === nextPageNumber &&
|
||||||
|
pageInfo.pageSize === nextPageSize &&
|
||||||
|
queryParamsEqual(queryParams.value, nextQueryParams)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pageInfo.pageNumber = nextPageNumber;
|
||||||
|
pageInfo.pageSize = nextPageSize;
|
||||||
|
queryParams.value = nextQueryParams;
|
||||||
|
emitPageState();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getPageState,
|
getPageState,
|
||||||
reload,
|
reload,
|
||||||
patchRowById,
|
patchRowById,
|
||||||
|
restoreState,
|
||||||
setQuery,
|
setQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
129
easyflow-ui-admin/app/src/composables/useListRouteState.test.ts
Normal file
129
easyflow-ui-admin/app/src/composables/useListRouteState.test.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { nextTick, reactive } from 'vue';
|
||||||
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
buildListRouteStateQuery,
|
||||||
|
defineListRouteStateSchema,
|
||||||
|
enumListRouteField,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
positiveIntegerListRouteField,
|
||||||
|
stringListRouteField,
|
||||||
|
watchListRouteState,
|
||||||
|
} from './useListRouteState';
|
||||||
|
|
||||||
|
interface TestListState {
|
||||||
|
categoryId: string;
|
||||||
|
keyword: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
status: 'all' | 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = defineListRouteStateSchema<TestListState>({
|
||||||
|
path: '/items',
|
||||||
|
fields: {
|
||||||
|
categoryId: stringListRouteField(),
|
||||||
|
keyword: stringListRouteField({ trim: true }),
|
||||||
|
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||||
|
pageSize: positiveIntegerListRouteField({
|
||||||
|
allowedValues: [10, 20],
|
||||||
|
defaultValue: 10,
|
||||||
|
}),
|
||||||
|
status: enumListRouteField(['all', 'enabled'], 'all'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list route state', () => {
|
||||||
|
it('parses valid state and safely falls back for invalid values', () => {
|
||||||
|
expect(
|
||||||
|
parseListRouteState(
|
||||||
|
{
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: ' 月报 ',
|
||||||
|
pageNumber: '-2',
|
||||||
|
pageSize: '999',
|
||||||
|
status: 'unknown',
|
||||||
|
},
|
||||||
|
schema,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
status: 'all',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits defaults and preserves query fields owned by other features', () => {
|
||||||
|
const state: TestListState = {
|
||||||
|
categoryId: '',
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
status: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(buildListRouteStateQuery(state, schema)).toEqual({});
|
||||||
|
expect(
|
||||||
|
mergeListRouteStateQuery(
|
||||||
|
{
|
||||||
|
devLogin: 'admin',
|
||||||
|
keyword: '旧关键字',
|
||||||
|
pageNumber: '4',
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
schema,
|
||||||
|
),
|
||||||
|
).toEqual({ devLogin: 'admin' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds the exact current list full path', () => {
|
||||||
|
const router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ component: {}, path: '/items' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const state: TestListState = {
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 20,
|
||||||
|
status: 'enabled',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildListRouteFullPath(router, { devLogin: 'admin' }, state, schema),
|
||||||
|
).toBe(
|
||||||
|
'/items?devLogin=admin&categoryId=7&keyword=%E6%9C%88%E6%8A%A5&pageNumber=3&pageSize=20&status=enabled',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits parsed state when the current list full path changes', async () => {
|
||||||
|
const route = reactive({
|
||||||
|
fullPath: '/items',
|
||||||
|
path: '/items',
|
||||||
|
query: {},
|
||||||
|
}) as any;
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const stop = watchListRouteState(route, schema, onChange);
|
||||||
|
|
||||||
|
route.query = { keyword: '月报', pageNumber: '4', pageSize: '20' };
|
||||||
|
route.fullPath = '/items?keyword=月报&pageNumber=4&pageSize=20';
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledOnce();
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
categoryId: '',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: 4,
|
||||||
|
pageSize: 20,
|
||||||
|
status: 'all',
|
||||||
|
});
|
||||||
|
stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
214
easyflow-ui-admin/app/src/composables/useListRouteState.ts
Normal file
214
easyflow-ui-admin/app/src/composables/useListRouteState.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import type {
|
||||||
|
LocationQuery,
|
||||||
|
LocationQueryRaw,
|
||||||
|
RouteLocationNormalizedLoaded,
|
||||||
|
Router,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
type ListRoutePrimitive = number | string;
|
||||||
|
|
||||||
|
interface ListRouteStateField<Value extends ListRoutePrimitive> {
|
||||||
|
defaultValue: Value;
|
||||||
|
parse: (value: string) => Value;
|
||||||
|
queryKey?: string;
|
||||||
|
serialize: (value: Value) => string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRouteStateSchema<State extends object> = {
|
||||||
|
fields: {
|
||||||
|
[Key in keyof State]: ListRouteStateField<
|
||||||
|
Extract<State[Key], ListRoutePrimitive>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StringListRouteFieldOptions {
|
||||||
|
defaultValue?: string;
|
||||||
|
queryKey?: string;
|
||||||
|
trim?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositiveIntegerListRouteFieldOptions {
|
||||||
|
allowedValues?: readonly number[];
|
||||||
|
defaultValue: number;
|
||||||
|
queryKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readListRouteQueryValue(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 stringListRouteField(
|
||||||
|
options: StringListRouteFieldOptions = {},
|
||||||
|
): ListRouteStateField<string> {
|
||||||
|
const defaultValue = options.defaultValue ?? '';
|
||||||
|
const normalize = (value: string) =>
|
||||||
|
options.trim ? value.trim() : String(value || '');
|
||||||
|
return {
|
||||||
|
defaultValue,
|
||||||
|
parse(value) {
|
||||||
|
return normalize(value) || defaultValue;
|
||||||
|
},
|
||||||
|
queryKey: options.queryKey,
|
||||||
|
serialize(value) {
|
||||||
|
const normalized = normalize(value) || defaultValue;
|
||||||
|
return normalized === defaultValue ? undefined : normalized;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumListRouteField<
|
||||||
|
const AllowedValues extends readonly string[],
|
||||||
|
const DefaultValue extends string,
|
||||||
|
>(
|
||||||
|
allowedValues: AllowedValues,
|
||||||
|
defaultValue: DefaultValue,
|
||||||
|
queryKey?: string,
|
||||||
|
): ListRouteStateField<AllowedValues[number] | DefaultValue> {
|
||||||
|
const allowed = new Set<string>(allowedValues);
|
||||||
|
return {
|
||||||
|
defaultValue,
|
||||||
|
parse(value) {
|
||||||
|
return allowed.has(value)
|
||||||
|
? (value as AllowedValues[number])
|
||||||
|
: defaultValue;
|
||||||
|
},
|
||||||
|
queryKey,
|
||||||
|
serialize(value) {
|
||||||
|
return value === defaultValue || !allowed.has(value) ? undefined : value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveIntegerListRouteField(
|
||||||
|
options: PositiveIntegerListRouteFieldOptions,
|
||||||
|
): ListRouteStateField<number> {
|
||||||
|
const allowedValues = options.allowedValues
|
||||||
|
? new Set(options.allowedValues)
|
||||||
|
: undefined;
|
||||||
|
const isValid = (value: number) =>
|
||||||
|
Number.isSafeInteger(value) &&
|
||||||
|
value > 0 &&
|
||||||
|
(!allowedValues || allowedValues.has(value));
|
||||||
|
return {
|
||||||
|
defaultValue: options.defaultValue,
|
||||||
|
parse(value) {
|
||||||
|
if (!/^\d+$/.test(value)) return options.defaultValue;
|
||||||
|
const parsed = Number(value);
|
||||||
|
return isValid(parsed) ? parsed : options.defaultValue;
|
||||||
|
},
|
||||||
|
queryKey: options.queryKey,
|
||||||
|
serialize(value) {
|
||||||
|
return value === options.defaultValue || !isValid(value)
|
||||||
|
? undefined
|
||||||
|
: String(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineListRouteStateSchema<State extends object>(
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
): ListRouteStateSchema<State> {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listRouteFieldEntries<State extends object>(
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
) {
|
||||||
|
return (Object.keys(schema.fields) as Array<keyof State>).map((key) => ({
|
||||||
|
field: schema.fields[key],
|
||||||
|
key,
|
||||||
|
queryKey: schema.fields[key].queryKey || String(key),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseListRouteState<State extends object>(
|
||||||
|
query: LocationQuery,
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
): State {
|
||||||
|
const state = {} as State;
|
||||||
|
for (const { field, key, queryKey } of listRouteFieldEntries(schema)) {
|
||||||
|
state[key] = field.parse(
|
||||||
|
readListRouteQueryValue(query, queryKey),
|
||||||
|
) as State[typeof key];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildListRouteStateQuery<State extends object>(
|
||||||
|
state: State,
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
): LocationQueryRaw {
|
||||||
|
const query: LocationQueryRaw = {};
|
||||||
|
for (const { field, key, queryKey } of listRouteFieldEntries(schema)) {
|
||||||
|
const value = field.serialize(
|
||||||
|
state[key] as Extract<State[typeof key], ListRoutePrimitive>,
|
||||||
|
);
|
||||||
|
if (value !== undefined) query[queryKey] = value;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeListRouteStateQuery<State extends object>(
|
||||||
|
query: LocationQuery,
|
||||||
|
state: State,
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
): LocationQueryRaw {
|
||||||
|
const ownedKeys = new Set(
|
||||||
|
listRouteFieldEntries(schema).map(({ queryKey }) => queryKey),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(query).filter(([key]) => !ownedKeys.has(key)),
|
||||||
|
),
|
||||||
|
...buildListRouteStateQuery(state, schema),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildListRouteFullPath<State extends object>(
|
||||||
|
router: Router,
|
||||||
|
query: LocationQuery,
|
||||||
|
state: State,
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
): string {
|
||||||
|
return router.resolve({
|
||||||
|
path: schema.path,
|
||||||
|
query: mergeListRouteStateQuery(query, state, schema),
|
||||||
|
}).fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchListRouteState<State extends object>(
|
||||||
|
route: RouteLocationNormalizedLoaded,
|
||||||
|
schema: ListRouteStateSchema<State>,
|
||||||
|
onChange: (state: State) => void,
|
||||||
|
) {
|
||||||
|
return watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
if (route.path !== schema.path) return;
|
||||||
|
onChange(parseListRouteState(route.query, schema));
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
buildListRouteStateQuery,
|
||||||
|
defineListRouteStateSchema,
|
||||||
|
enumListRouteField,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
positiveIntegerListRouteField,
|
||||||
|
readListRouteQueryValue,
|
||||||
|
stringListRouteField,
|
||||||
|
watchListRouteState,
|
||||||
|
};
|
||||||
|
export type { ListRouteStateField, ListRouteStateSchema };
|
||||||
@@ -1,78 +1,59 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
function readViewSource(relativePath: string) {
|
function readViewSource(relativePath: string) {
|
||||||
return readFileSync(resolve('app/src/views', relativePath), 'utf8');
|
const workspaceViews = resolve('app/src/views');
|
||||||
|
const viewsRoot = existsSync(workspaceViews)
|
||||||
|
? workspaceViews
|
||||||
|
: resolve('src/views');
|
||||||
|
return readFileSync(resolve(viewsRoot, relativePath), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('detail return navigation', () => {
|
describe('detail return navigation', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['system/sysFeedback/sysFeedbackDetail.vue', '/sys/sysFeedback'],
|
[
|
||||||
['system/sysJob/SysJobLogList.vue', '/sys/sysJob'],
|
'ai/documentCollection/DocumentCollection.vue',
|
||||||
['ai/workflow/WorkflowDesign.vue', '/ai/workflow'],
|
'ai/documentCollection/Document.vue',
|
||||||
['ai/workflow/components/WorkflowChatPage.vue', '/ai/workflow'],
|
],
|
||||||
['ai/workflow/execute/WorkflowExecResultList.vue', '/ai/workflow'],
|
['ai/skill/SkillList.vue', 'ai/skill/SkillDetail.vue'],
|
||||||
])('returns %s to its fixed parent list', (relativePath, parentPath) => {
|
['ai/agents/AgentList.vue', 'ai/agents/AgentDesigner.vue'],
|
||||||
const source = readViewSource(relativePath);
|
['ai/bots/index.vue', 'ai/bots/pages/setting/index.vue'],
|
||||||
|
[
|
||||||
|
'system/approval/ApprovalManage.vue',
|
||||||
|
'system/approval/ApprovalDetail.vue',
|
||||||
|
],
|
||||||
|
['system/sysJob/SysJobList.vue', 'system/sysJob/SysJobLogList.vue'],
|
||||||
|
[
|
||||||
|
'system/sysFeedback/sysFeedbackList.vue',
|
||||||
|
'system/sysFeedback/sysFeedbackDetail.vue',
|
||||||
|
],
|
||||||
|
['ai/workflow/WorkflowList.vue', 'ai/workflow/WorkflowDesign.vue'],
|
||||||
|
[
|
||||||
|
'ai/workflow/WorkflowList.vue',
|
||||||
|
'ai/workflow/components/WorkflowChatPage.vue',
|
||||||
|
],
|
||||||
|
['ai/plugin/Plugin.vue', 'ai/plugin/PluginTools.vue'],
|
||||||
|
['ai/plugin/PluginToolTable.vue', 'ai/plugin/PluginToolEdit.vue'],
|
||||||
|
])(
|
||||||
|
'passes an exact return target from %s and validates it in %s',
|
||||||
|
(listPath, detailPath) => {
|
||||||
|
expect(readViewSource(listPath)).toContain('withListReturnTo');
|
||||||
|
expect(readViewSource(detailPath)).toContain('navigateBackToList');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(source).not.toMatch(/router\.(?:back|go)\s*\(/);
|
it('preserves the nested execution-record list before opening a step', () => {
|
||||||
expect(source).toContain(`path: '${parentPath}'`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns approval details to the originating approval tab', () => {
|
|
||||||
const detailSource = readViewSource('system/approval/ApprovalDetail.vue');
|
|
||||||
const listSource = readViewSource('system/approval/ApprovalManage.vue');
|
|
||||||
|
|
||||||
expect(detailSource).not.toMatch(/router\.(?:back|go)\s*\(/);
|
|
||||||
expect(detailSource).toContain("path: '/sys/approval'");
|
|
||||||
expect(detailSource).toContain('query: { tab }');
|
|
||||||
expect(listSource).toContain('tab: activeTab.value');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns workflow design to the originating list page and filters', () => {
|
|
||||||
const detailSource = readViewSource('ai/workflow/WorkflowDesign.vue');
|
|
||||||
const listSource = readViewSource('ai/workflow/WorkflowList.vue');
|
|
||||||
|
|
||||||
expect(detailSource).toContain(
|
|
||||||
'parseWorkflowDesignReturnState(route.query)',
|
|
||||||
);
|
|
||||||
expect(detailSource).toContain(
|
|
||||||
'query: buildWorkflowListRouteQuery(listState)',
|
|
||||||
);
|
|
||||||
expect(listSource).toContain('buildWorkflowDesignReturnQuery');
|
|
||||||
expect(listSource).toContain('getPageState');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns plugin tool editing to its plugin tool list with a safe fallback', () => {
|
|
||||||
const editSource = readViewSource('ai/plugin/PluginToolEdit.vue');
|
|
||||||
const toolsSource = readViewSource('ai/plugin/PluginTools.vue');
|
|
||||||
const pluginListSource = readViewSource('ai/plugin/Plugin.vue');
|
|
||||||
const listSource = readViewSource('ai/plugin/PluginToolTable.vue');
|
|
||||||
|
|
||||||
expect(editSource).not.toMatch(/router\.(?:back|go)\s*\(/);
|
|
||||||
expect(editSource).toContain("path: '/ai/plugin/tools'");
|
|
||||||
expect(editSource).toContain("path: '/ai/plugin'");
|
|
||||||
expect(editSource).toContain('buildPluginToolsRouteQueryFromEdit');
|
|
||||||
expect(toolsSource).toContain('parsePluginToolsReturnState');
|
|
||||||
expect(toolsSource).toContain('buildPluginListRouteQuery');
|
|
||||||
expect(pluginListSource).toContain('buildPluginToolsReturnQuery');
|
|
||||||
expect(listSource).toContain('pluginId: props.pluginId');
|
|
||||||
expect(listSource).toContain('...route.query');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns execution steps to the filtered execution record list', () => {
|
|
||||||
const stepSource = readViewSource(
|
|
||||||
'ai/workflow/execute/WorkflowExecStepList.vue',
|
|
||||||
);
|
|
||||||
const recordSource = readViewSource(
|
const recordSource = readViewSource(
|
||||||
'ai/workflow/execute/WorkflowExecResultList.vue',
|
'ai/workflow/execute/WorkflowExecResultList.vue',
|
||||||
);
|
);
|
||||||
|
const stepSource = readViewSource(
|
||||||
|
'ai/workflow/execute/WorkflowExecStepList.vue',
|
||||||
|
);
|
||||||
|
|
||||||
expect(stepSource).not.toMatch(/router\.(?:back|go)\s*\(/);
|
expect(recordSource).toContain('withListReturnTo(currentListFullPath())');
|
||||||
expect(stepSource).toContain("name: 'ExecRecord'");
|
expect(stepSource).toContain('navigateBackToList');
|
||||||
expect(stepSource).toContain('query: workflowId ? { workflowId } : {}');
|
expect(stepSource).toContain("['/ai/workflow/executeRecords']");
|
||||||
expect(recordSource).toContain('workflowId: $route.query.workflowId');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
54
easyflow-ui-admin/app/src/router/list-return-context.test.ts
Normal file
54
easyflow-ui-admin/app/src/router/list-return-context.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
readListReturnTo,
|
||||||
|
resolveListReturnPath,
|
||||||
|
withListReturnTo,
|
||||||
|
} from './list-return-context';
|
||||||
|
|
||||||
|
function createTestRouter() {
|
||||||
|
return createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{ component: {}, path: '/items' },
|
||||||
|
{ component: {}, path: '/other' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('list return context', () => {
|
||||||
|
it('round trips an exact internal list URL', () => {
|
||||||
|
const router = createTestRouter();
|
||||||
|
const query = withListReturnTo('/items?pageNumber=4&keyword=月报', {
|
||||||
|
id: '7',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readListReturnTo(query as any)).toBe(
|
||||||
|
'/items?pageNumber=4&keyword=月报',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
resolveListReturnPath(router, query as any, ['/items'], '/items'),
|
||||||
|
).toBe('/items?pageNumber=4&keyword=月报');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'https://example.com/items?pageNumber=4',
|
||||||
|
'//example.com/items?pageNumber=4',
|
||||||
|
'/other?pageNumber=4',
|
||||||
|
'/missing?pageNumber=4',
|
||||||
|
'/items#unsafe',
|
||||||
|
String.raw`/items\unsafe`,
|
||||||
|
`/items?value=${'x'.repeat(4096)}`,
|
||||||
|
])('rejects an unsafe or unrelated return target: %s', (returnTo) => {
|
||||||
|
expect(
|
||||||
|
resolveListReturnPath(
|
||||||
|
createTestRouter(),
|
||||||
|
{ returnTo },
|
||||||
|
['/items'],
|
||||||
|
'/items',
|
||||||
|
),
|
||||||
|
).toBe('/items');
|
||||||
|
});
|
||||||
|
});
|
||||||
105
easyflow-ui-admin/app/src/router/list-return-context.ts
Normal file
105
easyflow-ui-admin/app/src/router/list-return-context.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type { LocationQuery, LocationQueryRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
|
const LIST_RETURN_QUERY_KEY = 'returnTo';
|
||||||
|
const MAX_LIST_RETURN_LENGTH = 4096;
|
||||||
|
|
||||||
|
function hasUnsafeReturnCharacter(value: string): boolean {
|
||||||
|
for (const character of value) {
|
||||||
|
const codePoint = character.codePointAt(0) || 0;
|
||||||
|
if (character === '\\' || codePoint <= 31 || codePoint === 127) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readListReturnTo(query: LocationQuery): string {
|
||||||
|
const value = query[LIST_RETURN_QUERY_KEY];
|
||||||
|
const normalized = Array.isArray(value) ? value[0] : value;
|
||||||
|
return normalized === null || normalized === undefined
|
||||||
|
? ''
|
||||||
|
: String(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withListReturnTo(
|
||||||
|
returnTo: string,
|
||||||
|
query: LocationQueryRaw = {},
|
||||||
|
): LocationQueryRaw {
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
[LIST_RETURN_QUERY_KEY]: returnTo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveListReturnPath(
|
||||||
|
router: Router,
|
||||||
|
query: LocationQuery,
|
||||||
|
allowedListPaths: readonly string[],
|
||||||
|
fallbackPath: string,
|
||||||
|
): string {
|
||||||
|
const raw = readListReturnTo(query);
|
||||||
|
if (
|
||||||
|
!raw ||
|
||||||
|
raw.length > MAX_LIST_RETURN_LENGTH ||
|
||||||
|
!raw.startsWith('/') ||
|
||||||
|
raw.startsWith('//') ||
|
||||||
|
hasUnsafeReturnCharacter(raw)
|
||||||
|
) {
|
||||||
|
return fallbackPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(raw, 'https://easyflow.local');
|
||||||
|
if (parsed.origin !== 'https://easyflow.local' || parsed.hash) {
|
||||||
|
return fallbackPath;
|
||||||
|
}
|
||||||
|
if (!allowedListPaths.includes(parsed.pathname)) {
|
||||||
|
return fallbackPath;
|
||||||
|
}
|
||||||
|
const resolved = router.resolve(raw);
|
||||||
|
return resolved.matched.length > 0 ? resolved.fullPath : fallbackPath;
|
||||||
|
} catch {
|
||||||
|
return fallbackPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHistoryBackPath(router: Router): string {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
const raw = window.history.state?.back;
|
||||||
|
if (typeof raw !== 'string' || !raw) return '';
|
||||||
|
const hashIndex = raw.indexOf('#');
|
||||||
|
const candidate = hashIndex === -1 ? raw : raw.slice(hashIndex + 1);
|
||||||
|
if (!candidate.startsWith('/')) return '';
|
||||||
|
try {
|
||||||
|
return router.resolve(candidate).fullPath;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateBackToList(
|
||||||
|
router: Router,
|
||||||
|
query: LocationQuery,
|
||||||
|
allowedListPaths: readonly string[],
|
||||||
|
fallbackPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const target = resolveListReturnPath(
|
||||||
|
router,
|
||||||
|
query,
|
||||||
|
allowedListPaths,
|
||||||
|
fallbackPath,
|
||||||
|
);
|
||||||
|
if (resolveHistoryBackPath(router) === target) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.replace(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
LIST_RETURN_QUERY_KEY,
|
||||||
|
navigateBackToList,
|
||||||
|
readListReturnTo,
|
||||||
|
resolveListReturnPath,
|
||||||
|
withListReturnTo,
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import { ElButton, ElMessage, ElMessageBox } from 'element-plus';
|
|||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import {
|
import {
|
||||||
canAiResourceOffline,
|
canAiResourceOffline,
|
||||||
canAiResourcePublish,
|
canAiResourcePublish,
|
||||||
@@ -291,8 +292,7 @@ async function loadMcpToolsForOption(id: number | string) {
|
|||||||
String(item.value) === key
|
String(item.value) === key
|
||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
label:
|
label: currentOption.label || 'MCP',
|
||||||
currentOption.label || 'MCP',
|
|
||||||
raw: mergedResource,
|
raw: mergedResource,
|
||||||
}
|
}
|
||||||
: item,
|
: item,
|
||||||
@@ -481,8 +481,13 @@ function handleCloseTryout() {
|
|||||||
selectBase();
|
selectBase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBack() {
|
async function handleBack() {
|
||||||
router.push(AGENT_TAB_PAGE_KEY);
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
[AGENT_TAB_PAGE_KEY],
|
||||||
|
AGENT_TAB_PAGE_KEY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import type {
|
|||||||
ActionButton,
|
ActionButton,
|
||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
import CardList from '#/components/page/CardList.vue';
|
|
||||||
|
|
||||||
import { computed, markRaw, onMounted, ref } from '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 { useAccess } from '@easyflow/access';
|
||||||
import { defaultAssistantAvatar } from '@easyflow/common-ui';
|
import { defaultAssistantAvatar } from '@easyflow/common-ui';
|
||||||
@@ -27,9 +26,17 @@ import { ElIcon, ElMessage, ElMessageBox, ElPopover } from 'element-plus';
|
|||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import CardList from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||||
import {
|
import {
|
||||||
canAiResourceDelete,
|
canAiResourceDelete,
|
||||||
@@ -40,6 +47,7 @@ import {
|
|||||||
resolveAiResourceDisplayStatus,
|
resolveAiResourceDisplayStatus,
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
|
import { agentListRouteSchema } from './agent-list-route-state';
|
||||||
import {
|
import {
|
||||||
getAgentCategories,
|
getAgentCategories,
|
||||||
submitAgentDeleteApproval,
|
submitAgentDeleteApproval,
|
||||||
@@ -48,9 +56,35 @@ import {
|
|||||||
updateAgentVisibilityScope,
|
updateAgentVisibilityScope,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const initialListState = parseListRouteState(route.query, agentListRouteSchema);
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
const sideList = ref<any[]>([]);
|
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 AGENT_TAB_PAGE_KEY = '/ai/agents';
|
||||||
const DEFAULT_AGENT_TITLE = '未命名智能体';
|
const DEFAULT_AGENT_TITLE = '未命名智能体';
|
||||||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||||||
@@ -104,6 +138,7 @@ const primaryAction: CardPrimaryAction = {
|
|||||||
query: {
|
query: {
|
||||||
pageKey: AGENT_TAB_PAGE_KEY,
|
pageKey: AGENT_TAB_PAGE_KEY,
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -149,10 +184,12 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleSearch(keyword: string) {
|
function handleSearch(keyword: string) {
|
||||||
|
searchKeyword.value = keyword;
|
||||||
pageDataRef.value?.setQuery({
|
pageDataRef.value?.setQuery({
|
||||||
|
categoryId: selectedCategoryId.value || undefined,
|
||||||
isQueryOr: true,
|
isQueryOr: true,
|
||||||
name: keyword,
|
name: keyword || undefined,
|
||||||
description: keyword,
|
description: keyword || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +200,7 @@ function handleButtonClick(payload: any) {
|
|||||||
query: {
|
query: {
|
||||||
pageKey: AGENT_TAB_PAGE_KEY,
|
pageKey: AGENT_TAB_PAGE_KEY,
|
||||||
navTitle: DEFAULT_AGENT_TITLE,
|
navTitle: DEFAULT_AGENT_TITLE,
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -221,7 +259,19 @@ async function updateVisibilityScope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeCategory(category: any) {
|
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() {
|
async function loadCategories() {
|
||||||
@@ -231,9 +281,57 @@ async function loadCategories() {
|
|||||||
{ id: '', categoryName: $t('common.allCategories') },
|
{ id: '', categoryName: $t('common.allCategories') },
|
||||||
...(res.data || []),
|
...(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(
|
function resolvePublishStatusMeta(
|
||||||
displayPublishStatus?: string,
|
displayPublishStatus?: string,
|
||||||
publishStatus?: string,
|
publishStatus?: string,
|
||||||
@@ -333,6 +431,7 @@ async function handleDeleteAction(row: AgentInfo) {
|
|||||||
<div class="agent-list-page">
|
<div class="agent-list-page">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -341,6 +440,7 @@ async function handleDeleteAction(row: AgentInfo) {
|
|||||||
label-key="categoryName"
|
label-key="categoryName"
|
||||||
value-key="id"
|
value-key="id"
|
||||||
:menus="sideList"
|
:menus="sideList"
|
||||||
|
:default-selected="selectedCategoryId"
|
||||||
@change="changeCategory"
|
@change="changeCategory"
|
||||||
/>
|
/>
|
||||||
<div class="agent-list-page__content">
|
<div class="agent-list-page__content">
|
||||||
@@ -348,7 +448,15 @@ async function handleDeleteAction(row: AgentInfo) {
|
|||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/agent/page"
|
page-url="/api/v1/agent/page"
|
||||||
:page-sizes="[12, 18, 24]"
|
: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 }">
|
<template #default="{ pageList }">
|
||||||
<CardList
|
<CardList
|
||||||
@@ -366,13 +474,12 @@ async function handleDeleteAction(row: AgentInfo) {
|
|||||||
<template #publish>
|
<template #publish>
|
||||||
<div
|
<div
|
||||||
class="agent-publish-chip"
|
class="agent-publish-chip"
|
||||||
:class="
|
:class="`agent-publish-chip--${
|
||||||
'agent-publish-chip--' +
|
|
||||||
resolvePublishStatusMeta(
|
resolvePublishStatusMeta(
|
||||||
item.displayPublishStatus,
|
item.displayPublishStatus,
|
||||||
item.publishStatus,
|
item.publishStatus,
|
||||||
).tone
|
).tone
|
||||||
"
|
}`"
|
||||||
>
|
>
|
||||||
<span class="agent-publish-chip__dot"></span>
|
<span class="agent-publish-chip__dot"></span>
|
||||||
<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';
|
} from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
import { computed, markRaw, onMounted, ref } from '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 { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
@@ -37,6 +37,13 @@ import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
|||||||
import CardList from '#/components/page/CardList.vue';
|
import CardList from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import {
|
import {
|
||||||
confirmPublishSubmission,
|
confirmPublishSubmission,
|
||||||
} from '#/views/ai/shared/approval-application-reason';
|
} from '#/views/ai/shared/approval-application-reason';
|
||||||
@@ -50,6 +57,7 @@ import {
|
|||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
|
import { botListRouteSchema } from './bot-list-route-state';
|
||||||
import Modal from './modal.vue';
|
import Modal from './modal.vue';
|
||||||
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
@@ -70,8 +78,33 @@ onMounted(() => {
|
|||||||
getSideList();
|
getSideList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const initialListState = parseListRouteState(route.query, botListRouteSchema);
|
||||||
const pageDataRef = ref();
|
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 modalRef = ref<InstanceType<typeof Modal>>();
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
|
||||||
@@ -98,6 +131,7 @@ const primaryAction: CardPrimaryAction = {
|
|||||||
query: {
|
query: {
|
||||||
pageKey: '/ai/bots',
|
pageKey: '/ai/bots',
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -273,7 +307,12 @@ function resolvePublishStatusMetaByInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = (params: string) => {
|
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 = () => {
|
const handleButtonClick = () => {
|
||||||
modalRef.value?.open('create');
|
modalRef.value?.open('create');
|
||||||
@@ -347,7 +386,18 @@ function initDict() {
|
|||||||
dictStore.fetchDictionary('dataStatus');
|
dictStore.fetchDictionary('dataStatus');
|
||||||
}
|
}
|
||||||
function changeCategory(category: any) {
|
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) {
|
function showControlDialog(item: any) {
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
@@ -412,14 +462,60 @@ const getSideList = async () => {
|
|||||||
},
|
},
|
||||||
...res.data,
|
...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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -430,6 +526,7 @@ const getSideList = async () => {
|
|||||||
:menus="sideList"
|
:menus="sideList"
|
||||||
:control-btns="controlBtns"
|
:control-btns="controlBtns"
|
||||||
:footer-button="footerButton"
|
:footer-button="footerButton"
|
||||||
|
:default-selected="selectedCategoryId"
|
||||||
@change="changeCategory"
|
@change="changeCategory"
|
||||||
/>
|
/>
|
||||||
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
|
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
|
||||||
@@ -437,7 +534,14 @@ const getSideList = async () => {
|
|||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/bot/page"
|
page-url="/api/v1/bot/page"
|
||||||
:page-sizes="[12, 18, 24]"
|
: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 }">
|
<template #default="{ pageList }">
|
||||||
<CardList
|
<CardList
|
||||||
@@ -462,7 +566,7 @@ const getSideList = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 创建&编辑Bot弹窗 -->
|
<!-- 创建&编辑Bot弹窗 -->
|
||||||
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
|
<Modal ref="modalRef" @success="pageDataRef.reload()" />
|
||||||
|
|
||||||
<EasyFlowFormModal
|
<EasyFlowFormModal
|
||||||
v-model:open="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import type { BotInfo } from '@easyflow/types';
|
|||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import { getBotDetails } from '#/api';
|
import { getBotDetails } from '#/api';
|
||||||
import { hasPermission } from '#/api/common/hasPermission';
|
import { hasPermission } from '#/api/common/hasPermission';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
|
|
||||||
import Config from './config.vue';
|
import Config from './config.vue';
|
||||||
import Preview from './preview.vue';
|
import Preview from './preview.vue';
|
||||||
@@ -55,10 +59,16 @@ const fetchBotDetail = async (id: string) => {
|
|||||||
syncNavTitle((res.data?.title || res.data?.name || '') as string);
|
syncNavTitle((res.data?.title || res.data?.name || '') as string);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
async function backToBotList() {
|
||||||
|
await navigateBackToList(router, route.query, ['/ai/bots'], '/ai/bots');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
|
<ElButton :icon="ArrowLeft" class="settings-back" @click="backToBotList">
|
||||||
|
{{ $t('button.back') }}
|
||||||
|
</ElButton>
|
||||||
<div class="row-container">
|
<div class="row-container">
|
||||||
<div class="row-item">
|
<div class="row-item">
|
||||||
<Prompt :bot="bot" :has-save-permission="hasSavePermission" />
|
<Prompt :bot="bot" :has-save-permission="hasSavePermission" />
|
||||||
@@ -74,14 +84,22 @@ const fetchBotDetail = async (id: string) => {
|
|||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.settings-container {
|
.settings-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
height: calc(100vh - 90px);
|
height: calc(100vh - 90px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-back {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.row-container {
|
.row-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-item {
|
.row-item {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ElButton, ElIcon, ElImage, ElMessage } from 'element-plus';
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import bookIcon from '#/assets/ai/knowledge/book.svg';
|
import bookIcon from '#/assets/ai/knowledge/book.svg';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||||
import DocumentImportBatchStatus from '#/views/ai/documentCollection/DocumentImportBatchStatus.vue';
|
import DocumentImportBatchStatus from '#/views/ai/documentCollection/DocumentImportBatchStatus.vue';
|
||||||
|
|
||||||
@@ -150,8 +151,13 @@ const getKnowledge = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getKnowledge();
|
getKnowledge();
|
||||||
});
|
});
|
||||||
const back = () => {
|
const back = async () => {
|
||||||
router.push({ path: '/ai/documentCollection' });
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
['/ai/documentCollection'],
|
||||||
|
'/ai/documentCollection',
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const isFaqCollection = computed(
|
const isFaqCollection = computed(
|
||||||
() => knowledgeInfo.value.collectionType === 'FAQ',
|
() => knowledgeInfo.value.collectionType === 'FAQ',
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import type {
|
|||||||
ActionButton,
|
ActionButton,
|
||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
|
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { useAccess } from '@easyflow/access';
|
import { useAccess } from '@easyflow/access';
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
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 CardPage from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.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 { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||||
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
|
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 AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||||
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
|
import { confirmPublishSubmission } from '#/views/ai/shared/approval-application-reason';
|
||||||
import {
|
import { buildOfflineImpactMessage } from '#/views/ai/shared/offline-impact';
|
||||||
buildOfflineImpactMessage,
|
|
||||||
type OfflineImpactCheck,
|
|
||||||
} from '#/views/ai/shared/offline-impact';
|
|
||||||
import {
|
import {
|
||||||
canAiResourceDelete,
|
canAiResourceDelete,
|
||||||
canAiResourceOffline,
|
canAiResourceOffline,
|
||||||
@@ -61,7 +67,35 @@ import {
|
|||||||
resolveAiResourceDisplayStatus,
|
resolveAiResourceDisplayStatus,
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
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 userStore = useUserStore();
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
const collectionTypeLabelMap = {
|
const collectionTypeLabelMap = {
|
||||||
@@ -153,6 +187,20 @@ function resolveNavTitle(row: Record<string, any>) {
|
|||||||
return row?.title || row?.name || '';
|
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: {
|
function openKnowledgeDetail(row: {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -164,6 +212,7 @@ function openKnowledgeDetail(row: {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/documentCollection',
|
pageKey: '/ai/documentCollection',
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -243,6 +292,7 @@ const actions: ActionButton[] = [
|
|||||||
pageKey: '/ai/documentCollection',
|
pageKey: '/ai/documentCollection',
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
activeMenu: 'knowledgeSearch',
|
activeMenu: 'knowledgeSearch',
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -451,18 +501,18 @@ function resolvePublishStatusMeta(
|
|||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'OFFLINE_PENDING': {
|
|
||||||
return {
|
|
||||||
label: $t('documentCollection.publishStatusOfflinePending'),
|
|
||||||
tone: 'pending',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'OFFLINE': {
|
case 'OFFLINE': {
|
||||||
return {
|
return {
|
||||||
label: $t('documentCollection.publishStatusOffline'),
|
label: $t('documentCollection.publishStatusOffline'),
|
||||||
tone: 'draft',
|
tone: 'draft',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'OFFLINE_PENDING': {
|
||||||
|
return {
|
||||||
|
label: $t('documentCollection.publishStatusOfflinePending'),
|
||||||
|
tone: 'pending',
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'PUBLISH_PENDING': {
|
case 'PUBLISH_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('documentCollection.publishStatusPublishPending'),
|
label: $t('documentCollection.publishStatusPublishPending'),
|
||||||
@@ -537,7 +587,12 @@ const formRules = computed(() => {
|
|||||||
return rules;
|
return rules;
|
||||||
});
|
});
|
||||||
const handleSearch = (params: any) => {
|
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 = () => {
|
const reloadKnowledgeList = () => {
|
||||||
pageDataRef.value?.reload?.();
|
pageDataRef.value?.reload?.();
|
||||||
@@ -567,6 +622,19 @@ const getCategoryList = async () => {
|
|||||||
},
|
},
|
||||||
...res.data,
|
...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) {
|
function removeCategory(row: any) {
|
||||||
@@ -686,7 +754,42 @@ function handleSubmit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function changeCategory(category: any) {
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -695,6 +798,7 @@ function changeCategory(category: any) {
|
|||||||
<div class="knowledge-header">
|
<div class="knowledge-header">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -706,15 +810,22 @@ function changeCategory(category: any) {
|
|||||||
:menus="categoryList"
|
:menus="categoryList"
|
||||||
:control-btns="controlBtns"
|
:control-btns="controlBtns"
|
||||||
:footer-button="footerButton"
|
:footer-button="footerButton"
|
||||||
|
:default-selected="selectedCategoryId"
|
||||||
@change="changeCategory"
|
@change="changeCategory"
|
||||||
/>
|
/>
|
||||||
<div class="h-full flex-1 overflow-auto">
|
<div class="h-full flex-1 overflow-auto">
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/documentCollection/page"
|
page-url="/api/v1/documentCollection/page"
|
||||||
:page-size="12"
|
:page-size="initialListState.pageSize"
|
||||||
:page-sizes="[12, 24, 36, 48]"
|
: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 }">
|
<template #default="{ pageList }">
|
||||||
<CardPage
|
<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 CardPage from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.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 { createLazyComponentController } from '#/utils/lazy-component';
|
||||||
|
|
||||||
import { buildPluginPageQueryParams } from './plugin-query';
|
import { buildPluginPageQueryParams } from './plugin-query';
|
||||||
import {
|
import {
|
||||||
buildPluginToolsReturnQuery,
|
|
||||||
mergePluginListRouteQuery,
|
mergePluginListRouteQuery,
|
||||||
parsePluginListRouteState,
|
parsePluginListRouteState,
|
||||||
|
pluginListRouteSchema,
|
||||||
} from './plugin-route-state';
|
} from './plugin-route-state';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -95,7 +100,7 @@ function openPluginTools(item: PluginRecord) {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
pageKey: '/ai/plugin',
|
pageKey: '/ai/plugin',
|
||||||
navTitle: resolveNavTitle(item),
|
navTitle: resolveNavTitle(item),
|
||||||
...buildPluginToolsReturnQuery(currentListState()),
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -217,6 +222,20 @@ const handleDelete = (item: PluginRecord) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pageDataRef = ref();
|
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 = [
|
const headerButtons = [
|
||||||
{
|
{
|
||||||
key: 'add',
|
key: 'add',
|
||||||
@@ -265,6 +284,15 @@ function currentListState(): PluginListRouteState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
currentListState(),
|
||||||
|
pluginListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function syncListRouteState() {
|
function syncListRouteState() {
|
||||||
const target = {
|
const target = {
|
||||||
path: '/ai/plugin',
|
path: '/ai/plugin',
|
||||||
@@ -325,7 +353,7 @@ const handleDeleteCategory = (params: PluginCategory) => {
|
|||||||
const handleClickCategory = (item: PluginCategory) => {
|
const handleClickCategory = (item: PluginCategory) => {
|
||||||
if (
|
if (
|
||||||
suppressInitialCategoryChange.value &&
|
suppressInitialCategoryChange.value &&
|
||||||
String(item.id) === initialListState.categoryId
|
String(item.id) === String(selectedCategoryId.value)
|
||||||
) {
|
) {
|
||||||
suppressInitialCategoryChange.value = false;
|
suppressInitialCategoryChange.value = false;
|
||||||
selectedCategoryId.value = item.id;
|
selectedCategoryId.value = item.id;
|
||||||
|
|||||||
@@ -19,16 +19,11 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
||||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||||
|
|
||||||
import {
|
|
||||||
buildPluginListRouteQuery,
|
|
||||||
buildPluginToolsRouteQueryFromEdit,
|
|
||||||
parsePluginToolsReturnState,
|
|
||||||
} from './plugin-route-state';
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -169,19 +164,20 @@ function handleClickHeader(index: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
async function back() {
|
||||||
const pluginId = String(route.query.pluginId || '');
|
const pluginId = String(route.query.pluginId || '');
|
||||||
if (pluginId) {
|
const fallbackPath = pluginId
|
||||||
void router.replace({
|
? router.resolve({
|
||||||
path: '/ai/plugin/tools',
|
path: '/ai/plugin/tools',
|
||||||
query: buildPluginToolsRouteQueryFromEdit(route.query, pluginId),
|
query: { id: pluginId },
|
||||||
});
|
}).fullPath
|
||||||
return;
|
: '/ai/plugin';
|
||||||
}
|
await navigateBackToList(
|
||||||
void router.replace({
|
router,
|
||||||
path: '/ai/plugin',
|
route.query,
|
||||||
query: buildPluginListRouteQuery(parsePluginToolsReturnState(route.query)),
|
pluginId ? ['/ai/plugin/tools'] : ['/ai/plugin'],
|
||||||
});
|
fallbackPath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePluginTool(index: number) {
|
function updatePluginTool(index: number) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
import AiPluginToolModal from '#/views/ai/plugin/AiPluginToolModal.vue';
|
||||||
|
|
||||||
import { buildPluginToolPageQueryParams } from './plugin-query';
|
import { buildPluginToolPageQueryParams } from './plugin-query';
|
||||||
@@ -31,6 +32,10 @@ const props = defineProps({
|
|||||||
default: 1,
|
default: 1,
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
returnTo: {
|
||||||
|
default: '/ai/plugin/tools',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
initialKeyword: {
|
initialKeyword: {
|
||||||
default: '',
|
default: '',
|
||||||
type: String,
|
type: String,
|
||||||
@@ -53,7 +58,6 @@ const emit = defineEmits<{
|
|||||||
},
|
},
|
||||||
): void;
|
): void;
|
||||||
}>();
|
}>();
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openPluginToolModal() {
|
openPluginToolModal() {
|
||||||
@@ -65,17 +69,27 @@ defineExpose({
|
|||||||
handleSearch: (params: string) => {
|
handleSearch: (params: string) => {
|
||||||
pageDataRef.value.setQuery(buildPluginToolPageQueryParams(params));
|
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 pageDataRef = ref();
|
||||||
const handleEdit = (row: any) => {
|
const handleEdit = (row: any) => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/ai/plugin/tool/edit',
|
path: '/ai/plugin/tool/edit',
|
||||||
query: {
|
query: withListReturnTo(props.returnTo, {
|
||||||
...route.query,
|
|
||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/plugin',
|
pageKey: '/ai/plugin',
|
||||||
pluginId: props.pluginId,
|
pluginId: props.pluginId,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import { Back, Plus } from '@element-plus/icons-vue';
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
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 PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildPluginListRouteQuery,
|
|
||||||
mergePluginToolListRouteQuery,
|
mergePluginToolListRouteQuery,
|
||||||
parsePluginToolListRouteState,
|
parsePluginToolListRouteState,
|
||||||
parsePluginToolsReturnState,
|
pluginToolListRouteSchema,
|
||||||
} from './plugin-route-state';
|
} from './plugin-route-state';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -29,6 +33,20 @@ const pluginToolRef = ref();
|
|||||||
const toolSearchKeyword = ref(initialToolListState.keyword.trim());
|
const toolSearchKeyword = ref(initialToolListState.keyword.trim());
|
||||||
const currentToolPageNumber = ref(initialToolListState.pageNumber);
|
const currentToolPageNumber = ref(initialToolListState.pageNumber);
|
||||||
const currentToolPageSize = ref(initialToolListState.pageSize);
|
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 headerButtons = computed<any[]>(() => {
|
||||||
const buttons: any[] = [
|
const buttons: any[] = [
|
||||||
@@ -73,6 +91,15 @@ function currentToolListState(): PluginToolListRouteState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentToolListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
currentToolListState(),
|
||||||
|
pluginToolListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function syncToolListRouteState() {
|
function syncToolListRouteState() {
|
||||||
const target = {
|
const target = {
|
||||||
path: '/ai/plugin/tools',
|
path: '/ai/plugin/tools',
|
||||||
@@ -100,12 +127,12 @@ function handleSearch(params: string) {
|
|||||||
function handleButtonClick(event: any) {
|
function handleButtonClick(event: any) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'back': {
|
case 'back': {
|
||||||
void router.replace({
|
void navigateBackToList(
|
||||||
path: '/ai/plugin',
|
router,
|
||||||
query: buildPluginListRouteQuery(
|
route.query,
|
||||||
parsePluginToolsReturnState(route.query),
|
['/ai/plugin'],
|
||||||
),
|
'/ai/plugin',
|
||||||
});
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'createTool': {
|
case 'createTool': {
|
||||||
@@ -133,6 +160,7 @@ function handleButtonClick(event: any) {
|
|||||||
:initial-keyword="initialToolListState.keyword"
|
:initial-keyword="initialToolListState.keyword"
|
||||||
:initial-page-number="initialToolListState.pageNumber"
|
:initial-page-number="initialToolListState.pageNumber"
|
||||||
:initial-page-size="initialToolListState.pageSize"
|
:initial-page-size="initialToolListState.pageSize"
|
||||||
|
:return-to="currentToolListFullPath()"
|
||||||
@state-change="handleToolPageStateChange"
|
@state-change="handleToolPageStateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildPluginListRouteQuery,
|
buildPluginListRouteQuery,
|
||||||
buildPluginToolsReturnQuery,
|
buildPluginToolListRouteQuery,
|
||||||
buildPluginToolsRouteQueryFromEdit,
|
|
||||||
mergePluginListRouteQuery,
|
mergePluginListRouteQuery,
|
||||||
mergePluginToolListRouteQuery,
|
mergePluginToolListRouteQuery,
|
||||||
parsePluginListRouteState,
|
parsePluginListRouteState,
|
||||||
parsePluginToolListRouteState,
|
parsePluginToolListRouteState,
|
||||||
parsePluginToolsReturnState,
|
|
||||||
} from './plugin-route-state';
|
} from './plugin-route-state';
|
||||||
|
|
||||||
describe('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 = {
|
const state = {
|
||||||
categoryId: '7',
|
categoryId: '7',
|
||||||
keyword: '天气',
|
keyword: '天气',
|
||||||
pageNumber: 3,
|
pageNumber: 3,
|
||||||
pageSize: 24,
|
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({
|
expect(buildPluginListRouteQuery(state)).toEqual({
|
||||||
categoryId: '7',
|
categoryId: '7',
|
||||||
keyword: '天气',
|
keyword: '天气',
|
||||||
@@ -66,8 +55,8 @@ describe('plugin route state', () => {
|
|||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
parsePluginToolListRouteState({
|
parsePluginToolListRouteState({
|
||||||
toolPageNumber: '0',
|
pageNumber: '0',
|
||||||
toolPageSize: '999',
|
pageSize: '999',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@@ -99,9 +88,8 @@ describe('plugin route state', () => {
|
|||||||
mergePluginToolListRouteQuery(
|
mergePluginToolListRouteQuery(
|
||||||
{
|
{
|
||||||
id: '88',
|
id: '88',
|
||||||
returnCategoryId: '7',
|
returnTo: '/ai/plugin?categoryId=7&pageNumber=3',
|
||||||
returnPageNumber: '3',
|
keyword: '旧工具',
|
||||||
toolKeyword: '旧工具',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keyword: '查询工具',
|
keyword: '查询工具',
|
||||||
@@ -111,29 +99,24 @@ describe('plugin route state', () => {
|
|||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
id: '88',
|
id: '88',
|
||||||
returnCategoryId: '7',
|
keyword: '查询工具',
|
||||||
returnPageNumber: '3',
|
pageNumber: '2',
|
||||||
toolKeyword: '查询工具',
|
pageSize: '20',
|
||||||
toolPageNumber: '2',
|
returnTo: '/ai/plugin?categoryId=7&pageNumber=3',
|
||||||
toolPageSize: '20',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restores plugin tools query after editing a tool', () => {
|
it('serializes plugin tool list state with common query keys', () => {
|
||||||
expect(
|
expect(
|
||||||
buildPluginToolsRouteQueryFromEdit(
|
buildPluginToolListRouteQuery({
|
||||||
{
|
keyword: '查询工具',
|
||||||
id: 'tool-1',
|
pageNumber: 2,
|
||||||
pluginId: 'plugin-1',
|
pageSize: 20,
|
||||||
returnCategoryId: '7',
|
}),
|
||||||
toolPageNumber: '2',
|
|
||||||
},
|
|
||||||
'plugin-1',
|
|
||||||
),
|
|
||||||
).toEqual({
|
).toEqual({
|
||||||
id: 'plugin-1',
|
keyword: '查询工具',
|
||||||
returnCategoryId: '7',
|
pageNumber: '2',
|
||||||
toolPageNumber: '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;
|
import {
|
||||||
const DEFAULT_PLUGIN_PAGE_SIZE = 12;
|
buildListRouteStateQuery,
|
||||||
const DEFAULT_PLUGIN_CATEGORY_ID = '0';
|
defineListRouteStateSchema,
|
||||||
const PLUGIN_PAGE_SIZES = new Set([12, 24, 36, 48]);
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
const DEFAULT_TOOL_PAGE_NUMBER = 1;
|
positiveIntegerListRouteField,
|
||||||
const DEFAULT_TOOL_PAGE_SIZE = 10;
|
stringListRouteField,
|
||||||
const TOOL_PAGE_SIZES = new Set([10, 20, 50, 100]);
|
} from '#/composables/useListRouteState';
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
interface PluginListRouteState {
|
interface PluginListRouteState {
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
@@ -42,156 +22,74 @@ interface PluginToolListRouteState {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readQueryValue(query: LocationQuery, key: string): string {
|
const pluginListRouteSchema = defineListRouteStateSchema<PluginListRouteState>({
|
||||||
const value = query[key];
|
path: '/ai/plugin',
|
||||||
const normalized = Array.isArray(value) ? value[0] : value;
|
fields: {
|
||||||
return normalized === null || normalized === undefined
|
categoryId: stringListRouteField({ defaultValue: '0' }),
|
||||||
? ''
|
keyword: stringListRouteField(),
|
||||||
: String(normalized);
|
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 {
|
function buildPluginListRouteQuery(
|
||||||
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(
|
|
||||||
state: PluginListRouteState,
|
state: PluginListRouteState,
|
||||||
keys: typeof PLUGIN_LIST_QUERY_KEYS | typeof PLUGIN_RETURN_QUERY_KEYS,
|
): LocationQueryRaw {
|
||||||
): LocationQuery {
|
return buildListRouteStateQuery(state, pluginListRouteSchema);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePluginListRouteQuery(
|
function mergePluginListRouteQuery(
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
state: PluginListRouteState,
|
state: PluginListRouteState,
|
||||||
): LocationQuery {
|
): LocationQueryRaw {
|
||||||
const listKeys = new Set<string>(Object.values(PLUGIN_LIST_QUERY_KEYS));
|
return mergeListRouteStateQuery(query, state, pluginListRouteSchema);
|
||||||
return {
|
|
||||||
...Object.fromEntries(
|
|
||||||
Object.entries(query).filter(([key]) => !listKeys.has(key)),
|
|
||||||
),
|
|
||||||
...buildPluginListRouteQuery(state),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePluginToolListRouteState(
|
function parsePluginToolListRouteState(query: LocationQuery) {
|
||||||
query: LocationQuery,
|
return parseListRouteState(query, pluginToolListRouteSchema);
|
||||||
): 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 buildPluginToolListRouteQuery(
|
function buildPluginToolListRouteQuery(
|
||||||
state: PluginToolListRouteState,
|
state: PluginToolListRouteState,
|
||||||
): LocationQuery {
|
): LocationQueryRaw {
|
||||||
return {
|
return buildListRouteStateQuery(state, pluginToolListRouteSchema);
|
||||||
...(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 } : {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergePluginToolListRouteQuery(
|
function mergePluginToolListRouteQuery(
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
state: PluginToolListRouteState,
|
state: PluginToolListRouteState,
|
||||||
): LocationQuery {
|
): LocationQueryRaw {
|
||||||
const listKeys = new Set<string>(Object.values(TOOL_LIST_QUERY_KEYS));
|
return mergeListRouteStateQuery(query, state, pluginToolListRouteSchema);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildPluginListRouteQuery,
|
buildPluginListRouteQuery,
|
||||||
buildPluginToolsReturnQuery,
|
buildPluginToolListRouteQuery,
|
||||||
buildPluginToolsRouteQueryFromEdit,
|
|
||||||
mergePluginListRouteQuery,
|
mergePluginListRouteQuery,
|
||||||
mergePluginToolListRouteQuery,
|
mergePluginToolListRouteQuery,
|
||||||
parsePluginListRouteState,
|
parsePluginListRouteState,
|
||||||
parsePluginToolListRouteState,
|
parsePluginToolListRouteState,
|
||||||
parsePluginToolsReturnState,
|
pluginListRouteSchema,
|
||||||
|
pluginToolListRouteSchema,
|
||||||
};
|
};
|
||||||
export type { PluginListRouteState, PluginToolListRouteState };
|
export type { PluginListRouteState, PluginToolListRouteState };
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { hasPermission } from '#/api/common/hasPermission';
|
import { hasPermission } from '#/api/common/hasPermission';
|
||||||
|
import {
|
||||||
|
navigateBackToList,
|
||||||
|
resolveListReturnPath,
|
||||||
|
} from '#/router/list-return-context';
|
||||||
import {
|
import {
|
||||||
canAiResourceDelete,
|
canAiResourceDelete,
|
||||||
canAiResourceOffline,
|
canAiResourceOffline,
|
||||||
@@ -211,15 +215,21 @@ async function confirmUnsavedNavigation() {
|
|||||||
onBeforeRouteLeave(confirmUnsavedNavigation);
|
onBeforeRouteLeave(confirmUnsavedNavigation);
|
||||||
onBeforeRouteUpdate(confirmUnsavedNavigation);
|
onBeforeRouteUpdate(confirmUnsavedNavigation);
|
||||||
|
|
||||||
async function replaceRouteDuringOperation(path: string) {
|
async function returnToSkillListDuringOperation() {
|
||||||
allowOperationNavigation = true;
|
allowOperationNavigation = true;
|
||||||
try {
|
try {
|
||||||
await router.replace(path);
|
await router.replace(
|
||||||
|
resolveListReturnPath(router, route.query, ['/ai/skill'], '/ai/skill'),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
allowOperationNavigation = false;
|
allowOperationNavigation = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backToSkillList() {
|
||||||
|
await navigateBackToList(router, route.query, ['/ai/skill'], '/ai/skill');
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const request = ++initRequest;
|
const request = ++initRequest;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -227,7 +237,7 @@ async function init() {
|
|||||||
loadAccessDenied.value = false;
|
loadAccessDenied.value = false;
|
||||||
try {
|
try {
|
||||||
if (isNew.value) {
|
if (isNew.value) {
|
||||||
await replaceRouteDuringOperation('/ai/skill');
|
await returnToSkillListDuringOperation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadCategories(request);
|
await loadCategories(request);
|
||||||
@@ -355,9 +365,8 @@ async function saveFiles(allowLifecycleAction = false, showFeedback = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function flushPendingChanges(allowLifecycleAction = false) {
|
async function flushPendingChanges(allowLifecycleAction = false) {
|
||||||
if (resourceDirty.value) {
|
if (resourceDirty.value && !(await saveFiles(allowLifecycleAction, false)))
|
||||||
if (!(await saveFiles(allowLifecycleAction, false))) return false;
|
return false;
|
||||||
}
|
|
||||||
const panel = capabilityPanelRef.value;
|
const panel = capabilityPanelRef.value;
|
||||||
if (capabilityDirty.value || panel?.hasDirty()) {
|
if (capabilityDirty.value || panel?.hasDirty()) {
|
||||||
if (!canBindCapabilities.value || !panel) {
|
if (!canBindCapabilities.value || !panel) {
|
||||||
@@ -524,7 +533,7 @@ async function remove() {
|
|||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
if (res.data === null || res.data === undefined) {
|
if (res.data === null || res.data === undefined) {
|
||||||
ElMessage.success(res.message || 'Skill 已删除');
|
ElMessage.success(res.message || 'Skill 已删除');
|
||||||
await replaceRouteDuringOperation('/ai/skill');
|
await returnToSkillListDuringOperation();
|
||||||
} else {
|
} else {
|
||||||
ElMessage.success(res.message || '已提交删除审批');
|
ElMessage.success(res.message || '已提交删除审批');
|
||||||
await init();
|
await init();
|
||||||
@@ -561,7 +570,7 @@ function handleSaveShortcut(event: KeyboardEvent) {
|
|||||||
text
|
text
|
||||||
:disabled="operationLocked"
|
:disabled="operationLocked"
|
||||||
aria-label="返回 Skill 列表"
|
aria-label="返回 Skill 列表"
|
||||||
@click="router.push('/ai/skill')"
|
@click="backToSkillList"
|
||||||
/>
|
/>
|
||||||
<div class="skill-detail-page__title">
|
<div class="skill-detail-page__title">
|
||||||
<div>
|
<div>
|
||||||
@@ -674,7 +683,7 @@ function handleSaveShortcut(event: KeyboardEvent) {
|
|||||||
<template #extra>
|
<template #extra>
|
||||||
<ElButton
|
<ElButton
|
||||||
:type="loadAccessDenied ? 'primary' : 'default'"
|
:type="loadAccessDenied ? 'primary' : 'default'"
|
||||||
@click="router.push('/ai/skill')"
|
@click="backToSkillList"
|
||||||
>
|
>
|
||||||
返回 Skill 列表
|
返回 Skill 列表
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { downloadFileFromBlob, formatDate } from '@easyflow/utils';
|
import { downloadFileFromBlob, formatDate } from '@easyflow/utils';
|
||||||
|
|
||||||
@@ -61,7 +61,14 @@ import { hasPermission } from '#/api/common/hasPermission';
|
|||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import {
|
import {
|
||||||
canAiResourceDelete,
|
canAiResourceDelete,
|
||||||
canAiResourceOffline,
|
canAiResourceOffline,
|
||||||
@@ -95,19 +102,43 @@ import {
|
|||||||
resolveSkillImportConflictReasonLabel,
|
resolveSkillImportConflictReasonLabel,
|
||||||
resolveSkillImportStep,
|
resolveSkillImportStep,
|
||||||
} from './skill-import';
|
} from './skill-import';
|
||||||
|
import { skillListRouteSchema } from './skill-list-route-state';
|
||||||
import SkillCategoryFormDialog from './SkillCategoryFormDialog.vue';
|
import SkillCategoryFormDialog from './SkillCategoryFormDialog.vue';
|
||||||
import SkillCreateDialog from './SkillCreateDialog.vue';
|
import SkillCreateDialog from './SkillCreateDialog.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const initialListState = parseListRouteState(route.query, skillListRouteSchema);
|
||||||
const pageDataRef = ref<any>();
|
const pageDataRef = ref<any>();
|
||||||
const headerSearchRef = ref<{ reset: () => void }>();
|
const headerSearchRef = ref<{ reset: () => void }>();
|
||||||
const importInputRef = ref<HTMLInputElement>();
|
const importInputRef = ref<HTMLInputElement>();
|
||||||
const categories = ref<SkillCategory[]>([]);
|
const categories = ref<SkillCategory[]>([]);
|
||||||
const categoryLoading = ref(false);
|
const categoryLoading = ref(false);
|
||||||
const selectedRows = ref<SkillInfo[]>([]);
|
const selectedRows = ref<SkillInfo[]>([]);
|
||||||
const selectedCategoryId = ref<number | string>('');
|
const selectedCategoryId = ref<number | string>(initialListState.categoryId);
|
||||||
const filters = reactive({
|
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);
|
const importDialogOpen = ref(false);
|
||||||
@@ -311,6 +342,16 @@ async function loadCategories() {
|
|||||||
const res = await getSkillCategories();
|
const res = await getSkillCategories();
|
||||||
if (res.errorCode !== 0) throw new Error(res.message);
|
if (res.errorCode !== 0) throw new Error(res.message);
|
||||||
categories.value = res.data || [];
|
categories.value = res.data || [];
|
||||||
|
if (
|
||||||
|
selectedCategoryId.value &&
|
||||||
|
!flatCategories.value.some(
|
||||||
|
(category) => String(category.id) === String(selectedCategoryId.value),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selectedCategoryId.value = '';
|
||||||
|
initialCategoryChangePending = false;
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
categories.value = [];
|
categories.value = [];
|
||||||
ElMessage.error(
|
ElMessage.error(
|
||||||
@@ -362,10 +403,53 @@ function selectedTargetCategory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectCategory(data?: SkillCategory) {
|
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();
|
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) {
|
function formatModified(value?: string) {
|
||||||
return value ? formatDate(value, 'YYYY-MM-DD HH:mm') : '—';
|
return value ? formatDate(value, 'YYYY-MM-DD HH:mm') : '—';
|
||||||
}
|
}
|
||||||
@@ -388,6 +472,7 @@ function openDetail(row: SkillInfo) {
|
|||||||
query: {
|
query: {
|
||||||
navTitle: row.displayName || row.name || 'Skill 详情',
|
navTitle: row.displayName || row.name || 'Skill 详情',
|
||||||
pageKey: '/ai/skill',
|
pageKey: '/ai/skill',
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -402,6 +487,7 @@ function handleSkillCreated(payload: {
|
|||||||
...(payload.intent === 'PUBLISH' ? { publishIntent: '1' } : {}),
|
...(payload.intent === 'PUBLISH' ? { publishIntent: '1' } : {}),
|
||||||
navTitle: payload.skill.displayName || payload.skill.name || 'Skill 详情',
|
navTitle: payload.skill.displayName || payload.skill.name || 'Skill 详情',
|
||||||
pageKey: '/ai/skill',
|
pageKey: '/ai/skill',
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1045,6 +1131,7 @@ function isRowBusy(row: SkillInfo) {
|
|||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
ref="headerSearchRef"
|
ref="headerSearchRef"
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="filters.keyword"
|
||||||
search-placeholder="请输入 Skill 名称或描述"
|
search-placeholder="请输入 Skill 名称或描述"
|
||||||
@search="handleHeaderSearch"
|
@search="handleHeaderSearch"
|
||||||
@button-click="handleHeaderButtonClick"
|
@button-click="handleHeaderButtonClick"
|
||||||
@@ -1094,8 +1181,14 @@ function isRowBusy(row: SkillInfo) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/skill/page"
|
page-url="/api/v1/skill/page"
|
||||||
:page-size="12"
|
:page-size="initialListState.pageSize"
|
||||||
:page-sizes="[12, 24, 48]"
|
: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 }">
|
<template #default="{ pageList }">
|
||||||
<ElTable
|
<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,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import {useRoute} from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import {usePreferences} from '@easyflow/preferences';
|
import {usePreferences} from '@easyflow/preferences';
|
||||||
import {getOptions, sortNodes} from '@easyflow/utils';
|
import {getOptions, sortNodes} from '@easyflow/utils';
|
||||||
@@ -22,6 +22,7 @@ import {api} from '#/api/request';
|
|||||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||||
import {$t} from '#/locales';
|
import {$t} from '#/locales';
|
||||||
import {router} from '#/router';
|
import {router} from '#/router';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import {
|
import {
|
||||||
resolveWorkflowShareFailureReason,
|
resolveWorkflowShareFailureReason,
|
||||||
resolveWorkflowShareWorkflowId,
|
resolveWorkflowShareWorkflowId,
|
||||||
@@ -51,11 +52,6 @@ import {
|
|||||||
isWorkflowDataEmpty,
|
isWorkflowDataEmpty,
|
||||||
normalizeWorkflowStartNodes,
|
normalizeWorkflowStartNodes,
|
||||||
} from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
} from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
||||||
import {
|
|
||||||
buildWorkflowListRouteQuery,
|
|
||||||
parseWorkflowDesignReturnState,
|
|
||||||
} from './workflow-list-route-state';
|
|
||||||
|
|
||||||
import '@tinyflow-ai/vue/dist/index.css';
|
import '@tinyflow-ai/vue/dist/index.css';
|
||||||
|
|
||||||
const WORKFLOW_VISIBLE_RENDER_NODE_THRESHOLD = 40;
|
const WORKFLOW_VISIBLE_RENDER_NODE_THRESHOLD = 40;
|
||||||
@@ -161,12 +157,13 @@ async function initializeWorkflow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function backToWorkflowList() {
|
async function backToWorkflowList() {
|
||||||
const listState = parseWorkflowDesignReturnState(route.query);
|
await navigateBackToList(
|
||||||
router.replace({
|
router,
|
||||||
path: '/ai/workflow',
|
route.query,
|
||||||
query: buildWorkflowListRouteQuery(listState),
|
['/ai/workflow'],
|
||||||
});
|
'/ai/workflow',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
||||||
let draftWriteTimer: ReturnType<typeof setTimeout> | undefined;
|
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 CardList from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||||
import { createLazyComponentController } from '#/utils/lazy-component';
|
import { createLazyComponentController } from '#/utils/lazy-component';
|
||||||
@@ -79,9 +84,9 @@ import {
|
|||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildWorkflowDesignReturnQuery,
|
|
||||||
mergeWorkflowListRouteQuery,
|
mergeWorkflowListRouteQuery,
|
||||||
parseWorkflowListRouteState,
|
parseWorkflowListRouteState,
|
||||||
|
workflowListRouteSchema,
|
||||||
} from './workflow-list-route-state';
|
} from './workflow-list-route-state';
|
||||||
|
|
||||||
const ElXMarkdown = defineAsyncComponent(
|
const ElXMarkdown = defineAsyncComponent(
|
||||||
@@ -193,6 +198,7 @@ const actions: ActionButton[] = [
|
|||||||
name: 'RunPage',
|
name: 'RunPage',
|
||||||
query: {
|
query: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -219,6 +225,7 @@ const actions: ActionButton[] = [
|
|||||||
name: 'ExecRecord',
|
name: 'ExecRecord',
|
||||||
query: {
|
query: {
|
||||||
workflowId: row.id,
|
workflowId: row.id,
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -305,6 +312,23 @@ const initialQueryParams = {
|
|||||||
categoryId: initialListState.categoryId || undefined,
|
categoryId: initialListState.categoryId || undefined,
|
||||||
title: initialListState.keyword || 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 dictStore = useDictStore();
|
||||||
const headerButtons = [
|
const headerButtons = [
|
||||||
{
|
{
|
||||||
@@ -389,6 +413,14 @@ function currentListState(): WorkflowListRouteState {
|
|||||||
pageSize: pageState?.pageSize ?? currentPageSize.value,
|
pageSize: pageState?.pageSize ?? currentPageSize.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function currentListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
currentListState(),
|
||||||
|
workflowListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
function syncListRouteState() {
|
function syncListRouteState() {
|
||||||
const target = {
|
const target = {
|
||||||
path: '/ai/workflow',
|
path: '/ai/workflow',
|
||||||
@@ -1134,7 +1166,7 @@ function toDesignPage(row: any) {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/workflow',
|
pageKey: '/ai/workflow',
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
...buildWorkflowDesignReturnQuery(currentListState()),
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1258,7 +1290,7 @@ function changeCategory(category: any) {
|
|||||||
const categoryId = category?.id ?? '';
|
const categoryId = category?.id ?? '';
|
||||||
if (
|
if (
|
||||||
suppressInitialCategoryChange.value &&
|
suppressInitialCategoryChange.value &&
|
||||||
String(categoryId) === initialListState.categoryId
|
String(categoryId) === String(selectedCategoryId.value)
|
||||||
) {
|
) {
|
||||||
suppressInitialCategoryChange.value = false;
|
suppressInitialCategoryChange.value = false;
|
||||||
selectedCategoryId.value = categoryId;
|
selectedCategoryId.value = categoryId;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
import { api, SseClient } from '#/api/request';
|
import { api, SseClient } from '#/api/request';
|
||||||
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
|
||||||
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
|
import { buildAbsoluteAppRouteUrl } from '#/utils/share-route-context';
|
||||||
import { resolveWorkflowShareFailureReason } from '#/utils/workflow-share-context';
|
import { resolveWorkflowShareFailureReason } from '#/utils/workflow-share-context';
|
||||||
@@ -266,6 +267,15 @@ function initializeAdditionalValues() {
|
|||||||
extraSubmitting.value = false;
|
extraSubmitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backToWorkflowList() {
|
||||||
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
['/ai/workflow'],
|
||||||
|
'/ai/workflow',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function submitAdditionalInputs() {
|
async function submitAdditionalInputs() {
|
||||||
if (extraSubmitted.value) {
|
if (extraSubmitted.value) {
|
||||||
return true;
|
return true;
|
||||||
@@ -865,7 +875,7 @@ function executionTraceText(
|
|||||||
circle
|
circle
|
||||||
text
|
text
|
||||||
aria-label="返回工作流"
|
aria-label="返回工作流"
|
||||||
@click="router.replace({ path: '/ai/workflow' })"
|
@click="backToWorkflowList"
|
||||||
/>
|
/>
|
||||||
<ElAvatar
|
<ElAvatar
|
||||||
:size="40"
|
:size="40"
|
||||||
|
|||||||
@@ -22,18 +22,53 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import {
|
||||||
|
navigateBackToList,
|
||||||
|
withListReturnTo,
|
||||||
|
} from '#/router/list-return-context';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
|
import { workflowExecListRouteSchema } from './workflow-exec-list-route-state';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
|
const initialListState = parseListRouteState(
|
||||||
|
$route.query,
|
||||||
|
workflowExecListRouteSchema,
|
||||||
|
);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initDict();
|
initDict();
|
||||||
});
|
});
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
const formInline = 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();
|
const dictStore = useDictStore();
|
||||||
function initDict() {
|
function initDict() {
|
||||||
@@ -42,12 +77,17 @@ function initDict() {
|
|||||||
function search(formEl: FormInstance | undefined) {
|
function search(formEl: FormInstance | undefined) {
|
||||||
formEl?.validate((valid) => {
|
formEl?.validate((valid) => {
|
||||||
if (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) {
|
function reset(formEl: FormInstance | undefined) {
|
||||||
formEl?.resetFields();
|
formInline.value.execKey = '';
|
||||||
|
appliedExecKey.value = '';
|
||||||
|
formEl?.clearValidate();
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.setQuery({});
|
||||||
}
|
}
|
||||||
function remove(row: any) {
|
function remove(row: any) {
|
||||||
@@ -64,7 +104,7 @@ function remove(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
reset(formRef.value);
|
pageDataRef.value?.reload?.();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -83,9 +123,51 @@ function toStepPage(row: any) {
|
|||||||
query: {
|
query: {
|
||||||
recordId: row.id,
|
recordId: row.id,
|
||||||
workflowId: $route.query.workflowId,
|
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) {
|
function getTagType(row: any) {
|
||||||
switch (row.status) {
|
switch (row.status) {
|
||||||
case 1: {
|
case 1: {
|
||||||
@@ -113,10 +195,7 @@ function getTagType(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container border-border border">
|
<div class="page-container border-border border">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<ElButton
|
<ElButton :icon="ArrowLeft" @click="backToWorkflowList">
|
||||||
:icon="ArrowLeft"
|
|
||||||
@click="router.replace({ path: '/ai/workflow' })"
|
|
||||||
>
|
|
||||||
{{ $t('button.back') }}
|
{{ $t('button.back') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,10 +219,15 @@ function getTagType(row: any) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/workflowExecResult/page"
|
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="{
|
:extra-query-params="{
|
||||||
workflowId: $route.query.workflowId,
|
workflowId: $route.query.workflowId,
|
||||||
}"
|
}"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable :data="pageList" border>
|
<ElTable :data="pageList" border>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
|
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,12 +45,18 @@ function reset(formEl: FormInstance | undefined) {
|
|||||||
formEl?.resetFields();
|
formEl?.resetFields();
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.setQuery({});
|
||||||
}
|
}
|
||||||
function backToExecRecords() {
|
async function backToExecRecords() {
|
||||||
const workflowId = $route.query.workflowId;
|
const workflowId = $route.query.workflowId;
|
||||||
void router.replace({
|
const fallbackPath = router.resolve({
|
||||||
name: 'ExecRecord',
|
name: 'ExecRecord',
|
||||||
query: workflowId ? { workflowId } : {},
|
query: workflowId ? { workflowId } : {},
|
||||||
});
|
}).fullPath;
|
||||||
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
$route.query,
|
||||||
|
['/ai/workflow/executeRecords'],
|
||||||
|
fallbackPath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function getTagType(row: any) {
|
function getTagType(row: any) {
|
||||||
switch (row.status) {
|
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 { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildWorkflowDesignReturnQuery,
|
|
||||||
buildWorkflowListRouteQuery,
|
buildWorkflowListRouteQuery,
|
||||||
mergeWorkflowListRouteQuery,
|
mergeWorkflowListRouteQuery,
|
||||||
parseWorkflowDesignReturnState,
|
|
||||||
parseWorkflowListRouteState,
|
parseWorkflowListRouteState,
|
||||||
} from './workflow-list-route-state';
|
} 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 = {
|
const state = {
|
||||||
categoryId: '7',
|
categoryId: '7',
|
||||||
keyword: '月报',
|
keyword: '月报',
|
||||||
@@ -47,15 +45,6 @@ describe('workflow list route state', () => {
|
|||||||
pageSize: 18,
|
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({
|
expect(buildWorkflowListRouteQuery(state)).toEqual({
|
||||||
categoryId: '7',
|
categoryId: '7',
|
||||||
keyword: '月报',
|
keyword: '月报',
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import type { LocationQuery } from 'vue-router';
|
import type { LocationQuery, LocationQueryRaw } from 'vue-router';
|
||||||
|
|
||||||
const DEFAULT_PAGE_NUMBER = 1;
|
import {
|
||||||
const DEFAULT_PAGE_SIZE = 12;
|
buildListRouteStateQuery,
|
||||||
const WORKFLOW_PAGE_SIZES = new Set([12, 18, 24]);
|
defineListRouteStateSchema,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
const LIST_QUERY_KEYS = {
|
parseListRouteState,
|
||||||
categoryId: 'categoryId',
|
positiveIntegerListRouteField,
|
||||||
keyword: 'keyword',
|
stringListRouteField,
|
||||||
pageNumber: 'pageNumber',
|
} from '#/composables/useListRouteState';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkflowListRouteState {
|
interface WorkflowListRouteState {
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
@@ -32,100 +16,43 @@ interface WorkflowListRouteState {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readQueryValue(query: LocationQuery, key: string): string {
|
const workflowListRouteSchema =
|
||||||
const value = query[key];
|
defineListRouteStateSchema<WorkflowListRouteState>({
|
||||||
const normalized = Array.isArray(value) ? value[0] : value;
|
path: '/ai/workflow',
|
||||||
return normalized === null || normalized === undefined
|
fields: {
|
||||||
? ''
|
categoryId: stringListRouteField(),
|
||||||
: String(normalized);
|
keyword: stringListRouteField(),
|
||||||
}
|
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||||
|
pageSize: positiveIntegerListRouteField({
|
||||||
function parsePositiveInteger(value: string, fallback: number): number {
|
allowedValues: [12, 18, 24],
|
||||||
if (!/^\d+$/.test(value)) {
|
defaultValue: 12,
|
||||||
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 } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseWorkflowListRouteState(
|
function parseWorkflowListRouteState(
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
): WorkflowListRouteState {
|
): WorkflowListRouteState {
|
||||||
return parseState(query, LIST_QUERY_KEYS);
|
return parseListRouteState(query, workflowListRouteSchema);
|
||||||
}
|
|
||||||
|
|
||||||
function parseWorkflowDesignReturnState(
|
|
||||||
query: LocationQuery,
|
|
||||||
): WorkflowListRouteState {
|
|
||||||
return parseState(query, RETURN_QUERY_KEYS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWorkflowListRouteQuery(
|
function buildWorkflowListRouteQuery(
|
||||||
state: WorkflowListRouteState,
|
state: WorkflowListRouteState,
|
||||||
): LocationQuery {
|
): LocationQueryRaw {
|
||||||
return buildStateQuery(state, LIST_QUERY_KEYS);
|
return buildListRouteStateQuery(state, workflowListRouteSchema);
|
||||||
}
|
|
||||||
|
|
||||||
function buildWorkflowDesignReturnQuery(
|
|
||||||
state: WorkflowListRouteState,
|
|
||||||
): LocationQuery {
|
|
||||||
return buildStateQuery(state, RETURN_QUERY_KEYS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeWorkflowListRouteQuery(
|
function mergeWorkflowListRouteQuery(
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
state: WorkflowListRouteState,
|
state: WorkflowListRouteState,
|
||||||
): LocationQuery {
|
): LocationQueryRaw {
|
||||||
const listQueryKeys = new Set<string>(Object.values(LIST_QUERY_KEYS));
|
return mergeListRouteStateQuery(query, state, workflowListRouteSchema);
|
||||||
const nextQuery = Object.fromEntries(
|
|
||||||
Object.entries(query).filter(([key]) => !listQueryKeys.has(key)),
|
|
||||||
) as LocationQuery;
|
|
||||||
return {
|
|
||||||
...nextQuery,
|
|
||||||
...buildWorkflowListRouteQuery(state),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildWorkflowDesignReturnQuery,
|
|
||||||
buildWorkflowListRouteQuery,
|
buildWorkflowListRouteQuery,
|
||||||
mergeWorkflowListRouteQuery,
|
mergeWorkflowListRouteQuery,
|
||||||
parseWorkflowDesignReturnState,
|
|
||||||
parseWorkflowListRouteState,
|
parseWorkflowListRouteState,
|
||||||
|
workflowListRouteSchema,
|
||||||
};
|
};
|
||||||
export type { WorkflowListRouteState };
|
export type { WorkflowListRouteState };
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { hasPermission } from '#/api/common/hasPermission';
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import AgentApprovalSnapshotPreview from '#/views/system/approval/components/AgentApprovalSnapshotPreview.vue';
|
import AgentApprovalSnapshotPreview from '#/views/system/approval/components/AgentApprovalSnapshotPreview.vue';
|
||||||
import BotApprovalSnapshotPreview from '#/views/system/approval/components/BotApprovalSnapshotPreview.vue';
|
import BotApprovalSnapshotPreview from '#/views/system/approval/components/BotApprovalSnapshotPreview.vue';
|
||||||
import KnowledgeApprovalSnapshotPreview from '#/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue';
|
import KnowledgeApprovalSnapshotPreview from '#/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue';
|
||||||
@@ -121,21 +122,13 @@ async function loadDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function backToApprovalList() {
|
async function backToApprovalList() {
|
||||||
const tab = String(route.query.tab || '');
|
await navigateBackToList(
|
||||||
if (
|
router,
|
||||||
tab === 'flow' ||
|
route.query,
|
||||||
tab === 'pending' ||
|
['/sys/approval'],
|
||||||
tab === 'processed' ||
|
'/sys/approval',
|
||||||
tab === 'initiated'
|
);
|
||||||
) {
|
|
||||||
void router.replace({
|
|
||||||
path: '/sys/approval',
|
|
||||||
query: { tab },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void router.replace({ path: '/sys/approval' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitApprovalAction(action: 'approve' | 'reject' | 'revoke') {
|
async function submitApprovalAction(action: 'approve' | 'reject' | 'revoke') {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import type {
|
||||||
|
ApprovalListRouteState,
|
||||||
|
ApprovalStatus,
|
||||||
|
ApprovalTabName,
|
||||||
|
} from './approval-list-route-state';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { useAccessStore } from '@easyflow/stores';
|
import { useAccessStore } from '@easyflow/stores';
|
||||||
@@ -26,14 +32,20 @@ import { hasPermission } from '#/api/common/hasPermission';
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
|
|
||||||
import { formatApprovalAccount } from './approval-format';
|
import { formatApprovalAccount } from './approval-format';
|
||||||
|
import { approvalListRouteSchema } from './approval-list-route-state';
|
||||||
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
||||||
|
|
||||||
type ApprovalTabName = 'flow' | 'initiated' | 'pending' | 'processed';
|
|
||||||
|
|
||||||
const RESOURCE_OPTIONS = [
|
const RESOURCE_OPTIONS = [
|
||||||
{ label: $t('approval.resource.agent'), value: 'AGENT' },
|
{ label: $t('approval.resource.agent'), value: 'AGENT' },
|
||||||
{ label: $t('approval.resource.bot'), value: 'BOT' },
|
{ label: $t('approval.resource.bot'), value: 'BOT' },
|
||||||
@@ -94,9 +106,17 @@ const TAB_CONFIG = [
|
|||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const initialListState = parseListRouteState(
|
||||||
|
route.query,
|
||||||
|
approvalListRouteSchema,
|
||||||
|
);
|
||||||
|
const initialApprovalStatus = normalizeApprovalStatus(
|
||||||
|
initialListState.tab,
|
||||||
|
initialListState.status,
|
||||||
|
);
|
||||||
const flowQueryRef = ref<FormInstance>();
|
const flowQueryRef = ref<FormInstance>();
|
||||||
const instanceQueryRef = ref<FormInstance>();
|
const instanceQueryRef = ref<FormInstance>();
|
||||||
const activeTab = ref<ApprovalTabName>('flow');
|
const activeTab = ref<ApprovalTabName>(initialListState.tab);
|
||||||
const flowModalRef = ref();
|
const flowModalRef = ref();
|
||||||
const flowPageRef = ref();
|
const flowPageRef = ref();
|
||||||
const pendingPageRef = ref();
|
const pendingPageRef = ref();
|
||||||
@@ -104,20 +124,28 @@ const processedPageRef = ref();
|
|||||||
const initiatedPageRef = ref();
|
const initiatedPageRef = ref();
|
||||||
const pendingBadgeCount = ref(0);
|
const pendingBadgeCount = ref(0);
|
||||||
const approvalActionLoadingKey = ref('');
|
const approvalActionLoadingKey = ref('');
|
||||||
|
const currentPageNumber = ref(initialListState.pageNumber);
|
||||||
|
const currentPageSize = ref(initialListState.pageSize);
|
||||||
|
|
||||||
const flowQuery = ref({
|
const flowQuery = ref({
|
||||||
actionType: '',
|
actionType:
|
||||||
name: '',
|
initialListState.tab === 'flow' ? initialListState.actionType : '',
|
||||||
resourceType: '',
|
name: initialListState.tab === 'flow' ? initialListState.name : '',
|
||||||
status: '',
|
resourceType:
|
||||||
|
initialListState.tab === 'flow' ? initialListState.resourceType : '',
|
||||||
|
status: initialListState.tab === 'flow' ? initialApprovalStatus : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const instanceQuery = ref({
|
const instanceQuery = ref({
|
||||||
actionType: '',
|
actionType:
|
||||||
keyword: '',
|
initialListState.tab === 'flow' ? '' : initialListState.actionType,
|
||||||
resourceType: '',
|
keyword: initialListState.tab === 'flow' ? '' : initialListState.keyword,
|
||||||
status: '',
|
resourceType:
|
||||||
|
initialListState.tab === 'flow' ? '' : initialListState.resourceType,
|
||||||
|
status: initialListState.tab === 'flow' ? '' : initialApprovalStatus,
|
||||||
});
|
});
|
||||||
|
const appliedFlowQuery = ref({ ...flowQuery.value });
|
||||||
|
const appliedInstanceQuery = ref({ ...instanceQuery.value });
|
||||||
const pendingBadgeText = computed(() =>
|
const pendingBadgeText = computed(() =>
|
||||||
pendingBadgeCount.value > 99 ? '99+' : String(pendingBadgeCount.value),
|
pendingBadgeCount.value > 99 ? '99+' : String(pendingBadgeCount.value),
|
||||||
);
|
);
|
||||||
@@ -156,32 +184,44 @@ const instanceStatusOptions = computed(() => {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void syncRouteTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(activeTab, () => {
|
|
||||||
if (
|
|
||||||
instanceQuery.value.status &&
|
|
||||||
!instanceStatusOptions.value.some(
|
|
||||||
(item) => item.value === instanceQuery.value.status,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
instanceQuery.value.status = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
watch(
|
watch(
|
||||||
[() => route.fullPath, visibleTabNames],
|
[() => route.path, () => route.query.tab, visibleTabNames],
|
||||||
async () => {
|
async () => {
|
||||||
await syncRouteTab();
|
await syncRouteTab();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
watchListRouteState(route, approvalListRouteSchema, (state) => {
|
||||||
|
if (state.tab !== activeTab.value || !hasTab(state.tab)) return;
|
||||||
|
void restoreApprovalRouteState(state);
|
||||||
|
});
|
||||||
|
|
||||||
function hasTab(name: ApprovalTabName) {
|
function hasTab(name: ApprovalTabName) {
|
||||||
return visibleTabNames.value.includes(name);
|
return visibleTabNames.value.includes(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeApprovalStatus(
|
||||||
|
tab: ApprovalTabName,
|
||||||
|
status: ApprovalStatus,
|
||||||
|
): ApprovalStatus {
|
||||||
|
if (tab === 'flow') {
|
||||||
|
return FLOW_STATUS_OPTIONS.some((item) => item.value === status)
|
||||||
|
? status
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
if (tab === 'processed') {
|
||||||
|
return PROCESSED_STATUS_OPTIONS.some((item) => item.value === status)
|
||||||
|
? status
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
if (tab === 'initiated') {
|
||||||
|
return INITIATED_STATUS_OPTIONS.some((item) => item.value === status)
|
||||||
|
? status
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTabNameByPath(path: string): ApprovalTabName {
|
function resolveTabNameByPath(path: string): ApprovalTabName {
|
||||||
if (path.endsWith('/pending')) {
|
if (path.endsWith('/pending')) {
|
||||||
return 'pending';
|
return 'pending';
|
||||||
@@ -223,7 +263,11 @@ async function syncRouteTab() {
|
|||||||
if (fallbackTab.name !== 'flow') {
|
if (fallbackTab.name !== 'flow') {
|
||||||
await router.replace({
|
await router.replace({
|
||||||
path: '/sys/approval',
|
path: '/sys/approval',
|
||||||
query: { tab: fallbackTab.name },
|
query: mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
defaultApprovalListState(fallbackTab.name),
|
||||||
|
approvalListRouteSchema,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -233,8 +277,13 @@ async function syncRouteTab() {
|
|||||||
if (visibleTabNames.value.includes(expectedTab)) {
|
if (visibleTabNames.value.includes(expectedTab)) {
|
||||||
activeTab.value = expectedTab;
|
activeTab.value = expectedTab;
|
||||||
} else {
|
} else {
|
||||||
const fallbackQuery =
|
const fallbackQuery = fallbackTab
|
||||||
fallbackTab?.name === 'flow' ? {} : { tab: fallbackTab?.name };
|
? mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
defaultApprovalListState(fallbackTab.name),
|
||||||
|
approvalListRouteSchema,
|
||||||
|
)
|
||||||
|
: {};
|
||||||
const currentQueryTab = String(route.query.tab || '');
|
const currentQueryTab = String(route.query.tab || '');
|
||||||
if (
|
if (
|
||||||
fallbackTab &&
|
fallbackTab &&
|
||||||
@@ -261,7 +310,12 @@ function handleTabChange(name: number | string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = visibleTabs.value.find((item) => item.name === name);
|
const target = visibleTabs.value.find((item) => item.name === name);
|
||||||
const nextQuery = name === 'flow' ? {} : { tab: name };
|
const nextTab = name as ApprovalTabName;
|
||||||
|
const nextQuery = mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
defaultApprovalListState(nextTab),
|
||||||
|
approvalListRouteSchema,
|
||||||
|
);
|
||||||
const currentQueryTab = String(route.query.tab || '');
|
const currentQueryTab = String(route.query.tab || '');
|
||||||
const currentTab = currentQueryTab || 'flow';
|
const currentTab = currentQueryTab || 'flow';
|
||||||
if (!target || currentTab === name) {
|
if (!target || currentTab === name) {
|
||||||
@@ -273,6 +327,167 @@ function handleTabChange(name: number | string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initialPageNumberFor(tab: ApprovalTabName) {
|
||||||
|
const state = parseListRouteState(route.query, approvalListRouteSchema);
|
||||||
|
return state.tab === tab ? state.pageNumber : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialPageSizeFor(tab: ApprovalTabName) {
|
||||||
|
const state = parseListRouteState(route.query, approvalListRouteSchema);
|
||||||
|
return state.tab === tab ? state.pageSize : 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialQueryFor(tab: ApprovalTabName) {
|
||||||
|
const state = parseListRouteState(route.query, approvalListRouteSchema);
|
||||||
|
if (state.tab !== tab) return {};
|
||||||
|
const status = normalizeApprovalStatus(tab, state.status);
|
||||||
|
if (tab === 'flow') {
|
||||||
|
return {
|
||||||
|
actionType: state.actionType || undefined,
|
||||||
|
name: state.name || undefined,
|
||||||
|
resourceType: state.resourceType || undefined,
|
||||||
|
status: status || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
actionType: state.actionType || undefined,
|
||||||
|
keyword: state.keyword || undefined,
|
||||||
|
resourceType: state.resourceType || undefined,
|
||||||
|
status: status || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultApprovalListState(tab: ApprovalTabName) {
|
||||||
|
return {
|
||||||
|
actionType: '' as const,
|
||||||
|
keyword: '',
|
||||||
|
name: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
resourceType: '' as const,
|
||||||
|
status: '' as const,
|
||||||
|
tab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function approvalPageRef(tab: ApprovalTabName) {
|
||||||
|
if (tab === 'flow') return flowPageRef.value;
|
||||||
|
if (tab === 'pending') return pendingPageRef.value;
|
||||||
|
if (tab === 'processed') return processedPageRef.value;
|
||||||
|
return initiatedPageRef.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function approvalQueryParams(
|
||||||
|
tab: ApprovalTabName,
|
||||||
|
query: Partial<
|
||||||
|
Record<
|
||||||
|
'actionType' | 'keyword' | 'name' | 'resourceType' | 'status',
|
||||||
|
string
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
if (tab === 'flow') {
|
||||||
|
return {
|
||||||
|
actionType: query.actionType || undefined,
|
||||||
|
name: query.name || undefined,
|
||||||
|
resourceType: query.resourceType || undefined,
|
||||||
|
status: query.status || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
actionType: query.actionType || undefined,
|
||||||
|
keyword: query.keyword || undefined,
|
||||||
|
resourceType: query.resourceType || undefined,
|
||||||
|
status: query.status || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreApprovalRouteState(state: ApprovalListRouteState) {
|
||||||
|
const status = normalizeApprovalStatus(state.tab, state.status);
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
if (state.tab === 'flow') {
|
||||||
|
const nextQuery = {
|
||||||
|
actionType: state.actionType,
|
||||||
|
name: state.name,
|
||||||
|
resourceType: state.resourceType,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
flowQuery.value = { ...nextQuery };
|
||||||
|
appliedFlowQuery.value = { ...nextQuery };
|
||||||
|
} else {
|
||||||
|
const nextQuery = {
|
||||||
|
actionType: state.actionType,
|
||||||
|
keyword: state.keyword,
|
||||||
|
resourceType: state.resourceType,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
instanceQuery.value = { ...nextQuery };
|
||||||
|
appliedInstanceQuery.value = { ...nextQuery };
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
const appliedQuery =
|
||||||
|
state.tab === 'flow' ? appliedFlowQuery.value : appliedInstanceQuery.value;
|
||||||
|
approvalPageRef(state.tab)?.restoreState?.({
|
||||||
|
pageNumber: state.pageNumber,
|
||||||
|
pageSize: state.pageSize,
|
||||||
|
queryParams: approvalQueryParams(state.tab, appliedQuery),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentListState(
|
||||||
|
pageNumber = currentPageNumber.value,
|
||||||
|
pageSize = currentPageSize.value,
|
||||||
|
) {
|
||||||
|
if (activeTab.value === 'flow') {
|
||||||
|
return {
|
||||||
|
actionType: appliedFlowQuery.value.actionType,
|
||||||
|
keyword: '',
|
||||||
|
name: appliedFlowQuery.value.name,
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
resourceType: appliedFlowQuery.value.resourceType,
|
||||||
|
status: appliedFlowQuery.value.status,
|
||||||
|
tab: activeTab.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
actionType: appliedInstanceQuery.value.actionType,
|
||||||
|
keyword: appliedInstanceQuery.value.keyword,
|
||||||
|
name: '',
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
resourceType: appliedInstanceQuery.value.resourceType,
|
||||||
|
status: appliedInstanceQuery.value.status,
|
||||||
|
tab: activeTab.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
currentListState(),
|
||||||
|
approvalListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageStateChange(
|
||||||
|
tab: ApprovalTabName,
|
||||||
|
state: { pageNumber: number; pageSize: number },
|
||||||
|
) {
|
||||||
|
if (activeTab.value !== tab) return;
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
const query = mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
currentListState(state.pageNumber, state.pageSize),
|
||||||
|
approvalListRouteSchema,
|
||||||
|
);
|
||||||
|
const target = router.resolve({ path: approvalListRouteSchema.path, query });
|
||||||
|
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||||
|
}
|
||||||
|
|
||||||
function reloadCurrentTab() {
|
function reloadCurrentTab() {
|
||||||
if (activeTab.value === 'flow') {
|
if (activeTab.value === 'flow') {
|
||||||
flowPageRef.value?.reload();
|
flowPageRef.value?.reload();
|
||||||
@@ -306,40 +521,53 @@ async function refreshPendingBadgeCount() {
|
|||||||
function searchFlow(formEl?: FormInstance) {
|
function searchFlow(formEl?: FormInstance) {
|
||||||
formEl?.validate((valid) => {
|
formEl?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
flowPageRef.value?.setQuery(flowQuery.value);
|
appliedFlowQuery.value = { ...flowQuery.value };
|
||||||
|
flowPageRef.value?.setQuery(
|
||||||
|
approvalQueryParams('flow', appliedFlowQuery.value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFlow(formEl?: FormInstance) {
|
function resetFlow(formEl?: FormInstance) {
|
||||||
formEl?.resetFields();
|
flowQuery.value = {
|
||||||
|
actionType: '',
|
||||||
|
name: '',
|
||||||
|
resourceType: '',
|
||||||
|
status: '',
|
||||||
|
};
|
||||||
|
appliedFlowQuery.value = { ...flowQuery.value };
|
||||||
|
formEl?.clearValidate();
|
||||||
flowPageRef.value?.setQuery({});
|
flowPageRef.value?.setQuery({});
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchInstance(formEl?: FormInstance) {
|
function searchInstance(formEl?: FormInstance) {
|
||||||
if (!formEl) {
|
if (!formEl) {
|
||||||
currentInstancePageRef()?.setQuery(instanceQuery.value);
|
appliedInstanceQuery.value = { ...instanceQuery.value };
|
||||||
|
currentInstancePageRef()?.setQuery(
|
||||||
|
approvalQueryParams(activeTab.value, appliedInstanceQuery.value),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formEl?.validate((valid) => {
|
formEl?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
currentInstancePageRef()?.setQuery(instanceQuery.value);
|
appliedInstanceQuery.value = { ...instanceQuery.value };
|
||||||
|
currentInstancePageRef()?.setQuery(
|
||||||
|
approvalQueryParams(activeTab.value, appliedInstanceQuery.value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetInstance(formEl?: FormInstance) {
|
function resetInstance(formEl?: FormInstance) {
|
||||||
if (!formEl) {
|
instanceQuery.value = {
|
||||||
instanceQuery.value = {
|
actionType: '',
|
||||||
actionType: '',
|
keyword: '',
|
||||||
keyword: '',
|
resourceType: '',
|
||||||
resourceType: '',
|
status: '',
|
||||||
status: '',
|
};
|
||||||
};
|
appliedInstanceQuery.value = { ...instanceQuery.value };
|
||||||
currentInstancePageRef()?.setQuery({});
|
formEl?.clearValidate();
|
||||||
return;
|
|
||||||
}
|
|
||||||
formEl?.resetFields();
|
|
||||||
currentInstancePageRef()?.setQuery({});
|
currentInstancePageRef()?.setQuery({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +699,7 @@ function openInstanceDetail(row: any) {
|
|||||||
path: `/sys/approval/detail/${row.id}`,
|
path: `/sys/approval/detail/${row.id}`,
|
||||||
query: {
|
query: {
|
||||||
tab: activeTab.value,
|
tab: activeTab.value,
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -521,6 +750,7 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<ElTabPane
|
<ElTabPane
|
||||||
v-if="hasTab('flow')"
|
v-if="hasTab('flow')"
|
||||||
:label="$t('approval.tab.flow')"
|
:label="$t('approval.tab.flow')"
|
||||||
|
lazy
|
||||||
name="flow"
|
name="flow"
|
||||||
>
|
>
|
||||||
<ListPageShell>
|
<ListPageShell>
|
||||||
@@ -597,7 +827,10 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="flowPageRef"
|
ref="flowPageRef"
|
||||||
page-url="/api/v1/approvalFlow/page"
|
page-url="/api/v1/approvalFlow/page"
|
||||||
:page-size="10"
|
:page-size="initialPageSizeFor('flow')"
|
||||||
|
:initial-page-number="initialPageNumberFor('flow')"
|
||||||
|
:initial-query-params="initialQueryFor('flow')"
|
||||||
|
@state-change="handlePageStateChange('flow', $event)"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable
|
<ElTable
|
||||||
@@ -702,7 +935,7 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
</ListPageShell>
|
</ListPageShell>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|
||||||
<ElTabPane v-if="hasTab('pending')" name="pending">
|
<ElTabPane v-if="hasTab('pending')" lazy name="pending">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="approval-manage__tab-label">
|
<span class="approval-manage__tab-label">
|
||||||
<span>{{ $t('approval.tab.pending') }}</span>
|
<span>{{ $t('approval.tab.pending') }}</span>
|
||||||
@@ -771,7 +1004,10 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pendingPageRef"
|
ref="pendingPageRef"
|
||||||
page-url="/api/v1/approvalInstance/pendingPage"
|
page-url="/api/v1/approvalInstance/pendingPage"
|
||||||
:page-size="10"
|
:page-size="initialPageSizeFor('pending')"
|
||||||
|
:initial-page-number="initialPageNumberFor('pending')"
|
||||||
|
:initial-query-params="initialQueryFor('pending')"
|
||||||
|
@state-change="handlePageStateChange('pending', $event)"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable
|
<ElTable
|
||||||
@@ -888,6 +1124,7 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<ElTabPane
|
<ElTabPane
|
||||||
v-if="hasTab('processed')"
|
v-if="hasTab('processed')"
|
||||||
:label="$t('approval.tab.processed')"
|
:label="$t('approval.tab.processed')"
|
||||||
|
lazy
|
||||||
name="processed"
|
name="processed"
|
||||||
>
|
>
|
||||||
<ListPageShell>
|
<ListPageShell>
|
||||||
@@ -969,7 +1206,10 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="processedPageRef"
|
ref="processedPageRef"
|
||||||
page-url="/api/v1/approvalInstance/processedPage"
|
page-url="/api/v1/approvalInstance/processedPage"
|
||||||
:page-size="10"
|
:page-size="initialPageSizeFor('processed')"
|
||||||
|
:initial-page-number="initialPageNumberFor('processed')"
|
||||||
|
:initial-query-params="initialQueryFor('processed')"
|
||||||
|
@state-change="handlePageStateChange('processed', $event)"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable
|
<ElTable
|
||||||
@@ -1059,6 +1299,7 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<ElTabPane
|
<ElTabPane
|
||||||
v-if="hasTab('initiated')"
|
v-if="hasTab('initiated')"
|
||||||
:label="$t('approval.tab.initiated')"
|
:label="$t('approval.tab.initiated')"
|
||||||
|
lazy
|
||||||
name="initiated"
|
name="initiated"
|
||||||
>
|
>
|
||||||
<ListPageShell>
|
<ListPageShell>
|
||||||
@@ -1140,7 +1381,10 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="initiatedPageRef"
|
ref="initiatedPageRef"
|
||||||
page-url="/api/v1/approvalInstance/initiatedPage"
|
page-url="/api/v1/approvalInstance/initiatedPage"
|
||||||
:page-size="10"
|
:page-size="initialPageSizeFor('initiated')"
|
||||||
|
:initial-page-number="initialPageNumberFor('initiated')"
|
||||||
|
:initial-query-params="initialQueryFor('initiated')"
|
||||||
|
@state-change="handlePageStateChange('initiated', $event)"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable
|
<ElTable
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
defineListRouteStateSchema,
|
||||||
|
enumListRouteField,
|
||||||
|
positiveIntegerListRouteField,
|
||||||
|
stringListRouteField,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
|
|
||||||
|
type ApprovalTabName = 'flow' | 'initiated' | 'pending' | 'processed';
|
||||||
|
type ApprovalActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||||
|
type ApprovalResourceType = '' | 'AGENT' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||||
|
type ApprovalStatus =
|
||||||
|
| ''
|
||||||
|
| 'APPROVED'
|
||||||
|
| 'DISABLED'
|
||||||
|
| 'ENABLED'
|
||||||
|
| 'PENDING'
|
||||||
|
| 'PROCESSING'
|
||||||
|
| 'REJECTED'
|
||||||
|
| 'REVOKED';
|
||||||
|
|
||||||
|
interface ApprovalListRouteState {
|
||||||
|
actionType: ApprovalActionType;
|
||||||
|
keyword: string;
|
||||||
|
name: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
resourceType: ApprovalResourceType;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
tab: ApprovalTabName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const approvalListRouteSchema =
|
||||||
|
defineListRouteStateSchema<ApprovalListRouteState>({
|
||||||
|
path: '/sys/approval',
|
||||||
|
fields: {
|
||||||
|
actionType: enumListRouteField(['DELETE', 'OFFLINE', 'PUBLISH'], ''),
|
||||||
|
keyword: stringListRouteField(),
|
||||||
|
name: stringListRouteField(),
|
||||||
|
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||||
|
pageSize: positiveIntegerListRouteField({
|
||||||
|
allowedValues: [10, 20, 50, 100],
|
||||||
|
defaultValue: 10,
|
||||||
|
}),
|
||||||
|
resourceType: enumListRouteField(
|
||||||
|
['AGENT', 'BOT', 'KNOWLEDGE', 'WORKFLOW'],
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
status: enumListRouteField(
|
||||||
|
[
|
||||||
|
'APPROVED',
|
||||||
|
'DISABLED',
|
||||||
|
'ENABLED',
|
||||||
|
'PENDING',
|
||||||
|
'PROCESSING',
|
||||||
|
'REJECTED',
|
||||||
|
'REVOKED',
|
||||||
|
],
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
tab: enumListRouteField(
|
||||||
|
['flow', 'pending', 'processed', 'initiated'],
|
||||||
|
'flow',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { approvalListRouteSchema };
|
||||||
|
export type { ApprovalListRouteState, ApprovalStatus, ApprovalTabName };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
defineListRouteStateSchema,
|
||||||
|
enumListRouteField,
|
||||||
|
positiveIntegerListRouteField,
|
||||||
|
stringListRouteField,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
|
|
||||||
|
interface SysFeedbackListRouteState {
|
||||||
|
feedbackContent: string;
|
||||||
|
feedbackType: '1' | '2' | '3' | '4' | '';
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
status: '0' | '1' | '2' | '3' | '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sysFeedbackListRouteSchema =
|
||||||
|
defineListRouteStateSchema<SysFeedbackListRouteState>({
|
||||||
|
path: '/sys/sysFeedback',
|
||||||
|
fields: {
|
||||||
|
feedbackContent: stringListRouteField(),
|
||||||
|
feedbackType: enumListRouteField(['1', '2', '3', '4'], ''),
|
||||||
|
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||||
|
pageSize: positiveIntegerListRouteField({
|
||||||
|
allowedValues: [10, 20, 50, 100],
|
||||||
|
defaultValue: 10,
|
||||||
|
}),
|
||||||
|
status: enumListRouteField(['0', '1', '2', '3'], ''),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { sysFeedbackListRouteSchema };
|
||||||
|
export type { SysFeedbackListRouteState };
|
||||||
@@ -17,6 +17,7 @@ import { tryit } from 'radash';
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
|
|
||||||
const feedbackTypeOptions = [
|
const feedbackTypeOptions = [
|
||||||
{
|
{
|
||||||
@@ -95,6 +96,14 @@ async function markStatus(_status: number) {
|
|||||||
}
|
}
|
||||||
loading[key] = false;
|
loading[key] = false;
|
||||||
}
|
}
|
||||||
|
async function backToFeedbackList() {
|
||||||
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
['/sys/sysFeedback'],
|
||||||
|
'/sys/sysFeedback',
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -102,7 +111,7 @@ async function markStatus(_status: number) {
|
|||||||
<ElButton
|
<ElButton
|
||||||
class="absolute left-5 top-5"
|
class="absolute left-5 top-5"
|
||||||
:icon="ArrowLeft"
|
:icon="ArrowLeft"
|
||||||
@click="router.replace({ path: '/sys/sysFeedback' })"
|
@click="backToFeedbackList"
|
||||||
>
|
>
|
||||||
{{ $t('button.back') }}
|
{{ $t('button.back') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { IconifyIcon } from '@easyflow/icons';
|
import { IconifyIcon } from '@easyflow/icons';
|
||||||
|
|
||||||
@@ -26,8 +27,23 @@ import { api } from '#/api/request';
|
|||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
|
|
||||||
|
import { sysFeedbackListRouteSchema } from './sys-feedback-list-route-state';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const initialListState = parseListRouteState(
|
||||||
|
route.query,
|
||||||
|
sysFeedbackListRouteSchema,
|
||||||
|
);
|
||||||
|
|
||||||
const feedbackTypeOptions = [
|
const feedbackTypeOptions = [
|
||||||
{
|
{
|
||||||
@@ -68,21 +84,49 @@ const statusType: Record<number, any> = {
|
|||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
feedbackType: '',
|
feedbackType: initialListState.feedbackType
|
||||||
status: '',
|
? Number(initialListState.feedbackType)
|
||||||
feedbackContent: '',
|
: '',
|
||||||
|
status: initialListState.status,
|
||||||
|
feedbackContent: initialListState.feedbackContent,
|
||||||
});
|
});
|
||||||
|
const appliedFormData = ref({ ...formData.value });
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
|
const currentPageNumber = ref(initialListState.pageNumber);
|
||||||
|
const currentPageSize = ref(initialListState.pageSize);
|
||||||
|
watchListRouteState(route, sysFeedbackListRouteSchema, (state) => {
|
||||||
|
const nextFormData = {
|
||||||
|
feedbackContent: state.feedbackContent,
|
||||||
|
feedbackType: state.feedbackType ? Number(state.feedbackType) : '',
|
||||||
|
status: state.status,
|
||||||
|
};
|
||||||
|
formData.value = { ...nextFormData };
|
||||||
|
appliedFormData.value = { ...nextFormData };
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
pageDataRef.value?.restoreState?.({
|
||||||
|
pageNumber: state.pageNumber,
|
||||||
|
pageSize: state.pageSize,
|
||||||
|
queryParams: {
|
||||||
|
feedbackContent: state.feedbackContent || undefined,
|
||||||
|
feedbackType: state.feedbackType || undefined,
|
||||||
|
status: state.status || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function search(formEl?: FormInstance) {
|
function search(formEl?: FormInstance) {
|
||||||
formEl?.validate((valid) => {
|
formEl?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
pageDataRef.value.setQuery(formData.value);
|
appliedFormData.value = { ...formData.value };
|
||||||
|
pageDataRef.value.setQuery(appliedFormData.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function reset(formEl?: FormInstance) {
|
function reset(formEl?: FormInstance) {
|
||||||
formEl?.resetFields();
|
formData.value = { feedbackContent: '', feedbackType: '', status: '' };
|
||||||
|
appliedFormData.value = { ...formData.value };
|
||||||
|
formEl?.clearValidate();
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.setQuery({});
|
||||||
}
|
}
|
||||||
function getFeedbackType(type: number) {
|
function getFeedbackType(type: number) {
|
||||||
@@ -96,9 +140,62 @@ async function markStatus(row: any, status: number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.errorCode === 0) {
|
if (res && res.errorCode === 0) {
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function currentListState(
|
||||||
|
pageNumber = currentPageNumber.value,
|
||||||
|
pageSize = currentPageSize.value,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
feedbackContent: appliedFormData.value.feedbackContent,
|
||||||
|
feedbackType: String(appliedFormData.value.feedbackType || '') as
|
||||||
|
| '1'
|
||||||
|
| '2'
|
||||||
|
| '3'
|
||||||
|
| '4'
|
||||||
|
| '',
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
status: String(appliedFormData.value.status || '') as
|
||||||
|
| '0'
|
||||||
|
| '1'
|
||||||
|
| '2'
|
||||||
|
| '3'
|
||||||
|
| '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function currentListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
currentListState(),
|
||||||
|
sysFeedbackListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function handlePageStateChange(state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
const query = mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
currentListState(state.pageNumber, state.pageSize),
|
||||||
|
sysFeedbackListRouteSchema,
|
||||||
|
);
|
||||||
|
const target = router.resolve({
|
||||||
|
path: sysFeedbackListRouteSchema.path,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||||
|
}
|
||||||
|
function openFeedbackDetail(row: any) {
|
||||||
|
router.push({
|
||||||
|
path: `/sys/sysFeedback/${row.id}`,
|
||||||
|
query: withListReturnTo(currentListFullPath()),
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -142,7 +239,14 @@ async function markStatus(row: any, status: number) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysUserFeedback/page"
|
page-url="/api/v1/sysUserFeedback/page"
|
||||||
:page-size="10"
|
:page-size="initialListState.pageSize"
|
||||||
|
:initial-page-number="initialListState.pageNumber"
|
||||||
|
:initial-query-params="{
|
||||||
|
feedbackContent: initialListState.feedbackContent || undefined,
|
||||||
|
feedbackType: initialListState.feedbackType || undefined,
|
||||||
|
status: initialListState.status || undefined,
|
||||||
|
}"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable border show-overflow-tooltip :data="pageList">
|
<ElTable border show-overflow-tooltip :data="pageList">
|
||||||
@@ -203,7 +307,7 @@ async function markStatus(row: any, status: number) {
|
|||||||
<ElButton
|
<ElButton
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="router.push(`/sys/sysFeedback/${row.id}`)"
|
@click="openFeedbackDetail(row)"
|
||||||
>
|
>
|
||||||
{{ $t('button.view') }}
|
{{ $t('button.view') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
|
||||||
|
|
||||||
import { markRaw, onMounted, ref } from 'vue';
|
import { markRaw, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CaretRight,
|
CaretRight,
|
||||||
@@ -26,17 +25,47 @@ import { api } from '#/api/request';
|
|||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
|
import {
|
||||||
|
buildListRouteFullPath,
|
||||||
|
mergeListRouteStateQuery,
|
||||||
|
parseListRouteState,
|
||||||
|
watchListRouteState,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
import { withListReturnTo } from '#/router/list-return-context';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
|
import { sysJobListRouteSchema } from './sys-job-list-route-state';
|
||||||
import SysJobModal from './SysJobModal.vue';
|
import SysJobModal from './SysJobModal.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const initialListState = parseListRouteState(
|
||||||
|
route.query,
|
||||||
|
sysJobListRouteSchema,
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initDict();
|
initDict();
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
|
const searchKeyword = ref(initialListState.keyword);
|
||||||
|
const currentPageNumber = ref(initialListState.pageNumber);
|
||||||
|
const currentPageSize = ref(initialListState.pageSize);
|
||||||
|
watchListRouteState(route, sysJobListRouteSchema, (state) => {
|
||||||
|
searchKeyword.value = state.keyword;
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
pageDataRef.value?.restoreState?.({
|
||||||
|
pageNumber: state.pageNumber,
|
||||||
|
pageSize: state.pageSize,
|
||||||
|
queryParams: {
|
||||||
|
isQueryOr: true,
|
||||||
|
jobName: state.keyword || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
const saveDialog = ref();
|
const saveDialog = ref();
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const headerButtons = [
|
const headerButtons = [
|
||||||
@@ -57,11 +86,14 @@ function initDict() {
|
|||||||
dictStore.fetchDictionary('misfirePolicy');
|
dictStore.fetchDictionary('misfirePolicy');
|
||||||
}
|
}
|
||||||
const handleSearch = (params: string) => {
|
const handleSearch = (params: string) => {
|
||||||
pageDataRef.value.setQuery({ jobName: params, isQueryOr: true });
|
searchKeyword.value = params;
|
||||||
|
pageDataRef.value.setQuery({
|
||||||
|
jobName: params || undefined,
|
||||||
|
isQueryOr: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
function reset(formEl?: FormInstance) {
|
function reloadCurrentList() {
|
||||||
formEl?.resetFields();
|
pageDataRef.value?.reload?.();
|
||||||
pageDataRef.value.setQuery({});
|
|
||||||
}
|
}
|
||||||
function showDialog(row: any) {
|
function showDialog(row: any) {
|
||||||
saveDialog.value.openDialog({ ...row });
|
saveDialog.value.openDialog({ ...row });
|
||||||
@@ -80,7 +112,7 @@ function remove(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
reset();
|
reloadCurrentList();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -105,7 +137,7 @@ function start(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
reset();
|
reloadCurrentList();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -127,7 +159,7 @@ function stop(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
reset();
|
reloadCurrentList();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -142,18 +174,52 @@ function toLogPage(row: any) {
|
|||||||
name: 'SysJobLog',
|
name: 'SysJobLog',
|
||||||
query: {
|
query: {
|
||||||
jobId: row.id,
|
jobId: row.id,
|
||||||
|
...withListReturnTo(currentListFullPath()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentListFullPath() {
|
||||||
|
return buildListRouteFullPath(
|
||||||
|
router,
|
||||||
|
route.query,
|
||||||
|
{
|
||||||
|
keyword: searchKeyword.value,
|
||||||
|
pageNumber: currentPageNumber.value,
|
||||||
|
pageSize: currentPageSize.value,
|
||||||
|
},
|
||||||
|
sysJobListRouteSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageStateChange(state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
const query = mergeListRouteStateQuery(
|
||||||
|
route.query,
|
||||||
|
{
|
||||||
|
keyword: searchKeyword.value,
|
||||||
|
pageNumber: state.pageNumber,
|
||||||
|
pageSize: state.pageSize,
|
||||||
|
},
|
||||||
|
sysJobListRouteSchema,
|
||||||
|
);
|
||||||
|
const target = router.resolve({ path: sysJobListRouteSchema.path, query });
|
||||||
|
if (target.fullPath !== route.fullPath) void router.replace(target);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysJobModal ref="saveDialog" @reload="reset" />
|
<SysJobModal ref="saveDialog" @reload="reloadCurrentList" />
|
||||||
<ListPageShell>
|
<ListPageShell>
|
||||||
<template #filters>
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({})"
|
@button-click="showDialog({})"
|
||||||
/>
|
/>
|
||||||
@@ -161,7 +227,13 @@ function toLogPage(row: any) {
|
|||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysJob/page"
|
page-url="/api/v1/sysJob/page"
|
||||||
:page-size="10"
|
:page-size="initialListState.pageSize"
|
||||||
|
:initial-page-number="initialListState.pageNumber"
|
||||||
|
:initial-query-params="{
|
||||||
|
jobName: initialListState.keyword || undefined,
|
||||||
|
isQueryOr: true,
|
||||||
|
}"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable :data="pageList" border>
|
<ElTable :data="pageList" border>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { navigateBackToList } from '#/router/list-return-context';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -43,15 +44,20 @@ function reset(formEl: FormInstance | undefined) {
|
|||||||
formEl?.resetFields();
|
formEl?.resetFields();
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.setQuery({});
|
||||||
}
|
}
|
||||||
|
async function backToJobList() {
|
||||||
|
await navigateBackToList(
|
||||||
|
router,
|
||||||
|
$route.query,
|
||||||
|
['/sys/sysJob'],
|
||||||
|
'/sys/sysJob',
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<ElButton
|
<ElButton :icon="ArrowLeft" @click="backToJobList">
|
||||||
:icon="ArrowLeft"
|
|
||||||
@click="router.replace({ path: '/sys/sysJob' })"
|
|
||||||
>
|
|
||||||
{{ $t('button.back') }}
|
{{ $t('button.back') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
defineListRouteStateSchema,
|
||||||
|
positiveIntegerListRouteField,
|
||||||
|
stringListRouteField,
|
||||||
|
} from '#/composables/useListRouteState';
|
||||||
|
|
||||||
|
interface SysJobListRouteState {
|
||||||
|
keyword: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sysJobListRouteSchema = defineListRouteStateSchema<SysJobListRouteState>({
|
||||||
|
path: '/sys/sysJob',
|
||||||
|
fields: {
|
||||||
|
keyword: stringListRouteField(),
|
||||||
|
pageNumber: positiveIntegerListRouteField({ defaultValue: 1 }),
|
||||||
|
pageSize: positiveIntegerListRouteField({
|
||||||
|
allowedValues: [10, 20, 50, 100],
|
||||||
|
defaultValue: 10,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { sysJobListRouteSchema };
|
||||||
|
export type { SysJobListRouteState };
|
||||||
Reference in New Issue
Block a user