重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-28 13:09:24 +08:00
parent ba9192e8bb
commit 9275f5d4ae
27 changed files with 643 additions and 1017 deletions

View File

@ -0,0 +1,263 @@
import * as React from 'react';
import { useState, useEffect, useRef, useCallback, useImperativeHandle } from 'react';
import { Loader2 } from 'lucide-react';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './table';
import { DataTablePagination } from './pagination';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
// 列定义
export interface ColumnDef<T> {
/** 列标识 */
key: string;
/** 列标题 */
title: React.ReactNode;
/** 列宽度 */
width?: string;
/** 是否固定列 */
sticky?: boolean;
/** 自定义渲染 */
render?: (value: any, record: T, index: number) => React.ReactNode;
/** 数据字段路径,支持嵌套如 'user.name' */
dataIndex?: string;
/** 单元格类名 */
className?: string;
}
// 查询参数基础类型
export interface BaseQuery {
pageNum?: number;
pageSize?: number;
[key: string]: any;
}
// 组件 Props
export interface PaginatedTableProps<T, Q extends BaseQuery = BaseQuery> {
/** 数据获取函数 */
fetchFn: (query: Q) => Promise<Page<T>>;
/** 列定义 */
columns: ColumnDef<T>[];
/** 查询参数(不含分页) */
query?: Omit<Q, 'pageNum' | 'pageSize'>;
/** 行唯一标识字段 */
rowKey?: keyof T | ((record: T) => string | number);
/** 表格最小宽度 */
minWidth?: string;
/** 初始页大小 */
pageSize?: number;
/** 空数据提示 */
emptyText?: React.ReactNode;
/** 加载中提示 */
loadingText?: React.ReactNode;
/** 是否在 query 变化时自动刷新 */
autoRefresh?: boolean;
/** 额外的依赖项,变化时触发刷新 */
deps?: any[];
}
// 暴露给外部的方法
export interface PaginatedTableRef<T> {
/** 刷新数据 */
refresh: (showLoading?: boolean) => Promise<void>;
/** 重置到第一页并刷新 */
reset: () => void;
/** 获取当前数据 */
getData: () => Page<T> | null;
/** 局部更新某条记录 */
updateRecord: (id: string | number, updates: Partial<T>) => void;
}
// 获取嵌套属性值
function getNestedValue(obj: any, path?: string): any {
if (!path) return undefined;
return path.split('.').reduce((acc, part) => acc?.[part], obj);
}
// 获取行 key
function getRowKey<T>(record: T, rowKey: keyof T | ((record: T) => string | number) | undefined, index: number): string | number {
if (!rowKey) return index;
if (typeof rowKey === 'function') return rowKey(record);
return record[rowKey] as string | number;
}
function PaginatedTableInner<T extends Record<string, any>, Q extends BaseQuery = BaseQuery>(
props: PaginatedTableProps<T, Q>,
ref: React.ForwardedRef<PaginatedTableRef<T>>
) {
const {
fetchFn,
columns,
query = {} as Omit<Q, 'pageNum' | 'pageSize'>,
rowKey,
minWidth,
pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
emptyText = '暂无数据',
loadingText = '加载中...',
autoRefresh = true,
deps = [],
} = props;
const [data, setData] = useState<Page<T> | null>(null);
const [loading, setLoading] = useState(false);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(initialPageSize);
// 是否首次加载
const isFirstLoad = useRef(true);
// 是否需要显示 loading
const shouldShowLoading = useRef(true);
// 上一次的 query用于检测变化
const prevQueryRef = useRef(query);
// 加载数据
const loadData = useCallback(async (showLoading = true) => {
if (showLoading) setLoading(true);
try {
const result = await fetchFn({
...query,
pageNum,
pageSize,
} as Q);
setData(result);
} catch (error) {
console.error('PaginatedTable 加载数据失败:', error);
} finally {
setLoading(false);
}
}, [fetchFn, query, pageNum, pageSize]);
// 监听分页变化
useEffect(() => {
loadData(shouldShowLoading.current || isFirstLoad.current);
isFirstLoad.current = false;
shouldShowLoading.current = false;
}, [pageNum, pageSize, ...deps]);
// 监听 query 变化(自动刷新)
useEffect(() => {
if (!autoRefresh) return;
const queryChanged = JSON.stringify(query) !== JSON.stringify(prevQueryRef.current);
prevQueryRef.current = query;
if (queryChanged && !isFirstLoad.current) {
shouldShowLoading.current = true;
setPageNum(DEFAULT_PAGE_NUM);
}
}, [query, autoRefresh]);
// 分页切换
const handlePageChange = useCallback((newPageNum: number) => {
shouldShowLoading.current = false;
setPageNum(newPageNum);
}, []);
// 暴露方法给外部
useImperativeHandle(ref, () => ({
refresh: async (showLoading = false) => {
shouldShowLoading.current = showLoading;
await loadData(showLoading);
},
reset: () => {
shouldShowLoading.current = true;
setPageNum(DEFAULT_PAGE_NUM);
},
getData: () => data,
updateRecord: (id, updates) => {
setData(prev => {
if (!prev) return prev;
return {
...prev,
content: prev.content.map(item =>
getRowKey(item, rowKey, -1) === id ? { ...item, ...updates } : item
),
};
});
},
}), [data, loadData, rowKey]);
// 渲染单元格内容
const renderCell = (column: ColumnDef<T>, record: T, index: number) => {
if (column.render) {
const value = column.dataIndex ? getNestedValue(record, column.dataIndex) : undefined;
return column.render(value, record, index);
}
if (column.dataIndex) {
return getNestedValue(record, column.dataIndex);
}
return null;
};
const pageCount = data?.totalPages || 0;
const hasData = data && data.content && data.content.length > 0;
return (
<div className="space-y-0">
<div className="rounded-md border">
<Table minWidth={minWidth}>
<TableHeader>
<TableRow>
{columns.map(col => (
<TableHead key={col.key} width={col.width} sticky={col.sticky}>
{col.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading && isFirstLoad.current ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground">{loadingText}</span>
</div>
</TableCell>
</TableRow>
) : hasData ? (
data.content.map((record, index) => (
<TableRow key={getRowKey(record, rowKey, index)}>
{columns.map(col => (
<TableCell
key={col.key}
width={col.width}
sticky={col.sticky}
className={col.className}
>
{renderCell(col, record, index)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<span className="text-muted-foreground">{emptyText}</span>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{pageCount > 0 && (
<DataTablePagination
pageIndex={pageNum}
pageSize={pageSize}
pageCount={pageCount}
onPageChange={handlePageChange}
/>
)}
</div>
);
}
// 使用 forwardRef 包装,支持泛型
export const PaginatedTable = React.forwardRef(PaginatedTableInner) as <
T extends Record<string, any>,
Q extends BaseQuery = BaseQuery
>(
props: PaginatedTableProps<T, Q> & { ref?: React.ForwardedRef<PaginatedTableRef<T>> }
) => React.ReactElement;
export default PaginatedTable;

View File

@ -1,71 +0,0 @@
import { useState, useCallback } from 'react';
import type { Page } from '@/types/base/page';
interface UsePageDataProps<T, P = any> {
service: (params: P) => Promise<Page<T>>;
defaultParams?: Partial<P>;
}
interface PageState<T> {
list: T[];
pagination: {
current: number;
pageSize: number;
total: number;
};
loading: boolean;
}
export function usePageData<T, P = any>({ service, defaultParams }: UsePageDataProps<T, P>) {
const [state, setState] = useState<PageState<T>>({
list: [],
pagination: {
current: 1,
pageSize: 10,
total: 0
},
loading: false
});
const loadData = useCallback(async (params?: Partial<P>) => {
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service({
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
} as P);
setState(prev => ({
list: pageData?.content || [],
pagination: {
current: prev.pagination.current,
pageSize: prev.pagination.pageSize,
total: pageData?.totalElements || 0
},
loading: false
}));
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
throw error;
}
}, [service, defaultParams, state.pagination]);
const onPageChange = useCallback((page: number, pageSize?: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
current: page,
pageSize: pageSize || prev.pagination.pageSize
}
}));
}, []);
return {
...state,
loadData,
onPageChange
};
}

View File

@ -1,217 +0,0 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { message, Modal } from 'antd';
import type { Page } from '@/types/base/page';
import type { TablePaginationConfig } from 'antd/es/table';
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
// 表格服务接口
export interface TableService<T, Q = any, C = Partial<T>, U = Partial<T>> {
list: (params: Q & { pageNum?: number; pageSize?: number }) => Promise<Page<T>>;
create?: (data: C) => Promise<T>;
update?: (id: number, data: U) => Promise<T>;
delete?: (id: number) => Promise<void>;
}
// 表格状态
interface TableState<T> {
list: T[];
pagination: TablePaginationConfig;
loading: boolean;
selectedRowKeys?: React.Key[];
selectedRows?: T[];
}
// 表格配置
interface TableConfig {
defaultPageSize?: number;
selection?: boolean;
message?: {
createSuccess?: string;
updateSuccess?: string;
deleteSuccess?: string;
};
}
export function useTableData<
T extends { id: number },
Q extends Record<string, any> = any,
C = any,
U = any
>({
service,
defaultParams,
config = {}
}: {
service: TableService<T, Q, C, U>;
defaultParams?: Partial<Q>;
config?: TableConfig;
}) {
// 初始化状态
const [state, setState] = useState<TableState<T>>({
list: [],
pagination: {
current: 1,
pageSize: config.defaultPageSize || 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`
},
loading: false
});
// 使用 ref 存储当前分页参数,避免循环依赖
const paginationRef = useRef({
current: 1,
pageSize: config.defaultPageSize || 10
});
// 同步 ref 和 state
useEffect(() => {
paginationRef.current = {
current: state.pagination.current || 1,
pageSize: state.pagination.pageSize || 10
};
}, [state.pagination.current, state.pagination.pageSize]);
// 加载数据 - 修复:使用 ref 避免依赖 state
const loadData = useCallback(async (params?: Partial<Q> & { pageNum?: number; pageSize?: number }) => {
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service.list({
...defaultParams,
...params,
pageNum: params?.pageNum ?? paginationRef.current.current,
pageSize: params?.pageSize ?? paginationRef.current.pageSize
} as Q & { pageNum?: number; pageSize?: number });
setState(prev => ({
...prev,
list: pageData.content || [],
pagination: {
...prev.pagination,
total: pageData.totalElements || 0
},
loading: false
}));
return pageData;
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
throw error;
}
}, [service, defaultParams]);
// 表格变化处理
const handleTableChange = (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<T> | SorterResult<T>[]
) => {
const { current, pageSize } = pagination;
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
current,
pageSize
}
}));
const params: Record<string, any> = {};
// 处理排序
if (!Array.isArray(sorter)) {
const { field, order } = sorter;
if (field && order) {
params.sortField = field as string;
params.sortOrder = order === 'ascend' ? 'asc' : 'desc';
}
}
// 处理筛选
Object.entries(filters).forEach(([key, value]) => {
if (value) {
params[key] = value;
}
});
loadData(params as Partial<Q>);
};
// 创建
const handleCreate = async (data: C) => {
if (!service.create) return false;
try {
await service.create(data);
message.success(config.message?.createSuccess || '创建成功');
loadData();
return true;
} catch (error) {
throw error;
}
};
// 更新
const handleUpdate = async (id: number, data: U) => {
if (!service.update) return false;
try {
await service.update(id, data);
message.success(config.message?.updateSuccess || '更新成功');
loadData();
return true;
} catch (error) {
throw error;
}
};
// 删除
const handleDelete = async (id: number) => {
if (!service.delete) return false;
return new Promise<boolean>((resolve, reject) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该记录吗?',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await service.delete(id);
message.success(config.message?.deleteSuccess || '删除成功');
loadData();
resolve(true);
} catch (error) {
reject(error);
}
},
onCancel: () => resolve(false)
});
});
};
// 选择行
const handleSelectChange = (selectedRowKeys: React.Key[], selectedRows: T[]) => {
if (!config.selection) return;
setState(prev => ({
...prev,
selectedRowKeys,
selectedRows
}));
};
// 监听分页变化自动加载数据
useEffect(() => {
loadData();
}, [state.pagination.current, state.pagination.pageSize]);
return {
...state,
loadData,
handleTableChange,
handleCreate,
handleUpdate,
handleDelete,
handleSelectChange,
refresh: () => loadData()
};
}

View File

@ -30,7 +30,7 @@ import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useForm } from "react-hook-form";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Edit,
@ -81,7 +81,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [deleteRecord, setDeleteRecord] = useState<ApplicationCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<ApplicationCategoryRequest>({
@ -336,7 +336,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageIndex={pageNum}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}

View File

@ -68,7 +68,7 @@ const ApplicationList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [selectedCategoryId, setSelectedCategoryId] = useState<number>();
const [pagination, setPagination] = useState({
pageNum: 1,
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -374,7 +374,7 @@ const ApplicationList: React.FC = () => {
} else {
setSelectedCategoryId(Number(value));
}
setPagination({ ...pagination, pageNum: 1 });
setPagination({ ...pagination, pageNum: 0 });
}}
>
<SelectTrigger className="max-w-[200px]">

View File

@ -32,7 +32,7 @@ const EnvironmentList: React.FC = () => {
const [list, setList] = useState<Environment[]>([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<EnvironmentQueryParams>({
pageNum: 1,
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
});
const [total, setTotal] = useState(0);
@ -72,13 +72,13 @@ const EnvironmentList: React.FC = () => {
// 搜索
const handleSearch = () => {
setQuery({ ...query, pageNum: 1 });
setQuery({ ...query, pageNum: 0 });
loadData();
};
// 重置
const handleReset = () => {
setQuery({ pageNum: 1, pageSize: DEFAULT_PAGE_SIZE });
setQuery({ pageNum: 0, pageSize: DEFAULT_PAGE_SIZE });
loadData();
};
@ -245,7 +245,7 @@ const EnvironmentList: React.FC = () => {
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={query.pageNum || 1}
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={Math.ceil(total / (query.pageSize || DEFAULT_PAGE_SIZE))}
onPageChange={(page) => setQuery({ ...query, pageNum: page })}

View File

@ -28,7 +28,7 @@ import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Search,
@ -78,7 +78,7 @@ const NotificationChannelList: React.FC = () => {
// 分页
const [pagination, setPagination] = useState({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -126,7 +126,7 @@ const NotificationChannelList: React.FC = () => {
try {
const res = await getChannelsPage({
...query,
page: pagination.pageNum - 1, // 后端从0开始前端从1开始
page: pagination.pageNum,
size: pagination.pageSize,
});
@ -148,7 +148,7 @@ const NotificationChannelList: React.FC = () => {
// 搜索
const handleSearch = () => {
setPagination((prev) => ({ ...prev, pageNum: DEFAULT_CURRENT }));
setPagination((prev) => ({ ...prev, pageNum: DEFAULT_PAGE_NUM }));
loadData();
};
@ -160,7 +160,7 @@ const NotificationChannelList: React.FC = () => {
enabled: undefined,
});
setPagination({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -168,7 +168,7 @@ const NotificationChannelList: React.FC = () => {
// 分页切换
const handlePageChange = (newPage: number) => {
setPagination({ ...pagination, pageNum: newPage + 1 });
setPagination({ ...pagination, pageNum: newPage });
};
// 创建

View File

@ -31,7 +31,7 @@ import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Search,
@ -88,7 +88,7 @@ const NotificationTemplateList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [list, setList] = useState<NotificationTemplateDTO[]>([]);
const [pagination, setPagination] = useState({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -181,7 +181,7 @@ const NotificationTemplateList: React.FC = () => {
// 搜索处理
const handleSearch = () => {
setPagination(prev => ({ ...prev, pageNum: DEFAULT_CURRENT }));
setPagination(prev => ({ ...prev, pageNum: DEFAULT_PAGE_NUM }));
loadData();
};

View File

@ -31,7 +31,7 @@ import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { useForm } from 'react-hook-form';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Edit,
@ -79,7 +79,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [deleteRecord, setDeleteRecord] = useState<JobCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<JobCategoryRequest>({

View File

@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import {
@ -52,7 +52,7 @@ const JobLogDialog: React.FC<JobLogDialogProps> = ({
const [data, setData] = useState<Page<ScheduleJobLogResponse> | null>(null);
const [selectedLog, setSelectedLog] = useState<ScheduleJobLogResponse | null>(null);
const [query, setQuery] = useState<ScheduleJobLogQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
jobId: undefined,
status: undefined,

View File

@ -1,21 +1,19 @@
import React, {useState, useEffect, useMemo} from 'react';
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card';
import {Table, TableHeader, TableBody, TableRow, TableHead, TableCell} from '@/components/ui/table';
import {Badge} from '@/components/ui/badge';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
import {DataTablePagination} from '@/components/ui/pagination';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Loader2, Plus, Search, Edit, Trash2, Play, Pause,
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List, Ban
Plus, Search, Edit, Trash2, Play, Pause,
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List, Ban
} from 'lucide-react';
import {useToast} from '@/components/ui/use-toast';
import {getScheduleJobs, getJobCategoryList, pauseJob, resumeJob, triggerJob, disableJob, deleteScheduleJob, enableJob} from './service';
import type {ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus} from './types';
import type {Page} from '@/types/base';
import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
import { useToast } from '@/components/ui/use-toast';
import { PaginatedTable, type ColumnDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
import { getScheduleJobs, getJobCategoryList, pauseJob, resumeJob, triggerJob, disableJob, deleteScheduleJob, enableJob } from './service';
import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types';
import { DEFAULT_PAGE_SIZE } from '@/utils/page';
import CategoryManageDialog from './components/CategoryManageDialog';
import JobLogDialog from './components/JobLogDialog';
import JobEditDialog from './components/JobEditDialog';
@ -27,650 +25,303 @@ import dayjs from 'dayjs';
*
*/
const ScheduleJobList: React.FC = () => {
const {toast} = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<ScheduleJobResponse> | null>(null);
const [categories, setCategories] = useState<JobCategoryResponse[]>([]);
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [logDialogOpen, setLogDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedJob, setSelectedJob] = useState<ScheduleJobResponse | null>(null);
const [editJob, setEditJob] = useState<ScheduleJobResponse | null>(null);
const [deleteJob, setDeleteJob] = useState<ScheduleJobResponse | null>(null);
const [query, setQuery] = useState<ScheduleJobQuery>({
pageNum: DEFAULT_CURRENT,
pageSize: DEFAULT_PAGE_SIZE,
jobName: '',
categoryId: undefined,
status: undefined
});
const { toast } = useToast();
const tableRef = useRef<PaginatedTableRef<ScheduleJobResponse>>(null);
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getScheduleJobs(query);
setData(result);
} catch (error) {
console.error('加载定时任务失败:', error);
} finally {
setLoading(false);
}
// 分类数据
const [categories, setCategories] = useState<JobCategoryResponse[]>([]);
// 查询条件(不含分页)
const [searchParams, setSearchParams] = useState({
jobName: '',
categoryId: undefined as number | undefined,
status: undefined as JobStatus | undefined,
});
// 弹窗状态
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [logDialogOpen, setLogDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedJob, setSelectedJob] = useState<ScheduleJobResponse | null>(null);
const [editJob, setEditJob] = useState<ScheduleJobResponse | null>(null);
const [deleteJob, setDeleteJob] = useState<ScheduleJobResponse | null>(null);
// 加载分类
const loadCategories = async () => {
try {
const result = await getJobCategoryList();
setCategories(result || []);
} catch (error) {
console.error('加载分类失败:', error);
}
};
useEffect(() => {
loadCategories();
}, []);
// 状态徽章
const getStatusBadge = (status: JobStatus) => {
const statusMap: Record<JobStatus, { variant: 'default' | 'secondary' | 'destructive' | 'outline'; text: string; icon: React.ElementType }> = {
ENABLED: { variant: 'default', text: '启用', icon: CheckCircle2 },
DISABLED: { variant: 'secondary', text: '禁用', icon: XCircle },
PAUSED: { variant: 'outline', text: '暂停', icon: Pause },
};
// 局部更新单条记录(避免全量刷新导致滚动位置丢失)
const updateRecordInList = (id: number, updates: Partial<ScheduleJobResponse>) => {
setData(prevData => {
if (!prevData) return prevData;
return {
...prevData,
content: prevData.content.map(item =>
item.id === id ? { ...item, ...updates } : item
)
};
});
};
// 加载分类
const loadCategories = async () => {
try {
const result = await getJobCategoryList();
setCategories(result || []);
} catch (error) {
console.error('加载分类失败:', error);
}
};
useEffect(() => {
loadCategories();
}, []);
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({...prev, pageNum: 1}));
};
// 重置
const handleReset = () => {
setQuery({
pageNum: 1,
pageSize: DEFAULT_PAGE_SIZE,
jobName: '',
categoryId: undefined,
status: undefined
});
};
// 新建任务
const handleCreate = () => {
setEditJob(null);
setEditDialogOpen(true);
};
// 编辑任务
const handleEdit = (record: ScheduleJobResponse) => {
setEditJob(record);
setEditDialogOpen(true);
};
// 打开删除确认对话框
const handleDeleteClick = (record: ScheduleJobResponse) => {
setDeleteJob(record);
setDeleteDialogOpen(true);
};
// 确认删除
const confirmDelete = async () => {
if (!deleteJob) return;
await deleteScheduleJob(deleteJob.id);
};
// 暂停任务
const handlePause = async (record: ScheduleJobResponse) => {
try {
await pauseJob(record.id);
toast({
title: '暂停成功',
description: `任务 "${record.jobName}" 已暂停`,
});
// 局部更新,避免页面滚动
updateRecordInList(record.id, { status: 'PAUSED' });
} catch (error) {
console.error('暂停失败:', error);
toast({
variant: 'destructive',
title: '暂停失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 恢复任务
const handleResume = async (record: ScheduleJobResponse) => {
try {
await resumeJob(record.id);
toast({
title: '恢复成功',
description: `任务 "${record.jobName}" 已恢复`,
});
// 局部更新,避免页面滚动
updateRecordInList(record.id, { status: 'ENABLED' });
} catch (error) {
console.error('恢复失败:', error);
toast({
variant: 'destructive',
title: '恢复失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 立即触发任务
const handleTrigger = async (record: ScheduleJobResponse) => {
try {
await triggerJob(record.id);
toast({
title: '触发成功',
description: `任务 "${record.jobName}" 已立即触发执行`,
});
// 立即执行不改变任务状态,无需更新列表
} catch (error) {
console.error('触发失败:', error);
toast({
variant: 'destructive',
title: '触发失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 禁用任务
const handleDisable = async (record: ScheduleJobResponse) => {
try {
await disableJob(record.id);
toast({
title: '禁用成功',
description: `任务 "${record.jobName}" 已禁用`,
});
// 局部更新,避免页面滚动
updateRecordInList(record.id, { status: 'DISABLED' });
} catch (error) {
console.error('禁用失败:', error);
toast({
variant: 'destructive',
title: '禁用失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 启用(解除禁用)任务
const handleEnable = async (record: ScheduleJobResponse) => {
try {
await enableJob(record.id);
toast({
title: '启用成功',
description: `任务 "${record.jobName}" 已启用`,
});
// 局部更新,避免页面滚动
updateRecordInList(record.id, { status: 'ENABLED' });
} catch (error) {
console.error('启用失败:', error);
toast({
variant: 'destructive',
title: '启用失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 查看日志
const handleViewLog = (record: ScheduleJobResponse) => {
setSelectedJob(record);
setLogDialogOpen(true);
};
// 状态徽章
const getStatusBadge = (status: JobStatus) => {
const statusMap: Record<JobStatus, {
variant: 'default' | 'secondary' | 'destructive' | 'outline';
text: string;
icon: React.ElementType
}> = {
ENABLED: {variant: 'default', text: '启用', icon: CheckCircle2},
DISABLED: {variant: 'secondary', text: '禁用', icon: XCircle},
PAUSED: {variant: 'outline', text: '暂停', icon: Pause},
};
const statusInfo = statusMap[status] || {variant: 'outline', text: status, icon: Clock};
const Icon = statusInfo.icon;
return (
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3"/>
{statusInfo.text}
</Badge>
);
};
// 统计数据
const stats = useMemo(() => {
const total = data?.totalElements || 0;
const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0;
const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0;
return {total, enabledCount, pausedCount};
}, [data]);
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock };
const Icon = statusInfo.icon;
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="text-muted-foreground mt-2">
</p>
</div>
{/* 视图切换 */}
<Tabs defaultValue="list" className="space-y-6">
<TabsList>
<TabsTrigger value="list" className="gap-2">
<List className="h-4 w-4"/>
</TabsTrigger>
<TabsTrigger value="dashboard" className="gap-2">
<BarChart3 className="h-4 w-4"/>
</TabsTrigger>
</TabsList>
{/* 列表视图 */}
<TabsContent value="list" className="space-y-6">
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700"></CardTitle>
<Activity className="h-4 w-4 text-blue-500"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700"></CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.enabledCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-yellow-700"></CardTitle>
<Pause className="h-4 w-4 text-yellow-500"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.pausedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<FolderKanban className="h-4 w-4 mr-2"/>
</Button>
<Button type="button" onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2"/>
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-md">
<Input
placeholder="搜索任务名称..."
value={query.jobName}
onChange={(e) => setQuery({...query, jobName: e.target.value})}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Select
value={query.categoryId?.toString() || 'all'}
onValueChange={(value) => setQuery({...query, categoryId: value !== 'all' ? Number(value) : undefined, pageNum: 1})}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="全部分类"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id.toString()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={query.status || 'all'}
onValueChange={(value) => setQuery({...query, status: value !== 'all' ? value as JobStatus : undefined, pageNum: 1})}
>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="全部状态"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ENABLED"></SelectItem>
<SelectItem value="DISABLED"></SelectItem>
<SelectItem value="PAUSED"></SelectItem>
</SelectContent>
</Select>
<Button type="button" onClick={handleSearch}>
<Search className="h-4 w-4 mr-2"/>
</Button>
<Button type="button" variant="outline" onClick={handleReset}>
</Button>
</div>
{/* 任务列表 */}
<div className="rounded-md border">
<Table minWidth="1600px">
<TableHeader>
<TableRow>
<TableHead width="180px"></TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="150px">Cron表达式</TableHead>
<TableHead width="120px">Bean名称</TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="100px"></TableHead>
<TableHead width="160px"></TableHead>
<TableHead width="160px"></TableHead>
<TableHead width="100px"></TableHead>
<TableHead width="90px"></TableHead>
<TableHead width="240px" sticky></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={11} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin"/>
<span className="text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : data && data.content.length > 0 ? (
data.content.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium whitespace-nowrap">{record.jobName}</TableCell>
<TableCell className="whitespace-nowrap">{record.category?.name || '-'}</TableCell>
<TableCell className="whitespace-nowrap">
<code className="text-xs bg-muted px-1 py-0.5 rounded">{record.cronExpression}</code>
</TableCell>
<TableCell className="whitespace-nowrap">
<code className="text-xs">{record.beanName}</code>
</TableCell>
<TableCell className="whitespace-nowrap">
<code className="text-xs">{record.methodName}</code>
</TableCell>
<TableCell className="whitespace-nowrap">{getStatusBadge(record.status)}</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
{record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
{record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</TableCell>
<TableCell className="text-center whitespace-nowrap">
<Badge variant="outline">{record.executeCount || 0}</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">
{record.executeCount && record.executeCount > 0 ? (
<Badge variant={
(record.successCount! / record.executeCount) >= 0.9 ? 'default' :
(record.successCount! / record.executeCount) >= 0.7 ? 'outline' : 'destructive'
}>
{((record.successCount! / record.executeCount) * 100).toFixed(1)}%
</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell sticky width="280px">
<div className="flex items-center gap-1">
{/* ENABLED 按钮控制 */}
{record.status === 'ENABLED' && (
<>
{/* 暂停 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handlePause(record)}
title="暂停"
>
<Pause className="h-4 w-4"/>
</Button>
{/* 禁用 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20"
onClick={() => handleDisable(record)}
title="禁用"
>
<Ban className="h-4 w-4"/>
</Button>
{/* 立即执行 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-green-600 hover:text-green-700 hover:bg-green-50 dark:text-green-500 dark:hover:bg-green-950/20"
onClick={() => handleTrigger(record)}
title="立即执行"
>
<PlayCircle className="h-4 w-4"/>
</Button>
</>
)}
{/* PAUSED 按钮控制 */}
{record.status === 'PAUSED' && (
<>
{/* 恢复 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleResume(record)}
title="恢复"
>
<Play className="h-4 w-4"/>
</Button>
{/* 禁用 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20"
onClick={() => handleDisable(record)}
title="禁用"
>
<Ban className="h-4 w-4"/>
</Button>
</>
)}
{/* DISABLED 按钮控制 */}
{record.status === 'DISABLED' && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/20"
onClick={() => handleEnable(record)}
title="启用"
>
<CheckCircle2 className="h-4 w-4"/>
</Button>
)}
{/* 删除按钮 所有状态显示 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDeleteClick(record)}
title="删除"
>
<Trash2 className="h-4 w-4"/>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleViewLog(record)}
title="查看日志"
>
<FileText className="h-4 w-4"/>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleEdit(record)}
title="编辑"
>
<Edit className="h-4 w-4"/>
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={11} className="h-24 text-center text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.content.length > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={data.totalPages}
onPageChange={(pageIndex) => setQuery({...query, pageNum: pageIndex})}
/>
</div>
)}
</CardContent>
</Card>
{/* 分类管理对话框 */}
<CategoryManageDialog
open={categoryDialogOpen}
onOpenChange={setCategoryDialogOpen}
onSuccess={loadCategories}
/>
{/* 任务日志对话框 */}
<JobLogDialog
open={logDialogOpen}
onOpenChange={setLogDialogOpen}
job={selectedJob}
/>
{/* 任务编辑对话框 */}
<JobEditDialog
open={editDialogOpen}
onOpenChange={setEditDialogOpen}
job={editJob}
categories={categories}
onSuccess={loadData}
/>
{/* 删除确认对话框 */}
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除定时任务"
description="您确定要删除以下定时任务吗?此操作无法撤销。"
item={deleteJob}
onConfirm={confirmDelete}
onSuccess={() => {
toast({
title: '删除成功',
description: `任务 "${deleteJob?.jobName}" 已删除`,
});
setDeleteJob(null);
loadData();
}}
variant="destructive"
confirmText="确认删除"
>
{deleteJob && (
<div className="rounded-md border p-3 space-y-2 bg-muted/50">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<span className="font-medium">{deleteJob.jobName}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Bean名称:</span>
<code className="text-xs bg-background px-2 py-1 rounded">
{deleteJob.beanName}
</code>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<code className="text-xs bg-background px-2 py-1 rounded">
{deleteJob.methodName}
</code>
</div>
{deleteJob.cronExpression && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Cron表达式:</span>
<code className="text-xs bg-background px-2 py-1 rounded">
{deleteJob.cronExpression}
</code>
</div>
)}
</div>
)}
</ConfirmDialog>
</TabsContent>
{/* 仪表盘视图 */}
<TabsContent value="dashboard">
<Dashboard/>
</TabsContent>
</Tabs>
</div>
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" />
{statusInfo.text}
</Badge>
);
};
// 操作处理函数
const handlePause = async (record: ScheduleJobResponse) => {
try {
await pauseJob(record.id);
toast({ title: '暂停成功', description: `任务 "${record.jobName}" 已暂停` });
tableRef.current?.updateRecord(record.id, { status: 'PAUSED' });
} catch (error) {
toast({ variant: 'destructive', title: '暂停失败', description: error instanceof Error ? error.message : '未知错误' });
}
};
const handleResume = async (record: ScheduleJobResponse) => {
try {
await resumeJob(record.id);
toast({ title: '恢复成功', description: `任务 "${record.jobName}" 已恢复` });
tableRef.current?.updateRecord(record.id, { status: 'ENABLED' });
} catch (error) {
toast({ variant: 'destructive', title: '恢复失败', description: error instanceof Error ? error.message : '未知错误' });
}
};
const handleTrigger = async (record: ScheduleJobResponse) => {
try {
await triggerJob(record.id);
toast({ title: '触发成功', description: `任务 "${record.jobName}" 已立即触发执行` });
} catch (error) {
toast({ variant: 'destructive', title: '触发失败', description: error instanceof Error ? error.message : '未知错误' });
}
};
const handleDisable = async (record: ScheduleJobResponse) => {
try {
await disableJob(record.id);
toast({ title: '禁用成功', description: `任务 "${record.jobName}" 已禁用` });
tableRef.current?.updateRecord(record.id, { status: 'DISABLED' });
} catch (error) {
toast({ variant: 'destructive', title: '禁用失败', description: error instanceof Error ? error.message : '未知错误' });
}
};
const handleEnable = async (record: ScheduleJobResponse) => {
try {
await enableJob(record.id);
toast({ title: '启用成功', description: `任务 "${record.jobName}" 已启用` });
tableRef.current?.updateRecord(record.id, { status: 'ENABLED' });
} catch (error) {
toast({ variant: 'destructive', title: '启用失败', description: error instanceof Error ? error.message : '未知错误' });
}
};
// 列定义
const columns: ColumnDef<ScheduleJobResponse>[] = useMemo(() => [
{ key: 'jobName', title: '任务名称', width: '180px', dataIndex: 'jobName', className: 'font-medium whitespace-nowrap' },
{ key: 'category', title: '任务分类', width: '120px', className: 'whitespace-nowrap', render: (_, record) => record.category?.name || '-' },
{ key: 'cronExpression', title: 'Cron表达式', width: '150px', className: 'whitespace-nowrap', render: (_, record) => <code className="text-xs bg-muted px-1 py-0.5 rounded">{record.cronExpression}</code> },
{ key: 'beanName', title: 'Bean名称', width: '120px', className: 'whitespace-nowrap', render: (_, record) => <code className="text-xs">{record.beanName}</code> },
{ key: 'methodName', title: '方法名称', width: '120px', className: 'whitespace-nowrap', render: (_, record) => <code className="text-xs">{record.methodName}</code> },
{ key: 'status', title: '状态', width: '100px', className: 'whitespace-nowrap', render: (_, record) => getStatusBadge(record.status) },
{ key: 'lastExecuteTime', title: '上次执行', width: '160px', className: 'text-xs text-muted-foreground whitespace-nowrap', render: (_, record) => record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-' },
{ key: 'nextExecuteTime', title: '下次执行', width: '160px', className: 'text-xs text-muted-foreground whitespace-nowrap', render: (_, record) => record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-' },
{ key: 'executeCount', title: '执行次数', width: '100px', className: 'text-center whitespace-nowrap', render: (_, record) => <Badge variant="outline">{record.executeCount || 0}</Badge> },
{
key: 'successRate', title: '成功率', width: '90px', className: 'whitespace-nowrap',
render: (_, record) => {
if (record.executeCount && record.executeCount > 0) {
const rate = record.successCount! / record.executeCount;
return <Badge variant={rate >= 0.9 ? 'default' : rate >= 0.7 ? 'outline' : 'destructive'}>{(rate * 100).toFixed(1)}%</Badge>;
}
return <span className="text-muted-foreground">-</span>;
}
},
{
key: 'actions', title: '操作', width: '240px', sticky: true,
render: (_, record) => (
<div className="flex items-center gap-1">
{record.status === 'ENABLED' && (
<>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7" onClick={() => handlePause(record)} title="暂停"><Pause className="h-4 w-4" /></Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20" onClick={() => handleDisable(record)} title="禁用"><Ban className="h-4 w-4" /></Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-green-600 hover:text-green-700 hover:bg-green-50 dark:text-green-500 dark:hover:bg-green-950/20" onClick={() => handleTrigger(record)} title="立即执行"><PlayCircle className="h-4 w-4" /></Button>
</>
)}
{record.status === 'PAUSED' && (
<>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleResume(record)} title="恢复"><Play className="h-4 w-4" /></Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20" onClick={() => handleDisable(record)} title="禁用"><Ban className="h-4 w-4" /></Button>
</>
)}
{record.status === 'DISABLED' && (
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/20" onClick={() => handleEnable(record)} title="启用"><CheckCircle2 className="h-4 w-4" /></Button>
)}
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => { setDeleteJob(record); setDeleteDialogOpen(true); }} title="删除"><Trash2 className="h-4 w-4" /></Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setSelectedJob(record); setLogDialogOpen(true); }} title="查看日志"><FileText className="h-4 w-4" /></Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditJob(record); setEditDialogOpen(true); }} title="编辑"><Edit className="h-4 w-4" /></Button>
</div>
)
},
], []);
// 统计数据
const stats = useMemo(() => {
const data = tableRef.current?.getData();
const total = data?.totalElements || 0;
const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0;
const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0;
return { total, enabledCount, pausedCount };
}, [tableRef.current?.getData()]);
// 搜索
const handleSearch = () => {
tableRef.current?.reset();
};
// 重置
const handleReset = () => {
setSearchParams({ jobName: '', categoryId: undefined, status: undefined });
tableRef.current?.reset();
};
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="text-muted-foreground mt-2"></p>
</div>
<Tabs defaultValue="list" className="space-y-6">
<TabsList>
<TabsTrigger value="list" className="gap-2"><List className="h-4 w-4" /></TabsTrigger>
<TabsTrigger value="dashboard" className="gap-2"><BarChart3 className="h-4 w-4" /></TabsTrigger>
</TabsList>
<TabsContent value="list" className="space-y-6">
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700"></CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700"></CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.enabledCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-yellow-700"></CardTitle>
<Pause className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.pausedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" onClick={() => setCategoryDialogOpen(true)}><FolderKanban className="h-4 w-4 mr-2" /></Button>
<Button type="button" onClick={() => { setEditJob(null); setEditDialogOpen(true); }}><Plus className="h-4 w-4 mr-2" /></Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-md">
<Input placeholder="搜索任务名称..." value={searchParams.jobName} onChange={(e) => setSearchParams({ ...searchParams, jobName: e.target.value })} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
</div>
<Select value={searchParams.categoryId?.toString() || 'all'} onValueChange={(value) => setSearchParams({ ...searchParams, categoryId: value !== 'all' ? Number(value) : undefined })}>
<SelectTrigger className="w-[200px]"><SelectValue placeholder="全部分类" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{categories.map((cat) => (<SelectItem key={cat.id} value={cat.id.toString()}>{cat.name}</SelectItem>))}
</SelectContent>
</Select>
<Select value={searchParams.status || 'all'} onValueChange={(value) => setSearchParams({ ...searchParams, status: value !== 'all' ? value as JobStatus : undefined })}>
<SelectTrigger className="w-[150px]"><SelectValue placeholder="全部状态" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ENABLED"></SelectItem>
<SelectItem value="DISABLED"></SelectItem>
<SelectItem value="PAUSED"></SelectItem>
</SelectContent>
</Select>
<Button type="button" onClick={handleSearch}><Search className="h-4 w-4 mr-2" /></Button>
<Button type="button" variant="outline" onClick={handleReset}></Button>
</div>
{/* 表格 */}
<PaginatedTable<ScheduleJobResponse, ScheduleJobQuery>
ref={tableRef}
fetchFn={getScheduleJobs}
columns={columns}
query={searchParams}
rowKey="id"
minWidth="1600px"
pageSize={DEFAULT_PAGE_SIZE}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="dashboard">
<Dashboard />
</TabsContent>
</Tabs>
{/* 弹窗 */}
<CategoryManageDialog open={categoryDialogOpen} onOpenChange={setCategoryDialogOpen} onSuccess={loadCategories} />
<JobLogDialog open={logDialogOpen} onOpenChange={setLogDialogOpen} job={selectedJob} />
<JobEditDialog open={editDialogOpen} onOpenChange={setEditDialogOpen} job={editJob} categories={categories} onSuccess={() => tableRef.current?.refresh()} />
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除定时任务"
description="您确定要删除以下定时任务吗?此操作无法撤销。"
item={deleteJob}
onConfirm={async () => { if (deleteJob) await deleteScheduleJob(deleteJob.id); }}
onSuccess={() => { toast({ title: '删除成功', description: `任务 "${deleteJob?.jobName}" 已删除` }); setDeleteJob(null); tableRef.current?.refresh(); }}
variant="destructive"
confirmText="确认删除"
>
{deleteJob && (
<div className="rounded-md border p-3 space-y-2 bg-muted/50">
<div className="flex items-center gap-2"><span className="text-sm font-medium text-muted-foreground">:</span><span className="font-medium">{deleteJob.jobName}</span></div>
<div className="flex items-center gap-2"><span className="text-sm font-medium text-muted-foreground">Bean名称:</span><code className="text-xs bg-background px-2 py-1 rounded">{deleteJob.beanName}</code></div>
</div>
)}
</ConfirmDialog>
</div>
);
};
export default ScheduleJobList;

View File

@ -32,7 +32,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Trash2,
@ -85,7 +85,7 @@ const ApplicationManageDialog: React.FC<ApplicationManageDialogProps> = ({
const [adding, setAdding] = useState(false);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);

View File

@ -32,7 +32,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Edit,
@ -92,7 +92,7 @@ const MemberManageDialog: React.FC<MemberManageDialogProps> = ({
const [editRole, setEditRole] = useState('');
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);

View File

@ -46,7 +46,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { searchFormSchema, type SearchFormValues } from './schema';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import type { Page } from '@/types/base';
import {
Plus,
@ -72,7 +72,7 @@ type Column = {
const TeamList: React.FC = () => {
const [list, setList] = useState<TeamResponse[]>([]);
const [pagination, setPagination] = useState({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -106,7 +106,7 @@ const TeamList: React.FC = () => {
const loadData = async (searchValues?: SearchFormValues) => {
try {
const query: TeamQuery = {
pageNum: pagination.pageNum - 1,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
teamCode: searchValues?.teamCode || form.getValues('teamCode') || undefined,
teamName: searchValues?.teamName || form.getValues('teamName') || undefined,
@ -452,7 +452,7 @@ const TeamList: React.FC = () => {
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum - 1}
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}

View File

@ -16,7 +16,7 @@ import { getEnabledCategories } from '../../Category/service';
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
import type { FormCategoryResponse } from '../../Category/types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import dayjs from 'dayjs';
/**
@ -29,7 +29,7 @@ const FormDataList: React.FC = () => {
const [data, setData] = useState<Page<FormDataResponse> | null>(null);
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
formDefinitionId: searchParams.get('formDefinitionId') ? Number(searchParams.get('formDefinitionId')) : undefined,
businessKey: '',
@ -389,7 +389,7 @@ const FormDataList: React.FC = () => {
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={query.pageNum + 1}
pageIndex={query.pageNum}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -30,7 +30,7 @@ import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useForm } from "react-hook-form";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Edit,
@ -78,7 +78,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [deleteRecord, setDeleteRecord] = useState<FormCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<FormCategoryRequest>({
@ -329,7 +329,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageIndex={pageNum}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}

View File

@ -28,7 +28,7 @@ import { getEnabledCategories } from '../../Category/service';
import type { FormDefinitionResponse, FormDefinitionStatus } from './types';
import type { FormCategoryResponse } from '../../Category/types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import FormBasicInfoModal from './components/FormBasicInfoModal';
import CategoryManageDialog from './components/CategoryManageDialog';
@ -41,7 +41,7 @@ const FormDefinitionList: React.FC = () => {
const [data, setData] = useState<Page<FormDefinitionResponse> | null>(null);
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
name: '',
categoryId: undefined as number | undefined,
@ -508,7 +508,7 @@ const FormDefinitionList: React.FC = () => {
{pageCount > 1 && (
<div className="mt-4">
<DataTablePagination
pageIndex={query.pageNum + 1}
pageIndex={query.pageNum}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -34,7 +34,7 @@ const ExternalPage: React.FC = () => {
const [list, setList] = useState<ExternalSystemResponse[]>([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<ExternalSystemQuery>({
pageNum: 1,
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
});
const [total, setTotal] = useState(0);
@ -74,13 +74,13 @@ const ExternalPage: React.FC = () => {
// 搜索
const handleSearch = () => {
setQuery({ ...query, pageNum: 1 });
setQuery({ ...query, pageNum: 0 });
loadData();
};
// 重置
const handleReset = () => {
setQuery({ pageNum: 1, pageSize: DEFAULT_PAGE_SIZE });
setQuery({ pageNum: 0, pageSize: DEFAULT_PAGE_SIZE });
loadData();
};
@ -326,7 +326,7 @@ const ExternalPage: React.FC = () => {
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={query.pageNum || 1}
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={Math.ceil(total / (query.pageSize || DEFAULT_PAGE_SIZE))}
onPageChange={(page) => setQuery({ ...query, pageNum: page })}

View File

@ -35,7 +35,7 @@ import {
import { useToast } from '@/components/ui/use-toast';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import type { ServerCategoryResponse } from '../types';
import { serverCategoryFormSchema, type ServerCategoryFormValues } from '../schema';
import {
@ -68,7 +68,7 @@ export const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [categoryToDelete, setCategoryToDelete] = useState<ServerCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<ServerCategoryFormValues>({
@ -450,7 +450,7 @@ export const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageIndex={pageNum}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}

View File

@ -20,7 +20,7 @@ import { useToast } from '@/components/ui/use-toast';
import { Plus, Edit, Trash2, Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import type { MenuResponse } from '../types';
import {
getPermissions,
@ -49,7 +49,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
const [loading, setLoading] = useState(false);
const [permissions, setPermissions] = useState<PermissionResponse[]>([]);
const [pagination, setPagination] = useState({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -62,7 +62,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
try {
const response = await getPermissions({
menuId: menu.id,
pageNum: pagination.pageNum - 1,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
});
if (response) {
@ -88,7 +88,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
if (open) {
// 每次打开都重置到初始状态
setPagination({
pageNum: DEFAULT_CURRENT,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
@ -144,8 +144,8 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
setEditDialogOpen(false);
// 保存后重置分页到第一页
if (!editRecord && pagination.pageNum !== 1) {
setPagination({ ...pagination, pageNum: 1 });
if (!editRecord && pagination.pageNum !== 0) {
setPagination({ ...pagination, pageNum: 0 });
} else {
loadPermissions();
}
@ -296,7 +296,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum - 1}
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}

View File

@ -13,7 +13,7 @@ import { useToast } from '@/components/ui/use-toast';
import { getRoleList, deleteRole, getRoleMenusAndPermissions, assignMenusAndPermissions } from './service';
import type { RoleResponse, RoleQuery } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import EditDialog from './components/EditDialog';
import DeleteDialog from './components/DeleteDialog';
import PermissionDialog from './components/PermissionDialog';
@ -39,7 +39,7 @@ const RolePage: React.FC = () => {
const [defaultMenuIds, setDefaultMenuIds] = useState<number[]>([]);
const [defaultPermissionIds, setDefaultPermissionIds] = useState<number[]>([]);
const [query, setQuery] = useState<RoleQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
code: '',
name: '',
@ -370,7 +370,7 @@ const RolePage: React.FC = () => {
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={(query.pageNum || 0) + 1}
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -16,7 +16,7 @@ import { getDepartmentTree } from '../../Department/List/service';
import type { UserResponse, UserQuery, Role } from './types';
import type { DepartmentResponse } from '../../Department/List/types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import EditModal from './components/EditModal';
import ResetPasswordDialog from './components/ResetPasswordDialog';
import AssignRolesDialog from './components/AssignRolesDialog';
@ -41,7 +41,7 @@ const UserPage: React.FC = () => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<UserResponse | null>(null);
const [query, setQuery] = useState<UserQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
username: '',
email: '',
@ -431,7 +431,7 @@ const UserPage: React.FC = () => {
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={(query.pageNum || 0) + 1}
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -31,7 +31,7 @@ import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useForm } from "react-hook-form";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import {
Plus,
Edit,
@ -82,7 +82,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [deleteRecord, setDeleteRecord] = useState<WorkflowCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<WorkflowCategoryRequest>({
@ -375,7 +375,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageIndex={pageNum}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}

View File

@ -15,7 +15,7 @@ import { useToast } from '@/components/ui/use-toast';
import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service';
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse, WorkflowDefinitionStatus } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import EditModal from './components/EditModal';
import DeleteDialog from './components/DeleteDialog';
import DeployDialog from './components/DeployDialog';
@ -44,7 +44,7 @@ const WorkflowDefinitionList: React.FC = () => {
const [startRecord, setStartRecord] = useState<WorkflowDefinition | null>(null);
const [formDefinition, setFormDefinition] = useState<FormDefinitionResponse | null>(null);
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
name: '',
categoryId: undefined,
@ -561,7 +561,7 @@ const WorkflowDefinitionList: React.FC = () => {
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={(query.pageNum || 0) + 1}
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -8,7 +8,7 @@ import { Loader2, History } from 'lucide-react';
import { WorkflowHistoricalInstance } from '../types';
import { getHistoricalInstances } from '../service';
import DetailModal from './DetailModal';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
interface HistoryModalProps {
visible: boolean;
@ -21,7 +21,7 @@ const HistoryModal: React.FC<HistoryModalProps> = ({ visible, onCancel, workflow
const [data, setData] = useState<WorkflowHistoricalInstance[]>([]);
const [total, setTotal] = useState(0);
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
});
const [detailVisible, setDetailVisible] = useState(false);
@ -136,7 +136,7 @@ const HistoryModal: React.FC<HistoryModalProps> = ({ visible, onCancel, workflow
{pageCount > 1 && (
<div className="mt-4">
<DataTablePagination
pageIndex={query.pageNum + 1}
pageIndex={query.pageNum}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -13,7 +13,7 @@ import {
import { getWorkflowInstances } from './service';
import type { WorkflowTemplateWithInstances } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
import HistoryModal from './components/HistoryModal';
import dayjs from 'dayjs';
@ -26,7 +26,7 @@ const WorkflowInstanceList: React.FC = () => {
const [historyVisible, setHistoryVisible] = useState(false);
const [selectedWorkflowDefinitionId, setSelectedWorkflowDefinitionId] = useState<number>();
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageNum: DEFAULT_PAGE_NUM,
pageSize: DEFAULT_PAGE_SIZE,
businessKey: '',
status: undefined as string | undefined,
@ -297,7 +297,7 @@ const WorkflowInstanceList: React.FC = () => {
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={query.pageNum + 1}
pageIndex={query.pageNum}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({

View File

@ -2,7 +2,7 @@ import type {Page} from '@/types/base';
// 默认分页参数
export const DEFAULT_PAGE_SIZE = 10;
export const DEFAULT_CURRENT = 1;
export const DEFAULT_PAGE_NUM = 0; // 页码从0开始
/**
*
@ -13,7 +13,7 @@ export const convertToPageParams = (params?: {
sortField?: string;
sortOrder?: string;
}): { sortOrder: any; sortField: string | undefined; pageSize: number; pageNum: number } => ({
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
pageNum: Math.max(0, (params?.current ?? 1) - 1), // 前端current从1开始转换为从0开始的页码
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
sortField: params?.sortField,
sortOrder: params?.sortOrder?.replace('end', '')