diff --git a/frontend/src/components/ui/paginated-table.tsx b/frontend/src/components/ui/paginated-table.tsx new file mode 100644 index 00000000..4e88e5bd --- /dev/null +++ b/frontend/src/components/ui/paginated-table.tsx @@ -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 { + /** 列标识 */ + 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 { + /** 数据获取函数 */ + fetchFn: (query: Q) => Promise>; + /** 列定义 */ + columns: ColumnDef[]; + /** 查询参数(不含分页) */ + query?: Omit; + /** 行唯一标识字段 */ + 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 { + /** 刷新数据 */ + refresh: (showLoading?: boolean) => Promise; + /** 重置到第一页并刷新 */ + reset: () => void; + /** 获取当前数据 */ + getData: () => Page | null; + /** 局部更新某条记录 */ + updateRecord: (id: string | number, updates: Partial) => void; +} + +// 获取嵌套属性值 +function getNestedValue(obj: any, path?: string): any { + if (!path) return undefined; + return path.split('.').reduce((acc, part) => acc?.[part], obj); +} + +// 获取行 key +function getRowKey(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, Q extends BaseQuery = BaseQuery>( + props: PaginatedTableProps, + ref: React.ForwardedRef> +) { + const { + fetchFn, + columns, + query = {} as Omit, + rowKey, + minWidth, + pageSize: initialPageSize = DEFAULT_PAGE_SIZE, + emptyText = '暂无数据', + loadingText = '加载中...', + autoRefresh = true, + deps = [], + } = props; + + const [data, setData] = useState | 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, 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 ( +
+
+ + + + {columns.map(col => ( + + {col.title} + + ))} + + + + {loading && isFirstLoad.current ? ( + + +
+ + {loadingText} +
+
+
+ ) : hasData ? ( + data.content.map((record, index) => ( + + {columns.map(col => ( + + {renderCell(col, record, index)} + + ))} + + )) + ) : ( + + + {emptyText} + + + )} +
+
+
+ + {pageCount > 0 && ( + + )} +
+ ); +} + +// 使用 forwardRef 包装,支持泛型 +export const PaginatedTable = React.forwardRef(PaginatedTableInner) as < + T extends Record, + Q extends BaseQuery = BaseQuery +>( + props: PaginatedTableProps & { ref?: React.ForwardedRef> } +) => React.ReactElement; + +export default PaginatedTable; diff --git a/frontend/src/hooks/usePageData.ts b/frontend/src/hooks/usePageData.ts deleted file mode 100644 index c5b1e210..00000000 --- a/frontend/src/hooks/usePageData.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { Page } from '@/types/base/page'; - -interface UsePageDataProps { - service: (params: P) => Promise>; - defaultParams?: Partial

