zhou zhou
17 小时以前 2dae7a77781f4ef123a673893a9a7ffb34285f8f
#列浮动和路径流程页
8个文件已修改
3个文件已添加
702 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/StickyDataTable.jsx 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx 203 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/utils/useTableLayout.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -220,9 +220,85 @@
        statisticCount: 'Statistic Count',
        preparation: "备料单",
        menuPda: 'MenuPda',
        taskPathTemplate: 'TaskPathTemplate',
        taskPathTemplateNode: 'TaskPathTemplateNode',
        subsystemFlowTemplate: 'SubsystemFlowTemplate',
        flowStepTemplate: 'FlowStepTemplate',
    },
    table: {
        field: {
            flowStepTemplate: {
                flowId: "flowId",
                flowCode: "flowCode",
                stepOrder: "stepOrder",
                stepCode: "stepCode",
                stepName: "stepName",
                stepType: "stepType",
                actionType: "actionType",
                actionConfig: "actionConfig",
                inputMapping: "inputMapping",
                outputMapping: "outputMapping",
                conditionExpression: "conditionExpression",
                skipOnFail: "skipOnFail",
                retryEnabled: "retryEnabled",
                retryConfig: "retryConfig",
                timeoutSeconds: "timeoutSeconds",
            },
            subsystemFlowTemplate: {
                flowCode: "flowCode",
                flowName: "flowName",
                systemCode: "systemCode",
                systemName: "systemName",
                nodeType: "nodeType",
                version: "version",
                isCurrent: "isCurrent",
                effectiveTime: "effectiveTime",
                timeoutStrategy: "timeoutStrategy",
                timeoutSeconds: "timeoutSeconds",
                maxRetryTimes: "maxRetryTimes",
                needNotify: "needNotify",
                notifyTemplate: "notifyTemplate",
                remark: "remark",
            },
            taskPathTemplateNode: {
                templateId: "templateId",
                templateCode: "templateCode",
                nodeOrder: "nodeOrder",
                nodeCode: "nodeCode",
                nodeName: "nodeName",
                nodeType: "nodeType",
                systemCode: "systemCode",
                systemName: "systemName",
                executeParams: "executeParams",
                resultSchema: "resultSchema",
                timeoutMinutes: "timeoutMinutes",
                mandatory: "mandatory",
                parallelExecutable: "parallelExecutable",
                preCondition: "preCondition",
                postCondition: "postCondition",
                nextNodeRules: "nextNodeRules",
            },
            taskPathTemplate: {
                templateCode: "templateCode",
                templateName: "templateName",
                sourceType: "sourceType",
                targetType: "targetType",
                conditionExpression: "conditionExpression",
                conditionDesc: "conditionDesc",
                version: "version",
                isCurrent: "isCurrent",
                effectiveTime: "effectiveTime",
                expireTime: "expireTime",
                priority: "priority",
                timeoutMinutes: "timeoutMinutes",
                maxRetryTimes: "maxRetryTimes",
                retryIntervalSeconds: "retryIntervalSeconds",
                remark: "remark",
                createdBy: "createdBy",
                updatedBy: "updatedBy",
                createdTime: "createdTime",
                updatedTime: "updatedTime",
            },
            outBound: {
                stockWithdrawal: 'StockWithdrawal',
                withdrawal: 'Withdrawal'
rsf-admin/src/i18n/zh.js
@@ -236,9 +236,85 @@
        freeze: '库存冻结',
        transferPoces: '调拨管理',
        menuPda: 'PDA菜单',
        taskPathTemplate: '任务路径模板',
        taskPathTemplateNode: '任务路径模板节点',
        subsystemFlowTemplate: '子系统流程模板',
        flowStepTemplate: '流程步骤模板',
    },
    table: {
        field: {
            flowStepTemplate: {
                flowId: "流程ID",
                flowCode: "流程编码",
                stepOrder: "步骤顺序",
                stepCode: "步骤编码",
                stepName: "步骤名称",
                stepType: "步骤类型",
                actionType: "动作类型",
                actionConfig: "动作配置",
                inputMapping: "输入映射",
                outputMapping: "输出映射",
                conditionExpression: "条件表达式",
                skipOnFail: "失败跳过",
                retryEnabled: "重试启用",
                retryConfig: "重试配置",
                timeoutSeconds: "超时秒数",
            },
            subsystemFlowTemplate: {
                flowCode: "流程编码",
                flowName: "流程名称",
                systemCode: "系统编码",
                systemName: "系统名称",
                nodeType: "节点类型",
                version: "版本",
                isCurrent: "是否当前",
                effectiveTime: "生效时间",
                timeoutStrategy: "超时策略",
                timeoutSeconds: "超时秒数",
                maxRetryTimes: "最大重试次数",
                needNotify: "是否通知",
                notifyTemplate: "通知模板",
                remark: "备注",
            },
            taskPathTemplateNode: {
                templateId: "模板ID",
                templateCode: "模板编码",
                nodeOrder: "节点顺序",
                nodeCode: "节点编码",
                nodeName: "节点名称",
                nodeType: "节点类型",
                systemCode: "系统编码",
                systemName: "系统名称",
                executeParams: "执行参数",
                resultSchema: "结果Schema",
                timeoutMinutes: "超时时间",
                mandatory: "是否必填",
                parallelExecutable: "是否并行执行",
                preCondition: "前置条件",
                postCondition: "后置条件",
                nextNodeRules: "节点规则",
            },
            taskPathTemplate: {
                templateCode: "模板编码",
                templateName: "模板名称",
                sourceType: "源类型",
                targetType: "目标类型",
                conditionExpression: "条件表达式",
                conditionDesc: "条件描述",
                version: "版本",
                isCurrent: "是否当前",
                effectiveTime: "生效时间",
                expireTime: "过期时间",
                priority: "优先级",
                timeoutMinutes: "超时时间",
                maxRetryTimes: "最大重试次数",
                retryIntervalSeconds: "重试间隔",
                remark: "备注",
                createdBy: "创建人",
                updatedBy: "更新人",
                createdTime: "创建时间",
                updatedTime: "更新时间",
            },
            stockTransfer: {
                orgLoc: '源库位',
                tarLoc: '目标库位',
rsf-admin/src/page/ResourceContent.js
@@ -67,7 +67,7 @@
import statisticCount from './statistics/stockStatisticNum';
import preparation from "./orders/preparation";
import menuPda from './menuPda';
// import locItem from "./basicInfo/locItem";
import taskPathTemplate from './taskPathTemplate';
const ResourceContent = (node) => {
  switch (node.component) {
@@ -197,6 +197,8 @@
      return preparation;
    case 'menuPda':
      return menuPda;
    case 'taskPathTemplate':
      return taskPathTemplate;
    // case "locItem":
    //   return locItem;
    default:
rsf-admin/src/page/components/StickyDataTable.jsx
New file
@@ -0,0 +1,78 @@
import React from 'react';
import { DataTable } from 'react-admin';
/**
 * StickyDataTable Component
 *
 * 封装 react-admin 的 DataTable,实现传入列名即可固定列。
 *
 * @param {Object} props
 * @param {string[]} props.stickyLeft - 需要固定在左侧的字段 source 列表
 * @param {string[]} props.stickyRight - 需要固定在右侧的字段 source 列表
 */
export const StickyDataTable = ({ stickyLeft = [], stickyRight = [], children, ...props }) => {
    // 递归处理 Children,确保即便是 Fragment 包裹的列也能被处理
    const processChildren = (children) => {
        return React.Children.map(children, (child) => {
            if (!React.isValidElement(child)) return child;
            // 如果是 Fragment,递归处理其 children
            if (child.type === React.Fragment) {
                return <React.Fragment>{processChildren(child.props.children)}</React.Fragment>;
            }
            const source = child.props.source;
            let stickyStyle = {};
            // 左侧固定
            if (stickyLeft.includes(source)) {
                stickyStyle = {
                    position: 'sticky',
                    left: 0,
                    zIndex: 2, // 比普通内容高
                    backgroundColor: '#FFFFFF',
                    '.MuiTableRow-root:not(.MuiTableRow-head):hover &': {
                        backgroundColor: '#f5f5f5'
                    }
                };
            }
            // 右侧固定
            if (stickyRight.includes(source)) {
                stickyStyle = {
                    position: 'sticky',
                    right: 0,
                    zIndex: 2,
                    backgroundColor: '#FFFFFF',
                    '.MuiTableRow-root:not(.MuiTableRow-head):hover &': {
                        backgroundColor: '#f5f5f5'
                    }
                };
            }
            if (Object.keys(stickyStyle).length > 0) {
                // 合并 sx
                return React.cloneElement(child, {
                    sx: { ...child.props.sx, ...stickyStyle }
                });
            }
            return child;
        });
    };
    return (
        <DataTable {...props} sx={{
            '& .MuiTableCell-head': {
                zIndex: 4,
                borderBottom: 'none' // 遵循之前的优化,去除表头下边框
            },
        }}>
            {processChildren(children)}
        </DataTable>
    );
};
export default StickyDataTable;
rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx
@@ -39,6 +39,8 @@
import { Box, Typography, Card, Stack, Dialog, DialogActions, DialogTitle, LinearProgress } from '@mui/material';
import { styled } from '@mui/material/styles';
import PageDrawer from "../../components/PageDrawer";
import StickyDataTable from "../../components/StickyDataTable";
import useTableLayout from '@/utils/useTableLayout';
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import { fetchInOrderItemColumns } from '../config/orderItemColumns';
@@ -128,13 +130,11 @@
    const [columns, setColumns] = useState([]);
    const { isLoading } = useListContext();
    const refresh = useRefresh();
    const [sidebarIsOpen] = useSidebarState();
    const omittedFields = [
        'id', 'orderId', 'orderCode', 'poCode', 'poId', 'wkType', 'type', 'checkType',
        'spec', 'model', 'purQty', 'purUnit', 'qrcode', 'trackCode', 'splrCode',
        'splrName', 'projectCode', 'supplierId', 'supplierName', 'priceUnitId',
        'shipperId', 'businessTime', 'extendFields.[businessTime]',
        'id', 'orderId', 'orderCode', 'poCode', 'poId', 'wkType', 'type', 'checkType',
        'spec', 'model', 'purQty', 'purUnit', 'qrcode', 'trackCode', 'splrCode',
        'splrName', 'projectCode', 'supplierId', 'supplierName', 'priceUnitId',
        'shipperId', 'businessTime', 'extendFields.[businessTime]',
        'extendFields.[wkType]', 'extendFields.[type]'
    ];
@@ -158,16 +158,11 @@
        }
    }
    const sidebarWidth = sidebarIsOpen ? 200 : 50;
    const contentPadding = 10; // 预留边距
    const rightDrawerWidth = drawerVal ? PAGE_DRAWER_WIDTH : 0;
    const boxMaxWidth = `calc(100vw - ${sidebarWidth + rightDrawerWidth + contentPadding}px)`;
    // 计算 maxHeight: 100vh - (Header+Tabs 86px) - (Toolbar ~50px) - (Filters ~60px) - (Pagination ~50px) - (Padding ~40px) ≈ 290px
    const boxMaxHeight = `calc(100vh - 210px)`;
    const { boxMaxWidth, boxMaxHeight } = useTableLayout(drawerVal);
    return (
        <Box sx={{
            position: 'relative',
            position: 'relative',
            maxHeight: boxMaxHeight,
            maxWidth: boxMaxWidth,
            overflowX: 'auto',
@@ -188,7 +183,8 @@
                />
            )}
            {columns.length > 0 &&
                <DataTable
                <StickyDataTable
                    stickyRight={['createTime']}
                    storeKey='asnOrderItem'
                    bulkActionButtons={false}
                    rowClick={false}
@@ -200,12 +196,13 @@
                                key={column.key || column.props.source}
                                source={column.props.source}
                                label={column.props.label}
                                sx={column.props.sx}
                            >
                                {column}
                            </DataTable.Col>
                        ))
                    }
                </DataTable>}
                </StickyDataTable>}
        </Box>
    )
}
rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx
@@ -2,10 +2,11 @@
import { useNavigate } from 'react-router-dom';
import {
    List,
    DataTable,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    ColumnsButton,
    EditButton,
    FilterButton,
    CreateButton,
@@ -43,20 +44,11 @@
import MyField from "../components/MyField";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
import StickyDataTable from "@/page/components/StickyDataTable";
import useTableLayout from '@/utils/useTableLayout';
import { Dialog, DialogContent, DialogActions, Button } from '@mui/material';
import TaskTemplateFlowViewer from "./TaskTemplateFlowViewer";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .column-name': {
    },
    '& .opt': {
        width: 200
    },
}));
const filters = [
    <SearchInput source="condition" alwaysOn />,
@@ -95,11 +87,54 @@
    />,
]
const baseColumns = [
    <NumberField source="id" />,
    <TextField source="templateCode" label="table.field.taskPathTemplate.templateCode" />,
    <TextField source="templateName" label="table.field.taskPathTemplate.templateName" />,
    <TextField source="sourceType" label="table.field.taskPathTemplate.sourceType" />,
    <TextField source="targetType" label="table.field.taskPathTemplate.targetType" />,
    <TextField source="conditionExpression" label="table.field.taskPathTemplate.conditionExpression" />,
    <TextField source="conditionDesc" label="table.field.taskPathTemplate.conditionDesc" />,
    <NumberField source="version" label="table.field.taskPathTemplate.version" />,
    <NumberField source="isCurrent" label="table.field.taskPathTemplate.isCurrent" />,
    <DateField source="effectiveTime" label="table.field.taskPathTemplate.effectiveTime" showTime />,
    <DateField source="expireTime" label="table.field.taskPathTemplate.expireTime" showTime />,
    <NumberField source="priority" label="table.field.taskPathTemplate.priority" />,
    <NumberField source="timeoutMinutes" label="table.field.taskPathTemplate.timeoutMinutes" />,
    <NumberField source="maxRetryTimes" label="table.field.taskPathTemplate.maxRetryTimes" />,
    <NumberField source="retryIntervalSeconds" label="table.field.taskPathTemplate.retryIntervalSeconds" />,
    <TextField source="remark" label="table.field.taskPathTemplate.remark" />,
    <TextField source="createdBy" label="table.field.taskPathTemplate.createdBy" />,
    <TextField source="updatedBy" label="table.field.taskPathTemplate.updatedBy" />,
    <DateField source="createdTime" label="table.field.taskPathTemplate.createdTime" showTime />,
    <DateField source="updatedTime" label="table.field.taskPathTemplate.updatedTime" showTime />,
    <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
        <TextField source="nickname" />
    </ReferenceField>,
    <DateField source="updateTime" label="common.field.updateTime" showTime />,
    <ReferenceField source="createBy" label="common.field.createBy" reference="user" link={false} sortable={false}>
        <TextField source="nickname" />
    </ReferenceField>,
    <DateField source="createTime" label="common.field.createTime" showTime />,
    <BooleanField source="statusBool" label="common.field.status" sortable={false} />,
    <TextField source="memo" label="common.field.memo" sortable={false} />,
]
const TaskPathTemplateList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const [flowDialog, setFlowDialog] = useState(false);
    const [currentRecord, setCurrentRecord] = useState(null);
    const handleOpenFlow = (record) => {
        setCurrentRecord(record);
        setFlowDialog(true);
    };
    const { boxMaxWidth, boxMaxHeight } = useTableLayout(drawerVal);
    return (
        <Box display="flex">
@@ -120,61 +155,33 @@
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='taskPathTemplate' />
                        <ColumnsButton storeKey='taskPathTemplate' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey='taskPathTemplate'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    expand={() => <TaskPathTemplatePanel />}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo']}
                >
                    <NumberField source="id" />
                    <TextField source="templateCode" label="table.field.taskPathTemplate.templateCode" />
                    <TextField source="templateName" label="table.field.taskPathTemplate.templateName" />
                    <TextField source="sourceType" label="table.field.taskPathTemplate.sourceType" />
                    <TextField source="targetType" label="table.field.taskPathTemplate.targetType" />
                    <TextField source="conditionExpression" label="table.field.taskPathTemplate.conditionExpression" />
                    <TextField source="conditionDesc" label="table.field.taskPathTemplate.conditionDesc" />
                    <NumberField source="version" label="table.field.taskPathTemplate.version" />
                    <NumberField source="isCurrent" label="table.field.taskPathTemplate.isCurrent" />
                    <DateField source="effectiveTime" label="table.field.taskPathTemplate.effectiveTime" showTime />
                    <DateField source="expireTime" label="table.field.taskPathTemplate.expireTime" showTime />
                    <NumberField source="priority" label="table.field.taskPathTemplate.priority" />
                    <NumberField source="timeoutMinutes" label="table.field.taskPathTemplate.timeoutMinutes" />
                    <NumberField source="maxRetryTimes" label="table.field.taskPathTemplate.maxRetryTimes" />
                    <NumberField source="retryIntervalSeconds" label="table.field.taskPathTemplate.retryIntervalSeconds" />
                    <TextField source="remark" label="table.field.taskPathTemplate.remark" />
                    <TextField source="createdBy" label="table.field.taskPathTemplate.createdBy" />
                    <TextField source="updatedBy" label="table.field.taskPathTemplate.updatedBy" />
                    <DateField source="createdTime" label="table.field.taskPathTemplate.createdTime" showTime />
                    <DateField source="updatedTime" label="table.field.taskPathTemplate.updatedTime" showTime />
                    <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <ReferenceField source="createBy" label="common.field.createBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <DateField source="createTime" label="common.field.createTime" showTime />
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
                <TableItems drawerVal={drawerVal} onOpenFlow={handleOpenFlow} />
            </List>
            <TaskPathTemplateCreate
                open={createDialog}
                setOpen={setCreateDialog}
            />
            <Dialog
                open={flowDialog}
                onClose={() => setFlowDialog(false)}
                fullWidth
                maxWidth="xl"
            >
                <DialogContent>
                    <TaskTemplateFlowViewer record={currentRecord} />
                </DialogContent>
                <DialogActions>
                    <Button onClick={() => setFlowDialog(false)} color="primary">
                        {translate('ra.action.close')}
                    </Button>
                </DialogActions>
            </Dialog>
            <PageDrawer
                title='TaskPathTemplate Detail'
                drawerVal={drawerVal}
@@ -185,4 +192,82 @@
    )
}
const TableItems = ({ drawerVal, onOpenFlow }) => {
    const { isLoading } = useListContext();
    const { boxMaxWidth, boxMaxHeight } = useTableLayout(drawerVal);
    // Wait, the above simple Button won't work easily because it doesn't have access to the record of the row being rendered
    // when defined in this scope. The `rowClick` works because the datagrid calls it with the record.
    // The `EditButton` works because it uses `useRecordContext`.
    // So I need a custom component for the button that uses `useRecordContext`.
    return (
        <Box sx={{
            position: 'relative',
            maxHeight: boxMaxHeight,
            maxWidth: boxMaxWidth,
            overflowX: 'auto',
            overflowY: 'auto',
            '& .MuiTableCell-root': {
                whiteSpace: 'nowrap',
            }
        }}>
            {baseColumns.length > 0 &&
                <StickyDataTable
                    stickyRight={['opt']}
                    storeKey='taskPathTemplate'
                    bulkActionButtons={false}
                    rowClick={false}
                    hiddenColumns={['id', 'createTime', 'createBy', 'memo', 'statusBool']}
                >
                    {baseColumns
                        .map((column) => (
                            <DataTable.Col
                                key={column.key || column.props.source}
                                source={column.props.source}
                                label={column.props.label}
                                sx={column.props.sx}
                            >
                                {column}
                            </DataTable.Col>
                        ))
                    }
                    <DataTable.Col
                        source="opt"
                        label="common.field.opt"
                    >
                        <WrapperField source="opt" cellClassName="opt" label="common.field.opt">
                            <ViewFlowButton onClick={onOpenFlow} />
                            <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                            <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                        </WrapperField>
                    </DataTable.Col>
                </StickyDataTable>}
        </Box>
    )
}
import AccountTreeIcon from '@mui/icons-material/AccountTree';
const ViewFlowButton = ({ onClick }) => {
    const record = useRecordContext();
    if (!record) return null;
    return (
        <Button
            onClick={(e) => {
                e.stopPropagation();
                onClick && onClick(record);
            }}
            sx={{ padding: '1px', fontSize: '.75rem', minWidth: 'auto', marginRight: 0.5 }}
            color="primary"
        >
            <AccountTreeIcon />
        </Button>
    )
}
export default TaskPathTemplateList;
rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx
New file
@@ -0,0 +1,169 @@
import React, { useState, useEffect } from "react";
import {
    useTranslate,
    useListController,
    ListContextProvider,
    TextField,
    NumberField,
    Pagination,
    DataTable,
} from 'react-admin';
import { Box, Grid, Card, CardContent, Typography } from '@mui/material';
import StickyDataTable from "@/page/components/StickyDataTable";
const ViewTable = ({ title, resource, filter, onClick, selectedId, columns }) => {
    const listContext = useListController({
        resource: resource,
        filter: filter,
        perPage: 100,
        sort: { field: 'id', order: 'ASC' },
        disableSyncWithLocation: true,
    });
    if (!filter) {
        return (
            <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
                <Box p={1} borderBottom={1} borderColor="divider">
                    <Typography variant="subtitle1">{title}</Typography>
                </Box>
                <CardContent sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    <Typography variant="body2" color="textSecondary">
                        请先选择上一级数据
                    </Typography>
                </CardContent>
            </Card>
        );
    }
    return (
        <ListContextProvider value={listContext}>
            <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
                <Box p={1} borderBottom={1} borderColor="divider">
                    <Typography variant="subtitle1">{title}</Typography>
                </Box>
                <Box flexGrow={1} overflow="auto" sx={{
                    '& .MuiTableCell-root': {
                        whiteSpace: 'nowrap',
                    },
                }}>
                    <StickyDataTable
                        storeKey={`${resource}_flow_view`}
                        bulkActionButtons={false}
                        rowClick={(id, resource, record) => {
                            if (onClick) {
                                onClick(record);
                            }
                            return false;
                        }}
                        sx={{
                            '& .RaDatagrid-row': {
                                cursor: 'pointer',
                                '&:hover': {
                                    backgroundColor: 'action.hover',
                                },
                            },
                            '& .RaDatagrid-row.Mui-selected': {
                                backgroundColor: 'action.selected',
                            }
                        }}
                    // Manually handle selection style if needed, or rely on StickyDataTable support
                    >
                        {columns.map((col, index) => (
                            <DataTable.Col
                                key={col.props.source}
                                source={col.props.source}
                                label={col.props.label}
                            >
                                {col}
                            </DataTable.Col>
                        ))}
                    </StickyDataTable>
                </Box>
                <Pagination rowsPerPageOptions={[10, 25, 50, 100]} />
            </Card>
        </ListContextProvider>
    );
};
const TaskTemplateFlowViewer = ({ record }) => {
    const translate = useTranslate();
    const [selectedNode, setSelectedNode] = useState(null);
    const [selectedFlow, setSelectedFlow] = useState(null);
    // Reset selection when record changes
    useEffect(() => {
        setSelectedNode(null);
        setSelectedFlow(null);
    }, [record]);
    const handleNodeClick = (node) => {
        if (!node) return;
        setSelectedNode(node);
        setSelectedFlow(null);
    };
    const handleFlowClick = (flow) => {
        if (!flow) return;
        setSelectedFlow(flow);
    };
    const nodeColumns = [
        <NumberField source="nodeOrder" label="table.field.taskPathTemplateNode.nodeOrder" key="nodeOrder" />,
        <TextField source="nodeCode" label="table.field.taskPathTemplateNode.nodeCode" key="nodeCode" />,
        <TextField source="nodeName" label="table.field.taskPathTemplateNode.nodeName" key="nodeName" />,
        <TextField source="systemCode" label="table.field.taskPathTemplateNode.systemCode" key="systemCode" />,
    ];
    const flowColumns = [
        <TextField source="flowCode" label="table.field.subsystemFlowTemplate.flowCode" key="flowCode" />,
        <TextField source="flowName" label="table.field.subsystemFlowTemplate.flowName" key="flowName" />,
        <TextField source="systemCode" label="table.field.subsystemFlowTemplate.systemCode" key="systemCode" />,
    ];
    const stepColumns = [
        <NumberField source="stepOrder" label="table.field.flowStepTemplate.stepOrder" key="stepOrder" />,
        <TextField source="stepCode" label="table.field.flowStepTemplate.stepCode" key="stepCode" />,
        <TextField source="stepName" label="table.field.flowStepTemplate.stepName" key="stepName" />,
        <TextField source="stepType" label="table.field.flowStepTemplate.stepType" key="stepType" />,
    ];
    if (!record) return null;
    return (
        <Box sx={{ height: '80vh', p: 1, backgroundColor: '#f0f0f0' }}>
            <Grid container spacing={1} sx={{ height: '100%' }}>
                <Grid item xs={4} sx={{ height: '100%' }}>
                    <ViewTable
                        title={translate('menu.taskPathTemplateNode')}
                        resource="taskPathTemplateNode"
                        filter={{ templateId: record.id }}
                        onClick={handleNodeClick}
                        selectedId={selectedNode?.id}
                        columns={nodeColumns}
                    />
                </Grid>
                <Grid item xs={4} sx={{ height: '100%' }}>
                    <ViewTable
                        title={translate('menu.subsystemFlowTemplate')}
                        resource="subsystemFlowTemplate"
                        filter={selectedNode ? { systemCode: selectedNode.systemCode } : null}
                        onClick={handleFlowClick}
                        selectedId={selectedFlow?.id}
                        columns={flowColumns}
                    />
                </Grid>
                <Grid item xs={4} sx={{ height: '100%' }}>
                    <ViewTable
                        title={translate('menu.flowStepTemplate')}
                        resource="flowStepTemplate"
                        filter={selectedFlow ? { flowId: selectedFlow.id } : null}
                        columns={stepColumns}
                    />
                </Grid>
            </Grid>
        </Box>
    );
};
export default TaskTemplateFlowViewer;
rsf-admin/src/utils/useTableLayout.js
New file
@@ -0,0 +1,33 @@
import { useSidebarState } from 'react-admin';
import { PAGE_DRAWER_WIDTH } from '@/config/setting';
/**
 * useTableLayout Hook
 *
 * 统一计算包含侧边栏、右侧抽屉的表格容器最大宽高。
 *
 * @param {boolean} drawerOpen - 右侧抽屉是否打开
 * @param {number} [customHeightOffset=210] - 自定义高度扣除偏移量 (Header + Toolbar + Filters + Pagination + Padding)
 * @returns {Object} { boxMaxWidth, boxMaxHeight } - 计算后的最大宽度和高度样式值
 */
