feat: 优化定时任务管理与日志体验

- 增加工作流选项、时间范围筛选和日志详情展示

- 支持可配置自动刷新、轻量局部更新与响应式布局
This commit is contained in:
2026-08-31 14:57:20 +08:00
parent 8c174e5c02
commit 155af9989c
9 changed files with 1533 additions and 133 deletions

View File

@@ -122,6 +122,109 @@ describe('page data recovery', () => {
expect(get).toHaveBeenCalledTimes(2);
});
it('does not let a lightweight refresh supersede an active page query', async () => {
let resolveInitialRequest: (value: {
data: { records: { id: string }[]; totalRow: number };
}) => void = () => {};
const initialRequest = new Promise<{
data: { records: { id: string }[]; totalRow: number };
}>((resolve) => {
resolveInitialRequest = resolve;
});
const get = vi.fn().mockReturnValueOnce(initialRequest);
const wrapper = mount(PageData, {
global: {
directives: { loading: {} },
},
props: {
pageUrl: '/page',
refreshUrl: '/refresh',
requestClient: { get },
},
});
const refreshRequest = (wrapper.vm as any).reload({
lightweight: true,
silent: true,
});
expect(get).toHaveBeenCalledTimes(1);
resolveInitialRequest({
data: { records: [{ id: 'counted-result' }], totalRow: 21 },
});
await refreshRequest;
await flushPromises();
expect(get).toHaveBeenCalledTimes(1);
expect(wrapper.emitted('loadSuccess')?.at(-1)?.[0]).toMatchObject({
lightweight: false,
recordCount: 1,
});
expect(wrapper.find('.el-pagination').exists()).toBe(true);
});
it('uses the lightweight refresh endpoint without replacing the total', async () => {
const get = vi
.fn()
.mockResolvedValueOnce({
data: { records: [{ id: 'initial' }], totalRow: 35 },
})
.mockResolvedValueOnce({ data: [{ id: 'latest' }] });
const wrapper = mount(PageData, {
global: {
directives: { loading: {} },
},
props: {
pageUrl: '/page',
refreshUrl: '/refresh',
requestClient: { get },
},
});
await flushPromises();
await (wrapper.vm as any).reload({ lightweight: true, silent: true });
await flushPromises();
expect(get).toHaveBeenLastCalledWith('/refresh', {
params: { pageNumber: 1, pageSize: 10 },
});
expect(wrapper.find('.el-pagination').exists()).toBe(true);
expect(wrapper.emitted('loadSuccess')?.at(-1)?.[0]).toMatchObject({
lightweight: true,
pageNumber: 1,
recordCount: 1,
});
});
it('falls back to the counted page endpoint outside the first page', async () => {
const get = vi
.fn()
.mockResolvedValue({ data: { records: [], totalRow: 30 } });
const wrapper = mount(PageData, {
global: {
directives: { loading: {} },
},
props: {
initialPageNumber: 2,
pageUrl: '/page',
refreshUrl: '/refresh',
requestClient: { get },
},
});
await flushPromises();
await (wrapper.vm as any).reload({ lightweight: true, silent: true });
await flushPromises();
expect(get).toHaveBeenLastCalledWith('/page', {
params: { pageNumber: 2, pageSize: 10 },
});
expect(wrapper.emitted('loadSuccess')?.at(-1)?.[0]).toMatchObject({
lightweight: false,
pageNumber: 2,
});
});
it('restores a mounted list to a new route state with one target request', async () => {
const get = vi
.fn()

View File

@@ -10,6 +10,7 @@ import { getEmptyStateImageUrl } from '#/utils/assets';
interface PageDataProps {
pageUrl: string;
refreshUrl?: string;
pageSize?: number;
pageSizes?: number[];
extraQueryParams?: Record<string, any>;
@@ -29,23 +30,38 @@ interface PageDataRestoreState extends PageDataState {
}
interface PageDataReloadOptions {
lightweight?: boolean;
silent?: boolean;
}
interface PageDataRequest {
lightweight: boolean;
silent: boolean;
version: number;
}
interface PageDataLoadEvent {
lightweight: boolean;
pageNumber: number;
recordCount: number;
}
interface PageDataLoadErrorEvent extends PageDataLoadEvent {
error: unknown;
}
const props = withDefaults(defineProps<PageDataProps>(), {
pageSize: 10,
pageSizes: () => [10, 20, 50, 100],
refreshUrl: undefined,
extraQueryParams: () => ({}),
initialPageNumber: 1,
initialQueryParams: () => ({}),
requestClient: () => api,
});
const emit = defineEmits<{
(e: 'loadError', event: PageDataLoadErrorEvent): void;
(e: 'loadSuccess', event: PageDataLoadEvent): void;
(e: 'stateChange', state: PageDataState): void;
}>();
@@ -67,10 +83,10 @@ const pageInfo = reactive({
});
// 模拟 API 调用 - 这里需要根据你的实际 API 调用方式调整
const doGet = async (params: Record<string, any>) => {
const doGet = async (url: string, params: Record<string, any>) => {
// 这里替换为你的实际 API 调用
// 例如return await api.get(props.pageUrl, { params })
const response = await props.requestClient.get(`${props.pageUrl}`, {
const response = await props.requestClient.get(url, {
params,
});
const data = await response.data;
@@ -78,14 +94,28 @@ const doGet = async (params: Record<string, any>) => {
};
const loadPageListOnce = async (request: PageDataRequest) => {
const lightweight = Boolean(
request.lightweight && props.refreshUrl && pageInfo.pageNumber === 1,
);
try {
const res = await doGet({
const res = await doGet(lightweight ? props.refreshUrl! : props.pageUrl, {
pageNumber: pageInfo.pageNumber,
pageSize: pageInfo.pageSize,
...props.extraQueryParams,
...queryParams.value,
});
if (request.version === pageRequestVersion) {
if (lightweight) {
pageList.value = Array.isArray(res.data)
? res.data
: res.data?.records || [];
emit('loadSuccess', {
lightweight: true,
pageNumber: pageInfo.pageNumber,
recordCount: pageList.value.length,
});
return;
}
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));
@@ -96,6 +126,11 @@ const loadPageListOnce = async (request: PageDataRequest) => {
return;
}
pageList.value = res.data?.records || [];
emit('loadSuccess', {
lightweight: false,
pageNumber: pageInfo.pageNumber,
recordCount: pageList.value.length,
});
}
} catch (error) {
if (request.version === pageRequestVersion) {
@@ -104,12 +139,24 @@ const loadPageListOnce = async (request: PageDataRequest) => {
pageList.value = [];
pageInfo.total = 0;
}
emit('loadError', {
error,
lightweight,
pageNumber: pageInfo.pageNumber,
recordCount: pageList.value.length,
});
}
}
};
const requestPageList = (silent: boolean) => {
const requestPageList = (silent: boolean, lightweight = false) => {
// 自动刷新只补充最新行;已有请求能够提供同等或更完整的数据时直接复用,
// 避免它使正在进行的分页查询失效或额外排队。
if (activePageRequest && lightweight) {
return activePageRequest;
}
const request: PageDataRequest = {
lightweight,
silent: silent && pageList.value.length > 0,
version: ++pageRequestVersion,
};
@@ -120,6 +167,7 @@ const requestPageList = (silent: boolean) => {
if (activePageRequest) {
pendingPageRequest = pendingPageRequest
? {
lightweight: pendingPageRequest.lightweight && request.lightweight,
silent: pendingPageRequest.silent && request.silent,
version: request.version,
}
@@ -143,10 +191,10 @@ const requestPageList = (silent: boolean) => {
};
// 获取页面数据
const getPageList = () => requestPageList(false);
const getPageList = () => requestPageList(false, false);
const reload = (options: PageDataReloadOptions = {}) =>
requestPageList(Boolean(options.silent));
requestPageList(Boolean(options.silent), Boolean(options.lightweight));
// 分页事件处理
const handleSizeChange = (newSize: number) => {