495 lines
14 KiB
TypeScript
Executable File
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;
|