From 44ea1353b2bbd6486132936b971bc53914afc541 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 28 Nov 2025 13:17:07 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ui/paginated-table.tsx | 202 +++++++++++------- .../pages/Deploy/ScheduleJob/List/index.tsx | 61 +----- 2 files changed, 137 insertions(+), 126 deletions(-) diff --git a/frontend/src/components/ui/paginated-table.tsx b/frontend/src/components/ui/paginated-table.tsx index 4e88e5bd..e6ea34bf 100644 --- a/frontend/src/components/ui/paginated-table.tsx +++ b/frontend/src/components/ui/paginated-table.tsx @@ -1,29 +1,36 @@ import * as React from 'react'; import { useState, useEffect, useRef, useCallback, useImperativeHandle } from 'react'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Search, RotateCcw } from 'lucide-react'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './table'; import { DataTablePagination } from './pagination'; +import { Button } from './button'; +import { Input } from './input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; 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 SearchFieldDef { + key: string; + type: 'input' | 'select'; + placeholder?: string; + options?: { label: string; value: string | number }[]; + width?: string; + /** 是否支持回车搜索(仅 input 类型) */ + enterSearch?: boolean; +} + // 查询参数基础类型 export interface BaseQuery { pageNum?: number; @@ -33,47 +40,40 @@ export interface BaseQuery { // 组件 Props export interface PaginatedTableProps { - /** 数据获取函数 */ fetchFn: (query: Q) => Promise>; - /** 列定义 */ columns: ColumnDef[]; - /** 查询参数(不含分页) */ - query?: Omit; - /** 行唯一标识字段 */ + /** 搜索字段定义(简化模式) */ + searchFields?: SearchFieldDef[]; + /** 自定义搜索栏(完全自定义模式) */ + searchBar?: React.ReactNode; + /** 工具栏(新建按钮等) */ + toolbar?: React.ReactNode; + /** 初始查询参数 */ + initialQuery?: Omit; rowKey?: keyof T | ((record: T) => string | number); - /** 表格最小宽度 */ minWidth?: string; - /** 初始页大小 */ pageSize?: number; - /** 空数据提示 */ emptyText?: React.ReactNode; - /** 加载中提示 */ loadingText?: React.ReactNode; - /** 是否在 query 变化时自动刷新 */ - autoRefresh?: boolean; - /** 额外的依赖项,变化时触发刷新 */ - deps?: any[]; + /** 搜索栏和表格之间的间距 */ + searchGap?: string; } // 暴露给外部的方法 export interface PaginatedTableRef { - /** 刷新数据 */ refresh: (showLoading?: boolean) => Promise; - /** 重置到第一页并刷新 */ reset: () => void; - /** 获取当前数据 */ + search: () => void; getData: () => Page | null; - /** 局部更新某条记录 */ + getQuery: () => Record; 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); @@ -87,34 +87,31 @@ function PaginatedTableInner, Q extends BaseQuery const { fetchFn, columns, - query = {} as Omit, + searchFields, + searchBar, + toolbar, + initialQuery = {} as Omit, rowKey, minWidth, pageSize: initialPageSize = DEFAULT_PAGE_SIZE, emptyText = '暂无数据', loadingText = '加载中...', - autoRefresh = true, - deps = [], + searchGap = 'mb-4', } = props; const [data, setData] = useState | null>(null); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); // 首次加载显示 loading const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM); const [pageSize] = useState(initialPageSize); + const [searchValues, setSearchValues] = useState>(initialQuery); - // 是否首次加载 const isFirstLoad = useRef(true); - // 是否需要显示 loading - const shouldShowLoading = useRef(true); - // 上一次的 query(用于检测变化) - const prevQueryRef = useRef(query); - // 加载数据 - const loadData = useCallback(async (showLoading = true) => { - if (showLoading) setLoading(true); + // 加载数据(不再控制 loading 状态,只有首次加载显示) + const loadData = useCallback(async () => { try { const result = await fetchFn({ - ...query, + ...searchValues, pageNum, pageSize, } as Q); @@ -122,47 +119,47 @@ function PaginatedTableInner, Q extends BaseQuery } catch (error) { console.error('PaginatedTable 加载数据失败:', error); } finally { - setLoading(false); + if (isFirstLoad.current) { + setLoading(false); + isFirstLoad.current = false; + } } - }, [fetchFn, query, pageNum, pageSize]); + }, [fetchFn, searchValues, pageNum, pageSize]); // 监听分页变化 useEffect(() => { - loadData(shouldShowLoading.current || isFirstLoad.current); - isFirstLoad.current = false; - shouldShowLoading.current = false; - }, [pageNum, pageSize, ...deps]); + loadData(); + }, [pageNum, pageSize]); - // 监听 query 变化(自动刷新) - useEffect(() => { - if (!autoRefresh) return; - - const queryChanged = JSON.stringify(query) !== JSON.stringify(prevQueryRef.current); - prevQueryRef.current = query; - - if (queryChanged && !isFirstLoad.current) { - shouldShowLoading.current = true; + // 搜索 + const handleSearch = useCallback(() => { + if (pageNum === DEFAULT_PAGE_NUM) { + loadData(); + } else { setPageNum(DEFAULT_PAGE_NUM); } - }, [query, autoRefresh]); + }, [pageNum, loadData]); + + // 重置 + const handleReset = useCallback(() => { + setSearchValues(initialQuery); + setPageNum(DEFAULT_PAGE_NUM); + }, [initialQuery]); // 分页切换 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); + refresh: async () => { + await loadData(); }, + reset: handleReset, + search: handleSearch, getData: () => data, + getQuery: () => searchValues, updateRecord: (id, updates) => { setData(prev => { if (!prev) return prev; @@ -174,9 +171,49 @@ function PaginatedTableInner, Q extends BaseQuery }; }); }, - }), [data, loadData, rowKey]); + }), [data, loadData, handleReset, handleSearch, searchValues, rowKey]); - // 渲染单元格内容 + // 渲染搜索字段 + const renderSearchField = (field: SearchFieldDef) => { + const value = searchValues[field.key]; + + if (field.type === 'input') { + return ( +
+ setSearchValues(prev => ({ ...prev, [field.key]: e.target.value }))} + onKeyDown={(e) => field.enterSearch !== false && e.key === 'Enter' && handleSearch()} + /> +
+ ); + } + + if (field.type === 'select') { + return ( + + ); + } + + return null; + }; + + // 渲染单元格 const renderCell = (column: ColumnDef, record: T, index: number) => { if (column.render) { const value = column.dataIndex ? getNestedValue(record, column.dataIndex) : undefined; @@ -190,9 +227,29 @@ function PaginatedTableInner, Q extends BaseQuery const pageCount = data?.totalPages || 0; const hasData = data && data.content && data.content.length > 0; + const showSearchBar = searchFields && searchFields.length > 0; return (
+ {/* 搜索栏 */} + {(showSearchBar || searchBar || toolbar) && ( +
+ {searchBar || ( + <> + {searchFields?.map(renderSearchField)} + + + + )} + {toolbar &&
{toolbar}
} +
+ )} + + {/* 表格 */}
@@ -205,7 +262,7 @@ function PaginatedTableInner, Q extends BaseQuery - {loading && isFirstLoad.current ? ( + {loading ? (
@@ -218,12 +275,7 @@ function PaginatedTableInner, Q extends BaseQuery data.content.map((record, index) => ( {columns.map(col => ( - + {renderCell(col, record, index)} ))} @@ -240,6 +292,7 @@ function PaginatedTableInner, Q extends BaseQuery
+ {/* 分页 */} {pageCount > 0 && ( , Q extends BaseQuery ); } -// 使用 forwardRef 包装,支持泛型 export const PaginatedTable = React.forwardRef(PaginatedTableInner) as < T extends Record, Q extends BaseQuery = BaseQuery diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx index a54eff2d..f8be6f91 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/index.tsx @@ -2,18 +2,15 @@ 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 { - Plus, Search, Edit, Trash2, Play, Pause, + Plus, 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 { PaginatedTable, type ColumnDef, type PaginatedTableRef } from '@/components/ui/paginated-table'; +import { PaginatedTable, type ColumnDef, type PaginatedTableRef, type SearchFieldDef } 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'; @@ -31,13 +28,6 @@ const ScheduleJobList: React.FC = () => { // 分类数据 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); @@ -61,6 +51,13 @@ const ScheduleJobList: React.FC = () => { loadCategories(); }, []); + // 搜索字段定义 + const searchFields: SearchFieldDef[] = useMemo(() => [ + { key: 'jobName', type: 'input', placeholder: '搜索任务名称...', enterSearch: true }, + { key: 'categoryId', type: 'select', placeholder: '全部分类', width: 'w-[200px]', options: categories.map(c => ({ label: c.name, value: c.id })) }, + { key: 'status', type: 'select', placeholder: '全部状态', width: 'w-[150px]', options: [{ label: '启用', value: 'ENABLED' }, { label: '禁用', value: 'DISABLED' }, { label: '暂停', value: 'PAUSED' }] }, + ], [categories]); + // 状态徽章 const getStatusBadge = (status: JobStatus) => { const statusMap: Record = { @@ -186,17 +183,6 @@ const ScheduleJobList: React.FC = () => { return { total, enabledCount, pausedCount }; }, [tableRef.current?.getData()]); - // 搜索 - const handleSearch = () => { - tableRef.current?.reset(); - }; - - // 重置 - const handleReset = () => { - setSearchParams({ jobName: '', categoryId: undefined, status: undefined }); - tableRef.current?.reset(); - }; - return (
@@ -254,40 +240,13 @@ const ScheduleJobList: React.FC = () => {
- {/* 搜索栏 */} -
-
- setSearchParams({ ...searchParams, jobName: e.target.value })} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} /> -
- - - - -
- - {/* 表格 */} ref={tableRef} fetchFn={getScheduleJobs} columns={columns} - query={searchParams} + searchFields={searchFields} rowKey="id" minWidth="1600px" - pageSize={DEFAULT_PAGE_SIZE} />