import React, { useMemo, useState, useCallback, useRef, useEffect, } from "react"; import { AgGridReact, AgGridReactProps } from "@ag-grid-community/react"; import { GetContextMenuItemsParams, GridApi, GridReadyEvent, MenuItemDef, StatusPanelDef, StoreRefreshedEvent, ModuleRegistry, ColumnRowGroupChangedEvent, IServerSideGetRowsParams, IServerSideDatasource, GridState, } from "@ag-grid-community/core"; import { ColumnsToolPanelModule } from "@ag-grid-enterprise/column-tool-panel"; import { FiltersToolPanelModule } from "@ag-grid-enterprise/filter-tool-panel"; import { RangeSelectionModule } from "@ag-grid-enterprise/range-selection"; import { SetFilterModule } from "@ag-grid-enterprise/set-filter"; import { MasterDetailModule } from "@ag-grid-enterprise/master-detail"; import { StatusBarModule } from "@ag-grid-enterprise/status-bar"; import { ClipboardModule } from "@ag-grid-enterprise/clipboard"; import { MenuModule } from "@ag-grid-enterprise/menu"; import { ServerSideRowModelModule } from "@ag-grid-enterprise/server-side-row-model"; import { AG_GRID_LOCALE_CH } from "@web/src/locale/ag-grid-locale"; import { api, CrudOperation, emitDataChange } from "@nice/client" import { message } from "antd"; import { useLocation } from "react-router-dom"; import { useAuth } from "@web/src/providers/auth-provider"; import { EventBus } from "@nice/client"; import { ObjectType } from "@nice/common"; ModuleRegistry.registerModules([ MasterDetailModule, ColumnsToolPanelModule, FiltersToolPanelModule, MenuModule, SetFilterModule, RangeSelectionModule, StatusBarModule, ClipboardModule, ServerSideRowModelModule, ]); interface AgTableSpecificProps { objectType?: ObjectType; onChange?: (selectedIds: string[]) => void; height?: string | number; defaultExpandedRows?: (string | number)[]; defaultRowGroupColumns?: string[]; params?: Record; rowHeight?: number } type AgTableProps = AgTableSpecificProps & Omit; const AgServerTable: React.FC = ({ objectType, onChange, height = 400, defaultExpandedRows = [], defaultRowGroupColumns = [], params: queryParams, rowHeight = 50, ...restProps // Catch all other passed props }) => { const utils = api.useUtils(); const { sessionId } = useAuth() const location = useLocation() // const { agTheme } = useAppTheme(); const [expandedRows, setExpandedRows] = useState(defaultExpandedRows); const gridApi = useRef(null); const groupFieldsRef = useRef() const rowRecordRef = useRef>() const [dragOverNodeId, setDragOverNodeId] = useState(null); useEffect(() => { const onDataChange = async ({ operation, data, type }) => { if (type === objectType) { console.log(objectType, operation, data) // 确保 data 转换为数组 const dataArray = Array.isArray(data) ? data : [data]; saveExpandedRowsState(); refreshData(dataArray); } } EventBus.on("dataChanged", onDataChange) return () => { EventBus.off("dataChanged", onDataChange) } }, []) const getRows = useCallback( async (params: IServerSideGetRowsParams) => { try { const request = { ...params.request, ...queryParams }; console.log(request) const result = await utils.client[objectType].getRows.query(request as any) console.log(result) params.success({ rowData: result?.rowData, rowCount: result?.rowCount, }); } catch (error) { console.error("Error in getRows function:", error); params.fail(); } }, [objectType, queryParams, utils] ); const datasource = useMemo(() => { return { getRows, }; }, [getRows]); useEffect(() => { if (gridApi.current) { gridApi.current.setGridOption("serverSideDatasource", datasource); } }, [datasource]); const statusBar = useMemo<{ statusPanels: StatusPanelDef[]; }>(() => { return { statusPanels: [ { statusPanel: "agSelectedRowCountComponent" }, { statusPanel: "agAggregationComponent" }, ], }; }, []); const getContextMenuItems = useCallback( (params: GetContextMenuItemsParams): (string | MenuItemDef)[] => { return ["copy", "separator", "export"]; }, [] ); const onFirstDataRendered = useCallback( (params) => { restoreExpandedRowsState(); }, [expandedRows] ); const containerStyle = useMemo( () => ({ width: "100%", display: "flex", }), [] ); const gridStyle = useMemo( () => ({ width: "100%", flexGrow: 1, backgroundColor: "#ffffff", }), [] ); function updateGroupFields(api: GridApi) { if (restProps.treeData) { groupFieldsRef.current = ['id'] } else { const colState = api.getColumnState(); const groupedColumns = colState.filter((state) => state.rowGroup); groupedColumns.sort((a, b) => a.rowGroupIndex! - b.rowGroupIndex!); groupFieldsRef.current = groupedColumns.map((col) => col.colId.replace(".", "_") ) || []; } } function onColumnRowGroupChanged(event: ColumnRowGroupChangedEvent) { updateGroupFields(event.api) if (gridApi.current) { gridApi.current.refreshServerSide({ purge: true }); } } const saveExpandedRowsState = () => { if (gridApi.current) { const expandedNodes: string[] = []; gridApi.current.forEachNode((node) => { if (node.expanded && (node.key || node.id)) { expandedNodes.push(node.key || node.id); } }); setExpandedRows(expandedNodes); return expandedNodes; } }; const restoreExpandedRowsState = () => { if (gridApi.current) { gridApi.current.forEachNode((node) => { if ( expandedRows.includes(node.key || node.id) || defaultExpandedRows.includes(node.key || node.id) ) { node.setExpanded(true); } }); } }; const firstRowIndexRef = useRef(-1) const initialState = useMemo(() => { const statekey = `${objectType}-${location.pathname}-${sessionId}-agstate` const storedState = localStorage.getItem(statekey) if (storedState) { const parsedState = JSON.parse(storedState) return parsedState } }, []) const handleStoreState = useCallback((state: GridState) => { const statekey = `${objectType}-${location.pathname}-${sessionId}-agstate` localStorage.setItem(statekey, JSON.stringify({ ...state, rowIndex: firstRowIndexRef.current })) }, []) const containerRef = useRef(null) const containerHeight = useMemo(() => { if (containerRef.current) { // console.log('grid view height', containerRef.current.clientHeight - 100) return containerRef.current.clientHeight - 100 } return 700 }, [containerRef.current]) const initialRowCount = useMemo(() => { if (initialState && !initialState.rowGroup) { // console.log('rowCount', initialState?.rowIndex + containerHeight / rowHeight) const rowCount = initialState?.rowIndex + containerHeight / rowHeight return rowCount < 31 ? 31 : rowCount } }, [containerHeight, initialState]) const onGridReady = useCallback( (params: GridReadyEvent) => { gridApi.current = params.api; gridApi.current.setGridOption("serverSideDatasource", datasource); // if (!isInit) { if (!initialState?.rowGroup && initialState?.rowIndex !== -1) { gridApi.current.ensureIndexVisible(initialState?.rowIndex, "top") // setIsInit(true) } // } gridApi.current.addEventListener("gridPreDestroyed", (event) => handleStoreState(event.state)) gridApi.current.addEventListener("bodyScroll", (event) => { firstRowIndexRef.current = Math.round(event.top / rowHeight) }) updateGroupFields(params.api) // if (defaultRowGroupColumns.length > 0) { // params.api.applyColumnState({ // state: defaultRowGroupColumns.map((colId) => ({ // colId, // rowGroup: true, // hide: true, // })), // applyOrder: true, // }); // } }, [datasource] ); const refreshData = useCallback((rows: any[]) => { if (!gridApi.current) return; const rowData = Object.values(rowRecordRef.current) // 对于树形数据,需要特殊处理 const refreshRouteForTreeData = (item: any) => { // 如果是树形数据,使用父级路径来刷新 const getParentRoute = (data: any): string[] => { const route: string[] = []; let currentParent = data.parent_id; while (currentParent) { const parentNode = rowData?.find(row => row.id === currentParent); console.log(parentNode) if (parentNode) { // 使用父节点的分组字段构建路由 const parentRoute = groupFieldsRef.current?.map(field => parentNode[field]).filter(Boolean); if (parentRoute && parentRoute.length) { route.unshift(...parentRoute); } currentParent = parentNode.parent_id; } else { break; } } return route; }; // 获取父级路由 const ancestorRoute = getParentRoute(item); console.log('ancestor route', ancestorRoute) // 刷新父级路由 if (ancestorRoute) { let parentRoute = [...ancestorRoute] parentRoute.pop() if (parentRoute) { console.log('parent route', parentRoute) gridApi.current.refreshServerSide({ route: parentRoute }); } gridApi.current.refreshServerSide({ route: ancestorRoute }); } }; console.log('refresh data', rows) console.log('rowdata', rowData) console.log(groupFieldsRef.current) console.log('tree fresh', restProps.treeData) // 处理每一个更新的行 rows.forEach(item => { // 检查是否存在于当前数据中 const existingItem = rowData?.find(row => row.id === item.id); // console.log('exsit item', existingItem) if (restProps.treeData) { refreshRouteForTreeData(item); } else { // 对于非树形数据,使用原有的分组刷新逻辑 for (let i = 0; i <= (groupFieldsRef.current?.length || 0); i++) { const newSliceRoute = groupFieldsRef.current ?.slice(0, i) .map((field) => item[field]) .filter(Boolean); const oldSliceRoute = groupFieldsRef.current ?.slice(0, i) .map((field) => existingItem?.[field]) .filter(Boolean); if (newSliceRoute && oldSliceRoute && newSliceRoute.join("-") !== oldSliceRoute.join("-")) { gridApi.current.refreshServerSide({ route: oldSliceRoute, }); } if (newSliceRoute) { gridApi.current.refreshServerSide({ route: newSliceRoute }); } } } }); }, [groupFieldsRef.current, gridApi.current, rowRecordRef.current]); return (
{ return data?.child_count; }} onColumnRowGroupChanged={onColumnRowGroupChanged} statusBar={statusBar} // theme={agTheme} initialState={initialState} rowModelType={"serverSide"} onStoreRefreshed={(params: StoreRefreshedEvent) => { restoreExpandedRowsState(); }} isServerSideGroupOpenByDefault={(params) => { return expandedRows.includes( params.rowNode.key || params.rowNode.id ); }} getRowId={(params) => { let rowId = ""; if (params.parentKeys && params.parentKeys.length) { rowId += params.parentKeys.join("-") + "-"; } const groupCols = params.api.getRowGroupColumns(); if (groupCols.length > params.level) { const thisGroupCol = groupCols[params.level]; rowId += params.data[ thisGroupCol.getColDef().field.replace(".", "_") ] + "-"; } if (params.data.id) { rowId = params.data.id; } rowRecordRef.current = { ...rowRecordRef.current, [rowId]: params.data } // setRowRecord((prevRowRecord) => ({ // ...prevRowRecord, // [rowId]: params.data, // })); return rowId; }} blockLoadDebounceMillis={100} onFirstDataRendered={onFirstDataRendered} detailRowAutoHeight={true} cellSelection={true} // loadThemeGoogleFonts={false} suppressServerSideFullWidthLoadingRow={true} allowContextMenuWithControlKey={true} getContextMenuItems={getContextMenuItems} onGridReady={onGridReady} onRowDragEnd={async (event) => { setDragOverNodeId(undefined); const { overNode, node: draggedNode } = event; if (!overNode || !draggedNode) return; const { id: overId, data: overData } = overNode; const { id: draggedId, data: draggedData } = draggedNode; // 合并条件判断,简化逻辑 if (!overData?.id || !draggedData?.id || overId === draggedId) return; try { console.log(overData, draggedData) if (overData?.parent_id === draggedData?.parent_id) { message.info("更新排序"); const result = await utils.client[objectType].updateOrder.mutate({ id: draggedId, overId: overId }); emitDataChange(objectType, result, CrudOperation.UPDATED) } } catch (error) { console.error("更新排序失败:", error); message.error("无法更新排序,请稍后重试。"); } }} rowHeight={rowHeight} cacheBlockSize={30} onRowDragLeave={(event) => { setDragOverNodeId(undefined); }} onRowDragEnter={(event) => { const overNode = event.overNode; setDragOverNodeId(overNode.id); }} onRowDragMove={(event) => { setDragOverNodeId(event.overNode.id); }} // debug={!import.meta.env.PROD} rowClassRules={{ "ag-custom-dragging-class": (params) => { return params.data && params.data.id && params.data.id === dragOverNodeId }, }} {...restProps} />
); }; export default AgServerTable;