; -} - -interface PageState { - list: T[]; - pagination: { - current: number; - pageSize: number; - total: number; - }; - loading: boolean; -} - -export function usePageData({ service, defaultParams }: UsePageDataProps) { - const [state, setState] = useState>({ - list: [], - pagination: { - current: 1, - pageSize: 10, - total: 0 - }, - loading: false - }); - - const loadData = useCallback(async (params?: Partial

) => { - 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 - }; -} \ No newline at end of file diff --git a/frontend/src/hooks/useTableData.ts b/frontend/src/hooks/useTableData.ts deleted file mode 100644 index 554def28..00000000 --- a/frontend/src/hooks/useTableData.ts +++ /dev/null @@ -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, U = Partial> { - list: (params: Q & { pageNum?: number; pageSize?: number }) => Promise>; - create?: (data: C) => Promise; - update?: (id: number, data: U) => Promise; - delete?: (id: number) => Promise; -} - -// 表格状态 -interface TableState { - 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 = any, - C = any, - U = any ->({ - service, - defaultParams, - config = {} -}: { - service: TableService; - defaultParams?: Partial; - config?: TableConfig; -}) { - // 初始化状态 - const [state, setState] = useState>({ - 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 & { 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, - sorter: SorterResult | SorterResult[] - ) => { - const { current, pageSize } = pagination; - setState(prev => ({ - ...prev, - pagination: { - ...prev.pagination, - current, - pageSize - } - })); - - const params: Record = {}; - - // 处理排序 - 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); - }; - - // 创建 - 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((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() - }; -} \ No newline at end of file diff --git a/frontend/src/pages/Deploy/Application/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Deploy/Application/List/components/CategoryManageDialog.tsx index 30d530ae..7dde0fbd 100644 --- a/frontend/src/pages/Deploy/Application/List/components/CategoryManageDialog.tsx +++ b/frontend/src/pages/Deploy/Application/List/components/CategoryManageDialog.tsx @@ -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 = ({ const [deleteRecord, setDeleteRecord] = useState(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({ @@ -336,7 +336,7 @@ const CategoryManageDialog: React.FC = ({ {data && data.totalElements > 0 && (

setPageNum(page - 1)} diff --git a/frontend/src/pages/Deploy/Application/List/index.tsx b/frontend/src/pages/Deploy/Application/List/index.tsx index da14ed90..5254e2c7 100644 --- a/frontend/src/pages/Deploy/Application/List/index.tsx +++ b/frontend/src/pages/Deploy/Application/List/index.tsx @@ -68,7 +68,7 @@ const ApplicationList: React.FC = () => { const [loading, setLoading] = useState(false); const [selectedCategoryId, setSelectedCategoryId] = useState(); 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 }); }} > diff --git a/frontend/src/pages/Deploy/Environment/List/index.tsx b/frontend/src/pages/Deploy/Environment/List/index.tsx index b54e0892..937c09e4 100644 --- a/frontend/src/pages/Deploy/Environment/List/index.tsx +++ b/frontend/src/pages/Deploy/Environment/List/index.tsx @@ -32,7 +32,7 @@ const EnvironmentList: React.FC = () => { const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [query, setQuery] = useState({ - 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 = () => {
setQuery({ ...query, pageNum: page })} diff --git a/frontend/src/pages/Deploy/NotificationChannel/List/index.tsx b/frontend/src/pages/Deploy/NotificationChannel/List/index.tsx index cfd2fecb..56b93c6e 100644 --- a/frontend/src/pages/Deploy/NotificationChannel/List/index.tsx +++ b/frontend/src/pages/Deploy/NotificationChannel/List/index.tsx @@ -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 }); }; // 创建 diff --git a/frontend/src/pages/Deploy/NotificationTemplate/List/index.tsx b/frontend/src/pages/Deploy/NotificationTemplate/List/index.tsx index 2d0d23f7..6b91aa2b 100644 --- a/frontend/src/pages/Deploy/NotificationTemplate/List/index.tsx +++ b/frontend/src/pages/Deploy/NotificationTemplate/List/index.tsx @@ -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([]); 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(); }; diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx index 3d6d7928..4b303139 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx @@ -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 = ({ const [deleteRecord, setDeleteRecord] = useState(null); // 分页状态 - const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); + const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM); const [pageSize] = useState(DEFAULT_PAGE_SIZE); const form = useForm({ diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx index 5b52be99..70af6183 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx @@ -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 = ({ const [data, setData] = useState | null>(null); const [selectedLog, setSelectedLog] = useState(null); const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT - 1, + pageNum: DEFAULT_PAGE_NUM, pageSize: DEFAULT_PAGE_SIZE, jobId: undefined, status: undefined, diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx index 72e56c24..a54eff2d 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx @@ -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 | null>(null); - const [categories, setCategories] = useState([]); - const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); - const [logDialogOpen, setLogDialogOpen] = useState(false); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedJob, setSelectedJob] = useState(null); - const [editJob, setEditJob] = useState(null); - const [deleteJob, setDeleteJob] = useState(null); - const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT, - pageSize: DEFAULT_PAGE_SIZE, - jobName: '', - categoryId: undefined, - status: undefined - }); + const { toast } = useToast(); + const tableRef = useRef>(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([]); + + // 查询条件(不含分页) + 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(null); + const [editJob, setEditJob] = useState(null); + const [deleteJob, setDeleteJob] = useState(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 = { + ENABLED: { variant: 'default', text: '启用', icon: CheckCircle2 }, + DISABLED: { variant: 'secondary', text: '禁用', icon: XCircle }, + PAUSED: { variant: 'outline', text: '暂停', icon: Pause }, }; - - // 局部更新单条记录(避免全量刷新导致滚动位置丢失) - const updateRecordInList = (id: number, updates: Partial) => { - 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 = { - 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 ( - - - {statusInfo.text} - - ); - }; - - // 统计数据 - 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 ( -
-
-

定时任务管理

-

- 创建和管理定时任务,配置执行计划和参数。 -

-
- - {/* 视图切换 */} - - - - - 列表视图 - - - - 仪表盘 - - - - {/* 列表视图 */} - - {/* 统计卡片 */} -
- - - 总任务 - - - -
{stats.total}
-

