72139f39a0845f8de31fd44bc5dd7077df17d48e..d6fcca25f2979710b008502e26aeceb8993f86ae
1 天以前 zhou zhou
#
d6fcca 对比 | 目录
1 天以前 zhou zhou
#
a87c92 对比 | 目录
27个文件已添加
10个文件已修改
2462 ■■■■■ 已修改文件
rsf-admin/src/i18n/core/chineseMessages.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/menuPda/MenuPdaEdit.jsx 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/menuPda/MenuPdaList.jsx 456 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/menuPda/index.jsx 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuCreate.jsx 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuEdit.jsx 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuList.jsx 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuPanel.jsx 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/pdaRoleMenu/index.jsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/role/AssignPermissions_pda.jsx 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/role/RoleList.jsx 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/resources/application-dev.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/PdaGeneralParam.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/InBoundService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MenuPdaController.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MenuPda.java 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MenuPdaMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MenuPdaService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MenuPdaServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/PdaRoleMenuController.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/PdaRoleMenu.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/PdaRoleMenuMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/PdaRoleMenuService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/PdaRoleMenuServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/menuPda.sql 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/pdaRoleMenu.sql 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/MenuPdaMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/system/PdaRoleMenuMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/core/chineseMessages.js
@@ -25,6 +25,7 @@
      save: "保存",
      search: "搜索",
      select_all_button: "全部选中",
      select_all: "全部选中",
      select_row: "选中一行",
      show: "显示",
      sort: "排序",