export const useTableLayout = (drawerOpen = false, customHeightOffset = 210) => {
    const [sidebarIsOpen] = useSidebarState();
    const sidebarWidth = sidebarIsOpen ? 200 : 50;
    const contentPadding = 10; // 预留边距
    const rightDrawerWidth = drawerOpen ? PAGE_DRAWER_WIDTH : 0;
    // 计算最大宽度:视口宽度 - (侧边栏 + 右侧抽屉 + 边距)
    const boxMaxWidth = `calc(100vw - ${sidebarWidth + rightDrawerWidth + contentPadding}px)`;
    // 计算最大高度:视口高度 - 顶部和其他UI元素的高度
    const boxMaxHeight = `calc(100vh - ${customHeightOffset}px)`;
    return {
        boxMaxWidth,
        boxMaxHeight
    };
};
export default useTableLayout;
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java
@@ -25,7 +25,6 @@
    @Autowired
    private FlowStepTemplateService flowStepTemplateService;
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @PostMapping("/flowStepTemplate/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
@@ -33,25 +32,21 @@
        return R.ok().add(flowStepTemplateService.page(pageParam, pageParam.buildWrapper(true)));
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @PostMapping("/flowStepTemplate/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(flowStepTemplateService.list());
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @PostMapping({"/flowStepTemplate/many/{ids}", "/flowStepTemplates/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(flowStepTemplateService.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @GetMapping("/flowStepTemplate/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(flowStepTemplateService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:save')")
    @OperationLog("Create 子流程步骤模板")
    @PostMapping("/flowStepTemplate/save")
    public R save(@RequestBody FlowStepTemplate flowStepTemplate) {
@@ -65,7 +60,6 @@
        return R.ok("Save Success").add(flowStepTemplate);
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:update')")
    @OperationLog("Update 子流程步骤模板")
    @PostMapping("/flowStepTemplate/update")
    public R update(@RequestBody FlowStepTemplate flowStepTemplate) {
@@ -77,7 +71,6 @@
        return R.ok("Update Success").add(flowStepTemplate);
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:remove')")
    @OperationLog("Delete 子流程步骤模板")
    @PostMapping("/flowStepTemplate/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
@@ -87,7 +80,6 @@
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @PostMapping("/flowStepTemplate/query")
    public R query(@RequestParam(required = false) String condition) {
        List<KeyValVo> vos = new ArrayList<>();
@@ -101,7 +93,6 @@
        return R.ok().add(vos);
    }
    @PreAuthorize("hasAuthority('system:flowStepTemplate:list')")
    @PostMapping("/flowStepTemplate/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(flowStepTemplateService.list(), FlowStepTemplate.class), response);
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java
@@ -25,7 +25,6 @@
    @Autowired
    private SubsystemFlowTemplateService subsystemFlowTemplateService;
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @PostMapping("/subsystemFlowTemplate/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
@@ -33,25 +32,21 @@
        return R.ok().add(subsystemFlowTemplateService.page(pageParam, pageParam.buildWrapper(true)));
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @PostMapping("/subsystemFlowTemplate/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(subsystemFlowTemplateService.list());
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @PostMapping({"/subsystemFlowTemplate/many/{ids}", "/subsystemFlowTemplates/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(subsystemFlowTemplateService.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @GetMapping("/subsystemFlowTemplate/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(subsystemFlowTemplateService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:save')")
    @OperationLog("Create 子系统模板")
    @PostMapping("/subsystemFlowTemplate/save")
    public R save(@RequestBody SubsystemFlowTemplate subsystemFlowTemplate) {
@@ -65,7 +60,6 @@
        return R.ok("Save Success").add(subsystemFlowTemplate);
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:update')")
    @OperationLog("Update 子系统模板")
    @PostMapping("/subsystemFlowTemplate/update")
    public R update(@RequestBody SubsystemFlowTemplate subsystemFlowTemplate) {
@@ -77,7 +71,6 @@
        return R.ok("Update Success").add(subsystemFlowTemplate);
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:remove')")
    @OperationLog("Delete 子系统模板")
    @PostMapping("/subsystemFlowTemplate/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
@@ -87,7 +80,6 @@
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @PostMapping("/subsystemFlowTemplate/query")
    public R query(@RequestParam(required = false) String condition) {
        List<KeyValVo> vos = new ArrayList<>();
@@ -101,7 +93,6 @@
        return R.ok().add(vos);
    }
    @PreAuthorize("hasAuthority('system:subsystemFlowTemplate:list')")
    @PostMapping("/subsystemFlowTemplate/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(subsystemFlowTemplateService.list(), SubsystemFlowTemplate.class), response);
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java
@@ -25,7 +25,7 @@
    @Autowired
    private TaskPathTemplateNodeService taskPathTemplateNodeService;
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @PostMapping("/taskPathTemplateNode/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
@@ -33,25 +33,25 @@
        return R.ok().add(taskPathTemplateNodeService.page(pageParam, pageParam.buildWrapper(true)));
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @PostMapping("/taskPathTemplateNode/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(taskPathTemplateNodeService.list());
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @PostMapping({"/taskPathTemplateNode/many/{ids}", "/taskPathTemplateNodes/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(taskPathTemplateNodeService.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @GetMapping("/taskPathTemplateNode/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(taskPathTemplateNodeService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:save')")
    @OperationLog("Create 物料权限")
    @PostMapping("/taskPathTemplateNode/save")
    public R save(@RequestBody TaskPathTemplateNode taskPathTemplateNode) {
@@ -65,7 +65,7 @@
        return R.ok("Save Success").add(taskPathTemplateNode);
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:update')")
    @OperationLog("Update 物料权限")
    @PostMapping("/taskPathTemplateNode/update")
    public R update(@RequestBody TaskPathTemplateNode taskPathTemplateNode) {
@@ -77,7 +77,7 @@
        return R.ok("Update Success").add(taskPathTemplateNode);
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:remove')")
    @OperationLog("Delete 物料权限")
    @PostMapping("/taskPathTemplateNode/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
@@ -87,7 +87,7 @@
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @PostMapping("/taskPathTemplateNode/query")
    public R query(@RequestParam(required = false) String condition) {
        List<KeyValVo> vos = new ArrayList<>();
@@ -101,7 +101,7 @@
        return R.ok().add(vos);
    }
    @PreAuthorize("hasAuthority('system:taskPathTemplateNode:list')")
    @PostMapping("/taskPathTemplateNode/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(taskPathTemplateNodeService.list(), TaskPathTemplateNode.class), response);