collect-system/apps/web/src/components/presentation/ag-server-table.tsx

495 lines
14 KiB
TypeScript
Executable File

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<string, any>;
rowHeight?: number
}
type AgTableProps = AgTableSpecificProps &
Omit<AgGridReactProps, keyof AgTableSpecificProps>;
const AgServerTable: React.FC<AgTableProps> = ({
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<any[]>(defaultExpandedRows);
const gridApi = useRef<GridApi | null>(null);
const groupFieldsRef = useRef<string[]>()
const rowRecordRef = useRef<Record<string, any>>()
const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(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<IServerSideDatasource>(() => {
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<GridState & { rowIndex: number }>(() => {
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<HTMLDivElement>(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 (
<div
ref={containerRef}
style={{
...containerStyle,
flexDirection: "column",
height,
}}>
<div style={{ ...gridStyle }} className="ag-theme-alpine">
<AgGridReact
serverSideInitialRowCount={initialRowCount}
localeText={AG_GRID_LOCALE_CH}
defaultColDef={{
flex: 1,
minWidth: 150,
resizable: true,
sortable: false,
// menuTabs: ['filterMenuTab'],
suppressHeaderMenuButton: true,
floatingFilter: true
}}
getChildCount={(data) => {
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}
/>
</div>
</div>
);
};
export default AgServerTable;