From 2dae7a77781f4ef123a673893a9a7ffb34285f8f Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期二, 03 二月 2026 14:34:13 +0800
Subject: [PATCH] #列浮动和路径流程页

---
 rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx                                           |  203 ++++++++++++----
 rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java      |    9 
 rsf-admin/src/utils/useTableLayout.js                                                                  |   33 ++
 rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java  |   18 
 rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx                                         |  169 ++++++++++++++
 rsf-admin/src/i18n/zh.js                                                                               |   76 ++++++
 rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java |    9 
 rsf-admin/src/i18n/en.js                                                                               |   76 ++++++
 rsf-admin/src/page/ResourceContent.js                                                                  |    4 
 rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx                                            |   27 +-
 rsf-admin/src/page/components/StickyDataTable.jsx                                                      |   78 ++++++
 11 files changed, 600 insertions(+), 102 deletions(-)

diff --git a/rsf-admin/src/i18n/en.js b/rsf-admin/src/i18n/en.js
index a12d985..ce9f2cb 100644
--- a/rsf-admin/src/i18n/en.js
+++ b/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'
diff --git a/rsf-admin/src/i18n/zh.js b/rsf-admin/src/i18n/zh.js
index 3189fc4..4088f47 100644
--- a/rsf-admin/src/i18n/zh.js
+++ b/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: '鐩爣搴撲綅',
diff --git a/rsf-admin/src/page/ResourceContent.js b/rsf-admin/src/page/ResourceContent.js
index e7bcb63..4820cef 100644
--- a/rsf-admin/src/page/ResourceContent.js
+++ b/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:
diff --git a/rsf-admin/src/page/components/StickyDataTable.jsx b/rsf-admin/src/page/components/StickyDataTable.jsx
new file mode 100644
index 0000000..fe4189b
--- /dev/null
+++ b/rsf-admin/src/page/components/StickyDataTable.jsx
@@ -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 鍖呰9鐨勫垪涔熻兘琚鐞�
+    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;
diff --git a/rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx b/rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx
index ff77191..a541c22 100644
--- a/rsf-admin/src/page/orders/asnOrderItem/AsnOrderItemList.jsx
+++ b/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>
     )
 }
diff --git a/rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx b/rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx
index 56117a7..7e3d17f 100644
--- a/rsf-admin/src/page/taskPathTemplate/TaskPathTemplateList.jsx
+++ b/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;
diff --git a/rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx b/rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx
new file mode 100644
index 0000000..c388420
--- /dev/null
+++ b/rsf-admin/src/page/taskPathTemplate/TaskTemplateFlowViewer.jsx
@@ -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;
diff --git a/rsf-admin/src/utils/useTableLayout.js b/rsf-admin/src/utils/useTableLayout.js
new file mode 100644
index 0000000..bbd1707
--- /dev/null
+++ b/rsf-admin/src/utils/useTableLayout.js
@@ -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)`;
+
+    // 璁$畻鏈�澶ч珮搴︼細瑙嗗彛楂樺害 - 椤堕儴鍜屽叾浠朥I鍏冪礌鐨勯珮搴�
+    const boxMaxHeight = `calc(100vh - ${customHeightOffset}px)`;
+
+    return {
+        boxMaxWidth,
+        boxMaxHeight
+    };
+};
+
+export default useTableLayout;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java
index 9ac3230..0b27bd8 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java
+++ b/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);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java
index 5aae0cc..60d3578 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java
+++ b/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);
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java b/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java
index 66a99c3..3d1f6f6 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java
+++ b/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);

--
Gitblit v1.9.1