全部定时任务

-
-
- - - 运行中 - - - -
{stats.enabledCount}
-

正在运行的任务

-
-
- - - 已暂停 - - - -
{stats.pausedCount}
-

暂停执行的任务

-
-
-
- - - - 任务列表 -
- - -
-
- - {/* 搜索栏 */} -
-
- setQuery({...query, jobName: e.target.value})} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - /> -
- - - - -
- - {/* 任务列表 */} -
- - - - 任务名称 - 任务分类 - Cron表达式 - Bean名称 - 方法名称 - 状态 - 上次执行 - 下次执行 - 执行次数 - 成功率 - 操作 - - - - {loading ? ( - - -
- - 加载中... -
-
-
- ) : data && data.content.length > 0 ? ( - data.content.map((record) => ( - - {record.jobName} - {record.category?.name || '-'} - - {record.cronExpression} - - - {record.beanName} - - - {record.methodName} - - {getStatusBadge(record.status)} - - {record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} - - - {record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'} - - - {record.executeCount || 0} - - - {record.executeCount && record.executeCount > 0 ? ( - = 0.9 ? 'default' : - (record.successCount! / record.executeCount) >= 0.7 ? 'outline' : 'destructive' - }> - {((record.successCount! / record.executeCount) * 100).toFixed(1)}% - - ) : ( - - - )} - - -
- {/* ENABLED 按钮控制 */} - {record.status === 'ENABLED' && ( - <> - {/* 暂停 */} - - {/* 禁用 */} - - {/* 立即执行 */} - - - )} - {/* PAUSED 按钮控制 */} - {record.status === 'PAUSED' && ( - <> - {/* 恢复 */} - - {/* 禁用 */} - - - )} - {/* DISABLED 按钮控制 */} - {record.status === 'DISABLED' && ( - - )} - {/* 删除按钮 所有状态显示 */} - - - -
-
-
- )) - ) : ( - - - 暂无数据 - - - )} -
-
-
- - {/* 分页 */} - {data && data.content.length > 0 && ( -
- setQuery({...query, pageNum: pageIndex})} - /> -
- )} -
-
- - {/* 分类管理对话框 */} - - - {/* 任务日志对话框 */} - - - {/* 任务编辑对话框 */} - - - {/* 删除确认对话框 */} - { - toast({ - title: '删除成功', - description: `任务 "${deleteJob?.jobName}" 已删除`, - }); - setDeleteJob(null); - loadData(); - }} - variant="destructive" - confirmText="确认删除" - > - {deleteJob && ( -
-
- 任务名称: - {deleteJob.jobName} -
-
- Bean名称: - - {deleteJob.beanName} - -
-
- 方法名称: - - {deleteJob.methodName} - -
- {deleteJob.cronExpression && ( -
- Cron表达式: - - {deleteJob.cronExpression} - -
- )} -
- )} -
-
- - {/* 仪表盘视图 */} - - - -
-
+ + + {statusInfo.text} + ); + }; + + // 操作处理函数 + 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[] = 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) => {record.cronExpression} }, + { key: 'beanName', title: 'Bean名称', width: '120px', className: 'whitespace-nowrap', render: (_, record) => {record.beanName} }, + { key: 'methodName', title: '方法名称', width: '120px', className: 'whitespace-nowrap', render: (_, record) => {record.methodName} }, + { 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) => {record.executeCount || 0} }, + { + key: 'successRate', title: '成功率', width: '90px', className: 'whitespace-nowrap', + render: (_, record) => { + if (record.executeCount && record.executeCount > 0) { + const rate = record.successCount! / record.executeCount; + return = 0.9 ? 'default' : rate >= 0.7 ? 'outline' : 'destructive'}>{(rate * 100).toFixed(1)}%; + } + return -; + } + }, + { + key: 'actions', title: '操作', width: '240px', sticky: true, + render: (_, record) => ( +
+ {record.status === 'ENABLED' && ( + <> + + + + + )} + {record.status === 'PAUSED' && ( + <> + + + + )} + {record.status === 'DISABLED' && ( + + )} + + + +
+ ) + }, + ], []); + + // 统计数据 + 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 ( +
+
+

定时任务管理

+

创建和管理定时任务,配置执行计划和参数。

+
+ + + + 列表视图 + 仪表盘 + + + + {/* 统计卡片 */} +
+ + + 总任务 + + + +
{stats.total}
+