rsf-admin/src/i18n/en.js
@@ -219,6 +219,7 @@
        stockStatistic: 'Stock Statistic',
        statisticCount: 'Statistic Count',
        preparation: "备料单",
        menuPda: 'MenuPda',
    },
    table: {
        field: {
@@ -311,6 +312,26 @@
                    button: 'Button',
                }
            },
            menuPda: {
                name: "name",
                parentId: "parent id",
                parentName: "higher",
                path: "path",
                pathName: "pathName",
                route: "route",
                component: "component",
                brief: "brief",
                code: "code",
                type: "type",
                authority: "authority",
                icon: "icon",
                sort: "sort",
                meta: "meta",
                enums: {
                    menu: 'Menu',
                    button: 'Button',
                }
            },
            user: {
                username: "username",
                password: "password",
rsf-admin/src/i18n/zh.js
@@ -235,6 +235,7 @@
        platform: '平台管理',
        freeze: '库存冻结',
        transferPoces: '调拨管理',
        menuPda: 'PDA菜单',
    },
    table: {
        field: {
@@ -340,6 +341,26 @@
                    button: '按钮',
                }
            },
            menuPda: {
                name: "菜单名称",
                parentId: "上级菜单ID",
                parentName: "上级菜单",
                path: "路径",
                pathName: "路径名",
                route: "路由地址",
                component: "组件",
                brief: "简述",
                code: "标识",
                type: "类型",
                authority: "鉴权",
                icon: "图标",
                sort: "排序",
                meta: "元",
                enums: {
                    menu: '菜单',
                    button: '按钮',
                }
            },
            user: {
                username: "账号",
                password: "密码",
rsf-admin/src/page/ResourceContent.js
@@ -64,6 +64,7 @@
import inStatisticItem from './statistics/inStockItem';
import statisticCount from './statistics/stockStatisticNum';
import preparation from "./orders/preparation";
import menuPda from './menuPda';
// import locItem from "./basicInfo/locItem";
const ResourceContent = (node) => {
@@ -174,13 +175,13 @@
      return transfer;
    case "locRevise":
      return locRevise;
    case "locDeadReport":
    case "locDeadReport":
      return locDeadReport;
    case "inStatistic":
      return inStatistic;
    case "outStatistic":
    case "outStatistic":
      return outStatistic;
    case "outStatisticItem":
    case "outStatisticItem":
      return outStatisticItem;
    case "inStatisticItem":
      return inStatisticItem;
@@ -188,6 +189,8 @@
      return statisticCount;
    case "preparation":
      return preparation;
    case 'menuPda':
      return menuPda;
    // case "locItem":
    //   return locItem;
    default:
rsf-admin/src/page/menuPda/MenuPdaEdit.jsx
New file
@@ -0,0 +1,225 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SaveButton,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    Toolbar,
    required,
    useNotify,
    Form,
    useUpdate,
    useCreate,
    useCreateContext,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
import TreeSelectInput from "@/page/components/TreeSelectInput";
const EditContent = ({ editRecord }) => {
    const { resource } = useCreateContext();
    return (
        <Grid container rowSpacing={2} columnSpacing={2}>
            <Grid item xs={6} display="flex" gap={1}>
                <TreeSelectInput
                    label="table.field.menu.parentName"
                    value={editRecord?.parentId}
                    isTranslate
                    resource={resource}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="table.field.menu.name"
                    source="name"
                    parse={v => v}
                    validate={required()}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="table.field.menu.route"
                    source="route"
                    validate={required()}
                    parse={v => v}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="table.field.menu.component"
                    source="component"
                    parse={v => v}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <SelectInput
                    label="table.field.menu.type"
                    source="type"
                    validate={required()}
                    choices={[
                        { id: 0, name: 'table.field.menu.enums.menu' },
                        { id: 1, name: 'table.field.menu.enums.button' },
                    ]}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="table.field.menu.authority"
                    source="authority"
                    parse={v => v}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="table.field.menu.icon"
                    source="icon"
                    parse={v => v}
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <NumberInput
                    label="table.field.menu.sort"
                    source="sort"
                />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <StatusSelectInput />
            </Grid>
            <Grid item xs={6} display="flex" gap={1}>
                <TextInput
                    label="common.field.memo"
                    source="memo"
                    parse={v => v}
                    fullWidth
                    multiline
                    minRows={2}
                    autoFocus
                />
            </Grid>
        </Grid>
    )
}
const MenuPdaEdit = (props) => {
    const { editRecord, open, setOpen, callback, resource } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const [update] = useUpdate();
    const [create] = useCreate();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async (data) => {
        setOpen(false);
        callback();
        notify('common.response.success', { type: 'info' });
    };
    const handleError = async (data) => {
        notify('common.response.fail', { type: 'error' });
    };
    const onSubmit = (data) => {
        const _params = { ...data };
        if (editRecord) {
            if (_params.parentId === editRecord.id) {
                notify('common.response.dataError', { type: 'error' });
                return;
            }
            update(
                resource,
                {
                    id: editRecord.id,
                    data: _params,
                },
                {
                    onSuccess: () => {
                        handleSuccess();
                    },
                    onError: (error) => {
                        handleError();
                    },
                }
            );
        } else {
            create(
                resource,
                { data: _params },
                {
                    onSuccess: () => {
                        handleSuccess();
                    },
                    onError: (error) => {
                        handleError();
                    },
                }
            );
        }
    };
    return (
        <>
            <CreateBase>
                <Dialog
                    open={open}
                    onClose={handleClose}
                    aria-labelledby="form-dialog-title"
                    fullWidth
                    disableRestoreFocus
                    maxWidth="md"   // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
                >
                    <Form record={editRecord} onSubmit={onSubmit}>
                        <DialogTitle id="form-dialog-title" sx={{
                            position: 'sticky',
                            top: 0,
                            backgroundColor: 'background.paper',
                            zIndex: 1000
                        }}
                        >
                            {editRecord ? translate('update.title') : translate('create.title')}
                            <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                                <DialogCloseButton onClose={handleClose} />
                            </Box>
                        </DialogTitle>
                        <DialogContent sx={{ mt: 2 }}>
                            <EditContent
                                editRecord={editRecord}
                            />
                        </DialogContent>
                        <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                            <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
                                <SaveButton />
                            </Toolbar>
                        </DialogActions>
                    </Form>
                </Dialog>
            </CreateBase>
        </>
    )
}
export default MenuPdaEdit;
rsf-admin/src/page/menuPda/MenuPdaList.jsx
New file
@@ -0,0 +1,456 @@
import React from 'react';
import {
    Title,
    useTranslate,
    useNotify,
    useRedirect,
    useRefresh,
    useDelete,
} from 'react-admin';
import { styled } from '@mui/material/styles';
import {
    Box,
    Collapse,
    IconButton,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Paper,
    Card,
    Typography,
    TextField,
    Tooltip,
    Button,
    Chip,
    LinearProgress,
} from '@mui/material';
import { Add, Edit, Delete } from '@mui/icons-material';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import RefreshIcon from '@mui/icons-material/Refresh';
import request from '@/utils/request';
import DeptEdit from './MenuPdaEdit';
import * as Icons from '@mui/icons-material';
const RESOURCE = 'menuPda';
const TITLE = 'menu.menuPda';
const columns = [
    {
        id: 'name',
        label: 'table.field.menuPda.name',
        minWidth: 200,
    },
    {
        id: 'icon',
        label: 'table.field.menuPda.icon',
        minWidth: 80,
        format: (iconStr) => {
            if (iconStr) {
                const IconComponent = getIconComponent(iconStr);
                if (IconComponent) {
                    return <IconComponent />;
                }
            }
        }
    },
    {
        id: 'route',
        label: 'table.field.menuPda.route',
        minWidth: 80,
    },
    {
        id: 'component',
        label: 'table.field.menuPda.component',
        minWidth: 100,
    },
    {
        id: 'authority',
        label: 'table.field.menuPda.authority',
        minWidth: 100,
    },
    {
        id: 'type',
        label: 'table.field.menuPda.type',
        minWidth: 100,
        format: (val) => {
            if (Number(val) === 0) {
                return <Chip variant='outlined' label="Menu" color="primary" size='small' />;
            } else {
                return <Chip variant='outlined' label="Button" color="default" size='small' />;
            }
        }
    },
    {
        id: 'sort',
        label: 'table.field.menuPda.sort',
        minWidth: 80,
        format: (val) => {
            return <Typography variant='body2' sx={{ fontWeight: 'bold' }}>{val}</Typography>
        }
    },
    // {
    //     id: 'updateTime',
    //     label: 'common.field.updateTime',
    //     minWidth: 120,
    //     format: (val) => {
    //         return new Date(val).toLocaleString();
    //     }
    // },
    {
        id: 'id',
        label: 'common.field.id',
        minWidth: 80,
    },
    {
        id: 'actions',
        label: 'common.field.opt',
        minWidth: 100,
    },
];
const getIconComponent = (iconStr) => {
    return Icons[iconStr] || null;
};
const StyledTableRow = styled(TableRow)(({ theme }) => ({
    '& .MuiButtonBase-root': {
        padding: '0px 8px'
    }
}));
const StyledTableCell = styled(TableCell)(({ theme }) => ({
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    maxWidth: 600,
    // 确保所有单元格有基本的内边距
    padding: '8px 16px',
}));
const TreeTableRow = (props) => {
    const { row, depth = 0, openNodes, setOpenNodes, onEdit, onDelete } = props;
    const translate = useTranslate();
    const toggleNode = (id) => {
        setOpenNodes(prevState => ({ ...prevState, [id]: !prevState[id] }));
    };
    const isOpen = openNodes[row.id] || false;
    // 更明显的透明度渐变
    const getOpacity = (currentDepth) => {
        // 第一级:100%,第二级:75%,第三级:50%,第四级:40%
        const opacities = [1, 0.9, 0.8, 0.7];
        return opacities[currentDepth] || 0.4;
    };
    const opacity = getOpacity(depth);
    return (
        <React.Fragment>
            <StyledTableRow
                hover
                tabIndex={-1}
                key={row.id}
                sx={{
                    opacity: depth > 0 ? opacity : 1,
                    // 添加背景色渐变增强效果
                    backgroundColor: depth > 0 ? `rgba(0, 0, 0, ${0.02 * (3 - depth)})` : 'inherit',
                }}
            >
                <StyledTableCell sx={{
                    padding: 0,
                    width: 60,
                    // 进一步减小缩进距离到12px
                    paddingLeft: depth * 4
                }}>
                    {row.children && row.children.length > 0 && (
                        <IconButton
                            aria-label="expand row"
                            size="small"
                            onClick={() => toggleNode(row.id)}
                            sx={{
                                opacity: depth > 0 ? opacity : 1,
                            }}
                        >
                            {isOpen ? <KeyboardArrowDownIcon /> : <KeyboardArrowRightIcon />}
                        </IconButton>
                    )}
                </StyledTableCell>
                {columns.map((column, idx) => {
                    if (column.id !== 'actions') {
                        let value = row[column.id];
                        if (column.id === 'name' && row['type'] === 0) {
                            value = translate(value);
                        }
                        return (
                            <StyledTableCell
                                key={column.id}
                                align={column.align || 'left'}
                                style={{
                                    // 名称列也使用12px缩进
                                    paddingLeft: idx === 0 ? (depth * 24 + 16) : 16,
                                    // opacity: column.id === 'icon' && .6
                                }}
                                onClick={() => column.id === 'name' && toggleNode(row.id)}
                                sx={{
                                    opacity: column.id === 'icon' ? 0.6 : (depth > 0 ? opacity : 1),
                                    fontWeight: 400,
                                    // 使用字体大小或颜色来区分层级
                                    fontSize: depth === 0 ? '0.95rem' : '0.9rem',
                                    // 或者使用不同的颜色
                                    color: depth === 0 ? 'text.primary' : `rgba(0, 0, 0, ${opacity})`,
                                }}
                            >
                                {column.format ? column.format(value) : value}
                            </StyledTableCell>
                        )
                    }
                })}
                <StyledTableCell>
                    <Tooltip title="Edit">
                        <IconButton
                            onClick={() => onEdit(row)}
                            sx={{
                                opacity: depth > 0 ? opacity : 1,
                            }}
                            size="small"
                        >
                            <Edit />
                        </IconButton>
                    </Tooltip>
                    <Tooltip title="Delete">
                        <IconButton
                            onClick={() => onDelete(row)}
                            sx={{
                                opacity: depth > 0 ? opacity : 1,
                            }}
                            size="small"
                        >
                            <Delete />
                        </IconButton>
                    </Tooltip>
                </StyledTableCell>
            </StyledTableRow>
            {row.children && row.children.length > 0 && isOpen && (
                row.children.map((child) => (
                    <TreeTableRow
                        key={child.id}
                        row={child}
                        depth={depth + 1}
                        onEdit={onEdit}
                        onDelete={onDelete}
                        openNodes={openNodes}
                        setOpenNodes={setOpenNodes}
                    />
                ))
            )}
        </React.Fragment>
    );
};
const MenuPdaList = () => {
    const translate = useTranslate();
    const notify = useNotify();
    const redirect = useRedirect();
    const refresh = useRefresh();
    const [deleteOne] = useDelete();
    const [treeData, setTreeData] = React.useState(null);
    const [filter, setFilter] = React.useState("");
    const [createDialog, setCreateDialog] = React.useState(false);
    const [editRecord, setEditRecord] = React.useState(null);
    const [openNodes, setOpenNodes] = React.useState({});
    const [expandAll, setExpandAll] = React.useState(false);
    const notifyState = React.useRef({ last: '', at: 0 });
    const pushNotify = React.useCallback((type, msg) => {
        const text = typeof msg === 'string' ? msg : (msg || '');
        const now = Date.now();
        if (notifyState.current.last === text && now - notifyState.current.at < 1500) return;
        notifyState.current = { last: text, at: now };
        notify(text, { type, messageArgs: { _: text } });
    }, [notify]);
    const http = async () => {
        const res = await request.post(RESOURCE + '/tree', {
            condition: filter
        });
        if (res?.data?.code === 200) {
            setTreeData(res.data.data);
        } else {
            const msg = translate('ra.notification.http_error', { _: res?.data?.msg || 'Request failed' });
            pushNotify('warning', msg);
        }
    }
    React.useEffect(() => {
        http();
    }, [filter]);
    const handleRefresh = () => {
        http();
    };
    const handleAdd = () => {
        setEditRecord(null);
        setCreateDialog(true);
    };
    const handleEdit = (node) => {
        setEditRecord(node);
        setCreateDialog(true);
    };
    const handleDelete = (node) => {
        if (window.confirm(translate('ra.message.delete_content'))) {
            deleteOne(
                RESOURCE,
                { id: node.id },
                {
                    onSuccess: () => {
                        handleRefresh();
                        const msg = translate('ra.message.delete_success', { _: 'Deleted successfully' });
                        pushNotify('success', msg);
                    },
                    onError: (error) => {
                        const msg = translate('ra.notification.http_error', { _: error?.message || 'Network error' });
                        pushNotify('error', msg);
                    },
                }
            );
        }
    };
    const toggleExpandAll = () => {
        setExpandAll(prevExpandAll => {
            const newExpandAll = !prevExpandAll;
            const newOpenNodes = {};
            const updateOpenNodes = (nodes) => {
                if (!nodes) return;
                nodes.forEach(node => {
                    newOpenNodes[node.id] = newExpandAll;
                    if (node.children) {
                        updateOpenNodes(node.children);
                    }
                });
            };
            updateOpenNodes(treeData);
            setOpenNodes(newOpenNodes);
            return newExpandAll;
        });
    };
    return (
        <div>
            <DeptEdit
                editRecord={editRecord}
                open={createDialog}
                setOpen={setCreateDialog}
                callback={() => {
                    handleRefresh();
                }}
                resource={RESOURCE}
            />
            <Title title={TITLE} />
            <Box sx={{ mt: 2, mr: 3, mb: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                <Box width={300} >
                    <Button
                        variant="outlined"
                        color="primary"
                        startIcon={expandAll ? <KeyboardArrowDownIcon /> : <KeyboardArrowRightIcon />}
                        onClick={toggleExpandAll}
                        sx={{ ml: 1 }}
                    >
                        {expandAll ? translate('common.action.collapseAll') : translate('common.action.expandAll')}
                    </Button>
                    {/* <TextField
                        label="Search"
                        value={filter}
                        onChange={({ target }) => {
                            setFilter(target.value)
                        }}
                        variant="filled"
                        size="small"
                        margin="dense"
                        fullWidth
                    /> */}
                </Box>
                <Box>
                    <Button
                        variant="outlined"
                        color="primary"
                        startIcon={<RefreshIcon />}
                        onClick={handleRefresh}
                        sx={{ ml: 1 }}
                    >
                        {translate('ra.action.refresh')}
                    </Button>
                    <Button
                        variant="outlined"
                        color="primary"
                        startIcon={<Add />}
                        onClick={handleAdd}
                        sx={{ ml: 1 }}
                    >
                        {translate('ra.action.add')}
                    </Button>
                </Box>
            </Box>
            <Card sx={{
                position: 'relative',
            }}>
                {!treeData && (
                    <LinearProgress
                        sx={{
                            height: "3px",
                            position: 'absolute',
                            top: 0,
                            left: 0,
                            right: 0,
                        }}
                    />
                )}
                <TableContainer component={Paper}>
                    <Table size="small">
                        <TableHead>
                            <TableRow>
                                <StyledTableCell sx={{ padding: 0, width: 60 }} />
                                {columns.map((column, idx) => (
                                    <StyledTableCell
                                        key={idx}
                                        align={column.align || 'left'}
                                        style={{
                                            minWidth: column.minWidth
                                        }}
                                    >
                                        {translate(column.label)}
                                    </StyledTableCell>
                                ))}
                            </TableRow>
                        </TableHead>
                        <TableBody>
                            {treeData && treeData.length > 0 && (
                                treeData.map((row) => (
                                    <TreeTableRow
                                        key={row.id}
                                        row={row}
                                        onEdit={handleEdit}
                                        onDelete={handleDelete}
                                        openNodes={openNodes}
                                        setOpenNodes={setOpenNodes}
                                    />
                                ))
                            )}
                        </TableBody>
                    </Table>
                </TableContainer>
            </Card>
        </div>
    );
};
export default MenuPdaList;
rsf-admin/src/page/menuPda/index.jsx
New file
@@ -0,0 +1,15 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    ListGuesser,
    EditGuesser,
    ShowGuesser,
} from "react-admin";
import MenuPdaList from "./MenuPdaList";
export default {
    list: MenuPdaList,
    recordRepresentation: (record) => {
        return `${record.name}`
    }
};
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuCreate.jsx
New file
@@ -0,0 +1,125 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SaveButton,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    Toolbar,
    required,
    useDataProvider,
    useNotify,
    Form,
    useCreateController,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "../components/DialogCloseButton";
import StatusSelectInput from "../components/StatusSelectInput";
import MemoInput from "../components/MemoInput";
const PdaRoleMenuCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async (data) => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <>
            <CreateBase
                record={{}}
                transform={(data) => {
                    return data;
                }}
                mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
            >
                <Dialog
                    open={open}
                    onClose={handleClose}
                    aria-labelledby="form-dialog-title"
                    fullWidth
                    disableRestoreFocus
                    maxWidth="md"   // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
                >
                    <Form>
                        <DialogTitle id="form-dialog-title" sx={{
                            position: 'sticky',
                            top: 0,
                            backgroundColor: 'background.paper',
                            zIndex: 1000
                        }}
                        >
                            {translate('create.title')}
                            <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                                <DialogCloseButton onClose={handleClose} />
                            </Box>
                        </DialogTitle>
                        <DialogContent sx={{ mt: 2 }}>
                            <Grid container rowSpacing={2} columnSpacing={2}>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                        label="table.field.pdaRoleMenu.roleId"
                                        source="roleId"
                                        autoFocus
                                        validate={required()}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                        label="table.field.pdaRoleMenu.menuId"
                                        source="menuId"
                                        validate={required()}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <StatusSelectInput />
                                </Grid>
                                <Grid item xs={12} display="flex" gap={1}>
                                    <Stack direction="column" spacing={1} width={'100%'}>
                                        <MemoInput />
                                    </Stack>
                                </Grid>
                            </Grid>
                        </DialogContent>
                        <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                            <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}  >
                                <SaveButton />
                            </Toolbar>
                        </DialogActions>
                    </Form>
                </Dialog>
            </CreateBase>
        </>
    )
}
export default PdaRoleMenuCreate;
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuEdit.jsx
New file
@@ -0,0 +1,97 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    Edit,
    SimpleForm,
    FormDataConsumer,
    useTranslate,
    TextInput,
    NumberInput,
    BooleanInput,
    DateInput,
    SelectInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    SaveButton,
    Toolbar,
    Labeled,
    NumberField,
    required,
    useRecordContext,
    DeleteButton,
} from 'react-admin';
import { useWatch, useFormContext } from "react-hook-form";
import { Stack, Grid, Box, Typography } from '@mui/material';
import * as Common from '@/utils/common';
import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import EditBaseAside from "../components/EditBaseAside";
import CustomerTopToolBar from "../components/EditTopToolBar";
import MemoInput from "../components/MemoInput";
import StatusSelectInput from "../components/StatusSelectInput";
const FormToolbar = () => {
    const { getValues } = useFormContext();
    return (
        <Toolbar sx={{ justifyContent: 'space-between' }}>
            <SaveButton />
            <DeleteButton mutationMode="optimistic" />
        </Toolbar>
    )
}
const PdaRoleMenuEdit = () => {
    const translate = useTranslate();
    return (
        <Edit
            redirect="list"
            mutationMode={EDIT_MODE}
            actions={<CustomerTopToolBar />}
            aside={<EditBaseAside />}
        >
            <SimpleForm
                shouldUnregister
                warnWhenUnsavedChanges
                toolbar={<FormToolbar />}
                mode="onTouched"
                defaultValues={{}}
            // validate={(values) => { }}
            >
                <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                    <Grid item xs={12} md={8}>
                        <Typography variant="h6" gutterBottom>
                            {translate('common.edit.title.main')}
                        </Typography>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                                label="table.field.pdaRoleMenu.roleId"
                                source="roleId"
                                autoFocus
                                validate={required()}
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                                label="table.field.pdaRoleMenu.menuId"
                                source="menuId"
                                validate={required()}
                            />
                        </Stack>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <Typography variant="h6" gutterBottom>
                            {translate('common.edit.title.common')}
                        </Typography>
                        <StatusSelectInput />
                        <Box mt="2em" />
                        <MemoInput />
                    </Grid>
                </Grid>
            </SimpleForm>
        </Edit >
    )
}
export default PdaRoleMenuEdit;
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuList.jsx
New file
@@ -0,0 +1,154 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { useNavigate } from 'react-router-dom';
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    CreateButton,
    ExportButton,
    BulkDeleteButton,
    WrapperField,
    useRecordContext,
    useTranslate,
    useNotify,
    useListContext,
    FunctionField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    ReferenceField,
    TextInput,
    DateTimeInput,
    DateInput,
    SelectInput,
    NumberInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    DeleteButton,
} from 'react-admin';
import { Box, Typography, Card, Stack } from '@mui/material';
import { styled } from '@mui/material/styles';
import PdaRoleMenuCreate from "./PdaRoleMenuCreate";
import PdaRoleMenuPanel from "./PdaRoleMenuPanel";
import EmptyData from "../components/EmptyData";
import MyCreateButton from "../components/MyCreateButton";
import MyExportButton from '../components/MyExportButton';
import PageDrawer from "../components/PageDrawer";
import MyField from "../components/MyField";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
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 />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <NumberInput source="roleId" label="table.field.pdaRoleMenu.roleId" />,
    <NumberInput source="menuId" label="table.field.pdaRoleMenu.menuId" />,
    <TextInput label="common.field.memo" source="memo" />,
    <SelectInput
        label="common.field.status"
        source="status"
        choices={[
            { id: '1', name: 'common.enums.statusTrue' },
            { id: '0', name: 'common.enums.statusFalse' },
        ]}
        resettable
    />,
]
const PdaRoleMenuList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    return (
        <Box display="flex">
            <List
                sx={{
                    flexGrow: 1,
                    transition: (theme) =>
                        theme.transitions.create(['all'], {
                            duration: theme.transitions.duration.enteringScreen,
                        }),
                    marginRight: !!drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={"menu.pdaRoleMenu"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "create_time", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='pdaRoleMenu' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StyledDatagrid
                    preferenceKey='pdaRoleMenu'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    expand={() => <PdaRoleMenuPanel />}
                    expandSingle={true}
                    omit={['id', 'createTime', 'createBy', 'memo']}
                >
                    <NumberField source="id" />
                    <NumberField source="roleId" label="table.field.pdaRoleMenu.roleId" />
                    <NumberField source="menuId" label="table.field.pdaRoleMenu.menuId" />
                    <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>
            </List>
            <PdaRoleMenuCreate
                open={createDialog}
                setOpen={setCreateDialog}
            />
            <PageDrawer
                title='PdaRoleMenu Detail'
                drawerVal={drawerVal}
                setDrawerVal={setDrawerVal}
            >
            </PageDrawer>
        </Box>
    )
}
export default PdaRoleMenuList;
rsf-admin/src/page/pdaRoleMenu/PdaRoleMenuPanel.jsx
New file
@@ -0,0 +1,63 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import { Box, Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
import {
    useTranslate,
    useRecordContext,
} from 'react-admin';
import PanelTypography from "../components/PanelTypography";
import * as Common from '@/utils/common'
const PdaRoleMenuPanel = () => {
    const record = useRecordContext();
    if (!record) return null;
    const translate = useTranslate();
    return (
        <>
            <Card sx={{ width: { xs: 300, sm: 500, md: 600, lg: 800 }, margin: 'auto' }}>
                <CardContent>
                    <Grid container spacing={2}>
                        <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'space-between' }}>
                            <Typography variant="h6" gutterBottom align="left" sx={{
                                maxWidth: { xs: '100px', sm: '180px', md: '260px', lg: '360px' },
                                whiteSpace: 'nowrap',
                                overflow: 'hidden',
                                textOverflow: 'ellipsis',
                            }}>
                                {Common.camelToPascalWithSpaces(translate('table.field.pdaRoleMenu.id'))}: {record.id}
                            </Typography>
                            {/*  inherit, primary, secondary, textPrimary, textSecondary, error */}
                            <Typography variant="h6" gutterBottom align="right" >
                                ID: {record.id}
                            </Typography>
                        </Grid>
                    </Grid>
                    <Grid container spacing={2}>
                        <Grid item xs={12} container alignContent="flex-end">
                            <Typography variant="caption" color="textSecondary" sx={{ wordWrap: 'break-word', wordBreak: 'break-all' }}>
                                {Common.camelToPascalWithSpaces(translate('common.field.memo'))}:{record.memo}
                            </Typography>
                        </Grid>
                    </Grid>
                    <Box height={20}>&nbsp;</Box>
                    <Grid container spacing={2}>
                        <Grid item xs={6}>
                            <PanelTypography
                                title="table.field.pdaRoleMenu.roleId"
                                property={record.roleId}
                            />
                        </Grid>
                        <Grid item xs={6}>
                            <PanelTypography
                                title="table.field.pdaRoleMenu.menuId"
                                property={record.menuId}
                            />
                        </Grid>
                    </Grid>
                </CardContent>
            </Card >
        </>
    );
};
export default PdaRoleMenuPanel;
rsf-admin/src/page/pdaRoleMenu/index.jsx
New file
@@ -0,0 +1,18 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
    ListGuesser,
    EditGuesser,
    ShowGuesser,
} from "react-admin";
import PdaRoleMenuList from "./PdaRoleMenuList";
import PdaRoleMenuEdit from "./PdaRoleMenuEdit";
export default {
    list: PdaRoleMenuList,
    edit: PdaRoleMenuEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => {
        return `${record.id}`
    }
};
rsf-admin/src/page/system/role/AssignPermissions_pda.jsx
New file
@@ -0,0 +1,380 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import {
    useTranslate,
    useNotify,
    TextInput
} from 'react-admin';
import { Box, Button, Card, Stack, CardContent, Skeleton, TextField } from '@mui/material';
import { SimpleTreeView, TreeItem, RichTreeView, useTreeViewApiRef } from '@mui/x-tree-view';
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request'
const DEFAULT_EXPAND_ALL = true;
const AssignPermissionsPda = (props) => {
    const { role, originMenuIds, setDrawerVal, closeCallback, authType } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const [loading, setLoading] = useState(false);
    const [treeData, setTreeData] = useState([]);
    const [selectedItems, setSelectedItems] = useState([]);
    const [expandedItems, setExpandedItems] = useState([]);
    const [parmas, setParmas] = useState({ condition: '', authType: authType });
    const toggledItemRef = useRef({});
    const apiRef = useTreeViewApiRef();
    useEffect(() => {
        reload()
    }, [role, originMenuIds])
    const reload = () => {
        setSelectedItems(originMenuIds.map(item => item + ""));
        const transformTree = (treeData) => {
            return treeData.map(data => {
                return {
                    id: data.id + '',
                    label: data.type === 0 ? translate(data.name || data.code) : data.name || data.code,
                    type: data.type,
                    children: (data.children && data.children.length > 0 ? transformTree(data.children) : null)
                }
            })
        }
        const http = async () => {
            const res = await request.post('/menuPda/tree', parmas);
            if (res?.data?.code === 200) {
                const transformData = transformTree(res.data.data);
                setTreeData(transformData);
                if (DEFAULT_EXPAND_ALL) {
                    setExpandedItems(getAllItemsWithChildrenItemIds(transformData));
                }
            } else {
                notify(res.data.msg, { type: 'error' });
            }
            setLoading(false);
        }
        setLoading(true);
        setTimeout(() => {
            http();
        }, 200);
    }
    const getAllItemItemIds = () => {
        const ids = [];
        const registerItemId = (item) => {
            ids.push(item.id);
            item.children?.forEach(registerItemId);
        };
        treeData.forEach(registerItemId);
        return ids;
    };
    const getAllItemsWithChildrenItemIds = (treeDataHandle) => {
        const itemIds = [];
        const registerItemId = (item) => {
            if (item.children?.length) {
                itemIds.push(item.id);
                item.children.forEach(registerItemId);
            }
        };
        if (treeDataHandle) {
            treeDataHandle.forEach(registerItemId);
        } else {
            treeData.forEach(registerItemId);
        }
        return itemIds;
    };
    const handleSelectedItemsChange = (event, newSelectedItems) => {
        const itemsToSelect = [];
        const itemsToUnSelect = {};
        Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => {
            const item = apiRef.current.getItem(itemId);
            if (isSelected) {
                itemsToSelect.push(...getItemDescendantsIds(item));
                const parentIds = getParentIds(treeData, itemId);
                itemsToSelect.push(...parentIds);
            } else {
                // 取消子节点
                const treeNode = checkoutTreeNode(treeData, itemId);
                if (treeNode?.children && treeNode?.children.length > 0) {
                    const allChildren = getItemDescendantsIds(treeNode);
                    const childrenSet = new Set(allChildren);
                    newSelectedItems = newSelectedItems.filter(item => !childrenSet.has(item));
                }
                // 取消父节点
                const removeParentIfAllSiblingsDeselected = (itemId, newSelectedItems, treeData) => {
                    let updatedSelectedItems = [...newSelectedItems];
                    let currentId = itemId;
                    while (true) {
                        const parentId = getParentId(treeData, currentId);
                        if (!parentId) break;
                        const siblings = getChildrenIds(treeData, parentId);
                        const allSiblingsDeselected = siblings.every(siblingId => !updatedSelectedItems.includes(siblingId));
                        if (allSiblingsDeselected) {
                            updatedSelectedItems = updatedSelectedItems.filter(id => id !== parentId);
                            currentId = parentId;
                        } else {
                            break;
                        }
                    }
                    return updatedSelectedItems;
                };
                newSelectedItems = removeParentIfAllSiblingsDeselected(itemId, newSelectedItems, treeData);
            }
        });
        const newSelectedItemsWithChildren = Array.from(
            new Set(
                [...newSelectedItems, ...itemsToSelect].filter(
                    (itemId) => !itemsToUnSelect[itemId],
                ),
            ),
        );
        setSelectedItems(newSelectedItemsWithChildren);
        toggledItemRef.current = {};
    };
    const handleSave = (event) => {
        request.post('/rolePda/scope/update', {
            id: role.id,
            menuIds: {
                checked: selectedItems,
                halfChecked: []
            }
        }).then(res => {
            if (res?.data.code === 200) {
                setDrawerVal(null);
                if (closeCallback) {
                    closeCallback();
                }
                notify(res.data.msg, { type: 'info', messageArgs: { _: res.data.msg } })
            } else {
                notify(res.data.msg, { type: 'error' })
            }
        })
    }
    const search = (e) => {
        const value = e.target.value;
        setParmas({
            ...parmas,
            condition: value
        })
        reload()
    }
    return (
        <>
            <Card sx={{
                ml: 1,
                mr: 1,
                height: 'calc(100vh - 140px)',
                overflowY: 'auto'
            }}>
                <CardContent sx={{
                    overflow: 'auto',
                    display: 'flex',
                    flexDirection: 'column',
                    justifyContent: 'space-between'
                }}>
                    <Box>
                        <Box mb={1} sx={{
                            display: 'flex',
                            justifyContent: 'space-between'
                        }}>
                            <Button onClick={() => {
                                setSelectedItems((oldSelected) =>
                                    oldSelected.length === 0 ? getAllItemItemIds() : [],
                                );
                            }}>
                                {selectedItems.length === 0 ? translate('ra.action.select_all') : translate('ra.action.unselect')}
                            </Button>
                            <Button onClick={() => {
                                setExpandedItems((oldExpanded) =>
                                    oldExpanded.length === 0 ? getAllItemsWithChildrenItemIds() : [],
                                );
                            }}>
                                {expandedItems.length === 0 ? translate('common.action.expandAll') : translate('common.action.collapseAll')}
                            </Button>
                        </Box>
                        <Box sx={{
                            display: 'flex',
                            justifyContent: 'space-between',
                            alignItems: 'center'
                        }}>
                            <TextField sx={{ width: '200px' }} label="搜索菜单" variant="outlined" value={parmas.condition} onChange={(e) => search(e)} />
                            <Button startIcon={<SaveIcon />} size="small" variant="contained" onClick={handleSave} sx={{ height: '40px' }}>
                                {translate('ra.action.save')}
                            </Button>
                        </Box>
                        <Box sx={{
                            minWidth: 290,
                            overflow: 'auto',
                            marginTop: '10px',
                            padding: 1,
                            borderBottom: '1px solid background.paper',
                            borderRadius: '4px',
                            boxShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
                            backgroundColor: 'background.paper',
                        }}>
                            {loading ? (
                                <SkeletonBox />
                            ) : (
                                <RichTreeView
                                    multiSelect
                                    checkboxSelection
                                    apiRef={apiRef}
                                    items={treeData}
                                    selectedItems={selectedItems}
                                    onSelectedItemsChange={handleSelectedItemsChange}
                                    onItemSelectionToggle={(event, itemId, isSelected) => {
                                        toggledItemRef.current[itemId] = isSelected;
                                    }}
                                    expandedItems={expandedItems}
                                    onExpandedItemsChange={(event, itemIds) => {
                                        setExpandedItems(itemIds);
                                    }}
                                />
                            )}
                        </Box>
                    </Box>
                </CardContent>
            </Card>
        </>
    )
}
const checkoutTreeNode = (treeData, targetId) => {
    let result = null;
    const checkout = (node) => {
        if (node.id === targetId) {
            result = node;
            return true;
        } else {
            if (node.children) {
                for (const child of node.children) {
                    if (checkout(child)) {
                        return true;
                    }
                }
            }
        }
        return false;
    };
    treeData.forEach(item => {
        if (checkout(item)) {
            return;
        }
    });
    return result;
};
const getItemDescendantsIds = (item) => {
    const ids = [];
    item.children?.forEach((child) => {
        ids.push(child.id);
        ids.push(...getItemDescendantsIds(child));
    });
    return ids;
}
const getParentIds = (tree, targetId) => {
    let parentIds = [];
    const searchTree = (node, path = []) => {
        if (node.id === targetId) {
            parentIds = [...path];
            return true;
        }
        if (node.children) {
            for (const child of node.children) {
                if (searchTree(child, [...path, node.id])) {
                    return true;
                }
            }
        }
        return false;
    };
    tree.forEach(item => {
        searchTree(item);
    })
    return parentIds;
};
const getParentId = (tree, targetId) => {
    let parentId = null;
    const searchTree = (node) => {
        if (node.children) {
            for (const child of node.children) {
                if (child.id === targetId) {
                    parentId = node.id;
                    return true;
                }
                if (searchTree(child)) {
                    return true;
                }
            }
        }
        return false;
    };
    tree.forEach(item => {
        if (searchTree(item)) {
            return parentId;
        }
    });
    return parentId;
};
const getChildrenIds = (tree, targetId) => {
    let childrenIds = [];
    const searchTree = (node) => {
        if (node.id === targetId && node.children) {
            childrenIds = node.children.map(child => child.id);
        } else if (node.children) {
            for (const child of node.children) {
                searchTree(child);
            }
        }
    };
    tree.forEach(item => {
        searchTree(item);
    });
    return childrenIds;
};
const SkeletonBox = () => {
    return (
        <Stack spacing={1}>
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
            <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} />
        </Stack>
    )
}
export default AssignPermissionsPda;
rsf-admin/src/page/system/role/RoleList.jsx
@@ -42,8 +42,10 @@
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
import AssignPermissions from "./AssignPermissions";
import AssignPermissionsPda from "./AssignPermissions_pda";
import request from '@/utils/request';
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd';
import AdUnitsIcon from '@mui/icons-material/AdUnits';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -80,10 +82,11 @@
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const [drawerValPda, setDrawerValPda] = useState(false);
    const [menuIds, setMenuIds] = useState([]);
    const [authType,setAuthType] = useState(0)
    const [authType, setAuthType] = useState(0)
    const assign = (record) => {
        request('/role/scope/list', {
@@ -102,6 +105,23 @@
        });
    }
    const assignPda = (record) => {
        request('/rolePda/scope/list', {
            method: 'GET',
            params: {
                roleId: record.id
            }
        }).then((res) => {
            if (res?.data?.code === 200) {
                const { data: menuIds } = res.data;
                setMenuIds(menuIds || []);
                setDrawerValPda(!!drawerValPda && drawerValPda === record ? null : record);
            } else {
                notify(res.data.msg, { type: 'error' });
            }
        });
    }
    return (
        <Box display="flex">
            <List
@@ -111,7 +131,7 @@
                        theme.transitions.create(['all'], {
                            duration: theme.transitions.duration.enteringScreen,
                        }),
                    marginRight: !!drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                    marginRight: (!!drawerVal || !!drawerValPda) ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={"menu.role"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
@@ -131,7 +151,7 @@
                    preferenceKey='role'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    omit={['id', 'createTime', 'memo','statusBool']}
                    omit={['id', 'createTime', 'memo', 'statusBool']}
                >
                    <NumberField source="id" />
                    <MyField source="name" label="table.field.role.name"
@@ -148,7 +168,7 @@
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <ScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assign={assign} auType={0} setAuthType={setAuthType} label="网页权限&nbsp;&nbsp;&nbsp;" />
                        <ScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assign={assign} auType={1} setAuthType={setAuthType} label="PDA权限&nbsp;&nbsp;&nbsp;" />
                        <PdaScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assignPda={assignPda} auType={1} setAuthType={setAuthType} label="PDA权限&nbsp;&nbsp;&nbsp;" />
                        <ScopeButton sx={{ padding: '1px', fontSize: '.75rem' }} assign={assign} auType={2} setAuthType={setAuthType} label="仓库权限&nbsp;" />
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
@@ -174,7 +194,25 @@
                    closeCallback={() => {
                        setMenuIds([]);
                    }}
                    authType = {authType}
                    authType={authType}
                />
            </PageDrawer>
            <PageDrawer
                drawerVal={drawerValPda}
                setDrawerVal={setDrawerValPda}
                title={!!drawerValPda ? `Scope by ${drawerValPda.code || drawerValPda.name}` : 'Role Detail'}
                closeCallback={() => {
                    setMenuIds([]);
                }}
            >
                <AssignPermissionsPda
                    role={drawerValPda}
                    originMenuIds={menuIds}
                    setDrawerVal={setDrawerValPda}
                    closeCallback={() => {
                        setMenuIds([]);
                    }}
                    authType={authType}
                />
            </PageDrawer>
        </Box>
@@ -183,7 +221,7 @@
const ScopeButton = (props) => {
    const record = useRecordContext();
    const { assign, auType, setAuthType, label, ...rest } = props;
    const { assign, auType, setAuthType, label, ...rest } = props;
    return (
        <Button
            variant="text"
@@ -200,4 +238,23 @@
    )
}
const PdaScopeButton = (props) => {
    const record = useRecordContext();
    const { assignPda, auType, setAuthType, label, ...rest } = props;
    return (
        <Button
            variant="text"
            color="primary"
            startIcon={<AdUnitsIcon />}
            label={label}
            onClick={(event) => {
                setAuthType(auType);
                event.stopPropagation();
                assignPda(record);
            }}
            {...rest}
        />
    )
}
export default RoleList;
rsf-open-api/src/main/resources/application-dev.yml
@@ -14,8 +14,8 @@
#    url: jdbc:mysql://47.76.147.249:3306/rsf?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    username: rsf
    username: root
    url: jdbc:mysql://127.0.0.1:3306/rsf?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    password: 34821015
    url: jdbc:mysql://192.168.4.15:3306/rsf_20250106?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    password: 1234
#    url: jdbc:mysql://10.10.10.200:3306/rsf?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    password: xltys1995
    type: com.alibaba.druid.pool.DruidDataSource
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java
New file
@@ -0,0 +1,34 @@
package com.vincent.rsf.server.api.controller.pda;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.api.entity.params.PdaGeneralParam;
import com.vincent.rsf.server.api.service.InBoundService;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Api(tags = "PDA入库操作接口")
@RequestMapping("/pda")
@RestController
public class InBoundController extends BaseController {
    @Autowired
    private InBoundService inBoundService;
    @PreAuthorize("hasAuthority('manager:task:list')")
    @PostMapping("/in/emptyContainer/warehousing")
    @ApiOperation("空容器入库")
    public R emptyContainerWarehousing(@RequestBody PdaGeneralParam param) {
        return R.ok();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/PdaGeneralParam.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.server.api.entity.params;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class PdaGeneralParam {
    // 容器号
    private String containerNo;
    // 接驳站号
    private String transferStationNo;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/InBoundService.java
New file
@@ -0,0 +1,10 @@
package com.vincent.rsf.server.api.service;
import com.vincent.rsf.framework.common.R;
/**
 * PDA入库操作Service接口
 */
public interface InBoundService {
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java
New file
@@ -0,0 +1,14 @@
package com.vincent.rsf.server.api.service.impl;
import com.vincent.rsf.server.api.service.InBoundService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
 * PDA入库操作Service实现类
 */
@Slf4j
@Service
public class InBoundServiceImpl implements InBoundService {
}
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
@@ -15,16 +15,16 @@
        generator.frontendPrefixPath = "rsf-admin/";
        generator.sqlOsType = SqlOsType.MYSQL;
        generator.url = "192.168.4.151:3306/rsf";
        generator.url = "192.168.4.15:3306/rsf_20250106";
        generator.username = "root";
        generator.password = "34821015";
        generator.password = "1234";
//        generator.url="47.97.1.152:51433;databasename=jkasrs";
//        generator.username="sa";
//        generator.password="Zoneyung@zy56$";
        generator.table = "view_stock_statistic";
        generator.tableDesc = "日库存统计";
        generator.packagePath = "com.vincent.rsf.server.manager";
        generator.table = "sys_pda_role_menu";
        generator.tableDesc = "PDA权限";
        generator.packagePath = "com.vincent.rsf.server.system";
        generator.build();
    }
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
@@ -52,6 +52,8 @@
                        "sys_user_role",
                        "sys_role_menu",
                        "sys_menu",
                        "sys_pda_role_menu",
                        "sys_menu_pda",
                        "man_loc_type_rela",
                        "man_qly_inspect_result",
                        "view_stock_manage",
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/MenuPdaController.java
New file
@@ -0,0 +1,157 @@
package com.vincent.rsf.server.manager.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.common.utils.Utils;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.NodeUtils;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.manager.service.MenuPdaService;
import com.vincent.rsf.server.system.controller.BaseController;
import com.vincent.rsf.server.system.entity.Menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
@RestController
public class MenuPdaController extends BaseController {
    @Autowired
    private MenuPdaService menuPdaService;
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @PostMapping("/menuPda/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<MenuPda, BaseParam> pageParam = new PageParam<>(baseParam, MenuPda.class);
        return R.ok().add(menuPdaService.page(pageParam, pageParam.buildWrapper(true)));
    }
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @PostMapping("/menuPda/list")
    public R list(@RequestBody Map<String, Object> map) {
        return R.ok().add(menuPdaService.list());
    }
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @PostMapping({"/menuPda/many/{ids}", "/menuPdas/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(menuPdaService.listByIds(Arrays.asList(ids)));
    }
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @GetMapping("/menuPda/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(menuPdaService.getById(id));
    }
    @PreAuthorize("hasAuthority('system:menu:list')")
    @PostMapping("/menuPda/tree")
    public R tree(@RequestBody Map<String, Object> map) {
//        PageParam<Menu, BaseParam> param = new PageParam<>(buildParam(map, BaseParam.class), Menu.class);
//        QueryWrapper<Menu> wrapper = param.buildWrapper(true, queryWrapper -> queryWrapper.orderByAsc("sort"));
//        List<Menu> menus = menuService.list(wrapper);
//        return R.ok().add(Utils.toTreeData(menus, 0L, Menu::getParentId, Menu::getId, Menu::setChildren));
        List<MenuPda> menuList = menuPdaService.list(new LambdaQueryWrapper<MenuPda>().orderByAsc(MenuPda::getSort));
        List<MenuPda> treeData = Utils.toTreeData(menuList, 0L, MenuPda::getParentId, MenuPda::getId, MenuPda::setChildren);
        if (!Cools.isEmpty(map.get("condition"))) {
            Utils.treeRemove(treeData, String.valueOf(map.get("condition")), MenuPda::getName, MenuPda::getChildren);
            Utils.treeRemove(treeData, String.valueOf(map.get("condition")), MenuPda::getName, MenuPda::getChildren);
        }
        return R.ok().add(treeData);
    }
    @PreAuthorize("hasAuthority('system:menu:save')")
    @OperationLog("Save Menu")
    @PostMapping("/menuPda/save")
    public R save(@RequestBody MenuPda menu) {
        if (menu.getParentId() != null && menu.getParentId() > 0) {
            MenuPda parent = menuPdaService.getById(menu.getParentId());
            if (parent != null) {
                menu.setParentName(parent.getName());
            }
        } else {
            menu.setParentId(0L);
        }
        NodeUtils nodeUtils = new NodeUtils();
        nodeUtils.generatePath0(item -> menuPdaService.getById(item.getParentId()), menu, MenuPda::getId, MenuPda::getName, MenuPda::getParentId);
        menu.setPath(nodeUtils.path.toString());
        menu.setPathName(nodeUtils.pathName.toString());
        menu.setCreateBy(getLoginUserId());
        menu.setCreateTime(new Date());
        menu.setUpdateBy(getLoginUserId());
        menu.setUpdateTime(new Date());
        if (!menuPdaService.save(menu)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(menu);
    }
    @PreAuthorize("hasAuthority('system:menu:update')")
    @OperationLog("Update Menu")
    @PostMapping("/menuPda/update")
    public R update(@RequestBody MenuPda menu) {
        if (menu.getParentId() != null && menu.getParentId() > 0) {
            MenuPda parent = menuPdaService.getById(menu.getParentId());
            if (parent != null) {
                menu.setParentName(parent.getName());
            }
        } else {
            menu.setParentId(0L);
        }
        NodeUtils nodeUtils = new NodeUtils();
        nodeUtils.generatePath0(item -> menuPdaService.getById(item.getParentId()), menu, MenuPda::getId, MenuPda::getName, MenuPda::getParentId);
        menu.setPath(nodeUtils.path.toString());
        menu.setPathName(nodeUtils.pathName.toString());
        menu.setUpdateBy(getLoginUserId());
        menu.setUpdateTime(new Date());
        if (!menuPdaService.updateById(menu)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(menu);
    }
    @PreAuthorize("hasAuthority('manager:menuPda:remove')")
    @OperationLog("Delete PDA权限")
    @PostMapping("/menuPda/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        if (!menuPdaService.removeByIds(Arrays.asList(ids))) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @PostMapping("/menuPda/query")
    public R query(@RequestParam(required = false) String condition) {
        List<KeyValVo> vos = new ArrayList<>();
        LambdaQueryWrapper<MenuPda> wrapper = new LambdaQueryWrapper<>();
        if (!Cools.isEmpty(condition)) {
            wrapper.like(MenuPda::getName, condition);
        }
        menuPdaService.page(new Page<>(1, 30), wrapper).getRecords().forEach(
                item -> vos.add(new KeyValVo(item.getId(), item.getName()))
        );
        return R.ok().add(vos);
    }
    @PreAuthorize("hasAuthority('manager:menuPda:list')")
    @PostMapping("/menuPda/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(menuPdaService.list(), MenuPda.class), response);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/MenuPda.java
New file
@@ -0,0 +1,258 @@
package com.vincent.rsf.server.manager.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.server.system.entity.Menu;
import com.vincent.rsf.server.system.entity.Tenant;
import com.vincent.rsf.server.system.service.TenantService;
import org.springframework.format.annotation.DateTimeFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableLogic;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.system.service.UserService;
import com.vincent.rsf.server.system.entity.User;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
@TableName("sys_menu_pda")
public class MenuPda implements Serializable, GrantedAuthority {
    private static final long serialVersionUID = 1L;
    public static final int TYPE_MENU = 0; // 菜单类型
    public static final int TYPE_BTN = 1; // 按钮类型
    /**
     * ID
     */
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 名称
     */
    @ApiModelProperty(value = "名称")
    private String name;
    /**
     * 上级菜单
     */
    @ApiModelProperty(value = "上级菜单")
    private Long parentId;
    /**
     * 上级菜单名
     */
    @ApiModelProperty(value = "上级菜单名")
    private String parentName;
    /**
     * 关联路径
     */
    @ApiModelProperty(value = "关联路径")
    private String path;
    /**
     * 关联路径名
     */
    @ApiModelProperty(value = "关联路径名")
    private String pathName;
    /**
     * 路由地址
     */
    @ApiModelProperty(value = "路由地址")
    private String route;
    /**
     * 页面组件
     */
    @ApiModelProperty(value = "页面组件")
    private String component;
    /**
     * 简述
     */
    @ApiModelProperty(value = "简述")
    private String brief;
    /**
     * 标识
     */
    @ApiModelProperty(value = "标识")
    private String code;
    /**
     * 类型 0: 菜单 1: 按钮
     */
    @ApiModelProperty(value = "类型 0: 菜单  1: 按钮  ")
    private Integer type;
    /**
     * 权限标识
     */
    @ApiModelProperty(value = "权限标识")
    private String authority;
    /**
     * 菜单图标
     */
    @ApiModelProperty(value = "菜单图标")
    private String icon;
    /**
     * 排序
     */
    @ApiModelProperty(value = "排序")
    private Integer sort;
    /**
     * 元信息
     */
    @ApiModelProperty(value = "元信息")
    private String meta;
    /**
     * 所属机构
     */
    @ApiModelProperty(value = "所属机构")
    private Long tenantId;
    /**
     * 状态 1: 正常 0: 禁用
     */
    @ApiModelProperty(value = "状态 1: 正常  0: 禁用  ")
    private Integer status;
    /**
     * 是否删除 1: 是 0: 否
     */
    @ApiModelProperty(value = "是否删除 1: 是  0: 否  ")
    @TableLogic
    private Integer deleted;
    /**
     * 添加时间
     */
    @ApiModelProperty(value = "添加时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    /**
     * 添加人员
     */
    @ApiModelProperty(value = "添加人员")
    private Long createBy;
    /**
     * 修改时间
     */
    @ApiModelProperty(value = "修改时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    /**
     * 修改人员
     */
    @ApiModelProperty(value = "修改人员")
    private Long updateBy;
    /**
     * 备注
     */
    @ApiModelProperty(value = "备注")
    private String memo;
    @TableField(exist = false)
    private List<MenuPda> children;
    public MenuPda() {
    }
    public String getType$() {
        if (null == this.type) {
            return null;
        }
        switch (this.type) {
            case 0:
                return "菜单";
            case 1:
                return "按钮";
            default:
                return String.valueOf(this.type);
        }
    }
    public String getTenantId$() {
        TenantService service = SpringUtils.getBean(TenantService.class);
        Tenant tenant = service.getById(this.tenantId);
        if (!Cools.isEmpty(tenant)) {
            return String.valueOf(tenant.getName());
        }
        return null;
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getCreateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.createBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
    public String getUpdateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.updateBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public Boolean getStatusBool() {
        if (null == this.status) {
            return null;
        }
        switch (this.status) {
            case 1:
                return true;
            case 0:
                return false;
            default:
                return null;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/MenuPdaMapper.java
New file
@@ -0,0 +1,12 @@
package com.vincent.rsf.server.manager.mapper;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface MenuPdaMapper extends BaseMapper<MenuPda> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java
@@ -237,7 +237,7 @@
    /**
     * 非光电站点任务下发
     */
    @Scheduled(cron = "0/5 * * * * ?  ")
//    @Scheduled(cron = "0/5 * * * * ?  ")
    @Transactional(rollbackFor = Exception.class)
    public void pubTaskToWcs() {
        Long loginUserId = SystemAuthUtils.getLoginUserId();
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/MenuPdaService.java
New file
@@ -0,0 +1,8 @@
package com.vincent.rsf.server.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.manager.entity.MenuPda;
public interface MenuPdaService extends IService<MenuPda> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MenuPdaServiceImpl.java
New file
@@ -0,0 +1,12 @@
package com.vincent.rsf.server.manager.service.impl;
import com.vincent.rsf.server.manager.mapper.MenuPdaMapper;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.manager.service.MenuPdaService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service("menuPdaService")
public class MenuPdaServiceImpl extends ServiceImpl<MenuPdaMapper, MenuPda> implements MenuPdaService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/PdaRoleMenuController.java
New file
@@ -0,0 +1,58 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.system.controller.param.RoleScopeParam;
import com.vincent.rsf.server.system.entity.PdaRoleMenu;
import com.vincent.rsf.server.system.entity.Role;
import com.vincent.rsf.server.system.entity.RoleMenu;
import com.vincent.rsf.server.system.service.PdaRoleMenuService;
import com.vincent.rsf.server.system.controller.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
@RestController
public class PdaRoleMenuController extends BaseController {
    @Autowired
    private PdaRoleMenuService pdaRoleMenuService;
    @PreAuthorize("hasAuthority('system:role:list')")
    @GetMapping("/rolePda/scope/list")
    public R scopeList(@RequestParam Long roleId) {
        return R.ok().add(pdaRoleMenuService.listStrictlyMenuByRoleId(roleId));
    }
    @PreAuthorize("hasAuthority('system:role:update')")
    @OperationLog("Assign Permissions")
    @PostMapping("/rolePda/scope/update")
    @Transactional
    public R scopeUpdate(@RequestBody RoleScopeParam param) {
        Long roleId = param.getId();
        List<Long> menuIds = new ArrayList<>(param.getMenuIds().getChecked());
        menuIds.addAll(param.getMenuIds().getHalfChecked());
        pdaRoleMenuService.remove(new LambdaQueryWrapper<PdaRoleMenu>().eq(PdaRoleMenu::getRoleId, roleId));
        for (Long menuId : menuIds) {
            if (!pdaRoleMenuService.save(new PdaRoleMenu(roleId, menuId))) {
                throw new CoolException("Internal Server Error!");
            }
        }
        return R.ok("Assign Success");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/PdaRoleMenu.java
New file
@@ -0,0 +1,50 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.system.service.UserService;
import com.vincent.rsf.server.system.entity.User;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("sys_pda_role_menu")
public class PdaRoleMenu implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value= "")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value= "")
    private Long roleId;
    @ApiModelProperty(value= "")
    private Long menuId;
    public PdaRoleMenu() {}
    public PdaRoleMenu(Long roleId,Long menuId) {
        this.roleId = roleId;
        this.menuId = menuId;
    }
//    PdaRoleMenu pdaRoleMenu = new PdaRoleMenu(
//            null,    // [非空]
//            null    // [非空]
//    );
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/PdaRoleMenuMapper.java
New file
@@ -0,0 +1,22 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.system.entity.Menu;
import com.vincent.rsf.server.system.entity.PdaRoleMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface PdaRoleMenuMapper extends BaseMapper<PdaRoleMenu> {
    @InterceptorIgnore(tenantLine = "true")
    List<MenuPda> listMenuByUserId(@Param("userId") Long userId, @Param("type") Integer menuType);
    List<Long> listStrictlyMenuByRoleId(@Param("roleId") Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/PdaRoleMenuService.java
New file
@@ -0,0 +1,14 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.system.entity.Menu;
import com.vincent.rsf.server.system.entity.PdaRoleMenu;
import java.util.List;
public interface PdaRoleMenuService extends IService<PdaRoleMenu> {
    List<MenuPda> listMenuByUserId(Long userId, Integer menuType);
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/PdaRoleMenuServiceImpl.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.server.system.service.impl;
import com.vincent.rsf.server.manager.entity.MenuPda;
import com.vincent.rsf.server.system.entity.Menu;
import com.vincent.rsf.server.system.mapper.PdaRoleMenuMapper;
import com.vincent.rsf.server.system.entity.PdaRoleMenu;
import com.vincent.rsf.server.system.service.PdaRoleMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("pdaRoleMenuService")
public class PdaRoleMenuServiceImpl extends ServiceImpl<PdaRoleMenuMapper, PdaRoleMenu> implements PdaRoleMenuService {
    @Override
    public List<MenuPda> listMenuByUserId(Long userId, Integer menuType) {
        return baseMapper.listMenuByUserId(userId, menuType);
    }
    @Override
    public List<Long> listStrictlyMenuByRoleId(Long roleId) {
        return baseMapper.listStrictlyMenuByRoleId(roleId);
    }
}
rsf-server/src/main/java/menuPda.sql
New file
@@ -0,0 +1,35 @@
-- save menuPda record
-- mysql
insert into `sys_menu` ( `name`, `parent_id`, `route`, `component`, `type`, `sort`, `tenant_id`, `status`) values ( 'menu.menuPda', '0', '/manager/menuPda', 'menuPda', '0' , '0', '1' , '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Query PDA权限', '', '1', 'manager:menuPda:list', '0', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Create PDA权限', '', '1', 'manager:menuPda:save', '1', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Update PDA权限', '', '1', 'manager:menuPda:update', '2', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Delete PDA权限', '', '1', 'manager:menuPda:remove', '3', '1', '1');
-- locale menu name
menuPda: 'MenuPda',
-- locale field
menuPda: {
    name: "name",
    parentId: "parentId",
    parentName: "parentName",
    path: "path",
    pathName: "pathName",
    route: "route",
    component: "component",
    brief: "brief",
    code: "code",
    type: "type",
    authority: "authority",
    icon: "icon",
    sort: "sort",
    meta: "meta",
},
-- ResourceContent
import menuPda from './menuPda';
case 'menuPda':
    return menuPda;
rsf-server/src/main/java/pdaRoleMenu.sql
New file
@@ -0,0 +1,23 @@
-- save pdaRoleMenu record
-- mysql
insert into `sys_menu` ( `name`, `parent_id`, `route`, `component`, `type`, `sort`, `tenant_id`, `status`) values ( 'menu.pdaRoleMenu', '0', '/system/pdaRoleMenu', 'pdaRoleMenu', '0' , '0', '1' , '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Query PDA权限', '', '1', 'system:pdaRoleMenu:list', '0', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Create PDA权限', '', '1', 'system:pdaRoleMenu:save', '1', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Update PDA权限', '', '1', 'system:pdaRoleMenu:update', '2', '1', '1');
insert into `sys_menu` ( `name`, `parent_id`, `type`, `authority`, `sort`, `tenant_id`, `status`) values ( 'Delete PDA权限', '', '1', 'system:pdaRoleMenu:remove', '3', '1', '1');
-- locale menu name
pdaRoleMenu: 'PdaRoleMenu',
-- locale field
pdaRoleMenu: {
    roleId: "roleId",
    menuId: "menuId",
},
-- ResourceContent
import pdaRoleMenu from './pdaRoleMenu';
case 'pdaRoleMenu':
    return pdaRoleMenu;
rsf-server/src/main/resources/application-dev.yml
@@ -16,7 +16,7 @@
    username: root
#    url: jdbc:mysql://10.10.10.200:3306/rsf?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    password: xltys1995
    url: jdbc:mysql://192.168.4.15:3306/rsf?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    url: jdbc:mysql://192.168.4.15:3306/rsf_20250106?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    password: 1234
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
rsf-server/src/main/resources/mapper/manager/MenuPdaMapper.xml
New file
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vincent.rsf.server.manager.mapper.MenuPdaMapper">
</mapper>
rsf-server/src/main/resources/mapper/system/PdaRoleMenuMapper.xml
New file
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vincent.rsf.server.system.mapper.PdaRoleMenuMapper">
    <select id="listMenuByUserId" resultType="com.vincent.rsf.server.manager.entity.MenuPda">
        SELECT DISTINCT sm.*
        FROM sys_menu_pda sm
        JOIN sys_pda_role_menu srm ON sm.id = srm.menu_id
        JOIN sys_user_role sur ON srm.role_id = sur.role_id
        JOIN sys_role sr ON sur.role_id = sr.id
        WHERE 1=1
        <if test="type != null">
            AND sm.type = #{type}
        </if>
        AND sur.user_id = #{userId}
        AND sr.deleted = 0
        AND sm.deleted = 0
        ORDER BY sm.sort
    </select>
    <select id="listStrictlyMenuByRoleId" resultType="java.lang.Long">
        select sm.id
        from sys_menu_pda sm
        left join sys_pda_role_menu srm on sm.id = srm.menu_id
        where 1=1
        and sm.deleted = 0
        and srm.role_id = #{roleId}
        <!--
        and sm.id not in (
            select sm.parent_id
            from sys_menu sm
            inner join sys_role_menu srm on sm.id = srm.menu_id
            and srm.role_id = #{roleId}
        )
        -->
        order by sm.sort
    </select>
</mapper>