全部定时任务

+
+
+ + + 运行中 + + + +
{stats.enabledCount}
+

正在运行的任务

+
+
+ + + 已暂停 + + + +
{stats.pausedCount}
+

暂停执行的任务

+
+
+
+ + + + 任务列表 +
+ + +
+
+ + {/* 搜索栏 */} +
+
+ setSearchParams({ ...searchParams, jobName: e.target.value })} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} /> +
+ + + + +
+ + {/* 表格 */} + + ref={tableRef} + fetchFn={getScheduleJobs} + columns={columns} + query={searchParams} + rowKey="id" + minWidth="1600px" + pageSize={DEFAULT_PAGE_SIZE} + /> +
+
+
+ + + + +
+ + {/* 弹窗 */} + + + tableRef.current?.refresh()} /> + { if (deleteJob) await deleteScheduleJob(deleteJob.id); }} + onSuccess={() => { toast({ title: '删除成功', description: `任务 "${deleteJob?.jobName}" 已删除` }); setDeleteJob(null); tableRef.current?.refresh(); }} + variant="destructive" + confirmText="确认删除" + > + {deleteJob && ( +
+
任务名称:{deleteJob.jobName}
+
Bean名称:{deleteJob.beanName}
+
+ )} +
+
+ ); }; export default ScheduleJobList; - diff --git a/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx index 563cc509..243ff867 100644 --- a/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/ApplicationManageDialog.tsx @@ -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 = ({ 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); diff --git a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx index 75f1f424..7d7fe43a 100644 --- a/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/MemberManageDialog.tsx @@ -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 = ({ const [editRole, setEditRole] = useState(''); // 分页状态 - const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); + const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM); const [pageSize] = useState(DEFAULT_PAGE_SIZE); diff --git a/frontend/src/pages/Deploy/Team/List/index.tsx b/frontend/src/pages/Deploy/Team/List/index.tsx index 77739e50..b21c0800 100644 --- a/frontend/src/pages/Deploy/Team/List/index.tsx +++ b/frontend/src/pages/Deploy/Team/List/index.tsx @@ -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([]); 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 = () => {
{ const [data, setData] = useState | null>(null); const [categories, setCategories] = useState([]); 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 && ( setQuery(prev => ({ diff --git a/frontend/src/pages/Form/Definition/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Form/Definition/List/components/CategoryManageDialog.tsx index e74764e6..cafe5dab 100644 --- a/frontend/src/pages/Form/Definition/List/components/CategoryManageDialog.tsx +++ b/frontend/src/pages/Form/Definition/List/components/CategoryManageDialog.tsx @@ -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 = ({ const [deleteRecord, setDeleteRecord] = useState(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({ @@ -329,7 +329,7 @@ const CategoryManageDialog: React.FC = ({ {data && data.totalElements > 0 && (
setPageNum(page - 1)} diff --git a/frontend/src/pages/Form/Definition/List/index.tsx b/frontend/src/pages/Form/Definition/List/index.tsx index 33870ff6..09f45c26 100644 --- a/frontend/src/pages/Form/Definition/List/index.tsx +++ b/frontend/src/pages/Form/Definition/List/index.tsx @@ -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 | null>(null); const [categories, setCategories] = useState([]); 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 && (
setQuery(prev => ({ diff --git a/frontend/src/pages/Resource/External/List/index.tsx b/frontend/src/pages/Resource/External/List/index.tsx index 048e3f1e..3dd443e0 100644 --- a/frontend/src/pages/Resource/External/List/index.tsx +++ b/frontend/src/pages/Resource/External/List/index.tsx @@ -34,7 +34,7 @@ const ExternalPage: React.FC = () => { const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [query, setQuery] = useState({ - 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 = () => {
setQuery({ ...query, pageNum: page })} diff --git a/frontend/src/pages/Resource/Server/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Resource/Server/List/components/CategoryManageDialog.tsx index 4fe88ee0..0d9519bf 100644 --- a/frontend/src/pages/Resource/Server/List/components/CategoryManageDialog.tsx +++ b/frontend/src/pages/Resource/Server/List/components/CategoryManageDialog.tsx @@ -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 = ({ const [categoryToDelete, setCategoryToDelete] = useState(null); // 分页状态 - const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); + const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM); const [pageSize] = useState(DEFAULT_PAGE_SIZE); const form = useForm({ @@ -450,7 +450,7 @@ export const CategoryManageDialog: React.FC = ({ {data && data.totalElements > 0 && (
setPageNum(page - 1)} diff --git a/frontend/src/pages/System/Menu/List/components/PermissionDialog.tsx b/frontend/src/pages/System/Menu/List/components/PermissionDialog.tsx index e0a26f65..6d1a9be3 100644 --- a/frontend/src/pages/System/Menu/List/components/PermissionDialog.tsx +++ b/frontend/src/pages/System/Menu/List/components/PermissionDialog.tsx @@ -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 = ({ const [loading, setLoading] = useState(false); const [permissions, setPermissions] = useState([]); 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 = ({ 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 = ({ if (open) { // 每次打开都重置到初始状态 setPagination({ - pageNum: DEFAULT_CURRENT, + pageNum: DEFAULT_PAGE_NUM, pageSize: DEFAULT_PAGE_SIZE, totalElements: 0, }); @@ -144,8 +144,8 @@ const PermissionDialog: React.FC = ({ 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 = ({
{ const [defaultMenuIds, setDefaultMenuIds] = useState([]); const [defaultPermissionIds, setDefaultPermissionIds] = useState([]); const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT - 1, + pageNum: DEFAULT_PAGE_NUM, pageSize: DEFAULT_PAGE_SIZE, code: '', name: '', @@ -370,7 +370,7 @@ const RolePage: React.FC = () => { {/* 分页 */} {pageCount > 1 && ( setQuery(prev => ({ diff --git a/frontend/src/pages/System/User/List/index.tsx b/frontend/src/pages/System/User/List/index.tsx index 9a9a10db..861d77bb 100644 --- a/frontend/src/pages/System/User/List/index.tsx +++ b/frontend/src/pages/System/User/List/index.tsx @@ -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(null); const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT - 1, + pageNum: DEFAULT_PAGE_NUM, pageSize: DEFAULT_PAGE_SIZE, username: '', email: '', @@ -431,7 +431,7 @@ const UserPage: React.FC = () => { {/* 分页 */} {pageCount > 1 && ( setQuery(prev => ({ diff --git a/frontend/src/pages/Workflow/Definition/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Workflow/Definition/List/components/CategoryManageDialog.tsx index edb98ef8..b9b037bb 100644 --- a/frontend/src/pages/Workflow/Definition/List/components/CategoryManageDialog.tsx +++ b/frontend/src/pages/Workflow/Definition/List/components/CategoryManageDialog.tsx @@ -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 = ({ const [deleteRecord, setDeleteRecord] = useState(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({ @@ -375,7 +375,7 @@ const CategoryManageDialog: React.FC = ({ {data && data.totalElements > 0 && (
setPageNum(page - 1)} diff --git a/frontend/src/pages/Workflow/Definition/List/index.tsx b/frontend/src/pages/Workflow/Definition/List/index.tsx index ed6e1442..450741f3 100644 --- a/frontend/src/pages/Workflow/Definition/List/index.tsx +++ b/frontend/src/pages/Workflow/Definition/List/index.tsx @@ -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(null); const [formDefinition, setFormDefinition] = useState(null); const [query, setQuery] = useState({ - 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 && ( setQuery(prev => ({ diff --git a/frontend/src/pages/Workflow/Instance/List/components/HistoryModal.tsx b/frontend/src/pages/Workflow/Instance/List/components/HistoryModal.tsx index 851a17e6..9d74ce28 100644 --- a/frontend/src/pages/Workflow/Instance/List/components/HistoryModal.tsx +++ b/frontend/src/pages/Workflow/Instance/List/components/HistoryModal.tsx @@ -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 = ({ visible, onCancel, workflow const [data, setData] = useState([]); 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 = ({ visible, onCancel, workflow {pageCount > 1 && (
setQuery(prev => ({ diff --git a/frontend/src/pages/Workflow/Instance/List/index.tsx b/frontend/src/pages/Workflow/Instance/List/index.tsx index a2efc89a..4aae0f8e 100644 --- a/frontend/src/pages/Workflow/Instance/List/index.tsx +++ b/frontend/src/pages/Workflow/Instance/List/index.tsx @@ -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(); 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 && ( setQuery(prev => ({ diff --git a/frontend/src/utils/page.ts b/frontend/src/utils/page.ts index ddbe279b..2ca18f1e 100644 --- a/frontend/src/utils/page.ts +++ b/frontend/src/utils/page.ts @@ -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', '')